import { describe, it, expect } from 'bun:test'; import { World } from '@common/rpg/core/world'; import { Stat, Health } from '@common/rpg/components/stat'; function world() { return new World(); } describe('Stat — value / base / modifiers', () => { it('value equals base when no modifiers', () => { const w = world(); const e = w.createEntity(); e.add(new Stat({ value: 10 }), 's'); const s = e.get(Stat, 's')!; expect(s.value).toBe(10); expect(s.base).toBe(10); }); it('set() changes base and value', () => { const w = world(); const e = w.createEntity(); e.add(new Stat({ value: 5 }), 's'); const s = e.get(Stat, 's')!; s.set(20); expect(s.base).toBe(20); expect(s.value).toBe(20); }); it('update() adds to base', () => { const w = world(); const e = w.createEntity(); e.add(new Stat({ value: 10 }), 's'); const s = e.get(Stat, 's')!; s.update(5); expect(s.value).toBe(15); }); it('applyModifier shifts value without changing base', () => { const w = world(); const e = w.createEntity(); e.add(new Stat({ value: 10 }), 's'); const s = e.get(Stat, 's')!; s.applyModifier(5); expect(s.base).toBe(10); expect(s.value).toBe(15); }); it('removeModifier reverts applyModifier', () => { const w = world(); const e = w.createEntity(); e.add(new Stat({ value: 10 }), 's'); const s = e.get(Stat, 's')!; s.applyModifier(5); s.removeModifier(5); expect(s.value).toBe(10); }); it('multiple modifiers stack', () => { const w = world(); const e = w.createEntity(); e.add(new Stat({ value: 10 }), 's'); const s = e.get(Stat, 's')!; s.applyModifier(3); s.applyModifier(7); expect(s.value).toBe(20); }); it('modifier on max field shifts effective max', () => { const w = world(); const e = w.createEntity(); e.add(new Stat({ value: 10, max: 20 }), 's'); const s = e.get(Stat, 's')!; s.applyModifier(10, 'max'); expect(s.max).toBe(30); }); it('modifier on min field shifts effective min', () => { const w = world(); const e = w.createEntity(); e.add(new Stat({ value: 10, min: 0 }), 's'); const s = e.get(Stat, 's')!; s.applyModifier(5, 'min'); expect(s.min).toBe(5); }); it('value is clamped to [min, max]', () => { const w = world(); const e = w.createEntity(); e.add(new Stat({ value: 10, min: 0, max: 20 }), 's'); const s = e.get(Stat, 's')!; s.set(100); expect(s.value).toBe(20); s.set(-5); expect(s.value).toBe(0); }); it('large positibe modifier clamps to max', () => { const w = world(); const e = w.createEntity(); e.add(new Stat({ value: 10, max: 20 }), 's'); const s = e.get(Stat, 's')!; s.applyModifier(100); expect(s.value).toBe(20); }); it('large negative modifier clamps to min', () => { const w = world(); const e = w.createEntity(); e.add(new Stat({ value: 10, min: 0 }), 's'); const s = e.get(Stat, 's')!; s.applyModifier(-100); expect(s.value).toBe(0); }); it("set() emits 'set' event with prev and value", () => { const w = world(); const e = w.createEntity(); e.add(new Stat({ value: 10 }), 's'); const s = e.get(Stat, 's')!; const events: unknown[] = []; e.on('Stat(s).set', ({ data }) => events.push(data)); s.set(20); expect(events).toEqual([{ prev: 10, value: 20 }]); }); it("set() does not emit 'set' when value unchanged", () => { const w = world(); const e = w.createEntity(); e.add(new Stat({ value: 10, min: 0 }), 's'); const s = e.get(Stat, 's')!; const events: unknown[] = []; e.on('Stat(s).set', ({ data }) => events.push(data)); s.set(-100); // clamped to 0, still changes s.set(-200); // still 0, no change expect(events.length).toBe(1); }); }); describe('Health', () => { it('update reduces health', () => { const w = world(); const e = w.createEntity(); e.add(new Health({ value: 100, min: 0 })); const h = e.get(Health)!; h.update(-30); expect(h.value).toBe(70); }); it('update to zero triggers kill()', () => { const w = world(); const e = w.createEntity(); e.add(new Health({ value: 10, min: 0 })); const h = e.get(Health)!; const killed: unknown[] = []; e.on('Health.killed', () => killed.push(true)); h.update(-10); expect(killed.length).toBe(1); expect(h.value).toBe(0); }); it('kill() emits killed and sets value to 0', () => { const w = world(); const e = w.createEntity(); e.add(new Health({ value: 50, min: 0 })); const h = e.get(Health)!; const killed: unknown[] = []; e.on('Health.killed', () => killed.push(true)); h.kill(); expect(killed.length).toBe(1); expect(h.value).toBe(0); }); it('kill() does not emit killed twice for overkill', () => { const w = world(); const e = w.createEntity(); e.add(new Health({ value: 10, min: 0 })); const h = e.get(Health)!; const killed: unknown[] = []; e.on('Health.killed', () => killed.push(true)); h.update(-999); expect(killed.length).toBe(1); }); it('kill() does not emit killed twice for already killed entity', () => { const w = world(); const e = w.createEntity(); e.add(new Health({ value: 10, min: 0 })); const h = e.get(Health)!; const killed: unknown[] = []; e.on('Health.killed', () => killed.push(true)); h.update(-999); h.update(-999); expect(killed.length).toBe(1); }); });