diff --git a/server/src/systems/__tests__/systems.test.ts b/server/src/systems/__tests__/systems.test.ts index b22e3df..7a613dd 100644 --- a/server/src/systems/__tests__/systems.test.ts +++ b/server/src/systems/__tests__/systems.test.ts @@ -5,12 +5,12 @@ import { needsDecaySystem } from '../needsDecaySystem.js'; import { npcBrainSystem } from '../npcBrainSystem.js'; import { movementSystem } from '../movementSystem.js'; import type { Needs, Movement, NPCBrain, Position, SocialState, Stats, StatModifiers } from '@dflike/shared'; -import { HUNGER_DECAY_PER_TICK, ENERGY_DECAY_PER_TICK, MOVE_SPEED } from '@dflike/shared'; +import { HUNGER_DECAY_PER_TICK, ENERGY_DECAY_PER_TICK, PRODUCTIVITY_DECAY_PER_TICK, MOVE_SPEED } from '@dflike/shared'; 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 }); + world.addComponent(e, 'needs', { hunger, energy, productivity: 80 }); world.addComponent(e, 'movement', { state: 'idle', target: null, path: [], direction: 0, moveProgress: 0 }); world.addComponent(e, 'npcBrain', { currentGoal: null, goalQueue: [] }); return e; @@ -79,6 +79,24 @@ describe('needsDecaySystem', () => { expect(needs.hunger).toBeCloseTo(50 - HUNGER_DECAY_PER_TICK); }); + it('decays productivity each tick', () => { + const world = new World(); + const e = createNPC(world, 0, 0, 50, 50); + needsDecaySystem(world); + const needs = world.getComponent(e, 'needs')!; + expect(needs.productivity).toBeCloseTo(80 - PRODUCTIVITY_DECAY_PER_TICK); + }); + + it('productivity decay is not affected by constitution', () => { + const world = new World(); + const e = createNPC(world, 0, 0, 50, 50); + addStats(world, e, { constitution: 18 }); + needsDecaySystem(world); + const needs = world.getComponent(e, 'needs')!; + // Same decay regardless of constitution + expect(needs.productivity).toBeCloseTo(80 - PRODUCTIVITY_DECAY_PER_TICK); + }); + it('clamps needs at 0', () => { const world = new World(); const e = createNPC(world, 0, 0, 0.01, 0.01); diff --git a/server/src/systems/needsDecaySystem.ts b/server/src/systems/needsDecaySystem.ts index 59a36c2..3b141a3 100644 --- a/server/src/systems/needsDecaySystem.ts +++ b/server/src/systems/needsDecaySystem.ts @@ -1,4 +1,4 @@ -import { HUNGER_DECAY_PER_TICK, ENERGY_DECAY_PER_TICK, HUNGER_THRESHOLD, ENERGY_THRESHOLD, type Needs } from '@dflike/shared'; +import { HUNGER_DECAY_PER_TICK, ENERGY_DECAY_PER_TICK, PRODUCTIVITY_DECAY_PER_TICK, HUNGER_THRESHOLD, ENERGY_THRESHOLD, PRODUCTIVITY_THRESHOLD, type Needs } from '@dflike/shared'; import type { World } from '../ecs/World.js'; import { getEffectiveStat } from './statHelpers.js'; import type { EventMemoryService } from '../llm/eventMemoryService.js'; @@ -10,8 +10,10 @@ export function needsDecaySystem(world: World, eventMemoryService?: EventMemoryS const conMultiplier = 1 - (con - 10) * 0.03; const prevHunger = needs.hunger; const prevEnergy = needs.energy; + const prevProductivity = needs.productivity; needs.hunger = Math.max(0, needs.hunger - HUNGER_DECAY_PER_TICK * conMultiplier); needs.energy = Math.max(0, needs.energy - ENERGY_DECAY_PER_TICK * conMultiplier); + needs.productivity = Math.max(0, needs.productivity - PRODUCTIVITY_DECAY_PER_TICK); if (eventMemoryService) { if (prevHunger >= HUNGER_THRESHOLD && needs.hunger < HUNGER_THRESHOLD) { @@ -32,6 +34,15 @@ export function needsDecaySystem(world: World, eventMemoryService?: EventMemoryS detail: `${name} became exhausted`, }); } + if (prevProductivity >= PRODUCTIVITY_THRESHOLD && needs.productivity < PRODUCTIVITY_THRESHOLD) { + const name = world.getComponent(entity, 'name') ?? 'Unknown'; + eventMemoryService.record(entity, { + type: 'need_crisis', + tick: 0, + need: 'productivity', + detail: `${name} felt unproductive`, + }); + } } } }