import { resolve } from "node:path"; import { describe, expect, it } from "vitest"; import { createEngine, ContentRegistry, InventorySystem, World, WorldEventBus } from "../src/index.js"; const repoRoot = resolve(process.cwd(), "../.."); const contentDir = resolve(repoRoot, "content"); const modsDir = resolve(repoRoot, "mods"); function setup() { const world = new World(); const registry = new ContentRegistry(); const events = new WorldEventBus(); world.create({ id: "player", type: "player", x: 0, y: 0 }); const inv = new InventorySystem(); inv.init({ world, registry, events }); return { inv, world, events }; } describe("InventorySystem", () => { it("init creates an inventory field on the player entity", () => { const { world } = setup(); expect(world.get("player")?.inventory).toEqual({}); }); it("give/count/items roundtrip", () => { const { inv } = setup(); expect(inv.give("hoe", 2)).toBe(2); expect(inv.give("hoe", 1)).toBe(3); expect(inv.count("hoe")).toBe(3); expect(inv.count("parsnip")).toBe(0); expect(inv.items()).toEqual({ hoe: 3 }); }); it("items() returns a snapshot copy, not a live reference", () => { const { inv } = setup(); inv.give("hoe", 1); const snapshot = inv.items(); inv.give("hoe", 1); expect(snapshot).toEqual({ hoe: 1 }); expect(inv.items()).toEqual({ hoe: 2 }); }); it("give emits item-acquire with itemId, qty, and total", () => { const { inv, events } = setup(); const seen: unknown[] = []; events.on("item-acquire", (p) => seen.push(p)); inv.give("hoe", 2); expect(seen).toEqual([{ itemId: "hoe", qty: 2, total: 2 }]); }); it("take decrements and emits item-use", () => { const { inv, events } = setup(); const seen: unknown[] = []; events.on("item-use", (p) => seen.push(p)); inv.give("hoe", 3); expect(inv.take("hoe", 1)).toBe(1); expect(seen).toEqual([{ itemId: "hoe", qty: 1, total: 2 }]); expect(inv.count("hoe")).toBe(2); }); it("take clamps to the available quantity when insufficient", () => { const { inv, events } = setup(); const seen: unknown[] = []; events.on("item-use", (p) => seen.push(p)); inv.give("hoe", 3); expect(inv.take("hoe", 10)).toBe(3); expect(inv.count("hoe")).toBe(0); expect(inv.items()).toEqual({}); expect(seen).toEqual([{ itemId: "hoe", qty: 3, total: 0 }]); }); it("take of an absent item returns 0 and emits nothing", () => { const { inv, events } = setup(); const seen: unknown[] = []; events.on("item-use", (p) => seen.push(p)); expect(inv.take("hoe", 5)).toBe(0); expect(seen).toHaveLength(0); }); it("give/take reject non-positive quantities", () => { const { inv } = setup(); expect(() => inv.give("hoe", 0)).toThrow(/positive integer/); expect(() => inv.give("hoe", -1)).toThrow(/positive integer/); expect(() => inv.give("hoe", 1.5)).toThrow(/positive integer/); expect(() => inv.take("hoe", 0)).toThrow(/positive integer/); }); it("wireEvents: inventory events fire onItemAcquire/onItemUse hooks with a HookContext", async () => { const engine = createEngine({ contentDir, modsDir }); await engine.start(); try { const acquired: unknown[] = []; const used: unknown[] = []; engine.hooks.on("onItemAcquire", (ctx) => { acquired.push({ world: ctx.world.get("player")?.id, time: ctx.time.get().day }); }); engine.hooks.on("onItemUse", (ctx) => { used.push({ world: ctx.world.get("player")?.id, time: ctx.time.get().day }); }); const inv = engine.systems.get("inventory"); expect(inv).toBeDefined(); inv!.give("hoe", 2); inv!.take("hoe", 1); expect(acquired).toEqual([{ world: "player", time: 1 }]); expect(used).toEqual([{ world: "player", time: 1 }]); expect(inv!.items()).toEqual({ hoe: 1 }); } finally { await engine.stop(); } }); });