diff --git a/.planning/STATE.md b/.planning/STATE.md index bd2c4af..855a5b3 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -5,10 +5,10 @@ current_phase: 07 current_phase_name: 数据端缺陷修复 status: executing stopped_at: Phase 07 complete, ready to plan Phase 1 -last_updated: "2026-09-17T12:25:14.190Z" +last_updated: "2026-09-17T12:32:39.331Z" last_activity: 2026-09-17 last_activity_desc: Phase 07 execution started -state_head: 7b187849953a5ab181b5d2451228302f3c0f3cd1 +state_head: 19ad1ea91ab7a69b0b65020d49bbe65ab30ec301 progress: total_phases: 7 completed_phases: 0 @@ -28,9 +28,9 @@ See: .planning/PROJECT.md (updated 2026-09-17) ## Current Position -Phase: 07 (数据端缺陷修复) — READY TO EXECUTE +Phase: 07 (数据端缺陷修复) — EXECUTING Plan: 1 of 15 -Status: Ready to execute +Status: Executing Phase 07 Last activity: 2026-09-17 — Phase 07 execution started Progress: [█████░░░░░] 50% diff --git a/.planning/state.json b/.planning/state.json index c3c63ae..1368771 100644 --- a/.planning/state.json +++ b/.planning/state.json @@ -44,5 +44,5 @@ "label": "Advance to the next step", "reason": "Phase 07 of 7 · executing" }, - "updated_at": "2026-09-17T12:25:14.300Z" + "updated_at": "2026-09-17T12:32:39.429Z" } diff --git a/packages-user/data-base/src/hero/location.ts b/packages-user/data-base/src/hero/location.ts index ba50cfd..7f410f4 100644 --- a/packages-user/data-base/src/hero/location.ts +++ b/packages-user/data-base/src/hero/location.ts @@ -12,6 +12,7 @@ import { IHeroLocationSave, IHeroMover } from './types'; +import { IGameMap } from '../map'; export class HeroLocation extends Hookable @@ -20,6 +21,7 @@ export class HeroLocation x: number; y: number; floorId: string | undefined = undefined; + map: IGameMap | null = null; readonly state: IDataCommon; readonly mover: IHeroMover; @@ -44,9 +46,10 @@ export class HeroLocation return new HookController(this, hook); } - setFloor(floorId: string | undefined): void { - this.floorId = floorId; - this.forEachHook(hook => hook.onSetFloor?.(floorId)); + setFloor(map: IGameMap): void { + this.floorId = map.floorId; + this.map = map; + this.forEachHook(hook => hook.onSetFloor?.(map)); } setPos(x: number, y: number): void { diff --git a/packages-user/data-base/src/hero/mover.ts b/packages-user/data-base/src/hero/mover.ts index 608310d..1ed7873 100644 --- a/packages-user/data-base/src/hero/mover.ts +++ b/packages-user/data-base/src/hero/mover.ts @@ -18,6 +18,7 @@ import { IHeroMoveTopImpl } from './types'; import { isNil } from 'lodash-es'; +import { IPassCheckHandler } from '../map'; export class HeroMover extends ObjectMover @@ -196,11 +197,11 @@ export class HeroMover if (this.ignoreTerrain) { return HeroMoveCode.Step; } else { - const passHandler = { + const passHandler: IPassCheckHandler = { currLoc: handler.currLoc, nextLoc: handler.nextLoc, direction: handler.direction, - floorId: handler.floorId, + map: this.tile.map, state: handler.state }; const predicate = this.topImpl.predicate(); @@ -215,6 +216,12 @@ export class HeroMover } } + // 瞬移录像记录 + if (type === ObjectMoveType.Teleport) { + const replay = this.state.replaySystem; + replay.array.add(ReplayCode.Teleport, [step.x, step.y]); + } + // 跳跃和瞬移 if (type === ObjectMoveType.Jump || type === ObjectMoveType.Teleport) { if (this.ignoreTerrain) { diff --git a/packages-user/data-base/src/hero/types.ts b/packages-user/data-base/src/hero/types.ts index 33dc0c2..4f7cd4c 100644 --- a/packages-user/data-base/src/hero/types.ts +++ b/packages-user/data-base/src/hero/types.ts @@ -10,7 +10,7 @@ import { ISaveableContent, SaveCompression } from '@user/data-common'; -import { IPassPredicate } from '../map'; +import { IGameMap, IPassPredicate } from '../map'; //#region 勇士属性 @@ -280,9 +280,9 @@ export interface IHeroLocationHooks extends IHookBase { /** * 当设置勇士所处楼层时触发 - * @param floorId 设置为的楼层 id,undefined 表示尚不处于任何楼层 + * @param map 设置为的楼层对象,`null` 表示不处于任何楼层 */ - onSetFloor?(floorId: string | undefined): void; + onSetFloor?(map: IGameMap | null): void; } export interface IHeroLocationSave { @@ -302,16 +302,18 @@ export interface IHeroLocation IObjectMovable, IHookable, IDataCommonExtended { - /** 当前所在楼层 id,undefined 表示尚不处于任何楼层 */ + /** 勇士所处楼层的楼层 id,若为 `undefined` 则表示不在任何楼层 */ readonly floorId: string | undefined; + /** 勇士所处地图,若为 `null` 则表示不在任何楼层 */ + readonly map: IGameMap | null; /** 勇士的移动对象 */ readonly mover: IHeroMover; /** - * 设置勇士所在的楼层 id,注意此方法会引起数据变化,但是不会产生楼层切换动画 - * @param floorId 目标楼层 id + * 设置勇士所在的楼层,注意此方法会引起数据变化,但是不会产生楼层切换动画 + * @param map 目标楼层对象 */ - setFloor(floorId: string | undefined): void; + setFloor(map: IGameMap | null): void; } //#endregion @@ -840,7 +842,7 @@ export interface IHeroEquipment export interface IHeroChangeFloorInfo { /** 要切换至的目标楼层 */ - readonly target: string; + readonly target: IGameMap; /** 要切换至的目标位置横坐标 */ readonly x: number; /** 要切换至的目标位置纵坐标 */ diff --git a/packages-user/data-base/src/map/types.ts b/packages-user/data-base/src/map/types.ts index 8e724c1..4dda8c2 100644 --- a/packages-user/data-base/src/map/types.ts +++ b/packages-user/data-base/src/map/types.ts @@ -855,13 +855,13 @@ export interface IPassCheckHandler extends IDataCommonExtended { readonly nextLoc: ITileLocator; /** 移动方向 */ readonly direction: FaceDirection; - /** 当前楼层 id */ - readonly floorId: string | undefined; + /** 地图对象 */ + readonly map: IGameMap | null; } export interface IPassPredicate { /** - * 检查在指定楼层中,能否从某一个移动入某一格 + * 检查在指定楼层中,能否从某一个移动入某一格,若会产生撞击,则此项应返回 `false` * @param handler 通行性检查信息对象 */ canPass(handler: IPassCheckHandler): boolean; diff --git a/packages-user/data-common/src/types.ts b/packages-user/data-common/src/types.ts index e31b975..fa34b01 100644 --- a/packages-user/data-common/src/types.ts +++ b/packages-user/data-common/src/types.ts @@ -1,4 +1,4 @@ -import { IDirectionMapper, ITileLocator } from '@motajs/common'; +import { ITileLocator } from '@motajs/common'; import { IFaceManager, IRoleFaceBinder } from './common'; import { IGameEventStore, IItemStore, IMapStore, ITileStore } from './store'; import { IReplaySystem } from './replay'; @@ -56,8 +56,6 @@ export interface IDataCommon { readonly roleFace: IRoleFaceBinder; /** 朝向管理 */ readonly faceManager: IFaceManager; - /** 方向组映射器 */ - readonly directionMapper: IDirectionMapper; /** 录像系统 */ readonly replaySystem: IReplaySystem; } diff --git a/packages-user/data-state/src/core.ts b/packages-user/data-state/src/core.ts index c4df8bb..2f9b476 100644 --- a/packages-user/data-state/src/core.ts +++ b/packages-user/data-state/src/core.ts @@ -192,15 +192,12 @@ export class CoreState implements ICoreState { this.replaySystem = new ReplaySystem(); // 寻路系统 - const pathfinding = new PathfindingSystem(this); - this.pathfinding = pathfinding; - pathfinding.useMover(this.hero.location.mover); - pathfinding.finder.useMapState(this.maps); + this.pathfinding = new PathfindingSystem(this); + this.pathfinding.useMover(this.hero.location.mover); // 初始状态下勇士不在任何楼层,切换楼层后再具体设置 - pathfinding.finder.useMapLayer(null); - pathfinding.finder.usePassPredicate( - new DefaultPassPredicateImpl(this.maps) - ); + this.pathfinding.finder.useMapLayer(null); + const predicate = new DefaultPassPredicateImpl(this); + this.pathfinding.finder.usePassPredicate(predicate); //#endregion diff --git a/packages-user/data-state/src/hero/moverImpl.ts b/packages-user/data-state/src/hero/moverImpl.ts index bece098..55a18ff 100644 --- a/packages-user/data-state/src/hero/moverImpl.ts +++ b/packages-user/data-state/src/hero/moverImpl.ts @@ -20,7 +20,7 @@ export class DefaultHeroMoveTopImpl implements IHeroMoveTopImpl { constructor(private readonly state: IStateSystem) { this.maps = state.maps; this.executor = state.eventSystem.executor; - this.passPredicate = new DefaultPassPredicateImpl(this.maps); + this.passPredicate = new DefaultPassPredicateImpl(this.state); } //#region 通行性判断 diff --git a/packages-user/data-state/src/hero/predicate.ts b/packages-user/data-state/src/hero/predicate.ts index ee9a953..c2eecc1 100644 --- a/packages-user/data-state/src/hero/predicate.ts +++ b/packages-user/data-state/src/hero/predicate.ts @@ -1,11 +1,11 @@ -import { IMapState, IPassCheckHandler, IPassPredicate } from '@user/data-base'; -import { FaceDirection, PassBit } from '@user/data-common'; -import { isNil } from 'lodash-es'; +import { IPassCheckHandler, IPassPredicate } from '@user/data-base'; +import { FaceDirection, FaceGroup, PassBit } from '@user/data-common'; +import { IStateSystem } from '@user/data-system'; export interface DefaultPassPredicate extends IPassPredicate {} export class DefaultPassPredicateImpl implements DefaultPassPredicate { - constructor(private readonly maps: IMapState) {} + constructor(readonly state: IStateSystem) {} private directionToPassBit(dir: FaceDirection): number { switch (dir) { @@ -22,43 +22,14 @@ export class DefaultPassPredicateImpl implements DefaultPassPredicate { } } - private oppositeDirection(dir: FaceDirection): FaceDirection { - switch (dir) { - case FaceDirection.Up: - return FaceDirection.Down; - case FaceDirection.Right: - return FaceDirection.Left; - case FaceDirection.Down: - return FaceDirection.Up; - case FaceDirection.Left: - return FaceDirection.Right; - case FaceDirection.LeftUp: - return FaceDirection.RightDown; - case FaceDirection.RightUp: - return FaceDirection.LeftDown; - case FaceDirection.LeftDown: - return FaceDirection.RightUp; - case FaceDirection.RightDown: - return FaceDirection.LeftUp; - default: - return FaceDirection.Unknown; - } - } - canPass(handler: IPassCheckHandler): boolean { - const { currLoc, nextLoc, direction, floorId } = handler; - if (isNil(floorId)) return false; + const { currLoc, nextLoc, direction, map } = handler; + const face = this.state.faceManager.get(FaceGroup.Dir4); + if (!face) return false; - if ( - direction === FaceDirection.LeftDown || - direction === FaceDirection.LeftUp || - direction === FaceDirection.RightDown || - direction === FaceDirection.RightUp - ) { - return true; - } + const degraded = face.degrade(direction); + if (degraded === FaceDirection.Unknown) return false; - const map = this.maps.getMap(floorId); if (!map) return false; const event = map.eventLayer; if (!event) return false; @@ -66,9 +37,7 @@ export class DefaultPassPredicateImpl implements DefaultPassPredicate { const { x, y } = currLoc; const { x: nx, y: ny } = nextLoc; const leaveMask = this.directionToPassBit(direction); - const enterMask = this.directionToPassBit( - this.oppositeDirection(direction) - ); + const enterMask = this.directionToPassBit(face.opposite(direction)); let canLeave = true; let canEnter = true; @@ -101,13 +70,11 @@ export class DefaultPassPredicateImpl implements DefaultPassPredicate { } shouldHit(handler: IPassCheckHandler): boolean { - const { nextLoc, floorId } = handler; - if (isNil(floorId)) return false; - const map = this.maps.getMap(floorId); + const { nextLoc, map } = handler; if (!map) return false; - const eventLayer = map.eventLayer; - if (!eventLayer) return false; - const next = eventLayer.getLocationData(nextLoc.x, nextLoc.y); + const event = map.eventLayer; + if (!event) return false; + const next = event.getLocationData(nextLoc.x, nextLoc.y); const nextRaw = next?.static?.raw(); return !!nextRaw && !nextRaw.eventPass; } diff --git a/packages-user/data-system/src/path/finder.ts b/packages-user/data-system/src/path/finder.ts index fa7e3b2..9a02adc 100644 --- a/packages-user/data-system/src/path/finder.ts +++ b/packages-user/data-system/src/path/finder.ts @@ -1,18 +1,17 @@ -import { InternalDirectionGroup, ITileLocator, logger } from '@motajs/common'; -import { - IMapLayer, - IMapState, - IPassPredicate, - IStateBase -} from '@user/data-base'; +import { ITileLocator, logger } from '@motajs/common'; +import { IMapLayer, IPassPredicate, IStateBase } from '@user/data-base'; import { isNil } from 'lodash-es'; -import { PathfindingGraphBuilder } from './graph'; +import { MapGraphBuilder } from './graph'; import { - IPathfinder, + IPathFinder, IPathfindingStep, - IPathGraph, - PathCostFunction + IMapGraph, + PathCostFunction, + IMapGraphBuilder, + IPathfindingResult, + PathfindingStatus } from './types'; +import { IFaceHandler } from '@user/data-common'; interface IDistanceHeapEntry { /** 条目的键值,堆中键值最小的条目最先取出 */ @@ -95,43 +94,37 @@ class DistanceHeap { } } -export class PathfindingFinder implements IPathfinder { +export class PathFinder 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; + /** 当前寻路对象使用的图构建器 */ + private readonly graph: IMapGraphBuilder = new MapGraphBuilder(); constructor(state: IStateBase) { this.state = state; } - useMapState(maps: IMapState | null): void { - this.maps = maps; - } - useMapLayer(layer: IMapLayer | null): void { - this.layer = layer; + if (layer) { + if (layer.state !== this.state) { + logger.warn(182, 'IMapLayer', 'IPathFinder'); + return; + } + } + this.graph.useMapLayer(layer); } useCostFunction(cost: PathCostFunction | null): void { - this.cost = cost; + this.graph.useCostFunction(cost); } usePassPredicate(predicate: IPassPredicate | null): void { - this.predicate = predicate; + this.graph.usePassPredicate(predicate); } - useDirGroup(group: number): void { - this.group = group; + useFaceHandler(handler: IFaceHandler): void { + this.graph.useFaceHandler(handler); } /** @@ -141,22 +134,24 @@ export class PathfindingFinder implements IPathfinder { * @param target 寻路目标位置 */ private search( - graph: IPathGraph, + graph: IMapGraph, start: ITileLocator, target: ITileLocator ): IPathfindingStep[] { - const startIndex = start.y * graph.width + start.x; - const targetIndex = target.y * graph.width + target.x; + const indexer = this.graph.indexer; + const startIndex = indexer.locaterToIndex(start); + const targetIndex = indexer.locaterToIndex(target); if (startIndex === targetIndex) return []; if (!graph.nodes.has(startIndex) || !graph.nodes.has(targetIndex)) { return []; } + const heap = new DistanceHeap(); const dist: Map = new Map(); const prev: Map = new Map(); const visited: Set = new Set(); + dist.set(startIndex, 0); - const heap: DistanceHeap = new DistanceHeap(); heap.push(0, startIndex); while (true) { @@ -196,39 +191,25 @@ export class PathfindingFinder implements IPathfinder { const step = prev.get(curr); if (!step) return []; steps.push(step); - curr = step.from.y * graph.width + step.from.x; + curr = indexer.locaterToIndex(step.from); } 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 []; + find(start: ITileLocator, target: ITileLocator): IPathfindingResult { + if (start.x === target.x && start.y === target.y) { + return { status: PathfindingStatus.TargetUnder, path: [] }; } - if ( - !layer.inMap(start.x, start.y) || - !layer.inMap(target.x, target.y) - ) { - logger.warn(173); - return []; + const graph = this.graph.build(start); + if (!graph) { + return { status: PathfindingStatus.InvalidInput, path: [] }; + } + const path = this.search(graph, start, target); + if (path.length === 0) { + return { status: PathfindingStatus.NoPath, path }; + } else { + return { status: PathfindingStatus.Success, path }; } - - // 数据端状态可变,每次寻路动态构建图,不做缓存 - 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); } } diff --git a/packages-user/data-system/src/path/graph.test.ts b/packages-user/data-system/src/path/graph.test.ts index eff197e..889301c 100644 --- a/packages-user/data-system/src/path/graph.test.ts +++ b/packages-user/data-system/src/path/graph.test.ts @@ -30,7 +30,7 @@ vi.hoisted(() => { }); interface TestModules { - PathfindingGraphBuilder: typeof import('./graph').PathfindingGraphBuilder; + PathfindingGraphBuilder: typeof import('./graph').MapGraphBuilder; MapState: typeof import('@user/data-base').MapState; TileStore: typeof import('@user/data-common').TileStore; FaceManager: typeof import('@user/data-common').FaceManager; @@ -50,7 +50,7 @@ beforeAll(async () => { const commonModule = await import('@user/data-common'); const motaModule = await import('@motajs/common'); modules = { - PathfindingGraphBuilder: graphModule.PathfindingGraphBuilder, + PathfindingGraphBuilder: graphModule.MapGraphBuilder, MapState: baseModule.MapState, TileStore: commonModule.TileStore, FaceManager: commonModule.FaceManager, @@ -343,7 +343,7 @@ describe('pathfinding graph building', () => { new modules.Dir8FaceHandler() ); builder.usePassPredicate(predicate); - builder.useDirGroup(InternalDirectionGroup.Dir8); + builder.useFaceHandler(InternalDirectionGroup.Dir8); const graph = builder.build({ x: 1, y: 1 }); const center = graph.nodes.get(1 * 3 + 1)!; diff --git a/packages-user/data-system/src/path/graph.ts b/packages-user/data-system/src/path/graph.ts index 5f322f3..147d137 100644 --- a/packages-user/data-system/src/path/graph.ts +++ b/packages-user/data-system/src/path/graph.ts @@ -1,61 +1,45 @@ +import { ITileLocator, logger } from '@motajs/common'; import { - IDirectionDescriptor, - InternalDirectionGroup, - ITileLocator, - logger -} from '@motajs/common'; -import { FaceDirection } from '@user/data-common'; + IFaceHandler, + ILocationIndexer, + MapLocIndexer +} from '@user/data-common'; import { ILayerLocation, IMapLayer, - IMapState, IPassCheckHandler, IPassPredicate } from '@user/data-base'; -import { isNil } from 'lodash-es'; import { - IPathGraph, + IMapGraph, IPathGraphEdge, IPathGraphNode, - IPathfindingGraphBuilder, + IMapGraphBuilder, PathCostFunction } 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; +export class MapGraphBuilder implements IMapGraphBuilder { /** 绑定的地图图层,图节点来源 */ private layer: IMapLayer | null = null; /** 注入的损失函数,未注入时每格损失 1 */ private cost: PathCostFunction | null = null; /** 注入的通行性谓词,用于判定边的可行性与终端节点 */ private predicate: IPassPredicate | null = null; - /** 邻域方向组别,默认四正交方向 */ - private group: number = InternalDirectionGroup.Dir4; + /** 邻域方向组别 */ + private face: IFaceHandler | null = null; - useMapState(maps: IMapState | null): void { - this.maps = maps; + /** 坐标索引器 */ + indexer: ILocationIndexer; + + constructor() { + this.indexer = new MapLocIndexer(); } useMapLayer(layer: IMapLayer | null): void { this.layer = layer; + if (layer) { + this.indexer.setWidth(layer.width); + } } useCostFunction(cost: PathCostFunction | null): void { @@ -66,108 +50,97 @@ export class PathfindingGraphBuilder implements IPathfindingGraphBuilder { this.predicate = predicate; } - useDirGroup(group: number): void { - this.group = group; + useFaceHandler(face: IFaceHandler | null): void { + this.face = face; } /** - * 解析绑定图层所属的楼层 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; - } - - /** - * 获取进入指定位置节点的损失,损失值为 NaN 或负数时告警并按损失 1 处理, - * Infinity 为合法损失值 + * 获取进入指定位置节点的损失 * @param block 位置信息 */ private resolveCost(block: ILayerLocation): number { - if (!this.cost) return 1; - const value = this.cost(block); - if (Number.isNaN(value) || value < 0) { - logger.warn(174); + if (this.cost) { + return this.cost(block); + } else { return 1; } - return value; } - build(start: ITileLocator): IPathGraph { + build(start: ITileLocator): IMapGraph | null { const layer = this.layer; - if (isNil(layer) || !layer.inMap(start.x, start.y)) { - logger.warn(173); - return { width: 0, height: 0, nodes: new Map() }; + const face = this.face; + const predicate = this.predicate; + if (!layer) { + logger.warn(173, 'IMapLayer'); + return null; + } + if (!predicate) { + logger.warn(173, 'IPassPredicate'); + return null; + } + if (!face) { + logger.warn(173, 'IFaceHandler'); + return null; + } + if (!layer.inMap(start.x, start.y)) { + logger.warn(183); + return null; } - const width = layer.width; - const height = layer.height; - const floorId = this.resolveFloorId(); - const state = layer.state; - const dirs: IDirectionDescriptor[] = [ - ...layer.state.directionMapper.map(this.group) - ]; + const indexer = this.indexer; + const { width, height, state } = layer; const terminals: Set = new Set(); const adjacency: Map = new Map(); - const blocks: Map = new Map(); - const startIndex = start.y * width + start.x; - blocks.set(startIndex, layer.getLocationData(start.x, start.y)!); + const mapped: Set = new Set(); + const startIndex = indexer.locaterToIndex(start); adjacency.set(startIndex, []); // 以起始位置为中心 BFS,仅沿可通行有向边扩展,不可达区域不入图 - const queue: number[] = [startIndex]; + const queue: ITileLocator[] = [start]; let head = 0; while (head < queue.length) { - const index = queue[head++]!; - const block = blocks.get(index)!; - const x = index % width; - const y = Math.floor(index / width); + const { x, y } = queue[head++]!; + const index = indexer.locToIndex(x, y); const edges: IPathGraphEdge[] = []; - for (const desc of dirs) { - const dir = directionOf(desc.x, desc.y); - if (dir === FaceDirection.Unknown) continue; + for (const [dir, desc] of face.mapMovement()) { + // 如果连接原地,那么应该忽略,避免陷入死循环 + if (desc.x === 0 && desc.y === 0) continue; const nx = x + desc.x; const ny = y + desc.y; if (!layer.inMap(nx, ny)) continue; - const next = layer.getLocationData(nx, ny)!; + + const nextIndex = indexer.locToIndex(nx, ny); + const next: ITileLocator = { x: nx, y: ny }; const handler: IPassCheckHandler = { - currLoc: block.locator, - nextLoc: next.locator, + currLoc: { x, y }, + nextLoc: next, direction: dir, - floorId, + map: layer.map, state }; - if (isNil(this.predicate) || !this.predicate.canPass(handler)) { - continue; - } - const nextIndex = ny * width + nx; - if (this.predicate.shouldHit(handler)) { + + if (!predicate.canPass(handler)) continue; + if (predicate.shouldHit(handler)) { terminals.add(nextIndex); } + edges.push({ dir, to: nextIndex }); - if (!blocks.has(nextIndex)) { - blocks.set(nextIndex, next); - adjacency.set(nextIndex, []); - queue.push(nextIndex); + if (!mapped.has(nextIndex)) { + mapped.add(nextIndex); + queue.push(next); } } adjacency.set(index, edges); } const nodes: Map = new Map(); - for (const [index, block] of blocks) { + for (const index of mapped) { + const { x, y } = indexer.indexToLocator(index); + const block = layer.getLocationData(x, y)!; nodes.set(index, { - index, - x: index % width, - y: Math.floor(index / width), - block, + x, + y, cost: this.resolveCost(block), terminal: terminals.has(index), edges: adjacency.get(index) ?? [] diff --git a/packages-user/data-system/src/path/performance.test.ts b/packages-user/data-system/src/path/performance.test.ts index bb9aebd..104e757 100644 --- a/packages-user/data-system/src/path/performance.test.ts +++ b/packages-user/data-system/src/path/performance.test.ts @@ -14,7 +14,7 @@ import { type IPassCheckHandler, type IPassPredicate } from '@user/data-base'; -import { PathfindingGraphBuilder } from './graph'; +import { MapGraphBuilder } from './graph'; import { type PathfindingSystem } from './system'; vi.hoisted(() => { @@ -363,8 +363,8 @@ function createPerformanceSystem( system.finder.usePassPredicate(predicate); return { system, - buildGraph: (): ReturnType => { - const builder = new PathfindingGraphBuilder(); + buildGraph: (): ReturnType => { + const builder = new MapGraphBuilder(); builder.useMapState(maps); builder.useMapLayer(layer); builder.usePassPredicate(predicate); diff --git a/packages-user/data-system/src/path/system.ts b/packages-user/data-system/src/path/system.ts index 922138c..4e0f286 100644 --- a/packages-user/data-system/src/path/system.ts +++ b/packages-user/data-system/src/path/system.ts @@ -1,55 +1,49 @@ import { ITileLocator, logger } from '@motajs/common'; -import { IObjectMovable, IObjectMover, ReplayCode } from '@user/data-common'; -import { HeroMover, IStateBase } from '@user/data-base'; -import { isNil } from 'lodash-es'; -import { PathfindingFinder } from './finder'; +import { IObjectMovable, IObjectMover } from '@user/data-common'; +import { IStateBase } from '@user/data-base'; +import { PathFinder } from './finder'; import { - IPathfinder, + IPathFinder, IPathfindingController, - IPathfindingStep, + IPathfindingResult, IPathfindingSystem, - PathFallbackPolicy + PathFallbackPolicy, + PathfindingStatus } from './types'; export class PathfindingSystem implements IPathfindingSystem { /** 寻路求解器 */ - readonly finder: IPathfinder; + readonly finder: IPathFinder; - /** 绑定的移动器对象 */ + /** 当前绑定的移动器 */ private mover: IObjectMover | null = null; - /** 绑定的移动对象,用于保留用户契约的对象绑定入口 */ - private movable: IObjectMovable | null = null; /** 注入的瞬移回退策略,未注入时必定回退为逐步寻路 */ private policy: PathFallbackPolicy | null = null; /** 最近一次寻路移动的控制器包装 */ private current: IPathfindingController | null = null; constructor(readonly state: IStateBase) { - this.finder = new PathfindingFinder(state); - } - - useMovable(movable: IObjectMovable | null): void { - this.movable = movable; + this.finder = new PathFinder(state); } useMover(mover: IObjectMover | null): void { this.mover = mover; - this.movable = mover ? mover.tile : null; } useFallbackPolicy(policy: PathFallbackPolicy | null): void { this.policy = policy; } - getPath(target: ITileLocator): IPathfindingStep[] { + getPath(target: ITileLocator): IPathfindingResult { const mover = this.mover; - const movable = this.movable; - if (isNil(mover) && isNil(movable)) { + if (!mover) { logger.warn(173); - return []; + return { + status: PathfindingStatus.InvalidInput, + path: [] + }; } - const tile = mover ? mover.tile : movable; - if (!tile) return []; + const tile = mover.tile; return this.finder.find({ x: tile.x, y: tile.y }, target); } @@ -59,7 +53,7 @@ export class PathfindingSystem implements IPathfindingSystem { * @param teleport 是否瞬移 */ private startMove( - path: readonly IPathfindingStep[], + path: IPathfindingResult, teleport: boolean ): IPathfindingController | null { const mover = this.mover; @@ -67,11 +61,13 @@ export class PathfindingSystem implements IPathfindingSystem { const current = this.current; if (current && !current.controller.done) return null; + const array = path.path; + if (teleport) { - const last = path.at(-1)!; + const last = array.at(-1)!; mover.tp(last.to.x, last.to.y); } else { - for (const step of path) { + for (const step of array) { mover.step(step.dir); } } @@ -85,22 +81,16 @@ export class PathfindingSystem implements IPathfindingSystem { moveTo(target: ITileLocator): IPathfindingController | null { const path = this.getPath(target); - if (path.length === 0) return null; + if (path.status !== PathfindingStatus.Success) return null; return this.startMove(path, false); } teleportTo(target: ITileLocator): IPathfindingController | null { const path = this.getPath(target); - if (path.length === 0) return null; - if (!this.policy || this.policy(path)) { + if (path.status !== PathfindingStatus.Success) return null; + if (!this.policy || this.policy(path.path)) { return this.startMove(path, false); } else { - // 录像记录 - // TODO:后续需要调整设计方式,最好不用 instanceof - if (this.mover instanceof HeroMover) { - const replay = this.state.replaySystem; - replay.array.add(ReplayCode.Teleport, [target.x, target.y]); - } return this.startMove(path, true); } } diff --git a/packages-user/data-system/src/path/types.ts b/packages-user/data-system/src/path/types.ts index a9faeca..0bbe769 100644 --- a/packages-user/data-system/src/path/types.ts +++ b/packages-user/data-system/src/path/types.ts @@ -3,31 +3,28 @@ import { IDataBaseExtended, ILayerLocation, IMapLayer, - IMapState, IPassPredicate } from '@user/data-base'; import { - FaceDirection, + IFaceHandler, + ILocationHelper, IMoverController, - IObjectMovable + IObjectMovable, + IObjectMover } from '@user/data-common'; export interface IPathGraphEdge { /** 本条边对应的移动方向 */ - readonly dir: FaceDirection; + readonly dir: number; /** 边指向的节点索引,值为 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 cost: number; /** 该节点是否仅可作为路径终点,不可作为中间节点 */ @@ -36,7 +33,7 @@ export interface IPathGraphNode { readonly edges: readonly IPathGraphEdge[]; } -export interface IPathGraph { +export interface IMapGraph { /** 图宽度 */ readonly width: number; /** 图高度 */ @@ -45,12 +42,9 @@ export interface IPathGraph { readonly nodes: ReadonlyMap; } -export interface IPathfindingGraphBuilder { - /** - * 绑定地图状态对象,用于解析图层所属楼层 id - * @param maps 地图状态对象 - */ - useMapState(maps: IMapState | null): void; +export interface IMapGraphBuilder { + /** 地图坐标索引器 */ + readonly indexer: ILocationHelper; /** * 绑定构建有向图所用的地图图层 @@ -71,22 +65,22 @@ export interface IPathfindingGraphBuilder { usePassPredicate(predicate: IPassPredicate | null): void; /** - * 设置邻域使用的方向组 - * @param group 朝向组 + * 设置邻域使用的方向控制对象 + * @param face 方向控制对象 */ - useDirGroup(group: number): void; + useFaceHandler(face: IFaceHandler | null): void; /** * 以起始位置为中心构建有向图:沿可通行有向边 BFS 扩展, * 仅包含从起始位置可达的节点。图层未绑定或起始位置越界时告警并返回空图 * @param start BFS 起始位置 */ - build(start: ITileLocator): IPathGraph; + build(start: ITileLocator): IMapGraph | null; } export interface IPathfindingStep { /** 移动方向 */ - readonly dir: FaceDirection; + readonly dir: number; /** 这一步的出发位置 */ readonly from: Readonly; /** 这一步移动到的位置 */ @@ -97,7 +91,7 @@ export interface IPathfindingController { /** 移动控制器对象 */ readonly controller: Readonly; /** 寻路路径 */ - readonly path: readonly IPathfindingStep[]; + readonly path: IPathfindingResult; } /** @@ -106,19 +100,25 @@ export interface IPathfindingController { */ export type PathCostFunction = (block: ILayerLocation) => number; -/** - * 瞬移回退策略函数,若返回 `true`,表示当前操作不允许瞬移,必须逐步寻路 - * @param path 完整路径列表 - */ -export type PathFallbackPolicy = (path: readonly IPathfindingStep[]) => boolean; +export const enum PathfindingStatus { + /** 成功寻找到路径 */ + Success, + /** 寻路输入不合法 */ + InvalidInput, + /** 无法找到合理路径到达目标点 */ + NoPath, + /** 目标点就在寻路起始位置脚下 */ + TargetUnder +} -export interface IPathfinder extends IDataBaseExtended { - /** - * 绑定寻路所用的地图状态对象,用于获取楼层与事件层信息,未绑定时寻路告警并返回空路径 - * @param maps 地图状态对象 - */ - useMapState(maps: IMapState | null): void; +export interface IPathfindingResult { + /** 当前的寻路结果状态 */ + readonly status: PathfindingStatus; + /** 寻路结果,只有寻路状态为 `PathfindingStatus.Success` 时此数组才不为空 */ + readonly path: readonly IPathfindingStep[]; +} +export interface IPathFinder extends IDataBaseExtended { /** * 绑定构建有向图所用的地图图层,通常绑定事件层 * @param layer 地图图层对象 @@ -138,28 +138,34 @@ export interface IPathfinder extends IDataBaseExtended { usePassPredicate(predicate: IPassPredicate | null): void; /** - * 设置寻路使用的朝向组 - * @param group 朝向组 + * 设置邻域使用的方向控制对象 + * @param face 方向控制对象 */ - useDirGroup(group: number): void; + useFaceHandler(face: IFaceHandler): void; /** - * 在当前状态下执行寻路操作 + * 在当前状态下执行寻路操作,若返回空数组,表示寻路目标不可达,或输入参数错误,或寻路目标为当前勇士脚下 * @param start 寻路起始位置 * @param target 寻路目标位置 */ - find(start: ITileLocator, target: ITileLocator): IPathfindingStep[]; + find(start: ITileLocator, target: ITileLocator): IPathfindingResult; } +/** + * 瞬移回退策略函数,若返回 `true`,表示当前操作不允许瞬移,必须逐步寻路 + * @param path 完整路径列表 + */ +export type PathFallbackPolicy = (path: readonly IPathfindingStep[]) => boolean; + export interface IPathfindingSystem extends IDataBaseExtended { /** 寻路求解器 */ - readonly finder: IPathfinder; + readonly finder: IPathFinder; /** - * 绑定寻路移动对象,可绑定勇士位置或任意 `IObjectMovable`,如动态图块 - * @param movable 移动对象 + * 绑定寻路移动器,可绑定勇士或任意 `IObjectMover`,如动态图块移动器 + * @param mover 移动对象的移动器 */ - useMovable(movable: IObjectMovable | null): void; + useMover(mover: IObjectMover | null): void; /** * 设置瞬移回退策略,默认必定回退为逐步寻路 @@ -171,7 +177,7 @@ export interface IPathfindingSystem extends IDataBaseExtended { * 仅获取从当前位置至目标位置的最小损失路径,不产生任何移动 * @param target 目标坐标 */ - getPath(target: ITileLocator): IPathfindingStep[]; + getPath(target: ITileLocator): IPathfindingResult; /** * 逐步寻路至目标位置,触发途经事件 diff --git a/packages/common/src/logger.json b/packages/common/src/logger.json index 6e964c5..41f2c94 100644 --- a/packages/common/src/logger.json +++ b/packages/common/src/logger.json @@ -247,13 +247,17 @@ "170": "Game event id '$1' has already been used, old event will be overridden.", "171": "Event id '$1' not found in event store, event will be skipped.", "172": "Event returned non-boolean value '$1' during reduction. JavaScript short-circuit semantics will be used.", - "173": "Pathfinding input is invalid or a required binding (map state, map layer) is missing. An empty result will be returned.", + "173": "A '$1' binding is missing for map graph building. An empty result will be returned.", "174": "Pathfinding cost function returned a negative or NaN value. Default cost 1 will be used instead.", "175": "Replay error: Replay not executed execution failed. Command code: $1.", "176": "Cannot add move step to replay system since only 'up,right,down,left' directions are acceptable.", "177": "Save data for saveable content $1 is needed, but there's no data in save data. Maybe a version issue for it? System will try to load other contents to make game run normally.", "178": "Save data with keys of '$1' are saved but not be loaded, is there some issue for it?", "179": "Replay $1 received an out-of-range index $2, the current replay length is $3. The operation will be ignored.", + "180": "Specified face group does not exist, face group setting failed.", + "181": "", + "182": "Cannot bind '$1' instance to '$2' since they belong to different CoreState instance.", + "183": "An in-map location is expected to begin map graph building, but got outside.", "1001": "Event(setBlock): Unknown tile '$1' when setting block." } }