refactor: 怪物管理器存档

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
unanmed 2026-04-24 22:17:01 +08:00
parent 8c1becc9f1
commit 2ce50bf23e
7 changed files with 464 additions and 21 deletions

View File

@ -0,0 +1,194 @@
# 需求综述
对怪物管理器 `IEnemyManager` 进行存档适配,使其能够参与游戏存档系统。
由于大多数情况下怪物模板不会被修改,不需要全量存储,
只需对比"参考状态"(游戏加载完成时的初始模板),仅保存发生了变化的模板。
为此需要:
- `IEnemy``ISpecial` 继承 `ISaveableContent` 以支持自身序列化;
- 给 `ISpecial` 添加 `deepEqualsTo` 接口用于特殊属性间的深度比较;
- `IEnemyManager` 继承 `ISaveableContent`,新增 `compareWith`、`modifyPrefabAttribute`、
`attachEnemyComparer`、`getEnemyComparer` 接口;
- `IEnemyManager` 内部维护 dirty 集合,以首次 `compareWith` 传入的参考为唯一基准;
- `getPrefab` / `getPrefabById` 返回值收窄为 `IReadonlyEnemy<TAttr>`
统一由 `modifyPrefabAttribute` 承担模板修改职责。
---
# 实现思路
## 1. 新增存档状态类型
`types.ts` 中新增如下类型,用于序列化怪物与管理器的状态:
```ts
/** 单个 IEnemy 的存档状态 */
interface IEnemySaveState<TAttr> {
readonly attrs: TAttr;
// 特殊属性按 code 映射,值为各 ISpecial.saveState() 的结果
readonly specials: ReadonlyMap<number, unknown>;
}
/** IEnemyManager 的存档状态,只保存与参考状态不同的模板 */
interface IEnemyManagerSaveState<TAttr> {
// code -> 变更后的 IEnemySaveState
readonly modified: ReadonlyMap<number, IEnemySaveState<TAttr>>;
}
```
## 2. 新增 `IEnemyComparer<TAttr>` 接口
由于管理器外部没有比较怪物属性的需求,将比较逻辑封装为独立的比较器,
附着在 `EnemyManager` 上。比较器接口如下:
```ts
interface IEnemyComparer<TAttr> {
compare(
enemyA: IReadonlyEnemy<TAttr>,
enemyB: IReadonlyEnemy<TAttr>
): boolean;
}
```
由用户在初始化时通过 `attachEnemyComparer` 提供。若未提供比较器,
在调用 `modifyPrefabAttribute``changePrefab` 时需发出警告,且视所有怪物均为脏。
## 3. `ISpecial<T>` 继承 `ISaveableContent<T>`
- `saveState` 返回 `structuredClone(this.value)`(即 `getValue()` 的深拷贝);
- `loadState` 调用 `setValue(state)`
- 新增 `deepEqualsTo(other: ISpecial<T>): boolean`:先对比 `code`
再对 `value` 进行深度比较。
各内置实现类的比较策略:
- `NonePropertySpecial`:只需比较 `code``value` 为 `void` 无需对比;
- `CommonSerializableSpecial``value` 为普通可序列化对象,
使用 `lodash-es``isEqual` 进行递归深度比较。
## 4. `IEnemy<TAttr>` 继承 `ISaveableContent<IEnemySaveState<TAttr>>`
- `saveState(compression)`:深拷贝 `attrs`,对每个 special 调用
`saveState(compression)` 收集到 `specials` Map返回 `IEnemySaveState<TAttr>`
- `loadState(state, compression)`:以 `state.attrs` 还原属性,
然后对已有的每个 special 按 code 查找存档中的对应条目并调用 `loadState`
若存档中出现当前怪物未注册的 special code发出 logger 警告并跳过。
## 5. `IEnemyManager<TAttr>` 接口修改
### 5a. 继承 `ISaveableContent<IEnemyManagerSaveState<TAttr>>`
- `saveState(compression)`:遍历 dirty 集合,对每个脏模板调用
`prefab.saveState(compression)`,汇总为 `IEnemyManagerSaveState<TAttr>` 并返回;
- `loadState(state, compression)`:遍历 `state.modified`
找到 code 对应的现有模板,调用 `prefab.loadState(enemyState, compression)` 还原;
若某 code 不在当前 prefab 表中,发出 logger 警告并跳过;
**不清空 dirty 集合**,始终以首次 `compareWith` 提供的参考为唯一基准;
`loadState` 结束后重新用比较器对每个已有脏模板进行比对,
刷新 dirty 集合(避免加载后实际已恢复初始值的模板仍停留在 dirty 中)。
### 5b. 新增 `compareWith`
```ts
compareWith(reference: ReadonlyMap<number, IReadonlyEnemy<TAttr>>): void;
```
- 由调用方在游戏初始化完成后提供参考快照,外部传入,管理器保存引用;
- **首次调用**:直接存储参考,清空 dirty 集合;
- **非首次调用**:通过 logger 发出警告,提示此操作风险高,
请作者确认操作意图,但仍然执行覆盖(直接替换参考,重置 dirty 集合)。
### 5c. `getPrefab` / `getPrefabById` 返回值改为 `IReadonlyEnemy<TAttr>`
原来返回 `IEnemy<TAttr>`,外部可以直接修改模板。
改为只读引用,外部不能直接修改,必须通过 `modifyPrefabAttribute` 完成。
### 5d. 新增 `modifyPrefabAttribute`
```ts
modifyPrefabAttribute(
code: number | string,
modify: (prefab: IEnemy<TAttr>) => IEnemy<TAttr>
): 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<TAttr>): void;
getEnemyComparer(): IEnemyComparer<TAttr> | 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<TAttr>` 类型:单个怪物的存档状态
- [ ] 新增 `IEnemyManagerSaveState<TAttr>` 类型:管理器的存档状态
- [ ] 新增 `IEnemyComparer<TAttr>` 接口:包含 `compare` 方法,由用户实现
- [ ] 修改 `ISpecial<T>`:继承 `ISaveableContent<T>`
新增 `deepEqualsTo(other: ISpecial<T>): boolean`
- [ ] 修改 `IEnemy<TAttr>`:继承 `ISaveableContent<IEnemySaveState<TAttr>>`
- [ ] 修改 `IEnemyManager<TAttr>`:继承 `ISaveableContent<IEnemyManagerSaveState<TAttr>>`
新增 `compareWith`、`modifyPrefabAttribute`、`attachEnemyComparer`、`getEnemyComparer`
修改 `getPrefab``getPrefabById` 返回类型为 `IReadonlyEnemy<TAttr>`
### `packages-user/data-base/src/enemy/enemy.ts``Enemy` 类)
- [ ] 实现 `saveState(compression): IEnemySaveState<TAttr>`
- [ ] 实现 `loadState(state, compression): void`
### `packages-user/data-base/src/enemy/manager.ts``EnemyManager` 类)
- [ ] 新增 `private readonly dirtySet: Set<number>` 成员:记录脏模板的 code
- [ ] 新增 `private referenceByCode: Map<number, IReadonlyEnemy<TAttr>>` 成员:
保存参考快照
- [ ] 新增 `private comparer: IEnemyComparer<TAttr> | 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`

