Compare commits

...

7 Commits

80 changed files with 1732 additions and 2160 deletions

15
dev.md
View File

@ -45,7 +45,7 @@
### 模块原则
- **无副作用**:所有模块只包含函数、类、常量的声明,不允许出现导出的变量声明或顶层代码执行,允许但不建议编写类的静态块。
- **模块初始化**:如需初始化,编写一个 `createXxx` 函数,在 `index.ts` 中整合后逐级向上传递,直至顶层模块统一执行。
- **模块初始化**:如需初始化,编写一个 `createXxx` 函数,在 `index.ts` 中整合后逐级向上传递,直至顶层模块统一执行。注意,当前设计理念下,**不应该**有场景会需要这种函数。
- **不转发导出**:不允许一个文件导出不属于当前 monorepo 或当前文件夹的内容。
- **无循环引用**:不允许出现循环引用。若遇到不得不循环引用的情况,应首先反思接口设计是否存在问题。
@ -122,14 +122,15 @@
数据端目前正在从旧引擎进行彻底性重构,分为三层:
**Layer 0 — 公共层**:包含公共接口、工具函数等内容,不依赖任何外部游戏逻辑,可被任意高层直接引用。内容较少,与 Layer 1 共同放在 `@user/data-base` 中,不单独开包
**Layer 0 — 公共层**:包含公共接口、工具函数等内容,不依赖任何外部游戏逻辑,可被任意高层直接引用。包含统一接口 `IDataCommon`
**Layer 1 — 数据层**:包含所有会影响游戏存档与流程的数据内容,如地图、怪物、玩家属性等。本层通过统一接口 `IStateBase` 对外暴露数据访问能力,各类数据模块均以此接口为核心组织。
**Layer 2 — 执行层**:直接引用 Layer 1负责产生影响游戏进程的动作如玩家控制、战斗计算等。本层内容不会进入存档仅通过修改 Layer 1 的数据来影响游戏状态,并通过统一接口 `ICoreState` 对外暴露执行能力。
| 包 | 层级 | 说明 |
| ------------------- | ----------------- | ------------------------------------------------------------------------------- |
| `@user/data-base` | Layer 0 / Layer 1 | 公共层与数据层,定义 `IStateBase` 及各类游戏数据(地图、怪物、玩家属性等) |
| `@user/data-state` | — | 数据端的顶层模块,指导 Layer 2 的执行行为,不直接参与执行 |
| `@user/data-system` | Layer 2 | 执行层,定义 `ICoreState`,依赖数据层实现玩家控制、战斗计算等影响游戏进程的动作 |
| 包 | 层级 | 说明 |
| ------------------- | ------- | ------------------------------------------------------------------------------- |
| `@user/data-common | Layer 0 | 公共层,定义 `IDataCommon` 及公共无依赖接口 |
| `@user/data-base` | Layer 1 | 数据层,定义 `IStateBase` 及可存档游戏数据(地图、怪物、玩家属性等) |
| `@user/data-system` | Layer 2 | 执行层,定义 `ICoreState`,依赖数据层实现玩家控制、战斗计算等影响游戏进程的动作 |
| `@user/data-state` | — | 数据端的顶层模块,指导 Layer 2 的执行行为,不直接参与执行 |

283
docs/dev/map/tile-info.md Normal file
View 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` 当前已经足够,复杂行为应交由自定义事件等更高层描述。

View File

@ -0,0 +1,308 @@
# 需求综述
当前关于地图触发器的设计卡在“图块定义本身尚未抽象出来”这一前置问题上。继续讨论 [docs/dev/map/tile-info.md](docs/dev/map/tile-info.md) 中的地图触发器存储方案,会立刻撞上一个更底层的问题:
1. 某个图块数字到底对应什么 id
2. 某个图块默认携带什么触发器类型;
3. 某个图块属于哪一类图块;
4. 旧引擎中的 `blocksInfo` 应该如何转移到新接口。
因此,下一步更合适的顺序不是继续扩展地图侧接口,而是先补齐一个独立的 `ITileStore`。它属于 Layer 0不参与存档也不承载运行时动态状态只负责提供“图块定义查询”这一底层能力。
这次设计的目标如下:
1. 将当前挂在全局状态上的 `idNumberMap``numberIdMap` 收拢到 `ITileStore` 内;
2. 为图块定义提供统一查询入口:`getData`、`getTrigger`、`getType`
3. 提供统一写入口 `addTile`,用于初始化阶段录入图块定义;
4. 提供 `attachLegacyConverter``fromLegacy` 两个接口,用于从旧引擎迁移图块定义;
5. 明确这部分能力不属于存档系统,且应当放在 Layer 0。
---
# 接口设计与预期
## ITileRawData
`ITileRawData` 表示单个图块的最小原始定义。按照当前结论,需要包含四个字段:图块数字、图块 id、触发器类型、图块类型。
- `ITileRawData.num`:预期频率**中频**。图块数字是整个图块定义系统的主键,`addTile`、`getData` 与旧引擎导入都会依赖它,但日常脚本中通常不会反复直接读写,故为中频。
- `ITileRawData.id`:预期频率**中频**。图块 id 常用于脚本层、兼容层与素材层之间的衔接,出现频率不低,但通常通过 `idToNumber` 间接使用,故为中频。典型使用场景:旧接口或脚本给出 `yellowDoor` 这类字符串 id希望先查到对应图块数字。
- `ITileRawData.trigger`:预期频率**中频**。当前触发器设计已经收敛到“数据层只存触发器类型数字”,因此这是图块定义中的核心字段之一,但大多数代码仍会优先通过 `getTrigger` 访问,故为中频。典型使用场景:地图初始化时根据图块默认定义,给某个格点填入默认触发器类型。
- `ITileRawData.type`:预期频率**中频**。图块类型会被 `getType` 高效访问但在定义录入阶段仍应直接作为图块原始数据的一部分保存避免再人为拆成第二套并行数据源。典型使用场景地图或逻辑层拿到某个图块定义后希望直接知道其属于地形、怪物、NPC 还是道具。
当前建议接口如下:
```ts
export interface ITileRawData {
readonly num: number;
readonly id: string;
readonly trigger: number;
readonly type: TileType;
}
```
之所以推荐使用 `num` 而不是 `number`,是因为当前仓库内地图与图块相关接口几乎都使用 `num` 表示图块数字,延续这一命名更自然。
## TileType
`getType` 需要返回一个图块类型枚举。按照当前需求,建议先定义以下八种:
1. `Unknown`
2. `None`
3. `Terrain`
4. `Animate`
5. `Item`
6. `Enemy`
7. `Npc`
8. `Tileset`
这里的目的不是完整复刻旧引擎的所有 `cls`,而是先给当前执行层与地图层提供足够稳定、足够粗粒度的类型划分。基于旧引擎的现有分类,当前建议映射关系如下:
1. `0` 号空白图块映射为 `None`
2. `terrains``autotile` 统一映射为 `Terrain`
3. `animates` 映射为 `Animate`
4. `items` 映射为 `Item`
5. `enemys``enemy48` 统一映射为 `Enemy`
6. `npcs``npc48` 统一映射为 `Npc`
7. `tileset` 映射为 `Tileset`
8. 其他尚未归类或不存在的图块映射为 `Unknown`
这样处理的原因是:当前数据端真正需要的是“足够稳定的逻辑分类”,而不是把渲染素材维度的细分 `cls` 原封不动搬进底层接口。
## ITileStore
`ITileStore` 是图块定义的统一查询与写入接口。由于它本身不会进入存档,也不承担运行时状态变化,因此整体频率分布会明显偏向“读取高于写入”。
- `ITileStore.getData(num)`:预期频率**中频**。这个接口会返回完整的 `ITileRawData`适合调试、兼容层、编辑器与初始化阶段使用但在真正的高频逻辑中调用方通常只关心某个单独字段因此为中频。典型使用场景兼容层需要同时读取图块数字、id 与默认触发器。
- `ITileStore.getTrigger(num)`:预期频率**高频**。这是 `getData(num).trigger` 的快捷接口,后续地图初始化、事件绑定与触发器相关逻辑都会优先使用这一接口,故为高频。典型使用场景:根据某个图块数字读取其默认触发器类型,再决定是否写入地图触发器稀疏表。
- `ITileStore.getType(num)`:预期频率**高频**。图块类型分类会直接影响地图逻辑、兼容层判断、后续的地图对象设计,因此它和 `getTrigger` 一样属于高频读取接口。典型使用场景:逻辑层拿到某个图块数字后,需要快速判断它属于地形、道具、怪物还是 NPC。
- `ITileStore.addTile(data)`:预期频率**低频**。图块定义在正常运行期不会动态修改,`addTile` 主要用于初始化与旧数据导入阶段,故为低频。
- `ITileStore.idToNumber(id)`:预期频率**中频**。它是 `idNumberMap` 的方法化替代,兼容层、脚本层和部分初始化逻辑都需要从字符串 id 反查图块数字,故为中频。典型使用场景:旧接口 `setBlock('yellowDoor', x, y)` 需要先把 id 转成图块数字。
- `ITileStore.numberToId(num)`:预期频率**中频**。它是 `numberIdMap` 的方法化替代,主要用于调试、兼容层与少量需要回推图块 id 的场景,故为中频。典型使用场景:拿到地图上的图块数字后,希望恢复出旧引擎语义下的图块 id。
- `ITileStore.attachLegacyConverter(converter)`:预期频率**低频**。仅在初始化或切换兼容转换器时调用,负责把旧引擎图块定义的解释规则注入到 store 中,故为低频。
- `ITileStore.fromLegacy(num, legacy)`:预期频率**低频**。用于将单个旧样板图块定义转换并写入 store整体使用方式应与 `IEnemyManager.fromLegacyEnemy` 类似,由外层自行遍历 legacy store 后逐个调用,故为低频。典型使用场景:初始化时遍历 `core.maps.blocksInfo`,对每一项执行 `tileStore.fromLegacy(num, block)`
当前建议接口如下:
```ts
export interface ITileStore {
getData(num: number): ITileRawData | null;
getTrigger(num: number): number;
getType(num: number): TileType;
addTile(data: ITileRawData): void;
idToNumber(id: string): number | null;
numberToId(num: number): string | null;
attachLegacyConverter<TLegacy>(
converter: ITileLegacyConverter<TLegacy>
): void;
fromLegacy<TLegacy>(num: number, legacy: TLegacy): ITileRawData;
}
```
返回值语义建议如下:
1. `getData(num)`:若图块不存在,返回 `null`
2. `getTrigger(num)`:若图块不存在或未配置触发器,返回 `-1`
3. `getType(num)`:若图块不存在或尚未归类,返回 `TileType.Unknown`
4. `idToNumber(id)` / `numberToId(num)`:不存在时返回 `null`
之所以让 `getTrigger``getType` 在缺失场景下返回稳定默认值,是因为这两者更偏“高频逻辑查询”,热路径上不适合层层判空。
## ITileLegacyConverter / attachLegacyConverter / fromLegacy
`ITileStore` 本身不应直接理解旧引擎里 `blocksInfo` 的全部细节,而应通过用户层提供的 legacy converter 完成转换。这样做的原因是:旧引擎中的默认触发器来源并不统一,既有显式 `trigger` 字段,也有通过 `cls` 或其他成员隐式决定的情况。
从 [public/project/maps.js](public/project/maps.js) 当前样板可以直接看到两类典型情况:
1. 部分图块显式写了 `trigger`,例如黄门的 `openDoor`、箱子的 `pushBox`、冰面或滑板的特殊触发;
2. 部分图块没有显式 `trigger`,但其行为仍然会根据 `cls` 或其他规则隐式确定,例如怪物图块。
因此更合适的设计是:
```ts
export interface ITileLegacyConverter<TLegacy> {
fromLegacy(num: number, legacy: TLegacy): ITileRawData;
}
```
其中:
1. `attachLegacyConverter(converter)` 负责向 store 注入转换器;
2. `fromLegacy(num, legacy)` 负责调用当前转换器完成单个 legacy 图块定义的转换,并将结果写入 store
3. 导入整个旧引擎 `blocksInfo` 时,由外层自行遍历并多次调用 `fromLegacy`
当前更推荐的使用形式是:
```ts
tileStore.attachLegacyConverter(converter);
for (const [key, value] of Object.entries(core.maps.blocksInfo)) {
tileStore.fromLegacy(Number(key), value);
}
```
其职责包括:
1. 用户层定义 legacy -> `ITileRawData` 的转换规则;
2. 显式处理“trigger 字段优先”与“按 cls 推导”并存的旧设计;
3. 保证 `ITileStore` 本体只负责存储与查询,不负责耦合旧样板细节。
---
# 实现思路
## 1. 先建立独立的 store 模块
因为 `ITileStore` 属于 Layer 0且不依赖地图、怪物、勇士等更高层模块所以更适合作为 `@user/data-base/src/store` 下的首个 Store 类接口存在。
当前不建议单独再开 `tile` 文件夹,而是直接放到 [packages-user/data-base/src/store](packages-user/data-base/src/store) 中。这样后续若还有其他内容迁移为新的 Store 类接口,也可以继续并列放在 `store` 目录下,而不是再拆出多个平行顶层目录。
## 2. 对外暴露方法,对内仍可继续使用双映射
`idNumberMap``numberIdMap` 迁移到 `ITileStore` 后,对外不再暴露原始 `Map`,而改成方法:
1. `idToNumber(id)`
2. `numberToId(num)`
但在内部实现上,仍然完全可以保留:
1. `Map<string, number>`
2. `Map<number, string>`
3. `Map<number, ITileRawData>`
也就是说,这次重构的重点是**收拢职责和稳定接口**,不是刻意放弃现有映射结构。
## 3. getType 直接从原始数据读取
按照当前结论,`ITileRawData` 包含:
1. `num`
2. `id`
3. `trigger`
4. `type`
这意味着 `getType(num)` 不需要再依赖额外并行映射,而可以直接读取 `getData(num)?.type`。这样做的好处是:
1. `addTile` 的输入结构完整且闭合;
2. 不会再出现“raw data 一套、type 映射又一套”的双数据源;
3. legacy converter 转换出的结果可以直接完整写入 store。
## 4. addTile 只负责录入定义,不负责存档
`addTile(data)` 的职责应当收敛到“向 store 录入一个图块定义”,而不是承担任何运行时逻辑。
因为这部分数据不会动态变更,也不参与存档,所以其主要使用时机只有:
1. 初次初始化
2. 旧引擎数据迁移
3. 极少量的测试或工具链注入
当前你已经给出了 number 冲突时“警告并覆盖”的语义,这一点应该直接保留。
此外,`id` 冲突但 `num` 不冲突时,也应当采用同样的警告并覆盖策略;并且警告内容需要明确指出冲突来源到底是 `num` 还是 `id`
## 5. 全局状态从两张 Map 改为一个 store
当前 [packages-user/data-base/src/types.ts](packages-user/data-base/src/types.ts) 里的 `IStateBase` 仍直接暴露:
1. `idNumberMap`
2. `numberIdMap`
如果 `ITileStore` 建立起来,那么全局状态更合理的暴露方式应当改为:
```ts
readonly tileStore: ITileStore;
```
后续所有调用点统一改成:
1. `state.tileStore.idToNumber(id)`
2. `state.tileStore.numberToId(num)`
3. `state.tileStore.getTrigger(num)`
4. `state.tileStore.getType(num)`
这样图块定义相关职责就不会再散落在全局状态根节点上。
## 6. 旧引擎迁移的职责边界
当前 [packages-user/data-state/src/index.ts](packages-user/data-state/src/index.ts) 在初始化阶段直接遍历 `core.maps.blocksInfo`,并手动填充 `state.idNumberMap` / `state.numberIdMap`
引入 `ITileStore` 后,这段初始化逻辑更适合拆成两段:
1. 先在用户层实现并挂载 legacy converter
2. 再遍历 `core.maps.blocksInfo`,逐个调用 `tileStore.fromLegacy(num, block)`
3. 其他真正依赖图块定义的初始化逻辑,例如朝向绑定,再从 `tileStore` 继续读取数据。
这样旧引擎兼容逻辑就不会和全局状态初始化逻辑搅在一起。
同时,因为旧样板里的触发器来源并不统一,这种“用户层 converter + store 只负责存储”的边界也更合理:
1. store 不需要知道 `trigger` 究竟来自字段、`cls`,还是更特殊的 legacy 规则;
2. 用户层可以按当前项目的具体兼容策略自由决定优先级;
3. 将来若 legacy 来源变化,只需要替换 converter不必改 `ITileStore` 本体。
---
# 涉及文件
## 需要引用的文件
- [packages-user/data-base/src/types.ts](packages-user/data-base/src/types.ts):当前 `IStateBase` 仍直接暴露 `idNumberMap``numberIdMap`
- [packages-user/data-state/src/index.ts](packages-user/data-state/src/index.ts):当前旧引擎图块定义导入逻辑的主要入口
- [packages-user/data-state/src/core.ts](packages-user/data-state/src/core.ts):当前 `CoreState` 中两张映射的实际持有位置
- [packages-user/client-modules/src/fallback/load.ts](packages-user/client-modules/src/fallback/load.ts):当前旧引擎图块 `cls` 分类的实际使用点,可作为 `TileType` 映射参考
- [docs/dev/map/tile-info.md](docs/dev/map/tile-info.md):后续地图触发器设计将直接依赖 `ITileStore`
## 需要修改的文件
### `@user/data-base/src/store/types.ts`
- [ ] 新增 `ITileRawData` 接口:定义图块最小原始定义,当前包含 `num`、`id`、`trigger`、`type`
- [ ] 新增 `TileType` 枚举:定义统一的图块逻辑分类
- [ ] 新增 `ITileLegacyConverter` 接口:定义 legacy 图块定义到 `ITileRawData` 的转换规则
- [ ] 新增 `ITileStore` 接口:提供图块定义的统一查询与写入入口
- [ ] 为 `ITileStore` 新增 `attachLegacyConverter``fromLegacy`
### `@user/data-base/src/store/tileStore.ts`
- [ ] 实现 `ITileStore`
- [ ] 内部维护 `num -> raw data`、`id -> num` 与 `num -> id` 的映射
- [ ] 实现 `addTile` 的警告覆盖逻辑,并区分 `num` 冲突与 `id` 冲突
- [ ] 实现 `attachLegacyConverter``fromLegacy`
### `@user/data-base/src/store/index.ts`
- [ ] 导出 tile 模块公共接口与实现
### `@user/data-base/src/types.ts`
- [ ] 从 `IStateBase` 中移除 `idNumberMap``numberIdMap`
- [ ] 新增 `readonly tileStore: ITileStore`
### `@user/data-base/src/index.ts`
- [ ] 补齐 tile 模块的公共导出
### `@user/data-state/src/core.ts`
- [ ] 移除 `CoreState` 对两张映射的直接持有
- [ ] 改为持有 `tileStore`
### `@user/data-state/src/index.ts`
- [ ] 将旧引擎 `blocksInfo` 的初始化逻辑迁移到“挂载 converter 后逐个调用 `fromLegacy`
- [ ] 后续朝向绑定等逻辑改为通过 `tileStore.idToNumber` 读取图块数字
### `docs/dev/map/tile-info.md`
- [ ] 在 `ITileStore` 定稿后,再继续补齐地图触发器设计文档中对图块默认触发器来源的描述
---
# 当前结论
1. `TileType` 应当直接包含进 `ITileRawData`,不再单独拆成并行数据源。
2. legacy 导入不再使用顶层工厂函数,而改为 `attachLegacyConverter + fromLegacy` 组合;由用户层自行提供 converter再逐个执行转换。
3. `addTile``num` 冲突与 `id` 冲突两种场景下都应警告并覆盖,且警告信息必须明确指出冲突来源。
4. `getTrigger(num)` 在图块不存在或无触发器时统一返回 `-1`
5. 旧引擎里的默认触发器来源是混合式的:有时来自显式 `trigger` 字段,有时来自 `cls` 或其他规则;这一差异应当由用户层 converter 消化,而不是让 `ITileStore` 本体直接耦合旧样板细节。

