// === MODULE ADVENTURE READER (ZELDA-STYLE) === class AdventureReaderGame { constructor(options) { this.container = options.container; this.content = options.content; this.onScoreUpdate = options.onScoreUpdate || (() => {}); this.onGameEnd = options.onGameEnd || (() => {}); // Game state this.score = 0; this.currentSentenceIndex = 0; this.currentVocabIndex = 0; this.potsDestroyed = 0; this.enemiesDefeated = 0; this.isGamePaused = false; // Game objects this.pots = []; this.enemies = []; this.player = { x: 0, y: 0 }; // Will be set when map is created this.isPlayerMoving = false; this.isPlayerInvulnerable = false; this.invulnerabilityTimeout = null; // TTS settings this.autoPlayTTS = true; this.ttsEnabled = true; // Expose content globally for SettingsManager TTS language detection window.currentGameContent = this.content; // Content extraction this.vocabulary = this.extractVocabulary(this.content); this.sentences = this.extractSentences(this.content); this.stories = this.extractStories(this.content); this.dialogues = this.extractDialogues(this.content); this.init(); } init() { const hasVocabulary = this.vocabulary && this.vocabulary.length > 0; const hasSentences = this.sentences && this.sentences.length > 0; const hasStories = this.stories && this.stories.length > 0; const hasDialogues = this.dialogues && this.dialogues.length > 0; if (!hasVocabulary && !hasSentences && !hasStories && !hasDialogues) { logSh('No compatible content found for Adventure Reader', 'ERROR'); this.showInitError(); return; } logSh(`Adventure Reader initialized with: ${this.vocabulary.length} vocab, ${this.sentences.length} sentences, ${this.stories.length} stories, ${this.dialogues.length} dialogues`, 'INFO'); this.createGameInterface(); this.initializePlayer(); this.setupEventListeners(); this.updateContentInfo(); this.generateGameObjects(); this.generateDecorations(); this.startGameLoop(); } showInitError() { this.container.innerHTML = `

❌ No Adventure Content Found

This content module needs adventure-compatible content:

Add adventure content to enable this game mode.

