import { describe, expect, it } from "vitest"; import type { PartyNavigationGraph } from "./partyNavigation"; import { emptyPartyRouteState, nextPartyRouteRecoveryWaypoint, nextPartyRouteWaypoint, partyRouteNeedsPlanning, planPartyRoute, } from "./partyPathing"; const corridorGraph: PartyNavigationGraph = { nodes: [ { id: 0, position: [0, 0, 0], neighbors: [{ to: 1, cost: 2 }] }, { id: 1, position: [2, 0, 0], neighbors: [{ to: 0, cost: 2 }, { to: 2, cost: 2 }] }, { id: 2, position: [2, 0, 2], neighbors: [{ to: 1, cost: 2 }] }, ], weldTolerance: 0.001, sourceVertexCount: 3, sourceTriangleCount: 1, weldedNodeCount: 3, componentCount: 1, discardedNodeCount: 0, }; describe("party path controller", () => { it("returns approved corridor points in order and appends only a short local finish", () => { const route = planPartyRoute(corridorGraph, "boss", [0, 0, 0], [2.5, 0, 2], 100, { maxSnapDistance: 1, localFinishDistance: 1, waypointReachedDistance: 0.2, directSegmentAllowed: () => true, }); expect(route.blocked).toBe(false); expect(route.waypoints).toEqual([ [0, 0, 0], [2, 0, 0], [2, 0, 2], [2.5, 0, 2], ]); const out: [number, number, number] = [99, 99, 99]; expect(nextPartyRouteWaypoint(route, [0, 0, 0], out, { waypointReachedDistance: 0.2 })).toBe(true); expect(out).toEqual([2, 0, 0]); }); it("holds instead of exposing a raw destination when no approved route exists", () => { const route = planPartyRoute(corridorGraph, "unreachable", [0, 0, 0], [50, 0, 50], 100, { maxSnapDistance: 2, }); const out: [number, number, number] = [7, 8, 9]; expect(route.blocked).toBe(true); expect(route.waypoints).toEqual([]); expect(nextPartyRouteWaypoint(route, [0, 0, 0], out)).toBe(false); expect(out).toEqual([7, 8, 9]); }); it("replans on goal changes, material destination movement, and blocked retries", () => { const empty = emptyPartyRouteState(); expect(partyRouteNeedsPlanning(empty, "player", [0, 0, 0], [1, 0, 0], 0)).toBe(true); const route = planPartyRoute(corridorGraph, "player", [0, 0, 0], [2, 0, 2], 100); expect(partyRouteNeedsPlanning(route, "mob", [0, 0, 0], [2, 0, 2], 200)).toBe(true); expect(partyRouteNeedsPlanning(route, "player", [0, 0, 0], [4.5, 0, 2], 800)).toBe(true); const blocked = planPartyRoute(null, "boss", [0, 0, 0], [2, 0, 2], 100); expect(partyRouteNeedsPlanning(blocked, "boss", [0, 0, 0], [2, 0, 2], 1_000)).toBe(false); expect(partyRouteNeedsPlanning(blocked, "boss", [0, 0, 0], [2, 0, 2], 1_600)).toBe(true); }); it("rejects an off-surface connector unless collision validation approves it", () => { const rejected = planPartyRoute(corridorGraph, "mob", [0, 0, 0], [2.5, 0, 2], 100, { maxSnapDistance: 1, localFinishDistance: 1, directSegmentAllowed: () => false, }); expect(rejected.blocked).toBe(true); const approved = planPartyRoute(corridorGraph, "mob", [0, 0, 0], [2.5, 0, 2], 100, { maxSnapDistance: 1, localFinishDistance: 1, directSegmentAllowed: () => true, }); expect(approved.blocked).toBe(false); }); it("rejects a graph segment without agent-radius route clearance", () => { const route = planPartyRoute(corridorGraph, "corridor", [0, 0, 0], [2, 0, 2], 100, { maxSnapDistance: 1, directSegmentAllowed: () => true, routeSegmentAllowed: (start, end) => !(start[0] === 2 && end[2] === 2), }); expect(route.blocked).toBe(true); }); it("accepts an in-range surface finish only when combat sight approves it", () => { const options = { maxSnapDistance: 1, localFinishDistance: 1, directSegmentAllowed: () => false }; const visible = planPartyRoute(corridorGraph, "mob", [0, 0, 0], [2.5, 0, 2], 100, { ...options, destinationReached: (surface, target) => Math.hypot(surface[0] - target[0], surface[2] - target[2]) <= 1, }); expect(visible.blocked).toBe(false); expect(visible.waypoints.at(-1)).toEqual([2, 0, 2]); const obstructed = planPartyRoute(corridorGraph, "mob", [0, 0, 0], [2.5, 0, 2], 100, { ...options, destinationReached: () => false, }); expect(obstructed.blocked).toBe(true); }); it("advances only one close-spaced waypoint at a corner", () => { const route = { ...emptyPartyRouteState(), goalKey: "corner", destination: [0.45, 0, 0.45] as const, waypoints: [ [0, 0, 0] as const, [0.45, 0, 0] as const, [0.45, 0, 0.45] as const, ], }; const out: [number, number, number] = [0, 0, 0]; expect(nextPartyRouteWaypoint(route, [0, 0, 0], out, { waypointReachedDistance: 0.12, })).toBe(true); expect(out).toEqual([0.45, 0, 0]); }); it("advances only through the validated prefix of a route blocked later", () => { const route = { ...emptyPartyRouteState(), blocked: true, waypoints: [ [0, 0, 0], [1, 0, 0], [2, 0, 0], ] as const, }; const out: [number, number, number] = [9, 9, 9]; expect(nextPartyRouteRecoveryWaypoint( route, [0, 0, 0], out, (_start, end) => end[0] <= 1, )).toBe(true); expect(out).toEqual([1, 0, 0]); expect(nextPartyRouteRecoveryWaypoint( route, [1, 0, 0], out, (_start, end) => end[0] <= 1, )).toBe(false); }); });