feat(02-02): add minimal-loss pathfinding system with getPath, controller contract and fallback policy

This commit is contained in:
unanmed 2026-09-09 17:29:22 +08:00
parent 0e6536f426
commit 53d019f62e
5 changed files with 933 additions and 1 deletions

View File

@ -1,4 +1,5 @@
export * from './combat';
export * from './event';
export * from './path';
export * from './types';

View File

@ -0,0 +1,3 @@
export * from './graph';
export * from './system';
export * from './types';

View File

@ -0,0 +1,575 @@
// 测试寻路系统:最小损失搜索、自定义损失、仅取路径、控制器契约与回退策略
import { beforeAll, describe, expect, it, vi } from 'vitest';
import { type ITileLocator } from '@motajs/common';
import { FaceDirection } from '@user/data-common';
import {
type IDataCommon,
type IFaceHandler,
type IObjectMovable,
type IObjectMover,
type ITileStore,
ObjectMoveStep,
ObjectMoveType
} from '@user/data-common';
import {
type IGameMap,
type IMapLayer,
type IPassCheckHandler,
type IPassPredicate
} from '@user/data-base';
import { type PathfindingSystem } from './system';
import { type IPathfindingStep, type PathCostFunction } from './types';
vi.hoisted(() => {
vi.stubGlobal('main', { replayChecking: true });
vi.stubGlobal('location', { origin: 'http://localhost' });
Map.prototype.getOrInsertComputed ??= function <K, V>(
this: Map<K, V>,
key: K,
callback: (key: K) => V
): V {
const existing = this.get(key);
if (existing !== undefined) return existing;
const value = callback(key);
this.set(key, value);
return value;
};
});
interface TestModules {
PathfindingSystem: typeof import('./system').PathfindingSystem;
MapState: typeof import('@user/data-base').MapState;
TileStore: typeof import('@user/data-common').TileStore;
ObjectMover: typeof import('@user/data-common').ObjectMover;
FaceManager: typeof import('@user/data-common').FaceManager;
Dir8FaceHandler: typeof import('@user/data-common').Dir8FaceHandler;
RoleFaceBinder: typeof import('@user/data-common').RoleFaceBinder;
logger: typeof import('@motajs/common').logger;
}
let modules: TestModules;
beforeAll(async () => {
vi.stubGlobal('main', { replayChecking: true });
vi.stubGlobal('location', { origin: 'http://localhost' });
const systemModule = await import('./system');
const baseModule = await import('@user/data-base');
const commonModule = await import('@user/data-common');
const motaModule = await import('@motajs/common');
modules = {
PathfindingSystem: systemModule.PathfindingSystem,
MapState: baseModule.MapState,
TileStore: commonModule.TileStore,
ObjectMover: commonModule.ObjectMover,
FaceManager: commonModule.FaceManager,
Dir8FaceHandler: commonModule.Dir8FaceHandler,
RoleFaceBinder: commonModule.RoleFaceBinder,
logger: motaModule.logger
};
});
/** 测试图块定义,键为图块数字 */
interface TestTileDefinition {
/** 图块数字 */
num: number;
/** 图块字符串 id */
id: string;
/** 可以离开的方向 */
outPass: number;
/** 可以进入的方向 */
inPass: number;
/** 事件可通行性,`false` 表示撞击触发 */
eventPass: boolean;
}
/** 开阔图块:四向可进可出 */
const OPEN_TILE: TestTileDefinition = {
num: 1,
id: 'open',
outPass: 15,
inPass: 15,
eventPass: true
};
/** 墙体图块:不可进入也不可离开 */
const WALL_TILE: TestTileDefinition = {
num: 6,
id: 'wall',
outPass: 0,
inPass: 0,
eventPass: true
};
/** 撞击图块:四向可进可出但事件不通行,构成终端节点 */
const HIT_TILE: TestTileDefinition = {
num: 5,
id: 'hit',
outPass: 15,
inPass: 15,
eventPass: false
};
const ALL_TILES: TestTileDefinition[] = [OPEN_TILE, WALL_TILE, HIT_TILE];
/** 测试用移动对象接口,记录 setPos 调用并携带移动器 */
interface TestTile extends IObjectMovable {
/** 当前横坐标 */
x: number;
/** 当前纵坐标 */
y: number;
/** 绑定的移动器 */
mover: IObjectMover<TestTile>;
/** setPos 调用记录 */
readonly setPosCalls: { x: number; y: number }[];
}
/**
* DefaultHeroMoveTopImpl
*
*/
class FixturePredicate implements IPassPredicate {
/** 绑定的楼层地图对象 */
private readonly map: IGameMap;
/** 朝向管理对象,用于求相反方向 */
private readonly face: IFaceHandler<FaceDirection>;
constructor(map: IGameMap, face: IFaceHandler<FaceDirection>) {
this.map = map;
this.face = face;
}
/**
*
* @param dir
*/
private passBit(dir: FaceDirection): number {
switch (dir) {
case FaceDirection.Up:
return 0b0001;
case FaceDirection.Right:
return 0b0010;
case FaceDirection.Down:
return 0b0100;
case FaceDirection.Left:
return 0b1000;
default:
return 0;
}
}
canPass(handler: IPassCheckHandler): boolean {
const event = this.map.eventLayer;
if (!event) return false;
const { currLoc, nextLoc, direction } = handler;
// 四角朝向直接判定为可通行,与 moverImpl 语义一致
if (
direction === FaceDirection.LeftDown ||
direction === FaceDirection.LeftUp ||
direction === FaceDirection.RightDown ||
direction === FaceDirection.RightUp
) {
return true;
}
const opposite = this.face.opposite(direction);
const leaveMask = this.passBit(direction);
const enterMask = this.passBit(opposite);
let canLeave = true;
let canEnter = true;
const curr = event.getLocationData(currLoc.x, currLoc.y);
const next = event.getLocationData(nextLoc.x, nextLoc.y);
const currRaw = curr?.static.raw();
const nextRaw = next?.static.raw();
if (currRaw) {
canLeave = !!(leaveMask & currRaw.pass.outPass);
}
if (nextRaw) {
canEnter = !!(enterMask & nextRaw.pass.inPass);
}
return canLeave && canEnter;
}
shouldHit(handler: IPassCheckHandler): boolean {
const event = this.map.eventLayer;
if (!event) return false;
const { nextLoc } = handler;
const next = event.getLocationData(nextLoc.x, nextLoc.y);
const nextRaw = next?.static.raw();
if (!nextRaw) return false;
return !nextRaw.eventPass;
}
}
/** 寻路系统测试夹具 */
interface SystemFixture {
/** 楼层地图对象 */
map: IGameMap;
/** 事件层图层对象 */
layer: IMapLayer;
/** 被测寻路系统 */
system: PathfindingSystem;
/** 绑定的测试移动对象 */
tile: TestTile;
}
/**
*
*/
function createTestTile(): TestTile {
class TestMover extends modules.ObjectMover<TestTile> {
readonly tile: TestTile;
constructor(tile: TestTile) {
super(new modules.Dir8FaceHandler(), FaceDirection.Down);
this.tile = tile;
}
protected override async onMoveStart(): Promise<void> {}
protected override async onMoveEnd(): Promise<void> {}
protected override async onStepStart(): Promise<number> {
// 移动代码对基类不透明,固定传 0 即可
return 0;
}
protected override async onStepEnd(
_code: number,
step: Readonly<ObjectMoveStep>,
tile: TestTile
): Promise<ITileLocator> {
if (step.type === ObjectMoveType.Teleport) {
return { x: step.x, y: step.y };
}
if (step.type === ObjectMoveType.Dir) {
switch (step.move) {
case FaceDirection.Right:
return { x: tile.x + 1, y: tile.y };
case FaceDirection.Left:
return { x: tile.x - 1, y: tile.y };
case FaceDirection.Up:
return { x: tile.x, y: tile.y - 1 };
case FaceDirection.Down:
return { x: tile.x, y: tile.y + 1 };
default:
return { x: tile.x, y: tile.y };
}
}
return { x: tile.x, y: tile.y };
}
protected override async onStepSettled(): Promise<void> {}
}
class TestTileImpl implements TestTile {
x: number = 0;
y: number = 0;
face: FaceDirection = FaceDirection.Down;
mover: IObjectMover<TestTile>;
readonly setPosCalls: { x: number; y: number }[] = [];
constructor() {
this.mover = new TestMover(this);
}
setPos(x: number, y: number): void {
this.x = x;
this.y = y;
this.setPosCalls.push({ x, y });
}
getCurrentFaceDirection(): FaceDirection {
return this.face;
}
}
return new TestTileImpl();
}
/**
*
*
* @param rows
* @param width
*/
function createSystem(rows: number[], width: number): SystemFixture {
const tileStore: ITileStore = new modules.TileStore() as never;
for (const tile of ALL_TILES) {
tileStore.addTile({
num: tile.num,
id: tile.id,
events: {},
type: 0,
pass: {
onlyEvents: false,
outPass: tile.outPass,
inPass: tile.inPass
},
eventPass: tile.eventPass
});
}
const faceManager = new modules.FaceManager();
faceManager.register(1, new modules.Dir8FaceHandler());
const commonState: IDataCommon = {
tileStore,
itemStore: {},
mapStore: {},
eventStore: {},
roleFace: new modules.RoleFaceBinder(),
faceManager,
saveSystem: {}
} as never;
const maps = new modules.MapState(tileStore, commonState);
const map = maps.fromRaw({
floorId: 'F1',
width,
map: { 0: rows },
layerAlias: { 0: 'event' },
events: { 0: {} }
})!;
const layer = map.getLayerByAlias('event')!;
const system = new modules.PathfindingSystem(commonState as never);
const tile = createTestTile();
system.useMovable(tile);
system.finder.useMapState(maps);
system.finder.useMapLayer(layer);
return { map, layer, system, tile };
}
/**
*
* @param fixture
*/
function injectPredicate(fixture: SystemFixture): void {
const predicate = new FixturePredicate(
fixture.map,
new modules.Dir8FaceHandler()
);
fixture.system.finder.usePassPredicate(predicate);
}
describe('pathfinding system', () => {
// 验证默认每格损失 1 时返回格数最少的步骤序列,每步含方向与起终点
it('returns the shortest step sequence with default unit cost', () => {
const fixture = createSystem([1, 1, 1, 1, 1, 1, 1, 1, 1], 3);
injectPredicate(fixture);
const steps = fixture.system.finder.find(
{ x: 0, y: 0 },
{ x: 2, y: 0 }
);
expect(steps).toEqual([
{
dir: FaceDirection.Right,
from: { x: 0, y: 0 },
to: { x: 1, y: 0 }
},
{
dir: FaceDirection.Right,
from: { x: 1, y: 0 },
to: { x: 2, y: 0 }
}
]);
});
// 验证注入自定义损失后选择损失更小的岔路而非步数最少的直路
it('reroutes through the cheaper fork with a custom cost function', () => {
const fixture = createSystem([1, 1, 1, 1, 1, 1, 1, 1, 1], 3);
injectPredicate(fixture);
const cost: PathCostFunction = block =>
block.locator.x === 1 && block.locator.y === 1 ? 10 : 1;
fixture.system.finder.useCostFunction(cost);
const steps = fixture.system.finder.find(
{ x: 0, y: 1 },
{ x: 2, y: 1 }
);
expect(steps).toHaveLength(4);
for (const step of steps) {
expect(step.to).not.toEqual({ x: 1, y: 1 });
}
});
// 验证损失函数返回 NaN 时告警新码 174 并按默认损失 1 处理
it('warns the cost guard code and falls back to unit cost on NaN', () => {
const fixture = createSystem([1, 1, 1, 1, 1, 1, 1, 1, 1], 3);
injectPredicate(fixture);
const cost: PathCostFunction = block =>
block.locator.x === 1 && block.locator.y === 1 ? Number.NaN : 1;
fixture.system.finder.useCostFunction(cost);
const result = modules.logger.catch(() =>
fixture.system.finder.find({ x: 0, y: 1 }, { x: 2, y: 1 })
);
expect(result.info.map(info => info.code)).toContain(174);
expect(result.ret).toHaveLength(2);
});
// 验证不可达目标返回空数组且不移动对象
it('returns an empty path and never moves for unreachable targets', () => {
const fixture = createSystem([1, 6, 1, 1, 6, 1, 1, 6, 1], 3);
injectPredicate(fixture);
fixture.tile.x = 0;
fixture.tile.y = 0;
const steps = fixture.system.finder.find(
{ x: 0, y: 0 },
{ x: 2, y: 1 }
);
const path = fixture.system.getPath({ x: 2, y: 1 });
expect(steps).toEqual([]);
expect(path).toEqual([]);
expect(fixture.tile.x).toBe(0);
expect(fixture.tile.y).toBe(0);
expect(fixture.tile.setPosCalls).toEqual([]);
});
// 验终端节点不可作为中间节点穿越但可作为路径终点
it('never passes through terminal nodes but may end on them', () => {
const fixture = createSystem([1, 5, 1, 1, 1, 1, 1, 1, 1], 3);
injectPredicate(fixture);
const detour = fixture.system.finder.find(
{ x: 0, y: 0 },
{ x: 2, y: 0 }
);
expect(detour).toHaveLength(4);
for (const step of detour) {
expect(step.to).not.toEqual({ x: 1, y: 0 });
}
const direct = fixture.system.finder.find(
{ x: 0, y: 0 },
{ x: 1, y: 0 }
);
expect(direct).toEqual([
{
dir: FaceDirection.Right,
from: { x: 0, y: 0 },
to: { x: 1, y: 0 }
}
]);
});
// 验证 moveTo 有路径时返回含控制器与路径的包装并真实到达目标
it('moves step by step to the target and wraps the controller', async () => {
const fixture = createSystem([1, 1, 1, 1, 1, 1, 1, 1, 1], 3);
injectPredicate(fixture);
const result = fixture.system.moveTo({ x: 2, y: 0 });
expect(result).not.toBeNull();
expect(result!.path).toHaveLength(2);
await result!.controller.onEnd;
expect(fixture.tile.x).toBe(2);
expect(fixture.tile.y).toBe(0);
});
// 验证 moveTo 未绑定移动对象或无路径时返回 null 而非异常
it('returns null from moveTo when movable is unbound or unreachable', () => {
const fixture = createSystem([1, 6, 1, 1, 6, 1, 1, 6, 1], 3);
injectPredicate(fixture);
fixture.system.useMovable(null);
expect(fixture.system.moveTo({ x: 2, y: 1 })).toBeNull();
fixture.system.useMovable(fixture.tile);
expect(fixture.system.moveTo({ x: 2, y: 1 })).toBeNull();
});
// 验证已有移动进行中时再次寻路返回 null
it('returns null on a new move while another move is in progress', () => {
const fixture = createSystem([1, 1, 1, 1, 1, 1, 1, 1, 1], 3);
injectPredicate(fixture);
const first = fixture.system.moveTo({ x: 2, y: 0 });
const second = fixture.system.moveTo({ x: 2, y: 0 });
expect(first).not.toBeNull();
expect(second).toBeNull();
return first!.controller.onEnd;
});
// 验证未注入回退策略时瞬移请求默认回退为逐步寻路
it('falls back to step-by-step movement when policy is null', async () => {
const fixture = createSystem([1, 1, 1], 3);
injectPredicate(fixture);
const result = fixture.system.teleportTo({ x: 2, y: 0 });
expect(result).not.toBeNull();
await result!.controller.onEnd;
expect(fixture.tile.setPosCalls).toEqual([
{ x: 1, y: 0 },
{ x: 2, y: 0 }
]);
expect(fixture.tile.x).toBe(2);
});
// 验证回退策略以路径步骤序列为入参被调用并生效于移动方式决策
it('consults the fallback policy with the path steps and honors it', async () => {
const fixture = createSystem([1, 1, 1], 3);
injectPredicate(fixture);
let received: readonly IPathfindingStep[] | null = null;
fixture.system.useFallbackPolicy(path => {
received = path;
return true;
});
const stepped = fixture.system.teleportTo({ x: 2, y: 0 });
expect(received).not.toBeNull();
expect(received).toHaveLength(2);
await stepped!.controller.onEnd;
expect(fixture.tile.setPosCalls).toEqual([
{ x: 1, y: 0 },
{ x: 2, y: 0 }
]);
const teleportFixture = createSystem([1, 1, 1], 3);
injectPredicate(teleportFixture);
teleportFixture.system.useFallbackPolicy(() => false);
const teleported = teleportFixture.system.teleportTo({ x: 2, y: 0 });
expect(teleported).not.toBeNull();
await teleported!.controller.onEnd;
expect(teleportFixture.tile.setPosCalls).toEqual([{ x: 2, y: 0 }]);
expect(teleportFixture.tile.x).toBe(2);
});
// 验证未绑定地图状态或移动对象时告警新码 173 并返回空结果
it('warns the guard code and returns empty results when unbound', () => {
const system = new modules.PathfindingSystem({} as never);
const findResult = modules.logger.catch(() =>
system.finder.find({ x: 0, y: 0 }, { x: 1, y: 0 })
);
const pathResult = modules.logger.catch(() =>
system.getPath({ x: 1, y: 0 })
);
expect(findResult.ret).toEqual([]);
expect(findResult.info.map(info => info.code)).toContain(173);
expect(pathResult.ret).toEqual([]);
expect(pathResult.info.map(info => info.code)).toContain(173);
});
// 验证打断入口可安全调用并停止进行中的移动
it('interrupts the ongoing pathfinding move safely', async () => {
const fixture = createSystem([1, 1, 1], 3);
injectPredicate(fixture);
const result = fixture.system.moveTo({ x: 2, y: 0 });
expect(result).not.toBeNull();
await fixture.system.interrupt();
await result!.controller.onEnd;
expect(result!.controller.done).toBe(true);
});
});

