feat: add thirst decay to needsDecaySystem with crisis events

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-03-10 20:37:46 +00:00
parent b75f3a05e2
commit 27353e9ef3
2 changed files with 44 additions and 2 deletions

View File

@@ -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<Position>(e, 'position', { x, y });
world.addComponent<Needs>(e, 'needs', { hunger, energy, productivity: 80 });
world.addComponent<Needs>(e, 'needs', { hunger, energy, thirst: 80, productivity: 80 });
world.addComponent<Movement>(e, 'movement', { state: 'idle', target: null, path: [], direction: 0, moveProgress: 0 });
world.addComponent<NPCBrain>(e, 'npcBrain', { currentGoal: null, goalQueue: [] });
return e;
@@ -139,6 +139,36 @@ describe('needsDecaySystem', () => {
const needs = world.getComponent<Needs>(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<Needs>(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<Needs>(e, 'needs')!;
needs.thirst = 50;
const brain = world.getComponent<NPCBrain>(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<Needs>(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', () => {

View File

@@ -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<string>(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<string>(entity, 'name') ?? 'Unknown';
eventMemoryService.record(entity, {