264 lines
10 KiB
TypeScript
264 lines
10 KiB
TypeScript
import { describe, it, expect } from 'bun:test';
|
||
import { World } from '@common/rpg/core/world';
|
||
import { Inventory } from '@common/rpg/components/inventory';
|
||
import { Items, Item, Stackable, Usable } from '@common/rpg/components/item';
|
||
import { Equipment, Equippable } from '@common/rpg/components/equipment';
|
||
import { Stat } from '@common/rpg/components/stat';
|
||
|
||
function world() { return new World(); }
|
||
|
||
describe('Items.register', () => {
|
||
it('creates entity with Item component', () => {
|
||
const w = world();
|
||
const e = Items.register(w, 'sword', 'Iron Sword');
|
||
expect(e.get(Item)!.name).toBe('Iron Sword');
|
||
});
|
||
|
||
it('adds Stackable when maxStack is provided', () => {
|
||
const w = world();
|
||
const e = Items.register(w, 'coin', 'Gold Coin', { maxStack: 99 });
|
||
expect(e.get(Stackable)!.maxStack).toBe(99);
|
||
});
|
||
|
||
it('does not add Stackable when maxStack is omitted', () => {
|
||
const w = world();
|
||
const e = Items.register(w, 'key', 'Old Key');
|
||
expect(e.has(Stackable)).toBeFalse();
|
||
});
|
||
|
||
it('adds Usable when usable options are provided', () => {
|
||
const w = world();
|
||
const e = Items.register(w, 'potion', 'Health Potion', {
|
||
usable: { actions: [{ type: 'health.update', arg: 50 }] },
|
||
});
|
||
expect(e.has(Usable)).toBeTrue();
|
||
expect(e.get(Usable)!.consumeOnUse).toBeTrue(); // default
|
||
});
|
||
|
||
it('respects consumeOnUse: false', () => {
|
||
const w = world();
|
||
const e = Items.register(w, 'ring', 'Magic Ring', {
|
||
usable: { actions: [], consumeOnUse: false },
|
||
});
|
||
expect(e.get(Usable)!.consumeOnUse).toBeFalse();
|
||
});
|
||
|
||
it('sets description', () => {
|
||
const w = world();
|
||
const e = Items.register(w, 'rune', 'Ancient Rune', { description: 'A glowing rune.' });
|
||
expect(e.get(Item)!.description).toBe('A glowing rune.');
|
||
});
|
||
|
||
it('registers entity under the given id', () => {
|
||
const w = world();
|
||
Items.register(w, 'gem', 'Ruby');
|
||
expect(w.getEntity('gem')).toBeDefined();
|
||
});
|
||
});
|
||
|
||
describe('Inventory — infinite mode', () => {
|
||
it('adds items without limit', () => {
|
||
const w = world();
|
||
Items.register(w, 'coin', 'Coin', { maxStack: 99 });
|
||
const e = w.createEntity('player');
|
||
e.add('inv', new Inventory());
|
||
e.get(Inventory)!.add({ itemId: 'coin', amount: 500 });
|
||
expect(e.get(Inventory)!.getAmount('coin')).toBe(500);
|
||
});
|
||
|
||
it('grows slot list automatically', () => {
|
||
const w = world();
|
||
Items.register(w, 'coin', 'Coin', { maxStack: 10 });
|
||
const e = w.createEntity('player');
|
||
e.add('inv', new Inventory());
|
||
e.get(Inventory)!.add({ itemId: 'coin', amount: 25 });
|
||
// 10 + 10 + 5 = 3 slots
|
||
expect(e.get(Inventory)!.getItems().get('coin')).toBe(25);
|
||
});
|
||
|
||
it('add returns true', () => {
|
||
const w = world();
|
||
Items.register(w, 'gem', 'Gem');
|
||
const e = w.createEntity('player');
|
||
e.add('inv', new Inventory());
|
||
expect(e.get(Inventory)!.add({ itemId: 'gem', amount: 1 })).toBeTrue();
|
||
});
|
||
|
||
it('warns and returns false for unknown item', () => {
|
||
const w = world();
|
||
const e = w.createEntity('player');
|
||
e.add('inv', new Inventory());
|
||
expect(e.get(Inventory)!.add({ itemId: 'ghost', amount: 1 })).toBeFalse();
|
||
});
|
||
});
|
||
|
||
describe('Inventory — finite mode (count)', () => {
|
||
it('accepts items up to slot capacity', () => {
|
||
const w = world();
|
||
Items.register(w, 'potion', 'Potion', { maxStack: 5 });
|
||
const e = w.createEntity('player');
|
||
e.add('inv', new Inventory(2)); // 2 slots × 5 stack = 10 max
|
||
expect(e.get(Inventory)!.add({ itemId: 'potion', amount: 10 })).toBeTrue();
|
||
expect(e.get(Inventory)!.getAmount('potion')).toBe(10);
|
||
});
|
||
|
||
it('rejects add when capacity exceeded', () => {
|
||
const w = world();
|
||
Items.register(w, 'potion', 'Potion', { maxStack: 5 });
|
||
const e = w.createEntity('player');
|
||
e.add('inv', new Inventory(1)); // 1 slot × 5 = 5 max
|
||
expect(e.get(Inventory)!.add({ itemId: 'potion', amount: 6 })).toBeFalse();
|
||
expect(e.get(Inventory)!.getAmount('potion')).toBe(0);
|
||
});
|
||
|
||
it('fills partial stacks before opening new slots', () => {
|
||
const w = world();
|
||
Items.register(w, 'stone', 'Stone', { maxStack: 10 });
|
||
const e = w.createEntity('player');
|
||
e.add('inv', new Inventory(2));
|
||
const inv = e.get(Inventory)!;
|
||
inv.add({ itemId: 'stone', amount: 8 });
|
||
inv.add({ itemId: 'stone', amount: 5 }); // fills slot 0 to 10, slot 1 to 3
|
||
expect(inv.getAmount('stone')).toBe(13);
|
||
});
|
||
});
|
||
|
||
describe('Inventory — named slots', () => {
|
||
it('add to specific named slotId', () => {
|
||
const w = world();
|
||
Items.register(w, 'herb', 'Herb', { maxStack: 10 });
|
||
const e = w.createEntity('player');
|
||
e.add('inv', new Inventory([{ slotId: 'pocket', limit: 10 }]));
|
||
const inv = e.get(Inventory)!;
|
||
expect(inv.add({ itemId: 'herb', amount: 3, slotId: 'pocket' })).toBeTrue();
|
||
expect(inv.getSlotContents('pocket')).toEqual({ itemId: 'herb', amount: 3 });
|
||
});
|
||
|
||
it('rejects add to unknown slotId', () => {
|
||
const w = world();
|
||
Items.register(w, 'herb', 'Herb');
|
||
const e = w.createEntity('player');
|
||
e.add('inv', new Inventory([{ slotId: 'pocket', limit: 10 }]));
|
||
expect(e.get(Inventory)!.add({ itemId: 'herb', amount: 1, slotId: 'wallet' })).toBeFalse();
|
||
});
|
||
|
||
it('rejects add exceeding slot limit', () => {
|
||
const w = world();
|
||
Items.register(w, 'herb', 'Herb', { maxStack: 99 });
|
||
const e = w.createEntity('player');
|
||
e.add('inv', new Inventory([{ slotId: 'slot', limit: 5 }]));
|
||
expect(e.get(Inventory)!.add({ itemId: 'herb', amount: 6, slotId: 'slot' })).toBeFalse();
|
||
});
|
||
});
|
||
|
||
describe('Inventory — remove', () => {
|
||
it('removes partial amount', () => {
|
||
const w = world();
|
||
Items.register(w, 'coin', 'Coin', { maxStack: 99 });
|
||
const e = w.createEntity('player');
|
||
e.add('inv', new Inventory());
|
||
const inv = e.get(Inventory)!;
|
||
inv.add({ itemId: 'coin', amount: 50 });
|
||
inv.remove({ itemId: 'coin', amount: 20 });
|
||
expect(inv.getAmount('coin')).toBe(30);
|
||
});
|
||
|
||
it('returns false when removing more than available', () => {
|
||
const w = world();
|
||
Items.register(w, 'coin', 'Coin');
|
||
const e = w.createEntity('player');
|
||
e.add('inv', new Inventory());
|
||
const inv = e.get(Inventory)!;
|
||
inv.add({ itemId: 'coin', amount: 1 });
|
||
expect(inv.remove({ itemId: 'coin', amount: 10 })).toBeFalse();
|
||
expect(inv.getAmount('coin')).toBe(1);
|
||
});
|
||
|
||
it('removes from a specific slot', () => {
|
||
const w = world();
|
||
Items.register(w, 'gem', 'Gem');
|
||
const e = w.createEntity('player');
|
||
e.add('inv', new Inventory([{ slotId: 'a' }, { slotId: 'b' }]));
|
||
const inv = e.get(Inventory)!;
|
||
inv.add({ itemId: 'gem', amount: 1, slotId: 'a' });
|
||
inv.add({ itemId: 'gem', amount: 1, slotId: 'b' });
|
||
inv.remove({ itemId: 'gem', amount: 1, slotId: 'a' });
|
||
expect(inv.getSlotContents('a')).toBeNull();
|
||
expect(inv.getSlotContents('b')).toEqual({ itemId: 'gem', amount: 1 });
|
||
});
|
||
|
||
it('slot contents becomes null after full removal', () => {
|
||
const w = world();
|
||
Items.register(w, 'herb', 'Herb');
|
||
const e = w.createEntity('player');
|
||
e.add('inv', new Inventory([{ slotId: 's' }]));
|
||
const inv = e.get(Inventory)!;
|
||
inv.add({ itemId: 'herb', amount: 1, slotId: 's' });
|
||
inv.remove({ itemId: 'herb', amount: 1, slotId: 's' });
|
||
expect(inv.getSlotContents('s')).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe('Inventory — getItems / getAmount', () => {
|
||
it('getItems aggregates across all slots', () => {
|
||
const w = world();
|
||
Items.register(w, 'coin', 'Coin', { maxStack: 5 });
|
||
const e = w.createEntity('player');
|
||
e.add('inv', new Inventory(3));
|
||
const inv = e.get(Inventory)!;
|
||
inv.add({ itemId: 'coin', amount: 12 });
|
||
expect(inv.getItems().get('coin')).toBe(12);
|
||
});
|
||
|
||
it('getAmount returns 0 for absent item', () => {
|
||
const w = world();
|
||
const e = w.createEntity('player');
|
||
e.add('inv', new Inventory());
|
||
expect(e.get(Inventory)!.getAmount('missing')).toBe(0);
|
||
});
|
||
});
|
||
|
||
describe('Inventory — equip', () => {
|
||
it('delegates to Equipment.equip', () => {
|
||
const w = world();
|
||
const sword = Items.register(w, 'sword', 'Sword');
|
||
sword.add('equippable', new Equippable('weapon'));
|
||
const player = w.createEntity('player');
|
||
player.add('str', new Stat({ value: 10 }));
|
||
player.add('equipment', new Equipment([{ slotName: 'weapon', type: 'weapon' }]));
|
||
player.add('inv', new Inventory());
|
||
const inv = player.get(Inventory)!;
|
||
inv.add({ itemId: 'sword', amount: 1 });
|
||
expect(inv.equip({ itemId: 'sword' })).toBeTrue();
|
||
expect(player.get(Equipment)!.getItem('weapon')).toBe('sword');
|
||
});
|
||
});
|
||
|
||
describe('Inventory — use', () => {
|
||
it('executes Usable actions', () => {
|
||
const w = world();
|
||
const player = w.createEntity('player');
|
||
player.add('str', new Stat({ value: 10 }));
|
||
player.add('inv', new Inventory());
|
||
Items.register(w, 'potion', 'Health Potion', {
|
||
usable: { actions: [{ type: 'Stat(str).update', arg: 5 }], consumeOnUse: false },
|
||
});
|
||
player.get(Inventory)!.add({ itemId: 'potion', amount: 1 });
|
||
player.get(Inventory)!.use({ itemId: 'potion' });
|
||
expect(player.get(Stat, 'str')!.value).toBe(15);
|
||
});
|
||
|
||
it('consumes item when consumeOnUse is true', () => {
|
||
const w = world();
|
||
const player = w.createEntity('player');
|
||
player.add('inv', new Inventory());
|
||
Items.register(w, 'herb', 'Herb', {
|
||
maxStack: 99,
|
||
usable: { actions: [], consumeOnUse: true },
|
||
});
|
||
player.get(Inventory)!.add({ itemId: 'herb', amount: 3 });
|
||
player.get(Inventory)!.use({ itemId: 'herb' });
|
||
expect(player.get(Inventory)!.getAmount('herb')).toBe(2);
|
||
});
|
||
});
|