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,58 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { pointInPolygon, nearestPointInPolygon, WalkArea } from "../src/core/walkable.js";
|
||||
|
||||
const square = [[0, 0], [100, 0], [100, 100], [0, 100]];
|
||||
|
||||
test("point in polygon", () => {
|
||||
assert.equal(pointInPolygon([50, 50], square), true);
|
||||
assert.equal(pointInPolygon([150, 50], square), false);
|
||||
const tri = [[0, 0], [100, 0], [50, 100]];
|
||||
assert.equal(pointInPolygon([50, 90], tri), true);
|
||||
assert.equal(pointInPolygon([5, 90], tri), false);
|
||||
});
|
||||
|
||||
test("nearest point clamps to boundary", () => {
|
||||
const p = nearestPointInPolygon([150, 50], [square]);
|
||||
assert.deepEqual(p, [100, 50]);
|
||||
});
|
||||
|
||||
test("walk area contains/clamp", () => {
|
||||
const wa = new WalkArea([square]);
|
||||
assert.equal(wa.contains([10, 10]), true);
|
||||
assert.deepEqual(wa.clamp([150, 50]), [100, 50]);
|
||||
assert.deepEqual(wa.clamp([10, 10]), [10, 10]);
|
||||
});
|
||||
|
||||
test("straight path inside one polygon", () => {
|
||||
const wa = new WalkArea([square], { sampleStep: 20 });
|
||||
const path = wa.path([10, 50], [90, 50]);
|
||||
assert.ok(path, "path found");
|
||||
assert.deepEqual(path[0], [10, 50]);
|
||||
assert.deepEqual(path.at(-1), [90, 50]);
|
||||
});
|
||||
|
||||
test("path between two polygons sharing an edge (doorway)", () => {
|
||||
const left = [[0, 0], [100, 0], [100, 100], [0, 100]];
|
||||
const right = [[100, 0], [200, 0], [200, 100], [100, 100]];
|
||||
const wa = new WalkArea([left, right], { sampleStep: 20, portalEpsilon: 5 });
|
||||
const path = wa.path([10, 50], [190, 50]);
|
||||
assert.ok(path, "cross-polygon path found");
|
||||
assert.deepEqual(path.at(-1), [190, 50]);
|
||||
// every waypoint must be inside the walkable area
|
||||
for (const p of path) assert.ok(wa.contains(p) || wa.clamp(p)[0] === p[0], `waypoint ${p} off-area`);
|
||||
});
|
||||
|
||||
test("unreachable area returns null", () => {
|
||||
const left = [[0, 0], [10, 0], [10, 10], [0, 10]];
|
||||
const far = [[500, 500], [510, 500], [510, 510], [500, 510]];
|
||||
const wa = new WalkArea([left, far], { sampleStep: 4, portalEpsilon: 5 });
|
||||
assert.equal(wa.path([5, 5], [505, 505]), null);
|
||||
});
|
||||
|
||||
test("click outside the area walks to the nearest edge", () => {
|
||||
const wa = new WalkArea([square], { sampleStep: 20 });
|
||||
const path = wa.path([50, 50], [150, 50]);
|
||||
assert.ok(path);
|
||||
assert.deepEqual(path.at(-1), wa.clamp([150, 50]));
|
||||
});
|
||||
Reference in new issue
Block a user