UNPKG

@esengine/pathfinding

Version:

寻路系统 | Pathfinding System - A*, Grid, NavMesh

1,911 lines (1,903 loc) 112 kB
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