1004 lines
38 KiB
JavaScript
1004 lines
38 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',
|
|
FETCHING: 'FETCHING', // Added new state for fetching resources
|
|
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
|
|
let moduleTimings = {}; // Track timing data for modules
|
|
|
|
/**
|
|
* 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 state change events
|
|
document.addEventListener('module:stateChange', handleModuleStateChange);
|
|
|
|
// Listen for module status message events
|
|
document.addEventListener('module:message', handleModuleMessage);
|
|
|
|
// Listen for module progress events
|
|
document.addEventListener('module:progress', handleModuleProgress);
|
|
}
|
|
|
|
/**
|
|
* Load all module scripts
|
|
* @returns {Promise} - Resolves when all module scripts are loaded
|
|
*/
|
|
async function loadModuleScripts() {
|
|
|
|
// Define dependency scripts that need to be loaded first but aren't modules themselves
|
|
const dependenciesToLoad = [
|
|
{ script: '/js/api-tts-module-base.js' }, // Abstract base class, not a module
|
|
{ script: '/js/tts-handler-module.js' } // Abstract base class for TTS handlers, not a module
|
|
];
|
|
|
|
// Define modules with their weights
|
|
let modulesToLoad = [
|
|
// Core functionality modules
|
|
{ id: 'persistence-manager', script: '/js/persistence-manager-module.js', weight: 12 },
|
|
{ id: 'localization', script: '/js/localization-module.js', weight: 12 },
|
|
{ id: 'text-processor', script: '/js/text-processor-module.js', weight: 15 },
|
|
{ id: 'paragraph-layout', script: '/js/paragraph-layout-module.js', weight: 17 },
|
|
{ id: 'sentence-queue', script: '/js/sentence-queue-module.js', weight: 12 },
|
|
{ id: 'layout-renderer', script: '/js/layout-renderer-module.js', weight: 13 }, // Add Layout Renderer module
|
|
{ id: 'animation-queue', script: '/js/animation-queue-module.js', weight: 12 },
|
|
|
|
// Audio and TTS modules
|
|
{ id: 'audio-manager', script: '/js/audio-manager-module.js', weight: 12 },
|
|
{ id: 'kokoro', script: '/js/kokoro-tts-module.js', weight: 50 },
|
|
{ id: 'browser', script: '/js/browser-tts-module.js', weight: 12 },
|
|
{ id: 'elevenlabs', script: '/js/elevenlabs-tts-module.js', weight: 12 },
|
|
{ id: 'openai', script: '/js/openai-tts-module.js', weight: 12 },
|
|
{ id: 'tts-factory', script: '/js/tts-factory-module.js', weight: 13 }, // TTSFactory must be loaded before TTSPlayer
|
|
|
|
// UI and interaction modules
|
|
{ id: 'text-buffer', script: '/js/text-buffer-module.js', weight: 12 },
|
|
{ id: 'ui-effects', script: '/js/ui-effects-module.js', weight: 12 }, // Add UI Effects module
|
|
{ id: 'ui-input-handler', script: '/js/ui-input-handler-module.js', weight: 27 }, // Add UI Input Handler module
|
|
{ id: 'ui-display-handler', script: '/js/ui-display-handler-module.js', weight: 27 }, // Add UI Display Handler module
|
|
{ id: 'ui-controller', script: '/js/ui-controller-module.js', weight: 27 },
|
|
{ id: 'options-ui', script: '/js/options-ui-module.js', weight: 13 },
|
|
{ id: 'socket-client', script: '/js/socket-client-module.js', weight: 17 },
|
|
|
|
// Main game module - should be last to load
|
|
{ id: 'game-loop', script: '/js/game-loop-module.js', weight: 27 }
|
|
];
|
|
|
|
// 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 => {
|
|
createModuleItem(module.id, getModuleNameFromId(module.id));
|
|
});
|
|
|
|
// Load dependencies first
|
|
const loadDependencies = dependenciesToLoad.map(dependency => loadScript(dependency.script));
|
|
await Promise.all(loadDependencies);
|
|
|
|
// Load each module script
|
|
const loadPromises = modulesToLoad.map(module => loadScript(module.script));
|
|
const loadResult = await Promise.all(loadPromises);
|
|
|
|
// Wait briefly for modules to register
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
|
|
// Analyze dependencies and detect circular references
|
|
analyzeModuleDependencies();
|
|
|
|
return loadResult;
|
|
}
|
|
|
|
/**
|
|
* Analyze module dependencies to detect circular references and print detailed diagnostics
|
|
*/
|
|
function analyzeModuleDependencies() {
|
|
const registry = window.moduleRegistry;
|
|
if (!registry || !registry.modules) {
|
|
console.error("Module Registry not available for dependency analysis");
|
|
return;
|
|
}
|
|
|
|
// Build dependency graph
|
|
const graph = {};
|
|
|
|
// Initialize the graph with all modules
|
|
Object.keys(registry.modules).forEach(moduleId => {
|
|
graph[moduleId] = [];
|
|
});
|
|
|
|
// Add dependencies to graph
|
|
Object.entries(registry.modules).forEach(([moduleId, module]) => {
|
|
if (module.dependencies && Array.isArray(module.dependencies)) {
|
|
module.dependencies.forEach(depId => {
|
|
// Check if dependency exists
|
|
if (!registry.modules[depId]) {
|
|
console.warn(`Module ${moduleId} depends on missing module ${depId}`);
|
|
} else {
|
|
graph[moduleId].push(depId);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// Detect circular dependencies using DFS
|
|
const detectCycles = () => {
|
|
const visited = {};
|
|
const recStack = {};
|
|
const cycles = [];
|
|
const pathStack = [];
|
|
|
|
const dfs = (node, path = []) => {
|
|
// Node is already in recursion stack - we found a cycle
|
|
if (recStack[node]) {
|
|
const cycleStart = path.indexOf(node);
|
|
if (cycleStart !== -1) {
|
|
const cycle = path.slice(cycleStart).concat(node);
|
|
cycles.push(cycle);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// If already visited and not in recursion, no cycle through this node
|
|
if (visited[node]) {
|
|
return false;
|
|
}
|
|
|
|
// Mark node as visited and add to recursion stack
|
|
visited[node] = true;
|
|
recStack[node] = true;
|
|
pathStack.push(node);
|
|
|
|
// Visit all neighbors
|
|
const hasCycle = graph[node].some(neighbor => {
|
|
return dfs(neighbor, [...pathStack]);
|
|
});
|
|
|
|
// Remove from recursion stack and path
|
|
recStack[node] = false;
|
|
pathStack.pop();
|
|
|
|
return hasCycle;
|
|
};
|
|
|
|
// Start DFS from each node
|
|
Object.keys(graph).forEach(node => {
|
|
if (!visited[node]) {
|
|
dfs(node);
|
|
}
|
|
});
|
|
|
|
return cycles;
|
|
};
|
|
|
|
// Find all circular dependencies
|
|
const cycles = detectCycles();
|
|
|
|
// Display detailed information about circular dependencies
|
|
if (cycles.length > 0) {
|
|
console.group("%cCircular Dependencies Detected", "color: red; font-weight: bold");
|
|
cycles.forEach((cycle, index) => {
|
|
console.log(`%cCircular Dependency Chain ${index + 1}:`, "font-weight: bold");
|
|
|
|
// Print the cycle with dependency details
|
|
cycle.forEach((moduleId, i) => {
|
|
const module = registry.modules[moduleId] || { name: 'Unknown' };
|
|
const nextModuleId = cycle[(i + 1) % cycle.length];
|
|
const nextModule = registry.modules[nextModuleId] || { name: 'Unknown' };
|
|
console.log(
|
|
`%c${moduleId}%c (${module.name}) depends on %c${nextModuleId}%c (${nextModule.name})`,
|
|
"color: blue; font-weight: bold",
|
|
"color: black",
|
|
"color: blue; font-weight: bold",
|
|
"color: black"
|
|
);
|
|
|
|
// Print the actual dependencies declared by this module
|
|
if (module.dependencies && Array.isArray(module.dependencies)) {
|
|
console.log(` Dependencies declared by ${moduleId}:`, module.dependencies);
|
|
}
|
|
});
|
|
|
|
// Suggest potential solutions
|
|
console.log('%cPossible solutions:', 'font-weight: bold');
|
|
console.log('1. Remove one of the dependencies from the chain');
|
|
console.log('2. Use dynamic dependency resolution instead of static dependencies');
|
|
console.log('3. Create an interface module that both modules depend on');
|
|
console.log('4. Refactor module responsibilities to eliminate circular needs');
|
|
});
|
|
console.groupEnd();
|
|
} else {
|
|
console.log("%cNo circular dependencies detected", "color: green; font-weight: bold");
|
|
}
|
|
|
|
// Calculate an optimized loading order using topological sort
|
|
const calculateOptimalLoadOrder = () => {
|
|
const result = [];
|
|
const visited = {};
|
|
const temp = {}; // Temporary marks for detecting cycles
|
|
|
|
const visit = (node) => {
|
|
// Node is already in result, skip
|
|
if (visited[node]) return;
|
|
|
|
// If temp is true, we have a cycle
|
|
if (temp[node]) {
|
|
console.warn(`Skipping cycle involving ${node} during topological sort`);
|
|
return;
|
|
}
|
|
|
|
// Mark node as being processed
|
|
temp[node] = true;
|
|
|
|
// Process all dependencies first
|
|
if (graph[node]) {
|
|
graph[node].forEach(dep => visit(dep));
|
|
}
|
|
|
|
// Mark as visited and add to result
|
|
temp[node] = false;
|
|
visited[node] = true;
|
|
result.push(node);
|
|
};
|
|
|
|
// Visit all nodes
|
|
Object.keys(graph).forEach(node => {
|
|
if (!visited[node]) {
|
|
visit(node);
|
|
}
|
|
});
|
|
|
|
return result.reverse(); // Reverse to get correct order
|
|
};
|
|
|
|
const optimalOrder = calculateOptimalLoadOrder();
|
|
|
|
// Print the optimal loading order
|
|
console.group("%cOptimal Module Loading Order", "color: green; font-weight: bold");
|
|
optimalOrder.forEach((moduleId, index) => {
|
|
const module = registry.modules[moduleId] || { name: 'Unknown' };
|
|
console.log(`${index + 1}. %c${moduleId}%c (${module.name})`,
|
|
"font-weight: bold", "font-weight: normal");
|
|
});
|
|
console.groupEnd();
|
|
|
|
// Compare with actual loading order and suggest improvements
|
|
console.log("%cRecommended changes to module loading order in loader.js:", "font-weight: bold");
|
|
if (optimalOrder.length > 0) {
|
|
console.log("const modulesToLoad = [");
|
|
optimalOrder.forEach((moduleId, index) => {
|
|
// Generate a reasonable path based on the moduleId
|
|
const scriptPath = `/js/${moduleId}.js`;
|
|
console.log(` { id: '${moduleId}', script: '${scriptPath}', weight: ${index * 5 + 5} },`);
|
|
});
|
|
console.log("];");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sort modules by their dependencies to create an optimal loading order
|
|
* This function can be used before initialization to ensure modules are loaded in the correct order
|
|
* @param {Array} modules - Array of module objects with id and dependencies
|
|
* @returns {Array} - Sorted array of modules
|
|
*/
|
|
function sortModulesByDependencies(modules) {
|
|
// Build a dependency graph
|
|
const graph = {};
|
|
|
|
// Initialize the graph with all modules
|
|
modules.forEach(module => {
|
|
graph[module.id] = { module, dependencies: [] };
|
|
});
|
|
|
|
// Add dependencies to the graph
|
|
// We need to do this in a second pass because some modules might reference others that come later in the array
|
|
modules.forEach(module => {
|
|
if (module.dependencies && Array.isArray(module.dependencies)) {
|
|
module.dependencies.forEach(depId => {
|
|
if (graph[depId]) {
|
|
graph[module.id].dependencies.push(depId);
|
|
} else {
|
|
console.warn(`Module ${module.id} depends on unknown module ${depId}`);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// Perform a topological sort
|
|
const result = [];
|
|
const visited = {};
|
|
const temp = {}; // For cycle detection
|
|
|
|
function visit(nodeId) {
|
|
// Node is already in result, skip
|
|
if (visited[nodeId]) return;
|
|
|
|
// If temp is true, we have a cycle
|
|
if (temp[nodeId]) {
|
|
console.warn(`Circular dependency detected involving ${nodeId}. Skipping.`);
|
|
return;
|
|
}
|
|
|
|
// Mark node as being processed
|
|
temp[nodeId] = true;
|
|
|
|
// Process all dependencies first
|
|
if (graph[nodeId] && graph[nodeId].dependencies) {
|
|
graph[nodeId].dependencies.forEach(depId => {
|
|
visit(depId);
|
|
});
|
|
}
|
|
|
|
// Mark as visited and add to result
|
|
temp[nodeId] = false;
|
|
visited[nodeId] = true;
|
|
result.push(graph[nodeId].module);
|
|
}
|
|
|
|
// Visit all nodes
|
|
Object.keys(graph).forEach(nodeId => {
|
|
if (!visited[nodeId]) {
|
|
visit(nodeId);
|
|
}
|
|
});
|
|
|
|
return result.reverse(); // Reverse for correct dependency order
|
|
}
|
|
|
|
/**
|
|
* Load a script dynamically
|
|
* @param {string} src - Script source URL
|
|
* @returns {Promise} - Resolves when script is loaded
|
|
*/
|
|
function loadScript(src) {
|
|
// Extract module ID from src path
|
|
const moduleId = src.split('/').pop().replace('.js', '');
|
|
|
|
// Update state to LOADING if this is a module
|
|
if (moduleId && moduleWeights[moduleId]) {
|
|
// Ensure module item exists in UI
|
|
const moduleItem = document.getElementById(`module-${moduleId}`);
|
|
if (!moduleItem) {
|
|
createModuleItem(moduleId, getModuleNameFromId(moduleId));
|
|
}
|
|
|
|
// Set initial progress to 0%
|
|
handleModuleProgress({
|
|
detail: { moduleId, progress: 0 }
|
|
});
|
|
|
|
// Set state to loading
|
|
updateModuleState(moduleId, ModuleState.LOADING);
|
|
|
|
// Record start time for this module (for timing data)
|
|
if (!moduleTimings[moduleId]) {
|
|
moduleTimings[moduleId] = {};
|
|
}
|
|
moduleTimings[moduleId].startTime = performance.now();
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const script = document.createElement('script');
|
|
script.type = 'module';
|
|
script.src = src;
|
|
|
|
// Monitor loading progress using a fake progress indicator (0-10%)
|
|
if (moduleId && moduleWeights[moduleId]) {
|
|
let loadProgress = 0;
|
|
const progressInterval = setInterval(() => {
|
|
loadProgress = Math.min(loadProgress + 1, 9); // Max 9% until actual load completes
|
|
handleModuleProgress({
|
|
detail: { moduleId, progress: loadProgress }
|
|
});
|
|
}, 100);
|
|
|
|
script.onload = () => {
|
|
clearInterval(progressInterval);
|
|
// Final progress at 10% when script is loaded
|
|
handleModuleProgress({
|
|
detail: { moduleId, progress: 10 }
|
|
});
|
|
|
|
// Record script load complete time
|
|
if (moduleTimings[moduleId]) {
|
|
moduleTimings[moduleId].scriptLoadTime = performance.now();
|
|
}
|
|
|
|
resolve();
|
|
};
|
|
|
|
script.onerror = (error) => {
|
|
clearInterval(progressInterval);
|
|
updateModuleState(moduleId, ModuleState.ERROR);
|
|
|
|
// Record error time
|
|
if (moduleTimings[moduleId]) {
|
|
moduleTimings[moduleId].errorTime = performance.now();
|
|
}
|
|
|
|
reject(new Error(`Failed to load script: ${src}`));
|
|
};
|
|
} else {
|
|
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
|
|
* @param {string} id - Module ID
|
|
* @param {string} name - Module name
|
|
* @returns {HTMLLIElement} List item element
|
|
*/
|
|
function createModuleItem(id, name) {
|
|
if (!modulesList || createdModules.has(id)) return null;
|
|
createdModules.add(id);
|
|
|
|
// Create elements dynamically
|
|
const li = document.createElement('li');
|
|
li.id = `module-${id}`;
|
|
li.className = 'module-item';
|
|
|
|
// Set initial progress to 0 using CSS variable
|
|
li.style.setProperty('--progress-width', '0%');
|
|
|
|
// Create module name element
|
|
const moduleName = document.createElement('div');
|
|
moduleName.className = 'module-name';
|
|
moduleName.textContent = name;
|
|
|
|
// Create module status element
|
|
const moduleStatus = document.createElement('div');
|
|
moduleStatus.className = 'module-status status-pending';
|
|
moduleStatus.textContent = 'Pending';
|
|
|
|
// Create module status details element
|
|
const moduleDetails = document.createElement('div');
|
|
moduleDetails.className = 'module-status-detail';
|
|
moduleDetails.textContent = '';
|
|
|
|
// Append all elements to the list item
|
|
li.appendChild(moduleName);
|
|
li.appendChild(moduleStatus);
|
|
li.appendChild(moduleDetails);
|
|
|
|
// Force a reflow to ensure animation works
|
|
void li.offsetWidth;
|
|
|
|
// Add to modules list
|
|
modulesList.appendChild(li);
|
|
|
|
return li;
|
|
}
|
|
|
|
/**
|
|
* Handle module progress events
|
|
*/
|
|
function handleModuleProgress(event) {
|
|
const { moduleId, progress } = event.detail;
|
|
|
|
// Get the module element
|
|
const moduleItem = document.querySelector(`#module-${moduleId}`);
|
|
if (moduleItem) {
|
|
// Update module item's before pseudo-element width using CSS variable
|
|
moduleItem.style.setProperty('--progress-width', `${progress}%`);
|
|
|
|
// Also set a data attribute for browsers that don't support CSS variables
|
|
moduleItem.setAttribute('data-progress', progress);
|
|
}
|
|
|
|
updateOverallProgress();
|
|
}
|
|
|
|
/**
|
|
* Handle module state change events
|
|
*/
|
|
function handleModuleStateChange(event) {
|
|
const { moduleId, state } = event.detail;
|
|
// Update UI with the new state
|
|
updateModuleState(moduleId, state);
|
|
|
|
// If module is finished, update overall completion
|
|
if (state === ModuleState.FINISHED) {
|
|
// This triggers only when ALL modules are complete, so modules would be removed too quickly
|
|
// if (areAllModulesComplete()) {
|
|
// hideLoadingOverlay();
|
|
// }
|
|
const moduleItem = document.getElementById(`module-${moduleId}`);
|
|
if (moduleItem) {
|
|
// Ensure module-finished class is added with a small delay to avoid race conditions
|
|
setTimeout(() => {
|
|
moduleItem.classList.add('module-finished');
|
|
}, 100);
|
|
}
|
|
}
|
|
|
|
updateOverallProgress();
|
|
|
|
// Record timing data
|
|
if (moduleTimings[moduleId]) {
|
|
moduleTimings[moduleId][state] = performance.now();
|
|
|
|
// If the module is finished or has an error, calculate total time
|
|
if (state === ModuleState.FINISHED || state === ModuleState.ERROR) {
|
|
const startTime = moduleTimings[moduleId].startTime || 0;
|
|
moduleTimings[moduleId].totalTime = performance.now() - startTime;
|
|
}
|
|
}
|
|
|
|
// 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();
|
|
|
|
// Add detailed logging of all module states
|
|
console.log('Module states:', Object.entries(modules).map(([id, module]) => {
|
|
return `${id}: ${module.getState()}`;
|
|
}));
|
|
|
|
// First determine which modules are pending
|
|
const pendingModules = Object.values(modules).filter(module => {
|
|
const state = module.getState();
|
|
return state !== ModuleState.FINISHED && state !== ModuleState.ERROR;
|
|
});
|
|
|
|
// Log pending modules (if any)
|
|
if (pendingModules.length > 0) {
|
|
console.log('Modules still pending:', pendingModules.map(m => `${m.id} (${m.getState()})`));
|
|
} else {
|
|
console.log('No modules pending - all modules are in FINISHED or ERROR state');
|
|
}
|
|
|
|
// Determine if all modules are finished based on pendingModules
|
|
const allFinished = pendingModules.length === 0;
|
|
|
|
if (allFinished && !isLoadingComplete) {
|
|
console.log('All modules finished loading. Proceeding to finalization...');
|
|
finalizeLoading();
|
|
} else if (allFinished && isLoadingComplete) {
|
|
console.log('All modules are finished but isLoadingComplete is already true');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Finalize the loading process
|
|
*/
|
|
function finalizeLoading() {
|
|
console.log('Loading completed. Finalizing...');
|
|
try {
|
|
// Display timing data
|
|
displayModuleTimings();
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Display module timing data to help with weight optimization
|
|
*/
|
|
function displayModuleTimings() {
|
|
console.group('Module Loading Performance Data');
|
|
console.log('This data can be used to optimize module weights:');
|
|
|
|
// Format timing data as tuples [moduleId, totalTime, weight]
|
|
const timingData = Object.entries(moduleTimings)
|
|
.filter(([moduleId, timing]) => timing.totalTime !== undefined)
|
|
.map(([moduleId, timing]) => {
|
|
return [moduleId, Math.round(timing.totalTime), moduleWeights[moduleId] || 1];
|
|
})
|
|
.sort((a, b) => b[1] - a[1]); // Sort by total time (descending)
|
|
|
|
// Create a table for easy reading
|
|
console.table(timingData.map(([moduleId, time, weight]) => {
|
|
return {
|
|
moduleId,
|
|
'totalTime (ms)': time,
|
|
'current weight': weight,
|
|
'suggested weight': Math.max(1, Math.round(time / 50)) // Simple heuristic based on time
|
|
};
|
|
}));
|
|
|
|
console.log('Raw timing data:');
|
|
console.table(moduleTimings);
|
|
console.groupEnd();
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
}
|
|
});
|
|
|
|
}
|
|
|
|
/**
|
|
* 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',
|
|
'status-fetching'
|
|
);
|
|
|
|
// Set the new status
|
|
let statusText = 'Unknown';
|
|
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.FETCHING:
|
|
statusElement.classList.add('status-fetching');
|
|
statusText = 'Fetching';
|
|
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 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();
|
|
});
|