// Game Engine - Main orchestrator for terminal games class GameEngine { constructor(gameDefinition) { this.definition = gameDefinition; 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 register() { if (!window.terminal) { console.warn( "Terminal not available, cannot register game:", this.definition.id, ); return; } this.terminal = window.terminal; const self = this; const def = this.definition; // this.terminal.registerCommand( // def.command || def.id, // def.description || `Play ${def.name}`, // async (args) => { // if (args[0] === "reset") { // 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; // } // await self.start(); // }, // ); } // Start the game async start(continueGame = false) { if (this.isRunning) { this.terminal.printWarning("Game is already running!"); 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); this.isRunning = true; // Initialize components this.adapter = new TerminalAdapter(this.terminal); this.state = new StateManager(this.definition.id, this.sharedState); this.input = new InputManager(this.adapter); // Initialize sound manager if SoundManager is available if (window.SoundManager) { this.sound = new SoundManager(this.adapter); } this.scenes = new SceneManager( this.adapter, this.state, this.input, this.sound, ); // Initialize state this.state.init(this.definition.initialState || {}); // Load and merge external scenes if defined const mergedScenes = this._getMergedScenes(); // Register scenes this.scenes.registerScenes(mergedScenes); // Hook into terminal input this._hookInput(); // Check for existing save const hasSave = this.state.hasSavedState(); // Show intro unless continuing if (!continueGame && !hasSave) { if (this.definition.intro) { await this._playIntro(); } } else if (hasSave && !continueGame) { // Ask if player wants to continue this.adapter.clear(); this.adapter.printInfo("A saved game was found."); const shouldContinue = await this.input.awaitConfirm( "Continue from where you left off?", ); if (!shouldContinue) { this.state.reset(); this.state.init(this.definition.initialState || {}); if (this.definition.intro) { await this._playIntro(); } } } // Go to start scene (or last scene if continuing) const startScene = this.state.get("_currentScene") || this.definition.startScene || "start"; await this.scenes.goTo(startScene); } async _playIntro() { this.adapter.clear(); // Show title if (this.definition.name) { this.adapter.printHTML( `
${this.definition.name}
`, ); this.adapter.print(""); } // Show intro content if (typeof this.definition.intro === "string") { this.adapter.print(this.definition.intro); } else if (Array.isArray(this.definition.intro)) { await this.scenes._renderContent(this.definition.intro); } this.adapter.print(""); // Wait for input to continue await this.input.awaitOption( [{ text: "Begin", value: true }], "Press Enter or 1 to start...", ); } _hookInput() { // Store original executeCommand this.originalExecuteCommand = this.terminal.executeCommand.bind( this.terminal, ); // Override to intercept input const self = this; this.terminal.executeCommand = function (commandString) { if (!self.isRunning) { return self.originalExecuteCommand(commandString); } // Check for exit commands const cmd = commandString.toLowerCase().trim(); if (cmd === "quit" || cmd === "exit" || cmd === "q") { self.stop(); return; } // Check for save command if (cmd === "save") { self._saveProgress(); return; } // Check for debug scene skip command (goto ) if (cmd.startsWith("goto ")) { const sceneName = commandString.trim().substring(5).trim(); self._debugGoToScene(sceneName); return; } // Pass to input manager if in text mode if (self.input.getMode() === "text") { self.input.handleTextInput(commandString); } }; } _saveProgress() { // Save current scene for resuming const currentSceneId = this.scenes.getCurrentSceneId(); if (currentSceneId) { this.state.set("_currentScene", currentSceneId); } this.adapter.printSuccess("Game progress saved."); } // Stop the game and restore terminal stop() { // Save progress before stopping this._saveProgress(); // Update the global HTML to remove the game in progress details document.body.classList.remove("game-in-progress"); this.isRunning = false; // Cleanup sound manager if (this.sound) { this.sound.stopAll(); } // Cleanup input manager if (this.input) { this.input.destroy(); } // Restore original command handler if (this.originalExecuteCommand) { this.terminal.executeCommand = this.originalExecuteCommand; } this.adapter.print(""); this.adapter.printInfo(`Exited ${this.definition.name}. Progress saved.`); this.adapter.print('Type "help" for available commands.'); this.adapter.print( `Type "${this.definition.command || this.definition.id}" to continue playing.`, ); } _reset() { if (this.state) { this.state.reset(); } else { // Create temporary state manager just to reset const tempState = new StateManager(this.definition.id); tempState.reset(); } this.terminal.printSuccess( `${this.definition.name} progress has been reset.`, ); } // 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); if (!scene) { this.adapter.printError(`Scene not found: ${sceneName}`); this.adapter.print(""); this.adapter.printInfo("Available scenes:"); const sceneNames = Object.keys(this.definition.scenes).sort(); for (const name of sceneNames) { this.adapter.print(` ${name}`); } return; } this.adapter.printWarning(`[DEBUG] Skipping to scene: ${sceneName}`); await this.scenes.goTo(sceneName); } // Check if game is currently running isActive() { return this.isRunning; } // Get game definition getDefinition() { return this.definition; } } // Make available globally window.GameEngine = GameEngine;