You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

707 lines
26 KiB

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<T> {
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<number, DistanceFieldBuffers>();
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<number, DistanceFieldBuffers>();
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<number>();
membershipAdd(visited, startIdx);
const prev = new Map<number, number>();
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<number>();
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<number>();
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<number> {
const reachable = createMembershipSet<number>();
const queue: number[] = [];
const visited = createMembershipSet<number>();
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;
}
}