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); }