diff --git a/.claude/settings.json b/.claude/settings.json index 9fb27991..5b29d497 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -15,7 +15,8 @@ "WebSearch", "Bash(git add *)", "Bash(git commit *)", - "Bash(npm install *)" + "Bash(npm install *)", + "Bash(npx tsc *)" ], "deny": [ "Bash(rm -rf *)", diff --git a/assets/script/game/common/config/heroSet.ts b/assets/script/game/common/config/heroSet.ts index ffd0e3b8..ac543a03 100644 --- a/assets/script/game/common/config/heroSet.ts +++ b/assets/script/game/common/config/heroSet.ts @@ -1,6 +1,8 @@ import { v3 } from "cc" -import { BoxSet, FacSet } from "./GameSet" -import { SkillOverrides, TGroup } from "./SkillSet" +import { BoxSet, FacSet, FightSet } from "./GameSet" +import { SkillOverrides, TGroup, SkillSet, FieldSkillSet, SkillConfig, SkillKind, FieldSkillConfig, FieldSkillType, mergeSkillParams } from "./SkillSet" +import { BuffList, BuffConfig, ModOp } from "./BuffSet" +import { Attrs } from "./HeroAttrs" export enum HType { Melee = 0, @@ -726,7 +728,293 @@ export const HeroList: number[] = [ // 5072, 5073, 5074, 5075, ]; +// ==================== 英雄强度评分公式 ==================== +/** 英雄强度公式调参权重(可在编辑器或运行时热调) */ +export const HERO_POWER_WEIGHTS = { + W_HP: 1.0, + W_DPS: 30.0, + W_atking: 1.0, + W_atked: 0.8, + W_fstart: 1.0, + W_fend: 1.2, + W_dead: 0.6, + W_field_alive: 1.0, + W_revive: 1.0, + W_heal: 0.8, + W_shield: 1.0, + W_control: 0.5, + expectedEnemyDmg: 20, +}; +/** 计算目标等级下 evolve 提供的累计属性加成 */ +function getEvolveBonuses(hero: heroInfo, targetLv: number): { hp: number, ap: number } { + let hp = 0; + let ap = 0; + if (!hero.evolve) return { hp, ap }; + const maxLv = Math.max(1, Math.min(targetLv, FightSet.HERO_MAX_LV)); + for (let lv = 2; lv <= maxLv; lv++) { + const ev = hero.evolve[lv]; + if (ev) { + hp += ev.hp_bonus || 0; + ap += ev.ap_bonus || 0; + } + } + return { hp, ap }; +} +/** 计算目标等级下的有效 HP/AP(含等级倍率与 evolve 加成) */ +function getEffectiveStats(hero: heroInfo, targetLv: number): { hp: number, ap: number } { + const bonuses = getEvolveBonuses(hero, targetLv); + const lv = Math.max(1, targetLv); + const multiplier = Math.pow(FightSet.HERO_LV_MULTIPLIER, lv - 1); + return { + hp: (hero.hp + bonuses.hp) * multiplier, + ap: (hero.ap + bonuses.ap) * multiplier, + }; +} +/** 获取英雄普通攻击间隔(取 skills[0] 的 cd) */ +function getAttackCd(hero: heroInfo): number { + const skillIds = Object.keys(hero.skills); + if (skillIds.length === 0) return 1.0; + const firstKey = Number(skillIds[0]); + const skill = hero.skills[firstKey]; + return skill?.cd || 1.0; +} + +/** 计算目标群体范围系数 */ +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 { + const weights = HERO_POWER_WEIGHTS; + let value = 0; + if (buff.modifiers) { + for (const mod of buff.modifiers) { + const dps = apEff / cd; + const modValue = buffValueOverride ?? mod.value; + switch (mod.attr) { + case Attrs.ap: + if (mod.op === ModOp.Flat) { + value += (modValue / cd) * weights.W_DPS; + } else if (mod.op === ModOp.PercentAdd) { + value += (modValue / 100) * dps * weights.W_DPS; + } + break; + case Attrs.hp: + case Attrs.hp_max: + value += modValue * weights.W_HP; + break; + case Attrs.critical: + value += (modValue / 100) * dps * 0.5 * weights.W_DPS; + break; + case Attrs.critical_damage: + value += (modValue / 100) * dps * 0.05 * weights.W_DPS; + break; + case Attrs.speed: + value += (modValue / 100) * dps * weights.W_DPS; + break; + } + } + } + if (buff.tick) { + const ticks = Math.max(1, buff.duration / buff.tick.interval); + const totalHeal = ticks * (buff.tick.damage_or_heal + (buff.tick.scale_by_ap_pct / 100) * apEff); + value += Math.max(0, totalHeal) * weights.W_heal; + } + return value; +} + +/** 计算 Support 类技能的效果价值 */ +function calcSupportEffectValue(config: SkillConfig, apEff: number, cd: number): number { + const weights = HERO_POWER_WEIGHTS; + const value = config.ap || 0; + + // 计时 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 duration = config.buff_duration ?? buff.duration; + const uptime = Math.min(duration / FightSet.FiIGHT_TIME, 1); + return permanentEquivalent * uptime; + } + } + + switch (config.buff_type) { + case Attrs.ap: + return (value / cd) * weights.W_DPS; + 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; + case Attrs.critical_damage: + return (value / 100) * (apEff / cd) * 0.05 * weights.W_DPS; + 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; + default: + return value * weights.W_control; + } +} + +/** 计算单条技能配置的效果价值 */ +function calcSkillEffectValue(skill: SkillConfig, overrides: SkillOverrides | undefined, hero: heroInfo, targetLv: number): number { + const { ap: apEff } = getEffectiveStats(hero, targetLv); + const cd = getAttackCd(hero); + const config = overrides ? mergeSkillParams(skill, overrides) : skill; + const weights = HERO_POWER_WEIGHTS; + + switch (config.kind) { + case SkillKind.Damage: + return ((config.ap || 0) / 100) * apEff; + 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); + default: + return 0; + } +} + +/** 计算普通触发类技能(atking/atked/fstart/fend/dead)的价值 */ +function calcTriggerPower( + triggerConfigs: { s_uuid: number, t_num?: number, overrides?: SkillOverrides }[], + hero: heroInfo, + targetLv: number, + type: SkillTriggerType +): number { + if (!triggerConfigs || triggerConfigs.length === 0) return 0; + const weights = HERO_POWER_WEIGHTS; + let total = 0; + + for (const trig of triggerConfigs) { + const skill = SkillSet[trig.s_uuid]; + if (!skill) continue; + + const effectValue = calcSkillEffectValue(skill, trig.overrides, hero, targetLv); + 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; + break; + case SkillTriggerType.FEnd: + weight = weights.W_fend; + break; + case SkillTriggerType.Dead: + weight = weights.W_dead; + break; + } + + total += effectValue * frequency * scopeMul * weight; + } + return total; +} + +/** 计算驻场光环技能的单位价值 */ +function calcFieldSkillValue(field: FieldSkillConfig, hpEff: number, apEff: number, cd: number, scopeMul: 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; + case FieldSkillType.HeroHp: + return field.value * hpEff * scopeMul * weights.W_HP; + case FieldSkillType.HeroSpeed: + return field.value * dps * scopeMul * weights.W_DPS; + case FieldSkillType.HeroCrit: + return field.value * dps * scopeMul * 0.5 * weights.W_DPS; + case FieldSkillType.HeroCritDamage: + return field.value * dps * scopeMul * 0.05 * weights.W_DPS; + case FieldSkillType.HeroStun: + case FieldSkillType.HeroWindFury: + case FieldSkillType.HeroPuncture: + return field.value * scopeMul * weights.W_control * 100; + default: + return field.value * scopeMul * weights.W_control * 10; + } +} + +/** 计算驻场光环总价值 */ +function calcFieldPower(fieldUuids: number[] | undefined, hero: heroInfo, targetLv: number): number { + if (!fieldUuids || fieldUuids.length === 0) return 0; + 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; + + for (const uuid of fieldUuids) { + const field = FieldSkillSet[uuid]; + if (!field) continue; + total += calcFieldSkillValue(field, hpEff, apEff, cd, scopeMul); + } + return total * weights.W_field_alive; +} + +/** + * 计算英雄强度评分(power score)。 + * @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; +} + +/** + * 归一化英雄强度评分。 + * @param hero 英雄配置 + * @param targetLv 目标等级 + * @param baseHeroId 基准英雄 uuid + * @param baseScore 基准分 + * @returns 以基准英雄为 baseScore 的相对分数 + */ +export function calcHeroPowerNormalized(hero: heroInfo, targetLv: number = 1, baseHeroId: number = 5011, baseScore: number = 100): number { + const baseHero = HeroInfo[baseHeroId]; + if (!baseHero) return 0; + const basePower = calcHeroPower(baseHero, targetLv); + if (basePower === 0) return 0; + return calcHeroPower(hero, targetLv) / basePower * baseScore; +}