mirror of
https://github.com/motajs/template.git
synced 2026-10-19 21:30:30 +08:00
refactor: Hero equipment system
This commit is contained in:
parent
e78309b326
commit
7743795948
@ -1,53 +1,14 @@
|
||||
import { logger } from '@motajs/common';
|
||||
import { SaveCompression } from '@user/data-common';
|
||||
import { SaveCompression, shouldReplay } from '@user/data-common';
|
||||
import {
|
||||
IHeroAttribute,
|
||||
IHeroAttributeCloneOption,
|
||||
IHeroAttributeSave,
|
||||
IHeroModifier,
|
||||
IHeroModifierOwner,
|
||||
IModifierStateSave
|
||||
} from './types';
|
||||
import { isNil } from 'lodash-es';
|
||||
|
||||
export abstract class BaseHeroModifier<T, V> implements IHeroModifier<T, V, V> {
|
||||
abstract readonly type: string;
|
||||
abstract readonly priority: number;
|
||||
|
||||
owner: IHeroModifierOwner | null = null;
|
||||
|
||||
constructor(private currentValue: V) {}
|
||||
|
||||
get value(): V {
|
||||
return this.currentValue;
|
||||
}
|
||||
|
||||
setValue(value: V): void {
|
||||
this.currentValue = value;
|
||||
this.owner?.markModifierDirty(this);
|
||||
}
|
||||
|
||||
getValue(): V {
|
||||
return this.currentValue;
|
||||
}
|
||||
|
||||
bindAttribute(attribute: IHeroModifierOwner | null): void {
|
||||
this.owner = attribute;
|
||||
}
|
||||
|
||||
saveState(_compression: SaveCompression): V {
|
||||
return this.currentValue;
|
||||
}
|
||||
|
||||
loadState(state: V, _compression: SaveCompression): void {
|
||||
this.setValue(state);
|
||||
}
|
||||
|
||||
abstract modify(value: T, baseValue: T, name: string): T;
|
||||
|
||||
abstract clone(): IHeroModifier<T, V>;
|
||||
}
|
||||
|
||||
export class HeroAttribute<THero> implements IHeroAttribute<THero> {
|
||||
/** 当前勇士属性修饰器 */
|
||||
private readonly modifier: Map<keyof THero, IHeroModifier[]> = new Map();
|
||||
@ -142,21 +103,25 @@ export class HeroAttribute<THero> implements IHeroAttribute<THero> {
|
||||
|
||||
//#region 属性操作
|
||||
|
||||
@shouldReplay('Hero attribute change should be replayed.')
|
||||
set<K extends keyof THero>(name: K, value: THero[K]): void {
|
||||
this.attribute[name] = value;
|
||||
this.markDirty(name);
|
||||
}
|
||||
|
||||
@shouldReplay('Hero attribute change should be replayed.')
|
||||
add(name: SelectKey<THero, number>, value: number): void {
|
||||
(this.attribute[name] as number) += value;
|
||||
this.markDirty(name);
|
||||
}
|
||||
|
||||
@shouldReplay('Hero attribute change should be replayed.')
|
||||
mul(name: SelectKey<THero, number>, value: number): void {
|
||||
(this.attribute[name] as number) *= value;
|
||||
this.markDirty(name);
|
||||
}
|
||||
|
||||
@shouldReplay('Hero attribute change should be replayed.')
|
||||
div(name: SelectKey<THero, number>, value: number): void {
|
||||
(this.attribute[name] as number) /= value;
|
||||
this.markDirty(name);
|
||||
@ -187,6 +152,7 @@ export class HeroAttribute<THero> implements IHeroAttribute<THero> {
|
||||
return arr.indexOf(modifier);
|
||||
}
|
||||
|
||||
@shouldReplay('Changing hero attrubute modifier should be replayed.')
|
||||
addModifier<K extends keyof THero>(
|
||||
name: K,
|
||||
modifier: IHeroModifier<THero[K]>,
|
||||
@ -210,6 +176,7 @@ export class HeroAttribute<THero> implements IHeroAttribute<THero> {
|
||||
this.markDirty(name);
|
||||
}
|
||||
|
||||
@shouldReplay('Changing hero attrubute modifier should be replayed.')
|
||||
deleteModifier<K extends keyof THero>(
|
||||
name: K,
|
||||
modifier: IHeroModifier<THero[K], unknown>
|
||||
@ -227,6 +194,7 @@ export class HeroAttribute<THero> implements IHeroAttribute<THero> {
|
||||
this.markDirty(name);
|
||||
}
|
||||
|
||||
@shouldReplay('Changing hero attrubute modifier should be replayed.')
|
||||
deleteModifierByIndex<K extends keyof THero>(
|
||||
name: K,
|
||||
index: number
|
||||
|
||||
@ -1,4 +1,9 @@
|
||||
import { IItemRawData, SaveCompression, IDataCommon } from '@user/data-common';
|
||||
import {
|
||||
IItemRawData,
|
||||
SaveCompression,
|
||||
IDataCommon,
|
||||
shouldReplay
|
||||
} from '@user/data-common';
|
||||
import { isNil, maxBy } from 'lodash-es';
|
||||
import { ValueModifier, PercentageModifier } from './modifier';
|
||||
import {
|
||||
@ -8,63 +13,131 @@ import {
|
||||
IHeroEquipsStore,
|
||||
IEquipmentSorter,
|
||||
IEquipmentSortHandler,
|
||||
IHeroEquipsStoreSave
|
||||
IHeroEquipsStoreSave,
|
||||
IEquipmentStateHooks
|
||||
} from './types';
|
||||
import { logger } from '@motajs/common';
|
||||
import {
|
||||
Hookable,
|
||||
HookController,
|
||||
IHookController,
|
||||
logger
|
||||
} from '@motajs/common';
|
||||
|
||||
export class EquipmentState<THero> implements IEquipmentState<THero> {
|
||||
type Modifiers<THero> = Map<SelectKey<THero, number>, IHeroModifier<number>>;
|
||||
|
||||
export class EquipmentState<THero>
|
||||
extends Hookable<IEquipmentStateHooks<THero>>
|
||||
implements IEquipmentState<THero>
|
||||
{
|
||||
readonly item: IItemRawData<THero>;
|
||||
|
||||
/** 装备产生的所有修饰器,每个元素为 [属性名, 修饰器] */
|
||||
readonly modifiers: [SelectKey<THero, number>, IHeroModifier<number>][];
|
||||
/** 数值修饰器列表 */
|
||||
readonly valueModifiers: Modifiers<THero> = new Map();
|
||||
/** 百分比修饰器列表 */
|
||||
readonly perModifiers: Modifiers<THero> = new Map();
|
||||
|
||||
/** 装备的数值属性 */
|
||||
private readonly value: Map<SelectKey<THero, number>, number>;
|
||||
private readonly value: Map<SelectKey<THero, number>, number> = new Map();
|
||||
/** 装备的百分比属性 */
|
||||
private readonly percentage: Map<SelectKey<THero, number>, number>;
|
||||
private readonly per: Map<SelectKey<THero, number>, number> = new Map();
|
||||
|
||||
/** 是否已经构建过修饰器 */
|
||||
private built: boolean = false;
|
||||
|
||||
constructor(
|
||||
readonly uid: number,
|
||||
item: IItemRawData<THero>,
|
||||
buildModifier: boolean = true
|
||||
item: IItemRawData<THero>
|
||||
) {
|
||||
super();
|
||||
this.item = item;
|
||||
this.modifiers = [];
|
||||
|
||||
const equip = item.equip;
|
||||
this.value = new Map(
|
||||
Object.entries(equip.value) as [SelectKey<THero, number>, number][]
|
||||
);
|
||||
this.percentage = new Map(
|
||||
Object.entries(equip.percentage) as [
|
||||
SelectKey<THero, number>,
|
||||
number
|
||||
][]
|
||||
);
|
||||
|
||||
if (buildModifier) {
|
||||
this.rebuildModifiers();
|
||||
for (const [key, value] of Object.entries(equip.value)) {
|
||||
this.value.set(key as SelectKey<THero, number>, value as number);
|
||||
}
|
||||
for (const [key, value] of Object.entries(equip.percentage)) {
|
||||
this.per.set(key as SelectKey<THero, number>, value as number);
|
||||
}
|
||||
}
|
||||
|
||||
protected createController(
|
||||
hook: Partial<IEquipmentStateHooks<THero>>
|
||||
): IHookController<IEquipmentStateHooks<THero>> {
|
||||
return new HookController(this, hook);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从装备属性重新生成装备修饰器
|
||||
* 触发修饰器变动钩子
|
||||
*/
|
||||
private rebuildModifiers() {
|
||||
this.modifiers.length = 0;
|
||||
|
||||
for (const [name, baseVal] of this.value) {
|
||||
this.modifiers.push([name, new ValueModifier(baseVal)]);
|
||||
}
|
||||
for (const [name, basePct] of this.percentage) {
|
||||
this.modifiers.push([name, new PercentageModifier(basePct)]);
|
||||
}
|
||||
private notifyModifierChange() {
|
||||
this.forEachHook(hook => hook.onChangeModifier?.(this.getModifiers()));
|
||||
}
|
||||
|
||||
getModifiers(): Iterable<
|
||||
[keyof SelectType<THero, number>, IHeroModifier<number>]
|
||||
@shouldReplay('Setting equipment value should be replayed.')
|
||||
setValue(name: keyof SelectType<THero, number>, value: number): void {
|
||||
if (value === 0) {
|
||||
this.value.delete(name);
|
||||
this.valueModifiers.delete(name);
|
||||
} else {
|
||||
this.value.set(name, value);
|
||||
const modifier = this.valueModifiers.getOrInsertComputed(
|
||||
name,
|
||||
() => new ValueModifier(value, 0)
|
||||
);
|
||||
modifier.setValue(value);
|
||||
}
|
||||
this.notifyModifierChange();
|
||||
}
|
||||
|
||||
@shouldReplay('Setting equipment percentage value should be replayed.')
|
||||
setPercentage(name: keyof SelectType<THero, number>, value: number): void {
|
||||
if (value === 0) {
|
||||
this.per.delete(name);
|
||||
this.perModifiers.delete(name);
|
||||
} else {
|
||||
this.per.set(name, value);
|
||||
const modifier = this.perModifiers.getOrInsertComputed(
|
||||
name,
|
||||
() => new PercentageModifier(value, 10)
|
||||
);
|
||||
modifier.setValue(value);
|
||||
}
|
||||
this.notifyModifierChange();
|
||||
}
|
||||
|
||||
getValue(name: keyof SelectType<THero, number>): number {
|
||||
return this.value.get(name) ?? 0;
|
||||
}
|
||||
|
||||
getPercentage(name: keyof SelectType<THero, number>): number {
|
||||
return this.per.get(name) ?? 0;
|
||||
}
|
||||
|
||||
buildModifiers() {
|
||||
if (this.built) return;
|
||||
this.built = true;
|
||||
|
||||
this.valueModifiers.clear();
|
||||
this.perModifiers.clear();
|
||||
|
||||
for (const [name, baseVal] of this.value) {
|
||||
this.valueModifiers.set(name, new ValueModifier(baseVal, 0));
|
||||
}
|
||||
for (const [name, basePct] of this.per) {
|
||||
this.perModifiers.set(name, new PercentageModifier(basePct, 10));
|
||||
}
|
||||
|
||||
this.notifyModifierChange();
|
||||
}
|
||||
|
||||
*getModifiers(): Iterable<
|
||||
[SelectKey<THero, number>, IHeroModifier<number>]
|
||||
> {
|
||||
return this.modifiers;
|
||||
if (!this.built) {
|
||||
this.buildModifiers();
|
||||
}
|
||||
yield* this.valueModifiers;
|
||||
yield* this.perModifiers;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -76,7 +149,7 @@ export class EquipmentState<THero> implements IEquipmentState<THero> {
|
||||
uid: this.uid,
|
||||
num: this.item.num,
|
||||
value: new Map(this.value),
|
||||
percentage: new Map(this.percentage)
|
||||
percentage: new Map(this.per)
|
||||
};
|
||||
}
|
||||
|
||||
@ -94,7 +167,7 @@ export class EquipmentState<THero> implements IEquipmentState<THero> {
|
||||
}
|
||||
}
|
||||
const perDiff = new Map<SelectKey<THero, number>, number>();
|
||||
for (const [name, equipPer] of this.percentage) {
|
||||
for (const [name, equipPer] of this.per) {
|
||||
const base = percentage[name];
|
||||
if (base !== equipPer) {
|
||||
perDiff.set(name, equipPer);
|
||||
@ -122,14 +195,14 @@ export class EquipmentState<THero> implements IEquipmentState<THero> {
|
||||
*/
|
||||
private loadNoCompression(state: IEquipmentStateSave<THero>): void {
|
||||
this.value.clear();
|
||||
this.percentage.clear();
|
||||
this.per.clear();
|
||||
for (const [name, value] of state.value) {
|
||||
this.value.set(name, value);
|
||||
}
|
||||
for (const [name, value] of state.percentage) {
|
||||
this.percentage.set(name, value);
|
||||
this.per.set(name, value);
|
||||
}
|
||||
this.rebuildModifiers();
|
||||
this.buildModifiers();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -138,7 +211,7 @@ export class EquipmentState<THero> implements IEquipmentState<THero> {
|
||||
*/
|
||||
private loadDiff(state: IEquipmentStateSave<THero>): void {
|
||||
this.value.clear();
|
||||
this.percentage.clear();
|
||||
this.per.clear();
|
||||
|
||||
// 基准为装备原始定义,再叠加存档中的差异条目
|
||||
for (const [name, value] of Object.entries<number>(
|
||||
@ -149,7 +222,7 @@ export class EquipmentState<THero> implements IEquipmentState<THero> {
|
||||
for (const [name, value] of Object.entries<number>(
|
||||
this.item.equip.percentage
|
||||
)) {
|
||||
this.percentage.set(name as SelectKey<THero, number>, value);
|
||||
this.per.set(name as SelectKey<THero, number>, value);
|
||||
}
|
||||
|
||||
// 差异内容
|
||||
@ -157,10 +230,10 @@ export class EquipmentState<THero> implements IEquipmentState<THero> {
|
||||
this.value.set(name, value);
|
||||
}
|
||||
for (const [name, value] of state.percentage) {
|
||||
this.percentage.set(name, value);
|
||||
this.per.set(name, value);
|
||||
}
|
||||
|
||||
this.rebuildModifiers();
|
||||
this.buildModifiers();
|
||||
}
|
||||
|
||||
loadState(
|
||||
@ -192,6 +265,7 @@ export class HeroEquipsStore<THero> implements IHeroEquipsStore<THero> {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
@shouldReplay('Adding equipment to equip store should be replayed.')
|
||||
add(item: number | string): number {
|
||||
const num = this.state.tileStore.num(item);
|
||||
if (isNil(num)) return -1;
|
||||
@ -202,9 +276,11 @@ export class HeroEquipsStore<THero> implements IHeroEquipsStore<THero> {
|
||||
const uid = this.nextUid++;
|
||||
const state = new EquipmentState<THero>(uid, raw);
|
||||
this.instanceMap.set(uid, state);
|
||||
state.buildModifiers();
|
||||
return uid;
|
||||
}
|
||||
|
||||
@shouldReplay('Deleting equipment from equip store should be replayed.')
|
||||
delete(uid: number): void {
|
||||
this.instanceMap.delete(uid);
|
||||
}
|
||||
@ -279,34 +355,16 @@ export class HeroEquipsStore<THero> implements IHeroEquipsStore<THero> {
|
||||
state: IHeroEquipsStoreSave<THero>,
|
||||
compression: SaveCompression
|
||||
): void {
|
||||
// 按 uid 复用现有实例原地读档,使外部持有的引用跨读档仍然有效;
|
||||
// 仅当 uid 相同但装备图块数字不同时才替换实例
|
||||
const savedUids = new Set<number>();
|
||||
this.instanceMap.clear();
|
||||
for (const save of state.equipments) {
|
||||
const raw = this.state.itemStore.getData(save.num);
|
||||
if (!raw) {
|
||||
logger.error(59, save.num.toString());
|
||||
continue;
|
||||
}
|
||||
savedUids.add(save.uid);
|
||||
const existing = this.instanceMap.get(save.uid);
|
||||
if (existing && existing.item.num === save.num) {
|
||||
existing.loadState(save, compression);
|
||||
} else {
|
||||
const instance = new EquipmentState<THero>(
|
||||
save.uid,
|
||||
raw,
|
||||
false
|
||||
);
|
||||
instance.loadState(save, compression);
|
||||
this.instanceMap.set(save.uid, instance);
|
||||
}
|
||||
}
|
||||
// 以存档为准:存档中不存在的装备实例一律删除
|
||||
for (const uid of this.instanceMap.keys()) {
|
||||
if (!savedUids.has(uid)) {
|
||||
this.instanceMap.delete(uid);
|
||||
}
|
||||
const ins = new EquipmentState<THero>(save.uid, raw);
|
||||
ins.loadState(save, compression);
|
||||
this.instanceMap.set(save.uid, ins);
|
||||
}
|
||||
if (state.equipments.length === 0) {
|
||||
this.nextUid = 0;
|
||||
@ -314,9 +372,10 @@ export class HeroEquipsStore<THero> implements IHeroEquipsStore<THero> {
|
||||
const maxUid = maxBy(state.equipments, 'uid');
|
||||
if (!maxUid) {
|
||||
logger.error(58);
|
||||
return;
|
||||
this.nextUid = 0;
|
||||
} else {
|
||||
this.nextUid = maxUid.uid + 1;
|
||||
}
|
||||
this.nextUid = maxUid.uid + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { isNil } from 'lodash-es';
|
||||
import { IDataCommon, ReplayCode } from '@user/data-common';
|
||||
import { IDataCommon, ReplayCode, shouldReplay } from '@user/data-common';
|
||||
import {
|
||||
EquipStatus,
|
||||
IEquipmentState,
|
||||
@ -150,6 +150,7 @@ export class HeroEquipment<THero> implements IHeroEquipment<THero> {
|
||||
}
|
||||
}
|
||||
|
||||
@shouldReplay('Equiping equipment should be replayed.')
|
||||
equip(
|
||||
uid: number,
|
||||
slot: number | string,
|
||||
@ -197,6 +198,7 @@ export class HeroEquipment<THero> implements IHeroEquipment<THero> {
|
||||
return curr;
|
||||
}
|
||||
|
||||
@shouldReplay('Unequiping equipment should be replayed.')
|
||||
unequip(slot: number): number | undefined {
|
||||
const uid = this.equips.get(slot);
|
||||
if (isNil(uid)) return void 0;
|
||||
|
||||
@ -1,5 +1,43 @@
|
||||
import { BaseHeroModifier } from './attribute';
|
||||
import { IHeroModifier } from './types';
|
||||
import { SaveCompression } from '@user/data-common';
|
||||
import { IHeroModifier, IHeroModifierOwner } from './types';
|
||||
|
||||
export abstract class BaseHeroModifier<T, V> implements IHeroModifier<T, V, V> {
|
||||
abstract readonly type: string;
|
||||
abstract readonly priority: number;
|
||||
|
||||
owner: IHeroModifierOwner | null = null;
|
||||
|
||||
constructor(private currentValue: V) {}
|
||||
|
||||
get value(): V {
|
||||
return this.currentValue;
|
||||
}
|
||||
|
||||
setValue(value: V): void {
|
||||
this.currentValue = value;
|
||||
this.owner?.markModifierDirty(this);
|
||||
}
|
||||
|
||||
getValue(): V {
|
||||
return this.currentValue;
|
||||
}
|
||||
|
||||
bindAttribute(attribute: IHeroModifierOwner | null): void {
|
||||
this.owner = attribute;
|
||||
}
|
||||
|
||||
saveState(_compression: SaveCompression): V {
|
||||
return this.currentValue;
|
||||
}
|
||||
|
||||
loadState(state: V, _compression: SaveCompression): void {
|
||||
this.setValue(state);
|
||||
}
|
||||
|
||||
abstract modify(value: T, baseValue: T, name: string): T;
|
||||
|
||||
abstract clone(): IHeroModifier<T, V>;
|
||||
}
|
||||
|
||||
export class ValueModifier extends BaseHeroModifier<number, number> {
|
||||
readonly type = '@system/value';
|
||||
|
||||
@ -662,18 +662,56 @@ export interface IHeroEquipsStoreSave<THero> {
|
||||
readonly equipments: readonly IEquipmentStateSave<THero>[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 装备实例。其修饰器由装备自身加成重建而来,是派生视图:
|
||||
* 跨读档不保留旧的修饰器对象引用属预期行为,读档后它们会被重新创建
|
||||
*/
|
||||
export interface IEquipmentState<THero> extends ISaveableContent<
|
||||
IEquipmentStateSave<THero>
|
||||
> {
|
||||
export interface IEquipmentStateHooks<THero> extends IHookBase {
|
||||
/**
|
||||
* 当装备的修饰器发生变化时执行,当发生修饰器增加、减少、数值修改时都会触发
|
||||
* @param modifiers 当前装备所拥有的所有修饰器
|
||||
*/
|
||||
onChangeModifier?(
|
||||
modifiers: Iterable<[SelectKey<THero, number>, IHeroModifier<number>]>
|
||||
): void;
|
||||
}
|
||||
|
||||
export interface IEquipmentState<THero>
|
||||
extends
|
||||
ISaveableContent<IEquipmentStateSave<THero>>,
|
||||
IHookable<IEquipmentStateHooks<THero>> {
|
||||
/** 装备实例 uid */
|
||||
readonly uid: number;
|
||||
/** 装备的定义数据引用 */
|
||||
readonly item: IItemRawData<THero>;
|
||||
|
||||
/**
|
||||
* 设置此装备对指定勇士属性值的增加量
|
||||
* @param name 勇士属性名
|
||||
* @param value 要将装备的此属性增量设置为的值
|
||||
*/
|
||||
setValue(name: SelectKey<THero, number>, value: number): void;
|
||||
|
||||
/**
|
||||
* 设置此装备对指定勇士属性值的百分比增加量
|
||||
* @param name 勇士属性名
|
||||
* @param value 要将装备的此属性百分比增量设置为的值
|
||||
*/
|
||||
setPercentage(name: SelectKey<THero, number>, value: number): void;
|
||||
|
||||
/**
|
||||
* 获取装备对指定勇士属性值的增加量,若装备不增加对应属性,则返回 `0`
|
||||
* @param name 勇士属性名
|
||||
*/
|
||||
getValue(name: SelectKey<THero, number>): number;
|
||||
|
||||
/**
|
||||
* 获取装备对指定勇士属性值的百分比增加量,若装备不增加对应属性,则返回 `0`
|
||||
* @param name 勇士属性名
|
||||
*/
|
||||
getPercentage(name: SelectKey<THero, number>): number;
|
||||
|
||||
/**
|
||||
* 构建所有的修饰器,每个装备实例只需要进行一次构建
|
||||
*/
|
||||
buildModifiers(): void;
|
||||
|
||||
/**
|
||||
* 获取装备产生的所有修饰器,每个元素为 [属性名, 修饰器]
|
||||
*/
|
||||
|
||||
@ -18,7 +18,7 @@ interface IReplaySafetyDetailQueue {
|
||||
type ReplayMethod<This, Args extends unknown[], Return> = (
|
||||
this: This,
|
||||
...args: Args
|
||||
) => Return;
|
||||
) => Return | any;
|
||||
|
||||
type ReplayDecorator = <This, Args extends unknown[], Return>(
|
||||
method: ReplayMethod<This, Args, Return>,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user