255 lines
8.5 KiB
TypeScript
255 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 ? playerController.getPlayerEntity() : null;
|
|
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 });
|
|
}
|
|
}
|
|
}
|
|
}
|