// === 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 || (() => {}); // Game state this.score = 0; this.errors = 0; this.maxErrors = 3; this.gameTime = 60; // 60 seconds this.timeLeft = this.gameTime; this.isRunning = false; this.gameMode = 'translation'; // 'translation', 'image', 'sound' this.showPronunciation = false; // Track pronunciation display state // Mole configuration this.holes = []; this.activeMoles = []; this.moleAppearTime = 3000; // 3 seconds display time (longer) this.spawnRate = 2000; // New wave every 2 seconds this.molesPerWave = 3; // 3 moles per wave // Timers this.gameTimer = null; this.spawnTimer = null; // Vocabulary for this game - adapted for the new system this.vocabulary = this.extractVocabulary(this.content); this.currentWords = []; this.targetWord = null; // Target word guarantee system this.spawnsSinceTarget = 0; this.maxSpawnsWithoutTarget = 10; // Target word must appear in the next 10 moles (1/10 chance) this.init(); } init() { // Check that we have vocabulary if (!this.vocabulary || this.vocabulary.length === 0) { logSh('No vocabulary available for Whack-a-Mole', 'ERROR'); this.showInitError(); return; } this.createGameBoard(); this.createGameUI(); this.setupEventListeners(); } showInitError() { this.container.innerHTML = `

❌ Loading Error

This content does not contain vocabulary compatible with Whack-a-Mole.

The game requires words with their translations.

