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) 新签名并同步测试描述
This commit is contained in:
unanmed 2026-09-09 18:47:40 +08:00
parent 9a604422f8
commit d9ee80f77f
6 changed files with 165 additions and 264 deletions

View File

@ -7,16 +7,14 @@ import {
IStateBase
} from '@user/data-base';
import { isNil } from 'lodash-es';
import { IPathGraph, PathfindingGraphBuilder } from './graph';
import { IPathfinder, IPathfindingStep, PathCostFunction } from './types';
import { PathfindingGraphBuilder } from './graph';
import {
IPathfinder,
IPathfindingStep,
IPathGraph,
PathCostFunction
} from './types';
//#region 寻路求解器
/**
*
* 1
*
*/
export class PathfindingFinder implements IPathfinder {
/** 当前对象对应的数据层对象 */
readonly state: IStateBase;
@ -36,80 +34,43 @@ export class PathfindingFinder implements IPathfinder {
this.state = state;
}
/**
*
* @param maps `null`
*/
useMapState(maps: IMapState | null): void {
this.maps = maps;
}
/**
*
* @param layer `null`
*/
useMapLayer(layer: IMapLayer | null): void {
this.layer = layer;
}
/**
*
* @param cost `null`
*/
useCostFunction(cost: PathCostFunction | null): void {
this.cost = cost;
}
/**
*
* @param predicate `null`
*/
usePassPredicate(predicate: IPassPredicate | null): void {
this.predicate = predicate;
}
/**
* 使
* @param group
*/
useDirGroup(group: number): void {
this.group = group;
}
/**
*
*
*
* @param start
* @param target
*
* 1
* @param block
*/
find(start: ITileLocator, target: ITileLocator): IPathfindingStep[] {
const maps = this.maps;
const layer = this.layer;
if (isNil(maps) || isNil(layer)) {
logger.warn(173);
return [];
private getNodeCost(block: ILayerLocation): number {
if (!this.cost) return 1;
const value = this.cost(block);
if (!Number.isFinite(value) || value < 0) {
logger.warn(174);
return 1;
}
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.usePassPredicate(this.predicate);
builder.useDirGroup(this.group);
const graph = builder.build();
return this.search(graph, start, target);
return value;
}
/**
* Dijkstra
*
*
* @param graph
* @param start
@ -177,19 +138,31 @@ export class PathfindingFinder implements IPathfinder {
}
/**
*
* 1
* @param block
*
*
*/
private getNodeCost(block: ILayerLocation): number {
if (!this.cost) return 1;
const value = this.cost(block);
if (!Number.isFinite(value) || value < 0) {
logger.warn(174);
return 1;
find(start: ITileLocator, target: ITileLocator): IPathfindingStep[] {
const maps = this.maps;
const layer = this.layer;
if (isNil(maps) || isNil(layer)) {
logger.warn(173);
return [];
}
return value;
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.usePassPredicate(this.predicate);
builder.useDirGroup(this.group);
const graph = builder.build();
return this.search(graph, start, target);
}
}
//#endregion

View File

@ -61,7 +61,6 @@ beforeAll(async () => {
};
});
/** 测试图块定义,键为图块数字 */
interface TestTileDefinition {
/** 图块数字 */
num: number;

View File

@ -14,43 +14,12 @@ import {
IPassPredicate
} from '@user/data-base';
import { isNil } from 'lodash-es';
//#region 图结构
export interface IPathGraphEdge {
/** 本条边对应的移动方向 */
readonly dir: FaceDirection;
/** 边指向的节点索引,值为 y * width + x */
readonly to: number;
}
export interface IPathGraphNode {
/** 节点索引,值为 y * width + x */
readonly index: number;
/** 节点横坐标 */
readonly x: number;
/** 节点纵坐标 */
readonly y: number;
/** 节点对应的位置信息,用于损失计算 */
readonly block: ILayerLocation;
/** 该节点是否仅可作为路径终点,不可作为中间节点 */
readonly terminal: boolean;
/** 该节点的全部出边 */
readonly edges: readonly IPathGraphEdge[];
}
export interface IPathGraph {
/** 图宽度 */
readonly width: number;
/** 图高度 */
readonly height: number;
/** 图内全部节点,键为节点索引,值为 y * width + x */
readonly nodes: ReadonlyMap<number, IPathGraphNode>;
}
//#endregion
//#region 方向解析
import {
IPathGraph,
IPathGraphEdge,
IPathGraphNode,
IPathfindingGraphBuilder
} from './types';
/**
*
@ -69,51 +38,6 @@ function directionOf(x: number, y: number): FaceDirection {
return FaceDirection.Unknown;
}
//#endregion
//#region 有向图构建
/**
*
*/
export interface IPathfindingGraphBuilder {
/**
* id
* @param maps `null`
*/
useMapState(maps: IMapState | null): void;
/**
*
* @param layer `null`
*/
useMapLayer(layer: IMapLayer | null): void;
/**
*
* @param predicate `null`
*/
usePassPredicate(predicate: IPassPredicate | null): void;
/**
* 使
* @param group
*/
useDirGroup(group: number): void;
/**
*
*
* @returns
*/
build(): IPathGraph;
}
/**
*
*
*
*/
export class PathfindingGraphBuilder implements IPathfindingGraphBuilder {
/** 绑定的地图状态对象,用于解析图层所属楼层 id */
private maps: IMapState | null = null;
@ -143,6 +67,20 @@ export class PathfindingGraphBuilder implements IPathfindingGraphBuilder {
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)) {
@ -220,20 +158,4 @@ export class PathfindingGraphBuilder implements IPathfindingGraphBuilder {
}
return { width, height, nodes };
}
/**
* 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;
}
}
//#endregion

View File

@ -68,7 +68,6 @@ beforeAll(async () => {
};
});
/** 测试图块定义,键为图块数字 */
interface TestTileDefinition {
/** 图块数字 */
num: number;
@ -111,7 +110,6 @@ const HIT_TILE: TestTileDefinition = {
const ALL_TILES: TestTileDefinition[] = [OPEN_TILE, WALL_TILE, HIT_TILE];
/** 测试用移动对象接口,记录 setPos 调用并携带移动器 */
interface TestTile extends IObjectMovable {
/** 当前横坐标 */
x: number;
@ -202,7 +200,6 @@ class FixturePredicate implements IPassPredicate {
}
}
/** 寻路系统测试夹具 */
interface SystemFixture {
/** 楼层地图对象 */
map: IGameMap;
@ -335,7 +332,7 @@ function createSystem(rows: number[], width: number): SystemFixture {
const layer = map.getLayerByAlias('event')!;
const system = new modules.PathfindingSystem(commonState as never);
const tile = createTestTile();
system.useMover(tile);
system.useMover(tile.mover);
system.finder.useMapState(maps);
system.finder.useMapLayer(layer);
return { map, layer, system, tile };
@ -474,15 +471,15 @@ describe('pathfinding system', () => {
expect(fixture.tile.y).toBe(0);
});
// 验证 moveTo 未绑定移动对象或无路径时返回 null 而非异常
it('returns null from moveTo when movable is unbound or unreachable', () => {
// 验证未绑定移动器或无路径时 moveTo 返回 null 而非异常
it('returns null from moveTo when mover is unbound or unreachable', () => {
const fixture = createSystem([1, 6, 1, 1, 6, 1, 1, 6, 1], 3);
injectPredicate(fixture);
fixture.system.useMover(null);
expect(fixture.system.moveTo({ x: 2, y: 1 })).toBeNull();
fixture.system.useMover(fixture.tile);
fixture.system.useMover(fixture.tile.mover);
expect(fixture.system.moveTo({ x: 2, y: 1 })).toBeNull();
});

View File

@ -11,39 +11,12 @@ import {
PathFallbackPolicy
} from './types';
//#region 移动器识别
/**
*
*/
export interface IMovableWithMover extends IObjectMovable {
/** 移动器对象 */
readonly mover: IObjectMover<IObjectMovable>;
}
/**
*
* @param movable
*/
function hasMover(movable: IObjectMovable): movable is IMovableWithMover {
return 'mover' in movable;
}
//#endregion
//#region 寻路系统
/**
*
* 退退
* 退
*/
export class PathfindingSystem implements IPathfindingSystem {
/** 寻路求解器 */
readonly finder: IPathfinder;
/** 绑定的移动对象 */
private movable: IObjectMovable | null = null;
/** 绑定的移动器对象 */
private mover: IObjectMover<IObjectMovable> | null = null;
/** 注入的瞬移回退策略,未注入时必定回退为逐步寻路 */
private policy: PathFallbackPolicy | null = null;
/** 最近一次寻路移动的控制器包装 */
@ -53,77 +26,31 @@ export class PathfindingSystem implements IPathfindingSystem {
this.finder = new PathfindingFinder(state);
}
/**
*
* @param movable `null`
*/
useMover(movable: IObjectMovable | null): void {
this.movable = movable;
useMover(mover: IObjectMover<IObjectMovable> | null): void {
this.mover = mover;
}
/**
* 退 `null`
* @param policy 退
*/
useFallbackPolicy(policy: PathFallbackPolicy | null): void {
this.policy = policy;
}
/**
*
*
*
* @param target
*/
getPath(target: ITileLocator): IPathfindingStep[] {
const movable = this.movable;
if (isNil(movable)) {
const mover = this.mover;
if (isNil(mover)) {
logger.warn(173);
return [];
}
return this.finder.find({ x: movable.x, y: movable.y }, target);
}
/**
*
* `null`
* @param target
*/
moveTo(target: ITileLocator): IPathfindingController | null {
const path = this.getPath(target);
if (path.length === 0) return null;
return this.startMove(path, false);
}
/**
* 退
* 退退
* `null`
* @param target
*/
teleportTo(target: ITileLocator): IPathfindingController | null {
const path = this.getPath(target);
if (path.length === 0) return null;
if (isNil(this.policy) || this.policy(path)) {
return this.startMove(path, false);
}
return this.startMove(path, true);
}
/**
*
* 03 线
*/
async interrupt(): Promise<void> {
const current = this.current;
this.current = null;
if (current && !current.controller.done) {
await current.controller.stop();
}
const tile = mover.tile;
return this.finder.find({ x: tile.x, y: tile.y }, target);
}
/**
*
* `null`
* `null`
* @param path
* @param teleport
*/
@ -131,12 +58,10 @@ export class PathfindingSystem implements IPathfindingSystem {
path: readonly IPathfindingStep[],
teleport: boolean
): IPathfindingController | null {
const movable = this.movable;
if (!movable) return null;
const mover = this.mover;
if (!mover) return null;
const current = this.current;
if (current && !current.controller.done) return null;
if (!hasMover(movable)) return null;
const mover = movable.mover;
if (teleport) {
const last = path[path.length - 1];
@ -154,6 +79,27 @@ export class PathfindingSystem implements IPathfindingSystem {
this.current = result;
return result;
}
}
//#endregion
moveTo(target: ITileLocator): IPathfindingController | null {
const path = this.getPath(target);
if (path.length === 0) return null;
return this.startMove(path, false);
}
teleportTo(target: ITileLocator): IPathfindingController | null {
const path = this.getPath(target);
if (path.length === 0) return null;
if (isNil(this.policy) || this.policy(path)) {
return this.startMove(path, false);
}
return this.startMove(path, true);
}
async interrupt(): Promise<void> {
const current = this.current;
this.current = null;
if (current && !current.controller.done) {
await current.controller.stop();
}
}
}

View File

@ -121,3 +121,67 @@ export interface IPathfindingSystem extends IDataBaseExtended {
*/
interrupt(): Promise<void>;
}
export interface IPathGraphEdge {
/** 本条边对应的移动方向 */
readonly dir: FaceDirection;
/** 边指向的节点索引,值为 y * width + x */
readonly to: number;
}
export interface IPathGraphNode {
/** 节点索引,值为 y * width + x */
readonly index: number;
/** 节点横坐标 */
readonly x: number;
/** 节点纵坐标 */
readonly y: number;
/** 节点对应的位置信息,用于损失计算 */
readonly block: ILayerLocation;
/** 该节点是否仅可作为路径终点,不可作为中间节点 */
readonly terminal: boolean;
/** 该节点的全部出边 */
readonly edges: readonly IPathGraphEdge[];
}
export interface IPathGraph {
/** 图宽度 */
readonly width: number;
/** 图高度 */
readonly height: number;
/** 图内全部节点,键为节点索引 */
readonly nodes: ReadonlyMap<number, IPathGraphNode>;
}
export interface IPathfindingGraphBuilder {
/**
* id
* @param maps `null`
*/
useMapState(maps: IMapState | null): void;
/**
*
* @param layer `null`
*/
useMapLayer(layer: IMapLayer | null): void;
/**
*
* @param predicate `null`
*/
usePassPredicate(predicate: IPassPredicate | null): void;
/**
* 使
* @param group
*/
useDirGroup(group: number): void;
/**
*
*
* @returns
*/
build(): IPathGraph;
}