template/packages-user/data-system/src/path/graph.ts
unanmed d9ee80f77f refactor(02-02): apply user review directives on path module
- 移除 system.ts 中 'mover' in movable 运行时形状检查与 IMovableWithMover,
  依 types.ts 修正后的类型直接绑定 IObjectMover<IObjectMovable>
- path 内导出类型(IPathGraph*、IPathfindingGraphBuilder)归位 types.ts,
  graph.ts/finder.ts 改由 types 导入
- 移除类与接口声明上的 jsdoc、类实现侧与接口重复的 jsdoc、region 标记;
  实现注释改为仅描述可观察行为(去除 Dijkstra 等实现叙述)
- 私有方法置于调用者之前(resolveFloorId、getNodeCost、search、startMove)
- system.test.ts 对齐 useMover(mover) 新签名并同步测试描述
2026-09-09 18:47:40 +08:00

162 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
DirectionMapper,
IDirectionDescriptor,
IDirectionMapper,
InternalDirectionGroup,
logger
} from '@motajs/common';
import { FaceDirection, IDataCommon } from '@user/data-common';
import {
ILayerLocation,
IMapLayer,
IMapState,
IPassCheckHandler,
IPassPredicate
} from '@user/data-base';
import { isNil } from 'lodash-es';
import {
IPathGraph,
IPathGraphEdge,
IPathGraphNode,
IPathfindingGraphBuilder
} from './types';
/**
* 将方向描述器的坐标增量解析为对应的朝向
* @param x 横坐标增量
* @param y 纵坐标增量
*/
function directionOf(x: number, y: number): FaceDirection {
if (x === 0 && y === -1) return FaceDirection.Up;
if (x === 0 && y === 1) return FaceDirection.Down;
if (x === -1 && y === 0) return FaceDirection.Left;
if (x === 1 && y === 0) return FaceDirection.Right;
if (x === -1 && y === -1) return FaceDirection.LeftUp;
if (x === 1 && y === -1) return FaceDirection.RightUp;
if (x === -1 && y === 1) return FaceDirection.LeftDown;
if (x === 1 && y === 1) return FaceDirection.RightDown;
return FaceDirection.Unknown;
}
export class PathfindingGraphBuilder implements IPathfindingGraphBuilder {
/** 绑定的地图状态对象,用于解析图层所属楼层 id */
private maps: IMapState | null = null;
/** 绑定的地图图层,图节点来源 */
private layer: IMapLayer | null = null;
/** 注入的通行性谓词,用于判定边的可行性与终端节点 */
private predicate: IPassPredicate | null = null;
/** 邻域方向组别,默认四正交方向 */
private group: number = InternalDirectionGroup.Dir4;
/** 方向组解析对象 */
private readonly mapper: IDirectionMapper = new DirectionMapper();
useMapState(maps: IMapState | null): void {
this.maps = maps;
}
useMapLayer(layer: IMapLayer | null): void {
this.layer = layer;
}
usePassPredicate(predicate: IPassPredicate | null): void {
this.predicate = predicate;
}
useDirGroup(group: number): void {
this.group = group;
}
/**
* 解析绑定图层所属的楼层 id
* @returns 楼层 id无法解析时为 `undefined`
*/
private resolveFloorId(): string | undefined {
const maps = this.maps;
const layer = this.layer;
if (!maps || !layer) return undefined;
for (const [floorId, map] of maps.iterateAllMaps()) {
if (map === layer.map) return floorId;
}
return undefined;
}
build(): IPathGraph {
const layer = this.layer;
if (isNil(layer)) {
logger.warn(173);
return { width: 0, height: 0, nodes: new Map() };
}
const width = layer.width;
const height = layer.height;
const floorId = this.resolveFloorId();
const state: IDataCommon = layer.state;
const blocks: (ILayerLocation | null)[] = new Array(
width * height
).fill(null);
// 收集图内全部图块作为图节点
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
if (!layer.inMap(x, y)) continue;
const loc = layer.getLocationData(x, y);
if (!loc) continue;
blocks[y * width + x] = loc;
}
}
const terminals: Set<number> = new Set();
const adjacency: Map<number, IPathGraphEdge[]> = new Map();
const dirs: IDirectionDescriptor[] = [...this.mapper.map(this.group)];
// 逐节点判定邻域边可行性
for (let index = 0; index < blocks.length; index++) {
const block = blocks[index];
if (!block) continue;
const x = index % width;
const y = Math.floor(index / width);
const edges: IPathGraphEdge[] = [];
for (const desc of dirs) {
const dir = directionOf(desc.x, desc.y);
if (dir === FaceDirection.Unknown) continue;
const nx = x + desc.x;
const ny = y + desc.y;
if (!layer.inMap(nx, ny)) continue;
const next = blocks[ny * width + nx];
if (!next) continue;
const handler: IPassCheckHandler = {
currLoc: block.locator,
nextLoc: next.locator,
direction: dir,
floorId,
state
};
if (isNil(this.predicate) || !this.predicate.canPass(handler)) {
continue;
}
if (this.predicate.shouldHit(handler)) {
terminals.add(ny * width + nx);
}
edges.push({ dir, to: ny * width + nx });
}
adjacency.set(index, edges);
}
const nodes: Map<number, IPathGraphNode> = new Map();
for (let index = 0; index < blocks.length; index++) {
const block = blocks[index];
if (!block) continue;
nodes.set(index, {
index,
x: index % width,
y: Math.floor(index / width),
block,
terminal: terminals.has(index),
edges: adjacency.get(index) ?? []
});
}
return { width, height, nodes };
}
}