diff --git a/server/src/systems/__tests__/statHelpers.test.ts b/server/src/systems/__tests__/statHelpers.test.ts new file mode 100644 index 0000000..f940374 --- /dev/null +++ b/server/src/systems/__tests__/statHelpers.test.ts @@ -0,0 +1,82 @@ +import { describe, it, expect } from 'vitest'; +import { World } from '../../ecs/World.js'; +import { getEffectiveStat } from '../statHelpers.js'; +import type { Stats, StatModifiers } from '@dflike/shared'; + +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); +} + +describe('getEffectiveStat', () => { + it('returns base stat when no modifiers', () => { + const world = new World(); + const e = world.createEntity(); + addStats(world, e, { strength: 14 }); + expect(getEffectiveStat(world, e, 'strength')).toBe(14); + }); + + it('adds positive modifier', () => { + const world = new World(); + const e = world.createEntity(); + addStats(world, e, { empathy: 12 }); + world.addComponent(e, 'statModifiers', { + modifiers: [{ stat: 'empathy', value: 2, remaining: 50 }], + }); + expect(getEffectiveStat(world, e, 'empathy')).toBe(14); + }); + + it('adds negative modifier', () => { + const world = new World(); + const e = world.createEntity(); + addStats(world, e, { intelligence: 10 }); + world.addComponent(e, 'statModifiers', { + modifiers: [{ stat: 'intelligence', value: -2, remaining: 50 }], + }); + expect(getEffectiveStat(world, e, 'intelligence')).toBe(8); + }); + + it('clamps to minimum 3', () => { + const world = new World(); + const e = world.createEntity(); + addStats(world, e, { dexterity: 4 }); + world.addComponent(e, 'statModifiers', { + modifiers: [{ stat: 'dexterity', value: -5, remaining: 50 }], + }); + expect(getEffectiveStat(world, e, 'dexterity')).toBe(3); + }); + + it('clamps to maximum 18', () => { + const world = new World(); + const e = world.createEntity(); + addStats(world, e, { strength: 17 }); + world.addComponent(e, 'statModifiers', { + modifiers: [{ stat: 'strength', value: 5, remaining: 50 }], + }); + expect(getEffectiveStat(world, e, 'strength')).toBe(18); + }); + + it('sums multiple modifiers for same stat', () => { + const world = new World(); + const e = world.createEntity(); + addStats(world, e, { sociability: 10 }); + world.addComponent(e, 'statModifiers', { + modifiers: [ + { stat: 'sociability', value: 2, remaining: 50 }, + { stat: 'sociability', value: -1, remaining: 30 }, + ], + }); + expect(getEffectiveStat(world, e, 'sociability')).toBe(11); + }); + + it('floors float base stats to integer', () => { + const world = new World(); + const e = world.createEntity(); + addStats(world, e, { courage: 10.7 }); + expect(getEffectiveStat(world, e, 'courage')).toBe(10); + }); +}); diff --git a/server/src/systems/statHelpers.ts b/server/src/systems/statHelpers.ts new file mode 100644 index 0000000..b5ce1ed --- /dev/null +++ b/server/src/systems/statHelpers.ts @@ -0,0 +1,15 @@ +import type { Stats, StatName, StatModifiers } from '@dflike/shared'; +import type { World } from '../ecs/World.js'; + +export function getEffectiveStat(world: World, entity: number, stat: StatName): number { + const base = world.getComponent(entity, 'stats'); + if (!base) return 10; // fallback for entities without stats + let value = base[stat]; + const mods = world.getComponent(entity, 'statModifiers'); + if (mods) { + for (const m of mods.modifiers) { + if (m.stat === stat) value += m.value; + } + } + return Math.max(3, Math.min(18, Math.floor(value))); +}