Files
healer-man/scripts/generate-bite-sized-navigation.ts
T
2026-09-27 21:48:00 -04:00

64 lines
3.6 KiB
TypeScript

import { mkdirSync, writeFileSync } from "node:fs";
import { gzipSync } from "node:zlib";
import { build } from "esbuild";
import { Group, Mesh } from "three";
import { DUNGEON_REGISTRY } from "../src/game/dungeonRegistry";
import { BITE_SIZED_LAYOUT_CATALOG } from "../shared/bite-sized-layouts.mjs";
import { preparePartyNavigationGraph } from "../src/game/partyNavigationAssetCache";
import { buildPartyNavigationTriangleNodes } from "../src/game/partyNavigation";
import { biteSizedRouteSegmentClear } from "../src/game/biteSizedSpotNavigation";
import { readDungeonGeometry } from "./dungeon-pipeline/wow-stair-geometry";
// Ship the exact gameplay navigation validator and graphs with the API server.
// The server does not need the downloadable rendering assets to validate a draft.
mkdirSync("server/generated", { recursive: true });
const graphs: Record<string, unknown> = {};
for (const entry of BITE_SIZED_LAYOUT_CATALOG.dungeons) {
const source = DUNGEON_REGISTRY[entry.sourceDungeonId];
const asset = source.assets.navigation!;
const file = asset.url?.startsWith("/") ? `public${asset.url}` : `src/assets/game/dungeons/${source.id}/${asset.fileName}`;
const geometries = await readDungeonGeometry(file);
const scene = new Group();
geometries.forEach((g) => scene.add(new Mesh(g)));
const graph = preparePartyNavigationGraph(source.id, scene, source.navigationLinks, source.traversalRamps, 1_000_000, true);
const triangleNodes = graph.triangleNodes ?? buildPartyNavigationTriangleNodes(graph.triangles!, graph.nodes.map((n) => n.position));
const regions = new Int32Array(triangleNodes.length).fill(graph.triangleNodes ? -1 : 0);
let region = 0;
for (let i = 0; i < triangleNodes.length; i++) {
if (regions[i] !== -1) continue;
const stack = [i]; regions[i] = region;
while (stack.length) {
const current = stack.pop()!;
for (const edge of triangleNodes[current].neighbors) if (regions[edge.to] === -1) { regions[edge.to] = region; stack.push(edge.to); }
}
region++;
}
let legacyRegions: number[] | undefined;
if (entry.legacyBlockers.length) {
const filtered = new Int32Array(triangleNodes.length).fill(-1);
let component = 0;
for (let i = 0; i < triangleNodes.length; i++) {
if (filtered[i] !== -1) continue;
const stack = [i]; filtered[i] = component;
while (stack.length) {
const current = stack.pop()!;
for (const edge of triangleNodes[current].neighbors) {
if (filtered[edge.to] !== -1 || !biteSizedRouteSegmentClear(entry.legacyBlockers, triangleNodes[current].centroid, triangleNodes[edge.to].centroid)) continue;
filtered[edge.to] = component; stack.push(edge.to);
}
}
component++;
}
legacyRegions = Array.from(filtered);
}
// Publication needs a surface and reachability components, not A* costs and
// portals for every triangle. Keep four-decimal coordinate precision.
graphs[source.id] = { nodes: graph.nodes.map((n) => ({ id: n.id, position: n.position.map((v) => Math.round(v * 10_000) / 10_000), neighbors: [] })),
triangles: graph.triangles, regions: Array.from(regions), legacyRegions };
geometries.forEach((g) => g.dispose());
console.log(`Navigation: ${source.id}`);
}
writeFileSync("server/generated/bite-sized-navigation.json.gz", gzipSync(JSON.stringify(graphs), { level: 9 }));
await build({ entryPoints: ["src/game/biteSizedSpotNavigation.ts"], bundle: true, platform: "node", format: "esm",
outfile: "server/generated/bite-sized-navigation.mjs", banner: { js: "// Generated by scripts/generate-bite-sized-navigation.ts; edit the shared TypeScript source." } });