Class_generator/js/games/letter-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

939 lines
33 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.

// === 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;