Class_generator/src/games/WordStorm.js
StillHammer 05142bdfbc Implement comprehensive AI text report/export system
- Add AIReportSystem.js for detailed AI response capture and report generation
- Add AIReportInterface.js UI component for report access and export
- Integrate AI reporting into LLMValidator and SmartPreviewOrchestrator
- Add missing modules to Application.js configuration (unifiedDRS, smartPreviewOrchestrator)
- Create missing content/chapters/sbs.json for book metadata
- Enhance Application.js with debug logging for module loading
- Add multi-format export capabilities (text, HTML, JSON)
- Implement automatic learning insights extraction from AI feedback
- Add session management and performance tracking for AI reports

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-26 21:24:13 +08:00

1253 lines
41 KiB
JavaScript

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 = `
<div class="word-storm-game">
<div class="word-storm-hud">
<div class="hud-section">
<div class="hud-stat">
<div class="hud-label">Score</div>
<div class="hud-value" id="score-display">0</div>
</div>
<div class="hud-stat">
<div class="hud-label">Level</div>
<div class="hud-value" id="level-display">1</div>
</div>
</div>
<div class="hud-section">
<div class="hud-stat">
<div class="hud-label">Lives</div>
<div class="hud-value" id="lives-display">3</div>
</div>
<div class="hud-stat">
<div class="hud-label">Combo</div>
<div class="hud-value" id="combo-display">0</div>
</div>
</div>
<div class="hud-section">
<button class="pause-btn" id="pause-btn">⏸️ Pause</button>
<button class="btn btn-outline btn-sm" id="exit-storm">
<span class="btn-icon">←</span>
<span class="btn-text">Exit</span>
</button>
</div>
</div>
<div class="word-storm-area" id="game-area"></div>
<div class="word-storm-answer-panel" id="answer-panel">
<div class="answer-buttons-grid" id="answer-buttons">
<!-- Dynamic answer buttons -->
</div>
</div>
</div>
`;
this._generateAnswerOptions();
}
_setupEventListeners() {
// Pause button
const pauseBtn = document.getElementById('pause-btn');
if (pauseBtn) {
pauseBtn.addEventListener('click', () => this._togglePause());
}
// Exit button
const exitButton = document.getElementById('exit-storm');
if (exitButton) {
exitButton.addEventListener('click', () => {
this._eventBus.emit('game:exit-request', { instanceId: this.name }, this.name);
});
}
// Answer button clicks
this._config.container.addEventListener('click', (event) => {
if (event.target.matches('.word-storm-answer-btn')) {
const answer = event.target.textContent;
this._checkAnswer(answer);
}
if (event.target.matches('.restart-btn')) {
this._restartGame();
}
});
// Keyboard support
document.addEventListener('keydown', (event) => {
if (event.key >= '1' && event.key <= '4') {
const btnIndex = parseInt(event.key) - 1;
const buttons = document.querySelectorAll('.word-storm-answer-btn');
if (buttons[btnIndex]) {
buttons[btnIndex].click();
}
}
if (event.key === ' ' || event.key === 'Escape') {
event.preventDefault();
this._togglePause();
}
});
}
_startSpawning() {
this._spawnInterval = setInterval(() => {
if (!this._isGamePaused && !this._isGameOver) {
this._spawnFallingWord();
}
}, this._config.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 = '80px'; // Start just below the HUD
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._config.wordLifetime);
}
_animateFalling(wordElement) {
wordElement.style.transition = `top ${this._config.fallSpeed}ms linear`;
setTimeout(() => {
wordElement.style.top = 'calc(100vh + 60px)'; // Continue falling past screen
}, 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 =>
`<button class="word-storm-answer-btn">${answer}</button>`
).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;
// Update display
this._updateHUD();
// 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();
}
// Emit correct answer event
this._eventBus.emit('word-storm:correct-answer', {
gameId: 'word-storm',
instanceId: this.name,
word: fallingWord.word,
points,
combo: this._combo,
score: this._score
}, this.name);
}
_wrongAnswer() {
this._combo = 0;
// 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);
}
this._updateHUD();
// Wrong answer vibration (stronger/longer)
if (navigator.vibrate) {
navigator.vibrate([200, 100, 200, 100, 200]);
}
// Emit wrong answer event
this._eventBus.emit('word-storm:wrong-answer', {
gameId: 'word-storm',
instanceId: this.name,
score: this._score
}, this.name);
}
_showPointsPopup(points, wordElement) {
const popup = document.createElement('div');
popup.textContent = `+${points}`;
popup.className = 'points-popup';
popup.style.left = wordElement.style.left;
popup.style.top = wordElement.offsetTop + 'px';
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;
this._updateHUD();
if (this._lives <= 0) {
this._gameOver();
}
// Emit word missed event
this._eventBus.emit('word-storm:word-missed', {
gameId: 'word-storm',
instanceId: this.name,
lives: this._lives,
score: this._score
}, this.name);
}
_levelUp() {
this._level++;
// Increase difficulty by 5% (x1.05 speed = /1.05 time)
this._config.fallSpeed = Math.max(1000, this._config.fallSpeed / 1.05);
this._config.spawnRate = Math.max(800, this._config.spawnRate / 1.05);
// Restart intervals with new timing
if (this._spawnInterval) {
clearInterval(this._spawnInterval);
this._startSpawning();
}
this._updateHUD();
// Show level up message
const gameArea = document.getElementById('game-area');
const levelUpMsg = document.createElement('div');
levelUpMsg.className = 'level-up-popup';
levelUpMsg.innerHTML = `
<h2>⚡ LEVEL UP! ⚡</h2>
<p>Level ${this._level}</p>
<p>Words fall faster!</p>
`;
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 = `
<div class="game-error">
<div class="error-icon">❌</div>
<h3>Word Storm Error</h3>
<p>${message}</p>
<button class="btn btn-primary" onclick="history.back()">Go Back</button>
</div>
`;
}
}
_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() {
this._isGamePaused = true;
const pauseBtn = document.getElementById('pause-btn');
if (pauseBtn) {
pauseBtn.textContent = '▶️ Resume';
}
this._eventBus.emit('game:paused', { instanceId: this.name }, this.name);
}
_handleResume() {
this._isGamePaused = false;
const pauseBtn = document.getElementById('pause-btn');
if (pauseBtn) {
pauseBtn.textContent = '⏸️ Pause';
}
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 = `
<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-item">
<div class="stat-label">${key}</div>
<div class="stat-value">${value}</div>
</div>
`).join('')}
</div>
<div class="victory-actions">
<button class="victory-btn victory-btn-primary" id="play-again-btn">
<span class="btn-icon">🔄</span>
<span class="btn-text">Play Again</span>
</button>
<button class="victory-btn victory-btn-secondary" id="different-game-btn">
<span class="btn-icon">🎮</span>
<span class="btn-text">Different Game</span>
</button>
<button class="victory-btn victory-btn-outline" id="main-menu-btn">
<span class="btn-icon">🏠</span>
<span class="btn-text">Main Menu</span>
</button>
</div>
</div>
`;
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 WordStorm;