Class_generator/src/games/ThematicQuestions.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

1142 lines
37 KiB
JavaScript

import Module from '../core/Module.js';
import ttsService from '../services/TTSService.js';
/**
* ThematicQuestionsGame - Listening and speaking practice with self-assessment
* Students listen to questions (TTS), answer them, can reveal text if needed,
* view example responses, and self-assess their spoken answers
*/
class ThematicQuestionsGame extends Module {
constructor(name, dependencies, config = {}) {
super(name, ['eventBus']);
// Validate dependencies
if (!dependencies.eventBus || !dependencies.content) {
throw new Error('ThematicQuestionsGame requires eventBus and content dependencies');
}
this._eventBus = dependencies.eventBus;
this._content = dependencies.content;
this._config = {
container: null,
autoPlayTTS: false, // Auto-play question on load
...config
};
// Game state
this._questions = [];
this._currentIndex = 0;
this._score = 0;
this._correctCount = 0;
this._incorrectCount = 0;
this._showingExamples = false;
this._hasAnswered = false;
this._gameStartTime = null;
this._questionStartTime = null;
Object.seal(this);
}
/**
* Get game metadata
* @returns {Object} Game metadata
*/
static getMetadata() {
return {
name: 'Thematic Questions',
description: 'Listen to questions and practice speaking with self-assessment',
difficulty: 'beginner',
category: 'listening',
estimatedTime: 10, // minutes
skills: ['listening', 'speaking', 'comprehension', 'self-assessment']
};
}
/**
* Calculate compatibility score with content
* @param {Object} content - Content to check compatibility with
* @returns {Object} Compatibility score and details
*/
static getCompatibilityScore(content) {
const thematicQuestions = content?.thematic_questions || {};
// Count total questions across all themes
let totalQuestions = 0;
for (const theme of Object.values(thematicQuestions)) {
if (Array.isArray(theme)) {
totalQuestions += theme.length;
}
}
if (totalQuestions < 5) {
return {
score: 0,
reason: `Insufficient questions (${totalQuestions}/5 required)`,
requirements: ['thematic_questions'],
minQuestions: 5,
details: 'Thematic Questions needs at least 5 questions to play'
};
}
// Perfect score at 20+ questions, partial score for 5-19
const score = Math.min(totalQuestions / 20, 1);
return {
score,
reason: `${totalQuestions} thematic questions available`,
requirements: ['thematic_questions'],
minQuestions: 5,
optimalQuestions: 20,
details: `Can create practice session with ${totalQuestions} questions`
};
}
async init() {
this._validateNotDestroyed();
try {
// Validate container
if (!this._config.container) {
throw new Error('Game container is required');
}
// Extract and validate questions
this._questions = this._extractQuestions();
if (this._questions.length === 0) {
throw new Error('No thematic questions found in content');
}
// 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._setupEventListeners();
// Start the game
this._gameStartTime = Date.now();
this._showQuestion();
// Emit game ready event
this._eventBus.emit('game:ready', {
gameId: 'thematic-questions',
instanceId: this.name,
questionsCount: this._questions.length
}, this.name);
this._setInitialized();
} catch (error) {
this._showError(error.message);
throw error;
}
}
async destroy() {
this._validateNotDestroyed();
// 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: 'thematic-questions',
instanceId: this.name,
score: this._score,
questionsAnswered: this._currentIndex,
totalQuestions: this._questions.length,
correctCount: this._correctCount,
incorrectCount: this._incorrectCount,
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._currentIndex,
totalQuestions: this._questions.length,
correctCount: this._correctCount,
incorrectCount: this._incorrectCount,
isComplete: this._currentIndex >= this._questions.length,
duration: this._gameStartTime ? Date.now() - this._gameStartTime : 0
};
}
// Private methods
_extractQuestions() {
const thematicQuestions = this._content?.thematic_questions || {};
const allQuestions = [];
// Flatten all themes into single array
for (const [themeName, questions] of Object.entries(thematicQuestions)) {
if (Array.isArray(questions)) {
questions.forEach(q => {
allQuestions.push({
...q,
themeName: themeName
});
});
}
}
// Shuffle questions randomly (Fisher-Yates algorithm)
return this._shuffleArray(allQuestions);
}
_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;
}
_injectCSS() {
const cssId = `thematic-questions-styles-${this.name}`;
if (document.getElementById(cssId)) return;
const style = document.createElement('style');
style.id = cssId;
style.textContent = `
.thematic-questions-game {
padding: 20px;
max-width: 800px;
margin: 0 auto;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.tq-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
color: white;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
}
.tq-stats {
display: flex;
gap: 30px;
}
.tq-stat {
text-align: center;
}
.tq-stat-label {
display: block;
font-size: 0.8rem;
opacity: 0.9;
margin-bottom: 5px;
}
.tq-stat-value {
display: block;
font-size: 1.5rem;
font-weight: bold;
}
.question-card {
background: white;
border-radius: 12px;
padding: 40px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
min-height: 400px;
display: flex;
flex-direction: column;
}
.question-theme {
display: inline-block;
padding: 6px 12px;
background: #e3f2fd;
color: #1976d2;
border-radius: 20px;
font-size: 0.85rem;
margin-bottom: 20px;
font-weight: 500;
}
.question-progress {
text-align: center;
color: #6c757d;
font-size: 0.9rem;
margin-bottom: 10px;
}
.question-display {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
margin-bottom: 30px;
}
.listening-prompt {
font-size: 1.5rem;
color: #667eea;
margin-bottom: 40px;
font-weight: 600;
padding: 20px;
background: linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%);
border-radius: 12px;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.02); }
}
.question-text {
font-size: 2rem;
color: #333;
margin-bottom: 15px;
font-weight: 600;
line-height: 1.4;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
border-left: 4px solid #28a745;
transition: all 0.4s ease;
}
.question-text.hidden {
display: none;
}
.question-text.visible {
display: block;
animation: slideDown 0.4s ease;
}
.question-translation {
font-size: 1.2rem;
color: #6c757d;
font-style: italic;
margin-bottom: 25px;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
border-left: 4px solid #dc3545;
transition: all 0.4s ease;
}
.question-translation.hidden {
display: none;
}
.question-translation.visible {
display: block;
animation: slideDown 0.4s ease;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.reveal-controls {
display: flex;
justify-content: center;
gap: 15px;
margin-bottom: 20px;
}
.btn-reveal {
padding: 12px 24px;
background: #17a2b8;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
}
.btn-reveal:hover:not(:disabled) {
background: #138496;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(23, 162, 184, 0.3);
}
.btn-reveal:disabled {
cursor: not-allowed;
opacity: 0.5;
}
.tts-controls {
display: flex;
justify-content: center;
gap: 15px;
margin-bottom: 30px;
}
.btn-tts {
padding: 12px 24px;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
}
.btn-tts:hover {
background: #5568d3;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.btn-tts:active {
transform: translateY(0);
}
.btn-show-examples {
padding: 12px 24px;
background: #28a745;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
transition: all 0.3s ease;
}
.btn-show-examples:hover {
background: #218838;
transform: translateY(-2px);
}
.btn-show-examples.active {
background: #ffc107;
color: #000;
}
.examples-container {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
margin-bottom: 30px;
border-left: 4px solid #667eea;
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease, padding 0.3s ease, margin 0.3s ease;
}
.examples-container.visible {
max-height: 500px;
margin-bottom: 30px;
padding: 20px;
}
.examples-container.hidden {
padding: 0;
margin: 0;
}
.examples-title {
font-size: 1.1rem;
font-weight: 600;
color: #333;
margin-bottom: 15px;
}
.example-item {
padding: 10px 15px;
background: white;
border-radius: 6px;
margin-bottom: 10px;
font-size: 1rem;
color: #495057;
border-left: 3px solid #667eea;
}
.example-item:last-child {
margin-bottom: 0;
}
.assessment-section {
text-align: center;
margin-top: 20px;
}
.assessment-title {
font-size: 1.1rem;
color: #333;
margin-bottom: 15px;
font-weight: 500;
}
.assessment-buttons {
display: flex;
gap: 15px;
justify-content: center;
}
.btn-correct {
padding: 15px 40px;
background: #28a745;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1.1rem;
font-weight: 600;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 10px;
}
.btn-correct:hover {
background: #218838;
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(40, 167, 69, 0.3);
}
.btn-incorrect {
padding: 15px 40px;
background: #dc3545;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1.1rem;
font-weight: 600;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 10px;
}
.btn-incorrect:hover {
background: #c82333;
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(220, 53, 69, 0.3);
}
.btn-next {
display: block;
margin: 20px auto 0;
padding: 12px 30px;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-next:hover {
background: #5568d3;
transform: translateY(-2px);
}
.feedback-message {
text-align: center;
padding: 15px;
border-radius: 8px;
margin: 20px 0;
font-size: 1.1rem;
font-weight: 500;
}
.feedback-message.correct {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.feedback-message.incorrect {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.tq-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) {
.tq-header {
flex-direction: column;
gap: 20px;
}
.tq-stats {
gap: 20px;
}
.question-card {
padding: 20px;
}
.question-text {
font-size: 1.5rem;
}
.question-translation {
font-size: 1rem;
}
.assessment-buttons {
flex-direction: column;
}
.btn-correct, .btn-incorrect {
width: 100%;
}
}
`;
document.head.appendChild(style);
}
_removeCSS() {
const cssId = `thematic-questions-styles-${this.name}`;
const existingStyle = document.getElementById(cssId);
if (existingStyle) {
existingStyle.remove();
}
}
_createGameInterface() {
this._config.container.innerHTML = `
<div class="thematic-questions-game">
<div class="tq-header">
<div class="tq-stats">
<div class="tq-stat">
<span class="tq-stat-label">✅ Correct</span>
<span class="tq-stat-value" id="tq-correct">0</span>
</div>
<div class="tq-stat">
<span class="tq-stat-label">❌ Incorrect</span>
<span class="tq-stat-value" id="tq-incorrect">0</span>
</div>
<div class="tq-stat">
<span class="tq-stat-label">Progress</span>
<span class="tq-stat-value">
<span id="tq-current">0</span>/${this._questions.length}
</span>
</div>
</div>
<button class="btn btn-outline btn-sm" id="exit-game">
<span class="btn-icon">←</span>
<span class="btn-text">Exit</span>
</button>
</div>
<div id="tq-content"></div>
</div>
`;
}
_setupEventListeners() {
// Exit button
const exitButton = this._config.container.querySelector('#exit-game');
if (exitButton) {
exitButton.addEventListener('click', () => {
this._eventBus.emit('game:exit-request', { instanceId: this.name }, this.name);
});
}
// Event delegation for dynamic buttons
this._config.container.addEventListener('click', (event) => {
if (event.target.matches('#tts-btn') || event.target.closest('#tts-btn')) {
this._handleTTS();
}
if (event.target.matches('#reveal-english-btn') || event.target.closest('#reveal-english-btn')) {
this._revealText('english');
}
if (event.target.matches('#reveal-chinese-btn') || event.target.closest('#reveal-chinese-btn')) {
this._revealText('chinese');
}
if (event.target.matches('#show-examples-btn') || event.target.closest('#show-examples-btn')) {
this._toggleExamples();
}
if (event.target.matches('#btn-correct') || event.target.closest('#btn-correct')) {
this._handleSelfAssessment(true);
}
if (event.target.matches('#btn-incorrect') || event.target.closest('#btn-incorrect')) {
this._handleSelfAssessment(false);
}
if (event.target.matches('#next-btn') || event.target.closest('#next-btn')) {
this._nextQuestion();
}
});
}
_showQuestion() {
if (this._currentIndex >= this._questions.length) {
this._showResults();
return;
}
const question = this._questions[this._currentIndex];
const content = document.getElementById('tq-content');
this._showingExamples = false;
this._hasAnswered = false;
this._questionStartTime = Date.now();
content.innerHTML = `
<div class="question-card">
<div class="question-progress">
Question ${this._currentIndex + 1} of ${this._questions.length}
</div>
<div class="question-theme">${this._formatThemeName(question.themeName)}</div>
<div class="question-display">
<div class="listening-prompt">
🎧 Listen to the question and answer it
</div>
<div class="question-text hidden" id="question-text-en">
${question.question}
</div>
<div class="question-translation hidden" id="question-text-zh">
${question.question_user_language}
</div>
</div>
<div class="reveal-controls">
<button class="btn-reveal" id="reveal-english-btn">
<span class="btn-icon">🇬🇧</span>
<span class="btn-text">Show English</span>
</button>
<button class="btn-reveal" id="reveal-chinese-btn">
<span class="btn-icon">🇨🇳</span>
<span class="btn-text">Show Chinese</span>
</button>
</div>
<div class="tts-controls">
<button class="btn-tts" id="tts-btn">
<span class="btn-icon">🔊</span>
<span class="btn-text">Listen Again</span>
</button>
<button class="btn-show-examples" id="show-examples-btn">
<span class="btn-icon">💡</span>
<span class="btn-text">Show Examples</span>
</button>
</div>
<div class="examples-container hidden" id="examples-container">
<div class="examples-title">Example Responses:</div>
${question.example_responses.map(example => `
<div class="example-item">${example}</div>
`).join('')}
</div>
<div class="assessment-section" id="assessment-section">
<div class="assessment-title">Was your answer correct?</div>
<div class="assessment-buttons">
<button class="btn-correct" id="btn-correct">
<span class="btn-icon">✅</span>
<span class="btn-text">Correct</span>
</button>
<button class="btn-incorrect" id="btn-incorrect">
<span class="btn-icon">❌</span>
<span class="btn-text">Incorrect</span>
</button>
</div>
</div>
<div id="feedback-container"></div>
</div>
`;
// Auto-play TTS on question load for listening exercise
if (question.tts_enabled) {
setTimeout(() => this._handleTTS(), 200);
}
}
_formatThemeName(theme) {
return theme
.split('_')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
_revealText(language) {
if (language === 'english') {
const textElement = document.getElementById('question-text-en');
const btn = document.getElementById('reveal-english-btn');
if (textElement && btn) {
textElement.classList.remove('hidden');
textElement.classList.add('visible');
btn.disabled = true;
btn.style.opacity = '0.5';
}
} else if (language === 'chinese') {
const textElement = document.getElementById('question-text-zh');
const btn = document.getElementById('reveal-chinese-btn');
if (textElement && btn) {
textElement.classList.remove('hidden');
textElement.classList.add('visible');
btn.disabled = true;
btn.style.opacity = '0.5';
}
}
}
_toggleExamples() {
const container = document.getElementById('examples-container');
const btn = document.getElementById('show-examples-btn');
if (!container || !btn) return;
this._showingExamples = !this._showingExamples;
if (this._showingExamples) {
container.classList.remove('hidden');
container.classList.add('visible');
btn.classList.add('active');
btn.innerHTML = `
<span class="btn-icon">👁️</span>
<span class="btn-text">Hide Examples</span>
`;
} else {
container.classList.remove('visible');
container.classList.add('hidden');
btn.classList.remove('active');
btn.innerHTML = `
<span class="btn-icon">💡</span>
<span class="btn-text">Show Examples</span>
`;
}
}
_handleSelfAssessment(isCorrect) {
if (this._hasAnswered) return;
this._hasAnswered = true;
const question = this._questions[this._currentIndex];
// Update stats
if (isCorrect) {
this._correctCount++;
this._score += 100;
} else {
this._incorrectCount++;
}
// Show feedback
this._showFeedback(isCorrect);
this._updateStats();
// Record time spent
const timeSpent = this._questionStartTime ? Date.now() - this._questionStartTime : 0;
// Emit answer event
this._eventBus.emit('thematic-questions:answer', {
gameId: 'thematic-questions',
instanceId: this.name,
questionNumber: this._currentIndex + 1,
question: question.question,
isCorrect,
score: this._score,
timeSpent
}, this.name);
}
_showFeedback(isCorrect) {
const assessmentSection = document.getElementById('assessment-section');
const feedbackContainer = document.getElementById('feedback-container');
if (!assessmentSection || !feedbackContainer) return;
// Hide assessment buttons
assessmentSection.style.display = 'none';
// Show feedback
const feedbackClass = isCorrect ? 'correct' : 'incorrect';
const feedbackIcon = isCorrect ? '🎉' : '💪';
const feedbackText = isCorrect
? 'Great job! Your answer was correct!'
: 'Keep practicing! Try to review the examples.';
feedbackContainer.innerHTML = `
<div class="feedback-message ${feedbackClass}">
${feedbackIcon} ${feedbackText}
</div>
<button class="btn-next" id="next-btn">
${this._currentIndex + 1 >= this._questions.length ? 'View Results' : 'Next Question →'}
</button>
`;
}
_nextQuestion() {
this._currentIndex++;
this._showQuestion();
}
_showResults() {
const accuracy = this._questions.length > 0
? Math.round((this._correctCount / this._questions.length) * 100)
: 0;
const totalTime = this._gameStartTime ? Date.now() - this._gameStartTime : 0;
// Store best score
const gameKey = 'thematic-questions';
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: 'Thematic Questions',
currentScore,
bestScore: isNewBest ? currentScore : bestScore,
isNewBest,
stats: {
'Questions': `${this._questions.length}`,
'Correct': `${this._correctCount}`,
'Incorrect': `${this._incorrectCount}`,
'Accuracy': `${accuracy}%`,
'Total Time': `${Math.round(totalTime / 1000)}s`
}
});
// Emit completion event
this._eventBus.emit('game:completed', {
gameId: 'thematic-questions',
instanceId: this.name,
score: this._score,
correctCount: this._correctCount,
incorrectCount: this._incorrectCount,
totalQuestions: this._questions.length,
accuracy,
duration: totalTime
}, this.name);
}
_updateStats() {
const correctElement = document.getElementById('tq-correct');
const incorrectElement = document.getElementById('tq-incorrect');
const currentElement = document.getElementById('tq-current');
if (correctElement) correctElement.textContent = this._correctCount;
if (incorrectElement) incorrectElement.textContent = this._incorrectCount;
if (currentElement) currentElement.textContent = this._currentIndex + 1;
}
_handleTTS() {
const question = this._questions[this._currentIndex];
if (question && question.tts_enabled && question.question) {
this._playAudio(question.question);
}
}
async _playAudio(text) {
// Get language from chapter content, fallback to en-US
const chapterLanguage = this._content?.language || 'en-US';
// Visual feedback
const ttsBtn = document.getElementById('tts-btn');
let originalHTML = '';
if (ttsBtn) {
originalHTML = ttsBtn.innerHTML;
ttsBtn.innerHTML = '<span class="btn-icon">🔄</span><span class="btn-text">Speaking...</span>';
ttsBtn.disabled = true;
}
try {
await ttsService.speak(text, chapterLanguage, { rate: 0.85, volume: 1.0 });
} catch (error) {
console.warn('🔊 Speech Synthesis error:', error);
} finally {
if (ttsBtn) {
ttsBtn.innerHTML = originalHTML;
ttsBtn.disabled = false;
}
}
}
_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);
requestAnimationFrame(() => {
popup.classList.add('show');
});
// Event listeners
popup.querySelector('#play-again-btn').addEventListener('click', () => {
popup.remove();
this._restartGame();
});
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 = '/';
}
});
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';
}
}
});
}
_restartGame() {
this._currentIndex = 0;
this._score = 0;
this._correctCount = 0;
this._incorrectCount = 0;
this._showingExamples = false;
this._hasAnswered = false;
this._gameStartTime = Date.now();
this._updateStats();
this._showQuestion();
}
_showError(message) {
if (this._config.container) {
this._config.container.innerHTML = `
<div class="tq-error">
<div class="error-icon">❌</div>
<h3>Game Error</h3>
<p>${message}</p>
<button class="btn btn-primary" onclick="history.back()">Go Back</button>
</div>
`;
}
}
_handlePause() {
this._eventBus.emit('game:paused', { instanceId: this.name }, this.name);
}
_handleResume() {
this._eventBus.emit('game:resumed', { instanceId: this.name }, this.name);
}
}
export default ThematicQuestionsGame;