/**
* 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 = `
Fatal Error: Circular Module Dependency
${errorMsg}
Please check the browser console for more details.
`;
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 = `
${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) {
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();
});