764 lines
33 KiB
JavaScript
764 lines
33 KiB
JavaScript
/**
|
|
* 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 { InkStoryPlayer } from './ink-story-player.js'; // Replaced by SocketClient logic
|
|
import { UiController } from './ui-controller.js';
|
|
// Assuming InputHandler and SocketClient are loaded globally via <script> tags in HTML
|
|
// If using modules properly, they would need imports:
|
|
// import { InputHandler } from './input-handler.js';
|
|
// import { SocketClient } from './socket-client.js';
|
|
|
|
export class AnimatedFiction {
|
|
/**
|
|
* Create a new AnimatedFiction application
|
|
* @param {Object} config - Configuration options
|
|
* @param {string} config.serverUrl - URL for the Socket.IO server (optional, defaults to window.location.origin)
|
|
* @param {string} config.storyContainerId - ID of the story container element
|
|
* @param {string} config.commandHistoryContainerId - ID of the command history container element
|
|
* @param {string} config.ttsApiKey - API key for TTS service (if applicable)
|
|
* @param {number} config.initialSpeed - Initial animation speed
|
|
* @param {string} config.locale - Locale for translations
|
|
* @param {Object} config.translations - Translations object
|
|
*/
|
|
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
|
|
|
|
// Initialize core components
|
|
this.initializeComponents();
|
|
this.bindGlobalEvents(); // Add global event bindings like focus management
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
});
|
|
this.persistenceManager = new PersistenceManager({
|
|
storage: localStorage // Note: Persistence might need rework for socket state
|
|
});
|
|
|
|
// Initialize the DOM-based text measurement system
|
|
this.initializeTextMeasurement();
|
|
|
|
// 2. Input, Socket, and UI Controller
|
|
// Ensure InputHandler and SocketClient classes are available globally
|
|
if (typeof InputHandler === 'undefined' || typeof SocketClient === 'undefined') {
|
|
console.error("InputHandler or SocketClient class not found. Ensure scripts are loaded correctly.");
|
|
return; // Stop initialization if core components are missing
|
|
}
|
|
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();
|
|
} // <<< Re-added missing closing brace for initializeComponents
|
|
|
|
/**
|
|
* 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() {
|
|
window.addEventListener('tts-ready', (event) => {
|
|
console.log('AnimatedFiction received tts-ready event:', event.detail);
|
|
const { available, type, handler } = event.detail;
|
|
|
|
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);
|
|
}
|
|
|
|
// 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");
|
|
// We'll use our DOM-based measureText function that we set up in initializeTextMeasurement
|
|
// Will use reversed measures array as in the original game.js implementation
|
|
const layout = this.paragraphLayout.calculateLayout(processed, measures.slice().reverse(), true);
|
|
|
|
// 5. Render paragraph using the LayoutRenderer
|
|
console.log("AnimatedFiction: Rendering paragraph with layout data");
|
|
// Also pass reversed measures array to renderer as in the original
|
|
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. Speak text if TTS is enabled
|
|
// First check our ttsPlayer
|
|
if (this.ttsPlayer && this.ttsPlayer.isEnabled()) {
|
|
console.log("AnimatedFiction: Speaking text with TTS via ttsPlayer");
|
|
this.ttsPlayer.speak(text);
|
|
}
|
|
// Also try the global TTS handler in case ttsPlayer isn't properly configured
|
|
else if (window.ttsHandler && typeof window.ttsHandler.isEnabled === 'function' && window.ttsHandler.isEnabled()) {
|
|
console.log("AnimatedFiction: Speaking text with global TTS handler");
|
|
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
|
|
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 = '<p>Thinking<span class="loading-indicator"><span>.</span><span>.</span><span>.</span></span></p>'; // 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: Connect socket and set up UI.
|
|
*/
|
|
start() {
|
|
// Set up UI event listeners (speed, TTS, etc.)
|
|
this.uiController.setupEventListeners();
|
|
|
|
// Force initial UI layout calculation immediately to ensure visibility
|
|
this.uiController.updateBookDimensions();
|
|
this.uiController.updateParagraphHeight();
|
|
|
|
// Set initial speed in AnimationQueue
|
|
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); // Update slider visually
|
|
|
|
// Connect the socket to the server without showing any loading messages
|
|
console.log("AnimatedFiction: Connecting to server...");
|
|
this.socketClient.connect();
|
|
|
|
this.uiController.handleWindowResize();
|
|
|
|
// Initial focus on input
|
|
this.inputHandler.focus();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize the application when the window loads
|
|
* Using addEventListener instead of window.onload to prevent overriding other handlers
|
|
*/
|
|
window.addEventListener('load', async () => {
|
|
// Define translations first
|
|
const translations = {
|
|
'en-us': {
|
|
by: "powered by Generative AI", // Updated
|
|
speed: "speed<sup>*</sup>", // 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: "<i><sup>*</sup>click on page or press spacebar to fast forward text animation</i>", // 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<sup>*</sup>",
|
|
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: "<i><sup>*</sup>Klicke auf die Seite oder drücke die Leertaste, um die Textanimation zu beschleunigen</i>",
|
|
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..."
|
|
}
|
|
};
|
|
|
|
// Configure Hyphenopoly before creating the application
|
|
window.Hyphenopoly = window.Hyphenopoly || {};
|
|
window.Hyphenopoly.config({
|
|
require: {
|
|
"en-us": "FORCEHYPHENOPOLY"
|
|
},
|
|
paths: {
|
|
maindir: "./js/",
|
|
patterndir: "./js/patterns/"
|
|
},
|
|
setup: {
|
|
selectors: {
|
|
".hyphenate": { // Default selector with soft hyphen
|
|
hyphen: "\u00AD"
|
|
},
|
|
".hyphenatePipe": { // Selector for Knuth-Plass with pipe hyphen
|
|
hyphen: "|",
|
|
// Explicitly add minWordLength here as a potential fix
|
|
// Although it should derive from language, this might help
|
|
minWordLength: 4
|
|
}
|
|
}
|
|
},
|
|
handleEvent: {
|
|
error: function(e) {
|
|
console.error("Hyphenopoly error:", e);
|
|
},
|
|
hyphenopolyEnd: function(e) {
|
|
console.log("Hyphenopoly fully initialized (hyphenopolyEnd event).");
|
|
// --- Move hyphenator setup logic inside this event handler ---
|
|
try {
|
|
if (window.Hyphenopoly && window.Hyphenopoly.hyphenators) {
|
|
const hyphenatorPromise = window.Hyphenopoly.hyphenators["en-us"];
|
|
|
|
hyphenatorPromise.then(hyphenatorFunction => {
|
|
console.log("Hyphenator function obtained after hyphenopolyEnd:", hyphenatorFunction);
|
|
|
|
if (window.app && window.app.textProcessor) {
|
|
const hyphenationSelector = ".hyphenatePipe";
|
|
console.log("Using hyphenation selector:", hyphenationSelector);
|
|
|
|
const wrappedHyphenator = (text) => {
|
|
try {
|
|
// Don't strip the dot - Hyphenopoly expects the full selector with dot
|
|
const result = hyphenatorFunction(text, hyphenationSelector);
|
|
return result;
|
|
} catch (error) {
|
|
console.error("Error during hyphenation call:", error);
|
|
console.error("Text being hyphenated:", text);
|
|
console.error("Selector used:", hyphenationSelector);
|
|
return text; // Fallback
|
|
}
|
|
};
|
|
|
|
window.app.textProcessor.setHyphenator(wrappedHyphenator);
|
|
console.log("Hyphenator successfully configured on TextProcessor using selector after hyphenopolyEnd.");
|
|
|
|
} else {
|
|
console.error("Failed to set hyphenator post-hyphenopolyEnd: window.app or window.app.textProcessor not found.");
|
|
}
|
|
|
|
}).catch(err => {
|
|
console.error("Failed to get hyphenator function from promise post-hyphenopolyEnd:", err);
|
|
});
|
|
} else {
|
|
console.error("Hyphenopoly.hyphenators not found post-hyphenopolyEnd.");
|
|
}
|
|
} catch (error) {
|
|
console.error("General error setting up hyphenator post-hyphenopolyEnd:", error);
|
|
}
|
|
// --- End of moved logic ---
|
|
}
|
|
}
|
|
});
|
|
|
|
// Create and initialize the application
|
|
window.app = new AnimatedFiction({
|
|
// storyUrl: 'Herrenhaus.ink.json', // No longer needed for socket version
|
|
storyContainerId: 'story',
|
|
commandHistoryContainerId: 'command_history', // Specify history container
|
|
initialSpeed: 50,
|
|
locale: window.locale || 'en-us',
|
|
translations: translations
|
|
// ttsApiKey: 'YOUR_API_KEY' // Add if needed for specific TTS service via TtsPlayer
|
|
});
|
|
|
|
// Start the application
|
|
window.app.start();
|
|
|
|
// Force another UI update once DOM is completely rendered
|
|
});
|