/** * Game Loop Module for AI Interactive Fiction * Manages the main game logic and connects various modules */ import { BaseModule } from './base-module.js'; class GameLoopModule extends BaseModule { constructor() { super('game-loop', 'Game Loop'); // Dependencies this.dependencies = ['ui-controller', 'socket-client', 'text-buffer', 'sentence-queue', 'playback-coordinator', 'animation-queue', 'audio-manager', 'tts-factory', 'ui-input-handler', 'story-history']; // Game state this.gameState = { started: false, canLoad: false, currentRoom: null, inventory: [], commandHistory: [] }; this.isRunning = false; // Bind methods using parent's bindMethods utility this.bindMethods([ 'start', 'setupUiEventListeners', 'setupSocketEventListeners', 'updateGameState', 'updateUIState', 'refreshGameApiState', 'hasSaveGame', 'queueUnrenderedHistoryBlocks', 'requestStartGame', 'requestSaveGame', 'requestLoadGame', 'resetClientPlaybackAndDisplay' ]); } async initialize() { this.reportProgress(100, "Game loop initialized"); return true; } start() { console.log("GameLoop: Starting game sequence..."); try { // The dependencies are now automatically available via getModule this.updateUIState(); this.setupUiEventListeners(); console.log("GameLoop: Setting up socket listeners and connecting..."); // Set up socket event listeners and connect this.setupSocketEventListeners(); // Set the game loop as running this.isRunning = true; } catch (error) { console.error("Error starting game loop:", error); } } setupUiEventListeners() { if (this.uiEventsBound) return; this.uiEventsBound = true; document.addEventListener('ui:game:restart', () => this.requestStartGame()); document.addEventListener('ui:game:save', () => this.requestSaveGame()); document.addEventListener('ui:game:load', () => this.requestLoadGame()); } setupSocketEventListeners() { // Get the socket client module using parent's getModule method const socketClient = this.getModule('socket-client'); if (!socketClient) { console.error("Socket client module not found"); return; } // Connect UI controller to socket client for command handling const uiController = this.getModule('ui-controller'); if (uiController) { uiController.socketClient = socketClient; } else { console.warn("GameLoop: UI Controller not ready for Socket Client assignment."); } // Listen for socket connection event socketClient.on('connect', () => { console.log("GameLoop: Socket connected event received."); this.refreshGameApiState(); }); // Listen for game state updates socketClient.on('gameStateUpdate', (data) => { console.log("GameLoop: Game state update received", data); this.updateGameState(data); }); // Listen for narrative responses socketClient.on('narrativeResponse', (data) => { console.log("GameLoop: Narrative response received", data); // Text processing is handled by socket-client -> text-buffer -> ui-controller pipeline }); socketClient.on('gameSaved', () => { this.gameState.canLoad = true; this.updateUIState(); }); socketClient.on('gameLoaded', () => { this.gameState.started = true; this.gameState.canSave = true; this.gameState.canLoad = true; this.updateUIState(); }); // Connect to the socket server socketClient.connect().then(success => { if (success) { console.log("GameLoop: Socket connection established successfully."); } else { console.error("GameLoop: Failed to connect to socket server"); } }); } async refreshGameApiState() { const socketClient = this.getModule('socket-client'); if (!socketClient || !socketClient.getConnectionStatus()) return; const [running, hasSave] = await Promise.all([ socketClient.isGameRunning(), this.hasSaveGame(1) ]); this.gameState.started = Boolean(running?.result); this.gameState.canSave = this.gameState.started; this.gameState.canLoad = Boolean(hasSave?.result); this.updateUIState(); } /** * Update the game state * @param {Object} data - New game state data */ updateGameState(data) { if (!data) return; // Update game state if (data.currentRoom) { this.gameState.currentRoom = data.currentRoom; } if (data.inventory) { this.gameState.inventory = data.inventory; } // Update UI with new game state this.updateUIState(); } /** * Update UI with current game state */ updateUIState() { const uiController = this.getModule('ui-controller'); if (!uiController) return; // Update UI components based on game state const state = { canRestart: true, canSave: Boolean(this.gameState.started), canLoad: Boolean(this.gameState.canLoad), gameStarted: Boolean(this.gameState.started) }; document.body.dataset.gameRunning = state.gameStarted ? 'true' : 'false'; uiController.updateButtonStates(state); } /** * Request to start a new game */ async requestStartGame() { const socketClient = this.getModule('socket-client'); if (!socketClient) return; await this.resetClientPlaybackAndDisplay(); const storyHistory = this.getModule('story-history'); if (storyHistory && typeof storyHistory.startNewGame === 'function') { await storyHistory.startNewGame(); } const response = await socketClient.newGame(); if (!response?.success) { console.error('GameLoop: newGame failed', response); return; } this.gameState.started = true; this.gameState.canSave = true; this.gameState.canLoad = Boolean(response.canLoad); this.updateUIState(); } /** * Request to save the current game */ async requestSaveGame() { const socketClient = this.getModule('socket-client'); if (!socketClient || !this.gameState.started) return; const response = await socketClient.saveGame(1); if (response?.success) { const storyHistory = this.getModule('story-history'); const audioManager = this.getModule('audio-manager'); if (storyHistory && typeof storyHistory.saveSlot === 'function') { await storyHistory.saveSlot(1, { inkState: response.savedState || null, latestRenderedBlockId: storyHistory.latestRenderedBlockId || 0, renderedLineCount: storyHistory.renderedLineCount || 0, musicState: audioManager?.getMusicState?.() || null }); } this.gameState.canLoad = true; this.updateUIState(); } } /** * Request to load a saved game */ async requestLoadGame() { const socketClient = this.getModule('socket-client'); if (!socketClient) return; const storyHistory = this.getModule('story-history'); const browserSave = storyHistory && typeof storyHistory.loadSlot === 'function' ? await storyHistory.loadSlot(1) : null; const hasSave = browserSave ? { result: true } : await socketClient.hasSaveGame(1); if (!hasSave?.result) { this.gameState.canLoad = false; this.updateUIState(); return; } await this.resetClientPlaybackAndDisplay(); document.dispatchEvent(new CustomEvent('story:history-restoring', { detail: { active: true, reason: 'load-game' } })); if (browserSave?.gameId && storyHistory?.setCurrentGame) { storyHistory.setCurrentGame( browserSave.gameId, browserSave.latestBlockId || 0, browserSave.latestRenderedBlockId || 0, browserSave.renderedLineCount || 0 ); } const uiController = this.getModule('ui-controller'); if (browserSave && uiController?.displayHandler?.restoreFromHistory) { await uiController.displayHandler.restoreFromHistory(browserSave); } const audioManager = this.getModule('audio-manager'); if (browserSave?.musicState && audioManager?.restoreMusicState) { await audioManager.restoreMusicState(browserSave.musicState); } const hasUnrenderedHistory = browserSave && Number(browserSave.latestBlockId || 0) > Number(browserSave.latestRenderedBlockId || 0); if (hasUnrenderedHistory) { const sentenceQueue = this.getModule('sentence-queue'); sentenceQueue?.pauseBeforeNext?.('load-resume'); document.dispatchEvent(new CustomEvent('story:process-state', { detail: { state: 'waiting-generating', reason: 'restoring-pending-output' } })); } const response = await socketClient.loadGame(1, browserSave?.inkState || null); if (response?.success && hasUnrenderedHistory) { await this.queueUnrenderedHistoryBlocks(browserSave); } if (response?.success) { this.gameState.started = true; this.gameState.canSave = true; this.gameState.canLoad = true; this.updateUIState(); } if (!hasUnrenderedHistory) { document.dispatchEvent(new CustomEvent('story:history-restoring', { detail: { active: false, reason: 'load-game-complete' } })); } else { const clearRestoring = () => { document.dispatchEvent(new CustomEvent('story:history-restoring', { detail: { active: false, reason: 'pending-output-drained' } })); document.removeEventListener('tts:queue-empty', clearRestoring); }; document.addEventListener('tts:queue-empty', clearRestoring); } } async hasSaveGame(slot = 1) { const storyHistory = this.getModule('story-history'); if (storyHistory && typeof storyHistory.hasSaveSlot === 'function') { const hasBrowserSave = await storyHistory.hasSaveSlot(slot); if (hasBrowserSave) return { success: true, result: true, slot }; } const socketClient = this.getModule('socket-client'); return socketClient?.hasSaveGame ? socketClient.hasSaveGame(slot) : { success: false, result: false }; } async queueUnrenderedHistoryBlocks(saveRecord = {}) { const storyHistory = this.getModule('story-history'); const textBuffer = this.getModule('text-buffer'); if (!storyHistory || !textBuffer || typeof textBuffer.addBlocks !== 'function') return; const start = Math.max(1, Number(saveRecord.latestRenderedBlockId || 0) + 1); const end = Math.max(0, Number(saveRecord.latestBlockId || 0)); if (end < start) return; const blocks = await storyHistory.getBlocksRange(saveRecord.gameId, start, end); textBuffer.addBlocks(blocks); } async resetClientPlaybackAndDisplay() { const playbackCoordinator = this.getModule('playback-coordinator'); if (playbackCoordinator && typeof playbackCoordinator.stop === 'function') { await playbackCoordinator.stop(); } const animationQueue = this.getModule('animation-queue'); if (animationQueue && typeof animationQueue.clearAll === 'function') { animationQueue.clearAll(); } const ttsFactory = this.getModule('tts-factory'); if (ttsFactory && typeof ttsFactory.stop === 'function') { ttsFactory.stop(); } const audioManager = this.getModule('audio-manager'); if (audioManager && typeof audioManager.stopAllSounds === 'function') { audioManager.stopAllSounds(); } const sentenceQueue = this.getModule('sentence-queue'); if (sentenceQueue && typeof sentenceQueue.clear === 'function') { sentenceQueue.clear(); } const textBuffer = this.getModule('text-buffer'); if (textBuffer && typeof textBuffer.clear === 'function') { textBuffer.clear(); } const uiController = this.getModule('ui-controller'); if (uiController) { uiController.clearDisplay(); } const inputHandler = this.getModule('ui-input-handler'); if (inputHandler && typeof inputHandler.clearHistory === 'function') { inputHandler.clearHistory(); } } } // Create the singleton instance const GameLoop = new GameLoopModule(); // Export the module export { GameLoop };