- 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>
339 lines
12 KiB
JavaScript
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; |