mirror of
https://github.com/motajs/template.git
synced 2026-05-19 08:21:10 +08:00
refactor: TileStore
This commit is contained in:
parent
501a598de0
commit
13fc4e1b7c
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` 本体直接耦合旧样板细节。
|
||||
@ -4,6 +4,7 @@ export * from './flag';
|
||||
export * from './hero';
|
||||
export * from './load';
|
||||
export * from './map';
|
||||
export * from './store';
|
||||
|
||||
export * from './game';
|
||||
export * from './types';
|
||||
|
||||
2
packages-user/data-base/src/store/index.ts
Normal file
2
packages-user/data-base/src/store/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './tileStore';
|
||||
export * from './types';
|
||||
86
packages-user/data-base/src/store/tileStore.ts
Normal file
86
packages-user/data-base/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-base/src/store/types.ts
Normal file
91
packages-user/data-base/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;
|
||||
}
|
||||
@ -3,6 +3,7 @@ import { IEnemyManager } from './enemy';
|
||||
import { IFlagSystem } from './flag';
|
||||
import { IFaceManager, IRoleFaceBinder, ISaveableContent } from './common';
|
||||
import { IMapStore } from './map';
|
||||
import { ITileStore } from './store';
|
||||
|
||||
export interface IStateSaveData {
|
||||
/** 跟随者列表 */
|
||||
@ -14,10 +15,8 @@ export interface IStateBase<TEnemy, THero> {
|
||||
readonly roleFace: IRoleFaceBinder;
|
||||
/** 朝向管理 */
|
||||
readonly faceManager: IFaceManager;
|
||||
/** id 到图块数字的映射 */
|
||||
readonly idNumberMap: Map<string, number>;
|
||||
/** 图块数字到 id 的映射 */
|
||||
readonly numberIdMap: Map<number, string>;
|
||||
/** 图块定义存储 */
|
||||
readonly tileStore: ITileStore;
|
||||
|
||||
/** 地图状态 */
|
||||
readonly maps: IMapStore;
|
||||
|
||||
@ -1,12 +1,8 @@
|
||||
import { ICoreState, ISaveableExecutor } from './types';
|
||||
import {
|
||||
DamageSystem,
|
||||
EnemyContext,
|
||||
EnemyManager,
|
||||
HeroMover,
|
||||
IEnemyContext,
|
||||
IEnemyManager,
|
||||
MapDamage,
|
||||
HeroAttribute,
|
||||
HeroState,
|
||||
IHeroState,
|
||||
@ -27,7 +23,9 @@ import {
|
||||
FaceManager,
|
||||
InternalFaceGroup,
|
||||
Dir4FaceHandler,
|
||||
Dir8FaceHandler
|
||||
Dir8FaceHandler,
|
||||
ITileStore,
|
||||
TileStore
|
||||
} from '@user/data-base';
|
||||
import {
|
||||
CommonAuraConverter,
|
||||
@ -52,17 +50,23 @@ 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';
|
||||
import {
|
||||
DamageSystem,
|
||||
EnemyContext,
|
||||
IEnemyContext,
|
||||
MapDamage
|
||||
} from '@user/data-system';
|
||||
|
||||
export class CoreState implements ICoreState {
|
||||
// 全局内容
|
||||
readonly roleFace: IRoleFaceBinder;
|
||||
readonly faceManager: IFaceManager;
|
||||
readonly idNumberMap: Map<string, number>;
|
||||
readonly numberIdMap: Map<number, string>;
|
||||
readonly tileStore: ITileStore<LegacyTileData>;
|
||||
|
||||
// 可存档内容
|
||||
readonly maps: IMapStore;
|
||||
@ -88,8 +92,9 @@ export class CoreState implements ICoreState {
|
||||
|
||||
constructor() {
|
||||
this.maps = new MapStore();
|
||||
this.idNumberMap = new Map();
|
||||
this.numberIdMap = new Map();
|
||||
const tileStore = new TileStore<LegacyTileData>();
|
||||
tileStore.attachLegacyConverter(new TileLegacyBridge());
|
||||
this.tileStore = tileStore;
|
||||
|
||||
this.loadProgress = new LoadProgressTotal();
|
||||
this.dataLoader = new MotaDataLoader(this.loadProgress);
|
||||
@ -118,7 +123,7 @@ export class CoreState implements ICoreState {
|
||||
registerSpecials(enemyManager);
|
||||
this.enemyManager = enemyManager;
|
||||
// 怪物上下文初始化
|
||||
const enemyContext = new EnemyContext<IEnemyAttr, IHeroAttr>();
|
||||
const enemyContext = new EnemyContext<IEnemyAttr, IHeroAttr>(this);
|
||||
const damageSystem = new DamageSystem(enemyContext);
|
||||
const mapDamage = new MapDamage(enemyContext);
|
||||
damageSystem.useCalculator(new MainDamageCalculator());
|
||||
@ -170,6 +175,7 @@ export class CoreState implements ICoreState {
|
||||
|
||||
// 加载先使用兼容层实现
|
||||
loading.once('loaded', () => {
|
||||
this.initTileStore(core.maps.blocksInfo);
|
||||
this.initEnemyManager(enemys_fcae963b_31c9_42b4_b48c_bb48d09f3f80);
|
||||
this.initMapStore(
|
||||
core.floorIds,
|
||||
@ -185,6 +191,37 @@ export class CoreState implements ICoreState {
|
||||
//#endregion
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化图块存储对象
|
||||
* @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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化怪物管理器对象
|
||||
* @param data 旧样板怪物存储对象
|
||||
@ -194,15 +231,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,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';
|
||||
|
||||
39
packages-user/data-state/src/legacy/tile.ts
Normal file
39
packages-user/data-state/src/legacy/tile.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { ITileLegacyConverter, ITileRawData, TileType } from '@user/data-base';
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -372,7 +372,7 @@ export function initFallback() {
|
||||
if (core.status.replay.speed === 24) {
|
||||
cb();
|
||||
} else {
|
||||
const num = state.idNumberMap.get(id)!;
|
||||
const num = state.tileStore.idToNumber(id)!;
|
||||
const layer = state.maps.getLayerByAlias('event')!;
|
||||
layer.closeDoor(num, x, y).then(cb);
|
||||
|
||||
|
||||
@ -55,6 +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.",
|
||||
"56": "Cannot convert legacy tile data since no tile legacy converter is attached to TileStore.",
|
||||
"1201": "Floor-damage extension needs 'floor-binder' extension as dependency."
|
||||
},
|
||||
"warn": {
|
||||
@ -190,6 +191,8 @@
|
||||
"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.",
|
||||
"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.",
|
||||
"1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency."
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user