Files
healer-man/src/game/dungeonTraversalRamps.ts
T
2026-09-12 15:14:56 -04:00

78 lines
3.1 KiB
TypeScript

import type { BufferGeometry } from "three";
import type { DungeonTraversalRamp, Vector3Tuple } from "./dungeonTypes";
// Only the stair relief is reshaped. Upper walls, ceilings and other floors
// remain untouched even where they share the staircase's horizontal footprint.
const MAX_STAIR_SURFACE_DELTA = 0.85;
const PORTAL_VERTEX_TOLERANCE = 0.002;
function rampHeight(
ramp: DungeonTraversalRamp,
x: number,
z: number,
): number | null {
const dx = ramp.end[0] - ramp.start[0];
const dz = ramp.end[2] - ramp.start[2];
const length = Math.hypot(dx, dz);
if (
![...ramp.start, ...ramp.end, ramp.width].every(Number.isFinite)
|| length <= 0
|| ramp.width <= 0
) {
throw new Error(`Invalid dungeon traversal ramp: ${ramp.id}.`);
}
const offsetX = x - ramp.start[0];
const offsetZ = z - ramp.start[2];
const along = (offsetX * dx + offsetZ * dz) / length;
const across = (offsetX * dz - offsetZ * dx) / length;
if (along < 0 || along > length || Math.abs(across) > ramp.width * 0.5) return null;
return ramp.start[1] + (ramp.end[1] - ramp.start[1]) * along / length;
}
/** Flatten tread/riser relief onto a shared plane, without removing walls. */
export function smoothDungeonStairGeometry(
geometry: BufferGeometry,
ramps: readonly DungeonTraversalRamp[],
surfaceClearance = 0,
): void {
const position = geometry.getAttribute("position");
for (let vertex = 0; vertex < position.count; vertex += 1) {
for (const ramp of ramps) {
const height = rampHeight(ramp, position.getX(vertex), position.getZ(vertex));
if (height === null || Math.abs(position.getY(vertex) - height) > MAX_STAIR_SURFACE_DELTA) continue;
position.setY(vertex, height + surfaceClearance);
break;
}
}
}
/** Stitch reviewed boundary edges, so routing can use filled triangles rather than vertex-only links. */
export function bridgeDungeonStairNavigation(
geometry: BufferGeometry,
ramps: readonly DungeonTraversalRamp[],
surfaceClearance: number,
): void {
const position = geometry.getAttribute("position");
const indices = geometry.index
? Array.from(geometry.index.array)
: Array.from({ length: position.count }, (_, vertex) => vertex);
const portalVertex = (ramp: DungeonTraversalRamp, point: Vector3Tuple): number => {
const height = rampHeight(ramp, point[0], point[2]);
if (height === null) throw new Error(`${ramp.id}: navigation portal is outside the stair ramp.`);
for (let vertex = 0; vertex < position.count; vertex += 1) {
if (Math.hypot(
position.getX(vertex) - point[0],
position.getY(vertex) - height - surfaceClearance,
position.getZ(vertex) - point[2],
) <= PORTAL_VERTEX_TOLERANCE) return vertex;
}
throw new Error(`${ramp.id}: reviewed navigation portal no longer matches the source GLB.`);
};
for (const ramp of ramps) {
const [a, b] = ramp.navigationBridge.startPortal.map((point) => portalVertex(ramp, point));
const [c, d] = ramp.navigationBridge.endPortal.map((point) => portalVertex(ramp, point));
indices.push(a, b, d, a, d, c);
}
geometry.setIndex(indices);
}