Compare commits

..

2 Commits

Author SHA1 Message Date
AncTe
77bf564022
Merge f4c77d152f into 1a569804d8 2026-07-26 12:23:18 +00:00
f4c77d152f feat: 楼层切换 2026-07-26 20:22:36 +08:00
15 changed files with 449 additions and 23 deletions

View File

@ -1,9 +1,22 @@
import { IFacedTileLocator } from '@motajs/common'; import {
Hookable,
HookController,
IFacedTileLocator,
IHookController
} from '@motajs/common';
import { FaceDirection, IDataCommon, IFaceHandler } from '@user/data-common'; import { FaceDirection, IDataCommon, IFaceHandler } from '@user/data-common';
import { HeroMover } from './mover'; import { HeroMover } from './mover';
import { IHeroLocation, IHeroLocationSave, IHeroMover } from './types'; import {
IHeroLocation,
IHeroLocationHook,
IHeroLocationSave,
IHeroMover
} from './types';
export class HeroLocation implements IHeroLocation { export class HeroLocation
extends Hookable<IHeroLocationHook>
implements IHeroLocation
{
x: number; x: number;
y: number; y: number;
floorId: string | undefined = undefined; floorId: string | undefined = undefined;
@ -16,6 +29,7 @@ export class HeroLocation implements IHeroLocation {
loc: IFacedTileLocator, loc: IFacedTileLocator,
faceHandler: IFaceHandler<FaceDirection> faceHandler: IFaceHandler<FaceDirection>
) { ) {
super();
this.state = state; this.state = state;
this.x = loc.x; this.x = loc.x;
this.y = loc.y; this.y = loc.y;
@ -24,9 +38,21 @@ export class HeroLocation implements IHeroLocation {
this.mover = mover; this.mover = mover;
} }
protected createController(
hook: Partial<IHeroLocationHook>
): IHookController<IHeroLocationHook> {
return new HookController(this, hook);
}
setFloor(floorId: string | undefined): void {
this.floorId = floorId;
this.forEachHook(hook => hook.onSetFloor?.(floorId));
}
setPos(x: number, y: number): void { setPos(x: number, y: number): void {
this.x = x; this.x = x;
this.y = y; this.y = y;
this.forEachHook(hook => hook.onSetPos?.(x, y));
} }
getCurrentFaceDirection(): FaceDirection { getCurrentFaceDirection(): FaceDirection {

View File

@ -40,7 +40,7 @@ export class HeroMover<T extends IHeroLocation>
readonly tile: T, readonly tile: T,
faceHandler: IFaceHandler<FaceDirection> faceHandler: IFaceHandler<FaceDirection>
) { ) {
super(faceHandler); super(faceHandler, tile.getCurrentFaceDirection());
this.state = tile.state; this.state = tile.state;
} }

View File

@ -15,7 +15,9 @@ import {
IHeroState, IHeroState,
IHeroStateSave, IHeroStateSave,
IModifierStateSave, IModifierStateSave,
IReadonlyHeroAttribute IReadonlyHeroAttribute,
IHeroChangeFloorInfo,
IHeroStateHook
} from './types'; } from './types';
import { import {
FaceDirection, FaceDirection,
@ -23,9 +25,18 @@ import {
IFaceHandler, IFaceHandler,
SaveCompression SaveCompression
} from '@user/data-common'; } from '@user/data-common';
import { IFacedTileLocator, logger } from '@motajs/common'; import {
Hookable,
HookController,
IFacedTileLocator,
IHookController,
logger
} from '@motajs/common';
export class HeroState<THero> implements IHeroState<THero> { export class HeroState<THero>
extends Hookable<IHeroStateHook>
implements IHeroState<THero>
{
/** 修饰器工厂函数注册表 */ /** 修饰器工厂函数注册表 */
private readonly registry: Map< private readonly registry: Map<
string, string,
@ -43,6 +54,7 @@ export class HeroState<THero> implements IHeroState<THero> {
faceHandler: IFaceHandler<FaceDirection>, faceHandler: IFaceHandler<FaceDirection>,
public attribute: IHeroAttribute<THero> public attribute: IHeroAttribute<THero>
) { ) {
super();
this.rendering = new HeroRendering(state); this.rendering = new HeroRendering(state);
const defaultLoc: IFacedTileLocator = { const defaultLoc: IFacedTileLocator = {
x: 0, x: 0,
@ -59,8 +71,10 @@ export class HeroState<THero> implements IHeroState<THero> {
this.equip = new HeroEquipment(this.items.equipment, this.attribute); this.equip = new HeroEquipment(this.items.equipment, this.attribute);
} }
attachAttribute(attribute: IHeroAttribute<THero>): void { protected createController(
this.attribute = attribute; hook: Partial<IHeroStateHook>
): IHookController<IHeroStateHook> {
return new HookController(this, hook);
} }
getLocation(): IFacedTileLocator { getLocation(): IFacedTileLocator {
@ -71,6 +85,12 @@ export class HeroState<THero> implements IHeroState<THero> {
}; };
} }
//#region 属性相关
attachAttribute(attribute: IHeroAttribute<THero>): void {
this.attribute = attribute;
}
getModifiableAttribute(): IHeroAttribute<THero> { getModifiableAttribute(): IHeroAttribute<THero> {
return this.attribute; return this.attribute;
} }
@ -109,6 +129,22 @@ export class HeroState<THero> implements IHeroState<THero> {
return modifier; return modifier;
} }
//#endregion
async changeFloor(info: IHeroChangeFloorInfo): Promise<void> {
await Promise.all(
this.forEachHook(hook => hook.onBeforeChangeFloor?.(info))
);
const mover = this.location.mover;
mover.setFaceDir(info.face);
mover.setMoveDir(info.face);
mover.setPos(info.x, info.y);
this.location.setFloor(info.target);
await Promise.all(
this.forEachHook(hook => hook.onAfterChangeFloor?.(info))
);
}
saveState(compression: SaveCompression): IHeroStateSave<THero> { saveState(compression: SaveCompression): IHeroStateSave<THero> {
const modifiers: IModifierStateSave<THero>[] = []; const modifiers: IModifierStateSave<THero>[] = [];
for (const [name, modifier] of this.attribute.iterateModifiers()) { for (const [name, modifier] of this.attribute.iterateModifiers()) {

View File

@ -227,6 +227,21 @@ export interface IHeroAttribute<THero> extends IReadonlyHeroAttribute<THero> {
//#region 勇士位置 //#region 勇士位置
export interface IHeroLocationHook extends IHookBase {
/**
*
* @param x
* @param y
*/
onSetPos?(x: number, y: number): void;
/**
*
* @param floorId idundefined
*/
onSetFloor?(floorId: string | undefined): void;
}
export interface IHeroLocationSave { export interface IHeroLocationSave {
/** 当前横坐标 */ /** 当前横坐标 */
readonly x: number; readonly x: number;
@ -242,11 +257,18 @@ export interface IHeroLocation
extends extends
ISaveableContent<IHeroLocationSave>, ISaveableContent<IHeroLocationSave>,
IObjectMovable, IObjectMovable,
IHookable<IHeroLocationHook>,
IDataCommonExtended { IDataCommonExtended {
/** 当前所在楼层 idundefined 表示尚不处于任何楼层 */ /** 当前所在楼层 idundefined 表示尚不处于任何楼层 */
readonly floorId: string | undefined; readonly floorId: string | undefined;
/** 勇士的移动对象 */ /** 勇士的移动对象 */
readonly mover: IHeroMover<this>; readonly mover: IHeroMover<this>;
/**
* id
* @param floorId id
*/
setFloor(floorId: string | undefined): void;
} }
//#endregion //#endregion
@ -728,6 +750,33 @@ export interface IHeroEquipment<THero>
//#region 勇士状态 //#region 勇士状态
export interface IHeroChangeFloorInfo {
/** 要切换至的目标楼层 */
readonly target: string;
/** 要切换至的目标位置横坐标 */
readonly x: number;
/** 要切换至的目标位置纵坐标 */
readonly y: number;
/** 要切换至的目标朝向 */
readonly face: FaceDirection;
}
export interface IHeroStateHook extends IHookBase {
/**
* `IHeroLocation` `onSetFloor`
*
* @param info
*/
onBeforeChangeFloor?(info: IHeroChangeFloorInfo): Promise<void>;
/**
* `IHeroLocation` `onSetFloor`
*
* @param info
*/
onAfterChangeFloor?(info: IHeroChangeFloorInfo): Promise<void>;
}
export interface IHeroStateSave<THero> { export interface IHeroStateSave<THero> {
/** 勇士属性状态 */ /** 勇士属性状态 */
readonly attribute: THero; readonly attribute: THero;
@ -745,9 +794,8 @@ export interface IHeroStateSave<THero> {
readonly equip: IHeroEquipmentSave; readonly equip: IHeroEquipmentSave;
} }
export interface IHeroState<THero> extends ISaveableContent< export interface IHeroState<THero>
IHeroStateSave<THero> extends ISaveableContent<IHeroStateSave<THero>>, IHookable<IHeroStateHook> {
> {
/** 勇士移动对象 */ /** 勇士移动对象 */
readonly location: IHeroLocation; readonly location: IHeroLocation;
/** 勇士属性对象 */ /** 勇士属性对象 */
@ -809,6 +857,12 @@ export interface IHeroState<THero> extends ISaveableContent<
type: string, type: string,
name: K name: K
): IHeroModifier<THero[K], V> | null; ): IHeroModifier<THero[K], V> | null;
/**
*
* @param info
*/
changeFloor(info: IHeroChangeFloorInfo): Promise<void>;
} }
//#endregion //#endregion

View File

@ -1,5 +1,5 @@
export * from './layerState'; export * from './layerState';
export * from './mapLayer'; export * from './mapLayer';
export * from './mapStore'; export * from './mapState';
export * from './mover'; export * from './mover';
export * from './types'; export * from './types';

View File

@ -1,5 +1,5 @@
import { logger } from '@motajs/common'; import { logger } from '@motajs/common';
import { IDataCommon, SaveCompression } from '@user/data-common'; import { IDataCommon, IMapRawData, SaveCompression } from '@user/data-common';
import { ITileStore } from '@user/data-common'; import { ITileStore } from '@user/data-common';
import { import {
ILayerState, ILayerState,
@ -11,7 +11,7 @@ import {
MapArea MapArea
} from './types'; } from './types';
import { LayerState } from './layerState'; import { LayerState } from './layerState';
import { uniq } from 'lodash-es'; import { isNil, uniq } from 'lodash-es';
export class MapState implements IMapState { export class MapState implements IMapState {
/** 楼层 id 到状态对象的映射 */ /** 楼层 id 到状态对象的映射 */
@ -39,6 +39,56 @@ export class MapState implements IMapState {
//#region 楼层管理 //#region 楼层管理
fromRaw(raw: IMapRawData): ILayerState | null {
let length = 0;
const entries = Object.entries(raw.map);
for (const [_, map] of Object.entries(raw.map)) {
if (length > 0 && map.length !== length) {
logger.error(60, map.length.toString(), length.toString());
return null;
} else {
length = map.length;
}
}
if (length % raw.width !== 0) {
logger.error(61, length.toString(), raw.width.toString());
return null;
}
// 设置楼层图块数据
const height = length / raw.width;
const state = this.createLayerState(raw.floorId, raw.width, height);
for (const [zIndex, map] of entries) {
const z = Number(zIndex);
if (isNaN(z)) {
logger.error(62, 'IMapRawData.map', String(zIndex));
continue;
}
const layer = state.addLayer();
const alias = raw.layerAlias[z];
layer.setMapRef(new Uint32Array(map));
layer.setZIndex(z);
state.setLayerAlias(layer, alias);
// 设置图块静态触发器
const extra = raw.blockData[z];
for (const [index, data] of Object.entries(extra)) {
const indexNum = Number(index);
if (isNaN(indexNum)) {
logger.error(62, 'IMapRawData.blockData', String(index));
continue;
}
if (!isNil(data.trigger)) {
const x = indexNum % raw.width;
const y = Math.floor(indexNum / raw.width);
layer.setTriggerType(data.trigger, x, y);
}
}
}
return state;
}
createLayerState(id: string, width: number, height: number): ILayerState { createLayerState(id: string, width: number, height: number): ILayerState {
if (this.mapData.has(id)) { if (this.mapData.has(id)) {
logger.warn(121, id); logger.warn(121, id);

View File

@ -18,7 +18,7 @@ const enum DynamicMoveCode {
export class DynamicTileMover extends ObjectMover<IDynamicTile> { export class DynamicTileMover extends ObjectMover<IDynamicTile> {
constructor(public readonly tile: IDynamicTile) { constructor(public readonly tile: IDynamicTile) {
const face = tile.state.faceManager; const face = tile.state.faceManager;
super(face.get(DYNAMIC_MOVER_FACE)!); super(face.get(DYNAMIC_MOVER_FACE)!, tile.getCurrentFaceDirection());
} }
protected onMoveStart(): Promise<void> { protected onMoveStart(): Promise<void> {

View File

@ -1,7 +1,14 @@
import { IHookable, IHookBase, IHookController } from '@motajs/common'; import {
IHookable,
IHookBase,
IHookController,
ITileLocator
} from '@motajs/common';
import { import {
FaceDirection, FaceDirection,
IDataCommonExtended, IDataCommonExtended,
IMapBlockRawData,
IMapRawData,
IMoverController, IMoverController,
IObjectMovable, IObjectMovable,
IObjectMover, IObjectMover,
@ -21,6 +28,8 @@ export interface IMapLayerData {
} }
export interface ILayerLocation { export interface ILayerLocation {
/** 该点的位置 */
readonly locator: ITileLocator;
/** 该点的静态图块数字 */ /** 该点的静态图块数字 */
readonly tile: number; readonly tile: number;
/** 该点的静态图块信息 */ /** 该点的静态图块信息 */
@ -29,6 +38,8 @@ export interface ILayerLocation {
readonly trigger: number; readonly trigger: number;
/** 该点包含的所有动态图块 */ /** 该点包含的所有动态图块 */
readonly dynamics: Iterable<IDynamicTile>; readonly dynamics: Iterable<IDynamicTile>;
/** 该点的静态图块原始数据 */
readonly block: IMapBlockRawData;
} }
export interface IMapLayerHooks extends IHookBase { export interface IMapLayerHooks extends IHookBase {
@ -442,6 +453,12 @@ export interface IMapState
*/ */
getActiveMap(id: string): ILayerState | null; getActiveMap(id: string): ILayerState | null;
/**
*
* @param raw
*/
fromRaw(raw: IMapRawData): ILayerState | null;
/** /**
* id * id
* @param id id * @param id id

View File

@ -371,9 +371,13 @@ export abstract class ObjectMover<T extends IObjectMovable>
readonly faceHandler: IFaceHandler<FaceDirection>; readonly faceHandler: IFaceHandler<FaceDirection>;
constructor(faceHandler: IFaceHandler<FaceDirection>) { constructor(
faceHandler: IFaceHandler<FaceDirection>,
direction: FaceDirection
) {
super(); super();
this.faceHandler = faceHandler; this.faceHandler = faceHandler;
this.faceDirection = direction;
} }
protected createController( protected createController(
@ -651,7 +655,6 @@ export abstract class ObjectMover<T extends IObjectMovable>
this.clear(); this.clear();
this.shouldStop = false; this.shouldStop = false;
this.faceDirection = this.tile.getCurrentFaceDirection();
this.moveDirection = FaceDirection.Unknown; this.moveDirection = FaceDirection.Unknown;
const { promise, resolve } = Promise.withResolvers<void>(); const { promise, resolve } = Promise.withResolvers<void>();

View File

@ -1,5 +1,7 @@
//#region tile //#region tile
import { IFacedTileLocator } from '@motajs/common';
export const enum TileType { export const enum TileType {
/** 未知或尚未归类的图块 */ /** 未知或尚未归类的图块 */
Unknown, Unknown,
@ -253,6 +255,54 @@ export interface IItemStore<THero, TLegacy> {
//#region map //#region map
export const enum ChangeFloorType {
/** 指定具体位置的楼层切换方式 */
Specified,
/** 仅指定相对关系的楼层切换方式 */
Related
}
export const enum ChangeFloorTarget {
/** 切换至前一层 */
Previous,
/** 切换至后一层 */
Next
}
export const enum ChangeFloorPos {
/** 原地不动 */
Stand,
/** 左右对称位置 */
SymmetryX,
/** 上下对称位置 */
SymmetryY,
/** 中心对称位置 */
CentralSymmetry
}
export interface IChangeFloorData {
/** 切换至的楼层 id 的切换方式 */
readonly floorType: ChangeFloorType;
/** 指定具体位置的切换楼层方式下,切换至的具体楼层 */
readonly targetFloor?: string;
/** 相对关系的楼层切换方式下,切换至的楼层代号 */
readonly relatedFloor?: ChangeFloorTarget;
/** 切换至的目标位置的切换方式 */
readonly posType: ChangeFloorType;
/** 指定具体位置的位置切换方式下,切换至的具体位置,朝向填写 `FaceDirection.Unknown` 视为维持 */
readonly targetPos?: IFacedTileLocator;
/** 相对关系的位置切换方式下,切换至的位置代号 */
readonly relatedPos?: ChangeFloorPos;
}
export interface IMapBlockRawData {
/** 此点的静态触发器类型(仅对该点生效,不会跟随任何图块移动) */
readonly trigger?: number;
/** 楼层切换信息 */
readonly changeFloor?: IChangeFloorData;
}
export interface IMapRawData { export interface IMapRawData {
/** 楼层 id */ /** 楼层 id */
readonly floorId: string; readonly floorId: string;
@ -262,6 +312,8 @@ export interface IMapRawData {
readonly map: Record<number, number[]>; readonly map: Record<number, number[]>;
/** 每个地图图层的字符串别名 */ /** 每个地图图层的字符串别名 */
readonly layerAlias: Record<number, string>; readonly layerAlias: Record<number, string>;
/** 每个地图图层中,指定位置图块的额外数据,外层 key 为图层 zIndex内层 key 为图块位置索引 */
readonly blockData: Record<number, Record<number, IMapBlockRawData>>;
} }
export interface IMapStore { export interface IMapStore {

View File

@ -0,0 +1,162 @@
import { IFacedTileLocator, logger } from '@motajs/common';
import {
IHeroChangeFloorInfo,
IHeroState,
IMapLayer,
IMapState,
IStateBase
} from '@user/data-base';
import {
ChangeFloorPos,
ChangeFloorTarget,
ChangeFloorType,
FaceDirection,
IChangeFloorData,
IHeroAttr
} from '@user/data-common';
import {
BaseTrigger,
ITrigger,
ITriggerCollection,
ITriggerHandler,
TriggerCollection
} from '@user/data-system';
import { isNil } from 'lodash-es';
export const enum TriggerType {
/** 楼层切换触发器 */
ChangeFloor
}
export class ChangeFloorTrigger extends BaseTrigger implements ITrigger {
readonly type: number = TriggerType.ChangeFloor;
readonly priority: number = 10;
constructor(
state: IStateBase,
readonly maps: IMapState,
readonly hero: IHeroState<IHeroAttr>
) {
super(state);
}
/**
*
* @param data
*/
private getFloorTarget(data: IChangeFloorData): string | undefined {
if (data.floorType === ChangeFloorType.Specified) {
if (isNil(data.targetFloor)) {
logger.warn(165);
return void 0;
}
return data.targetFloor;
} else {
if (isNil(data.relatedFloor)) {
logger.warn(166);
return void 0;
}
const curr = this.hero.location.floorId;
if (isNil(curr)) {
logger.warn(167);
return void 0;
}
const index = this.maps.maps.indexOf(curr);
if (index === -1) {
logger.warn(168);
return void 0;
}
if (data.relatedFloor === ChangeFloorTarget.Next) {
const next = this.maps.maps[index + 1];
if (isNil(next)) {
logger.warn(169);
return void 0;
}
return next;
} else {
const next = this.maps.maps[index - 1];
if (isNil(next)) {
logger.warn(169);
return void 0;
}
}
}
}
private getPosTarget(
data: IChangeFloorData,
layer: IMapLayer
): IFacedTileLocator | undefined {
if (data.posType === ChangeFloorType.Specified) {
if (!data.targetPos) {
logger.warn(165);
return void 0;
}
return data.targetPos;
} else {
if (isNil(data.relatedPos)) {
logger.warn(166);
return void 0;
}
const { x, y, floorId } = this.hero.location;
if (isNil(floorId)) {
logger.warn(167);
return void 0;
}
const { width, height } = layer;
switch (data.relatedPos) {
case ChangeFloorPos.Stand:
return {
direction: FaceDirection.Unknown,
x,
y
};
case ChangeFloorPos.SymmetryX:
return {
direction: FaceDirection.Unknown,
x: width - x - 1,
y
};
case ChangeFloorPos.SymmetryY:
return {
direction: FaceDirection.Unknown,
x,
y: height - y - 1
};
case ChangeFloorPos.CentralSymmetry:
return {
direction: FaceDirection.Unknown,
x: width - x - 1,
y: height - y - 1
};
}
}
}
async trigger(handler: ITriggerHandler): Promise<void> {
if (!handler.layer || !handler.locator || !handler.mapLayer) {
logger.warn(164);
return;
}
const { x, y } = handler.locator;
const data = handler.mapLayer.getLocationData(x, y);
if (!data || !data.block.changeFloor) return;
const floor = this.getFloorTarget(data.block.changeFloor);
const pos = this.getPosTarget(data.block.changeFloor, handler.mapLayer);
if (isNil(floor) || isNil(pos)) return;
const hero = handler.state.hero;
const isUnknown = pos.direction === FaceDirection.Unknown;
const defaults = hero.location.mover.faceDirection;
const info: IHeroChangeFloorInfo = {
target: floor,
x: pos.x,
y: pos.y,
face: isUnknown ? defaults : pos.direction
};
return handler.state.hero.changeFloor(info);
}
collection(): ITriggerCollection {
return new TriggerCollection([this]);
}
}

View File

@ -225,7 +225,7 @@ export class CoreState implements ICoreState {
this.initTileStore(core.maps.blocksInfo); this.initTileStore(core.maps.blocksInfo);
this.initItemStore(core.items.items); this.initItemStore(core.items.items);
this.initEnemyManager(enemys_fcae963b_31c9_42b4_b48c_bb48d09f3f80); this.initEnemyManager(enemys_fcae963b_31c9_42b4_b48c_bb48d09f3f80);
this.initMapStore( this.initMapState(
core.floorIds, core.floorIds,
core.floors as Record<FloorIds, ResolvedFloor> core.floors as Record<FloorIds, ResolvedFloor>
); );
@ -323,7 +323,7 @@ export class CoreState implements ICoreState {
manager.compareWith(reference); manager.compareWith(reference);
} }
private initMapStore( private initMapState(
floors: FloorIds[], floors: FloorIds[],
data: Record<FloorIds, ResolvedFloor> data: Record<FloorIds, ResolvedFloor>
) { ) {

View File

@ -1,4 +1,5 @@
export * from './collection'; export * from './collection';
export * from './collector'; export * from './collector';
export * from './registry'; export * from './registry';
export * from './trigger';
export * from './types'; export * from './types';

View File

@ -0,0 +1,16 @@
import { IStateBase } from '@user/data-base';
import { ITrigger, ITriggerCollection, ITriggerHandler } from './types';
import { TriggerCollection } from './collection';
export abstract class BaseTrigger implements ITrigger {
abstract type: number;
abstract priority: number;
constructor(readonly state: IStateBase) {}
abstract trigger(handler: ITriggerHandler): Promise<void>;
collection(): ITriggerCollection {
return new TriggerCollection([this]);
}
}

View File

@ -58,7 +58,10 @@
"56": "Cannot convert legacy tile data since no tile legacy converter is attached to TileStore.", "56": "Cannot convert legacy tile data since no tile legacy converter is attached to TileStore.",
"57": "Cannot convert legacy item data since no item legacy converter is attached to ItemStore.", "57": "Cannot convert legacy item data since no item legacy converter is attached to ItemStore.",
"58": "Cannot find max equipment uid while loading equipment store.", "58": "Cannot find max equipment uid while loading equipment store.",
"59": "Cannot find item raw data for tile num $1." "59": "Cannot find item raw data for tile num $1.",
"60": "Cannot create layer state from raw data since the area of every layer of the map does not equal: $1 vs $2.",
"61": "Cannot create layer state from raw data since map area cannot be divided by width evenly: area = $1 while divider = $2.",
"62": "Expected a number for the key of $1, but got $2."
}, },
"warn": { "warn": {
"1": "Resource with type of 'none' is loaded.", "1": "Resource with type of 'none' is loaded.",
@ -223,6 +226,12 @@
"160": "Replay safety collection ending function is called outside replay safety collection unexpectedly.", "160": "Replay safety collection ending function is called outside replay safety collection unexpectedly.",
"161": "Replay safety detected. Followings are simplified detect info. To get detailed info, run: '$1'.\n$2", "161": "Replay safety detected. Followings are simplified detect info. To get detailed info, run: '$1'.\n$2",
"162": "Cannot find replay safety detail info of code $1. The detail info may expired, or it just has not been existed.", "162": "Cannot find replay safety detail info of code $1. The detail info may expired, or it just has not been existed.",
"163": "Replay command code $1 has already been registered. This will not replace the old registry." "163": "Replay command code $1 has already been registered. This will not replace the old registry.",
"164": "Change floor trigger needs specified locator.",
"165": "Target floor id or target position needs to be specified while change floor type is 'ChangeFloorType.Specified'.",
"166": "Related floor id or related position needs to be specified while change floor type is 'ChangeFloorType.Related'.",
"167": "Hero must at a specified floor before using related change floor method.",
"168": "The current floor of hero does not in the specified map list.",
"169": "The current floor is the last or the first floor in the map list, so related floor change method will not work."
} }
} }