588 lines
21 KiB
JavaScript
588 lines
21 KiB
JavaScript
/**
|
|
* 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';
|
|
|
|
// Ensure moduleRegistry is available globally before anything else runs
|
|
window.moduleRegistry = moduleRegistry;
|
|
console.log('Module registry initialized and assigned to window.moduleRegistry');
|
|
|
|
/**
|
|
* 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');
|
|
|
|
// Check for circular dependencies before proceeding
|
|
const circularDependencies = moduleRegistry.checkForCircularDependencies();
|
|
if (circularDependencies) {
|
|
const errorMsg = `Circular dependency detected: ${circularDependencies.join(' -> ')} -> ${circularDependencies[0]}`;
|
|
console.error(errorMsg);
|
|
document.body.innerHTML = `<div style="padding: 20px; color: white; background-color: #ff3333;">
|
|
<h2>Fatal Error: Circular Module Dependency</h2>
|
|
<p>${errorMsg}</p>
|
|
<p>Please check the browser console for more details.</p>
|
|
</div>`;
|
|
return;
|
|
}
|
|
|
|
// 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: 'layout-renderer', script: '/js/layout-renderer.js', weight: 45 }, // Add Layout Renderer module
|
|
{ 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-factory', script: '/js/tts-factory.js', weight: 70 }, // TTSFactory must be loaded before TTSPlayer
|
|
{ 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');
|
|
|
|
// Log dependency information for debugging
|
|
console.log('Module dependencies:');
|
|
Object.keys(modules).forEach(moduleId => {
|
|
const dependencies = moduleRegistry.getDependencies(moduleId);
|
|
console.log(`${moduleId} depends on: ${dependencies.length ? dependencies.join(', ') : 'none'}`);
|
|
});
|
|
|
|
// 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
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
// Log start of initialization
|
|
console.log(`Starting initialization of module: ${module.id}`);
|
|
|
|
// Initialize the module with progress callback
|
|
await module.initializeInterface(progressCallback);
|
|
|
|
// Log completion of initialization
|
|
console.log(`Completed initialization of module: ${module.id}`);
|
|
|
|
} 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 = `
|
|
<span class="module-name">${name}</span>
|
|
<span class="module-status status-pending">Pending</span>
|
|
`;
|
|
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) {
|
|
console.log('All modules finished loading. Proceeding to finalization...');
|
|
finalizeLoading();
|
|
} else if (!allFinished) {
|
|
// Log which modules are not finished yet
|
|
const pendingModules = Object.values(modules).filter(module => {
|
|
const state = module.getState();
|
|
return state !== ModuleState.FINISHED && state !== ModuleState.ERROR;
|
|
});
|
|
|
|
if (pendingModules.length > 0) {
|
|
console.log('Modules still pending:', pendingModules.map(m => `${m.id} (${m.getState()})`))
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Finalize the loading process
|
|
*/
|
|
function finalizeLoading() {
|
|
console.log('Loading completed. Finalizing...');
|
|
try {
|
|
completeFinalization();
|
|
} catch (error) {
|
|
console.error('Error during finalization:', error);
|
|
// Force hide the overlay even if there was an error
|
|
hideOverlay();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.");
|
|
try {
|
|
gameLoopModule.start();
|
|
} catch (error) {
|
|
console.error("Error starting Game Loop:", error);
|
|
}
|
|
});
|
|
} 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();
|
|
});
|