import type { Cell } from './Cell'; import type { GridMap } from './GridMap'; import type { MapTopology } from './MapTopology'; import { createMembershipSet, membershipAdd, type MembershipSet } from '../../../tools/MembershipSet'; /** 最小堆(多源 Dijkstra 开放列表) */ class MinHeap { private _items: { value: T; priority: number }[] = []; get length(): number { return this._items.length; } clear(): void { this._items.length = 0; } push(value: T, priority: number): void { this._items.push({ value, priority }); this._bubbleUp(this._items.length - 1); } pop(): T | undefined { if (this._items.length === 0) return undefined; if (this._items.length === 1) return this._items.pop()!.value; const result = this._items[0].value; this._items[0] = this._items.pop()!; this._bubbleDown(0); return result; } private _bubbleUp(index: number): void { while (index > 0) { const parent = (index - 1) >> 1; if (this._items[parent].priority <= this._items[index].priority) break; [this._items[parent], this._items[index]] = [this._items[index], this._items[parent]]; index = parent; } } private _bubbleDown(index: number): void { const n = this._items.length; while (true) { let smallest = index; const left = index * 2 + 1; const right = left + 1; if (left < n && this._items[left].priority < this._items[smallest].priority) smallest = left; if (right < n && this._items[right].priority < this._items[smallest].priority) smallest = right; if (smallest === index) break; [this._items[smallest], this._items[index]] = [this._items[index], this._items[smallest]]; index = smallest; } } } const INF = 1_000_000_000; const ORTH_STEP = 1000; const DIAG_STEP = 1414; const ORTH_DIRS = [ { dr: -1, dc: 0, cost: ORTH_STEP }, { dr: 1, dc: 0, cost: ORTH_STEP }, { dr: 0, dc: -1, cost: ORTH_STEP }, { dr: 0, dc: 1, cost: ORTH_STEP }, ]; const DIAG_DIRS = [ { dr: -1, dc: -1, cost: DIAG_STEP }, { dr: -1, dc: 1, cost: DIAG_STEP }, { dr: 1, dc: -1, cost: DIAG_STEP }, { dr: 1, dc: 1, cost: DIAG_STEP }, ]; const DIRS4 = [ { dr: -1, dc: 0 }, { dr: 1, dc: 0 }, { dr: 0, dc: -1 }, { dr: 0, dc: 1 }, ]; const DIRS8 = [ ...DIRS4, { dr: -1, dc: -1 }, { dr: -1, dc: 1 }, { dr: 1, dc: -1 }, { dr: 1, dc: 1 }, ]; /** * 战场格子导航(纯逻辑,与实体类型无关) * * | API | 用途 | * |-----|------| * | `rebuild` / `shortestTargetCell` / `buildPathToTarget` | 朝攻击点多源最短路径 | * | `findPath` | 任意两点 8/4 向寻路(技能、投掷物等) | * | `checkMultiSourceReachability` | 多起点多终点可达性(扩格堵路等业务规则) | * | `estimatePathLength` | 沿路径剩余逻辑距离 | */ interface DistanceFieldBuffers { dist: Int32Array; bestTarget: Int32Array; nextStep: Int32Array; builtVersion: number; } export class Pathfinder { private _version = 0; private _builtVersion = -1; private _canWalk: Uint8Array | null = null; private _dist: Int32Array | null = null; private _bestTarget: Int32Array | null = null; private _nextStep: Int32Array | null = null; private _heap = new MinHeap<{ idx: number; d: number }>(); private _topology: MapTopology | null = null; /** 多阵营区域时按 campAreaId 分区的距离场 */ private _areaFields = new Map(); private _multiCampArea = false; /** 与 MapTopology.campAreaByCell 同索引;多阵营寻路时限制不得跨区移动 */ private _campAreaByCell: Int32Array | null = null; /** 上次成功 rebuild 时的 walkRevision / 拓扑指纹,供 early-out 判定是否需要重算 */ private _builtWalkRevision = -1; private _builtTopologySig = ''; /** estimatePathLength 写入格心,避免每格分配 */ private _worldScratch = { x: 0, y: 0 }; constructor(private readonly _grid: GridMap) {} /** 拓扑或可走状态变更后调用,使距离场失效 */ invalidate(): void { this._version++; } /** * 基于当前 GridMap + 拓扑重建 8 向距离场。 * 单阵营:全局一场;多阵营:每个 campAreaId 独立距离场(仅含该区域攻击点)。 * version / walkRevision / 攻击点指纹均未变时跳过全量 Dijkstra(不改任何寻路结果)。 */ rebuild(topology: MapTopology): void { const size = this._grid.rows * this._grid.cols; const walkRev = this._grid.walkRevision; const topoSig = this._topologySignature(topology); if (this._canReuseBuiltField(topology, size, walkRev, topoSig)) { return; } this._topology = topology; this._ensureBuffers(size); for (let i = 0; i < size; i++) { const cell = this._grid.cellFromIndex(i); this._canWalk![i] = this._grid.getCell(cell)?.walkable ? 1 : 0; } if (!this._campAreaByCell || this._campAreaByCell.length !== size) { this._campAreaByCell = new Int32Array(size); } for (let i = 0; i < size; i++) { this._campAreaByCell[i] = topology.campAreaByCell[i] ?? 0; } const distinctAreas = topology.getDistinctCampAreaIds(); this._multiCampArea = distinctAreas.length > 1; if (this._multiCampArea) { // 复用同 areaId 的场缓冲,避免每次 rebuild 新建 TypedArray const nextFields = new Map(); for (const areaId of distinctAreas) { const targets = topology.getAttackTargetsForCampArea(areaId); const reused = this._areaFields.get(areaId); const field = (reused && reused.dist.length === size) ? reused : this._createFieldBuffers(size); this._fillDistanceField(targets, field, areaId); nextFields.set(areaId, field); } this._areaFields = nextFields; } else { this._areaFields.clear(); this._dist!.fill(INF); this._bestTarget!.fill(-1); this._nextStep!.fill(-1); const field: DistanceFieldBuffers = { dist: this._dist!, bestTarget: this._bestTarget!, nextStep: this._nextStep!, builtVersion: -1, }; this._fillDistanceField(topology.attackTargets, field); this._builtVersion = this._version; this._builtWalkRevision = walkRev; this._builtTopologySig = topoSig; return; } this._builtVersion = this._version; this._builtWalkRevision = walkRev; this._builtTopologySig = topoSig; } /** 与上次成功 rebuild 完全一致时可跳过全量重算 */ private _canReuseBuiltField( topology: MapTopology, size: number, walkRev: number, topoSig: string, ): boolean { return this._builtVersion === this._version && this._builtWalkRevision === walkRev && this._builtTopologySig === topoSig && this._topology === topology && this._canWalk?.length === size && (this._multiCampArea ? this._areaFields.size > 0 : this._dist != null && this._bestTarget != null && this._nextStep != null); } /** * 攻击点 / 阵营区轻量指纹。 * applyTopology 未 invalidate 时也能识别「同一 topology 对象内容已变」。 */ private _topologySignature(topology: MapTopology): string { const targets = topology.attackTargets; const areas = topology.attackTargetCampArea; let sig = topology.enablePatrolLoop ? '1|' : '0|'; sig += targets.length; for (let i = 0; i < targets.length; i++) { const t = targets[i]; sig += `|${t.row},${t.col},${areas[i] ?? 0}`; } const distinct = topology.getDistinctCampAreaIds(); sig += `|#${distinct.length}`; for (let i = 0; i < distinct.length; i++) { sig += `,${distinct[i]}`; } return sig; } /** * @param from 起点格 * @returns 最近攻击目标格;不可达返回 null */ shortestTargetCell(from: Cell, campAreaId?: number): Cell | null { const field = this._resolveField(from, campAreaId); if (!field) return null; if (!this._grid.inBounds(from)) return null; const startIdx = this._grid.indexOf(from); if (!this._canWalk![startIdx]) return null; const targetIdx = field.bestTarget[startIdx]; if (targetIdx < 0) return null; return this._grid.cellFromIndex(targetIdx); } /** * @param from 当前格 * @returns 朝最近攻击点前进的下一格;已在目标或不可达返回 null */ nextStepCell(from: Cell, campAreaId?: number): Cell | null { const field = this._resolveField(from, campAreaId); if (!field) return null; if (!this._grid.inBounds(from)) return null; const idx = this._grid.indexOf(from); const ni = field.nextStep[idx]; if (ni < 0) return null; return this._grid.cellFromIndex(ni); } /** * @param from 起点 * @param to 终点 * @returns 8 向距离场回溯路径(含起点与终点) */ buildPathToTarget(from: Cell, to: Cell, campAreaId?: number): Cell[] { const areaId = campAreaId ?? this._inferCampAreaId(from); const path: Cell[] = []; let cur = { ...from }; const maxStep = this._grid.rows * this._grid.cols; for (let i = 0; i < maxStep; i++) { path.push({ ...cur }); if (cur.row === to.row && cur.col === to.col) return path; const next = this.nextStepCell(cur, areaId); if (!next) return []; cur = next; } return []; } /** * 距最近攻击目标的路径代价(与内部距离场同单位);不可达返回 Infinity。 */ getPathCost(cell: Cell, campAreaId?: number): number { const field = this._resolveField(cell, campAreaId); if (!field) return Number.POSITIVE_INFINITY; if (!this._grid.inBounds(cell)) return Number.POSITIVE_INFINITY; const idx = this._grid.indexOf(cell); if (!this._canWalk![idx]) return Number.POSITIVE_INFINITY; const d = field.dist[idx]; return d >= INF ? Number.POSITIVE_INFINITY : d; } /** * 出怪口 → 攻击点的进路(含两端)。 * @param spawn 出怪口 * @param spawnPort 出怪口编号 */ getApproachPath(spawn: Cell, spawnPort?: number): Cell[] { const campAreaId = spawnPort !== undefined ? this._topology?.getSpawnPortCampArea(spawnPort) : this._inferCampAreaId(spawn); const target = this.shortestTargetCell(spawn, campAreaId); if (!target) return []; let forward = this.buildPathToTarget(spawn, target, campAreaId); if (forward.length < 2) { forward = this.findPath(spawn, target, true, campAreaId); } return forward; } /** * 进路上离 from 最近的格。from 不必可走。 * @param from 参考格 * @param spawn 出怪口 * @param spawnPort 出怪口编号 */ nearestApproachCell(from: Cell, spawn: Cell, spawnPort?: number): Cell | null { const forward = this.getApproachPath(spawn, spawnPort); const idx = this._nearestIndexOnPath(forward, from); if (idx < 0) return null; return { ...forward[idx] }; } /** * 沿「出怪口 → 攻击点」前进路线反向后退 maxSteps 格。 * 只走该 spawn 的进路格,不会按距离场拐到旁路。 * 若当前不在进路上,先吸附到最近进路格再后退。 * @returns 含起点的路径;不可退返回 [] */ buildRetreatPath(from: Cell, maxSteps: number, spawn: Cell, spawnPort?: number): Cell[] { if (maxSteps <= 0) return []; if (!this._grid.inBounds(from) || !this._grid.inBounds(spawn)) return []; const fromIdx = this._grid.indexOf(from); if (!this._canWalk![fromIdx]) return []; const forward = this.getApproachPath(spawn, spawnPort); const idx = this._nearestIndexOnPath(forward, from); if (idx < 0) return []; if (idx === 0) return [{ ...forward[0] }]; const end = Math.max(0, idx - maxSteps); const path: Cell[] = []; for (let i = idx; i >= end; i--) { path.push({ ...forward[i] }); } return path; } /** * 折线上离 from 最近的下标(先精确格,再曼哈顿)。 * @param path 进路 * @param from 参考格 */ private _nearestIndexOnPath(path: readonly Cell[], from: Cell): number { if (path.length < 1) return -1; for (let i = 0; i < path.length; i++) { if (path[i].row === from.row && path[i].col === from.col) return i; } let best = 0; let bestD = Number.POSITIVE_INFINITY; for (let i = 0; i < path.length; i++) { const d = Math.abs(path[i].row - from.row) + Math.abs(path[i].col - from.col); if (d < bestD) { bestD = d; best = i; } } return best; } /** * 任意两点寻路(与实体类型无关:怪物、技能、投掷物等均可用) * @param from 起点格 * @param to 终点格 * @param use8Dir 是否 8 向(默认 true) * @returns 路径(含起点与终点);不可达返回空数组 */ findPath(from: Cell, to: Cell, use8Dir = true, campAreaId?: number): Cell[] { if (!this._grid.inBounds(from) || !this._grid.inBounds(to)) return []; if (!this._grid.isWalkable(from) || !this._grid.isWalkable(to)) return []; if (from.row === to.row && from.col === to.col) return [{ ...from }]; const scopeCampAreaId = this._multiCampArea ? (campAreaId ?? this._inferCampAreaId(from)) : undefined; if (scopeCampAreaId === undefined && this._multiCampArea) return []; const startIdx = this._grid.indexOf(from); const endIdx = this._grid.indexOf(to); const dirs = use8Dir ? DIRS8 : DIRS4; const queue: number[] = [startIdx]; const visited = createMembershipSet(); membershipAdd(visited, startIdx); const prev = new Map(); let head = 0; while (head < queue.length) { const idx = queue[head++]; if (idx === endIdx) { const path: Cell[] = []; let cur: number | undefined = endIdx; while (cur !== undefined) { path.unshift(this._grid.cellFromIndex(cur)); if (cur === startIdx) break; cur = prev.get(cur); } return path; } const cur = this._grid.cellFromIndex(idx); for (const dir of dirs) { const nb = { row: cur.row + dir.dr, col: cur.col + dir.dc }; if (!this._grid.inBounds(nb)) continue; const ni = this._grid.indexOf(nb); if (visited.has(ni)) continue; if (!this._canTraverse(idx, ni, scopeCampAreaId)) continue; if (use8Dir && Math.abs(dir.dr) + Math.abs(dir.dc) === 2) { const orth1Idx = (cur.row + dir.dr) * this._grid.cols + cur.col; const orth2Idx = cur.row * this._grid.cols + (cur.col + dir.dc); if (!this._canTraverse(idx, orth1Idx, scopeCampAreaId) || !this._canTraverse(idx, orth2Idx, scopeCampAreaId)) { continue; } } membershipAdd(visited, ni); prev.set(ni, idx); queue.push(ni); } } return []; } /** * 估算沿路径的剩余逻辑距离(像素,与 GridMap.cellToWorld 同坐标系) * @param origin 当前世界坐标 * @param path 路径格序列 * @param startIndex 从路径第几格开始累计(默认 0) */ estimatePathLength( origin: { x: number; y: number }, path: readonly Cell[], startIndex = 0, ): number { if (startIndex >= path.length) return 0; let total = 0; let x = origin.x; let y = origin.y; const w = this._worldScratch; for (let i = startIndex; i < path.length; i++) { this._grid.cellToWorldInto(path[i], w); const dx = w.x - x; const dy = w.y - y; total += Math.sqrt(dx * dx + dy * dy); x = w.x; y = w.y; } return total; } /** * 多起点多终点可达性(项目层业务规则用,如扩格堵路检测) * @param sources 起点格列表 * @param targets 终点格列表 * @param blocked 假设阻塞的格 * @param use8Dir 是否 8 向(默认 4 向,与现有 MapManager 扩格校验一致) */ checkMultiSourceReachability( sources: Cell[], targets: Cell[], blocked?: Cell, use8Dir = false, ): boolean { if (!sources.length || !targets.length) return false; const blockedIdx = blocked ? this._grid.indexOf(blocked) : -1; const startCells: Cell[] = []; const startSeen = createMembershipSet(); for (const c of sources) { if (!this._grid.inBounds(c)) continue; const idx = this._grid.indexOf(c); if (idx === blockedIdx) continue; if (!this._grid.getCell(c)?.walkable) continue; if (startSeen.has(idx)) continue; membershipAdd(startSeen, idx); startCells.push(c); } if (!startCells.length) return false; const endCells: Cell[] = []; const endSeen = createMembershipSet(); for (const c of targets) { if (!this._grid.inBounds(c)) continue; if (!this._grid.getCell(c)?.walkable) continue; const idx = this._grid.indexOf(c); if (endSeen.has(idx)) continue; membershipAdd(endSeen, idx); endCells.push(c); } if (!endCells.length) return false; const fromEnds = this._bfsReachable(endCells, use8Dir, blockedIdx); for (const s of startCells) { if (!fromEnds.has(this._grid.indexOf(s))) return false; } const fromStarts = this._bfsReachable(startCells, use8Dir, blockedIdx); for (const t of endCells) { if (!fromStarts.has(this._grid.indexOf(t))) return false; } return true; } private _ensureBuilt(): void { if (this._builtVersion !== this._version) { throw new Error('[Pathfinder] distance field stale; call rebuild() after topology/walkability change'); } } private _createFieldBuffers(size: number): DistanceFieldBuffers { return { dist: new Int32Array(size), bestTarget: new Int32Array(size), nextStep: new Int32Array(size), builtVersion: -1, }; } private _fillDistanceField( targets: readonly Cell[], field: DistanceFieldBuffers, scopeCampAreaId?: number, ): void { const size = this._grid.rows * this._grid.cols; field.dist.fill(INF); field.bestTarget.fill(-1); field.nextStep.fill(-1); this._heap.clear(); for (const target of targets) { if (!this._grid.inBounds(target)) continue; const idx = this._grid.indexOf(target); if (!this._canWalk![idx]) continue; if (field.dist[idx] > 0) { field.dist[idx] = 0; field.bestTarget[idx] = idx; field.nextStep[idx] = -1; this._heap.push({ idx, d: 0 }, 0); } } const canWalk = this._canWalk!; const dist = field.dist; const bestTarget = field.bestTarget; const nextStep = field.nextStep; const cols = this._grid.cols; const rows = this._grid.rows; const popFresh = (): { idx: number; d: number } | null => { while (this._heap.length > 0) { const item = this._heap.pop()!; if (item.d === dist[item.idx]) return item; } return null; }; while (true) { const item = popFresh(); if (!item) break; const curIdx = item.idx; const curDist = item.d; const curRow = Math.floor(curIdx / cols); const curCol = curIdx - curRow * cols; for (const dir of ORTH_DIRS) { this._relaxNeighbor( curIdx, curRow, curCol, curDist, dir.dr, dir.dc, dir.cost, rows, cols, canWalk, dist, bestTarget, nextStep, scopeCampAreaId, ); } for (const dir of DIAG_DIRS) { const orth1Idx = (curRow + dir.dr) * cols + curCol; const orth2Idx = curRow * cols + (curCol + dir.dc); if (!this._canTraverse(curIdx, orth1Idx, scopeCampAreaId) || !this._canTraverse(curIdx, orth2Idx, scopeCampAreaId)) { continue; } this._relaxNeighbor( curIdx, curRow, curCol, curDist, dir.dr, dir.dc, dir.cost, rows, cols, canWalk, dist, bestTarget, nextStep, scopeCampAreaId, ); } } field.builtVersion = this._version; } private _inferCampAreaId(cell: Cell): number | undefined { return this._topology?.getCampAreaAtCell(cell); } private _resolveField(from: Cell, campAreaId?: number): DistanceFieldBuffers | null { this._ensureBuilt(); if (!this._multiCampArea) { if (!this._dist || !this._bestTarget || !this._nextStep) return null; return { dist: this._dist, bestTarget: this._bestTarget, nextStep: this._nextStep, builtVersion: this._builtVersion, }; } const areaId = campAreaId ?? this._inferCampAreaId(from); if (areaId === undefined) { console.error('[Pathfinder] 多阵营地图缺少 campAreaId', from); return null; } const field = this._areaFields.get(areaId); if (!field) { console.error('[Pathfinder] 未找到阵营区域距离场', areaId); return null; } return field; } private _ensureBuffers(size: number): void { if (!this._canWalk || this._canWalk.length !== size) this._canWalk = new Uint8Array(size); if (!this._dist || this._dist.length !== size) this._dist = new Int32Array(size); if (!this._bestTarget || this._bestTarget.length !== size) this._bestTarget = new Int32Array(size); if (!this._nextStep || this._nextStep.length !== size) this._nextStep = new Int32Array(size); } private _canTraverse(curIdx: number, nbIdx: number, scopeCampAreaId?: number): boolean { if (!this._canWalk || !this._canWalk[nbIdx]) return false; if (scopeCampAreaId === undefined) return true; const areas = this._campAreaByCell; if (!areas) return false; const curArea = areas[curIdx]; const nbArea = areas[nbIdx]; return curArea === scopeCampAreaId && nbArea === scopeCampAreaId; } private _relaxNeighbor( curIdx: number, curRow: number, curCol: number, curDist: number, dr: number, dc: number, cost: number, rows: number, cols: number, canWalk: Uint8Array, dist: Int32Array, bestTarget: Int32Array, nextStep: Int32Array, scopeCampAreaId?: number, ): void { const nr = curRow + dr; const nc = curCol + dc; if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) return; const nbIdx = nr * cols + nc; if (!canWalk[nbIdx]) return; if (!this._canTraverse(curIdx, nbIdx, scopeCampAreaId)) return; const nd = curDist + cost; if (nd < dist[nbIdx]) { dist[nbIdx] = nd; bestTarget[nbIdx] = bestTarget[curIdx]; nextStep[nbIdx] = curIdx; this._heap.push({ idx: nbIdx, d: nd }, nd); } } private _bfsReachable(seeds: Cell[], use8Dir: boolean, blockedIdx: number): MembershipSet { const reachable = createMembershipSet(); const queue: number[] = []; const visited = createMembershipSet(); const dirs = use8Dir ? DIRS8 : DIRS4; for (const seed of seeds) { const idx = this._grid.indexOf(seed); if (visited.has(idx)) continue; membershipAdd(visited, idx); queue.push(idx); membershipAdd(reachable, idx); } let head = 0; while (head < queue.length) { const curIdx = queue[head++]; const cur = this._grid.cellFromIndex(curIdx); for (const dir of dirs) { const nb = { row: cur.row + dir.dr, col: cur.col + dir.dc }; if (!this._grid.inBounds(nb)) continue; const ni = this._grid.indexOf(nb); if (ni === blockedIdx) continue; if (!this._grid.getCell(nb)?.walkable) continue; if (visited.has(ni)) continue; membershipAdd(visited, ni); queue.push(ni); membershipAdd(reachable, ni); } } return reachable; } }