mirror of
https://github.com/motajs/template.git
synced 2026-11-03 15:34:31 +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> {
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user