feat(needs): add productivity decay to needsDecaySystem

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-03-08 22:46:57 +00:00
parent 4670bfc08b
commit e1edbd47c6
2 changed files with 32 additions and 3 deletions
+20 -2
View File
@@ -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<Position>(e, 'position', { x, y });
world.addComponent<Needs>(e, 'needs', { hunger, energy });
world.addComponent<Needs>(e, 'needs', { hunger, energy, 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;
@@ -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<Needs>(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<Needs>(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);
+12 -1
View File
@@ -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<string>(entity, 'name') ?? 'Unknown';
eventMemoryService.record(entity, {
type: 'need_crisis',
tick: 0,
need: 'productivity',
detail: `${name} felt unproductive`,
});
}
}
}
}