refactor: 怪物管理器存档

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

No files matched your search

@@ -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<TAttr> implements IEnemy<TAttr> {
/** 怪物身上的特殊属性列表 */
@@ -87,6 +89,29 @@ export class Enemy<TAttr> implements IEnemy<TAttr> {
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> {
+132 -3
View File
@@ -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<TAttr> implements IEnemyManager<TAttr> {
/** 特殊属性注册表,code -> 创建函数 */
@@ -19,6 +24,14 @@ export class EnemyManager<TAttr> implements IEnemyManager<TAttr> {
private readonly prefabById: Map<string, IEnemy<TAttr>> = new Map();
/** 旧样板怪物 id 到 code 的映射,用于 fromLegacyEnemy 快速查找已有模板 */
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>) {}
@@ -127,6 +140,7 @@ export class EnemyManager<TAttr> implements IEnemyManager<TAttr> {
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<TAttr> implements IEnemyManager<TAttr> {
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<TAttr> | null {
getPrefab(code: number): IReadonlyEnemy<TAttr> | 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;
}
@@ -160,6 +175,7 @@ export class EnemyManager<TAttr> implements IEnemyManager<TAttr> {
// 再添加新的模板
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<TAttr> implements IEnemyManager<TAttr> {
this.prefabByCode.set(code, 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);
}
}
}
}
@@ -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<T> implements ISpecial<T> {
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> {
@@ -78,6 +93,18 @@ export class NonePropertySpecial implements ISpecial<void> {
clone(): ISpecial<void> {
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>(
+69 -5
View File
@@ -1,9 +1,36 @@
import { IRange, ITileLocator } from '@motajs/common';
import { IHeroAttribute, IReadonlyHeroAttribute } from '../hero';
import { ISaveableContent } from '../common/types';
//#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;
/** 特殊属性需要的数值 */
@@ -40,6 +67,12 @@ export interface ISpecial<T = void> {
* 深拷贝此特殊属性
*/
clone(): ISpecial<T>;
/**
* 深度比较此特殊属性与另一特殊属性是否相同
* @param other 另一特殊属性
*/
deepEqualsTo(other: ISpecial<T>): boolean;
}
export interface IReadonlyEnemy<TAttr> {
@@ -82,7 +115,8 @@ export interface IReadonlyEnemy<TAttr> {
clone(): IReadonlyEnemy<TAttr>;
}
export interface IEnemy<TAttr> extends IReadonlyEnemy<TAttr> {
export interface IEnemy<TAttr>
extends IReadonlyEnemy<TAttr>, ISaveableContent<IEnemySaveState<TAttr>> {
/**
* 添加特殊属性
* @param special 特殊属性对象
@@ -138,7 +172,9 @@ export interface IEnemyLegacyBridge<TAttr> {
fromLegacyEnemy(enemy: Enemy, defaultValue: Partial<TAttr>): TAttr;
}
export interface IEnemyManager<TAttr> {
export interface IEnemyManager<TAttr> extends ISaveableContent<
IEnemyManagerSaveState<TAttr>
> {
/**
* 注册一个特殊属性
* @param code 特殊属性代码
@@ -193,13 +229,13 @@ export interface IEnemyManager<TAttr> {
* 获取指定怪物的模板
* @param code 怪物图块数字
*/
getPrefab(code: number): IEnemy<TAttr> | null;
getPrefab(code: number): IReadonlyEnemy<TAttr> | null;
/**
* 根据怪物的 `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
*/
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