Refactored modules and updated loader.
This commit is contained in:
@@ -0,0 +1,495 @@
|
||||
/**
|
||||
* 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<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();
|
||||
|
||||
// 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 };
|
||||
Reference in New Issue
Block a user