import Module from '../core/Module.js'; import ttsService from '../services/TTSService.js'; /** * WhackAMole - Classic whack-a-mole game with vocabulary learning * Players must hit moles showing the target word translation */ class WhackAMole extends Module { constructor(name, dependencies, config = {}) { super(name, ['eventBus']); // Validate dependencies if (!dependencies.eventBus || !dependencies.content) { throw new Error('WhackAMole requires eventBus and content dependencies'); } this._eventBus = dependencies.eventBus; this._content = dependencies.content; this._config = { container: null, gameTime: 60, // seconds maxErrors: 3, moleAppearTime: 2000, // ms spawnRate: 1500, // ms between spawns maxSpawnsWithoutTarget: 3, ...config }; // Game state this._score = 0; this._errors = 0; this._timeLeft = this._config.gameTime; this._isRunning = false; this._gameStartTime = null; this._showPronunciation = false; // Mole configuration this._holes = []; this._activeMoles = []; this._targetWord = null; this._spawnsSinceTarget = 0; // Timers this._gameTimer = null; this._spawnTimer = null; // Content this._vocabulary = null; Object.seal(this); } /** * Get game metadata * @returns {Object} Game metadata */ static getMetadata() { return { name: 'Whack A Mole', description: 'Classic whack-a-mole game with vocabulary learning and quick reflexes', difficulty: 'beginner', category: 'action', estimatedTime: 5, // minutes skills: ['vocabulary', 'reflexes', 'speed', 'recognition'] }; } /** * Calculate compatibility score with content * @param {Object} content - Content to check compatibility with * @returns {Object} Compatibility score and details */ static getCompatibilityScore(content) { const vocab = content?.vocabulary || {}; const vocabCount = Object.keys(vocab).length; if (vocabCount < 5) { return { score: 0, reason: `Insufficient vocabulary (${vocabCount}/5 required)`, requirements: ['vocabulary'], minWords: 5, details: 'Whack A Mole needs at least 5 vocabulary words for gameplay variety' }; } // Perfect score at 20+ words, partial score for 5-19 const score = Math.min(vocabCount / 20, 1); return { score, reason: `${vocabCount} vocabulary words available`, requirements: ['vocabulary'], minWords: 5, optimalWords: 20, details: `Can create engaging gameplay with ${vocabCount} vocabulary words` }; } async init() { this._validateNotDestroyed(); try { // Validate container if (!this._config.container) { throw new Error('Game container is required'); } // Extract and validate vocabulary this._vocabulary = this._extractVocabulary(); if (this._vocabulary.length < 5) { throw new Error(`Insufficient vocabulary: need 5, got ${this._vocabulary.length}`); } // Set up event listeners this._eventBus.on('game:pause', this._handlePause.bind(this), this.name); this._eventBus.on('game:resume', this._handleResume.bind(this), this.name); // Inject CSS this._injectCSS(); // Initialize game interface this._createGameInterface(); this._createHoles(); this._setupEventListeners(); // Emit game ready event this._eventBus.emit('game:ready', { gameId: 'whack-a-mole', instanceId: this.name, vocabulary: this._vocabulary.length }, this.name); this._setInitialized(); } catch (error) { this._showError(error.message); throw error; } } async destroy() { this._validateNotDestroyed(); // Stop timers this._stopTimers(); // Remove CSS this._removeCSS(); // Clean up event listeners if (this._config.container) { this._config.container.innerHTML = ''; } // Emit game end event this._eventBus.emit('game:ended', { gameId: 'whack-a-mole', instanceId: this.name, score: this._score, errors: this._errors, duration: this._gameStartTime ? Date.now() - this._gameStartTime : 0 }, this.name); this._setDestroyed(); } /** * Get current game state * @returns {Object} Current game state */ getGameState() { this._validateInitialized(); return { score: this._score, errors: this._errors, timeLeft: this._timeLeft, maxErrors: this._config.maxErrors, isRunning: this._isRunning, isComplete: this._timeLeft <= 0 || this._errors >= this._config.maxErrors, duration: this._gameStartTime ? Date.now() - this._gameStartTime : 0 }; } // Private methods _extractVocabulary() { const vocab = this._content?.vocabulary || {}; const vocabulary = []; for (const [word, data] of Object.entries(vocab)) { if (data.user_language || (typeof data === 'string')) { const translation = data.user_language || data; vocabulary.push({ original: word, translation: translation.split(';')[0], // First translation if multiple fullTranslation: translation, type: data.type || 'general', pronunciation: data.pronunciation }); } } return this._shuffleArray(vocabulary); } _injectCSS() { const cssId = `whack-a-mole-styles-${this.name}`; if (document.getElementById(cssId)) return; const style = document.createElement('style'); style.id = cssId; style.textContent = ` .whack-game-wrapper { padding: 6px; width: 100%; max-width: min(650px, 95vw); margin: 0 auto; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 8px; color: white; height: 75vh; max-height: 75vh; overflow: hidden; display: flex; flex-direction: column; box-sizing: border-box; } .whack-game-header { display: flex; justify-content: center; align-items: center; flex-wrap: wrap; gap: 4px; margin-bottom: 4px; padding: 4px; background: rgba(255, 255, 255, 0.1); border-radius: 6px; backdrop-filter: blur(10px); flex-shrink: 0; } .game-stats { display: flex; gap: 3px; align-items: center; flex-wrap: wrap; justify-content: center; } .stat-item { text-align: center; background: rgba(255, 255, 255, 0.1); padding: 2px 6px; border-radius: 4px; min-width: 45px; flex: 0 1 auto; } .stat-value { display: block; font-size: clamp(0.75rem, 1.3vh, 0.85rem); font-weight: bold; margin-bottom: 1px; line-height: 1.1; } .stat-label { font-size: clamp(0.5rem, 0.9vh, 0.6rem); opacity: 0.9; line-height: 1.1; } .target-display { background: rgba(255, 255, 255, 0.2); padding: 3px 8px; border-radius: 6px; text-align: center; border: 2px solid rgba(255, 255, 255, 0.3); min-width: 90px; max-width: 140px; flex-shrink: 1; } .target-label { font-size: clamp(0.5rem, 0.9vh, 0.6rem); opacity: 0.9; margin-bottom: 1px; line-height: 1.1; } .target-word { font-size: clamp(0.7rem, 1.2vh, 0.85rem); font-weight: bold; word-break: break-word; line-height: 1.2; } .game-controls { display: flex; gap: 3px; align-items: center; flex-wrap: wrap; justify-content: center; } .control-btn { padding: 3px 6px; border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 4px; background: rgba(255, 255, 255, 0.1); color: white; font-size: clamp(0.55rem, 1vh, 0.65rem); font-weight: 500; cursor: pointer; transition: all 0.3s ease; backdrop-filter: blur(5px); white-space: nowrap; min-width: fit-content; flex-shrink: 1; line-height: 1.2; } .control-btn:hover { background: rgba(255, 255, 255, 0.2); transform: translateY(-2px); } .control-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; } .control-btn.active { background: rgba(255, 255, 255, 0.3); border-color: rgba(255, 255, 255, 0.5); } .whack-game-board { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; margin: 0; padding: 6px; background: rgba(0, 0, 0, 0.2); border-radius: 8px; width: 100%; max-width: 300px; margin: 0 auto; box-sizing: border-box; align-self: center; } .whack-hole { position: relative; aspect-ratio: 1; background: radial-gradient(circle at center, #8b5cf6 0%, #7c3aed 100%); border-radius: 50%; border: 2px solid rgba(255, 255, 255, 0.3); overflow: hidden; cursor: pointer; transition: all 0.3s ease; width: 100%; height: auto; } .whack-hole:hover { transform: scale(1.05); box-shadow: 0 5px 20px rgba(139, 92, 246, 0.4); } .whack-mole { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0); background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); border-radius: 5px; padding: 3px 5px; color: white; text-align: center; font-weight: 600; font-size: clamp(0.55rem, 1.1vh, 0.7rem); line-height: 1.15; transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); cursor: pointer; max-width: 90%; word-wrap: break-word; overflow: hidden; } .whack-mole.active { transform: translate(-50%, -50%) scale(1); } .whack-mole.hit { background: linear-gradient(135deg, #10b981 0%, #059669 100%); animation: moleHit 0.5s ease-out; } .whack-mole:hover { transform: translate(-50%, -50%) scale(1.1); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); } .pronunciation { font-size: clamp(0.48rem, 0.9vh, 0.6rem); color: rgba(255, 255, 255, 0.8); font-style: italic; margin-bottom: 1px; font-weight: 400; line-height: 1.1; } .feedback-area { text-align: center; padding: 4px; background: rgba(255, 255, 255, 0.1); border-radius: 6px; margin-top: 4px; backdrop-filter: blur(10px); flex-shrink: 0; } .instruction { font-size: clamp(0.65rem, 1.1vh, 0.75rem); font-weight: 500; padding: 4px; border-radius: 4px; transition: all 0.3s ease; line-height: 1.2; } .instruction.info { background: rgba(59, 130, 246, 0.2); border: 1px solid rgba(59, 130, 246, 0.3); } .instruction.success { background: rgba(16, 185, 129, 0.2); border: 1px solid rgba(16, 185, 129, 0.3); animation: successPulse 0.6s ease-out; } .instruction.error { background: rgba(239, 68, 68, 0.2); border: 1px solid rgba(239, 68, 68, 0.3); animation: errorShake 0.6s ease-out; } .score-popup { position: fixed; font-size: 1.2rem; font-weight: bold; pointer-events: none; z-index: 1000; animation: scoreFloat 1s ease-out forwards; } .score-popup.correct-answer { color: #10b981; text-shadow: 0 2px 4px rgba(16, 185, 129, 0.5); } .score-popup.wrong-answer { color: #ef4444; text-shadow: 0 2px 4px rgba(239, 68, 68, 0.5); } .game-over-screen { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.9); display: flex; align-items: center; justify-content: center; border-radius: 10px; z-index: 1000; } .game-over-content { background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); color: white; padding: 20px; border-radius: 10px; text-align: center; max-width: 350px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); } .game-over-content h2 { margin: 0 0 15px 0; font-size: 1.8rem; } .final-stats { margin: 15px 0; padding: 15px; background: rgba(255, 255, 255, 0.1); border-radius: 8px; backdrop-filter: blur(10px); } .stat-row { display: flex; justify-content: space-between; margin: 8px 0; font-size: 0.9rem; } .game-over-btn { margin: 8px; padding: 8px 16px; border: 2px solid white; border-radius: 6px; background: transparent; color: white; font-size: 0.85rem; font-weight: 600; cursor: pointer; transition: all 0.3s ease; } .game-over-btn:hover { background: white; color: #ef4444; transform: translateY(-2px); } .game-error { text-align: center; padding: 20px; background: linear-gradient(135deg, #f87171 0%, #ef4444 100%); color: white; border-radius: 10px; height: 90vh; display: flex; flex-direction: column; align-items: center; justify-content: center; } .game-error h3 { font-size: 1.5rem; margin-bottom: 15px; } .back-btn { padding: 8px 16px; background: white; color: #ef4444; border: none; border-radius: 6px; font-size: 0.85rem; font-weight: 600; cursor: pointer; transition: all 0.3s ease; margin-top: 15px; } .back-btn:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(255, 255, 255, 0.3); } /* Animations */ @keyframes moleHit { 0% { transform: translate(-50%, -50%) scale(1); } 50% { transform: translate(-50%, -50%) scale(1.2); } 100% { transform: translate(-50%, -50%) scale(1); } } @keyframes successPulse { 0% { transform: scale(1); } 50% { transform: scale(1.05); } 100% { transform: scale(1); } } @keyframes errorShake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-5px); } 75% { transform: translateX(5px); } } @keyframes scoreFloat { 0% { transform: translateY(0) scale(1); opacity: 1; } 50% { transform: translateY(-30px) scale(1.2); opacity: 1; } 100% { transform: translateY(-60px) scale(0.8); opacity: 0; } } /* Ensure Exit button uses control-btn styles */ #exit-whack { padding: 3px 6px !important; border: 1px solid rgba(255, 255, 255, 0.3) !important; border-radius: 4px; background: rgba(255, 255, 255, 0.1) !important; color: white !important; font-size: clamp(0.55rem, 1vh, 0.65rem) !important; font-weight: 500; cursor: pointer; transition: all 0.3s ease; backdrop-filter: blur(5px); white-space: nowrap; min-width: fit-content; flex-shrink: 1; line-height: 1.2; } #exit-whack:hover { background: rgba(255, 255, 255, 0.2) !important; transform: translateY(-2px); } #exit-whack .btn-icon, #exit-whack .btn-text { font-size: clamp(0.55rem, 1vh, 0.65rem); } /* Media queries pour ajustements mineurs sur très petits écrans */ @media (max-width: 400px) { .whack-game-wrapper { border-radius: 6px; } .stat-item { min-width: 45px; } .target-display { min-width: 90px; max-width: 120px; } } /* Ajustements pour écrans très larges */ @media (min-width: 1200px) { .whack-game-wrapper { max-width: 650px; } } `; document.head.appendChild(style); } _removeCSS() { const cssId = `whack-a-mole-styles-${this.name}`; const existingStyle = document.getElementById(cssId); if (existingStyle) { existingStyle.remove(); } } _createGameInterface() { this._config.container.innerHTML = `
${this._config.gameTime} Time
0 Errors
0 Score
Find the word:
---
Click Start to begin the game!
`; } _createHoles() { const gameBoard = document.getElementById('game-board'); gameBoard.innerHTML = ''; this._holes = []; for (let i = 0; i < 9; i++) { 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 }); } } _setupEventListeners() { // Control buttons document.getElementById('pronunciation-btn').addEventListener('click', () => this._togglePronunciation()); document.getElementById('start-btn').addEventListener('click', () => this._startGame()); document.getElementById('pause-btn').addEventListener('click', () => this._pauseGame()); document.getElementById('restart-btn').addEventListener('click', () => this._restartGame()); // Exit button const exitButton = document.getElementById('exit-whack'); if (exitButton) { exitButton.addEventListener('click', () => { this._eventBus.emit('game:exit-request', { instanceId: this.name }, this.name); }); } // Mole clicks - click on hole OR mole text this._holes.forEach((hole, index) => { // Click on the entire hole (easier to target) hole.element.addEventListener('click', () => this._hitMole(index)); }); } _startGame() { if (this._isRunning) return; this._isRunning = true; this._score = 0; this._errors = 0; this._timeLeft = this._config.gameTime; this._gameStartTime = Date.now(); this._spawnsSinceTarget = 0; 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'); // Emit game start event this._eventBus.emit('whack-a-mole:game-started', { gameId: 'whack-a-mole', instanceId: this.name, vocabulary: this._vocabulary.length }, this.name); } _pauseGame() { 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'); } _restartGame() { this._stopGameWithoutEnd(); this._resetGame(); setTimeout(() => this._startGame(), 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'); } this._updateMoleDisplay(); } _updateMoleDisplay() { 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'; } } }); } _startTimers() { // Main game timer this._gameTimer = setInterval(() => { this._timeLeft--; this._updateUI(); if (this._timeLeft <= 0 && this._isRunning) { this._endGame(); } }, 1000); // Mole spawn timer this._spawnTimer = setInterval(() => { if (this._isRunning) { this._spawnMole(); } }, this._config.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() { // Find a free hole const availableHoles = this._holes.filter(hole => !hole.isActive); if (availableHoles.length === 0) return; const randomHole = availableHoles[Math.floor(Math.random() * availableHoles.length)]; const holeIndex = this._holes.indexOf(randomHole); // Choose a word according to guarantee strategy const word = this._getWordWithTargetGuarantee(); // Activate the mole this._activateMole(holeIndex, word); } _getWordWithTargetGuarantee() { this._spawnsSinceTarget++; // If we've reached the limit, force the target word if (this._spawnsSinceTarget >= this._config.maxSpawnsWithoutTarget) { this._spawnsSinceTarget = 0; return this._targetWord; } // Otherwise, 50% chance for target word, 50% random word if (Math.random() < 0.5) { 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'); this._activeMoles.push(holeIndex); // Timer to make the mole disappear hole.timer = setTimeout(() => { this._deactivateMole(holeIndex); }, this._config.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; // Speak the word (pronounce it) this._speakWord(hole.word.original); 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); // Emit correct hit event this._eventBus.emit('whack-a-mole:correct-hit', { gameId: 'whack-a-mole', instanceId: this.name, word: hole.word, score: this._score }, this.name); } 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'); // Emit wrong hit event this._eventBus.emit('whack-a-mole:wrong-hit', { gameId: 'whack-a-mole', instanceId: this.name, word: hole.word, targetWord: this._targetWord, score: this._score, errors: this._errors }, this.name); } this._updateUI(); // Check game end by errors if (this._errors >= this._config.maxErrors) { this._showFeedback('Too many errors! Game over.', 'error'); setTimeout(() => { if (this._isRunning) { this._endGame(); } }, 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)]; } this._spawnsSinceTarget = 0; 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; document.getElementById('score-display').textContent = this._score; } _endGame() { this._stopGameWithoutEnd(); this._showGameOverScreen(); // Emit game completion event this._eventBus.emit('game:completed', { gameId: 'whack-a-mole', instanceId: this.name, score: this._score, errors: this._errors, timeLeft: this._timeLeft, duration: this._gameStartTime ? Date.now() - this._gameStartTime : 0 }, this.name); } _stopGameWithoutEnd() { this._isRunning = false; this._stopTimers(); this._hideAllMoles(); document.getElementById('start-btn').disabled = false; document.getElementById('pause-btn').disabled = true; } _resetGame() { this._stopGameWithoutEnd(); this._score = 0; this._errors = 0; this._timeLeft = this._config.gameTime; this._isRunning = false; this._targetWord = null; this._activeMoles = []; this._spawnsSinceTarget = 0; this._stopTimers(); this._updateUI(); document.getElementById('target-word').textContent = '---'; this._showFeedback('Click Start to begin the game!', 'info'); document.getElementById('start-btn').disabled = false; document.getElementById('pause-btn').disabled = true; // Clear all holes 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'); } }); } _showGameOverScreen() { const duration = this._gameStartTime ? Math.round((Date.now() - this._gameStartTime) / 1000) : 0; const accuracy = this._errors > 0 ? Math.round((this._score / (this._score + this._errors * 2)) * 100) : 100; // Store best score const gameKey = 'whack-a-mole'; const currentScore = this._score; const bestScore = parseInt(localStorage.getItem(`${gameKey}-best-score`) || '0'); const isNewBest = currentScore > bestScore; if (isNewBest) { localStorage.setItem(`${gameKey}-best-score`, currentScore.toString()); } // Show victory popup this._showVictoryPopup({ gameTitle: 'Whack-A-Mole', currentScore, bestScore: isNewBest ? currentScore : bestScore, isNewBest, stats: { 'Accuracy': `${accuracy}%`, 'Errors': `${this._errors}/${this._config.maxErrors}`, 'Duration': `${duration}s`, 'Hits': this._score } }); } _showError(message) { if (this._config.container) { this._config.container.innerHTML = `

❌ Whack A Mole Error

${message}

This game requires vocabulary with translations.

`; } } async _speakWord(word) { const targetLanguage = this._content?.language || 'en-US'; await ttsService.speak(word, targetLanguage, { rate: 0.9, volume: 1.0 }); } _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; } _handlePause() { if (this._isRunning) { this._pauseGame(); } this._eventBus.emit('game:paused', { instanceId: this.name }, this.name); } _handleResume() { if (!this._isRunning) { this._startGame(); } this._eventBus.emit('game:resumed', { instanceId: this.name }, this.name); } _showVictoryPopup({ gameTitle, currentScore, bestScore, isNewBest, stats }) { const popup = document.createElement('div'); popup.className = 'victory-popup'; popup.innerHTML = `
🎯

${gameTitle} Complete!

${isNewBest ? '
🎉 New Best Score!
' : ''}
Your Score
${currentScore}
Best Score
${bestScore}
${Object.entries(stats).map(([key, value]) => `
${key}
${value}
`).join('')}
`; document.body.appendChild(popup); // Animate in requestAnimationFrame(() => { popup.classList.add('show'); }); // Add event listeners popup.querySelector('#play-again-btn').addEventListener('click', () => { popup.remove(); this._restartGame(); }); popup.querySelector('#different-game-btn').addEventListener('click', () => { popup.remove(); if (window.app && window.app.getCore().router) { window.app.getCore().router.navigate('/games'); } else { window.location.href = '/#/games'; } }); popup.querySelector('#main-menu-btn').addEventListener('click', () => { popup.remove(); if (window.app && window.app.getCore().router) { window.app.getCore().router.navigate('/'); } else { window.location.href = '/'; } }); // Close on backdrop click popup.addEventListener('click', (e) => { if (e.target === popup) { popup.remove(); if (window.app && window.app.getCore().router) { window.app.getCore().router.navigate('/games'); } else { window.location.href = '/#/games'; } } }); } } export default WhackAMole;