import { describe, expect, it } from "vitest"; import { createLoopedPath, hasUsableLoopedMobPath, normalizeMobPopulationPaths, sampleLoopedPath, samplePackFormation, samplePackMember, WAILING_CAVERNS_BOSS_SPAWNS, WAILING_CAVERNS_ENTRANCE_SPAWNS, WAILING_CAVERNS_ENTITIES, WAILING_CAVERNS_ROAMING_PACKS, WAILING_CAVERNS_STATIC_SPAWNS, } from "./mobPopulation"; import { DUNGEON_MANIFEST } from "./dungeonManifest"; import { DUNGEON_DEFINITIONS } from "./dungeonRegistry"; const square = createLoopedPath([ [0, 0, 0], [10, 0, 0], [10, 0, 10], [0, 0, 10], ]); describe("looped mob path sampling", () => { it("precomputes a closed loop including the final-to-first segment", () => { expect(square.segments).toHaveLength(4); expect(square.totalLength).toBeCloseTo(40); }); it("interpolates positions and returns a normalized forward vector", () => { const position: [number, number, number] = [0, 0, 0]; const forward: [number, number, number] = [0, 0, 0]; expect(sampleLoopedPath(square, 15, position, forward)).toBe(1); expect(position).toEqual([10, 0, 5]); expect(forward).toEqual([0, 0, 1]); }); it("wraps distances in both directions", () => { const position: [number, number, number] = [0, 0, 0]; const forward: [number, number, number] = [0, 0, 0]; sampleLoopedPath(square, 42.5, position, forward); expect(position).toEqual([2.5, 0, 0]); sampleLoopedPath(square, -2.5, position, forward); expect(position).toEqual([0, 0, 2.5]); expect(forward).toEqual([0, 0, -1]); }); it("keeps follower trailing distance on the path and applies lateral formation offset", () => { const position: [number, number, number] = [0, 0, 0]; const forward: [number, number, number] = [0, 0, 0]; samplePackMember(square, 15, [2, 0.5, 3], position, forward); expect(position).toEqual([12, 0.5, 2]); expect(forward).toEqual([0, 0, 1]); }); it("updates stable per-member buffers used by roaming proximity checks", () => { const members = [ { id: "leader", entityId: "leader", role: "leader", formationOffset: [0, 0, 0] }, { id: "follower", entityId: "follower", role: "follower", formationOffset: [2, 0.5, 3] }, ] as const; const positions: [number, number, number][] = [[0, 0, 0], [0, 0, 0]]; const forwards: [number, number, number][] = [[0, 0, 1], [0, 0, 1]]; const leaderPosition = positions[0]; samplePackFormation(square, 15, members, positions, forwards); expect(positions).toEqual([[10, 0, 5], [12, 0.5, 2]]); samplePackFormation(square, 25, members, positions, forwards); expect(positions[0]).toBe(leaderPosition); expect(positions).toEqual([[5, 0, 10], [8, 0.5, 12]]); }); it("rejects paths without distinct points", () => { expect(() => createLoopedPath([[1, 2, 3]])).toThrow(/at least two waypoints/i); expect(() => createLoopedPath([[1, 2, 3], [1, 2, 3]])).toThrow(/distinct waypoints/i); }); it("converts malformed imported patrols into stationary spawns", () => { const pack = { id: "stationary-patrol", name: "Stationary Patrol", speed: 1, formationSource: "database-solo", spawnMask: 2, waypoints: [[4, 5, 6], [4, 5, 6]], members: [{ id: "member", entityId: "guard", role: "leader", formationOffset: [1, 0.5, 2], }], } as const; const normalized = normalizeMobPopulationPaths([pack], []); expect(normalized.roamingPacks).toEqual([]); expect(normalized.staticSpawns).toEqual([{ id: "stationary-patrol:member", entityId: "guard", position: [5, 5.5, 4], spawnMask: 2, }]); }); it("keeps every packaged dungeon patrol on a usable loop", () => { expect(DUNGEON_DEFINITIONS).toHaveLength(86); for (const dungeon of DUNGEON_DEFINITIONS) { for (const pack of dungeon.roamingPacks) { expect(hasUsableLoopedMobPath(pack.waypoints), `${dungeon.id}:${pack.id}`).toBe(true); } } }); it("keeps the shipped patrol population internally consistent", () => { expect(WAILING_CAVERNS_ROAMING_PACKS).toHaveLength(6); const pathIds = new Set(); for (const pack of WAILING_CAVERNS_ROAMING_PACKS) { expect(pack.pathId).toBeTypeOf("number"); expect(pathIds.has(pack.pathId!)).toBe(false); pathIds.add(pack.pathId!); expect(() => createLoopedPath(pack.waypoints)).not.toThrow(); for (const member of pack.members) { expect(WAILING_CAVERNS_ENTITIES[member.entityId as keyof typeof WAILING_CAVERNS_ENTITIES]).toBeDefined(); } } }); it("does not duplicate roaming Kresh as a stationary boss", () => { expect(WAILING_CAVERNS_ROAMING_PACKS.some((pack) => pack.members.some((member) => member.entityId === "kresh"))).toBe(true); expect(WAILING_CAVERNS_BOSS_SPAWNS.some((spawn) => spawn.entityId === "kresh")).toBe(false); }); it("keeps a renderable encounter within immediate entrance range", () => { const entrance = DUNGEON_MANIFEST.entrance.footPosition; expect(WAILING_CAVERNS_ENTRANCE_SPAWNS).toHaveLength(3); for (const spawn of WAILING_CAVERNS_ENTRANCE_SPAWNS) { const distance = Math.hypot( spawn.position[0] - entrance[0], spawn.position[1] - entrance[1], spawn.position[2] - entrance[2], ); expect(distance).toBeLessThan(22); expect(WAILING_CAVERNS_ENTITIES[spawn.entityId as keyof typeof WAILING_CAVERNS_ENTITIES]).toBeDefined(); expect(WAILING_CAVERNS_STATIC_SPAWNS).toContain(spawn); } }); });