import { System } from '../core/System.ts'; import { GameConfig } from '../GameConfig.ts'; import { SystemName, ComponentType } from '../core/Constants.ts'; import type { Entity } from '../core/Entity.ts'; import type { Health } from '../components/Health.ts'; import type { AI } from '../components/AI.ts'; import type { Position } from '../components/Position.ts'; import type { Velocity } from '../components/Velocity.ts'; import type { Stealth } from '../components/Stealth.ts'; import type { Evolution } from '../components/Evolution.ts'; import type { Sprite } from '../components/Sprite.ts'; import type { Stats } from '../components/Stats.ts'; import type { Combat } from '../components/Combat.ts'; import type { Intent } from '../components/Intent.ts'; import type { PlayerControllerSystem } from './PlayerControllerSystem.ts'; /** * System responsible for managing AI behaviors (wandering, chasing, fleeing, combat). */ export class AISystem extends System { constructor() { super(SystemName.AI); this.requiredComponents = [ComponentType.POSITION, ComponentType.VELOCITY, ComponentType.AI]; this.priority = 15; } /** * Process AI logic for all entities with an AI component. * @param deltaTime - Time elapsed since last frame in seconds * @param entities - Entities matching system requirements */ process(deltaTime: number, entities: Entity[]): void { const playerController = this.engine.systems.find( (s) => s.name === SystemName.PLAYER_CONTROLLER ) as PlayerControllerSystem | undefined; const player = playerController?.getPlayerEntity(); if (!player) return; const playerPos = player?.getComponent(ComponentType.POSITION); const config = GameConfig.AI; entities.forEach((entity) => { const health = entity.getComponent(ComponentType.HEALTH); const ai = entity.getComponent(ComponentType.AI); const position = entity.getComponent(ComponentType.POSITION); const velocity = entity.getComponent(ComponentType.VELOCITY); if (!ai || !position || !velocity) return; if (health && health.isDead() && !health.isProjectile) { velocity.vx = 0; velocity.vy = 0; return; } ai.wanderChangeTime += deltaTime; if (playerPos) { const dx = playerPos.x - position.x; const dy = playerPos.y - position.y; const distance = Math.sqrt(dx * dx + dy * dy); const playerStealth = player?.getComponent(ComponentType.STEALTH); const playerVisibility = playerStealth ? playerStealth.visibility : 1.0; if (distance < ai.alertRadius) { const detectionChance = (1 - distance / ai.alertRadius) * playerVisibility; ai.updateAwareness(detectionChance * deltaTime * config.awarenessGainMultiplier); } else { ai.updateAwareness(-deltaTime * config.awarenessLossRate); } const playerEvolution = player?.getComponent(ComponentType.EVOLUTION); const playerForm = playerEvolution ? playerEvolution.getDominantForm() : 'slime'; const sprite = entity.getComponent(ComponentType.SPRITE); const entityType = sprite?.color === '#ffaa00' ? 'beast' : sprite?.color === '#ff5555' ? 'humanoid' : 'other'; let isPassive = false; let shouldFlee = false; if (entityType === 'humanoid' && playerForm === 'human') { if (ai.awareness < config.passiveAwarenessThreshold) isPassive = true; } else if (entityType === 'beast' && playerForm === 'beast') { const playerStats = player?.getComponent(ComponentType.STATS); const entityStats = entity.getComponent(ComponentType.STATS); if (playerStats && entityStats && playerStats.level > entityStats.level) { shouldFlee = true; } } if (shouldFlee && ai.awareness > config.fleeAwarenessThreshold) { ai.setBehavior('flee'); ai.state = 'fleeing'; ai.setTarget(player.id); } else if (isPassive) { if (ai.behaviorType === 'chase' || ai.behaviorType === 'combat') { ai.setBehavior('wander'); ai.state = 'idle'; ai.clearTarget(); } } else if (ai.awareness > config.detectionAwarenessThreshold && distance < ai.chaseRadius) { if (ai.behaviorType !== 'flee') { const combat = entity.getComponent(ComponentType.COMBAT); if (combat && distance <= combat.attackRange) { ai.setBehavior('combat'); ai.state = 'combat'; } else { ai.setBehavior('chase'); ai.state = 'chasing'; } ai.setTarget(player.id); } } else if (ai.awareness < 0.3) { if (ai.behaviorType === 'chase' || ai.behaviorType === 'combat') { ai.setBehavior('wander'); ai.state = 'idle'; ai.clearTarget(); } } else if (ai.behaviorType === 'chase') { const combat = entity.getComponent(ComponentType.COMBAT); if (combat && distance <= combat.attackRange) { ai.setBehavior('combat'); ai.state = 'combat'; } } } switch (ai.behaviorType) { case 'wander': this.wander(entity, ai, velocity, deltaTime); break; case 'chase': this.chase(entity, ai, velocity, position, playerPos); break; case 'flee': this.flee(entity, ai, velocity, position, playerPos); break; case 'combat': this.combat(entity, ai, velocity, position, playerPos); break; } }); } /** * Execute wandering behavior, moving in a random direction. */ wander(_entity: Entity, ai: AI, velocity: Velocity, _deltaTime: number): void { ai.state = 'moving'; if (ai.wanderChangeTime >= ai.wanderChangeInterval) { ai.wanderDirection = Math.random() * Math.PI * 2; ai.wanderChangeTime = 0; ai.wanderChangeInterval = 1 + Math.random() * 2; } velocity.vx = Math.cos(ai.wanderDirection) * ai.wanderSpeed; velocity.vy = Math.sin(ai.wanderDirection) * ai.wanderSpeed; } /** * Execute chasing behavior, moving toward a target. */ chase( entity: Entity, ai: AI, velocity: Velocity, position: Position, targetPos: Position | undefined ): void { if (!targetPos) return; const dx = targetPos.x - position.x; const dy = targetPos.y - position.y; const distance = Math.sqrt(dx * dx + dy * dy); const combat = entity.getComponent(ComponentType.COMBAT); if (combat && distance <= combat.attackRange) { ai.setBehavior('combat'); ai.state = 'combat'; return; } ai.state = 'chasing'; if (distance > 0.1) { const speed = ai.wanderSpeed * 1.5; velocity.vx = (dx / distance) * speed; velocity.vy = (dy / distance) * speed; } else { velocity.vx = 0; velocity.vy = 0; } } /** * Execute fleeing behavior, moving away from a target. */ flee( _entity: Entity, ai: AI, velocity: Velocity, position: Position, targetPos: Position | undefined ): void { if (!targetPos) return; ai.state = 'fleeing'; const dx = position.x - targetPos.x; const dy = position.y - targetPos.y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance > 0.1) { const speed = ai.wanderSpeed * 1.2; velocity.vx = (dx / distance) * speed; velocity.vy = (dy / distance) * speed; } } /** * Execute combat behavior, moving into range and setting attack intent. */ combat( entity: Entity, ai: AI, velocity: Velocity, position: Position, targetPos: Position | undefined ): void { if (!targetPos) return; ai.state = 'attacking'; const dx = targetPos.x - position.x; const dy = targetPos.y - position.y; const distance = Math.sqrt(dx * dx + dy * dy); const combat = entity.getComponent(ComponentType.COMBAT); if (combat && distance > combat.attackRange) { const speed = ai.wanderSpeed; velocity.vx = (dx / distance) * speed; velocity.vy = (dy / distance) * speed; } else { velocity.vx *= 0.5; velocity.vy *= 0.5; position.rotation = Math.atan2(dy, dx); const intent = entity.getComponent(ComponentType.INTENT); if (intent) { intent.setIntent('attack', { targetX: targetPos.x, targetY: targetPos.y }); } } } }