485 lines
14 KiB
JavaScript
485 lines
14 KiB
JavaScript
/**
|
|
* 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 = {
|
|
tts: {
|
|
enabled: false,
|
|
provider: 'none',
|
|
voice: '',
|
|
},
|
|
audio: {
|
|
masterVolume: 1.0,
|
|
ttsVolume: 1.0,
|
|
musicVolume: 0.7,
|
|
sfxVolume: 1.0,
|
|
},
|
|
app: {
|
|
locale: 'en-us',
|
|
speed: 1.0,
|
|
}
|
|
};
|
|
|
|
// 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<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();
|
|
|
|
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();
|
|
console.log("Saved preferences: ", category, setting, value, this.preferences)
|
|
|
|
// 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 };
|