feat: add statModifierSystem with needs-based modifiers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { World } from '../../ecs/World.js';
|
||||
import { statModifierSystem } from '../statModifierSystem.js';
|
||||
import type { Stats, StatModifiers, Needs } from '@dflike/shared';
|
||||
|
||||
function setupEntity(
|
||||
world: World,
|
||||
statsOverrides: Partial<Stats> = {},
|
||||
needs?: Partial<Needs>,
|
||||
modifiers: StatModifiers['modifiers'] = [],
|
||||
): number {
|
||||
const e = world.createEntity();
|
||||
const base: Stats = {
|
||||
strength: 10, dexterity: 10, constitution: 10, intelligence: 10, perception: 10,
|
||||
sociability: 10, courage: 10, curiosity: 10, empathy: 10, temperament: 10,
|
||||
...statsOverrides,
|
||||
};
|
||||
world.addComponent<Stats>(e, 'stats', base);
|
||||
world.addComponent<StatModifiers>(e, 'statModifiers', { modifiers: [...modifiers] });
|
||||
if (needs) {
|
||||
world.addComponent<Needs>(e, 'needs', { hunger: 80, energy: 80, ...needs });
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
describe('statModifierSystem', () => {
|
||||
describe('modifier decay', () => {
|
||||
it('decrements remaining on each modifier', () => {
|
||||
const world = new World();
|
||||
const e = setupEntity(world, {}, undefined, [
|
||||
{ stat: 'strength', value: 2, remaining: 10 },
|
||||
]);
|
||||
statModifierSystem(world);
|
||||
const mods = world.getComponent<StatModifiers>(e, 'statModifiers')!;
|
||||
expect(mods.modifiers[0].remaining).toBe(9);
|
||||
});
|
||||
|
||||
it('removes expired modifiers (remaining reaches 0)', () => {
|
||||
const world = new World();
|
||||
const e = setupEntity(world, {}, undefined, [
|
||||
{ stat: 'strength', value: 2, remaining: 1 },
|
||||
{ stat: 'dexterity', value: -1, remaining: 5 },
|
||||
]);
|
||||
statModifierSystem(world);
|
||||
const mods = world.getComponent<StatModifiers>(e, 'statModifiers')!;
|
||||
expect(mods.modifiers).toHaveLength(1);
|
||||
expect(mods.modifiers[0].stat).toBe('dexterity');
|
||||
});
|
||||
|
||||
it('does not decrement permanent modifiers (remaining = -1)', () => {
|
||||
const world = new World();
|
||||
const e = setupEntity(world, {}, undefined, [
|
||||
{ stat: 'intelligence', value: -2, remaining: -1 },
|
||||
]);
|
||||
statModifierSystem(world);
|
||||
const mods = world.getComponent<StatModifiers>(e, 'statModifiers')!;
|
||||
expect(mods.modifiers[0].remaining).toBe(-1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('needs-based modifiers', () => {
|
||||
it('applies intelligence -2 and sociability -1 when hunger < 30', () => {
|
||||
const world = new World();
|
||||
const e = setupEntity(world, {}, { hunger: 20, energy: 80 });
|
||||
statModifierSystem(world);
|
||||
const mods = world.getComponent<StatModifiers>(e, 'statModifiers')!;
|
||||
const intMod = mods.modifiers.find(m => m.stat === 'intelligence' && m.remaining === -1);
|
||||
const socMod = mods.modifiers.find(m => m.stat === 'sociability' && m.remaining === -1);
|
||||
expect(intMod?.value).toBe(-2);
|
||||
expect(socMod?.value).toBe(-1);
|
||||
});
|
||||
|
||||
it('applies dexterity -2 and temperament -2 when energy < 20', () => {
|
||||
const world = new World();
|
||||
const e = setupEntity(world, {}, { hunger: 80, energy: 10 });
|
||||
statModifierSystem(world);
|
||||
const mods = world.getComponent<StatModifiers>(e, 'statModifiers')!;
|
||||
const dexMod = mods.modifiers.find(m => m.stat === 'dexterity' && m.remaining === -1);
|
||||
const tempMod = mods.modifiers.find(m => m.stat === 'temperament' && m.remaining === -1);
|
||||
expect(dexMod?.value).toBe(-2);
|
||||
expect(tempMod?.value).toBe(-2);
|
||||
});
|
||||
|
||||
it('removes needs-based modifiers when needs recover', () => {
|
||||
const world = new World();
|
||||
const e = setupEntity(world, {}, { hunger: 20, energy: 80 });
|
||||
statModifierSystem(world);
|
||||
// Now hunger recovers
|
||||
const needs = world.getComponent<Needs>(e, 'needs')!;
|
||||
needs.hunger = 80;
|
||||
statModifierSystem(world);
|
||||
const mods = world.getComponent<StatModifiers>(e, 'statModifiers')!;
|
||||
const needsMods = mods.modifiers.filter(m => m.remaining === -1);
|
||||
expect(needsMods).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('does not stack needs-based modifiers across ticks', () => {
|
||||
const world = new World();
|
||||
const e = setupEntity(world, {}, { hunger: 20, energy: 80 });
|
||||
statModifierSystem(world);
|
||||
statModifierSystem(world);
|
||||
statModifierSystem(world);
|
||||
const mods = world.getComponent<StatModifiers>(e, 'statModifiers')!;
|
||||
const intMods = mods.modifiers.filter(m => m.stat === 'intelligence' && m.remaining === -1);
|
||||
expect(intMods).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { StatModifiers, Needs, StatModifier } from '@dflike/shared';
|
||||
import type { World } from '../ecs/World.js';
|
||||
import { HUNGER_THRESHOLD, ENERGY_THRESHOLD } from '@dflike/shared';
|
||||
|
||||
function applyNeedsModifiers(needs: Needs, modifiers: StatModifier[]): StatModifier[] {
|
||||
// Remove all existing permanent (needs-based) modifiers
|
||||
const filtered = modifiers.filter(m => m.remaining !== -1);
|
||||
|
||||
// Apply new needs-based modifiers
|
||||
if (needs.hunger < HUNGER_THRESHOLD) {
|
||||
filtered.push({ stat: 'intelligence', value: -2, remaining: -1 });
|
||||
filtered.push({ stat: 'sociability', value: -1, remaining: -1 });
|
||||
}
|
||||
if (needs.energy < ENERGY_THRESHOLD) {
|
||||
filtered.push({ stat: 'dexterity', value: -2, remaining: -1 });
|
||||
filtered.push({ stat: 'temperament', value: -2, remaining: -1 });
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
export function statModifierSystem(world: World): void {
|
||||
for (const entity of world.query('stats', 'statModifiers')) {
|
||||
const mods = world.getComponent<StatModifiers>(entity, 'statModifiers')!;
|
||||
|
||||
// Decay timed modifiers and remove expired
|
||||
mods.modifiers = mods.modifiers.filter(m => {
|
||||
if (m.remaining === -1) return true; // permanent, handled below
|
||||
m.remaining--;
|
||||
return m.remaining > 0;
|
||||
});
|
||||
|
||||
// Apply needs-based modifiers if entity has needs
|
||||
const needs = world.getComponent<Needs>(entity, 'needs');
|
||||
if (needs) {
|
||||
mods.modifiers = applyNeedsModifiers(needs, mods.modifiers);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user