`; } 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 holes 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'), pronunciationElement: hole.querySelector('.pronunciation'), isActive: false, word: null, timer: null }); } } createGameUI() { // UI elements are already created in 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('pronunciation-btn').addEventListener('click', () => this.togglePronunciation()); 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.translation}"`, 'info'); // Show loaded content info const contentName = this.content.name || 'Content'; logSh(`🎮 Whack-a-Mole started with: ${contentName} (${this.vocabulary.length} words, 'INFO');`); } 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(); // Stop without triggering game end this.resetGame(); setTimeout(() => this.start(), 100); } togglePronunciation() { this.showPronunciation = !this.showPronunciation; const btn = document.getElementById('pronunciation-btn'); if (this.showPronunciation) { btn.textContent = '🔊 Pronunciation ON'; btn.classList.add('active'); } else { btn.textContent = '🔊 Pronunciation OFF'; btn.classList.remove('active'); } // Update currently visible moles this.updateMoleDisplay(); } updateMoleDisplay() { // Update pronunciation display for all active moles this.holes.forEach(hole => { if (hole.isActive && hole.word) { if (this.showPronunciation && hole.word.pronunciation) { hole.pronunciationElement.textContent = hole.word.pronunciation; hole.pronunciationElement.style.display = 'block'; } else { hole.pronunciationElement.style.display = 'none'; } } }); } stop() { this.stopWithoutEnd(); this.onGameEnd(this.score); // Trigger game end only here } stopWithoutEnd() { this.isRunning = false; this.stopTimers(); this.hideAllMoles(); document.getElementById('start-btn').disabled = false; document.getElementById('pause-btn').disabled = true; } resetGame() { // Ensure everything is completely stopped this.stopWithoutEnd(); // Reset all state variables this.score = 0; this.errors = 0; this.timeLeft = this.gameTime; this.isRunning = false; this.targetWord = null; this.activeMoles = []; this.spawnsSinceTarget = 0; // Reset guarantee counter // Ensure all timers are properly stopped 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 with verification 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.pronunciationElement) { hole.pronunciationElement.textContent = ''; hole.pronunciationElement.style.display = 'none'; } if (hole.mole) { hole.mole.classList.remove('active', 'hit'); } }); logSh('🔄 Game completely reset', 'INFO'); } startTimers() { // Main game timer this.gameTimer = setInterval(() => { this.timeLeft--; this.updateUI(); if (this.timeLeft <= 0 && this.isRunning) { this.stop(); } }, 1000); // Mole spawn timer this.spawnTimer = setInterval(() => { if (this.isRunning) { this.spawnMole(); } }, this.spawnRate); // First immediate mole setTimeout(() => this.spawnMole(), 500); } stopTimers() { if (this.gameTimer) { clearInterval(this.gameTimer); this.gameTimer = null; } if (this.spawnTimer) { clearInterval(this.spawnTimer); this.spawnTimer = null; } } spawnMole() { // Hard mode: Spawn 3 moles at once this.spawnMultipleMoles(); } spawnMultipleMoles() { // Find all free holes const availableHoles = this.holes.filter(hole => !hole.isActive); // Spawn up to 3 moles (or fewer if not enough free holes) const molesToSpawn = Math.min(this.molesPerWave, availableHoles.length); if (molesToSpawn === 0) return; // Shuffle available holes const shuffledHoles = this.shuffleArray(availableHoles); // Spawn the moles for (let i = 0; i < molesToSpawn; i++) { const hole = shuffledHoles[i]; const holeIndex = this.holes.indexOf(hole); // Choose a word according to guarantee strategy const word = this.getWordWithTargetGuarantee(); // Activate the mole with a small delay for visual effect setTimeout(() => { if (this.isRunning && !hole.isActive) { this.activateMole(holeIndex, word); } }, i * 200); // 200ms delay between each mole } } getWordWithTargetGuarantee() { // Increment spawn counter since last target word this.spawnsSinceTarget++; // If we've reached the limit, force the target word if (this.spawnsSinceTarget >= this.maxSpawnsWithoutTarget) { logSh(`🎯 Forced target word spawn after ${this.spawnsSinceTarget} attempts`, 'INFO'); this.spawnsSinceTarget = 0; return this.targetWord; } // Otherwise, 10% chance for target word (1/10 instead of 1/2) if (Math.random() < 0.1) { logSh('🎯 Natural target word spawn (1/10)', 'INFO'); 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.original; // Show pronunciation if enabled and available if (this.showPronunciation && word.pronunciation) { hole.pronunciationElement.textContent = word.pronunciation; hole.pronunciationElement.style.display = 'block'; } else { hole.pronunciationElement.style.display = 'none'; } hole.mole.classList.add('active'); // Add to active moles list this.activeMoles.push(holeIndex); // Timer to make the mole disappear 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.pronunciationElement.textContent = ''; hole.pronunciationElement.style.display = 'none'; hole.mole.classList.remove('active'); if (hole.timer) { clearTimeout(hole.timer); hole.timer = null; } // Remove from active moles list 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.translation === this.targetWord.translation; if (isCorrect) { // Correct answer this.score += 10; this.deactivateMole(holeIndex); this.setNewTarget(); this.showScorePopup(holeIndex, '+10', true); this.showFeedback(`Well done! Now find: "${this.targetWord.translation}"`, '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.translation}" ≠ "${this.targetWord.translation}"`, '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() { // Choose a new target word const availableWords = this.vocabulary.filter(word => !this.activeMoles.some(moleIndex => this.holes[moleIndex].word && this.holes[moleIndex].word.original === word.original ) ); 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 counter for new target word this.spawnsSinceTarget = 0; logSh(`🎯 New target word: ${this.targetWord.original} -> ${this.targetWord.translation}`, 'INFO'); document.getElementById('target-word').textContent = this.targetWord.translation; } 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 = []; logSh('🔍 Extracting vocabulary from:', content?.name || 'content', 'INFO'); // Priority 1: Use raw module content (simple format) if (content.rawContent) { logSh('📦 Using raw module content', 'INFO'); return this.extractVocabularyFromRaw(content.rawContent); } // Priority 2: Ultra-modular format (vocabulary object) - ONLY format supported if (content.vocabulary && typeof content.vocabulary === 'object' && !Array.isArray(content.vocabulary)) { logSh('✨ Ultra-modular format detected (vocabulary object)', 'INFO'); vocabulary = Object.entries(content.vocabulary).map(([word, data]) => { // Support ultra-modular format ONLY if (typeof data === 'object' && data.user_language) { return { original: word, // Clé = original_language translation: data.user_language.split(';')[0], // First translation fullTranslation: data.user_language, // Complete translation type: data.type || 'general', audio: data.audio, image: data.image, examples: data.examples, pronunciation: data.pronunciation, category: data.type || 'general' }; } // Legacy fallback - simple string (temporary, will be removed) else if (typeof data === 'string') { return { original: word, translation: data.split(';')[0], fullTranslation: data, type: 'general', category: 'general' }; } return null; }).filter(Boolean); } // No other formats supported - ultra-modular only return this.finalizeVocabulary(vocabulary); } extractVocabularyFromRaw(rawContent) { logSh('🔧 Extracting from raw content:', rawContent.name || 'Module', 'INFO'); let vocabulary = []; // Ultra-modular format (vocabulary object) - ONLY format supported if (rawContent.vocabulary && typeof rawContent.vocabulary === 'object' && !Array.isArray(rawContent.vocabulary)) { vocabulary = Object.entries(rawContent.vocabulary).map(([word, data]) => { // Support ultra-modular format ONLY if (typeof data === 'object' && data.user_language) { return { original: word, // Clé = original_language translation: data.user_language.split(';')[0], // First translation fullTranslation: data.user_language, // Complete translation type: data.type || 'general', audio: data.audio, image: data.image, examples: data.examples, pronunciation: data.pronunciation, category: data.type || 'general' }; } // Legacy fallback - simple string (temporary, will be removed) else if (typeof data === 'string') { return { original: word, translation: data.split(';')[0], fullTranslation: data, type: 'general', category: 'general' }; } return null; }).filter(Boolean); logSh(`✨ ${vocabulary.length} words extracted from ultra-modular vocabulary`, 'INFO'); } // No other formats supported - ultra-modular only else { logSh('⚠️ Content format not supported - ultra-modular format required', 'WARN'); } return this.finalizeVocabulary(vocabulary); } finalizeVocabulary(vocabulary) { // Validation and cleanup for ultra-modular format vocabulary = vocabulary.filter(word => word && typeof word.original === 'string' && typeof word.translation === 'string' && word.original.trim() !== '' && word.translation.trim() !== '' ); if (vocabulary.length === 0) { logSh('❌ No valid vocabulary found', 'ERROR'); // Demo vocabulary as last resort vocabulary = [ { original: 'hello', translation: 'bonjour', category: 'greetings' }, { original: 'goodbye', translation: 'au revoir', category: 'greetings' }, { original: 'thank you', translation: 'merci', category: 'greetings' }, { original: 'cat', translation: 'chat', category: 'animals' }, { original: 'dog', translation: 'chien', category: 'animals' } ]; logSh('🚨 Using demo vocabulary', 'WARN'); } logSh(`✅ Whack-a-Mole: ${vocabulary.length} vocabulary words finalized`, 'INFO'); 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 = ''; } } // Module registration window.GameModules = window.GameModules || {}; window.GameModules.WhackAMoleHard = WhackAMoleHardGame;