219 lines
8.4 KiB
TypeScript
219 lines
8.4 KiB
TypeScript
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('equippable', new Equippable('weapon'));
|
|
return sword;
|
|
}
|
|
|
|
function makePlayer(w: World, slots: ConstructorParameters<typeof Equipment>[0] = { slotName: 'weapon', type: 'weapon' }) {
|
|
const player = w.createEntity('player');
|
|
player.add('str', new Stat({ value: 10 }));
|
|
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('equippable', 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('equipment', 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('bonus', new Effect({ targetStat: '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('burn', new EffectTemplate({ targetStat: '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('a', new Effect({ targetStat: 'str', delta: 3 }));
|
|
sword.add('b', new Effect({ targetStat: '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('passive', new Effect({ targetStat: 'str', delta: 5 }));
|
|
sword.add('burn', new EffectTemplate({ targetStat: '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('bonus', new Effect({ targetStat: '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('equipment', 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('equipment', 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('equipment', 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('equipment', new Equipment({ slotName: 'armor', type: 'armor' }));
|
|
const eq = player.get(Equipment)!;
|
|
expect(eq.findCompatibleSlot('weapon')).toBeNull();
|
|
});
|
|
});
|