Split everything up into dynamically loaded modules.
This commit is contained in:
@@ -0,0 +1,532 @@
|
||||
/**
|
||||
* 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 = `
|
||||
<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) {
|
||||
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();
|
||||
});
|
||||
Reference in New Issue
Block a user