80 lines
3.2 KiB
TypeScript
80 lines
3.2 KiB
TypeScript
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`);
|
|
}
|