Class_generator/js/games/letter-discovery.js
StillHammer e50dd624a0 Add comprehensive game suite with TTS integration and content modules
🎮 NEW GAMES:
- River Run: Endless runner with word collection and guaranteed target spawning
- Grammar Discovery: Focused grammar learning with 8-step rotation cycles
- Letter Discovery: Letter-first alphabet learning with progression system
- Enhanced Word Discovery: Shuffled practice mode with image support and auto-TTS

📚 NEW CONTENT MODULES:
- WTA1B-1: English letters U,V,T with pets vocabulary and Chinese translation
- SBS-1: English "To Be" introduction with comprehensive grammar lessons
- French Beginner Story: Story content for English speakers learning French

🔊 TTS ENHANCEMENTS:
- Story Reader: Multi-story support with TTS for sentences and individual words
- Adventure Reader: Auto-TTS for vocabulary popups and sentence modals
- Word Discovery: Immediate TTS playback with difficulty-based speed control
- Integrated SettingsManager compatibility across all games

🎯 GAMEPLAY IMPROVEMENTS:
- River Run: Target word guaranteed within 10 spawns, progressive spacing
- Story Reader: Story selector dropdown with independent progress tracking
- Adventure Reader: Fixed HUD overlap issue with proper viewport spacing
- Enhanced punctuation preservation in Story Reader word parsing

 SYSTEM UPDATES:
- Content scanner integration for all new modules
- Game loader mappings for seamless content discovery
- Simplified content titles: "WTA1B-1" and "SBS-1" for easy identification
- Comprehensive test files for isolated game development

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

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

774 lines
26 KiB
JavaScript

