// === MODULE WHACK-A-MOLE HARD === class WhackAMoleHardGame { constructor(options) { this.container = options.container; this.content = options.content; this.onScoreUpdate = options.onScoreUpdate || (() => {}); this.onGameEnd = options.onGameEnd || (() => {}); // État du jeu this.score = 0; this.errors = 0; this.maxErrors = 5; this.gameTime = 60; // 60 secondes this.timeLeft = this.gameTime; this.isRunning = false; this.gameMode = 'translation'; // 'translation', 'image', 'sound' // Configuration des taupes this.holes = []; this.activeMoles = []; this.moleAppearTime = 3000; // 3 secondes d'affichage (plus long) this.spawnRate = 2000; // Nouvelle vague toutes les 2 secondes this.molesPerWave = 3; // 3 taupes par vague // Timers this.gameTimer = null; this.spawnTimer = null; // Vocabulaire pour ce jeu - adapté pour le nouveau système this.vocabulary = this.extractVocabulary(this.content); this.currentWords = []; this.targetWord = null; // Système de garantie pour le mot cible this.spawnsSinceTarget = 0; this.maxSpawnsWithoutTarget = 10; // Le mot cible doit apparaître dans les 10 prochaines taupes (1/10 chance) this.init(); } init() { // Vérifier que nous avons du vocabulaire if (!this.vocabulary || this.vocabulary.length === 0) { console.error('Aucun vocabulaire disponible pour Whack-a-Mole'); this.showInitError(); return; } this.createGameBoard(); this.createGameUI(); this.setupEventListeners(); } showInitError() { this.container.innerHTML = `

❌ Erreur de chargement

Ce contenu ne contient pas de vocabulaire compatible avec Whack-a-Mole.

Le jeu nécessite des mots avec leurs traductions.