View File

@ -83,28 +83,28 @@ iterateModifiers(): Iterable<[keyof THero, IHeroModifier]>;
### `@user/data-base/hero/types.ts` ### `@user/data-base/hero/types.ts`
- [ ] 修改 `IHeroModifier<T, V>` 接口: - [x] 修改 `IHeroModifier<T, V>` 接口:
改为 `IHeroModifier<T, V, S = unknown>``identifier` 改名为 `type` 改为 `IHeroModifier<T, V, S = unknown>``identifier` 改名为 `type`
继承 `ISaveableContent<S>` 继承 `ISaveableContent<S>`
- [ ] 新增 `IModifierStateSave` 接口:单条修饰器的存档格式 - [x] 新增 `IModifierStateSave` 接口:单条修饰器的存档格式
- [ ] 修改 `IHeroStateSave<THero>`:新增 `readonly modifiers: readonly IModifierStateSave[]` 字段 - [x] 修改 `IHeroStateSave<THero>`:新增 `readonly modifiers: readonly IModifierStateSave[]` 字段
- [ ] 修改 `IReadonlyHeroAttribute<THero>`:新增 `iterateModifiers()` 方法签名 - [x] 修改 `IReadonlyHeroAttribute<THero>`:新增 `iterateModifiers()` 方法签名
- [ ] 修改 `IHeroState<THero>`:新增以下方法签名 - `registerModifier(type: string, cons: () => IHeroModifier): void` - `createModifier<V>(type: string): IHeroModifier<unknown, V>` - `createAndInsertModifier<K extends keyof THero, V>(type: string, name: K): IHeroModifier<unknown, V>` - [x] 修改 `IHeroState<THero>`:新增以下方法签名 - `registerModifier(type: string, cons: () => IHeroModifier): void` - `createModifier<V>(type: string): IHeroModifier<unknown, V>` - `createAndInsertModifier<K extends keyof THero, V>(type: string, name: K): IHeroModifier<unknown, V>`
### `@user/data-base/hero/attribute.ts` ### `@user/data-base/hero/attribute.ts`
- [ ] 修改 `BaseHeroModifier<T, V>` - [x] 修改 `BaseHeroModifier<T, V>`
`abstract readonly identifier` 改为 `abstract readonly type` `abstract readonly identifier` 改为 `abstract readonly type`
实现 `saveState` / `loadState` 实现 `saveState` / `loadState`
- [ ] 修改 `HeroAttribute<THero>`:实现 `iterateModifiers()` - [x] 修改 `HeroAttribute<THero>`:实现 `iterateModifiers()`
### `@user/data-base/hero/state.ts` ### `@user/data-base/hero/state.ts`
- [ ] 修改 `HeroState<THero>`:新增 `private readonly registry: Map<string, () => IHeroModifier>` 成员 - [x] 修改 `HeroState<THero>`:新增 `private readonly registry: Map<string, () => IHeroModifier>` 成员
- [ ] 实现 `HeroState.registerModifier`:将工厂函数写入 `registry` - [x] 实现 `HeroState.registerModifier`:将工厂函数写入 `registry`
- [ ] 实现 `HeroState.createModifier`:从 `registry` 取出工厂并调用,返回新实例; - [x] 实现 `HeroState.createModifier`:从 `registry` 取出工厂并调用,返回新实例;
`type` 未注册则抛出错误 `type` 未注册则抛出错误
- [ ] 实现 `HeroState.createAndInsertModifier`:调用 `createModifier` 后, - [x] 实现 `HeroState.createAndInsertModifier`:调用 `createModifier` 后,
再调用 `this.attribute.addModifier(name, modifier)`,返回同一实例 再调用 `this.attribute.addModifier(name, modifier)`,返回同一实例
- [ ] 修改 `HeroState.saveState`:遍历 `iterateModifiers()` 写入 `modifiers` 字段 - [x] 修改 `HeroState.saveState`:遍历 `iterateModifiers()` 写入 `modifiers` 字段
- [ ] 修改 `HeroState.loadState`:遍历 `state.modifiers` 重建修饰器并挂载 - [x] 修改 `HeroState.loadState`:遍历 `state.modifiers` 重建修饰器并挂载

