Major improvements: - Add TTSHelper utility for text-to-speech functionality - Enhance content compatibility scoring across all games - Improve sentence extraction from multiple content sources - Update all game modules to support diverse content formats - Refine MarioEducational physics and rendering - Polish UI styles and remove unused CSS Games updated: AdventureReader, FillTheBlank, FlashcardLearning, GrammarDiscovery, MarioEducational, QuizGame, RiverRun, WhackAMole, WhackAMoleHard, WizardSpellCaster, WordDiscovery, WordStorm 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
196 lines
7.1 KiB
JavaScript
196 lines
7.1 KiB
JavaScript
/**
|
|
* FlyingEye.js
|
|
* Flying enemy that chases Mario and performs dash attacks
|
|
* Appears in level 5+
|
|
*/
|
|
|
|
export class FlyingEye {
|
|
/**
|
|
* Generate flying eyes for a level
|
|
* @param {Object} level - Level data
|
|
* @param {number} difficulty - Difficulty level
|
|
* @returns {Array} - Array of flying eye objects
|
|
*/
|
|
static generate(level, difficulty) {
|
|
const eyes = [];
|
|
const eyeCount = Math.min(4, Math.max(3, difficulty - 2)); // 3-4 flying eyes
|
|
console.log(`👁️ Generating ${eyeCount} flying eyes for level 5+`);
|
|
|
|
for (let i = 0; i < eyeCount; i++) {
|
|
// Eyes spawn in the middle-upper area of the level
|
|
const eyeX = 300 + (i * 400) + Math.random() * 200; // Spread across level
|
|
const eyeY = 100 + Math.random() * 150; // Upper area of screen
|
|
|
|
eyes.push({
|
|
x: eyeX,
|
|
y: eyeY,
|
|
width: 30,
|
|
height: 30,
|
|
velocityX: (Math.random() - 0.5) * 2, // Random horizontal drift -1 to +1
|
|
velocityY: (Math.random() - 0.5) * 2, // Random vertical drift -1 to +1
|
|
color: '#DC143C', // Crimson red
|
|
pupilColor: '#000000', // Black pupil
|
|
type: 'flying_eye',
|
|
health: 1,
|
|
maxHealth: 1,
|
|
// AI behavior properties
|
|
chaseDistance: 200, // Start chasing Mario within 200px
|
|
chaseSpeed: 3.5, // Faster chase speed
|
|
idleSpeed: 1.2, // Faster idle movement
|
|
lastDirectionChange: Date.now(),
|
|
directionChangeInterval: 2000 + Math.random() * 3000, // Change direction every 2-5 seconds
|
|
isChasing: false,
|
|
// Dash behavior
|
|
dashCooldown: 0,
|
|
dashDuration: 0,
|
|
isDashing: false,
|
|
dashSpeed: 8, // Very fast dash
|
|
lastDashTime: Date.now(),
|
|
dashInterval: 3000 + Math.random() * 2000, // Dash every 3-5 seconds
|
|
// Visual properties
|
|
blinkTimer: 0,
|
|
isBlinking: false
|
|
});
|
|
|
|
console.log(`👁️ Flying eye ${i + 1} placed at x=${eyeX.toFixed(0)}, y=${eyeY.toFixed(0)}`);
|
|
}
|
|
|
|
return eyes;
|
|
}
|
|
|
|
/**
|
|
* Update all flying eyes
|
|
* @param {Array} eyes - Array of flying eyes
|
|
* @param {Object} mario - Mario object
|
|
* @param {Function} playSound - Sound callback
|
|
*/
|
|
static update(eyes, mario, playSound) {
|
|
const currentTime = Date.now();
|
|
|
|
eyes.forEach(eye => {
|
|
// Calculate distance from eye center to Mario center
|
|
const marioCenter = { x: mario.x + mario.width / 2, y: mario.y + mario.height / 2 };
|
|
const distanceToMario = Math.sqrt(
|
|
Math.pow(eye.x - marioCenter.x, 2) + Math.pow(eye.y - marioCenter.y, 2)
|
|
);
|
|
|
|
// Blinking animation
|
|
eye.blinkTimer++;
|
|
if (eye.blinkTimer > 120) {
|
|
eye.isBlinking = true;
|
|
}
|
|
if (eye.blinkTimer > 125) {
|
|
eye.isBlinking = false;
|
|
eye.blinkTimer = 0;
|
|
}
|
|
|
|
// Check if should chase Mario
|
|
eye.isChasing = distanceToMario < eye.chaseDistance;
|
|
|
|
// Dash behavior
|
|
if (eye.isDashing) {
|
|
eye.dashDuration--;
|
|
if (eye.dashDuration <= 0) {
|
|
eye.isDashing = false;
|
|
eye.dashCooldown = 60; // Cooldown frames after dash
|
|
}
|
|
} else if (eye.dashCooldown > 0) {
|
|
eye.dashCooldown--;
|
|
} else if (eye.isChasing && currentTime - eye.lastDashTime > eye.dashInterval) {
|
|
// Start dash towards Mario
|
|
eye.isDashing = true;
|
|
eye.dashDuration = 30; // 30 frames of dash
|
|
eye.lastDashTime = currentTime;
|
|
|
|
// Set dash velocity towards Mario center
|
|
const dx = marioCenter.x - eye.x;
|
|
const dy = marioCenter.y - eye.y;
|
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
eye.velocityX = (dx / distance) * eye.dashSpeed;
|
|
eye.velocityY = (dy / distance) * eye.dashSpeed;
|
|
|
|
console.log(`👁️ Flying eye dashes towards Mario!`);
|
|
}
|
|
|
|
// Movement behavior
|
|
if (eye.isDashing) {
|
|
// Continue dash movement
|
|
eye.x += eye.velocityX;
|
|
eye.y += eye.velocityY;
|
|
} else if (eye.isChasing) {
|
|
// Chase Mario center
|
|
const dx = marioCenter.x - eye.x;
|
|
const dy = marioCenter.y - eye.y;
|
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
eye.velocityX = (dx / distance) * eye.chaseSpeed;
|
|
eye.velocityY = (dy / distance) * eye.chaseSpeed;
|
|
|
|
eye.x += eye.velocityX;
|
|
eye.y += eye.velocityY;
|
|
} else {
|
|
// Idle wandering
|
|
if (currentTime - eye.lastDirectionChange > eye.directionChangeInterval) {
|
|
eye.velocityX = (Math.random() - 0.5) * eye.idleSpeed * 2;
|
|
eye.velocityY = (Math.random() - 0.5) * eye.idleSpeed * 2;
|
|
eye.lastDirectionChange = currentTime;
|
|
}
|
|
|
|
eye.x += eye.velocityX;
|
|
eye.y += eye.velocityY;
|
|
}
|
|
|
|
// Keep eyes within bounds (with some margin)
|
|
// Allow eyes to fly anywhere but not too close to edges
|
|
if (eye.x < 50) {
|
|
eye.x = 50;
|
|
eye.velocityX = Math.abs(eye.velocityX);
|
|
}
|
|
if (eye.y < 50) {
|
|
eye.y = 50;
|
|
eye.velocityY = Math.abs(eye.velocityY);
|
|
}
|
|
// Allow eyes to go much lower (near ground level) - 600px is just above ground (640)
|
|
if (eye.y > 600) {
|
|
eye.y = 600;
|
|
eye.velocityY = -Math.abs(eye.velocityY);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Check collision between Mario and flying eyes
|
|
* @param {Object} mario - Mario object
|
|
* @param {Array} eyes - Array of flying eyes
|
|
* @returns {Object|null} - Colliding eye or null
|
|
*/
|
|
static checkCollision(mario, eyes) {
|
|
for (const eye of eyes) {
|
|
// Eye position is CENTER, convert to top-left corner for collision
|
|
const eyeLeft = eye.x - eye.width / 2;
|
|
const eyeTop = eye.y - eye.height / 2;
|
|
|
|
// Rectangle collision with centered eye position
|
|
if (mario.x < eyeLeft + eye.width &&
|
|
mario.x + mario.width > eyeLeft &&
|
|
mario.y < eyeTop + eye.height &&
|
|
mario.y + mario.height > eyeTop) {
|
|
return eye;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Damage a flying eye
|
|
* @param {Object} eye - Flying eye to damage
|
|
* @returns {boolean} - True if eye was killed
|
|
*/
|
|
static damage(eye) {
|
|
eye.health--;
|
|
return eye.health <= 0;
|
|
}
|
|
}
|
|
|
|
export default FlyingEye;
|