`; } extractVocabulary(content) { let vocabulary = []; // Support pour Dragon's Pearl vocabulary structure if (content.vocabulary && typeof content.vocabulary === 'object') { vocabulary = Object.entries(content.vocabulary).map(([original_language, vocabData]) => { if (typeof vocabData === 'string') { // Simple format: "word": "translation" return { original_language: original_language, user_language: vocabData, type: 'unknown' }; } else if (typeof vocabData === 'object') { // Rich format: "word": { user_language: "translation", type: "noun", ... } return { original_language: original_language, user_language: vocabData.user_language || vocabData.translation || 'No translation', type: vocabData.type || 'unknown', pronunciation: vocabData.pronunciation, difficulty: vocabData.difficulty }; } return null; }).filter(item => item !== null); } // Ultra-modular format support else if (content.rawContent && content.rawContent.vocabulary) { if (typeof content.rawContent.vocabulary === 'object' && !Array.isArray(content.rawContent.vocabulary)) { vocabulary = Object.entries(content.rawContent.vocabulary).map(([original_language, vocabData]) => { if (typeof vocabData === 'string') { // Simple format: "word": "translation" return { original_language: original_language, user_language: vocabData, type: 'unknown' }; } else if (typeof vocabData === 'object') { // Rich format: "word": { user_language: "translation", type: "noun", ... } return { original_language: original_language, user_language: vocabData.user_language || vocabData.translation || 'No translation', type: vocabData.type || 'unknown', pronunciation: vocabData.pronunciation, difficulty: vocabData.difficulty }; } return null; }).filter(item => item !== null); } } return vocabulary.filter(item => item && item.original_language && item.user_language); } extractSentences(content) { let sentences = []; logSh('πŸ‰ Adventure Reader: Extracting sentences from content...', 'DEBUG'); logSh(`πŸ‰ Content structure: story=${!!content.story}, rawContent=${!!content.rawContent}`, 'DEBUG'); // Support pour Dragon's Pearl structure: content.story.chapters[].sentences[] if (content.story && content.story.chapters && Array.isArray(content.story.chapters)) { logSh(`πŸ‰ Dragon's Pearl structure detected, ${content.story.chapters.length} chapters`, 'DEBUG'); content.story.chapters.forEach((chapter, chapterIndex) => { logSh(`πŸ‰ Processing chapter ${chapterIndex}: ${chapter.title}`, 'DEBUG'); if (chapter.sentences && Array.isArray(chapter.sentences)) { logSh(`πŸ‰ Chapter ${chapterIndex} has ${chapter.sentences.length} sentences`, 'DEBUG'); chapter.sentences.forEach((sentence, sentenceIndex) => { if (sentence.original && sentence.translation) { // Construire la prononciation depuis les mots si pas disponible directement let pronunciation = sentence.pronunciation || ''; if (!pronunciation && sentence.words && Array.isArray(sentence.words)) { pronunciation = sentence.words .map(wordObj => wordObj.pronunciation || '') .filter(p => p.trim().length > 0) .join(' '); } sentences.push({ original_language: sentence.original, user_language: sentence.translation, pronunciation: pronunciation, chapter: chapter.title || '', id: sentence.id || sentences.length }); } else { logSh(`πŸ‰ WARNING: Skipping sentence ${sentenceIndex} in chapter ${chapterIndex} - missing original/translation`, 'WARN'); } }); } else { logSh(`πŸ‰ WARNING: Chapter ${chapterIndex} has no sentences array`, 'WARN'); } }); logSh(`πŸ‰ Dragon's Pearl extraction complete: ${sentences.length} sentences extracted`, 'INFO'); } // Support pour la structure ultra-modulaire existante else if (content.rawContent) { // Ultra-modular format: Extract from texts (stories/adventures) if (content.rawContent.texts && Array.isArray(content.rawContent.texts)) { content.rawContent.texts.forEach(text => { if (text.original_language && text.user_language) { // Split long texts into sentences for adventure reading const originalSentences = text.original_language.split(/[.!?]+/).filter(s => s.trim().length > 10); const userSentences = text.user_language.split(/[.!?]+/).filter(s => s.trim().length > 10); // Match sentences by index originalSentences.forEach((originalSentence, index) => { const userSentence = userSentences[index] || originalSentence; sentences.push({ original_language: originalSentence.trim() + '.', user_language: userSentence.trim() + '.', title: text.title || 'Adventure Text', id: text.id || `text_${index}` }); }); } }); } // Ultra-modular format: Extract from dialogues if (content.rawContent.dialogues && Array.isArray(content.rawContent.dialogues)) { content.rawContent.dialogues.forEach(dialogue => { if (dialogue.conversation && Array.isArray(dialogue.conversation)) { dialogue.conversation.forEach(line => { if (line.original_language && line.user_language) { sentences.push({ original_language: line.original_language, user_language: line.user_language, speaker: line.speaker || 'Character', title: dialogue.title || 'Dialogue', id: line.id || dialogue.id }); } }); } }); } // Legacy format support for backward compatibility if (content.rawContent.sentences && Array.isArray(content.rawContent.sentences)) { content.rawContent.sentences.forEach(sentence => { sentences.push({ original_language: sentence.english || sentence.original_language || '', user_language: sentence.chinese || sentence.french || sentence.user_language || sentence.translation || '', pronunciation: sentence.prononciation || sentence.pronunciation }); }); } } return sentences.filter(item => item && item.original_language && item.user_language); } extractStories(content) { let stories = []; // Support pour Dragon's Pearl structure if (content.story && content.story.chapters && Array.isArray(content.story.chapters)) { // CrΓ©er une histoire depuis les chapitres de Dragon's Pearl stories.push({ title: content.story.title || content.name || "Dragon's Pearl", original_language: content.story.chapters.map(ch => ch.sentences.map(s => s.original).join(' ') ).join('\n\n'), user_language: content.story.chapters.map(ch => ch.sentences.map(s => s.translation).join(' ') ).join('\n\n'), chapters: content.story.chapters.map(chapter => ({ title: chapter.title, sentences: chapter.sentences })) }); } // Support pour la structure ultra-modulaire existante else if (content.rawContent && content.rawContent.texts && Array.isArray(content.rawContent.texts)) { stories = content.rawContent.texts.filter(text => text.original_language && text.user_language && text.title ).map(text => ({ id: text.id || `story_${Date.now()}_${Math.random()}`, title: text.title, original_language: text.original_language, user_language: text.user_language, description: text.description || '', difficulty: text.difficulty || 'medium' })); } return stories; } extractDialogues(content) { let dialogues = []; if (content.rawContent && content.rawContent.dialogues && Array.isArray(content.rawContent.dialogues)) { dialogues = content.rawContent.dialogues.filter(dialogue => dialogue.conversation && Array.isArray(dialogue.conversation) && dialogue.conversation.length > 0 ).map(dialogue => ({ id: dialogue.id || `dialogue_${Date.now()}_${Math.random()}`, title: dialogue.title || 'Character Dialogue', conversation: dialogue.conversation.filter(line => line.original_language && line.user_language ).map(line => ({ id: line.id || `line_${Date.now()}_${Math.random()}`, speaker: line.speaker || 'Character', original_language: line.original_language, user_language: line.user_language, emotion: line.emotion || 'neutral' })) })); } return dialogues.filter(dialogue => dialogue.conversation.length > 0); } updateContentInfo() { const contentInfoEl = document.getElementById('content-info'); if (!contentInfoEl) return; const contentTypes = []; if (this.stories && this.stories.length > 0) { contentTypes.push(`πŸ“š ${this.stories.length} stories`); } if (this.dialogues && this.dialogues.length > 0) { contentTypes.push(`πŸ’¬ ${this.dialogues.length} dialogues`); } if (this.vocabulary && this.vocabulary.length > 0) { contentTypes.push(`πŸ“ ${this.vocabulary.length} words`); } if (this.sentences && this.sentences.length > 0) { contentTypes.push(`πŸ“– ${this.sentences.length} sentences`); } if (contentTypes.length > 0) { contentInfoEl.innerHTML = `
Adventure Content: ${contentTypes.join(' β€’ ')}
`; } } createGameInterface() { this.container.innerHTML = `
πŸ† 0
🏺 0
βš”οΈ 0
Start your adventure!
πŸ§™β€β™‚οΈ
Click 🏺 pots for vocabulary β€’ Click πŸ‘Ή enemies for sentences
`; } initializePlayer() { // Set player initial position to center of map const gameMap = document.getElementById('game-map'); const mapRect = gameMap.getBoundingClientRect(); this.player.x = mapRect.width / 2 - 20; // -20 for half player width this.player.y = mapRect.height / 2 - 20; // -20 for half player height const playerElement = document.getElementById('player'); playerElement.style.left = this.player.x + 'px'; playerElement.style.top = this.player.y + 'px'; } setupEventListeners() { document.getElementById('restart-btn').addEventListener('click', () => this.restart()); document.getElementById('continue-btn').addEventListener('click', () => this.closeModal()); // Map click handler const gameMap = document.getElementById('game-map'); gameMap.addEventListener('click', (e) => this.handleMapClick(e)); // Window resize handler window.addEventListener('resize', () => { setTimeout(() => this.initializePlayer(), 100); }); } generateGameObjects() { const gameMap = document.getElementById('game-map'); // Clear existing objects gameMap.querySelectorAll('.pot, .enemy').forEach(el => el.remove()); this.pots = []; this.enemies = []; // Generate pots (for vocabulary) const numPots = Math.min(8, this.vocabulary.length); for (let i = 0; i < numPots; i++) { const pot = this.createPot(); this.pots.push(pot); gameMap.appendChild(pot.element); } // Generate enemies (for sentences) - spawn across entire viewport const numEnemies = Math.min(8, this.sentences.length); for (let i = 0; i < numEnemies; i++) { const enemy = this.createEnemy(); this.enemies.push(enemy); gameMap.appendChild(enemy.element); } this.updateHUD(); } createPot() { const pot = document.createElement('div'); pot.className = 'pot'; pot.innerHTML = '🏺'; const position = this.getRandomPosition(); pot.style.left = position.x + 'px'; pot.style.top = position.y + 'px'; return { element: pot, x: position.x, y: position.y, destroyed: false }; } createEnemy() { const enemy = document.createElement('div'); enemy.className = 'enemy'; enemy.innerHTML = 'πŸ‘Ή'; const position = this.getRandomPosition(true); // Force away from player enemy.style.left = position.x + 'px'; enemy.style.top = position.y + 'px'; // Random movement pattern for each enemy const patterns = ['patrol', 'chase', 'wander', 'circle']; const pattern = patterns[Math.floor(Math.random() * patterns.length)]; return { element: enemy, x: position.x, y: position.y, defeated: false, moveDirection: Math.random() * Math.PI * 2, speed: 0.6 + Math.random() * 0.6, // Reduced speed pattern: pattern, patrolStartX: position.x, patrolStartY: position.y, patrolDistance: 80 + Math.random() * 60, circleCenter: { x: position.x, y: position.y }, circleRadius: 60 + Math.random() * 40, circleAngle: Math.random() * Math.PI * 2, changeDirectionTimer: 0, dashCooldown: 0, isDashing: false }; } getRandomPosition(forceAwayFromPlayer = false) { const gameMap = document.getElementById('game-map'); const mapRect = gameMap.getBoundingClientRect(); const mapWidth = mapRect.width; const mapHeight = mapRect.height; const margin = 40; let x, y; let tooClose; const minDistance = forceAwayFromPlayer ? 150 : 80; do { x = margin + Math.random() * (mapWidth - margin * 2); y = margin + Math.random() * (mapHeight - margin * 2); // Check distance from player const distFromPlayer = Math.sqrt( Math.pow(x - this.player.x, 2) + Math.pow(y - this.player.y, 2) ); tooClose = distFromPlayer < minDistance; } while (tooClose); return { x, y }; } handleMapClick(e) { if (this.isGamePaused || this.isPlayerMoving) return; const rect = e.currentTarget.getBoundingClientRect(); const clickX = e.clientX - rect.left; const clickY = e.clientY - rect.top; // Check pot clicks let targetFound = false; this.pots.forEach(pot => { if (!pot.destroyed && this.isNearPosition(clickX, clickY, pot)) { this.movePlayerToTarget(pot, 'pot'); targetFound = true; } }); // Check enemy clicks (only if no pot was clicked) if (!targetFound) { this.enemies.forEach(enemy => { if (!enemy.defeated && this.isNearPosition(clickX, clickY, enemy)) { this.movePlayerToTarget(enemy, 'enemy'); targetFound = true; } }); } // If no target found, move to empty area if (!targetFound) { this.movePlayerToPosition(clickX, clickY); } } isNearPosition(clickX, clickY, object) { const distance = Math.sqrt( Math.pow(clickX - (object.x + 20), 2) + Math.pow(clickY - (object.y + 20), 2) ); return distance < 60; // Larger clickable area } movePlayerToTarget(target, type) { this.isPlayerMoving = true; const playerElement = document.getElementById('player'); // Grant invulnerability IMMEDIATELY when attacking an enemy if (type === 'enemy') { this.grantAttackInvulnerability(); } // Calculate target position (near the object) const targetX = target.x; const targetY = target.y; // Update player position this.player.x = targetX; this.player.y = targetY; // Animate player movement playerElement.style.left = targetX + 'px'; playerElement.style.top = targetY + 'px'; // Add walking animation playerElement.style.transform = 'scale(1.1)'; // Wait for movement animation to complete, then interact setTimeout(() => { playerElement.style.transform = 'scale(1)'; this.isPlayerMoving = false; if (type === 'pot') { this.destroyPot(target); } else if (type === 'enemy') { this.defeatEnemy(target); } }, 800); // Match CSS transition duration } movePlayerToPosition(targetX, targetY) { this.isPlayerMoving = true; const playerElement = document.getElementById('player'); // Update player position this.player.x = targetX - 20; // Center the player on click point this.player.y = targetY - 20; // Keep player within bounds const gameMap = document.getElementById('game-map'); const mapRect = gameMap.getBoundingClientRect(); const margin = 20; this.player.x = Math.max(margin, Math.min(mapRect.width - 60, this.player.x)); this.player.y = Math.max(margin, Math.min(mapRect.height - 60, this.player.y)); // Animate player movement playerElement.style.left = this.player.x + 'px'; playerElement.style.top = this.player.y + 'px'; // Add walking animation playerElement.style.transform = 'scale(1.1)'; // Reset animation after movement setTimeout(() => { playerElement.style.transform = 'scale(1)'; this.isPlayerMoving = false; }, 800); } destroyPot(pot) { pot.destroyed = true; pot.element.classList.add('destroyed'); // Animation pot.element.innerHTML = 'πŸ’₯'; setTimeout(() => { pot.element.style.opacity = '0.3'; pot.element.innerHTML = 'πŸ’¨'; }, 200); this.potsDestroyed++; this.score += 10; // Show vocabulary if (this.currentVocabIndex < this.vocabulary.length) { this.showVocabPopup(this.vocabulary[this.currentVocabIndex]); this.currentVocabIndex++; } this.updateHUD(); this.checkGameComplete(); } defeatEnemy(enemy) { enemy.defeated = true; enemy.element.classList.add('defeated'); // Animation enemy.element.innerHTML = '☠️'; setTimeout(() => { enemy.element.style.opacity = '0.3'; }, 300); this.enemiesDefeated++; this.score += 25; // Invulnerability is already granted at start of movement // Just refresh the timer to ensure full 2 seconds from now this.refreshAttackInvulnerability(); // Show sentence (pause game) if (this.currentSentenceIndex < this.sentences.length) { this.showReadingModal(this.sentences[this.currentSentenceIndex]); this.currentSentenceIndex++; } this.updateHUD(); } showVocabPopup(vocab) { const popup = document.getElementById('vocab-popup'); const wordEl = document.getElementById('vocab-word'); const translationEl = document.getElementById('vocab-translation'); const pronunciationEl = document.getElementById('vocab-pronunciation'); wordEl.textContent = vocab.original_language; translationEl.textContent = vocab.user_language; // Afficher la prononciation si disponible if (vocab.pronunciation) { pronunciationEl.textContent = `πŸ—£οΈ ${vocab.pronunciation}`; pronunciationEl.style.display = 'block'; } else { pronunciationEl.style.display = 'none'; } popup.style.display = 'block'; popup.classList.add('show'); // Auto-play TTS for vocabulary if (this.autoPlayTTS && this.ttsEnabled) { setTimeout(() => { this.speakText(vocab.original_language, { rate: 0.8 }); }, 400); // Small delay to let popup appear } setTimeout(() => { popup.classList.remove('show'); setTimeout(() => { popup.style.display = 'none'; }, 300); }, 2000); } showReadingModal(sentence) { this.isGamePaused = true; const modal = document.getElementById('reading-modal'); const content = document.getElementById('reading-content'); const modalTitle = document.getElementById('modal-title'); // Determine content type and set appropriate modal title let modalTitleText = 'Adventure Text'; if (sentence.speaker) { modalTitleText = `πŸ’¬ ${sentence.speaker} says...`; } else if (sentence.title) { modalTitleText = `πŸ“š ${sentence.title}`; } modalTitle.textContent = modalTitleText; // Create content with appropriate styling based on type const speakerInfo = sentence.speaker ? `
🎭 ${sentence.speaker}
` : ''; const titleInfo = sentence.title && !sentence.speaker ? `
πŸ“– ${sentence.title}
` : ''; const emotionInfo = sentence.emotion && sentence.emotion !== 'neutral' ? `
😊 ${sentence.emotion}
` : ''; content.innerHTML = `
${titleInfo} ${speakerInfo} ${emotionInfo}

