- Enhanced Story Reader with text-to-story conversion methods - Added support for simple texts and sentences in Story Reader - Removed Text Reader game file (js/games/text-reader.js) - Updated all configuration files to remove Text Reader references - Modified game compatibility system to use Story Reader instead - Updated test fixtures to reflect game changes - Cleaned up debug/test HTML files 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
487 lines
16 KiB
JavaScript
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="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; |