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
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;
|
|
}
|
|
}
|
|
|