/** * Persistence Manager Module * Handles saving and loading game state and user preferences */ import { BaseModule } from './base-module.js'; class PersistenceManagerModule extends BaseModule { /** * Create a new persistence manager */ constructor() { super('persistence-manager', 'Persistence Manager'); // Storage keys this.keys = { gameState: 'ai-interactive-fiction-state', preferences: 'ai-interactive-fiction-preferences', saveSlots: 'ai-interactive-fiction-saves' }; // Current game state this.gameState = null; // User preferences this.preferences = null; // Save slots this.saveSlots = {}; // Default preferences this.defaultPreferences = { animation: { enabled: true, speed: 50 // 0-100 scale, 50 is default }, tts: { enabled: false, provider: 'browser', // 'browser', 'api', 'kokoro' voice: '', volume: 1.0, rate: 1.0, language: 'en-us' // Default language, will be updated during initialization }, audio: { masterVolume: 1.0, musicVolume: 0.7, sfxVolume: 1.0, musicEnabled: true, sfxEnabled: true }, accessibility: { highContrast: false, largerText: false }, app: { locale: 'en-us', theme: 'default' } }; // Bind methods this.bindMethods([ 'saveGameState', 'loadGameState', 'savePreferences', 'loadPreferences', 'getPreference', 'updatePreference', 'resetPreferences', 'createSaveSlot', 'loadSaveSlot', 'deleteSaveSlot', 'getAllSaveSlots' ]); // Remove circular dependency this.dependencies = []; } /** * Initialize the module * @returns {Promise} - Resolves with success status */ async initialize() { try { this.reportProgress(10, "Initializing persistence manager"); // Load preferences first (with default language settings) this.loadPreferences(); // Load save slots this.loadSaveSlots(); this.reportProgress(100, "Persistence manager ready"); return true; } catch (error) { console.error("Error initializing persistence manager:", error); this.reportProgress(100, "Persistence manager failed"); return false; } } /** * Save the current game state * @param {Object} state - Game state to save * @returns {boolean} - Success status */ saveGameState(state) { if (!state) return false; try { this.gameState = state; localStorage.setItem(this.keys.gameState, JSON.stringify(state)); // Dispatch event this.dispatchEvent('game-state-saved', { timestamp: new Date().toISOString() }); return true; } catch (error) { console.error("Error saving game state:", error); return false; } } /** * Load the current game state * @returns {Object|null} - Loaded game state or null if not found */ loadGameState() { try { const stateJson = localStorage.getItem(this.keys.gameState); if (!stateJson) return null; this.gameState = JSON.parse(stateJson); return this.gameState; } catch (error) { console.error("Error loading game state:", error); return null; } } /** * Save user preferences * @returns {boolean} - Success status */ savePreferences() { if (!this.preferences) return false; try { localStorage.setItem(this.keys.preferences, JSON.stringify(this.preferences)); // Dispatch event this.dispatchEvent('preferences-saved', { timestamp: new Date().toISOString() }); return true; } catch (error) { console.error("Error saving preferences:", error); return false; } } /** * Load user preferences * @returns {Object} - Loaded preferences or default preferences if not found */ loadPreferences() { try { const prefsJson = localStorage.getItem(this.keys.preferences); if (prefsJson) { // Parse stored preferences const storedPrefs = JSON.parse(prefsJson); // Merge with defaults to ensure all keys exist this.preferences = this.mergeWithDefaults(storedPrefs, this.defaultPreferences); } else { // Use defaults if no stored preferences found this.preferences = {...this.defaultPreferences}; } return this.preferences; } catch (error) { console.error("Error loading preferences:", error); this.preferences = {...this.defaultPreferences}; return this.preferences; } } /** * Merge stored preferences with defaults to ensure all keys exist * @param {Object} stored - Stored preferences * @param {Object} defaults - Default preferences * @returns {Object} - Merged preferences */ mergeWithDefaults(stored, defaults) { // Base case: if stored is not an object or is null, return defaults if (typeof stored !== 'object' || stored === null) { return defaults; } // Create a new object to avoid modifying the input objects const merged = {}; // Add all keys from defaults, overriding with stored values where they exist for (const key in defaults) { if (Object.prototype.hasOwnProperty.call(defaults, key)) { // If the default value is an object and not null, recurse if (typeof defaults[key] === 'object' && defaults[key] !== null) { merged[key] = this.mergeWithDefaults( Object.prototype.hasOwnProperty.call(stored, key) ? stored[key] : {}, defaults[key] ); } else { // Otherwise, use stored value if it exists, otherwise use default merged[key] = Object.prototype.hasOwnProperty.call(stored, key) ? stored[key] : defaults[key]; } } } return merged; } /** * Get a specific preference * @param {string} category - Preference category * @param {string} setting - Preference setting * @param {*} defaultValue - Default value if preference not found * @returns {*} - Preference value */ getPreference(category, setting, defaultValue = null) { if (!category || !setting) return defaultValue; // Ensure preferences are loaded if (!this.preferences) { this.loadPreferences(); } // Check if category exists if (!this.preferences[category]) return defaultValue; // Check if setting exists in category if (!Object.prototype.hasOwnProperty.call(this.preferences[category], setting)) { return defaultValue; } return this.preferences[category][setting]; } /** * Update a specific preference * @param {string} category - Preference category * @param {string} setting - Preference setting * @param {*} value - New value * @returns {boolean} - Success status */ updatePreference(category, setting, value) { if (!category || !setting) return false; // Ensure preferences are loaded if (!this.preferences) { this.loadPreferences(); } // Create category if it doesn't exist if (!this.preferences[category]) { this.preferences[category] = {}; } // Update preference this.preferences[category][setting] = value; // Save preferences const success = this.savePreferences(); // Dispatch event this.dispatchEvent('preference-updated', { category, setting, value, timestamp: new Date().toISOString() }); return success; } /** * Reset preferences to defaults * @returns {boolean} - Success status */ resetPreferences() { try { // Create a deep clone of default preferences this.preferences = JSON.parse(JSON.stringify(this.defaultPreferences)); // Save preferences const success = this.savePreferences(); // Dispatch event this.dispatchEvent('preferences-reset', { timestamp: new Date().toISOString() }); return success; } catch (error) { console.error("Error resetting preferences:", error); return false; } } /** * Get all preferences * @returns {Object} - All preferences */ getAllPreferences() { if (!this.preferences) { this.loadPreferences(); } return this.preferences; } /** * Load save slots * @returns {Object} - Save slots */ loadSaveSlots() { try { const slotsJson = localStorage.getItem(this.keys.saveSlots); if (slotsJson) { this.saveSlots = JSON.parse(slotsJson); // Validate each save slot for (const id in this.saveSlots) { const slot = this.saveSlots[id]; if (!slot.id || !slot.name || !slot.timestamp || !slot.state) { delete this.saveSlots[id]; } } } else { this.saveSlots = {}; } return this.saveSlots; } catch (error) { console.error("Error loading save slots:", error); this.saveSlots = {}; return this.saveSlots; } } /** * Save save slots * @returns {boolean} - Success status */ saveSaveSlots() { try { localStorage.setItem(this.keys.saveSlots, JSON.stringify(this.saveSlots)); return true; } catch (error) { console.error("Error saving save slots:", error); return false; } } /** * Create a new save slot * @param {string} name - Save slot name * @param {Object} state - Game state to save * @returns {string|null} - Save slot ID or null if failed */ createSaveSlot(name, state) { if (!name || !state) return null; try { // Generate unique ID const id = `save_${Date.now()}`; // Create save slot this.saveSlots[id] = { id, name, timestamp: new Date().toISOString(), state }; // Save save slots this.saveSaveSlots(); // Dispatch event this.dispatchEvent('save-slot-created', { id, name, timestamp: new Date().toISOString() }); return id; } catch (error) { console.error("Error creating save slot:", error); return null; } } /** * Load a save slot * @param {string} id - Save slot ID * @returns {Object|null} - Game state or null if not found */ loadSaveSlot(id) { if (!id || !this.saveSlots[id]) return null; try { const saveSlot = this.saveSlots[id]; // Set as current game state this.gameState = saveSlot.state; // Save current game state this.saveGameState(this.gameState); // Dispatch event this.dispatchEvent('save-slot-loaded', { id, name: saveSlot.name, timestamp: new Date().toISOString() }); return this.gameState; } catch (error) { console.error("Error loading save slot:", error); return null; } } /** * Delete a save slot * @param {string} id - Save slot ID * @returns {boolean} - Success status */ deleteSaveSlot(id) { if (!id || !this.saveSlots[id]) return false; try { // Get save slot name before deleting const name = this.saveSlots[id].name; // Delete save slot delete this.saveSlots[id]; // Save save slots this.saveSaveSlots(); // Dispatch event this.dispatchEvent('save-slot-deleted', { id, name, timestamp: new Date().toISOString() }); return true; } catch (error) { console.error("Error deleting save slot:", error); return false; } } /** * Get all save slots * @returns {Object} - All save slots */ getAllSaveSlots() { if (!this.saveSlots) { this.loadSaveSlots(); } return this.saveSlots; } /** * Clean up when module is disposed */ dispose() { // Nothing to clean up } } // Create the singleton instance const PersistenceManager = new PersistenceManagerModule(); // Export the module export { PersistenceManager };