Files
healer-man/scripts/dungeon-pipeline/detour-topology.test.mjs
T
2026-10-06 22:25:34 -04:00

142 lines
3.8 KiB
JavaScript

import assert from "node:assert/strict";
import test from "node:test";
import {
buildDetourGeometryRanges,
extractDetourTopology,
stableSha256,
} from "./detour-topology.mjs";
const NULL_LINK = 0xffffffff;
function fakePoly({
type = 0,
firstLink = NULL_LINK,
vertexCount = 4,
vertexIndices = [0, 1, 2, 3],
} = {}) {
return {
getType: () => type,
firstLink: () => firstLink,
vertCount: () => vertexCount,
verts: (index) => vertexIndices[index],
};
}
function fakeTile({ x, y, layer = 0, polys, details, links, vertices = [] }) {
return {
header: () => ({
polyCount: () => polys.length,
maxLinkCount: () => links.length,
x: () => x,
y: () => y,
layer: () => layer,
}),
polys: (index) => polys[index],
detailMeshes: (index) => details[index],
links: (index) => links[index],
verts: (index) => vertices[index],
};
}
test("extracts stable surface polygon IDs and only actual directed Detour links", () => {
const tiles = [
fakeTile({
x: 4,
y: 8,
polys: [
fakePoly({ firstLink: 0 }),
fakePoly({ type: 1 }), // Detour off-mesh connection polygons have no surface triangles.
],
details: [{ triCount: () => 2 }, { triCount: () => 0 }],
vertices: [0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1],
links: [{
ref: () => 2000,
next: () => NULL_LINK,
edge: () => 2,
side: () => 255,
bmin: () => 0,
bmax: () => 0,
}],
}),
fakeTile({
x: 5,
y: 8,
polys: [fakePoly({ firstLink: 0 }), fakePoly()],
details: [{ triCount: () => 1 }, { triCount: () => 4 }],
links: [{
ref: () => 1001, // Points to the omitted off-mesh polygon, not a floor polygon.
next: () => NULL_LINK,
edge: () => 3,
side: () => 1,
bmin: () => 32,
bmax: () => 192,
}],
}),
];
const topology = extractDetourTopology({
getMaxTiles: () => tiles.length,
getTile: (index) => tiles[index],
getPolyRefBase: (tile) => tile === tiles[0] ? 1000 : 2000,
});
assert.deepEqual(topology.polygons, [
[0, 0, 4, 8, 0, 0, 0, 2, 4],
[1, 1, 5, 8, 0, 0, 2, 1, 4],
[2, 1, 5, 8, 0, 1, 3, 4, 4],
]);
assert.deepEqual(topology.links, [[
0, 1, 2, 255, 0, 0,
2, 3,
1, 0, 1,
0, 0, 1,
]]);
assert.equal(topology.detailTriangleCount, 7);
assert.equal(topology.polygonFanTriangleCount, 6);
assert.equal(topology.offMeshPolygonCount, 1);
assert.equal(topology.unrepresentedLinkCount, 1);
assert.deepEqual(topology.weakComponents, {
count: 2,
largestPolygonCount: 2,
polygonCounts: [2, 1],
});
});
test("rejects invalid Detour link chains instead of emitting unverifiable topology", () => {
const tile = fakeTile({
x: 0,
y: 0,
polys: [fakePoly({ firstLink: 2 })],
details: [{ triCount: () => 1 }],
links: [{
ref: () => 1000,
next: () => NULL_LINK,
edge: () => 0,
side: () => 255,
bmin: () => 0,
bmax: () => 0,
}],
});
assert.throws(() => extractDetourTopology({
getMaxTiles: () => 1,
getTile: () => tile,
getPolyRefBase: () => 1000,
}), /invalid link index 2/);
});
test("fingerprints ignore object key order but include source values", () => {
assert.equal(stableSha256({ b: 2, a: 1 }), stableSha256({ a: 1, b: 2 }));
assert.notEqual(stableSha256({ a: 1 }), stableSha256({ a: 2 }));
});
test("maps filtered output triangles to contiguous per-polygon ranges", () => {
assert.deepEqual(buildDetourGeometryRanges(4, [0, 0, 2, 2, 3, 3]), [
[0, 2],
[-1, 0],
[2, 2],
[4, 2],
]);
assert.throws(() => buildDetourGeometryRanges(2, [1, 0]), /stable polygon order/);
assert.throws(() => buildDetourGeometryRanges(2, [2]), /invalid Detour polygon/);
});