/** * Persistence Manager Module * Handles saving and loading game state and user preferences */ import { BaseModule } from './base-module.js'; import { moduleRegistry } from './module-registry.js'; class PersistenceManagerModule extends BaseModule { /** * Create a new persistence manager */ constructor() { super('persistence-manager', 'Persistence Manager'); this.storage = window.localStorage; this.stateKey = 'ai_fiction_state'; this.prefsKey = 'ai_fiction_prefs'; // Default preferences this.defaultPreferences = { tts: { enabled: false, provider: 'browser', // 'browser', 'kokoro', 'elevenlabs' voice: '', volume: 1.0 }, audio: { masterVolume: 1.0, musicVolume: 0.7, sfxVolume: 1.0 }, animation: { speed: 50, // 0-100 scale fastForwardKey: ' ' // Space key }, accessibility: { highContrast: false, largerText: false } }; // Current preferences (will be loaded from storage) this.preferences = { ...this.defaultPreferences }; } /** * Initialize the module * @returns {Promise} - Resolves with success status */ async initialize() { try { // Test storage availability this.storage = this.getStorageObject(); // Load preferences automatically this.loadPreferences(); this.reportProgress(100, "Persistence manager ready"); return true; } catch (error) { console.error("Error initializing persistence manager:", error); // Continue without persistence rather than failing return true; } } /** * Get the appropriate storage object, testing availability * @returns {Storage} - The storage object to use */ getStorageObject() { try { // Test if localStorage is available if (window.localStorage) { const testKey = '__storage_test__'; window.localStorage.setItem(testKey, testKey); window.localStorage.removeItem(testKey); return window.localStorage; } } catch (e) { console.warn('localStorage not available, using memory storage'); // Create a memory-based storage fallback return this.createMemoryStorage(); } console.warn('localStorage not available, using memory storage'); return this.createMemoryStorage(); } /** * Create a memory-based storage fallback * @returns {Object} - A storage-like object */ createMemoryStorage() { const memoryStore = {}; return { getItem: (key) => memoryStore[key] || null, setItem: (key, value) => { memoryStore[key] = String(value); }, removeItem: (key) => { delete memoryStore[key]; }, clear: () => { Object.keys(memoryStore).forEach(key => { delete memoryStore[key]; }); } }; } /** * Save the current game state * @param {Object} state - The game state to save */ saveState(state) { if (!this.storage) { console.warn('No storage available, game state not saved.'); return false; } try { const stateString = JSON.stringify(state); this.storage.setItem(this.stateKey, stateString); console.log('Game state saved successfully.'); return true; } catch (error) { console.error('Error saving game state:', error); return false; } } /** * Load the saved game state * @returns {Object|null} The loaded state or null if no state exists */ loadState() { if (!this.storage) { console.warn('No storage available, cannot load game state.'); return null; } try { const stateString = this.storage.getItem(this.stateKey); if (!stateString) { console.info('No saved game state found.'); return null; } const state = JSON.parse(stateString); console.log('Game state loaded successfully.'); return state; } catch (error) { console.error('Error loading game state:', error); return null; } } /** * Check if a saved game state exists * @returns {boolean} Whether a saved state exists */ hasSavedState() { if (!this.storage) return false; return !!this.storage.getItem(this.stateKey); } /** * Delete the saved game state * @returns {boolean} Whether the state was successfully deleted */ clearState() { if (!this.storage) return false; try { this.storage.removeItem(this.stateKey); console.log('Game state cleared.'); return true; } catch (error) { console.error('Error clearing game state:', error); return false; } } /** * Save user preferences * @param {Object} [preferences] - Preferences to save (defaults to current preferences) * @returns {boolean} Whether preferences were successfully saved */ savePreferences(preferences = null) { if (!this.storage) { console.warn('No storage available, preferences not saved.'); return false; } // Use provided preferences or current preferences const prefsToSave = preferences || this.preferences; try { const prefsString = JSON.stringify(prefsToSave); this.storage.setItem(this.prefsKey, prefsString); console.log('Preferences saved successfully.'); // Update current preferences if (preferences) { this.preferences = { ...this.preferences, ...preferences }; } return true; } catch (error) { console.error('Error saving preferences:', error); return false; } } /** * Load user preferences * @returns {Object} The loaded preferences or default preferences if none exist */ loadPreferences() { if (!this.storage) { console.warn('No storage available, using default preferences.'); return { ...this.defaultPreferences }; } try { const prefsString = this.storage.getItem(this.prefsKey); if (!prefsString) { console.info('No saved preferences found, using defaults.'); this.preferences = { ...this.defaultPreferences }; return this.preferences; } const loadedPrefs = JSON.parse(prefsString); // Merge with default preferences to ensure all fields exist this.preferences = this.mergeWithDefaults(loadedPrefs, this.defaultPreferences); console.log('Preferences loaded successfully.'); return this.preferences; } catch (error) { console.error('Error loading preferences:', error); this.preferences = { ...this.defaultPreferences }; return this.preferences; } } /** * Merge loaded preferences with default values to ensure all fields exist * @param {Object} loaded - The loaded preferences * @param {Object} defaults - The default preferences * @returns {Object} Merged preferences * @private */ mergeWithDefaults(loaded, defaults) { const result = {}; // Start with defaults for (const key in defaults) { if (typeof defaults[key] === 'object' && defaults[key] !== null && !Array.isArray(defaults[key])) { // Recurse for nested objects if (loaded && loaded[key]) { result[key] = this.mergeWithDefaults(loaded[key], defaults[key]); } else { result[key] = { ...defaults[key] }; } } else { // Use loaded value if available, otherwise default result[key] = (loaded && loaded[key] !== undefined) ? loaded[key] : defaults[key]; } } return result; } /** * Update specific preferences * @param {string} category - The preference category (e.g., 'tts', 'audio') * @param {string} setting - The specific setting name * @param {any} value - The new value * @param {boolean} [saveImmediately=true] - Whether to save immediately */ updatePreference(category, setting, value, saveImmediately = true) { // Ensure the category exists if (!this.preferences[category]) { console.warn(`Preference category '${category}' doesn't exist.`); return false; } // Update the preference this.preferences[category][setting] = value; // Save if requested if (saveImmediately) { return this.savePreferences(); } return true; } /** * Get a specific preference value * @param {string} category - The preference category * @param {string} setting - The specific setting name * @param {any} [defaultValue] - Default value if the preference doesn't exist * @returns {any} The preference value */ getPreference(category, setting, defaultValue = null) { // Check if category exists if (!this.preferences[category]) { return defaultValue; } // Check if setting exists in category if (this.preferences[category].hasOwnProperty(setting)) { return this.preferences[category][setting]; } return defaultValue; } /** * Reset preferences to defaults * @param {string} [category] - Optional category to reset (resets all if not specified) * @param {boolean} [saveImmediately=true] - Whether to save immediately */ resetPreferences(category = null, saveImmediately = true) { if (category) { // Reset only specified category if (this.defaultPreferences[category]) { this.preferences[category] = { ...this.defaultPreferences[category] }; } } else { // Reset all preferences this.preferences = { ...this.defaultPreferences }; } // Save if requested if (saveImmediately) { return this.savePreferences(); } return true; } /** * Get all preferences * @returns {Object} The current preferences */ getAllPreferences() { return { ...this.preferences }; } } // Create the singleton instance const PersistenceManager = new PersistenceManagerModule(); // Register with the module registry moduleRegistry.register(PersistenceManager); // Export the module export { PersistenceManager }; // Keep a reference in window for loader system window.PersistenceManager = PersistenceManager;