From dacc7e98a152d7e7d42e323eb8f02dc2c965902a Mon Sep 17 00:00:00 2001 From: StillHammer Date: Fri, 19 Sep 2025 01:55:08 +0800 Subject: [PATCH] Implement Word Storm game and refactor CSS architecture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CLAUDE.md | 335 +++++++++++++++++- css/games.css | 207 ++++++++++- js/core/content-game-compatibility.js | 47 ++- js/core/game-loader.js | 70 ++-- js/core/navigation.js | 6 + js/games/word-storm.js | 487 ++++++++++++++++++++++++++ 6 files changed, 1126 insertions(+), 26 deletions(-) create mode 100644 js/games/word-storm.js 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 = ` +
+
+
+
Score: 0
+
Level: 1
+
+
+
Lives: 3
+
Combo: 0
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ `; + + this.setupEventListeners(); + this.generateAnswerOptions(); + } + + setupEventListeners() { + const pauseBtn = document.getElementById('pause-btn'); + if (pauseBtn) { + pauseBtn.addEventListener('click', () => this.togglePause()); + } + + // Answer button clicks + document.addEventListener('click', (e) => { + if (e.target.classList.contains('answer-btn')) { + const answer = e.target.textContent; + this.checkAnswer(answer); + } + }); + + // Keyboard support + document.addEventListener('keydown', (e) => { + if (e.key >= '1' && e.key <= '4') { + const btnIndex = parseInt(e.key) - 1; + const buttons = document.querySelectorAll('.answer-btn'); + if (buttons[btnIndex]) { + buttons[btnIndex].click(); + } + } + }); + } + + start() { + logSh('Word Storm game started', 'INFO'); + this.startSpawning(); + } + + startSpawning() { + this.spawnInterval = setInterval(() => { + if (!this.isGamePaused && !this.isGameOver) { + this.spawnFallingWord(); + } + }, this.spawnRate); + } + + spawnFallingWord() { + if (this.vocabulary.length === 0) return; + + const word = this.vocabulary[this.currentWordIndex % this.vocabulary.length]; + this.currentWordIndex++; + + const gameArea = document.getElementById('game-area'); + const wordElement = document.createElement('div'); + wordElement.className = 'falling-word'; + wordElement.textContent = word.original; + wordElement.style.left = Math.random() * 80 + 10 + '%'; + wordElement.style.top = '-60px'; + + gameArea.appendChild(wordElement); + + this.fallingWords.push({ + element: wordElement, + word: word, + startTime: Date.now() + }); + + // Generate new answer options when word spawns + this.generateAnswerOptions(); + + // Animate falling + this.animateFalling(wordElement); + + // Remove after lifetime + setTimeout(() => { + if (wordElement.parentNode) { + this.missWord(wordElement); + } + }, this.wordLifetime); + } + + animateFalling(wordElement) { + wordElement.style.transition = `top ${this.fallSpeed}ms linear`; + setTimeout(() => { + wordElement.style.top = '100vh'; + }, 50); + } + + generateAnswerOptions() { + if (this.vocabulary.length === 0) return; + + const buttons = []; + const correctWord = this.fallingWords.length > 0 ? + this.fallingWords[this.fallingWords.length - 1].word : + this.vocabulary[0]; + + // Add correct answer + buttons.push(correctWord.translation); + + // Add 3 random incorrect answers + while (buttons.length < 4) { + const randomWord = this.vocabulary[Math.floor(Math.random() * this.vocabulary.length)]; + if (!buttons.includes(randomWord.translation)) { + buttons.push(randomWord.translation); + } + } + + // Shuffle buttons + this.shuffleArray(buttons); + + // Update answer panel + const answerButtons = document.getElementById('answer-buttons'); + if (answerButtons) { + answerButtons.innerHTML = buttons.map(answer => + `` + ).join(''); + } + } + + checkAnswer(selectedAnswer) { + const activeFallingWords = this.fallingWords.filter(fw => fw.element.parentNode); + + for (let i = 0; i < activeFallingWords.length; i++) { + const fallingWord = activeFallingWords[i]; + if (fallingWord.word.translation === selectedAnswer) { + this.correctAnswer(fallingWord); + return; + } + } + + // Wrong answer + this.wrongAnswer(); + } + + correctAnswer(fallingWord) { + // Remove from game + if (fallingWord.element.parentNode) { + fallingWord.element.classList.add('exploding'); + setTimeout(() => { + if (fallingWord.element.parentNode) { + fallingWord.element.remove(); + } + }, 600); + } + + // Remove from tracking + this.fallingWords = this.fallingWords.filter(fw => fw !== fallingWord); + + // Update score + this.combo++; + const points = 10 + (this.combo * 2); + this.score += points; + this.onScoreUpdate(this.score); + + // Update display + document.getElementById('score').textContent = this.score; + document.getElementById('combo').textContent = this.combo; + + // Level up check + if (this.score > 0 && this.score % 100 === 0) { + this.levelUp(); + } + } + + wrongAnswer() { + this.combo = 0; + document.getElementById('combo').textContent = this.combo; + + // Flash effect + const answerPanel = document.getElementById('answer-panel'); + if (answerPanel) { + answerPanel.style.background = 'rgba(239, 68, 68, 0.3)'; + setTimeout(() => { + answerPanel.style.background = ''; + }, 300); + } + } + + missWord(wordElement) { + // Remove word + if (wordElement.parentNode) { + wordElement.remove(); + } + + // Remove from tracking + this.fallingWords = this.fallingWords.filter(fw => fw.element !== wordElement); + + // Lose life + this.lives--; + this.combo = 0; + + document.getElementById('lives').textContent = this.lives; + document.getElementById('combo').textContent = this.combo; + + if (this.lives <= 0) { + this.gameOver(); + } + } + + levelUp() { + this.level++; + document.getElementById('level').textContent = this.level; + + // Increase difficulty + this.fallSpeed = Math.max(1000, this.fallSpeed * 0.9); + this.spawnRate = Math.max(800, this.spawnRate * 0.95); + + // Restart intervals with new timing + if (this.spawnInterval) { + clearInterval(this.spawnInterval); + this.startSpawning(); + } + + // Show level up message + const gameArea = document.getElementById('game-area'); + const levelUpMsg = document.createElement('div'); + levelUpMsg.innerHTML = ` +
+

⚡ LEVEL UP! ⚡

+

Level ${this.level}

+
+ `; + gameArea.appendChild(levelUpMsg); + + setTimeout(() => { + if (levelUpMsg.parentNode) { + levelUpMsg.remove(); + } + }, 2000); + } + + togglePause() { + this.isGamePaused = !this.isGamePaused; + const pauseBtn = document.getElementById('pause-btn'); + if (pauseBtn) { + pauseBtn.textContent = this.isGamePaused ? '▶️ Resume' : '⏸️ Pause'; + } + } + + gameOver() { + this.isGameOver = true; + + // Clear intervals + if (this.spawnInterval) { + clearInterval(this.spawnInterval); + } + + // Clear falling words + this.fallingWords.forEach(fw => { + if (fw.element.parentNode) { + fw.element.remove(); + } + }); + + this.onGameEnd(this.score); + } + + showNoVocabularyMessage() { + this.container.innerHTML = ` +
+
+

🌪️ Word Storm

+

❌ No vocabulary found in this content.

+

This game requires content with vocabulary words.

+ +
+
+ `; + } + + shuffleArray(array) { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + } + + destroy() { + if (this.spawnInterval) { + clearInterval(this.spawnInterval); + } + + // Remove CSS + const styleSheet = document.getElementById('word-storm-styles'); + if (styleSheet) { + styleSheet.remove(); + } + + logSh('Word Storm destroyed', 'INFO'); + } +} + +// Export to global namespace +window.GameModules = window.GameModules || {}; +window.GameModules.WordStorm = WordStormGame; \ No newline at end of file