Files

150 lines
5.4 KiB
JavaScript
Raw Permalink Normal View History

2026-10-18 00:29:36 +00:00
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);
});