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>
939 lines
33 KiB
JavaScript
939 lines
33 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;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.word-type {
|
||
font-size: 0.9em;
|
||
color: #667eea;
|
||
background: rgba(102, 126, 234, 0.1);
|
||
padding: 4px 12px;
|
||
border-radius: 15px;
|
||
display: inline-block;
|
||
margin-bottom: 15px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.word-example {
|
||
font-size: 1em;
|
||
color: #555;
|
||
font-style: italic;
|
||
padding: 10px 15px;
|
||
background: rgba(0, 0, 0, 0.05);
|
||
border-left: 3px solid #667eea;
|
||
border-radius: 0 8px 8px 0;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
/* 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 for letters in content or rawContent
|
||
const letters = this.content.letters || this.content.rawContent?.letters;
|
||
|
||
if (letters && Object.keys(letters).length > 0) {
|
||
this.letters = Object.keys(letters).sort();
|
||
this.letterWords = letters;
|
||
logSh(`📝 Found ${this.letters.length} letters with words`, 'INFO');
|
||
} else {
|
||
this.showNoLettersMessage();
|
||
return;
|
||
}
|
||
|
||
logSh(`🎯 Letter Discovery ready: ${this.letters.length} letters`, 'INFO');
|
||
}
|
||
|
||
showNoLettersMessage() {
|
||
this.container.innerHTML = `
|
||
<div class="game-error">
|
||
<div class="error-content">
|
||
<h2>🔤 Letter Discovery</h2>
|
||
<p>❌ No letter structure found in this content.</p>
|
||
<p>This game requires content with a predefined letters system.</p>
|
||
<p>Try with content that includes letter-based learning material.</p>
|
||
<button class="back-btn" onclick="AppNavigation.navigateTo('games')">← Back to Games</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
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>` : ''}
|
||
${word.type ? `<div class="word-type">${word.type}</div>` : ''}
|
||
${word.example ? `<div class="word-example">"${word.example}"</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();
|
||
}
|
||
}
|
||
|
||
// === COMPATIBILITY SYSTEM ===
|
||
static getCompatibilityRequirements() {
|
||
return {
|
||
minimum: {
|
||
letters: 2,
|
||
wordsPerLetter: 3
|
||
},
|
||
optimal: {
|
||
letters: 8,
|
||
wordsPerLetter: 4
|
||
},
|
||
name: "Letter Discovery",
|
||
description: "Alphabet learning with words starting with each letter"
|
||
};
|
||
}
|
||
|
||
static checkContentCompatibility(content) {
|
||
const requirements = LetterDiscovery.getCompatibilityRequirements();
|
||
|
||
// Extract letter-based vocabulary using same method as instance
|
||
const letterData = LetterDiscovery.extractLetterDataStatic(content);
|
||
|
||
const letterCount = Object.keys(letterData).length;
|
||
const avgWordsPerLetter = letterCount > 0 ?
|
||
Object.values(letterData).reduce((sum, words) => sum + words.length, 0) / letterCount : 0;
|
||
|
||
// Dynamic percentage based on optimal volumes (2→8 letters, 3→4 words/letter)
|
||
// Letters: 0=0%, 4=50%, 8=100%
|
||
// Words per letter: 0=0%, 2=50%, 4=100%
|
||
const letterScore = Math.min(100, (letterCount / requirements.optimal.letters) * 100);
|
||
const wordsScore = Math.min(100, (avgWordsPerLetter / requirements.optimal.wordsPerLetter) * 100);
|
||
|
||
// Combined score (weighted average: 60% letters, 40% words per letter)
|
||
const finalScore = (letterScore * 0.6) + (wordsScore * 0.4);
|
||
|
||
const recommendations = [];
|
||
if (letterCount < requirements.optimal.letters) {
|
||
recommendations.push(`Add vocabulary for ${requirements.optimal.letters - letterCount} more letters`);
|
||
}
|
||
if (avgWordsPerLetter < requirements.optimal.wordsPerLetter) {
|
||
const wordsNeeded = Math.ceil((requirements.optimal.wordsPerLetter * letterCount) -
|
||
Object.values(letterData).reduce((sum, words) => sum + words.length, 0));
|
||
if (wordsNeeded > 0) {
|
||
recommendations.push(`Add ${wordsNeeded} more words for better letter coverage`);
|
||
}
|
||
}
|
||
|
||
return {
|
||
score: Math.round(finalScore),
|
||
details: {
|
||
letters: {
|
||
found: letterCount,
|
||
minimum: requirements.minimum.letters,
|
||
optimal: requirements.optimal.letters,
|
||
status: letterCount >= requirements.minimum.letters ? 'sufficient' : 'insufficient'
|
||
},
|
||
wordsPerLetter: {
|
||
average: Math.round(avgWordsPerLetter * 10) / 10,
|
||
minimum: requirements.minimum.wordsPerLetter,
|
||
optimal: requirements.optimal.wordsPerLetter,
|
||
status: avgWordsPerLetter >= requirements.minimum.wordsPerLetter ? 'sufficient' : 'insufficient'
|
||
}
|
||
},
|
||
recommendations: recommendations
|
||
};
|
||
}
|
||
|
||
static extractLetterDataStatic(content) {
|
||
const letterWords = {};
|
||
|
||
// Priority 1: Use raw module content (simple format)
|
||
if (content.rawContent) {
|
||
return LetterDiscovery.extractLetterDataFromRawStatic(content.rawContent);
|
||
}
|
||
|
||
// Priority 2: Ultra-modular format (vocabulary object) - ONLY format supported
|
||
if (content.vocabulary && typeof content.vocabulary === 'object' && !Array.isArray(content.vocabulary)) {
|
||
Object.entries(content.vocabulary).forEach(([word, data]) => {
|
||
let wordData;
|
||
|
||
// Support ultra-modular format ONLY
|
||
if (typeof data === 'object' && data.user_language) {
|
||
wordData = {
|
||
word: word,
|
||
translation: data.user_language.split(';')[0],
|
||
fullTranslation: data.user_language,
|
||
type: data.type || 'general',
|
||
pronunciation: data.pronunciation,
|
||
category: data.type || 'general'
|
||
};
|
||
}
|
||
// Legacy fallback - simple string (temporary, will be removed)
|
||
else if (typeof data === 'string') {
|
||
wordData = {
|
||
word: word,
|
||
translation: data.split(';')[0],
|
||
fullTranslation: data,
|
||
type: 'general',
|
||
category: 'general'
|
||
};
|
||
}
|
||
|
||
if (wordData && wordData.word && wordData.translation) {
|
||
const firstLetter = wordData.word.charAt(0).toUpperCase();
|
||
if (!letterWords[firstLetter]) {
|
||
letterWords[firstLetter] = [];
|
||
}
|
||
letterWords[firstLetter].push(wordData);
|
||
}
|
||
});
|
||
}
|
||
|
||
return letterWords;
|
||
}
|
||
|
||
static extractLetterDataFromRawStatic(rawContent) {
|
||
const letterWords = {};
|
||
|
||
// Ultra-modular format (vocabulary object) - ONLY format supported
|
||
if (rawContent.vocabulary && typeof rawContent.vocabulary === 'object' && !Array.isArray(rawContent.vocabulary)) {
|
||
Object.entries(rawContent.vocabulary).forEach(([word, data]) => {
|
||
let wordData;
|
||
|
||
// Support ultra-modular format ONLY
|
||
if (typeof data === 'object' && data.user_language) {
|
||
wordData = {
|
||
word: word,
|
||
translation: data.user_language.split(';')[0],
|
||
fullTranslation: data.user_language,
|
||
type: data.type || 'general',
|
||
pronunciation: data.pronunciation,
|
||
category: data.type || 'general'
|
||
};
|
||
}
|
||
// Legacy fallback - simple string (temporary, will be removed)
|
||
else if (typeof data === 'string') {
|
||
wordData = {
|
||
word: word,
|
||
translation: data.split(';')[0],
|
||
fullTranslation: data,
|
||
type: 'general',
|
||
category: 'general'
|
||
};
|
||
}
|
||
|
||
if (wordData && wordData.word && wordData.translation) {
|
||
const firstLetter = wordData.word.charAt(0).toUpperCase();
|
||
if (!letterWords[firstLetter]) {
|
||
letterWords[firstLetter] = [];
|
||
}
|
||
letterWords[firstLetter].push(wordData);
|
||
}
|
||
});
|
||
}
|
||
|
||
return letterWords;
|
||
}
|
||
}
|
||
|
||
// Register the game module
|
||
window.GameModules = window.GameModules || {};
|
||
window.GameModules.LetterDiscovery = LetterDiscovery; |