mirror of
https://github.com/motajs/template.git
synced 2026-05-02 12:23:13 +08:00
refactor: 地图存储
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
parent
476f735adc
commit
70a58ef4dc
294
docs/dev/map-store-save.md
Normal file
294
docs/dev/map-store-save.md
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
# 需求综述
|
||||||
|
|
||||||
|
当前 `LayerState` 只存储当前激活地图的数据,切换地图时原地图内容彻底丢失,
|
||||||
|
无法参与存档系统。为此引入 `IMapStore`,集中管理所有楼层的 `LayerState`,
|
||||||
|
并实现 `ISaveableContent` 接口以支持存档读档。
|
||||||
|
|
||||||
|
核心目标:
|
||||||
|
|
||||||
|
- 多楼层数据同时存在于内存中,通过 id 访问;
|
||||||
|
- 通过 `active` 标记区分"玩家可能到达"与"无需关注"的楼层,节省存档开销;
|
||||||
|
- 通过 `compareWith` 提供参考基准,配合分级压缩大幅减少存档体积;
|
||||||
|
- `IStateBase.layer` 类型由 `ILayerState` 改为 `IMapStore`,
|
||||||
|
操作楼层必须先通过 `getLayerState(id)` 取得具体楼层。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 实现思路
|
||||||
|
|
||||||
|
## 1. 给 LayerState 添加 active 成员
|
||||||
|
|
||||||
|
`ILayerState` 新增 `readonly active: boolean` 和 `setActiveStatus(active: boolean): void`
|
||||||
|
两个接口,`LayerState` 实现类中 `active` 默认为 `false`。
|
||||||
|
|
||||||
|
两者的关系:
|
||||||
|
|
||||||
|
- `ILayerState.setActiveStatus`:直接操作楼层对象;
|
||||||
|
- `IMapStore.setMapActiveStatus(id, active)`:通过 id 操作,
|
||||||
|
内部查找对应楼层后调用其 `setActiveStatus`。
|
||||||
|
|
||||||
|
## 2. 脏数据追踪(dirty tracking)
|
||||||
|
|
||||||
|
为支持 `LowCompression` 和 `HighCompression` 的差分存档,
|
||||||
|
需要知道哪些楼层相对于参考基准是否发生了修改。
|
||||||
|
|
||||||
|
**推荐方案:楼层级简单脏标记 + 存档时实际比较**
|
||||||
|
|
||||||
|
在 `LayerState` 内部维护 `private dirty: boolean = false`:
|
||||||
|
|
||||||
|
- 当楼层内任意 `MapLayer` 触发 `onUpdateBlock`、`onUpdateArea`、`onResize`
|
||||||
|
钩子时,将 `dirty` 置为 `true`;
|
||||||
|
- `dirty` 只在 `compareWith` 首次调用时根据实际数据对比结果初始化,
|
||||||
|
初始化后的 gameplay 过程中不再重置(仅置 true)。
|
||||||
|
|
||||||
|
在 `saveState` 时:
|
||||||
|
|
||||||
|
- 若 `dirty = false`,跳过该楼层(初始化后从未被触碰过);
|
||||||
|
- 若 `dirty = true`:
|
||||||
|
- **LowCompression**:与参考基准进行全量比较,若完全一致则跳过
|
||||||
|
(消除"改了又改回"场景下的误判),否则存储所有行;
|
||||||
|
- **HighCompression**:逐行与参考基准比较,只存储不一致的行。
|
||||||
|
|
||||||
|
在 `loadState` 时:若存档中此楼层没有任何数据(即未出现在 `floors` 中),
|
||||||
|
读档后将 `dirty` 置为 `false`(视为与参考基准一致)。
|
||||||
|
|
||||||
|
**不在 `MapLayer` 内维护 `dirtyRows`**,行级比较在 `saveState` 时直接对照参考基准进行。
|
||||||
|
这避免了每次 `setBlock`/`putMapData` 都更新行级标记的热路径开销,
|
||||||
|
且存档时实际比较已能消除误判,无需 `probablyDirty` + `setInterval` 或哈希方案。
|
||||||
|
|
||||||
|
存档时比较的开销:Uint32Array 内存连续,实际耗时极低,且保存操作本身是低频的,
|
||||||
|
若将来发现存档耗时问题,可考虑将比较逻辑移至 Web Worker。
|
||||||
|
|
||||||
|
## 3. compareWith 接口与参数类型
|
||||||
|
|
||||||
|
```ts
|
||||||
|
compareWith(ref: Map<string, Map<number, Uint32Array>>): void;
|
||||||
|
```
|
||||||
|
|
||||||
|
外层 `Map` 以楼层 id 为键,内层 `Map` 以图层 `zIndex` 为键,
|
||||||
|
值为对应图层的完整图块数组(Uint32Array,含所有行的扁平数据)。
|
||||||
|
|
||||||
|
使用此类型而非 `IMapStore` 的理由:接口更轻量,调用方可直接从游戏原始数据构建,
|
||||||
|
无需额外持有一个完整的 `IMapStore` 实例。
|
||||||
|
|
||||||
|
关于图层标识符:继续使用 `zIndex`,在单个楼层内 `zIndex` 是语义唯一的,
|
||||||
|
与已有 `MapLayer.zIndex` 接口保持一致。
|
||||||
|
|
||||||
|
**`compareWith` 以首次调用为唯一基准**,再次调用不更新参考(以游戏原始数据为基准,
|
||||||
|
避免存档之间产生依赖关系)。
|
||||||
|
|
||||||
|
实现步骤:
|
||||||
|
|
||||||
|
1. 若 `refData` 已存在,直接返回;
|
||||||
|
2. 保存 `ref` 引用到 `private refData`;
|
||||||
|
3. 遍历当前所有楼层,对每个楼层在 `ref` 中查找对应 id:
|
||||||
|
- 不存在:`dirty = true`(新楼层,视为全脏);
|
||||||
|
- 存在:对每个 `MapLayer`(按 `zIndex` 匹配)做全量比较,
|
||||||
|
若所有行与参考数据完全一致则 `dirty = false`,否则 `dirty = true`。
|
||||||
|
|
||||||
|
## 4. 楼层的创建与管理
|
||||||
|
|
||||||
|
`MapStore` 内部以 `Map<string, LayerState>` 存储所有楼层。
|
||||||
|
`getLayerState(id)` 对不存在的 id 直接返回 `null`,不自动创建。
|
||||||
|
|
||||||
|
只提供一个创建接口:
|
||||||
|
|
||||||
|
- `createLayerState(id: string): ILayerState`:创建并注册一个空白楼层
|
||||||
|
(无任何 `MapLayer`,用户拿到后再调用 `addLayer` 配置图层结构),返回楼层对象。
|
||||||
|
|
||||||
|
注册时若 id 已存在,发出 logger 警告并覆盖。
|
||||||
|
|
||||||
|
若 `compareWith` 已调用后再通过上述接口新增楼层,新楼层直接视为全脏(`dirty = true`),
|
||||||
|
因为 `refData` 中不存在对应数据。
|
||||||
|
|
||||||
|
## 5. 存档数据格式
|
||||||
|
|
||||||
|
### 类型定义
|
||||||
|
|
||||||
|
```ts
|
||||||
|
/** 单个 MapLayer 的存档数据 */
|
||||||
|
interface IMapLayerSave {
|
||||||
|
readonly width: number;
|
||||||
|
readonly height: number;
|
||||||
|
/**
|
||||||
|
* key = 行索引,value = 该行完整的 Uint32Array 数据;
|
||||||
|
* NoCompression/LowCompression 时包含所有行(0 到 height - 1);
|
||||||
|
* HighCompression 时只包含与参考基准不同的行;
|
||||||
|
* 读档时,不在此 Map 中的行从参考基准还原。
|
||||||
|
*/
|
||||||
|
readonly rows: ReadonlyMap<number, Uint32Array>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 单个楼层的存档数据 */
|
||||||
|
interface ILayerStateSave {
|
||||||
|
readonly background: number;
|
||||||
|
/**
|
||||||
|
* key = zIndex,value = 对应图层存档数据;
|
||||||
|
* 使用 Map 格式以支持图层的动态增删。
|
||||||
|
*/
|
||||||
|
readonly layers: ReadonlyMap<number, IMapLayerSave>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 整个 MapStore 的存档数据 */
|
||||||
|
interface IMapStoreSave {
|
||||||
|
/**
|
||||||
|
* key = 楼层 id,只包含 active 的楼层;
|
||||||
|
* inactive 的楼层不写入,读档时无需处理。
|
||||||
|
*/
|
||||||
|
readonly floors: ReadonlyMap<string, ILayerStateSave>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 各压缩等级存储策略
|
||||||
|
|
||||||
|
| 压缩级别 | 楼层粒度 | 行粒度 |
|
||||||
|
| ----------------- | --------------------------------------------------------------- | ------------------------ |
|
||||||
|
| `NoCompression` | 存储所有 active 楼层 | 存储该楼层所有行 |
|
||||||
|
| `LowCompression` | 跳过 `dirty = false` 的楼层;dirty 楼层全量比较后仍一致的也跳过 | 存储该楼层所有行 |
|
||||||
|
| `HighCompression` | 同 LowCompression | 只存储与参考基准不同的行 |
|
||||||
|
|
||||||
|
### 读档策略
|
||||||
|
|
||||||
|
读档时直接操作数组引用(通过 `setMapRef`),避免逐行拷贝的额外开销:
|
||||||
|
|
||||||
|
1. 若参考基准(`refData`)未设置,抛出 logger 错误,**不进行任何读档操作**;
|
||||||
|
2. 遍历 `state.floors`,对每个楼层 id:
|
||||||
|
- 若当前 `MapStore` 中不存在该 id,发出 logger 警告并跳过;
|
||||||
|
- 对该楼层每个图层,先从参考基准取出对应 `zIndex` 的数组,
|
||||||
|
将其深拷贝为新数组作为底层(确保未存档行使用参考基准值);
|
||||||
|
- 再将 `ILayerStateSave.layers` 中对应图层的 `rows` 数据写入该数组的对应行;
|
||||||
|
- 调用 `MapLayer.setMapRef(array)` 直接替换内部引用,无需额外拷贝;
|
||||||
|
3. 对未出现在 `state.floors` 中的 active 楼层,
|
||||||
|
从参考基准深拷贝完整数组后调用 `setMapRef` 还原,并将 `dirty` 置为 `false`。
|
||||||
|
|
||||||
|
## 6. saveState / loadState 实现
|
||||||
|
|
||||||
|
根据压缩等级分别编写三个存档函数和三个读档函数,
|
||||||
|
`saveState(compression)` 和 `loadState(state, compression)` 根据 `compression` 分发,
|
||||||
|
无需在每个楼层的遍历循环内部判断等级:
|
||||||
|
|
||||||
|
- `private saveNoCompression(): IMapStoreSave`
|
||||||
|
- `private saveLowCompression(): IMapStoreSave`
|
||||||
|
- `private saveHighCompression(): IMapStoreSave`
|
||||||
|
- `private loadNoCompression(state: IMapStoreSave): void`
|
||||||
|
- `private loadLowCompression(state: IMapStoreSave): void`
|
||||||
|
- `private loadHighCompression(state: IMapStoreSave): void`
|
||||||
|
|
||||||
|
`saveState` 结果需通过 `structuredClone` 深拷贝后返回。
|
||||||
|
|
||||||
|
## 7. IMapStore 接口设计(新增到 `map/types.ts`)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface IMapStore extends ISaveableContent<IMapStoreSave> {
|
||||||
|
/** 所有楼层的 id 集合 */
|
||||||
|
readonly maps: ReadonlySet<string>;
|
||||||
|
|
||||||
|
// --- 楼层访问 ---
|
||||||
|
/** 获取指定 id 的楼层状态,不存在则返回 null */
|
||||||
|
getLayerState(id: string): ILayerState | null;
|
||||||
|
/** 获取指定 id 的楼层状态,要求楼层必须是 active 的,否则返回 null */
|
||||||
|
getActiveMap(id: string): ILayerState | null;
|
||||||
|
|
||||||
|
// --- 楼层管理 ---
|
||||||
|
/** 创建并注册一个空白楼层,返回楼层状态对象 */
|
||||||
|
createLayerState(id: string): ILayerState;
|
||||||
|
|
||||||
|
// --- active 管理 ---
|
||||||
|
/** 获取指定 id 的楼层是否激活,不存在的 id 返回 false */
|
||||||
|
isMapActive(id: string): boolean;
|
||||||
|
/** 设置指定 id 楼层的激活状态 */
|
||||||
|
setMapActiveStatus(id: string, active: boolean): void;
|
||||||
|
/** 迭代所有 active 的楼层,yield [id, ILayerState] */
|
||||||
|
iterateActiveMaps(): Iterable<[string, ILayerState]>;
|
||||||
|
/** 迭代所有 inactive 的楼层,yield [id, ILayerState] */
|
||||||
|
iterateInactiveMaps(): Iterable<[string, ILayerState]>;
|
||||||
|
/** 迭代所有楼层,yield [id, ILayerState] */
|
||||||
|
iterateAllMaps(): Iterable<[string, ILayerState]>;
|
||||||
|
|
||||||
|
// --- 差分压缩基准 ---
|
||||||
|
/**
|
||||||
|
* 设置压缩参考基准,以首次调用为唯一基准,再次调用不更新。
|
||||||
|
* @param ref 外层 key = 楼层 id,内层 key = zIndex,value = 图层完整图块数据
|
||||||
|
*/
|
||||||
|
compareWith(ref: Map<string, Map<number, Uint32Array>>): void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. ILayerState 接口修改
|
||||||
|
|
||||||
|
在现有 `ILayerState` 上新增:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
/** 此楼层是否处于激活状态 */
|
||||||
|
readonly active: boolean;
|
||||||
|
/** 设置楼层激活状态 */
|
||||||
|
setActiveStatus(active: boolean): void;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. IStateBase 修改
|
||||||
|
|
||||||
|
将 `IStateBase.layer: ILayerState` 改为 `IStateBase.layer: IMapStore`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 涉及文件
|
||||||
|
|
||||||
|
## 需要引用的文件
|
||||||
|
|
||||||
|
- `@user/common/types.ts`: `ISaveableContent`, `SaveCompression`
|
||||||
|
- `@user/data-base/map/types.ts`: 全部现有地图接口(`IMapLayer`, `ILayerState`, 等)
|
||||||
|
|
||||||
|
## 需要修改的文件
|
||||||
|
|
||||||
|
### `@user/data-base/src/map/types.ts`
|
||||||
|
|
||||||
|
- [ ] 新增 `IMapLayerSave` 接口:单个 MapLayer 存档数据格式
|
||||||
|
- [ ] 新增 `ILayerStateSave` 接口:单个楼层存档数据格式
|
||||||
|
- [ ] 新增 `IMapStoreSave` 接口:MapStore 整体存档数据格式
|
||||||
|
- [ ] 修改 `ILayerState`:新增 `readonly active: boolean` 和
|
||||||
|
`setActiveStatus(active: boolean): void`
|
||||||
|
- [ ] 修改 `IMapLayer`:新增 `setMapRef(array: Uint32Array): void`
|
||||||
|
- [ ] 新增 `IMapStore` 接口:继承 `ISaveableContent<IMapStoreSave>`,
|
||||||
|
含全部接口(见第 7 节)
|
||||||
|
|
||||||
|
### `@user/data-base/src/map/mapLayer.ts`
|
||||||
|
|
||||||
|
### `@user/data-base/src/map/layerState.ts`
|
||||||
|
|
||||||
|
- [ ] 新增 `active: boolean = false` 成员:楼层激活状态
|
||||||
|
- [ ] 实现 `setActiveStatus(active: boolean): void`
|
||||||
|
- [ ] 新增 `private dirty: boolean = false` 成员:楼层级脏标记
|
||||||
|
- [ ] 修改 `StateMapLayerHook.onUpdateArea`、`onUpdateBlock`、`onResize`:
|
||||||
|
在转发钩子的同时,将 `state.dirty` 置 `true`
|
||||||
|
- [ ] 新增 `isDirty(): boolean` 方法:返回 `this.dirty`,供 `MapStore` 读取
|
||||||
|
- [ ] 新增 `setDirty(dirty: boolean): void` 方法:
|
||||||
|
供 `MapStore.compareWith` 时根据实际比较结果设置
|
||||||
|
|
||||||
|
### `@user/data-base/src/map/mapLayer.ts`
|
||||||
|
|
||||||
|
- [ ] 新增 `setMapRef(array: Uint32Array): void` 方法:
|
||||||
|
直接替换内部图块数组引用,跳过拷贝,供 `MapStore` 读档时使用。
|
||||||
|
需确保传入数组长度与 `width × height` 匹配,
|
||||||
|
并触发必要的钩子通知(不触发 `onResize`,应触发 `onUpdateArea` 通知全区域更新)。
|
||||||
|
在方法注释中明确标注:调用后不得再持有或修改传入的数组。
|
||||||
|
|
||||||
|
### `@user/data-base/src/map/mapStore.ts`(新文件)
|
||||||
|
|
||||||
|
- [ ] 实现 `MapStore` 类,实现 `IMapStore`
|
||||||
|
- [ ] `private mapData: Map<string, LayerState>`:楼层 id 到状态对象的映射
|
||||||
|
- [ ] `readonly maps: ReadonlySet<string>`:所有楼层 id 的只读集合视图
|
||||||
|
- [ ] `private refData: Map<string, Map<number, Uint32Array>> | null`:参考基准
|
||||||
|
- [ ] 实现 `getLayerState`、`getActiveMap`、`createLayerState`
|
||||||
|
- [ ] 实现 `isMapActive`、`setMapActiveStatus`、`iterateActiveMaps`、`iterateInactiveMaps`、`iterateAllMaps`
|
||||||
|
- [ ] 实现 `compareWith`
|
||||||
|
- [ ] 实现 `saveNoCompression`、`saveLowCompression`、`saveHighCompression`
|
||||||
|
- [ ] 实现 `loadNoCompression`、`loadLowCompression`、`loadHighCompression`
|
||||||
|
- [ ] 实现 `saveState(compression)` 和 `loadState(state, compression)` 分发
|
||||||
|
|
||||||
|
### `@user/data-base/src/map/index.ts`
|
||||||
|
|
||||||
|
- [ ] 补充导出 `mapStore.ts`
|
||||||
|
|
||||||
|
### `@user/data-base/src/types.ts`
|
||||||
|
|
||||||
|
- [ ] 将 `IStateBase.layer` 类型由 `ILayerState` 改为 `IMapStore`
|
||||||
@ -10,7 +10,7 @@ import {
|
|||||||
SpecialCreation,
|
SpecialCreation,
|
||||||
IEnemySaveState
|
IEnemySaveState
|
||||||
} from './types';
|
} from './types';
|
||||||
import { SaveCompression } from '../common/types';
|
import { SaveCompression } from '../common';
|
||||||
|
|
||||||
export class EnemyManager<TAttr> implements IEnemyManager<TAttr> {
|
export class EnemyManager<TAttr> implements IEnemyManager<TAttr> {
|
||||||
/** 特殊属性注册表,code -> 创建函数 */
|
/** 特殊属性注册表,code -> 创建函数 */
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
export * from './layerState';
|
export * from './layerState';
|
||||||
export * from './mapLayer';
|
export * from './mapLayer';
|
||||||
|
export * from './mapStore';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
|
|||||||
@ -29,6 +29,12 @@ export class LayerState
|
|||||||
/** 图层钩子映射 */
|
/** 图层钩子映射 */
|
||||||
private layerHookMap: Map<IMapLayer, IMapLayerHookController> = new Map();
|
private layerHookMap: Map<IMapLayer, IMapLayerHookController> = new Map();
|
||||||
|
|
||||||
|
/** 楼层是否处于激活状态 */
|
||||||
|
active: boolean = false;
|
||||||
|
|
||||||
|
/** 楼层级脏标记 */
|
||||||
|
private dirty: boolean = false;
|
||||||
|
|
||||||
addLayer(width: number, height: number): IMapLayer {
|
addLayer(width: number, height: number): IMapLayer {
|
||||||
const array = new Uint32Array(width * height);
|
const array = new Uint32Array(width * height);
|
||||||
const layer = new MapLayer(array, width, height);
|
const layer = new MapLayer(array, width, height);
|
||||||
@ -106,6 +112,18 @@ export class LayerState
|
|||||||
return this.backgroundTile;
|
return this.backgroundTile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setActiveStatus(active: boolean): void {
|
||||||
|
this.active = active;
|
||||||
|
}
|
||||||
|
|
||||||
|
isDirty(): boolean {
|
||||||
|
return this.dirty;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDirty(dirty: boolean): void {
|
||||||
|
this.dirty = dirty;
|
||||||
|
}
|
||||||
|
|
||||||
protected createController(
|
protected createController(
|
||||||
hook: Partial<ILayerStateHooks>
|
hook: Partial<ILayerStateHooks>
|
||||||
): IHookController<ILayerStateHooks> {
|
): IHookController<ILayerStateHooks> {
|
||||||
@ -120,18 +138,21 @@ class StateMapLayerHook implements Partial<IMapLayerHooks> {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
onUpdateArea(x: number, y: number, width: number, height: number): void {
|
onUpdateArea(x: number, y: number, width: number, height: number): void {
|
||||||
|
this.state.setDirty(true);
|
||||||
this.state.forEachHook(hook => {
|
this.state.forEachHook(hook => {
|
||||||
hook.onUpdateLayerArea?.(this.layer, x, y, width, height);
|
hook.onUpdateLayerArea?.(this.layer, x, y, width, height);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onUpdateBlock(block: number, x: number, y: number): void {
|
onUpdateBlock(block: number, x: number, y: number): void {
|
||||||
|
this.state.setDirty(true);
|
||||||
this.state.forEachHook(hook => {
|
this.state.forEachHook(hook => {
|
||||||
hook.onUpdateLayerBlock?.(this.layer, block, x, y);
|
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);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -186,14 +186,12 @@ export class MapLayer
|
|||||||
}
|
}
|
||||||
const res = new Uint32Array(width * height);
|
const res = new Uint32Array(width * height);
|
||||||
const arr = this.mapArray;
|
const arr = this.mapArray;
|
||||||
const nr = Math.min(r, w);
|
|
||||||
const nb = Math.min(b, h);
|
const nb = Math.min(b, h);
|
||||||
for (let nx = x; nx < nr; nx++) {
|
for (let ny = y; ny < nb; ny++) {
|
||||||
for (let ny = y; ny < nb; ny++) {
|
const lineStart = ny * w + x;
|
||||||
const origin = ny * w + nx;
|
const lineEnd = lineStart + width;
|
||||||
const target = (ny - y) * width + (nx - x);
|
const dy = ny - y;
|
||||||
res[target] = arr[origin];
|
res.set(arr.subarray(lineStart, lineEnd), dy * width);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
@ -205,6 +203,27 @@ export class MapLayer
|
|||||||
return this.mapData;
|
return this.mapData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setMapRef(array: Uint32Array): void {
|
||||||
|
if (array.length !== this.width * this.height) {
|
||||||
|
logger.warn(
|
||||||
|
123,
|
||||||
|
array.length.toString(),
|
||||||
|
(this.width * this.height).toString()
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.mapData.expired = true;
|
||||||
|
this.mapArray = array;
|
||||||
|
this.mapData = {
|
||||||
|
expired: false,
|
||||||
|
array: this.mapArray
|
||||||
|
};
|
||||||
|
this.empty = !array.some(v => v !== 0);
|
||||||
|
this.forEachHook(hook => {
|
||||||
|
hook.onUpdateArea?.(0, 0, this.width, this.height);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
protected createController(
|
protected createController(
|
||||||
hook: Partial<IMapLayerHooks>
|
hook: Partial<IMapLayerHooks>
|
||||||
): IMapLayerHookController {
|
): IMapLayerHookController {
|
||||||
|
|||||||
383
packages-user/data-base/src/map/mapStore.ts
Normal file
383
packages-user/data-base/src/map/mapStore.ts
Normal file
@ -0,0 +1,383 @@
|
|||||||
|
import { logger } from '@motajs/common';
|
||||||
|
import { SaveCompression } from '../common';
|
||||||
|
import {
|
||||||
|
ILayerState,
|
||||||
|
ILayerStateSave,
|
||||||
|
IMapLayer,
|
||||||
|
IMapLayerSave,
|
||||||
|
IMapStore,
|
||||||
|
IMapStoreSave
|
||||||
|
} from './types';
|
||||||
|
import { LayerState } from './layerState';
|
||||||
|
|
||||||
|
export class MapStore implements IMapStore {
|
||||||
|
/** 楼层 id 到状态对象的映射 */
|
||||||
|
private readonly mapData: Map<string, LayerState> = new Map();
|
||||||
|
|
||||||
|
/** 所有楼层 id 的只读集合视图 */
|
||||||
|
readonly maps: Set<string> = new Set();
|
||||||
|
|
||||||
|
/** 差分压缩参考基准,首次 compareWith 后设置,之后不再更新 */
|
||||||
|
private refData: Map<string, Map<number, Uint32Array>> | null = null;
|
||||||
|
|
||||||
|
//#region 楼层访问
|
||||||
|
|
||||||
|
getLayerState(id: string): ILayerState | null {
|
||||||
|
return this.mapData.get(id) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getActiveMap(id: string): ILayerState | null {
|
||||||
|
const state = this.mapData.get(id);
|
||||||
|
if (!state || !state.active) return null;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region 楼层管理
|
||||||
|
|
||||||
|
createLayerState(id: string): ILayerState {
|
||||||
|
if (this.mapData.has(id)) {
|
||||||
|
logger.warn(121, id);
|
||||||
|
}
|
||||||
|
const state = new LayerState();
|
||||||
|
// 若 refData 已存在,新楼层直接视为全脏
|
||||||
|
if (this.refData !== null) {
|
||||||
|
state.setDirty(true);
|
||||||
|
}
|
||||||
|
this.mapData.set(id, state);
|
||||||
|
this.maps.add(id);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region active 管理
|
||||||
|
|
||||||
|
isMapActive(id: string): boolean {
|
||||||
|
return this.mapData.get(id)?.active ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setMapActiveStatus(id: string, active: boolean): void {
|
||||||
|
this.mapData.get(id)?.setActiveStatus(active);
|
||||||
|
}
|
||||||
|
|
||||||
|
*iterateActiveMaps(): Iterable<[string, ILayerState]> {
|
||||||
|
for (const [id, state] of this.mapData) {
|
||||||
|
if (state.active) yield [id, state];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*iterateInactiveMaps(): Iterable<[string, ILayerState]> {
|
||||||
|
for (const [id, state] of this.mapData) {
|
||||||
|
if (!state.active) yield [id, state];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
iterateAllMaps(): Iterable<[string, ILayerState]> {
|
||||||
|
return this.mapData;
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region 存档及压缩
|
||||||
|
|
||||||
|
compareWith(ref: Map<string, Map<number, Uint32Array>>): void {
|
||||||
|
if (this.refData !== null) return;
|
||||||
|
this.refData = ref;
|
||||||
|
|
||||||
|
for (const [id, state] of this.mapData) {
|
||||||
|
const refFloor = ref.get(id);
|
||||||
|
if (!refFloor) {
|
||||||
|
state.setDirty(true);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let dirty = false;
|
||||||
|
for (const layer of state.layerList) {
|
||||||
|
const refArray = refFloor.get(layer.zIndex);
|
||||||
|
if (!refArray) {
|
||||||
|
dirty = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const cur = layer.getMapRef().array;
|
||||||
|
if (cur.length !== refArray.length) {
|
||||||
|
dirty = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (cur.some((v, i) => refArray[i] !== v)) {
|
||||||
|
dirty = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.setDirty(dirty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>();
|
||||||
|
for (const [id, state] of this.mapData) {
|
||||||
|
if (!state.active) continue;
|
||||||
|
// 非 dirty 或 dirty 但与参考基准完全一致 → 空 layers(读档时从参考基准恢复)
|
||||||
|
if (
|
||||||
|
!state.isDirty() ||
|
||||||
|
(this.refData && this.isStateEqualToRef(id, state))
|
||||||
|
) {
|
||||||
|
floors.set(id, {
|
||||||
|
background: state.getBackground(),
|
||||||
|
layers: new Map()
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
floors.set(id, this.saveLayerStateFull(state));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { floors };
|
||||||
|
}
|
||||||
|
|
||||||
|
private saveHighCompression(): IMapStoreSave {
|
||||||
|
const floors = new Map<string, ILayerStateSave>();
|
||||||
|
for (const [id, state] of this.mapData) {
|
||||||
|
if (!state.active) continue;
|
||||||
|
if (!state.isDirty()) {
|
||||||
|
floors.set(id, {
|
||||||
|
background: state.getBackground(),
|
||||||
|
layers: new Map()
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const refFloor = this.refData?.get(id);
|
||||||
|
const layersMap = new Map<number, IMapLayerSave>();
|
||||||
|
for (const layer of state.layerList) {
|
||||||
|
const refArray = refFloor?.get(layer.zIndex);
|
||||||
|
const rows = this.diffRows(layer, refArray);
|
||||||
|
if (rows.size === 0 && refArray) continue; // 与参考完全一致
|
||||||
|
layersMap.set(layer.zIndex, {
|
||||||
|
width: layer.width,
|
||||||
|
height: layer.height,
|
||||||
|
rows
|
||||||
|
});
|
||||||
|
}
|
||||||
|
floors.set(id, {
|
||||||
|
background: state.getBackground(),
|
||||||
|
layers: layersMap
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { floors };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NoCompression 读档:每个图层均有 fullMap,直接转移所有权,无需参考基准。
|
||||||
|
*/
|
||||||
|
private loadNoCompression(state: IMapStoreSave): void {
|
||||||
|
for (const [id, cur] of this.mapData) {
|
||||||
|
cur.setActiveStatus(state.floors.has(id));
|
||||||
|
}
|
||||||
|
for (const [id, layerStateSave] of state.floors) {
|
||||||
|
const cur = this.mapData.get(id);
|
||||||
|
if (!cur) {
|
||||||
|
logger.warn(122, id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
cur.setBackground(layerStateSave.background);
|
||||||
|
for (const layer of cur.layerList) {
|
||||||
|
const layerSave = layerStateSave.layers.get(layer.zIndex);
|
||||||
|
if (!layerSave?.fullMap) continue;
|
||||||
|
layer.setMapRef(new Uint32Array(layerSave.fullMap));
|
||||||
|
}
|
||||||
|
cur.setDirty(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LowCompression 读档:
|
||||||
|
* - layers 有数据(dirty 楼层)→ fullMap 直接转移所有权
|
||||||
|
* - layers 为空(非 dirty 楼层)→ 从参考基准恢复
|
||||||
|
*/
|
||||||
|
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, layerStateSave] 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(layerStateSave.background);
|
||||||
|
for (const layer of cur.layerList) {
|
||||||
|
const layerSave = layerStateSave.layers.get(layer.zIndex);
|
||||||
|
if (layerSave?.fullMap) {
|
||||||
|
layer.setMapRef(layerSave.fullMap);
|
||||||
|
} else {
|
||||||
|
const refArray = refFloor?.get(layer.zIndex);
|
||||||
|
if (!refArray) {
|
||||||
|
logger.warn(124, id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
layer.setMapRef(new Uint32Array(refArray));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cur.setDirty(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HighCompression 读档:
|
||||||
|
* - layers 有数据(dirty 楼层)→ 以参考基准为底,叠加差分行
|
||||||
|
* - layers 为空(非 dirty 楼层)或图层无变化(rows 缺失)→ 从参考基准恢复
|
||||||
|
*/
|
||||||
|
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, layerStateSave] 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(layerStateSave.background);
|
||||||
|
let isMapDirty = true;
|
||||||
|
for (const layer of cur.layerList) {
|
||||||
|
const refArray = refFloor.get(layer.zIndex);
|
||||||
|
if (!refArray) {
|
||||||
|
logger.warn(124, id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const layerSave = layerStateSave.layers.get(layer.zIndex);
|
||||||
|
if (!layerSave?.rows || layerSave.rows.size === 0) {
|
||||||
|
// 图层无变化或非 dirty 楼层,从参考基准恢复
|
||||||
|
layer.setMapRef(new Uint32Array(refArray));
|
||||||
|
} else {
|
||||||
|
// 以参考基准为底,叠加差分行
|
||||||
|
isMapDirty = false;
|
||||||
|
const size = layer.width * layer.height;
|
||||||
|
const buf = new Uint32Array(size);
|
||||||
|
if (refArray) buf.set(refArray.subarray(0, size));
|
||||||
|
for (const [rowIdx, rowData] of layerSave.rows) {
|
||||||
|
buf.set(
|
||||||
|
rowData.subarray(0, layer.width),
|
||||||
|
rowIdx * layer.width
|
||||||
|
);
|
||||||
|
}
|
||||||
|
layer.setMapRef(buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cur.setDirty(isMapDirty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveState(compression: SaveCompression): IMapStoreSave {
|
||||||
|
if (compression === SaveCompression.HighCompression) {
|
||||||
|
return this.saveHighCompression();
|
||||||
|
} else if (compression === SaveCompression.LowCompression) {
|
||||||
|
return this.saveLowCompression();
|
||||||
|
} else {
|
||||||
|
return this.saveNoCompression();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadState(state: IMapStoreSave, compression: SaveCompression): void {
|
||||||
|
if (compression === SaveCompression.HighCompression) {
|
||||||
|
this.loadHighCompression(state);
|
||||||
|
} else if (compression === SaveCompression.LowCompression) {
|
||||||
|
this.loadLowCompression(state);
|
||||||
|
} else {
|
||||||
|
this.loadNoCompression(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//#region 内部方法
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将楼层所有图层全量序列化(NoCompression / LowCompression 用)
|
||||||
|
*/
|
||||||
|
private saveLayerStateFull(state: LayerState): ILayerStateSave {
|
||||||
|
const layersMap = new Map<number, IMapLayerSave>();
|
||||||
|
for (const layer of state.layerList) {
|
||||||
|
const arr = layer.getMapRef().array;
|
||||||
|
layersMap.set(layer.zIndex, {
|
||||||
|
width: layer.width,
|
||||||
|
height: layer.height,
|
||||||
|
fullMap: new Uint32Array(arr)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { background: state.getBackground(), layers: layersMap };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仅返回与参考基准不同的行(HighCompression 用)
|
||||||
|
*/
|
||||||
|
private diffRows(
|
||||||
|
layer: IMapLayer,
|
||||||
|
refArray?: Uint32Array
|
||||||
|
): Map<number, Uint32Array> {
|
||||||
|
const rows = new Map<number, Uint32Array>();
|
||||||
|
const arr = layer.getMapRef().array;
|
||||||
|
if (refArray) {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (let row = 0; row < layer.height; row++) {
|
||||||
|
const start = row * layer.width;
|
||||||
|
const end = start + layer.width;
|
||||||
|
rows.set(row, new Uint32Array(arr.subarray(start, end)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断楼层所有图层是否与参考基准完全一致(LowCompression 去误判用)
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
for (let i = 0; i < cur.length; i++) {
|
||||||
|
if (cur[i] !== refArray[i]) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import { IHookable, IHookBase, IHookController } from '@motajs/common';
|
import { IHookable, IHookBase, IHookController } from '@motajs/common';
|
||||||
|
import { ISaveableContent } from '../common';
|
||||||
|
|
||||||
export interface IMapLayerData {
|
export interface IMapLayerData {
|
||||||
/** 当前引用是否过期,当地图图层内部的地图数组引用更新时,此项会变为 `true` */
|
/** 当前引用是否过期,当地图图层内部的地图数组引用更新时,此项会变为 `true` */
|
||||||
@ -48,8 +49,7 @@ export interface IMapLayerHooks extends IHookBase {
|
|||||||
onCloseDoor(num: number, x: number, y: number): Promise<void>;
|
onCloseDoor(num: number, x: number, y: number): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IMapLayerHookController
|
export interface IMapLayerHookController extends IHookController<IMapLayerHooks> {
|
||||||
extends IHookController<IMapLayerHooks> {
|
|
||||||
/** 拓展所属的图层对象 */
|
/** 拓展所属的图层对象 */
|
||||||
readonly layer: IMapLayer;
|
readonly layer: IMapLayer;
|
||||||
|
|
||||||
@ -59,8 +59,10 @@ export interface IMapLayerHookController
|
|||||||
getMapData(): Readonly<IMapLayerData>;
|
getMapData(): Readonly<IMapLayerData>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IMapLayer
|
export interface IMapLayer extends IHookable<
|
||||||
extends IHookable<IMapLayerHooks, IMapLayerHookController> {
|
IMapLayerHooks,
|
||||||
|
IMapLayerHookController
|
||||||
|
> {
|
||||||
/** 地图宽度 */
|
/** 地图宽度 */
|
||||||
readonly width: number;
|
readonly width: number;
|
||||||
/** 地图高度 */
|
/** 地图高度 */
|
||||||
@ -155,6 +157,15 @@ 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>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 直接替换内部图块数组引用,跳过拷贝,高性能但风险较高。
|
||||||
|
* 一般仅供 `MapStore` 读档时内部使用,外部正常情况下不应调用。
|
||||||
|
* 调用方需确保传入数组的长度与 `width * height` 匹配,
|
||||||
|
* 且调用后不得再持有或修改传入的数组。
|
||||||
|
* @param array 地图数组,会直接替换内部引用
|
||||||
|
*/
|
||||||
|
setMapRef(array: Uint32Array): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ILayerStateHooks extends IHookBase {
|
export interface ILayerStateHooks extends IHookBase {
|
||||||
@ -212,6 +223,8 @@ export interface ILayerStateHooks extends IHookBase {
|
|||||||
export interface ILayerState extends IHookable<ILayerStateHooks> {
|
export interface ILayerState extends IHookable<ILayerStateHooks> {
|
||||||
/** 地图列表 */
|
/** 地图列表 */
|
||||||
readonly layerList: Set<IMapLayer>;
|
readonly layerList: Set<IMapLayer>;
|
||||||
|
/** 此楼层是否处于激活状态 */
|
||||||
|
readonly active: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加图层
|
* 添加图层
|
||||||
@ -275,4 +288,107 @@ export interface ILayerState extends IHookable<ILayerStateHooks> {
|
|||||||
* 获取背景图块数字,如果没有设置过,则返回 0
|
* 获取背景图块数字,如果没有设置过,则返回 0
|
||||||
*/
|
*/
|
||||||
getBackground(): number;
|
getBackground(): number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置楼层激活状态
|
||||||
|
* @param active 激活状态
|
||||||
|
*/
|
||||||
|
setActiveStatus(active: boolean): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 楼层是否被修改过(相对于参考基准)
|
||||||
|
*/
|
||||||
|
isDirty(): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置楼层脏标记
|
||||||
|
*/
|
||||||
|
setDirty(dirty: boolean): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 单个 MapLayer 的存档数据 */
|
||||||
|
export interface IMapLayerSave {
|
||||||
|
readonly width: number;
|
||||||
|
readonly height: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* key = 行索引,value = 该行完整的 Uint32Array 数据;
|
||||||
|
* HighCompression 时使用此接口,仅包含与参考基准不同的行;
|
||||||
|
* 读档时,不在此 Map 中的行从参考基准还原。
|
||||||
|
*/
|
||||||
|
readonly rows?: ReadonlyMap<number, Uint32Array>;
|
||||||
|
|
||||||
|
/** 完整地图,当使用 `NoCompression` 和 `LowCompression` 时使用此接口 */
|
||||||
|
readonly fullMap?: Uint32Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 单个楼层的存档数据 */
|
||||||
|
export interface ILayerStateSave {
|
||||||
|
readonly background: number;
|
||||||
|
|
||||||
|
/** key = zIndex,value = 对应图层存档数据 */
|
||||||
|
readonly layers: ReadonlyMap<number, IMapLayerSave>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 整个 MapStore 的存档数据 */
|
||||||
|
export interface IMapStoreSave {
|
||||||
|
/** key = 楼层 id,只包含 active 的楼层,inactive 的楼层不写入,读档时无需处理 */
|
||||||
|
readonly floors: ReadonlyMap<string, ILayerStateSave>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMapStore extends ISaveableContent<IMapStoreSave> {
|
||||||
|
/** 所有楼层的 id 集合 */
|
||||||
|
readonly maps: ReadonlySet<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定 id 的楼层状态,不存在则返回 null
|
||||||
|
* @param id 楼层 id
|
||||||
|
*/
|
||||||
|
getLayerState(id: string): ILayerState | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定 id 的楼层状态,要求楼层必须是 active 的,否则返回 null
|
||||||
|
* @param id 楼层 id
|
||||||
|
*/
|
||||||
|
getActiveMap(id: string): ILayerState | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建并注册一个空白楼层,若 id 已存在则警告并覆盖,返回楼层状态对象
|
||||||
|
* @param id 楼层 id
|
||||||
|
*/
|
||||||
|
createLayerState(id: string): ILayerState;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定 id 的楼层是否激活,不存在的 id 返回 false
|
||||||
|
* @param id 楼层 id
|
||||||
|
*/
|
||||||
|
isMapActive(id: string): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置指定 id 楼层的激活状态
|
||||||
|
* @param id 楼层 id
|
||||||
|
* @param active 激活状态
|
||||||
|
*/
|
||||||
|
setMapActiveStatus(id: string, active: boolean): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 迭代所有 active 的楼层,yield [id, ILayerState]
|
||||||
|
*/
|
||||||
|
iterateActiveMaps(): Iterable<[string, ILayerState]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 迭代所有 inactive 的楼层,yield [id, ILayerState]
|
||||||
|
*/
|
||||||
|
iterateInactiveMaps(): Iterable<[string, ILayerState]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 迭代所有楼层,yield [id, ILayerState]
|
||||||
|
*/
|
||||||
|
iterateAllMaps(): Iterable<[string, ILayerState]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置压缩参考基准,以首次调用为唯一基准,再次调用不更新。
|
||||||
|
* @param ref 外层 key = 楼层 id,内层 key = zIndex,value = 图层完整图块数据
|
||||||
|
*/
|
||||||
|
compareWith(ref: Map<string, Map<number, Uint32Array>>): void;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { IHeroFollower, IHeroState } from './hero';
|
|||||||
import { IEnemyManager } from './enemy';
|
import { IEnemyManager } from './enemy';
|
||||||
import { IFlagSystem } from './flag';
|
import { IFlagSystem } from './flag';
|
||||||
import { IRoleFaceBinder, ISaveableContent } from './common';
|
import { IRoleFaceBinder, ISaveableContent } from './common';
|
||||||
import { ILayerState } from './map';
|
import { IMapStore } from './map';
|
||||||
|
|
||||||
export interface IStateSaveData {
|
export interface IStateSaveData {
|
||||||
/** 跟随者列表 */
|
/** 跟随者列表 */
|
||||||
@ -18,7 +18,7 @@ export interface IStateBase<TEnemy, THero> {
|
|||||||
readonly numberIdMap: Map<number, string>;
|
readonly numberIdMap: Map<number, string>;
|
||||||
|
|
||||||
/** 地图状态 */
|
/** 地图状态 */
|
||||||
readonly layer: ILayerState;
|
readonly layer: IMapStore;
|
||||||
/** 勇士状态 */
|
/** 勇士状态 */
|
||||||
readonly hero: IHeroState<THero>;
|
readonly hero: IHeroState<THero>;
|
||||||
|
|
||||||
|
|||||||
@ -54,6 +54,7 @@
|
|||||||
"52": "To get divider payload, an excitation binding is expected.",
|
"52": "To get divider payload, an excitation binding is expected.",
|
||||||
"53": "Expected serializable value set as enemy's default attribute.",
|
"53": "Expected serializable value set as enemy's default attribute.",
|
||||||
"54": "Legacy '$1' API has been removed, consider using new APIs: '$2'.",
|
"54": "Legacy '$1' API has been removed, consider using new APIs: '$2'.",
|
||||||
|
"55": "Cannot load MapStore state: reference data (compareWith) has not been set.",
|
||||||
"1201": "Floor-damage extension needs 'floor-binder' extension as dependency."
|
"1201": "Floor-damage extension needs 'floor-binder' extension as dependency."
|
||||||
},
|
},
|
||||||
"warn": {
|
"warn": {
|
||||||
@ -177,6 +178,10 @@
|
|||||||
"118": "No enemy comparer attached to EnemyManager. All enemies will be treated as dirty.",
|
"118": "No enemy comparer attached to EnemyManager. All enemies will be treated as dirty.",
|
||||||
"119": "Enemy prefab with code $1 not found during loadState, skipping.",
|
"119": "Enemy prefab with code $1 not found during loadState, skipping.",
|
||||||
"120": "Special with code $1 not found in enemy '$2' during loadState, skipping.",
|
"120": "Special with code $1 not found in enemy '$2' during loadState, skipping.",
|
||||||
|
"121": "MapStore.createLayerState: floor '$1' already exists, the existing floor will be overwritten.",
|
||||||
|
"122": "MapStore.loadState: floor '$1' not found in current map data, skipping.",
|
||||||
|
"123": "MapLayer.setMapRef: array length $1 does not match expected size $2, setMapRef will be ignored.",
|
||||||
|
"124": "MapStore.loadState: floor '$1' or its layer(s) not found in current reference data, skipping.",
|
||||||
"1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency."
|
"1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user