slime/src/systems/AISystem.ts
2026-01-06 23:25:33 -05:00

256 lines
8.5 KiB
TypeScript

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<Position>(ComponentType.POSITION);
const config = GameConfig.AI;
entities.forEach((entity) => {
const health = entity.getComponent<Health>(ComponentType.HEALTH);
const ai = entity.getComponent<AI>(ComponentType.AI);
const position = entity.getComponent<Position>(ComponentType.POSITION);
const velocity = entity.getComponent<Velocity>(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<Stealth>(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<Evolution>(ComponentType.EVOLUTION);
const playerForm = playerEvolution ? playerEvolution.getDominantForm() : 'slime';
const sprite = entity.getComponent<Sprite>(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<Stats>(ComponentType.STATS);
const entityStats = entity.getComponent<Stats>(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<Combat>(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<Combat>(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<Combat>(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<Combat>(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<Intent>(ComponentType.INTENT);
if (intent) {
intent.setIntent('attack', { targetX: targetPos.x, targetY: targetPos.y });
}
}
}
}