Update Healer Man server

This commit is contained in:
phenom committed 2026-10-06 22:25:34 -04:00
1 parent 6114a2295f
commit 69809f7228
593 files changed
+903643 -40774

No files matched your search

+493 -8
View File
@@ -1,6 +1,11 @@
import type { BufferGeometry } from "three";
export const PARTY_NAVIGATION_WELD_TOLERANCE = 0.001;
export const MAX_PARTY_NAVIGATION_DETOUR_POLYGONS = 50_000;
export const MAX_PARTY_NAVIGATION_DETOUR_LINKS = 100_000;
export const MAX_PARTY_NAVIGATION_DETAIL_TRIANGLES = 200_000;
export const PARTY_NAVIGATION_SURFACE_BUCKET_SIZE = 8;
const MAX_PARTY_NAVIGATION_SURFACE_BUCKET_REFERENCES = 2_000_000;
export type PartyNavigationPosition = readonly [x: number, y: number, z: number];
export type PartyNavigationPoint = PartyNavigationPosition | Readonly<{ x: number; y: number; z: number }>;
@@ -8,6 +13,8 @@ export type PartyNavigationPoint = PartyNavigationPosition | Readonly<{ x: numbe
export interface PartyNavigationEdge {
readonly to: number;
readonly cost: number;
/** Center of an exact Detour portal; present only for a source-pinned graph. */
readonly point?: PartyNavigationPosition;
}
export interface PartyNavigationNode {
@@ -33,7 +40,7 @@ export interface PartyNavigationTriangleNode {
export interface PartyNavigationGraph {
readonly nodes: readonly PartyNavigationNode[];
/** Triangle node indices retained by the builder from the largest connected surface. */
/** Triangle node indices retained by the builder, in matching node-index order. */
readonly triangles?: readonly PartyNavigationTriangle[];
/** Shared-edge adjacency for the retained triangles, in matching index order. */
readonly triangleNodes?: readonly PartyNavigationTriangleNode[];
@@ -44,11 +51,36 @@ export interface PartyNavigationGraph {
readonly weldedNodeCount: number;
readonly componentCount: number;
readonly discardedNodeCount: number;
/** Packed original detail triangles and their source Detour polygon IDs. */
readonly detourSurface?: PartyNavigationDetourSurface;
}
export interface PartyNavigationDetourSurface {
/** Three positions per triangle, in the original GLB detail-mesh order. */
readonly positions: Float32Array;
readonly polygonByTriangle: Uint32Array;
readonly bucketSize: number;
readonly buckets: ReadonlyMap<string, readonly number[]>;
}
export interface PartyNavigationDetourTopology {
readonly schemaVersion: 1;
readonly kind: "recast-detour-surface-links";
readonly polygonCount: number;
readonly detailTriangleCount: number;
readonly geometryTriangleCount: number;
readonly directedLinkCount: number;
readonly unrepresentedLinkCount: number;
/** Encoded records from the `runewakerNavigationTopology` GLB extra. */
readonly polygons: readonly (readonly number[])[];
readonly links: readonly (readonly number[])[];
}
export interface PartyNavigationBuildOptions {
/** Validation may retain floor triangles while gameplay routes off-mesh links through nodes. */
readonly retainSurfaceTriangles?: boolean;
/** Keep every disconnected source island selectable; routes still cannot cross components without links. */
readonly retainAllComponents?: boolean;
readonly weldTolerance?: number;
/** Defensive runtime ceiling; checked before graph allocations begin. */
readonly maxSourceTriangles?: number;
@@ -100,6 +132,7 @@ export interface PartyNavigationSurfaceMatch {
readonly position: PartyNavigationPosition;
readonly triangleId: number;
readonly distance: number;
readonly polygonId?: number;
}
const DISTANCE_EPSILON = 1e-9;
@@ -226,9 +259,9 @@ export function buildPartyNavigationTriangleNodes(
/**
* Converts an indexed or non-indexed triangle geometry into a deterministic
* navigation graph. Export seams within one millimeter are welded, then only
* the largest connected surface is retained so an isolated island cannot win
* a nearest-node query.
* navigation graph. Export seams within one millimeter are welded. By default
* only the largest connected surface is retained; source imports can opt into
* preserving all disconnected surfaces for local queries and routing.
*/
export function buildPartyNavigationGraph(
geometry: BufferGeometry,
@@ -391,12 +424,15 @@ export function buildPartyNavigationGraph(
: best;
}, []);
if (!largest.length) throw new Error("Party navigation geometry produced no usable graph nodes.");
const retained = options.retainAllComponents
? components.flat()
: largest;
const oldToNew = new Int32Array(welded.length);
oldToNew.fill(-1);
largest.forEach((oldId, newId) => { oldToNew[oldId] = newId; });
retained.forEach((oldId, newId) => { oldToNew[oldId] = newId; });
const retainedPositions = largest.map((oldId): PartyNavigationPosition => {
const retainedPositions = retained.map((oldId): PartyNavigationPosition => {
const node = welded[oldId];
return Object.freeze([
node.sumX / node.samples,
@@ -404,7 +440,7 @@ export function buildPartyNavigationGraph(
node.sumZ / node.samples,
]) as PartyNavigationPosition;
});
const nodes = largest.map((oldId, id): PartyNavigationNode => {
const nodes = retained.map((oldId, id): PartyNavigationNode => {
const neighbors = Array.from(welded[oldId].neighbors)
.filter((neighbor) => oldToNew[neighbor] >= 0)
.filter((neighbor) => !blockedOffMeshDirections.has(`${oldId}:${neighbor}`))
@@ -447,6 +483,323 @@ export function buildPartyNavigationGraph(
});
}
function topologyRecordNumber(record: readonly number[], index: number, label: string): number {
const value = record[index];
if (typeof value !== "number" || !Number.isFinite(value)) {
throw new Error(`Party navigation Detour topology ${label} contains a non-finite value.`);
}
return value;
}
function topologyInteger(
record: readonly number[],
index: number,
label: string,
minimum = 0,
): number {
const value = topologyRecordNumber(record, index, label);
if (!Number.isInteger(value) || value < minimum) {
throw new Error(`Party navigation Detour topology ${label} contains an invalid integer.`);
}
return value;
}
/**
* Builds the runtime graph from Recast's serialized Detour polygons and links.
* Surface projection continues to use the original detail triangles, while
* A* follows only the directed links authored by Detour.
*/
export function buildPartyNavigationGraphFromDetourTopology(
geometry: BufferGeometry,
rawTopology: unknown,
): PartyNavigationGraph {
if (!rawTopology || typeof rawTopology !== "object") {
throw new Error("Party navigation Detour topology must be an object.");
}
const topology = rawTopology as Partial<PartyNavigationDetourTopology> & {
readonly unrepresentedLinkCount?: number;
};
if (topology.schemaVersion !== 1 || topology.kind !== "recast-detour-surface-links") {
throw new Error("Party navigation Detour topology has an unsupported schema.");
}
const polygonCount = topology.polygonCount;
const detailTriangleCount = topology.detailTriangleCount;
const geometryTriangleCount = topology.geometryTriangleCount;
const directedLinkCount = topology.directedLinkCount;
if (
typeof polygonCount !== "number"
|| !Number.isInteger(polygonCount)
|| polygonCount <= 0
|| polygonCount > MAX_PARTY_NAVIGATION_DETOUR_POLYGONS
) {
throw new Error(`Party navigation Detour topology exceeds the ${MAX_PARTY_NAVIGATION_DETOUR_POLYGONS}-polygon limit.`);
}
if (
typeof directedLinkCount !== "number"
|| !Number.isInteger(directedLinkCount)
|| directedLinkCount < 0
|| directedLinkCount > MAX_PARTY_NAVIGATION_DETOUR_LINKS
) {
throw new Error(`Party navigation Detour topology exceeds the ${MAX_PARTY_NAVIGATION_DETOUR_LINKS}-link limit.`);
}
if (
typeof detailTriangleCount !== "number"
|| !Number.isInteger(detailTriangleCount)
|| detailTriangleCount <= 0
|| detailTriangleCount > MAX_PARTY_NAVIGATION_DETAIL_TRIANGLES
|| typeof geometryTriangleCount !== "number"
|| !Number.isInteger(geometryTriangleCount)
|| geometryTriangleCount !== detailTriangleCount
) {
throw new Error(`Party navigation Detour detail geometry exceeds the ${MAX_PARTY_NAVIGATION_DETAIL_TRIANGLES}-triangle limit or has inconsistent counts.`);
}
if (
!Array.isArray(topology.polygons)
|| topology.polygons.length !== polygonCount
|| !Array.isArray(topology.links)
|| topology.links.length !== directedLinkCount
|| topology.unrepresentedLinkCount !== 0
) {
throw new Error("Party navigation Detour topology records are incomplete.");
}
const position = geometry.getAttribute("position");
if (!position || position.itemSize < 3 || position.count < 3) {
throw new Error("Party navigation detail geometry requires at least three 3D positions.");
}
const index = geometry.getIndex();
const indexCount = index?.count ?? position.count;
if (indexCount % 3 !== 0 || indexCount / 3 !== geometryTriangleCount) {
throw new Error("Party navigation Detour topology does not match the detail triangle geometry.");
}
const polygonByTriangle = new Int32Array(detailTriangleCount);
polygonByTriangle.fill(-1);
const polygonVertices: number[] = [];
for (let polygonId = 0; polygonId < polygonCount; polygonId += 1) {
const record = topology.polygons[polygonId];
if (!Array.isArray(record) || record.length !== 9) {
throw new Error(`Party navigation Detour polygon ${polygonId} has an invalid record.`);
}
const id = topologyInteger(record, 0, `polygon ${polygonId} id`);
const tileIndex = topologyInteger(record, 1, `polygon ${polygonId} tile`);
topologyInteger(record, 2, `polygon ${polygonId} tile x`, Number.MIN_SAFE_INTEGER);
topologyInteger(record, 3, `polygon ${polygonId} tile y`, Number.MIN_SAFE_INTEGER);
topologyInteger(record, 4, `polygon ${polygonId} tile layer`, Number.MIN_SAFE_INTEGER);
topologyInteger(record, 5, `polygon ${polygonId} tile polygon`);
const firstTriangle = topologyInteger(record, 6, `polygon ${polygonId} first triangle`);
const triangleCount = topologyInteger(record, 7, `polygon ${polygonId} triangle count`);
const vertexCount = topologyInteger(record, 8, `polygon ${polygonId} vertex count`);
if (
id !== polygonId
|| tileIndex < 0
|| triangleCount <= 0
|| vertexCount < 3
|| vertexCount > 12
|| firstTriangle + triangleCount > detailTriangleCount
) {
throw new Error(`Party navigation Detour polygon ${polygonId} has an invalid detail range.`);
}
for (let triangleId = firstTriangle; triangleId < firstTriangle + triangleCount; triangleId += 1) {
if (polygonByTriangle[triangleId] !== -1) {
throw new Error(`Party navigation Detour detail triangle ${triangleId} is assigned more than once.`);
}
polygonByTriangle[triangleId] = polygonId;
}
polygonVertices.push(vertexCount);
}
for (let triangleId = 0; triangleId < detailTriangleCount; triangleId += 1) {
if (polygonByTriangle[triangleId] < 0) {
throw new Error(`Party navigation Detour detail triangle ${triangleId} has no source polygon.`);
}
}
const trianglePositions = new Float32Array(detailTriangleCount * 9);
const weightedCenters = new Float64Array(polygonCount * 4);
const triangleIndex = (offset: number): number => {
const value = index ? index.getX(offset) : offset;
if (!Number.isInteger(value) || value < 0 || value >= position.count) {
throw new Error(`Party navigation detail index ${offset} references an invalid vertex.`);
}
return value;
};
const bucketSize = PARTY_NAVIGATION_SURFACE_BUCKET_SIZE;
const buckets = new Map<string, number[]>();
let bucketReferences = 0;
for (let triangleId = 0; triangleId < detailTriangleCount; triangleId += 1) {
const positionOffset = triangleId * 9;
const sourceOffset = triangleId * 3;
const vertexIds = [
triangleIndex(sourceOffset),
triangleIndex(sourceOffset + 1),
triangleIndex(sourceOffset + 2),
];
const points = vertexIds.map((vertexId): PartyNavigationPosition => {
const point: PartyNavigationPosition = [
position.getX(vertexId),
position.getY(vertexId),
position.getZ(vertexId),
];
if (!point.every(Number.isFinite)) {
throw new Error(`Party navigation detail triangle ${triangleId} has a non-finite vertex.`);
}
return point;
});
for (let vertex = 0; vertex < 3; vertex += 1) {
trianglePositions[positionOffset + vertex * 3] = points[vertex][0];
trianglePositions[positionOffset + vertex * 3 + 1] = points[vertex][1];
trianglePositions[positionOffset + vertex * 3 + 2] = points[vertex][2];
}
const [a, b, c] = points;
const ab: PartyNavigationPosition = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
const ac: PartyNavigationPosition = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
const crossX = ab[1] * ac[2] - ab[2] * ac[1];
const crossY = ab[2] * ac[0] - ab[0] * ac[2];
const crossZ = ab[0] * ac[1] - ab[1] * ac[0];
const area = Math.max(1e-12, Math.hypot(crossX, crossY, crossZ) * 0.5);
const polygonId = polygonByTriangle[triangleId];
const centerOffset = polygonId * 4;
weightedCenters[centerOffset] += ((a[0] + b[0] + c[0]) / 3) * area;
weightedCenters[centerOffset + 1] += ((a[1] + b[1] + c[1]) / 3) * area;
weightedCenters[centerOffset + 2] += ((a[2] + b[2] + c[2]) / 3) * area;
weightedCenters[centerOffset + 3] += area;
const minCellX = Math.floor(Math.min(a[0], b[0], c[0]) / bucketSize);
const maxCellX = Math.floor(Math.max(a[0], b[0], c[0]) / bucketSize);
const minCellZ = Math.floor(Math.min(a[2], b[2], c[2]) / bucketSize);
const maxCellZ = Math.floor(Math.max(a[2], b[2], c[2]) / bucketSize);
const references = (maxCellX - minCellX + 1) * (maxCellZ - minCellZ + 1);
if (
!Number.isSafeInteger(references)
|| references <= 0
|| bucketReferences + references > MAX_PARTY_NAVIGATION_SURFACE_BUCKET_REFERENCES
) {
throw new Error("Party navigation Detour detail surface exceeds the spatial-index budget.");
}
bucketReferences += references;
for (let cellX = minCellX; cellX <= maxCellX; cellX += 1) {
for (let cellZ = minCellZ; cellZ <= maxCellZ; cellZ += 1) {
const key = bucketKey(cellX, cellZ, 0);
const contents = buckets.get(key);
if (contents) contents.push(triangleId);
else buckets.set(key, [triangleId]);
}
}
}
const centers: PartyNavigationPosition[] = [];
const weakNeighbors = Array.from({ length: polygonCount }, () => new Set<number>());
const outgoing: Array<PartyNavigationEdge[]> = Array.from({ length: polygonCount }, () => []);
for (let polygonId = 0; polygonId < polygonCount; polygonId += 1) {
const offset = polygonId * 4;
const weight = weightedCenters[offset + 3];
if (!Number.isFinite(weight) || weight <= 0) {
throw new Error(`Party navigation Detour polygon ${polygonId} has no usable detail surface.`);
}
centers.push(Object.freeze([
weightedCenters[offset] / weight,
weightedCenters[offset + 1] / weight,
weightedCenters[offset + 2] / weight,
]) as PartyNavigationPosition);
}
for (let linkId = 0; linkId < directedLinkCount; linkId += 1) {
const link = topology.links[linkId];
if (!Array.isArray(link) || link.length !== 14) {
throw new Error(`Party navigation Detour link ${linkId} has an invalid record.`);
}
const from = topologyInteger(link, 0, `link ${linkId} source`);
const to = topologyInteger(link, 1, `link ${linkId} destination`);
const edge = topologyInteger(link, 2, `link ${linkId} edge`);
const side = topologyInteger(link, 3, `link ${linkId} side`);
const bmin = topologyInteger(link, 4, `link ${linkId} portal start`);
const bmax = topologyInteger(link, 5, `link ${linkId} portal end`);
topologyInteger(link, 6, `link ${linkId} first vertex`);
topologyInteger(link, 7, `link ${linkId} second vertex`);
if (
from >= polygonCount
|| to >= polygonCount
|| edge >= polygonVertices[from]
|| (side > 7 && side !== 255)
|| bmin > 255
|| bmax > 255
|| bmin > bmax
) {
throw new Error(`Party navigation Detour link ${linkId} references an invalid source portal.`);
}
const start: PartyNavigationPosition = [
topologyRecordNumber(link, 8, `link ${linkId} start x`),
topologyRecordNumber(link, 9, `link ${linkId} start y`),
topologyRecordNumber(link, 10, `link ${linkId} start z`),
];
const end: PartyNavigationPosition = [
topologyRecordNumber(link, 11, `link ${linkId} end x`),
topologyRecordNumber(link, 12, `link ${linkId} end y`),
topologyRecordNumber(link, 13, `link ${linkId} end z`),
];
// Detour uses side=0xff for an internal full-edge link. Boundary links
// encode the traversable edge interval as bmin/bmax in byte coordinates.
const ratio = side === 255 ? 0.5 : (bmin + bmax) / 510;
const portal = Object.freeze([
start[0] + (end[0] - start[0]) * ratio,
start[1] + (end[1] - start[1]) * ratio,
start[2] + (end[2] - start[2]) * ratio,
]) as PartyNavigationPosition;
outgoing[from].push(Object.freeze({
to,
cost: distance(centers[from], centers[to]),
point: portal,
}));
weakNeighbors[from].add(to);
weakNeighbors[to].add(from);
}
const nodes = centers.map((center, id): PartyNavigationNode => Object.freeze({
id,
position: center,
neighbors: Object.freeze(outgoing[id].sort((left, right) => (
left.to - right.to
|| (left.point?.[0] ?? 0) - (right.point?.[0] ?? 0)
|| (left.point?.[1] ?? 0) - (right.point?.[1] ?? 0)
|| (left.point?.[2] ?? 0) - (right.point?.[2] ?? 0)
))),
}));
const visited = new Uint8Array(polygonCount);
let componentCount = 0;
for (let seed = 0; seed < polygonCount; seed += 1) {
if (visited[seed]) continue;
componentCount += 1;
visited[seed] = 1;
const pending = [seed];
while (pending.length) {
const current = pending.pop()!;
for (const neighbor of weakNeighbors[current]) {
if (visited[neighbor]) continue;
visited[neighbor] = 1;
pending.push(neighbor);
}
}
}
const detourSurface: PartyNavigationDetourSurface = Object.freeze({
positions: trianglePositions,
polygonByTriangle: Uint32Array.from(polygonByTriangle),
bucketSize,
buckets: new Map(Array.from(buckets, ([key, values]) => [key, Object.freeze(values)])),
});
return Object.freeze({
nodes: Object.freeze(nodes),
detourSurface,
weldTolerance: PARTY_NAVIGATION_WELD_TOLERANCE,
sourceVertexCount: position.count,
sourceTriangleCount: geometryTriangleCount,
weldedNodeCount: polygonCount,
componentCount,
discardedNodeCount: 0,
});
}
export function findNearestPartyNavigationNode(
graph: PartyNavigationGraph,
point: PartyNavigationPoint,
@@ -571,6 +924,70 @@ export function findNearestPartyNavigationSurface(
point: PartyNavigationPoint,
maxDistance = Number.POSITIVE_INFINITY,
): PartyNavigationSurfaceMatch | null {
if (graph.detourSurface) {
const target = pointComponents(point);
const limit = Number.isFinite(maxDistance) ? Math.max(0, maxDistance) : Number.POSITIVE_INFINITY;
const surface = graph.detourSurface;
const candidates: number[] = [];
if (!Number.isFinite(limit)) {
for (let triangleId = 0; triangleId < surface.polygonByTriangle.length; triangleId += 1) {
candidates.push(triangleId);
}
} else {
const minCellX = Math.floor((target[0] - limit) / surface.bucketSize);
const maxCellX = Math.floor((target[0] + limit) / surface.bucketSize);
const minCellZ = Math.floor((target[2] - limit) / surface.bucketSize);
const maxCellZ = Math.floor((target[2] + limit) / surface.bucketSize);
const cellCount = (maxCellX - minCellX + 1) * (maxCellZ - minCellZ + 1);
if (!Number.isSafeInteger(cellCount) || cellCount > 4_096) {
for (let triangleId = 0; triangleId < surface.polygonByTriangle.length; triangleId += 1) {
candidates.push(triangleId);
}
} else {
const found = new Set<number>();
for (let cellX = minCellX; cellX <= maxCellX; cellX += 1) {
for (let cellZ = minCellZ; cellZ <= maxCellZ; cellZ += 1) {
for (const triangleId of surface.buckets.get(bucketKey(cellX, cellZ, 0)) ?? []) {
found.add(triangleId);
}
}
}
candidates.push(...Array.from(found).sort((left, right) => left - right));
}
}
let nearestPosition: PartyNavigationPosition | null = null;
let nearestTriangleId = -1;
let nearestDistanceSquared = limit * limit;
for (const triangleId of candidates) {
const offset = triangleId * 9;
const candidate = closestPointOnTriangle(
target,
[surface.positions[offset], surface.positions[offset + 1], surface.positions[offset + 2]],
[surface.positions[offset + 3], surface.positions[offset + 4], surface.positions[offset + 5]],
[surface.positions[offset + 6], surface.positions[offset + 7], surface.positions[offset + 8]],
);
const candidateDistanceSquared = distanceSquared(target, candidate);
if (
candidateDistanceSquared < nearestDistanceSquared - DISTANCE_EPSILON
|| (Math.abs(candidateDistanceSquared - nearestDistanceSquared) <= DISTANCE_EPSILON
&& (nearestTriangleId < 0 || triangleId < nearestTriangleId))
) {
nearestPosition = candidate;
nearestTriangleId = triangleId;
nearestDistanceSquared = candidateDistanceSquared;
}
}
return nearestPosition
? Object.freeze({
position: Object.freeze([...nearestPosition]) as PartyNavigationPosition,
triangleId: nearestTriangleId,
polygonId: surface.polygonByTriangle[nearestTriangleId],
distance: Math.sqrt(nearestDistanceSquared),
})
: null;
}
const triangles = graph.triangles;
if (!triangles?.length) {
const nearest = findNearestPartyNavigationNode(graph, point, maxDistance);
@@ -703,7 +1120,9 @@ function findPartyNavigationNodePath(
graph: PartyNavigationGraph,
startId: number,
endId: number,
blocked?: Uint8Array,
): readonly number[] | null {
if (blocked?.[startId] || blocked?.[endId]) return null;
if (startId === endId) return Object.freeze([startId]);
const nodeCount = graph.nodes.length;
const cameFrom = new Int32Array(nodeCount);
@@ -723,7 +1142,7 @@ function findPartyNavigationNodePath(
closed[current.id] = 1;
for (const edge of graph.nodes[current.id].neighbors) {
if (closed[edge.to]) continue;
if (blocked?.[edge.to] || closed[edge.to]) continue;
const tentative = scores[current.id] + edge.cost;
if (tentative >= scores[edge.to] - DISTANCE_EPSILON) continue;
cameFrom[edge.to] = current.id;
@@ -833,6 +1252,72 @@ export function findPartyNavigationPath(
end: PartyNavigationPoint,
options: PartyNavigationPathOptions = {},
): readonly PartyNavigationPosition[] | null {
if (graph.detourSurface) {
const maxSnapDistance = options.maxSnapDistance ?? Number.POSITIVE_INFINITY;
const startMatch = findNearestPartyNavigationSurface(
graph,
start,
options.maxStartSnapDistance ?? maxSnapDistance,
);
const endMatch = findNearestPartyNavigationSurface(
graph,
end,
options.maxEndSnapDistance ?? maxSnapDistance,
);
if (
!startMatch
|| !endMatch
|| startMatch.polygonId === undefined
|| endMatch.polygonId === undefined
) return null;
const blocked = new Uint8Array(graph.nodes.length);
const maximumReroutes = Math.min(64, graph.nodes.length);
for (let reroute = 0; reroute <= maximumReroutes; reroute += 1) {
const polygonIds = findPartyNavigationNodePath(
graph,
startMatch.polygonId,
endMatch.polygonId,
blocked,
);
if (!polygonIds) return null;
const path: PartyNavigationPosition[] = [startMatch.position];
let missingPortal = false;
for (let index = 0; index < polygonIds.length - 1; index += 1) {
const edge = graph.nodes[polygonIds[index]].neighbors.find((candidate) => (
candidate.to === polygonIds[index + 1] && candidate.point !== undefined
));
if (!edge?.point) {
missingPortal = true;
break;
}
path.push(edge.point);
}
if (missingPortal) return null;
path.push(endMatch.position);
let rejectedSegment = -1;
if (options.routeSegmentAllowed) {
for (let index = 0; index < path.length - 1; index += 1) {
if (!options.routeSegmentAllowed(path[index], path[index + 1])) {
rejectedSegment = index;
break;
}
}
}
if (rejectedSegment < 0) return Object.freeze(path);
if (polygonIds.length <= 1 || reroute >= maximumReroutes) return null;
const blockedPolygonId = rejectedSegment === 0
? polygonIds[1]
: rejectedSegment === polygonIds.length - 1
? polygonIds[polygonIds.length - 2]
: polygonIds[rejectedSegment];
if (blocked[blockedPolygonId]) return null;
blocked[blockedPolygonId] = 1;
}
return null;
}
if (!graph.triangles?.length) {
return findPartyNavigationNodeSnappedPath(graph, start, end, options);
}