refactor: 游戏地图接口重构

This commit is contained in:
unanmed 2026-08-21 01:39:47 +08:00
parent 1717400945
commit 7eeab32134
28 changed files with 1408 additions and 1236 deletions

View File

@ -51,10 +51,11 @@
13. **非空判断**:对象直接用 `if (!object)`;字面量使用 `lodash``isNil``if (isNil(value))`),不得使用 `if (value === undefined)` 等方式。 13. **非空判断**:对象直接用 `if (!object)`;字面量使用 `lodash``isNil``if (isNil(value))`),不得使用 `if (value === undefined)` 等方式。
14. **公共方法**:不得出现仅在类中定义的公共方法,所有公共方法必须先在接口中定义,然后在类中使用 `implements` 实现。 14. **公共方法**:不得出现仅在类中定义的公共方法,所有公共方法必须先在接口中定义,然后在类中使用 `implements` 实现。
15. **成员只读**:所有需要被实现为类的接口,其成员必须只读,如果有赋值需求,那么应使用相应的方法来完成赋值操作。 15. **成员只读**:所有需要被实现为类的接口,其成员必须只读,如果有赋值需求,那么应使用相应的方法来完成赋值操作。
16. **对象类型**:对于任何接口或类的对象成员,必须使用接口作为类型,不得使用类作为类型,例如 `map: IGameMap` 符合要求,但 `map: GameMap` 不符合要求。
## 架构约束 ## 架构约束
16. **渲染端被动**:任何情况下渲染端不会主动向数据端推送更新。渲染端仅通过钩子与数据端通信,被动获取信息,仅在某些情况下通过钩子影响数据端行为。 17. **渲染端被动**:任何情况下渲染端不会主动向数据端推送更新。渲染端仅通过钩子与数据端通信,被动获取信息,仅在某些情况下通过钩子影响数据端行为。
# 开发流程 # 开发流程

3
dev.md
View File

@ -76,11 +76,12 @@
- 长文件可使用 `#region` / `#endregion` 分段以支持折叠。 - 长文件可使用 `#region` / `#endregion` 分段以支持折叠。
- TODO 使用 `// TODO:``// todo:` 格式。 - TODO 使用 `// TODO:``// todo:` 格式。
- 单行注释的 `//` 与注释内容之间留一个空格;不允许出现非 jsDoc 的多行注释,如需多行注释,使用多个单行注释代替。 - 单行注释的 `//` 与注释内容之间留一个空格;不允许出现非 jsDoc 的多行注释,如需多行注释,使用多个单行注释代替。
- 注释合理换行:考虑中文字符较宽,建议每 4060 个字符在标点符号后换行,不要频繁换行,每行长度应差不多,不允许在句中换行;参数注释换行后保持对齐。 - 注释合理换行:考虑中文字符较宽,建议每 4060 个字符在标点符号后换行,不要频繁换行,每行长度应差不多,不要过短也不要过长,不允许在句中换行;参数注释换行后保持对齐。
- 单行注释结尾不加句号;较长的多行注释结尾可加句号。 - 单行注释结尾不加句号;较长的多行注释结尾可加句号。
- 一般不建议给接口(`inteface` 本身)、类型别名、枚举或类本身写注释(即需要紧接着一长段缩进的时候,这时候写注释不好看),特殊情况除外。 - 一般不建议给接口(`inteface` 本身)、类型别名、枚举或类本身写注释(即需要紧接着一长段缩进的时候,这时候写注释不好看),特殊情况除外。
- 对于 jsDoc 注释,方法注释必须使用换行风格;对于成员 jsDoc 注释,除非注释较长需要换行,否则使用不换行的风格。 - 对于 jsDoc 注释,方法注释必须使用换行风格;对于成员 jsDoc 注释,除非注释较长需要换行,否则使用不换行的风格。
- 代码中的注释必须包含价值,说明下一行为什么会出现,不得出现解释下一行代码等类型的无价值注释,如 `// 清空 Xxx` 就属于无价值注释。 - 代码中的注释必须包含价值,说明下一行为什么会出现,不得出现解释下一行代码等类型的无价值注释,如 `// 清空 Xxx` 就属于无价值注释。
- jsDoc 注释应该包含主要信息,不允许假设其使用场景,非必要不使用符号或英文单词,注释的目的是让用户看完注释就立刻知道接口的用处,而不是给写代码的人看,不得为了注释的正确性而使用奇怪的语序。
### 类型规范 ### 类型规范

View File

