// === WORD STORM GAME === // Game where words fall from the sky like meteorites! class WordStormGame { constructor(options) { logSh('Word Storm constructor called', 'DEBUG'); this.container = options.container; this.content = options.content; this.onScoreUpdate = options.onScoreUpdate || (() => {}); this.onGameEnd = options.onGameEnd || (() => {}); // Inject game-specific CSS this.injectCSS(); logSh('Options processed, initializing game state...', 'DEBUG'); // Game state this.score = 0; this.level = 1; this.lives = 3; this.combo = 0; this.isGamePaused = false; this.isGameOver = false; // Game mechanics this.fallingWords = []; this.gameInterval = null; this.spawnInterval = null; this.currentWordIndex = 0; // Game settings this.fallSpeed = 8000; // ms to fall from top to bottom (very slow) this.spawnRate = 4000; // ms between spawns (not frequent) this.wordLifetime = 15000; // ms before word disappears (long time) logSh('Game state initialized, extracting vocabulary...', 'DEBUG'); // Content extraction try { this.vocabulary = this.extractVocabulary(this.content); this.shuffledVocab = [...this.vocabulary]; this.shuffleArray(this.shuffledVocab); logSh(`Word Storm initialized with ${this.vocabulary.length} words`, 'INFO'); } catch (error) { logSh(`Error extracting vocabulary: ${error.message}`, 'ERROR'); throw error; } logSh('Calling init()...', 'DEBUG'); this.init(); } injectCSS() { // Avoid injecting CSS multiple times if (document.getElementById('word-storm-styles')) return; const styleSheet = document.createElement('style'); styleSheet.id = 'word-storm-styles'; styleSheet.textContent = ` .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; } .falling-word.exploding { animation: explode 0.8s ease-out forwards; } .falling-word.wrong-shake { animation: wrongShake 0.6s ease-in-out forwards; } .answer-panel.wrong-flash { animation: wrongFlash 0.5s ease-in-out; } @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%); box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3), 0 0 20px rgba(102, 126, 234, 0.4); } 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: transparent; box-shadow: none; } 50% { background: rgba(239, 68, 68, 0.4); box-shadow: 0 0 20px rgba(239, 68, 68, 0.6), inset 0 0 20px rgba(239, 68, 68, 0.3); } } @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); } } @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; } } @media (max-width: 768px) { .falling-word { padding: 18px 25px; font-size: 1.8rem; } } @media (max-width: 480px) { .falling-word { font-size: 1.5rem; padding: 15px 20px; } } `; document.head.appendChild(styleSheet); logSh('Word Storm CSS injected', 'DEBUG'); } extractVocabulary(content) { let vocabulary = []; logSh(`Word Storm extracting vocabulary from content`, 'DEBUG'); // Support Dragon's Pearl and other formats if (content.vocabulary && typeof content.vocabulary === 'object') { vocabulary = Object.entries(content.vocabulary).map(([original, vocabData]) => { if (typeof vocabData === 'string') { return { original: original, translation: vocabData }; } else if (typeof vocabData === 'object') { return { original: original, translation: vocabData.user_language || vocabData.translation || 'No translation', pronunciation: vocabData.pronunciation }; } return null; }).filter(item => item !== null); logSh(`Extracted ${vocabulary.length} words from content.vocabulary`, 'DEBUG'); } // Support rawContent format if (content.rawContent && content.rawContent.vocabulary) { const rawVocab = Object.entries(content.rawContent.vocabulary).map(([original, vocabData]) => { if (typeof vocabData === 'string') { return { original: original, translation: vocabData }; } else if (typeof vocabData === 'object') { return { original: original, translation: vocabData.user_language || vocabData.translation, pronunciation: vocabData.pronunciation }; } return null; }).filter(item => item !== null); vocabulary = vocabulary.concat(rawVocab); logSh(`Added ${rawVocab.length} words from rawContent.vocabulary, total: ${vocabulary.length}`, 'DEBUG'); } // Limit to 50 words max for performance return vocabulary.slice(0, 50); } init() { if (this.vocabulary.length === 0) { this.showNoVocabularyMessage(); return; } this.container.innerHTML = `
Score: 0
Level: 1
Lives: 3
Combo: 0
`; this.setupEventListeners(); this.generateAnswerOptions(); } setupEventListeners() { const pauseBtn = document.getElementById('pause-btn'); if (pauseBtn) { pauseBtn.addEventListener('click', () => this.togglePause()); } // Answer button clicks document.addEventListener('click', (e) => { if (e.target.classList.contains('answer-btn')) { const answer = e.target.textContent; this.checkAnswer(answer); } }); // Keyboard support document.addEventListener('keydown', (e) => { if (e.key >= '1' && e.key <= '4') { const btnIndex = parseInt(e.key) - 1; const buttons = document.querySelectorAll('.answer-btn'); if (buttons[btnIndex]) { buttons[btnIndex].click(); } } }); } start() { logSh('Word Storm game started', 'INFO'); this.startSpawning(); } startSpawning() { this.spawnInterval = setInterval(() => { if (!this.isGamePaused && !this.isGameOver) { this.spawnFallingWord(); } }, this.spawnRate); } spawnFallingWord() { if (this.vocabulary.length === 0) return; const word = this.vocabulary[this.currentWordIndex % this.vocabulary.length]; this.currentWordIndex++; const gameArea = document.getElementById('game-area'); const wordElement = document.createElement('div'); wordElement.className = 'falling-word'; wordElement.textContent = word.original; wordElement.style.left = Math.random() * 80 + 10 + '%'; wordElement.style.top = '-60px'; gameArea.appendChild(wordElement); this.fallingWords.push({ element: wordElement, word: word, startTime: Date.now() }); // Generate new answer options when word spawns this.generateAnswerOptions(); // Animate falling this.animateFalling(wordElement); // Remove after lifetime setTimeout(() => { if (wordElement.parentNode) { this.missWord(wordElement); } }, this.wordLifetime); } animateFalling(wordElement) { wordElement.style.transition = `top ${this.fallSpeed}ms linear`; setTimeout(() => { wordElement.style.top = '100vh'; }, 50); } generateAnswerOptions() { if (this.vocabulary.length === 0) return; const buttons = []; const correctWord = this.fallingWords.length > 0 ? this.fallingWords[this.fallingWords.length - 1].word : this.vocabulary[0]; // Add correct answer buttons.push(correctWord.translation); // Add 3 random incorrect answers while (buttons.length < 4) { const randomWord = this.vocabulary[Math.floor(Math.random() * this.vocabulary.length)]; if (!buttons.includes(randomWord.translation)) { buttons.push(randomWord.translation); } } // Shuffle buttons this.shuffleArray(buttons); // Update answer panel const answerButtons = document.getElementById('answer-buttons'); if (answerButtons) { answerButtons.innerHTML = buttons.map(answer => `` ).join(''); } } checkAnswer(selectedAnswer) { const activeFallingWords = this.fallingWords.filter(fw => fw.element.parentNode); for (let i = 0; i < activeFallingWords.length; i++) { const fallingWord = activeFallingWords[i]; if (fallingWord.word.translation === selectedAnswer) { this.correctAnswer(fallingWord); return; } } // Wrong answer this.wrongAnswer(); } correctAnswer(fallingWord) { // Remove from game with epic explosion if (fallingWord.element.parentNode) { fallingWord.element.classList.add('exploding'); // Add screen shake effect const gameArea = document.getElementById('game-area'); if (gameArea) { gameArea.style.animation = 'none'; gameArea.offsetHeight; // Force reflow gameArea.style.animation = 'screenShake 0.3s ease-in-out'; setTimeout(() => { gameArea.style.animation = ''; }, 300); } setTimeout(() => { if (fallingWord.element.parentNode) { fallingWord.element.remove(); } }, 800); } // Remove from tracking this.fallingWords = this.fallingWords.filter(fw => fw !== fallingWord); // Update score this.combo++; const points = 10 + (this.combo * 2); this.score += points; this.onScoreUpdate(this.score); // Update display with animation document.getElementById('score').textContent = this.score; document.getElementById('combo').textContent = this.combo; // Add points popup animation this.showPointsPopup(points, fallingWord.element); // Vibration feedback (if supported) if (navigator.vibrate) { navigator.vibrate([50, 30, 50]); } // Level up check if (this.score > 0 && this.score % 100 === 0) { this.levelUp(); } } wrongAnswer() { this.combo = 0; document.getElementById('combo').textContent = this.combo; // Enhanced wrong answer animation const answerPanel = document.getElementById('answer-panel'); if (answerPanel) { answerPanel.classList.add('wrong-flash'); setTimeout(() => { answerPanel.classList.remove('wrong-flash'); }, 500); } // Shake all falling words to show disappointment this.fallingWords.forEach(fw => { if (fw.element.parentNode && !fw.element.classList.contains('exploding')) { fw.element.classList.add('wrong-shake'); setTimeout(() => { fw.element.classList.remove('wrong-shake'); }, 600); } }); // Screen flash red const gameArea = document.getElementById('game-area'); if (gameArea) { const overlay = document.createElement('div'); overlay.style.cssText = ` position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(239, 68, 68, 0.3); pointer-events: none; animation: wrongFlash 0.4s ease-in-out; z-index: 100; `; gameArea.appendChild(overlay); setTimeout(() => { if (overlay.parentNode) overlay.remove(); }, 400); } // Wrong answer vibration (stronger/longer) if (navigator.vibrate) { navigator.vibrate([200, 100, 200, 100, 200]); } } showPointsPopup(points, wordElement) { const popup = document.createElement('div'); popup.textContent = `+${points}`; popup.style.cssText = ` position: absolute; left: ${wordElement.style.left}; top: ${wordElement.offsetTop}px; 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; `; const gameArea = document.getElementById('game-area'); if (gameArea) { gameArea.appendChild(popup); setTimeout(() => { if (popup.parentNode) popup.remove(); }, 1500); } } missWord(wordElement) { // Remove word if (wordElement.parentNode) { wordElement.remove(); } // Remove from tracking this.fallingWords = this.fallingWords.filter(fw => fw.element !== wordElement); // Lose life this.lives--; this.combo = 0; document.getElementById('lives').textContent = this.lives; document.getElementById('combo').textContent = this.combo; if (this.lives <= 0) { this.gameOver(); } } levelUp() { this.level++; document.getElementById('level').textContent = this.level; // Increase difficulty this.fallSpeed = Math.max(1000, this.fallSpeed * 0.9); this.spawnRate = Math.max(800, this.spawnRate * 0.95); // Restart intervals with new timing if (this.spawnInterval) { clearInterval(this.spawnInterval); this.startSpawning(); } // Show level up message const gameArea = document.getElementById('game-area'); const levelUpMsg = document.createElement('div'); levelUpMsg.innerHTML = `

