refactor: 伤害计算

This commit is contained in:
unanmed 2026-04-17 17:49:24 +08:00
parent 4d9c6720aa
commit 79d06c31df
7 changed files with 249 additions and 9 deletions

5
dev.md
View File

@ -54,8 +54,9 @@
- 没用到的变量、方法使用下划线开头。 - 没用到的变量、方法使用下划线开头。
- 合理运用 `readonly` `protected` `private` 关键字。 - 合理运用 `readonly` `protected` `private` 关键字。
- 函数不建议使用过多可选参数,如果可选参数过多,可以考虑换用对象。 - 函数不建议使用过多可选参数,如果可选参数过多,可以考虑换用对象。
- 尽量少地使用 `as` 关键字进行类型断言,一般情况下不建议进行任何 `as` 类型断言 - 尽量少地使用 `as` 关键字进行类型断言,一般情况下不建议进行任何 `as` 类型断言
- 其他要求: - 其他要求:
- 严格遵循 `eslint` 配置,不允许出现 `eslint` 报错。 - 严格遵循 `eslint` 配置,不允许出现 `eslint` 报错。
- 尽量不使用 `?.` 运算符,一般建议仅在副作用函数调用(如 `this.obj?.func()``this.obj.func?.()`),或对象 `Required` 化(如 `{ value: obj?.value ?? 0 }`)中使用 `?.` 运算符 - 尽量不使用 `?.` 运算符,一般建议仅在副作用函数调用(如 `this.obj?.func()``this.obj.func?.()`),或对象 `Required` 化(如 `{ value: obj?.value ?? 0 }`)中使用 `?.` 运算符
- 只进行必要的非空判断,不必要的非空判断直接使用非空断言 `!` 实现。 - 只进行必要的非空判断,不必要的非空判断直接使用非空断言 `!` 实现。
- 除非参数要求传入函数等情况,不建议在函数内写任何局部函数。

View File

