diff --git a/docs/dev/enemy-manager-save.md b/docs/dev/enemy-manager-save.md new file mode 100644 index 0000000..c08cf0c --- /dev/null +++ b/docs/dev/enemy-manager-save.md @@ -0,0 +1,194 @@ +# 需求综述 + +对怪物管理器 `IEnemyManager` 进行存档适配,使其能够参与游戏存档系统。 +由于大多数情况下怪物模板不会被修改,不需要全量存储, +只需对比"参考状态"(游戏加载完成时的初始模板),仅保存发生了变化的模板。 + +为此需要: + +- `IEnemy` 和 `ISpecial` 继承 `ISaveableContent` 以支持自身序列化; +- 给 `ISpecial` 添加 `deepEqualsTo` 接口用于特殊属性间的深度比较; +- `IEnemyManager` 继承 `ISaveableContent`,新增 `compareWith`、`modifyPrefabAttribute`、 + `attachEnemyComparer`、`getEnemyComparer` 接口; +- `IEnemyManager` 内部维护 dirty 集合,以首次 `compareWith` 传入的参考为唯一基准; +- `getPrefab` / `getPrefabById` 返回值收窄为 `IReadonlyEnemy`, + 统一由 `modifyPrefabAttribute` 承担模板修改职责。 + +--- + +# 实现思路 + +## 1. 新增存档状态类型 + +在 `types.ts` 中新增如下类型,用于序列化怪物与管理器的状态: + +```ts +/** 单个 IEnemy 的存档状态 */ +interface IEnemySaveState { + readonly attrs: TAttr; + // 特殊属性按 code 映射,值为各 ISpecial.saveState() 的结果 + readonly specials: ReadonlyMap; +} + +/** IEnemyManager 的存档状态,只保存与参考状态不同的模板 */ +interface IEnemyManagerSaveState { + // code -> 变更后的 IEnemySaveState + readonly modified: ReadonlyMap>; +} +``` + +## 2. 新增 `IEnemyComparer` 接口 + +由于管理器外部没有比较怪物属性的需求,将比较逻辑封装为独立的比较器, +附着在 `EnemyManager` 上。比较器接口如下: + +```ts +interface IEnemyComparer { + compare( + enemyA: IReadonlyEnemy, + enemyB: IReadonlyEnemy + ): boolean; +} +``` + +由用户在初始化时通过 `attachEnemyComparer` 提供。若未提供比较器, +在调用 `modifyPrefabAttribute` 或 `changePrefab` 时需发出警告,且视所有怪物均为脏。 + +## 3. `ISpecial` 继承 `ISaveableContent` + +- `saveState` 返回 `structuredClone(this.value)`(即 `getValue()` 的深拷贝); +- `loadState` 调用 `setValue(state)`; +- 新增 `deepEqualsTo(other: ISpecial): boolean`:先对比 `code`, + 再对 `value` 进行深度比较。 + +各内置实现类的比较策略: + +- `NonePropertySpecial`:只需比较 `code`,`value` 为 `void` 无需对比; +- `CommonSerializableSpecial`:`value` 为普通可序列化对象, + 使用 `lodash-es` 的 `isEqual` 进行递归深度比较。 + +## 4. `IEnemy` 继承 `ISaveableContent>` + +- `saveState(compression)`:深拷贝 `attrs`,对每个 special 调用 + `saveState(compression)` 收集到 `specials` Map,返回 `IEnemySaveState`; +- `loadState(state, compression)`:以 `state.attrs` 还原属性, + 然后对已有的每个 special 按 code 查找存档中的对应条目并调用 `loadState`; + 若存档中出现当前怪物未注册的 special code,发出 logger 警告并跳过。 + +## 5. `IEnemyManager` 接口修改 + +### 5a. 继承 `ISaveableContent>` + +- `saveState(compression)`:遍历 dirty 集合,对每个脏模板调用 + `prefab.saveState(compression)`,汇总为 `IEnemyManagerSaveState` 并返回; +- `loadState(state, compression)`:遍历 `state.modified`, + 找到 code 对应的现有模板,调用 `prefab.loadState(enemyState, compression)` 还原; + 若某 code 不在当前 prefab 表中,发出 logger 警告并跳过; + **不清空 dirty 集合**,始终以首次 `compareWith` 提供的参考为唯一基准; + `loadState` 结束后重新用比较器对每个已有脏模板进行比对, + 刷新 dirty 集合(避免加载后实际已恢复初始值的模板仍停留在 dirty 中)。 + +### 5b. 新增 `compareWith` + +```ts +compareWith(reference: ReadonlyMap>): void; +``` + +- 由调用方在游戏初始化完成后提供参考快照,外部传入,管理器保存引用; +- **首次调用**:直接存储参考,清空 dirty 集合; +- **非首次调用**:通过 logger 发出警告,提示此操作风险高, + 请作者确认操作意图,但仍然执行覆盖(直接替换参考,重置 dirty 集合)。 + +### 5c. `getPrefab` / `getPrefabById` 返回值改为 `IReadonlyEnemy` + +原来返回 `IEnemy`,外部可以直接修改模板。 +改为只读引用,外部不能直接修改,必须通过 `modifyPrefabAttribute` 完成。 + +### 5d. 新增 `modifyPrefabAttribute` + +```ts +modifyPrefabAttribute( + code: number | string, + modify: (prefab: IEnemy) => IEnemy +): void; +``` + +执行流程: + +1. 根据 `code`(数字或 id 字符串)找到对应的模板; +2. 将模板以可写引用传入 `modify`,获得修改结果; +3. 若 `modify` 返回的是**新引用**(与传入的不同),则将该新对象替换模板表条目 + (同时更新 `prefabByCode` 与 `prefabById`); +4. 将最终生效的模板与 `compareWith` 中提供的参考模板进行 `IEnemyComparer.compare` 比较: + - 若不相等,则将此 code 加入 dirty 集合; + - 若相等(改回了初始值),则从 dirty 集合中移除; +5. 若未附加比较器,则始终视为脏,并发出 logger 警告。 + +### 5e. 新增 `attachEnemyComparer` / `getEnemyComparer` + +```ts +attachEnemyComparer(comparer: IEnemyComparer): void; +getEnemyComparer(): IEnemyComparer | null; +``` + +- `attachEnemyComparer`:设置当前管理器使用的比较器; +- `getEnemyComparer`:返回当前比较器,如未设置则返回 `null`, + 允许外部在特殊场景下借用比较器。 + +### 5f. `changePrefab` 也参与 dirty 追踪 + +`changePrefab` 直接替换模板表,修改完成后同样与参考模板进行比较, +更新 dirty 集合(逻辑与 `modifyPrefabAttribute` 步骤 4 相同)。 + +`deletePrefab` 不参与 dirty 追踪,存档时直接跳过被删除的模板。 + +--- + +# 涉及文件 + +## 需要引用的文件 + +- `lodash-es`:`CommonSerializableSpecial.deepEqualsTo` 中使用 `isEqual` 进行深度比较 +- `@motajs/common`:引用 `logger` 接口 +- `@user/data-base/common/types.ts`:引用 `ISaveableContent`、`SaveCompression` + +## 需要修改的文件 + +### `packages-user/data-base/src/enemy/types.ts` + +- [ ] 新增 `IEnemySaveState` 类型:单个怪物的存档状态 +- [ ] 新增 `IEnemyManagerSaveState` 类型:管理器的存档状态 +- [ ] 新增 `IEnemyComparer` 接口:包含 `compare` 方法,由用户实现 +- [ ] 修改 `ISpecial`:继承 `ISaveableContent`, + 新增 `deepEqualsTo(other: ISpecial): boolean` +- [ ] 修改 `IEnemy`:继承 `ISaveableContent>` +- [ ] 修改 `IEnemyManager`:继承 `ISaveableContent>`, + 新增 `compareWith`、`modifyPrefabAttribute`、`attachEnemyComparer`、`getEnemyComparer`; + 修改 `getPrefab` 与 `getPrefabById` 返回类型为 `IReadonlyEnemy` + +### `packages-user/data-base/src/enemy/enemy.ts`(`Enemy` 类) + +- [ ] 实现 `saveState(compression): IEnemySaveState` +- [ ] 实现 `loadState(state, compression): void` + +### `packages-user/data-base/src/enemy/manager.ts`(`EnemyManager` 类) + +- [ ] 新增 `private readonly dirtySet: Set` 成员:记录脏模板的 code +- [ ] 新增 `private referenceByCode: Map>` 成员: + 保存参考快照 +- [ ] 新增 `private comparer: IEnemyComparer | null` 成员:比较器 +- [ ] 新增 `private hasReference: boolean` 成员:标记是否已首次调用 `compareWith` +- [ ] 实现 `compareWith`:存储参考快照,非首次调用发出警告,重置 dirty 集合 +- [ ] 实现 `modifyPrefabAttribute`:调用 modify、处理引用变化、比较、更新 dirty 集合 +- [ ] 修改 `changePrefab`:替换模板后同步更新 dirty 集合 +- [ ] 修改 `getPrefab` / `getPrefabById` 返回类型(仅类型,实现无需改动) +- [ ] 实现 `attachEnemyComparer` / `getEnemyComparer` +- [ ] 实现 `saveState`:遍历 dirty 集合,序列化并返回 +- [ ] 实现 `loadState`:根据存档恢复脏模板,恢复后重新刷新 dirty 集合 + +### 引擎内置特殊属性(当前包内) + +- [ ] `NonePropertySpecial`:实现 `saveState`、`loadState`、`deepEqualsTo` + (`value` 为 `void`,`deepEqualsTo` 只比较 `code`) +- [ ] `CommonSerializableSpecial`:实现 `saveState`、`loadState`、`deepEqualsTo` + (`deepEqualsTo` 的 value 比较使用 `lodash-es` 的 `isEqual`) diff --git a/docs/dev/hero-modifier-save.md b/docs/dev/hero-modifier-save.md index c060db7..b5a87e1 100644 --- a/docs/dev/hero-modifier-save.md +++ b/docs/dev/hero-modifier-save.md @@ -83,28 +83,28 @@ iterateModifiers(): Iterable<[keyof THero, IHeroModifier]>; ### `@user/data-base/hero/types.ts` -- [ ] 修改 `IHeroModifier` 接口: +- [x] 修改 `IHeroModifier` 接口: 改为 `IHeroModifier`,`identifier` 改名为 `type`, 继承 `ISaveableContent` -- [ ] 新增 `IModifierStateSave` 接口:单条修饰器的存档格式 -- [ ] 修改 `IHeroStateSave`:新增 `readonly modifiers: readonly IModifierStateSave[]` 字段 -- [ ] 修改 `IReadonlyHeroAttribute`:新增 `iterateModifiers()` 方法签名 -- [ ] 修改 `IHeroState`:新增以下方法签名 - `registerModifier(type: string, cons: () => IHeroModifier): void` - `createModifier(type: string): IHeroModifier` - `createAndInsertModifier(type: string, name: K): IHeroModifier` +- [x] 新增 `IModifierStateSave` 接口:单条修饰器的存档格式 +- [x] 修改 `IHeroStateSave`:新增 `readonly modifiers: readonly IModifierStateSave[]` 字段 +- [x] 修改 `IReadonlyHeroAttribute`:新增 `iterateModifiers()` 方法签名 +- [x] 修改 `IHeroState`:新增以下方法签名 - `registerModifier(type: string, cons: () => IHeroModifier): void` - `createModifier(type: string): IHeroModifier` - `createAndInsertModifier(type: string, name: K): IHeroModifier` ### `@user/data-base/hero/attribute.ts` -- [ ] 修改 `BaseHeroModifier`: +- [x] 修改 `BaseHeroModifier`: 将 `abstract readonly identifier` 改为 `abstract readonly type`; 实现 `saveState` / `loadState` -- [ ] 修改 `HeroAttribute`:实现 `iterateModifiers()` +- [x] 修改 `HeroAttribute`:实现 `iterateModifiers()` ### `@user/data-base/hero/state.ts` -- [ ] 修改 `HeroState`:新增 `private readonly registry: Map IHeroModifier>` 成员 -- [ ] 实现 `HeroState.registerModifier`:将工厂函数写入 `registry` -- [ ] 实现 `HeroState.createModifier`:从 `registry` 取出工厂并调用,返回新实例; +- [x] 修改 `HeroState`:新增 `private readonly registry: Map IHeroModifier>` 成员 +- [x] 实现 `HeroState.registerModifier`:将工厂函数写入 `registry` +- [x] 实现 `HeroState.createModifier`:从 `registry` 取出工厂并调用,返回新实例; 若 `type` 未注册则抛出错误 -- [ ] 实现 `HeroState.createAndInsertModifier`:调用 `createModifier` 后, +- [x] 实现 `HeroState.createAndInsertModifier`:调用 `createModifier` 后, 再调用 `this.attribute.addModifier(name, modifier)`,返回同一实例 -- [ ] 修改 `HeroState.saveState`:遍历 `iterateModifiers()` 写入 `modifiers` 字段 -- [ ] 修改 `HeroState.loadState`:遍历 `state.modifiers` 重建修饰器并挂载 +- [x] 修改 `HeroState.saveState`:遍历 `iterateModifiers()` 写入 `modifiers` 字段 +- [x] 修改 `HeroState.loadState`:遍历 `state.modifiers` 重建修饰器并挂载 diff --git a/packages-user/data-base/src/enemy/enemy.ts b/packages-user/data-base/src/enemy/enemy.ts index 501da36..189e2f1 100644 --- a/packages-user/data-base/src/enemy/enemy.ts +++ b/packages-user/data-base/src/enemy/enemy.ts @@ -2,10 +2,12 @@ import { logger } from '@motajs/common'; import { IEnemy, IEnemyContext, + IEnemySaveState, IReadonlyEnemy, ISpecial, IEnemyView } from './types'; +import { SaveCompression } from '../common/types'; export class Enemy implements IEnemy { /** 怪物身上的特殊属性列表 */ @@ -87,6 +89,29 @@ export class Enemy implements IEnemy { this.addSpecial(special.clone()); } } + + saveState(_compression: SaveCompression): IEnemySaveState { + const specials: Map = new Map(); + for (const special of this.specials) { + specials.set(special.code, special.saveState(_compression)); + } + return { attrs: structuredClone(this.attributes), specials }; + } + + loadState( + state: IEnemySaveState, + compression: SaveCompression + ): void { + this.attributes = structuredClone(state.attrs); + for (const special of this.specials) { + const saved = state.specials.get(special.code); + if (saved === undefined) { + logger.warn(120, special.code.toString(), this.id); + continue; + } + special.loadState(saved, compression); + } + } } export class EnemyView implements IEnemyView { diff --git a/packages-user/data-base/src/enemy/manager.ts b/packages-user/data-base/src/enemy/manager.ts index 2f69c34..210f601 100644 --- a/packages-user/data-base/src/enemy/manager.ts +++ b/packages-user/data-base/src/enemy/manager.ts @@ -2,10 +2,15 @@ import { logger } from '@motajs/common'; import { Enemy as EnemyImpl } from './enemy'; import { IEnemy, + IEnemyComparer, IEnemyManager, + IEnemyManagerSaveState, IEnemyLegacyBridge, - SpecialCreation + IReadonlyEnemy, + SpecialCreation, + IEnemySaveState } from './types'; +import { SaveCompression } from '../common/types'; export class EnemyManager implements IEnemyManager { /** 特殊属性注册表,code -> 创建函数 */ @@ -19,6 +24,14 @@ export class EnemyManager implements IEnemyManager { private readonly prefabById: Map> = new Map(); /** 旧样板怪物 id 到 code 的映射,用于 fromLegacyEnemy 快速查找已有模板 */ private readonly legacyIdToCode: Map = new Map(); + /** 脏模板集合,存储发生了变化的模板 code */ + private readonly dirtySet: Set = new Set(); + /** 参考快照,code -> IReadonlyEnemy,由 compareWith 提供 */ + private referenceByCode: Map> = new Map(); + /** 当前附加的怪物比较器 */ + private comparer: IEnemyComparer | null = null; + /** 是否已首次调用 compareWith */ + private hasReference: boolean = false; constructor(readonly bridge: IEnemyLegacyBridge) {} @@ -127,6 +140,7 @@ export class EnemyManager implements IEnemyManager { const cloned = enemy.clone(); this.prefabByCode.set(enemy.code, cloned); this.prefabById.set(enemy.id, cloned); + this.updateDirty(cloned.code, cloned); } addPrefabFromLegacy(code: number, enemy: Enemy): void { @@ -137,13 +151,14 @@ export class EnemyManager implements IEnemyManager { this.prefabByCode.set(code, prefab); this.prefabById.set(prefab.id, prefab); this.legacyIdToCode.set(enemy.id, code); + this.updateDirty(code, prefab); } - getPrefab(code: number): IEnemy | null { + getPrefab(code: number): IReadonlyEnemy | null { return this.prefabByCode.get(code) ?? null; } - getPrefabById(id: string): IEnemy | null { + getPrefabById(id: string): IReadonlyEnemy | null { return this.prefabById.get(id) ?? null; } @@ -160,6 +175,7 @@ export class EnemyManager implements IEnemyManager { // 再添加新的模板 this.prefabByCode.set(enemy.code, enemy); this.prefabById.set(enemy.id, enemy); + this.updateDirty(enemy.code, enemy); } reusePrefab(source: number | string, code: number, id: string): void { @@ -168,4 +184,117 @@ export class EnemyManager implements IEnemyManager { this.prefabByCode.set(code, prefab); this.prefabById.set(id, prefab); } + + compareWith(reference: ReadonlyMap>): void { + const isSubsequentCall = this.hasReference; + if (isSubsequentCall) { + logger.warn(117); + } + this.referenceByCode = new Map(); + reference.forEach((enemy, key) => { + this.referenceByCode.set(key, enemy.clone()); + }); + this.hasReference = true; + this.dirtySet.clear(); + if (isSubsequentCall) { + this.refreshDirty(reference.keys()); + } + } + + modifyPrefabAttribute( + code: number | string, + modify: (prefab: IEnemy) => IEnemy + ): void { + const prefab = this.internalGetPrefab(code); + if (!prefab) return; + const result = modify(prefab); + const prefabCode = prefab.code; + if (result !== prefab) { + this.prefabByCode.set(result.code, result); + this.prefabById.set(result.id, result); + if (result.code !== prefabCode) { + this.prefabByCode.delete(prefabCode); + } + if (result.id !== prefab.id) { + this.prefabById.delete(prefab.id); + } + } + this.updateDirty(result.code, result); + } + + attachEnemyComparer(comparer: IEnemyComparer): void { + this.comparer = comparer; + } + + getEnemyComparer(): IEnemyComparer | null { + return this.comparer; + } + + saveState(compression: SaveCompression): IEnemyManagerSaveState { + const modified: Map> = new Map(); + for (const code of this.dirtySet) { + const prefab = this.prefabByCode.get(code); + if (!prefab) continue; + modified.set(code, prefab.saveState(compression)); + } + return { modified }; + } + + loadState( + state: IEnemyManagerSaveState, + compression: SaveCompression + ): void { + for (const [code, enemyState] of state.modified) { + const prefab = this.prefabByCode.get(code); + if (!prefab) { + logger.warn(119, code.toString()); + continue; + } + prefab.loadState(enemyState, compression); + } + // loadState 结束后重新刷新 dirty 集合 + this.refreshDirty(state.modified.keys()); + } + + /** + * 根据参考快照更新指定 code 的脏状态 + * @param code 怪物图块数字 + * @param current 当前模板对象 + */ + private updateDirty(code: number, current: IEnemy): void { + if (!this.hasReference) return; + if (!this.comparer) { + logger.warn(118); + this.dirtySet.add(code); + return; + } + const ref = this.referenceByCode.get(code); + if (!ref || !this.comparer.compare(current, ref)) { + this.dirtySet.add(code); + } else { + this.dirtySet.delete(code); + } + } + + /** + * 将所有模板加入脏集合,再与参考比较,去除未变化的模板 + */ + private refreshDirty(dirties: Iterable): void { + if (!this.hasReference) return; + for (const code of dirties) { + this.dirtySet.add(code); + } + if (!this.comparer) return; + for (const code of [...this.dirtySet]) { + const prefab = this.prefabByCode.get(code); + if (!prefab) { + this.dirtySet.delete(code); + continue; + } + const ref = this.referenceByCode.get(code); + if (ref && this.comparer.compare(prefab, ref)) { + this.dirtySet.delete(code); + } + } + } } diff --git a/packages-user/data-base/src/enemy/special.ts b/packages-user/data-base/src/enemy/special.ts index 5072728..59c1083 100644 --- a/packages-user/data-base/src/enemy/special.ts +++ b/packages-user/data-base/src/enemy/special.ts @@ -1,3 +1,5 @@ +import { isEqual } from 'lodash-es'; +import { SaveCompression } from '../common/types'; import { ISpecial, SpecialCreation } from './types'; // TODO: 颜色参数 @@ -45,6 +47,19 @@ export class CommonSerializableSpecial implements ISpecial { this.config ); } + + saveState(_compression: SaveCompression): T { + return structuredClone(this.value); + } + + loadState(state: T, _compression: SaveCompression): void { + this.setValue(state); + } + + deepEqualsTo(other: ISpecial): boolean { + if (this.code !== other.code) return false; + return isEqual(this.value, other.getValue()); + } } export class NonePropertySpecial implements ISpecial { @@ -78,6 +93,18 @@ export class NonePropertySpecial implements ISpecial { clone(): ISpecial { return new NonePropertySpecial(this.code, this.config); } + + saveState(_compression: SaveCompression): void { + return undefined; + } + + loadState(_state: void, _compression: SaveCompression): void { + // 无属性,无需操作 + } + + deepEqualsTo(other: ISpecial): boolean { + return this.code === other.code; + } } export function defineCommonSerializableSpecial( diff --git a/packages-user/data-base/src/enemy/types.ts b/packages-user/data-base/src/enemy/types.ts index 210381a..849a7a2 100644 --- a/packages-user/data-base/src/enemy/types.ts +++ b/packages-user/data-base/src/enemy/types.ts @@ -1,9 +1,36 @@ import { IRange, ITileLocator } from '@motajs/common'; import { IHeroAttribute, IReadonlyHeroAttribute } from '../hero'; +import { ISaveableContent } from '../common/types'; //#region 怪物基础 -export interface ISpecial { +/** 单个 IEnemy 的存档状态 */ +export interface IEnemySaveState { + /** 怪物属性的深拷贝 */ + readonly attrs: TAttr; + /** 特殊属性按 code 映射,值为各 ISpecial.saveState() 的结果 */ + readonly specials: ReadonlyMap; +} + +/** IEnemyManager 的存档状态,只保存与参考状态不同的模板 */ +export interface IEnemyManagerSaveState { + /** code -> 变更后的 IEnemySaveState,仅包含脏模板 */ + readonly modified: ReadonlyMap>; +} + +export interface IEnemyComparer { + /** + * 比较两个怪物是否完全相同 + * @param enemyA 怪物 A + * @param enemyB 怪物 B + */ + compare( + enemyA: IReadonlyEnemy, + enemyB: IReadonlyEnemy + ): boolean; +} + +export interface ISpecial extends ISaveableContent { /** 特殊属性代码 */ readonly code: number; /** 特殊属性需要的数值 */ @@ -40,6 +67,12 @@ export interface ISpecial { * 深拷贝此特殊属性 */ clone(): ISpecial; + + /** + * 深度比较此特殊属性与另一特殊属性是否相同 + * @param other 另一特殊属性 + */ + deepEqualsTo(other: ISpecial): boolean; } export interface IReadonlyEnemy { @@ -82,7 +115,8 @@ export interface IReadonlyEnemy { clone(): IReadonlyEnemy; } -export interface IEnemy extends IReadonlyEnemy { +export interface IEnemy + extends IReadonlyEnemy, ISaveableContent> { /** * 添加特殊属性 * @param special 特殊属性对象 @@ -138,7 +172,9 @@ export interface IEnemyLegacyBridge { fromLegacyEnemy(enemy: Enemy, defaultValue: Partial): TAttr; } -export interface IEnemyManager { +export interface IEnemyManager extends ISaveableContent< + IEnemyManagerSaveState +> { /** * 注册一个特殊属性 * @param code 特殊属性代码 @@ -193,13 +229,13 @@ export interface IEnemyManager { * 获取指定怪物的模板 * @param code 怪物图块数字 */ - getPrefab(code: number): IEnemy | null; + getPrefab(code: number): IReadonlyEnemy | null; /** * 根据怪物的 `id` 获取对应的怪物模板 * @param id 怪物 `id` */ - getPrefabById(id: string): IEnemy | null; + getPrefabById(id: string): IReadonlyEnemy | null; /** * 删除指定的怪物模板 @@ -221,6 +257,34 @@ export interface IEnemyManager { * @param id 复用怪物 id */ reusePrefab(source: number | string, code: number, id: string): void; + + /** + * 设置参考快照,后续对模板的修改将与此比较以确定是否脏。 + * 非首次调用时会发出警告,但仍执行覆盖 + * @param reference code -> 参考怪物的 Map + */ + compareWith(reference: ReadonlyMap>): void; + + /** + * 修改指定怪物模板的属性,修改完成后自动与参考模板比较并更新 dirty 集合 + * @param code 怪物的图块数字或 `id` + * @param modify 修改函数,传入可写怪物对象,返回修改后的对象 + */ + modifyPrefabAttribute( + code: number | string, + modify: (prefab: IEnemy) => IEnemy + ): void; + + /** + * 附加怪物比较器,用于 dirty 集合的判断 + * @param comparer 比较器对象 + */ + attachEnemyComparer(comparer: IEnemyComparer): void; + + /** + * 获取当前附加的怪物比较器,如未设置则返回 `null` + */ + getEnemyComparer(): IEnemyComparer | null; } //#endregion diff --git a/packages/common/src/logger.json b/packages/common/src/logger.json index 3f850b1..2028acd 100644 --- a/packages/common/src/logger.json +++ b/packages/common/src/logger.json @@ -173,6 +173,10 @@ "114": "Save operation took $1ms, exceeding the tolerance of $2ms. Consider reducing compression level.", "115": "Autosave operation took $1ms, exceeding the tolerance of $2ms. Consider reducing compression level.", "116": "Cannot construct modifier of type '$1' since no registry for it.", + "117": "EnemyManager.compareWith called more than once. The previous reference will be overridden. Please ensure you intend to do this.", + "118": "No enemy comparer attached to EnemyManager. All enemies will be treated as dirty.", + "119": "Enemy prefab with code $1 not found during loadState, skipping.", + "120": "Special with code $1 not found in enemy '$2' during loadState, skipping.", "1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency." } }