View File

@ -1,6 +1,7 @@
{
"name": "@user/data-base",
"dependencies": {
"@user/data-common": "workspace:*",
"@motajs/common": "workspace:*",
"@motajs/types": "workspace:*",
"@motajs/loader": "workspace:*"

View File

@ -1,5 +1,5 @@
import { logger } from '@motajs/common';
import { SaveCompression } from '../common';
import { SaveCompression } from '@user/data-common';
import { IEnemy, IEnemySaveState, IReadonlyEnemy, ISpecial } from './types';
export class Enemy<TAttr> implements IEnemy<TAttr> {

View File

@ -10,7 +10,7 @@ import {
SpecialCreation,
IEnemySaveState
} from './types';
import { SaveCompression } from '../common';
import { SaveCompression } from '@user/data-common';
export class EnemyManager<TAttr> implements IEnemyManager<TAttr> {
/** 特殊属性注册表code -> 创建函数 */

View File

@ -1,5 +1,5 @@
import { isEqual } from 'lodash-es';
import { SaveCompression } from '../common';
import { SaveCompression } from '@user/data-common';
import { ISpecial, SpecialCreation } from './types';
// TODO: 颜色参数

View File

@ -1,4 +1,4 @@
import { ISaveableContent } from '../common';
import { ISaveableContent } from '@user/data-common';
//#region 怪物基础

View File

@ -1,6 +1,6 @@
//#region 字段
import { ISaveableContent } from '../common';
import { ISaveableContent } from '@user/data-common';
export interface IFlagCommonField<T> {
/** 此字段所处的 Flag 系统 */

View File

@ -1,5 +1,5 @@
import { logger } from '@motajs/common';
import { SaveCompression } from '../common';
import { SaveCompression } from '@user/data-common';
import { IHeroAttribute, IHeroModifier } from './types';
export abstract class BaseHeroModifier<T, V> implements IHeroModifier<T, V, V> {

View File

@ -1,8 +1,8 @@
import { Hookable, HookController, IHookController } from '@motajs/common';
import { isNil } from 'lodash-es';
import { getFaceMovement, nextFaceDirection } from '../common/utils';
import { getFaceMovement, nextFaceDirection } from '@user/data-common';
import { IHeroFollower, IHeroMover, IHeroMovingHooks } from './types';
import { FaceDirection } from '../common';
import { FaceDirection } from '@user/data-common';
const DEFAULT_HERO_IMAGE: ImageIds = 'hero.png';

View File

@ -8,7 +8,7 @@ import {
IModifierStateSave,
IReadonlyHeroAttribute
} from './types';
import { SaveCompression } from '../common';
import { SaveCompression } from '@user/data-common';
import { logger } from '@motajs/common';
export class HeroState<THero> implements IHeroState<THero> {

View File

@ -1,5 +1,5 @@
import { IFacedTileLocator, IHookBase, IHookable } from '@motajs/common';
import { FaceDirection, ISaveableContent } from '../common';
import { FaceDirection, ISaveableContent } from '@user/data-common';
//#region 勇士属性

View File

@ -1,4 +1,3 @@
export * from './common';
export * from './enemy';
export * from './flag';
export * from './hero';

View File

@ -11,13 +11,15 @@ import {
IDynamicTile,
IMapLayer
} from './types';
import { FaceDirection, degradeFace } from '../common';
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();
@ -26,6 +28,7 @@ export class DynamicLayer
constructor(public readonly layer: IMapLayer) {
super();
this.state = layer.state;
}
protected createController(
@ -36,22 +39,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 +85,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 +150,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 +170,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) {

View File

@ -1,15 +1,18 @@
import { isNil } from 'lodash-es';
import {
FaceDirection,
IDataCommon,
IMoverController,
IObjectMover,
IRoleFaceBinder
} from '../common';
} from '@user/data-common';
import { IDynamicLayer, IDynamicTile } from './types';
import { DynamicTileMover } from './mover';
export class DynamicTile implements IDynamicTile {
readonly state: IDataCommon;
readonly mover: IObjectMover<IDynamicTile>;
triggerType: number;
/** 当前的朝向绑定对象 */
private face: IRoleFaceBinder | null = null;
@ -20,7 +23,9 @@ export class DynamicTile implements IDynamicTile {
public y: number,
public readonly layer: IDynamicLayer
) {
this.state = layer.state;
this.mover = new DynamicTileMover(this);
this.triggerType = -1;
}
setFaceBinder(binder: IRoleFaceBinder | null): void {
@ -36,6 +41,10 @@ export class DynamicTile implements IDynamicTile {
return this.num;
}
setTriggerType(type: number): void {
this.triggerType = type;
}
delete(): Promise<void> {
return this.layer.deleteDynamic(this);
}

View File

@ -11,6 +11,7 @@ import {
IMapLayerHookController,
IMapLayerHooks
} from './types';
import { IDataCommon, ITileStore } from '@user/data-common';
import { MapLayer } from './mapLayer';
export class LayerState
@ -38,6 +39,8 @@ export class LayerState
private dirty: boolean = false;
constructor(
public readonly state: IDataCommon,
public readonly tileStore: ITileStore,
public width: number,
public height: number
) {
@ -46,7 +49,13 @@ 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,
this.tileStore
);
this.layerList.add(layer);
this.mapLayerList.add(layer);
this.forEachHook(hook => {

View File

@ -1,6 +1,7 @@
import { isNil } from 'lodash-es';
import {
IDynamicLayer,
ILayerState,
IMapLayer,
IMapLayerData,
IMapLayerHookController,
@ -8,6 +9,7 @@ import {
} from './types';
import { Hookable, HookController, logger } from '@motajs/common';
import { DynamicLayer } from './dynamicLayer';
import { IDataCommon, ITileStore } from '@user/data-common';
// todo: 提供 core.setBlock 等方法的替代方法,同时添加 setBlockList以及前景背景的接口
@ -15,6 +17,8 @@ export class MapLayer
extends Hookable<IMapLayerHooks, IMapLayerHookController>
implements IMapLayer
{
readonly state: IDataCommon;
width: number;
height: number;
empty: boolean = true;
@ -24,11 +28,20 @@ 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,
public readonly layerState: ILayerState,
private readonly tileStore: ITileStore
) {
super();
this.state = layerState.state;
this.width = width;
this.height = height;
const area = width * height;
@ -42,6 +55,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 +87,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 +117,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 +157,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 +270,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 +291,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 {

View File

@ -1,5 +1,6 @@
import { logger } from '@motajs/common';
import { SaveCompression } from '../common';
import { IDataCommon, SaveCompression } from '@user/data-common';
import { ITileStore } from '@user/data-common';
import {
ILayerState,
ILayerStateSave,
@ -31,6 +32,11 @@ export class MapStore implements IMapStore {
/** 自动分区激活器开关 */
private autoActivitorEnabled: boolean = false;
constructor(
private readonly tileStore: ITileStore,
public readonly state: IDataCommon
) {}
//#region 楼层管理
createLayerState(id: string, width: number, height: number): ILayerState {
@ -39,7 +45,7 @@ export class MapStore implements IMapStore {
} else {
this.maps.push(id);
}
const state = new LayerState(width, height);
const state = new LayerState(this.state, this.tileStore, width, height);
// 若 refData 已存在,新楼层直接视为全脏
if (this.refData !== null) {
state.setDirty(true);
@ -117,6 +123,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 +138,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 +184,7 @@ export class MapStore implements IMapStore {
//#endregion
//#region 存档及压缩
//#region 存档工具
compareWith(ref: Map<string, Map<number, Uint32Array>>): void {
if (this.refData !== null) return;
@ -202,6 +217,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 +336,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 +456,7 @@ export class MapStore implements IMapStore {
* LowCompression
* - layers dirty fullMap
* - layers dirty
* @param state
*/
private loadLowCompression(state: IMapStoreSave): void {
if (!this.refData) {
@ -298,7 +466,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 +477,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 +504,7 @@ export class MapStore implements IMapStore {
* HighCompression
* - layers dirty
* - layers dirty rows
* @param state
*/
private loadHighCompression(state: IMapStoreSave): void {
if (!this.refData) {
@ -340,7 +514,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 +525,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 +574,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
}

View File

@ -4,8 +4,9 @@ import {
ObjectMover,
ObjectMoveStep,
ObjectMoveStepType
} from '../common';
} from '@user/data-common';
import { IDynamicTile } from './types';
import { DYNAMIC_MOVER_FACE } from '../shared';
//#region 动态图块
@ -16,7 +17,8 @@ const enum DynamicMoveCode {
export class DynamicTileMover extends ObjectMover<IDynamicTile> {
constructor(public readonly tile: IDynamicTile) {
super();
const face = tile.state.faceManager;
super(face.get(DYNAMIC_MOVER_FACE)!);
}
protected onMoveStart(): Promise<void> {

View File

@ -1,12 +1,14 @@
import { IHookable, IHookBase, IHookController } from '@motajs/common';
import {
FaceDirection,
IDataCommonExtended,
IMoverController,
IObjectMovable,
IObjectMover,
IRoleFaceBinder,
ISaveableContent
} from '../common';
} from '@user/data-common';
import { ITileStore } from '@user/data-common';
//#region 静态图层
@ -68,10 +70,10 @@ export interface IMapLayerHookController extends IHookController<IMapLayerHooks>
getMapData(): Readonly<IMapLayerData>;
}
export interface IMapLayer extends IHookable<
IMapLayerHooks,
IMapLayerHookController
> {
export interface IMapLayer
extends
IHookable<IMapLayerHooks, IMapLayerHookController>,
IDataCommonExtended {
/** 地图宽度 */
readonly width: number;
/** 地图高度 */
@ -84,6 +86,8 @@ export interface IMapLayer extends IHookable<
/** 图层纵深 */
readonly zIndex: number;
/** 当前图层所属的地图状态对象 */
readonly layerState: ILayerState;
/** 此图层对应的动态图块图层z 层级与静态图块一致 */
readonly dynamicLayer: IDynamicLayer;
@ -103,6 +107,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 +161,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 +206,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
@ -222,9 +264,12 @@ export interface ILayerStateHooks extends IHookBase {
onResizeLayer(layer: IMapLayer, width: number, height: number): void;
}
export interface ILayerState extends IHookable<ILayerStateHooks> {
export interface ILayerState
extends IHookable<ILayerStateHooks>, IDataCommonExtended {
/** 地图列表 */
readonly layerList: Set<IMapLayer>;
/** 当前楼层共享的图块定义 store */
readonly tileStore: ITileStore;
/** 此楼层是否处于激活状态 */
readonly active: boolean;
/** 此楼层的地图宽度 */
@ -316,7 +361,6 @@ export interface ILayerState extends IHookable<ILayerStateHooks> {
//#region 楼层管理
/** 单个 MapLayer 的存档数据 */
export interface IMapLayerSave {
readonly width: number;
readonly height: number;
@ -332,30 +376,31 @@ export interface IMapLayerSave {
readonly fullMap?: Uint32Array;
}
/** 单个楼层的存档数据 */
export interface ILayerStateSave {
/** 楼层背景 */
readonly background: number;
/** key = zIndexvalue = 对应图层存档数据 */
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> {
export interface IMapStore
extends ISaveableContent<IMapStoreSave>, IDataCommonExtended {
/** 所有楼层的 id 有序数组 */
readonly maps: ReadonlyArray<string>;
@ -484,7 +529,8 @@ export interface IDynamicLayerHooks extends IHookBase {
onUpdateTilePosition(tile: IDynamicTile, layer: IDynamicLayer): void;
}
export interface IDynamicLayer extends IHookable<IDynamicLayerHooks> {
export interface IDynamicLayer
extends IHookable<IDynamicLayerHooks>, IDataCommonExtended {
/** 当前动态图层所属的静态图层 */
readonly layer: IMapLayer;
@ -504,21 +550,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}
@ -554,9 +604,11 @@ export interface IDynamicLayer extends IHookable<IDynamicLayerHooks> {
updateDynamicTile(tile: IDynamicTile): void;
}
export interface IDynamicTile extends IObjectMovable {
export interface IDynamicTile extends IObjectMovable, IDataCommonExtended {
/** 当前图块数字 */
readonly num: number;
/** 当前动态图块携带的触发器类型,-1 表示无触发器 */
readonly triggerType: number;
/** 当前图块所属的动态图层 */
readonly layer: IDynamicLayer;
/** 当前动态图块的移动器 */
@ -568,6 +620,12 @@ export interface IDynamicTile extends IObjectMovable {
*/
setFaceDirection(direction: FaceDirection): number;
/**
*
* @param type
*/
setTriggerType(type: number): void;
/**
*
*/

View File

@ -0,0 +1,4 @@
import { FaceGroup } from '@user/data-common';
/** 动态图块所使用的默认移动组,不知道干什么的就别动 */
export const DYNAMIC_MOVER_FACE = FaceGroup.Dir8;

View File

@ -1,24 +1,15 @@
import { IHeroFollower, IHeroState } from './hero';
import { IEnemyManager } from './enemy';
import { IFlagSystem } from './flag';
import { IFaceManager, IRoleFaceBinder, ISaveableContent } from './common';
import { IMapStore } from './map';
import { IDataCommon, ISaveableContent } from '@user/data-common';
export interface IStateSaveData {
/** 跟随者列表 */
readonly followers: readonly IHeroFollower[];
}
export interface IStateBase<TEnemy, THero> {
/** 朝向绑定 */
readonly roleFace: IRoleFaceBinder;
/** 朝向管理 */
readonly faceManager: IFaceManager;
/** id 到图块数字的映射 */
readonly idNumberMap: Map<string, number>;
/** 图块数字到 id 的映射 */
readonly numberIdMap: Map<number, string>;
export interface IStateBase<TEnemy, THero> extends IDataCommon {
/** 地图状态 */
readonly maps: IMapStore;
/** 勇士状态 */
@ -43,3 +34,8 @@ export interface IStateBase<TEnemy, THero> {
*/
getSaveableContent<T>(id: string): ISaveableContent<T> | null;
}
export interface IStateBaseExtended<TEnemy = unknown, THero = unknown> {
/** 当前对象对应的数据层对象Layer 1 对象) */
readonly state: IStateBase<TEnemy, THero>;
}

View File

@ -0,0 +1,6 @@
{
"name": "@user/data-common",
"dependencies": {
"@motajs/common": "workspace:*"
}
}

View File

@ -1,5 +1,5 @@
import { logger } from '@motajs/common';
import { IFaceData, IRoleFaceBinder } from './types';
import { IFaceData, IRoleFaceBinder } from '../common';
import { isNil } from 'lodash-es';
import { FaceDirection } from '.';

View File

@ -2,7 +2,7 @@ import { FaceDirection } from './types';
//#region 接口与枚举
export const enum InternalFaceGroup {
export const enum FaceGroup {
/** 四方向(上下左右) */
Dir4,
/** 八方向(上下左右+斜向) */

View File

@ -0,0 +1,4 @@
export * from './common';
export * from './store';
export * from './types';

View File

@ -0,0 +1,2 @@
export * from './tileStore';
export * from './types';

View File

@ -0,0 +1,86 @@
import { isNil } from 'lodash-es';
import { logger } from '@motajs/common';
import {
ITileLegacyConverter,
ITileRawData,
ITileStore,
TileType
} from './types';
export class TileStore<TLegacy = unknown> implements ITileStore<TLegacy> {
/** 以图块数字为键的原始图块定义表 */
private readonly dataMap: Map<number, ITileRawData> = new Map();
/** 由图块 id 反查图块数字的映射表 */
private readonly idMap: Map<string, number> = new Map();
/** 由图块数字反查图块 id 的映射表 */
private readonly numMap: Map<number, string> = new Map();
/** 当前挂载的旧样板图块转换器 */
private legacyConverter: ITileLegacyConverter<TLegacy> | null = null;
getData(num: number): ITileRawData | null {
return this.dataMap.get(num) ?? null;
}
getTrigger(num: number): number {
return this.dataMap.get(num)?.trigger ?? -1;
}
getType(num: number): TileType {
return this.dataMap.get(num)?.type ?? TileType.Unknown;
}
addTile(data: ITileRawData): void {
const oldData = this.dataMap.get(data.num);
const oldNum = this.idMap.get(data.id);
if (oldData) {
logger.warn(133, data.num.toString(), oldData.id);
this.deleteBy(oldData.num, oldData.id);
}
if (!isNil(oldNum) && oldNum !== data.num) {
logger.warn(134, data.id, oldNum.toString());
const oldIdData = this.dataMap.get(oldNum);
if (oldIdData) {
this.deleteBy(oldIdData.num, oldIdData.id);
} else {
this.idMap.delete(data.id);
this.numMap.delete(oldNum);
}
}
this.dataMap.set(data.num, data);
this.idMap.set(data.id, data.num);
this.numMap.set(data.num, data.id);
}
idToNumber(id: string): number | null {
return this.idMap.get(id) ?? null;
}
numberToId(num: number): string | null {
return this.numMap.get(num) ?? null;
}
attachLegacyConverter(converter: ITileLegacyConverter<TLegacy>): void {
this.legacyConverter = converter;
}
fromLegacy(num: number, legacy: TLegacy): ITileRawData {
const converter = this.legacyConverter;
if (!converter) {
logger.error(56);
throw new Error('Expected a tile legacy converter');
}
const data = converter.fromLegacy(num, legacy);
this.addTile(data);
return data;
}
/** 删除一组旧的图块定义及其双向索引 */
private deleteBy(num: number, id: string): void {
this.dataMap.delete(num);
this.idMap.delete(id);
this.numMap.delete(num);
}
}

View File

@ -0,0 +1,91 @@
export const enum TileType {
/** 未知或尚未归类的图块 */
Unknown,
/** 空白图块 */
None,
/** 地形类图块 */
Terrain,
/** 动画类图块 */
Animate,
/** 道具类图块 */
Item,
/** 怪物类图块 */
Enemy,
/** NPC 类图块 */
Npc,
/** 自动元件 */
Autotile,
/** Tileset 切片图块 */
Tileset
}
export interface ITileRawData {
/** 图块数字 */
readonly num: number;
/** 图块字符串 id */
readonly id: string;
/** 默认触发器类型 */
readonly trigger: number;
/** 图块逻辑类型 */
readonly type: TileType;
}
export interface ITileLegacyConverter<TLegacy> {
/**
*
* @param num
* @param legacy
*/
fromLegacy(num: number, legacy: TLegacy): ITileRawData;
}
export interface ITileStore<TLegacy = unknown> {
/**
*
* @param num
*/
getData(num: number): ITileRawData | null;
/**
*
* @param num
*/
getTrigger(num: number): number;
/**
*
* @param num
*/
getType(num: number): TileType;
/**
* `num` `id`
* @param data
*/
addTile(data: ITileRawData): void;
/**
* id
* @param id id
*/
idToNumber(id: string): number | null;
/**
* id
* @param num
*/
numberToId(num: number): string | null;
/**
*
* @param converter
*/
attachLegacyConverter(converter: ITileLegacyConverter<TLegacy>): void;
/**
* 使
* @param num
* @param legacy
*/
fromLegacy(num: number, legacy: TLegacy): ITileRawData;
}

View File

@ -0,0 +1,16 @@
import { IFaceManager, IRoleFaceBinder } from './common';
import { ITileStore } from './store';
export interface IDataCommon {
/** 图块定义存储 */
readonly tileStore: ITileStore<MapDataOf<keyof NumberToId>>;
/** 朝向绑定 */
readonly roleFace: IRoleFaceBinder;
/** 朝向管理 */
readonly faceManager: IFaceManager;
}
export interface IDataCommonExtended {
/** 当前对象对应的公共层对象Layer 0 对象) */
readonly state: IDataCommon;
}

View File

@ -3,7 +3,6 @@
"dependencies": {
"@motajs/legacy-common": "workspace:*",
"@user/data-state": "workspace:*",
"@user/data-base": "workspace:*",
"@user/data-utils": "workspace:*"
"@user/data-base": "workspace:*"
}
}

View File

@ -1,270 +0,0 @@
import {
DamageEnemy,
ensureFloorDamage,
getEnemy,
state
} from '@user/data-state';
import { hook } from '@user/data-base';
import { Patch, PatchClass } from '@motajs/legacy-common';
import { isNil } from 'lodash-es';
export interface CurrentEnemy {
enemy: DamageEnemy;
// 这个是干啥的?
onMapEnemy: DamageEnemy[];
}
export function patchBattle() {
const patch = new Patch(PatchClass.Enemys);
const patch2 = new Patch(PatchClass.Events);
patch.add('canBattle', function (x, y, floorId) {
const enemy = typeof x === 'number' ? getEnemy(x, y!, floorId) : x;
if (!enemy) {
throw new Error(
`Cannot get enemy on x:${x}, y:${y}, floor: ${floorId}`
);
}
const { damage } = enemy.calDamage();
return damage < core.status.hero.hp;
});
function battle(
x: number | DamageEnemy,
y: number,
force: boolean = false,
callback?: () => void
) {
core.saveAndStopAutomaticRoute();
const isLoc = typeof x === 'number';
const enemy = isLoc ? getEnemy(x, y) : x;
if (!enemy) {
throw new Error(
`Cannot battle with enemy since no enemy on ${x},${y}`
);
}
// 非强制战斗
// @ts-expect-error 2.c 重构
if (!core.canBattle(x, y) && !force && !core.status.event.id) {
core.stopSound();
core.playSound('操作失败');
core.drawTip('你打不过此怪物!', enemy!.id);
return core.clearContinueAutomaticRoute(callback);
}
// 自动存档
if (!core.status.event.id) core.autosave(true);
// 战前事件
// 战后事件
core.afterBattle(enemy, isLoc ? x : enemy.x, y);
callback?.();
}
const getFacedId = (enemy: DamageEnemy) => {
const e = enemy.enemy;
if (e.displayIdInBook) return e.displayIdInBook;
if (e.faceIds) return e.faceIds.down;
return e.id;
};
patch.add('getCurrentEnemys', function (floorId = core.status.floorId) {
floorId = floorId || core.status.floorId;
const enemys: CurrentEnemy[] = [];
const used: Record<string, DamageEnemy[]> = {};
ensureFloorDamage(floorId);
const floor = core.status.maps[floorId];
floor.enemy.list.forEach(v => {
const id = getFacedId(v);
if (!(id in used)) {
const e = new DamageEnemy(v.enemy);
e.calAttribute();
e.getRealInfo();
e.calDamage();
const curr: CurrentEnemy = {
enemy: e,
onMapEnemy: [v]
};
enemys.push(curr);
used[id] = curr.onMapEnemy;
} else {
used[id].push(v);
}
});
return enemys.sort((a, b) => {
const ad = a.enemy.calDamage().damage;
const bd = b.enemy.calDamage().damage;
return ad - bd;
});
});
patch2.add('battle', battle);
patch2.add('_sys_battle', function (data: Block, callback?: () => void) {
// 检查战前事件
const floor = core.floors[core.status.floorId];
const beforeBattle: MotaEvent = [];
const loc = `${data.x},${data.y}` as LocString;
const enemy = getEnemy(data.x, data.y);
beforeBattle.push(...(floor.beforeBattle[loc] ?? []));
beforeBattle.push(...(enemy!.enemy.beforeBattle ?? []));
if (beforeBattle.length > 0) {
beforeBattle.push({ type: 'battle', x: data.x, y: data.y });
core.clearContinueAutomaticRoute();
// 自动存档
const inAction = core.status.event.id === 'action';
if (inAction) {
core.insertAction(beforeBattle, data.x, data.y);
core.doAction();
} else {
core.autosave(true);
core.insertAction(beforeBattle, data.x, data.y, callback);
}
} else {
battle(data.x, data.y, false, callback);
}
});
patch2.add('_action_battle', function (data, x, y, prefix) {
if (data.id) {
// const enemy = getSingleEnemy(data.id as EnemyIds);
// todo: 与不在地图上的怪物战斗
} else {
if (data.floorId !== core.status.floorId) {
core.doAction();
return;
}
const [ex, ey] = core.events.__action_getLoc(
data.loc,
x,
y,
prefix
) as LocArr;
battle(ex, ey, true, core.doAction);
}
});
patch2.add(
'afterBattle',
function (enemy: DamageEnemy, x?: number, y?: number) {
// 播放战斗动画
let animate: AnimationIds = 'hand';
// 检查当前装备是否存在攻击动画
const equipId = core.getEquip(0);
if (equipId && (core.material.items[equipId].equip || {}).animate)
animate = core.material.items[equipId].equip.animate;
// 检查该动画是否存在SE如果不存在则使用默认音效
if (!core.material.animates[animate]?.se)
core.playSound('attack.opus');
// 战斗伤害
const info = enemy.getRealInfo();
const damageInfo = enemy.calDamage(core.status.hero);
const damage = damageInfo.damage;
// 判定是否致死
if (damage >= core.status.hero.hp) {
core.status.hero.hp = 0;
core.updateStatusBar(false, true);
core.events.lose('战斗失败');
return;
}
// 扣减体力值并记录统计数据
core.status.hero.hp -= damage;
core.status.hero.statistics.battleDamage += damage;
core.status.hero.statistics.battle++;
// 获得金币经验
const money = core.hasFlag('curse') ? 0 : enemy.info.money!;
const exp = core.hasFlag('curse') ? 0 : enemy.info.exp!;
core.status.hero.money += money;
core.status.hero.statistics.money += money;
core.status.hero.exp += exp;
core.status.hero.statistics.exp += exp;
const hint = `打败 ${enemy.enemy.name},金币+${money},经验+${exp}`;
core.drawTip(hint, enemy.id);
// 毒衰咒
if (info.special.has(12)) core.setFlag('poison', true);
if (info.special.has(13)) core.setFlag('weak', true);
if (info.special.has(14)) core.setFlag('curse', true);
// 仇恨
if (info.special.has(17)) {
const hatred = state.flags.getFieldValueDefaults('hatred', 0);
core.setFlag('hatred', hatred / 2);
} else {
core.addFlag('hatred', core.values.hatred);
}
// 自爆
if (info.special.has(19)) {
core.status.hero.hp = 1;
}
// 退化
if (info.special.has(21)) {
core.status.hero.atk -= info.atkValue ?? 0;
core.status.hero.def -= info.defValue ?? 0;
}
// 事件的处理
const todo: MotaEvent = [];
// 战后事件
if (!isNil(core.status.floorId)) {
const loc = `${x},${y}` as LocString;
todo.push(
...(core.floors[core.status.floorId].afterBattle[loc] ?? [])
);
}
todo.push(...(enemy.enemy.afterBattle ?? []));
// 如果事件不为空,将其插入
if (todo.length > 0) core.insertAction(todo, x, y);
if (!isNil(x) && !isNil(y)) {
core.drawAnimate(animate, x, y);
core.removeBlock(x, y);
} else core.drawHeroAnimate(animate);
// 如果已有事件正在处理中
if (isNil(core.status.event.id)) core.continueAutomaticRoute();
else core.clearContinueAutomaticRoute();
core.checkAutoEvents();
hook.emit('afterBattle', enemy, x, y);
}
);
}
declare global {
interface Enemys {
getCurrentEnemys(floorId: FloorIds): CurrentEnemy[];
canBattle(enemy: DamageEnemy, _?: number, floorId?: FloorIds): boolean;
canBattle(x: number, y: number, floorId?: FloorIds): boolean;
}
interface Events {
battle(
enemy: DamageEnemy,
y?: number,
force?: boolean,
callback?: () => void
): void;
battle(
x: number,
y?: number,
force?: boolean,
callback?: () => void
): void;
}
}

View File

@ -1,124 +0,0 @@
import { Patch, PatchClass } from '@motajs/legacy-common';
import { EnemyCollection, ensureFloorDamage } from '@user/data-state';
import { formatDamage } from '@user/data-utils';
import { isNil } from 'lodash-es';
export function patchDamage() {
const patch = new Patch(PatchClass.Control);
patch.add(
'updateDamage',
function (
floorId = core.status.floorId,
ctx,
thumbnail: boolean = false
) {
if (!floorId || core.status.gameOver || main.mode !== 'play')
return;
const onMap = isNil(ctx);
const floor = core.status.maps[floorId];
// 没有怪物手册
// if (!core.hasItem('book')) return;
core.status.damage.posX = core.bigmap.posX;
core.status.damage.posY = core.bigmap.posY;
if (!onMap) {
const width = core.floors[floorId].width,
height = core.floors[floorId].height;
// 地图过大的缩略图不绘制显伤
if (width * height > core.bigmap.threshold) return;
}
// 计算伤害
ensureFloorDamage(floorId);
floor.enemy.extract();
floor.enemy.calRealAttribute();
floor.enemy.calMapDamage();
floor.enemy.emit('calculated');
core.status.damage.data = [];
// floor.enemy.render(true);
// getItemDetail(floorId, onMap); // 宝石血瓶详细信息
if (thumbnail) {
renderThumbnailDamage(floor.enemy);
core.control.drawDamage(ctx, floorId);
}
}
);
}
function renderThumbnailDamage(col: EnemyCollection) {
if (main.replayChecking) return;
core.status.damage.data = [];
core.status.damage.extraData = [];
// 怪物伤害
col.list.forEach(v => {
const { damage } = v.calDamage();
// 伤害全部相等,绘制在怪物本身所在位置
const { damage: dam, color } = formatDamage(damage);
const critical = v.calCritical(1)[0];
core.status.damage.data.push({
text: dam,
px: 32 * v.x! + 1,
py: 32 * (v.y! + 1) - 1,
color: color
});
const setting = Mota.require('@motajs/legacy-ui').mainSetting;
const criGem = setting.getValue('screen.criticalGem', false);
const n = critical?.atkDelta ?? Infinity;
const ratio = core.status.maps[col.floorId].ratio;
const cri = criGem ? Math.ceil(n / ratio) : n;
core.status.damage.data.push({
text: isFinite(cri) ? cri.toString() : '?',
px: 32 * v.x! + 1,
py: 32 * (v.y! + 1) - 11,
color: '#fff'
});
});
// 地图伤害
const floor = core.status.maps[col.floorId];
const width = floor.width;
const height = floor.height;
const objs = core.getMapBlocksObj(col.floorId);
const startX = 0;
const endX = width;
const startY = 0;
const endY = height;
for (let x = startX; x < endX; x++) {
for (let y = startY; y < endY; y++) {
const id = `${x},${y}` as LocString;
const dam = col.mapDamage[id];
if (!dam || objs[id]?.event.noPass) continue;
// 地图伤害
if (dam.damage !== 0) {
const damage = core.formatBigNumber(dam.damage, true);
const color = dam.damage < 0 ? '#6eff6a' : '#fa3';
core.status.damage.extraData.push({
text: damage,
px: 32 * x + 16,
py: 32 * y + 16,
color,
alpha: 1
});
}
// 追猎
if (dam.ambush) {
core.status.damage.extraData.push({
text: '!',
px: 32 * x + 16,
py: 32 * (y + 1) - 14,
color: '#fd4',
alpha: 1
});
}
}
}
}

View File

@ -1,12 +1,8 @@
import { ICoreState } from '@user/data-state';
import { patchBattle } from './battle';
import { patchDamage } from './damage';
import { patchFlags } from './flag';
import { patchHero } from './hero';
export function patchAll(state: ICoreState) {
patchBattle();
patchDamage();
patchFlags(state);
patchHero(state);
}

View File

@ -3,8 +3,8 @@
"dependencies": {
"@motajs/types": "workspace:*",
"@motajs/common": "workspace:*",
"@user/data-common": "workspace:*",
"@user/data-base": "workspace:*",
"@user/data-system": "workspace:*",
"@user/data-utils": "workspace:*"
"@user/data-system": "workspace:*"
}
}

View File

@ -1,12 +1,22 @@
import { ICoreState, ISaveableExecutor } from './types';
import {
DamageSystem,
EnemyContext,
IRoleFaceBinder,
IFaceManager,
ITileStore,
ISaveableContent,
TileStore,
SaveCompression,
RoleFaceBinder,
FaceManager,
Dir4FaceHandler,
Dir8FaceHandler,
FaceGroup,
FaceDirection
} from '@user/data-common';
import {
EnemyManager,
HeroMover,
IEnemyContext,
IEnemyManager,
MapDamage,
HeroAttribute,
HeroState,
IHeroState,
@ -15,20 +25,20 @@ import {
IMotaDataLoader,
MotaDataLoader,
loading,
IRoleFaceBinder,
RoleFaceBinder,
FaceDirection,
ISaveableContent,
SaveCompression,
IReadonlyEnemy,
IMapStore,
MapStore,
IFaceManager,
FaceManager,
InternalFaceGroup,
Dir4FaceHandler,
Dir8FaceHandler
MapStore
} from '@user/data-base';
import {
DamageSystem,
EnemyContext,
IEnemyContext,
ITriggerCollector,
ITriggerRegistry,
MapDamage,
TriggerCollector,
TriggerRegistry
} from '@user/data-system';
import {
CommonAuraConverter,
EnemyLegacyBridge,
@ -52,28 +62,32 @@ import {
TILE_WIDTH
} from './shared';
import { IHeroAttr } from './hero';
import { LegacyTileData, TileLegacyBridge } from './legacy';
import { ILoadProgressTotal, LoadProgressTotal } from '@motajs/loader';
import { isNil } from 'lodash-es';
import { logger } from '@motajs/common';
import { ISaveSystem, SaveSystem } from './save';
export class CoreState implements ICoreState {
// 全局内容
// Layer 0 内容
readonly roleFace: IRoleFaceBinder;
readonly faceManager: IFaceManager;
readonly idNumberMap: Map<string, number>;
readonly numberIdMap: Map<number, string>;
readonly tileStore: ITileStore<LegacyTileData>;
// 可存档内容
// Layer 1 内容
readonly maps: IMapStore;
readonly hero: IHeroState<IHeroAttr>;
readonly enemyManager: IEnemyManager<IEnemyAttr>;
readonly flags: IFlagSystem;
// 状态内容
// Layer 2 内容
readonly enemyContext: IEnemyContext<IEnemyAttr, IHeroAttr>;
readonly triggerRegistry: ITriggerRegistry;
readonly triggerCollector: ITriggerCollector;
// 用户层内容
readonly loadProgress: ILoadProgressTotal;
readonly dataLoader: IMotaDataLoader;
readonly enemyContext: IEnemyContext<IEnemyAttr, IHeroAttr>;
readonly saveSystem: ISaveSystem;
/** 可存档对象映射 */
@ -87,25 +101,43 @@ export class CoreState implements ICoreState {
> = new Map();
constructor() {
this.maps = new MapStore();
this.idNumberMap = new Map();
this.numberIdMap = new Map();
//#region L0 初始化
this.loadProgress = new LoadProgressTotal();
this.dataLoader = new MotaDataLoader(this.loadProgress);
// 朝向
this.roleFace = new RoleFaceBinder();
this.faceManager = new FaceManager();
const dir4 = new Dir4FaceHandler();
const dir8 = new Dir8FaceHandler();
this.faceManager.register(FaceGroup.Dir4, dir4);
this.faceManager.registerById('dir4', dir4);
this.faceManager.register(FaceGroup.Dir8, dir8);
this.faceManager.registerById('dir8', dir8);
//#region 勇士初始化
// 图块
const tileStore = new TileStore<LegacyTileData>();
tileStore.attachLegacyConverter(new TileLegacyBridge());
this.tileStore = tileStore;
//#endregion
//#region L1 初始化
// Flag 系统
this.flags = new FlagSystem();
// 地图
this.maps = new MapStore(tileStore, this);
// 勇士
const heroMover = new HeroMover();
const heroAttribute = new HeroAttribute(HERO_DEFAULT_ATTRIBUTE);
const heroState = new HeroState(heroMover, heroAttribute);
this.hero = heroState;
//#endregion
this.loadProgress = new LoadProgressTotal();
this.dataLoader = new MotaDataLoader(this.loadProgress);
//#region 怪物初始化
// 怪物管理器初始化
// 怪物管理器
const comparer = new MainEnemyComparer();
const enemyManager = new EnemyManager(new EnemyLegacyBridge());
enemyManager.attachEnemyComparer(comparer);
@ -117,8 +149,13 @@ export class CoreState implements ICoreState {
enemyManager.setAttributeDefaults('point', 0);
registerSpecials(enemyManager);
this.enemyManager = enemyManager;
// 怪物上下文初始化
const enemyContext = new EnemyContext<IEnemyAttr, IHeroAttr>();
//#endregion
//#region L2 初始化
// 怪物上下文
const enemyContext = new EnemyContext<IEnemyAttr, IHeroAttr>(this);
const damageSystem = new DamageSystem(enemyContext);
const mapDamage = new MapDamage(enemyContext);
damageSystem.useCalculator(new MainDamageCalculator());
@ -133,10 +170,18 @@ export class CoreState implements ICoreState {
enemyContext.bindHero(heroAttribute);
this.enemyContext = enemyContext;
// 触发器注册与收集器
const triggerRegistry = new TriggerRegistry();
const triggerCollector = new TriggerCollector();
triggerCollector.attachRegistry(triggerRegistry);
this.triggerRegistry = triggerRegistry;
this.triggerCollector = triggerCollector;
//#endregion
//#region 存档系统
//#region 顶层初始化
// 存档系统
this.saveSystem = new SaveSystem();
// 配置存档系统,一般情况下不建议动,除非你知道你在干什么
this.saveSystem.config({
@ -146,43 +191,55 @@ export class CoreState implements ICoreState {
saveTimeTolerance: 100,
autosaveStackSize: 20
});
this.addSaveableContent('@system/hero', this.hero);
this.addSaveableContent('@system/flags', this.flags);
this.addSaveableContent('@system/maps', this.maps);
this.addSaveableContent('@system/enemy', this.enemyManager);
// 初始化存档数据库,不要动
loading.once('coreInit', () => {
this.saveSystem.init(`@game/${core.firstData.name}`);
});
//#endregion
//#region 其他初始化
// 朝向
this.roleFace = new RoleFaceBinder();
this.faceManager = new FaceManager();
const dir4 = new Dir4FaceHandler();
const dir8 = new Dir8FaceHandler();
this.faceManager.register(InternalFaceGroup.Dir4, dir4);
this.faceManager.registerById('dir4', dir4);
this.faceManager.register(InternalFaceGroup.Dir8, dir8);
this.faceManager.registerById('dir8', dir8);
this.flags = new FlagSystem();
// 加载先使用兼容层实现
// 加载初始化,先使用兼容层实现
loading.once('loaded', () => {
this.initTileStore(core.maps.blocksInfo);
this.initEnemyManager(enemys_fcae963b_31c9_42b4_b48c_bb48d09f3f80);
this.initMapStore(
core.floorIds,
core.floors as Record<FloorIds, ResolvedFloor>
);
});
}
this.addSaveableContent('@system/hero', this.hero);
this.addSaveableContent('@system/flags', this.flags);
this.addSaveableContent('@system/maps', this.maps);
this.addSaveableContent('@system/enemy', this.enemyManager);
/**
*
* @param data
*/
private initTileStore(data: typeof core.maps.blocksInfo) {
const entries = Object.entries(data);
for (const [key, block] of entries) {
this.tileStore.fromLegacy(Number(key), block);
}
//#endregion
for (const [key, block] of entries) {
if (!block.faceIds) continue;
const { down, up, left, right } = block.faceIds;
const downNum = this.tileStore.idToNumber(down);
if (downNum !== Number(key)) continue;
const upNum = this.tileStore.idToNumber(up);
const leftNum = this.tileStore.idToNumber(left);
const rightNum = this.tileStore.idToNumber(right);
this.roleFace.malloc(downNum, FaceDirection.Down);
if (!isNil(upNum)) {
this.roleFace.bind(upNum, downNum, FaceDirection.Up);
}
if (!isNil(leftNum)) {
this.roleFace.bind(leftNum, downNum, FaceDirection.Left);
}
if (!isNil(rightNum)) {
this.roleFace.bind(rightNum, downNum, FaceDirection.Right);
}
}
}
/**
@ -194,15 +251,15 @@ export class CoreState implements ICoreState {
const manager = this.enemyManager;
const reference = new Map<number, IReadonlyEnemy<IEnemyAttr>>();
for (const [id, enemy] of Object.entries(structuredClone(data))) {
const num = this.idNumberMap.get(id);
const num = this.tileStore.idToNumber(id);
if (isNil(num)) continue;
if (enemy.faceIds) {
// 有 faceId 的要把其他的也映射到当前怪物
const { left, up, right, down } = enemy.faceIds;
const leftCode = this.idNumberMap.get(left)!;
const upCode = this.idNumberMap.get(up)!;
const rightCode = this.idNumberMap.get(right)!;
const downCode = this.idNumberMap.get(down)!;
const leftCode = this.tileStore.idToNumber(left)!;
const upCode = this.tileStore.idToNumber(up)!;
const rightCode = this.tileStore.idToNumber(right)!;
const downCode = this.tileStore.idToNumber(down)!;
const prefab = manager.fromLegacyEnemy(downCode, enemy);
reference.set(downCode, prefab);
manager.addPrefab(prefab);

View File

@ -1,813 +0,0 @@
import { getHeroStatusOf, getHeroStatusOn } from '../legacy/hero';
import { Range, ensureArray, has, manhattan } from '@user/data-utils';
import EventEmitter from 'eventemitter3';
import { hook } from '@user/data-base';
import {
EnemyInfo,
DamageInfo,
DamageDelta,
HaloData,
CriticalDamageDelta,
MapDamage,
HaloFn,
IEnemyCollection,
IDamageEnemy,
HaloType,
IEnemyCollectionEvent
} from '@motajs/types';
export class EnemyCollection
extends EventEmitter<IEnemyCollectionEvent>
implements IEnemyCollection
{
floorId: FloorIds;
list: Map<number, DamageEnemy> = new Map();
range: Range = new Range();
/** 地图伤害 */
mapDamage: Record<string, MapDamage> = {};
haloList: HaloData[] = [];
/** 楼层宽度 */
width: number = 0;
/** 楼层高度 */
height: number = 0;
constructor(floorId: FloorIds) {
super();
this.floorId = floorId;
this.extract();
}
get(x: number, y: number) {
const index = x + y * this.width;
return this.list.get(index) ?? null;
}
/**
*
*/
extract() {
this.list.clear();
core.extractBlocks(this.floorId);
const floor = core.status.maps[this.floorId];
this.width = floor.width;
this.height = floor.height;
floor.blocks.forEach(v => {
if (v.disable) return;
if (v.event.cls !== 'enemy48' && v.event.cls !== 'enemys') return;
const { x, y } = v;
const index = x + y * this.width;
const enemy = core.material.enemys[v.event.id as EnemyIds];
this.list.set(
index,
new DamageEnemy(enemy, v.x, v.y, this.floorId, this)
);
});
this.emit('extract');
hook.emit('enemyExtract', this);
}
/**
*
*/
calRealAttribute() {
this.haloList = [];
this.list.forEach(v => {
v.reset();
});
this.list.forEach(v => {
v.preProvideHalo();
});
this.list.forEach(v => {
v.calAttribute();
v.provideHalo();
});
this.list.forEach(v => {
v.getRealInfo();
});
}
/**
*
* @param noCache 使
*/
calDamage(noCache: boolean = false) {
if (noCache) this.calRealAttribute();
this.list.forEach(v => {
v.calDamage(void 0);
});
}
/**
*
*/
calMapDamage() {
this.mapDamage = {};
const hero = getHeroStatusOn(realStatus, this.floorId);
this.list.forEach(v => {
v.calMapDamage(this.mapDamage, hero);
});
}
/**
*
* @param type
* @param data
* @param halo
* @param recursion 使
*/
applyHalo<K extends keyof HaloType>(
type: K,
data: HaloType[K],
enemy: DamageEnemy,
halo: HaloFn | HaloFn[],
recursion: boolean = false
) {
const arr = ensureArray(halo);
const enemys = this.range.type(type).scan(this.list.values(), data);
if (!recursion) {
arr.forEach(v => {
enemys.forEach(e => {
e.injectHalo(v, enemy.info);
});
});
} else {
enemys.forEach(e => {
arr.forEach(v => {
e.injectHalo(v, enemy.info);
e.preProvideHalo();
});
});
}
}
/**
*
*/
preBalanceHalo() {
this.list.forEach(v => {
v.preProvideHalo();
});
}
}
export class DamageEnemy implements IDamageEnemy {
id: EnemyIds;
x?: number;
y?: number;
floorId?: FloorIds;
enemy: Enemy;
col?: EnemyCollection;
/**
*
* () ->
* -> provide inject -> ->
*/
info!: EnemyInfo;
/** 向其他怪提供过的光环 */
providedHalo: Set<number> = new Set();
/**
* 0 -> -> 1 -> -> 2 -> provide inject
* -> 3 -> -> 4 ->
*/
progress: number = 0;
constructor(
enemy: Enemy,
x?: number,
y?: number,
floorId?: FloorIds,
col?: EnemyCollection
) {
this.id = enemy.id;
this.enemy = enemy;
this.x = x;
this.y = y;
this.floorId = floorId;
this.col = col;
this.reset();
}
reset() {
const enemy = this.enemy;
this.info = {
hp: enemy.hp,
atk: enemy.atk,
def: enemy.def,
special: new Set(enemy.special),
atkBuff_: 0,
defBuff_: 0,
hpBuff_: 0,
guard: [],
enemy: this.enemy,
x: this.x,
y: this.y,
floorId: this.floorId
};
for (const [key, value] of Object.entries(enemy)) {
if (!(key in this.info) && has(value)) {
// @ts-expect-error 无法推导
this.info[key] = value;
}
}
this.progress = 0;
this.providedHalo.clear();
}
/**
* inject光环之前
*/
calAttribute() {
if (this.progress !== 1 && has(this.x) && has(this.floorId)) return;
this.progress = 2;
const special = this.info.special;
const info = this.info;
const { atk = 0, def = 0 } = getHeroStatusOn(realStatus);
// 坚固
if (special.has(3)) {
info.def = Math.max(info.def, atk - 1);
}
// 模仿
if (special.has(10)) {
info.atk = atk;
info.def = def;
}
}
/**
* inject光环后执行
*/
getRealInfo() {
if (this.progress < 3 && has(this.x) && has(this.floorId)) {
throw new Error(
`Unexpected early real info calculating. Progress: ${this.progress}`
);
}
if (this.progress === 4) return this.info;
this.progress = 4;
// 此时已经inject光环因此直接计算真实属性
const info = this.info;
info.atk = Math.floor(info.atk * (info.atkBuff_ / 100 + 1));
info.def = Math.floor(info.def * (info.defBuff_ / 100 + 1));
info.hp = Math.floor(info.hp * (info.hpBuff_ / 100 + 1));
return this.info;
}
/**
*
*/
preProvideHalo() {
if (this.progress !== 0) return;
this.progress = 1;
if (!this.floorId) return;
if (!has(this.x) || !has(this.y)) return;
// 这里可以做优先级更高的光环,比如加光环的光环怪等,写法与 provideHalo 类似
// e 是被加成怪的属性enemy 是施加光环的怪
}
/**
*
*/
provideHalo() {
if (this.progress !== 2) return;
this.progress = 3;
if (!this.floorId) return;
if (!has(this.x) || !has(this.y)) return;
const col = this.col ?? core.status.maps[this.floorId].enemy;
if (!col) return;
const special = this.info.special;
// e 是被加成怪的属性enemy 是施加光环的怪
// 普通光环
if (special.has(25)) {
// 光环效果,这里直接增加 e 的 buff 属性
const halo = (e: EnemyInfo, enemy: EnemyInfo) => {
if (enemy.haloAdd) {
e.hpBuff_ += enemy.hpBuff ?? 0;
e.atkBuff_ += enemy.atkBuff ?? 0;
e.defBuff_ += enemy.defBuff ?? 0;
} else {
e.hpBuff_ = Math.max(e.hpBuff_, enemy.hpBuff ?? 0);
e.atkBuff_ = Math.max(e.atkBuff_, enemy.atkBuff ?? 0);
e.defBuff_ = Math.max(e.defBuff_, enemy.defBuff ?? 0);
}
};
// 根据范围施加光环
const range = this.info.haloRange ?? 1;
if (this.info.haloSquare) {
col.applyHalo(
'square',
{ x: this.x, y: this.y, d: range * 2 + 1 },
this,
halo
);
} else {
col.applyHalo(
'manhattan',
{ x: this.x, y: this.y, d: range },
this,
halo
);
}
}
// 支援也是一类光环
if (special.has(26)) {
col.applyHalo(
'square',
{ x: this.x, y: this.y, d: 3 },
this,
(e, enemy) => {
e.guard.push(enemy);
}
);
}
}
/**
*
*/
injectHalo(halo: HaloFn, enemy: EnemyInfo) {
halo(this.info, enemy);
}
/**
*
*/
calDamage(hero: Partial<HeroStatus> = core.status.hero): DamageInfo {
const enemy = this.getRealInfo();
return this.calEnemyDamageOf(hero, enemy);
}
/**
*
* @param damage
*/
calMapDamage(
damage: Record<string, MapDamage> = {},
_hero: Partial<HeroStatus> = getHeroStatusOn(realStatus)
) {
if (!has(this.x) || !has(this.y) || !has(this.floorId)) return damage;
const enemy = this.enemy;
const floor = core.status.maps[this.floorId];
const w = floor.width;
const h = floor.height;
const objs = core.getMapBlocksObj(this.floorId);
// 领域
if (this.info.special.has(15)) {
const range = enemy.range ?? 1;
const startX = Math.max(0, this.x - range);
const startY = Math.max(0, this.y - range);
const endX = Math.min(floor.width - 1, this.x + range);
const endY = Math.min(floor.height - 1, this.y + range);
const dam = Math.max(enemy.zone ?? 0, 0);
for (let x = startX; x <= endX; x++) {
for (let y = startY; y <= endY; y++) {
if (
!enemy.zoneSquare &&
manhattan(x, y, this.x, this.y) > range
) {
// 如果是十字范围而且曼哈顿距离大于范围,则跳过此格
continue;
}
const loc = `${x},${y}` as LocString;
if (objs[loc]?.event.noPass) continue;
this.setMapDamage(damage, loc, dam, '领域');
}
}
}
// 激光
if (this.info.special.has(24)) {
const dirs: Dir[] = ['left', 'down', 'up', 'right'];
const dam = Math.max(enemy.laser ?? 0, 0);
for (const dir of dirs) {
let x = this.x;
let y = this.y;
const { x: dx, y: dy } = core.utils.scan[dir];
while (x >= 0 && y >= 0 && x < w && y < h) {
x += dx;
y += dy;
const loc = `${x},${y}` as LocString;
if (objs[loc]?.event.noPass) continue;
this.setMapDamage(damage, loc, dam, '激光');
}
}
}
// 阻击
if (this.info.special.has(18)) {
const dirs: Dir[] = ['left', 'down', 'up', 'right'];
for (const dir of dirs) {
const { x: dx, y: dy } = core.utils.scan[dir];
const x = this.x + dx;
const y = this.y + dy;
const loc = `${x},${y}` as LocString;
if (objs[loc]?.event.noPass) continue;
this.setMapDamage(damage, loc, this.info.repulse ?? 0, '阻击');
damage[loc].repulse ??= [];
damage[loc].repulse.push([this.x, this.y]);
}
}
// 捕捉
if (this.info.special.has(27)) {
const dirs: Dir[] = ['left', 'down', 'up', 'right'];
for (const dir of dirs) {
const { x: dx, y: dy } = core.utils.scan[dir];
const x = this.x + dx;
const y = this.y + dy;
const loc = `${x},${y}` as LocString;
if (objs[loc]?.event.noPass) continue;
damage[loc] ??= { damage: 0, type: new Set() };
damage[loc].ambush ??= [];
damage[loc].ambush.push([this.x, this.y]);
}
}
// 夹击
if (this.info.special.has(16)) {
// 只计算右方和下方的怪物,这样就可以避免一个点被重复计算两次
const dirs: Dir[] = ['down', 'right'];
for (const dir of dirs) {
const { x: dx, y: dy } = core.utils.scan[dir];
const x = this.x + dx * 2;
const y = this.y + dy * 2;
const e = this.col?.get(x, y);
if (!e) continue;
const info = e.getRealInfo();
if (!info.special.has(16)) continue;
const cx = this.x + dx;
const cy = this.y + dy;
const loc = `${cx},${cy}` as LocString;
if (objs[loc]?.event.noPass) continue;
const half = getHeroStatusOn('hp') / 2;
let bt = half;
// 夹击不超伤害值
if (core.flags.betweenAttackMax) {
const aDamage = this.calDamage().damage;
const bDamage = e.calDamage().damage;
bt = Math.min(aDamage, bDamage, half);
}
this.setMapDamage(damage, loc, bt, '夹击');
}
}
return damage;
}
private setMapDamage(
damage: Record<string, MapDamage>,
loc: string,
dam: number,
type: string
) {
damage[loc] ??= { damage: 0, type: new Set() };
damage[loc].damage += dam;
if (type) damage[loc].type.add(type);
}
private calEnemyDamageOf(
hero: Partial<HeroStatus>,
enemy: EnemyInfo
): DamageInfo {
const status = getHeroStatusOf(hero, realStatus, this.floorId);
const damage = calDamageWith(enemy, status) ?? Infinity;
return { damage };
}
/**
*
* @param num
* @param dir
* @param hero
*/
calCritical(
num: number = 1,
hero: Partial<HeroStatus> = core.status.hero
): CriticalDamageDelta[] {
const origin = this.calDamage(hero);
const seckill = this.getSeckillAtk();
return this.calCriticalWith(num, seckill, origin, hero);
}
/**
*
* @param num
* @param min
* @param seckill
* @param hero
*/
private calCriticalWith(
num: number,
seckill: number,
origin: DamageInfo,
hero: Partial<HeroStatus>
): CriticalDamageDelta[] {
if (!isFinite(seckill)) return [];
const res: CriticalDamageDelta[] = [];
const def = hero.def!;
const precision =
(seckill < Number.MAX_SAFE_INTEGER ? 1 : seckill / 1e15) * 2;
const enemy = this.getRealInfo();
let curr = hero.atk!;
let start = curr;
let end = seckill;
let ori = origin.damage;
const status = { atk: curr, def };
const calDam = () => {
status.atk = curr;
return this.calEnemyDamageOf(status, enemy).damage;
};
let i = 0;
while (res.length < num) {
if (end - start <= precision) {
// 到达二分所需精度,计算临界准确值
let cal = false;
for (const v of [(start + end) / 2, end]) {
curr = v;
const dam = calDam();
if (dam < ori) {
res.push({
damage: dam,
atkDelta: Math.ceil(v - hero.atk!),
delta: -(dam - origin.damage)
});
start = v;
end = seckill;
cal = true;
ori = dam;
break;
}
}
if (!cal) break;
}
curr = Math.floor((start + end) / 2);
const damage = calDam();
if (damage < ori) {
end = curr;
} else {
start = curr;
}
if (i++ >= 10000) {
// eslint-disable-next-line no-console
console.warn(
`Unexpected endless loop in calculating critical.` +
`Enemy Id: ${this.id}. Loc: ${this.x},${this.y}. Floor: ${this.floorId}`
);
break;
}
}
if (res.length === 0) {
curr = hero.atk!;
const dam = calDam();
res.push({
damage: dam,
atkDelta: 0,
delta: 0
});
}
return res;
}
/**
* n防减伤
* @param num
* @param dir
* @param hero
*/
calDefDamage(
num: number = 1,
hero: Partial<HeroStatus> = core.status.hero
): DamageDelta {
const damage = this.calDamage({
def: (hero.def ?? core.status.hero.def) + num
});
const origin = this.calDamage(hero);
const finite = isFinite(damage.damage);
return {
damage: damage.damage,
info: damage,
delta: -(finite ? damage.damage - origin.damage : Infinity)
};
}
/**
*
*/
getSeckillAtk(): number {
const info = this.getRealInfo();
// 坚固,不可能通过攻击秒杀
if (info.special.has(3)) {
return Infinity;
}
// 常规怪物秒杀攻击是怪物防御+怪物生命
return info.def + info.hp;
}
}
export interface DamageWithTurn {
damage: number;
turn: number;
}
/**
* buff加成core.status.hero取
* buff
*/
const realStatus: (keyof HeroStatus)[] = ['atk', 'def', 'mdef', 'hpmax'];
/** 当前是否正在计算支援怪的伤害 */
let inGuard = false;
/**
*
* @param info
* @param hero
*/
export function calDamageWithTurn(
info: EnemyInfo,
hero: Partial<HeroStatus>
): DamageWithTurn {
const { hp } = core.status.hero;
const { atk, def, mdef } = hero as HeroStatus;
const { atk: monAtk, def: monDef, special } = info;
let { hp: monHp } = info;
// 无敌
if (special.has(20) && core.itemCount('cross') < 1) {
return { damage: Infinity, turn: 0 };
}
/** 怪物会对勇士造成的总伤害 */
let damage = 0;
/** 勇士每轮造成的伤害 */
let heroPerDamage: number = 0;
/** 怪物每轮造成的伤害 */
let enemyPerDamage: number = 0;
// 勇士每轮伤害为勇士攻击减去怪物防御
heroPerDamage += atk - monDef;
// 吸血
if (special.has(11)) {
const vampire = info.vampire ?? 0;
const value = (vampire / 100) * hp;
damage += value;
// 如果吸血加到自身
if (info.add) {
monHp += value;
}
}
// 魔攻
if (special.has(2)) {
enemyPerDamage = monAtk;
} else {
enemyPerDamage = monAtk - def;
}
// 连击
if (special.has(4)) enemyPerDamage *= 2;
if (special.has(5)) enemyPerDamage *= 3;
if (special.has(6)) enemyPerDamage *= info.n!;
if (enemyPerDamage < 0) enemyPerDamage = 0;
let turn = Math.ceil(monHp / heroPerDamage);
// 支援,当怪物被支援且不包含支援标记时执行,因为支援怪不能再被支援了
if (info.guard.length > 0 && !inGuard) {
inGuard = true;
// 支援中魔防只会被计算一次,因此除了当前怪物,计算其他怪物伤害时魔防为 0
const status = { ...hero, mdef: 0 };
// 计算支援怪的伤害,同时把打支援怪花费的回合数加到当前怪物上,因为打支援怪的时候当前怪物也会打你
// 因此回合数需要加上打支援怪的回合数
for (const enemy of info.guard) {
// 直接把 enemy 传过去,因此支援的 enemy 会吃到其原本所在位置的光环加成
const extraInfo = calDamageWithTurn(enemy, status);
turn += extraInfo.turn;
damage += extraInfo.damage;
}
inGuard = false;
}
// 先攻
if (special.has(1)) {
damage += enemyPerDamage;
}
// 破甲
if (special.has(7)) {
const value = info.breakArmor ?? core.values.breakArmor;
damage += (value / 100) * def;
}
// 反击
if (special.has(8)) {
const value = info.counterAttack ?? core.values.counterAttack;
// 反击是每回合生效,因此加到 enemyPerDamage 上
enemyPerDamage += (value / 100) * atk;
}
// 净化
if (special.has(9)) {
const value = info.purify ?? core.values.purify;
damage += mdef * value;
}
damage += (turn - 1) * enemyPerDamage;
// 魔防
damage -= mdef;
// 未开启负伤时,如果伤害为负,则设为 0
if (!core.flags.enableNegativeDamage && damage < 0) {
damage = 0;
}
// 固伤,无法被魔防减伤
if (special.has(22)) {
damage += info.damage ?? 0;
}
// 仇恨,无法被魔防减伤
if (special.has(17)) {
damage += core.getFlag('hatred', 0);
}
return { damage: Math.floor(damage), turn };
}
/**
*
* @param info
* @param hero
*/
export function calDamageWith(
info: EnemyInfo,
hero: Partial<HeroStatus>
): number {
return calDamageWithTurn(info, hero).damage;
}
export function ensureFloorDamage(floorId: FloorIds) {
const floor = core.status.maps[floorId];
floor.enemy ??= new EnemyCollection(floorId);
}
export function getSingleEnemy(id: EnemyIds) {
const e = core.material.enemys[id];
const enemy = new DamageEnemy(e);
enemy.calAttribute();
enemy.getRealInfo();
enemy.calDamage(core.status.hero);
return enemy;
}
export function getEnemy(
x: number,
y: number,
floorId: FloorIds = core.status.floorId
) {
const enemy = core.status.maps[floorId].enemy.get(x, y);
return enemy;
}
declare global {
interface Floor {
enemy: EnemyCollection;
}
}

View File

@ -1,7 +1,6 @@
export * from './aura';
export * from './calculator';
export * from './comparer';
export * from './damage';
export * from './final';
export * from './legacy';
export * from './mapDamage';

View File

@ -19,15 +19,14 @@ import {
IMapDamageView
} from '@user/data-system';
import {
IFaceHandler,
ISpecial,
IReadonlyHeroAttribute,
IReadonlyEnemy,
InternalFaceGroup
IReadonlyEnemy
} from '@user/data-base';
import { IZoneValue } from './special';
import { IEnemyAttr, MapDamageType } from './types';
import { IHeroAttr } from '../hero';
import { IFaceHandler, FaceGroup } from '@user/data-common';
const RECT_RANGE = new RectRange();
const MANHATTAN_RANGE = new ManhattanRange();
@ -312,7 +311,7 @@ export class MainMapDamageConverter implements IMapDamageConverter<
const laser = enemy.getSpecial<number>(24);
if (laser) {
const face = handler.data.faceManager.get(InternalFaceGroup.Dir4)!;
const face = handler.data.faceManager.get(FaceGroup.Dir4)!;
views.push(new LaserDamageView(context, locator, laser, face));
}

View File

@ -1,47 +1,4 @@
import { FaceDirection, loading } from '@user/data-base';
import { isNil } from 'lodash-es';
import { ICoreState } from './types';
import { state } from './ins';
function createCoreState(state: ICoreState) {
//#region 图块部分
const data = Object.entries(core.maps.blocksInfo);
for (const [key, block] of data) {
const num = Number(key);
state.idNumberMap.set(block.id, num);
state.numberIdMap.set(num, block.id);
}
for (const [key, block] of data) {
if (!block.faceIds) continue;
const { down, up, left, right } = block.faceIds;
const downNum = state.idNumberMap.get(down);
if (downNum !== Number(key)) continue;
const upNum = state.idNumberMap.get(up);
const leftNum = state.idNumberMap.get(left);
const rightNum = state.idNumberMap.get(right);
state.roleFace.malloc(downNum, FaceDirection.Down);
if (!isNil(upNum)) {
state.roleFace.bind(upNum, downNum, FaceDirection.Up);
}
if (!isNil(leftNum)) {
state.roleFace.bind(leftNum, downNum, FaceDirection.Left);
}
if (!isNil(rightNum)) {
state.roleFace.bind(rightNum, downNum, FaceDirection.Right);
}
}
//#endregion
}
export function create() {
loading.once('loaded', () => {
// 加载后初始化全局状态
createCoreState(state);
});
}
export function create() {}
export * from './enemy';
export * from './hero';

View File

@ -4,4 +4,5 @@ export * from './hero';
export * from './interface';
export * from './item';
export * from './move';
export * from './tile';
export * from './utils';

View File

@ -1,9 +1,10 @@
import EventEmitter from 'eventemitter3';
import { backDir, toDir } from './utils';
import { fromDirectionString, loading } from '@user/data-base';
import { loading } from '@user/data-base';
import type { HeroKeyMover } from '@user/client-modules';
import { sleep } from '@motajs/common';
import { state } from '..';
import { fromDirectionString } from '@user/data-common';
// todo: 转身功能
@ -271,13 +272,13 @@ export class HeroMover extends ObjectMoverBase {
return super.startMove();
}
private checkAutoSave(x: number, y: number, nx: number, ny: number) {
const index = `${x},${y}`;
const nIndex = `${nx},${ny}`;
const map = core.status.thisMap.enemy.mapDamage;
const dam = map[index];
const nextDam = map[nIndex];
if (!dam || !nextDam) return;
private checkAutoSave(_x: number, _y: number, _nx: number, _ny: number) {
// const index = `${x},${y}`;
// const nIndex = `${nx},${ny}`;
// const map = core.status.thisMap.enemy.mapDamage;
// const dam = map[index];
// const nextDam = map[nIndex];
// if (!dam || !nextDam) return;
// 可以在这里判断地图伤害,并进行自动存档,例如在进入或离开地图伤害时存档
// if (dam.damage > 0 || nextDam.damage > 0) {
// core.autosave()

View File

@ -0,0 +1,43 @@
import {
ITileLegacyConverter,
ITileRawData,
TileType
} from '@user/data-common';
export type LegacyTileData = MapDataOf<keyof NumberToId>;
export class TileLegacyBridge implements ITileLegacyConverter<LegacyTileData> {
fromLegacy(num: number, legacy: LegacyTileData): ITileRawData {
return {
num,
id: legacy.id,
trigger: -1,
type: this.getTileType(num, legacy)
};
}
private getTileType(num: number, legacy: LegacyTileData): TileType {
if (num === 0) return TileType.None;
switch (legacy.cls) {
case 'terrains':
return TileType.Terrain;
case 'autotile':
return TileType.Autotile;
case 'animates':
return TileType.Animate;
case 'items':
return TileType.Item;
case 'enemys':
case 'enemy48':
return TileType.Enemy;
case 'npcs':
case 'npc48':
return TileType.Npc;
// @ts-expect-error 动态类型声明导致的错误,忽略即可
case 'tileset':
return TileType.Tileset;
default:
return TileType.Unknown;
}
}
}

View File

@ -6,7 +6,7 @@ import {
ISaveSystem,
ISaveSystemConfig
} from './types';
import { ISaveableContent, SaveCompression } from '@user/data-base';
import { ISaveableContent, SaveCompression } from '@user/data-common';
import { isNil } from 'lodash-es';
interface ISaveRecord {

View File

@ -1,4 +1,4 @@
import { ISaveableContent, SaveCompression } from '@user/data-base';
import { ISaveableContent, SaveCompression } from '@user/data-common';
import { Dexie, Table } from 'dexie';
export interface IGlobalTrasaction {

View File

@ -1,13 +1,10 @@
import {
IEnemyContext,
IMotaDataLoader,
ISaveableContent,
IStateBase
} from '@user/data-base';
import { IMotaDataLoader, IStateBase } from '@user/data-base';
import { IEnemyAttr } from './enemy';
import { IHeroAttr } from './hero';
import { ILoadProgressTotal } from '@motajs/loader';
import { ISaveSystem } from './save';
import { IStateSystem } from '@user/data-system';
import { ISaveableContent } from '@user/data-common';
export interface ISaveableExecutor<T, TEnemy = IEnemyAttr, THero = IHeroAttr> {
/**
@ -18,14 +15,11 @@ export interface ISaveableExecutor<T, TEnemy = IEnemyAttr, THero = IHeroAttr> {
afterLoad(data: T, state: IStateBase<TEnemy, THero>): void;
}
export interface ICoreState extends IStateBase<IEnemyAttr, IHeroAttr> {
export interface ICoreState extends IStateSystem<IEnemyAttr, IHeroAttr> {
/** 加载进度对象 */
readonly loadProgress: ILoadProgressTotal;
/** 数据端加载对象 */
readonly dataLoader: IMotaDataLoader;
/** 怪物上下文 */
readonly enemyContext: IEnemyContext<IEnemyAttr, IHeroAttr>;
/** 存档系统 */
readonly saveSystem: ISaveSystem;

View File

@ -19,11 +19,10 @@ import {
IEnemy,
IReadonlyEnemy,
ISpecial,
IStateBase,
ILocationIndexer,
MapLocIndexer
IStateBase
} from '@user/data-base';
import { EnemyView } from './enemy';
import { ILocationIndexer, MapLocIndexer } from '@user/data-common';
export class EnemyContext<TEnemy, THero> implements IEnemyContext<
TEnemy,

View File

@ -9,7 +9,8 @@ import {
IMapDamageReducer,
IMapDamageView
} from './types';
import { ILocationHelper, IStateBase } from '@user/data-base';
import { IStateBase } from '@user/data-base';
import { ILocationHelper } from '@user/data-common';
interface IPointInfo {
/** 该点所有的地图伤害 */

View File

@ -5,9 +5,9 @@ import {
ISpecial,
IReadonlyHeroAttribute,
IHeroAttribute,
IStateBase,
ILocationHelper
IStateBase
} from '@user/data-base';
import { ILocationHelper } from '@user/data-common';
//#region 辅助接口

View File

@ -1,2 +1,4 @@
export * from './combat';
export * from './trigger';
export * from './types';

View File

@ -5,7 +5,7 @@ export class TriggerCollection implements ITriggerCollection {
private readonly triggerList: ITrigger[];
constructor(triggers: Iterable<ITrigger>) {
this.triggerList = Array.from(triggers);
this.triggerList = [...triggers];
}
count(): number {

View File

@ -0,0 +1,112 @@
import { IMapLayer } from '@user/data-base';
import {
ITrigger,
ITriggerCollection,
ITriggerCollector,
ITriggerRegistry
} from './types';
import { logger } from '@motajs/common';
import { TriggerCollection } from './collection';
export class TriggerCollector implements ITriggerCollector {
/** 当前收集器使用的注册对象 */
private registry: ITriggerRegistry | null = null;
collect(x: number, y: number, layer: IMapLayer): ITriggerCollection {
if (!this.registry) {
logger.warn(135);
return new TriggerCollection([]);
}
const staticType = layer.getTriggerType(x, y);
const staticTrigger = this.registry.create(staticType);
const dynamics = [...layer.dynamicLayer.getDynamicTilesAt(x, y)];
if (dynamics.length === 0) {
// 没有动态图块
if (staticTrigger) {
return new TriggerCollection([staticTrigger]);
} else {
return new TriggerCollection([]);
}
} else if (dynamics.length === 1) {
// 一个动态图块,只需要进行一次额外判断即可
const dynamic = dynamics[0];
const dynamicTrigger = this.registry.create(dynamic.triggerType);
// 直接穷举所有可能情况
if (!staticTrigger && !dynamicTrigger) {
return new TriggerCollection([]);
} else if (staticTrigger && !dynamicTrigger) {
return new TriggerCollection([staticTrigger]);
} else if (!staticTrigger && dynamicTrigger) {
return new TriggerCollection([dynamicTrigger]);
} else {
// 静态动态都有,则需要额外判断优先级,动态图层在前,因此包含等号
if (dynamicTrigger!.priority >= staticTrigger!.priority) {
const arr = [dynamicTrigger!, staticTrigger!];
return new TriggerCollection(arr);
} else {
const arr = [staticTrigger!, dynamicTrigger!];
return new TriggerCollection(arr);
}
}
} else {
// 动态图块大于两个,使用通用方案,记录重复触发器并抛出警告
const usedPriority = new Set<number>();
const duplicate = new Set<number>();
if (staticTrigger) {
// 有静态触发器
const lessTriggers: ITrigger[] = [];
const greaterTriggers: ITrigger[] = [];
// 先收集所有的触发器,并记录重复情况
for (const tile of layer.dynamicLayer.getDynamicTilesAt(x, y)) {
const trigger = this.registry.create(tile.triggerType);
if (trigger) {
if (usedPriority.has(trigger.priority)) {
duplicate.add(trigger.priority);
}
usedPriority.add(trigger.priority);
// 同优先级下动态在前,因此包含等号
if (trigger.priority >= staticTrigger.priority) {
greaterTriggers.push(trigger);
} else {
lessTriggers.push(trigger);
}
}
}
if (duplicate.size > 0) {
logger.warn(136, [...duplicate].join(','));
}
const arr = [
...greaterTriggers.sort((a, b) => b.priority - a.priority),
staticTrigger,
...lessTriggers.sort((a, b) => b.priority - a.priority)
];
return new TriggerCollection(arr);
} else {
// 没有静态触发器
const triggers: ITrigger[] = [];
for (const tile of layer.dynamicLayer.getDynamicTilesAt(x, y)) {
const trigger = this.registry.create(tile.triggerType);
if (trigger) {
if (usedPriority.has(trigger.priority)) {
duplicate.add(trigger.priority);
}
usedPriority.add(trigger.priority);
triggers.push(trigger);
}
}
if (duplicate.size > 0) {
logger.warn(136, [...duplicate].join(','));
}
return new TriggerCollection(
triggers.sort((a, b) => b.priority - a.priority)
);
}
}
}
attachRegistry(registry: ITriggerRegistry | null): void {
this.registry = registry;
}
}

View File

@ -1,3 +1,4 @@
export * from './types';
export * from './collection';
export * from './collector';
export * from './registry';
export * from './types';

View File

@ -0,0 +1,17 @@
import { IStateBase } from '@user/data-base';
import { IEnemyContext } from './combat';
import { ITriggerCollector, ITriggerRegistry } from './trigger';
export interface IStateSystem<TEnemy, THero> extends IStateBase<TEnemy, THero> {
/** 怪物上下文 */
readonly enemyContext: IEnemyContext<TEnemy, THero>;
/** 触发器注册 */
readonly triggerRegistry: ITriggerRegistry;
/** 触发器收集器 */
readonly triggerCollector: ITriggerCollector;
}
export interface IStateSystemExtended<TEnemy = unknown, THero = unknown> {
/** 当前对象对应的执行层对象Layer 2 对象) */
readonly state: IStateSystem<TEnemy, THero>;
}

View File

@ -1,6 +0,0 @@
{
"name": "@user/data-utils",
"dependencies": {
"@user/data-base": "workspace:*"
}
}

View File

@ -1,2 +0,0 @@
export * from './range';
export * from './utils';

View File

@ -1,86 +0,0 @@
import { isNil } from 'lodash-es';
interface RangeTypeData {
square: { x: number; y: number; d: number };
rect: { x: number; y: number; w: number; h: number };
manhattan: { x: number; y: number; d: number };
}
type InRangeFn<E extends Partial<Loc>, T> = (item: E, data: T) => boolean;
export class Range {
static rangeType: Record<string, RangeType> = {};
/**
*
* @param type
*/
type<T extends string>(
type: T
): T extends keyof RangeTypeData ? RangeType<RangeTypeData[T]> : RangeType {
return Range.rangeType[type] as T extends keyof RangeTypeData
? RangeType<RangeTypeData[T]>
: RangeType;
}
/**
*
* @param type
* @param fn
*/
static register<K extends keyof RangeTypeData>(
type: K,
fn: InRangeFn<Partial<Loc>, RangeTypeData[K]>
): void;
static register(type: string, fn: InRangeFn<Partial<Loc>, any>): void;
static register(type: string, fn: InRangeFn<Partial<Loc>, any>): void {
const range = new RangeType(type, fn);
this.rangeType[type] = range;
}
}
class RangeType<Type = any> {
readonly type: string;
/**
*
* @param item
* @param data
*/
readonly inRange: InRangeFn<Partial<Loc>, Type>;
constructor(type: string, fn: InRangeFn<Partial<Loc>, Type>) {
this.type = type;
this.inRange = fn;
}
/**
*
* @param items
* @param data
*/
scan<T extends Partial<Loc>>(items: Iterable<T>, data: Type): T[] {
const res: T[] = [];
for (const ele of items) {
if (this.inRange(ele, data)) {
res.push(ele);
}
}
return res;
}
}
Range.register('square', (item, { x, y, d }) => {
if (isNil(item.x) || isNil(item.y)) return false;
const r = Math.floor(d / 2);
return Math.abs(item.x - x) <= r && Math.abs(item.y - y) <= r;
});
Range.register('rect', (item, { x, y, w, h }) => {
if (isNil(item.x) || isNil(item.y)) return false;
const ex = x + w;
const ey = y + h;
return item.x >= x && item.y >= y && item.x < ex && item.y < ey;
});
Range.register('manhattan', (item, { x, y, d }) => {
if (isNil(item.x) || isNil(item.y)) return false;
return Math.abs(item.x - x) + Math.abs(item.y - y) < d;
});

View File

@ -1,70 +0,0 @@
const backDirMap: Record<Dir2, Dir2> = {
up: 'down',
down: 'up',
left: 'right',
right: 'left',
leftup: 'rightdown',
rightup: 'leftdown',
leftdown: 'rightup',
rightdown: 'leftup'
};
export function backDir(dir: Dir): Dir;
export function backDir(dir: Dir2): Dir2;
export function backDir(dir: Dir2): Dir2 {
return backDirMap[dir];
}
export function has<T>(v: T): v is NonNullable<T> {
return v !== null && v !== void 0;
}
export function ensureArray<T>(arr: T): T extends any[] ? T : T[] {
// @ts-expect-error 需要弃用
return arr instanceof Array ? arr : [arr];
}
export function ofDir(x: number, y: number, dir: Dir2): LocArr {
const { x: dx, y: dy } = core.utils.scan2[dir];
return [x + dx, y + dy];
}
/**
*
*/
export function manhattan(x1: number, y1: number, x2: number, y2: number) {
return Math.abs(x1 - x2) + Math.abs(y1 - y2);
}
export function formatDamage(damage: number): DamageString {
let dam = '';
let color = '';
if (!Number.isFinite(damage)) {
dam = '???';
color = '#f22';
} else {
dam = core.formatBigNumber(damage, true);
if (damage <= 0) color = '#1f1';
else if (damage < core.status.hero.hp / 3) color = '#fff';
else if (damage < (core.status.hero.hp * 2) / 3) color = '#ff0';
else if (damage < core.status.hero.hp) color = '#f93';
else color = '#f22';
}
return { damage: dam, color: color as Color };
}
/**
*
* @param from
* @param to
*/
export function findDir(from: Loc, to: Loc): Dir | 'none' {
const dx = Math.sign(to.x - from.x);
const dy = Math.sign(to.y - from.y);
return (
(Object.entries(core.utils.scan).find(v => {
return v[1].x === dx && v[1].y === dy;
})?.[0] as Dir) ?? 'none'
);
}

View File

@ -2,10 +2,11 @@
"name": "@user/entry-data",
"dependencies": {
"@motajs/legacy-common": "workspace:*",
"@user/data-common": "workspace:*",
"@user/data-base": "workspace:*",
"@user/data-system": "workspace:*",
"@user/data-fallback": "workspace:*",
"@user/data-state": "workspace:*",
"@user/data-utils": "workspace:*",
"@user/legacy-plugin-data": "workspace:*"
}
}
}

View File

@ -1,10 +1,11 @@
import { Mota } from './mota';
import * as Common from '@motajs/common';
import * as LegacyCommon from '@motajs/legacy-common';
import * as DataCommon from '@user/data-common';
import * as DataBase from '@user/data-base';
import * as DataSystem from '@user/data-system';
import * as DataFallback from '@user/data-fallback';
import * as DataState from '@user/data-state';
import * as DataUtils from '@user/data-utils';
import * as LegacyPluginData from '@user/legacy-plugin-data';
export function create() {
@ -12,10 +13,11 @@ export function create() {
Mota.register('@motajs/common', Common);
Mota.register('@motajs/legacy-common', LegacyCommon);
Mota.register('@user/data-common', DataCommon);
Mota.register('@user/data-base', DataBase);
Mota.register('@user/data-system', DataSystem);
Mota.register('@user/data-fallback', DataFallback);
Mota.register('@user/data-state', DataState);
Mota.register('@user/data-utils', DataUtils);
Mota.register('@user/legacy-plugin-data', LegacyPluginData);
DataBase.loading.emit('dataRegistered');

View File

@ -10,10 +10,11 @@ import type * as RenderVue from '@motajs/render-vue';
import type * as System from '@motajs/system';
import type * as UserClientBase from '@user/client-base';
import type * as ClientModules from '@user/client-modules';
import type * as DataCommon from '@user/data-common';
import type * as DataBase from '@user/data-base';
import type * as DataFallback from '@user/data-fallback';
import type * as DataSystem from '@user/data-system';
import type * as DataState from '@user/data-state';
import type * as DataUtils from '@user/data-utils';
import type * as DataFallback from '@user/data-fallback';
import type * as LegacyPluginClient from '@user/legacy-plugin-client';
import type * as LegacyPluginData from '@user/legacy-plugin-data';
// ---------- 必要的第三方库
@ -34,10 +35,11 @@ interface ModuleInterface {
'@motajs/system': typeof System;
'@user/client-base': typeof UserClientBase;
'@user/client-modules': typeof ClientModules;
'@user/data-common': typeof DataCommon;
'@user/data-base': typeof DataBase;
'@user/data-system': typeof DataSystem;
'@user/data-fallback': typeof DataFallback;
'@user/data-state': typeof DataState;
'@user/data-utils': typeof DataUtils;
'@user/legacy-plugin-client': typeof LegacyPluginClient;
'@user/legacy-plugin-data': typeof LegacyPluginData;
// ---------- 必要的第三方库

View File

@ -2,7 +2,6 @@
"name": "@user/legacy-plugin-data",
"dependencies": {
"@user/data-state": "workspace:*",
"@user/data-base": "workspace:*",
"@user/data-utils": "workspace:*"
"@user/data-base": "workspace:*"
}
}
}

View File

@ -1,91 +0,0 @@
import { DamageEnemy } from '@user/data-state';
import { findDir, ofDir } from '@user/data-utils';
export function createCheckBlock() {
// 地图伤害在这实现。2.C 会修改实现方式
control.prototype.checkBlock = function () {
const heroLoc = core.status.hero.loc;
const { x, y } = heroLoc;
const loc = `${x},${y}`;
const col = core.status.thisMap.enemy;
const info = col.mapDamage[loc];
if (!info) return;
const damage = info.damage;
// 阻击夹域伤害
if (damage) {
core.status.hero.hp -= damage;
const type = [...info.type];
const text = type.join('') || '伤害';
core.drawTip('受到' + text + damage + '点');
core.drawHeroAnimate('zone');
this._checkBlock_disableQuickShop();
core.status.hero.statistics.extraDamage += damage;
if (core.status.hero.hp <= 0) {
core.status.hero.hp = 0;
core.updateStatusBar();
core.events.lose();
return;
} else {
core.updateStatusBar();
}
}
const actions: MotaAction[] = [];
// 阻击效果
if (info.repulse) {
for (const [x, y] of info.repulse) {
const loc2 = { x, y };
const dir = findDir(heroLoc, loc2);
if (dir === 'none') continue;
const [nx, ny] = ofDir(x, y, dir);
if (core.noPass(nx, ny) || !core.canMoveHero(x, y, dir)) {
continue;
}
actions.push({
type: 'move',
time: 250,
keep: true,
loc: [x, y],
steps: [`${dir}:1`],
async: true
});
}
}
/** 存储要和哪些捕捉怪战斗 */
const ambushEnemies: DamageEnemy[] = [];
// 捕捉效果
if (info.ambush) {
for (const [x, y] of info.ambush) {
const loc2 = { x, y };
const dir = findDir(loc2, heroLoc);
if (dir === 'none') continue;
actions.push({
type: 'move',
time: 250,
keep: false,
loc: [x, y],
steps: [`${dir}:1`],
async: true
});
const enemy = col.get(x, y);
if (enemy) {
ambushEnemies.push(enemy);
}
}
}
if (actions.length > 0) {
actions.push({ type: 'waitAsync' });
// 与捕捉怪战斗
core.insertAction(actions, void 0, void 0, () => {
ambushEnemies.forEach(v => {
core.battle(v, v.y, true);
});
});
}
};
}

View File

@ -1,7 +0,0 @@
import { createCheckBlock } from './checkblock';
export function createEnemy() {
createCheckBlock();
}
export * from './checkblock';

View File

@ -1,8 +1,9 @@
import type { TimingFn } from 'mutate-animate';
import { heroMoveCollection, MoveStep, state } from '@user/data-state';
import { fromDirectionString, hook, loading } from '@user/data-base';
import { hook, loading } from '@user/data-base';
import { Patch, PatchClass } from '@motajs/legacy-common';
import { isNil } from 'lodash-es';
import { fromDirectionString } from '@user/data-common';
// 向后兼容用,会充当两个版本间过渡的作用
@ -324,8 +325,8 @@ export function initFallback() {
callback?.();
};
const layer = state.maps.getLayerByAlias('event')!;
layer.openDoor(x, y).then(cb);
// const layer = state.maps.getLayerByAlias('event')!;
// layer.openDoor(x, y).then(cb);
const animate = fallbackIds++;
core.animateFrame.lastAsyncId = animate;
@ -372,9 +373,9 @@ export function initFallback() {
if (core.status.replay.speed === 24) {
cb();
} else {
const num = state.idNumberMap.get(id)!;
const layer = state.maps.getLayerByAlias('event')!;
layer.closeDoor(num, x, y).then(cb);
// const num = state.tileStore.idToNumber(id)!;
// const layer = state.maps.getLayerByAlias('event')!;
// layer.closeDoor(num, x, y).then(cb);
const animate = fallbackIds++;
core.animateFrame.lastAsyncId = animate;
@ -514,20 +515,20 @@ export function initFallback() {
// 先使用 mainMapRenderer 妥协
const { client } = Mota.require('@user/client-modules');
const renderer = client.mainMapRenderer;
if (renderer.layerState !== state.maps) {
callback?.();
return;
}
const layer = state.maps.getLayerByAlias('event');
if (!layer) {
callback?.();
return;
}
// if (renderer.layerState !== state.maps) {
// callback?.();
// return;
// }
// const layer = state.maps.getLayerByAlias('event');
// if (!layer) {
// callback?.();
// return;
// }
core.removeBlock(sx, sy);
const moving = renderer.addMovingBlock(layer, block.id, sx, sy);
// const moving = renderer.addMovingBlock(layer, block.id, sx, sy);
core.updateStatusBar();
await moving.moveRelative(fn, time);
moving.destroy();
// await moving.moveRelative(fn, time);
// moving.destroy();
if (keep) {
core.setBlock(block.id, ex, ey);

View File

@ -4,12 +4,10 @@ import { initFiveLayer } from './fiveLayer';
import { createHook } from './hook';
import { initReplay } from './replay';
import { initUI } from './ui';
import { createEnemy } from './enemy';
export function createLegacy() {
initFallback();
loading.once('coreInit', () => {
createEnemy();
initFiveLayer();
createHook();
initReplay();
@ -17,7 +15,6 @@ export function createLegacy() {
});
}
export * from './enemy';
export * from './fallback';
export * from './fiveLayer';
export * from './removeMap';

View File

@ -55,7 +55,7 @@
"53": "Expected serializable value set as enemy's default attribute.",
"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."
"56": "Cannot convert legacy tile data since no tile legacy converter is attached to TileStore."
},
"warn": {
"1": "Resource with type of 'none' is loaded.",
@ -190,6 +190,9 @@
"130": "The given tile is not managed by this dynamic layer.",
"131": "Event layer to set is not belong to current LayerState.",
"132": "Trigger registry entry of $1 '$2' already exists, new factory will override old factory.",
"1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency."
"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.",
"135": "Expected a trigger registry attched before collect triggers.",
"136": "Unexpected duplicate trigger priority $1, which may cause trigger executed in an unexpected order."
}
}

View File

@ -1,4 +1,4 @@
import { FaceDirection } from '@user/data-base';
import { FaceDirection } from '@user/data-common';
export interface ISearchable4Dir {
/** 获取上侧元素 */

View File

@ -4,7 +4,6 @@ import { VirtualKey } from '@motajs/legacy-system';
export const mainUi = new UiController();
mainUi.register(
new GameUi('book', UI.Book),
new GameUi('toolbox', UI.Toolbox),
new GameUi('equipbox', UI.Equipbox),
new GameUi('settings', UI.Settings),

View File

@ -522,45 +522,45 @@ export class MinimapDrawer {
ctx.textBaseline = 'middle';
ctx.font = `3px "normal"`;
ctx.strokeStyle = 'black';
Mota.require('@user/data-state').ensureFloorDamage(floorId);
// Mota.require('@user/data-state').ensureFloorDamage(floorId);
ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
ctx.fillRect(x - 6, y - 2, 12, 4);
ctx.fillStyle = 'white';
const enemy = core.status.maps[floorId].enemy.list;
if (enemy.size === 0) {
ctx.strokeStyle = 'lightgreen';
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.beginPath();
ctx.moveTo(x - 1.5, y);
ctx.lineTo(x - 0.5, y + 1);
ctx.lineTo(x + 1.5, y - 1);
ctx.stroke();
} else if (enemy.size <= 2) {
const idSet = new Set<EnemyIds>();
enemy.forEach(v => {
idSet.add(v.id);
});
const ids: EnemyIds[] = [...idSet];
if (ids.length === 1) {
core.drawIcon(ctx, ids[0], x - 2, y - 2, 4, 4);
} else if (ids.length === 2) {
core.drawIcon(ctx, ids[0], x - 4, y - 2, 4, 4);
core.drawIcon(ctx, ids[1], x, y - 2, 4, 4);
} else {
core.drawIcon(ctx, ids[0], x - 5, y - 2, 4, 4);
core.drawIcon(ctx, ids[1], x - 1, y - 2, 4, 4);
ctx.fillStyle = 'white';
ctx.strokeStyle = 'black';
ctx.strokeText('…', x + 4, y);
ctx.fillText('…', x + 4, y);
}
} else {
ctx.fillStyle = 'white';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(`+${enemy.size}`, x, y);
}
// const enemy = core.status.maps[floorId].enemy.list;
// if (enemy.size === 0) {
// ctx.strokeStyle = 'lightgreen';
// ctx.lineCap = 'round';
// ctx.lineJoin = 'round';
// ctx.beginPath();
// ctx.moveTo(x - 1.5, y);
// ctx.lineTo(x - 0.5, y + 1);
// ctx.lineTo(x + 1.5, y - 1);
// ctx.stroke();
// } else if (enemy.size <= 2) {
// const idSet = new Set<EnemyIds>();
// enemy.forEach(v => {
// idSet.add(v.id);
// });
// const ids: EnemyIds[] = [...idSet];
// if (ids.length === 1) {
// core.drawIcon(ctx, ids[0], x - 2, y - 2, 4, 4);
// } else if (ids.length === 2) {
// core.drawIcon(ctx, ids[0], x - 4, y - 2, 4, 4);
// core.drawIcon(ctx, ids[1], x, y - 2, 4, 4);
// } else {
// core.drawIcon(ctx, ids[0], x - 5, y - 2, 4, 4);
// core.drawIcon(ctx, ids[1], x - 1, y - 2, 4, 4);
// ctx.fillStyle = 'white';
// ctx.strokeStyle = 'black';
// ctx.strokeText('…', x + 4, y);
// ctx.fillText('…', x + 4, y);
// }
// } else {
// ctx.fillStyle = 'white';
// ctx.textAlign = 'center';
// ctx.textBaseline = 'middle';
// ctx.fillText(`+${enemy.size}`, x, y);
// }
ctx.restore();
}

View File

@ -1,254 +0,0 @@
<!-- 怪物手册ui -->
<template>
<div id="book">
<div id="tools">
<span id="back" class="button-text tools" @click="exit"
><left-outlined />返回游戏</span
>
</div>
<div v-if="enemy.length === 0" id="none">
<div>本层无怪物</div>
</div>
<Scroll
v-else
style="width: 100%; height: 94%; font-family: normal"
v-model:now="scroll"
v-model:drag="drag"
>
<div v-for="(e, i) of toShow" class="enemy">
<EnemyOne
:selected="i === selected"
:enemy="e"
:key="i"
@select="select(e, i)"
@hover="selected = i"
></EnemyOne>
<Divider
dashed
style="width: 100%; border-color: #ddd4"
></Divider>
</div>
</Scroll>
</div>
<BookDetail
v-if="detail"
:from-book="true"
@close="closeDetail()"
></BookDetail>
</template>
<script setup lang="tsx">
import { onUnmounted, ref } from 'vue';
import EnemyOne from '../components/enemyOne.vue';
import Scroll from '../components/scroll.vue';
import BookDetail from './bookDetail.vue';
import { LeftOutlined } from '@ant-design/icons-vue';
import { ToShowEnemy, detailInfo } from '../tools/book';
import { getDetailedEnemy } from '../tools/fixed';
import { gameKey } from '@motajs/system';
import { mainSetting } from '../preset/settingIns';
import { isMobile } from '../use';
import { IMountedVBind } from '../interface';
import { isNil } from 'lodash-es';
import { Divider } from 'ant-design-vue';
const props = defineProps<IMountedVBind>();
const floorId =
// @ts-ignore
core.floorIds[core.status.event?.ui?.index] ?? core.status.floorId;
const enemy = core.getCurrentEnemys(floorId);
const toShow: ToShowEnemy[] = enemy.map(v =>
getDetailedEnemy(v.enemy, floorId)
);
const scroll = ref(0);
const drag = ref(false);
const detail = ref(false);
const selected = ref(0);
const settingScale = mainSetting.getValue('ui.bookScale', 100) / 100;
const scale = isMobile
? Math.max(settingScale * 15, 20)
: Math.max(
(window.innerWidth / window.innerHeight) * 15 * settingScale,
20
);
/**
* 选择怪物展示详细信息
* @param enemy 选择的怪物
* @param index 选择的怪物索引
*/
function select(enemy: ToShowEnemy, index: number) {
if (drag.value) return;
const h = window.innerHeight;
const y = index * h * 0.2 - scroll.value;
detailInfo.enemy = enemy;
detailInfo.pos = y;
detail.value = true;
hide();
}
/**
* 隐藏怪物手册
*/
async function hide() {
const div = document.getElementById('book') as HTMLDivElement;
div.style.display = 'none';
}
/**
* 关闭详细信息
*/
async function closeDetail() {
show();
detail.value = false;
}
/**
* 显示怪物手册
*/
async function show() {
const div = document.getElementById('book') as HTMLDivElement;
div.style.display = 'flex';
}
/**
* 退出怪物手册
*/
async function exit() {
const hold = props.controller.holdOn();
props.controller.close(props.num);
if (core.events.recoverEvents(core.status.event.interval)) {
hold.end(true);
return;
} else if (!isNil(core.status.event.ui)) {
core.status.boxAnimateObjs = [];
// @ts-ignore
core.ui._drawViewMaps(core.status.event.ui);
hold.end(true);
} else hold.end();
}
function checkScroll() {
const h = window.innerHeight;
const y = (selected.value * h * scale) / 100 - scroll.value;
if (y < 0) {
scroll.value += y - 20;
}
if (y > h * 0.655) {
scroll.value += y - h * 0.655 + 20;
}
}
//
setTimeout(() => {
gameKey.use(props.ui.symbol);
gameKey
.realize(
'@book_up',
() => {
if (selected.value > 0) {
selected.value--;
}
checkScroll();
},
{ type: 'down-repeat' }
)
.realize(
'@book_down',
() => {
if (selected.value < enemy.length - 1) {
selected.value++;
}
checkScroll();
},
{ type: 'down-repeat' }
)
.realize(
'@book_pageDown',
() => {
if (selected.value >= enemy.length - 5) {
selected.value = enemy.length - 1;
} else {
selected.value += 5;
}
checkScroll();
},
{ type: 'down-repeat' }
)
.realize(
'@book_pageUp',
() => {
if (selected.value <= 4) {
selected.value = 0;
} else {
selected.value -= 5;
}
checkScroll();
},
{ type: 'down-repeat' }
)
.realize('exit', () => {
exit();
})
.realize('confirm', () => {
select(toShow[selected.value], selected.value);
});
}, 0);
onUnmounted(() => {
gameKey.dispose(props.ui.symbol);
});
</script>
<style lang="less" scoped>
#book {
user-select: none;
width: 80%;
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: space-between;
}
#tools {
height: 6%;
font-size: 3.2vh;
}
#none {
width: 100%;
height: 100%;
font-size: 6vw;
display: flex;
justify-content: center;
align-items: center;
}
.enemy {
display: flex;
flex-direction: column;
height: v-bind('scale + "vh"');
width: 100%;
padding: 0 1% 0 1%;
}
@media screen and (max-width: 600px) {
#tools {
transform: translateY(-50%);
}
#book {
width: 100%;
padding: 5%;
}
.enemy {
height: v-bind('scale * 2 / 3 + "vh"');
}
}
</style>

View File

@ -1,4 +1,3 @@
export { default as Book } from './book.vue';
export { default as BookDetail } from './bookDetail.vue';
export { default as Equipbox } from './equipbox.vue';
export { default as Fly } from './fly.vue';

View File

@ -288,6 +288,15 @@ importers:
'@motajs/types':
specifier: workspace:*
version: link:../../packages/types
'@user/data-common':
specifier: workspace:*
version: link:../data-common
packages-user/data-common:
dependencies:
'@motajs/common':
specifier: workspace:*
version: link:../../packages/common
packages-user/data-fallback:
dependencies:

View File

@ -57,6 +57,15 @@
- `IObjectMover.speed()`:预期频率**中频**。移动中修改移速有一定使用场景,但远不及 `forward`、`step` 等移动接口的频率通常只在特殊演出或逻辑中出现故为中频。典型使用场景NPC 逃离怪物时先定在原地,随后逐渐加速逃跑。
- `IObjectMover.stepFace()`:预期频率**低频**。移动方向与朝向不同的常见场景(后退)已由 `backward` 覆盖,只有极特殊情况才需要此接口(如角色朝向固定但沿垂直方向平移),相当罕见,故为低频。
## 预期体量
本节应当写出预期的代码体量,并分析原因。示例如下:
预期代码体量为 200-300 行。分析如下:
- `IObjectMover` 首先需要完成计划存储与计划的定义,这些接口基本大致就是向数组中添加元素,每个方法内容都不多,整体预计在 100 行左右。
- `IObjectMover` 还需要完成移动流程的编写工作,需要根据每个移动步按照流程执行不同的行为,这一过程较为复杂,预计需要 100-200 行。
# 实现思路
按照下面的格式分条描述实现思路。