/** * 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'); // 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() { 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 default preferences to ensure all keys exist this.preferences = this.mergeWithDefaults(storedPrefs, this.defaultPreferences); } else { // Use default preferences if none found this.preferences = JSON.parse(JSON.stringify(this.defaultPreferences)); // Try to set locale based on browser language const browserLocale = navigator.language.toLowerCase(); if (browserLocale) { this.preferences.app.locale = browserLocale; } } return this.preferences; } catch (error) { console.error("Error loading preferences:", error); // Fall back to default preferences this.preferences = JSON.parse(JSON.stringify(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) { const result = {}; // For each category in defaults for (const category in defaults) { result[category] = {}; // Copy all settings from defaults for this category for (const setting in defaults[category]) { // Use stored value if it exists, otherwise use default result[category][setting] = (stored[category] && stored[category][setting] !== undefined) ? stored[category][setting] : defaults[category][setting]; } // Copy any additional settings from stored that aren't in defaults if (stored[category]) { for (const setting in stored[category]) { if (result[category][setting] === undefined) { result[category][setting] = stored[category][setting]; } } } } // Copy any additional categories from stored that aren't in defaults for (const category in stored) { if (result[category] === undefined) { result[category] = stored[category]; } } return result; } /** * 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 (!this.preferences) { this.loadPreferences(); } if (this.preferences[category] && this.preferences[category][setting] !== undefined) { return this.preferences[category][setting]; } // If default value provided, use it if (defaultValue !== null) { return defaultValue; } // Otherwise check default preferences if (this.defaultPreferences[category] && this.defaultPreferences[category][setting] !== undefined) { return this.defaultPreferences[category][setting]; } // If all else fails, return null return null; } /** * 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 (!this.preferences) { this.loadPreferences(); } // Create category if it doesn't exist if (!this.preferences[category]) { this.preferences[category] = {}; } // Update preference const oldValue = this.preferences[category][setting]; this.preferences[category][setting] = value; // Save preferences this.savePreferences(); // Dispatch event if value changed if (oldValue !== value) { this.dispatchEvent('preference-changed', { category, setting, value, oldValue }); } return true; } /** * Reset preferences to defaults * @returns {boolean} - Success status */ resetPreferences() { try { // Clone default preferences this.preferences = JSON.parse(JSON.stringify(this.defaultPreferences)); // Save preferences this.savePreferences(); // Dispatch event this.dispatchEvent('preferences-reset', { timestamp: new Date().toISOString() }); return true; } 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); } 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(); // Register with the module registry moduleRegistry.register(PersistenceManager); // Export the module export { PersistenceManager };