Class_generator/js/games/word-discovery.js
StillHammer 24362165ab Implement dynamic percentage compatibility system across all games
Major architectural update to replace fixed 50%/100% scoring with true dynamic percentages based on content volume:

• Replace old interpolation system with Math.min(100, (count/optimal)*100) formula
• Add embedded compatibility methods to all 14 game modules with static requirements
• Remove compatibility cache system for real-time calculation
• Fix content loading to pass complete modules with vocabulary (not just metadata)
• Clean up duplicate syntax errors in adventure-reader and grammar-discovery
• Update navigation.js module mapping to match actual exported class names

Examples of new dynamic scoring:
- 15 words / 20 optimal = 75% (was 87.5% with old interpolation)
- 5 words / 10 minimum = 50% (was 25% with old linear system)
- 30 words / 20 optimal = 100% (unchanged)

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

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

1195 lines
44 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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();
}
}
// === COMPATIBILITY SYSTEM ===
static getCompatibilityRequirements() {
return {
minimum: {
vocabulary: 10
},
optimal: {
vocabulary: 20
},
name: "Word Discovery",
description: "Progressive vocabulary learning with discovery and practice phases"
};
}
static checkContentCompatibility(content) {
const requirements = WordDiscovery.getCompatibilityRequirements();
// Extract vocabulary using same method as instance
const vocabulary = WordDiscovery.extractVocabularyStatic(content);
const vocabCount = vocabulary.length;
// Dynamic percentage based on optimal volume (10 min → 20 optimal)
// 0 words = 0%, 10 words = 50%, 20 words = 100%
const score = Math.min(100, (vocabCount / requirements.optimal.vocabulary) * 100);
const recommendations = [];
if (vocabCount < requirements.optimal.vocabulary) {
recommendations.push(`Add ${requirements.optimal.vocabulary - vocabCount} more vocabulary words for optimal experience`);
}
// Count multimedia features for bonus recommendations
const hasImages = vocabulary.some(word => word.image);
const hasAudio = vocabulary.some(word => word.audioFile || word.pronunciation);
if (!hasImages) {
recommendations.push("Add images to vocabulary for visual learning challenges");
}
if (!hasAudio) {
recommendations.push("Add audio files or pronunciation guides for audio challenges");
}
return {
score: Math.round(score),
details: {
vocabulary: {
found: vocabCount,
minimum: requirements.minimum.vocabulary,
optimal: requirements.optimal.vocabulary,
status: vocabCount >= requirements.minimum.vocabulary ? 'sufficient' : 'insufficient'
},
multimedia: {
images: hasImages ? 'available' : 'missing',
audio: hasAudio ? 'available' : 'missing'
}
},
recommendations: recommendations
};
}
static extractVocabularyStatic(content) {
let vocabulary = [];
// Priority 1: Use raw module content (simple format)
if (content.rawContent) {
return WordDiscovery.extractVocabularyFromRawStatic(content.rawContent);
}
// Priority 2: Ultra-modular format (vocabulary object) - ONLY format supported
if (content.vocabulary && typeof content.vocabulary === 'object' && !Array.isArray(content.vocabulary)) {
vocabulary = Object.entries(content.vocabulary).map(([word, data]) => {
// Support ultra-modular format ONLY
if (typeof data === 'object' && data.user_language) {
return {
word: word,
translation: data.user_language.split('')[0],
fullTranslation: data.user_language,
type: data.type || 'general',
image: data.image,
audioFile: data.audio,
pronunciation: data.pronunciation,
category: data.type || 'general'
};
}
// Legacy fallback - simple string (temporary, will be removed)
else if (typeof data === 'string') {
return {
word: word,
translation: data.split('')[0],
fullTranslation: data,
type: 'general',
category: 'general'
};
}
return null;
}).filter(Boolean);
}
return WordDiscovery.finalizeVocabularyStatic(vocabulary);
}
static extractVocabularyFromRawStatic(rawContent) {
let vocabulary = [];
// Ultra-modular format (vocabulary object) - ONLY format supported
if (rawContent.vocabulary && typeof rawContent.vocabulary === 'object' && !Array.isArray(rawContent.vocabulary)) {
vocabulary = Object.entries(rawContent.vocabulary).map(([word, data]) => {
// Support ultra-modular format ONLY
if (typeof data === 'object' && data.user_language) {
return {
word: word,
translation: data.user_language.split('')[0],
fullTranslation: data.user_language,
type: data.type || 'general',
image: data.image,
audioFile: data.audio,
pronunciation: data.pronunciation,
category: data.type || 'general'
};
}
// Legacy fallback - simple string (temporary, will be removed)
else if (typeof data === 'string') {
return {
word: word,
translation: data.split('')[0],
fullTranslation: data,
type: 'general',
category: 'general'
};
}
return null;
}).filter(Boolean);
}
return WordDiscovery.finalizeVocabularyStatic(vocabulary);
}
static finalizeVocabularyStatic(vocabulary) {
// Validation and cleanup for ultra-modular format
vocabulary = vocabulary.filter(word =>
word &&
typeof word.word === 'string' &&
typeof word.translation === 'string' &&
word.word.trim() !== '' &&
word.translation.trim() !== ''
);
return vocabulary;
}
}
// Register the game module
window.GameModules = window.GameModules || {};
window.GameModules.WordDiscovery = WordDiscovery;