diff --git a/CLAUDE.md b/CLAUDE.md
index 82e0f0f..2ad2e72 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -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
+
// Global base class
+
// Global HUD structure
+
// Global game area
+
// 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
+
+
+
+
+
+
+
+
+```
+
+**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`
\ No newline at end of file
+- 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### **🌐 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!** 🎯
\ No newline at end of file
diff --git a/css/games.css b/css/games.css
index 6b49512..68ef0ae 100644
--- a/css/games.css
+++ b/css/games.css
@@ -1834,4 +1834,209 @@
.whack-game-board {
margin: 0 auto;
}
-}
\ No newline at end of file
+}
+
+/* === 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); }
+}
diff --git a/js/core/content-game-compatibility.js b/js/core/content-game-compatibility.js
index c8b069d..7888e52 100644
--- a/js/core/content-game-compatibility.js
+++ b/js/core/content-game-compatibility.js
@@ -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'];
diff --git a/js/core/game-loader.js b/js/core/game-loader.js
index 9eea727..c66240b 100644
--- a/js/core/game-loader.js
+++ b/js/core/game-loader.js
@@ -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;
},
diff --git a/js/core/navigation.js b/js/core/navigation.js
index b429f78..751e537 100644
--- a/js/core/navigation.js
+++ b/js/core/navigation.js
@@ -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: {
diff --git a/js/games/word-storm.js b/js/games/word-storm.js
new file mode 100644
index 0000000..da3b4f4
--- /dev/null
+++ b/js/games/word-storm.js
@@ -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 = `
+