/** * Animated Fiction - Main Application Integration * Integrates all modules to create an interactive fiction experience. */ import { AnimationQueue } from './animation-queue.js'; import { TextProcessor } from './text-processor.js'; import { ParagraphLayout } from './paragraph-layout.js'; import { LayoutRenderer } from './layout-renderer.js'; import { AudioManager } from './audio-manager.js'; import { TtsPlayer } from './tts-player.js'; import { PersistenceManager } from './persistence-manager.js'; import { InputHandler } from './input-handler.js'; import { SocketClient } from './socket-client.js'; import { UiController } from './ui-controller.js'; import { ttsFactory } from './tts-factory.js'; import { LoadingOverlay } from './loading-overlay.js'; import { TextBuffer } from './text-buffer.js'; import { OptionsUI } from './options-ui.js'; export class AnimatedFiction { /** * Create a new AnimatedFiction application * @param {Object} config - Configuration options */ constructor(config = {}) { this.config = config; this.storyContainer = document.getElementById(config.storyContainerId || 'story'); this.commandHistoryContainer = document.getElementById(config.commandHistoryContainerId || 'command_history'); // Added for user commands // Game state this.gameState = { started: false, currentRoomId: '', isThinking: false, textSpeed: config.initialSpeed || 50 // Keep track of speed locally if needed }; this.currentCommandTimeout = null; // To handle server timeouts // Use the pre-existing loading overlay if available, or create a new one if (window.initialOverlay) { console.log("Using pre-existing loading overlay"); // Create a wrapper for the initial overlay to match our LoadingOverlay API this.loadingOverlay = { updateProgressItem: (id, progress, complete) => { // Map progress items to overall percentages let totalProgress = 0; if (id === 'components') totalProgress = 30; else if (id === 'tts') totalProgress = complete ? 80 : 40 + (progress * 0.4); else if (id === 'ui') totalProgress = 90; else if (id === 'socket') totalProgress = 95; let message = 'Loading...'; if (id === 'components') message = 'Loading core components...'; else if (id === 'tts') message = complete ? 'TTS system ready' : 'Initializing Text-to-Speech...'; else if (id === 'ui') message = 'Preparing user interface...'; else if (id === 'socket') message = 'Connecting to server...'; window.initialOverlay.updateProgress(totalProgress, message); }, hide: (callback) => { window.initialOverlay.hide(); if (callback) setTimeout(callback, 500); }, show: () => {} // No-op as it's already showing }; } else { // Create a fallback LoadingOverlay if initialOverlay is not available this.loadingOverlay = new LoadingOverlay({ title: 'Initializing AI Fiction', fadeOutDuration: 800 }); this.loadingOverlay.show(); } // Add additional progress tracking items for hyphenation this.loadingOverlay.updateProgressItem('components', 10, false); this.loadingOverlay.updateProgressItem('hyphenation', 0, false); this.loadingOverlay.updateProgressItem('tts', 0, false); this.loadingOverlay.updateProgressItem('ui', 0, false); this.loadingOverlay.updateProgressItem('socket', 0, false); // Track when modules are ready to hide the loading overlay this.componentsReady = false; this.hyphenationReady = false; this.ttsReady = false; this.uiReady = false; // Initialize core components // (This only creates the component instances but doesn't start processing yet) this.initializeComponents(); this.bindGlobalEvents(); } /** * Initialize all components */ initializeComponents() { // 1. Core Components this.animationQueue = new AnimationQueue(); // Initialize TextProcessor without hyphenator initially this.textProcessor = new TextProcessor(window.SmartyPants); // Pass null for measure func initially, it will be provided after DOM measurement setup this.paragraphLayout = new ParagraphLayout(window.kap, null); this.layoutRenderer = new LayoutRenderer(this.animationQueue); this.audioManager = new AudioManager(); this.ttsPlayer = new TtsPlayer({ apiKey: this.config.ttsApiKey, animationQueue: this.animationQueue, persistenceManager: this.persistenceManager, audioManager: this.audioManager, ttsFactory: window.ttsFactory }); this.persistenceManager = new PersistenceManager({ storage: localStorage // Note: Persistence might need rework for socket state }); // Initialize the TextBuffer for sentence collection this.textBuffer = new TextBuffer({ ttsPlayer: this.ttsPlayer, onSentenceReady: (sentence) => this.handleSentenceReady(sentence) }); // Mark components as initialized for loading progress this.loadingOverlay.updateProgressItem('components', 60, false); // Initialize the DOM-based text measurement system this.initializeTextMeasurement(); // 2. Input, Socket, and UI Controller this.inputHandler = new InputHandler('player_input', 'cursor'); this.socketClient = new SocketClient(this.config.serverUrl); // Pass server URL if provided this.uiController = new UiController({ // storyPlayer: this.storyPlayer, // Remove storyPlayer dependency animationQueue: this.animationQueue, // Keep for speed control ttsPlayer: this.ttsPlayer, // Keep for TTS toggle speedSliderElement: document.getElementById('speed'), // choiceContainerElement: document.getElementById('choices'), // Choices are now suggestions/input commandHistoryContainerElement: this.commandHistoryContainer, // Pass command history storyContainerElement: this.storyContainer, // Pass story container rewindButtonElement: document.getElementById('rewind'), saveButtonElement: document.getElementById('save'), loadButtonElement: document.getElementById('reload'), speechButtonElement: document.getElementById('speech'), speedResetElement: document.getElementById('speed_reset'), inputHandler: this.inputHandler, // Pass input handler for suggestion clicks etc. socketClient: this.socketClient, // Pass socket client for actions translations: this.config.translations, locale: this.config.locale || 'en-us' }); // Configure TTS Player based on factory readiness this.listenForTTSReady(); // Link InputHandler submission to SocketClient this.inputHandler.onCommandSubmit((command) => { this.submitCommand(command); }); // Link UI Controller actions to SocketClient this.uiController.onRestartRequest = () => this.socketClient.requestStartGame(); this.uiController.onSaveRequest = () => this.socketClient.requestSaveGame(); this.uiController.onLoadRequest = () => this.socketClient.requestLoadGame(); // TTS toggle is likely handled within UiController using TtsPlayer/ttsHandler // Initialize Socket Listeners this.initializeSocketListeners(); } /** * Initialize DOM-based text measurement system * Recreates the ruler stack system from the original game.js */ initializeTextMeasurement() { // Set up ruler DOM element & stack for text measurement this.rulerElement = document.getElementById('ruler'); if (!this.rulerElement) { console.error("AnimatedFiction: Ruler element not found! Text measurement will fail."); return; } // Reset and initialize ruler stack this.rulerStack = [this.rulerElement]; // Create the DOM-based text measurement function this.measureText = (str) => { // Get current ruler from stack top let ruler = this.rulerStack[this.rulerStack.length - 1]; // Handle HTML tags specially if (str.substr(0, 2) == '') { // End tag: pop the last child off the stack let child = this.rulerStack.pop(); ruler = this.rulerStack[this.rulerStack.length - 1]; ruler.removeChild(child); return 0; } else if (str.substr(0, 1) == '<') { // Start tag: create and push element onto stack let tmp = document.createElement('div'); tmp.innerHTML = str; let word = tmp.firstChild; ruler = this.rulerStack[this.rulerStack.length - 1]; this.rulerStack.push(word); ruler.appendChild(word); return 0; } else if (str === '|') { // Pipe character (hyphenation marker): zero width return 0; } else if (str === ' ') { // Non-breaking space for measurement str = '\u00A0'; } // For normal text, measure width with a text node ruler = this.rulerStack[this.rulerStack.length - 1]; let textNode = document.createTextNode(str); ruler.appendChild(textNode); let width = ruler.getClientRects()[0].width; ruler.removeChild(textNode); return width; }; // Provide the measurement function to ParagraphLayout if (this.paragraphLayout) { this.paragraphLayout.setMeasureFunction(this.measureText); } } // Removed measureDomText method /** * Listen for the tts-ready event from the factory */ listenForTTSReady() { // Add a shorter timeout specifically for TTS initialization (8 seconds) const ttsTimeout = setTimeout(() => { console.warn("AnimatedFiction: TTS initialization timeout reached, continuing without it"); this.loadingOverlay.updateProgressItem('tts', 100, true, 'TTS not available, continuing anyway'); this.ttsReady = true; this.checkInitializationComplete(); }, 8000); // 8 second timeout is enough since the UI needs to become responsive // Listen for the normal TTS ready event window.addEventListener('tts-ready', (event) => { // Clear the timeout since we got the event clearTimeout(ttsTimeout); console.log('AnimatedFiction received tts-ready event:', event.detail); const { available, type, handler, systems } = event.detail; // Update loading progress for TTS initialization this.loadingOverlay.updateProgressItem('tts', 100, true, available ? `${type} TTS ready` : 'TTS not available'); this.ttsReady = true; // Check if we can hide the loading overlay this.checkInitializationComplete(); // Rest of TTS ready handling if (available && handler) { console.log(`AnimatedFiction: Using ${type} TTS system with handler:`, handler); // Store the handler for direct access window.ttsHandler = handler; // Pass the handler to the TtsPlayer if needed if (this.ttsPlayer) { this.ttsPlayer.setTtsHandler(handler); } // Initialize the options UI with available TTS systems if (systems) { this.initializeOptionsUI(systems); } else { // If no systems info provided, pass an empty object this.initializeOptionsUI({}); } // Ensure UI controller knows about it if (this.uiController) { this.uiController.setTtsHandler(handler); this.uiController.updateSpeechButtonAvailability(available, type); } // Set user activation flag once we have user interaction document.addEventListener('click', function setUserActivation() { if (window.ttsHandler) { window.ttsHandler.hasUserActivation = true; // If using Kokoro, try to resume the AudioContext if (window.ttsHandler.audioContext && window.ttsHandler.audioContext.state === 'suspended') { window.ttsHandler.audioContext.resume().catch(err => console.error('Error resuming AudioContext on click:', err)); } } // Only need this once document.removeEventListener('click', setUserActivation); }, { once: false }); } else { console.warn("AnimatedFiction: No TTS handler available after initialization."); if (this.uiController) { this.uiController.updateSpeechButtonAvailability(false); } } }); } /** * Initialize listeners for SocketClient events. */ initializeSocketListeners() { this.socketClient.on('connect', () => { console.log('AnimatedFiction: Socket connected.'); // Automatically start the game once connected if (!this.gameState.started) { // Don't display any message before starting the game this.socketClient.requestStartGame(); } }); this.socketClient.on('connect_error', (error) => { console.error('AnimatedFiction: Socket connection error:', error); this.displaySystemMessage('Connection error. Please check server and network.'); }); this.socketClient.on('disconnect', (reason) => { console.warn('AnimatedFiction: Socket disconnected.', reason); this.displaySystemMessage('Disconnected from server.'); this.gameState.started = false; // Reset started state on disconnect this.uiController.updateButtonStates(this.gameState); // Disable buttons }); this.socketClient.on('gameIntroduction', (data) => { // Clear any existing content this.clearStoryDisplay(); // Display the introduction text with proper animation this.displayNarrative(data.introduction); this.displayNarrative(data.initialRoomDescription); this.gameState.started = true; this.gameState.currentRoomId = data.currentRoomId; this.gameState.isThinking = false; this.uiController.updateButtonStates(this.gameState); // Enable buttons this.inputHandler.enableInput(); this.inputHandler.focus(); }); this.socketClient.on('narrativeResponse', (data) => { this.handleNarrativeResponse(data); }); this.socketClient.on('gameSaved', () => { this.displaySystemMessage('Game saved successfully.'); this.gameState.canLoad = true; // Assuming save enables load this.uiController.updateButtonStates(this.gameState); }); this.socketClient.on('gameLoaded', (data) => { this.clearStoryDisplay(); this.displaySystemMessage('Game loaded successfully.'); this.displayNarrative(data.currentRoomDescription); // Display current room after load this.gameState.started = true; // Ensure game is marked as started this.gameState.currentRoomId = data.currentRoomId; this.gameState.isThinking = false; this.gameState.canLoad = true; // Can still load after loading this.uiController.updateButtonStates(this.gameState); this.inputHandler.enableInput(); this.inputHandler.focus(); }); this.socketClient.on('error', (data) => { this.handleNarrativeResponse({ text: '' }); // Clear thinking indicator on error this.displaySystemMessage(`Server Error: ${data.message}`); this.inputHandler.enableInput(); // Re-enable input on server error }); } /** * Handles the narrative response from the server. * @param {object} data - The data received from the server. * @param {string} data.text - The narrative text. * @param {string[]} [data.suggestions] - Optional suggestions. * @param {object} [data.gameState] - Optional updated game state. */ handleNarrativeResponse(data) { // Clear thinking indicator and timeout if (this.currentCommandTimeout) { clearTimeout(this.currentCommandTimeout); this.currentCommandTimeout = null; } this.removeThinkingIndicator(); this.gameState.isThinking = false; // Display narrative using the proper text processing pipeline if (data.text) { this.displayNarrative(data.text); } // Display suggestions if (data.suggestions && data.suggestions.length > 0) { this.displaySuggestions(data.suggestions); } // Update game state if provided if (data.gameState) { this.gameState.currentRoomId = data.gameState.currentRoomId; // Update other relevant state parts if needed } // Re-enable input and focus this.inputHandler.enableInput(); this.scrollToBottom(); // Ensure view is scrolled down } /** * Submits a command entered by the user. * @param {string} command - The command text. */ submitCommand(command) { if (!this.gameState.started || this.gameState.isThinking) return; this.displayUserCommand(command); // Show command in history this.displayThinkingIndicator(); // Show thinking message this.gameState.isThinking = true; this.socketClient.sendCommand(command); // Failsafe timeout to re-enable input if server doesn't respond this.currentCommandTimeout = setTimeout(() => { if (this.gameState.isThinking) { // Only if still thinking console.warn("Server response timeout."); this.removeThinkingIndicator(); this.displaySystemMessage('The server is taking too long to respond.'); this.gameState.isThinking = false; this.inputHandler.enableInput(); } }, 15000); // 15 seconds timeout } /** * Displays a narrative paragraph using the rendering pipeline. * @param {string} text - The narrative text. */ displayNarrative(text) { if (!text) return; console.log("AnimatedFiction: Displaying narrative text:", text); try { // 1. Process the text with TextProcessor (SmartyPants, hyphenation) const processed = this.textProcessor.process(text); // 2. Get container width for line measure calculation const containerWidth = this.storyContainer.clientWidth; // 3. Setup measures array for the Knuth-Plass algorithm // Calculate indent width for the measures array const indentWidth = 2 * parseFloat(window.getComputedStyle(document.querySelector("#indent")).lineHeight); const measures = [ containerWidth, // Full width containerWidth - indentWidth, // Indented width containerWidth - indentWidth * 0.9 // Slightly less indented width ]; // 4. Calculate Layout using the Knuth-Plass algorithm with DOM-based measurement console.log("AnimatedFiction: Calculating paragraph layout with DOM ruler"); const layout = this.paragraphLayout.calculateLayout(processed, measures.slice().reverse(), true); // 5. Render paragraph using the LayoutRenderer console.log("AnimatedFiction: Rendering paragraph with layout data"); const renderResult = this.layoutRenderer.renderParagraph(layout, this.animationQueue.getDelay(), measures.slice().reverse()); if (!Array.isArray(renderResult) || renderResult.length < 2) { throw new Error("renderParagraph did not return the expected array [element, delay]"); } const [paragraphElement, finalDelay] = renderResult; // Update the animation queue's delay this.animationQueue.setDelay(finalDelay); // 6. Append the paragraph element to the story container this.storyContainer.appendChild(paragraphElement); // 7. Add text to the TextBuffer for TTS processing // This buffers text, extracts complete sentences, and processes them for TTS if (this.textBuffer) { console.log("AnimatedFiction: Adding text to buffer for sentence processing"); this.textBuffer.addText(text); } // Direct TTS fallback if TextBuffer isn't available else if (this.ttsPlayer && this.ttsPlayer.isEnabled()) { console.log("AnimatedFiction: Speaking text directly with TTS (no buffer)"); this.ttsPlayer.speak(text); } else if (window.ttsHandler && typeof window.ttsHandler.isEnabled === 'function' && window.ttsHandler.isEnabled()) { console.log("AnimatedFiction: Speaking text with global TTS handler (no buffer)"); window.ttsHandler.speak(text); } } catch (error) { console.error("Error during paragraph layout or rendering:", error); console.error(error.stack); // Display raw text as fallback with simple fade-in const fallbackPara = document.createElement('p'); fallbackPara.textContent = text; fallbackPara.classList.add("fallback"); fallbackPara.classList.add("fade-in"); this.storyContainer.appendChild(fallbackPara); // Still try to speak the text if TTS is enabled // Since rendering failed, don't use buffer, just speak directly if (this.ttsPlayer && this.ttsPlayer.isEnabled()) { this.ttsPlayer.speak(text); } else if (window.ttsHandler && typeof window.ttsHandler.isEnabled === 'function' && window.ttsHandler.isEnabled()) { window.ttsHandler.speak(text); } } this.scrollToBottom(); } /** * Displays the user's command in the history. * @param {string} command - The command text. */ displayUserCommand(command) { const element = document.createElement('p'); element.className = 'user-input'; element.textContent = `> ${command}`; this.commandHistoryContainer.appendChild(element); this.scrollToBottom(); } /** * Displays a system message (e.g., errors, confirmations). * @param {string} message - The message text. */ displaySystemMessage(message) { const element = document.createElement('p'); element.className = 'system-message'; element.textContent = message; this.storyContainer.appendChild(element); // Add to main story flow this.scrollToBottom(); } /** * Displays clickable suggestions. * @param {string[]} suggestions - An array of suggestion strings. */ displaySuggestions(suggestions) { // Remove previous suggestions if any const existingSuggestions = this.storyContainer.querySelector('.suggestions'); if (existingSuggestions) { existingSuggestions.remove(); } const element = document.createElement('div'); element.className = 'suggestions'; const heading = document.createElement('p'); heading.textContent = 'Suggestions:'; // TODO: Localize heading.style.fontStyle = 'italic'; heading.style.marginTop = '1em'; element.appendChild(heading); const list = document.createElement('ul'); suggestions.forEach(suggestion => { const item = document.createElement('li'); item.textContent = suggestion; item.style.cursor = 'pointer'; item.addEventListener('click', () => { this.inputHandler.setValue(suggestion); // Set input value this.submitCommand(suggestion); // Submit immediately }); list.appendChild(item); }); element.appendChild(list); this.storyContainer.appendChild(element); this.scrollToBottom(); } /** Displays a "Thinking..." indicator. */ displayThinkingIndicator() { this.removeThinkingIndicator(); // Ensure only one exists const id = 'thinking-' + Date.now(); const element = document.createElement('div'); element.id = id; element.className = 'thinking'; element.innerHTML = '
Thinking...
'; // TODO: Localize this.storyContainer.appendChild(element); this.scrollToBottom(); } /** Removes the "Thinking..." indicator. */ removeThinkingIndicator() { const thinkingElement = this.storyContainer.querySelector('.thinking'); if (thinkingElement) { thinkingElement.remove(); } } /** Clears the main story display area. */ clearStoryDisplay() { // Clear story container while (this.storyContainer.firstChild) { this.storyContainer.removeChild(this.storyContainer.firstChild); } // Optionally clear command history as well, or keep it // while (this.commandHistoryContainer.firstChild) { // this.commandHistoryContainer.removeChild(this.commandHistoryContainer.firstChild); // } } /** Scrolls the main content area to the bottom. */ scrollToBottom() { // Scroll the right page (story container's parent) const rightPage = document.getElementById('page_right'); if (rightPage) { // Use setTimeout to ensure rendering is complete before scrolling setTimeout(() => { rightPage.scrollTop = rightPage.scrollHeight; }, 50); // Small delay } } /** Binds global event listeners like focus management. */ bindGlobalEvents() { // Basic focus management: click anywhere focuses input unless on interactive element document.addEventListener('click', (e) => { if ( e.target.tagName !== 'BUTTON' && e.target.tagName !== 'A' && e.target.tagName !== 'INPUT' && // Allow range slider interaction !e.target.closest('.suggestions li') && // Allow clicking suggestions e.target !== this.inputHandler.playerInput // Don't refocus if already clicking input ) { this.inputHandler.focus(); } }); // Refocus on window visibility change window.addEventListener('focus', () => this.inputHandler.focus()); window.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { setTimeout(() => this.inputHandler.focus(), 100); } }); } /** * Start the application: Load all modules, then connect socket and set up UI */ async start() { try { // 1. Initialize UI components first (non-linguistic) this.uiController.setupEventListeners(); this.uiController.updateBookDimensions(); this.uiController.updateParagraphHeight(); const initialQueueSpeed = Math.pow(100.0 - (this.config.initialSpeed || 50), 3) / 10000 * 10 + 0.01; this.animationQueue.setSpeed(initialQueueSpeed); this.uiController.updateSpeedDisplay(this.config.initialSpeed || 50); this.loadingOverlay.updateProgressItem('components', 60, false); this.componentsReady = true; // 2. Load and initialize the hyphenation engine this.loadingOverlay.updateProgressItem('hyphenation', 20, false); await this.initializeHyphenation(); this.loadingOverlay.updateProgressItem('hyphenation', 100, true); this.hyphenationReady = true; // 3. The TTS system is initialized separately through events // We're already listening for the 'tts-ready' event in listenForTTSReady() // 4. Mark UI as ready this.loadingOverlay.updateProgressItem('ui', 80, false); this.uiReady = true; // 5. Connect to the socket server and wait for connection this.loadingOverlay.updateProgressItem('socket', 50, false); await this.connectToServer(); // Everything is now loaded, we can check initialization state // This will hide the loading overlay when all components are ready this.checkInitializationComplete(); // UI resize may be needed after everything is loaded this.uiController.handleWindowResize(); } catch (error) { console.error("Error during application startup:", error); // Display error message to user if (this.storyContainer) { const errorElement = document.createElement('div'); errorElement.className = 'system-error'; errorElement.textContent = `Failed to start application: ${error.message}`; this.storyContainer.appendChild(errorElement); } // Hide loading overlay anyway this.loadingOverlay.hide(); } } /** * Initialize and load hyphenation engine * @returns {Promise} Resolves when hyphenation is ready */ async initializeHyphenation() { try { this.loadingOverlay.updateProgressItem('hyphenation', 30, false, 'Loading hyphenation engine...'); // Import the hyphenopoly module const hyphenopoly = await import('./hyphenopoly.module.js').then(module => module.default); this.loadingOverlay.updateProgressItem('hyphenation', 50, false, 'Configuring hyphenation patterns...'); // Configure the hyphenator with proper exceptions to demonstrate it's working const hyphenatorConfig = hyphenopoly.config({ "require": ["en-us"], "hyphen": "|", "minWordLength": 4, "exceptions": { "en-us": "aban|doned, vic|to|ri|an, man|sion, stand|ing, im|pos|ing, ex|am|ine" }, "loader": async (file, patDir) => { console.log(`Loading pattern file: ${patDir}${file}`); try { const response = await fetch(`${patDir}${file}`); if (!response.ok) { throw new Error(`Failed to fetch ${file}: ${response.status}`); } return response.arrayBuffer(); } catch (error) { console.error(`Error loading pattern file ${file}:`, error); throw error; } } }); this.loadingOverlay.updateProgressItem('hyphenation', 80, false, 'Getting hyphenation function...'); // Get the hyphenateText function for en-us and await it const hyphenateFunction = await hyphenatorConfig.get("en-us"); // Test the hyphenator to verify it works const testWord = "abandoned"; const hyphenatedTest = hyphenateFunction(testWord); console.log(`Hyphenation test: "${testWord}" → "${hyphenatedTest}"`); // Create a wrapper function for our text processor that properly handles errors const hyphenateWrapper = (text) => { try { return hyphenateFunction(text); } catch (error) { console.error("Hyphenation error:", error); return text; // Return original text on error } }; // Set the hyphenator on the text processor this.textProcessor.setHyphenator(hyphenateWrapper); console.log("Hyphenator successfully set on text processor"); this.loadingOverlay.updateProgressItem('hyphenation', 100, true, 'Hyphenation ready'); return true; } catch (error) { console.error("Failed to initialize hyphenation:", error); this.loadingOverlay.updateProgressItem('hyphenation', 100, true, 'Hyphenation failed, continuing anyway'); // We'll continue without hyphenation rather than blocking the app return false; } } /** * Connect to the socket server * @returns {Promise} Resolves when socket is connected */ connectToServer() { return new Promise((resolve) => { console.log("AnimatedFiction: Connecting to server..."); this.loadingOverlay.updateProgressItem('socket', 50, false, 'Connecting to server...'); // Set up a connection handler that resolves the promise const connectionHandler = () => { console.log("AnimatedFiction: Socket connected successfully."); this.loadingOverlay.updateProgressItem('socket', 100, true, 'Connected to server'); this.socketClient.off('connect', connectionHandler); resolve(); }; // Set up an error handler const errorHandler = (error) => { console.error("AnimatedFiction: Socket connection failed:", error); this.loadingOverlay.updateProgressItem('socket', 100, true, 'Connection failed, will retry'); // We resolve anyway to not block the app this.socketClient.off('connect_error', errorHandler); resolve(); }; // Connect to socket server this.socketClient.on('connect', connectionHandler); this.socketClient.on('connect_error', errorHandler); this.socketClient.connect(); // Set a timeout to resolve anyway after 5 seconds setTimeout(() => { this.socketClient.off('connect', connectionHandler); this.socketClient.off('connect_error', errorHandler); console.warn("AnimatedFiction: Socket connection timeout, continuing anyway"); this.loadingOverlay.updateProgressItem('socket', 100, true, 'Connection timeout, will retry later'); resolve(); }, 5000); }); } /** * Check if all initialization tasks are complete and hide loading overlay if so */ checkInitializationComplete() { // We require core components, hyphenation and UI to be ready // TTS is optional and can continue loading in the background if (this.componentsReady && this.hyphenationReady && this.uiReady) { console.log("AnimatedFiction: All required initialization tasks complete, hiding loading overlay"); // Update UI progress item and mark as complete this.loadingOverlay.updateProgressItem('ui', 100, true); // Hide the loading overlay with fade-out animation this.loadingOverlay.hide(() => { console.log("AnimatedFiction: Loading overlay hidden, starting game"); // Focus the input handler once the overlay is gone if (this.inputHandler) { this.inputHandler.focus(); } }); } else { console.log("AnimatedFiction: Waiting for initialization to complete", "Components ready:", this.componentsReady, "Hyphenation ready:", this.hyphenationReady, "UI ready:", this.uiReady, "TTS ready:", this.ttsReady); } } /** * Initialize the options UI with available TTS systems * @param {Object} availableSystems - Object containing available TTS systems */ initializeOptionsUI(availableSystems) { // Create options button if it doesn't exist if (!document.getElementById('options')) { const optionsButton = document.createElement('button'); optionsButton.id = 'options'; optionsButton.className = 'l10n-options'; optionsButton.title = 'Open options menu'; optionsButton.innerText = 'options'; optionsButton.style.order = '5'; // Position it in the control bar // Add to control bar const controlBar = document.querySelector('.control-bar'); if (controlBar) { controlBar.appendChild(optionsButton); } } // Create and configure the options UI this.optionsUI = new OptionsUI({ persistenceManager: this.persistenceManager, ttsPlayer: this.ttsPlayer, audioManager: this.audioManager, ttsFactory: window.ttsFactory, onClose: () => { // Handle options UI closed - refresh any UI elements if needed if (this.uiController) { this.uiController.refreshFromPreferences(); } } }); // Link the options button to the options UI this.setupOptionsButton(); // Update available system information if (availableSystems && this.optionsUI) { // Update the provider selection in the UI with available systems console.log("Available TTS systems:", availableSystems); // Disable unavailable systems in the UI for (const system in availableSystems) { if (!availableSystems[system]) { console.log(`TTS system ${system} is not available, will be disabled in options`); // The OptionsUI handles this internally } } } } /** * Link the options button to the options UI */ setupOptionsButton() { // Get the options button or create it if needed let optionsButton = document.getElementById('options'); if (optionsButton) { // Connect options button to the UI optionsButton.addEventListener('click', () => { if (this.optionsUI) { this.optionsUI.open(); } else { console.warn("Options UI not initialized yet"); // Create options UI if it doesn't exist yet this.initializeOptionsUI(); } }); console.log("Options button connected to options UI"); } else { console.warn("Options button not found in DOM"); } } } /** * Initialize the application when the window loads */ window.addEventListener('load', async () => { // Define translations const translations = { 'en-us': { by: "powered by Generative AI", // Updated speed: "speed*", // Simpler superscript title_speed: "Set speed of text animation", restart: "restart", title_restart: "Restart game from beginning", // Clarified save: "save", title_save: "Save progress", load: "load", title_load: "Reload from save point", prompt: "What do you want to do next?", // Changed from italic remark: "*click on page or press spacebar to fast forward text animation", // Simplified end: "The End", // Keep for potential future use // Action prompts might not be needed for socket version, keep for now action_examine: "Examine", action_comment: "Comment", action_ask: "Ask", action_interact: "Interact", action_reflect: "Reflect", action_inventory: "Inventory", speech: "speech", // Lowercase to match button style title_speech: "Toggle text to speech", system_error: "Error", // Added system_connecting: "Connecting...", // Added system_thinking: "Thinking", // Added system_suggestions: "Suggestions:", // Added system_save_ok: "Game saved.", // Added system_load_ok: "Game loaded.", // Added system_connection_lost: "Connection lost.", // Added system_restarting: "Restarting game..." // Added }, 'de': { // Keep German translations, update/add as needed by: "powered by Generative AI", speed: "Geschwindigkeit*", title_speed: "Geschwindigkeit der Textanimation einstellen", restart: "Neustart", title_restart: "Spiel von vorne beginnen", save: "Speichern", title_save: "Fortschritt speichern", load: "Laden", title_load: "Gespeicherten Spielstand laden", prompt: "Was möchtest du als Nächstes tun?", remark: "*Klicke auf die Seite oder drücke die Leertaste, um die Textanimation zu beschleunigen", end: "Ende", action_examine: "Untersuchen", action_comment: "Kommentieren", action_ask: "Fragen", action_interact: "Interagieren", action_reflect: "Nachdenken", action_inventory: "Inventar", speech: "Sprache", title_speech: "Sprachausgabe umschalten", system_error: "Fehler", system_connecting: "Verbinde...", system_thinking: "Denke nach", system_suggestions: "Vorschläge:", system_save_ok: "Spiel gespeichert.", system_load_ok: "Spiel geladen.", system_connection_lost: "Verbindung verloren.", system_restarting: "Starte Spiel neu..." } }; // Create and initialize the application window.app = new AnimatedFiction({ storyContainerId: 'story', commandHistoryContainerId: 'command_history', initialSpeed: 50, locale: window.locale || 'en-us', translations: translations }); // Start the application with proper async initialization await window.app.start(); });