import { resolve } from "node:path"; import { describe, expect, it } from "vitest"; import { contentSchemas, createEngine, type ContentBundle, ContentLoader, ContentRegistry, type ContentKind, type IntentSystem, type MapDef, type TimeSystem, } from "../src/index.js"; const repoRoot = resolve(process.cwd(), "../.."); const sampleContentDir = resolve(repoRoot, "examples", "sample-game", "content"); /** Compile a bundle the way the pack tool does: fs-load the dir, then collect * every registered def per content kind. */ async function compileBundle(dir: string): Promise { const registry = new ContentRegistry(); const loader = new ContentLoader(registry); const report = await loader.load(dir); if (!report.ok) throw new Error("content load failed"); const content: Record = {}; for (const kind of Object.keys(contentSchemas) as ContentKind[]) { const defs = registry.list(kind); if (defs.length > 0) content[kind] = defs; } return { format: "nengine-bundle", version: 1, content }; } function validMap(): unknown { return { id: "farm", width: 2, height: 2, tilesetRef: "tiles", spawn: { x: 0, y: 0 }, layers: [{ name: "ground", data: [["grass", "grass"], ["grass", "grass"]] }], }; } function validTime(): unknown { return { id: "default", dayLengthTicks: 480, seasons: [{ name: "spring", dayCount: 28 }], start: { day: 1, season: "spring" }, }; } function validItem(): unknown { return { id: "hoe", name: "Hoe", kind: "tool", stackable: false, price: 500 }; } describe("ContentLoader.loadBundle", () => { it("registers a valid bundle and reports per-item ok", async () => { const bundle: ContentBundle = { format: "nengine-bundle", version: 1, content: { map: [validMap()], time: [validTime()], item: [validItem()], }, }; const registry = new ContentRegistry(); const loader = new ContentLoader(registry); const report = await loader.loadBundle(bundle); expect(report.ok).toBe(true); expect(report.items).toHaveLength(3); expect(report.items.every((i) => i.status === "ok")).toBe(true); expect(registry.ids("map")).toEqual(["farm"]); expect(registry.get("map", "farm")?.width).toBe(2); expect(registry.get("item", "hoe")).toMatchObject({ name: "Hoe", kind: "tool" }); }); it("reports an invalid kind entry as an error without aborting siblings", async () => { const bundle: ContentBundle = { format: "nengine-bundle", version: 1, content: { map: [validMap()], mystery: [{ id: "x" }], }, }; const registry = new ContentRegistry(); const loader = new ContentLoader(registry); const report = await loader.loadBundle(bundle); expect(report.ok).toBe(false); const ok = report.items.find((i) => i.kind === "map"); const bad = report.items.find((i) => i.kind === "mystery"); expect(ok?.status).toBe("ok"); expect(bad?.status).toBe("error"); expect(bad?.errors?.[0]).toContain("unknown content kind: mystery"); expect(registry.get("map", "farm")).toBeDefined(); }); it("reports a per-kind invalid def with per-field issues and registers nothing for it", async () => { const bundle: ContentBundle = { format: "nengine-bundle", version: 1, content: { item: [validItem(), { id: "bad", name: 42, kind: "nope" }], }, }; const registry = new ContentRegistry(); const loader = new ContentLoader(registry); const report = await loader.loadBundle(bundle); expect(report.ok).toBe(false); const bad = report.items.find((i) => i.id === "bad"); expect(bad?.status).toBe("error"); expect(bad?.errors?.length).toBeGreaterThan(0); expect(registry.get("item", "hoe")).toBeDefined(); expect(registry.get("item", "bad")).toBeUndefined(); }); it("treats a missing kind as a no-op", async () => { const bundle: ContentBundle = { format: "nengine-bundle", version: 1, content: { map: [validMap()] }, }; const registry = new ContentRegistry(); const loader = new ContentLoader(registry); const report = await loader.loadBundle(bundle); expect(report.ok).toBe(true); expect(report.items).toHaveLength(1); expect(registry.ids("sprite")).toEqual([]); }); it("throws a clear error on a structurally invalid bundle", async () => { const registry = new ContentRegistry(); const loader = new ContentLoader(registry); await expect( loader.loadBundle({ format: "nengine-bundle", version: 99, content: {} } as unknown as ContentBundle), ).rejects.toThrow("not a valid nengine-bundle"); }); it("accepts an additive mods map and rejects a malformed one (D36)", async () => { const registry = new ContentRegistry(); const loader = new ContentLoader(registry); const withMods: ContentBundle = { format: "nengine-bundle", version: 1, content: { map: [validMap()] }, mods: { "my-mod": { manifest: { id: "my-mod", version: "1.0.0" }, code: "export default {};" } }, }; const report = await loader.loadBundle(withMods); expect(report.ok).toBe(true); const registry2 = new ContentRegistry(); const loader2 = new ContentLoader(registry2); await expect( loader2.loadBundle({ format: "nengine-bundle", version: 1, content: { map: [validMap()] }, mods: { "my-mod": { manifest: { id: "my-mod" }, code: 42 } }, } as unknown as ContentBundle), ).rejects.toThrow("not a valid nengine-bundle"); }); it("fs load() behavior is unchanged (zero regression)", async () => { const registry = new ContentRegistry(); const loader = new ContentLoader(registry); const report = await loader.load(sampleContentDir); expect(report.ok).toBe(true); expect(report.items.length).toBeGreaterThan(0); expect(registry.ids("map")).toEqual(["farm"]); expect(registry.ids("time")).toEqual(["default"]); }); it("loadBundle populates a metadata-only asset table from assetsIndex (split mode, D37)", async () => { const registry = new ContentRegistry(); const loader = new ContentLoader(registry); const split: ContentBundle = { format: "nengine-bundle", version: 1, content: { map: [validMap()], time: [validTime()] }, assetsIndex: { "grass-1": { kind: "image", mime: "image/png", size: 128, file: "nengine-assets/grass-1.png" }, "click-1": { kind: "audio", mime: "audio/ogg", size: 64, file: "nengine-assets/click-1.ogg" }, }, }; const report = await loader.loadBundle(split); expect(report.ok).toBe(true); const grass = registry.assets.get("grass-1"); expect(grass).toMatchObject({ kind: "image", mime: "image/png", size: 128, file: "nengine-assets/grass-1.png" }); expect(grass?.data).toBeUndefined(); expect(registry.assets.get("click-1")?.kind).toBe("audio"); }); it("loadBundle inline mode merges index metadata (size) onto base64 entries (D37)", async () => { const registry = new ContentRegistry(); const loader = new ContentLoader(registry); const bundle: ContentBundle = { format: "nengine-bundle", version: 1, content: { map: [validMap()] }, assets: { "grass-1": { kind: "image", mime: "image/png", data: "aGVsbG8=" } }, assetsIndex: { "grass-1": { kind: "image", mime: "image/png", size: 5, file: "nengine-assets/grass-1.png" } }, }; const report = await loader.loadBundle(bundle); expect(report.ok).toBe(true); const grass = registry.assets.get("grass-1"); expect(grass?.data).toBe("aGVsbG8="); expect(grass?.size).toBe(5); expect(grass?.file).toBe("nengine-assets/grass-1.png"); }); }); describe("createEngine bundle mode", () => { it("boots from a bundle, seeds the world, and ticks systems with no fs access", async () => { const bundle = await compileBundle(sampleContentDir); const engine = createEngine({ bundle }); await engine.start(); try { expect(engine.contentDir).toBeUndefined(); expect(engine.registry.ids("map")).toEqual(["farm"]); expect(engine.registry.ids("item")).toContain("hoe"); expect(engine.registry.ids("sprite")).toContain("player"); const player = engine.world.get("player"); expect(player?.type).toBe("player"); expect(player?.x).toBe(2); expect(player?.y).toBe(4); const time = engine.systems.get("time"); expect(time?.get()).toEqual({ day: 1, season: "spring", tickInDay: 0 }); const intent = engine.systems.get("intent"); if (!intent) throw new Error("intent system not booted"); const result = intent.handle("move", { tile: { x: 3, y: 4 } }); expect(result.ok).toBe(true); expect(engine.world.get("player")?.x).toBe(3); engine.tick(480); expect(time?.get().day).toBe(2); } finally { await engine.stop(); } }); });