Implement Word Storm game and refactor CSS architecture

Major features:
- Add new Word Storm falling words vocabulary game
- Refactor CSS to modular architecture (global base + game injection)
- Fix template literal syntax errors causing loading failures
- Add comprehensive developer guidelines to prevent common mistakes

Technical changes:
- Word Storm: Complete game with falling words, scoring, levels, lives
- CSS Architecture: Move game-specific styles from global CSS to injectCSS()
- GameLoader: Add Word Storm mapping and improve error handling
- Navigation: Add Word Storm configuration
- Documentation: Add debugging guides and common pitfall prevention

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
StillHammer 2025-09-19 01:55:08 +08:00
parent fe7153d28b
commit dacc7e98a1
6 changed files with 1126 additions and 26 deletions

335
CLAUDE.md
View File

@ -263,6 +263,156 @@ The platform automatically adapts available games and exercises based on content
→ Unlock: Advanced difficulty levels, speed challenges
```
## 🚨 CRITICAL ARCHITECTURE GUIDELINES
**NEVER violate these principles to maintain system modularity and maintainability:**
### 🎨 CSS Architecture Rules
1. **NEVER modify `css/games.css` for game-specific styles**
- Global CSS is only for shared, reusable components
- Game-specific styles MUST be injected by the game itself
2. **USE the Global CSS Base System:**
```javascript
// ✅ CORRECT: Use global classes with specific overrides
<div class="game-wrapper compact"> // Global base class
<div class="game-hud"> // Global HUD structure
<div class="game-area"> // Global game area
<div class="answer-panel"> // Global answer panel
// ✅ CORRECT: Inject game-specific CSS
injectCSS() {
const styleSheet = document.createElement('style');
styleSheet.textContent = `
.my-game-specific-element { /* Game-only styles */ }
`;
document.head.appendChild(styleSheet);
}
```
3. **CSS Classes Hierarchy:**
- `.game-wrapper` - Base container (full screen)
- `.game-wrapper.compact` - Smaller viewport variant
- `.game-hud` - Top information bar
- `.game-area` - Main play zone
- `.answer-panel` - Bottom interaction zone
- `.answer-btn` - Interactive buttons
4. **❌ FORBIDDEN PATTERNS:**
```css
/* ❌ NEVER add game-specific classes to games.css */
.word-storm-wrapper { }
.my-game-specific-class { }
/* ❌ NEVER hardcode game-specific dimensions in global CSS */
.game-area { height: 600px; } /* This breaks other games */
```
### 🎮 Game Development Standards
1. **Self-Contained Games:**
- Each game MUST inject its own CSS via `injectCSS()`
- Games MUST use global base classes where possible
- Game-specific elements get their own CSS only
2. **Constructor Pattern (REQUIRED):**
```javascript
class MyGame {
constructor({ container, content, onScoreUpdate, onGameEnd }) {
this.injectCSS(); // Inject game-specific styles
this.init(); // Setup interface
}
start() { /* Start game logic */ }
destroy() { /* Cleanup */ }
}
```
3. **Module Registration (REQUIRED):**
```javascript
// MUST be at end of game file
window.GameModules = window.GameModules || {};
window.GameModules.MyGame = MyGame;
```
4. **GameLoader Integration:**
- Add game mapping in `game-loader.js``getModuleName()`
- Add compatibility rules in `content-game-compatibility.js`
- Add game config in `navigation.js` → game configuration
### 🔧 Modification Guidelines
**When adding new games:**
1. ✅ Use existing global CSS classes
2. ✅ Inject only game-specific CSS
3. ✅ Follow constructor pattern
4. ✅ Add to GameLoader mapping
**When modifying existing games:**
1. ✅ Keep changes within the game file
2. ✅ Don't break global CSS compatibility
3. ✅ Test with multiple content types
**When adding global features:**
1. ✅ Add to global CSS base classes
2. ✅ Ensure backward compatibility
3. ✅ Update this documentation
### 🚀 Benefits of This Architecture
- **Modularity**: Each game is self-contained
- **Reusability**: Base classes work for all games
- **Maintainability**: Changes don't break other games
- **Performance**: Only load CSS when game is used
- **Scalability**: Easy to add new games
### ⚠️ Common Mistakes to Avoid
**CSS Architecture Violations:**
```css
/* ❌ DON'T: Adding game-specific styles to games.css */
.word-storm-wrapper { height: 80vh; width: 90vw; }
/* ✅ DO: Use global classes with game-specific injection */
// In game file:
injectCSS() {
// Game-specific overrides only
styleSheet.textContent = `.falling-word { animation: wordGlow 2s; }`;
}
```
**Module Loading Issues:**
```javascript
// ❌ DON'T: Forget GameLoader mapping
// Game won't load because getModuleName() doesn't know about it
// ✅ DO: Add to game-loader.js
const names = {
'my-game': 'MyGame' // Add this mapping
};
```
**HTML Structure Violations:**
```html
<!-- ❌ DON'T: Create custom wrapper classes -->
<div class="my-custom-game-wrapper">
<!-- ✅ DO: Use standard global structure -->
<div class="game-wrapper compact">
<div class="game-hud">
<div class="game-area">
<div class="answer-panel">
```
**Integration Checklist:**
- [ ] Game CSS injected via `injectCSS()`
- [ ] Global classes used for structure
- [ ] GameLoader mapping added
- [ ] Compatibility rules defined
- [ ] Constructor pattern followed
- [ ] Module registration at file end
## Game Module Format
Game modules must export to `window.GameModules` with this pattern:
@ -418,4 +568,187 @@ Host altssh.bitbucket.org
### Push Commands
- Standard push: `git push`
- Set upstream: `git push --set-upstream origin master`
- Set upstream: `git push --set-upstream origin master`
## 🚨 **Developer Guidelines & Common Pitfalls**
**Critical information for future developers to avoid common mistakes and maintain code quality.**
### **🔥 Template Literals Syntax Errors**
**MOST COMMON BUG - Always check this first:**
```javascript
// ❌ FATAL ERROR - Will break entire module
styleSheet.textContent = \`css here\`; // Backslash = SyntaxError
// ✅ CORRECT
styleSheet.textContent = `css here`; // Backtick (grave accent)
```
**How to debug:**
```bash
# Test syntax before browser testing
node -c js/games/your-game.js
# Look for "Invalid or unexpected token" errors
# Usually points to template literal issues
```
### **🎮 Game Development Best Practices**
#### **Required Game Structure:**
```javascript
class NewGame {
constructor({ container, content, onScoreUpdate, onGameEnd }) {
this.injectCSS(); // CSS injection FIRST
this.extractContent(); // Content processing
this.init(); // UI initialization
}
start() {
// Separate start method - NOT in constructor
this.startGameLogic();
}
destroy() {
// Cleanup intervals, event listeners, injected CSS
this.cleanup();
}
}
// REQUIRED: Global module registration
window.GameModules = window.GameModules || {};
window.GameModules.NewGame = NewGame;
```
#### **CSS Architecture - Zero Tolerance Policy:**
```javascript
// ✅ CORRECT: Inject game-specific CSS
injectCSS() {
if (document.getElementById('my-game-styles')) return; // Prevent duplicates
const styleSheet = document.createElement('style');
styleSheet.id = 'my-game-styles';
styleSheet.textContent = `
.my-game-element { color: red; }
.falling-word { animation: myCustomAnimation 2s; }
`;
document.head.appendChild(styleSheet);
}
// ❌ FORBIDDEN: Modifying css/games.css for game-specific styles
// css/games.css should ONLY contain global reusable classes
```
### **🔍 Debug Templates for Quick Testing**
#### **Isolated Game Testing:**
```html
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"></head>
<body>
<div id="container" style="width:800px; height:600px; border:1px solid #ccc;"></div>
<script>
window.logSh = (msg, level) => console.log(`[${level}] ${msg}`);
window.Utils = { storage: { get: () => [], set: () => {} } };
window.GameModules = {};
window.ContentModules = {};
</script>
<script src="js/content/your-content.js"></script>
<script src="js/games/your-game.js"></script>
<script>
try {
const game = new window.GameModules.YourGame({
container: document.getElementById('container'),
content: window.ContentModules.YourContent,
onScoreUpdate: score => console.log('Score:', score),
onGameEnd: score => console.log('Game ended:', score)
});
if (game.start) game.start();
console.log('✅ Game loaded successfully!');
} catch (error) {
console.error('❌ Error:', error.message);
}
</script>
</body>
</html>
```
### **🌐 Interface Language Standards**
**All UI text must be in English:**
```javascript
// ✅ CORRECT
"Score: ${score}"
"Lives: ${lives}"
"Level Up!"
"Game Over"
"Back to Games"
// ❌ FORBIDDEN
"Score: ${score} points" // French
"Vies: ${lives}" // French
"Niveau supérieur!" // French
```
### **📋 Integration Checklist**
**Before committing any new game:**
- [ ] ✅ Syntax check: `node -c js/games/your-game.js`
- [ ] ✅ CSS injected via `injectCSS()` method
- [ ] ✅ No modifications to `css/games.css`
- [ ] ✅ Uses global classes: `.game-wrapper`, `.game-hud`, `.game-area`, `.answer-panel`
- [ ] ✅ GameLoader mapping added in `getModuleName()`
- [ ] ✅ Navigation config updated in `navigation.js`
- [ ] ✅ Constructor pattern followed exactly
- [ ] ✅ Module export at end of file
- [ ] ✅ English-only interface text
- [ ] ✅ Isolated test file created and working
### **⚡ Performance & Architecture Notes**
**Current System Architecture:**
- **CSS:** Global base classes + per-game injection
- **Content:** Auto-discovery + JSON/JS dual support
- **Navigation:** URL-based SPA with dynamic loading
- **Modules:** Dynamic import + backward compatibility
**Critical Files (DO NOT BREAK):**
- `js/core/game-loader.js` - Module name mapping
- `css/games.css` - Global CSS base (game-agnostic only)
- `js/core/navigation.js` - Game configuration and routing
### **🔧 Common Debugging Commands**
```bash
# Check file syntax
node -c js/games/your-game.js
# Check for non-ASCII characters (encoding issues)
grep -P '[^\x00-\x7F]' js/games/your-game.js
# Find template literal issues
grep -n "\\\`" js/games/your-game.js
# Test local server
python3 -m http.server 8000
# Then: http://localhost:8000/?page=play&game=your-game&content=available-content
```
### **🚀 Quick Win Tips**
1. **Copy working game structure** - Use existing games as templates
2. **Test isolated first** - Don't debug through the full app initially
3. **Check browser console** - JavaScript errors are usually obvious
4. **Verify content compatibility** - Make sure your content has the data your game needs
5. **Use global CSS classes** - Don't reinvent layout, build on existing structure
**Remember: Most bugs are simple syntax errors (especially template literals) or missing module registrations. Check these first!** 🎯

