mirror of
https://github.com/motajs/template.git
synced 2026-09-19 05:30:16 +08:00
235 lines
7.5 KiB
TypeScript
235 lines
7.5 KiB
TypeScript
import { InternalDirectionGroup, ITileLocator, logger } from '@motajs/common';
|
|
import {
|
|
IMapLayer,
|
|
IMapState,
|
|
IPassPredicate,
|
|
IStateBase
|
|
} from '@user/data-base';
|
|
import { isNil } from 'lodash-es';
|
|
import { PathfindingGraphBuilder } from './graph';
|
|
import {
|
|
IPathfinder,
|
|
IPathfindingStep,
|
|
IPathGraph,
|
|
PathCostFunction
|
|
} from './types';
|
|
|
|
interface IDistanceHeapEntry {
|
|
/** 条目的键值,堆中键值最小的条目最先取出 */
|
|
key: number;
|
|
/** 条目携带的值 */
|
|
value: number;
|
|
}
|
|
|
|
class DistanceHeap {
|
|
private readonly entries: IDistanceHeapEntry[] = [];
|
|
private size: number = 0;
|
|
|
|
/**
|
|
* 将指定位置的条目逐层上浮至其键值不再大于父条目的位置
|
|
* @param index 条目所在位置
|
|
*/
|
|
private siftUp(index: number): void {
|
|
while (index > 0) {
|
|
const parent = (index - 1) >> 1;
|
|
if (this.entries[parent]!.key <= this.entries[index]!.key) break;
|
|
const temp = this.entries[parent]!;
|
|
this.entries[parent] = this.entries[index]!;
|
|
this.entries[index] = temp;
|
|
index = parent;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 将指定位置的条目逐层下沉至其键值不再大于子条目的位置
|
|
* @param index 条目所在位置
|
|
*/
|
|
private siftDown(index: number): void {
|
|
while (true) {
|
|
const left = index * 2 + 1;
|
|
const right = left + 1;
|
|
let smallest = index;
|
|
if (
|
|
left < this.size &&
|
|
this.entries[left]!.key < this.entries[smallest]!.key
|
|
) {
|
|
smallest = left;
|
|
}
|
|
if (
|
|
right < this.size &&
|
|
this.entries[right]!.key < this.entries[smallest]!.key
|
|
) {
|
|
smallest = right;
|
|
}
|
|
if (smallest === index) break;
|
|
const temp = this.entries[smallest]!;
|
|
this.entries[smallest] = this.entries[index]!;
|
|
this.entries[index] = temp;
|
|
index = smallest;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 插入一个键值条目,键值相同的条目按插入先后取出
|
|
* @param key 条目的键值
|
|
* @param value 条目携带的值
|
|
*/
|
|
push(key: number, value: number): void {
|
|
this.entries[this.size] = { key, value };
|
|
this.siftUp(this.size);
|
|
this.size++;
|
|
}
|
|
|
|
/**
|
|
* 取出键值最小的条目,堆为空时返回 `null`
|
|
*/
|
|
pop(): IDistanceHeapEntry | null {
|
|
if (this.size === 0) return null;
|
|
const top = this.entries[0]!;
|
|
this.size--;
|
|
if (this.size > 0) {
|
|
this.entries[0] = this.entries[this.size]!;
|
|
this.siftDown(0);
|
|
}
|
|
return top;
|
|
}
|
|
}
|
|
|
|
export class PathfindingFinder implements IPathfinder {
|
|
/** 当前对象对应的数据层对象 */
|
|
readonly state: IStateBase;
|
|
|
|
/** 绑定的地图状态对象,用于解析楼层 id */
|
|
private maps: IMapState | null = null;
|
|
/** 绑定的地图图层,图节点来源 */
|
|
private layer: IMapLayer | null = null;
|
|
/** 注入的通行性谓词,用于判定边可行性与终端节点 */
|
|
private predicate: IPassPredicate | null = null;
|
|
/** 注入的损失函数,未注入时每格损失 1 */
|
|
private cost: PathCostFunction | null = null;
|
|
/** 邻域方向组别,默认四正交方向 */
|
|
private group: number = InternalDirectionGroup.Dir4;
|
|
|
|
constructor(state: IStateBase) {
|
|
this.state = state;
|
|
}
|
|
|
|
useMapState(maps: IMapState | null): void {
|
|
this.maps = maps;
|
|
}
|
|
|
|
useMapLayer(layer: IMapLayer | null): void {
|
|
this.layer = layer;
|
|
}
|
|
|
|
useCostFunction(cost: PathCostFunction | null): void {
|
|
this.cost = cost;
|
|
}
|
|
|
|
usePassPredicate(predicate: IPassPredicate | null): void {
|
|
this.predicate = predicate;
|
|
}
|
|
|
|
useDirGroup(group: number): void {
|
|
this.group = group;
|
|
}
|
|
|
|
/**
|
|
* 在有向图上执行最小损失搜索,终端节点仅可作为路径终点,不可作为中间节点
|
|
* @param graph 寻路有向图
|
|
* @param start 寻路起始位置
|
|
* @param target 寻路目标位置
|
|
*/
|
|
private search(
|
|
graph: IPathGraph,
|
|
start: ITileLocator,
|
|
target: ITileLocator
|
|
): IPathfindingStep[] {
|
|
const startIndex = start.y * graph.width + start.x;
|
|
const targetIndex = target.y * graph.width + target.x;
|
|
if (startIndex === targetIndex) return [];
|
|
if (!graph.nodes.has(startIndex) || !graph.nodes.has(targetIndex)) {
|
|
return [];
|
|
}
|
|
|
|
const dist: Map<number, number> = new Map();
|
|
const prev: Map<number, IPathfindingStep> = new Map();
|
|
const visited: Set<number> = new Set();
|
|
dist.set(startIndex, 0);
|
|
const heap: DistanceHeap = new DistanceHeap();
|
|
heap.push(0, startIndex);
|
|
|
|
while (true) {
|
|
const entry = heap.pop();
|
|
if (!entry) return [];
|
|
if (visited.has(entry.value)) continue;
|
|
const currIndex = entry.value;
|
|
const currDist = entry.key;
|
|
// 取出的条目可能已过期,仅当其键值与当前最小损失一致时才有效
|
|
if (currDist !== dist.get(currIndex)) continue;
|
|
if (currIndex === targetIndex) break;
|
|
visited.add(currIndex);
|
|
const node = graph.nodes.get(currIndex);
|
|
if (!node || node.terminal) continue;
|
|
for (const edge of node.edges) {
|
|
if (visited.has(edge.to)) continue;
|
|
const next = graph.nodes.get(edge.to);
|
|
if (!next) continue;
|
|
if (next.terminal && edge.to !== targetIndex) continue;
|
|
const total = currDist + next.cost;
|
|
const known = dist.get(edge.to);
|
|
if (isNil(known) || total < known) {
|
|
dist.set(edge.to, total);
|
|
prev.set(edge.to, {
|
|
dir: edge.dir,
|
|
from: { x: node.x, y: node.y },
|
|
to: { x: next.x, y: next.y }
|
|
});
|
|
heap.push(total, edge.to);
|
|
}
|
|
}
|
|
}
|
|
|
|
const steps: IPathfindingStep[] = [];
|
|
let curr = targetIndex;
|
|
while (curr !== startIndex) {
|
|
const step = prev.get(curr);
|
|
if (!step) return [];
|
|
steps.push(step);
|
|
curr = step.from.y * graph.width + step.from.x;
|
|
}
|
|
steps.reverse();
|
|
return steps;
|
|
}
|
|
|
|
/**
|
|
* 地图状态或图层未绑定、坐标越界等非法输入下告警并返回空数组,
|
|
* 目标不可达时同样返回空数组
|
|
*/
|
|
find(start: ITileLocator, target: ITileLocator): IPathfindingStep[] {
|
|
const maps = this.maps;
|
|
const layer = this.layer;
|
|
if (isNil(maps) || isNil(layer)) {
|
|
logger.warn(173);
|
|
return [];
|
|
}
|
|
if (
|
|
!layer.inMap(start.x, start.y) ||
|
|
!layer.inMap(target.x, target.y)
|
|
) {
|
|
logger.warn(173);
|
|
return [];
|
|
}
|
|
|
|
// 数据端状态可变,每次寻路动态构建图,不做缓存
|
|
const builder = new PathfindingGraphBuilder();
|
|
builder.useMapState(maps);
|
|
builder.useMapLayer(layer);
|
|
builder.useCostFunction(this.cost);
|
|
builder.usePassPredicate(this.predicate);
|
|
builder.useDirGroup(this.group);
|
|
const graph = builder.build(start);
|
|
return this.search(graph, start, target);
|
|
}
|
|
}
|