/** * Module Loader System * * Handles loading and initializing modules in the correct order, * with dependency management and progress reporting. */ import { moduleRegistry } from './module-registry.js'; import { ModuleEvent } from './base-module.js'; /** * Module States */ const ModuleState = { PENDING: 'PENDING', LOADING: 'LOADING', WAITING: 'WAITING', INITIALIZING: 'INITIALIZING', FINISHED: 'FINISHED', ERROR: 'ERROR' }; /** * Module Loader - Manages the loading of all modules */ const ModuleLoader = (function() { // Private variables let loadingOverlay = null; let modulesList = null; let progressIndicator = null; let progressText = null; let statusText = null; let isLoadingComplete = false; let moduleWeights = {}; let createdModules = new Set(); // Track which modules we've created UI elements for let gameLoopModule = null; // Add variable to hold game loop instance /** * Initialize the loader */ function init() { // Prevent duplicate initialization if (createdModules.size > 0) { console.warn('Module Loader already initialized'); return; } console.log('Module Loader: Initialization started'); // Create the loading overlay createLoadingOverlay(); // Setup event listeners setupEventListeners(); // Load available module scripts loadModuleScripts().then(() => { // Once scripts are loaded, initialize modules initializeModules(); }); } /** * Setup event listeners for module communication */ function setupEventListeners() { // Listen for module progress events document.addEventListener('module:progress', handleModuleProgress); // Listen for module state change events document.addEventListener('module:stateChange', handleModuleStateChange); // Listen for module status message events document.addEventListener('module:message', handleModuleMessage); } /** * Load all module scripts * @returns {Promise} - Resolves when all module scripts are loaded */ async function loadModuleScripts() { // Define modules with their weights const modulesToLoad = [ // Core functionality modules { id: 'persistence-manager', script: '/js/persistence-manager.js', weight: 40 }, { id: 'localization', script: '/js/localization.js', weight: 40 }, { id: 'text-processor', script: '/js/text-processor.js', weight: 40 }, { id: 'paragraph-layout', script: '/js/paragraph-layout.js', weight: 40 }, { id: 'animation-queue', script: '/js/animation-queue.js', weight: 50 }, // Audio and TTS modules { id: 'audio-manager', script: '/js/audio-manager.js', weight: 60 }, { id: 'tts', script: '/js/tts-player.js', weight: 75 }, // UI and interaction modules { id: 'text-buffer', script: '/js/text-buffer.js', weight: 50 }, { id: 'ui-effects', script: '/js/ui-effects.js', weight: 50 }, // Add UI Effects module { id: 'ui-input-handler', script: '/js/ui-input-handler.js', weight: 50 }, // Add UI Input Handler module { id: 'ui-display-handler', script: '/js/ui-display-handler.js', weight: 60 }, // Add UI Display Handler module { id: 'ui-controller', script: '/js/ui-controller.js', weight: 100 }, { id: 'options-ui', script: '/js/options-ui.js', weight: 40 }, { id: 'socket-client', script: '/js/socket-client.js', weight: 60 }, // Main game module - should be last to load { id: 'game-loop', script: '/js/game-loop.js', weight: 25 } ]; // Store module weights for progress calculation modulesToLoad.forEach(module => { moduleWeights[module.id] = module.weight; }); // Create a module list entry for each module modulesToLoad.forEach(module => { createModuleListItem(module.id, getModuleNameFromId(module.id)); }); // Load each module script const loadPromises = modulesToLoad.map(module => loadScript(module.script)); return Promise.all(loadPromises); } /** * Load a script dynamically * @param {string} src - Script source URL * @returns {Promise} - Resolves when script is loaded */ function loadScript(src) { return new Promise((resolve, reject) => { const script = document.createElement('script'); script.type = 'module'; script.src = src; script.onload = () => resolve(); script.onerror = () => reject(new Error(`Failed to load script: ${src}`)); document.head.appendChild(script); }); } /** * Initialize all registered modules */ function initializeModules() { const modules = moduleRegistry.getAllModules(); // Find the game loop module instance gameLoopModule = moduleRegistry.getModule('game-loop'); // For each registered module, start initialization Object.values(modules).forEach(async (module) => { try { // Create a progress callback for this module const progressCallback = (percent, message) => { handleModuleProgress({ detail: { moduleId: module.id, progress: percent } }); if (message) { handleModuleMessage({ detail: { moduleId: module.id, message } }); } }; // Initialize the module with progress callback await module.initializeInterface(progressCallback); } catch (error) { console.error(`Error initializing module ${module.id}:`, error); } }); } /** * Get a human-readable module name from its ID * @param {string} id - Module ID * @returns {string} - User-friendly module name */ function getModuleNameFromId(id) { // Convert kebab-case to Title Case return id .split('-') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); } /** * Create the loading overlay */ function createLoadingOverlay() { // Find existing elements or create minimal ones for progress reporting loadingOverlay = document.querySelector('.loading-overlay'); if (!loadingOverlay) { // If no overlay exists in the HTML, create a minimal one loadingOverlay = document.createElement('div'); loadingOverlay.className = 'loading-overlay'; loadingOverlay.style.transition = 'opacity 0.5s ease-out'; document.body.appendChild(loadingOverlay); const content = document.createElement('div'); content.className = 'loading-content'; const title = document.createElement('h2'); title.textContent = 'Loading Interface'; content.appendChild(title); const progressBar = document.createElement('div'); progressBar.className = 'loading-bar'; progressIndicator = document.createElement('div'); progressIndicator.className = 'loading-progress'; progressBar.appendChild(progressIndicator); progressText = document.createElement('div'); progressText.className = 'loading-text'; progressText.textContent = '0%'; progressBar.appendChild(progressText); statusText = document.createElement('div'); statusText.className = 'loading-status'; statusText.textContent = 'Loading modules...'; modulesList = document.createElement('ul'); modulesList.id = 'modules-list'; content.appendChild(progressBar); content.appendChild(statusText); content.appendChild(modulesList); loadingOverlay.appendChild(content); } else { // If overlay exists, find the progress elements progressIndicator = loadingOverlay.querySelector('.loading-progress'); progressText = loadingOverlay.querySelector('.loading-text'); statusText = loadingOverlay.querySelector('.loading-status'); modulesList = loadingOverlay.querySelector('#modules-list'); // Ensure transition is set loadingOverlay.style.transition = 'opacity 0.5s ease-out'; } } /** * Create a module list item in the UI * @param {string} id - Module ID * @param {string} name - Module display name */ function createModuleListItem(id, name) { if (!modulesList) return; // Check if we've already created this module item if (createdModules.has(id)) return; // Mark this module as created createdModules.add(id); const moduleItem = document.createElement('li'); moduleItem.className = 'module-item'; moduleItem.id = `module-${id}`; moduleItem.innerHTML = ` ${name} Pending `; modulesList.appendChild(moduleItem); } /** * Handle module progress events */ function handleModuleProgress(event) { const { moduleId, progress } = event.detail; updateModuleProgress(moduleId, progress); updateOverallProgress(); } /** * Handle module state change events */ function handleModuleStateChange(event) { const { moduleId, state } = event.detail; updateModuleState(moduleId, state); updateOverallProgress(); // Check if all modules are finished after each state change checkAllFinished(); } /** * Handle module status message events */ function handleModuleMessage(event) { const { moduleId, message } = event.detail; updateModuleStatusText(moduleId, message); } /** * Check if all modules are finished loading */ function checkAllFinished() { const modules = moduleRegistry.getAllModules(); const allFinished = Object.values(modules).every(module => { const state = module.getState(); return state === ModuleState.FINISHED || state === ModuleState.ERROR; }); if (allFinished && !isLoadingComplete) { finalizeLoading(); } } /** * Finalize the loading process */ function finalizeLoading() { console.log('Loading completed. Finalizing...'); completeFinalization(); } /** * Complete the finalization process */ function completeFinalization() { isLoadingComplete = true; // Call the start method on the game loop module directly // Ensure the game loop module was found during initialization if (gameLoopModule && typeof gameLoopModule.start === 'function') { // Hide the overlay first, then start the game loop hideOverlay(() => { console.log("Loader: Overlay hidden, starting Game Loop."); gameLoopModule.start(); }); } else { console.error("Loader: Game Loop module not found or start method missing."); // Hide overlay anyway, but log error hideOverlay(); } } /** * Hide the loading overlay with a fade out animation * Then completely remove it from the DOM * @param {Function} [callback] - Optional callback to execute after fade completes */ function hideOverlay(callback) { // Added callback parameter if (!loadingOverlay) { if (callback) callback(); // Call callback immediately if no overlay return; } // Set opacity to 0 to trigger the fade-out transition loadingOverlay.style.opacity = '0'; // Use transition event listener to remove from DOM after fade completes loadingOverlay.addEventListener('transitionend', function handler(e) { // Only handle the opacity transition if (e.propertyName === 'opacity') { console.log('Module Loader: Removing overlay from DOM'); // Remove from DOM completely if (loadingOverlay.parentNode) { loadingOverlay.parentNode.removeChild(loadingOverlay); } // Remove the event listener to prevent memory leaks loadingOverlay.removeEventListener('transitionend', handler); // Set to null to allow garbage collection loadingOverlay = null; // Execute the callback if provided if (callback) callback(); } }); // Fallback in case the transition event doesn't fire setTimeout(() => { if (loadingOverlay && loadingOverlay.parentNode) { console.log('Module Loader: Removing overlay from DOM (fallback)'); loadingOverlay.parentNode.removeChild(loadingOverlay); loadingOverlay = null; } // Execute callback in fallback as well if (callback) callback(); }, 1000); // Wait longer than the transition duration } /** * Update the state of a module * @param {string} id - Module ID * @param {string} state - New state */ function updateModuleState(id, state) { // Update UI const moduleItem = document.getElementById(`module-${id}`); if (!moduleItem) return; const statusElement = moduleItem.querySelector('.module-status'); if (!statusElement) return; // Remove all status classes statusElement.classList.remove( 'status-pending', 'status-loading', 'status-waiting', 'status-initializing', 'status-finished', 'status-error' ); // Add appropriate class and text let statusText = ''; switch (state) { case ModuleState.PENDING: statusElement.classList.add('status-pending'); statusText = 'Pending'; break; case ModuleState.LOADING: statusElement.classList.add('status-loading'); statusText = 'Loading'; break; case ModuleState.WAITING: statusElement.classList.add('status-waiting'); statusText = 'Waiting'; break; case ModuleState.INITIALIZING: statusElement.classList.add('status-initializing'); statusText = 'Initializing'; break; case ModuleState.FINISHED: statusElement.classList.add('status-finished'); statusText = 'Finished'; break; case ModuleState.ERROR: statusElement.classList.add('status-error'); statusText = 'Error'; break; } statusElement.textContent = statusText; } /** * Update the progress of a module * @param {string} id - Module ID * @param {number} progress - Progress percentage (0-100) */ function updateModuleProgress(id, progress) { // Module states are now managed by the module itself // Update any additional UI elements for module progress if needed const moduleItem = document.getElementById(`module-${id}`); if (moduleItem) { // Update progress display if needed } } /** * Update the status text of a module in the UI * @param {string} id - Module ID * @param {string} text - Status text to display */ function updateModuleStatusText(id, text) { const moduleItem = document.getElementById(`module-${id}`); if (!moduleItem) return; let statusDetailElement = moduleItem.querySelector('.module-status-detail'); if (!statusDetailElement) { statusDetailElement = document.createElement('span'); statusDetailElement.className = 'module-status-detail'; moduleItem.appendChild(statusDetailElement); } statusDetailElement.textContent = text; } /** * Update overall progress based on module weights and progress */ function updateOverallProgress() { const modules = moduleRegistry.getAllModules(); const moduleIds = Object.keys(modules); // Calculate total weight const totalWeight = moduleIds.reduce((sum, id) => { return sum + (moduleWeights[id] || 1); }, 0); // Calculate weighted progress let overallProgress = moduleIds.reduce((sum, id) => { const module = modules[id]; const weight = moduleWeights[id] || 1; return sum + (module.progress * weight / totalWeight); }, 0); overallProgress = Math.min(Math.round(overallProgress), 100); // Update progress bar if (progressIndicator) { progressIndicator.style.width = `${overallProgress}%`; } if (progressText) { progressText.textContent = `${overallProgress}%`; } // Update status text based on progress if (statusText) { if (overallProgress >= 100 && !isLoadingComplete) { statusText.textContent = 'Finalizing...'; } else if (isLoadingComplete) { statusText.textContent = 'Complete!'; } } } // Public API return { init, ModuleState }; })(); // Now that ModuleLoader is defined, add the event listener document.addEventListener('DOMContentLoaded', () => { // Start the loading process when the DOM is loaded ModuleLoader.init(); });