SCUMM-style web adventure engine v0.1
Headless, data-driven core (no DOM, no eval): loader with one-pass validation, verb-coin interaction resolution, polygon walkables with Dijkstra pathfinding, branching dialogue with mid-conversation saves, declarative JSON conditions/effects, versioned JSON saves. Thin integration layer: asset manifest with graceful placeholders, WebAudio music loop points (midi-playlist convention), canvas renderer (hotspot boxes drawn where hitTest clicks), HUD model. Web harness (web/) + skeleton example game exercising every feature. 69 tests (node --test), tools/run_all.sh gate, docs/SPEC.md design spec. Verified end-to-end in a real browser: intro scene, walking, look/take, room exits, dialogue choices.
This commit is contained in:
1 parent
ea0df1d475
commit
a4a2348a54
32 files changed
+2849
-1
No files matched your search
@@ -0,0 +1,68 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { check, conditionRefs } from "../src/core/conditions.js";
|
||||
|
||||
const world = () => ({
|
||||
flags: { metOtto: true, lied: false },
|
||||
vars: { gold: 5, trust: 0 },
|
||||
inventory: ["mug"],
|
||||
room: "tavern",
|
||||
characters: { otto: { room: "tavern" }, ann: { room: "deck" } },
|
||||
dialogueState: { otto1: { node: "greet" } },
|
||||
seenScenes: { intro: true },
|
||||
});
|
||||
|
||||
test("flag conditions", () => {
|
||||
assert.equal(check({ flag: "metOtto" }, world()), true);
|
||||
assert.equal(check({ flag: "lied" }, world()), false);
|
||||
assert.equal(check({ flag: "nonexistent" }, world()), false);
|
||||
assert.equal(check(null, world()), true);
|
||||
});
|
||||
|
||||
test("not / all / any", () => {
|
||||
assert.equal(check({ not: { flag: "lied" } }, world()), true);
|
||||
assert.equal(check({ all: [{ flag: "metOtto" }, { has: "mug" }] }, world()), true);
|
||||
assert.equal(check({ all: [{ flag: "metOtto" }, { has: "rope" }] }, world()), false);
|
||||
assert.equal(check({ any: [{ flag: "lied" }, { has: "mug" }] }, world()), true);
|
||||
assert.equal(check({ any: [] }, world()), false);
|
||||
assert.equal(check({ all: [] }, world()), true);
|
||||
});
|
||||
|
||||
test("var comparisons", () => {
|
||||
for (const [op, want] of [["eq", true], ["ne", false], ["lt", false],
|
||||
["lte", true], ["gt", false], ["gte", true]]) {
|
||||
assert.equal(check({ var: "gold", op, value: 5 }, world()), want, op);
|
||||
}
|
||||
assert.equal(check({ var: "missing", op: "eq", value: 0 }, world()), true);
|
||||
});
|
||||
|
||||
test("has / at / here / seen / dialogue", () => {
|
||||
assert.equal(check({ has: "mug" }, world()), true);
|
||||
assert.equal(check({ at: "tavern" }, world()), true);
|
||||
assert.equal(check({ at: "deck" }, world()), false);
|
||||
assert.equal(check({ here: "otto" }, world()), true);
|
||||
assert.equal(check({ here: "ann" }, world()), false);
|
||||
assert.equal(check({ seen: "intro" }, world()), true);
|
||||
assert.equal(check({ dialogue: "otto1", node: "greet" }, world()), true);
|
||||
assert.equal(check({ dialogue: "otto1", node: "other" }, world()), false);
|
||||
});
|
||||
|
||||
test("malformed conditions throw", () => {
|
||||
assert.throws(() => check({ bogus: 1 }, world()), /unknown condition key/);
|
||||
assert.throws(() => check({ flag: "x", has: "y" }, world()), /exactly one key/);
|
||||
assert.throws(() => check("nope", world()), /bad condition/);
|
||||
});
|
||||
|
||||
test("conditionRefs collects ids for dangling-ref checks", () => {
|
||||
const refs = conditionRefs({ all: [
|
||||
{ flag: "a" }, { var: "b" }, { has: "mug" }, { at: "tavern" },
|
||||
{ here: "otto" }, { dialogue: "d1" }, { seen: "intro" },
|
||||
] });
|
||||
assert.deepEqual(refs.flags, ["a"]);
|
||||
assert.deepEqual(refs.vars, ["b"]);
|
||||
assert.deepEqual(refs.objects, ["mug"]);
|
||||
assert.deepEqual(refs.rooms, ["tavern"]);
|
||||
assert.deepEqual(refs.characters, ["otto"]);
|
||||
assert.deepEqual(refs.dialogues, ["d1"]);
|
||||
assert.deepEqual(refs.scenes, ["intro"]);
|
||||
});
|
||||
Reference in new issue
Block a user