91 lines
3.0 KiB
JavaScript
91 lines
3.0 KiB
JavaScript
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/);
|
||
|
|
});
|