${sentence.original_language}

${sentence.user_language}

${sentence.pronunciation ? `

πŸ—£οΈ ${sentence.pronunciation}

` : ''}
`; modal.style.display = 'flex'; modal.classList.add('show'); // Auto-play TTS for sentence if (this.autoPlayTTS && this.ttsEnabled) { setTimeout(() => { this.speakText(sentence.original_language, { rate: 0.8 }); }, 600); // Longer delay for modal animation } } closeModal() { const modal = document.getElementById('reading-modal'); modal.classList.remove('show'); setTimeout(() => { modal.style.display = 'none'; this.isGamePaused = false; }, 300); this.checkGameComplete(); } checkGameComplete() { const allPotsDestroyed = this.pots.every(pot => pot.destroyed); const allEnemiesDefeated = this.enemies.every(enemy => enemy.defeated); if (allPotsDestroyed && allEnemiesDefeated) { setTimeout(() => { this.gameComplete(); }, 1000); } } gameComplete() { // Bonus for completion this.score += 100; this.updateHUD(); document.getElementById('progress-text').textContent = 'πŸ† Adventure Complete!'; setTimeout(() => { this.onGameEnd(this.score); }, 2000); } updateHUD() { document.getElementById('score-display').textContent = this.score; document.getElementById('pots-counter').textContent = this.potsDestroyed; document.getElementById('enemies-counter').textContent = this.enemiesDefeated; const totalObjects = this.pots.length + this.enemies.length; const destroyedObjects = this.potsDestroyed + this.enemiesDefeated; document.getElementById('progress-text').textContent = `Progress: ${destroyedObjects}/${totalObjects} objects`; this.onScoreUpdate(this.score); } generateDecorations() { const gameMap = document.getElementById('game-map'); const mapRect = gameMap.getBoundingClientRect(); const mapWidth = mapRect.width; const mapHeight = mapRect.height; // Remove existing decorations gameMap.querySelectorAll('.decoration').forEach(el => el.remove()); // Generate trees (fewer, larger) const numTrees = 4 + Math.floor(Math.random() * 4); // 4-7 trees for (let i = 0; i < numTrees; i++) { const tree = document.createElement('div'); tree.className = 'decoration tree'; tree.innerHTML = Math.random() < 0.5 ? '🌳' : '🌲'; const position = this.getDecorationPosition(mapWidth, mapHeight, 60); // Keep away from objects tree.style.left = position.x + 'px'; tree.style.top = position.y + 'px'; tree.style.fontSize = (25 + Math.random() * 15) + 'px'; // Random size gameMap.appendChild(tree); } // Generate grass patches (many, small) const numGrass = 15 + Math.floor(Math.random() * 10); // 15-24 grass for (let i = 0; i < numGrass; i++) { const grass = document.createElement('div'); grass.className = 'decoration grass'; const grassTypes = ['🌿', '🌱', 'πŸ€', '🌾']; grass.innerHTML = grassTypes[Math.floor(Math.random() * grassTypes.length)]; const position = this.getDecorationPosition(mapWidth, mapHeight, 30); // Smaller keepaway grass.style.left = position.x + 'px'; grass.style.top = position.y + 'px'; grass.style.fontSize = (15 + Math.random() * 8) + 'px'; // Smaller size gameMap.appendChild(grass); } // Generate rocks (medium amount) const numRocks = 3 + Math.floor(Math.random() * 3); // 3-5 rocks for (let i = 0; i < numRocks; i++) { const rock = document.createElement('div'); rock.className = 'decoration rock'; rock.innerHTML = Math.random() < 0.5 ? 'πŸͺ¨' : '⛰️'; const position = this.getDecorationPosition(mapWidth, mapHeight, 40); rock.style.left = position.x + 'px'; rock.style.top = position.y + 'px'; rock.style.fontSize = (20 + Math.random() * 10) + 'px'; gameMap.appendChild(rock); } } getDecorationPosition(mapWidth, mapHeight, keepAwayDistance) { const margin = 20; let x, y; let attempts = 0; let validPosition = false; do { x = margin + Math.random() * (mapWidth - margin * 2); y = margin + Math.random() * (mapHeight - margin * 2); // Check distance from player const distFromPlayer = Math.sqrt( Math.pow(x - this.player.x, 2) + Math.pow(y - this.player.y, 2) ); // Check distance from pots and enemies let tooClose = distFromPlayer < keepAwayDistance; if (!tooClose) { this.pots.forEach(pot => { const dist = Math.sqrt(Math.pow(x - pot.x, 2) + Math.pow(y - pot.y, 2)); if (dist < keepAwayDistance) tooClose = true; }); } if (!tooClose) { this.enemies.forEach(enemy => { const dist = Math.sqrt(Math.pow(x - enemy.x, 2) + Math.pow(y - enemy.y, 2)); if (dist < keepAwayDistance) tooClose = true; }); } validPosition = !tooClose; attempts++; } while (!validPosition && attempts < 50); return { x, y }; } startGameLoop() { const animate = () => { if (!this.isGamePaused) { this.moveEnemies(); } requestAnimationFrame(animate); }; animate(); } moveEnemies() { const gameMap = document.getElementById('game-map'); const mapRect = gameMap.getBoundingClientRect(); const mapWidth = mapRect.width; const mapHeight = mapRect.height; this.enemies.forEach(enemy => { if (enemy.defeated) return; // Apply movement pattern this.applyMovementPattern(enemy, mapWidth, mapHeight); // Bounce off walls (using dynamic map size) if (enemy.x < 10 || enemy.x > mapWidth - 50) { enemy.moveDirection = Math.PI - enemy.moveDirection; enemy.x = Math.max(10, Math.min(mapWidth - 50, enemy.x)); } if (enemy.y < 10 || enemy.y > mapHeight - 50) { enemy.moveDirection = -enemy.moveDirection; enemy.y = Math.max(10, Math.min(mapHeight - 50, enemy.y)); } enemy.element.style.left = enemy.x + 'px'; enemy.element.style.top = enemy.y + 'px'; // Check collision with player this.checkPlayerEnemyCollision(enemy); }); } applyMovementPattern(enemy, mapWidth, mapHeight) { enemy.changeDirectionTimer++; switch (enemy.pattern) { case 'patrol': // Patrol back and forth const distanceFromStart = Math.sqrt( Math.pow(enemy.x - enemy.patrolStartX, 2) + Math.pow(enemy.y - enemy.patrolStartY, 2) ); if (distanceFromStart > enemy.patrolDistance) { // Turn around and head back to start const angleToStart = Math.atan2( enemy.patrolStartY - enemy.y, enemy.patrolStartX - enemy.x ); enemy.moveDirection = angleToStart; } if (enemy.changeDirectionTimer > 120) { // Change direction every ~2 seconds enemy.moveDirection += (Math.random() - 0.5) * Math.PI * 0.5; enemy.changeDirectionTimer = 0; } enemy.x += Math.cos(enemy.moveDirection) * enemy.speed; enemy.y += Math.sin(enemy.moveDirection) * enemy.speed; break; case 'chase': enemy.dashCooldown--; if (enemy.isDashing) { // Continue dash movement with very high speed enemy.x += Math.cos(enemy.moveDirection) * (enemy.speed * 6); enemy.y += Math.sin(enemy.moveDirection) * (enemy.speed * 6); enemy.dashCooldown--; if (enemy.dashCooldown <= 0) { enemy.isDashing = false; enemy.dashCooldown = 120 + Math.random() * 60; // Reset cooldown } } else { // Normal chase behavior const angleToPlayer = Math.atan2( this.player.y - enemy.y, this.player.x - enemy.x ); // Sometimes do a perpendicular dash if (enemy.dashCooldown <= 0 && Math.random() < 0.3) { enemy.isDashing = true; enemy.dashCooldown = 50; // Much longer dash duration // Perpendicular angle (90 degrees from player direction) const perpAngle = angleToPlayer + (Math.random() < 0.5 ? Math.PI/2 : -Math.PI/2); enemy.moveDirection = perpAngle; // Start dash with visual effect enemy.element.style.filter = 'drop-shadow(0 0 8px red)'; setTimeout(() => { enemy.element.style.filter = 'drop-shadow(1px 1px 2px rgba(0,0,0,0.3))'; }, 300); } else { // Mix chasing with some randomness enemy.moveDirection = angleToPlayer + (Math.random() - 0.5) * 0.3; enemy.x += Math.cos(enemy.moveDirection) * (enemy.speed * 0.8); enemy.y += Math.sin(enemy.moveDirection) * (enemy.speed * 0.8); } } break; case 'wander': // Random wandering if (enemy.changeDirectionTimer > 60 + Math.random() * 60) { enemy.moveDirection += (Math.random() - 0.5) * Math.PI; enemy.changeDirectionTimer = 0; } enemy.x += Math.cos(enemy.moveDirection) * enemy.speed; enemy.y += Math.sin(enemy.moveDirection) * enemy.speed; break; case 'circle': // Move in circular pattern enemy.circleAngle += 0.03 + (enemy.speed * 0.01); enemy.x = enemy.circleCenter.x + Math.cos(enemy.circleAngle) * enemy.circleRadius; enemy.y = enemy.circleCenter.y + Math.sin(enemy.circleAngle) * enemy.circleRadius; // Occasionally change circle center if (enemy.changeDirectionTimer > 180) { enemy.circleCenter.x += (Math.random() - 0.5) * 100; enemy.circleCenter.y += (Math.random() - 0.5) * 100; // Keep circle center within bounds enemy.circleCenter.x = Math.max(enemy.circleRadius + 20, Math.min(mapWidth - enemy.circleRadius - 20, enemy.circleCenter.x)); enemy.circleCenter.y = Math.max(enemy.circleRadius + 20, Math.min(mapHeight - enemy.circleRadius - 20, enemy.circleCenter.y)); enemy.changeDirectionTimer = 0; } break; } } checkPlayerEnemyCollision(enemy) { if (this.isPlayerInvulnerable || enemy.defeated) return; const distance = Math.sqrt( Math.pow(this.player.x - enemy.x, 2) + Math.pow(this.player.y - enemy.y, 2) ); // Collision detected if (distance < 35) { this.takeDamage(); } } takeDamage() { if (this.isPlayerInvulnerable) return; // Apply damage this.score = Math.max(0, this.score - 20); this.updateHUD(); // Clear any existing invulnerability timeout if (this.invulnerabilityTimeout) { clearTimeout(this.invulnerabilityTimeout); } // Start damage invulnerability this.isPlayerInvulnerable = true; const playerElement = document.getElementById('player'); // Visual feedback - blinking effect let blinkCount = 0; const blinkInterval = setInterval(() => { playerElement.style.opacity = playerElement.style.opacity === '0.3' ? '1' : '0.3'; blinkCount++; if (blinkCount >= 8) { // 4 blinks in 2 seconds clearInterval(blinkInterval); playerElement.style.opacity = '1'; playerElement.style.filter = 'drop-shadow(2px 2px 4px rgba(0,0,0,0.3))'; playerElement.style.transform = 'scale(1)'; this.isPlayerInvulnerable = false; } }, 250); // Show damage feedback this.showDamagePopup(); } grantAttackInvulnerability() { // Always grant invulnerability after attack, even if already invulnerable this.isPlayerInvulnerable = true; const playerElement = document.getElementById('player'); // Clear any existing timeout if (this.invulnerabilityTimeout) { clearTimeout(this.invulnerabilityTimeout); } // Different visual effect for attack invulnerability (golden glow) playerElement.style.filter = 'drop-shadow(0 0 15px gold) brightness(1.4)'; this.invulnerabilityTimeout = setTimeout(() => { playerElement.style.filter = 'drop-shadow(2px 2px 4px rgba(0,0,0,0.3))'; this.isPlayerInvulnerable = false; }, 2000); // Show invulnerability feedback this.showInvulnerabilityPopup(); } refreshAttackInvulnerability() { // Refresh the invulnerability timer without changing visual state if (this.invulnerabilityTimeout) { clearTimeout(this.invulnerabilityTimeout); } const playerElement = document.getElementById('player'); this.isPlayerInvulnerable = true; this.invulnerabilityTimeout = setTimeout(() => { playerElement.style.filter = 'drop-shadow(2px 2px 4px rgba(0,0,0,0.3))'; this.isPlayerInvulnerable = false; }, 2000); } showInvulnerabilityPopup() { const popup = document.createElement('div'); popup.className = 'invulnerability-popup'; popup.innerHTML = 'Protected!'; popup.style.position = 'fixed'; popup.style.left = '50%'; popup.style.top = '25%'; popup.style.transform = 'translate(-50%, -50%)'; popup.style.color = '#FFD700'; popup.style.fontSize = '1.5rem'; popup.style.fontWeight = 'bold'; popup.style.zIndex = '999'; popup.style.pointerEvents = 'none'; popup.style.animation = 'protectionFloat 2s ease-out forwards'; document.body.appendChild(popup); setTimeout(() => { popup.remove(); }, 2000); } showDamagePopup() { // Create damage popup const damagePopup = document.createElement('div'); damagePopup.className = 'damage-popup'; damagePopup.innerHTML = '-20'; damagePopup.style.position = 'fixed'; damagePopup.style.left = '50%'; damagePopup.style.top = '30%'; damagePopup.style.transform = 'translate(-50%, -50%)'; damagePopup.style.color = '#EF4444'; damagePopup.style.fontSize = '2rem'; damagePopup.style.fontWeight = 'bold'; damagePopup.style.zIndex = '999'; damagePopup.style.pointerEvents = 'none'; damagePopup.style.animation = 'damageFloat 1.5s ease-out forwards'; document.body.appendChild(damagePopup); setTimeout(() => { damagePopup.remove(); }, 1500); } start() { logSh('βš”οΈ Adventure Reader: Starting', 'INFO'); document.getElementById('progress-text').textContent = 'Click objects to begin your adventure!'; } restart() { logSh('πŸ”„ Adventure Reader: Restarting', 'INFO'); this.reset(); this.start(); } reset() { this.score = 0; this.currentSentenceIndex = 0; this.currentVocabIndex = 0; this.potsDestroyed = 0; this.enemiesDefeated = 0; this.isGamePaused = false; this.isPlayerMoving = false; this.isPlayerInvulnerable = false; // Clear any existing timeout if (this.invulnerabilityTimeout) { clearTimeout(this.invulnerabilityTimeout); this.invulnerabilityTimeout = null; } this.generateGameObjects(); this.initializePlayer(); this.generateDecorations(); } // TTS Methods speakText(text, options = {}) { if (!text || !this.ttsEnabled) return; // Use SettingsManager if available for better language support if (window.SettingsManager && window.SettingsManager.speak) { const ttsOptions = { lang: this.getContentLanguage(), rate: options.rate || 0.8, ...options }; window.SettingsManager.speak(text, ttsOptions) .catch(error => { console.warn('πŸ”Š SettingsManager TTS failed:', error); this.fallbackTTS(text, ttsOptions); }); } else { this.fallbackTTS(text, options); } } fallbackTTS(text, options = {}) { if ('speechSynthesis' in window && text) { // Cancel any ongoing speech speechSynthesis.cancel(); const utterance = new SpeechSynthesisUtterance(text); utterance.lang = this.getContentLanguage(); utterance.rate = options.rate || 0.8; utterance.volume = 1.0; speechSynthesis.speak(utterance); } } getContentLanguage() { // Get language from content or use sensible defaults 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'; // Default fallback } destroy() { // Cancel any ongoing TTS if ('speechSynthesis' in window) { speechSynthesis.cancel(); } this.container.innerHTML = ''; } } // Module registration window.GameModules = window.GameModules || {}; window.GameModules.AdventureReader = AdventureReaderGame;