Class_generator/Legacy/js/games/word-discovery.js
StillHammer 38920cc858 Complete architectural rewrite with ultra-modular system
Major Changes:
- Moved legacy system to Legacy/ folder for archival
- Built new modular architecture with strict separation of concerns
- Created core system: Module, EventBus, ModuleLoader, Router
- Added Application bootstrap with auto-start functionality
- Implemented development server with ES6 modules support
- Created comprehensive documentation and project context
- Converted SBS-7-8 content to JSON format
- Copied all legacy games and content to new structure

New Architecture Features:
- Sealed modules with WeakMap private data
- Strict dependency injection system
- Event-driven communication only
- Inviolable responsibility patterns
- Auto-initialization without commands
- Component-based UI foundation ready

Technical Stack:
- Vanilla JS/HTML/CSS only
- ES6 modules with proper imports/exports
- HTTP development server (no file:// protocol)
- Modular CSS with component scoping
- Comprehensive error handling and debugging

Ready for Phase 2: Converting legacy modules to new architecture

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-22 07:08:39 +08:00

1046 lines
38 KiB
JavaScript

class WordDiscovery {
constructor({ container, content, onScoreUpdate, onGameEnd }) {
this.container = container;
this.content = content;
this.onScoreUpdate = onScoreUpdate;
this.onGameEnd = onGameEnd;
// Expose content globally for SettingsManager TTS language detection
window.currentGameContent = content;
this.currentWordIndex = 0;
this.discoveredWords = [];
this.currentPhase = 'discovery'; // discovery, practice
this.score = 0;
this.lives = 3;
this.wordsToLearn = [];
// Practice system - Global practice after all words discovered
this.practiceLevel = 1; // 1=Easy, 2=Medium, 3=Hard, 4=Expert
this.practiceRound = 0;
this.maxPracticeRounds = 6; // More rounds for mixed practice
this.practiceCorrectAnswers = 0;
this.practiceErrors = 0;
this.currentPracticeWords = []; // Mixed selection of all discovered words
this.injectCSS();
this.extractContent();
this.init();
}
injectCSS() {
if (document.getElementById('word-discovery-styles')) return;
const styleSheet = document.createElement('style');
styleSheet.id = 'word-discovery-styles';
styleSheet.textContent = `
.word-discovery-wrapper {
display: flex;
flex-direction: column;
height: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.discovery-hud {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
background: rgba(0,0,0,0.2);
backdrop-filter: blur(10px);
}
.discovery-progress {
display: flex;
align-items: center;
gap: 15px;
}
.progress-bar {
width: 200px;
height: 8px;
background: rgba(255,255,255,0.2);
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #00ff88, #00cc6a);
transition: width 0.3s ease;
}
.discovery-main {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 20px;
position: relative;
}
.word-card {
background: white;
border-radius: 20px;
padding: 40px;
box-shadow: 0 20px 40px rgba(0,0,0,0.3);
text-align: center;
max-width: 500px;
width: 100%;
color: #333;
transform: scale(0.9);
opacity: 0;
animation: cardAppear 0.5s ease forwards;
}
@keyframes cardAppear {
to {
transform: scale(1);
opacity: 1;
}
}
.word-image {
width: 200px;
height: 200px;
object-fit: cover;
border-radius: 15px;
margin-bottom: 20px;
box-shadow: 0 10px 20px rgba(0,0,0,0.2);
}
.word-text {
font-size: 2.5em;
font-weight: bold;
color: #2c3e50;
margin-bottom: 10px;
}
.word-pronunciation {
font-size: 1.2em;
color: #7f8c8d;
margin-bottom: 15px;
font-style: italic;
}
.word-translation {
font-size: 1.8em;
color: #e74c3c;
font-weight: 600;
margin-bottom: 20px;
}
.discovery-controls {
display: flex;
gap: 15px;
margin-top: 20px;
}
.discovery-btn {
padding: 12px 25px;
border: none;
border-radius: 25px;
font-size: 1.1em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
min-width: 120px;
}
.btn-primary {
background: linear-gradient(45deg, #667eea, #764ba2);
color: white;
}
.btn-secondary {
background: linear-gradient(45deg, #f093fb, #f5576c);
color: white;
}
.btn-success {
background: linear-gradient(45deg, #4facfe, #00f2fe);
color: white;
}
.discovery-btn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(0,0,0,0.2);
}
.association-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 30px;
max-width: 800px;
width: 100%;
}
.association-item {
background: white;
border-radius: 15px;
padding: 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
color: #333;
border: 3px solid transparent;
}
.association-item:hover {
transform: translateY(-5px);
box-shadow: 0 15px 30px rgba(0,0,0,0.2);
}
.association-item.selected {
border-color: #667eea;
background: #f8f9ff;
}
.association-item.correct {
border-color: #00ff88;
background: #f0fff4;
}
.association-item.incorrect {
border-color: #ff4757;
background: #fff0f0;
}
.association-image {
width: 120px;
height: 120px;
object-fit: cover;
border-radius: 10px;
margin-bottom: 15px;
}
.association-text {
font-size: 1.4em;
font-weight: 600;
}
.phase-indicator {
position: absolute;
top: 20px;
left: 20px;
background: rgba(255,255,255,0.2);
padding: 8px 16px;
border-radius: 20px;
font-weight: 600;
backdrop-filter: blur(10px);
}
.practice-progress {
position: absolute;
top: 20px;
right: 20px;
background: rgba(255,255,255,0.2);
padding: 8px 16px;
border-radius: 20px;
font-weight: 600;
backdrop-filter: blur(10px);
font-size: 0.9em;
}
.difficulty-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 15px;
font-size: 0.8em;
font-weight: bold;
margin-left: 10px;
}
.difficulty-easy { background: #4CAF50; color: white; }
.difficulty-medium { background: #FF9800; color: white; }
.difficulty-hard { background: #F44336; color: white; }
.difficulty-expert { background: #9C27B0; color: white; }
.practice-grid-6 {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
max-width: 900px;
width: 100%;
}
.practice-grid-8 {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 15px;
max-width: 1000px;
width: 100%;
}
.practice-challenge {
text-align: center;
margin-bottom: 30px;
padding: 20px;
background: rgba(255,255,255,0.1);
border-radius: 15px;
backdrop-filter: blur(10px);
}
.challenge-timer {
font-size: 2em;
font-weight: bold;
color: #FFD700;
margin-bottom: 10px;
}
.challenge-text {
font-size: 1.2em;
margin-bottom: 15px;
}
.practice-stats {
display: flex;
justify-content: space-around;
margin-top: 20px;
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);
}
.association-item.time-pressure {
animation: timePressure 0.5s ease-in-out infinite alternate;
}
@keyframes timePressure {
from { box-shadow: 0 0 10px rgba(255,215,0,0.5); }
to { box-shadow: 0 0 20px rgba(255,215,0,0.8); }
}
.audio-btn {
background: none;
border: none;
font-size: 2em;
cursor: pointer;
color: #667eea;
margin-left: 10px;
transition: all 0.3s ease;
}
.audio-btn:hover {
transform: scale(1.2);
color: #764ba2;
}
.completion-message {
text-align: center;
padding: 40px;
background: rgba(255,255,255,0.1);
border-radius: 20px;
backdrop-filter: blur(10px);
}
.completion-title {
font-size: 2.5em;
margin-bottom: 20px;
color: #00ff88;
}
.completion-stats {
font-size: 1.3em;
margin-bottom: 30px;
line-height: 1.6;
}
.content-warning {
background: rgba(255, 193, 7, 0.2);
border: 2px solid #FFC107;
border-radius: 10px;
padding: 15px;
margin: 20px 0;
color: #856404;
font-size: 0.9em;
}
.feature-missing {
opacity: 0.6;
position: relative;
}
.feature-missing::after {
content: '📵';
position: absolute;
top: 5px;
right: 5px;
font-size: 0.8em;
}
`;
document.head.appendChild(styleSheet);
}
extractContent() {
if (!this.content || !this.content.vocabulary) {
this.wordsToLearn = [];
return;
}
this.wordsToLearn = Object.entries(this.content.vocabulary).map(([word, data]) => ({
word: word,
translation: typeof data === 'string' ? data : data.translation,
pronunciation: typeof data === 'object' ? data.pronunciation : null,
image: typeof data === 'object' ? data.image : null,
type: typeof data === 'object' ? data.type : 'word'
})).filter(item => item.translation);
// Shuffle words for variety
this.wordsToLearn = this.shuffleArray([...this.wordsToLearn]);
}
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]];
}
return array;
}
init() {
this.container.innerHTML = `
<div class="word-discovery-wrapper">
<div class="discovery-hud">
<div class="discovery-progress">
<span>Progress:</span>
<div class="progress-bar">
<div class="progress-fill" style="width: 0%"></div>
</div>
<span class="progress-text">0/${this.wordsToLearn.length}</span>
</div>
<div class="discovery-stats">
<span>Score: <span class="score-display">0</span></span>
<span style="margin-left: 20px;">❤️ <span class="lives-display">3</span></span>
</div>
</div>
<div class="discovery-main">
<div class="phase-indicator">Discovery Phase</div>
<div class="game-content"></div>
</div>
</div>
`;
if (this.wordsToLearn.length === 0) {
this.showNoContent();
return;
}
this.updateHUD();
this.startDiscoveryPhase();
}
updateHUD() {
const progressFill = this.container.querySelector('.progress-fill');
const progressText = this.container.querySelector('.progress-text');
const scoreDisplay = this.container.querySelector('.score-display');
const livesDisplay = this.container.querySelector('.lives-display');
const progressPercent = (this.currentWordIndex / this.wordsToLearn.length) * 100;
progressFill.style.width = `${progressPercent}%`;
progressText.textContent = `${this.currentWordIndex}/${this.wordsToLearn.length}`;
scoreDisplay.textContent = this.score;
livesDisplay.textContent = this.lives;
}
startDiscoveryPhase() {
this.currentPhase = 'discovery';
this.container.querySelector('.phase-indicator').textContent = 'Discovery Phase';
this.showWordCard();
}
showWordCard() {
if (this.currentWordIndex >= this.wordsToLearn.length) {
// All words discovered - start global practice phase
this.startGlobalPractice();
return;
}
const word = this.wordsToLearn[this.currentWordIndex];
const gameContent = this.container.querySelector('.game-content');
// Check what features are missing for this word
const missingFeatures = [];
if (!word.image) missingFeatures.push('image');
if (!word.pronunciation) missingFeatures.push('pronunciation');
gameContent.innerHTML = `
<div class="word-card">
${word.image ?
`<img src="${word.image}" alt="${word.word}" class="word-image" onerror="this.style.display='none'; this.nextElementSibling.style.display='block';">` :
`<div class="word-image feature-missing" style="background: #f0f0f0; display: flex; align-items: center; justify-content: center; color: #999;">📷 No Image</div>`
}
<div class="word-text">${word.word}</div>
${word.pronunciation ?
`<div class="word-pronunciation">/${word.pronunciation}/</div>` :
`<div class="word-pronunciation feature-missing" style="color: #999; font-style: normal;">No pronunciation guide</div>`
}
<div class="word-translation">${word.translation}</div>
${missingFeatures.length > 0 ?
`<div class="content-warning">
⚠️ Missing: ${missingFeatures.join(', ')}. Practice questions will be adapted accordingly.
</div>` : ''
}
<div class="discovery-controls">
<button class="discovery-btn btn-primary ${!word.pronunciation ? 'feature-missing' : ''}" onclick="this.closest('.word-discovery-wrapper').game.hearPronunciation()">
🔊 Listen ${!word.pronunciation ? '(TTS)' : ''}
</button>
<button class="discovery-btn btn-success" onclick="this.closest('.word-discovery-wrapper').game.markAsLearned()">
Got It!
</button>
</div>
</div>
`;
// Store game reference for button access
this.container.querySelector('.word-discovery-wrapper').game = this;
// Auto-play TTS when new word appears (with delay for card animation)
setTimeout(() => {
this.hearPronunciation();
}, 800);
}
async hearPronunciation(options = {}) {
let wordToSpeak;
if (this.currentPhase === 'practice') {
// In practice phase, use current practice word
wordToSpeak = this.currentPracticeWords[this.practiceRound % this.currentPracticeWords.length];
} else {
// In discovery phase, use current word being learned
wordToSpeak = this.wordsToLearn[this.currentWordIndex];
}
if (!wordToSpeak) return;
// Try to play audio file first if available
if (wordToSpeak.audioFile || wordToSpeak.pronunciation) {
const audioPath = wordToSpeak.audioFile;
if (audioPath) {
try {
const audio = new Audio(audioPath);
// Handle audio loading errors
audio.onerror = () => {
console.warn(`Audio file not found: ${audioPath}, falling back to TTS`);
this.fallbackToTTS(wordToSpeak, options);
};
// Handle successful audio loading
audio.oncanplaythrough = () => {
// Adjust playback rate if supported
if (options.rate && audio.playbackRate !== undefined) {
audio.playbackRate = options.rate;
}
audio.play().catch(error => {
console.warn('Audio playback failed:', error);
this.fallbackToTTS(wordToSpeak, options);
});
};
// Load the audio
audio.load();
// Timeout fallback if audio takes too long
setTimeout(() => {
if (audio.readyState === 0) {
console.warn('Audio loading timeout, falling back to TTS');
this.fallbackToTTS(wordToSpeak, options);
}
}, 2000);
return; // Don't proceed to TTS if we're trying audio
} catch (error) {
console.warn('Audio creation failed:', error);
}
}
}
// Fallback to TTS immediately if no audio file
this.fallbackToTTS(wordToSpeak, options);
}
fallbackToTTS(wordToSpeak, options = {}) {
// Use SettingsManager if available, otherwise fallback to basic TTS
if (window.SettingsManager) {
// Pass custom rate if specified
const ttsOptions = {};
if (options.rate) {
ttsOptions.rate = options.rate;
}
window.SettingsManager.speak(wordToSpeak.word, ttsOptions)
.catch(error => {
console.warn('SettingsManager TTS failed:', error);
this.basicTTS(wordToSpeak, options);
});
} else {
this.basicTTS(wordToSpeak, options);
}
}
basicTTS(wordToSpeak, options = {}) {
// Try to speak the word using Web Speech API
if ('speechSynthesis' in window && wordToSpeak) {
const utterance = new SpeechSynthesisUtterance(wordToSpeak.word);
utterance.lang = 'en-US';
utterance.rate = options.rate || 0.8;
speechSynthesis.speak(utterance);
} else {
// Last resort: show pronunciation text if available
if (wordToSpeak.pronunciation) {
alert(`Pronunciation: /${wordToSpeak.pronunciation}/`);
} else {
alert(`Word: ${wordToSpeak.word}`);
}
}
}
updatePhaseIndicator() {
const phaseIndicator = this.container.querySelector('.phase-indicator');
const difficultyNames = ['', 'Easy', 'Medium', 'Hard', 'Expert'];
const difficultyClasses = ['', 'difficulty-easy', 'difficulty-medium', 'difficulty-hard', 'difficulty-expert'];
if (this.currentPhase === 'practice') {
phaseIndicator.innerHTML = `Mixed Practice <span class="difficulty-badge ${difficultyClasses[this.practiceLevel]}">${difficultyNames[this.practiceLevel]}</span>`;
} else {
phaseIndicator.textContent = 'Discovery Phase';
}
// Update or create practice progress indicator
let progressIndicator = this.container.querySelector('.practice-progress');
if (!progressIndicator) {
progressIndicator = document.createElement('div');
progressIndicator.className = 'practice-progress';
this.container.querySelector('.discovery-main').appendChild(progressIndicator);
}
if (this.currentPhase === 'practice') {
progressIndicator.textContent = `Round ${this.practiceRound + 1}/${this.maxPracticeRounds}`;
} else {
progressIndicator.textContent = '';
}
}
showMixedPracticeChallenge() {
// Get a random word from discovered words for this challenge
const currentWord = this.currentPracticeWords[this.practiceRound % this.currentPracticeWords.length];
const gameContent = this.container.querySelector('.game-content');
// Check available content features
const hasImages = this.discoveredWords.some(word => word.image);
const hasPronunciation = this.discoveredWords.some(word => word.pronunciation);
const hasAudioFiles = this.discoveredWords.some(word => word.audioFile);
const currentWordHasImage = currentWord.image;
const currentWordHasPronunciation = currentWord.pronunciation;
const currentWordHasAudio = currentWord.audioFile;
// Determine challenge based on practice level and available content
const challenges = {
1: { options: 4, time: null, question: 'translation' }, // Easy: 4 options, no timer
2: { options: 6, time: 15, question: 'translation' }, // Medium: 6 options, 15s timer
3: { options: 6, time: 10, question: hasImages ? 'mixed' : 'translation' }, // Hard: mixed if images available
4: { options: 8, time: 8, question: (hasAudioFiles || hasPronunciation) ? 'audio' : 'translation' } // Expert: audio if available
};
const challenge = challenges[this.practiceLevel];
const numOptions = challenge.options;
// Create options: current word + random others from ALL discovered words
const options = [currentWord];
const otherWords = this.discoveredWords.filter(word => word.word !== currentWord.word);
const randomOthers = this.shuffleArray([...otherWords]).slice(0, numOptions - 1);
options.push(...randomOthers);
// Shuffle the options
const shuffledOptions = this.shuffleArray([...options]);
// Determine question type - TEST FOREIGN WORD KNOWLEDGE, NOT NATIVE LANGUAGE
let questionText = '';
let showImages = true;
let showText = true;
if (challenge.question === 'translation') {
// Test: Show foreign word, find translation/image
questionText = `Which one means "${currentWord.word}"?`;
} else if (challenge.question === 'mixed') {
// Build available question types based on content
const questionTypes = [`Which one means "${currentWord.word}"?`];
// Add pronunciation question if available (text or audio)
if (currentWordHasPronunciation || currentWordHasAudio) {
questionTypes.push(`Find the word that sounds like "${currentWord.pronunciation || currentWord.word}"`);
}
// Add image question if current word has image AND other words have images for comparison
if (currentWordHasImage && hasImages) {
questionTypes.push(`Which image represents "${currentWord.word}"?`);
}
questionText = questionTypes[Math.floor(Math.random() * questionTypes.length)];
if (questionText.includes('image')) {
showText = false;
// Ensure we only show options that have images
const imageOptions = [currentWord];
const otherWordsWithImages = this.discoveredWords.filter(word =>
word.word !== currentWord.word && word.image
);
if (otherWordsWithImages.length >= numOptions - 1) {
const randomOthers = this.shuffleArray([...otherWordsWithImages]).slice(0, numOptions - 1);
options.length = 1; // Reset to just current word
options.push(...randomOthers);
}
}
} else if (challenge.question === 'audio') {
if (currentWordHasPronunciation || currentWordHasAudio) {
questionText = 'Listen and find the correct word!';
showImages = false;
// Auto-play pronunciation
setTimeout(() => this.hearPronunciation(), 500);
} else {
// Fallback to translation if no audio
questionText = `Which one means "${currentWord.word}"?`;
}
}
const gridClass = numOptions <= 4 ? 'association-grid' :
numOptions <= 6 ? 'practice-grid-6' : 'practice-grid-8';
gameContent.innerHTML = `
<div class="practice-challenge">
${challenge.time ? `<div class="challenge-timer" id="practice-timer">${challenge.time}</div>` : ''}
<div class="challenge-text">${questionText}</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">Level: ${this.practiceLevel}/4</div>
</div>
</div>
<div class="${gridClass}">
${shuffledOptions.map((option, index) => `
<div class="association-item${challenge.time ? ' time-pressure' : ''}" onclick="this.closest('.word-discovery-wrapper').game.selectMixedPractice(${shuffledOptions.indexOf(option)}, '${option.word}')">
${showImages && option.image ? `<img src="${option.image}" alt="${option.word}" class="association-image" onerror="this.style.display='none'">` : ''}
${showText ? `<div class="association-text">${option.translation}</div>` : ''}
${!showText && !showImages ? `<div class="association-text">?</div>` : ''}
</div>
`).join('')}
</div>
`;
// Start timer if needed
if (challenge.time) {
this.startPracticeTimer(challenge.time);
}
// Auto-play TTS based on practice level with appropriate speed
setTimeout(() => {
let ttsSpeed;
switch (this.practiceLevel) {
case 1: // Easy - 0.7 speed
ttsSpeed = 0.7;
break;
case 2: // Medium - 0.9 speed
ttsSpeed = 0.9;
break;
case 3: // Hard - 1.0 speed
ttsSpeed = 1.0;
break;
case 4: // Expert - 1.1 speed (already has audio auto-play)
ttsSpeed = 1.1;
break;
default:
ttsSpeed = 0.8; // Fallback
}
// Don't auto-play if it's an audio-only challenge (Expert mode already handles this)
if (challenge.question !== 'audio') {
this.hearPronunciation({ rate: ttsSpeed });
}
}, 1000); // Delay to let interface render
}
startPracticeTimer(seconds) {
this.practiceTimer = seconds;
this.practiceTimerInterval = setInterval(() => {
this.practiceTimer--;
const timerElement = document.getElementById('practice-timer');
if (timerElement) {
timerElement.textContent = this.practiceTimer;
if (this.practiceTimer <= 3) {
timerElement.style.color = '#FF4444';
timerElement.style.animation = 'pulse 0.5s infinite';
}
}
if (this.practiceTimer <= 0) {
this.clearPracticeTimer();
this.selectPractice(-1, 'TIMEOUT'); // Handle timeout
}
}, 1000);
}
clearPracticeTimer() {
if (this.practiceTimerInterval) {
clearInterval(this.practiceTimerInterval);
this.practiceTimerInterval = null;
}
}
selectMixedPractice(selectedIndex, selectedWord) {
this.clearPracticeTimer();
const currentWord = this.currentPracticeWords[this.practiceRound % this.currentPracticeWords.length];
const items = this.container.querySelectorAll('.association-item');
let isCorrect = false;
if (selectedWord === 'TIMEOUT') {
// Timer expired
this.practiceErrors++;
// Show correct answer
items.forEach((item) => {
const text = item.querySelector('.association-text');
if (text && text.textContent === currentWord.word) {
item.classList.add('correct');
}
});
} else if (selectedWord === currentWord.word) {
// Correct answer
isCorrect = true;
items[selectedIndex].classList.add('correct');
this.practiceCorrectAnswers++;
// Score based on difficulty level
const scoreBonus = [0, 5, 10, 15, 25][this.practiceLevel];
this.score += scoreBonus;
this.onScoreUpdate(this.score);
} else {
// Wrong answer
items[selectedIndex].classList.add('incorrect');
this.practiceErrors++;
// Show correct answer
items.forEach((item) => {
const text = item.querySelector('.association-text');
if (text && text.textContent === currentWord.word) {
item.classList.add('correct');
}
});
}
this.updateHUD();
// Continue to next practice round or advance
setTimeout(() => {
this.practiceRound++;
if (this.practiceRound >= this.maxPracticeRounds) {
// Check if ready for next level
const accuracy = this.practiceCorrectAnswers / this.maxPracticeRounds;
if (accuracy >= 0.75 && this.practiceLevel < 4) {
// Advance to next difficulty level
this.practiceLevel++;
this.practiceRound = 0;
this.practiceCorrectAnswers = 0;
this.practiceErrors = 0;
// SHUFFLE words again for new difficulty level
this.currentPracticeWords = this.shuffleArray([...this.discoveredWords]);
console.log(`🔀 Shuffled words for Level ${this.practiceLevel} - new variation order`);
this.updatePhaseIndicator();
setTimeout(() => {
this.showLevelUpMessage();
}, 500);
} else if (accuracy >= 0.5) {
// Passed all practice levels - show completion
this.showCompletion();
} else {
// Failed practice - retry current level
this.practiceRound = Math.max(0, this.practiceRound - 2); // Go back 2 rounds
this.practiceCorrectAnswers = 0;
this.practiceErrors = 0;
this.lives--;
if (this.lives <= 0) {
this.endGame();
return;
}
}
} else {
// Continue current difficulty level with next random word
this.updatePhaseIndicator();
this.showMixedPracticeChallenge();
}
}, 1500);
}
showLevelUpMessage() {
const gameContent = this.container.querySelector('.game-content');
const difficultyNames = ['', 'Easy', 'Medium', 'Hard', 'Expert'];
gameContent.innerHTML = `
<div class="completion-message">
<div class="completion-title">🎉 Level Up!</div>
<div class="completion-stats">
Advanced to ${difficultyNames[this.practiceLevel]} difficulty!<br>
Keep practicing to master this word!
</div>
</div>
`;
setTimeout(() => {
this.showMixedPracticeChallenge();
}, 2000);
}
markAsLearned() {
this.discoveredWords.push(this.wordsToLearn[this.currentWordIndex]);
this.currentWordIndex++;
this.score += 5;
this.onScoreUpdate(this.score);
this.updateHUD();
setTimeout(() => {
this.startDiscoveryPhase();
}, 300);
}
startGlobalPractice() {
// Transition message
const gameContent = this.container.querySelector('.game-content');
gameContent.innerHTML = `
<div class="completion-message">
<div class="completion-title">🏆 Discovery Complete!</div>
<div class="completion-stats">
You've discovered all ${this.discoveredWords.length} words!<br>
Now let's practice with mixed vocabulary challenges!
</div>
<div class="discovery-controls">
<button class="discovery-btn btn-primary" onclick="this.closest('.word-discovery-wrapper').game.startMixedPractice()">
Start Practice
</button>
<button class="discovery-btn btn-secondary" onclick="this.closest('.word-discovery-wrapper').game.skipToCompletion()">
Skip Practice
</button>
</div>
</div>
`;
}
startMixedPractice() {
this.currentPhase = 'practice';
this.practiceLevel = 1;
this.practiceRound = 0;
this.practiceCorrectAnswers = 0;
this.practiceErrors = 0;
// SHUFFLE discovered words for varied practice order
this.currentPracticeWords = this.shuffleArray([...this.discoveredWords]);
console.log(`🔀 Shuffled ${this.currentPracticeWords.length} words for practice variation`);
this.updatePhaseIndicator();
this.showMixedPracticeChallenge();
}
skipToCompletion() {
this.showCompletion();
}
showCompletion() {
const gameContent = this.container.querySelector('.game-content');
const accuracy = Math.round((this.discoveredWords.length / this.wordsToLearn.length) * 100);
const practiceAccuracy = this.practiceRound > 0 ? Math.round((this.practiceCorrectAnswers / this.practiceRound) * 100) : 0;
gameContent.innerHTML = `
<div class="completion-message">
<div class="completion-title">🏆 Vocabulary Mastered!</div>
<div class="completion-stats">
Words Discovered: ${this.discoveredWords.length}/${this.wordsToLearn.length}<br>
Practice Accuracy: ${practiceAccuracy}%<br>
Final Score: ${this.score}<br>
Practice Level Reached: ${this.practiceLevel}/4
</div>
<div class="discovery-controls">
<button class="discovery-btn btn-primary" onclick="this.closest('.word-discovery-wrapper').game.restart()">
Learn Again
</button>
<button class="discovery-btn btn-secondary" onclick="this.closest('.word-discovery-wrapper').game.endGame()">
Back to Games
</button>
</div>
</div>
`;
}
showNoContent() {
const gameContent = this.container.querySelector('.game-content');
gameContent.innerHTML = `
<div class="completion-message">
<div class="completion-title">📚 No Vocabulary Found</div>
<div class="completion-stats">
This content doesn't have vocabulary for the Word Discovery game.<br>
<small>Note: Images and audio are optional but enhance the experience!</small>
</div>
<div class="discovery-controls">
<button class="discovery-btn btn-secondary" onclick="this.closest('.word-discovery-wrapper').game.endGame()">
Back to Games
</button>
</div>
</div>
`;
}
start() {
// Game starts automatically in constructor
}
restart() {
this.currentWordIndex = 0;
this.discoveredWords = [];
this.score = 0;
this.lives = 3;
this.practiceLevel = 1;
this.practiceRound = 0;
this.practiceCorrectAnswers = 0;
this.practiceErrors = 0;
this.currentPracticeWords = [];
this.clearPracticeTimer();
this.wordsToLearn = this.shuffleArray([...this.wordsToLearn]);
this.updateHUD();
this.startDiscoveryPhase();
}
endGame() {
this.onGameEnd(this.score);
}
destroy() {
this.clearPracticeTimer();
// Clean up global content reference
if (window.currentGameContent === this.content) {
window.currentGameContent = null;
}
const styleSheet = document.getElementById('word-discovery-styles');
if (styleSheet) {
styleSheet.remove();
}
}
}
// Register the game module
window.GameModules = window.GameModules || {};
window.GameModules.WordDiscovery = WordDiscovery;