From 13fc4e1b7c11dcb8bff59685db94d8ed6b8527ed Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Sun, 17 May 2026 16:31:08 +0800 Subject: [PATCH] refactor: TileStore --- docs/dev/store/tile-store.md | 308 ++++++++++++++++++ packages-user/data-base/src/index.ts | 1 + packages-user/data-base/src/store/index.ts | 2 + .../data-base/src/store/tileStore.ts | 86 +++++ packages-user/data-base/src/store/types.ts | 91 ++++++ packages-user/data-base/src/types.ts | 7 +- packages-user/data-state/src/core.ts | 67 +++- packages-user/data-state/src/index.ts | 45 +-- packages-user/data-state/src/legacy/index.ts | 1 + packages-user/data-state/src/legacy/tile.ts | 39 +++ .../legacy-plugin-data/src/fallback.ts | 2 +- packages/common/src/logger.json | 3 + 12 files changed, 588 insertions(+), 64 deletions(-) create mode 100644 docs/dev/store/tile-store.md create mode 100644 packages-user/data-base/src/store/index.ts create mode 100644 packages-user/data-base/src/store/tileStore.ts create mode 100644 packages-user/data-base/src/store/types.ts create mode 100644 packages-user/data-state/src/legacy/tile.ts diff --git a/docs/dev/store/tile-store.md b/docs/dev/store/tile-store.md new file mode 100644 index 0000000..2f1fae4 --- /dev/null +++ b/docs/dev/store/tile-store.md @@ -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( + converter: ITileLegacyConverter + ): void; + fromLegacy(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 { + 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` +2. `Map` +3. `Map` + +也就是说,这次重构的重点是**收拢职责和稳定接口**,不是刻意放弃现有映射结构。 + +## 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` 本体直接耦合旧样板细节。 diff --git a/packages-user/data-base/src/index.ts b/packages-user/data-base/src/index.ts index 0be7276..55d11bf 100644 --- a/packages-user/data-base/src/index.ts +++ b/packages-user/data-base/src/index.ts @@ -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'; diff --git a/packages-user/data-base/src/store/index.ts b/packages-user/data-base/src/store/index.ts new file mode 100644 index 0000000..b86cf0f --- /dev/null +++ b/packages-user/data-base/src/store/index.ts @@ -0,0 +1,2 @@ +export * from './tileStore'; +export * from './types'; diff --git a/packages-user/data-base/src/store/tileStore.ts b/packages-user/data-base/src/store/tileStore.ts new file mode 100644 index 0000000..af88d98 --- /dev/null +++ b/packages-user/data-base/src/store/tileStore.ts @@ -0,0 +1,86 @@ +import { isNil } from 'lodash-es'; +import { logger } from '@motajs/common'; +import { + ITileLegacyConverter, + ITileRawData, + ITileStore, + TileType +} from './types'; + +export class TileStore implements ITileStore { + /** 以图块数字为键的原始图块定义表 */ + private readonly dataMap: Map = new Map(); + + /** 由图块 id 反查图块数字的映射表 */ + private readonly idMap: Map = new Map(); + + /** 由图块数字反查图块 id 的映射表 */ + private readonly numMap: Map = new Map(); + + /** 当前挂载的旧样板图块转换器 */ + private legacyConverter: ITileLegacyConverter | 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): 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); + } +} diff --git a/packages-user/data-base/src/store/types.ts b/packages-user/data-base/src/store/types.ts new file mode 100644 index 0000000..4528233 --- /dev/null +++ b/packages-user/data-base/src/store/types.ts @@ -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 { + /** + * 将旧样板图块定义转换为新的图块原始数据 + * @param num 图块数字 + * @param legacy 旧样板图块定义 + */ + fromLegacy(num: number, legacy: TLegacy): ITileRawData; +} + +export interface ITileStore { + /** + * 获取指定图块数字对应的完整原始定义 + * @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): void; + + /** + * 使用当前转换器转换并写入一个旧样板图块定义 + * @param num 图块数字 + * @param legacy 旧样板图块定义 + */ + fromLegacy(num: number, legacy: TLegacy): ITileRawData; +} diff --git a/packages-user/data-base/src/types.ts b/packages-user/data-base/src/types.ts index b733dbc..33b6e5f 100644 --- a/packages-user/data-base/src/types.ts +++ b/packages-user/data-base/src/types.ts @@ -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 { readonly roleFace: IRoleFaceBinder; /** 朝向管理 */ readonly faceManager: IFaceManager; - /** id 到图块数字的映射 */ - readonly idNumberMap: Map; - /** 图块数字到 id 的映射 */ - readonly numberIdMap: Map; + /** 图块定义存储 */ + readonly tileStore: ITileStore; /** 地图状态 */ readonly maps: IMapStore; diff --git a/packages-user/data-state/src/core.ts b/packages-user/data-state/src/core.ts index 970ef08..35e11cc 100644 --- a/packages-user/data-state/src/core.ts +++ b/packages-user/data-state/src/core.ts @@ -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; - readonly numberIdMap: Map; + readonly tileStore: ITileStore; // 可存档内容 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(); + 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(); + const enemyContext = new EnemyContext(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>(); 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); diff --git a/packages-user/data-state/src/index.ts b/packages-user/data-state/src/index.ts index 2cdc90c..d459569 100644 --- a/packages-user/data-state/src/index.ts +++ b/packages-user/data-state/src/index.ts @@ -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'; diff --git a/packages-user/data-state/src/legacy/index.ts b/packages-user/data-state/src/legacy/index.ts index 2f2826e..9dd9b6e 100644 --- a/packages-user/data-state/src/legacy/index.ts +++ b/packages-user/data-state/src/legacy/index.ts @@ -4,4 +4,5 @@ export * from './hero'; export * from './interface'; export * from './item'; export * from './move'; +export * from './tile'; export * from './utils'; diff --git a/packages-user/data-state/src/legacy/tile.ts b/packages-user/data-state/src/legacy/tile.ts new file mode 100644 index 0000000..8694394 --- /dev/null +++ b/packages-user/data-state/src/legacy/tile.ts @@ -0,0 +1,39 @@ +import { ITileLegacyConverter, ITileRawData, TileType } from '@user/data-base'; + +export type LegacyTileData = MapDataOf; + +export class TileLegacyBridge implements ITileLegacyConverter { + 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; + } + } +} diff --git a/packages-user/legacy-plugin-data/src/fallback.ts b/packages-user/legacy-plugin-data/src/fallback.ts index 4cb8edf..8cd88eb 100644 --- a/packages-user/legacy-plugin-data/src/fallback.ts +++ b/packages-user/legacy-plugin-data/src/fallback.ts @@ -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); diff --git a/packages/common/src/logger.json b/packages/common/src/logger.json index 8fde405..731b04c 100644 --- a/packages/common/src/logger.json +++ b/packages/common/src/logger.json @@ -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." } }