Class_generator/js/games/word-storm.js
StillHammer 638c734578 Add epic animations to Word Storm good/bad responses
🎉 GOOD ANSWER ANIMATIONS:
- Enhanced explosion with color transitions (blue→green→orange→red)
- Screen shake effect for impact feedback
- Floating points popup (+10, +12, etc.) with smooth animation
- Gentle vibration pattern for positive reinforcement

 BAD ANSWER ANIMATIONS:
- Red shake animation for all falling words
- Answer panel flash with red glow effect
- Full screen red overlay flash
- Strong vibration pattern for negative feedback

🎨 TECHNICAL IMPROVEMENTS:
- New CSS keyframes: explode, wrongShake, wrongFlash, screenShake, pointsFloat
- Enhanced correctAnswer() method with screen shake and points popup
- Enhanced wrongAnswer() method with multi-element animations
- Vibration API integration for tactile feedback
- Proper animation cleanup and timing

🎯 UX ENHANCEMENT:
- Much more satisfying and engaging gameplay experience
- Clear visual distinction between success and failure
- Gamification elements that motivate continued play

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-20 12:09:44 +08:00

656 lines
22 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.8s ease-out forwards;
}
.falling-word.wrong-shake {
animation: wrongShake 0.6s ease-in-out forwards;
}
.answer-panel.wrong-flash {
animation: wrongFlash 0.5s ease-in-out;
}
@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) rotate(0deg);
opacity: 1;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3), 0 0 20px rgba(102, 126, 234, 0.4);
}
25% {
transform: translateX(-50%) scale(1.3) rotate(5deg);
opacity: 0.9;
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
box-shadow: 0 8px 25px rgba(16, 185, 129, 0.5), 0 0 40px rgba(16, 185, 129, 0.8);
}
50% {
transform: translateX(-50%) scale(1.5) rotate(-3deg);
opacity: 0.7;
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
box-shadow: 0 12px 35px rgba(245, 158, 11, 0.6), 0 0 60px rgba(245, 158, 11, 0.9);
}
75% {
transform: translateX(-50%) scale(0.8) rotate(2deg);
opacity: 0.4;
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
}
100% {
transform: translateX(-50%) scale(0.1) rotate(0deg);
opacity: 0;
}
}
@keyframes wrongShake {
0%, 100% {
transform: translateX(-50%) scale(1);
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
10%, 30%, 50%, 70%, 90% {
transform: translateX(-60%) scale(0.95);
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
box-shadow: 0 4px 15px rgba(239, 68, 68, 0.6), 0 0 25px rgba(239, 68, 68, 0.8);
}
20%, 40%, 60%, 80% {
transform: translateX(-40%) scale(0.95);
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
box-shadow: 0 4px 15px rgba(239, 68, 68, 0.6), 0 0 25px rgba(239, 68, 68, 0.8);
}
}
@keyframes wrongFlash {
0%, 100% {
background: transparent;
box-shadow: none;
}
50% {
background: rgba(239, 68, 68, 0.4);
box-shadow: 0 0 20px rgba(239, 68, 68, 0.6), inset 0 0 20px rgba(239, 68, 68, 0.3);
}
}
@keyframes screenShake {
0%, 100% { transform: translateX(0); }
10% { transform: translateX(-3px) translateY(1px); }
20% { transform: translateX(3px) translateY(-1px); }
30% { transform: translateX(-2px) translateY(2px); }
40% { transform: translateX(2px) translateY(-2px); }
50% { transform: translateX(-1px) translateY(1px); }
60% { transform: translateX(1px) translateY(-1px); }
70% { transform: translateX(-2px) translateY(0px); }
80% { transform: translateX(2px) translateY(1px); }
90% { transform: translateX(-1px) translateY(-1px); }
}
@keyframes pointsFloat {
0% {
transform: translateY(0) scale(1);
opacity: 1;
}
30% {
transform: translateY(-20px) scale(1.3);
opacity: 1;
}
100% {
transform: translateY(-80px) scale(0.5);
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 with epic explosion
if (fallingWord.element.parentNode) {
fallingWord.element.classList.add('exploding');
// Add screen shake effect
const gameArea = document.getElementById('game-area');
if (gameArea) {
gameArea.style.animation = 'none';
gameArea.offsetHeight; // Force reflow
gameArea.style.animation = 'screenShake 0.3s ease-in-out';
setTimeout(() => {
gameArea.style.animation = '';
}, 300);
}
setTimeout(() => {
if (fallingWord.element.parentNode) {
fallingWord.element.remove();
}
}, 800);
}
// 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 with animation
document.getElementById('score').textContent = this.score;
document.getElementById('combo').textContent = this.combo;
// Add points popup animation
this.showPointsPopup(points, fallingWord.element);
// Vibration feedback (if supported)
if (navigator.vibrate) {
navigator.vibrate([50, 30, 50]);
}
// Level up check
if (this.score > 0 && this.score % 100 === 0) {
this.levelUp();
}
}
wrongAnswer() {
this.combo = 0;
document.getElementById('combo').textContent = this.combo;
// Enhanced wrong answer animation
const answerPanel = document.getElementById('answer-panel');
if (answerPanel) {
answerPanel.classList.add('wrong-flash');
setTimeout(() => {
answerPanel.classList.remove('wrong-flash');
}, 500);
}
// Shake all falling words to show disappointment
this.fallingWords.forEach(fw => {
if (fw.element.parentNode && !fw.element.classList.contains('exploding')) {
fw.element.classList.add('wrong-shake');
setTimeout(() => {
fw.element.classList.remove('wrong-shake');
}, 600);
}
});
// Screen flash red
const gameArea = document.getElementById('game-area');
if (gameArea) {
const overlay = document.createElement('div');
overlay.style.cssText = `
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(239, 68, 68, 0.3);
pointer-events: none;
animation: wrongFlash 0.4s ease-in-out;
z-index: 100;
`;
gameArea.appendChild(overlay);
setTimeout(() => {
if (overlay.parentNode) overlay.remove();
}, 400);
}
// Wrong answer vibration (stronger/longer)
if (navigator.vibrate) {
navigator.vibrate([200, 100, 200, 100, 200]);
}
}
showPointsPopup(points, wordElement) {
const popup = document.createElement('div');
popup.textContent = `+${points}`;
popup.style.cssText = `
position: absolute;
left: ${wordElement.style.left};
top: ${wordElement.offsetTop}px;
font-size: 2rem;
font-weight: bold;
color: #10b981;
pointer-events: none;
z-index: 1000;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
animation: pointsFloat 1.5s ease-out forwards;
`;
const gameArea = document.getElementById('game-area');
if (gameArea) {
gameArea.appendChild(popup);
setTimeout(() => {
if (popup.parentNode) popup.remove();
}, 1500);
}
}
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="AppNavigation.navigateTo('games')">← 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;