// === 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.6s ease-out forwards; } @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); opacity: 1; } 50% { transform: translateX(-50%) scale(1.2); opacity: 0.8; } 100% { transform: translateX(-50%) scale(0.3); 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 if (fallingWord.element.parentNode) { fallingWord.element.classList.add('exploding'); setTimeout(() => { if (fallingWord.element.parentNode) { fallingWord.element.remove(); } }, 600); } // 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 document.getElementById('score').textContent = this.score; document.getElementById('combo').textContent = this.combo; // Level up check if (this.score > 0 && this.score % 100 === 0) { this.levelUp(); } } wrongAnswer() { this.combo = 0; document.getElementById('combo').textContent = this.combo; // Flash effect const answerPanel = document.getElementById('answer-panel'); if (answerPanel) { answerPanel.style.background = 'rgba(239, 68, 68, 0.3)'; setTimeout(() => { answerPanel.style.background = ''; }, 300); } } 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;