feat: add poc

This commit is contained in:
Juan Sebastián Montoya 2026-01-06 14:02:09 -05:00
parent 43d27b04d9
commit 4a4fa05ce4
53 changed files with 6191 additions and 0 deletions

205
src/systems/AISystem.js Normal file
View file

@ -0,0 +1,205 @@
import { System } from '../core/System.js';
import { GameConfig } from '../GameConfig.js';
export class AISystem extends System {
constructor() {
super('AISystem');
this.requiredComponents = ['Position', 'Velocity', 'AI'];
this.priority = 15;
}
process(deltaTime, entities) {
const playerController = this.engine.systems.find(s => s.name === 'PlayerControllerSystem');
const player = playerController ? playerController.getPlayerEntity() : null;
const playerPos = player?.getComponent('Position');
const config = GameConfig.AI;
entities.forEach(entity => {
const ai = entity.getComponent('AI');
const position = entity.getComponent('Position');
const velocity = entity.getComponent('Velocity');
const _stealth = entity.getComponent('Stealth');
if (!ai || !position || !velocity) return;
// Update wander timer
ai.wanderChangeTime += deltaTime;
// Detect player
if (playerPos) {
const dx = playerPos.x - position.x;
const dy = playerPos.y - position.y;
const distance = Math.sqrt(dx * dx + dy * dy);
// Update awareness based on distance and player stealth
const playerStealth = player?.getComponent('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); // Lose awareness over time
}
// Biological Reputation Logic
const playerEvolution = player?.getComponent('Evolution');
const playerForm = playerEvolution ? playerEvolution.getDominantForm() : 'slime';
const entityType = entity.getComponent('Sprite')?.color === '#ffaa00' ? 'beast' :
entity.getComponent('Sprite')?.color === '#ff5555' ? 'humanoid' : 'other';
// Check if player is "one of us" or "too scary"
let isPassive = false;
let shouldFlee = false;
if (entityType === 'humanoid' && playerForm === 'human') {
// Humanoids are passive to human-form slime unless awareness is maxed (hostile action taken)
if (ai.awareness < config.passiveAwarenessThreshold) isPassive = true;
} else if (entityType === 'beast' && playerForm === 'beast') {
// Beasts might flee from a dominant beast player
const playerStats = player?.getComponent('Stats');
const entityStats = entity.getComponent('Stats');
if (playerStats && entityStats && playerStats.level > entityStats.level) {
shouldFlee = true;
}
}
// Behavior based on awareness, reputation, and distance
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') {
// Check if in attack range - if so, use combat behavior
const combat = entity.getComponent('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') {
// Update from chase to combat if in range
const combat = entity.getComponent('Combat');
if (combat && distance <= combat.attackRange) {
ai.setBehavior('combat');
ai.state = 'combat';
}
}
}
// Execute behavior
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;
}
});
}
wander(entity, ai, velocity, _deltaTime) {
ai.state = 'moving';
// Change direction periodically
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;
}
chase(entity, ai, velocity, position, targetPos) {
if (!targetPos) return;
const dx = targetPos.x - position.x;
const dy = targetPos.y - position.y;
const distance = Math.sqrt(dx * dx + dy * dy);
// Check if we should switch to combat
const combat = entity.getComponent('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;
}
}
flee(entity, ai, velocity, position, targetPos) {
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;
}
}
combat(entity, ai, velocity, position, targetPos) {
if (!targetPos) return;
ai.state = 'attacking';
// Stop moving when in combat range - let CombatSystem handle attacks
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');
if (combat && distance > combat.attackRange) {
// Move closer if out of range
const speed = ai.wanderSpeed;
velocity.vx = (dx / distance) * speed;
velocity.vy = (dy / distance) * speed;
} else {
// Stop and face target
velocity.vx *= 0.5;
velocity.vy *= 0.5;
if (position) {
position.rotation = Math.atan2(dy, dx);
}
}
}
}