feat: integrate constitution into needs decay rates

High constitution slows hunger/energy decay, low constitution speeds it.
Uses getEffectiveStat helper which defaults to 10 for entities without
stats, preserving backward compatibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-03-07 14:22:09 +00:00
parent c5d75fa0b0
commit 42b574259f
2 changed files with 44 additions and 3 deletions
+39 -1
View File
@@ -4,7 +4,7 @@ import { GameMap } from '../../map/GameMap.js';
import { needsDecaySystem } from '../needsDecaySystem.js';
import { npcBrainSystem } from '../npcBrainSystem.js';
import { movementSystem } from '../movementSystem.js';
import type { Needs, Movement, NPCBrain, Position, SocialState } from '@dflike/shared';
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';
function createNPC(world: World, x: number, y: number, hunger = 80, energy = 80) {
@@ -16,6 +16,16 @@ function createNPC(world: World, x: number, y: number, hunger = 80, energy = 80)
return e;
}
function addStats(world: World, entity: number, overrides: Partial<Stats> = {}): void {
const base: Stats = {
strength: 10, dexterity: 10, constitution: 10, intelligence: 10, perception: 10,
sociability: 10, courage: 10, curiosity: 10, empathy: 10, temperament: 10,
...overrides,
};
world.addComponent<Stats>(entity, 'stats', base);
world.addComponent<StatModifiers>(entity, 'statModifiers', { modifiers: [] });
}
function addSocialState(world: World, entity: number, phase: SocialState['phase'] = 'none') {
world.addComponent<SocialState>(entity, 'socialState', {
phase,
@@ -37,6 +47,34 @@ describe('needsDecaySystem', () => {
expect(needs.energy).toBeCloseTo(50 - ENERGY_DECAY_PER_TICK);
});
it('high constitution slows hunger decay', () => {
const world = new World();
const e = createNPC(world, 0, 0, 50, 50);
addStats(world, e, { constitution: 14 });
needsDecaySystem(world);
const needs = world.getComponent<Needs>(e, 'needs')!;
const expectedDecay = HUNGER_DECAY_PER_TICK * (1 - (14 - 10) * 0.03);
expect(needs.hunger).toBeCloseTo(50 - expectedDecay);
});
it('low constitution speeds hunger decay', () => {
const world = new World();
const e = createNPC(world, 0, 0, 50, 50);
addStats(world, e, { constitution: 6 });
needsDecaySystem(world);
const needs = world.getComponent<Needs>(e, 'needs')!;
const expectedDecay = HUNGER_DECAY_PER_TICK * (1 - (6 - 10) * 0.03);
expect(needs.hunger).toBeCloseTo(50 - expectedDecay);
});
it('uses default decay when entity has no stats', () => {
const world = new World();
const e = createNPC(world, 0, 0, 50, 50);
needsDecaySystem(world);
const needs = world.getComponent<Needs>(e, 'needs')!;
expect(needs.hunger).toBeCloseTo(50 - HUNGER_DECAY_PER_TICK);
});
it('clamps needs at 0', () => {
const world = new World();
const e = createNPC(world, 0, 0, 0.01, 0.01);
+5 -2
View File
@@ -1,10 +1,13 @@
import { HUNGER_DECAY_PER_TICK, ENERGY_DECAY_PER_TICK, type Needs } from '@dflike/shared';
import type { World } from '../ecs/World.js';
import { getEffectiveStat } from './statHelpers.js';
export function needsDecaySystem(world: World): void {
for (const entity of world.query('needs')) {
const needs = world.getComponent<Needs>(entity, 'needs')!;
needs.hunger = Math.max(0, needs.hunger - HUNGER_DECAY_PER_TICK);
needs.energy = Math.max(0, needs.energy - ENERGY_DECAY_PER_TICK);
const con = getEffectiveStat(world, entity, 'constitution');
const conMultiplier = 1 - (con - 10) * 0.03;
needs.hunger = Math.max(0, needs.hunger - HUNGER_DECAY_PER_TICK * conMultiplier);
needs.energy = Math.max(0, needs.energy - ENERGY_DECAY_PER_TICK * conMultiplier);
}
}