import { test } from "node:test"; import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { Engine } from "../src/core/engine.js"; const example = JSON.parse(readFileSync(new URL("../example/game.json", import.meta.url), "utf8")); function fresh() { let now = 0; const e = new Engine(structuredClone(example), { clock: () => now }); const events = []; e.emitter.on("*", (ev) => events.push(ev)); e.newGame(); const advance = (ms) => { now += ms; e.tick(ms); }; return { e, events, advance }; } test("intro scene runs on the clock and lands the player in the start room", () => { const { e, events, advance } = fresh(); e.startScene("intro"); assert.ok(events.some((ev) => ev.event === "playMusic" && ev.track === "calypso")); assert.ok(events.some((ev) => ev.event === "slide")); advance(4000); // slide done advance(3500); // text done -> goto jetty assert.equal(e.scene, null); assert.equal(e.world.room, "jetty"); assert.equal(e.world.seenScenes.intro, true); }); test("scene skip jumps to the final goto", () => { const { e } = fresh(); e.startScene("intro"); e.skipScene(); assert.equal(e.scene, null); assert.equal(e.world.room, "jetty"); }); test("clicking a walkable point schedules a walk that completes over time", () => { const { e, advance } = fresh(); const p = e.world.characters.player; const startX = p.x; e.clickPoint(400, 380); assert.ok(e.walk, "walk scheduled"); advance(1000); // 80 px/s -> 80px assert.ok(p.x > startX); advance(5000); assert.equal(e.walk, null); assert.ok(Math.abs(p.x - 400) < 1, `arrived at x=400, got ${p.x}`); }); test("click outside the walkable area clamps to the nearest edge", () => { const { e, advance } = fresh(); e.clickPoint(700, 380); // beyond the jetty polygon (x<=600) advance(10000); const p = e.world.characters.player; assert.ok(p.x <= 600 && p.x >= 590, `clamped near x=600, got ${p.x}`); }); test("verb + object click: take the mug, then it leaves the room", () => { const { e } = fresh(); e.selectVerb("use"); const resp = e.click({ kind: "object", id: "mug", tags: ["container"] }); assert.ok(resp.includes("pocket")); assert.deepEqual(e.world.inventory, ["mug"]); assert.ok(!e.world.objectsHere().some((o) => o.id === "mug"), "taken object leaves the room"); }); test("unmatched verb gets the default response — the engine always answers", () => { const { e } = fresh(); e.selectVerb("give"); const resp = e.click({ kind: "object", id: "post", tags: [] }); assert.equal(resp, "They're not interested."); }); test("walk verb + exit click changes rooms and fires onEnter (music)", () => { const { e, events } = fresh(); e.selectVerb("walk"); e.click({ kind: "exit", id: "tavern", tags: ["exit"] }); assert.equal(e.world.room, "tavern"); assert.equal(e.world.characters.player.room, "tavern"); assert.ok(events.some((ev) => ev.event === "roomEnter" && ev.room === "tavern")); }); test("talk verb starts the dialogue; clicks are swallowed mid-dialogue", () => { const { e } = fresh(); e.goTo("tavern"); e.selectVerb("talk"); e.click({ kind: "character", id: "barkeep", tags: ["talker"] }); assert.equal(e.dialogue.active, "barkeep_intro"); const before = e.world.flags; const resp = e.click({ kind: "object", id: "post", tags: [] }); assert.equal(resp, null, "clicks ignored during dialogue"); assert.deepEqual(e.world.flags, before); }); test("dialogue choice with give-mug chain: full interaction flow", () => { const { e, advance } = fresh(); e.selectVerb("use"); e.click({ kind: "object", id: "mug", tags: ["container"] }); // take mug e.goTo("tavern"); e.selectVerb("give"); const resp = e.click({ kind: "character", id: "barkeep", tags: ["talker"] }, "mug"); assert.ok(resp.includes("very quiet")); assert.deepEqual(e.world.inventory, []); assert.equal(e.world.flags.gaveMug, true); assert.equal(e.world.vars.barkeepTrust, 1); }); test("hitTest finds characters, objects, exits, then walkable", () => { const { e } = fresh(); assert.equal(e.hitTest(500, 340).id, "post"); assert.equal(e.hitTest(220, 375).id, "mug"); assert.equal(e.hitTest(580, 340).kind, "exit"); assert.equal(e.hitTest(300, 420).kind, "walkable"); e.goTo("tavern"); assert.equal(e.hitTest(400, 300).id, "barkeep"); }); test("endGame effect ends the run and blocks further clicks", () => { const spec = structuredClone(example); spec.interactions.push({ verb: "use", noun: "post", response: "The world ends.", effects: [{ endGame: "win" }] }); const e = new Engine(spec, { clock: () => 0 }); e.newGame(); e.selectVerb("use"); e.click({ kind: "object", id: "post", tags: [] }); assert.equal(e.phase, "ended"); assert.equal(e.world.ended, "win"); assert.equal(e.click({ kind: "object", id: "post", tags: [] }), null); }); test("room onEnter effects fire on entry", () => { const spec = structuredClone(example); spec.rooms[1].onEnter = [{ setFlag: "visitedTavern" }]; const e = new Engine(spec, { clock: () => 0 }); e.newGame(); e.goTo("tavern"); assert.equal(e.world.flags.visitedTavern, true); }); test("newGame resets everything", () => { const { e } = fresh(); e.selectVerb("use"); e.click({ kind: "object", id: "mug", tags: ["container"] }); e.goTo("tavern"); e.newGame(); assert.equal(e.world.room, "jetty"); assert.deepEqual(e.world.inventory, []); assert.equal(e.world.flags.tookMug, undefined); });