Refactored modules and updated loader.
This commit is contained in:
@@ -0,0 +1,565 @@
|
||||
import { BaseModule } from './base-module.js';
|
||||
import { ModuleEvent } from './base-module.js';
|
||||
|
||||
class UIControllerModule 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() {
|
||||
// Set up event listeners for menu buttons
|
||||
const saveButton = document.getElementById('save');
|
||||
const loadButton = document.getElementById('reload');
|
||||
const restartButton = document.getElementById('rewind');
|
||||
const speechToggle = document.getElementById('speech');
|
||||
const optionsButton = document.getElementById('options');
|
||||
|
||||
// Get persistence manager module
|
||||
const persistenceManager = this.getModule('persistence-manager');
|
||||
|
||||
// Set up save button
|
||||
if (saveButton) {
|
||||
saveButton.addEventListener('click', () => {
|
||||
document.dispatchEvent(new CustomEvent('ui:game:save'));
|
||||
});
|
||||
}
|
||||
|
||||
// Set up load button
|
||||
if (loadButton) {
|
||||
loadButton.addEventListener('click', () => {
|
||||
document.dispatchEvent(new CustomEvent('ui:game:load'));
|
||||
});
|
||||
}
|
||||
|
||||
// Set up restart button
|
||||
if (restartButton) {
|
||||
restartButton.addEventListener('click', () => {
|
||||
document.dispatchEvent(new CustomEvent('ui:game:restart'));
|
||||
});
|
||||
}
|
||||
|
||||
// Set up speech toggle button
|
||||
if (speechToggle) {
|
||||
// Initialize ttsEnabled from persistence manager
|
||||
if (persistenceManager) {
|
||||
const prefs = persistenceManager.getAllPreferences();
|
||||
this.ttsEnabled = prefs.tts?.enabled ?? false;
|
||||
|
||||
// Update button state
|
||||
this.updateButtonStates();
|
||||
}
|
||||
|
||||
speechToggle.addEventListener('click', () => {
|
||||
// Toggle TTS state
|
||||
this.ttsEnabled = !this.ttsEnabled;
|
||||
|
||||
// Update UI
|
||||
this.updateButtonStates();
|
||||
|
||||
// Save preference
|
||||
if (persistenceManager) {
|
||||
persistenceManager.updatePreference('tts', 'enabled', this.ttsEnabled);
|
||||
}
|
||||
|
||||
// Notify other components
|
||||
document.dispatchEvent(new CustomEvent('ui:tts:toggle', {
|
||||
detail: { enabled: this.ttsEnabled }
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
// Set up options button
|
||||
if (optionsButton) {
|
||||
optionsButton.addEventListener('click', () => {
|
||||
document.dispatchEvent(new CustomEvent('ui:options:toggle'));
|
||||
});
|
||||
}
|
||||
|
||||
// Listen for book events
|
||||
document.addEventListener('book:ready', () => {
|
||||
this.updateButtonStates({
|
||||
canSave: true,
|
||||
canLoad: true,
|
||||
canRestart: true
|
||||
});
|
||||
});
|
||||
|
||||
// Listen for restart events
|
||||
document.addEventListener('story:restart', () => {
|
||||
this.updateButtonStates({
|
||||
canSave: true,
|
||||
canLoad: false,
|
||||
canRestart: false
|
||||
});
|
||||
});
|
||||
|
||||
// Listen for save events
|
||||
document.addEventListener('story:save', () => {
|
||||
this.updateButtonStates({
|
||||
canSave: true,
|
||||
canLoad: true,
|
||||
canRestart: true
|
||||
});
|
||||
});
|
||||
|
||||
// Listen for TTS availability changes
|
||||
document.addEventListener('tts:availability', (event) => {
|
||||
if (event.detail && typeof event.detail.available === 'boolean') {
|
||||
this.ttsAvailable = event.detail.available;
|
||||
this.updateButtonStates();
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for TTS state changes (from options UI or TTS player)
|
||||
document.addEventListener('tts:stateChange', (event) => {
|
||||
if (event.detail && typeof event.detail.enabled === 'boolean') {
|
||||
this.ttsEnabled = event.detail.enabled;
|
||||
this.updateButtonStates();
|
||||
|
||||
// Ensure persistence is updated
|
||||
if (persistenceManager) {
|
||||
persistenceManager.updatePreference('tts', 'enabled', this.ttsEnabled);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for TTS engine changes
|
||||
document.addEventListener('tts:engine:change', (event) => {
|
||||
// Update button states since TTS engine changed
|
||||
this.updateButtonStates();
|
||||
});
|
||||
|
||||
// Listen for TTS toggle events from other components
|
||||
document.addEventListener('tts:enabled:change', (event) => {
|
||||
if (event.detail && typeof event.detail.enabled === 'boolean') {
|
||||
this.ttsEnabled = event.detail.enabled;
|
||||
this.updateButtonStates();
|
||||
|
||||
// Ensure persistence is updated
|
||||
if (persistenceManager) {
|
||||
persistenceManager.updatePreference('tts', 'enabled', this.ttsEnabled);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Set up speed slider in main UI
|
||||
const speedSlider = document.getElementById('speed');
|
||||
const speedReset = document.getElementById('speed_reset');
|
||||
|
||||
if (speedSlider) {
|
||||
// Initialize speed from persistence manager
|
||||
if (persistenceManager) {
|
||||
const prefs = persistenceManager.getAllPreferences();
|
||||
// Get the unified speed value (0-1 range)
|
||||
const speed = prefs.tts?.speed ?? 0.5;
|
||||
// Convert to slider range (0-100)
|
||||
speedSlider.value = Math.round(speed * 100);
|
||||
}
|
||||
|
||||
speedSlider.addEventListener('input', (e) => {
|
||||
// Convert slider value (0-100) to normalized speed (0-1)
|
||||
const speed = parseInt(e.target.value) / 100;
|
||||
|
||||
// Scale for different TTS engines
|
||||
// This value is used for real-time preview only
|
||||
const rate = this.ttsEnabled ? speed * 2 : 1;
|
||||
|
||||
// Update animation speed
|
||||
document.dispatchEvent(new CustomEvent('animation:speed:change', {
|
||||
detail: { speed: rate }
|
||||
}));
|
||||
|
||||
// Update TTS speed
|
||||
document.dispatchEvent(new CustomEvent('tts:speed:change', {
|
||||
detail: { speed: speed }
|
||||
}));
|
||||
|
||||
// Save preference
|
||||
if (persistenceManager) {
|
||||
persistenceManager.updatePreference('tts', 'speed', speed);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (speedReset) {
|
||||
speedReset.addEventListener('click', () => {
|
||||
// Reset to default speed (0.5)
|
||||
if (speedSlider) {
|
||||
// Default value is 0.5 in normalized form (0-1),
|
||||
// which is 50 in slider range (0-100)
|
||||
speedSlider.value = 50;
|
||||
|
||||
// Trigger the input event to update all components
|
||||
speedSlider.dispatchEvent(new Event('input'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Listen for speed change events from other components
|
||||
document.addEventListener('tts:speed:change', (event) => {
|
||||
if (event.detail && typeof event.detail.speed === 'number') {
|
||||
// Update the main UI speed slider
|
||||
const speedSlider = document.getElementById('speed');
|
||||
if (speedSlider) {
|
||||
// Convert normalized speed (0-1) to slider range (0-100)
|
||||
speedSlider.value = Math.round(event.detail.speed * 100);
|
||||
}
|
||||
|
||||
// Save to persistence manager
|
||||
if (persistenceManager) {
|
||||
persistenceManager.updatePreference('tts', 'speed', event.detail.speed);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 = this.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
|
||||
*/
|
||||
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');
|
||||
|
||||
// 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 using existing styles
|
||||
if (!this.ttsAvailable) {
|
||||
// TTS is not available, disable the button
|
||||
speechToggle.setAttribute('disabled', 'disabled');
|
||||
speechToggle.title = 'Text-to-speech is not available';
|
||||
} else {
|
||||
// TTS is available, remove disabled attribute
|
||||
speechToggle.removeAttribute('disabled');
|
||||
|
||||
// Update based on whether TTS is enabled
|
||||
if (this.ttsEnabled) {
|
||||
speechToggle.style.fontWeight = 'bold';
|
||||
speechToggle.style.color = '#000';
|
||||
speechToggle.title = 'Disable speech';
|
||||
} else {
|
||||
speechToggle.style.fontWeight = 'normal';
|
||||
speechToggle.style.color = '#999';
|
||||
speechToggle.title = 'Enable speech';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 UIControllerModule();
|
||||
|
||||
// Export the module
|
||||
export { uiController as UIController };
|
||||
Reference in New Issue
Block a user