82 lines
4.2 KiB
TypeScript
82 lines
4.2 KiB
TypeScript
import path from "node:path";
|
|
import RAPIER from "@dimforge/rapier3d-compat";
|
|
import { BufferGeometry, Group, Mesh } from "three";
|
|
import { beforeAll, describe, expect, it } from "vitest";
|
|
import type { DungeonTraversalRamp } from "./dungeonTypes";
|
|
import stairs from "./generated/wowDungeonStairs.generated.json";
|
|
import { requireDungeonDefinition } from "./dungeonRegistry";
|
|
import { bridgeDungeonStairNavigation, smoothDungeonStairGeometry } from "./dungeonTraversalRamps";
|
|
import { collisionTrimeshes, prepareCollision } from "../scene/productionDungeonCollision";
|
|
import { DUNGEON_PHYSICAL_COLLISION_GROUPS } from "../scene/dungeonLineOfSight";
|
|
import { createPartyDirectSegmentValidator } from "../scene/partyTraversal";
|
|
|
|
const pipelineModule = "../../scripts/dungeon-pipeline/wow-stair-geometry.ts";
|
|
const { readDungeonGeometry } = await import(pipelineModule) as {
|
|
readDungeonGeometry(file: string): Promise<BufferGeometry[]>;
|
|
};
|
|
const packages = Object.entries(stairs.dungeons).filter(([, entry]) => entry.ramps.length);
|
|
|
|
beforeAll(async () => { await RAPIER.init(); });
|
|
|
|
describe("shipping WoW dungeon stair proxies", () => {
|
|
it.each(packages)("%s supports live NPC capsule sweeps uphill/downhill in three lanes", async (id) => {
|
|
const definition = requireDungeonDefinition(id);
|
|
const ramps = definition.traversalRamps!;
|
|
const source = new Group();
|
|
for (const asset of definition.assets.collision) {
|
|
for (const geometry of await readDungeonGeometry(path.join(process.cwd(), "public", asset.url!))) {
|
|
source.add(new Mesh(geometry));
|
|
}
|
|
}
|
|
const prepared = prepareCollision(source, ramps);
|
|
const world = new RAPIER.World({ x: 0, y: -18, z: 0 });
|
|
try {
|
|
for (const mesh of collisionTrimeshes(prepared)) {
|
|
world.createCollider(RAPIER.ColliderDesc.trimesh(Float32Array.from(mesh.vertices), Uint32Array.from(mesh.indices))
|
|
.setCollisionGroups(DUNGEON_PHYSICAL_COLLISION_GROUPS));
|
|
}
|
|
world.step();
|
|
const allowed = createPartyDirectSegmentValidator(RAPIER, world);
|
|
for (const ramp of ramps) {
|
|
const dx = ramp.end[0] - ramp.start[0]; const dz = ramp.end[2] - ramp.start[2];
|
|
const length = Math.hypot(dx, dz);
|
|
const count = Math.max(3, Math.ceil(length / 0.35) + 1);
|
|
const lateral = Math.max(0, Math.min(ramp.width / 4, ramp.width / 2 - 0.45));
|
|
for (const lane of [0, -lateral, lateral]) {
|
|
const points = Array.from({ length: count }, (_, index): [number, number, number] => {
|
|
const t = index / (count - 1);
|
|
return [ramp.start[0] + dx * t - dz / length * lane,
|
|
ramp.start[1] + (ramp.end[1] - ramp.start[1]) * t,
|
|
ramp.start[2] + dz * t + dx / length * lane];
|
|
});
|
|
for (let index = 1; index < points.length; index += 1) {
|
|
const detail = `${id}/${ramp.id}, lane ${lane}, sample ${index}`;
|
|
expect(allowed(points[index - 1], points[index]), detail).toBe(true);
|
|
expect(allowed(points[index], points[index - 1]), detail).toBe(true);
|
|
}
|
|
}
|
|
}
|
|
} finally {
|
|
world.free();
|
|
for (const group of [source, prepared]) group.traverse((node) => { if ((node as Mesh).isMesh) (node as Mesh).geometry.dispose(); });
|
|
}
|
|
}, 120000);
|
|
|
|
it("every generated seam matches actual shipping navigation vertices", async () => {
|
|
for (const [id] of packages) {
|
|
const definition = requireDungeonDefinition(id);
|
|
const ramps = definition.traversalRamps as readonly DungeonTraversalRamp[];
|
|
const bridges = ramps.reduce((count, ramp) => count + (ramp.navigationBridges?.length ?? 0), 0);
|
|
if (!bridges) continue;
|
|
const geometries = await readDungeonGeometry(path.join(process.cwd(), "public", definition.assets.navigation!.url!));
|
|
const geometry = geometries.sort((a, b) => b.getAttribute("position").count - a.getAttribute("position").count)[0];
|
|
try {
|
|
const originalCount = geometry.index!.count;
|
|
smoothDungeonStairGeometry(geometry, ramps, 0.05);
|
|
expect(() => bridgeDungeonStairNavigation(geometry, ramps, 0.05)).not.toThrow();
|
|
expect(geometry.index!.count).toBe(originalCount + bridges * 6);
|
|
} finally { geometries.forEach((entry) => entry.dispose()); }
|
|
}
|
|
}, 120000);
|
|
});
|