Major improvements: - Add TTSHelper utility for text-to-speech functionality - Enhance content compatibility scoring across all games - Improve sentence extraction from multiple content sources - Update all game modules to support diverse content formats - Refine MarioEducational physics and rendering - Polish UI styles and remove unused CSS Games updated: AdventureReader, FillTheBlank, FlashcardLearning, GrammarDiscovery, MarioEducational, QuizGame, RiverRun, WhackAMole, WhackAMoleHard, WizardSpellCaster, WordDiscovery, WordStorm 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1559 lines
50 KiB
JavaScript
1559 lines
50 KiB
JavaScript
import Module from '../core/Module.js';
|
||
|
||
/**
|
||
* WhackAMoleHard - Advanced version with multiple moles per wave
|
||
* Hard mode features:
|
||
* - 5x3 grid (15 holes)
|
||
* - 3 moles per wave instead of 1
|
||
* - Faster spawn rate
|
||
* - Shorter display time
|
||
* - Target word guarantee system (appears within 10 spawns)
|
||
*/
|
||
class WhackAMoleHard extends Module {
|
||
constructor(name, dependencies, config = {}) {
|
||
super(name || 'whack-a-mole-hard', ['eventBus']);
|
||
|
||
// Validate dependencies
|
||
if (!dependencies.eventBus) {
|
||
throw new Error('WhackAMoleHard requires EventBus dependency');
|
||
}
|
||
|
||
// Ensure name is always defined
|
||
if (!this.name) {
|
||
this.name = 'whack-a-mole-hard';
|
||
}
|
||
|
||
this._eventBus = dependencies.eventBus;
|
||
this._content = dependencies.content;
|
||
this._config = {
|
||
container: null,
|
||
...config
|
||
};
|
||
|
||
// 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';
|
||
this._showPronunciation = false;
|
||
|
||
// Mole configuration (HARD MODE)
|
||
this._holes = [];
|
||
this._activeMoles = [];
|
||
this._moleAppearTime = 3000; // 3 seconds display time (longer for hard mode)
|
||
this._spawnRate = 2000; // New wave every 2 seconds
|
||
this._molesPerWave = 3; // 3 moles per wave (HARD MODE)
|
||
|
||
// Timers
|
||
this._gameTimer = null;
|
||
this._spawnTimer = null;
|
||
|
||
// Vocabulary and game content
|
||
this._vocabulary = [];
|
||
this._currentWords = [];
|
||
this._targetWord = null;
|
||
|
||
// Target word guarantee system
|
||
this._spawnsSinceTarget = 0;
|
||
this._maxSpawnsWithoutTarget = 10; // Target word must appear in the next 10 moles
|
||
|
||
// DOM references
|
||
this._container = null;
|
||
this._gameBoard = null;
|
||
this._feedbackArea = null;
|
||
|
||
// CSS injection
|
||
this._cssInjected = false;
|
||
|
||
Object.seal(this);
|
||
}
|
||
|
||
async init() {
|
||
this._validateNotDestroyed();
|
||
|
||
// Validate container
|
||
if (!this._config.container) {
|
||
throw new Error('Game container is required');
|
||
}
|
||
|
||
// Set up event listeners
|
||
this._eventBus.on('whack-hard:start', this._handleStart.bind(this), this.name);
|
||
this._eventBus.on('whack-hard:pause', this._handlePause.bind(this), this.name);
|
||
this._eventBus.on('whack-hard:restart', this._handleRestart.bind(this), this.name);
|
||
this._eventBus.on('whack-hard:toggle-pronunciation', this._handleTogglePronunciation.bind(this), this.name);
|
||
|
||
// Start game immediately
|
||
try {
|
||
this._container = this._config.container;
|
||
const content = this._content;
|
||
|
||
// Extract vocabulary from content
|
||
this._vocabulary = this._extractVocabulary(content);
|
||
|
||
if (this._vocabulary.length === 0) {
|
||
this._showInitError();
|
||
return;
|
||
}
|
||
|
||
// Inject CSS
|
||
this._injectCSS();
|
||
|
||
// Create game interface
|
||
this._createGameBoard();
|
||
this._setupEventListeners();
|
||
|
||
// Emit game ready event
|
||
this._eventBus.emit('game:ready', {
|
||
gameId: 'whack-a-mole-hard',
|
||
instanceId: this.name,
|
||
vocabulary: this._vocabulary.length
|
||
}, this.name);
|
||
|
||
} catch (error) {
|
||
console.error('Error starting Whack A Mole Hard:', error);
|
||
this._showInitError();
|
||
}
|
||
|
||
this._setInitialized();
|
||
}
|
||
|
||
async destroy() {
|
||
this._validateNotDestroyed();
|
||
|
||
// Stop game and cleanup
|
||
this._stopGame();
|
||
|
||
// Remove injected CSS
|
||
this._removeCSS();
|
||
|
||
// Clear DOM
|
||
if (this._container) {
|
||
this._container.innerHTML = '';
|
||
}
|
||
|
||
this._setDestroyed();
|
||
}
|
||
|
||
// Public interface methods
|
||
render(container, content) {
|
||
this._validateInitialized();
|
||
|
||
this._container = container;
|
||
this._content = content;
|
||
|
||
// Extract vocabulary from content
|
||
this._vocabulary = this._extractVocabulary(content);
|
||
|
||
if (this._vocabulary.length === 0) {
|
||
this._showInitError();
|
||
return;
|
||
}
|
||
|
||
// Inject CSS
|
||
this._injectCSS();
|
||
|
||
// Create game interface
|
||
this._createGameBoard();
|
||
this._setupEventListeners();
|
||
}
|
||
|
||
startGame() {
|
||
this._validateInitialized();
|
||
this._start();
|
||
}
|
||
|
||
pauseGame() {
|
||
this._validateInitialized();
|
||
this._pause();
|
||
}
|
||
|
||
restartGame() {
|
||
this._validateInitialized();
|
||
this._restart();
|
||
}
|
||
|
||
// Private implementation methods
|
||
_injectCSS() {
|
||
if (this._cssInjected) return;
|
||
|
||
const styleSheet = document.createElement('style');
|
||
styleSheet.id = 'whack-hard-styles';
|
||
styleSheet.textContent = `
|
||
.whack-game-wrapper {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 20px;
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||
}
|
||
|
||
.mode-selector {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.mode-btn {
|
||
padding: 10px 20px;
|
||
border: 2px solid #e5e7eb;
|
||
border-radius: 8px;
|
||
background: white;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.mode-btn:hover {
|
||
border-color: #3b82f6;
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.mode-btn.active {
|
||
background: #3b82f6;
|
||
color: white;
|
||
border-color: #3b82f6;
|
||
}
|
||
|
||
.game-info {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
width: 100%;
|
||
max-width: 800px;
|
||
margin-bottom: 30px;
|
||
padding: 20px;
|
||
background: #f8fafc;
|
||
border-radius: 12px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.game-stats {
|
||
display: flex;
|
||
gap: 30px;
|
||
}
|
||
|
||
.stat-item {
|
||
text-align: center;
|
||
}
|
||
|
||
.stat-value {
|
||
display: block;
|
||
font-size: 24px;
|
||
font-weight: bold;
|
||
color: #1f2937;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 12px;
|
||
color: #6b7280;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
.game-controls {
|
||
display: flex;
|
||
gap: 10px;
|
||
}
|
||
|
||
.control-btn {
|
||
padding: 8px 16px;
|
||
border: none;
|
||
border-radius: 6px;
|
||
background: #3b82f6;
|
||
color: white;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.control-btn:hover:not(:disabled) {
|
||
background: #2563eb;
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.control-btn:disabled {
|
||
background: #9ca3af;
|
||
cursor: not-allowed;
|
||
transform: none;
|
||
}
|
||
|
||
.control-btn.active {
|
||
background: #10b981;
|
||
}
|
||
|
||
.whack-game-board.hard-mode {
|
||
display: grid;
|
||
grid-template-columns: repeat(5, 1fr);
|
||
grid-template-rows: repeat(3, 1fr);
|
||
gap: 15px;
|
||
padding: 30px;
|
||
background: #1f2937;
|
||
border-radius: 20px;
|
||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||
margin-bottom: 30px;
|
||
width: 100%;
|
||
max-width: 800px;
|
||
aspect-ratio: 5/3;
|
||
}
|
||
|
||
.whack-hole {
|
||
position: relative;
|
||
background: #374151;
|
||
border-radius: 50%;
|
||
box-shadow: inset 0 4px 8px rgba(0, 0, 0, 0.3);
|
||
cursor: pointer;
|
||
overflow: hidden;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.whack-hole:hover {
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
.whack-mole {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
background: linear-gradient(45deg, #3b82f6, #1d4ed8);
|
||
color: white;
|
||
padding: 8px 12px;
|
||
border-radius: 12px;
|
||
font-size: 14px;
|
||
font-weight: bold;
|
||
text-align: center;
|
||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
|
||
opacity: 0;
|
||
transform: translate(-50%, -50%) scale(0);
|
||
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||
cursor: pointer;
|
||
max-width: 90%;
|
||
word-wrap: break-word;
|
||
}
|
||
|
||
.whack-mole.active {
|
||
opacity: 1;
|
||
transform: translate(-50%, -50%) scale(1);
|
||
}
|
||
|
||
.whack-mole.hit {
|
||
background: linear-gradient(45deg, #10b981, #059669);
|
||
animation: hitAnimation 0.5s ease;
|
||
}
|
||
|
||
@keyframes hitAnimation {
|
||
0% { transform: translate(-50%, -50%) scale(1); }
|
||
50% { transform: translate(-50%, -50%) scale(1.2); }
|
||
100% { transform: translate(-50%, -50%) scale(1); }
|
||
}
|
||
|
||
.whack-mole .pronunciation {
|
||
font-size: 0.8em;
|
||
color: #93c5fd;
|
||
font-style: italic;
|
||
margin-bottom: 5px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.whack-mole .word {
|
||
font-size: 1em;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.feedback-area {
|
||
width: 100%;
|
||
max-width: 800px;
|
||
min-height: 60px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 20px;
|
||
border-radius: 12px;
|
||
background: #f8fafc;
|
||
border: 2px solid #e5e7eb;
|
||
}
|
||
|
||
.instruction {
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
text-align: center;
|
||
color: #374151;
|
||
}
|
||
|
||
.instruction.info {
|
||
color: #3b82f6;
|
||
border-color: #3b82f6;
|
||
}
|
||
|
||
.instruction.success {
|
||
color: #10b981;
|
||
background: #ecfdf5;
|
||
border-color: #10b981;
|
||
}
|
||
|
||
.instruction.error {
|
||
color: #ef4444;
|
||
background: #fef2f2;
|
||
border-color: #ef4444;
|
||
}
|
||
|
||
.score-popup {
|
||
position: fixed;
|
||
pointer-events: none;
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
padding: 8px 16px;
|
||
border-radius: 8px;
|
||
color: white;
|
||
z-index: 1000;
|
||
animation: scorePopup 1s ease-out forwards;
|
||
}
|
||
|
||
.score-popup.correct-answer {
|
||
background: #10b981;
|
||
}
|
||
|
||
.score-popup.wrong-answer {
|
||
background: #ef4444;
|
||
}
|
||
|
||
@keyframes scorePopup {
|
||
0% {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
100% {
|
||
opacity: 0;
|
||
transform: translateY(-50px);
|
||
}
|
||
}
|
||
|
||
.game-error {
|
||
text-align: center;
|
||
padding: 40px;
|
||
background: #fef2f2;
|
||
border: 2px solid #ef4444;
|
||
border-radius: 12px;
|
||
color: #dc2626;
|
||
}
|
||
|
||
.game-error h3 {
|
||
margin: 0 0 16px 0;
|
||
font-size: 20px;
|
||
}
|
||
|
||
.game-error p {
|
||
margin: 8px 0;
|
||
color: #7f1d1d;
|
||
}
|
||
|
||
.back-btn {
|
||
padding: 12px 24px;
|
||
background: #3b82f6;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 8px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.back-btn:hover {
|
||
background: #2563eb;
|
||
}
|
||
|
||
/* Responsive design */
|
||
@media (max-width: 768px) {
|
||
.whack-game-board.hard-mode {
|
||
max-width: 95%;
|
||
gap: 10px;
|
||
padding: 20px;
|
||
}
|
||
|
||
.game-info {
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
text-align: center;
|
||
}
|
||
|
||
.game-controls {
|
||
justify-content: center;
|
||
}
|
||
|
||
.whack-mole {
|
||
font-size: 12px;
|
||
padding: 6px 10px;
|
||
}
|
||
}
|
||
|
||
/* Victory Popup Styles */
|
||
.victory-popup {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.8);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 10000;
|
||
animation: fadeIn 0.3s ease-out;
|
||
}
|
||
|
||
.victory-content {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
border-radius: 20px;
|
||
padding: 40px;
|
||
text-align: center;
|
||
color: white;
|
||
max-width: 500px;
|
||
width: 90%;
|
||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||
animation: slideUp 0.4s ease-out;
|
||
}
|
||
|
||
.victory-header {
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.victory-icon {
|
||
font-size: 4rem;
|
||
margin-bottom: 15px;
|
||
animation: bounce 0.6s ease-out;
|
||
}
|
||
|
||
.victory-title {
|
||
font-size: 2rem;
|
||
font-weight: bold;
|
||
margin: 0 0 10px 0;
|
||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.new-best-badge {
|
||
background: linear-gradient(45deg, #f093fb 0%, #f5576c 100%);
|
||
color: white;
|
||
padding: 8px 20px;
|
||
border-radius: 25px;
|
||
font-size: 0.9rem;
|
||
font-weight: bold;
|
||
display: inline-block;
|
||
margin-top: 10px;
|
||
animation: glow 1s ease-in-out infinite alternate;
|
||
}
|
||
|
||
.victory-scores {
|
||
display: flex;
|
||
justify-content: space-around;
|
||
margin: 30px 0;
|
||
gap: 20px;
|
||
}
|
||
|
||
.score-display {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border-radius: 15px;
|
||
padding: 20px;
|
||
flex: 1;
|
||
backdrop-filter: blur(10px);
|
||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||
}
|
||
|
||
.score-label {
|
||
font-size: 0.9rem;
|
||
opacity: 0.9;
|
||
margin-bottom: 8px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 1px;
|
||
}
|
||
|
||
.score-value {
|
||
font-size: 2rem;
|
||
font-weight: bold;
|
||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.victory-stats {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border-radius: 15px;
|
||
padding: 20px;
|
||
margin: 30px 0;
|
||
backdrop-filter: blur(10px);
|
||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||
}
|
||
|
||
.stat-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 8px 0;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.stat-row:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.stat-name {
|
||
font-size: 0.95rem;
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.stat-value {
|
||
font-weight: bold;
|
||
font-size: 1rem;
|
||
}
|
||
|
||
.victory-buttons {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
margin-top: 30px;
|
||
}
|
||
|
||
.victory-btn {
|
||
padding: 15px 30px;
|
||
border: none;
|
||
border-radius: 25px;
|
||
font-size: 1rem;
|
||
font-weight: bold;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
text-transform: uppercase;
|
||
letter-spacing: 1px;
|
||
}
|
||
|
||
.victory-btn.primary {
|
||
background: linear-gradient(45deg, #4facfe 0%, #00f2fe 100%);
|
||
color: white;
|
||
box-shadow: 0 8px 25px rgba(79, 172, 254, 0.3);
|
||
}
|
||
|
||
.victory-btn.primary:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 12px 35px rgba(79, 172, 254, 0.4);
|
||
}
|
||
|
||
.victory-btn.secondary {
|
||
background: linear-gradient(45deg, #a8edea 0%, #fed6e3 100%);
|
||
color: #333;
|
||
box-shadow: 0 8px 25px rgba(168, 237, 234, 0.3);
|
||
}
|
||
|
||
.victory-btn.secondary:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 12px 35px rgba(168, 237, 234, 0.4);
|
||
}
|
||
|
||
.victory-btn.tertiary {
|
||
background: rgba(255, 255, 255, 0.2);
|
||
color: white;
|
||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||
backdrop-filter: blur(10px);
|
||
}
|
||
|
||
.victory-btn.tertiary:hover {
|
||
background: rgba(255, 255, 255, 0.3);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
@keyframes fadeIn {
|
||
from { opacity: 0; }
|
||
to { opacity: 1; }
|
||
}
|
||
|
||
@keyframes slideUp {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(30px) scale(0.9);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0) scale(1);
|
||
}
|
||
}
|
||
|
||
@keyframes bounce {
|
||
0%, 20%, 50%, 80%, 100% {
|
||
transform: translateY(0);
|
||
}
|
||
40% {
|
||
transform: translateY(-10px);
|
||
}
|
||
60% {
|
||
transform: translateY(-5px);
|
||
}
|
||
}
|
||
|
||
@keyframes glow {
|
||
from {
|
||
box-shadow: 0 0 20px rgba(245, 87, 108, 0.5);
|
||
}
|
||
to {
|
||
box-shadow: 0 0 30px rgba(245, 87, 108, 0.8);
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.victory-content {
|
||
padding: 30px 20px;
|
||
width: 95%;
|
||
}
|
||
|
||
.victory-scores {
|
||
flex-direction: column;
|
||
gap: 15px;
|
||
}
|
||
|
||
.victory-icon {
|
||
font-size: 3rem;
|
||
}
|
||
|
||
.victory-title {
|
||
font-size: 1.5rem;
|
||
}
|
||
|
||
.victory-buttons {
|
||
gap: 10px;
|
||
}
|
||
|
||
.victory-btn {
|
||
padding: 12px 25px;
|
||
font-size: 0.9rem;
|
||
}
|
||
}
|
||
`;
|
||
document.head.appendChild(styleSheet);
|
||
this._cssInjected = true;
|
||
}
|
||
|
||
_removeCSS() {
|
||
const styleSheet = document.getElementById('whack-hard-styles');
|
||
if (styleSheet) {
|
||
styleSheet.remove();
|
||
this._cssInjected = false;
|
||
}
|
||
}
|
||
|
||
_createGameBoard() {
|
||
this._container.innerHTML = `
|
||
<div class="whack-game-wrapper">
|
||
<!-- Mode Selection -->
|
||
<div class="mode-selector">
|
||
<button class="mode-btn active" data-mode="translation">
|
||
🔤 Translation
|
||
</button>
|
||
<button class="mode-btn" data-mode="image">
|
||
🖼️ Image (soon)
|
||
</button>
|
||
<button class="mode-btn" data-mode="sound">
|
||
🔊 Sound (soon)
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Game Info -->
|
||
<div class="game-info">
|
||
<div class="game-stats">
|
||
<div class="stat-item">
|
||
<span class="stat-value" id="time-left">${this._timeLeft}</span>
|
||
<span class="stat-label">Time</span>
|
||
</div>
|
||
<div class="stat-item">
|
||
<span class="stat-value" id="errors-count">${this._errors}</span>
|
||
<span class="stat-label">Errors</span>
|
||
</div>
|
||
<div class="stat-item">
|
||
<span class="stat-value" id="target-word">---</span>
|
||
<span class="stat-label">Find</span>
|
||
</div>
|
||
</div>
|
||
<div class="game-controls">
|
||
<button class="control-btn" id="pronunciation-btn" title="Toggle pronunciation">🔊 Pronunciation</button>
|
||
<button class="control-btn" id="start-btn">🎮 Start</button>
|
||
<button class="control-btn" id="pause-btn" disabled>⏸️ Pause</button>
|
||
<button class="control-btn" id="restart-btn">🔄 Restart</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Game Board -->
|
||
<div class="whack-game-board hard-mode" id="game-board">
|
||
<!-- 15 holes will be generated here (5x3 grid) -->
|
||
</div>
|
||
|
||
<!-- Feedback Area -->
|
||
<div class="feedback-area" id="feedback-area">
|
||
<div class="instruction">
|
||
Select a mode and click Start!
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
this._gameBoard = this._container.querySelector('#game-board');
|
||
this._feedbackArea = this._container.querySelector('#feedback-area');
|
||
this._createHoles();
|
||
}
|
||
|
||
_createHoles() {
|
||
this._gameBoard.innerHTML = '';
|
||
this._holes = [];
|
||
|
||
for (let i = 0; i < 15; i++) { // 5x3 = 15 holes for hard mode
|
||
const hole = document.createElement('div');
|
||
hole.className = 'whack-hole';
|
||
hole.dataset.holeId = i;
|
||
|
||
hole.innerHTML = `
|
||
<div class="whack-mole" data-hole="${i}">
|
||
<div class="pronunciation" style="display: none;"></div>
|
||
<div class="word"></div>
|
||
</div>
|
||
`;
|
||
|
||
this._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() {
|
||
// Mode selection
|
||
this._container.querySelectorAll('.mode-btn').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
if (this._isRunning) return;
|
||
|
||
this._container.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
|
||
this._container.querySelector('.mode-btn[data-mode="translation"]').classList.add('active');
|
||
btn.classList.remove('active');
|
||
this._gameMode = 'translation';
|
||
}
|
||
});
|
||
});
|
||
|
||
// Game controls
|
||
this._container.querySelector('#pronunciation-btn').addEventListener('click', () => this._togglePronunciation());
|
||
this._container.querySelector('#start-btn').addEventListener('click', () => this._start());
|
||
this._container.querySelector('#pause-btn').addEventListener('click', () => this._pause());
|
||
this._container.querySelector('#restart-btn').addEventListener('click', () => this._restart());
|
||
|
||
// 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));
|
||
});
|
||
}
|
||
|
||
_start() {
|
||
if (this._isRunning) return;
|
||
|
||
this._isRunning = true;
|
||
this._score = 0;
|
||
this._errors = 0;
|
||
this._timeLeft = this._gameTime;
|
||
|
||
this._updateUI();
|
||
this._setNewTarget();
|
||
this._startTimers();
|
||
|
||
this._container.querySelector('#start-btn').disabled = true;
|
||
this._container.querySelector('#pause-btn').disabled = false;
|
||
|
||
this._showFeedback(`Find the word: "${this._targetWord.translation}"`, 'info');
|
||
|
||
// Notify EventBus
|
||
this._eventBus.emit('whack-hard:game-started', {
|
||
mode: this._gameMode,
|
||
vocabulary: this._vocabulary.length,
|
||
difficulty: 'hard'
|
||
}, this.name);
|
||
}
|
||
|
||
_pause() {
|
||
if (!this._isRunning) return;
|
||
|
||
this._isRunning = false;
|
||
this._stopTimers();
|
||
this._hideAllMoles();
|
||
|
||
this._container.querySelector('#start-btn').disabled = false;
|
||
this._container.querySelector('#pause-btn').disabled = true;
|
||
|
||
this._showFeedback('Game paused', 'info');
|
||
|
||
this._eventBus.emit('whack-hard:game-paused', { score: this._score }, this.name);
|
||
}
|
||
|
||
_restart() {
|
||
this._stopGame();
|
||
this._resetGame();
|
||
setTimeout(() => this._start(), 100);
|
||
}
|
||
|
||
_togglePronunciation() {
|
||
this._showPronunciation = !this._showPronunciation;
|
||
const btn = this._container.querySelector('#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';
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
_stopGame() {
|
||
this._isRunning = false;
|
||
this._stopTimers();
|
||
this._hideAllMoles();
|
||
|
||
this._container.querySelector('#start-btn').disabled = false;
|
||
this._container.querySelector('#pause-btn').disabled = true;
|
||
}
|
||
|
||
_resetGame() {
|
||
this._stopGame();
|
||
|
||
this._score = 0;
|
||
this._errors = 0;
|
||
this._timeLeft = this._gameTime;
|
||
this._targetWord = null;
|
||
this._activeMoles = [];
|
||
this._spawnsSinceTarget = 0;
|
||
|
||
this._updateUI();
|
||
this._container.querySelector('#target-word').textContent = '---';
|
||
this._showFeedback('Select a mode and click Start!', 'info');
|
||
|
||
// 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');
|
||
});
|
||
}
|
||
|
||
_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._spawnRate);
|
||
|
||
// First immediate mole wave
|
||
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 multiple moles at once
|
||
this._spawnMultipleMoles();
|
||
}
|
||
|
||
_spawnMultipleMoles() {
|
||
// Find all free holes
|
||
const availableHoles = this._holes.filter(hole => !hole.isActive);
|
||
|
||
// Spawn up to molesPerWave moles
|
||
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) {
|
||
this._spawnsSinceTarget = 0;
|
||
return this._targetWord;
|
||
}
|
||
|
||
// Otherwise, 10% chance for target word (1/10 instead of 1/2)
|
||
if (Math.random() < 0.1) {
|
||
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._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);
|
||
|
||
this._eventBus.emit('whack-hard:correct-hit', {
|
||
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');
|
||
|
||
this._eventBus.emit('whack-hard:wrong-hit', {
|
||
expected: this._targetWord,
|
||
actual: hole.word,
|
||
errors: this._errors
|
||
}, this.name);
|
||
}
|
||
|
||
this._updateUI();
|
||
|
||
// Check game end by errors
|
||
if (this._errors >= this._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)];
|
||
}
|
||
|
||
// Reset counter for new target word
|
||
this._spawnsSinceTarget = 0;
|
||
|
||
this._container.querySelector('#target-word').textContent = this._targetWord.translation;
|
||
|
||
this._eventBus.emit('whack-hard:new-target', { target: this._targetWord }, this.name);
|
||
}
|
||
|
||
_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') {
|
||
this._feedbackArea.innerHTML = `<div class="instruction ${type}">${message}</div>`;
|
||
}
|
||
|
||
_updateUI() {
|
||
this._container.querySelector('#time-left').textContent = this._timeLeft;
|
||
this._container.querySelector('#errors-count').textContent = this._errors;
|
||
}
|
||
|
||
_endGame() {
|
||
this._stopGame();
|
||
|
||
// Calculate stats for victory popup
|
||
const duration = this._gameTime - this._timeLeft;
|
||
const accuracy = this._score > 0 ? Math.round(((this._score / 10) / (this._score / 10 + this._errors)) * 100) : 0;
|
||
const hitRate = Math.round((this._score / 10) || 0); // since each hit = 10 points
|
||
|
||
// Handle localStorage best score
|
||
const currentScore = this._score;
|
||
const bestScore = parseInt(localStorage.getItem('whack-hard-best-score') || '0');
|
||
const isNewBest = currentScore > bestScore;
|
||
|
||
if (isNewBest) {
|
||
localStorage.setItem('whack-hard-best-score', currentScore.toString());
|
||
}
|
||
|
||
this._showVictoryPopup({
|
||
gameTitle: 'Whack-A-Mole Hard',
|
||
currentScore,
|
||
bestScore: isNewBest ? currentScore : bestScore,
|
||
isNewBest,
|
||
stats: {
|
||
'Accuracy': `${accuracy}%`,
|
||
'Successful Hits': hitRate,
|
||
'Errors': `${this._errors}/${this._maxErrors}`,
|
||
'Duration': `${duration}s`
|
||
}
|
||
});
|
||
}
|
||
|
||
_showInitError() {
|
||
this._container.innerHTML = `
|
||
<div class="game-error">
|
||
<h3>❌ Loading Error</h3>
|
||
<p>This content does not contain vocabulary compatible with Whack-a-Mole Hard.</p>
|
||
<p>The game requires words with their translations.</p>
|
||
<button onclick="window.app?.getCore().router.navigate('/games')" class="back-btn">← Back to Games</button>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
_extractVocabulary(content) {
|
||
let vocabulary = [];
|
||
|
||
// Use content from dependency injection
|
||
if (!content) {
|
||
return this._getDemoVocabulary();
|
||
}
|
||
|
||
// Priority 1: Ultra-modular format (vocabulary object)
|
||
if (content.vocabulary && typeof content.vocabulary === 'object' && !Array.isArray(content.vocabulary)) {
|
||
vocabulary = Object.entries(content.vocabulary).map(([word, data]) => {
|
||
if (typeof data === 'object' && data.user_language) {
|
||
return {
|
||
original: word,
|
||
translation: data.user_language.split(';')[0],
|
||
fullTranslation: data.user_language,
|
||
type: data.type || 'general',
|
||
audio: data.audio,
|
||
image: data.image,
|
||
examples: data.examples,
|
||
pronunciation: data.pronunciation,
|
||
category: data.type || 'general'
|
||
};
|
||
} else if (typeof data === 'string') {
|
||
return {
|
||
original: word,
|
||
translation: data.split(';')[0],
|
||
fullTranslation: data,
|
||
type: 'general',
|
||
category: 'general'
|
||
};
|
||
}
|
||
return null;
|
||
}).filter(Boolean);
|
||
}
|
||
|
||
// Priority 2: Legacy formats support
|
||
if (vocabulary.length === 0 && content.sentences) {
|
||
vocabulary = content.sentences.map(sentence => ({
|
||
original: sentence.english || sentence.chinese || '',
|
||
translation: sentence.chinese || sentence.english || '',
|
||
pronunciation: sentence.prononciation || sentence.pronunciation
|
||
})).filter(word => word.original && word.translation);
|
||
}
|
||
|
||
return this._finalizeVocabulary(vocabulary);
|
||
}
|
||
|
||
_finalizeVocabulary(vocabulary) {
|
||
// Validation and cleanup
|
||
vocabulary = vocabulary.filter(word =>
|
||
word &&
|
||
typeof word.original === 'string' &&
|
||
typeof word.translation === 'string' &&
|
||
word.original.trim() !== '' &&
|
||
word.translation.trim() !== ''
|
||
);
|
||
|
||
if (vocabulary.length === 0) {
|
||
vocabulary = this._getDemoVocabulary();
|
||
}
|
||
|
||
return this._shuffleArray(vocabulary);
|
||
}
|
||
|
||
_getDemoVocabulary() {
|
||
return [
|
||
{ 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' },
|
||
{ original: 'book', translation: 'livre', category: 'objects' },
|
||
{ original: 'water', translation: 'eau', category: 'nature' },
|
||
{ original: 'sun', translation: 'soleil', category: 'nature' }
|
||
];
|
||
}
|
||
|
||
async _speakWord(word) {
|
||
// Use Web Speech API to pronounce the word
|
||
if ('speechSynthesis' in window) {
|
||
// Cancel any ongoing speech
|
||
speechSynthesis.cancel();
|
||
|
||
const utterance = new SpeechSynthesisUtterance(word);
|
||
const targetLanguage = this._content?.language || 'en-US';
|
||
utterance.lang = targetLanguage;
|
||
utterance.rate = 0.9; // Slightly slower for clarity
|
||
utterance.pitch = 1.0;
|
||
utterance.volume = 1.0;
|
||
|
||
// Try to use a good voice for the target language
|
||
const voices = await this._getVoices();
|
||
const langPrefix = targetLanguage.split('-')[0];
|
||
const preferredVoice = voices.find(voice =>
|
||
voice.lang.startsWith(langPrefix) && (voice.name.includes('Google') || voice.name.includes('Neural') || voice.default)
|
||
) || voices.find(voice => voice.lang.startsWith(langPrefix));
|
||
|
||
if (preferredVoice) {
|
||
utterance.voice = preferredVoice;
|
||
console.log(`🔊 Using voice: ${preferredVoice.name} (${preferredVoice.lang})`);
|
||
} else {
|
||
console.warn(`🔊 No voice found for: ${targetLanguage}, available:`, voices.map(v => v.lang));
|
||
}
|
||
|
||
speechSynthesis.speak(utterance);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get available speech synthesis voices, waiting for them to load if necessary
|
||
* @returns {Promise<SpeechSynthesisVoice[]>} Array of available voices
|
||
* @private
|
||
*/
|
||
_getVoices() {
|
||
return new Promise((resolve) => {
|
||
let voices = window.speechSynthesis.getVoices();
|
||
|
||
// If voices are already loaded, return them immediately
|
||
if (voices.length > 0) {
|
||
resolve(voices);
|
||
return;
|
||
}
|
||
|
||
// Otherwise, wait for voiceschanged event
|
||
const voicesChangedHandler = () => {
|
||
voices = window.speechSynthesis.getVoices();
|
||
if (voices.length > 0) {
|
||
window.speechSynthesis.removeEventListener('voiceschanged', voicesChangedHandler);
|
||
resolve(voices);
|
||
}
|
||
};
|
||
|
||
window.speechSynthesis.addEventListener('voiceschanged', voicesChangedHandler);
|
||
|
||
// Fallback timeout in case voices never load
|
||
setTimeout(() => {
|
||
window.speechSynthesis.removeEventListener('voiceschanged', voicesChangedHandler);
|
||
resolve(window.speechSynthesis.getVoices());
|
||
}, 1000);
|
||
});
|
||
}
|
||
|
||
_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;
|
||
}
|
||
|
||
_showVictoryPopup({ gameTitle, currentScore, bestScore, isNewBest, stats }) {
|
||
const popup = document.createElement('div');
|
||
popup.className = 'victory-popup';
|
||
popup.innerHTML = `
|
||
<div class="victory-content">
|
||
<div class="victory-header">
|
||
<div class="victory-icon">🔨</div>
|
||
<h2 class="victory-title">${gameTitle} Complete!</h2>
|
||
${isNewBest ? '<div class="new-best-badge">🎉 New Best Score!</div>' : ''}
|
||
</div>
|
||
|
||
<div class="victory-scores">
|
||
<div class="score-display">
|
||
<div class="score-label">Your Score</div>
|
||
<div class="score-value">${currentScore}</div>
|
||
</div>
|
||
<div class="score-display best-score">
|
||
<div class="score-label">Best Score</div>
|
||
<div class="score-value">${bestScore}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="victory-stats">
|
||
${Object.entries(stats).map(([key, value]) => `
|
||
<div class="stat-row">
|
||
<span class="stat-name">${key}</span>
|
||
<span class="stat-value">${value}</span>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
|
||
<div class="victory-buttons">
|
||
<button class="victory-btn primary" onclick="this.closest('.victory-popup').remove(); window.location.reload();">🔄 Play Again</button>
|
||
<button class="victory-btn secondary" onclick="this.closest('.victory-popup').remove(); window.app.getCore().router.navigate('/games'); setTimeout(() => { const chapterId = window.currentChapterId || 'sbs'; window.app.getCore().eventBus.emit('navigation:games', { path: \`/games/\${chapterId}\`, data: { path: \`/games/\${chapterId}\` } }, 'Application'); }, 100);">🎮 Different Game</button>
|
||
<button class="victory-btn tertiary" onclick="this.closest('.victory-popup').remove(); window.app.getCore().router.navigate('/');">🏠 Main Menu</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
document.body.appendChild(popup);
|
||
|
||
// Emit completion event after showing popup
|
||
this._eventBus.emit('whack-hard:game-ended', {
|
||
score: currentScore,
|
||
errors: this._errors,
|
||
timeLeft: this._timeLeft,
|
||
mode: this._gameMode
|
||
}, this.name);
|
||
}
|
||
|
||
// Event handlers
|
||
_handleStart(event) {
|
||
this._validateInitialized();
|
||
this.startGame();
|
||
}
|
||
|
||
_handlePause(event) {
|
||
this._validateInitialized();
|
||
this.pauseGame();
|
||
}
|
||
|
||
_handleRestart(event) {
|
||
this._validateInitialized();
|
||
this.restartGame();
|
||
}
|
||
|
||
_handleTogglePronunciation(event) {
|
||
this._validateInitialized();
|
||
this._togglePronunciation();
|
||
}
|
||
|
||
// Static metadata methods
|
||
static getMetadata() {
|
||
return {
|
||
name: 'WhackAMoleHard',
|
||
version: '1.0.0',
|
||
description: 'Advanced Whack-a-Mole game with multiple moles per wave and increased difficulty',
|
||
author: 'Class Generator',
|
||
difficulty: 'hard',
|
||
category: 'action',
|
||
tags: ['vocabulary', 'reaction', 'translation', 'hard', 'multiple-targets'],
|
||
contentRequirements: ['vocabulary'],
|
||
supportedModes: ['translation'],
|
||
features: [
|
||
'Multiple moles per wave',
|
||
'15-hole grid (5x3)',
|
||
'Target word guarantee system',
|
||
'Pronunciation support',
|
||
'Score tracking',
|
||
'Time pressure',
|
||
'Error limits'
|
||
]
|
||
};
|
||
}
|
||
|
||
static getCompatibilityScore(content) {
|
||
let score = 0;
|
||
|
||
if (!content) return 0;
|
||
|
||
// Check vocabulary availability (required)
|
||
if (content.vocabulary && Object.keys(content.vocabulary).length > 0) {
|
||
score += 40;
|
||
|
||
// Bonus for rich vocabulary data
|
||
const sampleEntry = Object.values(content.vocabulary)[0];
|
||
if (typeof sampleEntry === 'object' && sampleEntry.user_language) {
|
||
score += 20;
|
||
}
|
||
|
||
// Pronunciation bonus
|
||
const haspronounciation = Object.values(content.vocabulary).some(entry =>
|
||
(typeof entry === 'object' && entry.pronunciation) ||
|
||
(typeof entry === 'object' && entry.prononciation)
|
||
);
|
||
if (haspronounciation) score += 15;
|
||
|
||
// Volume bonus
|
||
const vocabCount = Object.keys(content.vocabulary).length;
|
||
if (vocabCount >= 20) score += 10;
|
||
if (vocabCount >= 50) score += 10;
|
||
|
||
} else if (content.sentences && content.sentences.length > 0) {
|
||
// Fallback support for legacy format
|
||
score += 25;
|
||
}
|
||
|
||
return Math.min(score, 100);
|
||
}
|
||
}
|
||
|
||
export default WhackAMoleHard; |