mirror of
https://github.com/motajs/template.git
synced 2026-05-19 00:01:11 +08:00
feat: 静态触发器存储
This commit is contained in:
parent
13fc4e1b7c
commit
bf781c5ee8
283
docs/dev/map/tile-info.md
Normal file
283
docs/dev/map/tile-info.md
Normal file
@ -0,0 +1,283 @@
|
||||
# 需求综述
|
||||
|
||||
重新考虑之后,这一层需求更适合收敛成“图块数据”和“触发器数据”两套独立结构,而不是先抽象一层通用 `ITileInfo` 对象。
|
||||
|
||||
原因如下:
|
||||
|
||||
1. `IMapLayer` 内部的 `Uint32Array` 主要服务于高频查询与渲染,本身就应该尽量纯粹;
|
||||
2. 大多数格点只是墙壁、地板、空气,不含任何触发器,没有必要给每个格点额外挂一个信息对象;
|
||||
3. 当前触发器系统在数据层真正需要的并不是 `ITrigger` 对象,而只是“该载体对应哪一种触发器类型”;
|
||||
4. 动态图块是实例对象,天然适合作为触发器的运行时载体,触发器跟随动态图块移动也更自然;
|
||||
5. 存档是否需要记录触发器,取决于“触发器是跟随地图状态定义,还是跟随图块定义”,这应当单独讨论,而不是先把对象模型写死。
|
||||
|
||||
因此,本文不再优先设计 `IWrappedTileInfo`、`ITileInfo`、`createTileInfo`、`bindTileInfo` 这一套通用信息接口,而改为先构建一套**以 `ITileStore` 默认值为主、地图稀疏覆盖为辅的触发器方案**:
|
||||
|
||||
1. `ILayerState` 持有 `ITileStore` 引用,静态格点未主动设置时使用图块默认触发器类型;
|
||||
2. `IMapLayer` 仅按坐标稀疏存储“手动覆盖值”,并提供 `revertTrigger` 恢复默认触发器;
|
||||
3. 动态图块实例直接持有自己的触发器类型数字;
|
||||
4. `ITrigger` 对象仍只存在于 Layer 2,在收集阶段按需实例化;
|
||||
5. `setBlock` 与触发器解绑,不再通过 `keepInfo` 一类参数耦合两者。
|
||||
|
||||
---
|
||||
|
||||
# 接口设计与预期
|
||||
|
||||
## ILayerState.tileStore
|
||||
|
||||
- `ILayerState.tileStore`:当前图层状态持有的图块定义 store 引用。
|
||||
- 预期频率:**低频到中频**。外部直接访问它的频率不会太高,但 `IMapLayer.getTriggerType` 等底层逻辑会稳定依赖它作为默认触发器来源。
|
||||
- 典型使用场景:静态格点没有手动设置触发器时,`IMapLayer` 先读取当前图块数字,再通过 `layerState.tileStore.getTrigger(num)` 获取默认触发器类型。
|
||||
|
||||
## IMapLayer.getTriggerType
|
||||
|
||||
- `IMapLayer.getTriggerType(x, y)`:按坐标读取当前静态格点的“有效触发器类型”。若该点存在手动覆盖则返回覆盖值,否则回退到当前图块在 `ITileStore` 中的默认触发器类型。
|
||||
- 预期频率:**高频**。触发器收集时每次都需要先确认目标静态格点最终会暴露出哪一种触发器。
|
||||
- 典型使用场景:`ITriggerCollector.collect(x, y, layer)` 读取 `layer` 在 `(x, y)` 上的静态触发器类型。
|
||||
- 返回值建议:与 `ITileStore` 保持一致,越界或不存在触发器时统一返回 `-1`。
|
||||
|
||||
## IMapLayer.setTriggerType
|
||||
|
||||
- `IMapLayer.setTriggerType(type, x, y)`:为指定静态格点写入手动覆盖的触发器类型数字。
|
||||
- 预期频率:**中频**。主要出现在地图初始化、编辑器写回、运行时脚本修改事件配置等场景。
|
||||
- 典型使用场景:给某一格配置战斗触发器;或显式将某个原本有默认触发器的图块覆盖为“无触发器”。
|
||||
- 语义建议:`setTriggerType` 只负责写覆盖值,不负责恢复默认值。若 `type = -1`,表示显式覆盖为“无触发器”;若要恢复为图块默认触发器,应调用 `revertTrigger`。
|
||||
|
||||
## IMapLayer.revertTrigger
|
||||
|
||||
- `IMapLayer.revertTrigger(x, y)`:删除指定静态格点的手动触发器覆盖,使其重新回退到 `ITileStore` 的默认触发器类型。
|
||||
- 预期频率:**低频到中频**。主要出现在编辑器撤销覆盖、脚本恢复默认配置、动静态图块转换回写时。
|
||||
- 典型使用场景:某格点曾被脚本临时设置为其他触发器,演出结束后恢复为该图块原本的默认触发器。
|
||||
|
||||
## IDynamicTile.triggerType
|
||||
|
||||
- `IDynamicTile.triggerType`:当前动态图块携带的触发器类型数字,`-1` 表示无触发器。
|
||||
- 预期频率:**中频**。收集器在读取某格所有动态图块时需要访问;运行时若有脚本操作某个动态图块事件,也可能直接读写。
|
||||
- 典型使用场景:某个 NPC 作为动态图块移动时,其战斗或对话触发器跟随该实例一起移动。
|
||||
|
||||
## IDynamicTile.setTriggerType
|
||||
|
||||
- `IDynamicTile.setTriggerType(type)`:修改当前动态图块绑定的触发器类型。
|
||||
- 预期频率:**低频到中频**。通常只在创建动态图块、转换动静态图块、特殊脚本重配置时使用。
|
||||
- 典型使用场景:演出过程中把一个原本只是装饰的动态图块改成可交互单位。
|
||||
|
||||
## IDynamicLayer.transferToDynamic
|
||||
|
||||
- `IDynamicLayer.transferToDynamic(x, y, keepTrigger?)`:把静态图块转换为动态图块,并按参数决定是否保留原先的触发器。
|
||||
- 预期频率:**中频**。每次把地图中的静态物件实例化为可移动对象时都会用到。
|
||||
- 典型使用场景:把地图上的 NPC 图块转成 `IDynamicTile`,并让对话或战斗触发器跟着它一起移动。
|
||||
- 语义建议:`keepTrigger` 默认值先定为 `true`。当 `keepTrigger = true` 时,将静态格点当前的有效触发器类型迁移到新建的动态图块;当 `keepTrigger = false` 时,新动态图块不保留原触发器,并且静态格点上的手动覆盖应一并清除。
|
||||
|
||||
## IDynamicLayer.transferToStatic
|
||||
|
||||
- `IDynamicLayer.transferToStatic(tile, keepTrigger?)`:把动态图块还原为静态图块,并按参数决定是否把动态图块携带的触发器写回静态格点。
|
||||
- 预期频率:**中频**。会移动的图块在结束移动后重新落回地图时使用。
|
||||
- 典型使用场景:可推动箱子停止后重新固化为静态图块,同时决定是否保留它携带的触发器。
|
||||
- 语义建议:`keepTrigger` 默认值先定为 `true`。当 `keepTrigger = true` 时,将 `tile.triggerType` 写回目标静态格点;若写回值与该图块默认触发器一致,则应直接 `revertTrigger`,只在不一致时存入覆盖值。当 `keepTrigger = false` 时,不写回动态图块携带的触发器,静态格点自然回退到图块默认触发器。
|
||||
|
||||
## IDynamicLayer.transferToStaticIfSafe
|
||||
|
||||
- `IDynamicLayer.transferToStaticIfSafe(tile, keepTrigger?)`:在满足安全回写条件时将动态图块还原为静态图块,并保持与 `transferToStatic` 一致的触发器迁移语义。
|
||||
- 预期频率:**中频**。用于需要先判断目标格点是否允许回写的动静态转换场景。
|
||||
- 典型使用场景:某个可移动物体尝试停回地图时,若目标格点安全则回写图块和触发器,否则保留动态状态。
|
||||
- 语义建议:为保持三组转换接口的对称性,`keepTrigger` 也应加入该接口,且默认值同样先定为 `true`。
|
||||
|
||||
## ITriggerCollector.collect
|
||||
|
||||
- `ITriggerCollector.collect(x, y, layer)`:当前不再从 `getTileInfo` 中读取对象信息,而是分别读取静态格点与动态图块上的触发器类型数字,再按需实例化 `ITrigger`。
|
||||
- 预期频率:**中频**。玩家移动、交互判定、脚本触发等场景都会使用。
|
||||
- 典型使用场景:
|
||||
1. 读取 `layer.getTriggerType(x, y)`;
|
||||
2. 迭代 `layer.dynamicLayer.getDynamicTilesAt(x, y)`,读取每个 `tile.triggerType`;
|
||||
3. 将所有非 `-1` 的触发器类型交给 `ITriggerRegistry` 创建运行时 `ITrigger` 对象;
|
||||
4. 排序后组成 `ITriggerCollection`。
|
||||
|
||||
这里需要注意:**数据层不再存 `ITrigger` 对象,只存触发器类型数字;对象实例化延后到收集阶段完成。** 这样既保留了 Layer 2 的运行时灵活性,又降低了 Layer 1 的存储复杂度。
|
||||
|
||||
## 当前不再优先设计的接口
|
||||
|
||||
以下内容在这轮设计中不再作为主方案:
|
||||
|
||||
1. `ITileInfo` / `IWrappedTileInfo` 一类通用格点信息对象;
|
||||
2. `createTileInfo()` / `bindTileInfo()` 一类对象工厂接口;
|
||||
3. `setBlock(block, x, y, keepInfo?)` 这类把图块写入与触发器保留策略绑在一起的接口。
|
||||
|
||||
这并不表示将来永远不会有通用格点附加信息,只是当前真实需求已经收敛到“触发器类型数字”,继续上抽象只会把接口设计得更重。
|
||||
|
||||
---
|
||||
|
||||
# 实现思路
|
||||
|
||||
## 1. 图块数组与触发器数据彻底分离
|
||||
|
||||
`IMapLayer` 内部继续保留 `Uint32Array` 作为图块数字的权威存储,仅服务于:
|
||||
|
||||
1. 高频 `getBlock` 查询;
|
||||
2. 区域拷贝与渲染;
|
||||
3. 与现有 `openDoor`、`closeDoor`、`putMapData`、`setMapRef` 等图块相关操作配合。
|
||||
|
||||
触发器数据则不再混入图块数组逻辑中,而是作为独立结构维护。
|
||||
|
||||
## 2. 静态图层使用“默认值 + 稀疏覆盖”
|
||||
|
||||
静态图层不应自行持有整份默认触发器表,而应通过 `ILayerState.tileStore` 获取默认值,只在地图侧额外存储“手动覆盖值”。
|
||||
|
||||
当前建议结构如下:
|
||||
|
||||
```ts
|
||||
Map<number, number>;
|
||||
```
|
||||
|
||||
- key = `y * width + x` 形式的格点下标;
|
||||
- value = 手动覆盖的触发器类型数字。
|
||||
|
||||
`IMapLayer.getTriggerType(x, y)` 的读取顺序建议为:
|
||||
|
||||
1. 若坐标越界,返回 `-1`;
|
||||
2. 若该点存在手动覆盖,直接返回覆盖值;
|
||||
3. 否则读取当前图块数字,并通过 `tileStore.getTrigger(num)` 返回默认触发器类型。
|
||||
|
||||
这样可以自然满足“仅存必要”的目标:
|
||||
|
||||
1. 没有手动修改过触发器的格点不占额外存储;
|
||||
2. 大多数格点直接复用 `ITileStore` 中的默认定义,不需要在地图层重复抄一份;
|
||||
3. 只有真正偏离默认值的点,才进入地图侧稀疏映射;
|
||||
4. `revertTrigger` 只需删除覆盖记录即可恢复默认。
|
||||
|
||||
## 3. 动态图块直接携带触发器类型
|
||||
|
||||
动态图块已经是实例对象,因此不需要再为它额外建一层稀疏映射。更自然的方案是:
|
||||
|
||||
1. `DynamicTile` 内部直接保存 `triggerType: number`;
|
||||
2. 图块移动时,触发器天然跟随这个实例走;
|
||||
3. 收集器按坐标枚举动态图块后,直接读取其 `triggerType`。
|
||||
|
||||
这样“触发器跟着动态图块走”就不再需要额外维护坐标索引迁移,只要图块对象本身移动即可。
|
||||
|
||||
## 4. 动静态转换只负责迁移或丢弃触发器
|
||||
|
||||
因为静态格点与动态图块现在是两个不同的触发器载体,所以 `transferToDynamic` / `transferToStatic` / `transferToStaticIfSafe` 的核心职责也更清晰了:
|
||||
|
||||
1. `transferToDynamic(..., keepTrigger = true)`:把静态格点上的触发器迁到新动态图块;
|
||||
2. `transferToDynamic(..., keepTrigger = false)`:只转换图块,不保留原触发器,并清理静态格点上的手动覆盖;
|
||||
3. `transferToStatic(..., keepTrigger = true)`:把动态图块携带的触发器写回静态格点;若与默认值一致则回退默认,否则记为手动覆盖;
|
||||
4. `transferToStatic(..., keepTrigger = false)`:只还原图块,不回写动态图块触发器,让静态格点自然回退到图块默认触发器;
|
||||
5. `transferToStaticIfSafe(..., keepTrigger = true)`:在安全回写条件满足时,保持与 `transferToStatic` 一致的迁移语义。
|
||||
|
||||
这比把“是否保留触发器”揉进 `setBlock` 更符合职责边界,因为真正发生触发器载体切换的地方只有这些转换接口。
|
||||
|
||||
## 5. 图块写接口不再隐式影响触发器
|
||||
|
||||
既然图块数组与触发器数据已经拆开,则以下接口默认不应直接修改触发器:
|
||||
|
||||
1. `setBlock`
|
||||
2. `putMapData`
|
||||
3. `setMapRef`
|
||||
4. `openDoor`
|
||||
5. `closeDoor`
|
||||
|
||||
这样做的好处是:
|
||||
|
||||
1. 图块外观变化与逻辑触发变化彻底解耦;
|
||||
2. 不再需要 `keepInfo` / `keepTrigger` 之类参数污染高频图块写路径;
|
||||
3. 调用方若确实希望同步改动触发器,应显式调用 `setTriggerType` 或走动静态转换接口。
|
||||
|
||||
这里唯一需要特殊处理的是 `resize` / `resize2`:当地图缩小时,越界格点对应的稀疏触发器记录必须一起裁剪。
|
||||
|
||||
## 6. 触发器对象延后到 Layer 2 实例化
|
||||
|
||||
当前触发器接口仍然可以保留 `ITrigger` 对象模型,但这个对象不应该提前存进地图数据层。
|
||||
|
||||
更合理的职责划分是:
|
||||
|
||||
1. Layer 1 只存 `triggerType: number`;
|
||||
2. `ITriggerCollector` 在收集时根据 `triggerType` 向 `ITriggerRegistry` 要工厂;
|
||||
3. 再由工厂创建运行时 `ITrigger` 对象并执行后续排序、触发流程。
|
||||
|
||||
这样既满足“地图上只存一个触发器类型数字即可”的诉求,也不需要推翻当前 Layer 2 的触发器执行模型。
|
||||
|
||||
## 7. 当前存档边界
|
||||
|
||||
静态格点的触发器来源当前已经明确为“图块默认值 + 地图手动覆盖值”两层结构,因此存档边界也可以先做阶段性收敛:
|
||||
|
||||
1. 图块默认触发器由 `ITileStore` 提供,不需要在地图层重复存储;
|
||||
2. 若静态格点的手动覆盖值需要进入存档,那么只需要保存地图侧的稀疏覆盖映射,而不需要保存整份默认触发器表;
|
||||
3. 动态图块的存储方案目前尚未设计完成,因此其 `triggerType` 是否进入存档、读档后如何恢复,暂不在本轮接口设计中拍死。
|
||||
|
||||
因此,这一节当前只确认静态覆盖的存储边界;动态图块相关存档语义后续再单独设计。
|
||||
|
||||
## 8. 单个载体当前只存一个触发器类型
|
||||
|
||||
本轮设计中,每个静态格点与每个动态图块都只保存一个 `triggerType: number`。
|
||||
|
||||
这样做的原因是:
|
||||
|
||||
1. 当前触发器本身不接受用户自定义参数,地图侧没有必要提前背负对象列表;
|
||||
2. 对绝大多数格点而言,一个触发器类型已经足够;
|
||||
3. 同一点多触发器仍可通过“静态格点 1 个 + 多个动态图块各 1 个”的方式聚合。
|
||||
|
||||
当前这条限制已经足够。以现有触发器定位来看,一个触发器类型只是告诉系统“这次行为该走哪种处理分支”;更复杂的行为应当放到自定义事件等更高自由度的描述层中,而不是在单个图块上堆叠多个触发器类型。
|
||||
|
||||
---
|
||||
|
||||
# 涉及文件
|
||||
|
||||
## 需要引用的文件
|
||||
|
||||
- `@user/data-base/src/map/types.ts`:当前 `ILayerState`、`IMapLayer`、`IDynamicLayer`、`IDynamicTile` 的权威接口定义
|
||||
- `@user/data-base/src/store/types.ts`:`ITileStore` 的权威接口定义
|
||||
- `@user/data-base/src/map/mapLayer.ts`:静态图层当前的图块数组实现
|
||||
- `@user/data-base/src/map/dynamicLayer.ts`:动静态图块转换逻辑的当前实现
|
||||
- `@user/data-base/src/map/dynamicTile.ts`:动态图块实例对象,适合新增 `triggerType`
|
||||
- `docs/dev/map/trigger.md`:当前触发器文档仍假定从 `getTileInfo` 读取信息,后续需要同步调整
|
||||
|
||||
## 需要修改的文件
|
||||
|
||||
### `@user/data-base/src/map/types.ts`
|
||||
|
||||
- [ ] 不再新增 `ITileInfo` / `IWrappedTileInfo` 作为本轮主接口
|
||||
- [ ] 为 `ILayerState` 新增 `readonly tileStore: ITileStore`
|
||||
- [ ] 为 `IMapLayer` 新增 `getTriggerType(x, y): number`
|
||||
- [ ] 为 `IMapLayer` 新增 `setTriggerType(type: number, x: number, y: number): void`
|
||||
- [ ] 为 `IMapLayer` 新增 `revertTrigger(x: number, y: number): void`
|
||||
- [ ] 为 `IDynamicTile` 新增 `readonly triggerType: number`
|
||||
- [ ] 为 `IDynamicTile` 新增 `setTriggerType(type: number): void`
|
||||
- [ ] 修改 `IDynamicLayer.transferToDynamic`:新增 `keepTrigger?: boolean`
|
||||
- [ ] 修改 `IDynamicLayer.transferToStatic`:新增 `keepTrigger?: boolean`
|
||||
- [ ] 修改 `IDynamicLayer.transferToStaticIfSafe`:新增 `keepTrigger?: boolean`
|
||||
|
||||
### `@user/data-base/src/map/mapLayer.ts`
|
||||
|
||||
- [ ] 新增静态手动覆盖触发器类型的稀疏存储结构
|
||||
- [ ] 实现 `getTriggerType` / `setTriggerType` / `revertTrigger`
|
||||
- [ ] 保持 `setBlock`、`putMapData`、`setMapRef`、`openDoor`、`closeDoor` 与触发器逻辑解耦
|
||||
- [ ] 在 `resize` / `resize2` 中裁剪越界触发器记录
|
||||
|
||||
### `@user/data-base/src/map/dynamicTile.ts`
|
||||
|
||||
- [ ] 新增 `triggerType` 成员与对应写接口
|
||||
|
||||
### `@user/data-base/src/map/dynamicLayer.ts`
|
||||
|
||||
- [ ] 在 `transferToDynamic` 中实现静态格点与动态图块之间的触发器迁移或丢弃逻辑
|
||||
- [ ] 在 `transferToStatic` 中实现动态图块与静态格点之间的触发器迁移或丢弃逻辑
|
||||
- [ ] 若保留 `transferToStaticIfSafe` 的对称性,同步补齐其触发器迁移逻辑
|
||||
|
||||
### `@user/data-base/src/map/mapStore.ts`
|
||||
|
||||
- [ ] 若静态手动覆盖需要进入存档,仅保存地图侧稀疏覆盖映射
|
||||
- [ ] 动态图块触发器字段的存档逻辑待动态图块存储方案定稿后再补齐
|
||||
|
||||
### `docs/dev/map/trigger.md`
|
||||
|
||||
- [ ] 将 `collect` 过程从“读取 `getTileInfo` 中的对象信息”改为“读取静态与动态载体上的触发器类型数字,再按需实例化 `ITrigger`”
|
||||
|
||||
---
|
||||
|
||||
# 当前结论
|
||||
|
||||
1. 静态格点触发器来源采用“`ITileStore` 默认值 + 地图手动覆盖值”两层结构,`revertTrigger` 用于恢复默认值。
|
||||
2. `getTriggerType(x, y)` 与 `ITileStore.getTrigger(num)` 对齐,越界或不存在触发器时统一返回 `-1`。
|
||||
3. `transferToDynamic`、`transferToStatic`、`transferToStaticIfSafe` 的 `keepTrigger` 默认值暂定为 `true`。
|
||||
4. 静态图层只稀疏存储手动覆盖值,不重复保存整份默认触发器表。
|
||||
5. 动态图块触发器的存档语义暂不拍死,待动态图块存储方案单独定稿后再处理。
|
||||
6. 单个载体只保存一个 `triggerType` 当前已经足够,复杂行为应交由自定义事件等更高层描述。
|
||||
@ -36,22 +36,42 @@ export class DynamicLayer
|
||||
|
||||
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): IDynamicTile {
|
||||
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);
|
||||
return this.createDynamic(num, x, y);
|
||||
this.layer.revertTrigger(x, y);
|
||||
const tile = this.createDynamic(num, x, y);
|
||||
tile.setTriggerType(triggerType);
|
||||
return tile;
|
||||
}
|
||||
|
||||
transferToStatic(tile: IDynamicTile): void {
|
||||
/**
|
||||
* 将动态图块上的触发器同步回当前静态格点
|
||||
*/
|
||||
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) {
|
||||
@ -62,16 +82,26 @@ export class DynamicLayer
|
||||
logger.warn(129, x.toString(), y.toString());
|
||||
}
|
||||
this.layer.setBlock(tile.num, x, y);
|
||||
this.syncStaticTrigger(tile, keepTrigger);
|
||||
this.removeTile(tile);
|
||||
this.forEachHook(hook => {
|
||||
void hook.onDeleteTile?.(tile, this);
|
||||
});
|
||||
this.forEachHook(hook => hook.onDeleteTile?.(tile, this));
|
||||
}
|
||||
|
||||
transferToStaticIfSafe(tile: IDynamicTile): boolean {
|
||||
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, tile.x, tile.y);
|
||||
this.deleteDynamic(tile);
|
||||
this.layer.setBlock(tile.num, x, y);
|
||||
this.syncStaticTrigger(tile, keepTrigger);
|
||||
this.removeTile(tile);
|
||||
this.forEachHook(hook => hook.onDeleteTile?.(tile, this));
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -117,12 +147,18 @@ export class DynamicLayer
|
||||
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,
|
||||
@ -131,7 +167,9 @@ export class DynamicLayer
|
||||
this.tilePosMap.get(y)?.get(x)?.delete(tile);
|
||||
}
|
||||
|
||||
/** 从两个内部映射中移除图块记录 */
|
||||
/**
|
||||
* 从两个内部映射中移除图块记录
|
||||
*/
|
||||
private removeTile(tile: IDynamicTile): void {
|
||||
const pos = this.posTileMap.get(tile);
|
||||
if (pos) {
|
||||
|
||||
@ -10,6 +10,7 @@ import { DynamicTileMover } from './mover';
|
||||
|
||||
export class DynamicTile implements IDynamicTile {
|
||||
readonly mover: IObjectMover<IDynamicTile>;
|
||||
triggerType: number;
|
||||
|
||||
/** 当前的朝向绑定对象 */
|
||||
private face: IRoleFaceBinder | null = null;
|
||||
@ -21,6 +22,7 @@ export class DynamicTile implements IDynamicTile {
|
||||
public readonly layer: IDynamicLayer
|
||||
) {
|
||||
this.mover = new DynamicTileMover(this);
|
||||
this.triggerType = -1;
|
||||
}
|
||||
|
||||
setFaceBinder(binder: IRoleFaceBinder | null): void {
|
||||
@ -36,6 +38,10 @@ export class DynamicTile implements IDynamicTile {
|
||||
return this.num;
|
||||
}
|
||||
|
||||
setTriggerType(type: number): void {
|
||||
this.triggerType = type;
|
||||
}
|
||||
|
||||
delete(): Promise<void> {
|
||||
return this.layer.deleteDynamic(this);
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
IMapLayerHookController,
|
||||
IMapLayerHooks
|
||||
} from './types';
|
||||
import { ITileStore } from '../store';
|
||||
import { MapLayer } from './mapLayer';
|
||||
|
||||
export class LayerState
|
||||
@ -38,6 +39,7 @@ export class LayerState
|
||||
private dirty: boolean = false;
|
||||
|
||||
constructor(
|
||||
public readonly tileStore: ITileStore,
|
||||
public width: number,
|
||||
public height: number
|
||||
) {
|
||||
@ -46,7 +48,12 @@ export class LayerState
|
||||
|
||||
addLayer(): IMapLayer {
|
||||
const array = new Uint32Array(this.width * this.height);
|
||||
const layer = new MapLayer(array, this.width, this.height);
|
||||
const layer = new MapLayer(
|
||||
array,
|
||||
this.width,
|
||||
this.height,
|
||||
this.tileStore
|
||||
);
|
||||
this.layerList.add(layer);
|
||||
this.mapLayerList.add(layer);
|
||||
this.forEachHook(hook => {
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
} from './types';
|
||||
import { Hookable, HookController, logger } from '@motajs/common';
|
||||
import { DynamicLayer } from './dynamicLayer';
|
||||
import { ITileStore } from '../store';
|
||||
|
||||
// todo: 提供 core.setBlock 等方法的替代方法,同时添加 setBlockList,以及前景背景的接口
|
||||
|
||||
@ -24,10 +25,17 @@ export class MapLayer
|
||||
private mapArray: Uint32Array;
|
||||
/** 地图数据引用 */
|
||||
private mapData: IMapLayerData;
|
||||
/** 手动触发器覆盖映射,key = y * width + x */
|
||||
private triggerMap: Map<number, number> = new Map();
|
||||
|
||||
readonly dynamicLayer: IDynamicLayer;
|
||||
|
||||
constructor(array: Uint32Array, width: number, height: number) {
|
||||
constructor(
|
||||
array: Uint32Array,
|
||||
width: number,
|
||||
height: number,
|
||||
private readonly tileStore: ITileStore
|
||||
) {
|
||||
super();
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
@ -42,6 +50,25 @@ export class MapLayer
|
||||
this.dynamicLayer = new DynamicLayer(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在地图尺寸变化后重新映射手动触发器覆盖表
|
||||
*/
|
||||
private remapTriggerMap(
|
||||
beforeWidth: number,
|
||||
width: number,
|
||||
height: number
|
||||
): Map<number, number> {
|
||||
const next = new Map<number, number>();
|
||||
for (const [index, type] of this.triggerMap) {
|
||||
const x = index % beforeWidth;
|
||||
const y = Math.floor(index / beforeWidth);
|
||||
if (x < width && y < height) {
|
||||
next.set(y * width + x, type);
|
||||
}
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
resize(width: number, height: number): void {
|
||||
if (this.width === width && this.height === height) {
|
||||
return;
|
||||
@ -55,6 +82,7 @@ export class MapLayer
|
||||
this.height = height;
|
||||
const area = width * height;
|
||||
const newArray = new Uint32Array(area);
|
||||
this.triggerMap = this.remapTriggerMap(beforeWidth, width, height);
|
||||
this.mapArray = newArray;
|
||||
// 将原来的地图数组赋值给现在的
|
||||
if (beforeArea > area) {
|
||||
@ -84,13 +112,16 @@ export class MapLayer
|
||||
|
||||
resize2(width: number, height: number): void {
|
||||
if (this.width === width && this.height === height) {
|
||||
this.empty = true;
|
||||
this.mapArray.fill(0);
|
||||
this.triggerMap.clear();
|
||||
return;
|
||||
}
|
||||
this.mapData.expired = true;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.mapArray = new Uint32Array(width * height);
|
||||
this.triggerMap.clear();
|
||||
this.mapData = {
|
||||
expired: false,
|
||||
array: this.mapArray
|
||||
@ -121,6 +152,39 @@ export class MapLayer
|
||||
return this.mapArray[y * this.width + x];
|
||||
}
|
||||
|
||||
getTriggerType(x: number, y: number): number {
|
||||
if (x < 0 || y < 0 || x >= this.width || y >= this.height) {
|
||||
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 (x < 0 || y < 0 || x >= this.width || y >= this.height) {
|
||||
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 (x >= 0 && y >= 0 && x < this.width && y < this.height) {
|
||||
this.triggerMap.delete(y * this.width + x);
|
||||
}
|
||||
}
|
||||
|
||||
clearTrigger(): void {
|
||||
this.triggerMap.clear();
|
||||
}
|
||||
|
||||
putMapData(array: Uint32Array, x: number, y: number, width: number): void {
|
||||
if (array.length % width !== 0) {
|
||||
logger.warn(8);
|
||||
@ -201,13 +265,6 @@ export class MapLayer
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取地图数据的内部存储直接引用
|
||||
*/
|
||||
getMapRef(): IMapLayerData {
|
||||
return this.mapData;
|
||||
}
|
||||
|
||||
setMapRef(array: Uint32Array): void {
|
||||
if (array.length !== this.width * this.height) {
|
||||
logger.warn(
|
||||
@ -229,6 +286,18 @@ export class MapLayer
|
||||
});
|
||||
}
|
||||
|
||||
getMapRef(): IMapLayerData {
|
||||
return this.mapData;
|
||||
}
|
||||
|
||||
setTriggerRef(triggers: Map<number, number>): void {
|
||||
this.triggerMap = triggers;
|
||||
}
|
||||
|
||||
getTriggerRef(): ReadonlyMap<number, number> {
|
||||
return this.triggerMap;
|
||||
}
|
||||
|
||||
protected createController(
|
||||
hook: Partial<IMapLayerHooks>
|
||||
): IMapLayerHookController {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { logger } from '@motajs/common';
|
||||
import { SaveCompression } from '../common';
|
||||
import { ITileStore } from '../store';
|
||||
import {
|
||||
ILayerState,
|
||||
ILayerStateSave,
|
||||
@ -31,6 +32,8 @@ export class MapStore implements IMapStore {
|
||||
/** 自动分区激活器开关 */
|
||||
private autoActivitorEnabled: boolean = false;
|
||||
|
||||
constructor(private readonly tileStore: ITileStore) {}
|
||||
|
||||
//#region 楼层管理
|
||||
|
||||
createLayerState(id: string, width: number, height: number): ILayerState {
|
||||
@ -39,7 +42,7 @@ export class MapStore implements IMapStore {
|
||||
} else {
|
||||
this.maps.push(id);
|
||||
}
|
||||
const state = new LayerState(width, height);
|
||||
const state = new LayerState(this.tileStore, width, height);
|
||||
// 若 refData 已存在,新楼层直接视为全脏
|
||||
if (this.refData !== null) {
|
||||
state.setDirty(true);
|
||||
@ -117,6 +120,10 @@ export class MapStore implements IMapStore {
|
||||
this.lastFloorId = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 maps 下标查找其所属的分区
|
||||
* @param idx 楼层在 maps 中的下标
|
||||
*/
|
||||
private findAreaByIndex(idx: number): MapArea | null {
|
||||
for (const area of this.areaList) {
|
||||
for (const interval of area) {
|
||||
@ -128,6 +135,11 @@ export class MapStore implements IMapStore {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置一个分区内所有楼层的激活状态
|
||||
* @param area 目标分区
|
||||
* @param active 要设置的激活状态
|
||||
*/
|
||||
private setAreaActive(area: MapArea, active: boolean): void {
|
||||
for (const interval of area) {
|
||||
for (let i = interval.start; i <= interval.end; i++) {
|
||||
@ -169,7 +181,7 @@ export class MapStore implements IMapStore {
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 存档及压缩
|
||||
//#region 存档工具
|
||||
|
||||
compareWith(ref: Map<string, Map<number, Uint32Array>>): void {
|
||||
if (this.refData !== null) return;
|
||||
@ -202,6 +214,116 @@ export class MapStore implements IMapStore {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取楼层内所有图层的静态触发器覆盖映射
|
||||
* @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) {
|
||||
@ -211,77 +333,119 @@ export class MapStore implements IMapStore {
|
||||
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));
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
} 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));
|
||||
}
|
||||
}
|
||||
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 };
|
||||
}
|
||||
|
||||
/**
|
||||
* 以高压缩方式序列化所有激活楼层
|
||||
*/
|
||||
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, layerStateSave] of state.floors) {
|
||||
for (const [id, save] of state.floors) {
|
||||
const cur = this.mapData.get(id);
|
||||
if (!cur) {
|
||||
logger.warn(122, id);
|
||||
continue;
|
||||
}
|
||||
cur.setBackground(layerStateSave.background);
|
||||
cur.setBackground(save.background);
|
||||
for (const layer of cur.layerList) {
|
||||
const layerSave = layerStateSave.layers.get(layer.zIndex);
|
||||
if (!layerSave?.fullMap) continue;
|
||||
layer.setMapRef(new Uint32Array(layerSave.fullMap));
|
||||
// 地图
|
||||
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);
|
||||
}
|
||||
cur.setDirty(false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -289,6 +453,7 @@ export class MapStore implements IMapStore {
|
||||
* LowCompression 读档:
|
||||
* - layers 有数据(dirty 楼层)→ fullMap 直接转移所有权
|
||||
* - layers 为空(非 dirty 楼层)→ 从参考基准恢复
|
||||
* @param state 整体地图存档数据
|
||||
*/
|
||||
private loadLowCompression(state: IMapStoreSave): void {
|
||||
if (!this.refData) {
|
||||
@ -298,7 +463,7 @@ export class MapStore implements IMapStore {
|
||||
for (const [id, cur] of this.mapData) {
|
||||
cur.setActiveStatus(state.floors.has(id));
|
||||
}
|
||||
for (const [id, layerStateSave] of state.floors) {
|
||||
for (const [id, save] of state.floors) {
|
||||
const cur = this.mapData.get(id);
|
||||
const refFloor = this.refData.get(id);
|
||||
if (!cur) {
|
||||
@ -309,21 +474,26 @@ export class MapStore implements IMapStore {
|
||||
logger.warn(124, id);
|
||||
continue;
|
||||
}
|
||||
cur.setBackground(layerStateSave.background);
|
||||
cur.setBackground(save.background);
|
||||
let shouldDirty = false;
|
||||
for (const layer of cur.layerList) {
|
||||
const layerSave = layerStateSave.layers.get(layer.zIndex);
|
||||
// 地图
|
||||
const layerSave = save.layers.get(layer.zIndex);
|
||||
if (layerSave?.fullMap) {
|
||||
layer.setMapRef(layerSave.fullMap);
|
||||
layer.setMapRef(new Uint32Array(layerSave.fullMap));
|
||||
shouldDirty = true;
|
||||
} else {
|
||||
const refArray = refFloor?.get(layer.zIndex);
|
||||
const refArray = refFloor.get(layer.zIndex);
|
||||
if (!refArray) {
|
||||
logger.warn(124, id);
|
||||
return;
|
||||
}
|
||||
layer.setMapRef(new Uint32Array(refArray));
|
||||
}
|
||||
// 触发器
|
||||
this.loadTriggers(save, layer);
|
||||
}
|
||||
cur.setDirty(false);
|
||||
cur.setDirty(shouldDirty);
|
||||
}
|
||||
}
|
||||
|
||||
@ -331,6 +501,7 @@ export class MapStore implements IMapStore {
|
||||
* HighCompression 读档:
|
||||
* - layers 有数据(dirty 楼层)→ 以参考基准为底,叠加差分行
|
||||
* - layers 为空(非 dirty 楼层)或图层无变化(rows 缺失)→ 从参考基准恢复
|
||||
* @param state 整体地图存档数据
|
||||
*/
|
||||
private loadHighCompression(state: IMapStoreSave): void {
|
||||
if (!this.refData) {
|
||||
@ -340,7 +511,7 @@ export class MapStore implements IMapStore {
|
||||
for (const [id, cur] of this.mapData) {
|
||||
cur.setActiveStatus(state.floors.has(id));
|
||||
}
|
||||
for (const [id, layerStateSave] of state.floors) {
|
||||
for (const [id, save] of state.floors) {
|
||||
const cur = this.mapData.get(id);
|
||||
const refFloor = this.refData.get(id);
|
||||
if (!cur) {
|
||||
@ -351,34 +522,32 @@ export class MapStore implements IMapStore {
|
||||
logger.warn(124, id);
|
||||
continue;
|
||||
}
|
||||
cur.setBackground(layerStateSave.background);
|
||||
let isMapDirty = true;
|
||||
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 = layerStateSave.layers.get(layer.zIndex);
|
||||
// 地图
|
||||
const layerSave = save.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));
|
||||
shouldDirty = true;
|
||||
const buf = new Uint32Array(refArray);
|
||||
for (const [rowIdx, rowData] of layerSave.rows) {
|
||||
buf.set(
|
||||
rowData.subarray(0, layer.width),
|
||||
rowIdx * layer.width
|
||||
);
|
||||
buf.set(rowData, rowIdx * layer.width);
|
||||
}
|
||||
layer.setMapRef(buf);
|
||||
}
|
||||
// 触发器
|
||||
this.loadTriggers(save, layer);
|
||||
}
|
||||
cur.setDirty(isMapDirty);
|
||||
cur.setDirty(shouldDirty);
|
||||
}
|
||||
}
|
||||
|
||||
@ -402,71 +571,5 @@ export class MapStore implements IMapStore {
|
||||
}
|
||||
}
|
||||
|
||||
//#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
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
IRoleFaceBinder,
|
||||
ISaveableContent
|
||||
} from '../common';
|
||||
import { ITileStore } from '../store';
|
||||
|
||||
//#region 静态图层
|
||||
|
||||
@ -103,6 +104,33 @@ export interface IMapLayer extends IHookable<
|
||||
*/
|
||||
getBlock(x: number, y: number): number;
|
||||
|
||||
/**
|
||||
* 获取指定点的静态图块对应的有效触发器类型,若手动覆盖不存在则回退到图块默认触发器
|
||||
* @param x 图块横坐标
|
||||
* @param y 图块纵坐标
|
||||
*/
|
||||
getTriggerType(x: number, y: number): number;
|
||||
|
||||
/**
|
||||
* 设置指定点静态图块的触发器
|
||||
* @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 地图图块数组
|
||||
@ -130,11 +158,31 @@ export interface IMapLayer extends IHookable<
|
||||
height: number
|
||||
): Uint32Array;
|
||||
|
||||
/**
|
||||
* 直接替换内部图块数组引用,跳过拷贝,高性能但风险较高。
|
||||
* 一般仅供 `MapStore` 读档时内部使用,外部正常情况下不应调用。
|
||||
* 调用方需确保传入数组的长度与 `width * height` 匹配,
|
||||
* 且调用后不得再持有或修改传入的数组。
|
||||
* @param array 地图数组,会直接替换内部引用
|
||||
*/
|
||||
setMapRef(array: Uint32Array): void;
|
||||
|
||||
/**
|
||||
* 获取整个地图的地图数组,是对内部数组的直接引用
|
||||
*/
|
||||
getMapRef(): IMapLayerData;
|
||||
|
||||
/**
|
||||
* 直接设置内部触发器映射对象,一般仅供内部存读档使用,外部正常情况下不应调用
|
||||
* @param triggers 触发器映射
|
||||
*/
|
||||
setTriggerRef(triggers: Map<number, number>): void;
|
||||
|
||||
/**
|
||||
* 获取静态触发器覆盖映射,一般仅供内部存档逻辑使用
|
||||
*/
|
||||
getTriggerRef(): ReadonlyMap<number, number>;
|
||||
|
||||
/**
|
||||
* 设置地图纵深,会影响渲染的遮挡顺序
|
||||
* @param zIndex 纵深
|
||||
@ -155,15 +203,6 @@ export interface IMapLayer extends IHookable<
|
||||
* @param y 门纵坐标
|
||||
*/
|
||||
closeDoor(num: number, x: number, y: number): Promise<void>;
|
||||
|
||||
/**
|
||||
* 直接替换内部图块数组引用,跳过拷贝,高性能但风险较高。
|
||||
* 一般仅供 `MapStore` 读档时内部使用,外部正常情况下不应调用。
|
||||
* 调用方需确保传入数组的长度与 `width * height` 匹配,
|
||||
* 且调用后不得再持有或修改传入的数组。
|
||||
* @param array 地图数组,会直接替换内部引用
|
||||
*/
|
||||
setMapRef(array: Uint32Array): void;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
@ -225,6 +264,8 @@ export interface ILayerStateHooks extends IHookBase {
|
||||
export interface ILayerState extends IHookable<ILayerStateHooks> {
|
||||
/** 地图列表 */
|
||||
readonly layerList: Set<IMapLayer>;
|
||||
/** 当前楼层共享的图块定义 store */
|
||||
readonly tileStore: ITileStore;
|
||||
/** 此楼层是否处于激活状态 */
|
||||
readonly active: boolean;
|
||||
/** 此楼层的地图宽度 */
|
||||
@ -316,7 +357,6 @@ export interface ILayerState extends IHookable<ILayerStateHooks> {
|
||||
|
||||
//#region 楼层管理
|
||||
|
||||
/** 单个 MapLayer 的存档数据 */
|
||||
export interface IMapLayerSave {
|
||||
readonly width: number;
|
||||
readonly height: number;
|
||||
@ -332,27 +372,27 @@ export interface IMapLayerSave {
|
||||
readonly fullMap?: Uint32Array;
|
||||
}
|
||||
|
||||
/** 单个楼层的存档数据 */
|
||||
export interface ILayerStateSave {
|
||||
/** 楼层背景 */
|
||||
readonly background: number;
|
||||
|
||||
/** key = zIndex,value = 对应图层存档数据 */
|
||||
readonly layers: ReadonlyMap<number, IMapLayerSave>;
|
||||
/** 静态触发器覆盖映射,仅在存在覆盖时写入 */
|
||||
readonly triggers: ReadonlyMap<number, ReadonlyMap<number, number>>;
|
||||
}
|
||||
|
||||
/** 整个 MapStore 的存档数据 */
|
||||
export interface IMapStoreSave {
|
||||
/** key = 楼层 id,只包含 active 的楼层,inactive 的楼层不写入,读档时无需处理 */
|
||||
readonly floors: ReadonlyMap<string, ILayerStateSave>;
|
||||
}
|
||||
|
||||
/** 单段闭区间 [start, end],start 和 end 均为 maps 下标 */
|
||||
export interface IMapAreaInterval {
|
||||
/** 区域起始索引,包含 */
|
||||
readonly start: number;
|
||||
/** 区域结束索引,包含 */
|
||||
readonly end: number;
|
||||
}
|
||||
|
||||
/** 一个区域由一个或多个独立区间组成 */
|
||||
export type MapArea = IMapAreaInterval[];
|
||||
|
||||
export interface IMapStore extends ISaveableContent<IMapStoreSave> {
|
||||
@ -504,21 +544,25 @@ export interface IDynamicLayer extends IHookable<IDynamicLayerHooks> {
|
||||
* @param y 纵坐标
|
||||
* @returns 创建的动态图块引用
|
||||
*/
|
||||
transferToDynamic(x: number, y: number): IDynamicTile;
|
||||
transferToDynamic(
|
||||
x: number,
|
||||
y: number,
|
||||
keepTrigger?: boolean
|
||||
): IDynamicTile;
|
||||
|
||||
/**
|
||||
* 将动态图块还原为静态图块。坐标越界则警告并放弃,
|
||||
* 否则写回静态图层并触发 {@link IDynamicLayerHooks.onDeleteTile}
|
||||
* @param tile 要还原的动态图块
|
||||
*/
|
||||
transferToStatic(tile: IDynamicTile): void;
|
||||
transferToStatic(tile: IDynamicTile, keepTrigger?: boolean): void;
|
||||
|
||||
/**
|
||||
* 仅当目标位置静态图块为 0(空白)时才还原为静态图块,否则不转换
|
||||
* @param tile 要还原的动态图块
|
||||
* @returns 是否转换成功
|
||||
*/
|
||||
transferToStaticIfSafe(tile: IDynamicTile): boolean;
|
||||
transferToStaticIfSafe(tile: IDynamicTile, keepTrigger?: boolean): boolean;
|
||||
|
||||
/**
|
||||
* 删除指定动态图块,触发 {@link IDynamicLayerHooks.onDeleteTile} 钩子。
|
||||
@ -557,6 +601,8 @@ export interface IDynamicLayer extends IHookable<IDynamicLayerHooks> {
|
||||
export interface IDynamicTile extends IObjectMovable {
|
||||
/** 当前图块数字 */
|
||||
readonly num: number;
|
||||
/** 当前动态图块携带的触发器类型,-1 表示无触发器 */
|
||||
readonly triggerType: number;
|
||||
/** 当前图块所属的动态图层 */
|
||||
readonly layer: IDynamicLayer;
|
||||
/** 当前动态图块的移动器 */
|
||||
@ -568,6 +614,12 @@ export interface IDynamicTile extends IObjectMovable {
|
||||
*/
|
||||
setFaceDirection(direction: FaceDirection): number;
|
||||
|
||||
/**
|
||||
* 设置当前动态图块的触发器类型
|
||||
* @param type 触发器类型
|
||||
*/
|
||||
setTriggerType(type: number): void;
|
||||
|
||||
/**
|
||||
* 直接删除此图块
|
||||
*/
|
||||
|
||||
@ -91,10 +91,10 @@ export class CoreState implements ICoreState {
|
||||
> = new Map();
|
||||
|
||||
constructor() {
|
||||
this.maps = new MapStore();
|
||||
const tileStore = new TileStore<LegacyTileData>();
|
||||
tileStore.attachLegacyConverter(new TileLegacyBridge());
|
||||
this.tileStore = tileStore;
|
||||
this.maps = new MapStore(tileStore);
|
||||
|
||||
this.loadProgress = new LoadProgressTotal();
|
||||
this.dataLoader = new MotaDataLoader(this.loadProgress);
|
||||
|
||||
@ -57,6 +57,15 @@
|
||||
- `IObjectMover.speed()`:预期频率**中频**。移动中修改移速有一定使用场景,但远不及 `forward`、`step` 等移动接口的频率,通常只在特殊演出或逻辑中出现,故为中频。典型使用场景:NPC 逃离怪物时先定在原地,随后逐渐加速逃跑。
|
||||
- `IObjectMover.stepFace()`:预期频率**低频**。移动方向与朝向不同的常见场景(后退)已由 `backward` 覆盖,只有极特殊情况才需要此接口(如角色朝向固定但沿垂直方向平移),相当罕见,故为低频。
|
||||
|
||||
## 预期体量
|
||||
|
||||
本节应当写出预期的代码体量,并分析原因。示例如下:
|
||||
|
||||
预期代码体量为 200-300 行。分析如下:
|
||||
|
||||
- `IObjectMover` 首先需要完成计划存储与计划的定义,这些接口基本大致就是向数组中添加元素,每个方法内容都不多,整体预计在 100 行左右。
|
||||
- `IObjectMover` 还需要完成移动流程的编写工作,需要根据每个移动步按照流程执行不同的行为,这一过程较为复杂,预计需要 100-200 行。
|
||||
|
||||
# 实现思路
|
||||
|
||||
按照下面的格式分条描述实现思路。
|
||||
|
||||
Loading…
Reference in New Issue
Block a user