// === LETTER DISCOVERY GAME ===
// Discover letters first, then explore words that start with each letter
class LetterDiscovery {
constructor({ container, content, onScoreUpdate, onGameEnd }) {
this.container = container;
this.content = content;
this.onScoreUpdate = onScoreUpdate;
this.onGameEnd = onGameEnd;
// Game state
this.currentPhase = 'letter-discovery'; // letter-discovery, word-exploration, practice
this.currentLetterIndex = 0;
this.discoveredLetters = [];
this.currentLetter = null;
this.currentWordIndex = 0;
this.discoveredWords = [];
this.score = 0;
this.lives = 3;
// Content processing
this.letters = [];
this.letterWords = {}; // Map letter -> words starting with that letter
// Practice system
this.practiceLevel = 1;
this.practiceRound = 0;
this.maxPracticeRounds = 8;
this.practiceCorrectAnswers = 0;
this.practiceErrors = 0;
this.currentPracticeItems = [];
this.injectCSS();
this.extractContent();
this.init();
}
injectCSS() {
if (document.getElementById('letter-discovery-styles')) return;
const styleSheet = document.createElement('style');
styleSheet.id = 'letter-discovery-styles';
styleSheet.textContent = `
.letter-discovery-wrapper {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
position: relative;
overflow-y: auto;
}
.letter-discovery-hud {
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(255,255,255,0.1);
padding: 15px 20px;
border-radius: 15px;
backdrop-filter: blur(10px);
margin-bottom: 20px;
flex-wrap: wrap;
gap: 10px;
}
.hud-group {
display: flex;
align-items: center;
gap: 15px;
}
.hud-item {
color: white;
font-weight: bold;
font-size: 1.1em;
}
.phase-indicator {
background: rgba(255,255,255,0.2);
padding: 8px 16px;
border-radius: 20px;
font-size: 0.9em;
color: white;
backdrop-filter: blur(5px);
}
.letter-discovery-main {
background: rgba(255,255,255,0.1);
border-radius: 20px;
padding: 30px;
backdrop-filter: blur(10px);
min-height: 70vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.game-content {
width: 100%;
max-width: 900px;
text-align: center;
}
/* Letter Display Styles */
.letter-card {
background: rgba(255,255,255,0.95);
border-radius: 25px;
padding: 60px 40px;
margin: 30px auto;
max-width: 400px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
transform: scale(0.8);
animation: letterAppear 0.8s ease-out forwards;
}
@keyframes letterAppear {
to { transform: scale(1); }
}
.letter-display {
font-size: 8em;
font-weight: bold;
color: #667eea;
margin-bottom: 20px;
text-shadow: 0 4px 8px rgba(0,0,0,0.1);
font-family: 'Arial Black', Arial, sans-serif;
}
.letter-info {
font-size: 1.5em;
color: #333;
margin-bottom: 15px;
}
.letter-pronunciation {
font-size: 1.2em;
color: #666;
font-style: italic;
margin-bottom: 25px;
}
.letter-controls {
display: flex;
gap: 15px;
justify-content: center;
margin-top: 30px;
}
/* Word Exploration Styles */
.word-exploration-header {
background: rgba(255,255,255,0.1);
padding: 20px;
border-radius: 15px;
margin-bottom: 30px;
backdrop-filter: blur(5px);
}
.exploring-letter {
font-size: 3em;
color: white;
margin-bottom: 10px;
font-weight: bold;
}
.word-progress {
color: rgba(255,255,255,0.8);
font-size: 1.1em;
}
.word-card {
background: rgba(255,255,255,0.95);
border-radius: 20px;
padding: 40px 30px;
margin: 25px auto;
max-width: 500px;
box-shadow: 0 15px 30px rgba(0,0,0,0.1);
transform: translateY(20px);
animation: wordSlideIn 0.6s ease-out forwards;
}
@keyframes wordSlideIn {
to { transform: translateY(0); }
}
.word-text {
font-size: 2.5em;
color: #667eea;
margin-bottom: 15px;
font-weight: bold;
}
.word-translation {
font-size: 1.3em;
color: #333;
margin-bottom: 10px;
}
.word-pronunciation {
font-size: 1.1em;
color: #666;
font-style: italic;
}
/* Practice Challenge Styles */
.practice-challenge {
text-align: center;
margin-bottom: 30px;
}
.challenge-text {
font-size: 1.8em;
color: white;
margin-bottom: 25px;
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
.practice-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
max-width: 800px;
margin: 0 auto;
}
.practice-option {
background: rgba(255,255,255,0.9);
border: none;
border-radius: 15px;
padding: 20px;
font-size: 1.2em;
cursor: pointer;
transition: all 0.3s ease;
color: #333;
font-weight: 500;
}
.practice-option:hover {
background: rgba(255,255,255,1);
transform: translateY(-3px);
box-shadow: 0 8px 20px rgba(0,0,0,0.2);
}
.practice-option.correct {
background: #4CAF50;
color: white;
animation: correctPulse 0.6s ease;
}
.practice-option.incorrect {
background: #F44336;
color: white;
animation: incorrectShake 0.6s ease;
}
@keyframes correctPulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
@keyframes incorrectShake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
.practice-stats {
display: flex;
justify-content: space-around;
margin-top: 20px;
color: white;
font-size: 1.1em;
}
.stat-item {
text-align: center;
padding: 10px;
background: rgba(255,255,255,0.1);
border-radius: 10px;
backdrop-filter: blur(5px);
}
/* Control Buttons */
.discovery-btn {
background: linear-gradient(45deg, #667eea, #764ba2);
color: white;
border: none;
padding: 15px 30px;
border-radius: 25px;
font-size: 1.1em;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
margin: 0 10px;
}
.discovery-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(0,0,0,0.3);
}
.discovery-btn:active {
transform: translateY(0);
}
.audio-btn {
background: none;
border: none;
font-size: 2em;
cursor: pointer;
color: #667eea;
margin-left: 15px;
transition: all 0.3s ease;
}
.audio-btn:hover {
transform: scale(1.2);
color: #764ba2;
}
/* Completion Message */
.completion-message {
text-align: center;
padding: 40px;
background: rgba(255,255,255,0.1);
border-radius: 20px;
backdrop-filter: blur(10px);
color: white;
}
.completion-title {
font-size: 2.5em;
margin-bottom: 20px;
color: #00ff88;
text-shadow: 0 2px 10px rgba(0,255,136,0.3);
}
.completion-stats {
font-size: 1.3em;
margin-bottom: 30px;
line-height: 1.6;
}
/* Responsive Design */
@media (max-width: 768px) {
.letter-discovery-wrapper {
padding: 15px;
}
.letter-display {
font-size: 6em;
}
.word-text {
font-size: 2em;
}
.challenge-text {
font-size: 1.5em;
}
.practice-grid {
grid-template-columns: 1fr;
}
}
`;
document.head.appendChild(styleSheet);
}
extractContent() {
logSh('🔍 Letter Discovery - Extracting content...', 'INFO');
// Check if content has letter structure
if (this.content.letters) {
this.letters = Object.keys(this.content.letters);
this.letterWords = this.content.letters;
logSh(`📝 Found ${this.letters.length} letters with words`, 'INFO');
} else {
// Fallback: Create letter structure from vocabulary
this.generateLetterStructure();
}
if (this.letters.length === 0) {
throw new Error('No letters found in content');
}
logSh(`🎯 Letter Discovery ready: ${this.letters.length} letters`, 'INFO');
}
generateLetterStructure() {
logSh('🔧 Generating letter structure from vocabulary...', 'INFO');
const letterMap = {};
if (this.content.vocabulary) {
Object.keys(this.content.vocabulary).forEach(word => {
const firstLetter = word.charAt(0).toUpperCase();
if (!letterMap[firstLetter]) {
letterMap[firstLetter] = [];
}
const wordData = this.content.vocabulary[word];
letterMap[firstLetter].push({
word: word,
translation: typeof wordData === 'string' ? wordData : wordData.translation || wordData.user_language,
pronunciation: wordData.pronunciation || wordData.prononciation,
type: wordData.type,
image: wordData.image,
audioFile: wordData.audioFile
});
});
}
this.letters = Object.keys(letterMap).sort();
this.letterWords = letterMap;
logSh(`📝 Generated ${this.letters.length} letters from vocabulary`, 'INFO');
}
init() {
this.container.innerHTML = `
<div class="letter-discovery-wrapper">
<div class="letter-discovery-hud">
<div class="hud-group">
<div class="hud-item">Score: <span id="score-display">${this.score}</span></div>
<div class="hud-item">Lives: <span id="lives-display">${this.lives}</span></div>
</div>
<div class="phase-indicator" id="phase-indicator">Letter Discovery</div>
<div class="hud-group">
<div class="hud-item">Progress: <span id="progress-display">0/${this.letters.length}</span></div>
</div>
</div>
<div class="letter-discovery-main">
<div class="game-content" id="game-content">
<!-- Dynamic content here -->
</div>
</div>
</div>
`;
this.updateHUD();
}
start() {
this.showLetterCard();
}
updateHUD() {
const scoreDisplay = document.getElementById('score-display');
const livesDisplay = document.getElementById('lives-display');
const progressDisplay = document.getElementById('progress-display');
const phaseIndicator = document.getElementById('phase-indicator');
if (scoreDisplay) scoreDisplay.textContent = this.score;
if (livesDisplay) livesDisplay.textContent = this.lives;
if (this.currentPhase === 'letter-discovery') {
if (progressDisplay) progressDisplay.textContent = `${this.currentLetterIndex}/${this.letters.length}`;
if (phaseIndicator) phaseIndicator.textContent = 'Letter Discovery';
} else if (this.currentPhase === 'word-exploration') {
if (progressDisplay) progressDisplay.textContent = `${this.currentWordIndex}/${this.letterWords[this.currentLetter].length}`;
if (phaseIndicator) phaseIndicator.textContent = `Exploring Letter "${this.currentLetter}"`;
} else if (this.currentPhase === 'practice') {
if (progressDisplay) progressDisplay.textContent = `Round ${this.practiceRound + 1}/${this.maxPracticeRounds}`;
if (phaseIndicator) phaseIndicator.textContent = `Practice - Level ${this.practiceLevel}`;
}
}
showLetterCard() {
if (this.currentLetterIndex >= this.letters.length) {
this.showCompletion();
return;
}
const letter = this.letters[this.currentLetterIndex];
const gameContent = document.getElementById('game-content');
gameContent.innerHTML = `
<div class="letter-card">
<div class="letter-display">${letter}</div>
<div class="letter-info">Letter "${letter}"</div>
<div class="letter-pronunciation">${this.getLetterPronunciation(letter)}</div>
<div class="letter-controls">
<button class="discovery-btn" onclick="window.currentLetterGame.discoverLetter()">
🔍 Discover Letter
</button>
<button class="audio-btn" onclick="window.currentLetterGame.playLetterSound('${letter}')">
🔊
</button>
</div>
</div>
`;
// Store reference for button callbacks
window.currentLetterGame = this;
// Auto-play letter sound
setTimeout(() => this.playLetterSound(letter), 500);
}
getLetterPronunciation(letter) {
// Basic letter pronunciation guide
const pronunciations = {
'A': 'ay', 'B': 'bee', 'C': 'see', 'D': 'dee', 'E': 'ee',
'F': 'ef', 'G': 'gee', 'H': 'aych', 'I': 'eye', 'J': 'jay',
'K': 'kay', 'L': 'el', 'M': 'em', 'N': 'en', 'O': 'oh',
'P': 'pee', 'Q': 'cue', 'R': 'ar', 'S': 'ess', 'T': 'tee',
'U': 'you', 'V': 'vee', 'W': 'double-you', 'X': 'ex', 'Y': 'why', 'Z': 'zee'
};
return pronunciations[letter] || letter.toLowerCase();
}
playLetterSound(letter) {
if (window.SettingsManager && window.SettingsManager.speak) {
const speed = 0.8; // Slower for letters
window.SettingsManager.speak(letter, {
lang: this.content.language || 'en-US',
rate: speed
}).catch(error => {
console.warn('🔊 TTS failed for letter:', error);
});
}
}
discoverLetter() {
const letter = this.letters[this.currentLetterIndex];
this.discoveredLetters.push(letter);
this.score += 10;
this.onScoreUpdate(this.score);
// Start word exploration for this letter
this.currentLetter = letter;
this.currentPhase = 'word-exploration';
this.currentWordIndex = 0;
this.updateHUD();
this.showWordExploration();
}
showWordExploration() {
const words = this.letterWords[this.currentLetter];
if (!words || this.currentWordIndex >= words.length) {
// Finished exploring words for this letter
this.currentPhase = 'letter-discovery';
this.currentLetterIndex++;
this.updateHUD();
this.showLetterCard();
return;
}
const word = words[this.currentWordIndex];
const gameContent = document.getElementById('game-content');
gameContent.innerHTML = `
<div class="word-exploration-header">
<div class="exploring-letter">Letter "${this.currentLetter}"</div>
<div class="word-progress">Word ${this.currentWordIndex + 1} of ${words.length}</div>
</div>
<div class="word-card">
<div class="word-text">${word.word}</div>
<div class="word-translation">${word.translation}</div>
${word.pronunciation ? `<div class="word-pronunciation">[${word.pronunciation}]</div>` : ''}
<div class="letter-controls">
<button class="discovery-btn" onclick="window.currentLetterGame.nextWord()">
➡️ Next Word
</button>
<button class="audio-btn" onclick="window.currentLetterGame.playWordSound('${word.word}')">
🔊
</button>
</div>
</div>
`;
// Add word to discovered list
this.discoveredWords.push(word);
// Auto-play word sound
setTimeout(() => this.playWordSound(word.word), 500);
}
playWordSound(word) {
if (window.SettingsManager && window.SettingsManager.speak) {
const speed = 0.9;
window.SettingsManager.speak(word, {
lang: this.content.language || 'en-US',
rate: speed
}).catch(error => {
console.warn('🔊 TTS failed for word:', error);
});
}
}
nextWord() {
this.currentWordIndex++;
this.score += 5;
this.onScoreUpdate(this.score);
this.updateHUD();
this.showWordExploration();
}
showCompletion() {
const gameContent = document.getElementById('game-content');
const totalWords = Object.values(this.letterWords).reduce((sum, words) => sum + words.length, 0);
gameContent.innerHTML = `
<div class="completion-message">
<div class="completion-title">🎉 All Letters Discovered!</div>
<div class="completion-stats">
Letters Discovered: ${this.discoveredLetters.length}<br>
Words Learned: ${this.discoveredWords.length}<br>
Final Score: ${this.score}
</div>
<div class="letter-controls">
<button class="discovery-btn" onclick="window.currentLetterGame.startPractice()">
🎮 Start Practice
</button>
<button class="discovery-btn" onclick="window.currentLetterGame.restart()">
🔄 Play Again
</button>
</div>
</div>
`;
}
startPractice() {
this.currentPhase = 'practice';
this.practiceLevel = 1;
this.practiceRound = 0;
this.practiceCorrectAnswers = 0;
this.practiceErrors = 0;
// Create mixed practice from all discovered words
this.currentPracticeItems = this.shuffleArray([...this.discoveredWords]);
this.updateHUD();
this.showPracticeChallenge();
}
showPracticeChallenge() {
if (this.practiceRound >= this.maxPracticeRounds) {
this.endPractice();
return;
}
const currentItem = this.currentPracticeItems[this.practiceRound % this.currentPracticeItems.length];
const gameContent = document.getElementById('game-content');
// Generate options (correct + 3 random)
const allWords = this.discoveredWords.filter(w => w.word !== currentItem.word);
const randomOptions = this.shuffleArray([...allWords]).slice(0, 3);
const options = this.shuffleArray([currentItem, ...randomOptions]);
gameContent.innerHTML = `
<div class="practice-challenge">
<div class="challenge-text">What does "${currentItem.word}" mean?</div>
<div class="practice-grid">
${options.map((option, index) => `
<button class="practice-option" onclick="window.currentLetterGame.selectPracticeAnswer(${index}, '${option.word}')">
${option.translation}
</button>
`).join('')}
</div>
<div class="practice-stats">
<div class="stat-item">Correct: ${this.practiceCorrectAnswers}</div>
<div class="stat-item">Errors: ${this.practiceErrors}</div>
<div class="stat-item">Round: ${this.practiceRound + 1}/${this.maxPracticeRounds}</div>
</div>
</div>
`;
// Store correct answer for checking
this.currentCorrectAnswer = currentItem.word;
// Auto-play word
setTimeout(() => this.playWordSound(currentItem.word), 500);
}
selectPracticeAnswer(selectedIndex, selectedWord) {
const buttons = document.querySelectorAll('.practice-option');
const isCorrect = selectedWord === this.currentCorrectAnswer;
if (isCorrect) {
buttons[selectedIndex].classList.add('correct');
this.practiceCorrectAnswers++;
this.score += 10;
this.onScoreUpdate(this.score);
} else {
buttons[selectedIndex].classList.add('incorrect');
this.practiceErrors++;
// Show correct answer
buttons.forEach((btn, index) => {
if (btn.textContent.trim() === this.discoveredWords.find(w => w.word === this.currentCorrectAnswer)?.translation) {
btn.classList.add('correct');
}
});
}
setTimeout(() => {
this.practiceRound++;
this.updateHUD();
this.showPracticeChallenge();
}, 1500);
}
endPractice() {
const accuracy = Math.round((this.practiceCorrectAnswers / this.maxPracticeRounds) * 100);
const gameContent = document.getElementById('game-content');
gameContent.innerHTML = `
<div class="completion-message">
<div class="completion-title">🏆 Practice Complete!</div>
<div class="completion-stats">
Accuracy: ${accuracy}%<br>
Correct Answers: ${this.practiceCorrectAnswers}/${this.maxPracticeRounds}<br>
Final Score: ${this.score}
</div>
<div class="letter-controls">
<button class="discovery-btn" onclick="window.currentLetterGame.restart()">
🔄 Play Again
</button>
</div>
</div>
`;
// End game
setTimeout(() => {
this.onGameEnd(this.score);
}, 3000);
}
shuffleArray(array) {
const shuffled = [...array];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
}
restart() {
this.currentPhase = 'letter-discovery';
this.currentLetterIndex = 0;
this.discoveredLetters = [];
this.currentLetter = null;
this.currentWordIndex = 0;
this.discoveredWords = [];
this.score = 0;
this.lives = 3;
this.practiceLevel = 1;
this.practiceRound = 0;
this.practiceCorrectAnswers = 0;
this.practiceErrors = 0;
this.currentPracticeItems = [];
this.updateHUD();
this.start();
}
destroy() {
// Cleanup
if (window.currentLetterGame === this) {
delete window.currentLetterGame;
}
const styleSheet = document.getElementById('letter-discovery-styles');
if (styleSheet) {
styleSheet.remove();
}
}
}
// Register the game module
window.GameModules = window.GameModules || {};
window.GameModules.LetterDiscovery = LetterDiscovery;