View File

@ -2,10 +2,12 @@ import { logger } from '@motajs/common';
import { import {
IEnemy, IEnemy,
IEnemyContext, IEnemyContext,
IEnemySaveState,
IReadonlyEnemy, IReadonlyEnemy,
ISpecial, ISpecial,
IEnemyView IEnemyView
} from './types'; } from './types';
import { SaveCompression } from '../common/types';
export class Enemy<TAttr> implements IEnemy<TAttr> { export class Enemy<TAttr> implements IEnemy<TAttr> {
/** 怪物身上的特殊属性列表 */ /** 怪物身上的特殊属性列表 */
@ -87,6 +89,29 @@ export class Enemy<TAttr> implements IEnemy<TAttr> {
this.addSpecial(special.clone()); this.addSpecial(special.clone());
} }
} }
saveState(_compression: SaveCompression): IEnemySaveState<TAttr> {
const specials: Map<number, unknown> = new Map();
for (const special of this.specials) {
specials.set(special.code, special.saveState(_compression));
}
return { attrs: structuredClone(this.attributes), specials };
}
loadState(
state: IEnemySaveState<TAttr>,
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<TAttr> implements IEnemyView<TAttr> { export class EnemyView<TAttr> implements IEnemyView<TAttr> {

View File

@ -2,10 +2,15 @@ import { logger } from '@motajs/common';
import { Enemy as EnemyImpl } from './enemy'; import { Enemy as EnemyImpl } from './enemy';
import { import {
IEnemy, IEnemy,
IEnemyComparer,
IEnemyManager, IEnemyManager,
IEnemyManagerSaveState,
IEnemyLegacyBridge, IEnemyLegacyBridge,
SpecialCreation IReadonlyEnemy,
SpecialCreation,
IEnemySaveState
} from './types'; } from './types';
import { SaveCompression } from '../common/types';
export class EnemyManager<TAttr> implements IEnemyManager<TAttr> { export class EnemyManager<TAttr> implements IEnemyManager<TAttr> {
/** 特殊属性注册表code -> 创建函数 */ /** 特殊属性注册表code -> 创建函数 */
@ -19,6 +24,14 @@ export class EnemyManager<TAttr> implements IEnemyManager<TAttr> {
private readonly prefabById: Map<string, IEnemy<TAttr>> = new Map(); private readonly prefabById: Map<string, IEnemy<TAttr>> = new Map();
/** 旧样板怪物 id 到 code 的映射,用于 fromLegacyEnemy 快速查找已有模板 */ /** 旧样板怪物 id 到 code 的映射,用于 fromLegacyEnemy 快速查找已有模板 */
private readonly legacyIdToCode: Map<string, number> = new Map(); private readonly legacyIdToCode: Map<string, number> = new Map();
/** 脏模板集合,存储发生了变化的模板 code */
private readonly dirtySet: Set<number> = new Set();
/** 参考快照code -> IReadonlyEnemy由 compareWith 提供 */
private referenceByCode: Map<number, IReadonlyEnemy<TAttr>> = new Map();
/** 当前附加的怪物比较器 */
private comparer: IEnemyComparer<TAttr> | null = null;
/** 是否已首次调用 compareWith */
private hasReference: boolean = false;
constructor(readonly bridge: IEnemyLegacyBridge<TAttr>) {} constructor(readonly bridge: IEnemyLegacyBridge<TAttr>) {}
@ -127,6 +140,7 @@ export class EnemyManager<TAttr> implements IEnemyManager<TAttr> {
const cloned = enemy.clone(); const cloned = enemy.clone();
this.prefabByCode.set(enemy.code, cloned); this.prefabByCode.set(enemy.code, cloned);
this.prefabById.set(enemy.id, cloned); this.prefabById.set(enemy.id, cloned);
this.updateDirty(cloned.code, cloned);
} }
addPrefabFromLegacy(code: number, enemy: Enemy): void { addPrefabFromLegacy(code: number, enemy: Enemy): void {
@ -137,13 +151,14 @@ export class EnemyManager<TAttr> implements IEnemyManager<TAttr> {
this.prefabByCode.set(code, prefab); this.prefabByCode.set(code, prefab);
this.prefabById.set(prefab.id, prefab); this.prefabById.set(prefab.id, prefab);
this.legacyIdToCode.set(enemy.id, code); this.legacyIdToCode.set(enemy.id, code);
this.updateDirty(code, prefab);
} }
getPrefab(code: number): IEnemy<TAttr> | null { getPrefab(code: number): IReadonlyEnemy<TAttr> | null {
return this.prefabByCode.get(code) ?? null; return this.prefabByCode.get(code) ?? null;
} }
getPrefabById(id: string): IEnemy<TAttr> | null { getPrefabById(id: string): IReadonlyEnemy<TAttr> | null {
return this.prefabById.get(id) ?? null; return this.prefabById.get(id) ?? null;
} }
@ -160,6 +175,7 @@ export class EnemyManager<TAttr> implements IEnemyManager<TAttr> {
// 再添加新的模板 // 再添加新的模板
this.prefabByCode.set(enemy.code, enemy); this.prefabByCode.set(enemy.code, enemy);
this.prefabById.set(enemy.id, enemy); this.prefabById.set(enemy.id, enemy);
this.updateDirty(enemy.code, enemy);
} }
reusePrefab(source: number | string, code: number, id: string): void { reusePrefab(source: number | string, code: number, id: string): void {
@ -168,4 +184,117 @@ export class EnemyManager<TAttr> implements IEnemyManager<TAttr> {
this.prefabByCode.set(code, prefab); this.prefabByCode.set(code, prefab);
this.prefabById.set(id, prefab); this.prefabById.set(id, prefab);
} }
compareWith(reference: ReadonlyMap<number, IReadonlyEnemy<TAttr>>): 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<TAttr>) => IEnemy<TAttr>
): 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<TAttr>): void {
this.comparer = comparer;
}
getEnemyComparer(): IEnemyComparer<TAttr> | null {
return this.comparer;
}
saveState(compression: SaveCompression): IEnemyManagerSaveState<TAttr> {
const modified: Map<number, IEnemySaveState<TAttr>> = 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<TAttr>,
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<TAttr>): 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<number>): 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);
}
}
}
} }

