import { BufferGeometry, Float32BufferAttribute } from "three"; import { describe, expect, it } from "vitest"; import { bridgeDungeonStairNavigation, smoothDungeonStairGeometry } from "./dungeonTraversalRamps"; // The offline Node 24 pipeline uses native .ts specifiers; load it at runtime, // without making those tool-only modules part of the browser TypeScript build. const pipelineModule = "../../scripts/dungeon-pipeline/wow-stair-geometry.ts"; const { detectDungeonStairRamps, dungeonStairNavigationBridges, dungeonStairRisers } = await import(pipelineModule); function staircase(steps: number, rotated = false) { const vertices: number[] = []; for (let step = 0; step < steps; step += 1) { const x = step; const y = step * 0.5; const points = [[x, y, -2], [x, y + 0.5, -2], [x, y + 0.5, 2], [x, y, -2], [x, y + 0.5, 2], [x, y, 2]]; for (const point of points) vertices.push(...(rotated ? [-point[2], point[1], point[0]] : point)); } const geometry = new BufferGeometry(); geometry.setAttribute("position", new Float32BufferAttribute(vertices, 3)); return geometry; } describe("WoW shipped stair detection", () => { it.each([false, true])("detects repeated risers, including rotated stairs (%s)", (rotated) => { const geometry = staircase(7, rotated); const ramps = detectDungeonStairRamps(dungeonStairRisers(geometry)); expect(ramps).toHaveLength(1); expect(ramps[0].width).toBeCloseTo(4.02); expect(ramps[0].end[1] - ramps[0].start[1]).toBeCloseTo(3.5); geometry.dispose(); }); it("does not interpret a single ledge or tall wall as a staircase", () => { const ledge = staircase(1); expect(detectDungeonStairRamps(dungeonStairRisers(ledge))).toEqual([]); const wall = staircase(4); const p = wall.getAttribute("position"); for (let index = 0; index < p.count; index += 1) p.setY(index, p.getY(index) * 10); expect(detectDungeonStairRamps(dungeonStairRisers(wall))).toEqual([]); ledge.dispose(); wall.dispose(); }); it("fills separate stair portals using existing source vertices, without bridging an unrelated floor", () => { const geometry = new BufferGeometry(); geometry.setAttribute("position", new Float32BufferAttribute([ -1, 0, 0, 1, 0, 0, -1, 0.5, 2, 1, 0.5, 2, -1, 0.75, 3, 1, 0.75, 3, -1, 1.5, 6, 1, 1.5, 6, -1, 8, 3, 1, 8, 3, -1, 8, 4, ], 3)); geometry.setIndex([0, 1, 3, 0, 3, 2, 4, 5, 7, 4, 7, 6, 8, 9, 10]); const ramps = dungeonStairNavigationBridges(geometry, [{ id: "flight", start: [0, 0, -0.01], end: [0, 1.5, 6.01], width: 3 }]); expect(ramps[0].navigationBridges).toHaveLength(1); smoothDungeonStairGeometry(geometry, ramps, 0.05); expect(() => bridgeDungeonStairNavigation(geometry, ramps, 0.05)).not.toThrow(); expect(geometry.index!.count).toBe(21); expect(geometry.getAttribute("position").getY(8)).toBe(8); geometry.dispose(); }); });