From 42b574259fe22f46b4bc1fb500fb6f6732048b50 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 7 Mar 2026 14:22:09 +0000 Subject: [PATCH] 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 --- server/src/systems/__tests__/systems.test.ts | 40 +++++++++++++++++++- server/src/systems/needsDecaySystem.ts | 7 +++- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/server/src/systems/__tests__/systems.test.ts b/server/src/systems/__tests__/systems.test.ts index 4a9581f..03c3440 100644 --- a/server/src/systems/__tests__/systems.test.ts +++ b/server/src/systems/__tests__/systems.test.ts @@ -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 = {}): 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(entity, 'stats', base); + world.addComponent(entity, 'statModifiers', { modifiers: [] }); +} + function addSocialState(world: World, entity: number, phase: SocialState['phase'] = 'none') { world.addComponent(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(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(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(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); diff --git a/server/src/systems/needsDecaySystem.ts b/server/src/systems/needsDecaySystem.ts index 9e17fee..d6c0fa3 100644 --- a/server/src/systems/needsDecaySystem.ts +++ b/server/src/systems/needsDecaySystem.ts @@ -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(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); } }