mirror of
https://github.com/motajs/template.git
synced 2026-10-16 10:30:18 +08:00
refactor: 寻路系统改进
This commit is contained in:
parent
50b8b3e651
commit
4aaea68b65
@ -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%
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
/** 要切换至的目标位置纵坐标 */
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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 通行性判断
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)!;
|
||||
|
||||
@ -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) ?? []
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
/**
|
||||
* 逐步寻路至目标位置,触发途经事件
|
||||
|
||||
@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user