diff --git a/packages-user/data-base/src/enemy/context.ts b/packages-user/data-base/src/enemy/context.ts index f00cf5c..9b4c42a 100644 --- a/packages-user/data-base/src/enemy/context.ts +++ b/packages-user/data-base/src/enemy/context.ts @@ -3,6 +3,7 @@ import { ITileLocator } from '@user/types'; import { IAuraConverter, IAuraView, + IDamageSystem, IEnemy, IEnemyAuraView, IEnemyCommonQueryEffect, @@ -53,6 +54,7 @@ export class EnemyContext implements IEnemyContext { private readonly dirtyEnemy: Set> = new Set(); private mapDamage: IMapDamage | null = null; + private damageSystem: IDamageSystem | null = null; readonly indexer: MapLocIndexer = new MapLocIndexer(); private needUpdate: boolean = true; @@ -174,6 +176,9 @@ export class EnemyContext implements IEnemyContext { if (this.mapDamage) { this.mapDamage.deleteEnemy(view); } + if (this.damageSystem) { + this.damageSystem.deleteEnemy(view); + } this.needTotallyRefresh.delete(view); this.dirtyEnemy.delete(view); @@ -253,6 +258,15 @@ export class EnemyContext implements IEnemyContext { return this.mapDamage; } + attachDamageSystem(system: IDamageSystem): void { + this.damageSystem = system; + system.markAllDirty(); + } + + getDamageSystem(): IDamageSystem | null { + return this.damageSystem as IDamageSystem | null; + } + private convertSpecial( special: ISpecial, enemy: IReadonlyEnemy, @@ -545,6 +559,10 @@ export class EnemyContext implements IEnemyContext { this.buildupFinal(); } + if (this.damageSystem) { + this.damageSystem.markAllDirty(); + } + if (this.mapDamage) { this.mapDamage.refreshAll(); } @@ -553,6 +571,9 @@ export class EnemyContext implements IEnemyContext { markDirty(view: IEnemyView): void { if (!this.locatorViewMap.has(view)) return; this.dirtyEnemy.add(view); + if (this.damageSystem) { + this.damageSystem.markDirty(view); + } } private refreshSpecialModifier( @@ -687,6 +708,10 @@ export class EnemyContext implements IEnemyContext { this.dirtyEnemy.delete(view); + if (this.damageSystem) { + this.damageSystem.markDirty(view); + } + if (this.mapDamage) { this.mapDamage.markEnemyDirty(view); } @@ -721,6 +746,9 @@ export class EnemyContext implements IEnemyContext { this.needTotallyRefresh.clear(); this.requestedCommonContext.clear(); this.dirtyEnemy.clear(); + if (this.damageSystem) { + this.damageSystem.markAllDirty(); + } if (this.mapDamage) { this.mapDamage.refreshAll(); } @@ -729,6 +757,7 @@ export class EnemyContext implements IEnemyContext { destroy(): void { this.clear(); this.attachMapDamage(null); + this.damageSystem = null; this.auraConverter.clear(); this.commonQueryMap.clear(); this.specialQueryEffects.clear(); diff --git a/packages-user/data-base/src/enemy/damage.ts b/packages-user/data-base/src/enemy/damage.ts new file mode 100644 index 0000000..52bb965 --- /dev/null +++ b/packages-user/data-base/src/enemy/damage.ts @@ -0,0 +1,211 @@ +import { logger } from '@motajs/common'; +import { + CriticalableHeroStatus, + IDamageCalculator, + IDamageSystem, + IEnemyContext, + IEnemyCritical, + IEnemyDamageInfo, + IEnemyView, + IReadonlyEnemy +} from './types'; + +interface ICriticalSearchResult { + /** 此临界点的属性值 */ + readonly value: number; + /** 此临界点的伤害信息 */ + readonly info: IEnemyDamageInfo; +} + +export class DamageSystem implements IDamageSystem { + /** 当前正在使用的计算器 */ + private calculator: IDamageCalculator | null = null; + /** 当前勇士属性 */ + private heroStatus: Readonly | null = null; + /** 怪物伤害缓存 */ + private readonly cache: Map, IEnemyDamageInfo> = + new Map(); + + constructor(readonly context: IEnemyContext) {} + + useCalculator(calculator: IDamageCalculator): void { + this.calculator = calculator; + this.markAllDirty(); + } + + getCalculator(): IDamageCalculator | null { + return this.calculator; + } + + bindHeroStatus(hero: Readonly): void { + this.heroStatus = hero; + this.markAllDirty(); + } + + /** + * 深拷贝勇士属性 + */ + private cloneHeroStatus(): THero | null { + if (!this.heroStatus) return null; + else return structuredClone(this.heroStatus); + } + + /** + * 在修改勇士属性的情况下计算怪物伤害 + * @param enemy 怪物属性 + * @param attribute 修改的属性键名 + * @param value 修改为的属性值 + * @returns + */ + private calculateDamageWithModified( + enemy: IReadonlyEnemy, + attribute: CriticalableHeroStatus, + value: number + ): IEnemyDamageInfo { + const hero = this.cloneHeroStatus()!; + // @ts-expect-error 之后会进行修复 + hero[attribute] = value; + return this.calculator!.calculate(hero, enemy); + } + + getDamageInfo(enemy: IEnemyView): IEnemyDamageInfo | null { + if (!this.heroStatus) { + logger.warn(107); + return null; + } + if (!this.calculator) { + logger.warn(106); + return null; + } + const hero = this.cloneHeroStatus()!; + + const cached = this.cache.get(enemy); + if (cached) { + return cached; + } + + const info = this.calculator.calculate(hero, enemy.getComputedEnemy()); + this.cache.set(enemy, info); + return info; + } + + markDirty(enemy: IEnemyView): void { + this.cache.delete(enemy); + } + + deleteEnemy(enemy: IEnemyView): void { + this.cache.delete(enemy); + } + + markAllDirty(): void { + this.cache.clear(); + } + + *calculateCritical( + view: IEnemyView, + attribute: CriticalableHeroStatus, + precision: number + ): Generator { + if (!this.heroStatus) { + logger.warn(107); + return; + } + if (!this.calculator) { + logger.warn(106); + return; + } + + const currentInfo = this.getDamageInfo(view); + if (!currentInfo) return; + + const enemy = view.getComputedEnemy(); + const hero = this.cloneHeroStatus()!; + const currentValue = hero[attribute] as number; + + const upperLimit = Math.floor( + this.calculator.getCriticalLimit(hero, enemy, attribute) + ); + + if (currentValue >= upperLimit) return; + + const maxIterations = Math.max(0, Math.floor(precision)); + let baseValue = currentValue; + let baseInfo = currentInfo; + + while (baseValue < upperLimit) { + const next = this.findNextCritical( + enemy, + attribute, + baseValue, + upperLimit, + baseInfo.damage, + maxIterations + ); + if (!next) return; + + yield { + nextValue: next.value, + baseValue: currentValue, + nextDiff: next.value - currentValue, + baseInfo: currentInfo, + info: next.info, + damageDiff: next.info.damage - currentInfo.damage + }; + + baseValue = next.value; + baseInfo = next.info; + } + } + + /** + * 计算下一个临界点 + * @param enemy 怪物对象 + * @param attribute 勇士属性名 + * @param currentValue 当前勇士属性值 + * @param upperLimit 二分上界 + * @param referenceDamage 参考伤害值 + * @param maxIterations 最大迭代数量 + */ + private findNextCritical( + enemy: IReadonlyEnemy, + attribute: CriticalableHeroStatus, + currentValue: number, + upperLimit: number, + referenceDamage: number, + maxIterations: number + ): ICriticalSearchResult | null { + let left = currentValue; + let right = upperLimit; + let rightInfo = this.calculateDamageWithModified( + enemy, + attribute, + right + ); + + if (rightInfo.damage >= referenceDamage) return null; + + let iter = 0; + while (iter < maxIterations) { + const middle = Math.floor((left + right) / 2); + const middleInfo = this.calculateDamageWithModified( + enemy, + attribute, + middle + ); + if (middleInfo.damage < referenceDamage) { + right = middle; + rightInfo = middleInfo; + } else { + left = middle; + } + if (right - left <= 1) break; + + iter++; + } + + return { + value: right, + info: rightInfo + }; + } +} diff --git a/packages-user/data-base/src/enemy/index.ts b/packages-user/data-base/src/enemy/index.ts index 875a8b3..cc09d68 100644 --- a/packages-user/data-base/src/enemy/index.ts +++ b/packages-user/data-base/src/enemy/index.ts @@ -1,5 +1,6 @@ export * from './enemy'; export * from './context'; +export * from './damage'; export * from './mapDamage'; export * from './manager'; export * from './special'; diff --git a/packages-user/data-base/src/enemy/mapDamage.ts b/packages-user/data-base/src/enemy/mapDamage.ts index 8777a75..5d876eb 100644 --- a/packages-user/data-base/src/enemy/mapDamage.ts +++ b/packages-user/data-base/src/enemy/mapDamage.ts @@ -60,10 +60,12 @@ export class MapDamage implements IMapDamage { /** 合并后伤害缓存,索引 -> 合并结果 */ private readonly reducedCache: Map = new Map(); - constructor( - readonly context: IEnemyContext, - readonly indexer: IMapLocIndexer - ) {} + /** 坐标索引对象 */ + private readonly indexer: IMapLocIndexer; + + constructor(readonly context: IEnemyContext) { + this.indexer = context.indexer; + } useConverter(converter: IMapDamageConverter): void { this.converter = converter; diff --git a/packages-user/data-base/src/enemy/types.ts b/packages-user/data-base/src/enemy/types.ts index 1b937ed..3a2ee5b 100644 --- a/packages-user/data-base/src/enemy/types.ts +++ b/packages-user/data-base/src/enemy/types.ts @@ -540,6 +540,117 @@ export interface IMapDamage { //#endregion +//#region 伤害系统 + +export interface IEnemyDamageInfo { + /** 战斗伤害值 */ + readonly damage: number; + /** 战斗回合数 */ + readonly turn: number; +} + +export interface IEnemyCritical { + /** 此临界点中指定勇士属性的值 */ + readonly nextValue: number; + /** 当前勇士指定属性的值 */ + readonly baseValue: number; + /** 此临界点中指定勇士数值的值与当前值的差,即 `nextValue - baseValue` */ + readonly nextDiff: number; + /** 当前状态下怪物的伤害信息 */ + readonly baseInfo: IEnemyDamageInfo; + /** 此临界点下怪物的伤害信息 */ + readonly info: IEnemyDamageInfo; + /** 此临界点的伤害值与当前伤害值的差 */ + readonly damageDiff: number; +} + +export type CriticalableHeroStatus = keyof { + [P in keyof THero as THero[P] extends number ? P : never]: unknown; +}; + +export interface IDamageCalculator { + /** + * 计算战斗伤害信息 + * @param hero 勇士信息 + * @param enemy 怪物信息 + */ + calculate( + hero: Readonly, + enemy: IReadonlyEnemy + ): IEnemyDamageInfo; + + /** + * 获取临界计算的上界 + * @param hero 勇士信息 + * @param enemy 怪物信息 + * @param attribute 勇士的临界属性 + */ + getCriticalLimit( + hero: Readonly, + enemy: IReadonlyEnemy, + attribute: CriticalableHeroStatus + ): number; +} + +export interface IDamageSystem { + /** 伤害系统所属的上下文 */ + readonly context: IEnemyContext; + + /** + * 设置当前伤害计算系统使用的伤害计算器 + * @param calculator 伤害计算器 + */ + useCalculator(calculator: IDamageCalculator): void; + + /** + * 获取当前使用的伤害计算器 + */ + getCalculator(): IDamageCalculator | null; + + /** + * 绑定勇士信息 + * @param hero 勇士信息 + */ + bindHeroStatus(hero: Readonly): void; + + /** + * 获取战斗伤害信息 + * @param enemy 怪物视图 + */ + getDamageInfo(enemy: IEnemyView): IEnemyDamageInfo | null; + + /** + * 将指定的怪物标记为脏 + * @param enemy 怪物视图 + */ + markDirty(enemy: IEnemyView): void; + + /** + * 删除指定的怪物 + * @param enemy 怪物视图 + */ + deleteEnemy(enemy: IEnemyView): void; + + /** + * 将所有怪物标记为脏 + */ + markAllDirty(): void; + + /** + * 计算怪物在指定勇士属性下的临界 + * @param enemy 怪物视图 + * @param attribute 计算临界的目标勇士属性,比如计算攻击临界、自定义属性的临界等等 + * @param precision 临界计算精度,表示会进行多少次二分计算,一般填写 `12-16` 之间的数即可 + */ + calculateCritical( + enemy: IEnemyView, + attribute: CriticalableHeroStatus, + precision: number + ): Generator; +} + +//#endregion + //#region 上下文 export interface IEnemyContext { @@ -547,6 +658,8 @@ export interface IEnemyContext { readonly width: number; /** 怪物上下文高度 */ readonly height: number; + /** 此上下文使用的索引对象 */ + readonly indexer: IMapLocIndexer; /** * 调整上下文尺寸,并清空当前上下文中的所有怪物与状态 @@ -702,6 +815,17 @@ export interface IEnemyContext { */ getMapDamage(): IMapDamage | null; + /** + * 绑定伤害计算系统 + * @param system 伤害系统 + */ + attachDamageSystem(system: IDamageSystem): void; + + /** + * 获取当前绑定的伤害计算系统 + */ + getDamageSystem(): IDamageSystem | null; + /** * 重建当前上下文中的全部怪物计算结果 * diff --git a/packages-user/data-state/src/core.ts b/packages-user/data-state/src/core.ts index 2a7bc51..e687fd4 100644 --- a/packages-user/data-state/src/core.ts +++ b/packages-user/data-state/src/core.ts @@ -3,6 +3,19 @@ import { IHeroState, HeroState } from './hero'; import { ILayerState, LayerState } from './map'; import { IRoleFaceBinder, RoleFaceBinder } from './common'; import { GameDataState } from './data'; +import { + DamageSystem, + EnemyContext, + IEnemyContext, + MapDamage +} from '@user/data-base'; +import { IEnemyAttributes } from './enemy/types'; +import { + CommonAuraConverter, + GuardAuraConverter, + MainMapDamageConverter, + MainMapDamageReducer +} from './enemy'; export class CoreState implements ICoreState { readonly layer: ILayerState; @@ -11,6 +24,7 @@ export class CoreState implements ICoreState { readonly data: IGameDataState; readonly idNumberMap: Map; readonly numberIdMap: Map; + readonly enemyContext: IEnemyContext; constructor() { this.layer = new LayerState(); @@ -19,6 +33,19 @@ export class CoreState implements ICoreState { this.idNumberMap = new Map(); this.numberIdMap = new Map(); this.data = new GameDataState(); + + // 怪物上下文初始化 + const enemyContext = new EnemyContext(); + const damageSystem = new DamageSystem(enemyContext); + const mapDamage = new MapDamage(enemyContext); + mapDamage.useConverter(new MainMapDamageConverter()); + mapDamage.useReducer(new MainMapDamageReducer()); + enemyContext.attachDamageSystem(damageSystem); + enemyContext.attachMapDamage(mapDamage); + enemyContext.registerAuraConverter(new CommonAuraConverter()); + enemyContext.registerAuraConverter(new GuardAuraConverter()); + enemyContext.resize(core._WIDTH_, core._HEIGHT_); + this.enemyContext = enemyContext; } saveState(): IStateSaveData { diff --git a/packages-user/data-state/src/types.ts b/packages-user/data-state/src/types.ts index 7565083..278592f 100644 --- a/packages-user/data-state/src/types.ts +++ b/packages-user/data-state/src/types.ts @@ -1,7 +1,7 @@ import { ILayerState } from './map'; import { IHeroFollower, IHeroState } from './hero'; import { IRoleFaceBinder } from './common'; -import { IEnemyManager } from '@user/data-base'; +import { IEnemyContext, IEnemyManager } from '@user/data-base'; import { IEnemyAttributes } from './enemy/types'; export interface IGameDataState { @@ -28,6 +28,9 @@ export interface ICoreState { /** 图块数字到 id 的映射 */ readonly numberIdMap: Map; + /** 怪物上下文 */ + readonly enemyContext: IEnemyContext; + /** * 保存状态 */ diff --git a/packages/common/src/logger.json b/packages/common/src/logger.json index 7e019e0..38a4936 100644 --- a/packages/common/src/logger.json +++ b/packages/common/src/logger.json @@ -161,6 +161,8 @@ "103": "Map damage reducer is missing, reduced map damage is unavailable.", "104": "Enemy dirty marking failed since specific enemy is not in current context.", "105": "No specific map damage view stored, which seems like an internal bug of map damage system.", + "106": "Damage calculator is missing, damage calculation is unavailable.", + "107": "Hero status is not bound, damage calculation is unavailable.", "1001": "Item-detail extension needs 'floor-binder' and 'floor-damage' extension as dependency." } }