@esengine/pathfinding
Version:
寻路系统 | Pathfinding System - A*, Grid, NavMesh
1,911 lines (1,903 loc) • 112 kB
JavaScript
import {
EMPTY_PROGRESS,
PathfindingState
} from "./chunk-YKA3PWU3.js";
import {
createCollisionResolver,
createKDTree,
createORCASolver
} from "./chunk-3VEX32JO.js";
import {
__name,
__publicField
} from "./chunk-T626JPC7.js";
// src/core/IPathfinding.ts
function createPoint(x, y) {
return {
x,
y
};
}
__name(createPoint, "createPoint");
var EMPTY_PATH_RESULT = {
found: false,
path: [],
cost: 0,
nodesSearched: 0
};
function manhattanDistance(a, b) {
return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
}
__name(manhattanDistance, "manhattanDistance");
function euclideanDistance(a, b) {
const dx = a.x - b.x;
const dy = a.y - b.y;
return Math.sqrt(dx * dx + dy * dy);
}
__name(euclideanDistance, "euclideanDistance");
function chebyshevDistance(a, b) {
return Math.max(Math.abs(a.x - b.x), Math.abs(a.y - b.y));
}
__name(chebyshevDistance, "chebyshevDistance");
function octileDistance(a, b) {
const dx = Math.abs(a.x - b.x);
const dy = Math.abs(a.y - b.y);
const D = 1;
const D2 = Math.SQRT2;
return D * (dx + dy) + (D2 - 2 * D) * Math.min(dx, dy);
}
__name(octileDistance, "octileDistance");
var DEFAULT_PATHFINDING_OPTIONS = {
maxNodes: 1e4,
heuristicWeight: 1,
allowDiagonal: true,
avoidCorners: true,
agentRadius: 0
};
// src/core/BinaryHeap.ts
var _BinaryHeap = class _BinaryHeap {
/**
* @zh 创建二叉堆
* @en Create binary heap
*
* @param compare - @zh 比较函数,返回负数表示 a < b @en Compare function, returns negative if a < b
*/
constructor(compare) {
__publicField(this, "heap", []);
__publicField(this, "compare");
this.compare = compare;
}
/**
* @zh 堆大小
* @en Heap size
*/
get size() {
return this.heap.length;
}
/**
* @zh 是否为空
* @en Is empty
*/
get isEmpty() {
return this.heap.length === 0;
}
/**
* @zh 插入元素
* @en Push element
*/
push(item) {
this.heap.push(item);
this.bubbleUp(this.heap.length - 1);
}
/**
* @zh 弹出最小元素
* @en Pop minimum element
*/
pop() {
if (this.heap.length === 0) {
return void 0;
}
const result = this.heap[0];
const last = this.heap.pop();
if (this.heap.length > 0) {
this.heap[0] = last;
this.sinkDown(0);
}
return result;
}
/**
* @zh 查看最小元素(不移除)
* @en Peek minimum element (without removing)
*/
peek() {
return this.heap[0];
}
/**
* @zh 更新元素(重新排序)
* @en Update element (re-sort)
*/
update(item) {
const index = this.heap.indexOf(item);
if (index !== -1) {
this.bubbleUp(index);
this.sinkDown(index);
}
}
/**
* @zh 检查是否包含元素
* @en Check if contains element
*/
contains(item) {
return this.heap.indexOf(item) !== -1;
}
/**
* @zh 清空堆
* @en Clear heap
*/
clear() {
this.heap.length = 0;
}
/**
* @zh 上浮操作
* @en Bubble up operation
*/
bubbleUp(index) {
const item = this.heap[index];
while (index > 0) {
const parentIndex = Math.floor((index - 1) / 2);
const parent = this.heap[parentIndex];
if (this.compare(item, parent) >= 0) {
break;
}
this.heap[index] = parent;
index = parentIndex;
}
this.heap[index] = item;
}
/**
* @zh 下沉操作
* @en Sink down operation
*/
sinkDown(index) {
const length = this.heap.length;
const item = this.heap[index];
while (true) {
const leftIndex = 2 * index + 1;
const rightIndex = 2 * index + 2;
let smallest = index;
if (leftIndex < length && this.compare(this.heap[leftIndex], this.heap[smallest]) < 0) {
smallest = leftIndex;
}
if (rightIndex < length && this.compare(this.heap[rightIndex], this.heap[smallest]) < 0) {
smallest = rightIndex;
}
if (smallest === index) {
break;
}
this.heap[index] = this.heap[smallest];
this.heap[smallest] = item;
index = smallest;
}
}
};
__name(_BinaryHeap, "BinaryHeap");
var BinaryHeap = _BinaryHeap;
// src/core/IndexedBinaryHeap.ts
var _IndexedBinaryHeap = class _IndexedBinaryHeap {
/**
* @zh 创建带索引追踪的二叉堆
* @en Create indexed binary heap
*
* @param compare - @zh 比较函数,返回负数表示 a < b @en Compare function, returns negative if a < b
*/
constructor(compare) {
__publicField(this, "heap", []);
__publicField(this, "compare");
this.compare = compare;
}
/**
* @zh 堆大小
* @en Heap size
*/
get size() {
return this.heap.length;
}
/**
* @zh 是否为空
* @en Is empty
*/
get isEmpty() {
return this.heap.length === 0;
}
/**
* @zh 插入元素
* @en Push element
*/
push(item) {
item.heapIndex = this.heap.length;
this.heap.push(item);
this.bubbleUp(this.heap.length - 1);
}
/**
* @zh 弹出最小元素
* @en Pop minimum element
*/
pop() {
if (this.heap.length === 0) {
return void 0;
}
const result = this.heap[0];
result.heapIndex = -1;
const last = this.heap.pop();
if (this.heap.length > 0) {
last.heapIndex = 0;
this.heap[0] = last;
this.sinkDown(0);
}
return result;
}
/**
* @zh 查看最小元素(不移除)
* @en Peek minimum element (without removing)
*/
peek() {
return this.heap[0];
}
/**
* @zh 更新元素
* @en Update element
*/
update(item) {
const index = item.heapIndex;
if (index >= 0 && index < this.heap.length && this.heap[index] === item) {
this.bubbleUp(index);
this.sinkDown(item.heapIndex);
}
}
/**
* @zh 检查是否包含元素
* @en Check if contains element
*/
contains(item) {
const index = item.heapIndex;
return index >= 0 && index < this.heap.length && this.heap[index] === item;
}
/**
* @zh 从堆中移除指定元素
* @en Remove specific element from heap
*/
remove(item) {
const index = item.heapIndex;
if (index < 0 || index >= this.heap.length || this.heap[index] !== item) {
return false;
}
item.heapIndex = -1;
if (index === this.heap.length - 1) {
this.heap.pop();
return true;
}
const last = this.heap.pop();
last.heapIndex = index;
this.heap[index] = last;
this.bubbleUp(index);
this.sinkDown(last.heapIndex);
return true;
}
/**
* @zh 清空堆
* @en Clear heap
*/
clear() {
for (const item of this.heap) {
item.heapIndex = -1;
}
this.heap.length = 0;
}
/**
* @zh 上浮操作
* @en Bubble up operation
*/
bubbleUp(index) {
const item = this.heap[index];
while (index > 0) {
const parentIndex = index - 1 >> 1;
const parent = this.heap[parentIndex];
if (this.compare(item, parent) >= 0) {
break;
}
parent.heapIndex = index;
this.heap[index] = parent;
index = parentIndex;
}
item.heapIndex = index;
this.heap[index] = item;
}
/**
* @zh 下沉操作
* @en Sink down operation
*/
sinkDown(index) {
const length = this.heap.length;
const item = this.heap[index];
const halfLength = length >> 1;
while (index < halfLength) {
const leftIndex = (index << 1) + 1;
const rightIndex = leftIndex + 1;
let smallest = index;
let smallestItem = item;
const left = this.heap[leftIndex];
if (this.compare(left, smallestItem) < 0) {
smallest = leftIndex;
smallestItem = left;
}
if (rightIndex < length) {
const right = this.heap[rightIndex];
if (this.compare(right, smallestItem) < 0) {
smallest = rightIndex;
smallestItem = right;
}
}
if (smallest === index) {
break;
}
smallestItem.heapIndex = index;
this.heap[index] = smallestItem;
index = smallest;
}
item.heapIndex = index;
this.heap[index] = item;
}
};
__name(_IndexedBinaryHeap, "IndexedBinaryHeap");
var IndexedBinaryHeap = _IndexedBinaryHeap;
// src/core/AStarPathfinder.ts
var _AStarPathfinder = class _AStarPathfinder {
constructor(map) {
__publicField(this, "map");
__publicField(this, "nodeCache", /* @__PURE__ */ new Map());
__publicField(this, "openList");
this.map = map;
this.openList = new IndexedBinaryHeap((a, b) => a.f - b.f);
}
/**
* @zh 查找路径
* @en Find path
*/
findPath(startX, startY, endX, endY, options) {
const opts = {
...DEFAULT_PATHFINDING_OPTIONS,
...options
};
this.clear();
const startNode = this.map.getNodeAt(startX, startY);
const endNode = this.map.getNodeAt(endX, endY);
if (!startNode || !endNode) {
return EMPTY_PATH_RESULT;
}
if (!startNode.walkable || !endNode.walkable) {
return EMPTY_PATH_RESULT;
}
if (startNode.id === endNode.id) {
return {
found: true,
path: [
startNode.position
],
cost: 0,
nodesSearched: 1
};
}
const start = this.getOrCreateAStarNode(startNode);
start.g = 0;
start.h = this.map.heuristic(startNode.position, endNode.position) * opts.heuristicWeight;
start.f = start.h;
start.opened = true;
this.openList.push(start);
let nodesSearched = 0;
const endPosition = endNode.position;
while (!this.openList.isEmpty && nodesSearched < opts.maxNodes) {
const current = this.openList.pop();
current.closed = true;
nodesSearched++;
if (current.node.id === endNode.id) {
return this.buildPath(current, nodesSearched);
}
const neighbors = this.map.getNeighbors(current.node);
for (const neighborNode of neighbors) {
if (!neighborNode.walkable) {
continue;
}
const neighbor = this.getOrCreateAStarNode(neighborNode);
if (neighbor.closed) {
continue;
}
const movementCost = this.map.getMovementCost(current.node, neighborNode);
const tentativeG = current.g + movementCost;
if (!neighbor.opened) {
neighbor.g = tentativeG;
neighbor.h = this.map.heuristic(neighborNode.position, endPosition) * opts.heuristicWeight;
neighbor.f = neighbor.g + neighbor.h;
neighbor.parent = current;
neighbor.opened = true;
this.openList.push(neighbor);
} else if (tentativeG < neighbor.g) {
neighbor.g = tentativeG;
neighbor.f = neighbor.g + neighbor.h;
neighbor.parent = current;
this.openList.update(neighbor);
}
}
}
return {
found: false,
path: [],
cost: 0,
nodesSearched
};
}
/**
* @zh 清理状态
* @en Clear state
*/
clear() {
this.nodeCache.clear();
this.openList.clear();
}
/**
* @zh 获取或创建 A* 节点
* @en Get or create A* node
*/
getOrCreateAStarNode(node) {
let astarNode = this.nodeCache.get(node.id);
if (!astarNode) {
astarNode = {
node,
g: Infinity,
h: 0,
f: Infinity,
parent: null,
closed: false,
opened: false,
heapIndex: -1
};
this.nodeCache.set(node.id, astarNode);
}
return astarNode;
}
/**
* @zh 构建路径结果
* @en Build path result
*/
buildPath(endNode, nodesSearched) {
const path = [];
let current = endNode;
while (current) {
path.push(current.node.position);
current = current.parent;
}
path.reverse();
return {
found: true,
path,
cost: endNode.g,
nodesSearched
};
}
};
__name(_AStarPathfinder, "AStarPathfinder");
var AStarPathfinder = _AStarPathfinder;
function createAStarPathfinder(map) {
return new AStarPathfinder(map);
}
__name(createAStarPathfinder, "createAStarPathfinder");
// src/core/PathCache.ts
var DEFAULT_PATH_CACHE_CONFIG = {
maxEntries: 1e3,
ttlMs: 5e3,
enableApproximateMatch: false,
approximateRange: 2
};
var _PathCache = class _PathCache {
constructor(config = {}) {
__publicField(this, "config");
__publicField(this, "cache");
__publicField(this, "accessOrder");
this.config = {
...DEFAULT_PATH_CACHE_CONFIG,
...config
};
this.cache = /* @__PURE__ */ new Map();
this.accessOrder = [];
}
/**
* @zh 获取缓存的路径
* @en Get cached path
*
* @param startX - @zh 起点 X 坐标 @en Start X coordinate
* @param startY - @zh 起点 Y 坐标 @en Start Y coordinate
* @param endX - @zh 终点 X 坐标 @en End X coordinate
* @param endY - @zh 终点 Y 坐标 @en End Y coordinate
* @param mapVersion - @zh 地图版本号 @en Map version number
* @returns @zh 缓存的路径结果或 null @en Cached path result or null
*/
get(startX, startY, endX, endY, mapVersion) {
const key = this.generateKey(startX, startY, endX, endY);
const entry = this.cache.get(key);
if (!entry) {
if (this.config.enableApproximateMatch) {
return this.getApproximate(startX, startY, endX, endY, mapVersion);
}
return null;
}
if (!this.isValid(entry, mapVersion)) {
this.cache.delete(key);
this.removeFromAccessOrder(key);
return null;
}
this.updateAccessOrder(key);
return entry.result;
}
/**
* @zh 设置缓存路径
* @en Set cached path
*
* @param startX - @zh 起点 X 坐标 @en Start X coordinate
* @param startY - @zh 起点 Y 坐标 @en Start Y coordinate
* @param endX - @zh 终点 X 坐标 @en End X coordinate
* @param endY - @zh 终点 Y 坐标 @en End Y coordinate
* @param result - @zh 路径结果 @en Path result
* @param mapVersion - @zh 地图版本号 @en Map version number
*/
set(startX, startY, endX, endY, result, mapVersion) {
if (this.cache.size >= this.config.maxEntries) {
this.evictLRU();
}
const key = this.generateKey(startX, startY, endX, endY);
const entry = {
result,
timestamp: Date.now(),
mapVersion
};
this.cache.set(key, entry);
this.updateAccessOrder(key);
}
/**
* @zh 使所有缓存失效
* @en Invalidate all cache
*/
invalidateAll() {
this.cache.clear();
this.accessOrder.length = 0;
}
/**
* @zh 使指定区域的缓存失效
* @en Invalidate cache for specified region
*
* @param minX - @zh 最小 X 坐标 @en Minimum X coordinate
* @param minY - @zh 最小 Y 坐标 @en Minimum Y coordinate
* @param maxX - @zh 最大 X 坐标 @en Maximum X coordinate
* @param maxY - @zh 最大 Y 坐标 @en Maximum Y coordinate
*/
invalidateRegion(minX, minY, maxX, maxY) {
const keysToDelete = [];
for (const [key, entry] of this.cache) {
const path = entry.result.path;
if (path.length === 0) continue;
for (const point of path) {
if (point.x >= minX && point.x <= maxX && point.y >= minY && point.y <= maxY) {
keysToDelete.push(key);
break;
}
}
}
for (const key of keysToDelete) {
this.cache.delete(key);
this.removeFromAccessOrder(key);
}
}
/**
* @zh 获取缓存统计信息
* @en Get cache statistics
*/
getStats() {
return {
size: this.cache.size,
maxSize: this.config.maxEntries
};
}
/**
* @zh 清理过期条目
* @en Clean up expired entries
*/
cleanup() {
if (this.config.ttlMs === 0) return;
const now = Date.now();
const keysToDelete = [];
for (const [key, entry] of this.cache) {
if (now - entry.timestamp > this.config.ttlMs) {
keysToDelete.push(key);
}
}
for (const key of keysToDelete) {
this.cache.delete(key);
this.removeFromAccessOrder(key);
}
}
// =========================================================================
// 私有方法 | Private Methods
// =========================================================================
generateKey(startX, startY, endX, endY) {
return `${startX},${startY}->${endX},${endY}`;
}
isValid(entry, mapVersion) {
if (entry.mapVersion !== mapVersion) {
return false;
}
if (this.config.ttlMs > 0) {
const age = Date.now() - entry.timestamp;
if (age > this.config.ttlMs) {
return false;
}
}
return true;
}
getApproximate(startX, startY, endX, endY, mapVersion) {
const range = this.config.approximateRange;
for (let sx = startX - range; sx <= startX + range; sx++) {
for (let sy = startY - range; sy <= startY + range; sy++) {
for (let ex = endX - range; ex <= endX + range; ex++) {
for (let ey = endY - range; ey <= endY + range; ey++) {
const key = this.generateKey(sx, sy, ex, ey);
const entry = this.cache.get(key);
if (entry && this.isValid(entry, mapVersion)) {
this.updateAccessOrder(key);
return this.adjustPathForApproximate(entry.result, startX, startY, endX, endY);
}
}
}
}
}
return null;
}
adjustPathForApproximate(result, newStartX, newStartY, newEndX, newEndY) {
if (result.path.length === 0) {
return result;
}
const newPath = [];
const oldStart = result.path[0];
const oldEnd = result.path[result.path.length - 1];
if (newStartX !== oldStart.x || newStartY !== oldStart.y) {
newPath.push({
x: newStartX,
y: newStartY
});
}
newPath.push(...result.path);
if (newEndX !== oldEnd.x || newEndY !== oldEnd.y) {
newPath.push({
x: newEndX,
y: newEndY
});
}
return {
...result,
path: newPath
};
}
updateAccessOrder(key) {
this.removeFromAccessOrder(key);
this.accessOrder.push(key);
}
removeFromAccessOrder(key) {
const index = this.accessOrder.indexOf(key);
if (index !== -1) {
this.accessOrder.splice(index, 1);
}
}
evictLRU() {
const lruKey = this.accessOrder.shift();
if (lruKey) {
this.cache.delete(lruKey);
}
}
};
__name(_PathCache, "PathCache");
var PathCache = _PathCache;
function createPathCache(config) {
return new PathCache(config);
}
__name(createPathCache, "createPathCache");
// src/core/IncrementalAStarPathfinder.ts
var _IncrementalAStarPathfinder = class _IncrementalAStarPathfinder {
/**
* @zh 创建增量 A* 寻路器
* @en Create incremental A* pathfinder
*
* @param map - @zh 寻路地图实例 @en Pathfinding map instance
* @param config - @zh 配置选项 @en Configuration options
*/
constructor(map, config) {
__publicField(this, "map");
__publicField(this, "sessions", /* @__PURE__ */ new Map());
__publicField(this, "nextRequestId", 0);
__publicField(this, "affectedRegions", []);
__publicField(this, "maxRegionAge", 5e3);
__publicField(this, "cache");
__publicField(this, "enableCache");
__publicField(this, "mapVersion", 0);
__publicField(this, "cacheHits", 0);
__publicField(this, "cacheMisses", 0);
this.map = map;
this.enableCache = config?.enableCache ?? false;
this.cache = this.enableCache ? new PathCache(config?.cacheConfig) : null;
}
/**
* @zh 请求寻路(非阻塞)
* @en Request pathfinding (non-blocking)
*/
requestPath(startX, startY, endX, endY, options) {
const id = this.nextRequestId++;
const priority = options?.priority ?? 50;
const opts = {
...DEFAULT_PATHFINDING_OPTIONS,
...options
};
const request = {
id,
startX,
startY,
endX,
endY,
options: opts,
priority,
createdAt: Date.now()
};
if (this.cache) {
const cached = this.cache.get(startX, startY, endX, endY, this.mapVersion);
if (cached) {
this.cacheHits++;
const session2 = {
request,
state: cached.found ? PathfindingState.Completed : PathfindingState.Failed,
options: opts,
openList: new IndexedBinaryHeap((a, b) => a.f - b.f),
nodeCache: /* @__PURE__ */ new Map(),
startNode: this.map.getNodeAt(startX, startY),
endNode: this.map.getNodeAt(endX, endY),
endPosition: {
x: endX,
y: endY
},
nodesSearched: cached.nodesSearched,
framesUsed: 0,
initialDistance: 0,
result: {
requestId: id,
found: cached.found,
path: [
...cached.path
],
cost: cached.cost,
nodesSearched: cached.nodesSearched,
framesUsed: 0,
isPartial: false
},
affectedByChange: false
};
this.sessions.set(id, session2);
return request;
}
this.cacheMisses++;
}
const startNode = this.map.getNodeAt(startX, startY);
const endNode = this.map.getNodeAt(endX, endY);
if (!startNode || !endNode || !startNode.walkable || !endNode.walkable) {
const session2 = {
request,
state: PathfindingState.Failed,
options: opts,
openList: new IndexedBinaryHeap((a, b) => a.f - b.f),
nodeCache: /* @__PURE__ */ new Map(),
startNode,
endNode,
endPosition: endNode?.position ?? {
x: endX,
y: endY
},
nodesSearched: 0,
framesUsed: 0,
initialDistance: 0,
result: this.createEmptyResult(id),
affectedByChange: false
};
this.sessions.set(id, session2);
return request;
}
if (startNode.id === endNode.id) {
const session2 = {
request,
state: PathfindingState.Completed,
options: opts,
openList: new IndexedBinaryHeap((a, b) => a.f - b.f),
nodeCache: /* @__PURE__ */ new Map(),
startNode,
endNode,
endPosition: endNode.position,
nodesSearched: 1,
framesUsed: 0,
initialDistance: 0,
result: {
requestId: id,
found: true,
path: [
startNode.position
],
cost: 0,
nodesSearched: 1,
framesUsed: 0,
isPartial: false
},
affectedByChange: false
};
this.sessions.set(id, session2);
return request;
}
const initialDistance = this.map.heuristic(startNode.position, endNode.position);
const openList = new IndexedBinaryHeap((a, b) => a.f - b.f);
const nodeCache = /* @__PURE__ */ new Map();
const startAStarNode = {
node: startNode,
g: 0,
h: initialDistance * opts.heuristicWeight,
f: initialDistance * opts.heuristicWeight,
parent: null,
closed: false,
opened: true,
heapIndex: -1
};
nodeCache.set(startNode.id, startAStarNode);
openList.push(startAStarNode);
const session = {
request,
state: PathfindingState.InProgress,
options: opts,
openList,
nodeCache,
startNode,
endNode,
endPosition: endNode.position,
nodesSearched: 0,
framesUsed: 0,
initialDistance,
result: null,
affectedByChange: false
};
this.sessions.set(id, session);
return request;
}
/**
* @zh 执行一步搜索
* @en Execute one step of search
*/
step(requestId, maxIterations) {
const session = this.sessions.get(requestId);
if (!session) {
return EMPTY_PROGRESS;
}
if (session.state !== PathfindingState.InProgress) {
return this.createProgress(session);
}
session.framesUsed++;
let iterations = 0;
while (!session.openList.isEmpty && iterations < maxIterations) {
const current = session.openList.pop();
current.closed = true;
session.nodesSearched++;
iterations++;
if (current.node.id === session.endNode.id) {
session.state = PathfindingState.Completed;
session.result = this.buildResult(session, current);
if (this.cache && session.result.found) {
const req = session.request;
this.cache.set(req.startX, req.startY, req.endX, req.endY, {
found: true,
path: session.result.path,
cost: session.result.cost,
nodesSearched: session.result.nodesSearched
}, this.mapVersion);
}
return this.createProgress(session);
}
this.expandNeighbors(session, current);
if (session.nodesSearched >= session.options.maxNodes) {
session.state = PathfindingState.Failed;
session.result = this.createEmptyResult(requestId);
return this.createProgress(session);
}
}
if (session.openList.isEmpty && session.state === PathfindingState.InProgress) {
session.state = PathfindingState.Failed;
session.result = this.createEmptyResult(requestId);
}
return this.createProgress(session);
}
/**
* @zh 暂停寻路
* @en Pause pathfinding
*/
pause(requestId) {
const session = this.sessions.get(requestId);
if (session && session.state === PathfindingState.InProgress) {
session.state = PathfindingState.Paused;
}
}
/**
* @zh 恢复寻路
* @en Resume pathfinding
*/
resume(requestId) {
const session = this.sessions.get(requestId);
if (session && session.state === PathfindingState.Paused) {
session.state = PathfindingState.InProgress;
}
}
/**
* @zh 取消寻路
* @en Cancel pathfinding
*/
cancel(requestId) {
const session = this.sessions.get(requestId);
if (session && (session.state === PathfindingState.InProgress || session.state === PathfindingState.Paused)) {
session.state = PathfindingState.Cancelled;
session.result = this.createEmptyResult(requestId);
}
}
/**
* @zh 获取寻路结果
* @en Get pathfinding result
*/
getResult(requestId) {
const session = this.sessions.get(requestId);
return session?.result ?? null;
}
/**
* @zh 获取当前进度
* @en Get current progress
*/
getProgress(requestId) {
const session = this.sessions.get(requestId);
return session ? this.createProgress(session) : null;
}
/**
* @zh 清理已完成的请求
* @en Clean up completed request
*/
cleanup(requestId) {
const session = this.sessions.get(requestId);
if (session) {
session.openList.clear();
session.nodeCache.clear();
this.sessions.delete(requestId);
}
}
/**
* @zh 通知障碍物变化
* @en Notify obstacle change
*/
notifyObstacleChange(minX, minY, maxX, maxY) {
this.mapVersion++;
if (this.cache) {
this.cache.invalidateRegion(minX, minY, maxX, maxY);
}
const region = {
minX,
minY,
maxX,
maxY,
timestamp: Date.now()
};
this.affectedRegions.push(region);
for (const session of this.sessions.values()) {
if (session.state === PathfindingState.InProgress || session.state === PathfindingState.Paused) {
if (this.sessionAffectedByRegion(session, region)) {
session.affectedByChange = true;
}
}
}
this.cleanupOldRegions();
}
/**
* @zh 清理所有请求
* @en Clear all requests
*/
clear() {
for (const session of this.sessions.values()) {
session.openList.clear();
session.nodeCache.clear();
}
this.sessions.clear();
this.affectedRegions.length = 0;
}
/**
* @zh 清空路径缓存
* @en Clear path cache
*/
clearCache() {
if (this.cache) {
this.cache.invalidateAll();
this.cacheHits = 0;
this.cacheMisses = 0;
}
}
/**
* @zh 获取缓存统计信息
* @en Get cache statistics
*/
getCacheStats() {
if (!this.cache) {
return {
enabled: false,
hits: 0,
misses: 0,
hitRate: 0,
size: 0
};
}
const total = this.cacheHits + this.cacheMisses;
const hitRate = total > 0 ? this.cacheHits / total : 0;
return {
enabled: true,
hits: this.cacheHits,
misses: this.cacheMisses,
hitRate,
size: this.cache.getStats().size
};
}
/**
* @zh 检查会话是否被障碍物变化影响
* @en Check if session is affected by obstacle change
*/
isAffectedByChange(requestId) {
const session = this.sessions.get(requestId);
return session?.affectedByChange ?? false;
}
/**
* @zh 清除会话的变化标记
* @en Clear session's change flag
*/
clearChangeFlag(requestId) {
const session = this.sessions.get(requestId);
if (session) {
session.affectedByChange = false;
}
}
// =========================================================================
// 私有方法 | Private Methods
// =========================================================================
/**
* @zh 展开邻居节点
* @en Expand neighbor nodes
*/
expandNeighbors(session, current) {
const neighbors = this.map.getNeighbors(current.node);
for (const neighborNode of neighbors) {
if (!neighborNode.walkable) {
continue;
}
let neighbor = session.nodeCache.get(neighborNode.id);
if (!neighbor) {
neighbor = {
node: neighborNode,
g: Infinity,
h: 0,
f: Infinity,
parent: null,
closed: false,
opened: false,
heapIndex: -1
};
session.nodeCache.set(neighborNode.id, neighbor);
}
if (neighbor.closed) {
continue;
}
const movementCost = this.map.getMovementCost(current.node, neighborNode);
const tentativeG = current.g + movementCost;
if (!neighbor.opened) {
neighbor.g = tentativeG;
neighbor.h = this.map.heuristic(neighborNode.position, session.endPosition) * session.options.heuristicWeight;
neighbor.f = neighbor.g + neighbor.h;
neighbor.parent = current;
neighbor.opened = true;
session.openList.push(neighbor);
} else if (tentativeG < neighbor.g) {
neighbor.g = tentativeG;
neighbor.f = neighbor.g + neighbor.h;
neighbor.parent = current;
session.openList.update(neighbor);
}
}
}
/**
* @zh 创建进度对象
* @en Create progress object
*/
createProgress(session) {
let estimatedProgress = 0;
if (session.state === PathfindingState.Completed) {
estimatedProgress = 1;
} else if (session.state === PathfindingState.InProgress && session.initialDistance > 0) {
const bestNode = session.openList.peek();
if (bestNode) {
const currentDistance = bestNode.h / session.options.heuristicWeight;
estimatedProgress = Math.max(0, Math.min(1, 1 - currentDistance / session.initialDistance));
}
}
return {
state: session.state,
nodesSearched: session.nodesSearched,
openListSize: session.openList.size,
estimatedProgress
};
}
/**
* @zh 构建路径结果
* @en Build path result
*/
buildResult(session, endNode) {
const path = [];
let current = endNode;
while (current) {
path.push(current.node.position);
current = current.parent;
}
path.reverse();
return {
requestId: session.request.id,
found: true,
path,
cost: endNode.g,
nodesSearched: session.nodesSearched,
framesUsed: session.framesUsed,
isPartial: false
};
}
/**
* @zh 创建空结果
* @en Create empty result
*/
createEmptyResult(requestId) {
return {
requestId,
found: false,
path: [],
cost: 0,
nodesSearched: 0,
framesUsed: 0,
isPartial: false
};
}
/**
* @zh 检查会话是否被区域影响
* @en Check if session is affected by region
*/
sessionAffectedByRegion(session, region) {
for (const astarNode of session.nodeCache.values()) {
if (astarNode.opened || astarNode.closed) {
const pos = astarNode.node.position;
if (pos.x >= region.minX && pos.x <= region.maxX && pos.y >= region.minY && pos.y <= region.maxY) {
return true;
}
}
}
const start = session.request;
const end = session.endPosition;
if (start.startX >= region.minX && start.startX <= region.maxX && start.startY >= region.minY && start.startY <= region.maxY || end.x >= region.minX && end.x <= region.maxX && end.y >= region.minY && end.y <= region.maxY) {
return true;
}
return false;
}
/**
* @zh 清理过期的变化区域
* @en Clean up expired change regions
*/
cleanupOldRegions() {
const now = Date.now();
let i = 0;
while (i < this.affectedRegions.length) {
if (now - this.affectedRegions[i].timestamp > this.maxRegionAge) {
this.affectedRegions.splice(i, 1);
} else {
i++;
}
}
}
};
__name(_IncrementalAStarPathfinder, "IncrementalAStarPathfinder");
var IncrementalAStarPathfinder = _IncrementalAStarPathfinder;
function createIncrementalAStarPathfinder(map) {
return new IncrementalAStarPathfinder(map);
}
__name(createIncrementalAStarPathfinder, "createIncrementalAStarPathfinder");
// src/core/JPSPathfinder.ts
var _JPSPathfinder = class _JPSPathfinder {
constructor(map) {
__publicField(this, "map");
__publicField(this, "width");
__publicField(this, "height");
__publicField(this, "openList");
__publicField(this, "nodeGrid");
this.map = map;
const bounds = this.getMapBounds();
this.width = bounds.width;
this.height = bounds.height;
this.openList = new BinaryHeap((a, b) => a.f - b.f);
this.nodeGrid = [];
}
/**
* @zh 寻找路径
* @en Find path
*/
findPath(startX, startY, endX, endY, options) {
const opts = {
...DEFAULT_PATHFINDING_OPTIONS,
...options
};
if (!this.map.isWalkable(startX, startY) || !this.map.isWalkable(endX, endY)) {
return EMPTY_PATH_RESULT;
}
if (startX === endX && startY === endY) {
return {
found: true,
path: [
{
x: startX,
y: startY
}
],
cost: 0,
nodesSearched: 1
};
}
this.initGrid();
this.openList.clear();
const startNode = this.getOrCreateNode(startX, startY);
startNode.g = 0;
startNode.h = this.heuristic(startX, startY, endX, endY) * opts.heuristicWeight;
startNode.f = startNode.h;
this.openList.push(startNode);
let nodesSearched = 0;
while (!this.openList.isEmpty && nodesSearched < opts.maxNodes) {
const current = this.openList.pop();
current.closed = true;
nodesSearched++;
if (current.x === endX && current.y === endY) {
return {
found: true,
path: this.buildPath(current),
cost: current.g,
nodesSearched
};
}
this.identifySuccessors(current, endX, endY, opts);
}
return {
found: false,
path: [],
cost: 0,
nodesSearched
};
}
/**
* @zh 清理状态
* @en Clear state
*/
clear() {
this.openList.clear();
this.nodeGrid = [];
}
// =========================================================================
// 私有方法 | Private Methods
// =========================================================================
/**
* @zh 获取地图边界
* @en Get map bounds
*/
getMapBounds() {
const mapAny = this.map;
if (typeof mapAny.width === "number" && typeof mapAny.height === "number") {
return {
width: mapAny.width,
height: mapAny.height
};
}
return {
width: 1e3,
height: 1e3
};
}
/**
* @zh 初始化节点网格
* @en Initialize node grid
*/
initGrid() {
this.nodeGrid = [];
for (let i = 0; i < this.width; i++) {
this.nodeGrid[i] = [];
}
}
/**
* @zh 获取或创建节点
* @en Get or create node
*/
getOrCreateNode(x, y) {
const xi = x | 0;
const yi = y | 0;
if (xi < 0 || xi >= this.width || yi < 0 || yi >= this.height) {
throw new Error("[JPSPathfinder] Invalid grid coordinates");
}
if (!this.nodeGrid[xi]) {
this.nodeGrid[xi] = [];
}
if (!this.nodeGrid[xi][yi]) {
this.nodeGrid[xi][yi] = {
x: xi,
y: yi,
g: Infinity,
h: 0,
f: Infinity,
parent: null,
closed: false
};
}
return this.nodeGrid[xi][yi];
}
/**
* @zh 启发式函数(八方向距离)
* @en Heuristic function (octile distance)
*/
heuristic(x1, y1, x2, y2) {
const dx = Math.abs(x1 - x2);
const dy = Math.abs(y1 - y2);
return dx + dy + (Math.SQRT2 - 2) * Math.min(dx, dy);
}
/**
* @zh 识别后继节点(跳跃点)
* @en Identify successors (jump points)
*/
identifySuccessors(node, endX, endY, opts) {
const neighbors = this.findNeighbors(node);
for (const neighbor of neighbors) {
const jumpPoint = this.jump(neighbor.x, neighbor.y, node.x, node.y, endX, endY);
if (jumpPoint) {
const jx = jumpPoint.x;
const jy = jumpPoint.y;
const jpNode = this.getOrCreateNode(jx, jy);
if (jpNode.closed) continue;
const dx = Math.abs(jx - node.x);
const dy = Math.abs(jy - node.y);
const distance = Math.sqrt(dx * dx + dy * dy);
const tentativeG = node.g + distance;
if (tentativeG < jpNode.g) {
jpNode.g = tentativeG;
jpNode.h = this.heuristic(jx, jy, endX, endY) * opts.heuristicWeight;
jpNode.f = jpNode.g + jpNode.h;
jpNode.parent = node;
if (!this.openList.contains(jpNode)) {
this.openList.push(jpNode);
} else {
this.openList.update(jpNode);
}
}
}
}
}
/**
* @zh 查找邻居(根据父节点方向剪枝)
* @en Find neighbors (pruned based on parent direction)
*/
findNeighbors(node) {
const { x, y, parent } = node;
const neighbors = [];
if (!parent) {
for (let dx2 = -1; dx2 <= 1; dx2++) {
for (let dy2 = -1; dy2 <= 1; dy2++) {
if (dx2 === 0 && dy2 === 0) continue;
const nx = x + dx2;
const ny = y + dy2;
if (this.isWalkableAt(nx, ny)) {
if (dx2 !== 0 && dy2 !== 0) {
if (this.isWalkableAt(x + dx2, y) || this.isWalkableAt(x, y + dy2)) {
neighbors.push({
x: nx,
y: ny
});
}
} else {
neighbors.push({
x: nx,
y: ny
});
}
}
}
}
return neighbors;
}
const dx = Math.sign(x - parent.x);
const dy = Math.sign(y - parent.y);
if (dx !== 0 && dy !== 0) {
if (this.isWalkableAt(x, y + dy)) {
neighbors.push({
x,
y: y + dy
});
}
if (this.isWalkableAt(x + dx, y)) {
neighbors.push({
x: x + dx,
y
});
}
if (this.isWalkableAt(x, y + dy) || this.isWalkableAt(x + dx, y)) {
if (this.isWalkableAt(x + dx, y + dy)) {
neighbors.push({
x: x + dx,
y: y + dy
});
}
}
if (!this.isWalkableAt(x - dx, y) && this.isWalkableAt(x, y + dy)) {
if (this.isWalkableAt(x - dx, y + dy)) {
neighbors.push({
x: x - dx,
y: y + dy
});
}
}
if (!this.isWalkableAt(x, y - dy) && this.isWalkableAt(x + dx, y)) {
if (this.isWalkableAt(x + dx, y - dy)) {
neighbors.push({
x: x + dx,
y: y - dy
});
}
}
} else if (dx !== 0) {
if (this.isWalkableAt(x + dx, y)) {
neighbors.push({
x: x + dx,
y
});
if (!this.isWalkableAt(x, y + 1) && this.isWalkableAt(x + dx, y + 1)) {
neighbors.push({
x: x + dx,
y: y + 1
});
}
if (!this.isWalkableAt(x, y - 1) && this.isWalkableAt(x + dx, y - 1)) {
neighbors.push({
x: x + dx,
y: y - 1
});
}
}
} else if (dy !== 0) {
if (this.isWalkableAt(x, y + dy)) {
neighbors.push({
x,
y: y + dy
});
if (!this.isWalkableAt(x + 1, y) && this.isWalkableAt(x + 1, y + dy)) {
neighbors.push({
x: x + 1,
y: y + dy
});
}
if (!this.isWalkableAt(x - 1, y) && this.isWalkableAt(x - 1, y + dy)) {
neighbors.push({
x: x - 1,
y: y + dy
});
}
}
}
return neighbors;
}
/**
* @zh 跳跃函数(迭代版本,避免递归开销)
* @en Jump function (iterative version to avoid recursion overhead)
*/
jump(startX, startY, px, py, endX, endY) {
const dx = startX - px;
const dy = startY - py;
let x = startX;
let y = startY;
while (true) {
if (!this.isWalkableAt(x, y)) {
return null;
}
if (x === endX && y === endY) {
return {
x,
y
};
}
if (dx !== 0 && dy !== 0) {
if (this.isWalkableAt(x - dx, y + dy) && !this.isWalkableAt(x - dx, y) || this.isWalkableAt(x + dx, y - dy) && !this.isWalkableAt(x, y - dy)) {
return {
x,
y
};
}
if (this.jumpStraight(x + dx, y, dx, 0, endX, endY) || this.jumpStraight(x, y + dy, 0, dy, endX, endY)) {
return {
x,
y
};
}
if (!this.isWalkableAt(x + dx, y) && !this.isWalkableAt(x, y + dy)) {
return null;
}
} else if (dx !== 0) {
if (this.isWalkableAt(x + dx, y + 1) && !this.isWalkableAt(x, y + 1) || this.isWalkableAt(x + dx, y - 1) && !this.isWalkableAt(x, y - 1)) {
return {
x,
y
};
}
} else if (dy !== 0) {
if (this.isWalkableAt(x + 1, y + dy) && !this.isWalkableAt(x + 1, y) || this.isWalkableAt(x - 1, y + dy) && !this.isWalkableAt(x - 1, y)) {
return {
x,
y
};
}
}
x += dx;
y += dy;
}
}
/**
* @zh 直线跳跃(水平或垂直方向)
* @en Straight jump (horizontal or vertical direction)
*/
jumpStraight(startX, startY, dx, dy, endX, endY) {
let x = startX;
let y = startY;
while (true) {
if (!this.isWalkableAt(x, y)) {
return false;
}
if (x === endX && y === endY) {
return true;
}
if (dx !== 0) {
if (this.isWalkableAt(x + dx, y + 1) && !this.isWalkableAt(x, y + 1) || this.isWalkableAt(x + dx, y - 1) && !this.isWalkableAt(x, y - 1)) {
return true;
}
} else if (dy !== 0) {
if (this.isWalkableAt(x + 1, y + dy) && !this.isWalkableAt(x + 1, y) || this.isWalkableAt(x - 1, y + dy) && !this.isWalkableAt(x - 1, y)) {
return true;
}
}
x += dx;
y += dy;
}
}
/**
* @zh 检查位置是否可通行
* @en Check if position is walkable
*/
isWalkableAt(x, y) {
if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
return false;
}
return this.map.isWalkable(x, y);
}
/**
* @zh 构建路径
* @en Build path
*/
buildPath(endNode) {
const path = [];
let current = endNode;
while (current) {
path.unshift({
x: current.x,
y: current.y
});
current = current.parent;
}
return this.interpolatePath(path);
}
/**
* @zh 插值路径(在跳跃点之间填充中间点)
* @en Interpolate path (fill intermediate points between jump points)
*/
interpolatePath(jumpPoints) {
if (jumpPoints.length < 2) {
return jumpPoints;
}
const path = [
jumpPoints[0]
];
for (let i = 1; i < jumpPoints.length; i++) {
const prev = jumpPoints[i - 1];
const curr = jumpPoints[i];
const dx = curr.x - prev.x;
const dy = curr.y - prev.y;
const steps = Math.max(Math.abs(dx), Math.abs(dy));
const stepX = dx === 0 ? 0 : dx / Math.abs(dx);
const stepY = dy === 0 ? 0 : dy / Math.abs(dy);
let x = prev.x;
let y = prev.y;
for (let j = 0; j < steps; j++) {
if (x !== curr.x && y !== curr.y) {
x += stepX;
y += stepY;
} else if (x !== curr.x) {
x += stepX;
} else if (y !== curr.y) {
y += stepY;
}
if (x !== prev.x || y !== prev.y) {
path.push({
x,
y
});
}
}
}
return path;
}
};
__name(_JPSPathfinder, "JPSPathfinder");
var JPSPathfinder = _JPSPathfinder;
function createJPSPathfinder(map) {
return new JPSPathfinder(map);
}
__name(createJPSPathfinder, "createJPSPathfinder");
// src/core/HPAPathfinder.ts
var DEFAULT_HPA_CONFIG = {
clusterSize: 64,
maxEntranceWidth: 16,
cacheInternalPaths: true,
entranceStrategy: "end",
lazyIntraEdges: true
};
var _a;
var SubMap = (_a = class {
constructor(parentMap, originX, originY, width, height) {
__publicField(this, "parentMap");
__publicField(this, "originX");
__publicField(this, "originY");
__publicField(this, "width");
__publicField(this, "height");
this.parentMap = parentMap;
this.originX = originX;
this.originY = originY;
this.width = width;
this.height = height;
}
/**
* @zh 局部坐标转全局坐标
* @en Convert local to global coordinates
*/
localToGlobal(localX, localY) {
return {
x: this.originX + localX,
y: this.originY + localY
};
}
/**
* @zh 全局坐标转局部坐标
* @en Convert global to local coordinates
*/
globalToLocal(globalX, globalY) {
return {
x: globalX - this.originX,
y: globalY - this.originY
};
}
isWalkable(x, y) {
if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
return false;
}
return this.parentMap.isWalkable(this.originX + x, this.originY + y);
}
getNodeAt(x, y) {
if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
return null;
}
const globalNode = this.parentMap.getNodeAt(this.originX + x, this.originY + y);
if (!globalNode) return null;
return {
id: y * this.width + x,
position: {
x,
y
},
cost: globalNode.cost,
walkable: globalNode.walkable
};
}
getNeighbors(node) {
const neighbors = [];
const { x, y } = node.position;
const directions = [
{
dx: 0,
dy: -1
},
{
dx: 1,
dy: -1
},
{
dx: 1,
dy: 0
},
{
dx: 1,
dy: 1
},
{
dx: 0,
dy: 1
},
{
dx: -1,
dy: 1
},
{
dx: -1,
dy: 0
},
{
dx: -1,
dy: -1
}
// NW
];
for (const dir of directions) {
const nx = x + dir.dx;
const ny = y + dir.dy;
if (nx < 0 || nx >= this.width || ny < 0 || ny >= this.height) {
continue;
}
if (!this.isWalkable(nx, ny)) {
continue;
}
if (dir.dx !== 0 && dir.dy !== 0) {
if (!this.isWalkable(x + dir.dx, y) || !this.isWalkable(x, y + dir.dy)) {
continue;
}
}
const neighborNode = this.getNodeAt(nx, ny);
if (neighborNode) {
neighbors.push(neighborNode);
}
}
return neighbors;
}
heuristic(a, b) {
const dx = Math.abs(a.x - b.x);
const dy = Math.abs(a.y - b.y);
return dx + dy + (Math.SQRT2 - 2) * Math.min(dx, dy);
}
getMovementCost(from, to) {
const dx = Math.abs(to.position.x - from.position.x);
const dy = Math.abs(to.position.y - from.position.y);
const baseCost = dx !== 0 && dy !== 0 ? Math.SQRT2 : 1;
return baseCost * to.cost;
}
}, __name(_a, "SubMap"), _a);
var _a2;
var Cluster = (_a2 = class {
constructor(id, originX, originY, width, height, parentMap) {
__publicField(this, "id");
__publicField(this, "originX");
__publicField(this, "originY");
__publicField(this, "width");
__publicField(this, "height");
__publicField(this, "subMap");
/** @zh 集群内的抽象节点 ID 列表 @en Abstract node IDs in this cluster */
__publicField(this, "nodeIds", []);
/** @zh 预计算的距离缓存 @en Precomputed distance cache */
__publicField(this, "distanceCache", /* @__PURE__ */ new Map());
/** @zh 预计算的路径缓存 @en Precomputed path cache */
__publicField(this, "pathCache", /* @__PURE__ */ new Map());
this.id = id;
this.originX = originX;
this.originY = originY;
this.width = width;
this.height = height;
this.subMap = new SubMap(parentMap, originX, originY, width, height);
}
/**
* @zh 检查点是否在集群内
* @en Check if point is in cluster
*/
containsPoint(x, y) {
return x >= this.originX && x < this.originX + this.width && y >= this.originY && y < this.originY + this.height;
}
/**
* @zh 添加节点 ID
* @en Add node ID
*/
addNodeId(nodeId) {
if (!this.nodeIds.includes(nodeId)) {
this.nodeIds.push(nodeId);
}
}
/**
* @zh 移除节点 ID
* @en Remove node ID
*/
removeNodeId(nodeId) {
const idx = this.nodeIds.indexOf(nodeId);
if (idx !== -1) {
this.nodeIds.splice(idx, 1);
}
}
/**
* @zh 生成缓存键
* @en Generate cache key
*/
getCacheKey(fromId, toId) {
return `${fromId}->${toId}`;
}
/**
* @zh 设置缓存
* @en Set cache
*/
setCache(fromId, toId, cost, path) {
const key = this.getCacheKey(fromId, toId);
this.distanceCache.set(key, cost);
this.pathCache.set(key, path);
}
/**
* @zh 获取缓存的距离
* @en Get cached distance
*/
getCachedDistance(fromId, toId) {
return this.distanceCache.get(this.getCacheKey(fromId, toId));
}
/**
* @zh 获取缓存的路径
* @en Get cached path
*/
getCachedPath(fromId, toId) {
return this.pathCache.get(this.getCacheKey(fromId, toId));
}
/**
* @zh 清除缓存
* @en Clear cache
*/
clearCache() {
this.distanceCache.clear();
this.pathCache.clear();
}
/**
* @zh 获取缓存大小
* @en Get cache size
*/
getCacheSize() {
return this.distanceCache.size;
}
}, __name(_a2, "Cluster"), _a2);
var _HPAPathfinder = class _HPAPathfinder {
constructor(map, config) {
__publicField(this, "map");
__publicField(this, "config");
__publicField(this, "mapWidth");
__publicField(this, "mapHeight");
// 集群管理
__publicField(this, "clusters", []);
__publicField(this, "clusterGrid", []);
__publicField(this, "clustersX", 0);
__publicField(this, "clustersY", 0);
// 抽象图
__publicField(this, "abstractNodes", /* @__PURE__ */ new Map());
__publicField(this, "nod