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); }); });