541 lines
16 KiB
JavaScript
541 lines
16 KiB
JavaScript
/**
|
|
* 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'
|
|
]);
|
|
|
|
// Add localization as a dependency
|
|
this.dependencies = ['localization'];
|
|
}
|
|
|
|
/**
|
|
* Initialize the module
|
|
* @returns {Promise<boolean>} - 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();
|
|
|
|
// Get localization module
|
|
const localization = this.getModule('localization');
|
|
if (localization) {
|
|
// Update language preferences with current language
|
|
const language = localization.getLanguage();
|
|
|
|
// Update default preferences
|
|
this.defaultPreferences.tts.language = language;
|
|
this.defaultPreferences.app.locale = language;
|
|
|
|
// Update current preferences if they exist
|
|
if (this.preferences) {
|
|
// Only update if not already set by user
|
|
if (!this.preferences.tts.language || this.preferences.tts.language === 'en-us') {
|
|
this.preferences.tts.language = language;
|
|
}
|
|
|
|
if (!this.preferences.app.locale || this.preferences.app.locale === 'en-us') {
|
|
this.preferences.app.locale = language;
|
|
}
|
|
|
|
// Save updated preferences
|
|
this.savePreferences();
|
|
}
|
|
|
|
this.reportProgress(80, "Updated language preferences");
|
|
} else {
|
|
console.warn("Localization module not found or not ready, using default language settings");
|
|
// We'll continue without localization - it might initialize later
|
|
this.reportProgress(80, "Using default language settings");
|
|
}
|
|
|
|
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 };
|