From 79d06c31df9f93d70cb3f6c6713228af4737d768 Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Fri, 17 Apr 2026 17:49:24 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=BC=A4=E5=AE=B3=E8=AE=A1?= =?UTF-8?q?=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dev.md | 5 +- packages-user/data-base/src/enemy/damage.ts | 35 +++- packages-user/data-base/src/enemy/types.ts | 8 + packages-user/data-state/src/core.ts | 2 + .../data-state/src/enemy/calculator.ts | 177 ++++++++++++++++++ packages-user/data-state/src/enemy/index.ts | 1 + .../data-state/src/enemy/mapDamage.ts | 30 ++- 7 files changed, 249 insertions(+), 9 deletions(-) create mode 100644 packages-user/data-state/src/enemy/calculator.ts diff --git a/dev.md b/dev.md index cc5c81e..a061603 100644 --- a/dev.md +++ b/dev.md @@ -54,8 +54,9 @@ - 没用到的变量、方法使用下划线开头。 - 合理运用 `readonly` `protected` `private` 关键字。 - 函数不建议使用过多可选参数,如果可选参数过多,可以考虑换用对象。 - - 尽量少地使用 `as` 关键字进行类型断言,一般情况下不建议进行任何 `as` 类型断言 + - 尽量少地使用 `as` 关键字进行类型断言,一般情况下不建议进行任何 `as` 类型断言。 - 其他要求: - 严格遵循 `eslint` 配置,不允许出现 `eslint` 报错。 - - 尽量不使用 `?.` 运算符,一般建议仅在副作用函数调用(如 `this.obj?.func()`,`this.obj.func?.()`),或对象 `Required` 化(如 `{ value: obj?.value ?? 0 }`)中使用 `?.` 运算符 + - 尽量不使用 `?.` 运算符,一般建议仅在副作用函数调用(如 `this.obj?.func()`,`this.obj.func?.()`),或对象 `Required` 化(如 `{ value: obj?.value ?? 0 }`)中使用 `?.` 运算符。 - 只进行必要的非空判断,不必要的非空判断直接使用非空断言 `!` 实现。 + - 除非参数要求传入函数等情况,不建议在函数内写任何局部函数。 diff --git a/packages-user/data-base/src/enemy/damage.ts b/packages-user/data-base/src/enemy/damage.ts index 32b785c..733b32a 100644 --- a/packages-user/data-base/src/enemy/damage.ts +++ b/packages-user/data-base/src/enemy/damage.ts @@ -11,6 +11,7 @@ import { IReadonlyEnemy } from './types'; import { IHeroAttribute, IReadonlyHeroAttribute } from '../hero'; +import { clamp } from 'lodash-es'; interface ICriticalSearchResult { /** 此临界点的属性值 */ @@ -84,6 +85,37 @@ export class DamageSystem implements IDamageSystem { return info; } + getDamageInfoByComputed( + enemy: IReadonlyEnemy + ): IEnemyDamageInfo | null { + if (!this.heroStatus) { + logger.warn(107); + return null; + } + if (!this.calculator) { + logger.warn(106); + return null; + } + + const hero = this.heroStatus; + if (!hero) return null; + const view = this.context.getViewByComputed(enemy); + if (!view) return null; + const locator = this.context.getEnemyLocatorByView(view); + if (!locator) return null; + + const cached = this.cache.get(view); + if (cached) { + return cached; + } + + const handler = this.createReadonlyHandler(enemy, locator, hero); + const info = this.calculator.calculate(handler); + this.cache.set(view, info); + + return info; + } + markDirty(enemy: IEnemyView): void { this.cache.delete(enemy); } @@ -127,7 +159,8 @@ export class DamageSystem implements IDamageSystem { if (currentValue >= upperLimit) return; - const maxIterations = Math.max(0, Math.floor(precision)); + // 超过 64 位的精度没有意义,所以最高设置为 64 + const maxIterations = clamp(Math.floor(precision), 4, 64); let baseValue = currentValue; let baseInfo = currentInfo; diff --git a/packages-user/data-base/src/enemy/types.ts b/packages-user/data-base/src/enemy/types.ts index 81d77e1..4577bfa 100644 --- a/packages-user/data-base/src/enemy/types.ts +++ b/packages-user/data-base/src/enemy/types.ts @@ -650,6 +650,14 @@ export interface IDamageSystem { */ getDamageInfo(enemy: IEnemyView): IEnemyDamageInfo | null; + /** + * 根据怪物对象获取战斗伤害信息 + * @param enemy 怪物对象 + */ + getDamageInfoByComputed( + enemy: IReadonlyEnemy + ): IEnemyDamageInfo | null; + /** * 将指定的怪物标记为脏 * @param enemy 怪物视图 diff --git a/packages-user/data-state/src/core.ts b/packages-user/data-state/src/core.ts index 7719a6b..28625f1 100644 --- a/packages-user/data-state/src/core.ts +++ b/packages-user/data-state/src/core.ts @@ -18,6 +18,7 @@ import { CommonAuraConverter, EnemyLegacyBridge, GuardAuraConverter, + MainDamageCalculator, MainEnemyFinalEffect, MainMapDamageConverter, MainMapDamageReducer, @@ -67,6 +68,7 @@ export class CoreState implements ICoreState { // 怪物上下文初始化 const enemyContext = new EnemyContext(); const damageSystem = new DamageSystem(enemyContext); + damageSystem.useCalculator(new MainDamageCalculator()); const mapDamage = new MapDamage(enemyContext); mapDamage.useConverter(new MainMapDamageConverter()); mapDamage.useReducer(new MainMapDamageReducer()); diff --git a/packages-user/data-state/src/enemy/calculator.ts b/packages-user/data-state/src/enemy/calculator.ts new file mode 100644 index 0000000..52d72d1 --- /dev/null +++ b/packages-user/data-state/src/enemy/calculator.ts @@ -0,0 +1,177 @@ +import { + CriticalableHeroStatus, + IDamageCalculator, + IEnemyDamageInfo, + IReadonlyEnemyHandler +} from '@user/data-base'; +import { IEnemyAttr } from './types'; +import { IVampireValue } from './special'; +import { IHeroAttr } from '../hero'; + +export class MainDamageCalculator implements IDamageCalculator< + IEnemyAttr, + IHeroAttr +> { + /** 当前是否正在计算支援怪的伤害 */ + private inGuard: boolean = false; + + /** + * 计算战斗伤害信息 + * @param handler 信息对象 + */ + calculate( + handler: IReadonlyEnemyHandler + ): IEnemyDamageInfo { + const { enemy, locator, hero } = handler; + const hp = hero.getBaseAttribute('hp'); + const atk = hero.getFinalAttribute('atk'); + const def = hero.getFinalAttribute('def'); + const mdef = this.inGuard ? 0 : hero.getFinalAttribute('mdef'); + // 支援中魔防只会被计算一次,因此除了当前怪物,计算其他怪物伤害时魔防为 0 + const monAtk = enemy.getAttribute('atk'); + const monDef = enemy.getAttribute('def'); + let monHp = enemy.getAttribute('hp'); + + // 无敌 + if (enemy.hasSpecial(20) && core.itemCount('cross') < 1) { + return { damage: Infinity, turn: 0 }; + } + + /** 怪物会对勇士造成的总伤害 */ + let damage = 0; + + /** 勇士每轮造成的伤害 */ + let heroPerDamage = 0; + /** 怪物每轮造成的伤害 */ + let enemyPerDamage = 0; + + // 勇士每轮伤害为勇士攻击减去怪物防御 + heroPerDamage += atk - monDef; + if (heroPerDamage <= 0) { + return { damage: Infinity, turn: 0 }; + } + + // 吸血 + const vampire = enemy.getSpecial(11); + if (vampire) { + const value = (vampire.value.vampire / 100) * hp; + damage += value; + // 如果吸血加到自身 + if (vampire.value.add) { + monHp += value; + } + } + + // 魔攻 + if (enemy.hasSpecial(2)) { + enemyPerDamage = monAtk; + } else { + enemyPerDamage = monAtk - def; + } + + // 连击 + if (enemy.hasSpecial(4)) enemyPerDamage *= 2; + if (enemy.hasSpecial(5)) enemyPerDamage *= 3; + + const multiHit = enemy.getSpecial(6); + if (multiHit) { + enemyPerDamage *= multiHit.value; + } + + if (enemyPerDamage < 0) enemyPerDamage = 0; + + let turn = Math.ceil(monHp / heroPerDamage); + + // 支援,当怪物被支援且不包含支援标记时执行,因为支援怪不能再被支援了 + const guards = enemy.getAttribute('guard'); + if (guards.size > 0 && !this.inGuard) { + this.inGuard = true; + // 计算支援怪的伤害,同时把打支援怪花费的回合数加到当前怪物上,因为打支援怪的时候当前怪物也会打你 + // 因此回合数需要加上打支援怪的回合数 + for (const guard of guards) { + // 直接把 enemy 传过去,因此支援的 enemy 会吃到其原本所在位置的光环加成 + const extraInfo = this.calculate({ + enemy: guard.getComputedEnemy(), + locator, + hero + }); + turn += extraInfo.turn; + damage += extraInfo.damage; + } + this.inGuard = false; + } + + // 先攻 + if (enemy.hasSpecial(1)) { + damage += enemyPerDamage; + } + + // 破甲 + const breakArmor = enemy.getSpecial(7); + if (breakArmor) { + damage += (breakArmor.value / 100) * def; + } + + // 反击 + const counterAttack = enemy.getSpecial(8); + if (counterAttack) { + // 反击是每回合生效,因此加到 enemyPerDamage 上 + enemyPerDamage += (counterAttack.value / 100) * atk; + } + + // 净化 + const purify = enemy.getSpecial(9); + if (purify) { + damage += purify.value * mdef; + } + + damage += (turn - 1) * enemyPerDamage; + + // 魔防 + damage -= mdef; + + // 未开启负伤时,如果伤害为负,则设为 0 + if (!core.flags.enableNegativeDamage && damage < 0) { + damage = 0; + } + + // 固伤,无法被魔防减伤 + const fixedDamage = enemy.getSpecial(22); + if (fixedDamage) { + damage += fixedDamage.value; + } + + // 仇恨,无法被魔防减伤 + if (enemy.hasSpecial(17)) { + damage += core.getFlag('hatred', 0); + } + + return { + damage: Math.floor(damage), + turn + }; + } + + /** + * 获取临界计算的上界 + * @param handler 信息对象 + * @param attribute 目标属性名 + */ + getCriticalLimit( + handler: IReadonlyEnemyHandler, + attribute: CriticalableHeroStatus + ): number { + switch (attribute) { + case 'atk': { + if (handler.enemy.hasSpecial(3)) { + return Infinity; + } + return ( + handler.enemy.getAttribute('def') + + handler.enemy.getAttribute('hp') + ); + } + } + return handler.hero.getFinalAttribute(attribute); + } +} diff --git a/packages-user/data-state/src/enemy/index.ts b/packages-user/data-state/src/enemy/index.ts index a5b8d19..c923f4c 100644 --- a/packages-user/data-state/src/enemy/index.ts +++ b/packages-user/data-state/src/enemy/index.ts @@ -1,4 +1,5 @@ export * from './aura'; +export * from './calculator'; export * from './damage'; export * from './final'; export * from './legacy'; diff --git a/packages-user/data-state/src/enemy/mapDamage.ts b/packages-user/data-state/src/enemy/mapDamage.ts index edd110c..26bea1e 100644 --- a/packages-user/data-state/src/enemy/mapDamage.ts +++ b/packages-user/data-state/src/enemy/mapDamage.ts @@ -20,7 +20,8 @@ import { IReadonlyEnemyHandler, ISpecial, IMapDamageView, - IReadonlyHeroAttribute + IReadonlyHeroAttribute, + IReadonlyEnemy } from '@user/data-base'; import { IZoneValue } from './special'; import { IEnemyAttr, MapDamageType } from './types'; @@ -182,6 +183,7 @@ export class LaserDamageView extends BaseMapDamageView { export class BetweenDamageView extends BaseMapDamageView { constructor( context: IEnemyContext, + private readonly enemy: IReadonlyEnemy, private readonly locator: Readonly, private readonly hero: IReadonlyHeroAttribute ) { @@ -224,12 +226,28 @@ export class BetweenDamageView extends BaseMapDamageView { if (!other) { return null; } - if (!other.getComputedEnemy().hasSpecial(16)) { + const otherEnemy = other.getComputedEnemy(); + if (!otherEnemy.hasSpecial(16)) { return null; } - const damage = this.hero.getFinalAttribute('hp'); - return this.createInfo(damage, MapDamageType.Between); + const half = this.hero.getFinalAttribute('hp') / 2; + if (core.flags.betweenAttackMax) { + // 夹击不超伤害值,需要获取两个怪物的伤害 + const sys = this.context.getDamageSystem(); + if (!sys) { + return this.createInfo(half, MapDamageType.Between); + } else { + const currInfo = sys.getDamageInfoByComputed(this.enemy); + const otherInfo = sys.getDamageInfoByComputed(otherEnemy); + const currDamage = currInfo?.damage ?? Infinity; + const otherDamage = otherInfo?.damage ?? Infinity; + const min = Math.min(half, currDamage, otherDamage); + return this.createInfo(min, MapDamageType.Between); + } + } else { + return this.createInfo(half, MapDamageType.Between); + } } } @@ -273,7 +291,7 @@ export class MainMapDamageConverter implements IMapDamageConverter< context: IEnemyContext ): IMapDamageView[] { const views: IMapDamageView[] = []; - const { enemy, locator } = handler; + const { enemy, locator, hero } = handler; const zone = enemy.getSpecial(15); if (zone) { @@ -281,7 +299,7 @@ export class MainMapDamageConverter implements IMapDamageConverter< } if (enemy.hasSpecial(16)) { - views.push(new BetweenDamageView(context, locator, handler.hero)); + views.push(new BetweenDamageView(context, enemy, locator, hero)); } const repulse = enemy.getSpecial(18);