import { HUNGER_THRESHOLD, ENERGY_THRESHOLD, PRODUCTIVITY_THRESHOLD, Direction, type GoalType, type Needs, type Movement, type NPCBrain, type Position, type SocialState, } from '@dflike/shared'; import type { World } from '../ecs/World.js'; import type { GameMap } from '../map/GameMap.js'; import { findPath } from '../map/pathfinding.js'; import type { EventMemoryService } from '../llm/eventMemoryService.js'; function findNearestResource(map: GameMap, from: Position): Position | null { const tiles = map.resourceTiles; if (tiles.length === 0) return null; let best = tiles[0]; let bestDist = Math.abs(from.x - best.x) + Math.abs(from.y - best.y); for (let i = 1; i < tiles.length; i++) { const d = Math.abs(from.x - tiles[i].x) + Math.abs(from.y - tiles[i].y); if (d < bestDist) { best = tiles[i]; bestDist = d; } } return { x: best.x, y: best.y }; } function closestPOI(map: GameMap, from: Position, type: 'food' | 'rest'): Position | null { const pois = map.getPointsOfInterest(type); if (pois.length === 0) return null; let best = pois[0].position; let bestDist = Math.abs(from.x - best.x) + Math.abs(from.y - best.y); for (let i = 1; i < pois.length; i++) { const d = Math.abs(from.x - pois[i].position.x) + Math.abs(from.y - pois[i].position.y); if (d < bestDist) { best = pois[i].position; bestDist = d; } } return best; } function directionTo(from: Position, to: Position): number { const dx = to.x - from.x; const dy = to.y - from.y; if (Math.abs(dx) > Math.abs(dy)) return dx > 0 ? Direction.RIGHT : Direction.LEFT; return dy > 0 ? Direction.DOWN : Direction.UP; } export function npcBrainSystem(world: World, map: GameMap, eventMemoryService?: EventMemoryService): void { for (const entity of world.query('npcBrain', 'needs', 'movement', 'position')) { const brain = world.getComponent(entity, 'npcBrain')!; const needs = world.getComponent(entity, 'needs')!; const movement = world.getComponent(entity, 'movement')!; const pos = world.getComponent(entity, 'position')!; const socialState = world.getComponent(entity, 'socialState'); if (socialState && socialState.phase !== 'none') continue; // Skip if currently walking toward a goal if (movement.state === 'walking' && movement.path.length > 0) continue; // Check if at goal destination — recover needs if (brain.currentGoal === 'eat' && movement.state === 'idle') { const prevHunger = needs.hunger; needs.hunger = Math.min(100, needs.hunger + 20); if (eventMemoryService && prevHunger < HUNGER_THRESHOLD && needs.hunger >= HUNGER_THRESHOLD) { const name = world.getComponent(entity, 'name') ?? 'Unknown'; eventMemoryService.record(entity, { type: 'need_recovery', tick: 0, need: 'hunger', detail: `${name} satisfied their hunger`, }); } brain.currentGoal = null; } if (brain.currentGoal === 'rest' && movement.state === 'idle') { const prevEnergy = needs.energy; needs.energy = Math.min(100, needs.energy + 20); if (eventMemoryService && prevEnergy < ENERGY_THRESHOLD && needs.energy >= ENERGY_THRESHOLD) { const name = world.getComponent(entity, 'name') ?? 'Unknown'; eventMemoryService.record(entity, { type: 'need_recovery', tick: 0, need: 'energy', detail: `${name} recovered their energy`, }); } brain.currentGoal = null; } // Determine new goal based on needs priority const prevGoal = brain.currentGoal; let goal: GoalType = 'wander'; if (needs.energy < ENERGY_THRESHOLD) goal = 'rest'; else if (needs.hunger < HUNGER_THRESHOLD) goal = 'eat'; else if (needs.productivity < PRODUCTIVITY_THRESHOLD) goal = 'gather'; brain.currentGoal = goal; if (eventMemoryService && brain.currentGoal !== prevGoal && prevGoal !== null) { const name = world.getComponent(entity, 'name') ?? 'Unknown'; const goalLabels: Record = { wander: 'wandering', eat: 'looking for food', rest: 'looking for rest', gather: 'looking for resources' }; eventMemoryService.record(entity, { type: 'goal_change', tick: 0, oldGoal: prevGoal, newGoal: brain.currentGoal ?? 'idle', detail: `${name} started ${goalLabels[brain.currentGoal ?? ''] ?? brain.currentGoal}`, }); } // Pick target based on goal let target: Position | null = null; if (goal === 'eat') { target = closestPOI(map, pos, 'food'); } else if (goal === 'rest') { target = closestPOI(map, pos, 'rest'); } else if (goal === 'gather') { target = findNearestResource(map, pos); if (!target) { goal = 'wander'; brain.currentGoal = 'wander'; } } if (!target) { target = map.getRandomWalkable(); } // Pathfind to target const path = findPath(map, pos, target); if (path && path.length > 0) { movement.state = 'walking'; movement.target = target; movement.path = path; movement.direction = directionTo(pos, path[0]); } } }