refactor for shared states and multi chapters
This commit is contained in:
parent
691b809b41
commit
b9bea16f78
13 changed files with 2308 additions and 1786 deletions
|
|
@ -5,12 +5,17 @@ class GameEngine {
|
|||
this.terminal = null;
|
||||
this.adapter = null;
|
||||
this.state = null;
|
||||
this.sharedState = null; // For series games
|
||||
this.input = null;
|
||||
this.sound = null;
|
||||
this.scenes = null;
|
||||
|
||||
this.isRunning = false;
|
||||
this.originalExecuteCommand = null;
|
||||
|
||||
// Series support
|
||||
this.seriesId = gameDefinition.seriesId || null;
|
||||
this.chapterNumber = gameDefinition.chapterNumber || null;
|
||||
}
|
||||
|
||||
// Register this game as a terminal command
|
||||
|
|
@ -35,6 +40,10 @@ class GameEngine {
|
|||
self._reset();
|
||||
return;
|
||||
}
|
||||
if (args[0] === "reset-series" && self.seriesId) {
|
||||
self._resetSeries();
|
||||
return;
|
||||
}
|
||||
if (args[0] === "continue" || args[0] === "resume") {
|
||||
await self.start(true);
|
||||
return;
|
||||
|
|
@ -51,6 +60,23 @@ class GameEngine {
|
|||
return;
|
||||
}
|
||||
|
||||
// Initialize shared state for series games (do this early for chapter check)
|
||||
if (this.seriesId && window.SharedStateManager) {
|
||||
this.sharedState = new SharedStateManager(this.seriesId);
|
||||
this.sharedState.init(this.definition.sharedStateDefaults || {});
|
||||
|
||||
// Check if this chapter can be played (sequential unlock)
|
||||
if (this.chapterNumber && this.chapterNumber > 1) {
|
||||
if (!this.sharedState.canPlayChapter(this.chapterNumber)) {
|
||||
const prevChapter = this.chapterNumber - 1;
|
||||
this.terminal.printError(
|
||||
`You must complete Chapter ${prevChapter} before playing Chapter ${this.chapterNumber}.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the global HTML to add the game in progress details
|
||||
document.body.classList.add("game-in-progress");
|
||||
console.log(document.body.classList);
|
||||
|
|
@ -59,7 +85,7 @@ class GameEngine {
|
|||
|
||||
// Initialize components
|
||||
this.adapter = new TerminalAdapter(this.terminal);
|
||||
this.state = new StateManager(this.definition.id);
|
||||
this.state = new StateManager(this.definition.id, this.sharedState);
|
||||
this.input = new InputManager(this.adapter);
|
||||
|
||||
// Initialize sound manager if SoundManager is available
|
||||
|
|
@ -77,8 +103,11 @@ class GameEngine {
|
|||
// Initialize state
|
||||
this.state.init(this.definition.initialState || {});
|
||||
|
||||
// Load and merge external scenes if defined
|
||||
const mergedScenes = this._getMergedScenes();
|
||||
|
||||
// Register scenes
|
||||
this.scenes.registerScenes(this.definition.scenes);
|
||||
this.scenes.registerScenes(mergedScenes);
|
||||
|
||||
// Hook into terminal input
|
||||
this._hookInput();
|
||||
|
|
@ -236,6 +265,66 @@ class GameEngine {
|
|||
);
|
||||
}
|
||||
|
||||
// Reset entire series progress (for series games)
|
||||
_resetSeries() {
|
||||
// Reset chapter state
|
||||
this._reset();
|
||||
|
||||
// Reset shared state
|
||||
if (this.seriesId) {
|
||||
const sharedState = new SharedStateManager(this.seriesId);
|
||||
sharedState.reset();
|
||||
this.terminal.printSuccess(
|
||||
`All ${this.definition.name} series progress has been reset.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Create context for scene factories
|
||||
_createSceneContext() {
|
||||
return {
|
||||
chapterNumber: this.chapterNumber || 1,
|
||||
seriesId: this.seriesId,
|
||||
sharedState: this.sharedState,
|
||||
art: this.definition.art || {},
|
||||
additionalOptions: this.definition.additionalHubOptions || [],
|
||||
};
|
||||
}
|
||||
|
||||
// Load external scenes and merge with inline scenes
|
||||
_getMergedScenes() {
|
||||
const inlineScenes = this.definition.scenes || {};
|
||||
|
||||
// If no external scenes defined, just return inline scenes
|
||||
if (
|
||||
!this.definition.externalScenes ||
|
||||
this.definition.externalScenes.length === 0
|
||||
) {
|
||||
return inlineScenes;
|
||||
}
|
||||
|
||||
// Load external scenes from registered factories
|
||||
const externalScenes = {};
|
||||
const context = this._createSceneContext();
|
||||
|
||||
for (const sceneRef of this.definition.externalScenes) {
|
||||
const factory = window.SceneFactories?.[sceneRef];
|
||||
if (factory) {
|
||||
try {
|
||||
const scenes = factory(context);
|
||||
Object.assign(externalScenes, scenes);
|
||||
} catch (e) {
|
||||
console.error(`Failed to load external scenes from ${sceneRef}:`, e);
|
||||
}
|
||||
} else {
|
||||
console.warn(`Scene factory not found: ${sceneRef}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Inline scenes override external scenes (allows chapter-specific overrides)
|
||||
return { ...externalScenes, ...inlineScenes };
|
||||
}
|
||||
|
||||
// Debug command to skip to a specific scene
|
||||
async _debugGoToScene(sceneName) {
|
||||
const scene = this.scenes.getScene(sceneName);
|
||||
|
|
|
|||
|
|
@ -322,6 +322,19 @@ class SceneManager {
|
|||
this.state.set(action.set, action.value);
|
||||
}
|
||||
|
||||
// Set value in shared state (for series games)
|
||||
if (action.setShared !== undefined) {
|
||||
this.state.setShared(action.setShared, action.value);
|
||||
}
|
||||
|
||||
// Mark a chapter as complete in shared state
|
||||
if (action.markChapterComplete !== undefined) {
|
||||
const sharedState = this.state.getSharedState();
|
||||
if (sharedState) {
|
||||
sharedState.markChapterComplete(action.markChapterComplete);
|
||||
}
|
||||
}
|
||||
|
||||
if (action.increment !== undefined) {
|
||||
this.state.increment(action.increment, action.amount || 1);
|
||||
}
|
||||
|
|
|
|||
66
assets/js/games/engine/scene-registry.js
Normal file
66
assets/js/games/engine/scene-registry.js
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
// Scene Registry - Global registry for shared scene factories
|
||||
// Scene factories are functions that take a context and return scene definitions
|
||||
|
||||
// Initialize global scene factories object
|
||||
window.SceneFactories = window.SceneFactories || {};
|
||||
|
||||
// Helper class for working with scene factories
|
||||
class SceneRegistry {
|
||||
// Register a scene factory
|
||||
static register(id, factory) {
|
||||
if (typeof factory !== "function") {
|
||||
console.error(`Scene factory must be a function: ${id}`);
|
||||
return false;
|
||||
}
|
||||
window.SceneFactories[id] = factory;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get a scene factory
|
||||
static get(id) {
|
||||
return window.SceneFactories[id] || null;
|
||||
}
|
||||
|
||||
// Check if a factory exists
|
||||
static has(id) {
|
||||
return id in window.SceneFactories;
|
||||
}
|
||||
|
||||
// List all registered factories
|
||||
static list() {
|
||||
return Object.keys(window.SceneFactories);
|
||||
}
|
||||
|
||||
// Unregister a factory
|
||||
static unregister(id) {
|
||||
delete window.SceneFactories[id];
|
||||
}
|
||||
|
||||
// Create scenes from a factory with context
|
||||
static createScenes(id, context) {
|
||||
const factory = this.get(id);
|
||||
if (!factory) {
|
||||
console.warn(`Scene factory not found: ${id}`);
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
return factory(context);
|
||||
} catch (e) {
|
||||
console.error(`Error creating scenes from factory ${id}:`, e);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// Merge multiple factories into one scene object
|
||||
static mergeFactories(factoryIds, context) {
|
||||
const merged = {};
|
||||
for (const id of factoryIds) {
|
||||
const scenes = this.createScenes(id, context);
|
||||
Object.assign(merged, scenes);
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
}
|
||||
|
||||
// Make available globally
|
||||
window.SceneRegistry = SceneRegistry;
|
||||
190
assets/js/games/engine/shared-state-manager.js
Normal file
190
assets/js/games/engine/shared-state-manager.js
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
// Shared State Manager - Manages game state across multiple chapters in a series
|
||||
class SharedStateManager {
|
||||
constructor(seriesId) {
|
||||
this.seriesId = seriesId;
|
||||
this.state = {};
|
||||
this.storageKey = `series_${seriesId}_state`;
|
||||
}
|
||||
|
||||
// Initialize with default state (only sets values not already in storage)
|
||||
init(defaultState = {}) {
|
||||
this.state = this._deepClone(defaultState);
|
||||
this._loadFromStorage();
|
||||
}
|
||||
|
||||
// Get value using dot notation path (e.g., "inventory.sword")
|
||||
get(path, defaultValue = undefined) {
|
||||
const parts = path.split(".");
|
||||
let current = this.state;
|
||||
|
||||
for (const part of parts) {
|
||||
if (current === undefined || current === null) {
|
||||
return defaultValue;
|
||||
}
|
||||
current = current[part];
|
||||
}
|
||||
|
||||
return current !== undefined ? current : defaultValue;
|
||||
}
|
||||
|
||||
// Set value using dot notation path
|
||||
set(path, value) {
|
||||
const parts = path.split(".");
|
||||
let current = this.state;
|
||||
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
const part = parts[i];
|
||||
if (current[part] === undefined) {
|
||||
current[part] = {};
|
||||
}
|
||||
current = current[part];
|
||||
}
|
||||
|
||||
current[parts[parts.length - 1]] = value;
|
||||
this._saveToStorage();
|
||||
}
|
||||
|
||||
// Increment a numeric value
|
||||
increment(path, amount = 1) {
|
||||
const current = this.get(path, 0);
|
||||
this.set(path, current + amount);
|
||||
}
|
||||
|
||||
// Decrement a numeric value
|
||||
decrement(path, amount = 1) {
|
||||
this.increment(path, -amount);
|
||||
}
|
||||
|
||||
// Add item to an array (if not already present)
|
||||
addToArray(path, item) {
|
||||
const arr = this.get(path, []);
|
||||
if (!arr.includes(item)) {
|
||||
arr.push(item);
|
||||
this.set(path, arr);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove item from an array
|
||||
removeFromArray(path, item) {
|
||||
const arr = this.get(path, []);
|
||||
const index = arr.indexOf(item);
|
||||
if (index > -1) {
|
||||
arr.splice(index, 1);
|
||||
this.set(path, arr);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if array contains item
|
||||
hasItem(path, item) {
|
||||
const arr = this.get(path, []);
|
||||
return arr.includes(item);
|
||||
}
|
||||
|
||||
// Check if a specific chapter is complete
|
||||
isChapterComplete(chapterNumber) {
|
||||
const completed = this.get("chapters_completed", []);
|
||||
return completed.includes(chapterNumber);
|
||||
}
|
||||
|
||||
// Mark a chapter as complete
|
||||
markChapterComplete(chapterNumber) {
|
||||
this.addToArray("chapters_completed", chapterNumber);
|
||||
}
|
||||
|
||||
// Check if chapter can be played (previous chapters complete)
|
||||
canPlayChapter(chapterNumber) {
|
||||
if (chapterNumber <= 1) return true;
|
||||
return this.isChapterComplete(chapterNumber - 1);
|
||||
}
|
||||
|
||||
// Get the highest completed chapter number
|
||||
getHighestCompletedChapter() {
|
||||
const completed = this.get("chapters_completed", []);
|
||||
return completed.length > 0 ? Math.max(...completed) : 0;
|
||||
}
|
||||
|
||||
// Get entire state (for debugging)
|
||||
getAll() {
|
||||
return this._deepClone(this.state);
|
||||
}
|
||||
|
||||
// Reset all series state
|
||||
reset() {
|
||||
this.state = {};
|
||||
try {
|
||||
localStorage.removeItem(this.storageKey);
|
||||
} catch (e) {
|
||||
console.warn("Failed to clear series state from storage:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there is saved state
|
||||
hasSavedState() {
|
||||
try {
|
||||
return localStorage.getItem(this.storageKey) !== null;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Export state for debugging or backup
|
||||
exportState() {
|
||||
return JSON.stringify(this.state, null, 2);
|
||||
}
|
||||
|
||||
// Import state from backup
|
||||
importState(jsonString) {
|
||||
try {
|
||||
const data = JSON.parse(jsonString);
|
||||
this.state = data;
|
||||
this._saveToStorage();
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error("Failed to import state:", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_saveToStorage() {
|
||||
try {
|
||||
localStorage.setItem(this.storageKey, JSON.stringify(this.state));
|
||||
} catch (e) {
|
||||
console.warn("Failed to save series state:", e);
|
||||
}
|
||||
}
|
||||
|
||||
_loadFromStorage() {
|
||||
try {
|
||||
const saved = localStorage.getItem(this.storageKey);
|
||||
if (saved) {
|
||||
const parsed = JSON.parse(saved);
|
||||
this.state = this._mergeDeep(this.state, parsed);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Failed to load series state:", e);
|
||||
}
|
||||
}
|
||||
|
||||
_deepClone(obj) {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
_mergeDeep(target, source) {
|
||||
const result = { ...target };
|
||||
for (const key of Object.keys(source)) {
|
||||
if (
|
||||
source[key] &&
|
||||
typeof source[key] === "object" &&
|
||||
!Array.isArray(source[key])
|
||||
) {
|
||||
result[key] = this._mergeDeep(result[key] || {}, source[key]);
|
||||
} else {
|
||||
result[key] = source[key];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Make available globally
|
||||
window.SharedStateManager = SharedStateManager;
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
// State Manager - Manages game state with persistence and conditions
|
||||
class StateManager {
|
||||
constructor(gameId) {
|
||||
constructor(gameId, sharedState = null) {
|
||||
this.gameId = gameId;
|
||||
this.sharedState = sharedState; // Optional SharedStateManager for series games
|
||||
this.state = {};
|
||||
this.storageKey = `game_${gameId}_state`;
|
||||
}
|
||||
|
|
@ -13,18 +14,38 @@ class StateManager {
|
|||
}
|
||||
|
||||
// Get value using dot notation path (e.g., "inventory.sword")
|
||||
// Checks local state first, then falls back to shared state if available
|
||||
get(path, defaultValue = undefined) {
|
||||
// First check local state
|
||||
const localValue = this._getFromState(this.state, path);
|
||||
if (localValue !== undefined) {
|
||||
return localValue;
|
||||
}
|
||||
|
||||
// Fall back to shared state if available
|
||||
if (this.sharedState) {
|
||||
const sharedValue = this.sharedState.get(path);
|
||||
if (sharedValue !== undefined) {
|
||||
return sharedValue;
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
// Internal helper to get value from a specific state object
|
||||
_getFromState(stateObj, path) {
|
||||
const parts = path.split(".");
|
||||
let current = this.state;
|
||||
let current = stateObj;
|
||||
|
||||
for (const part of parts) {
|
||||
if (current === undefined || current === null) {
|
||||
return defaultValue;
|
||||
return undefined;
|
||||
}
|
||||
current = current[part];
|
||||
}
|
||||
|
||||
return current !== undefined ? current : defaultValue;
|
||||
return current;
|
||||
}
|
||||
|
||||
// Set value using dot notation path
|
||||
|
|
@ -80,6 +101,25 @@ class StateManager {
|
|||
return arr.includes(item);
|
||||
}
|
||||
|
||||
// Set value in shared state (for series games)
|
||||
setShared(path, value) {
|
||||
if (!this.sharedState) {
|
||||
console.warn("No shared state manager - setting locally instead");
|
||||
return this.set(path, value);
|
||||
}
|
||||
this.sharedState.set(path, value);
|
||||
}
|
||||
|
||||
// Check if path exists in shared state
|
||||
hasShared(path) {
|
||||
return this.sharedState?.get(path) !== undefined;
|
||||
}
|
||||
|
||||
// Get reference to shared state manager
|
||||
getSharedState() {
|
||||
return this.sharedState;
|
||||
}
|
||||
|
||||
// Evaluate a condition against current state
|
||||
evaluate(condition) {
|
||||
if (typeof condition === "boolean") {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue