import { test } from "node:test"; import assert from "node:assert/strict"; import { compile } from "../src/core/loader.js"; import { GameWorld } from "../src/core/world.js"; import { DialogueRunner } from "../src/core/dialogue.js"; import { Emitter } from "../src/core/events.js"; import { readFileSync } from "node:fs"; const example = JSON.parse(readFileSync(new URL("../example/game.json", import.meta.url), "utf8")); function setup() { const c = compile(example); const w = new GameWorld(c); const em = new Emitter(); const lines = []; em.on("dialogueLine", (ev) => lines.push(ev)); let now = 0; const d = new DialogueRunner(c, w, em); d.clock = () => now; return { c, w, em, d, lines, advance: (ms) => { now += ms; d.tick(now); } }; } test("start emits first line and exposes choices", () => { const { d, lines } = setup(); d.start("barkeep_intro"); assert.equal(lines.length, 1); assert.equal(lines[0].speaker, "barkeep"); const choices = d.choices(); assert.equal(choices.length, 2); assert.equal(choices[0].text, "What's in the mug?"); }); test("choosing branches and auto-advancing nodes time out", () => { const { d, lines, advance } = setup(); d.start("barkeep_intro"); d.choose(0); // -> mug node (auto-advance, has next) assert.equal(lines.length, 2); assert.ok(lines[1].text.includes("never a mug")); assert.equal(d.world.flags.heardAboutMug, true); // onEnter fired advance(10000); // hold elapsed -> back to greet assert.equal(lines.length, 3); assert.equal(d.current().node, "greet"); }); test("end choice closes the dialogue", () => { const { d, em } = setup(); let ended = 0; em.on("dialogueEnd", () => ended++); d.start("barkeep_intro"); d.choose(1); assert.equal(d.active, null); assert.equal(ended, 1); }); test("choice conditions hide options", () => { const c = compile(structuredClone(example)); c.dialogues.barkeep_intro.nodes.greet.choices[0].conditions = { flag: "hasClue" }; const w = new GameWorld(c); const d = new DialogueRunner(c, w, new Emitter()); d.start("barkeep_intro"); assert.equal(d.choices().length, 1); w.flags.hasClue = true; assert.equal(d.choices().length, 2); }); test("mid-dialogue state survives snapshot/restore", () => { const { d, w } = setup(); d.start("barkeep_intro"); d.choose(0); const snap = w.snapshot(); assert.equal(snap.dialogueState.barkeep_intro.node, "mug"); const w2 = new GameWorld(setup().c); w2.restore(snap); assert.equal(w2.dialogueState.barkeep_intro.node, "mug"); }); test("choice effects apply to the world", () => { const spec = structuredClone(example); spec.dialogues.barkeep_intro.nodes.greet.choices[0].effects = [{ setFlag: "askedAboutMug" }]; const c = compile(spec); const w = new GameWorld(c); const d = new DialogueRunner(c, w, new Emitter()); d.start("barkeep_intro"); d.choose(0); assert.equal(w.flags.askedAboutMug, true); }); test("unknown dialogue/node errors loudly", () => { const { d } = setup(); assert.throws(() => d.start("nope"), /unknown dialogue/); });