- New Word Discovery game with image support and practice phases - Auto-play TTS on word appearance with speed control (0.7x-1.1x) - Complete Settings page with TTS controls and debug interface - Language standardization with BCP 47 codes (en-US, zh-CN, fr-FR) - Media fallback handling for missing images and audio - Settings Manager with voice selection and debug tools 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1049 lines
38 KiB
JavaScript
1049 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}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
startPracticePhase() {
|
|
this.currentPhase = 'practice';
|
|
this.practiceLevel = 1;
|
|
this.practiceRound = 0;
|
|
this.practiceCorrectAnswers = 0;
|
|
this.practiceErrors = 0;
|
|
this.updatePhaseIndicator();
|
|
this.showPracticeChallenge();
|
|
}
|
|
|
|
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;
|
|
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;
|
|
|
|
// Create mixed practice selection from all discovered words
|
|
this.currentPracticeWords = this.shuffleArray([...this.discoveredWords]);
|
|
|
|
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; |