118 lines
4.1 KiB
TypeScript
118 lines
4.1 KiB
TypeScript
import { NodeIO } from "@gltf-transform/core";
|
|
import { fileURLToPath } from "node:url";
|
|
import { dirname, resolve } from "node:path";
|
|
import { BufferAttribute, BufferGeometry, DoubleSide, Ray, Vector3 } from "three";
|
|
import { MeshBVH } from "three-mesh-bvh";
|
|
import {
|
|
createLoopedPath,
|
|
samplePackMember,
|
|
WAILING_CAVERNS_BOSS_SPAWNS,
|
|
WAILING_CAVERNS_ENTRANCE_SPAWNS,
|
|
WAILING_CAVERNS_ROAMING_PACKS,
|
|
} from "../src/game/mobPopulation";
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
const collisionPath = resolve(
|
|
here,
|
|
"../src/assets/game/dungeons/wailing-caverns/wailing-caverns-collision.glb",
|
|
);
|
|
const document = await new NodeIO().read(collisionPath);
|
|
const primitive = document.getRoot().listMeshes()[0]?.listPrimitives()[0];
|
|
if (!primitive) throw new Error("Collision GLB has no mesh primitive.");
|
|
|
|
const positions = primitive.getAttribute("POSITION")?.getArray();
|
|
const indices = primitive.getIndices()?.getArray();
|
|
if (!positions || !indices) throw new Error("Collision GLB lacks indexed positions.");
|
|
|
|
const geometry = new BufferGeometry();
|
|
geometry.setAttribute("position", new BufferAttribute(positions, 3));
|
|
geometry.setIndex(new BufferAttribute(indices, 1));
|
|
const bvh = new MeshBVH(geometry);
|
|
const down = new Vector3(0, -1, 0);
|
|
const origin = new Vector3();
|
|
const sampledPosition: [number, number, number] = [0, 0, 0];
|
|
const sampledForward: [number, number, number] = [0, 0, 1];
|
|
const entranceCamera = new Vector3(168.44, -69.7, 135.31);
|
|
const sightTarget = new Vector3();
|
|
const sightDirection = new Vector3();
|
|
|
|
function floorDelta(point: readonly [number, number, number]): number | null {
|
|
origin.set(point[0], point[1] + 2.5, point[2]);
|
|
const hit = bvh.raycastFirst(new Ray(origin, down), DoubleSide, 0, 7);
|
|
return hit ? Math.abs(hit.point.y - point[1]) : null;
|
|
}
|
|
|
|
function hasEntranceLineOfSight(point: readonly [number, number, number]): boolean {
|
|
sightTarget.set(point[0], point[1] + 1.1, point[2]);
|
|
sightDirection.copy(sightTarget).sub(entranceCamera);
|
|
const distance = sightDirection.length();
|
|
sightDirection.multiplyScalar(1 / distance);
|
|
return bvh.raycastFirst(
|
|
new Ray(entranceCamera, sightDirection),
|
|
DoubleSide,
|
|
0,
|
|
Math.max(0, distance - 0.25),
|
|
) === null;
|
|
}
|
|
|
|
const routeReports = WAILING_CAVERNS_ROAMING_PACKS.map((pack) => {
|
|
const path = createLoopedPath(pack.waypoints);
|
|
let missingFloorSamples = 0;
|
|
let maxFloorDelta = 0;
|
|
const sampleCount = Math.max(24, Math.ceil(path.totalLength / 2));
|
|
|
|
for (let sampleIndex = 0; sampleIndex < sampleCount; sampleIndex += 1) {
|
|
const leaderDistance = (sampleIndex / sampleCount) * path.totalLength;
|
|
for (const member of pack.members) {
|
|
samplePackMember(
|
|
path,
|
|
leaderDistance,
|
|
member.formationOffset,
|
|
sampledPosition,
|
|
sampledForward,
|
|
);
|
|
const delta = floorDelta(sampledPosition);
|
|
if (delta === null) missingFloorSamples += 1;
|
|
else maxFloorDelta = Math.max(maxFloorDelta, delta);
|
|
}
|
|
}
|
|
|
|
return {
|
|
id: pack.id,
|
|
pathId: pack.pathId,
|
|
members: pack.members.length,
|
|
pathLength: Number(path.totalLength.toFixed(2)),
|
|
maxFloorDelta: Number(maxFloorDelta.toFixed(3)),
|
|
missingFloorSamples,
|
|
};
|
|
});
|
|
|
|
const bossReports = WAILING_CAVERNS_BOSS_SPAWNS.map((spawn) => {
|
|
const closest = bvh.closestPointToPoint(new Vector3(...spawn.position));
|
|
return {
|
|
id: spawn.id,
|
|
closestSurfaceDistance: closest ? Number(closest.distance.toFixed(3)) : null,
|
|
};
|
|
});
|
|
|
|
const entranceReports = WAILING_CAVERNS_ENTRANCE_SPAWNS.map((spawn) => ({
|
|
id: spawn.id,
|
|
floorDelta: floorDelta(spawn.position),
|
|
entranceLineOfSight: hasEntranceLineOfSight(spawn.position),
|
|
}));
|
|
|
|
const failedRoutes = routeReports.filter(
|
|
(report) => report.missingFloorSamples > 0 || report.maxFloorDelta > 4,
|
|
);
|
|
const failedEntranceSpawns = entranceReports.filter(
|
|
(report) => report.floorDelta === null || report.floorDelta > 0.2 || !report.entranceLineOfSight,
|
|
);
|
|
console.log(JSON.stringify({
|
|
routeReports,
|
|
bossReports,
|
|
entranceReports,
|
|
failedRoutes,
|
|
failedEntranceSpawns,
|
|
}, null, 2));
|
|
if (failedRoutes.length > 0 || failedEntranceSpawns.length > 0) process.exitCode = 1;
|