`; } createGameBoard() { this.container.innerHTML = `
${this.timeLeft} Time
${this.errors} Errors
--- Find
Select a mode and click Start!
`; this.createHoles(); } createHoles() { const gameBoard = document.getElementById('game-board'); gameBoard.innerHTML = ''; for (let i = 0; i < 15; i++) { // 5x3 = 15 trous const hole = document.createElement('div'); hole.className = 'whack-hole'; hole.dataset.holeId = i; hole.innerHTML = `
`; gameBoard.appendChild(hole); this.holes.push({ element: hole, mole: hole.querySelector('.whack-mole'), wordElement: hole.querySelector('.word'), isActive: false, word: null, timer: null }); } } createGameUI() { // Les éléments UI sont déjà créés dans createGameBoard } setupEventListeners() { // Mode selection document.querySelectorAll('.mode-btn').forEach(btn => { btn.addEventListener('click', (e) => { if (this.isRunning) return; document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); this.gameMode = btn.dataset.mode; if (this.gameMode !== 'translation') { this.showFeedback('This mode will be available soon!', 'info'); // Return to translation mode document.querySelector('.mode-btn[data-mode="translation"]').classList.add('active'); btn.classList.remove('active'); this.gameMode = 'translation'; } }); }); // Game controls document.getElementById('start-btn').addEventListener('click', () => this.start()); document.getElementById('pause-btn').addEventListener('click', () => this.pause()); document.getElementById('restart-btn').addEventListener('click', () => this.restart()); // Mole clicks this.holes.forEach((hole, index) => { hole.mole.addEventListener('click', () => this.hitMole(index)); }); } start() { if (this.isRunning) return; this.isRunning = true; this.score = 0; this.errors = 0; this.timeLeft = this.gameTime; this.updateUI(); this.setNewTarget(); this.startTimers(); document.getElementById('start-btn').disabled = true; document.getElementById('pause-btn').disabled = false; this.showFeedback(`Find the word: "${this.targetWord.french}"`, 'info'); // Show loaded content info const contentName = this.content.name || 'Content'; console.log(`🎮 Whack-a-Mole started with: ${contentName} (${this.vocabulary.length} words)`); } pause() { if (!this.isRunning) return; this.isRunning = false; this.stopTimers(); this.hideAllMoles(); document.getElementById('start-btn').disabled = false; document.getElementById('pause-btn').disabled = true; this.showFeedback('Game paused', 'info'); } restart() { this.stopWithoutEnd(); // Arrêter sans déclencher la fin de jeu this.resetGame(); setTimeout(() => this.start(), 100); } stop() { this.stopWithoutEnd(); this.onGameEnd(this.score); // Déclencher la fin de jeu seulement ici } stopWithoutEnd() { this.isRunning = false; this.stopTimers(); this.hideAllMoles(); document.getElementById('start-btn').disabled = false; document.getElementById('pause-btn').disabled = true; } resetGame() { // S'assurer que tout est complètement arrêté this.stopWithoutEnd(); // Reset de toutes les variables d'état this.score = 0; this.errors = 0; this.timeLeft = this.gameTime; this.isRunning = false; this.targetWord = null; this.activeMoles = []; this.spawnsSinceTarget = 0; // Reset du compteur de garantie // S'assurer que tous les timers sont bien arrêtés this.stopTimers(); // Reset UI this.updateUI(); this.onScoreUpdate(0); // Clear feedback document.getElementById('target-word').textContent = '---'; this.showFeedback('Select a mode and click Start!', 'info'); // Reset buttons document.getElementById('start-btn').disabled = false; document.getElementById('pause-btn').disabled = true; // Clear all holes avec vérification this.holes.forEach(hole => { if (hole.timer) { clearTimeout(hole.timer); hole.timer = null; } hole.isActive = false; hole.word = null; if (hole.wordElement) { hole.wordElement.textContent = ''; } if (hole.mole) { hole.mole.classList.remove('active', 'hit'); } }); console.log('🔄 Game completely reset'); } startTimers() { // Timer principal du jeu this.gameTimer = setInterval(() => { this.timeLeft--; this.updateUI(); if (this.timeLeft <= 0 && this.isRunning) { this.stop(); } }, 1000); // Timer d'apparition des taupes this.spawnTimer = setInterval(() => { if (this.isRunning) { this.spawnMole(); } }, this.spawnRate); // Première taupe immédiate setTimeout(() => this.spawnMole(), 500); } stopTimers() { if (this.gameTimer) { clearInterval(this.gameTimer); this.gameTimer = null; } if (this.spawnTimer) { clearInterval(this.spawnTimer); this.spawnTimer = null; } } spawnMole() { // Mode Hard: Spawn 3 taupes à la fois this.spawnMultipleMoles(); } spawnMultipleMoles() { // Trouver tous les trous libres const availableHoles = this.holes.filter(hole => !hole.isActive); // Spawn jusqu'à 3 taupes (ou moins si pas assez de trous libres) const molesToSpawn = Math.min(this.molesPerWave, availableHoles.length); if (molesToSpawn === 0) return; // Mélanger les trous disponibles const shuffledHoles = this.shuffleArray(availableHoles); // Spawn les taupes for (let i = 0; i < molesToSpawn; i++) { const hole = shuffledHoles[i]; const holeIndex = this.holes.indexOf(hole); // Choisir un mot selon la stratégie de garantie const word = this.getWordWithTargetGuarantee(); // Activer la taupe avec un petit délai pour un effet visuel setTimeout(() => { if (this.isRunning && !hole.isActive) { this.activateMole(holeIndex, word); } }, i * 200); // Délai de 200ms entre chaque taupe } } getWordWithTargetGuarantee() { // Incrémenter le compteur de spawns depuis le dernier mot cible this.spawnsSinceTarget++; // Si on a atteint la limite, forcer le mot cible if (this.spawnsSinceTarget >= this.maxSpawnsWithoutTarget) { console.log(`🎯 Spawn forcé du mot cible après ${this.spawnsSinceTarget} tentatives`); this.spawnsSinceTarget = 0; return this.targetWord; } // Sinon, 10% de chance d'avoir le mot cible (1/10 au lieu de 1/2) if (Math.random() < 0.1) { console.log('🎯 Spawn naturel du mot cible (1/10)'); this.spawnsSinceTarget = 0; return this.targetWord; } else { return this.getRandomWord(); } } activateMole(holeIndex, word) { const hole = this.holes[holeIndex]; if (hole.isActive) return; hole.isActive = true; hole.word = word; hole.wordElement.textContent = word.english; hole.mole.classList.add('active'); // Ajouter à la liste des taupes actives this.activeMoles.push(holeIndex); // Timer pour faire disparaître la taupe hole.timer = setTimeout(() => { this.deactivateMole(holeIndex); }, this.moleAppearTime); } deactivateMole(holeIndex) { const hole = this.holes[holeIndex]; if (!hole.isActive) return; hole.isActive = false; hole.word = null; hole.wordElement.textContent = ''; hole.mole.classList.remove('active'); if (hole.timer) { clearTimeout(hole.timer); hole.timer = null; } // Retirer de la liste des taupes actives const activeIndex = this.activeMoles.indexOf(holeIndex); if (activeIndex > -1) { this.activeMoles.splice(activeIndex, 1); } } hitMole(holeIndex) { if (!this.isRunning) return; const hole = this.holes[holeIndex]; if (!hole.isActive || !hole.word) return; const isCorrect = hole.word.french === this.targetWord.french; if (isCorrect) { // Bonne réponse this.score += 10; this.deactivateMole(holeIndex); this.setNewTarget(); this.showScorePopup(holeIndex, '+10', true); this.showFeedback(`Well done! Now find: "${this.targetWord.french}"`, 'success'); // Success animation hole.mole.classList.add('hit'); setTimeout(() => hole.mole.classList.remove('hit'), 500); } else { // Wrong answer this.errors++; this.score = Math.max(0, this.score - 2); this.showScorePopup(holeIndex, '-2', false); this.showFeedback(`Oops! "${hole.word.french}" ≠ "${this.targetWord.french}"`, 'error'); } this.updateUI(); this.onScoreUpdate(this.score); // Check game end by errors if (this.errors >= this.maxErrors) { this.showFeedback('Too many errors! Game over.', 'error'); setTimeout(() => { if (this.isRunning) { // Check if game is still running this.stop(); } }, 1500); } } setNewTarget() { // Choisir un nouveau mot cible const availableWords = this.vocabulary.filter(word => !this.activeMoles.some(moleIndex => this.holes[moleIndex].word && this.holes[moleIndex].word.english === word.english ) ); if (availableWords.length > 0) { this.targetWord = availableWords[Math.floor(Math.random() * availableWords.length)]; } else { this.targetWord = this.vocabulary[Math.floor(Math.random() * this.vocabulary.length)]; } // Reset du compteur pour le nouveau mot cible this.spawnsSinceTarget = 0; console.log(`🎯 Nouveau mot cible: ${this.targetWord.english} -> ${this.targetWord.french}`); document.getElementById('target-word').textContent = this.targetWord.french; } getRandomWord() { return this.vocabulary[Math.floor(Math.random() * this.vocabulary.length)]; } hideAllMoles() { this.holes.forEach((hole, index) => { if (hole.isActive) { this.deactivateMole(index); } }); this.activeMoles = []; } showScorePopup(holeIndex, scoreText, isPositive) { const hole = this.holes[holeIndex]; const popup = document.createElement('div'); popup.className = `score-popup ${isPositive ? 'correct-answer' : 'wrong-answer'}`; popup.textContent = scoreText; const rect = hole.element.getBoundingClientRect(); popup.style.left = rect.left + rect.width / 2 + 'px'; popup.style.top = rect.top + 'px'; document.body.appendChild(popup); setTimeout(() => { if (popup.parentNode) { popup.parentNode.removeChild(popup); } }, 1000); } showFeedback(message, type = 'info') { const feedbackArea = document.getElementById('feedback-area'); feedbackArea.innerHTML = `
${message}
`; } updateUI() { document.getElementById('time-left').textContent = this.timeLeft; document.getElementById('errors-count').textContent = this.errors; } extractVocabulary(content) { let vocabulary = []; console.log('🔍 Extraction vocabulaire depuis:', content?.name || 'contenu'); // Priorité 1: Utiliser le contenu brut du module (format simple) if (content.rawContent) { console.log('📦 Utilisation du contenu brut du module'); return this.extractVocabularyFromRaw(content.rawContent); } // Priorité 2: Format simple avec vocabulary object (nouveau format préféré) if (content.vocabulary && typeof content.vocabulary === 'object' && !Array.isArray(content.vocabulary)) { console.log('✨ Format simple détecté (vocabulary object)'); vocabulary = Object.entries(content.vocabulary).map(([english, translation]) => ({ english: english, french: translation.split(';')[0], // Prendre la première traduction si plusieurs chinese: translation, // Garder la traduction complète en chinois category: 'general' })); } // Priorité 3: Format legacy avec vocabulary array else if (content.vocabulary && Array.isArray(content.vocabulary)) { console.log('📚 Format legacy détecté (vocabulary array)'); vocabulary = content.vocabulary.filter(word => word.english && word.french); } // Priorité 4: Format moderne avec contentItems else if (content.contentItems && Array.isArray(content.contentItems)) { console.log('🆕 Format contentItems détecté'); vocabulary = content.contentItems .filter(item => item.type === 'vocabulary' && item.english && item.french) .map(item => ({ english: item.english, french: item.french, image: item.image || null, category: item.category || 'general' })); } return this.finalizeVocabulary(vocabulary); } extractVocabularyFromRaw(rawContent) { console.log('🔧 Extraction depuis contenu brut:', rawContent.name || 'Module'); let vocabulary = []; // Format simple avec vocabulary object (PRÉFÉRÉ) if (rawContent.vocabulary && typeof rawContent.vocabulary === 'object' && !Array.isArray(rawContent.vocabulary)) { vocabulary = Object.entries(rawContent.vocabulary).map(([english, translation]) => ({ english: english, french: translation.split(';')[0], // Première traduction pour le français chinese: translation, // Traduction complète en chinois category: 'general' })); console.log(`✨ ${vocabulary.length} mots extraits depuis vocabulary object (format simple)`); } // Format legacy (vocabulary array) else if (rawContent.vocabulary && Array.isArray(rawContent.vocabulary)) { vocabulary = rawContent.vocabulary.filter(word => word.english && word.french); console.log(`📚 ${vocabulary.length} mots extraits depuis vocabulary array`); } // Format contentItems (ancien format complexe) else if (rawContent.contentItems && Array.isArray(rawContent.contentItems)) { vocabulary = rawContent.contentItems .filter(item => item.type === 'vocabulary' && item.english && item.french) .map(item => ({ english: item.english, french: item.french, image: item.image || null, category: item.category || 'general' })); console.log(`📝 ${vocabulary.length} mots extraits depuis contentItems`); } // Fallback else { console.warn('⚠️ Format de contenu brut non reconnu'); } return this.finalizeVocabulary(vocabulary); } finalizeVocabulary(vocabulary) { // Validation et nettoyage vocabulary = vocabulary.filter(word => word && typeof word.english === 'string' && typeof word.french === 'string' && word.english.trim() !== '' && word.french.trim() !== '' ); if (vocabulary.length === 0) { console.error('❌ Aucun vocabulaire valide trouvé'); // Vocabulaire de démonstration en dernier recours vocabulary = [ { english: 'hello', french: 'bonjour', category: 'greetings' }, { english: 'goodbye', french: 'au revoir', category: 'greetings' }, { english: 'thank you', french: 'merci', category: 'greetings' }, { english: 'cat', french: 'chat', category: 'animals' }, { english: 'dog', french: 'chien', category: 'animals' } ]; console.warn('🚨 Utilisation du vocabulaire de démonstration'); } console.log(`✅ Whack-a-Mole: ${vocabulary.length} mots de vocabulaire finalisés`); return this.shuffleArray(vocabulary); } shuffleArray(array) { const shuffled = [...array]; for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } return shuffled; } destroy() { this.stop(); this.container.innerHTML = ''; } } // Enregistrement du module window.GameModules = window.GameModules || {}; window.GameModules.WhackAMoleHard = WhackAMoleHardGame;