@ -11,6 +11,7 @@ import {
IReadonlyEnemy IReadonlyEnemy
} from './types'; } from './types';
import { IHeroAttribute, IReadonlyHeroAttribute } from '../hero'; import { IHeroAttribute, IReadonlyHeroAttribute } from '../hero';
import { clamp } from 'lodash-es';
interface ICriticalSearchResult { interface ICriticalSearchResult {
/** 此临界点的属性值 */ /** 此临界点的属性值 */
@ -84,6 +85,37 @@ export class DamageSystem<TAttr, THero> implements IDamageSystem<TAttr, THero> {
return info; return info;
} }
getDamageInfoByComputed(
enemy: IReadonlyEnemy<TAttr>
): 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<TAttr>): void { markDirty(enemy: IEnemyView<TAttr>): void {
this.cache.delete(enemy); this.cache.delete(enemy);
} }
@ -127,7 +159,8 @@ export class DamageSystem<TAttr, THero> implements IDamageSystem<TAttr, THero> {
if (currentValue >= upperLimit) return; 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 baseValue = currentValue;
let baseInfo = currentInfo; let baseInfo = currentInfo;

View File

@ -650,6 +650,14 @@ export interface IDamageSystem<TAttr, THero> {
*/ */
getDamageInfo(enemy: IEnemyView<TAttr>): IEnemyDamageInfo | null; getDamageInfo(enemy: IEnemyView<TAttr>): IEnemyDamageInfo | null;
/**
*
* @param enemy
*/
getDamageInfoByComputed(
enemy: IReadonlyEnemy<TAttr>
): IEnemyDamageInfo | null;
/** /**
* *
* @param enemy * @param enemy

View File

@ -18,6 +18,7 @@ import {
CommonAuraConverter, CommonAuraConverter,
EnemyLegacyBridge, EnemyLegacyBridge,
GuardAuraConverter, GuardAuraConverter,
MainDamageCalculator,
MainEnemyFinalEffect, MainEnemyFinalEffect,
MainMapDamageConverter, MainMapDamageConverter,
MainMapDamageReducer, MainMapDamageReducer,
@ -67,6 +68,7 @@ export class CoreState implements ICoreState {
// 怪物上下文初始化 // 怪物上下文初始化
const enemyContext = new EnemyContext<IEnemyAttr, IHeroAttr>(); const enemyContext = new EnemyContext<IEnemyAttr, IHeroAttr>();
const damageSystem = new DamageSystem(enemyContext); const damageSystem = new DamageSystem(enemyContext);
damageSystem.useCalculator(new MainDamageCalculator());
const mapDamage = new MapDamage(enemyContext); const mapDamage = new MapDamage(enemyContext);
mapDamage.useConverter(new MainMapDamageConverter()); mapDamage.useConverter(new MainMapDamageConverter());
mapDamage.useReducer(new MainMapDamageReducer()); mapDamage.useReducer(new MainMapDamageReducer());

View File

@ -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<IEnemyAttr, IHeroAttr>
): 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<IVampireValue>(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<number>(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<number>(7);
if (breakArmor) {
damage += (breakArmor.value / 100) * def;
}
// 反击
const counterAttack = enemy.getSpecial<number>(8);
if (counterAttack) {
// 反击是每回合生效,因此加到 enemyPerDamage 上
enemyPerDamage += (counterAttack.value / 100) * atk;
}
// 净化
const purify = enemy.getSpecial<number>(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<number>(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<IEnemyAttr, IHeroAttr>,
attribute: CriticalableHeroStatus<IHeroAttr>
): 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);
}
}

View File

@ -1,4 +1,5 @@
export * from './aura'; export * from './aura';
export * from './calculator';
export * from './damage'; export * from './damage';
export * from './final'; export * from './final';
export * from './legacy'; export * from './legacy';

View File

@ -20,7 +20,8 @@ import {
IReadonlyEnemyHandler, IReadonlyEnemyHandler,
ISpecial, ISpecial,
IMapDamageView, IMapDamageView,
IReadonlyHeroAttribute IReadonlyHeroAttribute,
IReadonlyEnemy
} from '@user/data-base'; } from '@user/data-base';
import { IZoneValue } from './special'; import { IZoneValue } from './special';
import { IEnemyAttr, MapDamageType } from './types'; import { IEnemyAttr, MapDamageType } from './types';
@ -182,6 +183,7 @@ export class LaserDamageView extends BaseMapDamageView<IRayRangeParam> {
export class BetweenDamageView extends BaseMapDamageView<IManhattanRangeParam> { export class BetweenDamageView extends BaseMapDamageView<IManhattanRangeParam> {
constructor( constructor(
context: IEnemyContext<IEnemyAttr, IHeroAttr>, context: IEnemyContext<IEnemyAttr, IHeroAttr>,
private readonly enemy: IReadonlyEnemy<IEnemyAttr>,
private readonly locator: Readonly<ITileLocator>, private readonly locator: Readonly<ITileLocator>,
private readonly hero: IReadonlyHeroAttribute<IHeroAttr> private readonly hero: IReadonlyHeroAttribute<IHeroAttr>
) { ) {
@ -224,12 +226,28 @@ export class BetweenDamageView extends BaseMapDamageView<IManhattanRangeParam> {
if (!other) { if (!other) {
return null; return null;
} }
if (!other.getComputedEnemy().hasSpecial(16)) { const otherEnemy = other.getComputedEnemy();
if (!otherEnemy.hasSpecial(16)) {
return null; return null;
} }
const damage = this.hero.getFinalAttribute('hp'); const half = this.hero.getFinalAttribute('hp') / 2;
return this.createInfo(damage, MapDamageType.Between); 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<IEnemyAttr, IHeroAttr> context: IEnemyContext<IEnemyAttr, IHeroAttr>
): IMapDamageView<any>[] { ): IMapDamageView<any>[] {
const views: IMapDamageView<any>[] = []; const views: IMapDamageView<any>[] = [];
const { enemy, locator } = handler; const { enemy, locator, hero } = handler;
const zone = enemy.getSpecial<IZoneValue>(15); const zone = enemy.getSpecial<IZoneValue>(15);
if (zone) { if (zone) {
@ -281,7 +299,7 @@ export class MainMapDamageConverter implements IMapDamageConverter<
} }
if (enemy.hasSpecial(16)) { if (enemy.hasSpecial(16)) {
views.push(new BetweenDamageView(context, locator, handler.hero)); views.push(new BetweenDamageView(context, enemy, locator, hero));
} }
const repulse = enemy.getSpecial<number>(18); const repulse = enemy.getSpecial<number>(18);