From 7eeab321343876738e2df56cc376b0f5ff028e43 Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Fri, 21 Aug 2026 01:39:47 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=B8=B8=E6=88=8F=E5=9C=B0?= =?UTF-8?q?=E5=9B=BE=E6=8E=A5=E5=8F=A3=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agents/code.md | 3 +- dev.md | 3 +- .../src/render/elements/props.ts | 4 +- .../client-modules/src/render/map/element.ts | 4 +- .../client-modules/src/render/map/renderer.ts | 12 +- .../client-modules/src/render/map/types.ts | 6 +- packages-user/data-base/src/hero/location.ts | 8 +- packages-user/data-base/src/hero/state.ts | 8 +- packages-user/data-base/src/hero/types.ts | 10 +- .../data-base/src/map/dynamicLayer.ts | 183 ----- .../data-base/src/map/dynamicTile.ts | 113 ++- .../src/map/{layerState.ts => gameMap.ts} | 132 +-- packages-user/data-base/src/map/index.ts | 2 +- packages-user/data-base/src/map/mapLayer.ts | 755 ++++++++++++++---- packages-user/data-base/src/map/mapState.ts | 455 ++--------- packages-user/data-base/src/map/mover.ts | 5 - packages-user/data-base/src/map/staticTile.ts | 59 ++ packages-user/data-base/src/map/tile.ts | 70 ++ packages-user/data-base/src/map/types.ts | 750 +++++++++-------- .../data-common/src/replay/sandbox.ts | 8 +- .../data-common/src/replay/system.ts | 8 +- packages-user/data-common/src/replay/types.ts | 8 +- packages-user/data-state/src/core.ts | 6 +- .../data-state/src/hero/moverImpl.ts | 8 +- .../data-system/src/combat/combat.ts | 8 +- packages-user/data-system/src/combat/types.ts | 4 +- .../data-system/src/trigger/types.ts | 4 +- packages/common/src/logger.json | 8 +- 28 files changed, 1408 insertions(+), 1236 deletions(-) delete mode 100644 packages-user/data-base/src/map/dynamicLayer.ts rename packages-user/data-base/src/map/{layerState.ts => gameMap.ts} (56%) create mode 100644 packages-user/data-base/src/map/staticTile.ts create mode 100644 packages-user/data-base/src/map/tile.ts diff --git a/.agents/code.md b/.agents/code.md index 6b7f4bf..2229b98 100644 --- a/.agents/code.md +++ b/.agents/code.md @@ -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. **渲染端被动**:任何情况下渲染端不会主动向数据端推送更新。渲染端仅通过钩子与数据端通信,被动获取信息,仅在某些情况下通过钩子影响数据端行为。 # 开发流程 diff --git a/dev.md b/dev.md index afc79ac..3eb3db5 100644 --- a/dev.md +++ b/dev.md @@ -76,11 +76,12 @@ - 长文件可使用 `#region` / `#endregion` 分段以支持折叠。 - TODO 使用 `// TODO:` 或 `// todo:` 格式。 - 单行注释的 `//` 与注释内容之间留一个空格;不允许出现非 jsDoc 的多行注释,如需多行注释,使用多个单行注释代替。 -- 注释合理换行:考虑中文字符较宽,建议每 40–60 个字符在标点符号后换行,不要频繁换行,每行长度应差不多,不允许在句中换行;参数注释换行后保持对齐。 +- 注释合理换行:考虑中文字符较宽,建议每 40–60 个字符在标点符号后换行,不要频繁换行,每行长度应差不多,不要过短也不要过长,不允许在句中换行;参数注释换行后保持对齐。 - 单行注释结尾不加句号;较长的多行注释结尾可加句号。 - 一般不建议给接口(`inteface` 本身)、类型别名、枚举或类本身写注释(即需要紧接着一长段缩进的时候,这时候写注释不好看),特殊情况除外。 - 对于 jsDoc 注释,方法注释必须使用换行风格;对于成员 jsDoc 注释,除非注释较长需要换行,否则使用不换行的风格。 - 代码中的注释必须包含价值,说明下一行为什么会出现,不得出现解释下一行代码等类型的无价值注释,如 `// 清空 Xxx` 就属于无价值注释。 +- jsDoc 注释应该包含主要信息,不允许假设其使用场景,非必要不使用符号或英文单词,注释的目的是让用户看完注释就立刻知道接口的用处,而不是给写代码的人看,不得为了注释的正确性而使用奇怪的语序。 ### 类型规范 diff --git a/packages-user/client-modules/src/render/elements/props.ts b/packages-user/client-modules/src/render/elements/props.ts index a3c7a7d..c029aa9 100644 --- a/packages-user/client-modules/src/render/elements/props.ts +++ b/packages-user/client-modules/src/render/elements/props.ts @@ -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; } diff --git a/packages-user/client-modules/src/render/map/element.ts b/packages-user/client-modules/src/render/map/element.ts index a2352dd..21c72e4 100644 --- a/packages-user/client-modules/src/render/map/element.ts +++ b/packages-user/client-modules/src/render/map/element.ts @@ -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 ) { diff --git a/packages-user/client-modules/src/render/map/renderer.ts b/packages-user/client-modules/src/render/map/renderer.ts index 6292c82..d086922 100644 --- a/packages-user/client-modules/src/render/map/renderer.ts +++ b/packages-user/client-modules/src/render/map/renderer.ts @@ -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; + private layerStateHook: IHookController; /** 排序后的图层 */ 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 { +class RendererLayerStateHook implements Partial { constructor(readonly renderer: MapRenderer) {} onChangeBackground(tile: number): void { diff --git a/packages-user/client-modules/src/render/map/types.ts b/packages-user/client-modules/src/render/map/types.ts index 438586c..3bff2df 100644 --- a/packages-user/client-modules/src/render/map/types.ts +++ b/packages-user/client-modules/src/render/map/types.ts @@ -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; /** * 根据标识符获取图层 diff --git a/packages-user/data-base/src/hero/location.ts b/packages-user/data-base/src/hero/location.ts index 2165766..c5d3501 100644 --- a/packages-user/data-base/src/hero/location.ts +++ b/packages-user/data-base/src/hero/location.ts @@ -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 + extends Hookable implements IHeroLocation { x: number; @@ -39,8 +39,8 @@ export class HeroLocation } protected createController( - hook: Partial - ): IHookController { + hook: Partial + ): IHookController { return new HookController(this, hook); } diff --git a/packages-user/data-base/src/hero/state.ts b/packages-user/data-base/src/hero/state.ts index b1ceaed..54aede6 100644 --- a/packages-user/data-base/src/hero/state.ts +++ b/packages-user/data-base/src/hero/state.ts @@ -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 - extends Hookable + extends Hookable implements IHeroState { /** 修饰器工厂函数注册表 */ @@ -72,8 +72,8 @@ export class HeroState } protected createController( - hook: Partial - ): IHookController { + hook: Partial + ): IHookController { return new HookController(this, hook); } diff --git a/packages-user/data-base/src/hero/types.ts b/packages-user/data-base/src/hero/types.ts index f5cddb1..2fb0c3f 100644 --- a/packages-user/data-base/src/hero/types.ts +++ b/packages-user/data-base/src/hero/types.ts @@ -227,7 +227,7 @@ export interface IHeroAttribute extends IReadonlyHeroAttribute { //#region 勇士位置 -export interface IHeroLocationHook extends IHookBase { +export interface IHeroLocationHooks extends IHookBase { /** * 当设置勇士位置时触发 * @param x 设置为的横坐标 @@ -257,7 +257,7 @@ export interface IHeroLocation extends ISaveableContent, IObjectMovable, - IHookable, + IHookable, 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 { } export interface IHeroState - extends ISaveableContent>, IHookable { + extends + ISaveableContent>, + IHookable { /** 勇士移动对象 */ readonly location: IHeroLocation; /** 勇士属性对象 */ diff --git a/packages-user/data-base/src/map/dynamicLayer.ts b/packages-user/data-base/src/map/dynamicLayer.ts deleted file mode 100644 index 8afc2a4..0000000 --- a/packages-user/data-base/src/map/dynamicLayer.ts +++ /dev/null @@ -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 - implements IDynamicLayer -{ - readonly state: IDataCommon; - - /** 坐标到动态图块集合的映射,外层 key = y,内层 key = x,不使用 index 是为了支持地图外图块 */ - private readonly tilePosMap: Map>> = - new Map(); - /** 动态图块到其当前坐标的映射 */ - private readonly posTileMap: Map = new Map(); - - constructor(public readonly layer: IMapLayer) { - super(); - this.state = layer.state; - } - - protected createController( - hook: Partial - ): IHookController { - 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 { - 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 { - return this.tilePosMap.get(y)?.get(x) ?? new Set(); - } - - iterateDynamicTiles(): Iterable { - 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); - } -} diff --git a/packages-user/data-base/src/map/dynamicTile.ts b/packages-user/data-base/src/map/dynamicTile.ts index b2f68c2..2253a81 100644 --- a/packages-user/data-base/src/map/dynamicTile.ts +++ b/packages-user/data-base/src/map/dynamicTile.ts @@ -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 + implements IDynamicTile +{ readonly mover: IObjectMover; - 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; + raw(): ITileRawData | null { + return this.tileRaw; + } + + 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; } - return this.num; } - setTriggerType(type: number): void { - this.triggerType = type; + setPos(x: number, y: number): void { + this.x = x; + this.y = y; + this.locator = { x, y }; + this.layer.updateDynamicTile(this); } - delete(): Promise { - return this.layer.deleteDynamic(this); + getCurrentFaceDirection(): FaceDirection { + const curr = this.layer.faceBinder.getFaceDirection(this.tileNum); + if (isNil(curr)) { + return FaceDirection.Unknown; + } else { + return curr; + } } - toStatic(): void { - this.layer.transferToStatic(this); + toStatic(): IStaticTile | null { + return this.layer.transferToStatic(this); } - toStaticIfSafe(): boolean { + 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 { + 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 { + const save: IDynamicBlockSave = { + num: this.num() + }; + + if (this.triggers) { + save.triggers = this.triggers; + } + + return save; + } + + loadState(save: Readonly): void { + if (save.triggers) { + if (save.triggers.size === 0) { + this.useEmptyTrigger(); + } else { + save.triggers.forEach(v => this.addTrigger(v)); + } } else { - return FaceDirection.Down; + this.clearTrigger(); } } } diff --git a/packages-user/data-base/src/map/layerState.ts b/packages-user/data-base/src/map/gameMap.ts similarity index 56% rename from packages-user/data-base/src/map/layerState.ts rename to packages-user/data-base/src/map/gameMap.ts index b11638b..692964e 100644 --- a/packages-user/data-base/src/map/layerState.ts +++ b/packages-user/data-base/src/map/gameMap.ts @@ -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 - implements ILayerState -{ - readonly layerList: Set = new Set(); - /** 具体 MapLayer 实例列表,供内部 resize 使用 */ - private readonly mapLayerList: Set = new Set(); +export class GameMap extends Hookable implements IGameMap { + readonly layerList: Set = new Set(); /** 图层到图层别名映射 */ readonly layerAliasMap: WeakMap = new WeakMap(); /** 图层别名到图层的映射 */ @@ -32,11 +35,14 @@ export class LayerState /** 图层钩子映射 */ private layerHookMap: Map = 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): 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(); + 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 - ): IHookController { + hook: Partial + ): IHookController { return new HookController(this, hook); } } class StateMapLayerHook implements Partial { 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); }); diff --git a/packages-user/data-base/src/map/index.ts b/packages-user/data-base/src/map/index.ts index 4ccc629..e397ef6 100644 --- a/packages-user/data-base/src/map/index.ts +++ b/packages-user/data-base/src/map/index.ts @@ -1,4 +1,4 @@ -export * from './layerState'; +export * from './gameMap'; export * from './mapLayer'; export * from './mapState'; export * from './mover'; diff --git a/packages-user/data-base/src/map/mapLayer.ts b/packages-user/data-base/src/map/mapLayer.ts index 6ccb711..e01a2b0 100644 --- a/packages-user/data-base/src/map/mapLayer.ts +++ b/packages-user/data-base/src/map/mapLayer.ts @@ -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 @@ -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 = new Map(); - - readonly dynamicLayer: IDynamicLayer; + /** 静态图块实例缓存,key = y * width + x */ + private readonly staticTileCache: Map = new Map(); + /** 坐标到动态图块集合的映射,外层 key = y,内层 key = x */ + private readonly tilePosMap: Map>> = + new Map(); + /** 动态图块到其当前坐标的映射 */ + private readonly posTileMap: Map = 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 { - const next = new Map(); - 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.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); + 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); + } } } 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,24 +290,154 @@ export class MapLayer return this.mapData; } - setTriggerRef(triggers: Map): 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 { - return this.triggerMap; + *iterateBlocks(): Iterable { + 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 - ): 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 { + 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 { + return this.tilePosMap.get(y)?.get(x) ?? new Set(); + } + + iterateDynamicTiles(): Iterable { + 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 { const index = y * this.width + x; const num = this.mapArray[index]; @@ -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 + ): 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 { + const blocks = new Map(); + 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 { + const blocks = new Map(); + 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 { + const rows = new Map(); + 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): 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): 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 diff --git a/packages-user/data-base/src/map/mapState.ts b/packages-user/data-base/src/map/mapState.ts index 5038447..b4e0ce4 100644 --- a/packages-user/data-base/src/map/mapState.ts +++ b/packages-user/data-base/src/map/mapState.ts @@ -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 = new Map(); + private readonly mapData: Map = new Map(); /** 所有楼层 id 的有序数组 */ readonly maps: string[] = []; - /** 差分压缩参考基准,首次 compareWith 后设置,之后不再更新 */ - private refData: Map> | null = null; + /** 是否已经调用过 compareWith 设置参考基准 */ + private compared: boolean = false; /** 分区列表 */ private areaList: Set = 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>): 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>(); - 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(); - const triggers = new Map>(); - 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 { - const rows = new Map(); - 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(); - 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(); - 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(); - // 没有参考标准,直接退回低压缩级别 - 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(); - // 对每一个地图的每一行进行遍历,然后仅存储有差别的行 - 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(); + 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); } } diff --git a/packages-user/data-base/src/map/mover.ts b/packages-user/data-base/src/map/mover.ts index 05093d4..aaf9891 100644 --- a/packages-user/data-base/src/map/mover.ts +++ b/packages-user/data-base/src/map/mover.ts @@ -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 { return Promise.resolve(); } } - -//#endregion diff --git a/packages-user/data-base/src/map/staticTile.ts b/packages-user/data-base/src/map/staticTile.ts new file mode 100644 index 0000000..94695be --- /dev/null +++ b/packages-user/data-base/src/map/staticTile.ts @@ -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 + 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 { + const save: IStaticBlockSave = {}; + + if (this.triggers) { + save.triggers = this.triggers; + } + + return save; + } + + loadState(save: Readonly): void { + if (save.triggers) { + if (save.triggers.size === 0) { + this.useEmptyTrigger(); + } else { + save.triggers.forEach(v => this.addTrigger(v)); + } + } else { + this.clearTrigger(); + } + } +} diff --git a/packages-user/data-base/src/map/tile.ts b/packages-user/data-base/src/map/tile.ts new file mode 100644 index 0000000..95ae10e --- /dev/null +++ b/packages-user/data-base/src/map/tile.ts @@ -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 + implements ITileBase, IDataCommonExtended, ISaveableContent> +{ + readonly state: IDataCommon; + readonly layer: IMapLayer; + locator: ITileLocator; + triggers: Set | 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; + + abstract loadState(save: Readonly): void; +} diff --git a/packages-user/data-base/src/map/types.ts b/packages-user/data-base/src/map/types.ts index 99c05f2..9cc0f21 100644 --- a/packages-user/data-base/src/map/types.ts +++ b/packages-user/data-base/src/map/types.ts @@ -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; +} + +export interface IStaticBlockSave extends IMapBlockSaveBase {} + +export interface IDynamicBlockSave extends IMapBlockSaveBase { + /** 此动态图块的图块数字 */ + num: number; +} + +export interface ITileBase { + /** 该图块所处的位置 */ + readonly locator: ITileLocator; + /** 该图块所处的图层对象 */ + readonly layer: IMapLayer; + /** 包含的触发器列表,不存在时表示使用图块本身的触发器 */ + readonly triggers: ReadonlySet | 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> { + /** + * 将此静态图块转换为动态图块 + */ + toDynamic(): IDynamicTile; + + /** + * 判断当前静态图块是否需要存档 + */ + shouldSave(): boolean; +} + +export interface IDynamicTile + extends + ITileBase, + IObjectMovable, + IDataCommonExtended, + ISaveableContent> { + /** 该动态图块的移动器 */ + readonly mover: IObjectMover; + /** 该动态图块所属的动态图层 */ + 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; +} + +//#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; /** 该点的静态图块原始数据 */ - 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; + + /** + * 当动态图块被创建时触发,包括从静态图块转换为动态图块 + * @param tile 被创建的动态图块 + */ + onCreateDynamic?(tile: IDynamicTile): void; + + /** + * 当动态图块被删除时触发 + * @param tile 被删除的动态图块 + */ + onDeleteDynamic?(tile: IDynamicTile): Promise; + + /** + * 当更新动态图块的位置时触发 + * @param tile 更新位置的图块 + */ + onUpdateDynamicPosition?(tile: IDynamicTile): void; } export interface IMapLayerHookController extends IHookController { @@ -93,36 +236,9 @@ export interface IMapLayerHookController extends IHookController getMapData(): Readonly; } -export interface IMapLayer - extends - IHookable, - 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): void; + setStaticDirection(x: number, y: number, direction: FaceDirection): number; /** - * 获取静态触发器覆盖映射,一般仅供内部存档逻辑使用 + * 迭代所有的图块 */ - getTriggerRef(): ReadonlyMap; + iterateBlocks(): Iterable; +} + +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; + + /** + * 获取指定格点上所有动态图块的可迭代对象 + * @param x 横坐标 + * @param y 纵坐标 + */ + getDynamicTilesAt(x: number, y: number): Iterable; + + /** + * 迭代所有的动态图块 + */ + iterateDynamicTiles(): Iterable; + + /** + * 设置动态图块的朝向 + * @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; + + /** 完整地图,当使用 `NoCompression` 和 `LowCompression` 时使用此接口 */ + readonly fullMap?: Uint32Array; + + /** + * 每个静态图块实例的存储,在 `LowCompression` 和 `HighCompression` 下, + * 此存储只会保存被标记为脏的图块,避免占用过大的存储空间。 + */ + readonly staticBlocks?: ReadonlyMap>; + /** + * 每个动态图块实例的存储,在 `LowCompression` 和 `HighCompression` 下, + * 此存储只会保存被标记为脏的图块,避免占用过大的存储空间。 + */ + readonly dynamicBlocks?: ReadonlyMap[]>; +} + +export interface IMapLayer + extends + IHookable, + IDataCommonExtended, + ILayerStatic, + ILayerDynamic, + ISaveableContent { + /** 地图宽度 */ + 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; + + /** + * 当前图层是否为脏,即是否经过了修改,相对于设置的参考基准。 + * 图层相对于参考基准变化时一定为 `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): 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): 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, IDataCommonExtended { +export interface IGameMapSave { + /** 楼层背景 */ + readonly background: number; + /** 该地图每一个图层的存档信息,键表示纵深,值对应图层存档数据 */ + readonly layers: ReadonlyMap; +} + +export interface IGameMap + extends + IHookable, + IDataCommonExtended, + ISaveableContent { + /** 坐标索引器,用于坐标与扁平索引之间的双向转换 */ + readonly indexer: ILocationHelper; /** 地图列表 */ readonly layerList: Set; /** 当前楼层共享的图块定义 store */ @@ -352,6 +606,12 @@ export interface ILayerState */ getLayerAlias(layer: IMapLayer): string | undefined; + /** + * 设置地图的参考基准,存档时会根据参考基准进行必要的压缩,仅可调用一次,多次调用无效 + * @param data 地图的静态地图矩阵,键表示纵深,值表示对应图层的矩阵 + */ + compareWith(data: Map): 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; - - /** 完整地图,当使用 `NoCompression` 和 `LowCompression` 时使用此接口 */ - readonly fullMap?: Uint32Array; -} - -export interface ILayerStateSave { - /** 楼层背景 */ - readonly background: number; - /** key = zIndex,value = 对应图层存档数据 */ - readonly layers: ReadonlyMap; - /** 静态触发器覆盖映射,仅在存在覆盖时写入 */ - readonly triggers: ReadonlyMap>; -} - export interface IMapStoreSave { - /** key = 楼层 id,只包含 active 的楼层,inactive 的楼层不写入,读档时无需处理 */ - readonly floors: ReadonlyMap; + /** 键表示楼层 id,只包含已激活的楼层,未激活的楼层不写入 */ + readonly floors: ReadonlyMap; } export interface IMapAreaInterval { @@ -441,34 +680,34 @@ export interface IMapState /** 所有楼层的 id 有序数组 */ readonly maps: ReadonlyArray; - /** - * 获取指定 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>): 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; - - /** - * 当更新动态图块的位置时触发(包括使用 `mover` 触发的移动) - * @param tile 更新位置的图块 - * @param layer 所属的动态图层 - */ - onUpdateTilePosition?(tile: IDynamicTile, layer: IDynamicLayer): void; -} - -export interface IDynamicLayer - extends IHookable, 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; - - /** - * 获取指定格点上所有动态图块的可迭代对象 - * @param x 横坐标 - * @param y 纵坐标 - */ - getDynamicTilesAt(x: number, y: number): Iterable; - - /** - * 迭代所有的动态图块 - */ - iterateDynamicTiles(): Iterable; - - /** - * 手动设置动态图块的朝向,更新 `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; - /** 当前动态图块的图块数据 */ - readonly raw: ITileRawData | null; - - /** - * 设置图块朝向,会一并修改 {@link num},返回设置后的当前图块数字 - * @param direction 图块朝向 - */ - setFaceDirection(direction: FaceDirection): number; - - /** - * 设置当前动态图块的触发器类型 - * @param type 触发器类型 - */ - setTriggerType(type: number): void; - - /** - * 直接删除此图块 - */ - delete(): Promise; - - /** - * 将当前图块还原为静态图块 - */ - 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 diff --git a/packages-user/data-common/src/replay/sandbox.ts b/packages-user/data-common/src/replay/sandbox.ts index 92973dd..add7e46 100644 --- a/packages-user/data-common/src/replay/sandbox.ts +++ b/packages-user/data-common/src/replay/sandbox.ts @@ -8,12 +8,12 @@ import { IReplayArray, IReplayReadStream, IReplaySandbox, - IReplaySandboxHook, + IReplaySandboxHooks, IReplaySystem } from './types'; export class ReplaySandbox - extends Hookable + extends Hookable implements IReplaySandbox { pausing: boolean = true; @@ -41,8 +41,8 @@ export class ReplaySandbox } protected createController( - hook: Partial - ): IHookController { + hook: Partial + ): IHookController { return new HookController(this, hook); } diff --git a/packages-user/data-common/src/replay/system.ts b/packages-user/data-common/src/replay/system.ts index f12793a..7990424 100644 --- a/packages-user/data-common/src/replay/system.ts +++ b/packages-user/data-common/src/replay/system.ts @@ -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 + extends Hookable implements IReplaySystem { replaying: boolean = false; @@ -42,8 +42,8 @@ export class ReplaySystem } protected createController( - hook: Partial - ): IHookController { + hook: Partial + ): IHookController { return new HookController(this, hook); } diff --git a/packages-user/data-common/src/replay/types.ts b/packages-user/data-common/src/replay/types.ts index 8b4e0a2..c28e14d 100644 --- a/packages-user/data-common/src/replay/types.ts +++ b/packages-user/data-common/src/replay/types.ts @@ -20,7 +20,7 @@ export interface IReplayCommand { execute(step: IReplayStepHandler): Promise; } -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 { +export interface IReplaySandbox extends IHookable { /** 是否处于暂停状态 */ 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; } -export interface IReplaySystem extends IHookable { +export interface IReplaySystem extends IHookable { /** 当前是否处在录像播放状态 */ readonly replaying: boolean; /** 当前正在播放的录像沙箱实例 */ diff --git a/packages-user/data-state/src/core.ts b/packages-user/data-state/src/core.ts index a029dab..9d9845f 100644 --- a/packages-user/data-state/src/core.ts +++ b/packages-user/data-state/src/core.ts @@ -330,11 +330,7 @@ export class CoreState implements ICoreState { const reference = new Map>(); 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(); diff --git a/packages-user/data-state/src/hero/moverImpl.ts b/packages-user/data-state/src/hero/moverImpl.ts index 4bcbec0..288871d 100644 --- a/packages-user/data-state/src/hero/moverImpl.ts +++ b/packages-user/data-state/src/hero/moverImpl.ts @@ -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 { 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(); diff --git a/packages-user/data-system/src/combat/combat.ts b/packages-user/data-system/src/combat/combat.ts index a7ce68e..fa5909c 100644 --- a/packages-user/data-system/src/combat/combat.ts +++ b/packages-user/data-system/src/combat/combat.ts @@ -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 - extends Hookable> + extends Hookable> implements ICombatFlow { hero: IHeroAttribute | null = null; @@ -42,8 +42,8 @@ export class CombatFlow //#region 对象控制 protected createController( - hook: Partial> - ): IHookController> { + hook: Partial> + ): IHookController> { return new HookController(this, hook); } diff --git a/packages-user/data-system/src/combat/types.ts b/packages-user/data-system/src/combat/types.ts index 10a9cd3..78c64d2 100644 --- a/packages-user/data-system/src/combat/types.ts +++ b/packages-user/data-system/src/combat/types.ts @@ -750,7 +750,7 @@ export interface ICombatFlowHandler extends IDataBaseExtended { readonly locator: ITileLocator; } -export interface ICombatFlowHook extends IHookBase { +export interface ICombatFlowHooks extends IHookBase { /** * 战前脚本 * @param info 战斗伤害信息 @@ -799,7 +799,7 @@ export interface ICombatResult { } export interface ICombatFlow - extends IHookable>, IDataBaseExtended { + extends IHookable>, IDataBaseExtended { /** 勇士属性对象 */ readonly hero: IReadonlyHeroAttribute | null; /** 怪物上下文对象 */ diff --git a/packages-user/data-system/src/trigger/types.ts b/packages-user/data-system/src/trigger/types.ts index 8f61071..fb3385f 100644 --- a/packages-user/data-system/src/trigger/types.ts +++ b/packages-user/data-system/src/trigger/types.ts @@ -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; /** 当前触发点定位符 */ diff --git a/packages/common/src/logger.json b/packages/common/src/logger.json index e929598..39bea1a 100644 --- a/packages/common/src/logger.json +++ b/packages/common/src/logger.json @@ -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.",