refactor: 寻路系统改进

This commit is contained in:
unanmed 2026-09-17 21:21:08 +08:00
parent 50b8b3e651
commit 4aaea68b65
17 changed files with 251 additions and 323 deletions

View File

@ -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%

View File

@ -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"
}

View File

@ -12,6 +12,7 @@ import {
IHeroLocationSave,
IHeroMover
} from './types';
import { IGameMap } from '../map';
export class HeroLocation
extends Hookable<IHeroLocationHooks>
@ -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<this>;
@ -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 {

View File

@ -18,6 +18,7 @@ import {
IHeroMoveTopImpl
} from './types';
import { isNil } from 'lodash-es';
import { IPassCheckHandler } from '../map';
export class HeroMover<T extends IHeroLocation>
extends ObjectMover<T>
@ -196,11 +197,11 @@ export class HeroMover<T extends IHeroLocation>
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<T extends IHeroLocation>
}
}
// 瞬移录像记录
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) {

View File

@ -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<IHeroLocationHooks>,
IDataCommonExtended {
/** 当前所在楼层 id,undefined 表示尚不处于任何楼层 */
/** 勇士所处楼层的楼层 id,若为 `undefined` 则表示不在任何楼层 */
readonly floorId: string | undefined;
/** 勇士所处地图,若为 `null` 则表示不在任何楼层 */
readonly map: IGameMap | null;
/** 勇士的移动对象 */
readonly mover: IHeroMover<this>;
/**
* 设置勇士所在的楼层 id,注意此方法会引起数据变化,但是不会产生楼层切换动画
* @param floorId 目标楼层 id
* 设置勇士所在的楼层,注意此方法会引起数据变化,但是不会产生楼层切换动画
* @param map 目标楼层对象
*/
setFloor(floorId: string | undefined): void;
setFloor(map: IGameMap | null): void;
}
//#endregion
@ -840,7 +842,7 @@ export interface IHeroEquipment<THero>
export interface IHeroChangeFloorInfo {
/** 要切换至的目标楼层 */
readonly target: string;
readonly target: IGameMap;
/** 要切换至的目标位置横坐标 */
readonly x: number;
/** 要切换至的目标位置纵坐标 */

View File

@ -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;

View File

@ -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;
}

View File

@ -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

View File

@ -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 通行性判断

View File

@ -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;
}

View File

@ -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<number>): 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<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) {
@ -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);
}
}

View File

@ -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)!;

View File

@ -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<number> | 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<number> | 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<number> = new Set();
const adjacency: Map<number, IPathGraphEdge[]> = new Map();
const blocks: Map<number, ILayerLocation> = new Map();
const startIndex = start.y * width + start.x;
blocks.set(startIndex, layer.getLocationData(start.x, start.y)!);
const mapped: Set<number> = 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<number, IPathGraphNode> = 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) ?? []

View File

@ -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<PathfindingGraphBuilder['build']> => {
const builder = new PathfindingGraphBuilder();
buildGraph: (): ReturnType<MapGraphBuilder['build']> => {
const builder = new MapGraphBuilder();
builder.useMapState(maps);
builder.useMapLayer(layer);
builder.usePassPredicate(predicate);

View File

@ -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<IObjectMovable> | 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<IObjectMovable> | 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);
}
}

View File

@ -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<number, IPathGraphNode>;
}
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<number> | 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<ITileLocator>;
/** 这一步移动到的位置 */
@ -97,7 +91,7 @@ export interface IPathfindingController {
/** 移动控制器对象 */
readonly controller: Readonly<IMoverController>;
/** 寻路路径 */
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<number>): 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<IObjectMovable> | null): void;
/**
* 设置瞬移回退策略,默认必定回退为逐步寻路
@ -171,7 +177,7 @@ export interface IPathfindingSystem extends IDataBaseExtended {
* 仅获取从当前位置至目标位置的最小损失路径,不产生任何移动
* @param target 目标坐标
*/
getPath(target: ITileLocator): IPathfindingStep[];
getPath(target: ITileLocator): IPathfindingResult;
/**
* 逐步寻路至目标位置,触发途经事件

View File

@ -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."
}
}