Class_generator/js/games/word-storm.js
StillHammer dacc7e98a1 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>
2025-09-19 01:55:08 +08:00

487 lines
16 KiB
JavaScript

// === 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;