377 lines
14 KiB
JavaScript
377 lines
14 KiB
JavaScript
/**
|
|
* 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 };
|