Class_generator/js/core/game-loader.js
StillHammer 1f8688c4aa Fix WebSocket logging system and add comprehensive network features
- Fix WebSocket server to properly broadcast logs to all connected clients
- Integrate professional logging system with real-time WebSocket interface
- Add network status indicator with DigitalOcean Spaces connectivity
- Implement AWS Signature V4 authentication for private bucket access
- Add JSON content loader with backward compatibility to JS modules
- Restore navigation breadcrumb system with comprehensive logging
- Add multiple content formats: JSON + JS with automatic discovery
- Enhance top bar with logger toggle and network status indicator
- Remove deprecated temp-games module and clean up unused files

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-15 23:05:14 +08:00

339 lines
12 KiB
JavaScript

// === CHARGEUR DE JEUX DYNAMIQUE ===
const GameLoader = {
currentGame: null,
contentScanner: new ContentScanner(),
jsonLoader: new JSONContentLoader(),
loadedModules: {
games: {},
content: {}
},
async loadGame(gameType, contentType) {
try {
// Nettoyage du jeu précédent
this.cleanup();
// Chargement parallèle du module de jeu et du contenu
const [gameModule, contentModule] = await Promise.all([
this.loadGameModule(gameType),
this.loadContentModule(contentType)
]);
// Initialisation du jeu
this.initGame(gameType, gameModule, contentModule);
} catch (error) {
logSh('Erreur lors du chargement du jeu:', error, 'ERROR');
throw error;
}
},
async loadGameModule(gameType) {
// Vérifier si le module est déjà chargé
if (this.loadedModules.games[gameType]) {
return this.loadedModules.games[gameType];
}
try {
// Chargement dynamique du script
await this.loadScript(`js/games/${gameType}.js`);
// Récupération du module depuis l'objet global
const module = window.GameModules?.[this.getModuleName(gameType)];
if (!module) {
throw new Error(`Module de jeu ${gameType} non trouvé`);
}
// Cache du module
this.loadedModules.games[gameType] = module;
return module;
} catch (error) {
logSh(`Erreur chargement module jeu ${gameType}:`, error, 'ERROR');
throw error;
}
},
async loadContentModule(contentType) {
// Utiliser le ContentScanner pour récupérer le contenu découvert
try {
// Récupérer le contenu déjà découvert par le scanner
const contentInfo = await this.contentScanner.getContentById(contentType);
if (!contentInfo) {
throw new Error(`Contenu ${contentType} non trouvé par le scanner`);
}
// Charger le module JavaScript correspondant
await this.loadScript(`js/content/${contentInfo.filename}`);
// Récupérer le module depuis l'objet global
const moduleName = this.getContentModuleName(contentType);
const rawModule = window.ContentModules?.[moduleName];
if (!rawModule) {
throw new Error(`Module ${moduleName} non trouvé après chargement`);
}
// Combiner les informations du scanner avec le contenu brut
const enrichedContent = {
...rawModule,
...contentInfo,
// S'assurer que le contenu brut du module est disponible
rawContent: rawModule
};
this.loadedModules.content[contentType] = enrichedContent;
return enrichedContent;
} catch (error) {
logSh(`Erreur chargement contenu ${contentType}:`, error, 'ERROR');
throw error;
}
},
loadScript(src) {
return new Promise((resolve, reject) => {
// Vérifier si le script est déjà chargé
const existingScript = document.querySelector(`script[src="${src}"]`);
if (existingScript) {
resolve();
return;
}
const script = document.createElement('script');
script.src = src;
script.onload = resolve;
script.onerror = () => reject(new Error(`Impossible de charger ${src}`));
document.head.appendChild(script);
});
},
initGame(gameType, GameClass, contentData) {
const gameContainer = document.getElementById('game-container');
const gameTitle = document.getElementById('game-title');
const scoreDisplay = document.getElementById('current-score');
// Adapter le contenu avec le JSON Loader pour compatibilité avec les jeux
const adaptedContent = this.jsonLoader.loadContent(contentData);
// Mise à jour du titre
const contentName = adaptedContent.name || contentType;
gameTitle.textContent = this.getGameTitle(gameType, contentName);
// Réinitialisation du score
scoreDisplay.textContent = '0';
// Création de l'instance de jeu avec contenu enrichi
this.currentGame = new GameClass({
container: gameContainer,
content: adaptedContent,
contentScanner: this.contentScanner, // Passer le scanner pour accès aux métadonnées
onScoreUpdate: (score) => this.updateScore(score),
onGameEnd: (finalScore) => this.handleGameEnd(finalScore)
});
// Démarrage du jeu
this.currentGame.start();
},
updateScore(score) {
const scoreDisplay = document.getElementById('current-score');
scoreDisplay.textContent = score.toString();
// Animation du score
Utils.animateElement(scoreDisplay, 'pulse', 200);
},
handleGameEnd(finalScore) {
// Sauvegarde du score
this.saveScore(finalScore);
// Affichage du résultat
Utils.showToast(`Jeu terminé ! Score final: ${finalScore}`, 'success');
// Afficher les options de fin de jeu
this.showGameEndOptions(finalScore);
},
showGameEndOptions(finalScore) {
const gameContainer = document.getElementById('game-container');
// Créer l'overlay de fin de jeu
const endOverlay = document.createElement('div');
endOverlay.className = 'game-end-overlay';
endOverlay.innerHTML = `
<div class="game-end-modal">
<h3>🎉 Jeu Terminé !</h3>
<div class="final-score">Score final: <strong>${finalScore}</strong></div>
<div class="best-score">Meilleur score: <strong>${this.getBestScoreForCurrentGame()}</strong></div>
<div class="game-end-buttons">
<button class="restart-game-btn">🔄 Rejouer</button>
<button class="back-to-levels-btn">← Changer de niveau</button>
<button class="back-to-games-btn">🎮 Autres jeux</button>
</div>
</div>
`;
gameContainer.appendChild(endOverlay);
// Ajouter les event listeners
endOverlay.querySelector('.restart-game-btn').addEventListener('click', () => {
this.removeGameEndOverlay();
this.restartCurrentGame();
});
endOverlay.querySelector('.back-to-levels-btn').addEventListener('click', () => {
const params = Utils.getUrlParams();
AppNavigation.navigateTo('levels', params.game);
});
endOverlay.querySelector('.back-to-games-btn').addEventListener('click', () => {
AppNavigation.navigateTo('games');
});
// Fermer avec ESC
const handleEscape = (e) => {
if (e.key === 'Escape') {
this.removeGameEndOverlay();
AppNavigation.goBack();
document.removeEventListener('keydown', handleEscape);
}
};
document.addEventListener('keydown', handleEscape);
},
removeGameEndOverlay() {
const overlay = document.querySelector('.game-end-overlay');
if (overlay) {
overlay.remove();
}
},
getBestScoreForCurrentGame() {
const params = Utils.getUrlParams();
return this.getBestScore(params.game, params.content);
},
restartCurrentGame() {
if (this.currentGame && this.currentGame.restart) {
this.currentGame.restart();
document.getElementById('current-score').textContent = '0';
}
},
cleanup() {
if (this.currentGame && this.currentGame.destroy) {
this.currentGame.destroy();
}
// Supprimer l'overlay de fin de jeu s'il existe
this.removeGameEndOverlay();
const gameContainer = document.getElementById('game-container');
gameContainer.innerHTML = '';
this.currentGame = null;
},
saveScore(score) {
const params = Utils.getUrlParams();
const scoreKey = `score_${params.game}_${params.content}`;
const currentScores = Utils.storage.get(scoreKey, []);
currentScores.push({
score: score,
date: new Date().toISOString(),
timestamp: Date.now()
});
// Garder seulement les 10 meilleurs scores
currentScores.sort((a, b) => b.score - a.score);
const bestScores = currentScores.slice(0, 10);
Utils.storage.set(scoreKey, bestScores);
},
getBestScore(gameType, contentType) {
const scoreKey = `score_${gameType}_${contentType}`;
const scores = Utils.storage.get(scoreKey, []);
return scores.length > 0 ? scores[0].score : 0;
},
// Utilitaires de nommage
getModuleName(gameType) {
const names = {
'whack-a-mole': 'WhackAMole',
'whack-a-mole-hard': 'WhackAMoleHard',
'memory-match': 'MemoryMatch',
'quiz-game': 'QuizGame',
'fill-the-blank': 'FillTheBlank',
'text-reader': 'TextReader',
'adventure-reader': 'AdventureReader',
'chinese-study': 'ChineseStudy'
};
return names[gameType] || gameType;
},
getContentModuleName(contentType) {
// Utilise la même logique que le ContentScanner
const mapping = {
'sbs-level-7-8-new': 'SBSLevel78New',
'basic-chinese': 'BasicChinese',
'english-class-demo': 'EnglishClassDemo'
};
return mapping[contentType] || this.toPascalCase(contentType);
},
toPascalCase(str) {
return str.split('-').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join('');
},
getGameTitle(gameType, contentName) {
const gameNames = {
'whack-a-mole': 'Whack-a-Mole',
'whack-a-mole-hard': 'Whack-a-Mole Hard',
'memory-match': 'Memory Match',
'quiz-game': 'Quiz Game',
'fill-the-blank': 'Fill the Blank',
'text-reader': 'Text Reader',
'adventure-reader': 'Adventure Reader'
};
const gameName = gameNames[gameType] || gameType;
return `${gameName} - ${contentName}`;
},
// API pour les jeux
createGameAPI() {
return {
showFeedback: (message, type = 'info') => Utils.showToast(message, type),
playSound: (soundFile) => this.playSound(soundFile),
updateScore: (score) => this.updateScore(score),
endGame: (score) => this.handleGameEnd(score),
getBestScore: () => {
const params = Utils.getUrlParams();
return this.getBestScore(params.game, params.content);
}
};
},
playSound(soundFile) {
if (Utils.canPlayAudio()) {
try {
const audio = new Audio(`assets/sounds/${soundFile}`);
audio.volume = 0.5;
audio.play().catch(e => logSh('Cannot play sound:', e, 'WARN'););
} catch (error) {
logSh('Sound error:', error, 'WARN');
}
}
}
};
// Export global
window.GameLoader = GameLoader;