diff --git a/assets/script/game/common/config/heroSet.ts b/assets/script/game/common/config/heroSet.ts index ac543a03..ecda15f9 100644 --- a/assets/script/game/common/config/heroSet.ts +++ b/assets/script/game/common/config/heroSet.ts @@ -730,10 +730,17 @@ export const HeroList: number[] = [ // ==================== 英雄强度评分公式 ==================== -/** 英雄强度公式调参权重(可在编辑器或运行时热调) */ +/** 英雄强度公式调参权重(可在编辑器或运行时热调) + * 说明: + * - W_HP: 每 1 点生命值折算 1 分(生命值不随时间变化) + * - W_DPS_PER_SEC: 每 1 点 DPS 持续 1 秒折算 0.5 分 + * (默认 60 秒评估窗下,1 DPS ≈ 30 分,与旧版 W_DPS=30 等效) + * - WAVE_DURATION / HITS_PER_WAVE: 波次参数,用于估算 atked/fstart/fend/dead 触发次数 + * - DEFAULT_EVAL_SECONDS: calcHeroPower 默认使用的评估时间窗 + */ export const HERO_POWER_WEIGHTS = { W_HP: 1.0, - W_DPS: 30.0, + W_DPS_PER_SEC: 0.5, W_atking: 1.0, W_atked: 0.8, W_fstart: 1.0, @@ -745,6 +752,9 @@ export const HERO_POWER_WEIGHTS = { W_shield: 1.0, W_control: 0.5, expectedEnemyDmg: 20, + WAVE_DURATION: 30, + HITS_PER_WAVE: 10, + DEFAULT_EVAL_SECONDS: 60, }; /** 计算目标等级下 evolve 提供的累计属性加成 */ @@ -788,8 +798,45 @@ function calcScopeMul(group: TGroup): number { return (group === TGroup.Ally || group === TGroup.Team) ? FightSet.HERO_MAX_NUM : 1; } -/** 将计时 buff 折算为永久等效价值 */ -function calcBuffPermanentEquivalent(buff: BuffConfig, apEff: number, cd: number, buffValueOverride?: number): number { +/** 计算目标时间窗内的普攻次数 */ +function getAttackCount(hero: heroInfo, seconds: number): number { + return seconds / getAttackCd(hero); +} + +/** 计算目标时间窗内的受击次数 */ +function getHitCount(seconds: number): number { + const weights = HERO_POWER_WEIGHTS; + return (seconds / weights.WAVE_DURATION) * weights.HITS_PER_WAVE; +} + +/** 计算目标时间窗内的波次数 */ +function getWaveCount(seconds: number): number { + return seconds / HERO_POWER_WEIGHTS.WAVE_DURATION; +} + +/** 计算触发技在目标时间窗内的触发次数 */ +function getTriggerCount( + type: SkillTriggerType, + t_num: number, + hero: heroInfo, + seconds: number +): number { + switch (type) { + case SkillTriggerType.Atking: + return getAttackCount(hero, seconds) / t_num; + case SkillTriggerType.Atked: + return getHitCount(seconds) / t_num; + case SkillTriggerType.FStart: + case SkillTriggerType.FEnd: + return getWaveCount(seconds); + case SkillTriggerType.Dead: + return getWaveCount(seconds) * (1 + (hero.revive?.r_num || 0)); + } + return 1; +} + +/** 将计时 buff 折算为“在整个时间窗内持续生效”的等效价值 */ +function calcBuffPermanentEquivalent(buff: BuffConfig, apEff: number, cd: number, seconds: number, buffValueOverride?: number): number { const weights = HERO_POWER_WEIGHTS; let value = 0; if (buff.modifiers) { @@ -799,9 +846,9 @@ function calcBuffPermanentEquivalent(buff: BuffConfig, apEff: number, cd: number switch (mod.attr) { case Attrs.ap: if (mod.op === ModOp.Flat) { - value += (modValue / cd) * weights.W_DPS; + value += (modValue / cd) * weights.W_DPS_PER_SEC * seconds; } else if (mod.op === ModOp.PercentAdd) { - value += (modValue / 100) * dps * weights.W_DPS; + value += (modValue / 100) * dps * weights.W_DPS_PER_SEC * seconds; } break; case Attrs.hp: @@ -809,13 +856,13 @@ function calcBuffPermanentEquivalent(buff: BuffConfig, apEff: number, cd: number value += modValue * weights.W_HP; break; case Attrs.critical: - value += (modValue / 100) * dps * 0.5 * weights.W_DPS; + value += (modValue / 100) * dps * 0.5 * weights.W_DPS_PER_SEC * seconds; break; case Attrs.critical_damage: - value += (modValue / 100) * dps * 0.05 * weights.W_DPS; + value += (modValue / 100) * dps * 0.05 * weights.W_DPS_PER_SEC * seconds; break; case Attrs.speed: - value += (modValue / 100) * dps * weights.W_DPS; + value += (modValue / 100) * dps * weights.W_DPS_PER_SEC * seconds; break; } } @@ -828,45 +875,45 @@ function calcBuffPermanentEquivalent(buff: BuffConfig, apEff: number, cd: number return value; } -/** 计算 Support 类技能的效果价值 */ -function calcSupportEffectValue(config: SkillConfig, apEff: number, cd: number): number { +/** 计算 Support 类技能单次触发的效果价值 */ +function calcSupportEffectValue(config: SkillConfig, apEff: number, cd: number, seconds: number): number { const weights = HERO_POWER_WEIGHTS; const value = config.ap || 0; - // 计时 buff:先算永久等效,再按战斗时长覆盖率折算 + // 计时 buff:先算整个时间窗的永久等效,再按覆盖率折算 if (config.timed_buff_id) { const buff = BuffList[config.timed_buff_id]; if (buff) { - const permanentEquivalent = calcBuffPermanentEquivalent(buff, apEff, cd, config.buff_value); + const permanentEquivalent = calcBuffPermanentEquivalent(buff, apEff, cd, seconds, config.buff_value); const duration = config.buff_duration ?? buff.duration; - const uptime = Math.min(duration / FightSet.FiIGHT_TIME, 1); + const uptime = seconds > 0 ? Math.min(duration / seconds, 1) : 0; return permanentEquivalent * uptime; } } switch (config.buff_type) { case Attrs.ap: - return (value / cd) * weights.W_DPS; + return (value / cd) * weights.W_DPS_PER_SEC * seconds; case Attrs.hp_max: case Attrs.hp: return value * weights.W_HP; case Attrs.critical: - return (value / 100) * (apEff / cd) * 0.5 * weights.W_DPS; + return (value / 100) * (apEff / cd) * 0.5 * weights.W_DPS_PER_SEC * seconds; case Attrs.critical_damage: - return (value / 100) * (apEff / cd) * 0.05 * weights.W_DPS; + return (value / 100) * (apEff / cd) * 0.05 * weights.W_DPS_PER_SEC * seconds; case Attrs.stun_chance: case Attrs.freeze_chance: case Attrs.knockback_chance: return value * weights.W_control; case Attrs.speed: - return (value / 100) * (apEff / cd) * weights.W_DPS; + return (value / 100) * (apEff / cd) * weights.W_DPS_PER_SEC * seconds; default: return value * weights.W_control; } } -/** 计算单条技能配置的效果价值 */ -function calcSkillEffectValue(skill: SkillConfig, overrides: SkillOverrides | undefined, hero: heroInfo, targetLv: number): number { +/** 计算单条技能配置单次触发的效果价值 */ +function calcSkillEffectValue(skill: SkillConfig, overrides: SkillOverrides | undefined, hero: heroInfo, targetLv: number, seconds: number): number { const { ap: apEff } = getEffectiveStats(hero, targetLv); const cd = getAttackCd(hero); const config = overrides ? mergeSkillParams(skill, overrides) : skill; @@ -874,49 +921,86 @@ function calcSkillEffectValue(skill: SkillConfig, overrides: SkillOverrides | un switch (config.kind) { case SkillKind.Damage: - return ((config.ap || 0) / 100) * apEff; + return ((config.ap || 0) / 100) * apEff * weights.W_DPS_PER_SEC; case SkillKind.Heal: return ((config.ap || 0) / 100) * apEff * weights.W_heal; case SkillKind.Shield: return (config.ap || 0) * weights.expectedEnemyDmg * weights.W_shield; case SkillKind.Support: - return calcSupportEffectValue(config, apEff, cd); + return calcSupportEffectValue(config, apEff, cd, seconds); default: return 0; } } -/** 计算普通触发类技能(atking/atked/fstart/fend/dead)的价值 */ -function calcTriggerPower( +/** 触发技评估项 */ +export interface TriggerSkillEval { + type: SkillTriggerType; + s_uuid: number; + name: string; + t_num: number; + value: number; +} + +/** 驻场光环评估项 */ +export interface FieldSkillEval { + uuid: number; + name: string; + value: number; +} + +/** 英雄强度分数构成 */ +export interface HeroPowerBreakdown { + hp: number; + dps: number; + atking: number; + atked: number; + fstart: number; + fend: number; + dead: number; + field: number; + revive: number; +} + +/** 英雄强度详细评估结果 */ +export interface HeroPowerDetailed { + total: number; + breakdown: HeroPowerBreakdown; + triggerSkills: TriggerSkillEval[]; + fieldSkills: FieldSkillEval[]; + normalAttack: { s_uuid: number, name: string, cd: number, dps: number, value: number }; +} + +/** 计算普通触发类技能(atking/atked/fstart/fend/dead)的详细价值 */ +function calcTriggerPowerDetailed( triggerConfigs: { s_uuid: number, t_num?: number, overrides?: SkillOverrides }[], hero: heroInfo, targetLv: number, - type: SkillTriggerType -): number { - if (!triggerConfigs || triggerConfigs.length === 0) return 0; + type: SkillTriggerType, + seconds: number +): { total: number, items: TriggerSkillEval[] } { + if (!triggerConfigs || triggerConfigs.length === 0) return { total: 0, items: [] }; const weights = HERO_POWER_WEIGHTS; let total = 0; + const items: TriggerSkillEval[] = []; for (const trig of triggerConfigs) { const skill = SkillSet[trig.s_uuid]; if (!skill) continue; - const effectValue = calcSkillEffectValue(skill, trig.overrides, hero, targetLv); + const effectValue = calcSkillEffectValue(skill, trig.overrides, hero, targetLv, seconds); const overrides = trig.overrides || {}; const group = overrides.TGroup ?? skill.TGroup; const scopeMul = calcScopeMul(group); const tNum = trig.t_num || 1; let weight = 1; - let frequency = 1; switch (type) { case SkillTriggerType.Atking: weight = weights.W_atking; - frequency = 1 / tNum; break; case SkillTriggerType.Atked: weight = weights.W_atked; - frequency = 1 / tNum; break; case SkillTriggerType.FStart: weight = weights.W_fstart; @@ -929,27 +1013,30 @@ function calcTriggerPower( break; } - total += effectValue * frequency * scopeMul * weight; + const triggerCount = getTriggerCount(type, tNum, hero, seconds); + const value = effectValue * triggerCount * scopeMul * weight; + total += value; + items.push({ type, s_uuid: trig.s_uuid, name: skill.name, t_num: tNum, value }); } - return total; + return { total, items }; } -/** 计算驻场光环技能的单位价值 */ -function calcFieldSkillValue(field: FieldSkillConfig, hpEff: number, apEff: number, cd: number, scopeMul: number): number { +/** 计算驻场光环技能在时间窗内的总价值 */ +function calcFieldSkillValue(field: FieldSkillConfig, hpEff: number, apEff: number, cd: number, scopeMul: number, seconds: number): number { const weights = HERO_POWER_WEIGHTS; const dps = apEff / cd; switch (field.type) { case FieldSkillType.HeroAtk: - return field.value * dps * scopeMul * weights.W_DPS; + return field.value * dps * scopeMul * weights.W_DPS_PER_SEC * seconds; case FieldSkillType.HeroHp: return field.value * hpEff * scopeMul * weights.W_HP; case FieldSkillType.HeroSpeed: - return field.value * dps * scopeMul * weights.W_DPS; + return field.value * dps * scopeMul * weights.W_DPS_PER_SEC * seconds; case FieldSkillType.HeroCrit: - return field.value * dps * scopeMul * 0.5 * weights.W_DPS; + return field.value * dps * scopeMul * 0.5 * weights.W_DPS_PER_SEC * seconds; case FieldSkillType.HeroCritDamage: - return field.value * dps * scopeMul * 0.05 * weights.W_DPS; + return field.value * dps * scopeMul * 0.05 * weights.W_DPS_PER_SEC * seconds; case FieldSkillType.HeroStun: case FieldSkillType.HeroWindFury: case FieldSkillType.HeroPuncture: @@ -959,48 +1046,112 @@ function calcFieldSkillValue(field: FieldSkillConfig, hpEff: number, apEff: numb } } -/** 计算驻场光环总价值 */ -function calcFieldPower(fieldUuids: number[] | undefined, hero: heroInfo, targetLv: number): number { - if (!fieldUuids || fieldUuids.length === 0) return 0; +/** 计算驻场光环的详细价值 */ +function calcFieldPowerDetailed(fieldUuids: number[] | undefined, hero: heroInfo, targetLv: number, seconds: number): { total: number, items: FieldSkillEval[] } { + if (!fieldUuids || fieldUuids.length === 0) return { total: 0, items: [] }; const weights = HERO_POWER_WEIGHTS; const { hp: hpEff, ap: apEff } = getEffectiveStats(hero, targetLv); const cd = getAttackCd(hero); const scopeMul = FightSet.HERO_MAX_NUM; let total = 0; + const items: FieldSkillEval[] = []; for (const uuid of fieldUuids) { const field = FieldSkillSet[uuid]; if (!field) continue; - total += calcFieldSkillValue(field, hpEff, apEff, cd, scopeMul); + const value = calcFieldSkillValue(field, hpEff, apEff, cd, scopeMul, seconds) * weights.W_field_alive; + total += value; + items.push({ uuid, name: field.name, value }); } - return total * weights.W_field_alive; + return { total, items }; +} + +/** 获取普通攻击信息 */ +function getNormalAttackInfo(hero: heroInfo, targetLv: number = 1, seconds: number = HERO_POWER_WEIGHTS.DEFAULT_EVAL_SECONDS): { s_uuid: number, name: string, cd: number, dps: number, value: number } { + const weights = HERO_POWER_WEIGHTS; + const { ap: apEff } = getEffectiveStats(hero, targetLv); + const cd = getAttackCd(hero); + const skillIds = Object.keys(hero.skills); + const firstKey = skillIds.length > 0 ? Number(skillIds[0]) : 0; + const skill = firstKey ? SkillSet[firstKey] : undefined; + const dps = apEff / cd; + return { + s_uuid: firstKey, + name: skill?.name ?? '普攻', + cd, + dps, + value: dps * weights.W_DPS_PER_SEC * seconds, + }; } /** - * 计算英雄强度评分(power score)。 + * 计算英雄强度详细评估(含分数构成与每个技能价值)。 + * @param hero 英雄/怪物配置 + * @param targetLv 目标等级,默认 1 + * @param seconds 评估时间窗(秒),默认 HERO_POWER_WEIGHTS.DEFAULT_EVAL_SECONDS + * @returns 详细强度评估结果 + */ +export function calcHeroPowerDetailed(hero: heroInfo, targetLv: number = 1, seconds: number = HERO_POWER_WEIGHTS.DEFAULT_EVAL_SECONDS): HeroPowerDetailed { + const weights = HERO_POWER_WEIGHTS; + const { hp: hpEff, ap: apEff } = getEffectiveStats(hero, targetLv); + const cd = getAttackCd(hero); + + const hpValue = hpEff * weights.W_HP; + const dpsValue = (apEff / cd) * weights.W_DPS_PER_SEC * seconds; + const normalAttack = getNormalAttackInfo(hero, targetLv, seconds); + + const atking = hero.atking ? calcTriggerPowerDetailed(hero.atking, hero, targetLv, SkillTriggerType.Atking, seconds) : { total: 0, items: [] as TriggerSkillEval[] }; + const atked = hero.atked ? calcTriggerPowerDetailed(hero.atked, hero, targetLv, SkillTriggerType.Atked, seconds) : { total: 0, items: [] as TriggerSkillEval[] }; + const fstart = hero.fstart ? calcTriggerPowerDetailed(hero.fstart, hero, targetLv, SkillTriggerType.FStart, seconds) : { total: 0, items: [] as TriggerSkillEval[] }; + const fend = hero.fend ? calcTriggerPowerDetailed(hero.fend, hero, targetLv, SkillTriggerType.FEnd, seconds) : { total: 0, items: [] as TriggerSkillEval[] }; + const dead = hero.dead ? calcTriggerPowerDetailed(hero.dead, hero, targetLv, SkillTriggerType.Dead, seconds) : { total: 0, items: [] as TriggerSkillEval[] }; + const field = hero.field ? calcFieldPowerDetailed(hero.field, hero, targetLv, seconds) : { total: 0, items: [] as FieldSkillEval[] }; + + let reviveValue = 0; + if (hero.revive) { + reviveValue = hpEff * hero.revive.upr * hero.revive.r_num * weights.W_revive * getWaveCount(seconds); + } + + const total = hpValue + dpsValue + atking.total + atked.total + fstart.total + fend.total + dead.total + field.total + reviveValue; + + return { + total, + breakdown: { + hp: hpValue, + dps: dpsValue, + atking: atking.total, + atked: atked.total, + fstart: fstart.total, + fend: fend.total, + dead: dead.total, + field: field.total, + revive: reviveValue, + }, + triggerSkills: [...atking.items, ...atked.items, ...fstart.items, ...fend.items, ...dead.items], + fieldSkills: field.items, + normalAttack, + }; +} + +/** + * 计算指定时间窗内的英雄强度评分。 + * @param hero 英雄/怪物配置 + * @param targetLv 目标等级,默认 1 + * @param seconds 评估时间窗(秒) + * @returns 无量纲强度分,数值越大越强 + */ +export function calcHeroPowerOverTime(hero: heroInfo, targetLv: number = 1, seconds: number = HERO_POWER_WEIGHTS.DEFAULT_EVAL_SECONDS): number { + return calcHeroPowerDetailed(hero, targetLv, seconds).total; +} + +/** + * 计算英雄强度评分(默认时间窗)。 * @param hero 英雄/怪物配置 * @param targetLv 目标等级,默认 1 * @returns 无量纲强度分,数值越大越强 */ export function calcHeroPower(hero: heroInfo, targetLv: number = 1): number { - const weights = HERO_POWER_WEIGHTS; - const { hp: hpEff, ap: apEff } = getEffectiveStats(hero, targetLv); - const cd = getAttackCd(hero); - - const basePower = hpEff * weights.W_HP + (apEff / cd) * weights.W_DPS; - let triggerPower = 0; - - if (hero.atking) triggerPower += calcTriggerPower(hero.atking, hero, targetLv, SkillTriggerType.Atking); - if (hero.atked) triggerPower += calcTriggerPower(hero.atked, hero, targetLv, SkillTriggerType.Atked); - if (hero.fstart) triggerPower += calcTriggerPower(hero.fstart, hero, targetLv, SkillTriggerType.FStart); - if (hero.fend) triggerPower += calcTriggerPower(hero.fend, hero, targetLv, SkillTriggerType.FEnd); - if (hero.dead) triggerPower += calcTriggerPower(hero.dead, hero, targetLv, SkillTriggerType.Dead); - if (hero.field) triggerPower += calcFieldPower(hero.field, hero, targetLv); - if (hero.revive) { - triggerPower += hpEff * hero.revive.upr * hero.revive.r_num * weights.W_revive; - } - - return basePower + triggerPower; + return calcHeroPowerOverTime(hero, targetLv); } /**