Class_generator/js/core/game-loader.js
StillHammer 30a2028da6 Remove Text Reader game and enhance Story Reader
- Enhanced Story Reader with text-to-story conversion methods
- Added support for simple texts and sentences in Story Reader
- Removed Text Reader game file (js/games/text-reader.js)
- Updated all configuration files to remove Text Reader references
- Modified game compatibility system to use Story Reader instead
- Updated test fixtures to reflect game changes
- Cleaned up debug/test HTML files

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-20 11:22:56 +08:00

401 lines
14 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.message}`, 'ERROR');
logSh(`❌ Stack trace: ${error.stack}`, '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`);
}
// NOUVEAU: Utiliser le JSONContentLoader pour adapter le contenu ultra-modulaire
const adaptedContent = this.jsonLoader.loadContent(rawModule);
logSh(`🔄 Contenu adapté pour ${contentType}: ${adaptedContent._adapted ? 'JSON ultra-modulaire' : 'format legacy'}`, 'INFO');
// Combiner les informations du scanner avec le contenu adapté
const enrichedContent = {
...adaptedContent,
...contentInfo,
// S'assurer que le contenu brut du module est disponible
rawContent: rawModule,
adaptedContent: adaptedContent
};
this.loadedModules.content[contentType] = enrichedContent;
return enrichedContent;
} catch (error) {
logSh(`Erreur chargement contenu ${contentType}:`, error, 'ERROR');
// Fallback: essayer de charger directement le fichier JSON
if (contentType.includes('json') || contentType.includes('ultra') || contentType.includes('exemple')) {
try {
logSh(`🔄 Tentative de chargement JSON direct pour ${contentType}...`, 'INFO');
const jsonResponse = await fetch(`${contentType}.json`);
if (jsonResponse.ok) {
const jsonContent = await jsonResponse.json();
const adaptedContent = this.jsonLoader.adapt(jsonContent);
this.loadedModules.content[contentType] = adaptedContent;
return adaptedContent;
}
} catch (jsonError) {
logSh(`⚠️ Fallback JSON échoué: ${jsonError.message}`, 'WARN');
}
}
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) {
logSh(`🎮 Initializing game: ${gameType}`, 'DEBUG');
try {
const gameContainer = document.getElementById('game-container');
const gameTitle = document.getElementById('game-title');
const scoreDisplay = document.getElementById('current-score');
logSh('🎮 DOM elements found, adapting content...', 'DEBUG');
// Adapter le contenu avec le JSON Loader pour compatibilité avec les jeux
const adaptedContent = this.jsonLoader.loadContent(contentData);
logSh('🎮 Content adapted, updating UI...', 'DEBUG');
// Mise à jour du titre
const contentName = adaptedContent.name || contentType;
gameTitle.textContent = this.getGameTitle(gameType, contentName);
// Réinitialisation du score
scoreDisplay.textContent = '0';
logSh('🎮 Creating game instance...', 'DEBUG');
// 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)
});
logSh('🎮 Game instance created successfully', 'DEBUG');
// Démarrage du jeu (seulement si la méthode start existe)
if (typeof this.currentGame.start === 'function') {
logSh('🎮 Starting game with start() method...', 'DEBUG');
this.currentGame.start();
} else {
logSh('🎮 Game auto-started in constructor (no start() method)', 'DEBUG');
}
logSh('✅ Game initialization completed successfully', 'DEBUG');
} catch (error) {
logSh(`❌ Error in initGame: ${error.message}`, 'ERROR');
logSh(`❌ initGame stack: ${error.stack}`, 'ERROR');
throw error;
}
},
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',
'adventure-reader': 'AdventureReader',
'chinese-study': 'ChineseStudy',
'story-reader': 'StoryReader',
'grammar-discovery': 'GrammarDiscovery',
'word-storm': 'WordStorm',
'word-discovery': 'WordDiscovery',
'letter-discovery': 'LetterDiscovery',
'river-run': 'RiverRun'
};
return names[gameType] || gameType;
},
getContentModuleName(contentType) {
// Utilise la même logique que le ContentScanner
const mapping = {
'sbs-level-7-8-new': 'SBSLevel78New',
'sbs-level-1': 'SBSLevel1',
'basic-chinese': 'BasicChinese',
'english-class-demo': 'EnglishClassDemo',
'chinese-long-story': 'ChineseLongStory',
'french-beginner-story': 'FrenchBeginnerStory',
'wta1b1': 'WTA1B1',
'story-prototype-optimized': 'StoryPrototypeOptimized',
'test-minimal-content': 'TestMinimalContent',
'test-rich-content': 'TestRichContent',
'test-sentence-only': 'TestSentenceOnly',
'test-minimal': 'TestMinimal',
'test-rich': 'TestRich'
};
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',
'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;