import { AttackType, EnemyType, IEnemyConfig } from '../data/Interfaces'; /** * Enemy AI base class + four concrete subclasses (req 6.1-6.7). * * Each enemy is modelled as a tiny state machine that ticks on `update(dt)` * and emits an `IEnemyAction[]` the level manager then spawns into the world * (bullets / smoke bombs / fireballs / sword swings). * * Enemies outside the camera's culling rect can be frozen by simply skipping * `update()` — see `EnemyManager.update()` below (requirement 6.7 / 18.5). */ export interface IEnemyAction { kind: 'fire_bullet' | 'melee_swing' | 'spawn_item' | 'drop_item'; attackType?: AttackType; /** Origin of the projectile, world coords. */ originX?: number; originY?: number; /** Velocity of the projectile. */ velX?: number; velY?: number; /** For drop_item only — item id + world coords. */ itemId?: string; } export interface IPlayerSense { x: number; y: number; isGrounded: boolean; } export interface IEnemyAABB { x: number; y: number; w: number; h: number; } export interface IEnemyUpdateCtx { dtSec: number; nowMs: number; player: IPlayerSense; } export abstract class EnemyAIBase { public readonly type: EnemyType; public pos: { x: number; y: number }; public alive = true; protected cooldownSec = 0; protected readonly cfg: IEnemyConfig; constructor(cfg: IEnemyConfig, spawnX: number, spawnY: number) { this.cfg = cfg; this.type = cfg.id; this.pos = { x: spawnX, y: spawnY }; } public abstract update(ctx: IEnemyUpdateCtx): IEnemyAction[]; public get aabb(): IEnemyAABB { return { x: this.pos.x, y: this.pos.y, w: this.cfg.size.w, h: this.cfg.size.h }; } protected decrementCooldown(dtSec: number): boolean { this.cooldownSec -= dtSec; if (this.cooldownSec > 0) return false; this.cooldownSec = this.cfg.attackIntervalSec; return true; } } // ---------- Qing Ren (req 6.1) -------------------------------------------- export class QingRenAI extends EnemyAIBase { public update(ctx: IEnemyUpdateCtx): IEnemyAction[] { if (!this.decrementCooldown(ctx.dtSec)) return []; const dx = ctx.player.x - this.pos.x; const horizontalDistance = Math.abs(dx); const direction = dx >= 0 ? 1 : -1; if (horizontalDistance < 64) { return [{ kind: 'melee_swing', attackType: 'sword' }]; } return [ { kind: 'fire_bullet', attackType: 'shuriken', originX: this.pos.x, originY: this.pos.y, velX: 240 * direction, velY: 0, }, ]; } } // ---------- Chi Ren (req 6.2-6.4) ----------------------------------------- export class ChiRenAI extends EnemyAIBase { private interceptCooldown = 0; public update(ctx: IEnemyUpdateCtx): IEnemyAction[] { // Patrol horizontally toward the player at 120px/s (req 6.2). const dx = ctx.player.x - this.pos.x; const direction = dx >= 0 ? 1 : -1; this.pos.x += direction * this.cfg.moveSpeed * ctx.dtSec; // Proactive intercept jump when player stands still within vision (req 6.3). if (this.interceptCooldown > 0) this.interceptCooldown -= ctx.dtSec; const playerIdle = Math.abs(ctx.player.x - this.pos.x) < 200 && ctx.player.isGrounded; if (playerIdle && this.interceptCooldown <= 0) { this.interceptCooldown = 3; // Intercept arc: +X velocity + upward bounce, treated as position warp. this.pos.y += 48; } if (!this.decrementCooldown(ctx.dtSec)) return []; return [ { kind: 'fire_bullet', attackType: 'smoke_bomb', originX: this.pos.x, originY: this.pos.y, velX: 140 * direction, velY: 0, }, ]; } } // ---------- Hei Ren (req 6.5) --------------------------------------------- export class HeiRenAI extends EnemyAIBase { private hasDroppedMagicFlute = false; public update(ctx: IEnemyUpdateCtx): IEnemyAction[] { if (!this.decrementCooldown(ctx.dtSec)) return []; const dx = ctx.player.x - this.pos.x; const direction = dx >= 0 ? 1 : -1; if (Math.abs(dx) < 96) { return [{ kind: 'melee_swing', attackType: 'sword' }]; } return [ { kind: 'fire_bullet', attackType: 'shuriken', originX: this.pos.x, originY: this.pos.y, velX: 200 * direction, velY: 0, }, ]; } /** Called by EnemyManager on death — yields at most one magic flute. */ public onKilled(): IEnemyAction[] { if (this.hasDroppedMagicFlute) return []; this.hasDroppedMagicFlute = true; return [{ kind: 'drop_item', itemId: 'mo_di', originX: this.pos.x, originY: this.pos.y }]; } } // ---------- Yao Fang (req 6.6) -------------------------------------------- export class YaoFangAI extends EnemyAIBase { public update(ctx: IEnemyUpdateCtx): IEnemyAction[] { if (!this.decrementCooldown(ctx.dtSec)) return []; const dx = ctx.player.x - this.pos.x; const direction = dx >= 0 ? 1 : -1; return [ { kind: 'fire_bullet', attackType: 'fireball', originX: this.pos.x, originY: this.pos.y, velX: 260 * direction, velY: 0, }, ]; } } // ---------- Manager with camera-culling (req 6.7) ------------------------- export interface ICullingRect { leftX: number; rightX: number; topY: number; bottomY: number; } export class EnemyManager { private readonly enemies: EnemyAIBase[] = []; public spawn(enemy: EnemyAIBase): void { this.enemies.push(enemy); } public get all(): ReadonlyArray { return this.enemies; } /** * Update all live enemies that intersect `cull`. Returns the concatenated * list of actions emitted so the caller (LevelMgr) can instantiate * projectiles, drops, etc. */ public update(dtSec: number, nowMs: number, player: IPlayerSense, cull: ICullingRect): IEnemyAction[] { const actions: IEnemyAction[] = []; for (const e of this.enemies) { if (!e.alive) continue; if (!this.inside(e, cull)) continue; const ctx: IEnemyUpdateCtx = { dtSec, nowMs, player }; actions.push(...e.update(ctx)); } return actions; } public kill(enemy: EnemyAIBase): IEnemyAction[] { enemy.alive = false; if (enemy instanceof HeiRenAI) return enemy.onKilled(); return []; } public clear(): void { this.enemies.length = 0; } private inside(e: EnemyAIBase, cull: ICullingRect): boolean { return ( e.pos.x + e.aabb.w / 2 >= cull.leftX && e.pos.x - e.aabb.w / 2 <= cull.rightX && e.pos.y + e.aabb.h / 2 >= cull.bottomY && e.pos.y - e.aabb.h / 2 <= cull.topY ); } }