'use strict'; // === ENEMY AI SYSTEM === function updateEnemies(dt) { // Update regular enemies for (const enemy of [...gameState.enemies]) { enemy.moveTimer -= dt; enemy.attackTimer -= dt; if (enemy.hitTimer > 0) enemy.hitTimer -= dt; if (enemy.specialTimer > 0) { enemy.specialTimer -= dt; if (enemy.specialTimer <= 0) { enemy.stunned = false; enemy.frozen = false; enemy.onFire = false; } } if (enemy.onFire) { enemy.attackTimer -= dt * 0.5; // Fire damage if (enemy.attackTimer <= 0) { enemy.attackTimer = 1000; enemy.hp -= 1; spawnParticles(enemy.x * TILE + TILE / 2, enemy.y * TILE + TILE / 2, '#FF4500', 2); } } // Skip AI if stunned or frozen if (enemy.stunned || enemy.frozen) continue; // Movement AI if (enemy.moveTimer <= 0) { enemy.moveTimer = enemy.moveInterval || ENEMY_MOVE_INTERVAL; updateEnemyMovement(enemy); } // Attack AI if (manhattanDist(enemy.x, enemy.y, gameState.player.x, gameState.player.y) <= (enemy.attackRange || 1)) { if (enemy.attackTimer <= 0) { enemy.attackTimer = enemy.attackInterval || ENEMY_ATTACK_INTERVAL; enemyAttack(enemy); } } // Special behaviors if (enemy.behavior === 'ranged' && enemy.projectileTimer !== undefined) { enemy.projectileTimer -= dt; if (enemy.projectileTimer <= 0) { enemy.projectileTimer = enemy.shootInterval || 3000; enemyShoot(enemy); } } if (enemy.behavior === 'charge') { if (enemy.chargeTimer !== undefined) { enemy.chargeTimer -= dt; if (enemy.chargeTimer <= 0 && !enemy.charging) { enemy.charging = true; enemy.chargeDuration = 1000; } } if (enemy.charging) { enemy.chargeDuration -= dt; if (enemy.chargeDuration <= 0) { enemy.charging = false; enemy.chargeTimer = enemy.chargeCooldown || 5000; enemyCharge(enemy); } } } } // Update boss AI if (gameState.currentBoss) { const boss = gameState.currentBoss; boss.moveTimer -= dt; boss.attackTimer -= dt; if (boss.hitTimer > 0) boss.hitTimer -= dt; if (boss.specialTimer > 0) { boss.specialTimer -= dt; if (boss.specialTimer <= 0) { boss.stunned = false; boss.frozen = false; } } if (!boss.stunned && !boss.frozen) { if (boss.moveTimer <= 0) { boss.moveTimer = boss.moveInterval || ENEMY_MOVE_INTERVAL; updateBossMovement(boss); } if (boss.attackTimer <= 0) { boss.attackTimer = boss.attackInterval || ENEMY_ATTACK_INTERVAL; bossAttack(boss); } } } // Update projectiles updateProjectiles(dt); } function updateEnemyMovement(enemy) { const px = gameState.player.x; const py = gameState.player.y; const dist = manhattanDist(enemy.x, enemy.y, px, py); // Behavior: chase if (enemy.behavior === 'chase' || enemy.behavior === 'ranged' || enemy.behavior === 'charge') { if (enemy.behavior === 'ranged' && dist < (enemy.minRange || 3)) return; // Back away if too close if (enemy.behavior === 'ranged' && dist > (enemy.maxRange || 8)) { // Move closer if too far moveToward(enemy, px, py); return; } moveToward(enemy, px, py); return; } // Behavior: patrol if (enemy.behavior === 'patrol') { if (!enemy.patrolPath || enemy.patrolIdx === undefined) { enemy.patrolIdx = 0; enemy.patrolPath = generatePatrolPath(enemy); } if (enemy.patrolPath) { const target = enemy.patrolPath[enemy.patrolIdx]; if (target) { moveToward(enemy, target.x, target.y); if (enemy.x === target.x && enemy.y === target.y) { enemy.patrolIdx = (enemy.patrolIdx + 1) % enemy.patrolPath.length; } } } return; } // Behavior: stationary if (enemy.behavior === 'stationary') { if (dist <= (enemy.activateRange || 2)) { moveToward(enemy, px, py); } return; } // Behavior: teleport if (enemy.behavior === 'teleport') { if (enemy.teleportTimer !== undefined) { enemy.teleportTimer -= dt; if (enemy.teleportTimer <= 0) { enemy.teleportTimer = enemy.teleportInterval || 4000; // Teleport near player const tx = clamp(px + randInt(-2, 2), 1, COLS - 2); const ty = clamp(py + randInt(-2, 2), 1, ROWS - 2); if (isPassable(gameState.layout, tx, ty)) { spawnParticles(enemy.x * TILE + TILE / 2, enemy.y * TILE + TILE / 2, '#8B008B', 6); enemy.x = tx; enemy.y = ty; spawnParticles(enemy.x * TILE + TILE / 2, enemy.y * TILE + TILE / 2, '#8B008B', 6); } } } return; } // Behavior: explode if (enemy.behavior === 'explode') { moveToward(enemy, px, py); if (manhattanDist(enemy.x, enemy.y, px, py) === 0) { // Explode! spawnParticles(enemy.x * TILE + TILE / 2, enemy.y * TILE + TILE / 2, '#FF4500', 12); enemy.hp = 0; // Damage player if adjacent if (manhattanDist(enemy.x, enemy.y, px, py) <= 1 && !gameState.player.invincible) { gameState.player.hp -= (enemy.explodeDamage || 3); gameState.player.hitTimer = HIT_FLASH; showMessage('Explosion!'); } // Damage in radius for (const other of gameState.enemies) { if (other !== enemy && manhattanDist(enemy.x, enemy.y, other.x, other.y) <= (enemy.explodeRadius || 2)) { dealDamage(other, enemy.explodeDamage || 2, { name: 'explosion' }); } } } } } function moveToward(entity, tx, ty) { const dx = tx - entity.x; const dy = ty - entity.y; let bestDir = null; let bestDist = manhattanDist(entity.x, entity.y, tx, ty); for (let d = 0; d < 4; d++) { const nx = entity.x + DIRS[d].x; const ny = entity.y + DIRS[d].y; if (!inBounds(nx, ny)) continue; if (isWall(gameState.layout, nx, ny)) continue; if (gameState.layout[ny][nx] === TILE_TYPES.LAVA) continue; if (gameState.rocks.find(r => r.x === nx && r.y === ny)) continue; if (gameState.enemies.find(e => e !== entity && e.x === nx && e.y === ny)) continue; if (gameState.chests.find(c => c.x === nx && c.y === ny)) continue; if (gameState.player.x === nx && gameState.player.y === ny) continue; if (gameState.currentBoss && gameState.currentBoss.x === nx && gameState.currentBoss.y === ny) continue; const newDist = manhattanDist(nx, ny, tx, ty); if (newDist < bestDist) { bestDist = newDist; bestDir = d; } } if (bestDir !== null) { entity.x += DIRS[bestDir].x; entity.y += DIRS[bestDir].y; } } function updateBossMovement(boss) { moveToward(boss, gameState.player.x, gameState.player.y); } function enemyAttack(enemy) { if (gameState.player.invincible || gameState.player.invincibleTimer > 0) return; const damage = enemy.attackDamage || 2; gameState.player.hp -= damage; gameState.player.hitTimer = HIT_FLASH; showMessage(`Hit by ${enemy.name}! (-${damage} HP)`); } function bossAttack(boss) { if (gameState.player.invincible || gameState.player.invincibleTimer > 0) return; const attacks = boss.attacks || [{ type: 'melee', damage: 3 }]; const attack = attacks[Math.floor((boss.rng ? boss.rng() : Math.random()) * attacks.length)]; if (attack.type === 'melee') { if (manhattanDist(boss.x, boss.y, gameState.player.x, gameState.player.y) <= (attack.range || 1)) { gameState.player.hp -= attack.damage; gameState.player.hitTimer = HIT_FLASH; showMessage(`Boss attack! (-${attack.damage} HP)`); } } if (attack.type === 'ranged') { const dir = computeDirection(boss.x, boss.y, gameState.player.x, gameState.player.y); spawnProjectile(boss.x, boss.y, dir, { damage: attack.damage, projSpeed: attack.speed || 3, projRange: attack.range || 6, projColor: attack.color || '#FF0000', name: 'boss_proj', }); } if (attack.type === 'aoe') { spawnParticles(boss.x * TILE + TILE / 2, boss.y * TILE + TILE / 2, attack.color || '#FF00FF', 15); if (manhattanDist(boss.x, boss.y, gameState.player.x, gameState.player.y) <= (attack.radius || 3)) { gameState.player.hp -= attack.damage; gameState.player.hitTimer = HIT_FLASH; showMessage(`Boss AoE attack! (-${attack.damage} HP)`); } } } function enemyShoot(enemy) { const dir = computeDirection(enemy.x, enemy.y, gameState.player.x, gameState.player.y); spawnProjectile(enemy.x, enemy.y, dir, { damage: enemy.shootDamage || 1, projSpeed: enemy.shootSpeed || 3, projRange: enemy.shootRange || 5, projColor: enemy.projColor || '#FFFF00', name: 'enemy_proj', }); } function enemyCharge(enemy) { const dir = computeDirection(enemy.x, enemy.y, gameState.player.x, gameState.player.y); const speed = enemy.chargeSpeed || 3; for (let i = 0; i < speed; i++) { const nx = enemy.x + dir.x; const ny = enemy.y + dir.y; if (!isPassable(gameState.layout, nx, ny)) break; if (gameState.enemies.find(e => e !== enemy && e.x === nx && e.y === ny)) break; if (gameState.player.x === nx && gameState.player.y === ny) { if (!gameState.player.invincible) { gameState.player.hp -= (enemy.chargeDamage || 3); gameState.player.hitTimer = HIT_FLASH; showMessage('Charged into you!'); } break; } enemy.x = nx; enemy.y = ny; spawnParticles(nx * TILE + TILE / 2, ny * TILE + TILE / 2, '#FFFFFF', 2); } } function computeDirection(x1, y1, x2, y2) { const dx = x2 - x1; const dy = y2 - y1; if (Math.abs(dx) > Math.abs(dy)) { return { x: dx > 0 ? 1 : -1, y: 0 }; } else { return { x: 0, y: dy > 0 ? 1 : -1 }; } } function generatePatrolPath(enemy) { const path = []; const points = enemy.patrolPoints || []; if (points.length >= 2) return points; // Generate random patrol points around spawn position const sx = enemy.spawnX || enemy.x; const sy = enemy.spawnY || enemy.y; for (let i = 0; i < 4; i++) { const px = clamp(sx + randInt(-3, 3), 1, COLS - 2); const py = clamp(sy + randInt(-3, 3), 1, ROWS - 2); if (isPassable(gameState.layout, px, py)) { path.push({ x: px, y: py }); } } if (path.length < 2) path.push({ x: sx, y: sy }, { x: clamp(sx + 2, 1, COLS - 2), y: sy }); return path; } function createEnemyInstance(def, x, y) { return { name: def.name, id: def.id, x: x, y: y, spawnX: x, spawnY: y, hp: def.hp, maxHp: def.hp, defense: def.defense || 0, attackDamage: def.attackDamage || 1, moveInterval: def.moveInterval || ENEMY_MOVE_INTERVAL, attackInterval: def.attackInterval || ENEMY_ATTACK_INTERVAL, attackRange: def.attackRange || 1, behavior: def.behavior || 'chase', moveTimer: ENEMY_MOVE_INTERVAL, attackTimer: ENEMY_ATTACK_INTERVAL, hitTimer: 0, stunned: false, frozen: false, onFire: false, specialTimer: 0, projectileTimer: def.projectileInterval ? def.projectileInterval : undefined, color: def.color, colorLight: def.colorLight, eyeColor: def.eyeColor, }; } if (typeof module !== 'undefined' && module.exports) { module.exports = { updateEnemies, updateEnemyMovement, moveToward, enemyAttack, bossAttack, createEnemyInstance }; }