365 lines
12 KiB
JavaScript
365 lines
12 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');
|
|
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<boolean>} - 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;
|