136 lines
4.2 KiB
TypeScript
136 lines
4.2 KiB
TypeScript
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<typeof Experience>[0]) {
|
|
const w = new World();
|
|
const e = w.createEntity();
|
|
e.add(new Experience(spec), 'xp');
|
|
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);
|
|
});
|
|
});
|