From d9ee80f77fc7f08db4b0b29c4070459e04969754 Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Wed, 9 Sep 2026 18:47:40 +0800 Subject: [PATCH] refactor(02-02): apply user review directives on path module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 system.ts 中 'mover' in movable 运行时形状检查与 IMovableWithMover, 依 types.ts 修正后的类型直接绑定 IObjectMover - path 内导出类型(IPathGraph*、IPathfindingGraphBuilder)归位 types.ts, graph.ts/finder.ts 改由 types 导入 - 移除类与接口声明上的 jsdoc、类实现侧与接口重复的 jsdoc、region 标记; 实现注释改为仅描述可观察行为(去除 Dijkstra 等实现叙述) - 私有方法置于调用者之前(resolveFloorId、getNodeCost、search、startMove) - system.test.ts 对齐 useMover(mover) 新签名并同步测试描述 --- packages-user/data-system/src/path/finder.ts | 111 ++++++---------- .../data-system/src/path/graph.test.ts | 1 - packages-user/data-system/src/path/graph.ts | 118 +++-------------- .../data-system/src/path/system.test.ts | 11 +- packages-user/data-system/src/path/system.ts | 124 +++++------------- packages-user/data-system/src/path/types.ts | 64 +++++++++ 6 files changed, 165 insertions(+), 264 deletions(-) diff --git a/packages-user/data-system/src/path/finder.ts b/packages-user/data-system/src/path/finder.ts index c11bc94..5dec29b 100644 --- a/packages-user/data-system/src/path/finder.ts +++ b/packages-user/data-system/src/path/finder.ts @@ -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 diff --git a/packages-user/data-system/src/path/graph.test.ts b/packages-user/data-system/src/path/graph.test.ts index d3305df..42fb57b 100644 --- a/packages-user/data-system/src/path/graph.test.ts +++ b/packages-user/data-system/src/path/graph.test.ts @@ -61,7 +61,6 @@ beforeAll(async () => { }; }); -/** 测试图块定义,键为图块数字 */ interface TestTileDefinition { /** 图块数字 */ num: number; diff --git a/packages-user/data-system/src/path/graph.ts b/packages-user/data-system/src/path/graph.ts index a6b2361..03395d4 100644 --- a/packages-user/data-system/src/path/graph.ts +++ b/packages-user/data-system/src/path/graph.ts @@ -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; -} - -//#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 diff --git a/packages-user/data-system/src/path/system.test.ts b/packages-user/data-system/src/path/system.test.ts index 11d8a68..93c83e1 100644 --- a/packages-user/data-system/src/path/system.test.ts +++ b/packages-user/data-system/src/path/system.test.ts @@ -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(); }); diff --git a/packages-user/data-system/src/path/system.ts b/packages-user/data-system/src/path/system.ts index daac7a7..4162b30 100644 --- a/packages-user/data-system/src/path/system.ts +++ b/packages-user/data-system/src/path/system.ts @@ -11,39 +11,12 @@ import { PathFallbackPolicy } from './types'; -//#region 移动器识别 - -/** - * 携带移动器的移动对象,动态图块等真实移动对象均满足此结构 - */ -export interface IMovableWithMover extends IObjectMovable { - /** 移动器对象 */ - readonly mover: IObjectMover; -} - -/** - * 判断移动对象是否携带移动器 - * @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 | 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 | 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 { - 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 { + const current = this.current; + this.current = null; + if (current && !current.controller.done) { + await current.controller.stop(); + } + } +} diff --git a/packages-user/data-system/src/path/types.ts b/packages-user/data-system/src/path/types.ts index 20c1289..e97d03f 100644 --- a/packages-user/data-system/src/path/types.ts +++ b/packages-user/data-system/src/path/types.ts @@ -121,3 +121,67 @@ export interface IPathfindingSystem extends IDataBaseExtended { */ interrupt(): Promise; } + +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; +} + +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; +}