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
333
CLAUDE.md
333
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
|
||||
<div class="game-wrapper compact"> // Global base class
|
||||
<div class="game-hud"> // Global HUD structure
|
||||
<div class="game-area"> // Global game area
|
||||
<div class="answer-panel"> // Global answer panel
|
||||
|
||||
// ✅ CORRECT: Inject game-specific CSS
|
||||
injectCSS() {
|
||||
const styleSheet = document.createElement('style');
|
||||
styleSheet.textContent = `
|
||||
.my-game-specific-element { /* Game-only styles */ }
|
||||
`;
|
||||
document.head.appendChild(styleSheet);
|
||||
}
|
||||
```
|
||||
|
||||
3. **CSS Classes Hierarchy:**
|
||||
- `.game-wrapper` - Base container (full screen)
|
||||
- `.game-wrapper.compact` - Smaller viewport variant
|
||||
- `.game-hud` - Top information bar
|
||||
- `.game-area` - Main play zone
|
||||
- `.answer-panel` - Bottom interaction zone
|
||||
- `.answer-btn` - Interactive buttons
|
||||
|
||||
4. **❌ FORBIDDEN PATTERNS:**
|
||||
```css
|
||||
/* ❌ NEVER add game-specific classes to games.css */
|
||||
.word-storm-wrapper { }
|
||||
.my-game-specific-class { }
|
||||
|
||||
/* ❌ NEVER hardcode game-specific dimensions in global CSS */
|
||||
.game-area { height: 600px; } /* This breaks other games */
|
||||
```
|
||||
|
||||
### 🎮 Game Development Standards
|
||||
|
||||
1. **Self-Contained Games:**
|
||||
- Each game MUST inject its own CSS via `injectCSS()`
|
||||
- Games MUST use global base classes where possible
|
||||
- Game-specific elements get their own CSS only
|
||||
|
||||
2. **Constructor Pattern (REQUIRED):**
|
||||
```javascript
|
||||
class MyGame {
|
||||
constructor({ container, content, onScoreUpdate, onGameEnd }) {
|
||||
this.injectCSS(); // Inject game-specific styles
|
||||
this.init(); // Setup interface
|
||||
}
|
||||
|
||||
start() { /* Start game logic */ }
|
||||
destroy() { /* Cleanup */ }
|
||||
}
|
||||
```
|
||||
|
||||
3. **Module Registration (REQUIRED):**
|
||||
```javascript
|
||||
// MUST be at end of game file
|
||||
window.GameModules = window.GameModules || {};
|
||||
window.GameModules.MyGame = MyGame;
|
||||
```
|
||||
|
||||
4. **GameLoader Integration:**
|
||||
- Add game mapping in `game-loader.js` → `getModuleName()`
|
||||
- Add compatibility rules in `content-game-compatibility.js`
|
||||
- Add game config in `navigation.js` → game configuration
|
||||
|
||||
### 🔧 Modification Guidelines
|
||||
|
||||
**When adding new games:**
|
||||
1. ✅ Use existing global CSS classes
|
||||
2. ✅ Inject only game-specific CSS
|
||||
3. ✅ Follow constructor pattern
|
||||
4. ✅ Add to GameLoader mapping
|
||||
|
||||
**When modifying existing games:**
|
||||
1. ✅ Keep changes within the game file
|
||||
2. ✅ Don't break global CSS compatibility
|
||||
3. ✅ Test with multiple content types
|
||||
|
||||
**When adding global features:**
|
||||
1. ✅ Add to global CSS base classes
|
||||
2. ✅ Ensure backward compatibility
|
||||
3. ✅ Update this documentation
|
||||
|
||||
### 🚀 Benefits of This Architecture
|
||||
|
||||
- **Modularity**: Each game is self-contained
|
||||
- **Reusability**: Base classes work for all games
|
||||
- **Maintainability**: Changes don't break other games
|
||||
- **Performance**: Only load CSS when game is used
|
||||
- **Scalability**: Easy to add new games
|
||||
|
||||
### ⚠️ Common Mistakes to Avoid
|
||||
|
||||
**CSS Architecture Violations:**
|
||||
```css
|
||||
/* ❌ DON'T: Adding game-specific styles to games.css */
|
||||
.word-storm-wrapper { height: 80vh; width: 90vw; }
|
||||
|
||||
/* ✅ DO: Use global classes with game-specific injection */
|
||||
// In game file:
|
||||
injectCSS() {
|
||||
// Game-specific overrides only
|
||||
styleSheet.textContent = `.falling-word { animation: wordGlow 2s; }`;
|
||||
}
|
||||
```
|
||||
|
||||
**Module Loading Issues:**
|
||||
```javascript
|
||||
// ❌ DON'T: Forget GameLoader mapping
|
||||
// Game won't load because getModuleName() doesn't know about it
|
||||
|
||||
// ✅ DO: Add to game-loader.js
|
||||
const names = {
|
||||
'my-game': 'MyGame' // Add this mapping
|
||||
};
|
||||
```
|
||||
|
||||
**HTML Structure Violations:**
|
||||
```html
|
||||
<!-- ❌ DON'T: Create custom wrapper classes -->
|
||||
<div class="my-custom-game-wrapper">
|
||||
|
||||
<!-- ✅ DO: Use standard global structure -->
|
||||
<div class="game-wrapper compact">
|
||||
<div class="game-hud">
|
||||
<div class="game-area">
|
||||
<div class="answer-panel">
|
||||
```
|
||||
|
||||
**Integration Checklist:**
|
||||
- [ ] Game CSS injected via `injectCSS()`
|
||||
- [ ] Global classes used for structure
|
||||
- [ ] GameLoader mapping added
|
||||
- [ ] Compatibility rules defined
|
||||
- [ ] Constructor pattern followed
|
||||
- [ ] Module registration at file end
|
||||
|
||||
## Game Module Format
|
||||
|
||||
Game modules must export to `window.GameModules` with this pattern:
|
||||
@ -419,3 +569,186 @@ Host altssh.bitbucket.org
|
||||
### Push Commands
|
||||
- Standard push: `git push`
|
||||
- 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!** 🎯
|
||||
205
css/games.css
205
css/games.css
@ -1835,3 +1835,208 @@
|
||||
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,
|
||||
'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'];
|
||||
|
||||
@ -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;
|
||||
},
|
||||
|
||||
@ -91,6 +91,12 @@ const AppNavigation = {
|
||||
name: 'Story Reader',
|
||||
icon: '📚',
|
||||
description: 'Read long stories with sentence chunking and word-by-word translation'
|
||||
},
|
||||
'word-storm': {
|
||||
enabled: true,
|
||||
name: 'Word Storm',
|
||||
icon: '🌪️',
|
||||
description: 'Catch falling words before they hit the ground!'
|
||||
}
|
||||
},
|
||||
content: {
|
||||
|
||||
487
js/games/word-storm.js
Normal file
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