View File

@ -1,3 +1,5 @@
import { isEqual } from 'lodash-es';
import { SaveCompression } from '../common/types';
import { ISpecial, SpecialCreation } from './types'; import { ISpecial, SpecialCreation } from './types';
// TODO: 颜色参数 // TODO: 颜色参数
@ -45,6 +47,19 @@ export class CommonSerializableSpecial<T> implements ISpecial<T> {
this.config this.config
); );
} }
saveState(_compression: SaveCompression): T {
return structuredClone(this.value);
}
loadState(state: T, _compression: SaveCompression): void {
this.setValue(state);
}
deepEqualsTo(other: ISpecial<T>): boolean {
if (this.code !== other.code) return false;
return isEqual(this.value, other.getValue());
}
} }
export class NonePropertySpecial implements ISpecial<void> { export class NonePropertySpecial implements ISpecial<void> {
@ -78,6 +93,18 @@ export class NonePropertySpecial implements ISpecial<void> {
clone(): ISpecial<void> { clone(): ISpecial<void> {
return new NonePropertySpecial(this.code, this.config); return new NonePropertySpecial(this.code, this.config);
} }
saveState(_compression: SaveCompression): void {
return undefined;
}
loadState(_state: void, _compression: SaveCompression): void {
// 无属性,无需操作
}
deepEqualsTo(other: ISpecial<void>): boolean {
return this.code === other.code;
}
} }
export function defineCommonSerializableSpecial<T, TAttr = any>( export function defineCommonSerializableSpecial<T, TAttr = any>(

View File

@ -1,9 +1,36 @@
import { IRange, ITileLocator } from '@motajs/common'; import { IRange, ITileLocator } from '@motajs/common';
import { IHeroAttribute, IReadonlyHeroAttribute } from '../hero'; import { IHeroAttribute, IReadonlyHeroAttribute } from '../hero';
import { ISaveableContent } from '../common/types';
//#region 怪物基础 //#region 怪物基础
export interface ISpecial<T = void> { /** 单个 IEnemy 的存档状态 */
export interface IEnemySaveState<TAttr> {
/** 怪物属性的深拷贝 */
readonly attrs: TAttr;
/** 特殊属性按 code 映射,值为各 ISpecial.saveState() 的结果 */
readonly specials: ReadonlyMap<number, unknown>;
}
/** IEnemyManager 的存档状态,只保存与参考状态不同的模板 */
export interface IEnemyManagerSaveState<TAttr> {
/** code -> 变更后的 IEnemySaveState仅包含脏模板 */
readonly modified: ReadonlyMap<number, IEnemySaveState<TAttr>>;
}
export interface IEnemyComparer<TAttr> {
/**
*
* @param enemyA A
* @param enemyB B
*/
compare(
enemyA: IReadonlyEnemy<TAttr>,
enemyB: IReadonlyEnemy<TAttr>
): boolean;
}
export interface ISpecial<T = void> extends ISaveableContent<T> {
/** 特殊属性代码 */ /** 特殊属性代码 */
readonly code: number; readonly code: number;
/** 特殊属性需要的数值 */ /** 特殊属性需要的数值 */
@ -40,6 +67,12 @@ export interface ISpecial<T = void> {
* *
*/ */
clone(): ISpecial<T>; clone(): ISpecial<T>;
/**
*
* @param other
*/
deepEqualsTo(other: ISpecial<T>): boolean;
} }
export interface IReadonlyEnemy<TAttr> { export interface IReadonlyEnemy<TAttr> {
@ -82,7 +115,8 @@ export interface IReadonlyEnemy<TAttr> {
clone(): IReadonlyEnemy<TAttr>; clone(): IReadonlyEnemy<TAttr>;
} }
export interface IEnemy<TAttr> extends IReadonlyEnemy<TAttr> { export interface IEnemy<TAttr>
extends IReadonlyEnemy<TAttr>, ISaveableContent<IEnemySaveState<TAttr>> {
/** /**
* *
* @param special * @param special
@ -138,7 +172,9 @@ export interface IEnemyLegacyBridge<TAttr> {
fromLegacyEnemy(enemy: Enemy, defaultValue: Partial<TAttr>): TAttr; fromLegacyEnemy(enemy: Enemy, defaultValue: Partial<TAttr>): TAttr;
} }
export interface IEnemyManager<TAttr> { export interface IEnemyManager<TAttr> extends ISaveableContent<
IEnemyManagerSaveState<TAttr>
> {
/** /**
* *
* @param code * @param code
@ -193,13 +229,13 @@ export interface IEnemyManager<TAttr> {
* *
* @param code * @param code
*/ */
getPrefab(code: number): IEnemy<TAttr> | null; getPrefab(code: number): IReadonlyEnemy<TAttr> | null;
/** /**
* `id` * `id`
* @param id `id` * @param id `id`
*/ */
getPrefabById(id: string): IEnemy<TAttr> | null; getPrefabById(id: string): IReadonlyEnemy<TAttr> | null;
/** /**
* *
@ -221,6 +257,34 @@ export interface IEnemyManager<TAttr> {
* @param id id * @param id id
*/ */
reusePrefab(source: number | string, code: number, id: string): void; reusePrefab(source: number | string, code: number, id: string): void;
/**
*
*
* @param reference code -> Map
*/
compareWith(reference: ReadonlyMap<number, IReadonlyEnemy<TAttr>>): void;
/**
* dirty
* @param code `id`
* @param modify
*/
modifyPrefabAttribute(
code: number | string,
modify: (prefab: IEnemy<TAttr>) => IEnemy<TAttr>
): void;
/**
* dirty
* @param comparer
*/
attachEnemyComparer(comparer: IEnemyComparer<TAttr>): void;
/**
* `null`
*/
getEnemyComparer(): IEnemyComparer<TAttr> | null;
} }
//#endregion //#endregion

View File

@ -173,6 +173,10 @@
"114": "Save operation took $1ms, exceeding the tolerance of $2ms. Consider reducing compression level.", "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.", "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.", "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." "1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency."
} }
} }