/** * 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;