import { NodeIO } from "@gltf-transform/core"; import { BufferGeometry, Float32BufferAttribute, Matrix4, Vector3 } from "three"; import { requireDungeonDefinition } from "../src/game/dungeonRegistry"; import { buildPartyNavigationGraph, findNearestPartyNavigationSurface, findPartyNavigationPath, } from "../src/game/partyNavigation"; const dungeonIds = process.argv.slice(2).filter((argument) => !argument.startsWith("--")); if (!dungeonIds.length) throw new Error("Pass at least one dungeon id."); async function navigationGraph(dungeonId: string) { const dungeon = requireDungeonDefinition(dungeonId); const navigation = dungeon.assets.navigation; if (!navigation) throw new Error(`${dungeonId} has no navigation asset.`); const file = `src/assets/game/dungeons/${dungeonId}/${navigation.fileName}`; const document = await new NodeIO().read(file); const positions: number[] = []; const vertex = new Vector3(); for (const node of document.getRoot().listNodes()) { const mesh = node.getMesh(); if (!mesh) continue; const world = new Matrix4().fromArray(node.getWorldMatrix()); for (const primitive of mesh.listPrimitives()) { const source = primitive.getAttribute("POSITION")?.getArray(); if (!source) continue; const indices = primitive.getIndices()?.getArray() ?? Array.from({ length: source.length / 3 }, (_, index) => index); for (const index of indices) { vertex.fromArray(source, Number(index) * 3).applyMatrix4(world); positions.push(vertex.x, vertex.y, vertex.z); } } } const geometry = new BufferGeometry(); geometry.setAttribute("position", new Float32BufferAttribute(positions, 3)); try { return buildPartyNavigationGraph(geometry); } finally { geometry.dispose(); } } for (const dungeonId of dungeonIds) { const dungeon = requireDungeonDefinition(dungeonId); const boss = dungeon.bosses.find((candidate) => !candidate.eventOnly); if (!boss) throw new Error(`${dungeonId} has no normal campaign boss.`); const graph = await navigationGraph(dungeonId); const start = findNearestPartyNavigationSurface(graph, dungeon.entrance.footPosition, 3); const end = findNearestPartyNavigationSurface(graph, boss.position, 6); if (!start || !end) throw new Error(`${dungeonId} entrance or ${boss.name} does not snap to navigation.`); const path = findPartyNavigationPath(graph, start.position, end.position, { maxStartSnapDistance: 3, maxEndSnapDistance: 6, simplifyTolerance: 0.01, }); if (!path?.length) throw new Error(`${dungeonId} has no navigation path to ${boss.name}.`); let expectedDistance = 0; for (let index = 1; index < path.length; index += 1) { expectedDistance += Math.hypot( path[index][0] - path[index - 1][0], path[index][1] - path[index - 1][1], path[index][2] - path[index - 1][2], ); } process.stdout.write(`${JSON.stringify({ dungeonId, status: "navigation-guidance-only; live UI traversal required", objective: boss.name, entrance: start.position, objectivePosition: end.position, startSnapDistance: start.distance, objectiveSnapDistance: end.distance, waypointCount: path.length, expectedNavigationDistance: Number(expectedDistance.toFixed(2)), waypoints: path, })}\n`); }