- 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>
1253 lines
41 KiB
JavaScript
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; |