mirror of
https://github.com/motajs/template.git
synced 2026-08-31 16:02:30 +08:00
refactor: 游戏地图接口重构
This commit is contained in:
parent
1717400945
commit
7eeab32134
@ -51,10 +51,11 @@
|
||||
13. **非空判断**:对象直接用 `if (!object)`;字面量使用 `lodash` 的 `isNil`(`if (isNil(value))`),不得使用 `if (value === undefined)` 等方式。
|
||||
14. **公共方法**:不得出现仅在类中定义的公共方法,所有公共方法必须先在接口中定义,然后在类中使用 `implements` 实现。
|
||||
15. **成员只读**:所有需要被实现为类的接口,其成员必须只读,如果有赋值需求,那么应使用相应的方法来完成赋值操作。
|
||||
16. **对象类型**:对于任何接口或类的对象成员,必须使用接口作为类型,不得使用类作为类型,例如 `map: IGameMap` 符合要求,但 `map: GameMap` 不符合要求。
|
||||
|
||||
## 架构约束
|
||||
|
||||
16. **渲染端被动**:任何情况下渲染端不会主动向数据端推送更新。渲染端仅通过钩子与数据端通信,被动获取信息,仅在某些情况下通过钩子影响数据端行为。
|
||||
17. **渲染端被动**:任何情况下渲染端不会主动向数据端推送更新。渲染端仅通过钩子与数据端通信,被动获取信息,仅在某些情况下通过钩子影响数据端行为。
|
||||
|
||||
# 开发流程
|
||||
|
||||
|
||||
3
dev.md
3
dev.md
@ -76,11 +76,12 @@
|
||||
- 长文件可使用 `#region` / `#endregion` 分段以支持折叠。
|
||||
- TODO 使用 `// TODO:` 或 `// todo:` 格式。
|
||||
- 单行注释的 `//` 与注释内容之间留一个空格;不允许出现非 jsDoc 的多行注释,如需多行注释,使用多个单行注释代替。
|
||||
- 注释合理换行:考虑中文字符较宽,建议每 40–60 个字符在标点符号后换行,不要频繁换行,每行长度应差不多,不允许在句中换行;参数注释换行后保持对齐。
|
||||
- 注释合理换行:考虑中文字符较宽,建议每 40–60 个字符在标点符号后换行,不要频繁换行,每行长度应差不多,不要过短也不要过长,不允许在句中换行;参数注释换行后保持对齐。
|
||||
- 单行注释结尾不加句号;较长的多行注释结尾可加句号。
|
||||
- 一般不建议给接口(`inteface` 本身)、类型别名、枚举或类本身写注释(即需要紧接着一长段缩进的时候,这时候写注释不好看),特殊情况除外。
|
||||
- 对于 jsDoc 注释,方法注释必须使用换行风格;对于成员 jsDoc 注释,除非注释较长需要换行,否则使用不换行的风格。
|
||||
- 代码中的注释必须包含价值,说明下一行为什么会出现,不得出现解释下一行代码等类型的无价值注释,如 `// 清空 Xxx` 就属于无价值注释。
|
||||
- jsDoc 注释应该包含主要信息,不允许假设其使用场景,非必要不使用符号或英文单词,注释的目的是让用户看完注释就立刻知道接口的用处,而不是给写代码的人看,不得为了注释的正确性而使用奇怪的语序。
|
||||
|
||||
### 类型规范
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { BaseProps, TagDefine } from '@motajs/render-vue';
|
||||
import { ERenderItemEvent, SizedCanvasImageSource } from '@motajs/render';
|
||||
import { ILayerState } from '@user/data-base';
|
||||
import { IGameMap } from '@user/data-base';
|
||||
import { IMapExtensionManager, IMapRenderer } from '../map';
|
||||
|
||||
export interface IconProps extends BaseProps {
|
||||
@ -24,7 +24,7 @@ export interface WinskinProps extends BaseProps {
|
||||
}
|
||||
|
||||
export interface MapRenderProps extends BaseProps {
|
||||
layerState: ILayerState;
|
||||
layerState: IGameMap;
|
||||
renderer: IMapRenderer;
|
||||
extension: IMapExtensionManager;
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { MotaOffscreenCanvas2D, RenderItem } from '@motajs/render';
|
||||
import { ILayerState } from '@user/data-base';
|
||||
import { IGameMap } from '@user/data-base';
|
||||
import { IMapRenderer } from './types';
|
||||
import { ElementNamespace, ComponentInternalInstance } from 'vue';
|
||||
import { CELL_HEIGHT, CELL_WIDTH, MAP_HEIGHT, MAP_WIDTH } from '../../shared';
|
||||
@ -11,7 +11,7 @@ export class MapRenderItem extends RenderItem {
|
||||
* @param renderer 地图渲染器对象
|
||||
*/
|
||||
constructor(
|
||||
readonly layerState: ILayerState,
|
||||
readonly layerState: IGameMap,
|
||||
readonly renderer: IMapRenderer,
|
||||
readonly exManager: IMapExtensionManager
|
||||
) {
|
||||
|
||||
@ -30,7 +30,7 @@ import {
|
||||
MapTileBehavior,
|
||||
MapTileSizeTestMode
|
||||
} from './types';
|
||||
import { ILayerState, ILayerStateHooks, IMapLayer } from '@user/data-base';
|
||||
import { IGameMap, IGameMapHooks, IMapLayer } from '@user/data-base';
|
||||
import { IHookController, logger } from '@motajs/common';
|
||||
import { compileProgramWith } from '@motajs/client-base';
|
||||
import { isNil, maxBy } from 'lodash-es';
|
||||
@ -78,9 +78,9 @@ export class MapRenderer
|
||||
assetWidth: number = 4096;
|
||||
assetHeight: number = 4096;
|
||||
|
||||
layerState: ILayerState;
|
||||
layerState: IGameMap;
|
||||
/** 地图状态钩子控制器 */
|
||||
private layerStateHook: IHookController<ILayerStateHooks>;
|
||||
private layerStateHook: IHookController<IGameMapHooks>;
|
||||
|
||||
/** 排序后的图层 */
|
||||
private sortedLayers: IMapLayer[] = [];
|
||||
@ -210,7 +210,7 @@ export class MapRenderer
|
||||
*/
|
||||
constructor(
|
||||
readonly manager: IMaterialManager,
|
||||
layerState: ILayerState
|
||||
layerState: IGameMap
|
||||
) {
|
||||
this.movingIndexPool.push(
|
||||
...Array.from({ length: this.movingCount }, (_, i) => i).reverse()
|
||||
@ -427,7 +427,7 @@ export class MapRenderer
|
||||
this.vertex.updateLayerArray();
|
||||
}
|
||||
|
||||
setLayerState(layerState: ILayerState): void {
|
||||
setLayerState(layerState: IGameMap): void {
|
||||
if (layerState === this.layerState) return;
|
||||
this.layerStateHook.unload();
|
||||
this.layerState = layerState;
|
||||
@ -1735,7 +1735,7 @@ export class MapRenderer
|
||||
//#endregion
|
||||
}
|
||||
|
||||
class RendererLayerStateHook implements Partial<ILayerStateHooks> {
|
||||
class RendererLayerStateHook implements Partial<IGameMapHooks> {
|
||||
constructor(readonly renderer: MapRenderer) {}
|
||||
|
||||
onChangeBackground(tile: number): void {
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
IMaterialManager,
|
||||
ITrackedAssetData
|
||||
} from '@user/client-base';
|
||||
import { ILayerState, IMapLayer } from '@user/data-base';
|
||||
import { IGameMap, IMapLayer } from '@user/data-base';
|
||||
import { TimingFn } from 'mutate-animate';
|
||||
|
||||
export const enum MapBackgroundRepeat {
|
||||
@ -339,7 +339,7 @@ export interface IMapRenderer {
|
||||
/** 顶点数组生成器 */
|
||||
readonly vertex: IMapVertexGenerator;
|
||||
/** 使用的地图状态对象 */
|
||||
readonly layerState: ILayerState;
|
||||
readonly layerState: IGameMap;
|
||||
|
||||
/** 地图宽度 */
|
||||
readonly mapWidth: number;
|
||||
@ -432,7 +432,7 @@ export interface IMapRenderer {
|
||||
* 设置渲染器使用的地图状态
|
||||
* @param layerState 地图状态
|
||||
*/
|
||||
setLayerState(layerState: ILayerState): void;
|
||||
setLayerState(layerState: IGameMap): void;
|
||||
|
||||
/**
|
||||
* 根据标识符获取图层
|
||||
|
||||
@ -8,13 +8,13 @@ import { FaceDirection, IDataCommon, IFaceHandler } from '@user/data-common';
|
||||
import { HeroMover } from './mover';
|
||||
import {
|
||||
IHeroLocation,
|
||||
IHeroLocationHook,
|
||||
IHeroLocationHooks,
|
||||
IHeroLocationSave,
|
||||
IHeroMover
|
||||
} from './types';
|
||||
|
||||
export class HeroLocation
|
||||
extends Hookable<IHeroLocationHook>
|
||||
extends Hookable<IHeroLocationHooks>
|
||||
implements IHeroLocation
|
||||
{
|
||||
x: number;
|
||||
@ -39,8 +39,8 @@ export class HeroLocation
|
||||
}
|
||||
|
||||
protected createController(
|
||||
hook: Partial<IHeroLocationHook>
|
||||
): IHookController<IHeroLocationHook> {
|
||||
hook: Partial<IHeroLocationHooks>
|
||||
): IHookController<IHeroLocationHooks> {
|
||||
return new HookController(this, hook);
|
||||
}
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ import {
|
||||
IModifierStateSave,
|
||||
IReadonlyHeroAttribute,
|
||||
IHeroChangeFloorInfo,
|
||||
IHeroStateHook
|
||||
IHeroStateHooks
|
||||
} from './types';
|
||||
import {
|
||||
FaceDirection,
|
||||
@ -34,7 +34,7 @@ import {
|
||||
} from '@motajs/common';
|
||||
|
||||
export class HeroState<THero>
|
||||
extends Hookable<IHeroStateHook>
|
||||
extends Hookable<IHeroStateHooks>
|
||||
implements IHeroState<THero>
|
||||
{
|
||||
/** 修饰器工厂函数注册表 */
|
||||
@ -72,8 +72,8 @@ export class HeroState<THero>
|
||||
}
|
||||
|
||||
protected createController(
|
||||
hook: Partial<IHeroStateHook>
|
||||
): IHookController<IHeroStateHook> {
|
||||
hook: Partial<IHeroStateHooks>
|
||||
): IHookController<IHeroStateHooks> {
|
||||
return new HookController(this, hook);
|
||||
}
|
||||
|
||||
|
||||
@ -227,7 +227,7 @@ export interface IHeroAttribute<THero> extends IReadonlyHeroAttribute<THero> {
|
||||
|
||||
//#region 勇士位置
|
||||
|
||||
export interface IHeroLocationHook extends IHookBase {
|
||||
export interface IHeroLocationHooks extends IHookBase {
|
||||
/**
|
||||
* 当设置勇士位置时触发
|
||||
* @param x 设置为的横坐标
|
||||
@ -257,7 +257,7 @@ export interface IHeroLocation
|
||||
extends
|
||||
ISaveableContent<IHeroLocationSave>,
|
||||
IObjectMovable,
|
||||
IHookable<IHeroLocationHook>,
|
||||
IHookable<IHeroLocationHooks>,
|
||||
IDataCommonExtended {
|
||||
/** 当前所在楼层 id,undefined 表示尚不处于任何楼层 */
|
||||
readonly floorId: string | undefined;
|
||||
@ -779,7 +779,7 @@ export interface IHeroChangeFloorInfo {
|
||||
readonly face: FaceDirection;
|
||||
}
|
||||
|
||||
export interface IHeroStateHook extends IHookBase {
|
||||
export interface IHeroStateHooks extends IHookBase {
|
||||
/**
|
||||
* 当勇士切换楼层前触发,此钩子执行完毕后会立刻触发 `IHeroLocation` 的 `onSetFloor` 钩子。
|
||||
* 此钩子应该用于切换楼层前的过渡及必要的数据准备,不应该修改勇士位置相关的数据。
|
||||
@ -813,7 +813,9 @@ export interface IHeroStateSave<THero> {
|
||||
}
|
||||
|
||||
export interface IHeroState<THero>
|
||||
extends ISaveableContent<IHeroStateSave<THero>>, IHookable<IHeroStateHook> {
|
||||
extends
|
||||
ISaveableContent<IHeroStateSave<THero>>,
|
||||
IHookable<IHeroStateHooks> {
|
||||
/** 勇士移动对象 */
|
||||
readonly location: IHeroLocation;
|
||||
/** 勇士属性对象 */
|
||||
|
||||
@ -1,183 +0,0 @@
|
||||
import {
|
||||
HookController,
|
||||
Hookable,
|
||||
IHookController,
|
||||
ITileLocator,
|
||||
logger
|
||||
} from '@motajs/common';
|
||||
import {
|
||||
IDynamicLayer,
|
||||
IDynamicLayerHooks,
|
||||
IDynamicTile,
|
||||
IMapLayer
|
||||
} from './types';
|
||||
import { FaceDirection, IDataCommon, degradeFace } from '@user/data-common';
|
||||
import { DynamicTile } from './dynamicTile';
|
||||
|
||||
export class DynamicLayer
|
||||
extends Hookable<IDynamicLayerHooks>
|
||||
implements IDynamicLayer
|
||||
{
|
||||
readonly state: IDataCommon;
|
||||
|
||||
/** 坐标到动态图块集合的映射,外层 key = y,内层 key = x,不使用 index 是为了支持地图外图块 */
|
||||
private readonly tilePosMap: Map<number, Map<number, Set<IDynamicTile>>> =
|
||||
new Map();
|
||||
/** 动态图块到其当前坐标的映射 */
|
||||
private readonly posTileMap: Map<IDynamicTile, ITileLocator> = new Map();
|
||||
|
||||
constructor(public readonly layer: IMapLayer) {
|
||||
super();
|
||||
this.state = layer.state;
|
||||
}
|
||||
|
||||
protected createController(
|
||||
hook: Partial<IDynamicLayerHooks>
|
||||
): IHookController<IDynamicLayerHooks> {
|
||||
return new HookController(this, hook);
|
||||
}
|
||||
|
||||
createDynamic(num: number, x: number, y: number): IDynamicTile {
|
||||
const tile = new DynamicTile(num, x, y, this);
|
||||
tile.setTriggerType(this.layer.getTriggerType(x, y));
|
||||
this.addTileToPosMap(tile, x, y);
|
||||
this.posTileMap.set(tile, { x, y });
|
||||
this.forEachHook(hook => hook.onCreateTile?.(tile, this));
|
||||
return tile;
|
||||
}
|
||||
|
||||
transferToDynamic(
|
||||
x: number,
|
||||
y: number,
|
||||
keepTrigger: boolean = true
|
||||
): IDynamicTile {
|
||||
const num = this.layer.getBlock(x, y);
|
||||
const triggerType = keepTrigger ? this.layer.getTriggerType(x, y) : -1;
|
||||
if (num === 0) {
|
||||
logger.warn(127, x.toString(), y.toString());
|
||||
}
|
||||
this.layer.setBlock(0, x, y);
|
||||
this.layer.revertTrigger(x, y);
|
||||
const tile = this.createDynamic(num, x, y);
|
||||
tile.setTriggerType(triggerType);
|
||||
return tile;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将动态图块上的触发器同步回当前静态格点
|
||||
*/
|
||||
private syncStaticTrigger(tile: IDynamicTile, keepTrigger: boolean): void {
|
||||
if (keepTrigger) {
|
||||
this.layer.setTriggerType(tile.triggerType, tile.x, tile.y);
|
||||
} else {
|
||||
this.layer.revertTrigger(tile.x, tile.y);
|
||||
}
|
||||
}
|
||||
|
||||
transferToStatic(tile: IDynamicTile, keepTrigger: boolean = true): void {
|
||||
const { x, y } = tile;
|
||||
const { width, height } = this.layer;
|
||||
if (x < 0 || y < 0 || x >= width || y >= height) {
|
||||
logger.warn(128, x.toString(), y.toString());
|
||||
return;
|
||||
}
|
||||
if (this.layer.getBlock(x, y) !== 0) {
|
||||
logger.warn(129, x.toString(), y.toString());
|
||||
}
|
||||
this.layer.setBlock(tile.num, x, y);
|
||||
this.syncStaticTrigger(tile, keepTrigger);
|
||||
this.removeTile(tile);
|
||||
this.forEachHook(hook => hook.onDeleteTile?.(tile, this));
|
||||
}
|
||||
|
||||
transferToStaticIfSafe(
|
||||
tile: IDynamicTile,
|
||||
keepTrigger: boolean = true
|
||||
): boolean {
|
||||
const { x, y } = tile;
|
||||
const { width, height } = this.layer;
|
||||
if (x < 0 || y < 0 || x >= width || y >= height) {
|
||||
logger.warn(128, x.toString(), y.toString());
|
||||
return false;
|
||||
}
|
||||
if (this.layer.getBlock(tile.x, tile.y) !== 0) return false;
|
||||
this.layer.setBlock(tile.num, x, y);
|
||||
this.syncStaticTrigger(tile, keepTrigger);
|
||||
this.removeTile(tile);
|
||||
this.forEachHook(hook => hook.onDeleteTile?.(tile, this));
|
||||
return true;
|
||||
}
|
||||
|
||||
async deleteDynamic(tile: IDynamicTile): Promise<void> {
|
||||
if (!this.posTileMap.has(tile)) {
|
||||
logger.warn(130);
|
||||
return;
|
||||
}
|
||||
this.removeTile(tile);
|
||||
const hooks = this.forEachHook(hook => hook.onDeleteTile?.(tile, this));
|
||||
await Promise.all(hooks);
|
||||
}
|
||||
|
||||
getDynamicTilesAt(x: number, y: number): Iterable<IDynamicTile> {
|
||||
return this.tilePosMap.get(y)?.get(x) ?? new Set();
|
||||
}
|
||||
|
||||
iterateDynamicTiles(): Iterable<IDynamicTile> {
|
||||
return this.posTileMap.keys();
|
||||
}
|
||||
|
||||
setDynamicDirection(tile: IDynamicTile, direction: FaceDirection): void {
|
||||
const numBefore = tile.num;
|
||||
tile.setFaceDirection(direction);
|
||||
if (tile.num !== numBefore) return;
|
||||
const degraded = degradeFace(direction);
|
||||
if (degraded !== direction) {
|
||||
tile.setFaceDirection(degraded);
|
||||
}
|
||||
}
|
||||
|
||||
updateDynamicTile(tile: IDynamicTile): void {
|
||||
const oldPos = this.posTileMap.get(tile);
|
||||
if (oldPos) {
|
||||
this.removeTileFromPosMap(tile, oldPos.x, oldPos.y);
|
||||
oldPos.x = tile.x;
|
||||
oldPos.y = tile.y;
|
||||
this.addTileToPosMap(tile, tile.x, tile.y);
|
||||
} else {
|
||||
this.addTileToPosMap(tile, tile.x, tile.y);
|
||||
this.posTileMap.set(tile, { x: tile.x, y: tile.y });
|
||||
}
|
||||
this.forEachHook(hook => hook.onUpdateTilePosition?.(tile, this));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将动态图块登记到指定坐标的索引表中
|
||||
*/
|
||||
private addTileToPosMap(tile: IDynamicTile, x: number, y: number): void {
|
||||
const xMap = this.tilePosMap.getOrInsertComputed(y, () => new Map());
|
||||
const set = xMap.getOrInsertComputed(x, () => new Set());
|
||||
set.add(tile);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将动态图块从指定坐标的索引表中移除
|
||||
*/
|
||||
private removeTileFromPosMap(
|
||||
tile: IDynamicTile,
|
||||
x: number,
|
||||
y: number
|
||||
): void {
|
||||
this.tilePosMap.get(y)?.get(x)?.delete(tile);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从两个内部映射中移除图块记录
|
||||
*/
|
||||
private removeTile(tile: IDynamicTile): void {
|
||||
const pos = this.posTileMap.get(tile);
|
||||
if (pos) {
|
||||
this.removeTileFromPosMap(tile, pos.x, pos.y);
|
||||
}
|
||||
this.posTileMap.delete(tile);
|
||||
}
|
||||
}
|
||||
@ -1,70 +1,89 @@
|
||||
import { isNil } from 'lodash-es';
|
||||
import {
|
||||
FaceDirection,
|
||||
IDataCommon,
|
||||
IMoverController,
|
||||
IObjectMover,
|
||||
IRoleFaceBinder,
|
||||
ITileRawData
|
||||
} from '@user/data-common';
|
||||
import { IDynamicLayer, IDynamicTile } from './types';
|
||||
import {
|
||||
IDynamicBlockSave,
|
||||
IDynamicTile,
|
||||
IMapLayer,
|
||||
IStaticTile
|
||||
} from './types';
|
||||
import { DynamicTileMover } from './mover';
|
||||
import { logger } from '@motajs/common';
|
||||
import { MapTileBase } from './tile';
|
||||
|
||||
export class DynamicTile implements IDynamicTile {
|
||||
readonly state: IDataCommon;
|
||||
export class DynamicTile
|
||||
extends MapTileBase<IDynamicBlockSave>
|
||||
implements IDynamicTile
|
||||
{
|
||||
readonly mover: IObjectMover<IDynamicTile>;
|
||||
|
||||
raw: ITileRawData | null;
|
||||
triggerType: number;
|
||||
|
||||
/** 当前的朝向绑定对象 */
|
||||
private face: IRoleFaceBinder | null = null;
|
||||
/** 内部存储的图块数字 */
|
||||
private tileNum: number;
|
||||
/** 内部存储的原始图块信息 */
|
||||
private tileRaw: ITileRawData | null;
|
||||
|
||||
constructor(
|
||||
public num: number,
|
||||
num: number,
|
||||
public x: number,
|
||||
public y: number,
|
||||
public readonly layer: IDynamicLayer
|
||||
layer: IMapLayer
|
||||
) {
|
||||
this.state = layer.state;
|
||||
super(x, y, layer);
|
||||
this.tileNum = num;
|
||||
this.mover = new DynamicTileMover(this);
|
||||
this.triggerType = -1;
|
||||
const data = this.state.tileStore.getData(num);
|
||||
if (!data) {
|
||||
logger.warn(143, num.toString());
|
||||
this.raw = null;
|
||||
this.tileRaw = null;
|
||||
} else {
|
||||
this.raw = data;
|
||||
this.tileRaw = data;
|
||||
}
|
||||
}
|
||||
|
||||
setFaceBinder(binder: IRoleFaceBinder | null): void {
|
||||
this.face = binder;
|
||||
num(): number {
|
||||
return this.tileNum;
|
||||
}
|
||||
|
||||
setFaceDirection(direction: FaceDirection): number {
|
||||
if (!this.face) return this.num;
|
||||
const next = this.face.getFaceOf(this.num, direction);
|
||||
if (next) {
|
||||
this.num = next.identifier;
|
||||
}
|
||||
return this.num;
|
||||
raw(): ITileRawData | null {
|
||||
return this.tileRaw;
|
||||
}
|
||||
|
||||
setTriggerType(type: number): void {
|
||||
this.triggerType = type;
|
||||
set(num: number): void {
|
||||
this.tileNum = num;
|
||||
const data = this.state.tileStore.getData(num);
|
||||
if (!data) {
|
||||
logger.warn(143, num.toString());
|
||||
this.tileRaw = null;
|
||||
} else {
|
||||
this.tileRaw = data;
|
||||
}
|
||||
}
|
||||
|
||||
delete(): Promise<void> {
|
||||
return this.layer.deleteDynamic(this);
|
||||
setPos(x: number, y: number): void {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.locator = { x, y };
|
||||
this.layer.updateDynamicTile(this);
|
||||
}
|
||||
|
||||
toStatic(): void {
|
||||
this.layer.transferToStatic(this);
|
||||
getCurrentFaceDirection(): FaceDirection {
|
||||
const curr = this.layer.faceBinder.getFaceDirection(this.tileNum);
|
||||
if (isNil(curr)) {
|
||||
return FaceDirection.Unknown;
|
||||
} else {
|
||||
return curr;
|
||||
}
|
||||
}
|
||||
|
||||
toStaticIfSafe(): boolean {
|
||||
toStatic(): IStaticTile | null {
|
||||
return this.layer.transferToStatic(this);
|
||||
}
|
||||
|
||||
toStaticIfSafe(): IStaticTile | null {
|
||||
return this.layer.transferToStaticIfSafe(this);
|
||||
}
|
||||
|
||||
@ -74,19 +93,31 @@ export class DynamicTile implements IDynamicTile {
|
||||
return this.mover.start();
|
||||
}
|
||||
|
||||
setPos(x: number, y: number): void {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.layer.updateDynamicTile(this);
|
||||
delete(): Promise<void> {
|
||||
return this.layer.deleteDynamic(this);
|
||||
}
|
||||
|
||||
getCurrentFaceDirection(): FaceDirection {
|
||||
if (this.face) {
|
||||
const face = this.face.getFaceDirection(this.num);
|
||||
if (isNil(face)) return FaceDirection.Unknown;
|
||||
else return face;
|
||||
saveState(): Readonly<IDynamicBlockSave> {
|
||||
const save: IDynamicBlockSave = {
|
||||
num: this.num()
|
||||
};
|
||||
|
||||
if (this.triggers) {
|
||||
save.triggers = this.triggers;
|
||||
}
|
||||
|
||||
return save;
|
||||
}
|
||||
|
||||
loadState(save: Readonly<IDynamicBlockSave>): void {
|
||||
if (save.triggers) {
|
||||
if (save.triggers.size === 0) {
|
||||
this.useEmptyTrigger();
|
||||
} else {
|
||||
return FaceDirection.Down;
|
||||
save.triggers.forEach(v => this.addTrigger(v));
|
||||
}
|
||||
} else {
|
||||
this.clearTrigger();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,22 +5,25 @@ import {
|
||||
logger
|
||||
} from '@motajs/common';
|
||||
import {
|
||||
ILayerState,
|
||||
ILayerStateHooks,
|
||||
IGameMap,
|
||||
IGameMapHooks,
|
||||
IGameMapSave,
|
||||
IMapLayer,
|
||||
IMapLayerHookController,
|
||||
IMapLayerHooks
|
||||
IMapLayerHooks,
|
||||
IMapLayerSave,
|
||||
IResizableMapLayer
|
||||
} from './types';
|
||||
import { IDataCommon, ITileStore } from '@user/data-common';
|
||||
import {
|
||||
IDataCommon,
|
||||
ITileStore,
|
||||
MapLocIndexer,
|
||||
SaveCompression
|
||||
} from '@user/data-common';
|
||||
import { MapLayer } from './mapLayer';
|
||||
|
||||
export class LayerState
|
||||
extends Hookable<ILayerStateHooks>
|
||||
implements ILayerState
|
||||
{
|
||||
readonly layerList: Set<IMapLayer> = new Set();
|
||||
/** 具体 MapLayer 实例列表,供内部 resize 使用 */
|
||||
private readonly mapLayerList: Set<MapLayer> = new Set();
|
||||
export class GameMap extends Hookable<IGameMapHooks> implements IGameMap {
|
||||
readonly layerList: Set<IResizableMapLayer> = new Set();
|
||||
/** 图层到图层别名映射 */
|
||||
readonly layerAliasMap: WeakMap<IMapLayer, string> = new WeakMap();
|
||||
/** 图层别名到图层的映射 */
|
||||
@ -32,11 +35,14 @@ export class LayerState
|
||||
/** 图层钩子映射 */
|
||||
private layerHookMap: Map<IMapLayer, IMapLayerHookController> = new Map();
|
||||
|
||||
/** 坐标索引器 */
|
||||
readonly indexer = new MapLocIndexer();
|
||||
|
||||
active: boolean = false;
|
||||
eventLayer: IMapLayer | null = null;
|
||||
|
||||
/** 楼层级脏标记 */
|
||||
private dirty: boolean = false;
|
||||
/** 楼层自身脏标记 */
|
||||
private selfDirty: boolean = false;
|
||||
|
||||
constructor(
|
||||
public readonly state: IDataCommon,
|
||||
@ -45,19 +51,13 @@ export class LayerState
|
||||
public height: number
|
||||
) {
|
||||
super();
|
||||
this.indexer.setWidth(width);
|
||||
}
|
||||
|
||||
addLayer(): IMapLayer {
|
||||
const array = new Uint32Array(this.width * this.height);
|
||||
const layer = new MapLayer(
|
||||
array,
|
||||
this.width,
|
||||
this.height,
|
||||
this,
|
||||
this.tileStore
|
||||
);
|
||||
const layer = new MapLayer(array, this.width, this.height, this);
|
||||
this.layerList.add(layer);
|
||||
this.mapLayerList.add(layer);
|
||||
this.forEachHook(hook => {
|
||||
hook.onUpdateLayer?.(this.layerList);
|
||||
});
|
||||
@ -68,8 +68,7 @@ export class LayerState
|
||||
}
|
||||
|
||||
removeLayer(layer: IMapLayer): void {
|
||||
this.layerList.delete(layer);
|
||||
this.mapLayerList.delete(layer as MapLayer);
|
||||
this.layerList.delete(layer as IResizableMapLayer);
|
||||
const alias = this.layerAliasMap.get(layer);
|
||||
if (alias) {
|
||||
const symbol = Symbol.for(alias);
|
||||
@ -86,7 +85,7 @@ export class LayerState
|
||||
}
|
||||
|
||||
hasLayer(layer: IMapLayer): boolean {
|
||||
return this.layerList.has(layer);
|
||||
return this.layerList.has(layer as IResizableMapLayer);
|
||||
}
|
||||
|
||||
setLayerAlias(layer: IMapLayer, alias: string): void {
|
||||
@ -115,7 +114,8 @@ export class LayerState
|
||||
): void {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
for (const layer of this.mapLayerList) {
|
||||
this.indexer.setWidth(width);
|
||||
for (const layer of this.layerList) {
|
||||
if (keepBlock) {
|
||||
layer.resize(width, height);
|
||||
} else {
|
||||
@ -143,50 +143,86 @@ export class LayerState
|
||||
if (!layer) {
|
||||
this.eventLayer = null;
|
||||
} else {
|
||||
if (!this.layerList.has(layer)) {
|
||||
if (!this.layerList.has(layer as IResizableMapLayer)) {
|
||||
logger.warn(131);
|
||||
return;
|
||||
}
|
||||
this.eventLayer = layer;
|
||||
}
|
||||
}
|
||||
|
||||
isDirty(): boolean {
|
||||
return this.dirty;
|
||||
dirty(): boolean {
|
||||
if (this.selfDirty) return true;
|
||||
for (const layer of this.layerList) {
|
||||
if (layer.dirty()) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
setDirty(dirty: boolean): void {
|
||||
this.dirty = dirty;
|
||||
markDirty(dirty: boolean): void {
|
||||
this.selfDirty = dirty;
|
||||
}
|
||||
|
||||
compareWith(data: Map<number, Uint32Array>): void {
|
||||
for (const layer of this.layerList) {
|
||||
const refArray = data.get(layer.zIndex);
|
||||
if (refArray) {
|
||||
layer.compareWith(refArray);
|
||||
} else {
|
||||
layer.markDirty(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断图层存档是否不含任何有效内容
|
||||
* @param save 图层存档
|
||||
*/
|
||||
private isEmptyLayerSave(save: IMapLayerSave): boolean {
|
||||
if (save.fullMap) return false;
|
||||
if (save.rows && save.rows.size > 0) return false;
|
||||
if (save.staticBlocks && save.staticBlocks.size > 0) return false;
|
||||
if (save.dynamicBlocks && save.dynamicBlocks.size > 0) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
saveState(compression: SaveCompression): IGameMapSave {
|
||||
const layers = new Map<number, IMapLayerSave>();
|
||||
for (const layer of this.layerList) {
|
||||
const save = layer.saveState(compression);
|
||||
if (this.isEmptyLayerSave(save)) continue;
|
||||
layers.set(layer.zIndex, save);
|
||||
}
|
||||
return {
|
||||
background: this.backgroundTile,
|
||||
layers
|
||||
};
|
||||
}
|
||||
|
||||
loadState(save: IGameMapSave, compression: SaveCompression): void {
|
||||
this.setBackground(save.background);
|
||||
for (const layer of this.layerList) {
|
||||
const layerSave = save.layers.get(layer.zIndex);
|
||||
if (!layerSave) continue;
|
||||
layer.loadState(layerSave, compression);
|
||||
}
|
||||
this.markDirty(false);
|
||||
}
|
||||
|
||||
protected createController(
|
||||
hook: Partial<ILayerStateHooks>
|
||||
): IHookController<ILayerStateHooks> {
|
||||
hook: Partial<IGameMapHooks>
|
||||
): IHookController<IGameMapHooks> {
|
||||
return new HookController(this, hook);
|
||||
}
|
||||
}
|
||||
|
||||
class StateMapLayerHook implements Partial<IMapLayerHooks> {
|
||||
constructor(
|
||||
readonly state: LayerState,
|
||||
readonly state: GameMap,
|
||||
readonly layer: IMapLayer
|
||||
) {}
|
||||
|
||||
onUpdateArea(x: number, y: number, width: number, height: number): void {
|
||||
this.state.setDirty(true);
|
||||
this.state.forEachHook(hook => {
|
||||
hook.onUpdateLayerArea?.(this.layer, x, y, width, height);
|
||||
});
|
||||
}
|
||||
|
||||
onUpdateBlock(block: number, x: number, y: number): void {
|
||||
this.state.setDirty(true);
|
||||
this.state.forEachHook(hook => {
|
||||
hook.onUpdateLayerBlock?.(this.layer, block, x, y);
|
||||
});
|
||||
}
|
||||
|
||||
onResize(width: number, height: number): void {
|
||||
this.state.setDirty(true);
|
||||
this.state.forEachHook(hook => {
|
||||
hook.onResizeLayer?.(this.layer, width, height);
|
||||
});
|
||||
@ -1,4 +1,4 @@
|
||||
export * from './layerState';
|
||||
export * from './gameMap';
|
||||
export * from './mapLayer';
|
||||
export * from './mapState';
|
||||
export * from './mover';
|
||||
|
||||
@ -1,18 +1,27 @@
|
||||
import { isNil } from 'lodash-es';
|
||||
import {
|
||||
IDynamicLayer,
|
||||
IDynamicBlockSave,
|
||||
IDynamicTile,
|
||||
IGameMap,
|
||||
ILayerLocation,
|
||||
ILayerState,
|
||||
IMapLayer,
|
||||
IMapLayerData,
|
||||
IMapLayerHookController,
|
||||
IMapLayerHooks
|
||||
IMapLayerHooks,
|
||||
IMapLayerSave,
|
||||
IStaticBlockSave,
|
||||
IStaticTile
|
||||
} from './types';
|
||||
import { Hookable, HookController, logger } from '@motajs/common';
|
||||
import { DynamicLayer } from './dynamicLayer';
|
||||
import { IDataCommon, ITileStore } from '@user/data-common';
|
||||
|
||||
// todo: 提供 core.setBlock 等方法的替代方法,同时添加 setBlockList,以及前景背景的接口
|
||||
import { Hookable, HookController, ITileLocator, logger } from '@motajs/common';
|
||||
import {
|
||||
degradeFace,
|
||||
FaceDirection,
|
||||
IDataCommon,
|
||||
IRoleFaceBinder,
|
||||
SaveCompression
|
||||
} from '@user/data-common';
|
||||
import { DynamicTile } from './dynamicTile';
|
||||
import { StaticTile } from './staticTile';
|
||||
|
||||
export class MapLayer
|
||||
extends Hookable<IMapLayerHooks, IMapLayerHookController>
|
||||
@ -25,127 +34,116 @@ export class MapLayer
|
||||
empty: boolean = true;
|
||||
zIndex: number = 0;
|
||||
|
||||
faceBinder: IRoleFaceBinder;
|
||||
|
||||
/** 地图图块数组 */
|
||||
private mapArray: Uint32Array;
|
||||
/** 地图数据引用 */
|
||||
private mapData: IMapLayerData;
|
||||
/** 手动触发器覆盖映射,key = y * width + x */
|
||||
private triggerMap: Map<number, number> = new Map();
|
||||
|
||||
readonly dynamicLayer: IDynamicLayer;
|
||||
/** 静态图块实例缓存,key = y * width + x */
|
||||
private readonly staticTileCache: Map<number, StaticTile> = new Map();
|
||||
/** 坐标到动态图块集合的映射,外层 key = y,内层 key = x */
|
||||
private readonly tilePosMap: Map<number, Map<number, Set<IDynamicTile>>> =
|
||||
new Map();
|
||||
/** 动态图块到其当前坐标的映射 */
|
||||
private readonly posTileMap: Map<IDynamicTile, ITileLocator> = new Map();
|
||||
/** 图层脏标记 */
|
||||
private layerDirty: boolean = false;
|
||||
/** 图层参考基准,用于存档压缩对比 */
|
||||
private refArray: Uint32Array | null = null;
|
||||
|
||||
constructor(
|
||||
array: Uint32Array,
|
||||
width: number,
|
||||
height: number,
|
||||
public readonly layerState: ILayerState,
|
||||
private readonly tileStore: ITileStore
|
||||
public readonly map: IGameMap
|
||||
) {
|
||||
super();
|
||||
this.state = layerState.state;
|
||||
this.state = map.state;
|
||||
this.faceBinder = this.state.roleFace;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
const area = width * height;
|
||||
this.mapArray = new Uint32Array(area);
|
||||
// 超出的裁剪,不足的补零
|
||||
this.mapArray.set(array);
|
||||
this.mapData = {
|
||||
expired: false,
|
||||
array: this.mapArray
|
||||
};
|
||||
this.dynamicLayer = new DynamicLayer(this);
|
||||
}
|
||||
|
||||
inMap(x: number, y: number): boolean {
|
||||
return x >= 0 && y >= 0 && x < this.width && y < this.height;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在地图尺寸变化后重新映射手动触发器覆盖表
|
||||
* 将动态图块登记到指定坐标的索引表中
|
||||
* @param tile 动态图块
|
||||
* @param x 横坐标
|
||||
* @param y 纵坐标
|
||||
*/
|
||||
private remapTriggerMap(
|
||||
beforeWidth: number,
|
||||
width: number,
|
||||
height: number
|
||||
): Map<number, number> {
|
||||
const next = new Map<number, number>();
|
||||
for (const [index, type] of this.triggerMap) {
|
||||
const x = index % beforeWidth;
|
||||
const y = Math.floor(index / beforeWidth);
|
||||
if (x < width && y < height) {
|
||||
next.set(y * width + x, type);
|
||||
}
|
||||
}
|
||||
return next;
|
||||
private addTileToPosMap(tile: IDynamicTile, x: number, y: number): void {
|
||||
const xMap = this.tilePosMap.getOrInsertComputed(y, () => new Map());
|
||||
const set = xMap.getOrInsertComputed(x, () => new Set());
|
||||
set.add(tile);
|
||||
}
|
||||
|
||||
resize(width: number, height: number): void {
|
||||
if (this.width === width && this.height === height) {
|
||||
return;
|
||||
/**
|
||||
* 将动态图块从指定坐标的索引表中移除
|
||||
* @param tile 动态图块
|
||||
* @param x 横坐标
|
||||
* @param y 纵坐标
|
||||
*/
|
||||
private removeTileFromPosMap(
|
||||
tile: IDynamicTile,
|
||||
x: number,
|
||||
y: number
|
||||
): void {
|
||||
this.tilePosMap.get(y)?.get(x)?.delete(tile);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从两个内部映射中移除图块记录
|
||||
* @param tile 动态图块
|
||||
*/
|
||||
private removeTile(tile: IDynamicTile): void {
|
||||
const pos = this.posTileMap.get(tile);
|
||||
if (pos) {
|
||||
this.removeTileFromPosMap(tile, pos.x, pos.y);
|
||||
}
|
||||
this.posTileMap.delete(tile);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将动态图块的触发器同步回当前静态格点
|
||||
* @param tile 动态图块
|
||||
* @param keepTrigger 是否保留触发器
|
||||
*/
|
||||
private syncStaticTrigger(tile: IDynamicTile, keepTrigger: boolean): void {
|
||||
const staticTile = this.getTile(tile.x, tile.y);
|
||||
if (!staticTile) return;
|
||||
if (keepTrigger) {
|
||||
const triggers = tile.triggers;
|
||||
if (!triggers) {
|
||||
staticTile.clearTrigger();
|
||||
} else {
|
||||
staticTile.clearTrigger();
|
||||
for (const trigger of triggers) {
|
||||
staticTile.addTrigger(trigger);
|
||||
}
|
||||
this.mapData.expired = true;
|
||||
const before = this.mapArray;
|
||||
const beforeWidth = this.width;
|
||||
const beforeHeight = this.height;
|
||||
const beforeArea = beforeWidth * beforeHeight;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
const area = width * height;
|
||||
const newArray = new Uint32Array(area);
|
||||
this.triggerMap = this.remapTriggerMap(beforeWidth, width, height);
|
||||
this.mapArray = newArray;
|
||||
// 将原来的地图数组赋值给现在的
|
||||
if (beforeArea > area) {
|
||||
// 如果地图变小了,那么直接设置,不需要补零
|
||||
for (let ny = 0; ny < height; ny++) {
|
||||
const begin = ny * beforeWidth;
|
||||
newArray.set(before.subarray(begin, begin + width), ny * width);
|
||||
}
|
||||
} else {
|
||||
// 如果地图变大了,那么需要补零。因为新数组本来就是用 0 填充的,实际上只要赋值就可以了
|
||||
for (let ny = 0; ny < beforeHeight; ny++) {
|
||||
const begin = ny * beforeWidth;
|
||||
newArray.set(
|
||||
before.subarray(begin, begin + beforeWidth),
|
||||
ny * width
|
||||
);
|
||||
staticTile.clearTrigger();
|
||||
}
|
||||
}
|
||||
this.mapData = {
|
||||
expired: false,
|
||||
array: this.mapArray
|
||||
};
|
||||
this.forEachHook(hook => {
|
||||
hook.onResize?.(width, height);
|
||||
});
|
||||
}
|
||||
|
||||
resize2(width: number, height: number): void {
|
||||
if (this.width === width && this.height === height) {
|
||||
this.empty = true;
|
||||
this.mapArray.fill(0);
|
||||
this.triggerMap.clear();
|
||||
return;
|
||||
}
|
||||
this.mapData.expired = true;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.mapArray = new Uint32Array(width * height);
|
||||
this.triggerMap.clear();
|
||||
this.mapData = {
|
||||
expired: false,
|
||||
array: this.mapArray
|
||||
};
|
||||
this.empty = true;
|
||||
this.forEachHook(hook => {
|
||||
hook.onResize?.(width, height);
|
||||
});
|
||||
//#region 静态图层操作
|
||||
|
||||
inMap(x: number, y: number): boolean {
|
||||
return x >= 0 && y >= 0 && x < this.width && y < this.height;
|
||||
}
|
||||
|
||||
setBlock(block: number, x: number, y: number): void {
|
||||
const index = y * this.width + x;
|
||||
if (block === this.mapArray[index]) return;
|
||||
this.mapArray[index] = block;
|
||||
this.layerDirty = true;
|
||||
this.forEachHook(hook => {
|
||||
hook.onUpdateBlock?.(block, x, y);
|
||||
});
|
||||
@ -161,74 +159,47 @@ export class MapLayer
|
||||
return this.mapArray[y * this.width + x];
|
||||
}
|
||||
|
||||
getLocationData(x: number, y: number): ILayerLocation | null {
|
||||
getTile(x: number, y: number): IStaticTile | null {
|
||||
if (!this.inMap(x, y)) return null;
|
||||
const index = y * this.width + x;
|
||||
const num = this.mapArray[index];
|
||||
const raw = this.state.tileStore.getData(num);
|
||||
const dynamics = this.dynamicLayer.getDynamicTilesAt(x, y);
|
||||
const trigger = this.triggerMap.get(index) ?? -1;
|
||||
const index = this.map.indexer.locToIndex(x, y);
|
||||
let staticTile = this.staticTileCache.get(index);
|
||||
if (!staticTile) {
|
||||
staticTile = new StaticTile(x, y, this);
|
||||
this.staticTileCache.set(index, staticTile);
|
||||
}
|
||||
return staticTile;
|
||||
}
|
||||
|
||||
const data: ILayerLocation = {
|
||||
tile: num,
|
||||
raw,
|
||||
trigger,
|
||||
dynamics,
|
||||
getLocationData(x: number, y: number): ILayerLocation | null {
|
||||
const staticTile = this.getTile(x, y);
|
||||
if (!staticTile) return null;
|
||||
const num = staticTile.num();
|
||||
const dynamics = this.getDynamicTilesAt(x, y);
|
||||
return {
|
||||
locator: { x, y },
|
||||
block: raw
|
||||
tile: num,
|
||||
dynamics,
|
||||
static: staticTile
|
||||
};
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
getTriggerType(x: number, y: number): number {
|
||||
if (!this.inMap(x, y)) {
|
||||
return -1;
|
||||
}
|
||||
const index = y * this.width + x;
|
||||
if (this.triggerMap.has(index)) {
|
||||
return this.triggerMap.get(index)!;
|
||||
}
|
||||
return this.tileStore.getTrigger(this.mapArray[index]);
|
||||
}
|
||||
|
||||
setTriggerType(type: number, x: number, y: number): void {
|
||||
if (!this.inMap(x, y)) {
|
||||
return;
|
||||
}
|
||||
const index = y * this.width + x;
|
||||
if (this.tileStore.getTrigger(this.mapArray[index]) === type) {
|
||||
this.triggerMap.delete(index);
|
||||
} else {
|
||||
this.triggerMap.set(index, type);
|
||||
}
|
||||
}
|
||||
|
||||
revertTrigger(x: number, y: number): void {
|
||||
if (this.inMap(x, y)) {
|
||||
this.triggerMap.delete(y * this.width + x);
|
||||
}
|
||||
}
|
||||
|
||||
clearTrigger(): void {
|
||||
this.triggerMap.clear();
|
||||
}
|
||||
|
||||
putMapData(array: Uint32Array, x: number, y: number, width: number): void {
|
||||
if (array.length % width !== 0) {
|
||||
logger.warn(8);
|
||||
}
|
||||
const height = Math.ceil(array.length / width);
|
||||
if (width === this.width && height === this.height) {
|
||||
this.layerDirty = true;
|
||||
const h = Math.ceil(array.length / width);
|
||||
if (width === this.width && h === this.height) {
|
||||
this.mapArray.set(array);
|
||||
this.staticTileCache.clear();
|
||||
this.forEachHook(hook => {
|
||||
hook.onUpdateArea?.(x, y, width, height);
|
||||
hook.onUpdateArea?.(x, y, width, h);
|
||||
});
|
||||
return;
|
||||
}
|
||||
const w = this.width;
|
||||
const r = x + width;
|
||||
const b = y + height;
|
||||
const b = y + h;
|
||||
if (x < 0 || y < 0 || r > w || b > this.height) {
|
||||
logger.warn(9);
|
||||
}
|
||||
@ -244,13 +215,12 @@ export class MapLayer
|
||||
const offset = (ny + nt) * w + nl;
|
||||
const sub = array.subarray(start, start + nw);
|
||||
if (empty && sub.some(v => v !== 0)) {
|
||||
// 空地图判断
|
||||
empty = false;
|
||||
}
|
||||
this.mapArray.set(array.subarray(start, start + nw), offset);
|
||||
}
|
||||
this.forEachHook(hook => {
|
||||
hook.onUpdateArea?.(x, y, width, height);
|
||||
hook.onUpdateArea?.(x, y, width, h);
|
||||
});
|
||||
this.empty &&= empty;
|
||||
}
|
||||
@ -305,6 +275,7 @@ export class MapLayer
|
||||
}
|
||||
this.mapData.expired = true;
|
||||
this.mapArray = array;
|
||||
this.staticTileCache.clear();
|
||||
this.mapData = {
|
||||
expired: false,
|
||||
array: this.mapArray
|
||||
@ -319,23 +290,153 @@ export class MapLayer
|
||||
return this.mapData;
|
||||
}
|
||||
|
||||
setTriggerRef(triggers: Map<number, number>): void {
|
||||
this.triggerMap = triggers;
|
||||
setStaticDirection(x: number, y: number, direction: FaceDirection): number {
|
||||
const tile = this.getTile(x, y);
|
||||
if (!tile) return -1;
|
||||
return tile.setFaceDirection(direction);
|
||||
}
|
||||
|
||||
getTriggerRef(): ReadonlyMap<number, number> {
|
||||
return this.triggerMap;
|
||||
*iterateBlocks(): Iterable<ILayerLocation> {
|
||||
for (let y = 0; y < this.height; y++) {
|
||||
for (let x = 0; x < this.width; x++) {
|
||||
const num = this.getBlock(x, y);
|
||||
if (num !== 0) {
|
||||
yield this.getLocationData(x, y)!;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected createController(
|
||||
hook: Partial<IMapLayerHooks>
|
||||
): IMapLayerHookController {
|
||||
return new MapLayerHookController(this, hook);
|
||||
//#endregion
|
||||
|
||||
//#region 动态图层操作
|
||||
|
||||
createDynamic(num: number, x: number, y: number): IDynamicTile {
|
||||
const tile = new DynamicTile(num, x, y, this);
|
||||
const location = this.getLocationData(x, y);
|
||||
if (location) {
|
||||
const staticTriggers = location.static.triggers;
|
||||
if (staticTriggers) {
|
||||
for (const trigger of staticTriggers) {
|
||||
tile.addTrigger(trigger);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.addTileToPosMap(tile, x, y);
|
||||
this.posTileMap.set(tile, { x, y });
|
||||
this.forEachHook(hook => hook.onCreateDynamic?.(tile));
|
||||
return tile;
|
||||
}
|
||||
|
||||
setZIndex(zIndex: number): void {
|
||||
this.zIndex = zIndex;
|
||||
transferToDynamic(
|
||||
x: number,
|
||||
y: number,
|
||||
keepTrigger: boolean = true
|
||||
): IDynamicTile | null {
|
||||
if (!this.inMap(x, y)) {
|
||||
logger.warn(131, x.toString(), y.toString());
|
||||
return null;
|
||||
}
|
||||
const num = this.getBlock(x, y);
|
||||
if (num === 0) {
|
||||
logger.warn(127, x.toString(), y.toString());
|
||||
}
|
||||
this.setBlock(0, x, y);
|
||||
const tile = this.createDynamic(num, x, y);
|
||||
const staticTile = this.getTile(x, y);
|
||||
if (staticTile) {
|
||||
staticTile.clearTrigger();
|
||||
}
|
||||
if (!keepTrigger) {
|
||||
tile.clearTrigger();
|
||||
}
|
||||
return tile;
|
||||
}
|
||||
|
||||
transferToStatic(
|
||||
tile: IDynamicTile,
|
||||
keepTrigger: boolean = true
|
||||
): IStaticTile | null {
|
||||
const x = tile.x;
|
||||
const y = tile.y;
|
||||
if (x < 0 || y < 0 || x >= this.width || y >= this.height) {
|
||||
logger.warn(128, x.toString(), y.toString());
|
||||
return null;
|
||||
}
|
||||
if (this.getBlock(x, y) !== 0) {
|
||||
logger.warn(129, x.toString(), y.toString());
|
||||
}
|
||||
this.setBlock(tile.num(), x, y);
|
||||
this.syncStaticTrigger(tile, keepTrigger);
|
||||
this.removeTile(tile);
|
||||
this.forEachHook(hook => hook.onDeleteDynamic?.(tile));
|
||||
return this.getTile(x, y);
|
||||
}
|
||||
|
||||
transferToStaticIfSafe(
|
||||
tile: IDynamicTile,
|
||||
keepTrigger: boolean = true
|
||||
): IStaticTile | null {
|
||||
const x = tile.x;
|
||||
const y = tile.y;
|
||||
if (x < 0 || y < 0 || x >= this.width || y >= this.height) {
|
||||
logger.warn(128, x.toString(), y.toString());
|
||||
return null;
|
||||
}
|
||||
if (this.getBlock(tile.x, tile.y) !== 0) return null;
|
||||
this.setBlock(tile.num(), x, y);
|
||||
this.syncStaticTrigger(tile, keepTrigger);
|
||||
this.removeTile(tile);
|
||||
this.forEachHook(hook => hook.onDeleteDynamic?.(tile));
|
||||
return this.getTile(x, y);
|
||||
}
|
||||
|
||||
async deleteDynamic(tile: IDynamicTile): Promise<void> {
|
||||
if (!this.posTileMap.has(tile)) {
|
||||
logger.warn(130);
|
||||
return;
|
||||
}
|
||||
this.removeTile(tile);
|
||||
const hooks = this.forEachHook(hook => hook.onDeleteDynamic?.(tile));
|
||||
await Promise.all(hooks);
|
||||
}
|
||||
|
||||
getDynamicTilesAt(x: number, y: number): Iterable<IDynamicTile> {
|
||||
return this.tilePosMap.get(y)?.get(x) ?? new Set();
|
||||
}
|
||||
|
||||
iterateDynamicTiles(): Iterable<IDynamicTile> {
|
||||
return this.posTileMap.keys();
|
||||
}
|
||||
|
||||
setDynamicDirection(tile: IDynamicTile, direction: FaceDirection): number {
|
||||
const numBefore = tile.num();
|
||||
tile.setFaceDirection(direction);
|
||||
if (tile.num() !== numBefore) return tile.num();
|
||||
const degraded = degradeFace(direction);
|
||||
if (degraded !== direction) {
|
||||
tile.setFaceDirection(degraded);
|
||||
}
|
||||
return tile.num();
|
||||
}
|
||||
|
||||
updateDynamicTile(tile: IDynamicTile): void {
|
||||
const oldPos = this.posTileMap.get(tile);
|
||||
if (oldPos) {
|
||||
this.removeTileFromPosMap(tile, oldPos.x, oldPos.y);
|
||||
oldPos.x = tile.x;
|
||||
oldPos.y = tile.y;
|
||||
this.addTileToPosMap(tile, tile.x, tile.y);
|
||||
} else {
|
||||
this.addTileToPosMap(tile, tile.x, tile.y);
|
||||
this.posTileMap.set(tile, { x: tile.x, y: tile.y });
|
||||
}
|
||||
this.forEachHook(hook => hook.onUpdateDynamicPosition?.(tile));
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 开关门
|
||||
|
||||
async openDoor(x: number, y: number): Promise<void> {
|
||||
const index = y * this.width + x;
|
||||
@ -363,6 +464,352 @@ export class MapLayer
|
||||
);
|
||||
this.setBlock(num, x, y);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 图层操作
|
||||
|
||||
setZIndex(zIndex: number): void {
|
||||
this.zIndex = zIndex;
|
||||
}
|
||||
|
||||
setFaceBinder(binder: IRoleFaceBinder | null): void {
|
||||
if (!binder) return;
|
||||
this.faceBinder = binder;
|
||||
}
|
||||
|
||||
dirty(): boolean {
|
||||
return this.layerDirty;
|
||||
}
|
||||
|
||||
markDirty(dirty: boolean): void {
|
||||
this.layerDirty = dirty;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前图层的地图矩阵是否与参考基准完全一致
|
||||
*/
|
||||
private isEqualToRef(): boolean {
|
||||
const ref = this.refArray;
|
||||
if (!ref) return false;
|
||||
if (this.mapArray.length !== ref.length) return false;
|
||||
return !this.mapArray.some((v, i) => ref[i] !== v);
|
||||
}
|
||||
|
||||
compareWith(data: Uint32Array): void {
|
||||
if (this.refArray) return;
|
||||
this.refArray = data;
|
||||
this.layerDirty = !this.isEqualToRef();
|
||||
}
|
||||
|
||||
protected createController(
|
||||
hook: Partial<IMapLayerHooks>
|
||||
): IMapLayerHookController {
|
||||
return new MapLayerHookController(this, hook);
|
||||
}
|
||||
|
||||
resize(width: number, height: number): void {
|
||||
if (this.width === width && this.height === height) {
|
||||
return;
|
||||
}
|
||||
this.layerDirty = true;
|
||||
this.mapData.expired = true;
|
||||
const before = this.mapArray;
|
||||
const beforeWidth = this.width;
|
||||
const beforeHeight = this.height;
|
||||
const beforeArea = beforeWidth * beforeHeight;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
const area = width * height;
|
||||
const newArray = new Uint32Array(area);
|
||||
this.mapArray = newArray;
|
||||
this.staticTileCache.clear();
|
||||
if (beforeArea > area) {
|
||||
for (let ny = 0; ny < height; ny++) {
|
||||
const begin = ny * beforeWidth;
|
||||
newArray.set(before.subarray(begin, begin + width), ny * width);
|
||||
}
|
||||
} else {
|
||||
for (let ny = 0; ny < beforeHeight; ny++) {
|
||||
const begin = ny * beforeWidth;
|
||||
newArray.set(
|
||||
before.subarray(begin, begin + beforeWidth),
|
||||
ny * width
|
||||
);
|
||||
}
|
||||
}
|
||||
this.mapData = {
|
||||
expired: false,
|
||||
array: this.mapArray
|
||||
};
|
||||
this.forEachHook(hook => {
|
||||
hook.onResize?.(width, height);
|
||||
});
|
||||
}
|
||||
|
||||
resize2(width: number, height: number): void {
|
||||
this.layerDirty = true;
|
||||
if (this.width === width && this.height === height) {
|
||||
this.empty = true;
|
||||
this.mapArray.fill(0);
|
||||
this.staticTileCache.clear();
|
||||
return;
|
||||
}
|
||||
this.mapData.expired = true;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.mapArray = new Uint32Array(width * height);
|
||||
this.staticTileCache.clear();
|
||||
this.mapData = {
|
||||
expired: false,
|
||||
array: this.mapArray
|
||||
};
|
||||
this.empty = true;
|
||||
this.forEachHook(hook => {
|
||||
hook.onResize?.(width, height);
|
||||
});
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 存读档
|
||||
|
||||
/**
|
||||
* 保存静态图块实例
|
||||
*/
|
||||
private saveStatics(): Map<number, IStaticBlockSave> {
|
||||
const blocks = new Map<number, IStaticBlockSave>();
|
||||
for (const location of this.iterateBlocks()) {
|
||||
const tile = location.static;
|
||||
if (!tile.shouldSave()) continue;
|
||||
const index = this.map.indexer.locaterToIndex(location.locator);
|
||||
blocks.set(index, tile.saveState(SaveCompression.NoCompression));
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存动态图块实例
|
||||
*/
|
||||
private saveDynamics(): Map<number, IDynamicBlockSave[]> {
|
||||
const blocks = new Map<number, IDynamicBlockSave[]>();
|
||||
for (const tile of this.iterateDynamicTiles()) {
|
||||
const index = this.map.indexer.locaterToIndex(tile.locator);
|
||||
const list = blocks.getOrInsert(index, []);
|
||||
list.push(tile.saveState(SaveCompression.NoCompression));
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* 与参考行比较,返回与参考基准不同的行
|
||||
* @param refArray 参考基准数组
|
||||
*/
|
||||
private diffRows(refArray: Uint32Array): Map<number, Uint32Array> {
|
||||
const rows = new Map<number, Uint32Array>();
|
||||
for (let row = 0; row < this.height; row++) {
|
||||
const start = row * this.width;
|
||||
const end = start + this.width;
|
||||
const slice = this.mapArray.subarray(start, end);
|
||||
const refSlice = refArray.subarray(start, end);
|
||||
const same = refSlice.every((v, i) => slice[i] === v);
|
||||
if (!same) {
|
||||
rows.set(row, new Uint32Array(slice));
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* 以无压缩方式序列化当前图层
|
||||
*/
|
||||
private saveNoCompression(): IMapLayerSave {
|
||||
return {
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
fullMap: new Uint32Array(this.mapArray),
|
||||
staticBlocks: this.saveStatics(),
|
||||
dynamicBlocks: this.saveDynamics()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 以低压缩方式序列化当前图层
|
||||
*/
|
||||
private saveLowCompression(): IMapLayerSave {
|
||||
const staticBlocks = this.saveStatics();
|
||||
const dynamicBlocks = this.saveDynamics();
|
||||
if (this.layerDirty && (!this.refArray || !this.isEqualToRef())) {
|
||||
return {
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
fullMap: new Uint32Array(this.mapArray),
|
||||
staticBlocks,
|
||||
dynamicBlocks
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
staticBlocks,
|
||||
dynamicBlocks
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 以高压缩方式序列化当前图层
|
||||
*/
|
||||
private saveHighCompression(): IMapLayerSave {
|
||||
const staticBlocks = this.saveStatics();
|
||||
const dynamicBlocks = this.saveDynamics();
|
||||
if (this.layerDirty) {
|
||||
if (this.refArray) {
|
||||
return {
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
rows: this.diffRows(this.refArray),
|
||||
staticBlocks,
|
||||
dynamicBlocks
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
fullMap: new Uint32Array(this.mapArray),
|
||||
staticBlocks,
|
||||
dynamicBlocks
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
staticBlocks,
|
||||
dynamicBlocks
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
saveState(compression: SaveCompression): IMapLayerSave {
|
||||
if (compression === SaveCompression.HighCompression) {
|
||||
return this.saveHighCompression();
|
||||
} else if (compression === SaveCompression.LowCompression) {
|
||||
return this.saveLowCompression();
|
||||
} else {
|
||||
return this.saveNoCompression();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取静态图块实例存档数据
|
||||
* @param save 静态图块实例存档
|
||||
*/
|
||||
private loadStatics(save: ReadonlyMap<number, IStaticBlockSave>): void {
|
||||
for (const [index, tileSave] of save) {
|
||||
const { x, y } = this.map.indexer.indexToLocator(index);
|
||||
const location = this.getLocationData(x, y);
|
||||
if (!location) continue;
|
||||
location.static.loadState(tileSave, SaveCompression.NoCompression);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取动态图块实例存档数据
|
||||
* @param save 动态图块实例存档
|
||||
*/
|
||||
private loadDynamics(save: ReadonlyMap<number, IDynamicBlockSave[]>): void {
|
||||
for (const [index, dynamics] of save) {
|
||||
const { x, y } = this.map.indexer.indexToLocator(index);
|
||||
for (const block of dynamics) {
|
||||
const tile = this.createDynamic(block.num, x, y);
|
||||
tile.loadState(block, SaveCompression.NoCompression);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 以无压缩方式读取当前图层
|
||||
* @param save 图层存档
|
||||
*/
|
||||
private loadNoCompression(save: IMapLayerSave): void {
|
||||
if (save.fullMap) {
|
||||
this.setMapRef(new Uint32Array(save.fullMap));
|
||||
}
|
||||
if (save.staticBlocks) {
|
||||
this.loadStatics(save.staticBlocks);
|
||||
}
|
||||
if (save.dynamicBlocks) {
|
||||
this.loadDynamics(save.dynamicBlocks);
|
||||
}
|
||||
this.layerDirty = !this.isEqualToRef();
|
||||
}
|
||||
|
||||
/**
|
||||
* 以低压缩方式读取当前图层
|
||||
* @param save 图层存档
|
||||
*/
|
||||
private loadLowCompression(save: IMapLayerSave): void {
|
||||
if (save.fullMap) {
|
||||
this.setMapRef(new Uint32Array(save.fullMap));
|
||||
this.layerDirty = true;
|
||||
} else if (this.refArray) {
|
||||
this.setMapRef(new Uint32Array(this.refArray));
|
||||
this.layerDirty = false;
|
||||
} else {
|
||||
logger.warn(124, this.zIndex.toString());
|
||||
}
|
||||
|
||||
if (save.staticBlocks) {
|
||||
this.loadStatics(save.staticBlocks);
|
||||
}
|
||||
if (save.dynamicBlocks) {
|
||||
this.loadDynamics(save.dynamicBlocks);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 以高压缩方式读取当前图层
|
||||
* @param save 图层存档
|
||||
*/
|
||||
private loadHighCompression(save: IMapLayerSave): void {
|
||||
if (save.rows && save.rows.size > 0) {
|
||||
if (this.refArray) {
|
||||
const buf = new Uint32Array(this.refArray);
|
||||
for (const [rowIdx, rowData] of save.rows) {
|
||||
buf.set(rowData, rowIdx * this.width);
|
||||
}
|
||||
this.setMapRef(buf);
|
||||
this.layerDirty = true;
|
||||
} else {
|
||||
logger.warn(124, this.zIndex.toString());
|
||||
}
|
||||
} else if (this.refArray) {
|
||||
this.setMapRef(new Uint32Array(this.refArray));
|
||||
this.layerDirty = false;
|
||||
} else {
|
||||
logger.warn(124, this.zIndex.toString());
|
||||
}
|
||||
|
||||
if (save.staticBlocks) {
|
||||
this.loadStatics(save.staticBlocks);
|
||||
}
|
||||
if (save.dynamicBlocks) {
|
||||
this.loadDynamics(save.dynamicBlocks);
|
||||
}
|
||||
}
|
||||
|
||||
loadState(save: IMapLayerSave, compression: SaveCompression): void {
|
||||
if (compression === SaveCompression.HighCompression) {
|
||||
this.loadHighCompression(save);
|
||||
} else if (compression === SaveCompression.LowCompression) {
|
||||
this.loadLowCompression(save);
|
||||
} else {
|
||||
this.loadNoCompression(save);
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
class MapLayerHookController
|
||||
|
||||
@ -1,27 +1,25 @@
|
||||
import { logger } from '@motajs/common';
|
||||
import { isNil, uniq } from 'lodash-es';
|
||||
import { IDataCommon, IMapRawData, SaveCompression } from '@user/data-common';
|
||||
import { ITileStore } from '@user/data-common';
|
||||
import {
|
||||
ILayerState,
|
||||
ILayerStateSave,
|
||||
IMapLayer,
|
||||
IMapLayerSave,
|
||||
IGameMap,
|
||||
IGameMapSave,
|
||||
IMapState,
|
||||
IMapStoreSave,
|
||||
MapArea
|
||||
} from './types';
|
||||
import { LayerState } from './layerState';
|
||||
import { isNil, uniq } from 'lodash-es';
|
||||
import { GameMap } from './gameMap';
|
||||
import { logger } from '@motajs/common';
|
||||
|
||||
export class MapState implements IMapState {
|
||||
/** 楼层 id 到状态对象的映射 */
|
||||
private readonly mapData: Map<string, LayerState> = new Map();
|
||||
private readonly mapData: Map<string, IGameMap> = new Map();
|
||||
|
||||
/** 所有楼层 id 的有序数组 */
|
||||
readonly maps: string[] = [];
|
||||
|
||||
/** 差分压缩参考基准,首次 compareWith 后设置,之后不再更新 */
|
||||
private refData: Map<string, Map<number, Uint32Array>> | null = null;
|
||||
/** 是否已经调用过 compareWith 设置参考基准 */
|
||||
private compared: boolean = false;
|
||||
|
||||
/** 分区列表 */
|
||||
private areaList: Set<MapArea> = new Set();
|
||||
@ -39,7 +37,7 @@ export class MapState implements IMapState {
|
||||
|
||||
//#region 楼层管理
|
||||
|
||||
fromRaw(raw: IMapRawData): ILayerState | null {
|
||||
fromRaw(raw: IMapRawData): IGameMap | null {
|
||||
let length = 0;
|
||||
const entries = Object.entries(raw.map);
|
||||
for (const [_, map] of Object.entries(raw.map)) {
|
||||
@ -57,7 +55,7 @@ export class MapState implements IMapState {
|
||||
|
||||
// 设置楼层图块数据
|
||||
const height = length / raw.width;
|
||||
const state = this.createLayerState(raw.floorId, raw.width, height);
|
||||
const state = this.createMap(raw.floorId, raw.width, height);
|
||||
for (const [zIndex, map] of entries) {
|
||||
const z = Number(zIndex);
|
||||
if (isNaN(z)) {
|
||||
@ -81,7 +79,10 @@ export class MapState implements IMapState {
|
||||
if (!isNil(data.trigger)) {
|
||||
const x = indexNum % raw.width;
|
||||
const y = Math.floor(indexNum / raw.width);
|
||||
layer.setTriggerType(data.trigger, x, y);
|
||||
const location = layer.getLocationData(x, y);
|
||||
if (location) {
|
||||
location.static.addTrigger(data.trigger);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -89,16 +90,16 @@ export class MapState implements IMapState {
|
||||
return state;
|
||||
}
|
||||
|
||||
createLayerState(id: string, width: number, height: number): ILayerState {
|
||||
createMap(id: string, width: number, height: number): IGameMap {
|
||||
if (this.mapData.has(id)) {
|
||||
logger.warn(121, id);
|
||||
} else {
|
||||
this.maps.push(id);
|
||||
}
|
||||
const state = new LayerState(this.state, this.tileStore, width, height);
|
||||
// 若 refData 已存在,新楼层直接视为全脏
|
||||
if (this.refData !== null) {
|
||||
state.setDirty(true);
|
||||
const state = new GameMap(this.state, this.tileStore, width, height);
|
||||
// 若已设置参考基准,新楼层直接视为全脏
|
||||
if (this.compared) {
|
||||
state.markDirty(true);
|
||||
}
|
||||
this.mapData.set(id, state);
|
||||
return state;
|
||||
@ -109,24 +110,11 @@ export class MapState implements IMapState {
|
||||
this.maps.push(...uniq(maps));
|
||||
}
|
||||
|
||||
useManualOrder(sort: (arr: string[]) => string[]): void {
|
||||
const copy = this.maps.slice();
|
||||
const sorted = sort(copy);
|
||||
const oldSet = new Set(this.maps);
|
||||
const newSet = new Set(sorted);
|
||||
if (oldSet.size !== newSet.size || !newSet.isSubsetOf(oldSet)) {
|
||||
logger.warn(125);
|
||||
return;
|
||||
}
|
||||
this.maps.length = 0;
|
||||
this.maps.push(...uniq(sorted));
|
||||
}
|
||||
|
||||
getLayerState(id: string): ILayerState | null {
|
||||
getMap(id: string): IGameMap | null {
|
||||
return this.mapData.get(id) ?? null;
|
||||
}
|
||||
|
||||
getActiveMap(id: string): ILayerState | null {
|
||||
getActiveMap(id: string): IGameMap | null {
|
||||
const state = this.mapData.get(id);
|
||||
if (!state || !state.active) return null;
|
||||
return state;
|
||||
@ -216,411 +204,66 @@ export class MapState implements IMapState {
|
||||
this.mapData.get(id)?.setActiveStatus(active);
|
||||
}
|
||||
|
||||
*iterateActiveMaps(): Iterable<[string, ILayerState]> {
|
||||
*iterateActiveMaps(): Iterable<[string, IGameMap]> {
|
||||
for (const [id, state] of this.mapData) {
|
||||
if (state.active) yield [id, state];
|
||||
}
|
||||
}
|
||||
|
||||
*iterateInactiveMaps(): Iterable<[string, ILayerState]> {
|
||||
*iterateInactiveMaps(): Iterable<[string, IGameMap]> {
|
||||
for (const [id, state] of this.mapData) {
|
||||
if (!state.active) yield [id, state];
|
||||
}
|
||||
}
|
||||
|
||||
iterateAllMaps(): Iterable<[string, ILayerState]> {
|
||||
iterateAllMaps(): Iterable<[string, IGameMap]> {
|
||||
return this.mapData;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 存档工具
|
||||
//#region 存档对比
|
||||
|
||||
compareWith(ref: Map<string, Map<number, Uint32Array>>): void {
|
||||
if (this.refData !== null) return;
|
||||
this.refData = ref;
|
||||
if (this.compared) return;
|
||||
this.compared = true;
|
||||
|
||||
for (const [id, state] of this.mapData) {
|
||||
const refFloor = ref.get(id);
|
||||
if (!refFloor) {
|
||||
state.setDirty(true);
|
||||
state.markDirty(true);
|
||||
continue;
|
||||
}
|
||||
let dirty = false;
|
||||
for (const layer of state.layerList) {
|
||||
const refArray = refFloor.get(layer.zIndex);
|
||||
if (!refArray) {
|
||||
dirty = true;
|
||||
break;
|
||||
}
|
||||
const cur = layer.getMapRef().array;
|
||||
if (cur.length !== refArray.length) {
|
||||
dirty = true;
|
||||
break;
|
||||
}
|
||||
if (cur.some((v, i) => refArray[i] !== v)) {
|
||||
dirty = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
state.setDirty(dirty);
|
||||
state.compareWith(refFloor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取楼层内所有图层的静态触发器覆盖映射
|
||||
* @param state 目标楼层状态
|
||||
*/
|
||||
private getTriggerMap(state: ILayerState) {
|
||||
const triggers = new Map<number, Map<number, number>>();
|
||||
for (const layer of state.layerList) {
|
||||
const map = layer.getTriggerRef();
|
||||
if (map.size > 0) {
|
||||
triggers.set(layer.zIndex, new Map(map));
|
||||
}
|
||||
}
|
||||
return triggers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造仅包含背景和静态触发器的空楼层存档
|
||||
* @param state 目标楼层状态
|
||||
*/
|
||||
private emptySave(state: ILayerState): ILayerStateSave {
|
||||
return {
|
||||
background: state.getBackground(),
|
||||
layers: new Map(),
|
||||
triggers: this.getTriggerMap(state)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 将单个图层序列化为完整地图存档
|
||||
* @param layer 目标图层
|
||||
*/
|
||||
private fullLayer(layer: IMapLayer): IMapLayerSave {
|
||||
return {
|
||||
width: layer.width,
|
||||
height: layer.height,
|
||||
fullMap: new Uint32Array(layer.getMapRef().array)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 将楼层所有图层全量序列化(NoCompression / LowCompression 用)
|
||||
* @param state 目标楼层状态
|
||||
*/
|
||||
private saveLayerStateFull(state: LayerState): ILayerStateSave {
|
||||
const background = state.getBackground();
|
||||
const layers = new Map<number, IMapLayerSave>();
|
||||
const triggers = new Map<number, Map<number, number>>();
|
||||
for (const layer of state.layerList) {
|
||||
const arr = layer.getMapRef().array;
|
||||
layers.set(layer.zIndex, {
|
||||
width: layer.width,
|
||||
height: layer.height,
|
||||
fullMap: new Uint32Array(arr)
|
||||
});
|
||||
triggers.set(layer.zIndex, new Map(layer.getTriggerRef()));
|
||||
}
|
||||
return { background, layers, triggers };
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅返回与参考基准不同的行(HighCompression 用)
|
||||
* @param layer 目标图层
|
||||
* @param refArray 该图层对应的参考地图数据
|
||||
*/
|
||||
private diffRows(
|
||||
layer: IMapLayer,
|
||||
refArray: Uint32Array
|
||||
): Map<number, Uint32Array> {
|
||||
const rows = new Map<number, Uint32Array>();
|
||||
const arr = layer.getMapRef().array;
|
||||
|
||||
for (let row = 0; row < layer.height; row++) {
|
||||
const start = row * layer.width;
|
||||
const end = start + layer.width;
|
||||
const slice = arr.subarray(start, end);
|
||||
const refSlice = refArray.subarray(start, end);
|
||||
const same = refSlice.every((v, i) => slice[i] === v);
|
||||
if (!same) {
|
||||
rows.set(row, new Uint32Array(slice));
|
||||
}
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断楼层所有图层是否与参考基准完全一致(LowCompression 去误判用)
|
||||
* @param id 楼层 id
|
||||
* @param state 目标楼层状态
|
||||
*/
|
||||
private isStateEqualToRef(id: string, state: LayerState): boolean {
|
||||
const refFloor = this.refData?.get(id);
|
||||
if (!refFloor) return false;
|
||||
for (const layer of state.layerList) {
|
||||
const refArray = refFloor.get(layer.zIndex);
|
||||
if (!refArray) return false;
|
||||
const cur = layer.getMapRef().array;
|
||||
if (cur.length !== refArray.length) return false;
|
||||
if (cur.some((v, i) => v !== refArray[i])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 存档
|
||||
|
||||
/**
|
||||
* 以无压缩方式序列化所有激活楼层
|
||||
*/
|
||||
private saveNoCompression(): IMapStoreSave {
|
||||
const floors = new Map<string, ILayerStateSave>();
|
||||
for (const [id, state] of this.mapData) {
|
||||
if (!state.active) continue;
|
||||
floors.set(id, this.saveLayerStateFull(state));
|
||||
}
|
||||
return { floors };
|
||||
}
|
||||
|
||||
/**
|
||||
* 以低压缩方式序列化所有激活楼层
|
||||
*/
|
||||
private saveLowCompression(): IMapStoreSave {
|
||||
const floors = new Map<string, ILayerStateSave>();
|
||||
if (this.refData) {
|
||||
// 包含参考标准时需要对比
|
||||
for (const [id, state] of this.mapData) {
|
||||
if (!state.active) continue;
|
||||
if (state.isDirty() && this.isStateEqualToRef(id, state)) {
|
||||
floors.set(id, this.saveLayerStateFull(state));
|
||||
} else {
|
||||
floors.set(id, this.emptySave(state));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 不包含参考标准时仅看 dirty 标记
|
||||
for (const [id, state] of this.mapData) {
|
||||
if (!state.active) continue;
|
||||
if (state.isDirty()) {
|
||||
floors.set(id, this.saveLayerStateFull(state));
|
||||
} else {
|
||||
floors.set(id, this.emptySave(state));
|
||||
}
|
||||
}
|
||||
}
|
||||
return { floors };
|
||||
}
|
||||
|
||||
/**
|
||||
* 以高压缩方式序列化所有激活楼层
|
||||
*/
|
||||
private saveHighCompression(): IMapStoreSave {
|
||||
const floors = new Map<string, ILayerStateSave>();
|
||||
// 没有参考标准,直接退回低压缩级别
|
||||
if (!this.refData) return this.saveLowCompression();
|
||||
|
||||
for (const [id, state] of this.mapData) {
|
||||
if (!state.active) continue;
|
||||
if (state.isDirty()) {
|
||||
const refFloor = this.refData.get(id);
|
||||
const layersMap = new Map<number, IMapLayerSave>();
|
||||
// 对每一个地图的每一行进行遍历,然后仅存储有差别的行
|
||||
for (const layer of state.layerList) {
|
||||
const refArray = refFloor?.get(layer.zIndex);
|
||||
if (refArray) {
|
||||
const rows = this.diffRows(layer, refArray);
|
||||
if (rows.size === 0) continue;
|
||||
layersMap.set(layer.zIndex, {
|
||||
width: layer.width,
|
||||
height: layer.height,
|
||||
rows
|
||||
});
|
||||
} else {
|
||||
layersMap.set(layer.zIndex, this.fullLayer(layer));
|
||||
}
|
||||
}
|
||||
floors.set(id, {
|
||||
background: state.getBackground(),
|
||||
layers: layersMap,
|
||||
triggers: this.getTriggerMap(state)
|
||||
});
|
||||
} else {
|
||||
floors.set(id, this.emptySave(state));
|
||||
}
|
||||
}
|
||||
|
||||
return { floors };
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载指定图层的触发器
|
||||
* @param save 楼层存档数据
|
||||
* @param layer 目标图层
|
||||
*/
|
||||
private loadTriggers(save: ILayerStateSave, layer: IMapLayer) {
|
||||
const triggers = save.triggers.get(layer.zIndex);
|
||||
layer.clearTrigger();
|
||||
if (triggers) {
|
||||
layer.setTriggerRef(new Map(triggers));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NoCompression 读档:每个图层均有 fullMap,直接转移所有权,无需参考基准。
|
||||
* @param state 整体地图存档数据
|
||||
*/
|
||||
private loadNoCompression(state: IMapStoreSave): void {
|
||||
for (const [id, cur] of this.mapData) {
|
||||
cur.setActiveStatus(state.floors.has(id));
|
||||
}
|
||||
for (const [id, save] of state.floors) {
|
||||
const cur = this.mapData.get(id);
|
||||
if (!cur) {
|
||||
logger.warn(122, id);
|
||||
continue;
|
||||
}
|
||||
cur.setBackground(save.background);
|
||||
for (const layer of cur.layerList) {
|
||||
// 地图
|
||||
const layerSave = save.layers.get(layer.zIndex);
|
||||
if (layerSave?.fullMap) {
|
||||
layer.setMapRef(new Uint32Array(layerSave.fullMap));
|
||||
}
|
||||
// 触发器
|
||||
this.loadTriggers(save, layer);
|
||||
}
|
||||
// 需要额外进行判断是否与参考地图相同
|
||||
if (this.isStateEqualToRef(id, cur)) {
|
||||
cur.setDirty(false);
|
||||
} else {
|
||||
cur.setDirty(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LowCompression 读档:
|
||||
* - layers 有数据(dirty 楼层)→ fullMap 直接转移所有权
|
||||
* - layers 为空(非 dirty 楼层)→ 从参考基准恢复
|
||||
* @param state 整体地图存档数据
|
||||
*/
|
||||
private loadLowCompression(state: IMapStoreSave): void {
|
||||
if (!this.refData) {
|
||||
logger.error(55);
|
||||
return;
|
||||
}
|
||||
for (const [id, cur] of this.mapData) {
|
||||
cur.setActiveStatus(state.floors.has(id));
|
||||
}
|
||||
for (const [id, save] of state.floors) {
|
||||
const cur = this.mapData.get(id);
|
||||
const refFloor = this.refData.get(id);
|
||||
if (!cur) {
|
||||
logger.warn(122, id);
|
||||
continue;
|
||||
}
|
||||
if (!refFloor) {
|
||||
logger.warn(124, id);
|
||||
continue;
|
||||
}
|
||||
cur.setBackground(save.background);
|
||||
let shouldDirty = false;
|
||||
for (const layer of cur.layerList) {
|
||||
// 地图
|
||||
const layerSave = save.layers.get(layer.zIndex);
|
||||
if (layerSave?.fullMap) {
|
||||
layer.setMapRef(new Uint32Array(layerSave.fullMap));
|
||||
shouldDirty = true;
|
||||
} else {
|
||||
const refArray = refFloor.get(layer.zIndex);
|
||||
if (!refArray) {
|
||||
logger.warn(124, id);
|
||||
return;
|
||||
}
|
||||
layer.setMapRef(new Uint32Array(refArray));
|
||||
}
|
||||
// 触发器
|
||||
this.loadTriggers(save, layer);
|
||||
}
|
||||
cur.setDirty(shouldDirty);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HighCompression 读档:
|
||||
* - layers 有数据(dirty 楼层)→ 以参考基准为底,叠加差分行
|
||||
* - layers 为空(非 dirty 楼层)或图层无变化(rows 缺失)→ 从参考基准恢复
|
||||
* @param state 整体地图存档数据
|
||||
*/
|
||||
private loadHighCompression(state: IMapStoreSave): void {
|
||||
if (!this.refData) {
|
||||
logger.error(55);
|
||||
return;
|
||||
}
|
||||
for (const [id, cur] of this.mapData) {
|
||||
cur.setActiveStatus(state.floors.has(id));
|
||||
}
|
||||
for (const [id, save] of state.floors) {
|
||||
const cur = this.mapData.get(id);
|
||||
const refFloor = this.refData.get(id);
|
||||
if (!cur) {
|
||||
logger.warn(122, id);
|
||||
continue;
|
||||
}
|
||||
if (!refFloor) {
|
||||
logger.warn(124, id);
|
||||
continue;
|
||||
}
|
||||
cur.setBackground(save.background);
|
||||
let shouldDirty = false;
|
||||
for (const layer of cur.layerList) {
|
||||
const refArray = refFloor.get(layer.zIndex);
|
||||
if (!refArray) {
|
||||
logger.warn(124, id);
|
||||
continue;
|
||||
}
|
||||
// 地图
|
||||
const layerSave = save.layers.get(layer.zIndex);
|
||||
if (!layerSave?.rows || layerSave.rows.size === 0) {
|
||||
// 图层无变化或非 dirty 楼层,从参考基准恢复
|
||||
layer.setMapRef(new Uint32Array(refArray));
|
||||
} else {
|
||||
// 以参考基准为底,叠加差分行
|
||||
shouldDirty = true;
|
||||
const buf = new Uint32Array(refArray);
|
||||
for (const [rowIdx, rowData] of layerSave.rows) {
|
||||
buf.set(rowData, rowIdx * layer.width);
|
||||
}
|
||||
layer.setMapRef(buf);
|
||||
}
|
||||
// 触发器
|
||||
this.loadTriggers(save, layer);
|
||||
}
|
||||
cur.setDirty(shouldDirty);
|
||||
}
|
||||
}
|
||||
//#region 存读档
|
||||
|
||||
saveState(compression: SaveCompression): IMapStoreSave {
|
||||
if (compression === SaveCompression.HighCompression) {
|
||||
return this.saveHighCompression();
|
||||
} else if (compression === SaveCompression.LowCompression) {
|
||||
return this.saveLowCompression();
|
||||
} else {
|
||||
return this.saveNoCompression();
|
||||
const floors = new Map<string, IGameMapSave>();
|
||||
for (const [id, state] of this.mapData) {
|
||||
if (!state.active) continue;
|
||||
floors.set(id, state.saveState(compression));
|
||||
}
|
||||
return { floors };
|
||||
}
|
||||
|
||||
loadState(state: IMapStoreSave, compression: SaveCompression): void {
|
||||
if (compression === SaveCompression.HighCompression) {
|
||||
this.loadHighCompression(state);
|
||||
} else if (compression === SaveCompression.LowCompression) {
|
||||
this.loadLowCompression(state);
|
||||
} else {
|
||||
this.loadNoCompression(state);
|
||||
if (compression !== SaveCompression.NoCompression && !this.compared) {
|
||||
logger.error(55);
|
||||
return;
|
||||
}
|
||||
for (const [id, cur] of this.mapData) {
|
||||
cur.setActiveStatus(state.floors.has(id));
|
||||
}
|
||||
for (const [id, save] of state.floors) {
|
||||
const cur = this.mapData.get(id);
|
||||
if (!cur) {
|
||||
logger.warn(122, id);
|
||||
continue;
|
||||
}
|
||||
cur.loadState(save, compression);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { ITileLocator, logger } from '@motajs/common';
|
||||
import {
|
||||
getFaceMovement,
|
||||
IMoverController,
|
||||
ObjectMover,
|
||||
ObjectMoveStep,
|
||||
ObjectMoveType
|
||||
@ -9,8 +8,6 @@ import {
|
||||
import { IDynamicTile } from './types';
|
||||
import { DYNAMIC_MOVER_FACE } from '../shared';
|
||||
|
||||
//#region 动态图块
|
||||
|
||||
const enum DynamicMoveCode {
|
||||
/** 正常执行 */
|
||||
Success
|
||||
@ -94,5 +91,3 @@ export class DynamicTileMover extends ObjectMover<IDynamicTile> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
59
packages-user/data-base/src/map/staticTile.ts
Normal file
59
packages-user/data-base/src/map/staticTile.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { ITileRawData } from '@user/data-common';
|
||||
import {
|
||||
IDynamicTile,
|
||||
IMapLayer,
|
||||
IStaticBlockSave,
|
||||
IStaticTile
|
||||
} from './types';
|
||||
import { MapTileBase } from './tile';
|
||||
|
||||
export class StaticTile
|
||||
extends MapTileBase<IStaticBlockSave>
|
||||
implements IStaticTile
|
||||
{
|
||||
constructor(x: number, y: number, layer: IMapLayer) {
|
||||
super(x, y, layer);
|
||||
}
|
||||
|
||||
num(): number {
|
||||
return this.layer.getBlock(this.locator.x, this.locator.y);
|
||||
}
|
||||
|
||||
raw(): ITileRawData | null {
|
||||
return this.state.tileStore.getData(this.num());
|
||||
}
|
||||
|
||||
set(num: number): void {
|
||||
this.layer.setBlock(num, this.locator.x, this.locator.y);
|
||||
}
|
||||
|
||||
toDynamic(): IDynamicTile {
|
||||
return this.layer.transferToDynamic(this.locator.x, this.locator.y)!;
|
||||
}
|
||||
|
||||
shouldSave(): boolean {
|
||||
return !!this.triggers;
|
||||
}
|
||||
|
||||
saveState(): Readonly<IStaticBlockSave> {
|
||||
const save: IStaticBlockSave = {};
|
||||
|
||||
if (this.triggers) {
|
||||
save.triggers = this.triggers;
|
||||
}
|
||||
|
||||
return save;
|
||||
}
|
||||
|
||||
loadState(save: Readonly<IStaticBlockSave>): void {
|
||||
if (save.triggers) {
|
||||
if (save.triggers.size === 0) {
|
||||
this.useEmptyTrigger();
|
||||
} else {
|
||||
save.triggers.forEach(v => this.addTrigger(v));
|
||||
}
|
||||
} else {
|
||||
this.clearTrigger();
|
||||
}
|
||||
}
|
||||
}
|
||||
70
packages-user/data-base/src/map/tile.ts
Normal file
70
packages-user/data-base/src/map/tile.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import { ITileLocator } from '@motajs/common';
|
||||
import {
|
||||
FaceDirection,
|
||||
IDataCommon,
|
||||
IDataCommonExtended,
|
||||
IMapBlockRawData,
|
||||
ISaveableContent,
|
||||
ITileRawData
|
||||
} from '@user/data-common';
|
||||
import { IMapBlockSaveBase, IMapLayer, ITileBase } from './types';
|
||||
|
||||
export abstract class MapTileBase<TSave extends IMapBlockSaveBase>
|
||||
implements ITileBase, IDataCommonExtended, ISaveableContent<Readonly<TSave>>
|
||||
{
|
||||
readonly state: IDataCommon;
|
||||
readonly layer: IMapLayer;
|
||||
locator: ITileLocator;
|
||||
triggers: Set<number> | null = null;
|
||||
|
||||
constructor(x: number, y: number, layer: IMapLayer) {
|
||||
this.layer = layer;
|
||||
this.state = layer.state;
|
||||
this.locator = { x, y };
|
||||
}
|
||||
|
||||
abstract num(): number;
|
||||
|
||||
abstract raw(): ITileRawData | null;
|
||||
|
||||
abstract set(num: number): void;
|
||||
|
||||
block(): IMapBlockRawData | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
setFaceDirection(direction: FaceDirection): number {
|
||||
const cur = this.num();
|
||||
const next = this.layer.faceBinder.getFaceOf(cur, direction);
|
||||
if (next) {
|
||||
this.set(next.identifier);
|
||||
return next.identifier;
|
||||
} else {
|
||||
return cur;
|
||||
}
|
||||
}
|
||||
|
||||
clearTrigger(): void {
|
||||
this.triggers = null;
|
||||
}
|
||||
|
||||
addTrigger(trigger: number): void {
|
||||
if (!this.triggers) {
|
||||
this.triggers = new Set();
|
||||
}
|
||||
this.triggers.add(trigger);
|
||||
}
|
||||
|
||||
deleteTrigger(trigger: number): void {
|
||||
if (!this.triggers) return;
|
||||
this.triggers.delete(trigger);
|
||||
}
|
||||
|
||||
useEmptyTrigger(): void {
|
||||
this.triggers = new Set();
|
||||
}
|
||||
|
||||
abstract saveState(): Readonly<TSave>;
|
||||
|
||||
abstract loadState(save: Readonly<TSave>): void;
|
||||
}
|
||||
@ -7,6 +7,7 @@ import {
|
||||
import {
|
||||
FaceDirection,
|
||||
IDataCommonExtended,
|
||||
ILocationHelper,
|
||||
IMapBlockRawData,
|
||||
IMapRawData,
|
||||
IMoverController,
|
||||
@ -18,7 +19,135 @@ import {
|
||||
} from '@user/data-common';
|
||||
import { ITileStore } from '@user/data-common';
|
||||
|
||||
//#region 静态图层
|
||||
//#region 图块信息
|
||||
|
||||
export interface IMapBlockSaveBase {
|
||||
/** 此图块的触发器 */
|
||||
triggers?: ReadonlySet<number>;
|
||||
}
|
||||
|
||||
export interface IStaticBlockSave extends IMapBlockSaveBase {}
|
||||
|
||||
export interface IDynamicBlockSave extends IMapBlockSaveBase {
|
||||
/** 此动态图块的图块数字 */
|
||||
num: number;
|
||||
}
|
||||
|
||||
export interface ITileBase {
|
||||
/** 该图块所处的位置 */
|
||||
readonly locator: ITileLocator;
|
||||
/** 该图块所处的图层对象 */
|
||||
readonly layer: IMapLayer;
|
||||
/** 包含的触发器列表,不存在时表示使用图块本身的触发器 */
|
||||
readonly triggers: ReadonlySet<number> | null;
|
||||
|
||||
/**
|
||||
* 获取该图块的原始图块信息
|
||||
*/
|
||||
raw(): ITileRawData | null;
|
||||
|
||||
/**
|
||||
* 获取该图块的原始点位信息
|
||||
*/
|
||||
block(): IMapBlockRawData | null;
|
||||
|
||||
/**
|
||||
* 获取该图块的图块数字
|
||||
*/
|
||||
num(): number;
|
||||
|
||||
/**
|
||||
* 设置此图块为指定图块
|
||||
* @param num 图块数字
|
||||
*/
|
||||
set(num: number): void;
|
||||
|
||||
/**
|
||||
* 设置图块朝向,会一并修改 {@link num},返回设置后的当前图块数字
|
||||
* @param direction 图块朝向
|
||||
*/
|
||||
setFaceDirection(direction: FaceDirection): number;
|
||||
|
||||
/**
|
||||
* 清空此图块实例的触发器,回退为图块本身的触发器
|
||||
*/
|
||||
clearTrigger(): void;
|
||||
|
||||
/**
|
||||
* 在此图块实例上添加触发器,当图块实例包含触发器时,将会覆盖图块本身的触发器
|
||||
* @param trigger 触发器数字
|
||||
*/
|
||||
addTrigger(trigger: number): void;
|
||||
|
||||
/**
|
||||
* 删除此图块实例上添加的触发器
|
||||
* @param trigger 触发器数字
|
||||
*/
|
||||
deleteTrigger(trigger: number): void;
|
||||
|
||||
/**
|
||||
* 将此图块实例的触发器设置为无触发器,并覆盖图块本身的触发器。若图块本身包含触发器,此方法可以将其禁用
|
||||
*/
|
||||
useEmptyTrigger(): void;
|
||||
}
|
||||
|
||||
export interface IStaticTile
|
||||
extends
|
||||
ITileBase,
|
||||
IDataCommonExtended,
|
||||
ISaveableContent<Readonly<IStaticBlockSave>> {
|
||||
/**
|
||||
* 将此静态图块转换为动态图块
|
||||
*/
|
||||
toDynamic(): IDynamicTile;
|
||||
|
||||
/**
|
||||
* 判断当前静态图块是否需要存档
|
||||
*/
|
||||
shouldSave(): boolean;
|
||||
}
|
||||
|
||||
export interface IDynamicTile
|
||||
extends
|
||||
ITileBase,
|
||||
IObjectMovable,
|
||||
IDataCommonExtended,
|
||||
ISaveableContent<Readonly<IDynamicBlockSave>> {
|
||||
/** 该动态图块的移动器 */
|
||||
readonly mover: IObjectMover<IDynamicTile>;
|
||||
/** 该动态图块所属的动态图层 */
|
||||
readonly layer: IMapLayer;
|
||||
|
||||
/**
|
||||
* 将该动态图块转换为静态图块,如果该动态图块的坐标不为整数或正在移动,那么会转换失败并返回 `null`。
|
||||
* 如果目标点有静态图块,那么会将其覆盖。
|
||||
*/
|
||||
toStatic(): IStaticTile | null;
|
||||
|
||||
/**
|
||||
* 将该动态图块转换为静态图块,如果该动态图块的坐标不为整数或正在移动,或目标点不为空,那么会转换失败并返回 `null`。
|
||||
* 即在 `toStatic` 的基础上添加了一个目标点是否为空的判断。
|
||||
*/
|
||||
toStaticIfSafe(): IStaticTile | null;
|
||||
|
||||
/**
|
||||
* 单步便捷移动接口,适用于简单移动场景,复杂路径通过 `tile.mover` 访问。
|
||||
* 等价于:
|
||||
*
|
||||
* ```ts
|
||||
* mover.step(dir, count);
|
||||
* return mover.start();
|
||||
* ```
|
||||
*/
|
||||
step(dir: FaceDirection, count?: number): IMoverController | null;
|
||||
|
||||
/**
|
||||
* 直接删除此图块
|
||||
*/
|
||||
delete(): Promise<void>;
|
||||
}
|
||||
|
||||
//#region 地图图层
|
||||
|
||||
export interface IMapLayerData {
|
||||
/** 当前引用是否过期,当地图图层内部的地图数组引用更新时,此项会变为 `true` */
|
||||
@ -32,14 +161,10 @@ export interface ILayerLocation {
|
||||
readonly locator: ITileLocator;
|
||||
/** 该点的静态图块数字 */
|
||||
readonly tile: number;
|
||||
/** 该点的静态图块信息 */
|
||||
readonly raw: ITileRawData | null;
|
||||
/** 该点的静态触发器,-1 表示未设置(即使用图块本身的触发器),否则表示该点的静态触发器,覆盖图块本身的触发器 */
|
||||
readonly trigger: number;
|
||||
/** 该点包含的所有动态图块 */
|
||||
readonly dynamics: Iterable<IDynamicTile>;
|
||||
/** 该点的静态图块原始数据 */
|
||||
readonly block: IMapBlockRawData;
|
||||
readonly static: IStaticTile;
|
||||
}
|
||||
|
||||
export interface IMapLayerHooks extends IHookBase {
|
||||
@ -51,7 +176,7 @@ export interface IMapLayerHooks extends IHookBase {
|
||||
onResize(width: number, height: number): void;
|
||||
|
||||
/**
|
||||
* 当更新某个区域的图块时执行
|
||||
* 当更新某个区域的图块时执行,对应于 `putMapData` 方法
|
||||
* @param x 更新区域左上角横坐标
|
||||
* @param y 更新区域左上角纵坐标
|
||||
* @param width 更新区域宽度
|
||||
@ -60,7 +185,7 @@ export interface IMapLayerHooks extends IHookBase {
|
||||
onUpdateArea(x: number, y: number, width: number, height: number): void;
|
||||
|
||||
/**
|
||||
* 当更新某个点的图块时执行,如果设置的图块与原先一样,则不会触发此方法
|
||||
* 当更新某个点的图块时执行,如果设置的图块与原先一样,则不会触发此方法,对应于 `setBlock` 方法
|
||||
* @param block 更新为的图块数字
|
||||
* @param x 更新点横坐标
|
||||
* @param y 更新点纵坐标
|
||||
@ -81,6 +206,24 @@ export interface IMapLayerHooks extends IHookBase {
|
||||
* @param y 门纵坐标
|
||||
*/
|
||||
onCloseDoor(num: number, x: number, y: number): Promise<void>;
|
||||
|
||||
/**
|
||||
* 当动态图块被创建时触发,包括从静态图块转换为动态图块
|
||||
* @param tile 被创建的动态图块
|
||||
*/
|
||||
onCreateDynamic?(tile: IDynamicTile): void;
|
||||
|
||||
/**
|
||||
* 当动态图块被删除时触发
|
||||
* @param tile 被删除的动态图块
|
||||
*/
|
||||
onDeleteDynamic?(tile: IDynamicTile): Promise<void>;
|
||||
|
||||
/**
|
||||
* 当更新动态图块的位置时触发
|
||||
* @param tile 更新位置的图块
|
||||
*/
|
||||
onUpdateDynamicPosition?(tile: IDynamicTile): void;
|
||||
}
|
||||
|
||||
export interface IMapLayerHookController extends IHookController<IMapLayerHooks> {
|
||||
@ -93,36 +236,9 @@ export interface IMapLayerHookController extends IHookController<IMapLayerHooks>
|
||||
getMapData(): Readonly<IMapLayerData>;
|
||||
}
|
||||
|
||||
export interface IMapLayer
|
||||
extends
|
||||
IHookable<IMapLayerHooks, IMapLayerHookController>,
|
||||
IDataCommonExtended {
|
||||
/** 地图宽度 */
|
||||
readonly width: number;
|
||||
/** 地图高度 */
|
||||
readonly height: number;
|
||||
interface ILayerStatic {
|
||||
/**
|
||||
* 地图是否全部空白,此值具有充分性,但不具有必要性,
|
||||
* 即如果其为 `true`,则地图一定空白,但是如果其为 `false`,那么地图也有可能空白
|
||||
*/
|
||||
readonly empty: boolean;
|
||||
/** 图层纵深 */
|
||||
readonly zIndex: number;
|
||||
|
||||
/** 当前图层所属的地图状态对象 */
|
||||
readonly layerState: ILayerState;
|
||||
/** 此图层对应的动态图块图层,z 层级与静态图块一致 */
|
||||
readonly dynamicLayer: IDynamicLayer;
|
||||
|
||||
/**
|
||||
* 判断指定坐标是否在地图内
|
||||
* @param x 横坐标
|
||||
* @param y 纵坐标
|
||||
*/
|
||||
inMap(x: number, y: number): boolean;
|
||||
|
||||
/**
|
||||
* 设置某一点的图块
|
||||
* 设置某一点的图块,会标记图层为脏
|
||||
* @param block 图块数字
|
||||
* @param x 图块横坐标
|
||||
* @param y 图块纵坐标
|
||||
@ -130,7 +246,7 @@ export interface IMapLayer
|
||||
setBlock(block: number, x: number, y: number): void;
|
||||
|
||||
/**
|
||||
* 获取指定点的图块
|
||||
* 获取指定点的图块数字
|
||||
* @param x 图块横坐标
|
||||
* @param y 图块纵坐标
|
||||
* @returns 指定点的图块,如果没有图块,返回 0,如果不在地图上,返回 -1
|
||||
@ -138,41 +254,14 @@ export interface IMapLayer
|
||||
getBlock(x: number, y: number): number;
|
||||
|
||||
/**
|
||||
* 获取指定点的所有图块信息
|
||||
* @param x 横坐标
|
||||
* @param y 纵坐标
|
||||
*/
|
||||
getLocationData(x: number, y: number): ILayerLocation | null;
|
||||
|
||||
/**
|
||||
* 获取指定点的静态图块对应的有效触发器类型,若手动覆盖不存在则回退到图块默认触发器
|
||||
* 获取指定点的静态图块实例
|
||||
* @param x 图块横坐标
|
||||
* @param y 图块纵坐标
|
||||
*/
|
||||
getTriggerType(x: number, y: number): number;
|
||||
getTile(x: number, y: number): IStaticTile | null;
|
||||
|
||||
/**
|
||||
* 设置指定点静态图块的触发器
|
||||
* @param type 触发器类型
|
||||
* @param x 图块横坐标
|
||||
* @param y 图块纵坐标
|
||||
*/
|
||||
setTriggerType(type: number, x: number, y: number): void;
|
||||
|
||||
/**
|
||||
* 删除指定点静态图块的触发器,回退为图块默认触发器
|
||||
* @param x 图块横坐标
|
||||
* @param y 图块纵坐标
|
||||
*/
|
||||
revertTrigger(x: number, y: number): void;
|
||||
|
||||
/**
|
||||
* 清空地图上所有静态图块的手动设置的触发器,恢复为图块默认触发器
|
||||
*/
|
||||
clearTrigger(): void;
|
||||
|
||||
/**
|
||||
* 设置地图图块
|
||||
* 设置地图图块,会标记图层为脏
|
||||
* @param array 地图图块数组
|
||||
* @param x 数组第一项代表的横坐标
|
||||
* @param y 数组第一项代表的纵坐标
|
||||
@ -199,9 +288,8 @@ export interface IMapLayer
|
||||
): Uint32Array;
|
||||
|
||||
/**
|
||||
* 直接替换内部图块数组引用,跳过拷贝,高性能但风险较高。
|
||||
* 一般仅供 `MapStore` 读档时内部使用,外部正常情况下不应调用。
|
||||
* 调用方需确保传入数组的长度与 `width * height` 匹配,
|
||||
* 直接替换内部图块数组引用,跳过拷贝,高性能但风险较高。一般仅供内部使用,外部不需要调用。
|
||||
* 此操作不会标记图层为脏。调用方需确保传入数组的长度与 `width * height` 匹配,
|
||||
* 且调用后不得再持有或修改传入的数组。
|
||||
* @param array 地图数组,会直接替换内部引用
|
||||
*/
|
||||
@ -213,15 +301,170 @@ export interface IMapLayer
|
||||
getMapRef(): IMapLayerData;
|
||||
|
||||
/**
|
||||
* 直接设置内部触发器映射对象,一般仅供内部存读档使用,外部正常情况下不应调用
|
||||
* @param triggers 触发器映射
|
||||
* 设置静态图块的朝向
|
||||
* @param x 要设置的静态图块横坐标
|
||||
* @param y 要设置的静态图块纵坐标
|
||||
* @param direction 目标朝向
|
||||
* @returns 更新朝向后该静态图块的图块数字
|
||||
*/
|
||||
setTriggerRef(triggers: Map<number, number>): void;
|
||||
setStaticDirection(x: number, y: number, direction: FaceDirection): number;
|
||||
|
||||
/**
|
||||
* 获取静态触发器覆盖映射,一般仅供内部存档逻辑使用
|
||||
* 迭代所有的图块
|
||||
*/
|
||||
getTriggerRef(): ReadonlyMap<number, number>;
|
||||
iterateBlocks(): Iterable<ILayerLocation>;
|
||||
}
|
||||
|
||||
interface ILayerDynamic {
|
||||
/**
|
||||
* 在指定位置创建一个动态图块
|
||||
* @param num 图块数字
|
||||
* @param x 横坐标
|
||||
* @param y 纵坐标
|
||||
*/
|
||||
createDynamic(num: number, x: number, y: number): IDynamicTile;
|
||||
|
||||
/**
|
||||
* 从静态图层读取并清除指定位置的图块,创建对应动态图块并返回
|
||||
* @param x 横坐标
|
||||
* @param y 纵坐标
|
||||
* @param keepTrigger 是否将静态图块的触发器保留至动态图块,不论是否保留,此格的静态触发器都会被清空
|
||||
*/
|
||||
transferToDynamic(
|
||||
x: number,
|
||||
y: number,
|
||||
keepTrigger?: boolean
|
||||
): IDynamicTile | null;
|
||||
|
||||
/**
|
||||
* 将动态图块还原为静态图块
|
||||
* @param tile 要还原的动态图块
|
||||
* @param keepTrigger 是否保留动态图块的触发器至静态图块,若不保留,那么此格将回退为静态图块本身的触发器
|
||||
*/
|
||||
transferToStatic(
|
||||
tile: IDynamicTile,
|
||||
keepTrigger?: boolean
|
||||
): IStaticTile | null;
|
||||
|
||||
/**
|
||||
* 仅当目标位置不存在静态图块时才还原为静态图块,否则不转换
|
||||
* @param tile 要还原的动态图块
|
||||
* @param keepTrigger 是否保留动态图块的触发器至静态图块,若不保留,那么此格将回退为静态图块本身的触发器
|
||||
*/
|
||||
transferToStaticIfSafe(
|
||||
tile: IDynamicTile,
|
||||
keepTrigger?: boolean
|
||||
): IStaticTile | null;
|
||||
|
||||
/**
|
||||
* 删除指定动态图块,当删除完成后兑现返回值
|
||||
* @param tile 要删除的动态图块
|
||||
*/
|
||||
deleteDynamic(tile: IDynamicTile): Promise<void>;
|
||||
|
||||
/**
|
||||
* 获取指定格点上所有动态图块的可迭代对象
|
||||
* @param x 横坐标
|
||||
* @param y 纵坐标
|
||||
*/
|
||||
getDynamicTilesAt(x: number, y: number): Iterable<IDynamicTile>;
|
||||
|
||||
/**
|
||||
* 迭代所有的动态图块
|
||||
*/
|
||||
iterateDynamicTiles(): Iterable<IDynamicTile>;
|
||||
|
||||
/**
|
||||
* 设置动态图块的朝向
|
||||
* @param tile 要设置朝向的动态图块
|
||||
* @param direction 目标朝向
|
||||
* @returns 更新朝向后该动态图块的图块数字
|
||||
*/
|
||||
setDynamicDirection(tile: IDynamicTile, direction: FaceDirection): number;
|
||||
|
||||
/**
|
||||
* 更新动态图块位置,用于在图块发生移动时更新内部存储,一般不需要手动调用
|
||||
* @param tile 动态图块
|
||||
*/
|
||||
updateDynamicTile(tile: IDynamicTile): void;
|
||||
}
|
||||
|
||||
export interface IMapLayerSave {
|
||||
/** 地图宽度 */
|
||||
readonly width: number;
|
||||
/** 地图高度 */
|
||||
readonly height: number;
|
||||
|
||||
/**
|
||||
* 每行的地图数据,键表示行索引,值表示该行完整的 Uint32Array 数据。
|
||||
* HighCompression 时使用此接口,仅包含与参考基准不同的行。
|
||||
*/
|
||||
readonly rows?: ReadonlyMap<number, Uint32Array>;
|
||||
|
||||
/** 完整地图,当使用 `NoCompression` 和 `LowCompression` 时使用此接口 */
|
||||
readonly fullMap?: Uint32Array;
|
||||
|
||||
/**
|
||||
* 每个静态图块实例的存储,在 `LowCompression` 和 `HighCompression` 下,
|
||||
* 此存储只会保存被标记为脏的图块,避免占用过大的存储空间。
|
||||
*/
|
||||
readonly staticBlocks?: ReadonlyMap<number, Readonly<IStaticBlockSave>>;
|
||||
/**
|
||||
* 每个动态图块实例的存储,在 `LowCompression` 和 `HighCompression` 下,
|
||||
* 此存储只会保存被标记为脏的图块,避免占用过大的存储空间。
|
||||
*/
|
||||
readonly dynamicBlocks?: ReadonlyMap<number, Readonly<IDynamicBlockSave>[]>;
|
||||
}
|
||||
|
||||
export interface IMapLayer
|
||||
extends
|
||||
IHookable<IMapLayerHooks, IMapLayerHookController>,
|
||||
IDataCommonExtended,
|
||||
ILayerStatic,
|
||||
ILayerDynamic,
|
||||
ISaveableContent<IMapLayerSave> {
|
||||
/** 地图宽度 */
|
||||
readonly width: number;
|
||||
/** 地图高度 */
|
||||
readonly height: number;
|
||||
/**
|
||||
* 地图是否全部空白,此值具有充分性,但不具有必要性,
|
||||
* 即如果其为 `true`,则地图一定空白,但是如果其为 `false`,那么地图也有可能空白
|
||||
*/
|
||||
readonly empty: boolean;
|
||||
/** 图层纵深 */
|
||||
readonly zIndex: number;
|
||||
|
||||
/** 当前图层所属的地图状态对象 */
|
||||
readonly map: IGameMap;
|
||||
/** 该图层使用的朝向绑定器 */
|
||||
readonly faceBinder: IRoleFaceBinder;
|
||||
|
||||
/**
|
||||
* 设置图层的参考基准,存档时会根据参考基准进行必要的压缩处理,仅可调用一次,多次调用无效
|
||||
* @param data 图层的静态地图矩阵
|
||||
*/
|
||||
compareWith(data: Uint32Array): void;
|
||||
|
||||
/**
|
||||
* 判断指定坐标是否在地图内
|
||||
* @param x 横坐标
|
||||
* @param y 纵坐标
|
||||
*/
|
||||
inMap(x: number, y: number): boolean;
|
||||
|
||||
/**
|
||||
* 设置该图层使用的朝向绑定器,用于控制图块转向操作
|
||||
* @param binder 朝向绑定器
|
||||
*/
|
||||
setFaceBinder(binder: IRoleFaceBinder | null): void;
|
||||
|
||||
/**
|
||||
* 获取指定点的所有图块信息
|
||||
* @param x 横坐标
|
||||
* @param y 纵坐标
|
||||
*/
|
||||
getLocationData(x: number, y: number): ILayerLocation | null;
|
||||
|
||||
/**
|
||||
* 设置地图纵深,会影响渲染的遮挡顺序
|
||||
@ -243,54 +486,53 @@ export interface IMapLayer
|
||||
* @param y 门纵坐标
|
||||
*/
|
||||
closeDoor(num: number, x: number, y: number): Promise<void>;
|
||||
|
||||
/**
|
||||
* 当前图层是否为脏,即是否经过了修改,相对于设置的参考基准。
|
||||
* 图层相对于参考基准变化时一定为 `true`,但为 `true` 时图层不一定相对参考基准发生了变化。
|
||||
* 此标记仅针对静态图层,动态图块的改动不会影响此值。
|
||||
*/
|
||||
dirty(): boolean;
|
||||
|
||||
/**
|
||||
* 标记当前图层是否为脏,会影响存档,如果不明白原理不建议随意调用
|
||||
* @param dirty 图层是否为脏
|
||||
*/
|
||||
markDirty(dirty: boolean): void;
|
||||
}
|
||||
|
||||
export interface IResizableMapLayer extends IMapLayer {
|
||||
/**
|
||||
* 重设图层宽高,清空地图上的所有内容,会标记图层为脏
|
||||
* @param width 要设置为的宽度
|
||||
* @param height 要设置为的高度
|
||||
*/
|
||||
resize(width: number, height: number): void;
|
||||
|
||||
/**
|
||||
* 重设图层宽高,会保留已有图块内容,扩大的区域补零,缩小的区域裁剪,会标记图层为脏
|
||||
* @param width 要设置为的宽度
|
||||
* @param height 要设置为的高度
|
||||
*/
|
||||
resize2(width: number, height: number): void;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 图层管理
|
||||
|
||||
export interface ILayerStateHooks extends IHookBase {
|
||||
export interface IGameMapHooks extends IHookBase {
|
||||
/**
|
||||
* 当设置背景图块时执行,如果设置的背景图块与原先一样,则不会执行
|
||||
* @param tile 背景图块
|
||||
*/
|
||||
onChangeBackground(tile: number): void;
|
||||
onChangeBackground?(tile: number): void;
|
||||
|
||||
/**
|
||||
* 当地图列表发生变化时执行
|
||||
* @param layerList 地图图层列表
|
||||
*/
|
||||
onUpdateLayer(layerList: Set<IMapLayer>): void;
|
||||
|
||||
/**
|
||||
* 当地图状态对象的某个图层发生区域更新时执行
|
||||
* @param layer 触发更新的地图图层对象
|
||||
* @param x 更新区域左上角横坐标
|
||||
* @param y 更新区域左上角纵坐标
|
||||
* @param width 更新区域宽度
|
||||
* @param height 更新区域高度
|
||||
*/
|
||||
onUpdateLayerArea(
|
||||
layer: IMapLayer,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number
|
||||
): void;
|
||||
|
||||
/**
|
||||
* 当地图状态对象的某个图层设置图块时执行,如果设置的图块与原先一样则不会触发
|
||||
* @param layer 触发更新的地图图层对象
|
||||
* @param block 设置为的图块
|
||||
* @param x 图块横坐标
|
||||
* @param y 图块纵坐标
|
||||
*/
|
||||
onUpdateLayerBlock(
|
||||
layer: IMapLayer,
|
||||
block: number,
|
||||
x: number,
|
||||
y: number
|
||||
): void;
|
||||
onUpdateLayer?(layerList: Set<IMapLayer>): void;
|
||||
|
||||
/**
|
||||
* 当地图状态对象的某个图层大小发生变化时执行
|
||||
@ -298,11 +540,23 @@ export interface ILayerStateHooks extends IHookBase {
|
||||
* @param width 地图的新宽度
|
||||
* @param height 地图的新高度
|
||||
*/
|
||||
onResizeLayer(layer: IMapLayer, width: number, height: number): void;
|
||||
onResizeLayer?(layer: IMapLayer, width: number, height: number): void;
|
||||
}
|
||||
|
||||
export interface ILayerState
|
||||
extends IHookable<ILayerStateHooks>, IDataCommonExtended {
|
||||
export interface IGameMapSave {
|
||||
/** 楼层背景 */
|
||||
readonly background: number;
|
||||
/** 该地图每一个图层的存档信息,键表示纵深,值对应图层存档数据 */
|
||||
readonly layers: ReadonlyMap<number, IMapLayerSave>;
|
||||
}
|
||||
|
||||
export interface IGameMap
|
||||
extends
|
||||
IHookable<IGameMapHooks>,
|
||||
IDataCommonExtended,
|
||||
ISaveableContent<IGameMapSave> {
|
||||
/** 坐标索引器,用于坐标与扁平索引之间的双向转换 */
|
||||
readonly indexer: ILocationHelper;
|
||||
/** 地图列表 */
|
||||
readonly layerList: Set<IMapLayer>;
|
||||
/** 当前楼层共享的图块定义 store */
|
||||
@ -352,6 +606,12 @@ export interface ILayerState
|
||||
*/
|
||||
getLayerAlias(layer: IMapLayer): string | undefined;
|
||||
|
||||
/**
|
||||
* 设置地图的参考基准,存档时会根据参考基准进行必要的压缩,仅可调用一次,多次调用无效
|
||||
* @param data 地图的静态地图矩阵,键表示纵深,值表示对应图层的矩阵
|
||||
*/
|
||||
compareWith(data: Map<number, Uint32Array>): void;
|
||||
|
||||
/**
|
||||
* 重新设置所有图层的大小,同时更新楼层预设宽高
|
||||
* @param width 新的地图宽度
|
||||
@ -384,47 +644,26 @@ export interface ILayerState
|
||||
setEventLayer(layer: IMapLayer | null): void;
|
||||
|
||||
/**
|
||||
* 楼层是否被修改过(相对于参考基准)
|
||||
* 当前地图是否为脏,相对于参考基准,当任意图层或地图设置发生变化时为脏。
|
||||
* 地图相对于参考基准变化时一定为 `true`,但为 `true` 时地图不一定相对参考基准发生了变化。
|
||||
* 此标记仅针对静态图层和本地图的地图设置,动态图块的改动不会影响此值。
|
||||
*/
|
||||
isDirty(): boolean;
|
||||
dirty(): boolean;
|
||||
|
||||
/**
|
||||
* 设置楼层脏标记
|
||||
* 标记当前地图是否为脏,会影响存档,如果不明白原理不建议随意调用
|
||||
* @param dirty 地图是否为脏
|
||||
*/
|
||||
setDirty(dirty: boolean): void;
|
||||
markDirty(dirty: boolean): void;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 楼层管理
|
||||
|
||||
export interface IMapLayerSave {
|
||||
readonly width: number;
|
||||
readonly height: number;
|
||||
|
||||
/**
|
||||
* key = 行索引,value = 该行完整的 Uint32Array 数据;
|
||||
* HighCompression 时使用此接口,仅包含与参考基准不同的行;
|
||||
* 读档时,不在此 Map 中的行从参考基准还原。
|
||||
*/
|
||||
readonly rows?: ReadonlyMap<number, Uint32Array>;
|
||||
|
||||
/** 完整地图,当使用 `NoCompression` 和 `LowCompression` 时使用此接口 */
|
||||
readonly fullMap?: Uint32Array;
|
||||
}
|
||||
|
||||
export interface ILayerStateSave {
|
||||
/** 楼层背景 */
|
||||
readonly background: number;
|
||||
/** key = zIndex,value = 对应图层存档数据 */
|
||||
readonly layers: ReadonlyMap<number, IMapLayerSave>;
|
||||
/** 静态触发器覆盖映射,仅在存在覆盖时写入 */
|
||||
readonly triggers: ReadonlyMap<number, ReadonlyMap<number, number>>;
|
||||
}
|
||||
|
||||
export interface IMapStoreSave {
|
||||
/** key = 楼层 id,只包含 active 的楼层,inactive 的楼层不写入,读档时无需处理 */
|
||||
readonly floors: ReadonlyMap<string, ILayerStateSave>;
|
||||
/** 键表示楼层 id,只包含已激活的楼层,未激活的楼层不写入 */
|
||||
readonly floors: ReadonlyMap<string, IGameMapSave>;
|
||||
}
|
||||
|
||||
export interface IMapAreaInterval {
|
||||
@ -441,34 +680,34 @@ export interface IMapState
|
||||
/** 所有楼层的 id 有序数组 */
|
||||
readonly maps: ReadonlyArray<string>;
|
||||
|
||||
/**
|
||||
* 获取指定 id 的楼层状态,不存在则返回 null
|
||||
* @param id 楼层 id
|
||||
*/
|
||||
getLayerState(id: string): ILayerState | null;
|
||||
|
||||
/**
|
||||
* 获取指定 id 的楼层状态,要求楼层必须是 active 的,否则返回 null
|
||||
* @param id 楼层 id
|
||||
*/
|
||||
getActiveMap(id: string): ILayerState | null;
|
||||
|
||||
/**
|
||||
* 从楼层原始数据生成楼层状态
|
||||
* @param raw 楼层原始数据
|
||||
*/
|
||||
fromRaw(raw: IMapRawData): ILayerState | null;
|
||||
|
||||
/**
|
||||
* 创建并注册一个空白楼层,若 id 已存在则警告并覆盖,返回楼层状态对象
|
||||
* @param id 楼层 id
|
||||
* @param width 地图宽度
|
||||
* @param height 地图高度
|
||||
*/
|
||||
createLayerState(id: string, width: number, height: number): ILayerState;
|
||||
createMap(id: string, width: number, height: number): IGameMap;
|
||||
|
||||
/**
|
||||
* 获取指定 id 的楼层是否激活,不存在的 id 返回 false
|
||||
* 获取指定 id 的楼层状态,不存在则返回 `null`
|
||||
* @param id 楼层 id
|
||||
*/
|
||||
getMap(id: string): IGameMap | null;
|
||||
|
||||
/**
|
||||
* 从楼层原始数据生成楼层状态
|
||||
* @param raw 楼层原始数据
|
||||
*/
|
||||
fromRaw(raw: IMapRawData): IGameMap | null;
|
||||
|
||||
/**
|
||||
* 获取指定 id 的楼层状态,要求楼层必须是已激活的,否则返回 `null`
|
||||
* @param id 楼层 id
|
||||
*/
|
||||
getActiveMap(id: string): IGameMap | null;
|
||||
|
||||
/**
|
||||
* 获取指定 id 的楼层是否激活,不存在的 id 返回 `false`
|
||||
* @param id 楼层 id
|
||||
*/
|
||||
isMapActive(id: string): boolean;
|
||||
@ -481,23 +720,23 @@ export interface IMapState
|
||||
setMapActiveStatus(id: string, active: boolean): void;
|
||||
|
||||
/**
|
||||
* 迭代所有 active 的楼层,yield [id, ILayerState]
|
||||
* 迭代所有已激活的楼层
|
||||
*/
|
||||
iterateActiveMaps(): Iterable<[string, ILayerState]>;
|
||||
iterateActiveMaps(): Iterable<[string, IGameMap]>;
|
||||
|
||||
/**
|
||||
* 迭代所有 inactive 的楼层,yield [id, ILayerState]
|
||||
* 迭代所有未激活的楼层
|
||||
*/
|
||||
iterateInactiveMaps(): Iterable<[string, ILayerState]>;
|
||||
iterateInactiveMaps(): Iterable<[string, IGameMap]>;
|
||||
|
||||
/**
|
||||
* 迭代所有楼层,yield [id, ILayerState]
|
||||
* 迭代所有楼层
|
||||
*/
|
||||
iterateAllMaps(): Iterable<[string, ILayerState]>;
|
||||
iterateAllMaps(): Iterable<[string, IGameMap]>;
|
||||
|
||||
/**
|
||||
* 设置压缩参考基准,以首次调用为唯一基准,再次调用不更新。
|
||||
* @param ref 外层 key = 楼层 id,内层 key = zIndex,value = 图层完整图块数据
|
||||
* 设置参考基准,存档时会根据参考基准进行必要的压缩处理,仅可调用一次,多次调用无效
|
||||
* @param ref 外层键表示楼层 id,内层键表示图层纵深,值表示图层完整图块数据
|
||||
*/
|
||||
compareWith(ref: Map<string, Map<number, Uint32Array>>): void;
|
||||
|
||||
@ -507,13 +746,6 @@ export interface IMapState
|
||||
*/
|
||||
setMapList(maps: string[]): void;
|
||||
|
||||
/**
|
||||
* 使用自定义排序函数重排 maps。排序函数接收当前列表的副本,返回新顺序。
|
||||
* 若返回的数组元素集合与原列表不一致,则警告并放弃本次排序
|
||||
* @param sort 排序函数
|
||||
*/
|
||||
useManualOrder(sort: (arr: string[]) => string[]): void;
|
||||
|
||||
/**
|
||||
* 设定分区列表。每个分区由一个或多个区间组成
|
||||
* @param areas 分区集合
|
||||
@ -527,7 +759,7 @@ export interface IMapState
|
||||
activeArea(id: string): void;
|
||||
|
||||
/**
|
||||
* 去激活指定楼层所属分区的所有楼层
|
||||
* 取消激活指定楼层所属分区的所有楼层
|
||||
* @param id 楼层 id
|
||||
*/
|
||||
deactiveArea(id: string): void;
|
||||
@ -539,169 +771,11 @@ export interface IMapState
|
||||
useAutoActivitor(enable: boolean): void;
|
||||
|
||||
/**
|
||||
* 通知当前进入的楼层。开启自动激活器时,将自动去激活上一个分区并激活新分区
|
||||
* 通知当前勇士进入的楼层,用于自动分区。当开启自动分区激活器时,若进入的楼层不属于当前分区,
|
||||
* 那么会自动取消激活当前分区,然后激活新进入的分区。此函数一般不需要手动调用。
|
||||
* @param id 楼层 id
|
||||
*/
|
||||
notifyEnterFloor(id: string): void;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 动态图块
|
||||
|
||||
export interface IDynamicLayerHooks extends IHookBase {
|
||||
/**
|
||||
* 当图块被创建(含从静态图块转换)时触发
|
||||
* @param tile 被创建的动态图块
|
||||
* @param layer 所属的动态图层
|
||||
*/
|
||||
onCreateTile?(tile: IDynamicTile, layer: IDynamicLayer): void;
|
||||
|
||||
/**
|
||||
* 当图块被删除时触发
|
||||
* @param tile 被删除的动态图块
|
||||
* @param layer 所属的动态图层
|
||||
*/
|
||||
onDeleteTile?(tile: IDynamicTile, layer: IDynamicLayer): Promise<void>;
|
||||
|
||||
/**
|
||||
* 当更新动态图块的位置时触发(包括使用 `mover` 触发的移动)
|
||||
* @param tile 更新位置的图块
|
||||
* @param layer 所属的动态图层
|
||||
*/
|
||||
onUpdateTilePosition?(tile: IDynamicTile, layer: IDynamicLayer): void;
|
||||
}
|
||||
|
||||
export interface IDynamicLayer
|
||||
extends IHookable<IDynamicLayerHooks>, IDataCommonExtended {
|
||||
/** 当前动态图层所属的静态图层 */
|
||||
readonly layer: IMapLayer;
|
||||
|
||||
/**
|
||||
* 在指定位置创建一个动态图块
|
||||
* @param num 图块数字
|
||||
* @param x 横坐标
|
||||
* @param y 纵坐标
|
||||
* @returns 创建的动态图块引用
|
||||
*/
|
||||
createDynamic(num: number, x: number, y: number): IDynamicTile;
|
||||
|
||||
/**
|
||||
* 从所属静态图层读取并清除指定位置的图块,创建对应动态图块并返回引用。
|
||||
* 若该位置图块为 0,则发出警告并仍然创建 `num = 0` 的动态图块
|
||||
* @param x 横坐标
|
||||
* @param y 纵坐标
|
||||
* @returns 创建的动态图块引用
|
||||
*/
|
||||
transferToDynamic(
|
||||
x: number,
|
||||
y: number,
|
||||
keepTrigger?: boolean
|
||||
): IDynamicTile;
|
||||
|
||||
/**
|
||||
* 将动态图块还原为静态图块。坐标越界则警告并放弃,
|
||||
* 否则写回静态图层并触发 {@link IDynamicLayerHooks.onDeleteTile}
|
||||
* @param tile 要还原的动态图块
|
||||
*/
|
||||
transferToStatic(tile: IDynamicTile, keepTrigger?: boolean): void;
|
||||
|
||||
/**
|
||||
* 仅当目标位置静态图块为 0(空白)时才还原为静态图块,否则不转换
|
||||
* @param tile 要还原的动态图块
|
||||
* @returns 是否转换成功
|
||||
*/
|
||||
transferToStaticIfSafe(tile: IDynamicTile, keepTrigger?: boolean): boolean;
|
||||
|
||||
/**
|
||||
* 删除指定动态图块,触发 {@link IDynamicLayerHooks.onDeleteTile} 钩子。
|
||||
* 若图块不属于此层则发出警告
|
||||
* @param tile 要删除的动态图块
|
||||
*/
|
||||
deleteDynamic(tile: IDynamicTile): Promise<void>;
|
||||
|
||||
/**
|
||||
* 获取指定格点上所有动态图块的可迭代对象
|
||||
* @param x 横坐标
|
||||
* @param y 纵坐标
|
||||
*/
|
||||
getDynamicTilesAt(x: number, y: number): Iterable<IDynamicTile>;
|
||||
|
||||
/**
|
||||
* 迭代所有的动态图块
|
||||
*/
|
||||
iterateDynamicTiles(): Iterable<IDynamicTile>;
|
||||
|
||||
/**
|
||||
* 手动设置动态图块的朝向,更新 `tile.num`(若有朝向绑定)。
|
||||
* 转向逻辑与移动时的转向逻辑相同,但不触发移动
|
||||
* @param tile 要设置朝向的动态图块
|
||||
* @param direction 目标朝向
|
||||
*/
|
||||
setDynamicDirection(tile: IDynamicTile, direction: FaceDirection): void;
|
||||
|
||||
/**
|
||||
* 更新图块内部存储位置
|
||||
* @param tile 动态图块
|
||||
*/
|
||||
updateDynamicTile(tile: IDynamicTile): void;
|
||||
}
|
||||
|
||||
export interface IDynamicTile extends IObjectMovable, IDataCommonExtended {
|
||||
/** 当前图块数字 */
|
||||
readonly num: number;
|
||||
/** 当前动态图块携带的触发器类型,-1 表示无触发器 */
|
||||
readonly triggerType: number;
|
||||
/** 当前图块所属的动态图层 */
|
||||
readonly layer: IDynamicLayer;
|
||||
/** 当前动态图块的移动器 */
|
||||
readonly mover: IObjectMover<IDynamicTile>;
|
||||
/** 当前动态图块的图块数据 */
|
||||
readonly raw: ITileRawData | null;
|
||||
|
||||
/**
|
||||
* 设置图块朝向,会一并修改 {@link num},返回设置后的当前图块数字
|
||||
* @param direction 图块朝向
|
||||
*/
|
||||
setFaceDirection(direction: FaceDirection): number;
|
||||
|
||||
/**
|
||||
* 设置当前动态图块的触发器类型
|
||||
* @param type 触发器类型
|
||||
*/
|
||||
setTriggerType(type: number): void;
|
||||
|
||||
/**
|
||||
* 直接删除此图块
|
||||
*/
|
||||
delete(): Promise<void>;
|
||||
|
||||
/**
|
||||
* 将当前图块还原为静态图块
|
||||
*/
|
||||
toStatic(): void;
|
||||
|
||||
/**
|
||||
* 还原为静态图块,如果当前位置有东西则不转换
|
||||
*/
|
||||
toStaticIfSafe(): boolean;
|
||||
|
||||
/**
|
||||
* 单步便捷移动接口,适用于简单移动场景,复杂路径通过 `tile.mover` 访问。
|
||||
* 等价于:
|
||||
*
|
||||
* ```ts
|
||||
* mover.step(dir, count);
|
||||
* return mover.start();
|
||||
* ```
|
||||
*/
|
||||
step(dir: FaceDirection, count?: number): IMoverController | null;
|
||||
|
||||
/**
|
||||
* 注入朝向绑定器,初始状态视为无朝向绑定
|
||||
* @param binder 朝向绑定器
|
||||
*/
|
||||
setFaceBinder(binder: IRoleFaceBinder | null): void;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
@ -8,12 +8,12 @@ import {
|
||||
IReplayArray,
|
||||
IReplayReadStream,
|
||||
IReplaySandbox,
|
||||
IReplaySandboxHook,
|
||||
IReplaySandboxHooks,
|
||||
IReplaySystem
|
||||
} from './types';
|
||||
|
||||
export class ReplaySandbox
|
||||
extends Hookable<IReplaySandboxHook>
|
||||
extends Hookable<IReplaySandboxHooks>
|
||||
implements IReplaySandbox
|
||||
{
|
||||
pausing: boolean = true;
|
||||
@ -41,8 +41,8 @@ export class ReplaySandbox
|
||||
}
|
||||
|
||||
protected createController(
|
||||
hook: Partial<IReplaySandboxHook>
|
||||
): IHookController<IReplaySandboxHook> {
|
||||
hook: Partial<IReplaySandboxHooks>
|
||||
): IHookController<IReplaySandboxHooks> {
|
||||
return new HookController(this, hook);
|
||||
}
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
IReplaySandbox,
|
||||
IReplaySandboxConfig,
|
||||
IReplaySystem,
|
||||
IReplaySystemHook,
|
||||
IReplaySystemHooks,
|
||||
ReplayCommandWidth,
|
||||
ReplayParamValue
|
||||
} from './types';
|
||||
@ -18,7 +18,7 @@ import { ReplayArray } from './array';
|
||||
import { ReplaySandbox } from './sandbox';
|
||||
|
||||
export class ReplaySystem
|
||||
extends Hookable<IReplaySystemHook>
|
||||
extends Hookable<IReplaySystemHooks>
|
||||
implements IReplaySystem
|
||||
{
|
||||
replaying: boolean = false;
|
||||
@ -42,8 +42,8 @@ export class ReplaySystem
|
||||
}
|
||||
|
||||
protected createController(
|
||||
hook: Partial<IReplaySystemHook>
|
||||
): IHookController<IReplaySystemHook> {
|
||||
hook: Partial<IReplaySystemHooks>
|
||||
): IHookController<IReplaySystemHooks> {
|
||||
return new HookController(this, hook);
|
||||
}
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@ export interface IReplayCommand {
|
||||
execute(step: IReplayStepHandler): Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface IReplaySandboxHook extends IHookBase {
|
||||
export interface IReplaySandboxHooks extends IHookBase {
|
||||
/**
|
||||
* 当录像沙箱将某一步录像操作执行完毕后触发
|
||||
* @param step 当前录像步信息
|
||||
@ -54,7 +54,7 @@ export interface IReplaySandboxHook extends IHookBase {
|
||||
onResumeReplay?(): void;
|
||||
}
|
||||
|
||||
export interface IReplaySandbox extends IHookable<IReplaySandboxHook> {
|
||||
export interface IReplaySandbox extends IHookable<IReplaySandboxHooks> {
|
||||
/** 是否处于暂停状态 */
|
||||
readonly pausing: boolean;
|
||||
/** 当前录像是否已经播放完毕 */
|
||||
@ -282,7 +282,7 @@ export interface IReplayArray {
|
||||
): void;
|
||||
}
|
||||
|
||||
export interface IReplaySystemHook extends IHookBase {
|
||||
export interface IReplaySystemHooks extends IHookBase {
|
||||
/**
|
||||
* 当创建新的录像沙盒时触发
|
||||
* @param sandbox 创建的录像沙盒
|
||||
@ -313,7 +313,7 @@ export interface IReplaySandboxConfig {
|
||||
save?: Map<string, unknown>;
|
||||
}
|
||||
|
||||
export interface IReplaySystem extends IHookable<IReplaySystemHook> {
|
||||
export interface IReplaySystem extends IHookable<IReplaySystemHooks> {
|
||||
/** 当前是否处在录像播放状态 */
|
||||
readonly replaying: boolean;
|
||||
/** 当前正在播放的录像沙箱实例 */
|
||||
|
||||
@ -330,11 +330,7 @@ export class CoreState implements ICoreState {
|
||||
const reference = new Map<string, Map<number, Uint32Array>>();
|
||||
for (const id of floors) {
|
||||
const floor = data[id];
|
||||
const state = this.maps.createLayerState(
|
||||
id,
|
||||
floor.width,
|
||||
floor.height
|
||||
);
|
||||
const state = this.maps.createMap(id, floor.width, floor.height);
|
||||
const bg = state.addLayer();
|
||||
const bg2 = state.addLayer();
|
||||
const event = state.addLayer();
|
||||
|
||||
@ -46,7 +46,7 @@ export class DefaultHeroMoveTopImpl implements IHeroMoveTopImpl {
|
||||
|
||||
inBound(x: number, y: number, floorId: string | undefined): boolean {
|
||||
if (isNil(floorId)) return false;
|
||||
const layerState = this.maps.getLayerState(floorId);
|
||||
const layerState = this.maps.getMap(floorId);
|
||||
if (!layerState) return false;
|
||||
const { width, height } = layerState;
|
||||
return x >= 0 && y >= 0 && x < width && y < height;
|
||||
@ -66,7 +66,7 @@ export class DefaultHeroMoveTopImpl implements IHeroMoveTopImpl {
|
||||
return true;
|
||||
}
|
||||
|
||||
const map = this.maps.getLayerState(floorId);
|
||||
const map = this.maps.getMap(floorId);
|
||||
if (!map) return false;
|
||||
const event = map.eventLayer;
|
||||
if (!event) return false;
|
||||
@ -115,7 +115,7 @@ export class DefaultHeroMoveTopImpl implements IHeroMoveTopImpl {
|
||||
shouldHit(handler: IHeroMoveTopHandler): boolean {
|
||||
const { nextLoc, floorId } = handler;
|
||||
if (isNil(floorId)) return false;
|
||||
const layerState = this.maps.getLayerState(floorId);
|
||||
const layerState = this.maps.getMap(floorId);
|
||||
if (!layerState) return false;
|
||||
const eventLayer = layerState.eventLayer;
|
||||
if (!eventLayer) return false;
|
||||
@ -146,7 +146,7 @@ export class DefaultHeroMoveTopImpl implements IHeroMoveTopImpl {
|
||||
): Promise<void> {
|
||||
if (isNil(handler.floorId)) return Promise.resolve();
|
||||
|
||||
const map = this.maps.getLayerState(handler.floorId);
|
||||
const map = this.maps.getMap(handler.floorId);
|
||||
if (!map) return Promise.resolve();
|
||||
const event = map.eventLayer;
|
||||
if (!event) return Promise.resolve();
|
||||
|
||||
@ -8,7 +8,7 @@ import {
|
||||
import {
|
||||
ICombatFlow,
|
||||
ICombatFlowHandler,
|
||||
ICombatFlowHook,
|
||||
ICombatFlowHooks,
|
||||
ICombatScript,
|
||||
IDamageContext,
|
||||
IEnemyContext,
|
||||
@ -25,7 +25,7 @@ import {
|
||||
} from '@user/data-base';
|
||||
|
||||
export class CombatFlow<TEnemy, THero>
|
||||
extends Hookable<ICombatFlowHook<TEnemy, THero>>
|
||||
extends Hookable<ICombatFlowHooks<TEnemy, THero>>
|
||||
implements ICombatFlow<TEnemy, THero>
|
||||
{
|
||||
hero: IHeroAttribute<THero> | null = null;
|
||||
@ -42,8 +42,8 @@ export class CombatFlow<TEnemy, THero>
|
||||
//#region 对象控制
|
||||
|
||||
protected createController(
|
||||
hook: Partial<ICombatFlowHook<TEnemy, THero>>
|
||||
): IHookController<ICombatFlowHook<TEnemy, THero>> {
|
||||
hook: Partial<ICombatFlowHooks<TEnemy, THero>>
|
||||
): IHookController<ICombatFlowHooks<TEnemy, THero>> {
|
||||
return new HookController(this, hook);
|
||||
}
|
||||
|
||||
|
||||
@ -750,7 +750,7 @@ export interface ICombatFlowHandler<TEnemy, THero> extends IDataBaseExtended {
|
||||
readonly locator: ITileLocator;
|
||||
}
|
||||
|
||||
export interface ICombatFlowHook<TEnemy, THero> extends IHookBase {
|
||||
export interface ICombatFlowHooks<TEnemy, THero> extends IHookBase {
|
||||
/**
|
||||
* 战前脚本
|
||||
* @param info 战斗伤害信息
|
||||
@ -799,7 +799,7 @@ export interface ICombatResult<TEnemy, THero> {
|
||||
}
|
||||
|
||||
export interface ICombatFlow<TEnemy, THero>
|
||||
extends IHookable<ICombatFlowHook<TEnemy, THero>>, IDataBaseExtended {
|
||||
extends IHookable<ICombatFlowHooks<TEnemy, THero>>, IDataBaseExtended {
|
||||
/** 勇士属性对象 */
|
||||
readonly hero: IReadonlyHeroAttribute<THero> | null;
|
||||
/** 怪物上下文对象 */
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { ITileLocator } from '@motajs/common';
|
||||
import {
|
||||
ILayerState,
|
||||
IGameMap,
|
||||
IMapLayer,
|
||||
IStateBase,
|
||||
IDataBaseExtended
|
||||
@ -21,7 +21,7 @@ export interface ITriggerHandler {
|
||||
/** 当前全局状态对象 */
|
||||
readonly state: IStateBase;
|
||||
/** 当前楼层状态对象 */
|
||||
readonly layer?: ILayerState;
|
||||
readonly layer?: IGameMap;
|
||||
/** 当前参与触发的图层对象 */
|
||||
readonly mapLayer?: IMapLayer;
|
||||
/** 当前触发点定位符 */
|
||||
|
||||
@ -191,10 +191,10 @@
|
||||
"125": "Expected sorted floor id array has a same floor id set, but an array with a different floor id set is returned.",
|
||||
"126": "Only move code '$1' is expected, but got '$2'.",
|
||||
"127": "Block at $1,$2 is 0, the dynamic tile will have num 0.",
|
||||
"128": "Position $1,$2 is out of map bounds, operation cancelled.",
|
||||
"129": "Position $1,$2 is not empty, existing block will be overwritten.",
|
||||
"128": "Cannot transfer $1 to $2, since target position $3,$4 out of bounds.",
|
||||
"129": "Position $1,$2 is not empty, transfering dynamic block to static will overwrite existing static tile.",
|
||||
"130": "The given tile is not managed by this dynamic layer.",
|
||||
"131": "Event layer to set is not belong to current LayerState.",
|
||||
"131": "Cannot set event layer since target map layer does not belongs to current GameMap instance.",
|
||||
"132": "Trigger registry entry of $1 '$2' already exists, new factory will override old factory.",
|
||||
"133": "TileStore.addTile: tile number $1 already exists (id: '$2'), old tile data will be overridden.",
|
||||
"134": "TileStore.addTile: tile id '$1' already maps to number $2, old tile data will be overridden.",
|
||||
@ -206,7 +206,7 @@
|
||||
"140": "Different priority of combat script is expected, but got a same one.",
|
||||
"141": "Some issue may occur in binded damage system on combat flow, please check the console.",
|
||||
"142": "Expected a specific tile number after convert id '$1' to number, but got null.",
|
||||
"143": "Cannot create dynamic tile for $1 since its raw data is not found.",
|
||||
"143": "Cannot create dynamic tile for $1 since its raw data does not found.",
|
||||
"144": "A hero move top implementation object binding is required for hero moving.",
|
||||
"145": "No tile registered for item '$1'.",
|
||||
"146": "No equipment with uid of $1 in hero's inventory.",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user