⚡ LEVEL UP! ⚡

Level ${this.level}

`; gameArea.appendChild(levelUpMsg); setTimeout(() => { if (levelUpMsg.parentNode) { levelUpMsg.remove(); } }, 2000); } togglePause() { this.isGamePaused = !this.isGamePaused; const pauseBtn = document.getElementById('pause-btn'); if (pauseBtn) { pauseBtn.textContent = this.isGamePaused ? '▶️ Resume' : '⏸️ Pause'; } } gameOver() { this.isGameOver = true; // Clear intervals if (this.spawnInterval) { clearInterval(this.spawnInterval); } // Clear falling words this.fallingWords.forEach(fw => { if (fw.element.parentNode) { fw.element.remove(); } }); this.onGameEnd(this.score); } showNoVocabularyMessage() { this.container.innerHTML = `

🌪️ Word Storm

❌ No vocabulary found in this content.

This game requires content with vocabulary words.

`; } shuffleArray(array) { for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } } destroy() { if (this.spawnInterval) { clearInterval(this.spawnInterval); } // Remove CSS const styleSheet = document.getElementById('word-storm-styles'); if (styleSheet) { styleSheet.remove(); } logSh('Word Storm destroyed', 'INFO'); } } // Export to global namespace window.GameModules = window.GameModules || {}; window.GameModules.WordStorm = WordStormGame;