View File

@ -0,0 +1,352 @@
import { InternalDirectionGroup, ITileLocator, logger } from '@motajs/common';
import { IObjectMovable, IObjectMover } from '@user/data-common';
import {
ILayerLocation,
IMapLayer,
IMapState,
IPassPredicate,
IStateBase
} from '@user/data-base';
import { isNil } from 'lodash-es';
import { IPathGraph, PathfindingGraphBuilder } from './graph';
import {
IPathfinder,
IPathfindingController,
IPathfindingStep,
IPathfindingSystem,
PathCostFunction,
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 寻路求解器
/**
*
* 1
*
*/
export class PathfindingFinder 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;
constructor(state: IStateBase) {
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
*/
find(start: ITileLocator, target: ITileLocator): IPathfindingStep[] {
const maps = this.maps;
const layer = this.layer;
if (isNil(maps) || isNil(layer)) {
logger.warn(173);
return [];
}
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);
}
/**
* Dijkstra
*
* @param graph
* @param start
* @param target
*/
private search(
graph: IPathGraph,
start: ITileLocator,
target: ITileLocator
): IPathfindingStep[] {
const startIndex = start.y * graph.width + start.x;
const targetIndex = target.y * graph.width + target.x;
if (startIndex === targetIndex) return [];
if (!graph.nodes.has(startIndex) || !graph.nodes.has(targetIndex)) {
return [];
}
const dist: Map<number, number> = new Map();
const prev: Map<number, IPathfindingStep> = new Map();
const visited: Set<number> = new Set();
dist.set(startIndex, 0);
while (true) {
let currIndex = -1;
let currDist = Infinity;
for (const [index, value] of dist) {
if (!visited.has(index) && value < currDist) {
currIndex = index;
currDist = value;
}
}
if (currIndex === -1) return [];
if (currIndex === targetIndex) break;
visited.add(currIndex);
const node = graph.nodes.get(currIndex);
if (!node || node.terminal) continue;
for (const edge of node.edges) {
if (visited.has(edge.to)) continue;
const next = graph.nodes.get(edge.to);
if (!next) continue;
if (next.terminal && edge.to !== targetIndex) continue;
const total = currDist + this.getNodeCost(next.block);
const known = dist.get(edge.to);
if (isNil(known) || total < known) {
dist.set(edge.to, total);
prev.set(edge.to, {
dir: edge.dir,
from: { x: node.x, y: node.y },
to: { x: next.x, y: next.y }
});
}
}
}
const steps: IPathfindingStep[] = [];
let curr = targetIndex;
while (curr !== startIndex) {
const step = prev.get(curr);
if (!step) return [];
steps.push(step);
curr = step.from.y * graph.width + step.from.x;
}
steps.reverse();
return steps;
}
/**
*
* 1
* @param block
*/
private getNodeCost(block: ILayerLocation): number {
const cost = this.cost;
if (!cost) return 1;
const value = cost(block);
if (!Number.isFinite(value) || value < 0) {
logger.warn(174);
return 1;
}
return value;
}
}
//#endregion
//#region 寻路系统
/**
*
* 退退
* 退
*/
export class PathfindingSystem implements IPathfindingSystem {
/** 寻路求解器 */
readonly finder: IPathfinder;
/** 绑定的移动对象 */
private movable: IObjectMovable | null = null;
/** 注入的瞬移回退策略,未注入时必定回退为逐步寻路 */
private policy: PathFallbackPolicy | null = null;
/** 最近一次寻路移动的控制器包装 */
private current: IPathfindingController | null = null;
constructor(readonly state: IStateBase) {
this.finder = new PathfindingFinder(state);
}
/**
*
* @param movable `null`
*/
useMovable(movable: IObjectMovable | null): void {
this.movable = movable;
}
/**
* 退 `null`
* @param policy 退
*/
useFallbackPolicy(policy: PathFallbackPolicy | null): void {
this.policy = policy;
}
/**
*
*
* @param target
*/
getPath(target: ITileLocator): IPathfindingStep[] {
const movable = this.movable;
if (isNil(movable)) {
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;
const policy = this.policy;
if (isNil(policy) || 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();
}
}
/**
*
* `null`
* @param path
* @param teleport
*/
private startMove(
path: readonly IPathfindingStep[],
teleport: boolean
): IPathfindingController | null {
const movable = this.movable;
if (!movable) 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];
mover.tp(last.to.x, last.to.y);
} else {
for (const step of path) {
mover.step(step.dir);
}
}
// 移动器移动中时启动失败,对应已有移动进行中的契约
const controller = mover.start();
if (!controller) return null;
const result: IPathfindingController = { controller, path };
this.current = result;
return result;
}
}
//#endregion

View File

@ -238,6 +238,7 @@
"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": "Pathfinding input is invalid or a required binding (map state, map layer) is missing. An empty result will be returned.",
"174": "Pathfinding cost function returned a non-finite or negative value. Default cost 1 will be used instead."
}
}