Files
ai.interactive.fiction/public/js/ui-controller.js
T

449 lines
16 KiB
JavaScript

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;