View File

@ -1834,4 +1834,209 @@
.whack-game-board {
margin: 0 auto;
}
}
}
/* === GLOBAL GAME SYSTEM STYLES === */
/* Base wrapper for all games - can be overridden by specific games */
.game-wrapper {
height: 100vh;
width: 100vw;
margin: 0 auto;
overflow: hidden;
position: relative;
background: linear-gradient(180deg, #1a1a2e 0%, #16213e 50%, #0f0f23 100%);
display: flex;
flex-direction: column;
}
/* Compact game variant - smaller viewport */
.game-wrapper.compact {
height: 80vh;
width: 90vw;
max-width: 800px;
border-radius: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
}
/* Game HUD - top information bar */
.game-hud {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 80px;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(10px);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
z-index: 100;
border-bottom: 2px solid rgba(59, 130, 246, 0.3);
}
/* Compact HUD variant */
.game-wrapper.compact .game-hud {
height: 60px;
padding: 0 15px;
border-radius: 20px 20px 0 0;
}
/* HUD sections */
.hud-left, .hud-center, .hud-right {
display: flex;
align-items: center;
gap: 20px;
}
.game-wrapper.compact .hud-left,
.game-wrapper.compact .hud-center,
.game-wrapper.compact .hud-right {
gap: 15px;
}
/* HUD elements */
.hud-score, .hud-level, .hud-combo, .hud-lives {
color: white;
font-weight: 600;
font-size: 1rem;
}
.game-wrapper.compact .hud-score,
.game-wrapper.compact .hud-level,
.game-wrapper.compact .hud-combo,
.game-wrapper.compact .hud-lives {
font-size: 0.9rem;
}
.hud-combo.active {
color: #F59E0B;
text-shadow: 0 0 10px rgba(245, 158, 11, 0.5);
}
/* Game area - main play zone */
.game-area {
position: absolute;
top: 80px;
bottom: 120px;
left: 0;
right: 0;
width: 100%;
overflow: hidden;
}
.game-wrapper.compact .game-area {
top: 60px;
bottom: 100px;
}
/* Answer panel - bottom interaction area */
.answer-panel {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 120px;
background: rgba(0, 0, 0, 0.9);
backdrop-filter: blur(15px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
border-top: 2px solid rgba(59, 130, 246, 0.3);
}
.game-wrapper.compact .answer-panel {
height: 100px;
border-top: 3px solid #3B82F6;
border-radius: 0 0 20px 20px;
}
/* Answer buttons */
.answer-buttons {
display: flex;
gap: 20px;
flex-wrap: wrap;
justify-content: center;
}
.game-wrapper.compact .answer-buttons {
gap: 15px;
}
.answer-btn {
background: linear-gradient(135deg, #4F46E5, #7C3AED);
color: white;
border: 2px solid rgba(79, 70, 229, 0.5);
padding: 15px 25px;
border-radius: 15px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
min-width: 120px;
text-align: center;
}
.game-wrapper.compact .answer-btn {
border: 3px solid #FFFFFF;
padding: 20px 30px;
font-size: 1.2rem;
font-weight: 700;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
min-width: 150px;
}
.answer-btn:hover {
transform: translateY(-2px) scale(1.05);
box-shadow: 0 6px 20px rgba(79, 70, 229, 0.4);
}
.game-wrapper.compact .answer-btn:hover {
transform: translateY(-3px) scale(1.05);
box-shadow: 0 8px 25px rgba(79, 70, 229, 0.4);
}
.hud-center {
flex: 1;
justify-content: center;
}
.score, .level, .combo, .lives {
color: white;
font-weight: 600;
font-size: 1.1rem;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
}
.combo.active {
color: #F59E0B;
text-shadow: 0 0 10px #F59E0B;
animation: pulse 1s infinite;
}
.pause-btn {
background: rgba(59, 130, 246, 0.2);
border: 1px solid #3B82F6;
color: white;
padding: 8px 12px;
border-radius: 8px;
cursor: pointer;
font-size: 1.2rem;
transition: all 0.3s ease;
}
.pause-btn:hover {
background: rgba(59, 130, 246, 0.4);
transform: scale(1.1);
}
/* Game-specific CSS moved to individual games */
@keyframes backgroundShift {
0%, 100% { transform: translateX(0) translateY(0); }
25% { transform: translateX(-10px) translateY(-10px); }
50% { transform: translateX(10px) translateY(10px); }
75% { transform: translateX(-5px) translateY(5px); }
}

View File

@ -13,7 +13,8 @@ class ContentGameCompatibility {
'adventure-reader': 50,
'chinese-study': 35,
'story-builder': 35,
'story-reader': 40
'story-reader': 40,
'word-storm': 15
};
}
@ -131,6 +132,9 @@ class ContentGameCompatibility {
case 'story-builder':
return this.calculateStoryBuilderCompat(capabilities);
case 'word-storm':
return this.calculateWordStormCompat(capabilities);
default:
return { compatible: true, score: 50, reason: 'Jeu non spécifiquement analysé' };
}
@ -350,6 +354,44 @@ class ContentGameCompatibility {
};
}
calculateWordStormCompat(capabilities) {
let score = 0;
const reasons = [];
// Word Storm nécessite principalement du vocabulaire
if (capabilities.hasVocabulary && capabilities.vocabularyCount >= 5) {
score += 60;
reasons.push(`${capabilities.vocabularyCount} mots de vocabulaire`);
} else if (capabilities.vocabularyCount > 0) {
score += 30;
reasons.push(`${capabilities.vocabularyCount} mots (peu mais suffisant)`);
}
// Bonus pour plus de vocabulaire
if (capabilities.vocabularyCount >= 20) {
score += 15;
reasons.push('Vocabulaire riche');
}
// Bonus si les mots ont des prononciations
if (capabilities.hasPronunciation) {
score += 10;
reasons.push('Prononciations disponibles');
}
// Word Storm peut fonctionner même avec peu de contenu
if (score === 0 && (capabilities.hasSentences || capabilities.hasDialogues)) {
score = 25;
reasons.push('Peut extraire vocabulaire des phrases/dialogues');
}
return {
compatible: score >= 15,
score,
reason: score >= 15 ? `Compatible: ${reasons.join(', ')}` : 'Nécessite au moins quelques mots de vocabulaire'
};
}
// === UTILITAIRES ===
hasContent(content, type) {
@ -429,7 +471,8 @@ class ContentGameCompatibility {
'adventure-reader': ['Dialogues + contenu narratif riche', 'Histoire cohérente'],
'chinese-study': ['Vocabulaire et phrases chinoises', 'Audio recommandé'],
'story-builder': ['Dialogues OU 5+ phrases', 'Vocabulaire varié'],
'story-reader': ['Textes à lire, dialogues recommandés', 'Contenu narratif']
'story-reader': ['Textes à lire, dialogues recommandés', 'Contenu narratif'],
'word-storm': ['3+ mots de vocabulaire', 'Prononciations recommandées']
};
return requirements[gameType] || ['Contenu de base'];

View File

@ -24,7 +24,8 @@ const GameLoader = {
this.initGame(gameType, gameModule, contentModule);
} catch (error) {
logSh('Erreur lors du chargement du jeu:', error, 'ERROR');
logSh(`❌ Erreur lors du chargement du jeu: ${error.message}`, 'ERROR');
logSh(`❌ Stack trace: ${error.stack}`, 'ERROR');
throw error;
}
},
@ -135,31 +136,55 @@ const GameLoader = {
},
initGame(gameType, GameClass, contentData) {
const gameContainer = document.getElementById('game-container');
const gameTitle = document.getElementById('game-title');
const scoreDisplay = document.getElementById('current-score');
logSh(`🎮 Initializing game: ${gameType}`, 'DEBUG');
// Adapter le contenu avec le JSON Loader pour compatibilité avec les jeux
const adaptedContent = this.jsonLoader.loadContent(contentData);
try {
const gameContainer = document.getElementById('game-container');
const gameTitle = document.getElementById('game-title');
const scoreDisplay = document.getElementById('current-score');
// Mise à jour du titre
const contentName = adaptedContent.name || contentType;
gameTitle.textContent = this.getGameTitle(gameType, contentName);
logSh('🎮 DOM elements found, adapting content...', 'DEBUG');
// Réinitialisation du score
scoreDisplay.textContent = '0';
// Adapter le contenu avec le JSON Loader pour compatibilité avec les jeux
const adaptedContent = this.jsonLoader.loadContent(contentData);
// Création de l'instance de jeu avec contenu enrichi
this.currentGame = new GameClass({
container: gameContainer,
content: adaptedContent,
contentScanner: this.contentScanner, // Passer le scanner pour accès aux métadonnées
onScoreUpdate: (score) => this.updateScore(score),
onGameEnd: (finalScore) => this.handleGameEnd(finalScore)
});
logSh('🎮 Content adapted, updating UI...', 'DEBUG');
// Démarrage du jeu
this.currentGame.start();
// Mise à jour du titre
const contentName = adaptedContent.name || contentType;
gameTitle.textContent = this.getGameTitle(gameType, contentName);
// Réinitialisation du score
scoreDisplay.textContent = '0';
logSh('🎮 Creating game instance...', 'DEBUG');
// Création de l'instance de jeu avec contenu enrichi
this.currentGame = new GameClass({
container: gameContainer,
content: adaptedContent,
contentScanner: this.contentScanner, // Passer le scanner pour accès aux métadonnées
onScoreUpdate: (score) => this.updateScore(score),
onGameEnd: (finalScore) => this.handleGameEnd(finalScore)
});
logSh('🎮 Game instance created successfully', 'DEBUG');
// Démarrage du jeu (seulement si la méthode start existe)
if (typeof this.currentGame.start === 'function') {
logSh('🎮 Starting game with start() method...', 'DEBUG');
this.currentGame.start();
} else {
logSh('🎮 Game auto-started in constructor (no start() method)', 'DEBUG');
}
logSh('✅ Game initialization completed successfully', 'DEBUG');
} catch (error) {
logSh(`❌ Error in initGame: ${error.message}`, 'ERROR');
logSh(`❌ initGame stack: ${error.stack}`, 'ERROR');
throw error;
}
},
updateScore(score) {
@ -296,7 +321,8 @@ const GameLoader = {
'text-reader': 'TextReader',
'adventure-reader': 'AdventureReader',
'chinese-study': 'ChineseStudy',
'story-reader': 'StoryReader'
'story-reader': 'StoryReader',
'word-storm': 'WordStorm'
};
return names[gameType] || gameType;
},

View File

@ -91,6 +91,12 @@ const AppNavigation = {
name: 'Story Reader',
icon: '📚',
description: 'Read long stories with sentence chunking and word-by-word translation'
},
'word-storm': {
enabled: true,
name: 'Word Storm',
icon: '🌪️',
description: 'Catch falling words before they hit the ground!'
}
},
content: {

487
js/games/word-storm.js Normal file
View File

@ -0,0 +1,487 @@
// === 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 = `
<div class="game-wrapper compact">
<div class="game-hud">
<div class="hud-left">
<div class="score">Score: <span id="score">0</span></div>
<div class="level">Level: <span id="level">1</span></div>
</div>
<div class="hud-center">
<div class="lives">Lives: <span id="lives">3</span></div>
<div class="combo">Combo: <span id="combo">0</span></div>
</div>
<div class="hud-right">
<button class="pause-btn" id="pause-btn"> Pause</button>
</div>
</div>
<div class="game-area" id="game-area" style="position: relative; height: 80vh; background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); overflow: hidden;">
</div>
<div class="answer-panel" id="answer-panel">
<div class="answer-buttons" id="answer-buttons">
<!-- Dynamic answer buttons -->
</div>
</div>
</div>
`;
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 =>
`<button class="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
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 = `
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
background: rgba(0,0,0,0.8); color: white; padding: 20px; border-radius: 10px;
text-align: center; z-index: 1000;">
<h2> LEVEL UP! </h2>
<p>Level ${this.level}</p>
</div>
`;
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 = `
<div class="game-error">
<div class="error-content">
<h2>🌪 Word Storm</h2>
<p> No vocabulary found in this content.</p>
<p>This game requires content with vocabulary words.</p>
<button class="back-btn" onclick="window.history.back()"> Back to Games</button>
</div>
</div>
`;
}
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;