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:
parent
fe7153d28b
commit
dacc7e98a1
335
CLAUDE.md
335
CLAUDE.md
@ -263,6 +263,156 @@ The platform automatically adapts available games and exercises based on content
|
|||||||
→ Unlock: Advanced difficulty levels, speed challenges
|
→ 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 Module Format
|
||||||
|
|
||||||
Game modules must export to `window.GameModules` with this pattern:
|
Game modules must export to `window.GameModules` with this pattern:
|
||||||
@ -418,4 +568,187 @@ Host altssh.bitbucket.org
|
|||||||
|
|
||||||
### Push Commands
|
### Push Commands
|
||||||
- Standard push: `git push`
|
- 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!** 🎯
|
||||||
207
css/games.css
207
css/games.css
@ -1834,4 +1834,209 @@
|
|||||||
.whack-game-board {
|
.whack-game-board {
|
||||||
margin: 0 auto;
|
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); }
|
||||||
|
}
|
||||||
|
|||||||
@ -13,7 +13,8 @@ class ContentGameCompatibility {
|
|||||||
'adventure-reader': 50,
|
'adventure-reader': 50,
|
||||||
'chinese-study': 35,
|
'chinese-study': 35,
|
||||||
'story-builder': 35,
|
'story-builder': 35,
|
||||||
'story-reader': 40
|
'story-reader': 40,
|
||||||
|
'word-storm': 15
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,6 +132,9 @@ class ContentGameCompatibility {
|
|||||||
case 'story-builder':
|
case 'story-builder':
|
||||||
return this.calculateStoryBuilderCompat(capabilities);
|
return this.calculateStoryBuilderCompat(capabilities);
|
||||||
|
|
||||||
|
case 'word-storm':
|
||||||
|
return this.calculateWordStormCompat(capabilities);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return { compatible: true, score: 50, reason: 'Jeu non spécifiquement analysé' };
|
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 ===
|
// === UTILITAIRES ===
|
||||||
|
|
||||||
hasContent(content, type) {
|
hasContent(content, type) {
|
||||||
@ -429,7 +471,8 @@ class ContentGameCompatibility {
|
|||||||
'adventure-reader': ['Dialogues + contenu narratif riche', 'Histoire cohérente'],
|
'adventure-reader': ['Dialogues + contenu narratif riche', 'Histoire cohérente'],
|
||||||
'chinese-study': ['Vocabulaire et phrases chinoises', 'Audio recommandé'],
|
'chinese-study': ['Vocabulaire et phrases chinoises', 'Audio recommandé'],
|
||||||
'story-builder': ['Dialogues OU 5+ phrases', 'Vocabulaire varié'],
|
'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'];
|
return requirements[gameType] || ['Contenu de base'];
|
||||||
|
|||||||
@ -24,7 +24,8 @@ const GameLoader = {
|
|||||||
this.initGame(gameType, gameModule, contentModule);
|
this.initGame(gameType, gameModule, contentModule);
|
||||||
|
|
||||||
} catch (error) {
|
} 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;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -135,31 +136,55 @@ const GameLoader = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
initGame(gameType, GameClass, contentData) {
|
initGame(gameType, GameClass, contentData) {
|
||||||
const gameContainer = document.getElementById('game-container');
|
logSh(`🎮 Initializing game: ${gameType}`, 'DEBUG');
|
||||||
const gameTitle = document.getElementById('game-title');
|
|
||||||
const scoreDisplay = document.getElementById('current-score');
|
|
||||||
|
|
||||||
// Adapter le contenu avec le JSON Loader pour compatibilité avec les jeux
|
try {
|
||||||
const adaptedContent = this.jsonLoader.loadContent(contentData);
|
const gameContainer = document.getElementById('game-container');
|
||||||
|
const gameTitle = document.getElementById('game-title');
|
||||||
|
const scoreDisplay = document.getElementById('current-score');
|
||||||
|
|
||||||
// Mise à jour du titre
|
logSh('🎮 DOM elements found, adapting content...', 'DEBUG');
|
||||||
const contentName = adaptedContent.name || contentType;
|
|
||||||
gameTitle.textContent = this.getGameTitle(gameType, contentName);
|
|
||||||
|
|
||||||
// Réinitialisation du score
|
// Adapter le contenu avec le JSON Loader pour compatibilité avec les jeux
|
||||||
scoreDisplay.textContent = '0';
|
const adaptedContent = this.jsonLoader.loadContent(contentData);
|
||||||
|
|
||||||
// Création de l'instance de jeu avec contenu enrichi
|
logSh('🎮 Content adapted, updating UI...', 'DEBUG');
|
||||||
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)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Démarrage du jeu
|
// Mise à jour du titre
|
||||||
this.currentGame.start();
|
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) {
|
updateScore(score) {
|
||||||
@ -296,7 +321,8 @@ const GameLoader = {
|
|||||||
'text-reader': 'TextReader',
|
'text-reader': 'TextReader',
|
||||||
'adventure-reader': 'AdventureReader',
|
'adventure-reader': 'AdventureReader',
|
||||||
'chinese-study': 'ChineseStudy',
|
'chinese-study': 'ChineseStudy',
|
||||||
'story-reader': 'StoryReader'
|
'story-reader': 'StoryReader',
|
||||||
|
'word-storm': 'WordStorm'
|
||||||
};
|
};
|
||||||
return names[gameType] || gameType;
|
return names[gameType] || gameType;
|
||||||
},
|
},
|
||||||
|
|||||||
@ -91,6 +91,12 @@ const AppNavigation = {
|
|||||||
name: 'Story Reader',
|
name: 'Story Reader',
|
||||||
icon: '📚',
|
icon: '📚',
|
||||||
description: 'Read long stories with sentence chunking and word-by-word translation'
|
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: {
|
content: {
|
||||||
|
|||||||
487
js/games/word-storm.js
Normal file
487
js/games/word-storm.js
Normal 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;
|
||||||
Loading…
Reference in New Issue
Block a user