forked from phenom/healer-man
Update Healer Man server
This commit is contained in:
1 parent
6114a2295f
commit
69809f7228
593 files changed
+903643
-40774
No files matched your search
+493
-8
@@ -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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user