diff --git a/server/src/systems/__tests__/systems.test.ts b/server/src/systems/__tests__/systems.test.ts index b8b2cfc..65652c3 100644 --- a/server/src/systems/__tests__/systems.test.ts +++ b/server/src/systems/__tests__/systems.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; import { World } from '../../ecs/World.js'; import { GameMap } from '../../map/GameMap.js'; import { needsDecaySystem } from '../needsDecaySystem.js'; @@ -13,7 +13,7 @@ const rc = createRuntimeConstants(); function createNPC(world: World, x: number, y: number, hunger = 80, energy = 80) { const e = world.createEntity(); world.addComponent(e, 'position', { x, y }); - world.addComponent(e, 'needs', { hunger, energy, productivity: 80 }); + world.addComponent(e, 'needs', { hunger, energy, thirst: 80, productivity: 80 }); world.addComponent(e, 'movement', { state: 'idle', target: null, path: [], direction: 0, moveProgress: 0 }); world.addComponent(e, 'npcBrain', { currentGoal: null, goalQueue: [] }); return e; @@ -139,6 +139,36 @@ describe('needsDecaySystem', () => { const needs = world.getComponent(e, 'needs')!; expect(needs.energy).toBe(100); }); + + it('decays thirst each tick', () => { + const world = new World(); + const e = createNPC(world, 0, 0, 50, 50); + const needs = world.getComponent(e, 'needs')!; + needs.thirst = 50; + needsDecaySystem(world, undefined, rc); + expect(needs.thirst).toBeCloseTo(50 - rc.get('THIRST_DECAY_PER_TICK')); + }); + + it('decays thirst at half rate during sleep', () => { + const world = new World(); + const e = createNPC(world, 0, 0, 50, 50); + const needs = world.getComponent(e, 'needs')!; + needs.thirst = 50; + const brain = world.getComponent(e, 'npcBrain')!; + brain.currentGoal = 'sleep'; + needsDecaySystem(world, undefined, rc); + expect(needs.thirst).toBeCloseTo(50 - rc.get('THIRST_DECAY_PER_TICK') * rc.get('SLEEP_HUNGER_DECAY_MULTIPLIER')); + }); + + it('fires thirst crisis event when crossing threshold', () => { + const world = new World(); + const e = createNPC(world, 0, 0, 50, 50); + const needs = world.getComponent(e, 'needs')!; + needs.thirst = rc.get('THIRST_THRESHOLD') + 0.01; + const mockEMS = { record: vi.fn() }; + needsDecaySystem(world, mockEMS as any, rc); + expect(mockEMS.record).toHaveBeenCalledWith(e, expect.objectContaining({ type: 'need_crisis', need: 'thirst' })); + }); }); describe('npcBrainSystem', () => { diff --git a/server/src/systems/needsDecaySystem.ts b/server/src/systems/needsDecaySystem.ts index 6c86e6b..68e07a3 100644 --- a/server/src/systems/needsDecaySystem.ts +++ b/server/src/systems/needsDecaySystem.ts @@ -10,6 +10,7 @@ export function needsDecaySystem(world: World, eventMemoryService: EventMemorySe const con = getEffectiveStat(world, entity, 'constitution'); const conMultiplier = 1 - (con - 10) * 0.03; const prevHunger = needs.hunger; + const prevThirst = needs.thirst; const prevEnergy = needs.energy; const prevProductivity = needs.productivity; @@ -19,9 +20,11 @@ export function needsDecaySystem(world: World, eventMemoryService: EventMemorySe if (isSleeping) { needs.energy = Math.min(100, needs.energy + rc.get('SLEEP_ENERGY_RECOVERY_PER_TICK')); needs.hunger = Math.max(0, needs.hunger - rc.get('HUNGER_DECAY_PER_TICK') * conMultiplier * rc.get('SLEEP_HUNGER_DECAY_MULTIPLIER')); + needs.thirst = Math.max(0, needs.thirst - rc.get('THIRST_DECAY_PER_TICK') * conMultiplier * rc.get('SLEEP_HUNGER_DECAY_MULTIPLIER')); } else { needs.energy = Math.max(0, needs.energy - rc.get('ENERGY_DECAY_PER_TICK') * conMultiplier); needs.hunger = Math.max(0, needs.hunger - rc.get('HUNGER_DECAY_PER_TICK') * conMultiplier); + needs.thirst = Math.max(0, needs.thirst - rc.get('THIRST_DECAY_PER_TICK') * conMultiplier); } needs.productivity = Math.max(0, needs.productivity - rc.get('PRODUCTIVITY_DECAY_PER_TICK')); @@ -44,6 +47,15 @@ export function needsDecaySystem(world: World, eventMemoryService: EventMemorySe detail: `${name} became exhausted`, }); } + if (prevThirst >= rc.get('THIRST_THRESHOLD') && needs.thirst < rc.get('THIRST_THRESHOLD')) { + const name = world.getComponent(entity, 'name') ?? 'Unknown'; + eventMemoryService.record(entity, { + type: 'need_crisis', + tick: 0, + need: 'thirst', + detail: `${name} is getting dehydrated`, + }); + } if (prevProductivity >= rc.get('PRODUCTIVITY_THRESHOLD') && needs.productivity < rc.get('PRODUCTIVITY_THRESHOLD')) { const name = world.getComponent(entity, 'name') ?? 'Unknown'; eventMemoryService.record(entity, {