import { describe, it, expect } from 'bun:test'; import { World } from '@common/rpg/core/world'; import { Stat } from '@common/rpg/components/stat'; import { Effect, EffectTemplate } from '@common/rpg/components/effect'; import { Equipment, Equippable } from '@common/rpg/components/equipment'; function world() { return new World(); } function makeSword(w: World, id = 'sword') { const sword = w.createEntity(id); sword.add(new Equippable('weapon')); return sword; } function makePlayer(w: World, slots: ConstructorParameters[0] = { slotName: 'weapon', type: 'weapon' }) { const player = w.createEntity('player'); player.add(new Stat({ value: 10 }), 'str'); player.add(new Equipment(slots)); return player; } describe('Equipment — equip', () => { it('equips item into a typed slot', () => { const w = world(); makeSword(w); const player = makePlayer(w); const result = player.get(Equipment)!.equip({ slotName: 'weapon', itemId: 'sword' }); expect(result).toBeTrue(); expect(player.get(Equipment)!.getItem('weapon')).toBe('sword'); }); it('returns false for unknown slot', () => { const w = world(); makeSword(w); const player = makePlayer(w); expect(player.get(Equipment)!.equip({ slotName: 'head', itemId: 'sword' })).toBeFalse(); }); it('returns false for missing item entity', () => { const w = world(); const player = makePlayer(w); expect(player.get(Equipment)!.equip({ slotName: 'weapon', itemId: 'ghost' })).toBeFalse(); }); it('returns false when item has no Equippable component', () => { const w = world(); w.createEntity('rock'); // no Equippable const player = makePlayer(w); expect(player.get(Equipment)!.equip({ slotName: 'weapon', itemId: 'rock' })).toBeFalse(); }); it('returns false when slot type does not match Equippable.slotType', () => { const w = world(); const helmet = w.createEntity('helmet'); helmet.add(new Equippable('armor')); const player = makePlayer(w); // slot type = 'weapon' expect(player.get(Equipment)!.equip({ slotName: 'weapon', itemId: 'helmet' })).toBeFalse(); }); it('generic slot (no type) accepts any Equippable', () => { const w = world(); makeSword(w); const player = w.createEntity('player'); player.add(new Equipment('slot1')); // generic slot expect(player.get(Equipment)!.equip({ slotName: 'slot1', itemId: 'sword' })).toBeTrue(); }); it('equipping occupied slot auto-unequips first', () => { const w = world(); makeSword(w, 'sword1'); makeSword(w, 'sword2'); const player = makePlayer(w); const eq = player.get(Equipment)!; eq.equip({ slotName: 'weapon', itemId: 'sword1' }); eq.equip({ slotName: 'weapon', itemId: 'sword2' }); expect(eq.getItem('weapon')).toBe('sword2'); }); it("emits 'equip' event", () => { const w = world(); makeSword(w); const player = makePlayer(w); const events: unknown[] = []; player.on('Equipment.equip', ({ data }) => events.push(data)); player.get(Equipment)!.equip({ slotName: 'weapon', itemId: 'sword' }); expect(events).toEqual([{ slotName: 'weapon', itemId: 'sword' }]); }); }); describe('Equipment — equip-scope effects', () => { it('clones equip-scope Effect onto owner on equip', () => { const w = world(); const sword = makeSword(w); sword.add(new Effect({ targetKey: 'str', delta: 5 })); // scope: equip (default) const player = makePlayer(w); player.get(Equipment)!.equip({ slotName: 'weapon', itemId: 'sword' }); expect(player.get(Stat, 'str')!.value).toBe(15); }); it('does NOT clone onHit-scope Effect onto owner on equip', () => { const w = world(); const sword = makeSword(w); sword.add(new EffectTemplate({ targetKey: 'str', delta: 99 })); const player = makePlayer(w); player.get(Equipment)!.equip({ slotName: 'weapon', itemId: 'sword' }); expect(player.get(Stat, 'str')!.value).toBe(10); // unaffected expect(player.getAll(Effect).length).toBe(0); }); it('clones multiple equip effects', () => { const w = world(); const sword = makeSword(w); sword.add(new Effect({ targetKey: 'str', delta: 3 })); sword.add(new Effect({ targetKey: 'str', delta: 7 })); const player = makePlayer(w); player.get(Equipment)!.equip({ slotName: 'weapon', itemId: 'sword' }); expect(player.get(Stat, 'str')!.value).toBe(20); }); it('equip + onHit: only equip effects reach owner', () => { const w = world(); const sword = makeSword(w); sword.add(new Effect({ targetKey: 'str', delta: 5 })); sword.add(new EffectTemplate({ targetKey: 'str', delta: 99 })); const player = makePlayer(w); player.get(Equipment)!.equip({ slotName: 'weapon', itemId: 'sword' }); expect(player.get(Stat, 'str')!.value).toBe(15); expect(player.getAll(Effect).length).toBe(1); }); }); describe('Equipment — unequip', () => { it('unequip reverts cloned effects', () => { const w = world(); const sword = makeSword(w); sword.add(new Effect({ targetKey: 'str', delta: 5 })); const player = makePlayer(w); const eq = player.get(Equipment)!; eq.equip({ slotName: 'weapon', itemId: 'sword' }); expect(player.get(Stat, 'str')!.value).toBe(15); eq.unequip('weapon'); expect(player.get(Stat, 'str')!.value).toBe(10); }); it('unequip clears slot itemId', () => { const w = world(); makeSword(w); const player = makePlayer(w); const eq = player.get(Equipment)!; eq.equip({ slotName: 'weapon', itemId: 'sword' }); eq.unequip('weapon'); expect(eq.getItem('weapon')).toBeNull(); }); it('unequip returns false on empty slot', () => { const w = world(); const player = makePlayer(w); expect(player.get(Equipment)!.unequip('weapon')).toBeFalse(); }); it("unequip emits 'unequip' event", () => { const w = world(); makeSword(w); const player = makePlayer(w); const eq = player.get(Equipment)!; eq.equip({ slotName: 'weapon', itemId: 'sword' }); const events: unknown[] = []; player.on('Equipment.unequip', ({ data }) => events.push(data)); eq.unequip('weapon'); expect(events).toEqual([{ slotName: 'weapon', itemId: 'sword' }]); }); }); describe('Equipment — queries', () => { it('getEquipped returns all filled slots', () => { const w = world(); makeSword(w, 'sword1'); makeSword(w, 'sword2'); const player = w.createEntity('player'); player.add(new Equipment( { slotName: 'main', type: 'weapon' }, { slotName: 'off', type: 'weapon' }, )); const eq = player.get(Equipment)!; eq.equip({ slotName: 'main', itemId: 'sword1' }); eq.equip({ slotName: 'off', itemId: 'sword2' }); const equipped = eq.getEquipped(); expect(equipped.length).toBe(2); expect(equipped.map(e => e.slotName).sort()).toEqual(['main', 'off']); }); it('findCompatibleSlot prefers typed slot over generic', () => { const w = world(); const player = w.createEntity('player'); player.add(new Equipment( 'generic', { slotName: 'weapon', type: 'weapon' }, )); const eq = player.get(Equipment)!; expect(eq.findCompatibleSlot('weapon')).toBe('weapon'); }); it('findCompatibleSlot falls back to generic slot', () => { const w = world(); const player = w.createEntity('player'); player.add(new Equipment('generic')); const eq = player.get(Equipment)!; expect(eq.findCompatibleSlot('weapon')).toBe('generic'); }); it('findCompatibleSlot returns null when no compatible slot', () => { const w = world(); const player = w.createEntity('player'); player.add(new Equipment({ slotName: 'armor', type: 'armor' })); const eq = player.get(Equipment)!; expect(eq.findCompatibleSlot('weapon')).toBeNull(); }); });