import { BaseModule } from './base-module.js'; import { moduleRegistry } from './module-registry.js'; import { ModuleEvent } from './base-module.js'; class UIController extends BaseModule { constructor() { super('ui-controller', 'UI Controller'); // Remove 'tts' from direct dependencies to break circular dependency // UI Controller will access TTS through the Game Loop instead this.dependencies = ['animation-queue', 'ui-display-handler', 'ui-input-handler', 'ui-effects', 'text-buffer', 'socket-client']; // References to sub-modules this.displayHandler = null; this.inputHandler = null; this.effects = null; // UI state this.isReady = false; this.isVisible = false; // Book interface elements this.bookElement = null; this.leftPage = null; this.rightPage = null; this.storyElement = null; // Additional module references this.textBuffer = null; this.ttsHandler = null; this.socketClient = null; this.animationQueue = null; // Add TTS toggle state this.ttsEnabled = false; this.ttsAvailable = true; // Add TTS availability state // Bind methods using the parent class bindMethods utility this.bindMethods([ 'initialize', 'handleCommand', 'displayText', 'setupBookInterface', 'applyBookSizing', 'setupEventListeners', 'setupMainUI', 'initializeTextBuffer', 'showUI', 'hideUI', 'clearDisplay', 'sendCommand', 'updateButtonStates' ]); } async initialize() { try { this.reportProgress(0, 'Initializing UI Controller'); this.reportProgress(20, 'Setting up book interface'); // Set up book interface this.setupBookInterface(); this.reportProgress(30, 'Getting module dependencies'); // Get module references using parent's getModule method this.displayHandler = this.getModule('ui-display-handler'); this.inputHandler = this.getModule('ui-input-handler'); this.effects = this.getModule('ui-effects'); this.textBuffer = this.getModule('text-buffer'); this.socketClient = this.getModule('socket-client'); this.animationQueue = this.getModule('animation-queue'); // Check for required UI modules if (!this.displayHandler) { console.error('UI Controller: Display handler module not found'); return false; } if (!this.inputHandler) { console.error('UI Controller: Input handler module not found'); return false; } if (!this.effects) { console.error('UI Controller: UI effects module not found'); return false; } // Check for other required modules if (!this.textBuffer) { console.error('UI Controller: Text buffer module not found'); return false; } if (!this.socketClient) { console.error('UI Controller: Socket client module not found'); return false; } if (!this.animationQueue) { console.error('UI Controller: Animation queue module not found'); return false; } this.reportProgress(50, 'Setting up event listeners'); // Set up event listeners between components this.setupEventListeners(); this.reportProgress(70, 'Setting up main UI'); // Initialize main UI container await this.setupMainUI(); this.reportProgress(80, 'Initializing text buffer'); // Initialize text buffer handler this.initializeTextBuffer(); this.reportProgress(100, 'UI Controller ready'); this.isReady = true; this.isVisible = true; this.dispatchEvent(new ModuleEvent('ui:ready', { controller: this })); // Start ambient effects this.effects.startAmbientEffects(); return true; } catch (error) { console.error('Error initializing UI Controller:', error); this.changeState('ERROR'); return false; } } setupBookInterface() { // Create or get the book interface elements this.bookElement = document.getElementById('book'); this.leftPage = document.getElementById('page_left'); this.rightPage = document.getElementById('page_right'); this.storyElement = document.getElementById('story'); // Apply book sizing based on viewport this.applyBookSizing(); // Set up window resize handler window.addEventListener('resize', () => this.applyBookSizing()); } applyBookSizing() { // Apply book sizing based on viewport dimensions const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; const aspectRatio = viewportWidth / viewportHeight; document.documentElement.style.setProperty('--viewport-aspect-ratio', aspectRatio); const maxBookHeight = viewportHeight * 0.9; document.documentElement.style.setProperty('--book-height', `${maxBookHeight}px`); const bookWidth = maxBookHeight * Math.min(aspectRatio, 1.613); document.documentElement.style.setProperty('--book-width', `${bookWidth}px`); } setupEventListeners() { // Listen for command events from input handler - use arrow function to preserve context document.addEventListener('ui:command', (event) => { this.handleCommand(event.detail); }); // Listen for text display events - use arrow function to preserve context document.addEventListener('ui:text:complete', (event) => { console.log('UIController: Text complete event received, ready for next text'); }); // Listen for socket connection events document.addEventListener('socket:connected', () => { console.log('UIController: Socket connected'); this.updateButtonStates(); }); document.addEventListener('socket:disconnected', () => { console.log('UIController: Socket disconnected'); this.updateButtonStates(); }); // Listen for TTS state change events document.addEventListener('tts:stateChange', (event) => { if (event.detail) { if (typeof event.detail.enabled === 'boolean') { this.ttsEnabled = event.detail.enabled; } if (typeof event.detail.available === 'boolean') { this.ttsAvailable = event.detail.available; } this.updateButtonStates(); } }); // Listen for TTS availability events document.addEventListener('tts:availability', (event) => { if (event.detail && typeof event.detail.available === 'boolean') { this.ttsAvailable = event.detail.available; this.updateButtonStates(); } }); // Add options button to controls section const controlsSection = document.getElementById('controls'); if (controlsSection) { // Check if options button already exists if (!document.getElementById('options-button')) { const optionsButton = document.createElement('a'); optionsButton.id = 'options-button'; optionsButton.href = '#'; optionsButton.textContent = 'options'; optionsButton.title = 'Show game options'; optionsButton.className = 'control-button'; optionsButton.addEventListener('click', (e) => { e.preventDefault(); document.dispatchEvent(new CustomEvent('ui:showOptions')); }); controlsSection.appendChild(optionsButton); } // Add speech toggle button const speechToggle = document.getElementById('speech-toggle'); if (speechToggle) { speechToggle.addEventListener('click', (e) => { e.preventDefault(); // Dispatch an event for the TTS module to handle instead of calling directly document.dispatchEvent(new CustomEvent('tts:toggle')); }); } } // Listen for window resize events window.addEventListener('resize', () => { this.applyBookSizing(); }); // Listen for key events document.addEventListener('keydown', (event) => { // Pass to input handler if (this.inputHandler) { this.inputHandler.handleKeyboardInput(event); } }); } async setupMainUI() { // Ensure all UI components exist if (!this.bookElement || !this.leftPage || !this.rightPage || !this.storyElement) { console.log('UI Controller: Creating missing UI elements'); this.displayHandler.setupBookStructure(); // Re-get elements this.bookElement = document.getElementById('book'); this.leftPage = document.getElementById('page_left'); this.rightPage = document.getElementById('page_right'); this.storyElement = document.getElementById('story'); } } initializeTextBuffer() { // Initialize text buffer handling if (this.textBuffer) { console.log('UIController: Setting up text buffer callback'); this.textBuffer.setOnSentenceReady((text, callback) => { console.log('UIController: Received sentence from text buffer, displaying'); // Use the display handler to show text with proper formatting and TTS this.displayText(text) .then(() => { console.log('UIController: Display of sentence completed, continuing...'); // Signal that we're ready to process the next sentence if (typeof callback === 'function') { // Use a small timeout to prevent potential stack overflow with many sentences setTimeout(() => callback(), 10); } }) .catch(error => { console.error('UIController: Error displaying text:', error); // Continue anyway to prevent blocking if (typeof callback === 'function') callback(); }); }); console.log('UIController: Text buffer callback set up'); } else { console.warn('UIController: Text buffer module not found'); } } handleCommand(command) { // Route commands to appropriate handlers switch (command.type) { case 'display': this.displayHandler.processCommand(command); break; case 'effect': this.effects.processCommand(command); break; case 'continue': if (this.animationQueue) { this.animationQueue.fastForward(); } break; case 'input': if (this.socketClient) { console.log(`UI Controller: Sending command to socket: "${command.text}"`); const success = this.socketClient.sendCommand(command.text); if (success) { console.log('UI Controller: Command sent successfully'); } else { console.error('UI Controller: Failed to send command to socket'); // Display an error message to the user this.displayHandler.displayText('⚠️ Unable to send command. Server connection might be lost.', { style: { color: '#990000' } }); } } else { console.error('UI Controller: Socket client not available for sending commands'); } break; case 'menu': // Toggle options menu const optionsUI = moduleRegistry.getModule('options-ui'); if (optionsUI) { optionsUI.toggle(); } break; default: // Handle general UI commands or pass to game logic this.dispatchEvent(new ModuleEvent('ui:command', command)); } } /** * Update UI button states based on game state * @param {Object} state - Game state information */ updateButtonStates(state = {}) { const { canSave, canLoad, canRestart } = state; // Get button elements const saveButton = document.getElementById('save'); const loadButton = document.getElementById('reload'); const restartButton = document.getElementById('rewind'); const speechToggle = document.getElementById('speech-toggle'); // Update save button state if (saveButton) { if (canSave) { saveButton.removeAttribute('disabled'); } else { saveButton.setAttribute('disabled', 'disabled'); } } // Update load button state if (loadButton) { if (canLoad) { loadButton.removeAttribute('disabled'); } else { loadButton.setAttribute('disabled', 'disabled'); } } // Update restart button state if (restartButton) { if (canRestart) { restartButton.removeAttribute('disabled'); } else { restartButton.setAttribute('disabled', 'disabled'); } } // Update speech toggle button state if (speechToggle) { // Update the button appearance based on TTS state if (this.ttsEnabled) { speechToggle.classList.add('active'); speechToggle.title = 'Disable speech'; } else { speechToggle.classList.remove('active'); speechToggle.title = 'Enable speech'; } // Disable the button completely if TTS is not available if (this.ttsAvailable === false) { speechToggle.setAttribute('disabled', 'disabled'); speechToggle.title = 'Speech not available'; } else { speechToggle.removeAttribute('disabled'); } } } // Public API methods showUI() { if (!this.isVisible) { this.isVisible = true; this.displayHandler.show(); this.effects.startAmbientEffects(); } } hideUI() { if (this.isVisible) { this.isVisible = false; this.displayHandler.hide(); this.effects.stopAmbientEffects(); } } displayText(text, options = {}) { return this.displayHandler.displayText(text, options); } clearDisplay() { this.displayHandler.clear(); } sendCommand(command) { if (this.socketClient) { return this.socketClient.sendCommand(command); } return false; } } // Create the singleton instance const uiController = new UIController(); // Register with the module registry moduleRegistry.register(uiController); // Export the module export { uiController as UIController }; // Keep a reference in window for loader system window.UIController = uiController;