/** * Catapult.js * Enemy that launches boulders and stone rain at Mario * Catapults appear in level 4+, Onagers (stronger) in level 5+ */ export class Catapult { /** * Generate catapults for a level * @param {Object} level - Level data * @param {number} levelIndex - Level index * @param {number} levelWidth - Width of the level * @param {number} canvasHeight - Canvas height * @returns {Array} - Array of catapult objects */ static generate(level, levelIndex, levelWidth, canvasHeight) { const catapults = []; let catapultCount = 1; // Always 1 catapult for level 4+ let onagerCount = 0; // Level 5+ gets onagers if (levelIndex >= 4) { onagerCount = 1; // 1 onager for level 5+ } const totalCount = catapultCount + onagerCount; console.log(`🏹 Generating ${catapultCount} catapult(s) and ${onagerCount} onager(s) for level ${levelIndex + 1}`); for (let i = 0; i < totalCount; i++) { const isOnager = i >= catapultCount; // Onagers come after catapults // Place catapults near END of level const nearEndX = levelWidth * 0.7; // 70% through level const catapultX = nearEndX + (i * 300) + Math.random() * 200; let catapultY = canvasHeight - 100; // Default: on background ground // Check if there's a platform, wall, or stair above this position const platformAbove = this._findPlatformAbove(catapultX, catapultY, level.platforms || []); const wallAbove = this._findWallAbove(catapultX, catapultY, level.walls || []); const stairAbove = this._findStairAbove(catapultX, catapultY, level.stairs || []); // Choose the lowest obstacle (closest to ground = highest Y value) const obstacles = [platformAbove, wallAbove, stairAbove].filter(obs => obs !== null); if (obstacles.length > 0) { const obstacleAbove = obstacles.reduce((lowest, current) => current.y > lowest.y ? current : lowest ); catapultY = obstacleAbove.y - 80; // 80 is catapult height console.log(`🏹 Catapult moved to obstacle at y=${catapultY.toFixed(0)}`); } catapults.push({ x: catapultX, y: catapultY, width: 60, height: 80, color: isOnager ? '#654321' : '#8B4513', lastShot: 0, shootCooldown: isOnager ? 6000 + Math.random() * 2000 : 4000 + Math.random() * 2000, type: isOnager ? 'onager' : 'catapult', isOnager: isOnager, armAngle: 0 // For rendering }); console.log(`${isOnager ? '🏛️' : '🏹'} ${isOnager ? 'Onager' : 'Catapult'} placed at x=${catapultX.toFixed(0)}, y=${catapultY.toFixed(0)}`); } return catapults; } /** * Find platform above a position */ static _findPlatformAbove(x, groundY, platforms) { let bestPlatform = null; let lowestY = 0; platforms.forEach(platform => { const catapultLeft = x; const catapultRight = x + 60; const platformLeft = platform.x; const platformRight = platform.x + platform.width; const hasHorizontalOverlap = catapultLeft < platformRight && catapultRight > platformLeft; if (hasHorizontalOverlap && platform.y < groundY && platform.y > lowestY) { bestPlatform = platform; lowestY = platform.y; } }); return bestPlatform; } /** * Find wall above a position */ static _findWallAbove(x, groundY, walls) { let bestWall = null; let lowestY = 0; walls.forEach(wall => { const catapultLeft = x; const catapultRight = x + 60; const wallLeft = wall.x; const wallRight = wall.x + wall.width; const hasHorizontalOverlap = catapultLeft < wallRight && catapultRight > wallLeft; if (hasHorizontalOverlap && wall.y < groundY && wall.y > lowestY) { bestWall = wall; lowestY = wall.y; } }); return bestWall; } /** * Find stair above a position */ static _findStairAbove(x, groundY, stairs) { let bestStair = null; let lowestY = 0; stairs.forEach(stair => { const catapultLeft = x; const catapultRight = x + 60; const stairLeft = stair.x; const stairRight = stair.x + stair.width; const hasHorizontalOverlap = catapultLeft < stairRight && catapultRight > stairLeft; if (hasHorizontalOverlap && stair.y < groundY && stair.y > lowestY) { bestStair = stair; lowestY = stair.y; } }); return bestStair; } /** * Update all catapults * @param {Array} catapults - Array of catapults * @param {Object} mario - Mario object * @param {Array} boulders - Boulders array * @param {Array} stones - Stones array (for onagers) * @param {Function} playSound - Sound callback */ static update(catapults, mario, boulders, stones, playSound) { const currentTime = Date.now(); catapults.forEach(catapult => { // Arm animation catapult.armAngle = Math.sin(Date.now() / 200) * 0.2; // Check if it's time to shoot if (currentTime - catapult.lastShot > catapult.shootCooldown) { const distanceToMario = Math.abs(catapult.x - mario.x); // Catapult shoots boulders (single target) if (!catapult.isOnager && distanceToMario < 600) { // Calculate trajectory to Mario const dx = mario.x - catapult.x; const dy = mario.y - catapult.y; const distance = Math.sqrt(dx * dx + dy * dy); const speed = 8; const velocityX = (dx / distance) * speed; const velocityY = (dy / distance) * speed; boulders.push({ x: catapult.x + catapult.width / 2, y: catapult.y, velocityX: velocityX, velocityY: velocityY, radius: 20, type: 'boulder', launched: true }); catapult.lastShot = currentTime; if (playSound) playSound('jump'); // Boulder launch sound console.log(`🪨 Catapult launched boulder towards Mario!`); } // Onager shoots stone rain (area attack) else if (catapult.isOnager && distanceToMario < 800) { // Create stone rain above Mario's area const stoneCount = 8 + Math.floor(Math.random() * 5); // 8-12 stones for (let i = 0; i < stoneCount; i++) { const offsetX = (Math.random() - 0.5) * 400; // Spread 400px around Mario stones.push({ x: mario.x + offsetX, y: -50 - Math.random() * 100, // Start above screen velocityX: (Math.random() - 0.5) * 2, velocityY: 2 + Math.random() * 3, width: 15 + Math.random() * 10, height: 15 + Math.random() * 10, type: 'stone', rotation: Math.random() * Math.PI * 2 }); } catapult.lastShot = currentTime; if (playSound) playSound('enemy_defeat'); // Different sound for stone rain console.log(`☄️ Onager launched stone rain (${stoneCount} stones)!`); } } }); } /** * Update boulders * @param {Array} boulders - Array of boulders * @param {Object} mario - Mario object * @param {Array} platforms - Platforms for collision * @param {Array} walls - Walls for collision * @param {Function} onImpact - Callback when boulder hits something * @returns {Array} - Updated boulders array */ static updateBoulders(boulders, mario, platforms, walls, onImpact) { const GRAVITY = 0.3; const updatedBoulders = []; boulders.forEach((boulder, index) => { // Apply physics boulder.velocityY += GRAVITY; boulder.x += boulder.velocityX; boulder.y += boulder.velocityY; // Check collision with platforms let hitPlatform = false; platforms.forEach((platform, platformIndex) => { if (this._isCollidingCircleRect(boulder, platform)) { hitPlatform = true; if (onImpact) { onImpact(boulder, index, boulder.x, boulder.y, platform, platformIndex, 'platform'); } } }); // Check collision with walls let hitWall = false; walls.forEach((wall, wallIndex) => { if (this._isCollidingCircleRect(boulder, wall)) { hitWall = true; if (onImpact) { onImpact(boulder, index, boulder.x, boulder.y, wall, wallIndex, 'wall'); } } }); // Check collision with Mario if (this._isCollidingCircleRect(boulder, mario)) { if (onImpact) { onImpact(boulder, index, boulder.x, boulder.y, mario, -1, 'mario'); } return; // Remove boulder } // Remove if out of bounds or hit something if (!hitPlatform && !hitWall && boulder.y < 1000) { updatedBoulders.push(boulder); } }); return updatedBoulders; } /** * Update stones (stone rain) * @param {Array} stones - Array of stones * @param {Object} mario - Mario object * @param {Array} platforms - Platforms for collision * @param {Function} onImpact - Callback when stone hits something * @returns {Array} - Updated stones array */ static updateStones(stones, mario, platforms, onImpact) { const GRAVITY = 0.5; const updatedStones = []; stones.forEach((stone, index) => { // Apply physics stone.velocityY += GRAVITY; stone.x += stone.velocityX; stone.y += stone.velocityY; stone.rotation += 0.1; // Check collision with platforms let hitPlatform = false; platforms.forEach(platform => { if (this._isCollidingRectRect(stone, platform)) { hitPlatform = true; if (onImpact) { onImpact(stone, index, 'platform'); } } }); // Check collision with Mario if (this._isCollidingRectRect(stone, mario)) { if (onImpact) { onImpact(stone, index, 'mario'); } return; // Remove stone } // Keep stone if not hit and still on screen if (!hitPlatform && stone.y < 1000) { updatedStones.push(stone); } }); return updatedStones; } /** * Circle-Rectangle collision detection */ static _isCollidingCircleRect(circle, rect) { const closestX = Math.max(rect.x, Math.min(circle.x, rect.x + rect.width)); const closestY = Math.max(rect.y, Math.min(circle.y, rect.y + rect.height)); const distanceX = circle.x - closestX; const distanceY = circle.y - closestY; const distanceSquared = distanceX * distanceX + distanceY * distanceY; return distanceSquared < (circle.radius * circle.radius); } /** * Rectangle-Rectangle collision detection */ static _isCollidingRectRect(rect1, rect2) { return rect1.x < rect2.x + rect2.width && rect1.x + rect1.width > rect2.x && rect1.y < rect2.y + rect2.height && rect1.y + rect1.height > rect2.y; } } export default Catapult;