446 lines
17 KiB
JavaScript
446 lines
17 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');
|
|
|
|
// 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;
|