forked from phenom/healer-man
64 lines
3.6 KiB
TypeScript
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." } });
|