import { describe, it, expect } from 'bun:test'; import { World } from '@common/rpg/core/world'; import { Experience } from '@common/rpg/components/experience'; function withXp(spec: ConstructorParameters[0]) { const w = new World(); const e = w.createEntity(); e.add('xp', new Experience(spec)); return { e, xp: e.get(Experience, 'xp')! }; } describe('Experience — initial state', () => { it('starts at level 1, xp 0', () => { const { xp } = withXp([100, 200, 400]); expect(xp.level).toBe(1); expect(xp.xp).toBe(0); expect(xp.xpInLevel).toBe(0); }); it('progress is 0 at start', () => { const { xp } = withXp([100]); expect(xp.progress).toBe(0); }); it('xpToNext equals first threshold', () => { const { xp } = withXp([100, 300]); expect(xp.xpToNext).toBe(100); }); }); describe('Experience — award XP (array spec)', () => { it('accumulates xp without leveling up', () => { const { xp } = withXp([100]); xp.award(50); expect(xp.xp).toBe(50); expect(xp.level).toBe(1); expect(xp.xpInLevel).toBe(50); }); it('levels up when xp meets threshold', () => { const { xp } = withXp([100]); xp.award(100); expect(xp.level).toBe(2); expect(xp.xpInLevel).toBe(0); }); it("emits 'levelup' with prev and new level", () => { const { e, xp } = withXp([100]); const events: unknown[] = []; e.on('Experience(xp).levelup', ({ data }) => events.push(data)); xp.award(100); expect(events).toEqual([{ level: 2, prev: 1 }]); }); it('handles multiple level-ups in a single award', () => { const { xp } = withXp([100, 200, 400]); xp.award(350); // 100 (L1→2) + 200 (L2→3) + 50 leftover expect(xp.level).toBe(3); expect(xp.xpInLevel).toBe(50); }); it("emits 'levelup' for each level gained", () => { const { e, xp } = withXp([100, 200]); const events: unknown[] = []; e.on('Experience(xp).levelup', ({ data }) => events.push(data)); xp.award(300); // gains 2 levels expect(events.length).toBe(2); expect((events[0] as any).level).toBe(2); expect((events[1] as any).level).toBe(3); }); it('stops leveling at max level (array spec)', () => { const { xp } = withXp([100]); // max level = 2 xp.award(9999); expect(xp.level).toBe(2); }); it('xpToNext is null at max level', () => { const { xp } = withXp([100]); xp.award(9999); expect(xp.xpToNext).toBeNull(); }); it('progress is 1 at max level', () => { const { xp } = withXp([100]); xp.award(9999); expect(xp.progress).toBe(1); }); it('xp.total never resets across levels', () => { const { xp } = withXp([100, 200]); xp.award(400); expect(xp.xp).toBe(400); }); }); describe('Experience — award XP (geometric spec)', () => { it('levels up using geometric thresholds', () => { // base=100, factor=2 → L1→L2: 100xp, L2→L3: 200xp const { xp } = withXp({ base: 100, factor: 2 }); xp.award(100); expect(xp.level).toBe(2); xp.award(200); expect(xp.level).toBe(3); }); it('has no max level with geometric spec', () => { const { xp } = withXp({ base: 10, factor: 1 }); // 10xp per level, forever xp.award(10000); expect(xp.level).toBeGreaterThan(100); expect(xp.xpToNext).not.toBeNull(); }); it('progress tracks correctly within a level', () => { const { xp } = withXp({ base: 100, factor: 2 }); // 100xp for L1→2 xp.award(40); expect(xp.progress).toBeCloseTo(0.4); }); }); describe('Experience — xpInLevel / xpToNext', () => { it('xpInLevel resets after level-up', () => { const { xp } = withXp([100, 200]); xp.award(150); // levels up at 100, 50 left expect(xp.xpInLevel).toBe(50); expect(xp.xpToNext).toBe(150); // 200 - 50 }); it('xpToNext decreases as xp accumulates', () => { const { xp } = withXp([100]); expect(xp.xpToNext).toBe(100); xp.award(30); expect(xp.xpToNext).toBe(70); }); });