- Add AIReportSystem.js for detailed AI response capture and report generation - Add AIReportInterface.js UI component for report access and export - Integrate AI reporting into LLMValidator and SmartPreviewOrchestrator - Add missing modules to Application.js configuration (unifiedDRS, smartPreviewOrchestrator) - Create missing content/chapters/sbs.json for book metadata - Enhance Application.js with debug logging for module loading - Add multi-format export capabilities (text, HTML, JSON) - Implement automatic learning insights extraction from AI feedback - Add session management and performance tracking for AI reports 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1205 lines
40 KiB
JavaScript
1205 lines
40 KiB
JavaScript
import Module from '../core/Module.js';
|
|
|
|
/**
|
|
* LetterDiscovery - Interactive letter and word discovery game
|
|
* Three phases: letter discovery, word exploration, and practice challenges
|
|
*/
|
|
class LetterDiscovery extends Module {
|
|
constructor(name, dependencies, config = {}) {
|
|
super(name, ['eventBus']);
|
|
|
|
// Validate dependencies
|
|
if (!dependencies.eventBus || !dependencies.content) {
|
|
throw new Error('LetterDiscovery requires eventBus and content dependencies');
|
|
}
|
|
|
|
this._eventBus = dependencies.eventBus;
|
|
this._content = dependencies.content;
|
|
this._config = {
|
|
container: null,
|
|
maxPracticeRounds: 8,
|
|
autoPlayTTS: true,
|
|
ttsSpeed: 0.8,
|
|
...config
|
|
};
|
|
|
|
// 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 data
|
|
this._letters = [];
|
|
this._letterWords = {}; // Map letter -> words starting with that letter
|
|
|
|
// Practice system
|
|
this._practiceLevel = 1;
|
|
this._practiceRound = 0;
|
|
this._practiceCorrectAnswers = 0;
|
|
this._practiceErrors = 0;
|
|
this._currentPracticeItems = [];
|
|
this._currentCorrectAnswer = null;
|
|
|
|
Object.seal(this);
|
|
}
|
|
|
|
/**
|
|
* Get game metadata
|
|
* @returns {Object} Game metadata
|
|
*/
|
|
static getMetadata() {
|
|
return {
|
|
name: 'Letter Discovery',
|
|
description: 'Discover letters and explore words that start with each letter',
|
|
difficulty: 'beginner',
|
|
category: 'letters',
|
|
estimatedTime: 10, // minutes
|
|
skills: ['alphabet', 'vocabulary', 'pronunciation']
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Calculate compatibility score with content
|
|
* @param {Object} content - Content to check compatibility with
|
|
* @returns {Object} Compatibility score and details
|
|
*/
|
|
static getCompatibilityScore(content) {
|
|
const letters = content?.letters || content?.rawContent?.letters;
|
|
|
|
// Try to create letters from vocabulary if direct letters not found
|
|
let lettersData = letters;
|
|
if (!lettersData && content?.vocabulary) {
|
|
lettersData = this._createLettersFromVocabulary(content.vocabulary);
|
|
}
|
|
|
|
if (!lettersData || Object.keys(lettersData).length === 0) {
|
|
return {
|
|
score: 0,
|
|
reason: 'No letter structure found',
|
|
requirements: ['letters'],
|
|
details: 'Letter Discovery requires content with predefined letters system'
|
|
};
|
|
}
|
|
|
|
const letterCount = letters ? Object.keys(letters).length : 0;
|
|
const totalWords = letters ? Object.values(letters).reduce((sum, words) => sum + (words?.length || 0), 0) : 0;
|
|
|
|
if (totalWords === 0) {
|
|
return {
|
|
score: 0.2,
|
|
reason: 'Letters found but no words',
|
|
requirements: ['letters with words'],
|
|
details: `Found ${letterCount} letters but no associated words`
|
|
};
|
|
}
|
|
|
|
// Perfect score at 26 letters, good score for 10+ letters
|
|
const score = Math.min(letterCount / 26, 1);
|
|
|
|
return {
|
|
score,
|
|
reason: `${letterCount} letters with ${totalWords} total words`,
|
|
requirements: ['letters'],
|
|
optimalLetters: 26,
|
|
details: `Can create discovery experience with ${letterCount} letters and ${totalWords} words`
|
|
};
|
|
}
|
|
|
|
async init() {
|
|
this._validateNotDestroyed();
|
|
|
|
try {
|
|
// Validate container
|
|
if (!this._config.container) {
|
|
throw new Error('Game container is required');
|
|
}
|
|
|
|
// Extract and validate content
|
|
this._extractContent();
|
|
|
|
if (this._letters.length === 0) {
|
|
throw new Error('No letter content found for discovery');
|
|
}
|
|
|
|
// 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);
|
|
|
|
// Initialize game interface
|
|
this._injectCSS();
|
|
this._createGameInterface();
|
|
this._setupEventListeners();
|
|
|
|
// Start with first letter
|
|
this._showLetterCard();
|
|
|
|
// Emit game ready event
|
|
this._eventBus.emit('game:ready', {
|
|
gameId: 'letter-discovery',
|
|
instanceId: this.name,
|
|
letters: this._letters.length,
|
|
totalWords: Object.values(this._letterWords).reduce((sum, words) => sum + words.length, 0)
|
|
}, this.name);
|
|
|
|
this._setInitialized();
|
|
|
|
} catch (error) {
|
|
this._showError(error.message);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async destroy() {
|
|
this._validateNotDestroyed();
|
|
|
|
// Clean up container
|
|
if (this._config.container) {
|
|
this._config.container.innerHTML = '';
|
|
}
|
|
|
|
// Remove injected CSS
|
|
this._removeInjectedCSS();
|
|
|
|
// Emit game end event
|
|
this._eventBus.emit('game:ended', {
|
|
gameId: 'letter-discovery',
|
|
instanceId: this.name,
|
|
score: this._score,
|
|
lettersDiscovered: this._discoveredLetters.length,
|
|
wordsLearned: this._discoveredWords.length
|
|
}, this.name);
|
|
|
|
this._setDestroyed();
|
|
}
|
|
|
|
/**
|
|
* Get current game state
|
|
* @returns {Object} Current game state
|
|
*/
|
|
getGameState() {
|
|
this._validateInitialized();
|
|
|
|
return {
|
|
phase: this._currentPhase,
|
|
score: this._score,
|
|
lives: this._lives,
|
|
currentLetter: this._currentLetter,
|
|
lettersDiscovered: this._discoveredLetters.length,
|
|
totalLetters: this._letters.length,
|
|
wordsLearned: this._discoveredWords.length,
|
|
practiceAccuracy: this._config.maxPracticeRounds > 0 ?
|
|
(this._practiceCorrectAnswers / this._config.maxPracticeRounds) * 100 : 0
|
|
};
|
|
}
|
|
|
|
// Private methods
|
|
_extractContent() {
|
|
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;
|
|
} else {
|
|
this._letters = [];
|
|
this._letterWords = {};
|
|
}
|
|
}
|
|
|
|
_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, #e2e8f0 0%, #cbd5e0 100%);
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
position: relative;
|
|
overflow-y: auto;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.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: 60vh;
|
|
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: #2d3748;
|
|
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;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
/* 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: #2d3748;
|
|
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: #2d3748;
|
|
background: rgba(66, 153, 225, 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 #4299e1;
|
|
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:not(.correct):not(.incorrect) {
|
|
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;
|
|
gap: 10px;
|
|
}
|
|
|
|
.stat-item {
|
|
text-align: center;
|
|
padding: 10px;
|
|
background: rgba(255,255,255,0.1);
|
|
border-radius: 10px;
|
|
backdrop-filter: blur(5px);
|
|
flex: 1;
|
|
}
|
|
|
|
/* Control Buttons */
|
|
.discovery-btn {
|
|
background: linear-gradient(45deg, #4299e1, #3182ce);
|
|
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 5px;
|
|
}
|
|
|
|
.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: #2d3748;
|
|
transition: all 0.3s ease;
|
|
padding: 10px;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.audio-btn:hover {
|
|
transform: scale(1.2);
|
|
color: #3182ce;
|
|
background: rgba(255,255,255,0.1);
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
|
|
.exit-btn {
|
|
background: rgba(255,255,255,0.2);
|
|
color: white;
|
|
border: 1px solid rgba(255,255,255,0.3);
|
|
padding: 10px 20px;
|
|
border-radius: 15px;
|
|
font-size: 1em;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.exit-btn:hover {
|
|
background: rgba(255,255,255,0.3);
|
|
}
|
|
|
|
/* Responsive Design */
|
|
@media (max-width: 768px) {
|
|
.letter-discovery-wrapper {
|
|
padding: 15px;
|
|
}
|
|
|
|
.letter-display {
|
|
font-size: 5em;
|
|
}
|
|
|
|
.word-text {
|
|
font-size: 2em;
|
|
}
|
|
|
|
.challenge-text {
|
|
font-size: 1.4em;
|
|
}
|
|
|
|
.practice-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.letter-controls {
|
|
flex-direction: column;
|
|
align-items: center;
|
|
}
|
|
|
|
.discovery-btn {
|
|
margin: 5px 0;
|
|
width: 100%;
|
|
max-width: 250px;
|
|
}
|
|
|
|
.practice-stats {
|
|
flex-direction: column;
|
|
}
|
|
}
|
|
`;
|
|
document.head.appendChild(styleSheet);
|
|
}
|
|
|
|
_removeInjectedCSS() {
|
|
const styleSheet = document.getElementById('letter-discovery-styles');
|
|
if (styleSheet) {
|
|
styleSheet.remove();
|
|
}
|
|
}
|
|
|
|
_createGameInterface() {
|
|
this._config.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>
|
|
<button class="exit-btn" id="exit-btn">← Exit</button>
|
|
</div>
|
|
</div>
|
|
<div class="letter-discovery-main">
|
|
<div class="game-content" id="game-content">
|
|
<!-- Dynamic content here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
_setupEventListeners() {
|
|
// Exit button
|
|
document.getElementById('exit-btn').addEventListener('click', () => {
|
|
this._eventBus.emit('game:exit-request', { instanceId: this.name }, this.name);
|
|
});
|
|
}
|
|
|
|
_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') {
|
|
const words = this._letterWords[this._currentLetter] || [];
|
|
if (progressDisplay) progressDisplay.textContent = `${this._currentWordIndex}/${words.length}`;
|
|
if (phaseIndicator) phaseIndicator.textContent = `Exploring Letter "${this._currentLetter}"`;
|
|
} else if (this._currentPhase === 'practice') {
|
|
if (progressDisplay) progressDisplay.textContent = `Round ${this._practiceRound + 1}/${this._config.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" id="discover-letter-btn">
|
|
🔍 Discover Letter
|
|
</button>
|
|
<button class="audio-btn" id="play-letter-btn" title="Play sound">
|
|
🔊
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Set up button listeners
|
|
document.getElementById('discover-letter-btn').addEventListener('click', () => {
|
|
this._discoverLetter();
|
|
});
|
|
|
|
document.getElementById('play-letter-btn').addEventListener('click', () => {
|
|
this._playLetterSound(letter);
|
|
});
|
|
|
|
this._updateHUD();
|
|
|
|
// Auto-play letter sound if enabled
|
|
if (this._config.autoPlayTTS) {
|
|
setTimeout(() => this._playLetterSound(letter), 500);
|
|
}
|
|
}
|
|
|
|
_getLetterPronunciation(letter) {
|
|
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) {
|
|
this._speakText(letter, { rate: this._config.ttsSpeed * 0.8 }); // Slower for letters
|
|
}
|
|
|
|
_discoverLetter() {
|
|
const letter = this._letters[this._currentLetterIndex];
|
|
this._discoveredLetters.push(letter);
|
|
this._score += 10;
|
|
|
|
// Emit score update event
|
|
this._eventBus.emit('game:score-update', {
|
|
gameId: 'letter-discovery',
|
|
instanceId: this.name,
|
|
score: this._score
|
|
}, this.name);
|
|
|
|
// Start word exploration for this letter
|
|
this._currentLetter = letter;
|
|
this._currentPhase = 'word-exploration';
|
|
this._currentWordIndex = 0;
|
|
|
|
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._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" id="next-word-btn">
|
|
➡️ Next Word
|
|
</button>
|
|
<button class="audio-btn" id="play-word-btn" title="Play sound">
|
|
🔊
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Set up button listeners
|
|
document.getElementById('next-word-btn').addEventListener('click', () => {
|
|
this._nextWord();
|
|
});
|
|
|
|
document.getElementById('play-word-btn').addEventListener('click', () => {
|
|
this._playWordSound(word.word);
|
|
});
|
|
|
|
// Add word to discovered list
|
|
this._discoveredWords.push(word);
|
|
|
|
this._updateHUD();
|
|
|
|
// Auto-play word sound if enabled
|
|
if (this._config.autoPlayTTS) {
|
|
setTimeout(() => this._playWordSound(word.word), 500);
|
|
}
|
|
}
|
|
|
|
_playWordSound(word) {
|
|
this._speakText(word, { rate: this._config.ttsSpeed });
|
|
}
|
|
|
|
_nextWord() {
|
|
this._currentWordIndex++;
|
|
this._score += 5;
|
|
|
|
// Emit score update event
|
|
this._eventBus.emit('game:score-update', {
|
|
gameId: 'letter-discovery',
|
|
instanceId: this.name,
|
|
score: this._score
|
|
}, this.name);
|
|
|
|
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" id="start-practice-btn">
|
|
🎮 Start Practice
|
|
</button>
|
|
<button class="discovery-btn" id="restart-btn">
|
|
🔄 Play Again
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Set up button listeners
|
|
document.getElementById('start-practice-btn').addEventListener('click', () => {
|
|
this._startPractice();
|
|
});
|
|
|
|
document.getElementById('restart-btn').addEventListener('click', () => {
|
|
this._restart();
|
|
});
|
|
|
|
this._updateHUD();
|
|
}
|
|
|
|
_startPractice() {
|
|
this._currentPhase = 'practice';
|
|
this._practiceLevel = 1;
|
|
this._practiceRound = 0;
|
|
this._practiceCorrectAnswers = 0;
|
|
this._practiceErrors = 0;
|
|
|
|
// Create shuffled practice items from all discovered words
|
|
this._currentPracticeItems = this._shuffleArray([...this._discoveredWords]);
|
|
|
|
this._showPracticeChallenge();
|
|
}
|
|
|
|
_showPracticeChallenge() {
|
|
if (this._practiceRound >= this._config.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" data-option-index="${index}" data-word="${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._config.maxPracticeRounds}</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Set up option listeners
|
|
document.querySelectorAll('.practice-option').forEach(button => {
|
|
button.addEventListener('click', (e) => {
|
|
const selectedIndex = parseInt(e.target.dataset.optionIndex);
|
|
const selectedWord = e.target.dataset.word;
|
|
this._selectPracticeAnswer(selectedIndex, selectedWord);
|
|
});
|
|
});
|
|
|
|
// Store correct answer for checking
|
|
this._currentCorrectAnswer = currentItem.word;
|
|
|
|
this._updateHUD();
|
|
|
|
// Auto-play word if enabled
|
|
if (this._config.autoPlayTTS) {
|
|
setTimeout(() => this._playWordSound(currentItem.word), 500);
|
|
}
|
|
}
|
|
|
|
_selectPracticeAnswer(selectedIndex, selectedWord) {
|
|
const buttons = document.querySelectorAll('.practice-option');
|
|
const isCorrect = selectedWord === this._currentCorrectAnswer;
|
|
|
|
// Disable all buttons to prevent multiple clicks
|
|
buttons.forEach(btn => btn.disabled = true);
|
|
|
|
if (isCorrect) {
|
|
buttons[selectedIndex].classList.add('correct');
|
|
this._practiceCorrectAnswers++;
|
|
this._score += 10;
|
|
|
|
// Emit score update event
|
|
this._eventBus.emit('game:score-update', {
|
|
gameId: 'letter-discovery',
|
|
instanceId: this.name,
|
|
score: this._score
|
|
}, this.name);
|
|
} else {
|
|
buttons[selectedIndex].classList.add('incorrect');
|
|
this._practiceErrors++;
|
|
|
|
// Show correct answer
|
|
buttons.forEach((btn) => {
|
|
if (btn.dataset.word === this._currentCorrectAnswer) {
|
|
btn.classList.add('correct');
|
|
}
|
|
});
|
|
}
|
|
|
|
setTimeout(() => {
|
|
this._practiceRound++;
|
|
this._showPracticeChallenge();
|
|
}, 1500);
|
|
}
|
|
|
|
_endPractice() {
|
|
const accuracy = Math.round((this._practiceCorrectAnswers / this._config.maxPracticeRounds) * 100);
|
|
|
|
// Store best score
|
|
const gameKey = 'letter-discovery';
|
|
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: 'Letter Discovery',
|
|
currentScore,
|
|
bestScore: isNewBest ? currentScore : bestScore,
|
|
isNewBest,
|
|
stats: {
|
|
'Letters Found': this._discoveredLetters.length,
|
|
'Words Learned': this._discoveredWords.length,
|
|
'Practice Accuracy': `${accuracy}%`,
|
|
'Correct Answers': `${this._practiceCorrectAnswers}/${this._config.maxPracticeRounds}`
|
|
}
|
|
});
|
|
|
|
// Emit game completion event
|
|
this._eventBus.emit('game:completed', {
|
|
gameId: 'letter-discovery',
|
|
instanceId: this.name,
|
|
score: this._score,
|
|
lettersDiscovered: this._discoveredLetters.length,
|
|
wordsLearned: this._discoveredWords.length,
|
|
practiceAccuracy: accuracy
|
|
}, this.name);
|
|
|
|
this._updateHUD();
|
|
}
|
|
|
|
_restart() {
|
|
// Reset all game state
|
|
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._currentCorrectAnswer = null;
|
|
|
|
this._showLetterCard();
|
|
}
|
|
|
|
_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;
|
|
}
|
|
|
|
_speakText(text, options = {}) {
|
|
if (!text) return;
|
|
|
|
try {
|
|
if ('speechSynthesis' in window) {
|
|
speechSynthesis.cancel();
|
|
|
|
const utterance = new SpeechSynthesisUtterance(text);
|
|
utterance.lang = this._getContentLanguage();
|
|
utterance.rate = options.rate || this._config.ttsSpeed;
|
|
utterance.volume = 1.0;
|
|
|
|
speechSynthesis.speak(utterance);
|
|
}
|
|
} catch (error) {
|
|
console.warn('TTS error:', error);
|
|
}
|
|
}
|
|
|
|
_getContentLanguage() {
|
|
if (this._content.language) {
|
|
const langMap = {
|
|
'chinese': 'zh-CN',
|
|
'english': 'en-US',
|
|
'french': 'fr-FR',
|
|
'spanish': 'es-ES'
|
|
};
|
|
return langMap[this._content.language] || this._content.language;
|
|
}
|
|
return 'en-US';
|
|
}
|
|
|
|
_showError(message) {
|
|
if (this._config.container) {
|
|
this._config.container.innerHTML = `
|
|
<div class="letter-discovery-wrapper">
|
|
<div class="completion-message" style="margin-top: 50px;">
|
|
<div style="font-size: 3em; margin-bottom: 20px;">❌</div>
|
|
<h3>Letter Discovery Error</h3>
|
|
<p>${message}</p>
|
|
<div class="letter-controls">
|
|
<button class="discovery-btn" onclick="history.back()">Go Back</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
_handlePause() {
|
|
this._eventBus.emit('game:paused', { instanceId: this.name }, this.name);
|
|
}
|
|
|
|
_handleResume() {
|
|
this._eventBus.emit('game:resumed', { instanceId: this.name }, this.name);
|
|
}
|
|
|
|
_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._restart();
|
|
});
|
|
|
|
popup.querySelector('#different-game-btn').addEventListener('click', () => {
|
|
popup.remove();
|
|
if (window.app && window.app.getCore().router) {
|
|
window.app.getCore().router.navigate('/games');
|
|
// Force content reload by re-emitting navigation event
|
|
setTimeout(() => {
|
|
const chapterId = window.currentChapterId || 'sbs';
|
|
this._eventBus.emit('navigation:games', {
|
|
path: `/games/${chapterId}`,
|
|
data: { path: `/games/${chapterId}` }
|
|
}, 'Application');
|
|
}, 100);
|
|
} 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');
|
|
// Force content reload by re-emitting navigation event
|
|
setTimeout(() => {
|
|
const chapterId = window.currentChapterId || 'sbs';
|
|
this._eventBus.emit('navigation:games', {
|
|
path: `/games/${chapterId}`,
|
|
data: { path: `/games/${chapterId}` }
|
|
}, 'Application');
|
|
}, 100);
|
|
} else {
|
|
window.location.href = '/#/games';
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Helper method to convert vocabulary to letters format
|
|
static _createLettersFromVocabulary(vocabulary) {
|
|
const letters = {};
|
|
|
|
Object.entries(vocabulary).forEach(([word, data]) => {
|
|
const firstLetter = word.charAt(0).toUpperCase();
|
|
if (!letters[firstLetter]) {
|
|
letters[firstLetter] = [];
|
|
}
|
|
|
|
letters[firstLetter].push({
|
|
word: word,
|
|
translation: data.user_language || data.translation || data,
|
|
type: data.type || 'word',
|
|
pronunciation: data.pronunciation
|
|
});
|
|
});
|
|
|
|
return letters;
|
|
}
|
|
}
|
|
|
|
export default LetterDiscovery; |