1
0
Fork 0
tsgames/test/common/rpg/inventory.test.ts

264 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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