mirror of
https://github.com/motajs/template.git
synced 2026-06-14 02:31:11 +08:00
Compare commits
7 Commits
501a598de0
...
03dc5ea60b
| Author | SHA1 | Date | |
|---|---|---|---|
| 03dc5ea60b | |||
| 7428f41c93 | |||
| 0f5b1f4d80 | |||
| b779e62eb2 | |||
| 8f4c4b7df2 | |||
| bf781c5ee8 | |||
| 13fc4e1b7c |
15
dev.md
15
dev.md
@ -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
283
docs/dev/map/tile-info.md
Normal file
@ -0,0 +1,283 @@
|
||||
# 需求综述
|
||||
|
||||
重新考虑之后,这一层需求更适合收敛成“图块数据”和“触发器数据”两套独立结构,而不是先抽象一层通用 `ITileInfo` 对象。
|
||||
|
||||
原因如下:
|
||||
|
||||
1. `IMapLayer` 内部的 `Uint32Array` 主要服务于高频查询与渲染,本身就应该尽量纯粹;
|
||||
2. 大多数格点只是墙壁、地板、空气,不含任何触发器,没有必要给每个格点额外挂一个信息对象;
|
||||
3. 当前触发器系统在数据层真正需要的并不是 `ITrigger` 对象,而只是“该载体对应哪一种触发器类型”;
|
||||
4. 动态图块是实例对象,天然适合作为触发器的运行时载体,触发器跟随动态图块移动也更自然;
|
||||
5. 存档是否需要记录触发器,取决于“触发器是跟随地图状态定义,还是跟随图块定义”,这应当单独讨论,而不是先把对象模型写死。
|
||||
|
||||
因此,本文不再优先设计 `IWrappedTileInfo`、`ITileInfo`、`createTileInfo`、`bindTileInfo` 这一套通用信息接口,而改为先构建一套**以 `ITileStore` 默认值为主、地图稀疏覆盖为辅的触发器方案**:
|
||||
|
||||
1. `ILayerState` 持有 `ITileStore` 引用,静态格点未主动设置时使用图块默认触发器类型;
|
||||
2. `IMapLayer` 仅按坐标稀疏存储“手动覆盖值”,并提供 `revertTrigger` 恢复默认触发器;
|
||||
3. 动态图块实例直接持有自己的触发器类型数字;
|
||||
4. `ITrigger` 对象仍只存在于 Layer 2,在收集阶段按需实例化;
|
||||
5. `setBlock` 与触发器解绑,不再通过 `keepInfo` 一类参数耦合两者。
|
||||
|
||||
---
|
||||
|
||||
# 接口设计与预期
|
||||
|
||||
## ILayerState.tileStore
|
||||
|
||||
- `ILayerState.tileStore`:当前图层状态持有的图块定义 store 引用。
|
||||
- 预期频率:**低频到中频**。外部直接访问它的频率不会太高,但 `IMapLayer.getTriggerType` 等底层逻辑会稳定依赖它作为默认触发器来源。
|
||||
- 典型使用场景:静态格点没有手动设置触发器时,`IMapLayer` 先读取当前图块数字,再通过 `layerState.tileStore.getTrigger(num)` 获取默认触发器类型。
|
||||
|
||||
## IMapLayer.getTriggerType
|
||||
|
||||
- `IMapLayer.getTriggerType(x, y)`:按坐标读取当前静态格点的“有效触发器类型”。若该点存在手动覆盖则返回覆盖值,否则回退到当前图块在 `ITileStore` 中的默认触发器类型。
|
||||
- 预期频率:**高频**。触发器收集时每次都需要先确认目标静态格点最终会暴露出哪一种触发器。
|
||||
- 典型使用场景:`ITriggerCollector.collect(x, y, layer)` 读取 `layer` 在 `(x, y)` 上的静态触发器类型。
|
||||
- 返回值建议:与 `ITileStore` 保持一致,越界或不存在触发器时统一返回 `-1`。
|
||||
|
||||
## IMapLayer.setTriggerType
|
||||
|
||||
- `IMapLayer.setTriggerType(type, x, y)`:为指定静态格点写入手动覆盖的触发器类型数字。
|
||||
- 预期频率:**中频**。主要出现在地图初始化、编辑器写回、运行时脚本修改事件配置等场景。
|
||||
- 典型使用场景:给某一格配置战斗触发器;或显式将某个原本有默认触发器的图块覆盖为“无触发器”。
|
||||
- 语义建议:`setTriggerType` 只负责写覆盖值,不负责恢复默认值。若 `type = -1`,表示显式覆盖为“无触发器”;若要恢复为图块默认触发器,应调用 `revertTrigger`。
|
||||
|
||||
## IMapLayer.revertTrigger
|
||||
|
||||
- `IMapLayer.revertTrigger(x, y)`:删除指定静态格点的手动触发器覆盖,使其重新回退到 `ITileStore` 的默认触发器类型。
|
||||
- 预期频率:**低频到中频**。主要出现在编辑器撤销覆盖、脚本恢复默认配置、动静态图块转换回写时。
|
||||
- 典型使用场景:某格点曾被脚本临时设置为其他触发器,演出结束后恢复为该图块原本的默认触发器。
|
||||
|
||||
## IDynamicTile.triggerType
|
||||
|
||||
- `IDynamicTile.triggerType`:当前动态图块携带的触发器类型数字,`-1` 表示无触发器。
|
||||
- 预期频率:**中频**。收集器在读取某格所有动态图块时需要访问;运行时若有脚本操作某个动态图块事件,也可能直接读写。
|
||||
- 典型使用场景:某个 NPC 作为动态图块移动时,其战斗或对话触发器跟随该实例一起移动。
|
||||
|
||||
## IDynamicTile.setTriggerType
|
||||
|
||||
- `IDynamicTile.setTriggerType(type)`:修改当前动态图块绑定的触发器类型。
|
||||
- 预期频率:**低频到中频**。通常只在创建动态图块、转换动静态图块、特殊脚本重配置时使用。
|
||||
- 典型使用场景:演出过程中把一个原本只是装饰的动态图块改成可交互单位。
|
||||
|
||||
## IDynamicLayer.transferToDynamic
|
||||
|
||||
- `IDynamicLayer.transferToDynamic(x, y, keepTrigger?)`:把静态图块转换为动态图块,并按参数决定是否保留原先的触发器。
|
||||
- 预期频率:**中频**。每次把地图中的静态物件实例化为可移动对象时都会用到。
|
||||
- 典型使用场景:把地图上的 NPC 图块转成 `IDynamicTile`,并让对话或战斗触发器跟着它一起移动。
|
||||
- 语义建议:`keepTrigger` 默认值先定为 `true`。当 `keepTrigger = true` 时,将静态格点当前的有效触发器类型迁移到新建的动态图块;当 `keepTrigger = false` 时,新动态图块不保留原触发器,并且静态格点上的手动覆盖应一并清除。
|
||||
|
||||
## IDynamicLayer.transferToStatic
|
||||
|
||||
- `IDynamicLayer.transferToStatic(tile, keepTrigger?)`:把动态图块还原为静态图块,并按参数决定是否把动态图块携带的触发器写回静态格点。
|
||||
- 预期频率:**中频**。会移动的图块在结束移动后重新落回地图时使用。
|
||||
- 典型使用场景:可推动箱子停止后重新固化为静态图块,同时决定是否保留它携带的触发器。
|
||||
- 语义建议:`keepTrigger` 默认值先定为 `true`。当 `keepTrigger = true` 时,将 `tile.triggerType` 写回目标静态格点;若写回值与该图块默认触发器一致,则应直接 `revertTrigger`,只在不一致时存入覆盖值。当 `keepTrigger = false` 时,不写回动态图块携带的触发器,静态格点自然回退到图块默认触发器。
|
||||
|
||||
## IDynamicLayer.transferToStaticIfSafe
|
||||
|
||||
- `IDynamicLayer.transferToStaticIfSafe(tile, keepTrigger?)`:在满足安全回写条件时将动态图块还原为静态图块,并保持与 `transferToStatic` 一致的触发器迁移语义。
|
||||
- 预期频率:**中频**。用于需要先判断目标格点是否允许回写的动静态转换场景。
|
||||
- 典型使用场景:某个可移动物体尝试停回地图时,若目标格点安全则回写图块和触发器,否则保留动态状态。
|
||||
- 语义建议:为保持三组转换接口的对称性,`keepTrigger` 也应加入该接口,且默认值同样先定为 `true`。
|
||||
|
||||
## ITriggerCollector.collect
|
||||
|
||||
- `ITriggerCollector.collect(x, y, layer)`:当前不再从 `getTileInfo` 中读取对象信息,而是分别读取静态格点与动态图块上的触发器类型数字,再按需实例化 `ITrigger`。
|
||||
- 预期频率:**中频**。玩家移动、交互判定、脚本触发等场景都会使用。
|
||||
- 典型使用场景:
|
||||
1. 读取 `layer.getTriggerType(x, y)`;
|
||||
2. 迭代 `layer.dynamicLayer.getDynamicTilesAt(x, y)`,读取每个 `tile.triggerType`;
|
||||
3. 将所有非 `-1` 的触发器类型交给 `ITriggerRegistry` 创建运行时 `ITrigger` 对象;
|
||||
4. 排序后组成 `ITriggerCollection`。
|
||||
|
||||
这里需要注意:**数据层不再存 `ITrigger` 对象,只存触发器类型数字;对象实例化延后到收集阶段完成。** 这样既保留了 Layer 2 的运行时灵活性,又降低了 Layer 1 的存储复杂度。
|
||||
|
||||
## 当前不再优先设计的接口
|
||||
|
||||
以下内容在这轮设计中不再作为主方案:
|
||||
|
||||
1. `ITileInfo` / `IWrappedTileInfo` 一类通用格点信息对象;
|
||||
2. `createTileInfo()` / `bindTileInfo()` 一类对象工厂接口;
|
||||
3. `setBlock(block, x, y, keepInfo?)` 这类把图块写入与触发器保留策略绑在一起的接口。
|
||||
|
||||
这并不表示将来永远不会有通用格点附加信息,只是当前真实需求已经收敛到“触发器类型数字”,继续上抽象只会把接口设计得更重。
|
||||
|
||||
---
|
||||
|
||||
# 实现思路
|
||||
|
||||
## 1. 图块数组与触发器数据彻底分离
|
||||
|
||||
`IMapLayer` 内部继续保留 `Uint32Array` 作为图块数字的权威存储,仅服务于:
|
||||
|
||||
1. 高频 `getBlock` 查询;
|
||||
2. 区域拷贝与渲染;
|
||||
3. 与现有 `openDoor`、`closeDoor`、`putMapData`、`setMapRef` 等图块相关操作配合。
|
||||
|
||||
触发器数据则不再混入图块数组逻辑中,而是作为独立结构维护。
|
||||
|
||||
## 2. 静态图层使用“默认值 + 稀疏覆盖”
|
||||
|
||||
静态图层不应自行持有整份默认触发器表,而应通过 `ILayerState.tileStore` 获取默认值,只在地图侧额外存储“手动覆盖值”。
|
||||
|
||||
当前建议结构如下:
|
||||
|
||||
```ts
|
||||
Map<number, number>;
|
||||
```
|
||||
|
||||
- key = `y * width + x` 形式的格点下标;
|
||||
- value = 手动覆盖的触发器类型数字。
|
||||
|
||||
`IMapLayer.getTriggerType(x, y)` 的读取顺序建议为:
|
||||
|
||||
1. 若坐标越界,返回 `-1`;
|
||||
2. 若该点存在手动覆盖,直接返回覆盖值;
|
||||
3. 否则读取当前图块数字,并通过 `tileStore.getTrigger(num)` 返回默认触发器类型。
|
||||
|
||||
这样可以自然满足“仅存必要”的目标:
|
||||
|
||||
1. 没有手动修改过触发器的格点不占额外存储;
|
||||
2. 大多数格点直接复用 `ITileStore` 中的默认定义,不需要在地图层重复抄一份;
|
||||
3. 只有真正偏离默认值的点,才进入地图侧稀疏映射;
|
||||
4. `revertTrigger` 只需删除覆盖记录即可恢复默认。
|
||||
|
||||
## 3. 动态图块直接携带触发器类型
|
||||
|
||||
动态图块已经是实例对象,因此不需要再为它额外建一层稀疏映射。更自然的方案是:
|
||||
|
||||
1. `DynamicTile` 内部直接保存 `triggerType: number`;
|
||||
2. 图块移动时,触发器天然跟随这个实例走;
|
||||
3. 收集器按坐标枚举动态图块后,直接读取其 `triggerType`。
|
||||
|
||||
这样“触发器跟着动态图块走”就不再需要额外维护坐标索引迁移,只要图块对象本身移动即可。
|
||||
|
||||
## 4. 动静态转换只负责迁移或丢弃触发器
|
||||
|
||||
因为静态格点与动态图块现在是两个不同的触发器载体,所以 `transferToDynamic` / `transferToStatic` / `transferToStaticIfSafe` 的核心职责也更清晰了:
|
||||
|
||||
1. `transferToDynamic(..., keepTrigger = true)`:把静态格点上的触发器迁到新动态图块;
|
||||
2. `transferToDynamic(..., keepTrigger = false)`:只转换图块,不保留原触发器,并清理静态格点上的手动覆盖;
|
||||
3. `transferToStatic(..., keepTrigger = true)`:把动态图块携带的触发器写回静态格点;若与默认值一致则回退默认,否则记为手动覆盖;
|
||||
4. `transferToStatic(..., keepTrigger = false)`:只还原图块,不回写动态图块触发器,让静态格点自然回退到图块默认触发器;
|
||||
5. `transferToStaticIfSafe(..., keepTrigger = true)`:在安全回写条件满足时,保持与 `transferToStatic` 一致的迁移语义。
|
||||
|
||||
这比把“是否保留触发器”揉进 `setBlock` 更符合职责边界,因为真正发生触发器载体切换的地方只有这些转换接口。
|
||||
|
||||
## 5. 图块写接口不再隐式影响触发器
|
||||
|
||||
既然图块数组与触发器数据已经拆开,则以下接口默认不应直接修改触发器:
|
||||
|
||||
1. `setBlock`
|
||||
2. `putMapData`
|
||||
3. `setMapRef`
|
||||
4. `openDoor`
|
||||
5. `closeDoor`
|
||||
|
||||
这样做的好处是:
|
||||
|
||||
1. 图块外观变化与逻辑触发变化彻底解耦;
|
||||
2. 不再需要 `keepInfo` / `keepTrigger` 之类参数污染高频图块写路径;
|
||||
3. 调用方若确实希望同步改动触发器,应显式调用 `setTriggerType` 或走动静态转换接口。
|
||||
|
||||
这里唯一需要特殊处理的是 `resize` / `resize2`:当地图缩小时,越界格点对应的稀疏触发器记录必须一起裁剪。
|
||||
|
||||
## 6. 触发器对象延后到 Layer 2 实例化
|
||||
|
||||
当前触发器接口仍然可以保留 `ITrigger` 对象模型,但这个对象不应该提前存进地图数据层。
|
||||
|
||||
更合理的职责划分是:
|
||||
|
||||
1. Layer 1 只存 `triggerType: number`;
|
||||
2. `ITriggerCollector` 在收集时根据 `triggerType` 向 `ITriggerRegistry` 要工厂;
|
||||
3. 再由工厂创建运行时 `ITrigger` 对象并执行后续排序、触发流程。
|
||||
|
||||
这样既满足“地图上只存一个触发器类型数字即可”的诉求,也不需要推翻当前 Layer 2 的触发器执行模型。
|
||||
|
||||
## 7. 当前存档边界
|
||||
|
||||
静态格点的触发器来源当前已经明确为“图块默认值 + 地图手动覆盖值”两层结构,因此存档边界也可以先做阶段性收敛:
|
||||
|
||||
1. 图块默认触发器由 `ITileStore` 提供,不需要在地图层重复存储;
|
||||
2. 若静态格点的手动覆盖值需要进入存档,那么只需要保存地图侧的稀疏覆盖映射,而不需要保存整份默认触发器表;
|
||||
3. 动态图块的存储方案目前尚未设计完成,因此其 `triggerType` 是否进入存档、读档后如何恢复,暂不在本轮接口设计中拍死。
|
||||
|
||||
因此,这一节当前只确认静态覆盖的存储边界;动态图块相关存档语义后续再单独设计。
|
||||
|
||||
## 8. 单个载体当前只存一个触发器类型
|
||||
|
||||
本轮设计中,每个静态格点与每个动态图块都只保存一个 `triggerType: number`。
|
||||
|
||||
这样做的原因是:
|
||||
|
||||
1. 当前触发器本身不接受用户自定义参数,地图侧没有必要提前背负对象列表;
|
||||
2. 对绝大多数格点而言,一个触发器类型已经足够;
|
||||
3. 同一点多触发器仍可通过“静态格点 1 个 + 多个动态图块各 1 个”的方式聚合。
|
||||
|
||||
当前这条限制已经足够。以现有触发器定位来看,一个触发器类型只是告诉系统“这次行为该走哪种处理分支”;更复杂的行为应当放到自定义事件等更高自由度的描述层中,而不是在单个图块上堆叠多个触发器类型。
|
||||
|
||||
---
|
||||
|
||||
# 涉及文件
|
||||
|
||||
## 需要引用的文件
|
||||
|
||||
- `@user/data-base/src/map/types.ts`:当前 `ILayerState`、`IMapLayer`、`IDynamicLayer`、`IDynamicTile` 的权威接口定义
|
||||
- `@user/data-base/src/store/types.ts`:`ITileStore` 的权威接口定义
|
||||
- `@user/data-base/src/map/mapLayer.ts`:静态图层当前的图块数组实现
|
||||
- `@user/data-base/src/map/dynamicLayer.ts`:动静态图块转换逻辑的当前实现
|
||||
- `@user/data-base/src/map/dynamicTile.ts`:动态图块实例对象,适合新增 `triggerType`
|
||||
- `docs/dev/map/trigger.md`:当前触发器文档仍假定从 `getTileInfo` 读取信息,后续需要同步调整
|
||||
|
||||
## 需要修改的文件
|
||||
|
||||
### `@user/data-base/src/map/types.ts`
|
||||
|
||||
- [ ] 不再新增 `ITileInfo` / `IWrappedTileInfo` 作为本轮主接口
|
||||
- [ ] 为 `ILayerState` 新增 `readonly tileStore: ITileStore`
|
||||
- [ ] 为 `IMapLayer` 新增 `getTriggerType(x, y): number`
|
||||
- [ ] 为 `IMapLayer` 新增 `setTriggerType(type: number, x: number, y: number): void`
|
||||
- [ ] 为 `IMapLayer` 新增 `revertTrigger(x: number, y: number): void`
|
||||
- [ ] 为 `IDynamicTile` 新增 `readonly triggerType: number`
|
||||
- [ ] 为 `IDynamicTile` 新增 `setTriggerType(type: number): void`
|
||||
- [ ] 修改 `IDynamicLayer.transferToDynamic`:新增 `keepTrigger?: boolean`
|
||||
- [ ] 修改 `IDynamicLayer.transferToStatic`:新增 `keepTrigger?: boolean`
|
||||
- [ ] 修改 `IDynamicLayer.transferToStaticIfSafe`:新增 `keepTrigger?: boolean`
|
||||
|
||||
### `@user/data-base/src/map/mapLayer.ts`
|
||||
|
||||
- [ ] 新增静态手动覆盖触发器类型的稀疏存储结构
|
||||
- [ ] 实现 `getTriggerType` / `setTriggerType` / `revertTrigger`
|
||||
- [ ] 保持 `setBlock`、`putMapData`、`setMapRef`、`openDoor`、`closeDoor` 与触发器逻辑解耦
|
||||
- [ ] 在 `resize` / `resize2` 中裁剪越界触发器记录
|
||||
|
||||
### `@user/data-base/src/map/dynamicTile.ts`
|
||||
|
||||
- [ ] 新增 `triggerType` 成员与对应写接口
|
||||
|
||||
### `@user/data-base/src/map/dynamicLayer.ts`
|
||||
|
||||
- [ ] 在 `transferToDynamic` 中实现静态格点与动态图块之间的触发器迁移或丢弃逻辑
|
||||
- [ ] 在 `transferToStatic` 中实现动态图块与静态格点之间的触发器迁移或丢弃逻辑
|
||||
- [ ] 若保留 `transferToStaticIfSafe` 的对称性,同步补齐其触发器迁移逻辑
|
||||
|
||||
### `@user/data-base/src/map/mapStore.ts`
|
||||
|
||||
- [ ] 若静态手动覆盖需要进入存档,仅保存地图侧稀疏覆盖映射
|
||||
- [ ] 动态图块触发器字段的存档逻辑待动态图块存储方案定稿后再补齐
|
||||
|
||||
### `docs/dev/map/trigger.md`
|
||||
|
||||
- [ ] 将 `collect` 过程从“读取 `getTileInfo` 中的对象信息”改为“读取静态与动态载体上的触发器类型数字,再按需实例化 `ITrigger`”
|
||||
|
||||
---
|
||||
|
||||
# 当前结论
|
||||
|
||||
1. 静态格点触发器来源采用“`ITileStore` 默认值 + 地图手动覆盖值”两层结构,`revertTrigger` 用于恢复默认值。
|
||||
2. `getTriggerType(x, y)` 与 `ITileStore.getTrigger(num)` 对齐,越界或不存在触发器时统一返回 `-1`。
|
||||
3. `transferToDynamic`、`transferToStatic`、`transferToStaticIfSafe` 的 `keepTrigger` 默认值暂定为 `true`。
|
||||
4. 静态图层只稀疏存储手动覆盖值,不重复保存整份默认触发器表。
|
||||
5. 动态图块触发器的存档语义暂不拍死,待动态图块存储方案单独定稿后再处理。
|
||||
6. 单个载体只保存一个 `triggerType` 当前已经足够,复杂行为应交由自定义事件等更高层描述。
|
||||
308
docs/dev/store/tile-store.md
Normal file
308
docs/dev/store/tile-store.md
Normal 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` 本体直接耦合旧样板细节。
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "@user/data-base",
|
||||
"dependencies": {
|
||||
"@user/data-common": "workspace:*",
|
||||
"@motajs/common": "workspace:*",
|
||||
"@motajs/types": "workspace:*",
|
||||
"@motajs/loader": "workspace:*"
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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 -> 创建函数 */
|
||||
|
||||
@ -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: 颜色参数
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ISaveableContent } from '../common';
|
||||
import { ISaveableContent } from '@user/data-common';
|
||||
|
||||
//#region 怪物基础
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
//#region 字段
|
||||
|
||||
import { ISaveableContent } from '../common';
|
||||
import { ISaveableContent } from '@user/data-common';
|
||||
|
||||
export interface IFlagCommonField<T> {
|
||||
/** 此字段所处的 Flag 系统 */
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { IFacedTileLocator, IHookBase, IHookable } from '@motajs/common';
|
||||
import { FaceDirection, ISaveableContent } from '../common';
|
||||
import { FaceDirection, ISaveableContent } from '@user/data-common';
|
||||
|
||||
//#region 勇士属性
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
export * from './common';
|
||||
export * from './enemy';
|
||||
export * from './flag';
|
||||
export * from './hero';
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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 => {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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 = zIndex,value = 对应图层存档数据 */
|
||||
readonly layers: ReadonlyMap<number, IMapLayerSave>;
|
||||
/** 静态触发器覆盖映射,仅在存在覆盖时写入 */
|
||||
readonly triggers: ReadonlyMap<number, ReadonlyMap<number, number>>;
|
||||
}
|
||||
|
||||
/** 整个 MapStore 的存档数据 */
|
||||
export interface IMapStoreSave {
|
||||
/** key = 楼层 id,只包含 active 的楼层,inactive 的楼层不写入,读档时无需处理 */
|
||||
readonly floors: ReadonlyMap<string, ILayerStateSave>;
|
||||
}
|
||||
|
||||
/** 单段闭区间 [start, end],start 和 end 均为 maps 下标 */
|
||||
export interface IMapAreaInterval {
|
||||
/** 区域起始索引,包含 */
|
||||
readonly start: number;
|
||||
/** 区域结束索引,包含 */
|
||||
readonly end: number;
|
||||
}
|
||||
|
||||
/** 一个区域由一个或多个独立区间组成 */
|
||||
export type MapArea = IMapAreaInterval[];
|
||||
|
||||
export interface IMapStore extends ISaveableContent<IMapStoreSave> {
|
||||
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;
|
||||
|
||||
/**
|
||||
* 直接删除此图块
|
||||
*/
|
||||
|
||||
4
packages-user/data-base/src/shared.ts
Normal file
4
packages-user/data-base/src/shared.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { FaceGroup } from '@user/data-common';
|
||||
|
||||
/** 动态图块所使用的默认移动组,不知道干什么的就别动 */
|
||||
export const DYNAMIC_MOVER_FACE = FaceGroup.Dir8;
|
||||
@ -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>;
|
||||
}
|
||||
|
||||
6
packages-user/data-common/package.json
Normal file
6
packages-user/data-common/package.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@user/data-common",
|
||||
"dependencies": {
|
||||
"@motajs/common": "workspace:*"
|
||||
}
|
||||
}
|
||||
@ -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 '.';
|
||||
|
||||
@ -2,7 +2,7 @@ import { FaceDirection } from './types';
|
||||
|
||||
//#region 接口与枚举
|
||||
|
||||
export const enum InternalFaceGroup {
|
||||
export const enum FaceGroup {
|
||||
/** 四方向(上下左右) */
|
||||
Dir4,
|
||||
/** 八方向(上下左右+斜向) */
|
||||
4
packages-user/data-common/src/index.ts
Normal file
4
packages-user/data-common/src/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from './common';
|
||||
export * from './store';
|
||||
|
||||
export * from './types';
|
||||
2
packages-user/data-common/src/store/index.ts
Normal file
2
packages-user/data-common/src/store/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './tileStore';
|
||||
export * from './types';
|
||||
86
packages-user/data-common/src/store/tileStore.ts
Normal file
86
packages-user/data-common/src/store/tileStore.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
91
packages-user/data-common/src/store/types.ts
Normal file
91
packages-user/data-common/src/store/types.ts
Normal 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;
|
||||
}
|
||||
16
packages-user/data-common/src/types.ts
Normal file
16
packages-user/data-common/src/types.ts
Normal 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;
|
||||
}
|
||||
@ -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:*"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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:*"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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';
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -4,4 +4,5 @@ export * from './hero';
|
||||
export * from './interface';
|
||||
export * from './item';
|
||||
export * from './move';
|
||||
export * from './tile';
|
||||
export * from './utils';
|
||||
|
||||
@ -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()
|
||||
|
||||
43
packages-user/data-state/src/legacy/tile.ts
Normal file
43
packages-user/data-state/src/legacy/tile.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 {
|
||||
/** 该点所有的地图伤害 */
|
||||
|
||||
@ -5,9 +5,9 @@ import {
|
||||
ISpecial,
|
||||
IReadonlyHeroAttribute,
|
||||
IHeroAttribute,
|
||||
IStateBase,
|
||||
ILocationHelper
|
||||
IStateBase
|
||||
} from '@user/data-base';
|
||||
import { ILocationHelper } from '@user/data-common';
|
||||
|
||||
//#region 辅助接口
|
||||
|
||||
|
||||
@ -1,2 +1,4 @@
|
||||
export * from './combat';
|
||||
export * from './trigger';
|
||||
|
||||
export * from './types';
|
||||
|
||||
@ -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 {
|
||||
|
||||
112
packages-user/data-system/src/trigger/collector.ts
Normal file
112
packages-user/data-system/src/trigger/collector.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
export * from './types';
|
||||
export * from './collection';
|
||||
export * from './collector';
|
||||
export * from './registry';
|
||||
export * from './types';
|
||||
|
||||
17
packages-user/data-system/src/types.ts
Normal file
17
packages-user/data-system/src/types.ts
Normal 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>;
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "@user/data-utils",
|
||||
"dependencies": {
|
||||
"@user/data-base": "workspace:*"
|
||||
}
|
||||
}
|
||||
@ -1,2 +0,0 @@
|
||||
export * from './range';
|
||||
export * from './utils';
|
||||
@ -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;
|
||||
});
|
||||
@ -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'
|
||||
);
|
||||
}
|
||||
@ -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:*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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;
|
||||
// ---------- 必要的第三方库
|
||||
|
||||
@ -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:*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
import { createCheckBlock } from './checkblock';
|
||||
|
||||
export function createEnemy() {
|
||||
createCheckBlock();
|
||||
}
|
||||
|
||||
export * from './checkblock';
|
||||
@ -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);
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { FaceDirection } from '@user/data-base';
|
||||
import { FaceDirection } from '@user/data-common';
|
||||
|
||||
export interface ISearchable4Dir {
|
||||
/** 获取上侧元素 */
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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>
|
||||
@ -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';
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -57,6 +57,15 @@
|
||||
- `IObjectMover.speed()`:预期频率**中频**。移动中修改移速有一定使用场景,但远不及 `forward`、`step` 等移动接口的频率,通常只在特殊演出或逻辑中出现,故为中频。典型使用场景:NPC 逃离怪物时先定在原地,随后逐渐加速逃跑。
|
||||
- `IObjectMover.stepFace()`:预期频率**低频**。移动方向与朝向不同的常见场景(后退)已由 `backward` 覆盖,只有极特殊情况才需要此接口(如角色朝向固定但沿垂直方向平移),相当罕见,故为低频。
|
||||
|
||||
## 预期体量
|
||||
|
||||
本节应当写出预期的代码体量,并分析原因。示例如下:
|
||||
|
||||
预期代码体量为 200-300 行。分析如下:
|
||||
|
||||
- `IObjectMover` 首先需要完成计划存储与计划的定义,这些接口基本大致就是向数组中添加元素,每个方法内容都不多,整体预计在 100 行左右。
|
||||
- `IObjectMover` 还需要完成移动流程的编写工作,需要根据每个移动步按照流程执行不同的行为,这一过程较为复杂,预计需要 100-200 行。
|
||||
|
||||
# 实现思路
|
||||
|
||||
按照下面的格式分条描述实现思路。
|
||||
|
||||
Loading…
Reference in New Issue
Block a user