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 = {}; 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." } });