Class_generator/src/games/QuizGame.js
StillHammer 8ebc0b2334 Add TTS service, deployment docs, and refactor game modules
- Add TTSService.js for text-to-speech functionality
- Add comprehensive deployment documentation (guides, checklists, diagnostics)
- Add new SBS content (chapters 8 & 9)
- Refactor 14 game modules for better maintainability (-947 lines)
- Enhance SettingsDebug.js with improved debugging capabilities
- Update configuration files and startup scripts

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 23:41:12 +08:00

1153 lines
38 KiB
JavaScript

import Module from '../core/Module.js';
import ttsService from '../services/TTSService.js';
/**
* QuizGame - Educational vocabulary quiz with multiple choice questions
* Supports bidirectional quizzing (English to translation or translation to English)
*/
class QuizGame extends Module {
constructor(name, dependencies, config = {}) {
super(name, ['eventBus']);
// Validate dependencies
if (!dependencies.eventBus || !dependencies.content) {
throw new Error('QuizGame requires eventBus and content dependencies');
}
this._eventBus = dependencies.eventBus;
this._content = dependencies.content;
this._config = {
container: null,
questionCount: 10,
optionsCount: 6,
timeLimit: 30, // seconds per question
...config
};
// Game state
this._vocabulary = null;
this._currentQuestion = 0;
this._score = 0;
this._questions = [];
this._currentQuestionData = null;
this._isAnswering = false;
this._gameStartTime = null;
this._questionStartTime = null;
this._quizDirection = 'en-to-translation'; // or 'translation-to-en'
this._timeRemaining = 0;
this._timer = null;
Object.seal(this);
}
/**
* Get game metadata
* @returns {Object} Game metadata
*/
static getMetadata() {
return {
name: 'Vocabulary Quiz',
description: 'Multiple choice vocabulary quiz with bidirectional testing',
difficulty: 'intermediate',
category: 'quiz',
estimatedTime: 8, // minutes
skills: ['vocabulary', 'comprehension', 'recognition', 'speed']
};
}
/**
* Calculate compatibility score with content
* @param {Object} content - Content to check compatibility with
* @returns {Object} Compatibility score and details
*/
static getCompatibilityScore(content) {
const vocab = content?.vocabulary || {};
const vocabCount = Object.keys(vocab).length;
if (vocabCount < 6) {
return {
score: 0,
reason: `Insufficient vocabulary (${vocabCount}/6 required)`,
requirements: ['vocabulary'],
minWords: 6,
details: 'Quiz Game needs at least 6 vocabulary words for multiple choice options'
};
}
// Perfect score at 20+ words, partial score for 6-19
const score = Math.min(vocabCount / 20, 1);
return {
score,
reason: `${vocabCount} vocabulary words available`,
requirements: ['vocabulary'],
minWords: 6,
optimalWords: 20,
details: `Can create quiz with ${Math.min(vocabCount, 10)} questions`
};
}
async init() {
this._validateNotDestroyed();
try {
// Validate container
if (!this._config.container) {
throw new Error('Game container is required');
}
// Extract and validate vocabulary
this._vocabulary = this._extractVocabulary();
if (this._vocabulary.length < this._config.optionsCount) {
throw new Error(`Insufficient vocabulary: need ${this._config.optionsCount}, got ${this._vocabulary.length}`);
}
// Set up event listeners
this._eventBus.on('game:pause', this._handlePause.bind(this), this.name);
this._eventBus.on('game:resume', this._handleResume.bind(this), this.name);
// Inject CSS
this._injectCSS();
// Initialize game interface
this._createGameInterface();
this._generateQuestions();
this._setupEventListeners();
// Start the game
this._gameStartTime = Date.now();
this._showQuizDirectionChoice();
// Emit game ready event
this._eventBus.emit('game:ready', {
gameId: 'quiz-game',
instanceId: this.name,
vocabulary: this._vocabulary.length,
questionsCount: this._questions.length
}, this.name);
this._setInitialized();
} catch (error) {
this._showError(error.message);
throw error;
}
}
async destroy() {
this._validateNotDestroyed();
// Clear timer
if (this._timer) {
clearInterval(this._timer);
this._timer = null;
}
// Remove CSS
this._removeCSS();
// Clean up event listeners
if (this._config.container) {
this._config.container.innerHTML = '';
}
// Emit game end event
this._eventBus.emit('game:ended', {
gameId: 'quiz-game',
instanceId: this.name,
score: this._score,
questionsAnswered: this._currentQuestion,
totalQuestions: this._questions.length,
duration: this._gameStartTime ? Date.now() - this._gameStartTime : 0
}, this.name);
this._setDestroyed();
}
/**
* Get current game state
* @returns {Object} Current game state
*/
getGameState() {
this._validateInitialized();
return {
score: this._score,
currentQuestion: this._currentQuestion,
totalQuestions: this._questions.length,
isComplete: this._currentQuestion >= this._questions.length,
duration: this._gameStartTime ? Date.now() - this._gameStartTime : 0,
timeRemaining: this._timeRemaining
};
}
// Private methods
_extractVocabulary() {
const vocab = this._content?.vocabulary || {};
const vocabulary = [];
for (const [word, data] of Object.entries(vocab)) {
if (data.user_language) {
vocabulary.push({
english: word,
translation: data.user_language,
type: data.type || 'unknown',
pronunciation: data.pronunciation || ''
});
}
}
return this._shuffleArray(vocabulary);
}
_injectCSS() {
const cssId = `quiz-game-styles-${this.name}`;
if (document.getElementById(cssId)) return;
const style = document.createElement('style');
style.id = cssId;
style.textContent = `
.quiz-game {
padding: 20px;
max-width: 800px;
margin: 0 auto;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.quiz-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding: 20px;
background: linear-gradient(135deg, #e2e8f0 0%, #cbd5e0 100%);
border-radius: 12px;
color: #2d3748;
border: 2px solid #4299e1;
font-weight: 600;
}
.quiz-stats {
display: flex;
gap: 30px;
}
.quiz-stat {
text-align: center;
}
.stat-label {
display: block;
font-size: 0.8rem;
opacity: 0.9;
margin-bottom: 5px;
}
.stat-value {
display: block;
font-size: 1.5rem;
font-weight: bold;
}
.quiz-direction-choice {
text-align: center;
padding: 40px;
background: #f8f9fa;
border-radius: 12px;
margin-bottom: 30px;
}
.quiz-direction-choice h3 {
margin-bottom: 20px;
color: #333;
}
.direction-buttons {
display: flex;
gap: 20px;
justify-content: center;
flex-wrap: wrap;
}
.direction-btn {
padding: 15px 30px;
border: none;
border-radius: 8px;
background: #007bff;
color: white;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
min-width: 200px;
}
.direction-btn:hover {
background: #0056b3;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.3);
}
.question-container {
background: white;
border-radius: 12px;
padding: 30px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
.question-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25px;
padding-bottom: 15px;
border-bottom: 2px solid #e9ecef;
}
.question-number {
font-size: 1.1rem;
color: #6c757d;
font-weight: 500;
}
.question-timer {
display: flex;
align-items: center;
gap: 10px;
color: #dc3545;
font-weight: bold;
}
.timer-circle {
width: 40px;
height: 40px;
border-radius: 50%;
background: #dc3545;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.9rem;
}
.timer-circle.warning {
background: #ffc107;
color: #000;
}
.timer-circle.safe {
background: #28a745;
}
.question-text {
font-size: 1.5rem;
text-align: center;
margin-bottom: 30px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
color: #333;
font-weight: 500;
}
.quiz-options {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 15px;
margin-bottom: 30px;
}
.quiz-option {
padding: 20px;
border: 2px solid #e9ecef;
border-radius: 8px;
background: white;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
font-size: 1.1rem;
position: relative;
}
.quiz-option:hover {
border-color: #007bff;
background: #f8f9fa;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.2);
}
.quiz-option.selected {
border-color: #007bff;
background: #e3f2fd;
}
.quiz-option.correct {
border-color: #28a745;
background: #d4edda;
color: #155724;
}
.quiz-option.incorrect {
border-color: #dc3545;
background: #f8d7da;
color: #721c24;
}
.quiz-option.disabled {
pointer-events: none;
opacity: 0.7;
}
.option-pronunciation {
font-size: 0.85rem;
font-style: italic;
color: #6c757d;
margin-top: 8px;
transition: all 0.3s ease;
}
.quiz-feedback {
text-align: center;
padding: 20px;
border-radius: 8px;
margin: 20px 0;
font-size: 1.1rem;
font-weight: 500;
}
.quiz-feedback.correct {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.quiz-feedback.incorrect {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.next-question-btn {
display: block;
margin: 20px auto;
padding: 12px 30px;
background: #28a745;
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
}
.next-question-btn:hover {
background: #218838;
transform: translateY(-2px);
}
.quiz-complete {
text-align: center;
padding: 40px;
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
color: white;
border-radius: 12px;
}
.quiz-complete h2 {
margin-bottom: 20px;
}
.final-score {
font-size: 2rem;
font-weight: bold;
margin: 20px 0;
}
.quiz-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 20px;
margin: 30px 0;
background: rgba(255, 255, 255, 0.1);
padding: 20px;
border-radius: 8px;
}
.summary-stat {
text-align: center;
}
.summary-stat-value {
display: block;
font-size: 1.5rem;
font-weight: bold;
margin-bottom: 5px;
}
.summary-stat-label {
font-size: 0.9rem;
opacity: 0.9;
}
.restart-btn, .exit-btn {
margin: 10px;
padding: 12px 25px;
border: 2px solid white;
border-radius: 8px;
background: transparent;
color: white;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
}
.restart-btn:hover, .exit-btn:hover {
background: white;
color: #28a745;
}
.quiz-error {
text-align: center;
padding: 40px;
background: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 12px;
color: #721c24;
}
.error-icon {
font-size: 3rem;
margin-bottom: 20px;
}
@media (max-width: 768px) {
.quiz-header {
flex-direction: column;
gap: 20px;
}
.quiz-stats {
gap: 20px;
}
.quiz-options {
grid-template-columns: 1fr;
}
.direction-buttons {
flex-direction: column;
align-items: center;
}
}
`;
document.head.appendChild(style);
}
_removeCSS() {
const cssId = `quiz-game-styles-${this.name}`;
const existingStyle = document.getElementById(cssId);
if (existingStyle) {
existingStyle.remove();
}
}
_createGameInterface() {
this._config.container.innerHTML = `
<div class="quiz-game">
<div class="quiz-header">
<div class="quiz-stats">
<div class="quiz-stat">
<span class="stat-label">Score</span>
<span class="stat-value" id="quiz-score">0</span>
</div>
<div class="quiz-stat">
<span class="stat-label">Question</span>
<span class="stat-value">
<span id="current-question">0</span>/${this._config.questionCount}
</span>
</div>
<div class="quiz-stat">
<span class="stat-label">Accuracy</span>
<span class="stat-value" id="quiz-accuracy">0%</span>
</div>
</div>
<button class="btn btn-outline btn-sm" id="exit-quiz">
<span class="btn-icon">←</span>
<span class="btn-text">Exit Quiz</span>
</button>
</div>
<div id="quiz-content"></div>
</div>
`;
}
_generateQuestions() {
this._questions = [];
const questionCount = Math.min(this._config.questionCount, this._vocabulary.length);
const selectedVocab = this._shuffleArray([...this._vocabulary]).slice(0, questionCount);
selectedVocab.forEach((vocab, index) => {
this._questions.push({
id: index,
vocabulary: vocab,
options: this._generateOptions(vocab),
correctAnswer: null, // Will be set based on quiz direction
userAnswer: null,
timeSpent: 0,
answered: false
});
});
}
_generateOptions(correctVocab) {
const options = [];
const otherVocab = this._vocabulary.filter(v => v !== correctVocab);
const shuffledOthers = this._shuffleArray(otherVocab);
// Add correct answer
options.push(correctVocab);
// Add incorrect options
for (let i = 0; i < this._config.optionsCount - 1 && i < shuffledOthers.length; i++) {
options.push(shuffledOthers[i]);
}
return this._shuffleArray(options);
}
_showQuizDirectionChoice() {
const content = document.getElementById('quiz-content');
content.innerHTML = `
<div class="quiz-direction-choice">
<h3>Choose Quiz Direction</h3>
<p>How would you like to be tested?</p>
<div class="direction-buttons">
<button class="direction-btn" data-direction="en-to-translation">
English → Translation
<br><small>See English words, choose translations</small>
</button>
<button class="direction-btn" data-direction="translation-to-en">
Translation → English
<br><small>See translations, choose English words</small>
</button>
</div>
</div>
`;
}
_startQuiz(direction) {
this._quizDirection = direction;
this._currentQuestion = 0;
this._score = 0;
// Set correct answers based on direction
this._questions.forEach(question => {
if (direction === 'en-to-translation') {
question.correctAnswer = question.vocabulary.translation;
} else {
question.correctAnswer = question.vocabulary.english;
}
});
this._showQuestion();
}
_showQuestion() {
if (this._currentQuestion >= this._questions.length) {
this._showResults();
return;
}
const question = this._questions[this._currentQuestion];
const content = document.getElementById('quiz-content');
// Determine question text based on direction
const questionText = this._quizDirection === 'en-to-translation'
? question.vocabulary.english
: question.vocabulary.translation;
content.innerHTML = `
<div class="question-container">
<div class="question-header">
<div class="question-number">
Question ${this._currentQuestion + 1} of ${this._questions.length}
</div>
<div class="question-timer">
<div class="timer-circle safe" id="timer-circle">
<span id="timer-value">${this._config.timeLimit}</span>
</div>
</div>
</div>
<div class="question-text">
${questionText}
</div>
<div class="quiz-options" id="quiz-options">
${this._generateOptionHTML(question)}
</div>
<div id="quiz-feedback"></div>
</div>
`;
this._startQuestionTimer();
this._questionStartTime = Date.now();
}
_generateOptionHTML(question) {
return question.options.map((vocab, index) => {
const optionText = this._quizDirection === 'en-to-translation'
? vocab.translation
: vocab.english;
// Store the Chinese word and pronunciation for TTS
const chineseWord = vocab.english; // In our data structure, english is the Chinese word
const pronunciation = vocab.pronunciation || '';
return `
<div class="quiz-option"
data-option="${index}"
data-value="${optionText}"
data-word="${chineseWord}"
data-pronunciation="${pronunciation}"
title="Click to hear pronunciation">
${optionText}
${pronunciation ? `<div class="option-pronunciation" style="display: none;">[${pronunciation}]</div>` : ''}
</div>
`;
}).join('');
}
_startQuestionTimer() {
this._timeRemaining = this._config.timeLimit;
this._timer = setInterval(() => {
this._timeRemaining--;
this._updateTimer();
if (this._timeRemaining <= 0) {
this._handleTimeout();
}
}, 1000);
}
_updateTimer() {
const timerValue = document.getElementById('timer-value');
const timerCircle = document.getElementById('timer-circle');
if (timerValue) {
timerValue.textContent = this._timeRemaining;
}
if (timerCircle) {
timerCircle.className = 'timer-circle';
if (this._timeRemaining <= 5) {
timerCircle.classList.add('warning');
} else if (this._timeRemaining > 15) {
timerCircle.classList.add('safe');
}
}
}
_setupEventListeners() {
// Direction choice listeners
this._config.container.addEventListener('click', (event) => {
if (event.target.matches('.direction-btn')) {
const direction = event.target.dataset.direction;
this._startQuiz(direction);
}
if (event.target.matches('.quiz-option')) {
this._handleAnswerClick(event.target);
}
if (event.target.matches('.next-question-btn')) {
this._nextQuestion();
}
if (event.target.matches('.restart-btn')) {
this._restartQuiz();
}
});
// Exit button
const exitButton = this._config.container.querySelector('#exit-quiz');
if (exitButton) {
exitButton.addEventListener('click', () => {
this._eventBus.emit('game:exit-request', { instanceId: this.name }, this.name);
});
}
}
_handleAnswerClick(optionElement) {
if (this._isAnswering || optionElement.classList.contains('disabled')) {
return;
}
this._isAnswering = true;
const question = this._questions[this._currentQuestion];
const selectedAnswer = optionElement.dataset.value;
const isCorrect = selectedAnswer === question.correctAnswer;
// Clear timer
if (this._timer) {
clearInterval(this._timer);
this._timer = null;
}
// Record answer
question.userAnswer = selectedAnswer;
question.answered = true;
question.timeSpent = this._questionStartTime ? Date.now() - this._questionStartTime : 0;
// Update score
if (isCorrect) {
const timeBonus = Math.max(0, this._timeRemaining * 2);
this._score += 100 + timeBonus;
}
// Play TTS and show pronunciation
if (isCorrect) {
// For correct answer, play the clicked option
const word = optionElement.dataset.word;
const pronunciation = optionElement.dataset.pronunciation;
if (word) {
this._playAudio(word);
if (pronunciation) {
const pronunciationElement = optionElement.querySelector('.option-pronunciation');
if (pronunciationElement) {
pronunciationElement.style.display = 'block';
this._highlightPronunciation(optionElement);
}
}
}
} else {
// For incorrect answer, find and play the correct option's TTS
const correctOption = this._findCorrectOption(question.correctAnswer);
if (correctOption) {
const word = correctOption.dataset.word;
const pronunciation = correctOption.dataset.pronunciation;
if (word) {
this._playAudio(word);
if (pronunciation) {
const pronunciationElement = correctOption.querySelector('.option-pronunciation');
if (pronunciationElement) {
pronunciationElement.style.display = 'block';
this._highlightPronunciation(correctOption);
}
}
}
}
}
// Show feedback
this._showAnswerFeedback(isCorrect, question);
this._updateStats();
// Emit answer event
this._eventBus.emit('quiz:answer', {
gameId: 'quiz-game',
instanceId: this.name,
questionNumber: this._currentQuestion + 1,
isCorrect,
score: this._score,
timeSpent: question.timeSpent
}, this.name);
}
_findCorrectOption(correctAnswer) {
const optionsContainer = document.getElementById('quiz-options');
if (!optionsContainer) return null;
const options = optionsContainer.querySelectorAll('.quiz-option');
for (const option of options) {
if (option.dataset.value === correctAnswer) {
return option;
}
}
return null;
}
_handleTimeout() {
if (this._timer) {
clearInterval(this._timer);
this._timer = null;
}
const question = this._questions[this._currentQuestion];
question.answered = true;
question.timeSpent = this._config.timeLimit * 1000;
this._showAnswerFeedback(false, question, true);
this._updateStats();
}
_showAnswerFeedback(isCorrect, question, timeout = false) {
const optionsContainer = document.getElementById('quiz-options');
const feedbackContainer = document.getElementById('quiz-feedback');
// Disable all options
optionsContainer.querySelectorAll('.quiz-option').forEach(option => {
option.classList.add('disabled');
const optionValue = option.dataset.value;
if (optionValue === question.correctAnswer) {
option.classList.add('correct');
} else if (optionValue === question.userAnswer) {
option.classList.add('incorrect');
}
});
// Show feedback message
let feedbackMessage;
if (timeout) {
feedbackMessage = `⏰ Time's up! The correct answer was: <strong>${question.correctAnswer}</strong>`;
} else if (isCorrect) {
feedbackMessage = `🎉 Correct! +${100 + Math.max(0, this._timeRemaining * 2)} points`;
} else {
feedbackMessage = `❌ Incorrect. The correct answer was: <strong>${question.correctAnswer}</strong>`;
}
feedbackContainer.innerHTML = `
<div class="quiz-feedback ${isCorrect ? 'correct' : 'incorrect'}">
${feedbackMessage}
</div>
<button class="next-question-btn">
${this._currentQuestion + 1 >= this._questions.length ? 'View Results' : 'Next Question'}
</button>
`;
}
_nextQuestion() {
this._currentQuestion++;
this._isAnswering = false;
this._showQuestion();
}
_showResults() {
const correctAnswers = this._questions.filter(q => q.userAnswer === q.correctAnswer).length;
const accuracy = Math.round((correctAnswers / this._questions.length) * 100);
const totalTime = this._gameStartTime ? Date.now() - this._gameStartTime : 0;
const avgTimePerQuestion = Math.round(totalTime / this._questions.length / 1000);
// Store best score
const gameKey = 'quiz-game';
const currentScore = this._score;
const bestScore = parseInt(localStorage.getItem(`${gameKey}-best-score`) || '0');
const isNewBest = currentScore > bestScore;
if (isNewBest) {
localStorage.setItem(`${gameKey}-best-score`, currentScore.toString());
}
// Show victory popup
this._showVictoryPopup({
gameTitle: 'Vocabulary Quiz',
currentScore,
bestScore: isNewBest ? currentScore : bestScore,
isNewBest,
stats: {
'Questions': `${correctAnswers}/${this._questions.length}`,
'Accuracy': `${accuracy}%`,
'Avg Time': `${avgTimePerQuestion}s`,
'Total Time': `${Math.round(totalTime / 1000)}s`
}
});
// Emit completion event
this._eventBus.emit('game:completed', {
gameId: 'quiz-game',
instanceId: this.name,
score: this._score,
correctAnswers,
totalQuestions: this._questions.length,
accuracy,
duration: totalTime
}, this.name);
}
_restartQuiz() {
this._currentQuestion = 0;
this._score = 0;
this._isAnswering = false;
this._generateQuestions();
this._showQuizDirectionChoice();
this._updateStats();
}
_updateStats() {
const scoreElement = document.getElementById('quiz-score');
const questionElement = document.getElementById('current-question');
const accuracyElement = document.getElementById('quiz-accuracy');
if (scoreElement) scoreElement.textContent = this._score;
if (questionElement) questionElement.textContent = this._currentQuestion + 1;
if (accuracyElement && this._currentQuestion > 0) {
const answeredQuestions = this._questions.slice(0, this._currentQuestion + 1).filter(q => q.answered);
const correctAnswers = answeredQuestions.filter(q => q.userAnswer === q.correctAnswer).length;
const accuracy = answeredQuestions.length > 0 ? Math.round((correctAnswers / answeredQuestions.length) * 100) : 0;
accuracyElement.textContent = `${accuracy}%`;
}
}
_showError(message) {
if (this._config.container) {
this._config.container.innerHTML = `
<div class="quiz-error">
<div class="error-icon">❌</div>
<h3>Quiz Error</h3>
<p>${message}</p>
<button class="btn btn-primary" onclick="history.back()">Go Back</button>
</div>
`;
}
}
_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;
}
_handlePause() {
if (this._timer) {
clearInterval(this._timer);
this._timer = null;
}
this._eventBus.emit('game:paused', { instanceId: this.name }, this.name);
}
_handleResume() {
if (this._timeRemaining > 0 && !this._isAnswering) {
this._startQuestionTimer();
}
this._eventBus.emit('game:resumed', { instanceId: this.name }, this.name);
}
async _playAudio(text) {
const chapterLanguage = this._content?.language || 'en-US';
await ttsService.speak(text, chapterLanguage, { rate: 0.8 });
}
_highlightPronunciation(optionElement) {
const pronunciation = optionElement.querySelector('.option-pronunciation');
if (pronunciation) {
// Store original styles
const originalColor = pronunciation.style.color;
const originalFontWeight = pronunciation.style.fontWeight;
// Add highlight
pronunciation.style.color = '#007bff';
pronunciation.style.fontWeight = 'bold';
pronunciation.style.transform = 'scale(1.2)';
pronunciation.style.transition = 'all 0.3s ease';
// Remove highlight after animation
setTimeout(() => {
pronunciation.style.color = originalColor;
pronunciation.style.fontWeight = originalFontWeight;
pronunciation.style.transform = 'scale(1)';
}, 2000);
}
}
_showVictoryPopup({ gameTitle, currentScore, bestScore, isNewBest, stats }) {
const popup = document.createElement('div');
popup.className = 'victory-popup';
popup.innerHTML = `
<div class="victory-content">
<div class="victory-header">
<div class="victory-icon">🏆</div>
<h2 class="victory-title">${gameTitle} Complete!</h2>
${isNewBest ? '<div class="new-best-badge">🎉 New Best Score!</div>' : ''}
</div>
<div class="victory-scores">
<div class="score-display">
<div class="score-label">Your Score</div>
<div class="score-value">${currentScore}</div>
</div>
<div class="score-display best-score">
<div class="score-label">Best Score</div>
<div class="score-value">${bestScore}</div>
</div>
</div>
<div class="victory-stats">
${Object.entries(stats).map(([key, value]) => `
<div class="stat-item">
<div class="stat-label">${key}</div>
<div class="stat-value">${value}</div>
</div>
`).join('')}
</div>
<div class="victory-actions">
<button class="victory-btn victory-btn-primary" id="play-again-btn">
<span class="btn-icon">🔄</span>
<span class="btn-text">Play Again</span>
</button>
<button class="victory-btn victory-btn-secondary" id="different-game-btn">
<span class="btn-icon">🎮</span>
<span class="btn-text">Different Game</span>
</button>
<button class="victory-btn victory-btn-outline" id="main-menu-btn">
<span class="btn-icon">🏠</span>
<span class="btn-text">Main Menu</span>
</button>
</div>
</div>
`;
document.body.appendChild(popup);
// Animate in
requestAnimationFrame(() => {
popup.classList.add('show');
});
// Add event listeners
popup.querySelector('#play-again-btn').addEventListener('click', () => {
popup.remove();
this._restartQuiz();
});
popup.querySelector('#different-game-btn').addEventListener('click', () => {
popup.remove();
if (window.app && window.app.getCore().router) {
window.app.getCore().router.navigate('/games');
} else {
window.location.href = '/#/games';
}
});
popup.querySelector('#main-menu-btn').addEventListener('click', () => {
popup.remove();
if (window.app && window.app.getCore().router) {
window.app.getCore().router.navigate('/');
} else {
window.location.href = '/';
}
});
// Close on backdrop click
popup.addEventListener('click', (e) => {
if (e.target === popup) {
popup.remove();
if (window.app && window.app.getCore().router) {
window.app.getCore().router.navigate('/games');
} else {
window.location.href = '/#/games';
}
}
});
}
}
export default QuizGame;