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'); // Declare dependencies on TTS, animation-queue, and our new UI modules this.dependencies = ['tts', 'animation-queue', 'ui-display-handler', 'ui-input-handler', 'ui-effects']; // 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; // Bind methods that use 'this' internally or are used as callbacks/event handlers this.initialize = this.initialize.bind(this); // Bind initialize as it calls dispatchEvent this.handleCommand = this.handleCommand.bind(this); // Bind event handler this.displayText = this.displayText.bind(this); // Bind if passed as callback this.setupBookInterface = this.setupBookInterface.bind(this); this.applyBookSizing = this.applyBookSizing.bind(this); this.setupEventListeners = this.setupEventListeners.bind(this); this.setupMainUI = this.setupMainUI.bind(this); this.initializeTextBuffer = this.initializeTextBuffer.bind(this); this.showUI = this.showUI.bind(this); this.hideUI = this.hideUI.bind(this); this.clearDisplay = this.clearDisplay.bind(this); this.sendCommand = this.sendCommand.bind(this); this.updateButtonStates = this.updateButtonStates.bind(this); // Store a bound version of dispatchEvent for use in methods this._dispatchModuleEvent = (name, detail) => { document.dispatchEvent(new CustomEvent(name, { detail: { moduleId: this.id, ...detail }, bubbles: true })); }; } async initialize() { this.reportProgress(0, 'Initializing UI Controller'); try { this.reportProgress(20, 'Setting up book interface'); // Set up book interface this.setupBookInterface(); this.reportProgress(30, 'Setting up UI components'); // Get module references this.displayHandler = moduleRegistry.getModule('ui-display-handler'); this.inputHandler = moduleRegistry.getModule('ui-input-handler'); this.effects = moduleRegistry.getModule('ui-effects'); // Get additional dependencies this.textBuffer = moduleRegistry.getModule('text-buffer'); this.ttsHandler = moduleRegistry.getModule('tts'); this.socketClient = moduleRegistry.getModule('socket-client'); this.animationQueue = moduleRegistry.getModule('animation-queue'); if (!this.displayHandler || !this.inputHandler || !this.effects) { console.error('UI Controller: Required UI modules not found'); return false; } this.reportProgress(50, 'Setting up event listeners'); // Set up event listeners between components this.setupEventListeners(); this.reportProgress(80, 'Finalizing UI initialization'); // Initialize main UI container await this.setupMainUI(); // Initialize text buffer handler this.initializeTextBuffer(); this.isReady = true; this.isVisible = true; this.reportProgress(100, 'UI Controller ready'); // Start ambient effects this.effects.startAmbientEffects(); // Use the DOM event API directly instead of this.dispatchEvent this._dispatchModuleEvent('ui:ready', { controller: this }); 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', () => { // Use the DOM event API directly this._dispatchModuleEvent('ui:ready:for:next', {}); }); // Listen for socket connection events document.addEventListener('socket:connected', () => { console.log('UI Controller: Socket connected'); }); document.addEventListener('socket:disconnected', () => { console.log('UI Controller: Socket disconnected'); }); // Handle speed reset const speedReset = document.getElementById('speed_reset'); if (speedReset) { speedReset.addEventListener('click', (e) => { e.preventDefault(); const speedSlider = document.getElementById('speed'); if (speedSlider) { speedSlider.value = 50; if (this.animationQueue) { this.animationQueue.setSpeed(1.0); } } }); } // Handle speed slider change for animation speed const speedSlider = document.getElementById('speed'); if (speedSlider) { speedSlider.addEventListener('input', (e) => { if (this.animationQueue) { // Convert slider value (0-100) to animation speed // Using formula from Documentation.md: lower values = slower speed const value = parseInt(e.target.value); const speed = Math.pow(100.0 - value, 3) / 10000 * 10 + 0.01; this.animationQueue.setSpeed(speed); console.log(`UI Controller: Animation speed set to ${speed.toFixed(3)}`); // Save to persistence manager if available if (window.PersistenceManager) { window.PersistenceManager.updatePreference('animation', 'speed', value); } } }); // Set initial speed from persistence manager if available if (window.PersistenceManager) { const savedSpeed = window.PersistenceManager.getPreference('animation', 'speed', 50); speedSlider.value = savedSpeed; // Apply initial speed if (this.animationQueue) { const speed = Math.pow(100.0 - savedSpeed, 3) / 10000 * 10 + 0.01; this.animationQueue.setSpeed(speed); } } } // Handle speech toggle with proper state management const speechToggle = document.getElementById('speech'); if (speechToggle && this.ttsHandler) { // Remove disabled attribute to make it clickable speechToggle.removeAttribute('disabled'); speechToggle.addEventListener('click', (e) => { e.preventDefault(); console.log('Speech toggle clicked'); // Toggle TTS state if (this.ttsHandler && typeof this.ttsHandler.toggle === 'function') { this.ttsEnabled = this.ttsHandler.toggle(); // Update button text speechToggle.textContent = this.ttsEnabled ? 'mute' : 'speech'; // Save preference if persistence manager is available const persistenceManager = moduleRegistry.getModule('persistence-manager'); if (persistenceManager) { persistenceManager.updatePreference('tts', 'enabled', this.ttsEnabled); } console.log(`UI Controller: TTS ${this.ttsEnabled ? 'enabled' : 'disabled'}`); } else { console.warn('TTS Handler does not have toggle method'); } }); } // 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'; // Add event listener optionsButton.addEventListener('click', (e) => { e.preventDefault(); const optionsUI = moduleRegistry.getModule('options-ui'); if (optionsUI && optionsUI.toggle) { optionsUI.toggle(); } }); // Add to controls controlsSection.appendChild(document.createTextNode(' | ')); controlsSection.appendChild(optionsButton); } } // Enable all controls buttons const controlButtons = document.querySelectorAll('#controls a'); controlButtons.forEach(button => { button.removeAttribute('disabled'); }); // Book click for fast-forwarding - make sure it triggers the animation queue if (this.bookElement) { this.bookElement.addEventListener('click', (event) => { // Only if not clicking on a link or control if (event.target.tagName !== 'A' && !event.target.closest('#controls') && !event.target.closest('#command_input')) { if (this.animationQueue) { console.log('UI Controller: Fast-forwarding animations'); this.animationQueue.fastForward(); } } }); } // Space key for fast-forwarding document.addEventListener('keydown', (e) => { if (e.key === ' ' && document.activeElement.tagName !== 'TEXTAREA' && document.activeElement.tagName !== 'INPUT') { if (this.animationQueue) { console.log('UI Controller: Fast-forwarding animations (space key)'); this.animationQueue.fastForward(); e.preventDefault(); // Prevent page scrolling } } }); } 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) { this.textBuffer.setOnSentenceReady((text, callback) => { console.log('UI Controller: Displaying sentence'); this.displayText(text).then(callback); }); } } 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) { this.socketClient.sendCommand(command.text); } 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._dispatchModuleEvent('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'); // 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'); } } } // 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;