@ -1,6 +1,6 @@
import { BaseProps, TagDefine } from '@motajs/render-vue'; import { BaseProps, TagDefine } from '@motajs/render-vue';
import { ERenderItemEvent, SizedCanvasImageSource } from '@motajs/render'; import { ERenderItemEvent, SizedCanvasImageSource } from '@motajs/render';
import { ILayerState } from '@user/data-base'; import { IGameMap } from '@user/data-base';
import { IMapExtensionManager, IMapRenderer } from '../map'; import { IMapExtensionManager, IMapRenderer } from '../map';
export interface IconProps extends BaseProps { export interface IconProps extends BaseProps {
@ -24,7 +24,7 @@ export interface WinskinProps extends BaseProps {
} }
export interface MapRenderProps extends BaseProps { export interface MapRenderProps extends BaseProps {
layerState: ILayerState; layerState: IGameMap;
renderer: IMapRenderer; renderer: IMapRenderer;
extension: IMapExtensionManager; extension: IMapExtensionManager;
} }

View File

@ -1,5 +1,5 @@
import { MotaOffscreenCanvas2D, RenderItem } from '@motajs/render'; import { MotaOffscreenCanvas2D, RenderItem } from '@motajs/render';
import { ILayerState } from '@user/data-base'; import { IGameMap } from '@user/data-base';
import { IMapRenderer } from './types'; import { IMapRenderer } from './types';
import { ElementNamespace, ComponentInternalInstance } from 'vue'; import { ElementNamespace, ComponentInternalInstance } from 'vue';
import { CELL_HEIGHT, CELL_WIDTH, MAP_HEIGHT, MAP_WIDTH } from '../../shared'; import { CELL_HEIGHT, CELL_WIDTH, MAP_HEIGHT, MAP_WIDTH } from '../../shared';
@ -11,7 +11,7 @@ export class MapRenderItem extends RenderItem {
* @param renderer * @param renderer
*/ */
constructor( constructor(
readonly layerState: ILayerState, readonly layerState: IGameMap,
readonly renderer: IMapRenderer, readonly renderer: IMapRenderer,
readonly exManager: IMapExtensionManager readonly exManager: IMapExtensionManager
) { ) {

View File

@ -30,7 +30,7 @@ import {
MapTileBehavior, MapTileBehavior,
MapTileSizeTestMode MapTileSizeTestMode
} from './types'; } 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 { IHookController, logger } from '@motajs/common';
import { compileProgramWith } from '@motajs/client-base'; import { compileProgramWith } from '@motajs/client-base';
import { isNil, maxBy } from 'lodash-es'; import { isNil, maxBy } from 'lodash-es';
@ -78,9 +78,9 @@ export class MapRenderer
assetWidth: number = 4096; assetWidth: number = 4096;
assetHeight: number = 4096; assetHeight: number = 4096;
layerState: ILayerState; layerState: IGameMap;
/** 地图状态钩子控制器 */ /** 地图状态钩子控制器 */
private layerStateHook: IHookController<ILayerStateHooks>; private layerStateHook: IHookController<IGameMapHooks>;
/** 排序后的图层 */ /** 排序后的图层 */
private sortedLayers: IMapLayer[] = []; private sortedLayers: IMapLayer[] = [];
@ -210,7 +210,7 @@ export class MapRenderer
*/ */
constructor( constructor(
readonly manager: IMaterialManager, readonly manager: IMaterialManager,
layerState: ILayerState layerState: IGameMap
) { ) {
this.movingIndexPool.push( this.movingIndexPool.push(
...Array.from({ length: this.movingCount }, (_, i) => i).reverse() ...Array.from({ length: this.movingCount }, (_, i) => i).reverse()
@ -427,7 +427,7 @@ export class MapRenderer
this.vertex.updateLayerArray(); this.vertex.updateLayerArray();
} }
setLayerState(layerState: ILayerState): void { setLayerState(layerState: IGameMap): void {
if (layerState === this.layerState) return; if (layerState === this.layerState) return;
this.layerStateHook.unload(); this.layerStateHook.unload();
this.layerState = layerState; this.layerState = layerState;
@ -1735,7 +1735,7 @@ export class MapRenderer
//#endregion //#endregion
} }
class RendererLayerStateHook implements Partial<ILayerStateHooks> { class RendererLayerStateHook implements Partial<IGameMapHooks> {
constructor(readonly renderer: MapRenderer) {} constructor(readonly renderer: MapRenderer) {}
onChangeBackground(tile: number): void { onChangeBackground(tile: number): void {

View File

@ -10,7 +10,7 @@ import {
IMaterialManager, IMaterialManager,
ITrackedAssetData ITrackedAssetData
} from '@user/client-base'; } from '@user/client-base';
import { ILayerState, IMapLayer } from '@user/data-base'; import { IGameMap, IMapLayer } from '@user/data-base';
import { TimingFn } from 'mutate-animate'; import { TimingFn } from 'mutate-animate';
export const enum MapBackgroundRepeat { export const enum MapBackgroundRepeat {
@ -339,7 +339,7 @@ export interface IMapRenderer {
/** 顶点数组生成器 */ /** 顶点数组生成器 */
readonly vertex: IMapVertexGenerator; readonly vertex: IMapVertexGenerator;
/** 使用的地图状态对象 */ /** 使用的地图状态对象 */
readonly layerState: ILayerState; readonly layerState: IGameMap;
/** 地图宽度 */ /** 地图宽度 */
readonly mapWidth: number; readonly mapWidth: number;
@ -432,7 +432,7 @@ export interface IMapRenderer {
* 使 * 使
* @param layerState * @param layerState
*/ */
setLayerState(layerState: ILayerState): void; setLayerState(layerState: IGameMap): void;
/** /**
* *

View File

@ -8,13 +8,13 @@ import { FaceDirection, IDataCommon, IFaceHandler } from '@user/data-common';
import { HeroMover } from './mover'; import { HeroMover } from './mover';
import { import {
IHeroLocation, IHeroLocation,
IHeroLocationHook, IHeroLocationHooks,
IHeroLocationSave, IHeroLocationSave,
IHeroMover IHeroMover
} from './types'; } from './types';
export class HeroLocation export class HeroLocation
extends Hookable<IHeroLocationHook> extends Hookable<IHeroLocationHooks>
implements IHeroLocation implements IHeroLocation
{ {
x: number; x: number;
@ -39,8 +39,8 @@ export class HeroLocation
} }
protected createController( protected createController(
hook: Partial<IHeroLocationHook> hook: Partial<IHeroLocationHooks>
): IHookController<IHeroLocationHook> { ): IHookController<IHeroLocationHooks> {
return new HookController(this, hook); return new HookController(this, hook);
} }

View File

@ -17,7 +17,7 @@ import {
IModifierStateSave, IModifierStateSave,
IReadonlyHeroAttribute, IReadonlyHeroAttribute,
IHeroChangeFloorInfo, IHeroChangeFloorInfo,
IHeroStateHook IHeroStateHooks
} from './types'; } from './types';
import { import {
FaceDirection, FaceDirection,
@ -34,7 +34,7 @@ import {
} from '@motajs/common'; } from '@motajs/common';
export class HeroState<THero> export class HeroState<THero>
extends Hookable<IHeroStateHook> extends Hookable<IHeroStateHooks>
implements IHeroState<THero> implements IHeroState<THero>
{ {
/** 修饰器工厂函数注册表 */ /** 修饰器工厂函数注册表 */
@ -72,8 +72,8 @@ export class HeroState<THero>
} }
protected createController( protected createController(
hook: Partial<IHeroStateHook> hook: Partial<IHeroStateHooks>
): IHookController<IHeroStateHook> { ): IHookController<IHeroStateHooks> {
return new HookController(this, hook); return new HookController(this, hook);
} }

View File

@ -227,7 +227,7 @@ export interface IHeroAttribute<THero> extends IReadonlyHeroAttribute<THero> {
//#region 勇士位置 //#region 勇士位置
export interface IHeroLocationHook extends IHookBase { export interface IHeroLocationHooks extends IHookBase {
/** /**
* *
* @param x * @param x
@ -257,7 +257,7 @@ export interface IHeroLocation
extends extends
ISaveableContent<IHeroLocationSave>, ISaveableContent<IHeroLocationSave>,
IObjectMovable, IObjectMovable,
IHookable<IHeroLocationHook>, IHookable<IHeroLocationHooks>,
IDataCommonExtended { IDataCommonExtended {
/** 当前所在楼层 idundefined 表示尚不处于任何楼层 */ /** 当前所在楼层 idundefined 表示尚不处于任何楼层 */
readonly floorId: string | undefined; readonly floorId: string | undefined;
@ -779,7 +779,7 @@ export interface IHeroChangeFloorInfo {
readonly face: FaceDirection; readonly face: FaceDirection;
} }
export interface IHeroStateHook extends IHookBase { export interface IHeroStateHooks extends IHookBase {
/** /**
* `IHeroLocation` `onSetFloor` * `IHeroLocation` `onSetFloor`
* *
@ -813,7 +813,9 @@ export interface IHeroStateSave<THero> {
} }
export interface IHeroState<THero> export interface IHeroState<THero>
extends ISaveableContent<IHeroStateSave<THero>>, IHookable<IHeroStateHook> { extends
ISaveableContent<IHeroStateSave<THero>>,
IHookable<IHeroStateHooks> {
/** 勇士移动对象 */ /** 勇士移动对象 */
readonly location: IHeroLocation; readonly location: IHeroLocation;
/** 勇士属性对象 */ /** 勇士属性对象 */

View File

@ -1,183 +0,0 @@
import {
HookController,
Hookable,
IHookController,
ITileLocator,
logger
} from '@motajs/common';
import {
IDynamicLayer,
IDynamicLayerHooks,
IDynamicTile,
IMapLayer
} from './types';
import { FaceDirection, IDataCommon, degradeFace } from '@user/data-common';
import { DynamicTile } from './dynamicTile';
export class DynamicLayer
extends Hookable<IDynamicLayerHooks>
implements IDynamicLayer
{
readonly state: IDataCommon;
/** 坐标到动态图块集合的映射,外层 key = y内层 key = x不使用 index 是为了支持地图外图块 */
private readonly tilePosMap: Map<number, Map<number, Set<IDynamicTile>>> =
new Map();
/** 动态图块到其当前坐标的映射 */
private readonly posTileMap: Map<IDynamicTile, ITileLocator> = new Map();
constructor(public readonly layer: IMapLayer) {
super();
this.state = layer.state;
}
protected createController(
hook: Partial<IDynamicLayerHooks>
): IHookController<IDynamicLayerHooks> {
return new HookController(this, hook);
}
createDynamic(num: number, x: number, y: number): IDynamicTile {
const tile = new DynamicTile(num, x, y, this);
tile.setTriggerType(this.layer.getTriggerType(x, y));
this.addTileToPosMap(tile, x, y);
this.posTileMap.set(tile, { x, y });
this.forEachHook(hook => hook.onCreateTile?.(tile, this));
return tile;
}
transferToDynamic(
x: number,
y: number,
keepTrigger: boolean = true
): IDynamicTile {
const num = this.layer.getBlock(x, y);
const triggerType = keepTrigger ? this.layer.getTriggerType(x, y) : -1;
if (num === 0) {
logger.warn(127, x.toString(), y.toString());
}
this.layer.setBlock(0, x, y);
this.layer.revertTrigger(x, y);
const tile = this.createDynamic(num, x, y);
tile.setTriggerType(triggerType);
return tile;
}
/**
*
*/
private syncStaticTrigger(tile: IDynamicTile, keepTrigger: boolean): void {
if (keepTrigger) {
this.layer.setTriggerType(tile.triggerType, tile.x, tile.y);
} else {
this.layer.revertTrigger(tile.x, tile.y);
}
}
transferToStatic(tile: IDynamicTile, keepTrigger: boolean = true): void {
const { x, y } = tile;
const { width, height } = this.layer;
if (x < 0 || y < 0 || x >= width || y >= height) {
logger.warn(128, x.toString(), y.toString());
return;
}
if (this.layer.getBlock(x, y) !== 0) {
logger.warn(129, x.toString(), y.toString());
}
this.layer.setBlock(tile.num, x, y);
this.syncStaticTrigger(tile, keepTrigger);
this.removeTile(tile);
this.forEachHook(hook => hook.onDeleteTile?.(tile, this));
}
transferToStaticIfSafe(
tile: IDynamicTile,
keepTrigger: boolean = true
): boolean {
const { x, y } = tile;
const { width, height } = this.layer;
if (x < 0 || y < 0 || x >= width || y >= height) {
logger.warn(128, x.toString(), y.toString());
return false;
}
if (this.layer.getBlock(tile.x, tile.y) !== 0) return false;
this.layer.setBlock(tile.num, x, y);
this.syncStaticTrigger(tile, keepTrigger);
this.removeTile(tile);
this.forEachHook(hook => hook.onDeleteTile?.(tile, this));
return true;
}
async deleteDynamic(tile: IDynamicTile): Promise<void> {
if (!this.posTileMap.has(tile)) {
logger.warn(130);
return;
}
this.removeTile(tile);
const hooks = this.forEachHook(hook => hook.onDeleteTile?.(tile, this));
await Promise.all(hooks);
}
getDynamicTilesAt(x: number, y: number): Iterable<IDynamicTile> {
return this.tilePosMap.get(y)?.get(x) ?? new Set();
}
iterateDynamicTiles(): Iterable<IDynamicTile> {
return this.posTileMap.keys();
}
setDynamicDirection(tile: IDynamicTile, direction: FaceDirection): void {
const numBefore = tile.num;
tile.setFaceDirection(direction);
if (tile.num !== numBefore) return;
const degraded = degradeFace(direction);
if (degraded !== direction) {
tile.setFaceDirection(degraded);
}
}
updateDynamicTile(tile: IDynamicTile): void {
const oldPos = this.posTileMap.get(tile);
if (oldPos) {
this.removeTileFromPosMap(tile, oldPos.x, oldPos.y);
oldPos.x = tile.x;
oldPos.y = tile.y;
this.addTileToPosMap(tile, tile.x, tile.y);
} else {
this.addTileToPosMap(tile, tile.x, tile.y);
this.posTileMap.set(tile, { x: tile.x, y: tile.y });
}
this.forEachHook(hook => hook.onUpdateTilePosition?.(tile, this));
}
/**
*
*/
private addTileToPosMap(tile: IDynamicTile, x: number, y: number): void {
const xMap = this.tilePosMap.getOrInsertComputed(y, () => new Map());
const set = xMap.getOrInsertComputed(x, () => new Set());
set.add(tile);
}
/**
*
*/
private removeTileFromPosMap(
tile: IDynamicTile,
x: number,
y: number
): void {
this.tilePosMap.get(y)?.get(x)?.delete(tile);
}
/**
*
*/
private removeTile(tile: IDynamicTile): void {
const pos = this.posTileMap.get(tile);
if (pos) {
this.removeTileFromPosMap(tile, pos.x, pos.y);
}
this.posTileMap.delete(tile);
}
}

View File

@ -1,70 +1,89 @@
import { isNil } from 'lodash-es'; import { isNil } from 'lodash-es';
import { import {
FaceDirection, FaceDirection,
IDataCommon,
IMoverController, IMoverController,
IObjectMover, IObjectMover,
IRoleFaceBinder,
ITileRawData ITileRawData
} from '@user/data-common'; } from '@user/data-common';
import { IDynamicLayer, IDynamicTile } from './types'; import {
IDynamicBlockSave,
IDynamicTile,
IMapLayer,
IStaticTile
} from './types';
import { DynamicTileMover } from './mover'; import { DynamicTileMover } from './mover';
import { logger } from '@motajs/common'; import { logger } from '@motajs/common';
import { MapTileBase } from './tile';
export class DynamicTile implements IDynamicTile { export class DynamicTile
readonly state: IDataCommon; extends MapTileBase<IDynamicBlockSave>
implements IDynamicTile
{
readonly mover: IObjectMover<IDynamicTile>; readonly mover: IObjectMover<IDynamicTile>;
raw: ITileRawData | null; /** 内部存储的图块数字 */
triggerType: number; private tileNum: number;
/** 内部存储的原始图块信息 */
/** 当前的朝向绑定对象 */ private tileRaw: ITileRawData | null;
private face: IRoleFaceBinder | null = null;
constructor( constructor(
public num: number, num: number,
public x: number, public x: number,
public y: 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.mover = new DynamicTileMover(this);
this.triggerType = -1;
const data = this.state.tileStore.getData(num); const data = this.state.tileStore.getData(num);
if (!data) { if (!data) {
logger.warn(143, num.toString()); logger.warn(143, num.toString());
this.raw = null; this.tileRaw = null;
} else { } else {
this.raw = data; this.tileRaw = data;
} }
} }
setFaceBinder(binder: IRoleFaceBinder | null): void { num(): number {
this.face = binder; return this.tileNum;
} }
setFaceDirection(direction: FaceDirection): number { raw(): ITileRawData | null {
if (!this.face) return this.num; return this.tileRaw;
const next = this.face.getFaceOf(this.num, direction); }
if (next) {
this.num = next.identifier; 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 { setPos(x: number, y: number): void {
this.triggerType = type; this.x = x;
this.y = y;
this.locator = { x, y };
this.layer.updateDynamicTile(this);
} }
delete(): Promise<void> { getCurrentFaceDirection(): FaceDirection {
return this.layer.deleteDynamic(this); const curr = this.layer.faceBinder.getFaceDirection(this.tileNum);
if (isNil(curr)) {
return FaceDirection.Unknown;
} else {
return curr;
}
} }
toStatic(): void { toStatic(): IStaticTile | null {
this.layer.transferToStatic(this); return this.layer.transferToStatic(this);
} }
toStaticIfSafe(): boolean { toStaticIfSafe(): IStaticTile | null {
return this.layer.transferToStaticIfSafe(this); return this.layer.transferToStaticIfSafe(this);
} }
@ -74,19 +93,31 @@ export class DynamicTile implements IDynamicTile {
return this.mover.start(); return this.mover.start();
} }
setPos(x: number, y: number): void { delete(): Promise<void> {
this.x = x; return this.layer.deleteDynamic(this);
this.y = y;
this.layer.updateDynamicTile(this);
} }
getCurrentFaceDirection(): FaceDirection { saveState(): Readonly<IDynamicBlockSave> {
if (this.face) { const save: IDynamicBlockSave = {
const face = this.face.getFaceDirection(this.num); num: this.num()
if (isNil(face)) return FaceDirection.Unknown; };
else return face;
if (this.triggers) {
save.triggers = this.triggers;
}
return save;
}
loadState(save: Readonly<IDynamicBlockSave>): void {
if (save.triggers) {
if (save.triggers.size === 0) {
this.useEmptyTrigger();
} else {
save.triggers.forEach(v => this.addTrigger(v));
}
} else { } else {
return FaceDirection.Down; this.clearTrigger();
} }
} }
} }

View File

@ -5,22 +5,25 @@ import {
logger logger
} from '@motajs/common'; } from '@motajs/common';
import { import {
ILayerState, IGameMap,
ILayerStateHooks, IGameMapHooks,
IGameMapSave,
IMapLayer, IMapLayer,
IMapLayerHookController, IMapLayerHookController,
IMapLayerHooks IMapLayerHooks,
IMapLayerSave,
IResizableMapLayer
} from './types'; } from './types';
import { IDataCommon, ITileStore } from '@user/data-common'; import {
IDataCommon,
ITileStore,
MapLocIndexer,
SaveCompression
} from '@user/data-common';
import { MapLayer } from './mapLayer'; import { MapLayer } from './mapLayer';
export class LayerState export class GameMap extends Hookable<IGameMapHooks> implements IGameMap {
extends Hookable<ILayerStateHooks> readonly layerList: Set<IResizableMapLayer> = new Set();
implements ILayerState
{
readonly layerList: Set<IMapLayer> = new Set();
/** 具体 MapLayer 实例列表,供内部 resize 使用 */
private readonly mapLayerList: Set<MapLayer> = new Set();
/** 图层到图层别名映射 */ /** 图层到图层别名映射 */
readonly layerAliasMap: WeakMap<IMapLayer, string> = new WeakMap(); readonly layerAliasMap: WeakMap<IMapLayer, string> = new WeakMap();
/** 图层别名到图层的映射 */ /** 图层别名到图层的映射 */
@ -32,11 +35,14 @@ export class LayerState
/** 图层钩子映射 */ /** 图层钩子映射 */
private layerHookMap: Map<IMapLayer, IMapLayerHookController> = new Map(); private layerHookMap: Map<IMapLayer, IMapLayerHookController> = new Map();
/** 坐标索引器 */
readonly indexer = new MapLocIndexer();
active: boolean = false; active: boolean = false;
eventLayer: IMapLayer | null = null; eventLayer: IMapLayer | null = null;
/** 楼层脏标记 */ /** 楼层自身脏标记 */
private dirty: boolean = false; private selfDirty: boolean = false;
constructor( constructor(
public readonly state: IDataCommon, public readonly state: IDataCommon,
@ -45,19 +51,13 @@ export class LayerState
public height: number public height: number
) { ) {
super(); super();
this.indexer.setWidth(width);
} }
addLayer(): IMapLayer { addLayer(): IMapLayer {
const array = new Uint32Array(this.width * this.height); const array = new Uint32Array(this.width * this.height);
const layer = new MapLayer( const layer = new MapLayer(array, this.width, this.height, this);
array,
this.width,
this.height,
this,
this.tileStore
);
this.layerList.add(layer); this.layerList.add(layer);
this.mapLayerList.add(layer);
this.forEachHook(hook => { this.forEachHook(hook => {
hook.onUpdateLayer?.(this.layerList); hook.onUpdateLayer?.(this.layerList);
}); });
@ -68,8 +68,7 @@ export class LayerState
} }
removeLayer(layer: IMapLayer): void { removeLayer(layer: IMapLayer): void {
this.layerList.delete(layer); this.layerList.delete(layer as IResizableMapLayer);
this.mapLayerList.delete(layer as MapLayer);
const alias = this.layerAliasMap.get(layer); const alias = this.layerAliasMap.get(layer);
if (alias) { if (alias) {
const symbol = Symbol.for(alias); const symbol = Symbol.for(alias);
@ -86,7 +85,7 @@ export class LayerState
} }
hasLayer(layer: IMapLayer): boolean { hasLayer(layer: IMapLayer): boolean {
return this.layerList.has(layer); return this.layerList.has(layer as IResizableMapLayer);
} }
setLayerAlias(layer: IMapLayer, alias: string): void { setLayerAlias(layer: IMapLayer, alias: string): void {
@ -115,7 +114,8 @@ export class LayerState
): void { ): void {
this.width = width; this.width = width;
this.height = height; this.height = height;
for (const layer of this.mapLayerList) { this.indexer.setWidth(width);
for (const layer of this.layerList) {
if (keepBlock) { if (keepBlock) {
layer.resize(width, height); layer.resize(width, height);
} else { } else {
@ -143,50 +143,86 @@ export class LayerState
if (!layer) { if (!layer) {
this.eventLayer = null; this.eventLayer = null;
} else { } else {
if (!this.layerList.has(layer)) { if (!this.layerList.has(layer as IResizableMapLayer)) {
logger.warn(131);
return; return;
} }
this.eventLayer = layer; this.eventLayer = layer;
} }
} }
isDirty(): boolean { dirty(): boolean {
return this.dirty; if (this.selfDirty) return true;
for (const layer of this.layerList) {
if (layer.dirty()) return true;
}
return false;
} }
setDirty(dirty: boolean): void { markDirty(dirty: boolean): void {
this.dirty = dirty; this.selfDirty = dirty;
}
compareWith(data: Map<number, Uint32Array>): void {
for (const layer of this.layerList) {
const refArray = data.get(layer.zIndex);
if (refArray) {
layer.compareWith(refArray);
} else {
layer.markDirty(true);
}
}
}
/**
*
* @param save
*/
private isEmptyLayerSave(save: IMapLayerSave): boolean {
if (save.fullMap) return false;
if (save.rows && save.rows.size > 0) return false;
if (save.staticBlocks && save.staticBlocks.size > 0) return false;
if (save.dynamicBlocks && save.dynamicBlocks.size > 0) return false;
return true;
}
saveState(compression: SaveCompression): IGameMapSave {
const layers = new Map<number, IMapLayerSave>();
for (const layer of this.layerList) {
const save = layer.saveState(compression);
if (this.isEmptyLayerSave(save)) continue;
layers.set(layer.zIndex, save);
}
return {
background: this.backgroundTile,
layers
};
}
loadState(save: IGameMapSave, compression: SaveCompression): void {
this.setBackground(save.background);
for (const layer of this.layerList) {
const layerSave = save.layers.get(layer.zIndex);
if (!layerSave) continue;
layer.loadState(layerSave, compression);
}
this.markDirty(false);
} }
protected createController( protected createController(
hook: Partial<ILayerStateHooks> hook: Partial<IGameMapHooks>
): IHookController<ILayerStateHooks> { ): IHookController<IGameMapHooks> {
return new HookController(this, hook); return new HookController(this, hook);
} }
} }
class StateMapLayerHook implements Partial<IMapLayerHooks> { class StateMapLayerHook implements Partial<IMapLayerHooks> {
constructor( constructor(
readonly state: LayerState, readonly state: GameMap,
readonly layer: IMapLayer 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 { onResize(width: number, height: number): void {
this.state.setDirty(true);
this.state.forEachHook(hook => { this.state.forEachHook(hook => {
hook.onResizeLayer?.(this.layer, width, height); hook.onResizeLayer?.(this.layer, width, height);
}); });

View File

@ -1,4 +1,4 @@
export * from './layerState'; export * from './gameMap';
export * from './mapLayer'; export * from './mapLayer';
export * from './mapState'; export * from './mapState';
export * from './mover'; export * from './mover';

View File

@ -1,18 +1,27 @@
import { isNil } from 'lodash-es'; import { isNil } from 'lodash-es';
import { import {
IDynamicLayer, IDynamicBlockSave,
IDynamicTile,
IGameMap,
ILayerLocation, ILayerLocation,
ILayerState,
IMapLayer, IMapLayer,
IMapLayerData, IMapLayerData,
IMapLayerHookController, IMapLayerHookController,
IMapLayerHooks IMapLayerHooks,
IMapLayerSave,
IStaticBlockSave,
IStaticTile
} from './types'; } from './types';
import { Hookable, HookController, logger } from '@motajs/common'; import { Hookable, HookController, ITileLocator, logger } from '@motajs/common';
import { DynamicLayer } from './dynamicLayer'; import {
import { IDataCommon, ITileStore } from '@user/data-common'; degradeFace,
FaceDirection,
// todo: 提供 core.setBlock 等方法的替代方法,同时添加 setBlockList以及前景背景的接口 IDataCommon,
IRoleFaceBinder,
SaveCompression
} from '@user/data-common';
import { DynamicTile } from './dynamicTile';
import { StaticTile } from './staticTile';
export class MapLayer export class MapLayer
extends Hookable<IMapLayerHooks, IMapLayerHookController> extends Hookable<IMapLayerHooks, IMapLayerHookController>
@ -25,127 +34,116 @@ export class MapLayer
empty: boolean = true; empty: boolean = true;
zIndex: number = 0; zIndex: number = 0;
faceBinder: IRoleFaceBinder;
/** 地图图块数组 */ /** 地图图块数组 */
private mapArray: Uint32Array; private mapArray: Uint32Array;
/** 地图数据引用 */ /** 地图数据引用 */
private mapData: IMapLayerData; private mapData: IMapLayerData;
/** 手动触发器覆盖映射key = y * width + x */ /** 静态图块实例缓存key = y * width + x */
private triggerMap: Map<number, number> = new Map(); private readonly staticTileCache: Map<number, StaticTile> = new Map();
/** 坐标到动态图块集合的映射,外层 key = y内层 key = x */
readonly dynamicLayer: IDynamicLayer; private readonly tilePosMap: Map<number, Map<number, Set<IDynamicTile>>> =
new Map();
/** 动态图块到其当前坐标的映射 */
private readonly posTileMap: Map<IDynamicTile, ITileLocator> = new Map();
/** 图层脏标记 */
private layerDirty: boolean = false;
/** 图层参考基准,用于存档压缩对比 */
private refArray: Uint32Array | null = null;
constructor( constructor(
array: Uint32Array, array: Uint32Array,
width: number, width: number,
height: number, height: number,
public readonly layerState: ILayerState, public readonly map: IGameMap
private readonly tileStore: ITileStore
) { ) {
super(); super();
this.state = layerState.state; this.state = map.state;
this.faceBinder = this.state.roleFace;
this.width = width; this.width = width;
this.height = height; this.height = height;
const area = width * height; const area = width * height;
this.mapArray = new Uint32Array(area); this.mapArray = new Uint32Array(area);
// 超出的裁剪,不足的补零
this.mapArray.set(array); this.mapArray.set(array);
this.mapData = { this.mapData = {
expired: false, expired: false,
array: this.mapArray 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( private addTileToPosMap(tile: IDynamicTile, x: number, y: number): void {
beforeWidth: number, const xMap = this.tilePosMap.getOrInsertComputed(y, () => new Map());
width: number, const set = xMap.getOrInsertComputed(x, () => new Set());
height: number set.add(tile);
): Map<number, number> {
const next = new Map<number, number>();
for (const [index, type] of this.triggerMap) {
const x = index % beforeWidth;
const y = Math.floor(index / beforeWidth);
if (x < width && y < height) {
next.set(y * width + x, type);
}
}
return next;
} }
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; this.posTileMap.delete(tile);
const before = this.mapArray; }
const beforeWidth = this.width;
const beforeHeight = this.height; /**
const beforeArea = beforeWidth * beforeHeight; *
this.width = width; * @param tile
this.height = height; * @param keepTrigger
const area = width * height; */
const newArray = new Uint32Array(area); private syncStaticTrigger(tile: IDynamicTile, keepTrigger: boolean): void {
this.triggerMap = this.remapTriggerMap(beforeWidth, width, height); const staticTile = this.getTile(tile.x, tile.y);
this.mapArray = newArray; if (!staticTile) return;
// 将原来的地图数组赋值给现在的 if (keepTrigger) {
if (beforeArea > area) { const triggers = tile.triggers;
// 如果地图变小了,那么直接设置,不需要补零 if (!triggers) {
for (let ny = 0; ny < height; ny++) { staticTile.clearTrigger();
const begin = ny * beforeWidth; } else {
newArray.set(before.subarray(begin, begin + width), ny * width); staticTile.clearTrigger();
for (const trigger of triggers) {
staticTile.addTrigger(trigger);
}
} }
} else { } else {
// 如果地图变大了,那么需要补零。因为新数组本来就是用 0 填充的,实际上只要赋值就可以了 staticTile.clearTrigger();
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 { //#region 静态图层操作
if (this.width === width && this.height === height) {
this.empty = true; inMap(x: number, y: number): boolean {
this.mapArray.fill(0); return x >= 0 && y >= 0 && x < this.width && y < this.height;
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);
});
} }
setBlock(block: number, x: number, y: number): void { setBlock(block: number, x: number, y: number): void {
const index = y * this.width + x; const index = y * this.width + x;
if (block === this.mapArray[index]) return; if (block === this.mapArray[index]) return;
this.mapArray[index] = block; this.mapArray[index] = block;
this.layerDirty = true;
this.forEachHook(hook => { this.forEachHook(hook => {
hook.onUpdateBlock?.(block, x, y); hook.onUpdateBlock?.(block, x, y);
}); });
@ -161,74 +159,47 @@ export class MapLayer
return this.mapArray[y * this.width + x]; 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; if (!this.inMap(x, y)) return null;
const index = y * this.width + x; const index = this.map.indexer.locToIndex(x, y);
const num = this.mapArray[index]; let staticTile = this.staticTileCache.get(index);
const raw = this.state.tileStore.getData(num); if (!staticTile) {
const dynamics = this.dynamicLayer.getDynamicTilesAt(x, y); staticTile = new StaticTile(x, y, this);
const trigger = this.triggerMap.get(index) ?? -1; this.staticTileCache.set(index, staticTile);
}
return staticTile;
}
const data: ILayerLocation = { getLocationData(x: number, y: number): ILayerLocation | null {
tile: num, const staticTile = this.getTile(x, y);
raw, if (!staticTile) return null;
trigger, const num = staticTile.num();
dynamics, const dynamics = this.getDynamicTilesAt(x, y);
return {
locator: { x, y }, 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 { putMapData(array: Uint32Array, x: number, y: number, width: number): void {
if (array.length % width !== 0) { if (array.length % width !== 0) {
logger.warn(8); logger.warn(8);
} }
const height = Math.ceil(array.length / width); this.layerDirty = true;
if (width === this.width && height === this.height) { const h = Math.ceil(array.length / width);
if (width === this.width && h === this.height) {
this.mapArray.set(array); this.mapArray.set(array);
this.staticTileCache.clear();
this.forEachHook(hook => { this.forEachHook(hook => {
hook.onUpdateArea?.(x, y, width, height); hook.onUpdateArea?.(x, y, width, h);
}); });
return; return;
} }
const w = this.width; const w = this.width;
const r = x + width; const r = x + width;
const b = y + height; const b = y + h;
if (x < 0 || y < 0 || r > w || b > this.height) { if (x < 0 || y < 0 || r > w || b > this.height) {
logger.warn(9); logger.warn(9);
} }
@ -244,13 +215,12 @@ export class MapLayer
const offset = (ny + nt) * w + nl; const offset = (ny + nt) * w + nl;
const sub = array.subarray(start, start + nw); const sub = array.subarray(start, start + nw);
if (empty && sub.some(v => v !== 0)) { if (empty && sub.some(v => v !== 0)) {
// 空地图判断
empty = false; empty = false;
} }
this.mapArray.set(array.subarray(start, start + nw), offset); this.mapArray.set(array.subarray(start, start + nw), offset);
} }
this.forEachHook(hook => { this.forEachHook(hook => {
hook.onUpdateArea?.(x, y, width, height); hook.onUpdateArea?.(x, y, width, h);
}); });
this.empty &&= empty; this.empty &&= empty;
} }
@ -305,6 +275,7 @@ export class MapLayer
} }
this.mapData.expired = true; this.mapData.expired = true;
this.mapArray = array; this.mapArray = array;
this.staticTileCache.clear();
this.mapData = { this.mapData = {
expired: false, expired: false,
array: this.mapArray array: this.mapArray
@ -319,24 +290,154 @@ export class MapLayer
return this.mapData; return this.mapData;
} }
setTriggerRef(triggers: Map<number, number>): void { setStaticDirection(x: number, y: number, direction: FaceDirection): number {
this.triggerMap = triggers; const tile = this.getTile(x, y);
if (!tile) return -1;
return tile.setFaceDirection(direction);
} }
getTriggerRef(): ReadonlyMap<number, number> { *iterateBlocks(): Iterable<ILayerLocation> {
return this.triggerMap; 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( //#endregion
hook: Partial<IMapLayerHooks>
): IMapLayerHookController { //#region 动态图层操作
return new MapLayerHookController(this, hook);
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 { transferToDynamic(
this.zIndex = zIndex; x: number,
y: number,
keepTrigger: boolean = true
): IDynamicTile | null {
if (!this.inMap(x, y)) {
logger.warn(131, x.toString(), y.toString());
return null;
}
const num = this.getBlock(x, y);
if (num === 0) {
logger.warn(127, x.toString(), y.toString());
}
this.setBlock(0, x, y);
const tile = this.createDynamic(num, x, y);
const staticTile = this.getTile(x, y);
if (staticTile) {
staticTile.clearTrigger();
}
if (!keepTrigger) {
tile.clearTrigger();
}
return tile;
} }
transferToStatic(
tile: IDynamicTile,
keepTrigger: boolean = true
): IStaticTile | null {
const x = tile.x;
const y = tile.y;
if (x < 0 || y < 0 || x >= this.width || y >= this.height) {
logger.warn(128, x.toString(), y.toString());
return null;
}
if (this.getBlock(x, y) !== 0) {
logger.warn(129, x.toString(), y.toString());
}
this.setBlock(tile.num(), x, y);
this.syncStaticTrigger(tile, keepTrigger);
this.removeTile(tile);
this.forEachHook(hook => hook.onDeleteDynamic?.(tile));
return this.getTile(x, y);
}
transferToStaticIfSafe(
tile: IDynamicTile,
keepTrigger: boolean = true
): IStaticTile | null {
const x = tile.x;
const y = tile.y;
if (x < 0 || y < 0 || x >= this.width || y >= this.height) {
logger.warn(128, x.toString(), y.toString());
return null;
}
if (this.getBlock(tile.x, tile.y) !== 0) return null;
this.setBlock(tile.num(), x, y);
this.syncStaticTrigger(tile, keepTrigger);
this.removeTile(tile);
this.forEachHook(hook => hook.onDeleteDynamic?.(tile));
return this.getTile(x, y);
}
async deleteDynamic(tile: IDynamicTile): Promise<void> {
if (!this.posTileMap.has(tile)) {
logger.warn(130);
return;
}
this.removeTile(tile);
const hooks = this.forEachHook(hook => hook.onDeleteDynamic?.(tile));
await Promise.all(hooks);
}
getDynamicTilesAt(x: number, y: number): Iterable<IDynamicTile> {
return this.tilePosMap.get(y)?.get(x) ?? new Set();
}
iterateDynamicTiles(): Iterable<IDynamicTile> {
return this.posTileMap.keys();
}
setDynamicDirection(tile: IDynamicTile, direction: FaceDirection): number {
const numBefore = tile.num();
tile.setFaceDirection(direction);
if (tile.num() !== numBefore) return tile.num();
const degraded = degradeFace(direction);
if (degraded !== direction) {
tile.setFaceDirection(degraded);
}
return tile.num();
}
updateDynamicTile(tile: IDynamicTile): void {
const oldPos = this.posTileMap.get(tile);
if (oldPos) {
this.removeTileFromPosMap(tile, oldPos.x, oldPos.y);
oldPos.x = tile.x;
oldPos.y = tile.y;
this.addTileToPosMap(tile, tile.x, tile.y);
} else {
this.addTileToPosMap(tile, tile.x, tile.y);
this.posTileMap.set(tile, { x: tile.x, y: tile.y });
}
this.forEachHook(hook => hook.onUpdateDynamicPosition?.(tile));
}
//#endregion
//#region 开关门
async openDoor(x: number, y: number): Promise<void> { async openDoor(x: number, y: number): Promise<void> {
const index = y * this.width + x; const index = y * this.width + x;
const num = this.mapArray[index]; const num = this.mapArray[index];
@ -363,6 +464,352 @@ export class MapLayer
); );
this.setBlock(num, x, y); this.setBlock(num, x, y);
} }
//#endregion
//#region 图层操作
setZIndex(zIndex: number): void {
this.zIndex = zIndex;
}
setFaceBinder(binder: IRoleFaceBinder | null): void {
if (!binder) return;
this.faceBinder = binder;
}
dirty(): boolean {
return this.layerDirty;
}
markDirty(dirty: boolean): void {
this.layerDirty = dirty;
}
/**
*
*/
private isEqualToRef(): boolean {
const ref = this.refArray;
if (!ref) return false;
if (this.mapArray.length !== ref.length) return false;
return !this.mapArray.some((v, i) => ref[i] !== v);
}
compareWith(data: Uint32Array): void {
if (this.refArray) return;
this.refArray = data;
this.layerDirty = !this.isEqualToRef();
}
protected createController(
hook: Partial<IMapLayerHooks>
): IMapLayerHookController {
return new MapLayerHookController(this, hook);
}
resize(width: number, height: number): void {
if (this.width === width && this.height === height) {
return;
}
this.layerDirty = true;
this.mapData.expired = true;
const before = this.mapArray;
const beforeWidth = this.width;
const beforeHeight = this.height;
const beforeArea = beforeWidth * beforeHeight;
this.width = width;
this.height = height;
const area = width * height;
const newArray = new Uint32Array(area);
this.mapArray = newArray;
this.staticTileCache.clear();
if (beforeArea > area) {
for (let ny = 0; ny < height; ny++) {
const begin = ny * beforeWidth;
newArray.set(before.subarray(begin, begin + width), ny * width);
}
} else {
for (let ny = 0; ny < beforeHeight; ny++) {
const begin = ny * beforeWidth;
newArray.set(
before.subarray(begin, begin + beforeWidth),
ny * width
);
}
}
this.mapData = {
expired: false,
array: this.mapArray
};
this.forEachHook(hook => {
hook.onResize?.(width, height);
});
}
resize2(width: number, height: number): void {
this.layerDirty = true;
if (this.width === width && this.height === height) {
this.empty = true;
this.mapArray.fill(0);
this.staticTileCache.clear();
return;
}
this.mapData.expired = true;
this.width = width;
this.height = height;
this.mapArray = new Uint32Array(width * height);
this.staticTileCache.clear();
this.mapData = {
expired: false,
array: this.mapArray
};
this.empty = true;
this.forEachHook(hook => {
hook.onResize?.(width, height);
});
}
//#endregion
//#region 存读档
/**
*
*/
private saveStatics(): Map<number, IStaticBlockSave> {
const blocks = new Map<number, IStaticBlockSave>();
for (const location of this.iterateBlocks()) {
const tile = location.static;
if (!tile.shouldSave()) continue;
const index = this.map.indexer.locaterToIndex(location.locator);
blocks.set(index, tile.saveState(SaveCompression.NoCompression));
}
return blocks;
}
/**
*
*/
private saveDynamics(): Map<number, IDynamicBlockSave[]> {
const blocks = new Map<number, IDynamicBlockSave[]>();
for (const tile of this.iterateDynamicTiles()) {
const index = this.map.indexer.locaterToIndex(tile.locator);
const list = blocks.getOrInsert(index, []);
list.push(tile.saveState(SaveCompression.NoCompression));
}
return blocks;
}
/**
*
* @param refArray
*/
private diffRows(refArray: Uint32Array): Map<number, Uint32Array> {
const rows = new Map<number, Uint32Array>();
for (let row = 0; row < this.height; row++) {
const start = row * this.width;
const end = start + this.width;
const slice = this.mapArray.subarray(start, end);
const refSlice = refArray.subarray(start, end);
const same = refSlice.every((v, i) => slice[i] === v);
if (!same) {
rows.set(row, new Uint32Array(slice));
}
}
return rows;
}
/**
*
*/
private saveNoCompression(): IMapLayerSave {
return {
width: this.width,
height: this.height,
fullMap: new Uint32Array(this.mapArray),
staticBlocks: this.saveStatics(),
dynamicBlocks: this.saveDynamics()
};
}
/**
*
*/
private saveLowCompression(): IMapLayerSave {
const staticBlocks = this.saveStatics();
const dynamicBlocks = this.saveDynamics();
if (this.layerDirty && (!this.refArray || !this.isEqualToRef())) {
return {
width: this.width,
height: this.height,
fullMap: new Uint32Array(this.mapArray),
staticBlocks,
dynamicBlocks
};
} else {
return {
width: this.width,
height: this.height,
staticBlocks,
dynamicBlocks
};
}
}
/**
*
*/
private saveHighCompression(): IMapLayerSave {
const staticBlocks = this.saveStatics();
const dynamicBlocks = this.saveDynamics();
if (this.layerDirty) {
if (this.refArray) {
return {
width: this.width,
height: this.height,
rows: this.diffRows(this.refArray),
staticBlocks,
dynamicBlocks
};
} else {
return {
width: this.width,
height: this.height,
fullMap: new Uint32Array(this.mapArray),
staticBlocks,
dynamicBlocks
};
}
} else {
return {
width: this.width,
height: this.height,
staticBlocks,
dynamicBlocks
};
}
}
saveState(compression: SaveCompression): IMapLayerSave {
if (compression === SaveCompression.HighCompression) {
return this.saveHighCompression();
} else if (compression === SaveCompression.LowCompression) {
return this.saveLowCompression();
} else {
return this.saveNoCompression();
}
}
/**
*
* @param save
*/
private loadStatics(save: ReadonlyMap<number, IStaticBlockSave>): void {
for (const [index, tileSave] of save) {
const { x, y } = this.map.indexer.indexToLocator(index);
const location = this.getLocationData(x, y);
if (!location) continue;
location.static.loadState(tileSave, SaveCompression.NoCompression);
}
}
/**
*
* @param save
*/
private loadDynamics(save: ReadonlyMap<number, IDynamicBlockSave[]>): void {
for (const [index, dynamics] of save) {
const { x, y } = this.map.indexer.indexToLocator(index);
for (const block of dynamics) {
const tile = this.createDynamic(block.num, x, y);
tile.loadState(block, SaveCompression.NoCompression);
}
}
}
/**
*
* @param save
*/
private loadNoCompression(save: IMapLayerSave): void {
if (save.fullMap) {
this.setMapRef(new Uint32Array(save.fullMap));
}
if (save.staticBlocks) {
this.loadStatics(save.staticBlocks);
}
if (save.dynamicBlocks) {
this.loadDynamics(save.dynamicBlocks);
}
this.layerDirty = !this.isEqualToRef();
}
/**
*
* @param save
*/
private loadLowCompression(save: IMapLayerSave): void {
if (save.fullMap) {
this.setMapRef(new Uint32Array(save.fullMap));
this.layerDirty = true;
} else if (this.refArray) {
this.setMapRef(new Uint32Array(this.refArray));
this.layerDirty = false;
} else {
logger.warn(124, this.zIndex.toString());
}
if (save.staticBlocks) {
this.loadStatics(save.staticBlocks);
}
if (save.dynamicBlocks) {
this.loadDynamics(save.dynamicBlocks);
}
}
/**
*
* @param save
*/
private loadHighCompression(save: IMapLayerSave): void {
if (save.rows && save.rows.size > 0) {
if (this.refArray) {
const buf = new Uint32Array(this.refArray);
for (const [rowIdx, rowData] of save.rows) {
buf.set(rowData, rowIdx * this.width);
}
this.setMapRef(buf);
this.layerDirty = true;
} else {
logger.warn(124, this.zIndex.toString());
}
} else if (this.refArray) {
this.setMapRef(new Uint32Array(this.refArray));
this.layerDirty = false;
} else {
logger.warn(124, this.zIndex.toString());
}
if (save.staticBlocks) {
this.loadStatics(save.staticBlocks);
}
if (save.dynamicBlocks) {
this.loadDynamics(save.dynamicBlocks);
}
}
loadState(save: IMapLayerSave, compression: SaveCompression): void {
if (compression === SaveCompression.HighCompression) {
this.loadHighCompression(save);
} else if (compression === SaveCompression.LowCompression) {
this.loadLowCompression(save);
} else {
this.loadNoCompression(save);
}
}
//#endregion
} }
class MapLayerHookController class MapLayerHookController

View File

@ -1,27 +1,25 @@
import { logger } from '@motajs/common'; import { isNil, uniq } from 'lodash-es';
import { IDataCommon, IMapRawData, SaveCompression } from '@user/data-common'; import { IDataCommon, IMapRawData, SaveCompression } from '@user/data-common';
import { ITileStore } from '@user/data-common'; import { ITileStore } from '@user/data-common';
import { import {
ILayerState, IGameMap,
ILayerStateSave, IGameMapSave,
IMapLayer,
IMapLayerSave,
IMapState, IMapState,
IMapStoreSave, IMapStoreSave,
MapArea MapArea
} from './types'; } from './types';
import { LayerState } from './layerState'; import { GameMap } from './gameMap';
import { isNil, uniq } from 'lodash-es'; import { logger } from '@motajs/common';
export class MapState implements IMapState { export class MapState implements IMapState {
/** 楼层 id 到状态对象的映射 */ /** 楼层 id 到状态对象的映射 */
private readonly mapData: Map<string, LayerState> = new Map(); private readonly mapData: Map<string, IGameMap> = new Map();
/** 所有楼层 id 的有序数组 */ /** 所有楼层 id 的有序数组 */
readonly maps: string[] = []; readonly maps: string[] = [];
/** 差分压缩参考基准,首次 compareWith 后设置,之后不再更新 */ /** 是否已经调用过 compareWith 设置参考基准 */
private refData: Map<string, Map<number, Uint32Array>> | null = null; private compared: boolean = false;
/** 分区列表 */ /** 分区列表 */
private areaList: Set<MapArea> = new Set(); private areaList: Set<MapArea> = new Set();
@ -39,7 +37,7 @@ export class MapState implements IMapState {
//#region 楼层管理 //#region 楼层管理
fromRaw(raw: IMapRawData): ILayerState | null { fromRaw(raw: IMapRawData): IGameMap | null {
let length = 0; let length = 0;
const entries = Object.entries(raw.map); const entries = Object.entries(raw.map);
for (const [_, map] of 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 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) { for (const [zIndex, map] of entries) {
const z = Number(zIndex); const z = Number(zIndex);
if (isNaN(z)) { if (isNaN(z)) {
@ -81,7 +79,10 @@ export class MapState implements IMapState {
if (!isNil(data.trigger)) { if (!isNil(data.trigger)) {
const x = indexNum % raw.width; const x = indexNum % raw.width;
const y = Math.floor(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; return state;
} }
createLayerState(id: string, width: number, height: number): ILayerState { createMap(id: string, width: number, height: number): IGameMap {
if (this.mapData.has(id)) { if (this.mapData.has(id)) {
logger.warn(121, id); logger.warn(121, id);
} else { } else {
this.maps.push(id); this.maps.push(id);
} }
const state = new LayerState(this.state, this.tileStore, width, height); const state = new GameMap(this.state, this.tileStore, width, height);
// 若 refData 已存在,新楼层直接视为全脏 // 若已设置参考基准,新楼层直接视为全脏
if (this.refData !== null) { if (this.compared) {
state.setDirty(true); state.markDirty(true);
} }
this.mapData.set(id, state); this.mapData.set(id, state);
return state; return state;
@ -109,24 +110,11 @@ export class MapState implements IMapState {
this.maps.push(...uniq(maps)); this.maps.push(...uniq(maps));
} }
useManualOrder(sort: (arr: string[]) => string[]): void { getMap(id: string): IGameMap | null {
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 {
return this.mapData.get(id) ?? null; return this.mapData.get(id) ?? null;
} }
getActiveMap(id: string): ILayerState | null { getActiveMap(id: string): IGameMap | null {
const state = this.mapData.get(id); const state = this.mapData.get(id);
if (!state || !state.active) return null; if (!state || !state.active) return null;
return state; return state;
@ -216,411 +204,66 @@ export class MapState implements IMapState {
this.mapData.get(id)?.setActiveStatus(active); this.mapData.get(id)?.setActiveStatus(active);
} }
*iterateActiveMaps(): Iterable<[string, ILayerState]> { *iterateActiveMaps(): Iterable<[string, IGameMap]> {
for (const [id, state] of this.mapData) { for (const [id, state] of this.mapData) {
if (state.active) yield [id, state]; if (state.active) yield [id, state];
} }
} }
*iterateInactiveMaps(): Iterable<[string, ILayerState]> { *iterateInactiveMaps(): Iterable<[string, IGameMap]> {
for (const [id, state] of this.mapData) { for (const [id, state] of this.mapData) {
if (!state.active) yield [id, state]; if (!state.active) yield [id, state];
} }
} }
iterateAllMaps(): Iterable<[string, ILayerState]> { iterateAllMaps(): Iterable<[string, IGameMap]> {
return this.mapData; return this.mapData;
} }
//#endregion //#endregion
//#region 存档工具 //#region 存档对比
compareWith(ref: Map<string, Map<number, Uint32Array>>): void { compareWith(ref: Map<string, Map<number, Uint32Array>>): void {
if (this.refData !== null) return; if (this.compared) return;
this.refData = ref; this.compared = true;
for (const [id, state] of this.mapData) { for (const [id, state] of this.mapData) {
const refFloor = ref.get(id); const refFloor = ref.get(id);
if (!refFloor) { if (!refFloor) {
state.setDirty(true); state.markDirty(true);
continue; continue;
} }
let dirty = false; state.compareWith(refFloor);
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);
} }
} }
/** //#region 存读档
*
* @param state
*/
private getTriggerMap(state: ILayerState) {
const triggers = new Map<number, Map<number, number>>();
for (const layer of state.layerList) {
const map = layer.getTriggerRef();
if (map.size > 0) {
triggers.set(layer.zIndex, new Map(map));
}
}
return triggers;
}
/**
*
* @param state
*/
private emptySave(state: ILayerState): ILayerStateSave {
return {
background: state.getBackground(),
layers: new Map(),
triggers: this.getTriggerMap(state)
};
}
/**
*
* @param layer
*/
private fullLayer(layer: IMapLayer): IMapLayerSave {
return {
width: layer.width,
height: layer.height,
fullMap: new Uint32Array(layer.getMapRef().array)
};
}
/**
* NoCompression / LowCompression
* @param state
*/
private saveLayerStateFull(state: LayerState): ILayerStateSave {
const background = state.getBackground();
const layers = new Map<number, IMapLayerSave>();
const triggers = new Map<number, Map<number, number>>();
for (const layer of state.layerList) {
const arr = layer.getMapRef().array;
layers.set(layer.zIndex, {
width: layer.width,
height: layer.height,
fullMap: new Uint32Array(arr)
});
triggers.set(layer.zIndex, new Map(layer.getTriggerRef()));
}
return { background, layers, triggers };
}
/**
* HighCompression
* @param layer
* @param refArray
*/
private diffRows(
layer: IMapLayer,
refArray: Uint32Array
): Map<number, Uint32Array> {
const rows = new Map<number, Uint32Array>();
const arr = layer.getMapRef().array;
for (let row = 0; row < layer.height; row++) {
const start = row * layer.width;
const end = start + layer.width;
const slice = arr.subarray(start, end);
const refSlice = refArray.subarray(start, end);
const same = refSlice.every((v, i) => slice[i] === v);
if (!same) {
rows.set(row, new Uint32Array(slice));
}
}
return rows;
}
/**
* LowCompression
* @param id id
* @param state
*/
private isStateEqualToRef(id: string, state: LayerState): boolean {
const refFloor = this.refData?.get(id);
if (!refFloor) return false;
for (const layer of state.layerList) {
const refArray = refFloor.get(layer.zIndex);
if (!refArray) return false;
const cur = layer.getMapRef().array;
if (cur.length !== refArray.length) return false;
if (cur.some((v, i) => v !== refArray[i])) return false;
}
return true;
}
//#endregion
//#region 存档
/**
*
*/
private saveNoCompression(): IMapStoreSave {
const floors = new Map<string, ILayerStateSave>();
for (const [id, state] of this.mapData) {
if (!state.active) continue;
floors.set(id, this.saveLayerStateFull(state));
}
return { floors };
}
/**
*
*/
private saveLowCompression(): IMapStoreSave {
const floors = new Map<string, ILayerStateSave>();
if (this.refData) {
// 包含参考标准时需要对比
for (const [id, state] of this.mapData) {
if (!state.active) continue;
if (state.isDirty() && this.isStateEqualToRef(id, state)) {
floors.set(id, this.saveLayerStateFull(state));
} else {
floors.set(id, this.emptySave(state));
}
}
} else {
// 不包含参考标准时仅看 dirty 标记
for (const [id, state] of this.mapData) {
if (!state.active) continue;
if (state.isDirty()) {
floors.set(id, this.saveLayerStateFull(state));
} else {
floors.set(id, this.emptySave(state));
}
}
}
return { floors };
}
/**
*
*/
private saveHighCompression(): IMapStoreSave {
const floors = new Map<string, ILayerStateSave>();
// 没有参考标准,直接退回低压缩级别
if (!this.refData) return this.saveLowCompression();
for (const [id, state] of this.mapData) {
if (!state.active) continue;
if (state.isDirty()) {
const refFloor = this.refData.get(id);
const layersMap = new Map<number, IMapLayerSave>();
// 对每一个地图的每一行进行遍历,然后仅存储有差别的行
for (const layer of state.layerList) {
const refArray = refFloor?.get(layer.zIndex);
if (refArray) {
const rows = this.diffRows(layer, refArray);
if (rows.size === 0) continue;
layersMap.set(layer.zIndex, {
width: layer.width,
height: layer.height,
rows
});
} else {
layersMap.set(layer.zIndex, this.fullLayer(layer));
}
}
floors.set(id, {
background: state.getBackground(),
layers: layersMap,
triggers: this.getTriggerMap(state)
});
} else {
floors.set(id, this.emptySave(state));
}
}
return { floors };
}
/**
*
* @param save
* @param layer
*/
private loadTriggers(save: ILayerStateSave, layer: IMapLayer) {
const triggers = save.triggers.get(layer.zIndex);
layer.clearTrigger();
if (triggers) {
layer.setTriggerRef(new Map(triggers));
}
}
/**
* NoCompression fullMap
* @param state
*/
private loadNoCompression(state: IMapStoreSave): void {
for (const [id, cur] of this.mapData) {
cur.setActiveStatus(state.floors.has(id));
}
for (const [id, save] of state.floors) {
const cur = this.mapData.get(id);
if (!cur) {
logger.warn(122, id);
continue;
}
cur.setBackground(save.background);
for (const layer of cur.layerList) {
// 地图
const layerSave = save.layers.get(layer.zIndex);
if (layerSave?.fullMap) {
layer.setMapRef(new Uint32Array(layerSave.fullMap));
}
// 触发器
this.loadTriggers(save, layer);
}
// 需要额外进行判断是否与参考地图相同
if (this.isStateEqualToRef(id, cur)) {
cur.setDirty(false);
} else {
cur.setDirty(true);
}
}
}
/**
* LowCompression
* - layers dirty fullMap
* - layers dirty
* @param state
*/
private loadLowCompression(state: IMapStoreSave): void {
if (!this.refData) {
logger.error(55);
return;
}
for (const [id, cur] of this.mapData) {
cur.setActiveStatus(state.floors.has(id));
}
for (const [id, save] of state.floors) {
const cur = this.mapData.get(id);
const refFloor = this.refData.get(id);
if (!cur) {
logger.warn(122, id);
continue;
}
if (!refFloor) {
logger.warn(124, id);
continue;
}
cur.setBackground(save.background);
let shouldDirty = false;
for (const layer of cur.layerList) {
// 地图
const layerSave = save.layers.get(layer.zIndex);
if (layerSave?.fullMap) {
layer.setMapRef(new Uint32Array(layerSave.fullMap));
shouldDirty = true;
} else {
const refArray = refFloor.get(layer.zIndex);
if (!refArray) {
logger.warn(124, id);
return;
}
layer.setMapRef(new Uint32Array(refArray));
}
// 触发器
this.loadTriggers(save, layer);
}
cur.setDirty(shouldDirty);
}
}
/**
* HighCompression
* - layers dirty
* - layers dirty rows
* @param state
*/
private loadHighCompression(state: IMapStoreSave): void {
if (!this.refData) {
logger.error(55);
return;
}
for (const [id, cur] of this.mapData) {
cur.setActiveStatus(state.floors.has(id));
}
for (const [id, save] of state.floors) {
const cur = this.mapData.get(id);
const refFloor = this.refData.get(id);
if (!cur) {
logger.warn(122, id);
continue;
}
if (!refFloor) {
logger.warn(124, id);
continue;
}
cur.setBackground(save.background);
let shouldDirty = false;
for (const layer of cur.layerList) {
const refArray = refFloor.get(layer.zIndex);
if (!refArray) {
logger.warn(124, id);
continue;
}
// 地图
const layerSave = save.layers.get(layer.zIndex);
if (!layerSave?.rows || layerSave.rows.size === 0) {
// 图层无变化或非 dirty 楼层,从参考基准恢复
layer.setMapRef(new Uint32Array(refArray));
} else {
// 以参考基准为底,叠加差分行
shouldDirty = true;
const buf = new Uint32Array(refArray);
for (const [rowIdx, rowData] of layerSave.rows) {
buf.set(rowData, rowIdx * layer.width);
}
layer.setMapRef(buf);
}
// 触发器
this.loadTriggers(save, layer);
}
cur.setDirty(shouldDirty);
}
}
saveState(compression: SaveCompression): IMapStoreSave { saveState(compression: SaveCompression): IMapStoreSave {
if (compression === SaveCompression.HighCompression) { const floors = new Map<string, IGameMapSave>();
return this.saveHighCompression(); for (const [id, state] of this.mapData) {
} else if (compression === SaveCompression.LowCompression) { if (!state.active) continue;
return this.saveLowCompression(); floors.set(id, state.saveState(compression));
} else {
return this.saveNoCompression();
} }
return { floors };
} }
loadState(state: IMapStoreSave, compression: SaveCompression): void { loadState(state: IMapStoreSave, compression: SaveCompression): void {
if (compression === SaveCompression.HighCompression) { if (compression !== SaveCompression.NoCompression && !this.compared) {
this.loadHighCompression(state); logger.error(55);
} else if (compression === SaveCompression.LowCompression) { return;
this.loadLowCompression(state); }
} else { for (const [id, cur] of this.mapData) {
this.loadNoCompression(state); 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);
} }
} }

View File

@ -1,7 +1,6 @@
import { ITileLocator, logger } from '@motajs/common'; import { ITileLocator, logger } from '@motajs/common';
import { import {
getFaceMovement, getFaceMovement,
IMoverController,
ObjectMover, ObjectMover,
ObjectMoveStep, ObjectMoveStep,
ObjectMoveType ObjectMoveType
@ -9,8 +8,6 @@ import {
import { IDynamicTile } from './types'; import { IDynamicTile } from './types';
import { DYNAMIC_MOVER_FACE } from '../shared'; import { DYNAMIC_MOVER_FACE } from '../shared';
//#region 动态图块
const enum DynamicMoveCode { const enum DynamicMoveCode {
/** 正常执行 */ /** 正常执行 */
Success Success
@ -94,5 +91,3 @@ export class DynamicTileMover extends ObjectMover<IDynamicTile> {
return Promise.resolve(); return Promise.resolve();
} }
} }
//#endregion

View File

@ -0,0 +1,59 @@
import { ITileRawData } from '@user/data-common';
import {
IDynamicTile,
IMapLayer,
IStaticBlockSave,
IStaticTile
} from './types';
import { MapTileBase } from './tile';
export class StaticTile
extends MapTileBase<IStaticBlockSave>
implements IStaticTile
{
constructor(x: number, y: number, layer: IMapLayer) {
super(x, y, layer);
}
num(): number {
return this.layer.getBlock(this.locator.x, this.locator.y);
}
raw(): ITileRawData | null {
return this.state.tileStore.getData(this.num());
}
set(num: number): void {
this.layer.setBlock(num, this.locator.x, this.locator.y);
}
toDynamic(): IDynamicTile {
return this.layer.transferToDynamic(this.locator.x, this.locator.y)!;
}
shouldSave(): boolean {
return !!this.triggers;
}
saveState(): Readonly<IStaticBlockSave> {
const save: IStaticBlockSave = {};
if (this.triggers) {
save.triggers = this.triggers;
}
return save;
}
loadState(save: Readonly<IStaticBlockSave>): void {
if (save.triggers) {
if (save.triggers.size === 0) {
this.useEmptyTrigger();
} else {
save.triggers.forEach(v => this.addTrigger(v));
}
} else {
this.clearTrigger();
}
}
}

View File

@ -0,0 +1,70 @@
import { ITileLocator } from '@motajs/common';
import {
FaceDirection,
IDataCommon,
IDataCommonExtended,
IMapBlockRawData,
ISaveableContent,
ITileRawData
} from '@user/data-common';
import { IMapBlockSaveBase, IMapLayer, ITileBase } from './types';
export abstract class MapTileBase<TSave extends IMapBlockSaveBase>
implements ITileBase, IDataCommonExtended, ISaveableContent<Readonly<TSave>>
{
readonly state: IDataCommon;
readonly layer: IMapLayer;
locator: ITileLocator;
triggers: Set<number> | null = null;
constructor(x: number, y: number, layer: IMapLayer) {
this.layer = layer;
this.state = layer.state;
this.locator = { x, y };
}
abstract num(): number;
abstract raw(): ITileRawData | null;
abstract set(num: number): void;
block(): IMapBlockRawData | null {
return null;
}
setFaceDirection(direction: FaceDirection): number {
const cur = this.num();
const next = this.layer.faceBinder.getFaceOf(cur, direction);
if (next) {
this.set(next.identifier);
return next.identifier;
} else {
return cur;
}
}
clearTrigger(): void {
this.triggers = null;
}
addTrigger(trigger: number): void {
if (!this.triggers) {
this.triggers = new Set();
}
this.triggers.add(trigger);
}
deleteTrigger(trigger: number): void {
if (!this.triggers) return;
this.triggers.delete(trigger);
}
useEmptyTrigger(): void {
this.triggers = new Set();
}
abstract saveState(): Readonly<TSave>;
abstract loadState(save: Readonly<TSave>): void;
}

View File

@ -7,6 +7,7 @@ import {
import { import {
FaceDirection, FaceDirection,
IDataCommonExtended, IDataCommonExtended,
ILocationHelper,
IMapBlockRawData, IMapBlockRawData,
IMapRawData, IMapRawData,
IMoverController, IMoverController,
@ -18,7 +19,135 @@ import {
} from '@user/data-common'; } from '@user/data-common';
import { ITileStore } from '@user/data-common'; import { ITileStore } from '@user/data-common';
//#region 静态图层 //#region 图块信息
export interface IMapBlockSaveBase {
/** 此图块的触发器 */
triggers?: ReadonlySet<number>;
}
export interface IStaticBlockSave extends IMapBlockSaveBase {}
export interface IDynamicBlockSave extends IMapBlockSaveBase {
/** 此动态图块的图块数字 */
num: number;
}
export interface ITileBase {
/** 该图块所处的位置 */
readonly locator: ITileLocator;
/** 该图块所处的图层对象 */
readonly layer: IMapLayer;
/** 包含的触发器列表,不存在时表示使用图块本身的触发器 */
readonly triggers: ReadonlySet<number> | null;
/**
*
*/
raw(): ITileRawData | null;
/**
*
*/
block(): IMapBlockRawData | null;
/**
*
*/
num(): number;
/**
*
* @param num
*/
set(num: number): void;
/**
* {@link num}
* @param direction
*/
setFaceDirection(direction: FaceDirection): number;
/**
* 退
*/
clearTrigger(): void;
/**
*
* @param trigger
*/
addTrigger(trigger: number): void;
/**
*
* @param trigger
*/
deleteTrigger(trigger: number): void;
/**
*
*/
useEmptyTrigger(): void;
}
export interface IStaticTile
extends
ITileBase,
IDataCommonExtended,
ISaveableContent<Readonly<IStaticBlockSave>> {
/**
*
*/
toDynamic(): IDynamicTile;
/**
*
*/
shouldSave(): boolean;
}
export interface IDynamicTile
extends
ITileBase,
IObjectMovable,
IDataCommonExtended,
ISaveableContent<Readonly<IDynamicBlockSave>> {
/** 该动态图块的移动器 */
readonly mover: IObjectMover<IDynamicTile>;
/** 该动态图块所属的动态图层 */
readonly layer: IMapLayer;
/**
* `null`
*
*/
toStatic(): IStaticTile | null;
/**
* `null`
* `toStatic`
*/
toStaticIfSafe(): IStaticTile | null;
/**
* 便 `tile.mover` 访
*
*
* ```ts
* mover.step(dir, count);
* return mover.start();
* ```
*/
step(dir: FaceDirection, count?: number): IMoverController | null;
/**
*
*/
delete(): Promise<void>;
}
//#region 地图图层
export interface IMapLayerData { export interface IMapLayerData {
/** 当前引用是否过期,当地图图层内部的地图数组引用更新时,此项会变为 `true` */ /** 当前引用是否过期,当地图图层内部的地图数组引用更新时,此项会变为 `true` */
@ -32,14 +161,10 @@ export interface ILayerLocation {
readonly locator: ITileLocator; readonly locator: ITileLocator;
/** 该点的静态图块数字 */ /** 该点的静态图块数字 */
readonly tile: number; readonly tile: number;
/** 该点的静态图块信息 */
readonly raw: ITileRawData | null;
/** 该点的静态触发器,-1 表示未设置(即使用图块本身的触发器),否则表示该点的静态触发器,覆盖图块本身的触发器 */
readonly trigger: number;
/** 该点包含的所有动态图块 */ /** 该点包含的所有动态图块 */
readonly dynamics: Iterable<IDynamicTile>; readonly dynamics: Iterable<IDynamicTile>;
/** 该点的静态图块原始数据 */ /** 该点的静态图块原始数据 */
readonly block: IMapBlockRawData; readonly static: IStaticTile;
} }
export interface IMapLayerHooks extends IHookBase { export interface IMapLayerHooks extends IHookBase {
@ -51,7 +176,7 @@ export interface IMapLayerHooks extends IHookBase {
onResize(width: number, height: number): void; onResize(width: number, height: number): void;
/** /**
* * `putMapData`
* @param x * @param x
* @param y * @param y
* @param width * @param width
@ -60,7 +185,7 @@ export interface IMapLayerHooks extends IHookBase {
onUpdateArea(x: number, y: number, width: number, height: number): void; onUpdateArea(x: number, y: number, width: number, height: number): void;
/** /**
* * `setBlock`
* @param block * @param block
* @param x * @param x
* @param y * @param y
@ -81,6 +206,24 @@ export interface IMapLayerHooks extends IHookBase {
* @param y * @param y
*/ */
onCloseDoor(num: number, x: number, y: number): Promise<void>; onCloseDoor(num: number, x: number, y: number): Promise<void>;
/**
*
* @param tile
*/
onCreateDynamic?(tile: IDynamicTile): void;
/**
*
* @param tile
*/
onDeleteDynamic?(tile: IDynamicTile): Promise<void>;
/**
*
* @param tile
*/
onUpdateDynamicPosition?(tile: IDynamicTile): void;
} }
export interface IMapLayerHookController extends IHookController<IMapLayerHooks> { export interface IMapLayerHookController extends IHookController<IMapLayerHooks> {
@ -93,36 +236,9 @@ export interface IMapLayerHookController extends IHookController<IMapLayerHooks>
getMapData(): Readonly<IMapLayerData>; getMapData(): Readonly<IMapLayerData>;
} }
export interface IMapLayer interface ILayerStatic {
extends
IHookable<IMapLayerHooks, IMapLayerHookController>,
IDataCommonExtended {
/** 地图宽度 */
readonly width: number;
/** 地图高度 */
readonly height: number;
/** /**
* *
* `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 block
* @param x * @param x
* @param y * @param y
@ -130,7 +246,7 @@ export interface IMapLayer
setBlock(block: number, x: number, y: number): void; setBlock(block: number, x: number, y: number): void;
/** /**
* *
* @param x * @param x
* @param y * @param y
* @returns 0 -1 * @returns 0 -1
@ -138,41 +254,14 @@ export interface IMapLayer
getBlock(x: number, y: number): number; getBlock(x: number, y: number): number;
/** /**
* *
* @param x
* @param y
*/
getLocationData(x: number, y: number): ILayerLocation | null;
/**
* 退
* @param x * @param x
* @param y * @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 array
* @param x * @param x
* @param y * @param y
@ -199,9 +288,8 @@ export interface IMapLayer
): Uint32Array; ): Uint32Array;
/** /**
* * 使
* `MapStore` 使 * `width * height`
* `width * height`
* *
* @param array * @param array
*/ */
@ -213,15 +301,170 @@ export interface IMapLayer
getMapRef(): IMapLayerData; getMapRef(): IMapLayerData;
/** /**
* 使 *
* @param triggers * @param x
* @param y
* @param direction
* @returns
*/ */
setTriggerRef(triggers: Map<number, number>): void; setStaticDirection(x: number, y: number, direction: FaceDirection): number;
/** /**
* 使 *
*/ */
getTriggerRef(): ReadonlyMap<number, number>; iterateBlocks(): Iterable<ILayerLocation>;
}
interface ILayerDynamic {
/**
*
* @param num
* @param x
* @param y
*/
createDynamic(num: number, x: number, y: number): IDynamicTile;
/**
*
* @param x
* @param y
* @param keepTrigger
*/
transferToDynamic(
x: number,
y: number,
keepTrigger?: boolean
): IDynamicTile | null;
/**
*
* @param tile
* @param keepTrigger 退
*/
transferToStatic(
tile: IDynamicTile,
keepTrigger?: boolean
): IStaticTile | null;
/**
*
* @param tile
* @param keepTrigger 退
*/
transferToStaticIfSafe(
tile: IDynamicTile,
keepTrigger?: boolean
): IStaticTile | null;
/**
*
* @param tile
*/
deleteDynamic(tile: IDynamicTile): Promise<void>;
/**
*
* @param x
* @param y
*/
getDynamicTilesAt(x: number, y: number): Iterable<IDynamicTile>;
/**
*
*/
iterateDynamicTiles(): Iterable<IDynamicTile>;
/**
*
* @param tile
* @param direction
* @returns
*/
setDynamicDirection(tile: IDynamicTile, direction: FaceDirection): number;
/**
*
* @param tile
*/
updateDynamicTile(tile: IDynamicTile): void;
}
export interface IMapLayerSave {
/** 地图宽度 */
readonly width: number;
/** 地图高度 */
readonly height: number;
/**
* Uint32Array
* HighCompression 使
*/
readonly rows?: ReadonlyMap<number, Uint32Array>;
/** 完整地图,当使用 `NoCompression` 和 `LowCompression` 时使用此接口 */
readonly fullMap?: Uint32Array;
/**
* `LowCompression` `HighCompression`
*
*/
readonly staticBlocks?: ReadonlyMap<number, Readonly<IStaticBlockSave>>;
/**
* `LowCompression` `HighCompression`
*
*/
readonly dynamicBlocks?: ReadonlyMap<number, Readonly<IDynamicBlockSave>[]>;
}
export interface IMapLayer
extends
IHookable<IMapLayerHooks, IMapLayerHookController>,
IDataCommonExtended,
ILayerStatic,
ILayerDynamic,
ISaveableContent<IMapLayerSave> {
/** 地图宽度 */
readonly width: number;
/** 地图高度 */
readonly height: number;
/**
*
* `true` `false`
*/
readonly empty: boolean;
/** 图层纵深 */
readonly zIndex: number;
/** 当前图层所属的地图状态对象 */
readonly map: IGameMap;
/** 该图层使用的朝向绑定器 */
readonly faceBinder: IRoleFaceBinder;
/**
*
* @param data
*/
compareWith(data: Uint32Array): void;
/**
*
* @param x
* @param y
*/
inMap(x: number, y: number): boolean;
/**
* 使
* @param binder
*/
setFaceBinder(binder: IRoleFaceBinder | null): void;
/**
*
* @param x
* @param y
*/
getLocationData(x: number, y: number): ILayerLocation | null;
/** /**
* *
@ -243,54 +486,53 @@ export interface IMapLayer
* @param y * @param y
*/ */
closeDoor(num: number, x: number, y: number): Promise<void>; closeDoor(num: number, x: number, y: number): Promise<void>;
/**
*
* `true` `true`
*
*/
dirty(): boolean;
/**
*
* @param dirty
*/
markDirty(dirty: boolean): void;
}
export interface IResizableMapLayer extends IMapLayer {
/**
*
* @param width
* @param height
*/
resize(width: number, height: number): void;
/**
*
* @param width
* @param height
*/
resize2(width: number, height: number): void;
} }
//#endregion //#endregion
//#region 图层管理 //#region 图层管理
export interface ILayerStateHooks extends IHookBase { export interface IGameMapHooks extends IHookBase {
/** /**
* *
* @param tile * @param tile
*/ */
onChangeBackground(tile: number): void; onChangeBackground?(tile: number): void;
/** /**
* *
* @param layerList * @param layerList
*/ */
onUpdateLayer(layerList: Set<IMapLayer>): void; onUpdateLayer?(layerList: Set<IMapLayer>): void;
/**
*
* @param layer
* @param x
* @param y
* @param width
* @param height
*/
onUpdateLayerArea(
layer: IMapLayer,
x: number,
y: number,
width: number,
height: number
): void;
/**
*
* @param layer
* @param block
* @param x
* @param y
*/
onUpdateLayerBlock(
layer: IMapLayer,
block: number,
x: number,
y: number
): void;
/** /**
* *
@ -298,11 +540,23 @@ export interface ILayerStateHooks extends IHookBase {
* @param width * @param width
* @param height * @param height
*/ */
onResizeLayer(layer: IMapLayer, width: number, height: number): void; onResizeLayer?(layer: IMapLayer, width: number, height: number): void;
} }
export interface ILayerState export interface IGameMapSave {
extends IHookable<ILayerStateHooks>, IDataCommonExtended { /** 楼层背景 */
readonly background: number;
/** 该地图每一个图层的存档信息,键表示纵深,值对应图层存档数据 */
readonly layers: ReadonlyMap<number, IMapLayerSave>;
}
export interface IGameMap
extends
IHookable<IGameMapHooks>,
IDataCommonExtended,
ISaveableContent<IGameMapSave> {
/** 坐标索引器,用于坐标与扁平索引之间的双向转换 */
readonly indexer: ILocationHelper;
/** 地图列表 */ /** 地图列表 */
readonly layerList: Set<IMapLayer>; readonly layerList: Set<IMapLayer>;
/** 当前楼层共享的图块定义 store */ /** 当前楼层共享的图块定义 store */
@ -352,6 +606,12 @@ export interface ILayerState
*/ */
getLayerAlias(layer: IMapLayer): string | undefined; getLayerAlias(layer: IMapLayer): string | undefined;
/**
*
* @param data
*/
compareWith(data: Map<number, Uint32Array>): void;
/** /**
* *
* @param width * @param width
@ -384,47 +644,26 @@ export interface ILayerState
setEventLayer(layer: IMapLayer | null): void; setEventLayer(layer: IMapLayer | null): void;
/** /**
* *
* `true` `true`
*
*/ */
isDirty(): boolean; dirty(): boolean;
/** /**
* *
* @param dirty
*/ */
setDirty(dirty: boolean): void; markDirty(dirty: boolean): void;
} }
//#endregion //#endregion
//#region 楼层管理 //#region 楼层管理
export interface IMapLayerSave {
readonly width: number;
readonly height: number;
/**
* key = value = Uint32Array
* HighCompression 使
* Map
*/
readonly rows?: ReadonlyMap<number, Uint32Array>;
/** 完整地图,当使用 `NoCompression` 和 `LowCompression` 时使用此接口 */
readonly fullMap?: Uint32Array;
}
export interface ILayerStateSave {
/** 楼层背景 */
readonly background: number;
/** key = zIndexvalue = 对应图层存档数据 */
readonly layers: ReadonlyMap<number, IMapLayerSave>;
/** 静态触发器覆盖映射,仅在存在覆盖时写入 */
readonly triggers: ReadonlyMap<number, ReadonlyMap<number, number>>;
}
export interface IMapStoreSave { export interface IMapStoreSave {
/** key = 楼层 id只包含 active 的楼层inactive 的楼层不写入,读档时无需处理 */ /** 键表示楼层 id只包含已激活的楼层未激活的楼层不写入 */
readonly floors: ReadonlyMap<string, ILayerStateSave>; readonly floors: ReadonlyMap<string, IGameMapSave>;
} }
export interface IMapAreaInterval { export interface IMapAreaInterval {
@ -441,34 +680,34 @@ export interface IMapState
/** 所有楼层的 id 有序数组 */ /** 所有楼层的 id 有序数组 */
readonly maps: ReadonlyArray<string>; readonly maps: ReadonlyArray<string>;
/**
* id null
* @param id id
*/
getLayerState(id: string): ILayerState | null;
/**
* id active null
* @param id id
*/
getActiveMap(id: string): ILayerState | null;
/**
*
* @param raw
*/
fromRaw(raw: IMapRawData): ILayerState | null;
/** /**
* id * id
* @param id id * @param id id
* @param width * @param width
* @param height * @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 * @param id id
*/ */
isMapActive(id: string): boolean; isMapActive(id: string): boolean;
@ -481,23 +720,23 @@ export interface IMapState
setMapActiveStatus(id: string, active: boolean): void; 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 = zIndexvalue = * @param ref id
*/ */
compareWith(ref: Map<string, Map<number, Uint32Array>>): void; compareWith(ref: Map<string, Map<number, Uint32Array>>): void;
@ -507,13 +746,6 @@ export interface IMapState
*/ */
setMapList(maps: string[]): void; setMapList(maps: string[]): void;
/**
* 使 maps
*
* @param sort
*/
useManualOrder(sort: (arr: string[]) => string[]): void;
/** /**
* *
* @param areas * @param areas
@ -527,7 +759,7 @@ export interface IMapState
activeArea(id: string): void; activeArea(id: string): void;
/** /**
* *
* @param id id * @param id id
*/ */
deactiveArea(id: string): void; deactiveArea(id: string): void;
@ -539,169 +771,11 @@ export interface IMapState
useAutoActivitor(enable: boolean): void; useAutoActivitor(enable: boolean): void;
/** /**
* *
*
* @param id id * @param id id
*/ */
notifyEnterFloor(id: string): void; notifyEnterFloor(id: string): void;
} }
//#endregion //#endregion
//#region 动态图块
export interface IDynamicLayerHooks extends IHookBase {
/**
*
* @param tile
* @param layer
*/
onCreateTile?(tile: IDynamicTile, layer: IDynamicLayer): void;
/**
*
* @param tile
* @param layer
*/
onDeleteTile?(tile: IDynamicTile, layer: IDynamicLayer): Promise<void>;
/**
* 使 `mover`
* @param tile
* @param layer
*/
onUpdateTilePosition?(tile: IDynamicTile, layer: IDynamicLayer): void;
}
export interface IDynamicLayer
extends IHookable<IDynamicLayerHooks>, IDataCommonExtended {
/** 当前动态图层所属的静态图层 */
readonly layer: IMapLayer;
/**
*
* @param num
* @param x
* @param y
* @returns
*/
createDynamic(num: number, x: number, y: number): IDynamicTile;
/**
*
* 0 `num = 0`
* @param x
* @param y
* @returns
*/
transferToDynamic(
x: number,
y: number,
keepTrigger?: boolean
): IDynamicTile;
/**
*
* {@link IDynamicLayerHooks.onDeleteTile}
* @param tile
*/
transferToStatic(tile: IDynamicTile, keepTrigger?: boolean): void;
/**
* 0
* @param tile
* @returns
*/
transferToStaticIfSafe(tile: IDynamicTile, keepTrigger?: boolean): boolean;
/**
* {@link IDynamicLayerHooks.onDeleteTile}
*
* @param tile
*/
deleteDynamic(tile: IDynamicTile): Promise<void>;
/**
*
* @param x
* @param y
*/
getDynamicTilesAt(x: number, y: number): Iterable<IDynamicTile>;
/**
*
*/
iterateDynamicTiles(): Iterable<IDynamicTile>;
/**
* `tile.num`
*
* @param tile
* @param direction
*/
setDynamicDirection(tile: IDynamicTile, direction: FaceDirection): void;
/**
*
* @param tile
*/
updateDynamicTile(tile: IDynamicTile): void;
}
export interface IDynamicTile extends IObjectMovable, IDataCommonExtended {
/** 当前图块数字 */
readonly num: number;
/** 当前动态图块携带的触发器类型,-1 表示无触发器 */
readonly triggerType: number;
/** 当前图块所属的动态图层 */
readonly layer: IDynamicLayer;
/** 当前动态图块的移动器 */
readonly mover: IObjectMover<IDynamicTile>;
/** 当前动态图块的图块数据 */
readonly raw: ITileRawData | null;
/**
* {@link num}
* @param direction
*/
setFaceDirection(direction: FaceDirection): number;
/**
*
* @param type
*/
setTriggerType(type: number): void;
/**
*
*/
delete(): Promise<void>;
/**
*
*/
toStatic(): void;
/**
* 西
*/
toStaticIfSafe(): boolean;
/**
* 便 `tile.mover` 访
*
*
* ```ts
* mover.step(dir, count);
* return mover.start();
* ```
*/
step(dir: FaceDirection, count?: number): IMoverController | null;
/**
*
* @param binder
*/
setFaceBinder(binder: IRoleFaceBinder | null): void;
}
//#endregion

View File

@ -8,12 +8,12 @@ import {
IReplayArray, IReplayArray,
IReplayReadStream, IReplayReadStream,
IReplaySandbox, IReplaySandbox,
IReplaySandboxHook, IReplaySandboxHooks,
IReplaySystem IReplaySystem
} from './types'; } from './types';
export class ReplaySandbox export class ReplaySandbox
extends Hookable<IReplaySandboxHook> extends Hookable<IReplaySandboxHooks>
implements IReplaySandbox implements IReplaySandbox
{ {
pausing: boolean = true; pausing: boolean = true;
@ -41,8 +41,8 @@ export class ReplaySandbox
} }
protected createController( protected createController(
hook: Partial<IReplaySandboxHook> hook: Partial<IReplaySandboxHooks>
): IHookController<IReplaySandboxHook> { ): IHookController<IReplaySandboxHooks> {
return new HookController(this, hook); return new HookController(this, hook);
} }

View File

@ -10,7 +10,7 @@ import {
IReplaySandbox, IReplaySandbox,
IReplaySandboxConfig, IReplaySandboxConfig,
IReplaySystem, IReplaySystem,
IReplaySystemHook, IReplaySystemHooks,
ReplayCommandWidth, ReplayCommandWidth,
ReplayParamValue ReplayParamValue
} from './types'; } from './types';
@ -18,7 +18,7 @@ import { ReplayArray } from './array';
import { ReplaySandbox } from './sandbox'; import { ReplaySandbox } from './sandbox';
export class ReplaySystem export class ReplaySystem
extends Hookable<IReplaySystemHook> extends Hookable<IReplaySystemHooks>
implements IReplaySystem implements IReplaySystem
{ {
replaying: boolean = false; replaying: boolean = false;
@ -42,8 +42,8 @@ export class ReplaySystem
} }
protected createController( protected createController(
hook: Partial<IReplaySystemHook> hook: Partial<IReplaySystemHooks>
): IHookController<IReplaySystemHook> { ): IHookController<IReplaySystemHooks> {
return new HookController(this, hook); return new HookController(this, hook);
} }

View File

@ -20,7 +20,7 @@ export interface IReplayCommand {
execute(step: IReplayStepHandler): Promise<boolean>; execute(step: IReplayStepHandler): Promise<boolean>;
} }
export interface IReplaySandboxHook extends IHookBase { export interface IReplaySandboxHooks extends IHookBase {
/** /**
* *
* @param step * @param step
@ -54,7 +54,7 @@ export interface IReplaySandboxHook extends IHookBase {
onResumeReplay?(): void; onResumeReplay?(): void;
} }
export interface IReplaySandbox extends IHookable<IReplaySandboxHook> { export interface IReplaySandbox extends IHookable<IReplaySandboxHooks> {
/** 是否处于暂停状态 */ /** 是否处于暂停状态 */
readonly pausing: boolean; readonly pausing: boolean;
/** 当前录像是否已经播放完毕 */ /** 当前录像是否已经播放完毕 */
@ -282,7 +282,7 @@ export interface IReplayArray {
): void; ): void;
} }
export interface IReplaySystemHook extends IHookBase { export interface IReplaySystemHooks extends IHookBase {
/** /**
* *
* @param sandbox * @param sandbox
@ -313,7 +313,7 @@ export interface IReplaySandboxConfig {
save?: Map<string, unknown>; save?: Map<string, unknown>;
} }
export interface IReplaySystem extends IHookable<IReplaySystemHook> { export interface IReplaySystem extends IHookable<IReplaySystemHooks> {
/** 当前是否处在录像播放状态 */ /** 当前是否处在录像播放状态 */
readonly replaying: boolean; readonly replaying: boolean;
/** 当前正在播放的录像沙箱实例 */ /** 当前正在播放的录像沙箱实例 */

View File

@ -330,11 +330,7 @@ export class CoreState implements ICoreState {
const reference = new Map<string, Map<number, Uint32Array>>(); const reference = new Map<string, Map<number, Uint32Array>>();
for (const id of floors) { for (const id of floors) {
const floor = data[id]; const floor = data[id];
const state = this.maps.createLayerState( const state = this.maps.createMap(id, floor.width, floor.height);
id,
floor.width,
floor.height
);
const bg = state.addLayer(); const bg = state.addLayer();
const bg2 = state.addLayer(); const bg2 = state.addLayer();
const event = state.addLayer(); const event = state.addLayer();

View File

@ -46,7 +46,7 @@ export class DefaultHeroMoveTopImpl implements IHeroMoveTopImpl {
inBound(x: number, y: number, floorId: string | undefined): boolean { inBound(x: number, y: number, floorId: string | undefined): boolean {
if (isNil(floorId)) return false; if (isNil(floorId)) return false;
const layerState = this.maps.getLayerState(floorId); const layerState = this.maps.getMap(floorId);
if (!layerState) return false; if (!layerState) return false;
const { width, height } = layerState; const { width, height } = layerState;
return x >= 0 && y >= 0 && x < width && y < height; return x >= 0 && y >= 0 && x < width && y < height;
@ -66,7 +66,7 @@ export class DefaultHeroMoveTopImpl implements IHeroMoveTopImpl {
return true; return true;
} }
const map = this.maps.getLayerState(floorId); const map = this.maps.getMap(floorId);
if (!map) return false; if (!map) return false;
const event = map.eventLayer; const event = map.eventLayer;
if (!event) return false; if (!event) return false;
@ -115,7 +115,7 @@ export class DefaultHeroMoveTopImpl implements IHeroMoveTopImpl {
shouldHit(handler: IHeroMoveTopHandler): boolean { shouldHit(handler: IHeroMoveTopHandler): boolean {
const { nextLoc, floorId } = handler; const { nextLoc, floorId } = handler;
if (isNil(floorId)) return false; if (isNil(floorId)) return false;
const layerState = this.maps.getLayerState(floorId); const layerState = this.maps.getMap(floorId);
if (!layerState) return false; if (!layerState) return false;
const eventLayer = layerState.eventLayer; const eventLayer = layerState.eventLayer;
if (!eventLayer) return false; if (!eventLayer) return false;
@ -146,7 +146,7 @@ export class DefaultHeroMoveTopImpl implements IHeroMoveTopImpl {
): Promise<void> { ): Promise<void> {
if (isNil(handler.floorId)) return Promise.resolve(); 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(); if (!map) return Promise.resolve();
const event = map.eventLayer; const event = map.eventLayer;
if (!event) return Promise.resolve(); if (!event) return Promise.resolve();

View File

@ -8,7 +8,7 @@ import {
import { import {
ICombatFlow, ICombatFlow,
ICombatFlowHandler, ICombatFlowHandler,
ICombatFlowHook, ICombatFlowHooks,
ICombatScript, ICombatScript,
IDamageContext, IDamageContext,
IEnemyContext, IEnemyContext,
@ -25,7 +25,7 @@ import {
} from '@user/data-base'; } from '@user/data-base';
export class CombatFlow<TEnemy, THero> export class CombatFlow<TEnemy, THero>
extends Hookable<ICombatFlowHook<TEnemy, THero>> extends Hookable<ICombatFlowHooks<TEnemy, THero>>
implements ICombatFlow<TEnemy, THero> implements ICombatFlow<TEnemy, THero>
{ {
hero: IHeroAttribute<THero> | null = null; hero: IHeroAttribute<THero> | null = null;
@ -42,8 +42,8 @@ export class CombatFlow<TEnemy, THero>
//#region 对象控制 //#region 对象控制
protected createController( protected createController(
hook: Partial<ICombatFlowHook<TEnemy, THero>> hook: Partial<ICombatFlowHooks<TEnemy, THero>>
): IHookController<ICombatFlowHook<TEnemy, THero>> { ): IHookController<ICombatFlowHooks<TEnemy, THero>> {
return new HookController(this, hook); return new HookController(this, hook);
} }

View File

@ -750,7 +750,7 @@ export interface ICombatFlowHandler<TEnemy, THero> extends IDataBaseExtended {
readonly locator: ITileLocator; readonly locator: ITileLocator;
} }
export interface ICombatFlowHook<TEnemy, THero> extends IHookBase { export interface ICombatFlowHooks<TEnemy, THero> extends IHookBase {
/** /**
* *
* @param info * @param info
@ -799,7 +799,7 @@ export interface ICombatResult<TEnemy, THero> {
} }
export interface ICombatFlow<TEnemy, THero> export interface ICombatFlow<TEnemy, THero>
extends IHookable<ICombatFlowHook<TEnemy, THero>>, IDataBaseExtended { extends IHookable<ICombatFlowHooks<TEnemy, THero>>, IDataBaseExtended {
/** 勇士属性对象 */ /** 勇士属性对象 */
readonly hero: IReadonlyHeroAttribute<THero> | null; readonly hero: IReadonlyHeroAttribute<THero> | null;
/** 怪物上下文对象 */ /** 怪物上下文对象 */

View File

@ -1,6 +1,6 @@
import { ITileLocator } from '@motajs/common'; import { ITileLocator } from '@motajs/common';
import { import {
ILayerState, IGameMap,
IMapLayer, IMapLayer,
IStateBase, IStateBase,
IDataBaseExtended IDataBaseExtended
@ -21,7 +21,7 @@ export interface ITriggerHandler {
/** 当前全局状态对象 */ /** 当前全局状态对象 */
readonly state: IStateBase; readonly state: IStateBase;
/** 当前楼层状态对象 */ /** 当前楼层状态对象 */
readonly layer?: ILayerState; readonly layer?: IGameMap;
/** 当前参与触发的图层对象 */ /** 当前参与触发的图层对象 */
readonly mapLayer?: IMapLayer; readonly mapLayer?: IMapLayer;
/** 当前触发点定位符 */ /** 当前触发点定位符 */

View File

@ -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.", "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'.", "126": "Only move code '$1' is expected, but got '$2'.",
"127": "Block at $1,$2 is 0, the dynamic tile will have num 0.", "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.", "128": "Cannot transfer $1 to $2, since target position $3,$4 out of bounds.",
"129": "Position $1,$2 is not empty, existing block will be overwritten.", "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.", "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.", "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.", "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.", "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.", "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.", "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.", "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.", "144": "A hero move top implementation object binding is required for hero moving.",
"145": "No tile registered for item '$1'.", "145": "No tile registered for item '$1'.",
"146": "No equipment with uid of $1 in hero's inventory.", "146": "No equipment with uid of $1 in hero's inventory.",