import Module from '../core/Module.js'; /** * WordStorm - Fast-paced falling words game where players match vocabulary * Words fall from the sky like meteorites and players must select correct translations */ class WordStorm extends Module { constructor(name, dependencies, config = {}) { super(name, ['eventBus']); // Validate dependencies if (!dependencies.eventBus || !dependencies.content) { throw new Error('WordStorm requires eventBus and content dependencies'); } this._eventBus = dependencies.eventBus; this._content = dependencies.content; this._config = { container: null, maxWords: 50, fallSpeed: 8000, // ms to fall from top to bottom spawnRate: 4000, // ms between spawns wordLifetime: 9200, // ms before word disappears (+15% more time) startingLives: 3, ...config }; // Game state this._vocabulary = null; this._score = 0; this._level = 1; this._lives = this._config.startingLives; this._combo = 0; this._isGamePaused = false; this._isGameOver = false; this._gameStartTime = null; // Game mechanics this._fallingWords = []; this._currentWordIndex = 0; this._spawnInterval = null; this._gameInterval = null; Object.seal(this); } /** * Get game metadata * @returns {Object} Game metadata */ static getMetadata() { return { name: 'Word Storm', description: 'Fast-paced falling words game with vocabulary matching', difficulty: 'intermediate', category: 'action', estimatedTime: 6, // minutes skills: ['vocabulary', 'speed', 'reflexes', 'concentration'] }; } /** * 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 < 8) { return { score: 0, reason: `Insufficient vocabulary (${vocabCount}/8 required)`, requirements: ['vocabulary'], minWords: 8, details: 'Word Storm needs at least 8 vocabulary words for meaningful gameplay' }; } // Perfect score at 30+ words, partial score for 8-29 const score = Math.min(vocabCount / 30, 1); return { score, reason: `${vocabCount} vocabulary words available`, requirements: ['vocabulary'], minWords: 8, optimalWords: 30, details: `Can create dynamic gameplay with ${Math.min(vocabCount, this._config?.maxWords || 50)} 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 < 8) { throw new Error(`Insufficient vocabulary: need 8, 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._setupEventListeners(); // Start the game this._gameStartTime = Date.now(); this._startSpawning(); // Emit game ready event this._eventBus.emit('game:ready', { gameId: 'word-storm', instanceId: this.name, vocabulary: this._vocabulary.length }, this.name); this._setInitialized(); } catch (error) { this._showError(error.message); throw error; } } async destroy() { this._validateNotDestroyed(); // Clear intervals if (this._spawnInterval) { clearInterval(this._spawnInterval); this._spawnInterval = null; } if (this._gameInterval) { clearInterval(this._gameInterval); this._gameInterval = null; } // 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: 'word-storm', instanceId: this.name, score: this._score, level: this._level, combo: this._combo, 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, level: this._level, lives: this._lives, combo: this._combo, isGameOver: this._isGameOver, isPaused: this._isGamePaused, duration: this._gameStartTime ? Date.now() - this._gameStartTime : 0, fallingWordsCount: this._fallingWords.length }; } // 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')) { vocabulary.push({ original: word, translation: data.user_language || data, type: data.type || 'unknown' }); } } // Limit vocabulary and shuffle return this._shuffleArray(vocabulary).slice(0, this._config.maxWords); } _injectCSS() { const cssId = `word-storm-styles-${this.name}`; if (document.getElementById(cssId)) return; const style = document.createElement('style'); style.id = cssId; style.textContent = ` .word-storm-game { height: 100vh; overflow: hidden; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; position: relative; } .word-storm-hud { display: flex; justify-content: space-between; align-items: center; padding: 15px 20px; background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); color: white; position: relative; z-index: 100; } .hud-section { display: flex; gap: 20px; align-items: center; } .hud-stat { display: flex; flex-direction: column; align-items: center; min-width: 60px; } .hud-label { font-size: 0.8rem; opacity: 0.9; margin-bottom: 2px; } .hud-value { font-size: 1.2rem; font-weight: bold; } .pause-btn { padding: 8px 15px; background: rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.3); color: white; border-radius: 6px; cursor: pointer; font-size: 0.9rem; transition: all 0.3s ease; } .pause-btn:hover { background: rgba(255, 255, 255, 0.3); } .word-storm-area { position: relative; height: calc(80vh - 180px); background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); overflow: hidden; border-radius: 20px 20px 0 0; margin: 10px 10px 0 10px; box-shadow: inset 0 0 30px rgba(0, 0, 0, 0.3); } .falling-word { position: absolute; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px 30px; border-radius: 25px; font-size: 2rem; font-weight: 600; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3), 0 0 20px rgba(102, 126, 234, 0.4); cursor: default; user-select: none; transform: translateX(-50%); animation: wordGlow 2s ease-in-out infinite; z-index: 10; } .falling-word.exploding { animation: explode 0.8s ease-out forwards; } .falling-word.wrong-shake { animation: wrongShake 0.6s ease-in-out forwards; } .word-storm-answer-panel { position: relative; background: rgba(0, 0, 0, 0.9); padding: 15px; border-top: 3px solid #667eea; border-radius: 0 0 20px 20px; margin: 0 10px 10px 10px; z-index: 100; } .word-storm-answer-panel.wrong-flash { animation: wrongFlash 0.5s ease-in-out; } .answer-buttons-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; max-width: 600px; margin: 0 auto; } .word-storm-answer-btn { padding: 10px 16px; background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%); color: white; border: none; border-radius: 16px; font-size: 0.95rem; font-weight: 500; cursor: pointer; transition: all 0.3s ease; position: relative; overflow: hidden; } .word-storm-answer-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(59, 130, 246, 0.4); } .word-storm-answer-btn:active { transform: translateY(0); } .word-storm-answer-btn.correct { background: linear-gradient(135deg, #10b981 0%, #059669 100%); animation: correctPulse 0.6s ease-out; } .word-storm-answer-btn.incorrect { background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); animation: incorrectShake 0.6s ease-out; } .level-up-popup { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0, 0, 0, 0.9); color: white; padding: 30px; border-radius: 15px; text-align: center; z-index: 1000; animation: levelUpAppear 2s ease-out forwards; } .points-popup { position: absolute; font-size: 2rem; font-weight: bold; color: #10b981; pointer-events: none; z-index: 1000; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); animation: pointsFloat 1.5s ease-out forwards; } .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; z-index: 2000; } .game-over-content { background: linear-gradient(135deg, #dc2626 0%, #991b1b 100%); color: white; padding: 40px; border-radius: 15px; text-align: center; max-width: 400px; } .game-over-content h2 { margin: 0 0 20px 0; font-size: 2.5rem; } .final-stats { margin: 20px 0; padding: 20px; background: rgba(255, 255, 255, 0.1); border-radius: 8px; } .stat-row { display: flex; justify-content: space-between; margin: 10px 0; } .restart-btn, .exit-btn { margin: 10px; padding: 12px 25px; border: 2px solid white; border-radius: 8px; background: transparent; color: white; font-size: 1rem; cursor: pointer; transition: all 0.3s ease; } .restart-btn:hover, .exit-btn:hover { background: white; color: #dc2626; } /* Animations */ @keyframes wordGlow { 0%, 100% { box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3), 0 0 20px rgba(102, 126, 234, 0.4); } 50% { box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3), 0 0 30px rgba(102, 126, 234, 0.6); } } @keyframes explode { 0% { transform: translateX(-50%) scale(1) rotate(0deg); opacity: 1; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } 25% { transform: translateX(-50%) scale(1.3) rotate(5deg); opacity: 0.9; background: linear-gradient(135deg, #10b981 0%, #059669 100%); box-shadow: 0 8px 25px rgba(16, 185, 129, 0.5), 0 0 40px rgba(16, 185, 129, 0.8); } 50% { transform: translateX(-50%) scale(1.5) rotate(-3deg); opacity: 0.7; background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); box-shadow: 0 12px 35px rgba(245, 158, 11, 0.6), 0 0 60px rgba(245, 158, 11, 0.9); } 75% { transform: translateX(-50%) scale(0.8) rotate(2deg); opacity: 0.4; background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); } 100% { transform: translateX(-50%) scale(0.1) rotate(0deg); opacity: 0; } } @keyframes wrongShake { 0%, 100% { transform: translateX(-50%) scale(1); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } 10%, 30%, 50%, 70%, 90% { transform: translateX(-60%) scale(0.95); background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); box-shadow: 0 4px 15px rgba(239, 68, 68, 0.6), 0 0 25px rgba(239, 68, 68, 0.8); } 20%, 40%, 60%, 80% { transform: translateX(-40%) scale(0.95); background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); box-shadow: 0 4px 15px rgba(239, 68, 68, 0.6), 0 0 25px rgba(239, 68, 68, 0.8); } } @keyframes wrongFlash { 0%, 100% { background: rgba(0, 0, 0, 0.8); } 50% { background: rgba(239, 68, 68, 0.6); box-shadow: 0 0 20px rgba(239, 68, 68, 0.6), inset 0 0 20px rgba(239, 68, 68, 0.3); } } @keyframes correctPulse { 0% { transform: scale(1); } 50% { transform: scale(1.05); } 100% { transform: scale(1); } } @keyframes incorrectShake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-5px); } 75% { transform: translateX(5px); } } @keyframes pointsFloat { 0% { transform: translateY(0) scale(1); opacity: 1; } 30% { transform: translateY(-20px) scale(1.3); opacity: 1; } 100% { transform: translateY(-80px) scale(0.5); opacity: 0; } } @keyframes levelUpAppear { 0% { transform: translate(-50%, -50%) scale(0.5); opacity: 0; } 20% { transform: translate(-50%, -50%) scale(1.2); opacity: 1; } 80% { transform: translate(-50%, -50%) scale(1); opacity: 1; } 100% { transform: translate(-50%, -50%) scale(1); opacity: 0; } } @keyframes screenShake { 0%, 100% { transform: translateX(0); } 10% { transform: translateX(-3px) translateY(1px); } 20% { transform: translateX(3px) translateY(-1px); } 30% { transform: translateX(-2px) translateY(2px); } 40% { transform: translateX(2px) translateY(-2px); } 50% { transform: translateX(-1px) translateY(1px); } 60% { transform: translateX(1px) translateY(-1px); } 70% { transform: translateX(-2px) translateY(0px); } 80% { transform: translateX(2px) translateY(1px); } 90% { transform: translateX(-1px) translateY(-1px); } } @media (max-width: 768px) { .falling-word { padding: 15px 25px; font-size: 1.8rem; } .hud-section { gap: 15px; } .answer-buttons-grid { grid-template-columns: 1fr 1fr; gap: 10px; } } @media (max-width: 480px) { .falling-word { font-size: 1.5rem; padding: 12px 20px; } .answer-buttons-grid { grid-template-columns: 1fr; } } `; document.head.appendChild(style); } _removeCSS() { const cssId = `word-storm-styles-${this.name}`; const existingStyle = document.getElementById(cssId); if (existingStyle) { existingStyle.remove(); } } _createGameInterface() { this._config.container.innerHTML = `
Level ${this._level}
Words fall faster!
`; gameArea.appendChild(levelUpMsg); setTimeout(() => { if (levelUpMsg.parentNode) { levelUpMsg.remove(); } }, 2000); // Emit level up event this._eventBus.emit('word-storm:level-up', { gameId: 'word-storm', instanceId: this.name, level: this._level, score: this._score }, this.name); } _togglePause() { this._isGamePaused = !this._isGamePaused; const pauseBtn = document.getElementById('pause-btn'); if (pauseBtn) { pauseBtn.textContent = this._isGamePaused ? '▶️ Resume' : '⏸️ Pause'; } if (this._isGamePaused) { this._eventBus.emit('game:paused', { instanceId: this.name }, this.name); } else { this._eventBus.emit('game:resumed', { instanceId: this.name }, this.name); } } _gameOver() { this._isGameOver = true; // Clear intervals if (this._spawnInterval) { clearInterval(this._spawnInterval); this._spawnInterval = null; } // Clear falling words this._fallingWords.forEach(fw => { if (fw.element.parentNode) { fw.element.remove(); } }); this._fallingWords = []; // Show game over screen this._showGameOverScreen(); // Emit game over event this._eventBus.emit('game:completed', { gameId: 'word-storm', instanceId: this.name, score: this._score, level: this._level, duration: this._gameStartTime ? Date.now() - this._gameStartTime : 0 }, this.name); } _showGameOverScreen() { const duration = this._gameStartTime ? Math.round((Date.now() - this._gameStartTime) / 1000) : 0; // Store best score const gameKey = 'word-storm'; 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: 'Word Storm', currentScore, bestScore: isNewBest ? currentScore : bestScore, isNewBest, stats: { 'Level Reached': this._level, 'Duration': `${duration}s`, 'Words Caught': this._wordsCaught || 0, 'Accuracy': this._wordsCaught ? `${Math.round((this._wordsCaught / (this._wordsCaught + this._wordsMissed || 0)) * 100)}%` : '0%' } }); } _restartGame() { // Reset game state this._score = 0; this._level = 1; this._lives = this._config.startingLives; this._combo = 0; this._isGamePaused = false; this._isGameOver = false; this._currentWordIndex = 0; this._gameStartTime = Date.now(); // Reset fall speed and spawn rate this._config.fallSpeed = 8000; this._config.spawnRate = 4000; // Clear existing intervals if (this._spawnInterval) { clearInterval(this._spawnInterval); } // Clear falling words this._fallingWords.forEach(fw => { if (fw.element.parentNode) { fw.element.remove(); } }); this._fallingWords = []; // Victory popup is handled by its own close events // Update HUD and restart this._updateHUD(); this._generateAnswerOptions(); this._startSpawning(); } _updateHUD() { const scoreDisplay = document.getElementById('score-display'); const levelDisplay = document.getElementById('level-display'); const livesDisplay = document.getElementById('lives-display'); const comboDisplay = document.getElementById('combo-display'); if (scoreDisplay) scoreDisplay.textContent = this._score; if (levelDisplay) levelDisplay.textContent = this._level; if (livesDisplay) livesDisplay.textContent = this._lives; if (comboDisplay) comboDisplay.textContent = this._combo; } _showError(message) { if (this._config.container) { this._config.container.innerHTML = `${message}