Class_generator/js/games/adventure-reader.js
StillHammer b004382cee Initial commit: Interactive English Learning Platform
- Complete SPA architecture with dynamic module loading
- 9 different educational games (whack-a-mole, memory, quiz, etc.)
- Rich content system supporting multimedia (audio, images, video)
- Chinese study mode with character recognition
- Adaptive game system based on available content
- Content types: vocabulary, grammar, poems, fill-blanks, corrections
- AI-powered text evaluation for open-ended answers
- Flexible content schema with backward compatibility

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-15 14:25:13 +08:00

950 lines
34 KiB
JavaScript

// === 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;
// Content extraction
this.vocabulary = this.extractVocabulary(this.content);
this.sentences = this.extractSentences(this.content);
this.init();
}
init() {
if ((!this.vocabulary || this.vocabulary.length === 0) &&
(!this.sentences || this.sentences.length === 0)) {
console.error('No content available for Adventure Reader');
this.showInitError();
return;
}
this.createGameInterface();
this.initializePlayer();
this.setupEventListeners();
this.generateGameObjects();
this.generateDecorations();
this.startGameLoop();
}
showInitError() {
this.container.innerHTML = `
<div class="game-error">
<h3>❌ Error loading</h3>
<p>This content doesn't contain texts compatible with Adventure Reader.</p>
<button onclick="AppNavigation.goBack()" class="back-btn">← Back</button>
</div>
`;
}
extractVocabulary(content) {
let vocabulary = [];
if (content.rawContent && content.rawContent.vocabulary) {
if (typeof content.rawContent.vocabulary === 'object' && !Array.isArray(content.rawContent.vocabulary)) {
vocabulary = Object.entries(content.rawContent.vocabulary).map(([english, translation]) => ({
english: english,
translation: translation
}));
} else if (Array.isArray(content.rawContent.vocabulary)) {
vocabulary = content.rawContent.vocabulary;
}
}
return vocabulary.filter(item => item && item.english && item.translation);
}
extractSentences(content) {
let sentences = [];
if (content.rawContent) {
if (content.rawContent.sentences && Array.isArray(content.rawContent.sentences)) {
sentences = content.rawContent.sentences;
} else if (content.rawContent.texts && Array.isArray(content.rawContent.texts)) {
// Extract sentences from texts
content.rawContent.texts.forEach(text => {
const textSentences = text.content.split(/[.!?]+/).filter(s => s.trim().length > 10);
textSentences.forEach(sentence => {
sentences.push({
english: sentence.trim() + '.',
translation: sentence.trim() + '.' // Fallback
});
});
});
}
}
return sentences.filter(item => item && item.english);
}
createGameInterface() {
this.container.innerHTML = `
<div class="adventure-reader-wrapper">
<!-- Game HUD -->
<div class="game-hud">
<div class="hud-left">
<div class="stat-item">
<span class="stat-icon">🏆</span>
<span id="score-display">0</span>
</div>
<div class="stat-item">
<span class="stat-icon">🏺</span>
<span id="pots-counter">0</span>
</div>
<div class="stat-item">
<span class="stat-icon">⚔️</span>
<span id="enemies-counter">0</span>
</div>
</div>
<div class="hud-right">
<div class="progress-info">
<span id="progress-text">Start your adventure!</span>
</div>
</div>
</div>
<!-- Game Map -->
<div class="game-map" id="game-map">
<!-- Player -->
<div class="player" id="player">🧙‍♂️</div>
<!-- Game objects will be generated here -->
</div>
<!-- Game Controls -->
<div class="game-controls">
<div class="instructions">
Click 🏺 pots for vocabulary • Click 👹 enemies for sentences
</div>
<button class="control-btn secondary" id="restart-btn">🔄 Restart Adventure</button>
</div>
<!-- Reading Modal -->
<div class="reading-modal" id="reading-modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3 id="modal-title">Enemy Defeated!</h3>
</div>
<div class="modal-body">
<div class="reading-content" id="reading-content">
<!-- Sentence content -->
</div>
</div>
<div class="modal-footer">
<button class="control-btn primary" id="continue-btn">Continue Adventure →</button>
</div>
</div>
</div>
<!-- Vocab Popup -->
<div class="vocab-popup" id="vocab-popup" style="display: none;">
<div class="popup-content">
<div class="vocab-word" id="vocab-word"></div>
<div class="vocab-translation" id="vocab-translation"></div>
</div>
</div>
</div>
`;
}
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');
wordEl.textContent = vocab.english;
translationEl.textContent = vocab.translation;
popup.style.display = 'block';
popup.classList.add('show');
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');
content.innerHTML = `
<div class="sentence-content">
<p class="english-text">${sentence.english}</p>
${sentence.translation ? `<p class="translation-text">${sentence.translation}</p>` : ''}
</div>
`;
modal.style.display = 'flex';
modal.classList.add('show');
}
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() {
console.log('⚔️ Adventure Reader: Starting');
document.getElementById('progress-text').textContent = 'Click objects to begin your adventure!';
}
restart() {
console.log('🔄 Adventure Reader: Restarting');
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();
}
destroy() {
this.container.innerHTML = '';
}
}
// Module registration
window.GameModules = window.GameModules || {};
window.GameModules.AdventureReader = AdventureReaderGame;