feat(hero config): add hero power score calculation system
新增英雄强度评分系统,实现了基于属性、技能、buff的量化战力计算,支持归一化相对评分。同时添加了npx tsc命令到允许的bash操作列表中。
This commit is contained in:
@@ -15,7 +15,8 @@
|
||||
"WebSearch",
|
||||
"Bash(git add *)",
|
||||
"Bash(git commit *)",
|
||||
"Bash(npm install *)"
|
||||
"Bash(npm install *)",
|
||||
"Bash(npx tsc *)"
|
||||
],
|
||||
"deny": [
|
||||
"Bash(rm -rf *)",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user