// === 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 = `
${sentence.english}
${sentence.translation ? `
${sentence.translation}
` : ''}
`;
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;