Files
pixelheros/assets/script/game/common/config/TalentSet.ts
panFD 707e25e73f refactor: 重构天赋系统与技能触发逻辑
1.  重构天赋系统架构:将天赋加成与触发修正统一收敛到 HeroAttrsComp.applyTalent
2.  新增天赋触发修正字段:talent_t_num_reduce 与 talent_extra_trigger_chance
3.  重构天赋加点逻辑:统一 1 点/级,新增软前置校验
4.  优化天赋UI组件:改为编辑器静态配置驱动,重构层位展示逻辑
5.  修复技能触发判定:在 SkillTriggerHelper 中应用天赋触发修正
6.  调整天赋配置结构:改为 5 层递进解锁,新增 L4/L5 专属触发修正配置
2026-09-19 11:20:55 +08:00

521 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Attrs } from "./HeroAttrs";
import { HRole } from "./heroSet";
/**
* 天赋系统配置(局外成长:升级即激活,战斗中对英雄属性常驻加成)。
*
* 五层递进结构(逐层解锁,门槛见 TALENT_TIER_UNLOCK_THRESHOLD):
* - 第 1 层(9001-9099):恒解锁,建议放置全局基础属性;
* - 第 2 层(9101-9199):第 1 层累计投入 ≥5 点解锁;
* - 第 3 层(9201-9299):第 2 层累计投入 ≥5 点解锁;
* - 第 4 层(9301-9399):第 3 层累计投入 ≥5 点解锁;
* - 第 5 层(9401-9499):第 4 层累计投入 ≥5 点解锁。
*
* 解锁规则:第 1 层恒可升级;第 N 层需第 N-1 层累计投入 ≥5 点。
* 前置天赋(prereq_uuid/prereq_lv)为软前置:仅限制加点,不影响已有点数的战斗结算。
*
* 结算口径(与项目 buff 规范一致):
* - percent 模式:按英雄基础值(base_ap/base_hp/hero.def)相对放大;
* - flat 模式:概率/抗性类按百分点累加,defense 按固定点数累加。
*/
/** 天赋数值模式:percent=按基础值百分比放大;flat=固定值/百分点累加 */
export enum TalentValueMode {
Percent = 0,
Flat = 1,
}
/** 天赋层位:第 1 层 ~ 第 5 层递进解锁 */
export enum TalentTier {
/** 第 1 层:恒解锁 */
L1 = 1,
/** 第 2 层:第 1 层投入 5 点解锁 */
L2 = 2,
/** 第 3 层:第 2 层投入 5 点解锁 */
L3 = 3,
/** 第 4 层:第 3 层投入 5 点解锁 */
L4 = 4,
/** 第 5 层:第 4 层投入 5 点解锁 */
L5 = 5,
}
/** 单条属性加成词条 */
export interface TalentAttrBonus {
/** 目标属性(Attrs 枚举键) */
attr: Attrs;
/** 数值模式 */
mode: TalentValueMode;
/** 每级数值(percent 模式为百分比;flat 模式为百分点/固定值) */
per_lv: number;
}
/** 天赋配置 */
export interface TalentConfig {
/** 天赋 uuid(段位见 TalentSet 注释) */
uuid: number;
/** 所属层位(升级校验:升下一层天赋需前一层累计投入点数达标) */
tier: TalentTier;
/** 天赋名称 */
name: string;
/** 描述(UI 展示,{value}/{value2} 为按当前等级计算后的数值占位) */
info: string;
/** 图标(uiconsAtlas 帧名,缺省时 UI 端按资源目录回退) */
icon?: string;
/** 属性加成词条列表(每级叠加) */
bonuses: TalentAttrBonus[];
/** 最大等级 */
max_lv: number;
/** 专精/职业天赋:仅对该职业英雄生效;全局天赋省略此字段 */
role?: HRole;
/** 软前置:前置天赋 uuid(与 prereq_lv 成对出现;缺省视为无前置) */
prereq_uuid?: number;
/** 软前置:前置天赋要求达到的等级(仅限制加点,不影响已有点数的结算) */
prereq_lv?: number;
/** L4 触发修正:触发类技能计数阈值减少量(每级,下限 1;与 bonuses 互斥) */
t_num_reduce_per_lv?: number;
/** L5 触发修正:触发类技能额外触发 1 次的概率(每级百分点;与 bonuses 互斥) */
extra_trigger_chance_per_lv?: number;
}
/** 单条已解析的加成(等级已乘算) */
export interface TalentResolvedBonus {
attr: Attrs;
mode: TalentValueMode;
/** 总值 = per_lv × 天赋等级 */
value: number;
}
/**
* 天赋加成作用目标的最小结构接口(TS 结构化类型,HeroAttrsComp 天然满足)。
* Why: 用结构化接口替代直接 import HeroAttrsComp,避免 config → hero → SingletonModuleComp → config 的循环依赖。
*/
export interface ITalentTarget {
ap: number;
hp: number;
hp_max: number;
defense: number;
/** 基础攻击快照(percent 模式的基数) */
base_ap: number;
/** 基础血量快照(percent 模式的基数) */
base_hp: number;
/** L4 写入:触发类技能计数阈值减少量 */
talent_t_num_reduce: number;
/** L5 写入:触发类技能额外触发 1 次的概率(百分点) */
talent_extra_trigger_chance: number;
/** 防御基础值(percent 模式的基数,等于 hero.def) */
[key: string]: any;
}
/**
* 天赋配置表。
* uuid 段位:9001-9099 第 1 层;9101-9199 第 2 层;9201-9299 第 3 层;
* 9301-9399 第 4 层;9401-9499 第 5 层。
* 每层建议放置 4-6 个天赋;前置天赋用 prereq_uuid + prereq_lv 声明(软前置,仅限制加点)。
* icon 复用 SkillSet/uiconsAtlas 既有帧名,后续可在编辑器侧替换为专属图标。
*/
export const TalentSet: Record<number, TalentConfig> = {
// ==================== 第 1 层(9001-9099,恒解锁,基础属性) ====================
9001: {
uuid: 9001, tier: TalentTier.L1, name: "攻击强化", info: "所有英雄攻击 +{value}%", icon: "3-Aggression",
bonuses: [{ attr: Attrs.ap, mode: TalentValueMode.Percent, per_lv: 3 }],
max_lv: 5,
},
9002: {
uuid: 9002, tier: TalentTier.L1, name: "生命强化", info: "所有英雄生命 +{value}%", icon: "29-Endure",
bonuses: [{ attr: Attrs.hp_max, mode: TalentValueMode.Percent, per_lv: 5 }],
max_lv: 5,
},
9003: {
uuid: 9003, tier: TalentTier.L1, name: "防御强化", info: "所有英雄防御 +{value} 点", icon: "46-Iron-Clad",
bonuses: [{ attr: Attrs.defense, mode: TalentValueMode.Flat, per_lv: 8 }],
max_lv: 5,
},
9004: {
uuid: 9004, tier: TalentTier.L1, name: "反伤荆棘", info: "所有英雄反伤 +{value} 点", icon: "45-Induration",
bonuses: [{ attr: Attrs.thorns_dmg, mode: TalentValueMode.Flat, per_lv: 5 }],
max_lv: 5,
},
// ==================== 第 2 层(9101-9199,第 1 层投入 5 点解锁,概率属性) ====================
9101: {
uuid: 9101, tier: TalentTier.L2, name: "暴击强化", info: "所有英雄暴击率 +{value}%", icon: "27-Hawkeye",
bonuses: [{ attr: Attrs.critical, mode: TalentValueMode.Flat, per_lv: 2 }],
max_lv: 5,
},
9102: {
uuid: 9102, tier: TalentTier.L2, name: "击退强化", info: "所有英雄击退距离 +{value}", icon: "34-Force-Push",
bonuses: [{ attr: Attrs.knockback_distance, mode: TalentValueMode.Flat, per_lv: 10 }],
max_lv: 5,
},
9103: {
uuid: 9103, tier: TalentTier.L2, name: "冰冻强化", info: "所有英雄冰冻率 +{value}%", icon: "92-Frost-Bite",
bonuses: [{ attr: Attrs.freeze_chance, mode: TalentValueMode.Flat, per_lv: 2 }],
max_lv: 5,
},
9104: {
uuid: 9104, tier: TalentTier.L2, name: "击晕强化", info: "所有英雄击晕率 +{value}%", icon: "13-Concussion",
bonuses: [{ attr: Attrs.stun_chance, mode: TalentValueMode.Flat, per_lv: 2 }],
max_lv: 5,
},
9105: {
uuid: 9105, tier: TalentTier.L2, name: "穿透强化", info: "所有英雄穿透率 +{value}%", icon: "56-Pierce-Shot",
bonuses: [{ attr: Attrs.puncture_chance, mode: TalentValueMode.Flat, per_lv: 2 }],
max_lv: 5,
},
9106: {
uuid: 9106, tier: TalentTier.L2, name: "暴伤强化", info: "所有英雄暴击伤害 +{value}%", icon: "37-Head-Shot",
bonuses: [{ attr: Attrs.critical_damage, mode: TalentValueMode.Flat, per_lv: 5 }],
max_lv: 5,
},
// ==================== 第 3 层(9201-9299,第 2 层投入 5 点解锁,职业加成) ====================
9201: {
uuid: 9201, tier: TalentTier.L3, name: "坦克专精", info: "坦克生命 +{value}%、防御 +{value2} 点", icon: "36-Guardian-Stance",
bonuses: [
{ attr: Attrs.hp_max, mode: TalentValueMode.Percent, per_lv: 8 },
{ attr: Attrs.defense, mode: TalentValueMode.Flat, per_lv: 10 },
],
max_lv: 5, role: HRole.Tank,
},
9202: {
uuid: 9202, tier: TalentTier.L3, name: "战士专精", info: "战士攻击 +{value}%、生命 +{value2}%", icon: "1-Battle-Roar",
bonuses: [
{ attr: Attrs.ap, mode: TalentValueMode.Percent, per_lv: 6 },
{ attr: Attrs.hp_max, mode: TalentValueMode.Percent, per_lv: 6 },
],
max_lv: 5, role: HRole.Warrior,
},
9203: {
uuid: 9203, tier: TalentTier.L3, name: "刺客专精", info: "刺客攻击 +{value}%、攻速 +{value2}%", icon: "127-Hide",
bonuses: [
{ attr: Attrs.ap, mode: TalentValueMode.Percent, per_lv: 8 },
{ attr: Attrs.speed, mode: TalentValueMode.Flat, per_lv: 3 },
],
max_lv: 5, role: HRole.Assassin,
},
9204: {
uuid: 9204, tier: TalentTier.L3, name: "射手专精", info: "射手攻击 +{value}%、攻速 +{value2}%", icon: "56-Pierce-Shot",
bonuses: [
{ attr: Attrs.ap, mode: TalentValueMode.Percent, per_lv: 8 },
{ attr: Attrs.speed, mode: TalentValueMode.Flat, per_lv: 3 },
],
max_lv: 5, role: HRole.Archer,
},
9205: {
uuid: 9205, tier: TalentTier.L3, name: "法师专精", info: "法师攻击 +{value}%、技能增益效果 +{value2}%", icon: "145-Electromancy",
bonuses: [
{ attr: Attrs.ap, mode: TalentValueMode.Percent, per_lv: 8 },
{ attr: Attrs.buff_power, mode: TalentValueMode.Flat, per_lv: 6 },
],
max_lv: 5, role: HRole.Mage,
},
9206: {
uuid: 9206, tier: TalentTier.L3, name: "辅助专精", info: "辅助治疗量 +{value}%、增益效果 +{value2}%", icon: "38-Healing-Light",
bonuses: [
{ attr: Attrs.heal_power, mode: TalentValueMode.Flat, per_lv: 10 },
{ attr: Attrs.buff_power, mode: TalentValueMode.Flat, per_lv: 6 },
],
max_lv: 5, role: HRole.Support,
},
// ==================== 第 4 层(9301-9399,第 3 层投入 5 点解锁,触发条件减少,按职业) ====================
9301: {
uuid: 9301, tier: TalentTier.L4, name: "坦克·蓄势", info: "坦克触发类技能所需次数 -{value}", icon: "36-Guardian-Stance",
bonuses: [], t_num_reduce_per_lv: 1,
max_lv: 2, role: HRole.Tank,
},
9302: {
uuid: 9302, tier: TalentTier.L4, name: "战士·蓄势", info: "战士触发类技能所需次数 -{value}", icon: "113-Rage",
bonuses: [], t_num_reduce_per_lv: 1,
max_lv: 2, role: HRole.Warrior,
},
9303: {
uuid: 9303, tier: TalentTier.L4, name: "刺客·蓄势", info: "刺客触发类技能所需次数 -{value}", icon: "136-Heart-Rip",
bonuses: [], t_num_reduce_per_lv: 1,
max_lv: 2, role: HRole.Assassin,
},
9304: {
uuid: 9304, tier: TalentTier.L4, name: "射手·蓄势", info: "射手触发类技能所需次数 -{value}", icon: "74-Satisfaction",
bonuses: [], t_num_reduce_per_lv: 1,
max_lv: 2, role: HRole.Archer,
},
9305: {
uuid: 9305, tier: TalentTier.L4, name: "法师·蓄势", info: "法师触发类技能所需次数 -{value}", icon: "144-Cryomancy",
bonuses: [], t_num_reduce_per_lv: 1,
max_lv: 2, role: HRole.Mage,
},
9306: {
uuid: 9306, tier: TalentTier.L4, name: "辅助·蓄势", info: "辅助触发类技能所需次数 -{value}", icon: "40-Holy",
bonuses: [], t_num_reduce_per_lv: 1,
max_lv: 2, role: HRole.Support,
},
// ==================== 第 5 层(9401-9499,第 4 层投入 5 点解锁,概率额外触发,按职业) ====================
9401: {
uuid: 9401, tier: TalentTier.L5, name: "坦克·余震", info: "坦克触发类技能 +{value}% 概率额外触发 1 次", icon: "36-Guardian-Stance",
bonuses: [], extra_trigger_chance_per_lv: 10,
max_lv: 5, role: HRole.Tank, prereq_uuid: 9301, prereq_lv: 1,
},
9402: {
uuid: 9402, tier: TalentTier.L5, name: "战士·余威", info: "战士触发类技能 +{value}% 概率额外触发 1 次", icon: "113-Rage",
bonuses: [], extra_trigger_chance_per_lv: 10,
max_lv: 5, role: HRole.Warrior, prereq_uuid: 9302, prereq_lv: 1,
},
9403: {
uuid: 9403, tier: TalentTier.L5, name: "刺客·残影", info: "刺客触发类技能 +{value}% 概率额外触发 1 次", icon: "136-Heart-Rip",
bonuses: [], extra_trigger_chance_per_lv: 10,
max_lv: 5, role: HRole.Assassin, prereq_uuid: 9303, prereq_lv: 1,
},
9404: {
uuid: 9404, tier: TalentTier.L5, name: "射手·连珠", info: "射手触发类技能 +{value}% 概率额外触发 1 次", icon: "74-Satisfaction",
bonuses: [], extra_trigger_chance_per_lv: 10,
max_lv: 5, role: HRole.Archer, prereq_uuid: 9304, prereq_lv: 1,
},
9405: {
uuid: 9405, tier: TalentTier.L5, name: "法师·回响", info: "法师触发类技能 +{value}% 概率额外触发 1 次", icon: "144-Cryomancy",
bonuses: [], extra_trigger_chance_per_lv: 10,
max_lv: 5, role: HRole.Mage, prereq_uuid: 9305, prereq_lv: 1,
},
9406: {
uuid: 9406, tier: TalentTier.L5, name: "辅助·福音", info: "辅助触发类技能 +{value}% 概率额外触发 1 次", icon: "40-Holy",
bonuses: [], extra_trigger_chance_per_lv: 10,
max_lv: 5, role: HRole.Support, prereq_uuid: 9306, prereq_lv: 1,
},
};
/** 天赋升级消耗:全部天赋统一 1 点/级 */
export const TALENT_UPGRADE_COST = 1;
/**
* 查询升级所需天赋点数
* @param _tier 天赋所属层位(保留参数以兼容旧调用;统一点价后不再按层区分)
* @returns 升 1 级所需点数(恒为 1)
*/
export function getTalentUpgradeCost(_tier: TalentTier): number {
return TALENT_UPGRADE_COST;
}
/** 天赋等级上限(不含):等级从 0 起步,区间 0~29,共 30 级;每升 1 级产出 1 点天赋点 */
export const TALENT_LEVEL_MAX = 30;
/** 天赋升级经验表:索引为当前等级(0~28),值为升到下一级所需经验 */
export const TALENT_LEVEL_EXP: number[] = [
100, 120, 140, 170, 200, 240, 280, 330, 380, 440,
510, 580, 660, 750, 850, 960, 1080, 1210, 1350, 1500,
1670, 1850, 2050, 2270, 2510, 2770, 3060, 3380, 3730,
];
/**
* 查询天赋升到下一级所需经验
* @param curLv 当前等级(0 起步)
* @returns 升到下一级所需经验;已满级或非法等级返回 -1
*/
export function getTalentLevelExp(curLv: number): number {
if (curLv < 0 || curLv >= TALENT_LEVEL_EXP.length) return -1;
return TALENT_LEVEL_EXP[curLv];
}
/** 层位解锁门槛:前一层累计投入 ≥ 该点数才能升级下一层天赋 */
export const TALENT_TIER_UNLOCK_THRESHOLD = 5;
/**
* 计算指定层位已投入的天赋点数(统一点价 × 等级累加)。
* Why: 层位解锁按"实际消耗点数"而非"等级总和"口径——与玩家直觉(投入了多少点)
* 及重置退款口径(resetTalents 同表对账)保持一致。
* @param levels 天赋等级表(key 为天赋 uuid,value 为当前等级)
* @param tier 目标层位
* @returns 该层位全部天赋已投入的点数总和
*/
export function getTalentTierInvestedPoints(levels: Record<number, number>, tier: TalentTier): number {
if (!levels) return 0;
let invested = 0;
for (const key in levels) {
const cfg = TalentSet[key];
if (!cfg || cfg.tier !== tier) continue;
invested += levels[key] * TALENT_UPGRADE_COST;
}
return invested;
}
/**
* 判断指定层位是否已解锁(加点入口校验用;战斗结算无需关心层位——已投入的等级恒生效)。
* 规则:第 1 层恒解锁;第 N 层要求第 N-1 层累计投入 ≥ TALENT_TIER_UNLOCK_THRESHOLD。
* @param levels 天赋等级表
* @param tier 目标层位
*/
export function isTalentTierUnlocked(levels: Record<number, number>, tier: TalentTier): boolean {
if (tier === TalentTier.L1) return true;
const prevTier = (tier - 1) as TalentTier;
return getTalentTierInvestedPoints(levels, prevTier) >= TALENT_TIER_UNLOCK_THRESHOLD;
}
/**
* 判断天赋的软前置是否已满足(加点入口校验用;未满足仅限制加点,不影响已有点数的结算)。
* @param levels 天赋等级表
* @param cfg 天赋配置
* @returns 无前置声明或前置天赋等级达标时返回 true
*/
export function isTalentPrereqMet(levels: Record<number, number>, cfg: TalentConfig): boolean {
if (!cfg.prereq_uuid || !cfg.prereq_lv) return true;
return (levels?.[cfg.prereq_uuid] ?? 0) >= cfg.prereq_lv;
}
/** 天赋属性的卡面显示名(迷你卡空间有限,用短标签) */
const TALENT_ATTR_LABELS: Partial<Record<Attrs, string>> = {
[Attrs.ap]: "攻击",
[Attrs.hp_max]: "生命",
[Attrs.defense]: "防御",
[Attrs.critical]: "暴击",
[Attrs.critical_damage]: "暴伤",
[Attrs.stun_chance]: "击晕",
[Attrs.freeze_chance]: "冰冻",
[Attrs.knockback_distance]: "击退",
[Attrs.puncture_chance]: "穿透",
[Attrs.puncture_dmg_bonus]: "穿透伤",
[Attrs.heal_power]: "治疗",
[Attrs.buff_power]: "增益",
};
/** 天赋属性的单位后缀:概率/治疗/增益类为百分点(%),防御为"点",击退距离为纯数值 */
const TALENT_ATTR_SUFFIX: Partial<Record<Attrs, string>> = {
[Attrs.defense]: "点",
[Attrs.knockback_distance]: "",
};
/**
* 生成天赋迷你卡的卡面文案(自包含:不显示满级效果)。
* 规则:已加点(lv>0)显示当前已生效数值,如「攻击 +9%」;
* 未加点(lv=0)显示每级数值引导,如「攻击 +3%/级」。
* Why: 迷你点选卡没有二级详情入口,文案必须一眼看懂当前收益。
* @param cfg 天赋配置
* @param lv 当前等级
*/
export function getTalentDisplayText(cfg: TalentConfig, lv: number): string {
const mult = lv > 0 ? lv : 1;
// L4/L5 效果词条:不走 bonuses,按专属字段生成文案
if (cfg.t_num_reduce_per_lv) {
const text = `触发次数 -${cfg.t_num_reduce_per_lv * mult}`;
return lv > 0 ? text : `${text}/级`;
}
if (cfg.extra_trigger_chance_per_lv) {
const text = `额外触发 +${cfg.extra_trigger_chance_per_lv * mult}%`;
return lv > 0 ? text : `${text}/级`;
}
const parts: string[] = [];
for (const b of cfg.bonuses) {
const label = TALENT_ATTR_LABELS[b.attr] ?? String(b.attr);
const suffix = TALENT_ATTR_SUFFIX[b.attr] ?? "%";
parts.push(`${label} +${b.per_lv * mult}${suffix}`);
}
const text = parts.join("、");
return lv > 0 ? text : `${text}/级`;
}
/**
* 聚合指定职业英雄生效的全部天赋加成。
* Why: 全局天赋全局生效,职业天赋按 role 过滤;
* 层位解锁与软前置只约束加点阶段,战斗加载时(Hero.load / applyHeroLevel)按当前等级直接取加成。
* @param levels 天赋等级表(key 为天赋 uuid,value 为当前等级)
* @param role 英雄职业(职业过滤依据;无 role 时只应用全局天赋)
* @returns 已解析的加成列表(等级已乘算进 value)
*/
export function getTalentBonusesForRole(levels: Record<number, number>, role?: HRole): TalentResolvedBonus[] {
const result: TalentResolvedBonus[] = [];
if (!levels) return result;
for (const key in levels) {
const lv = levels[key];
if (lv <= 0) continue;
const cfg = TalentSet[key];
if (!cfg) continue;
// 职业天赋:职业不匹配则跳过
if (cfg.role !== undefined && cfg.role !== role) continue;
for (const b of cfg.bonuses) {
result.push({ attr: b.attr, mode: b.mode, value: b.per_lv * lv });
}
}
return result;
}
/**
* 聚合指定职业英雄的 L4 触发阈值修正量。
* @param levels 天赋等级表
* @param role 英雄职业
* @returns 触发计数阈值的总减少量(SkillTriggerHelper 端再 clamp 下限 1)
*/
export function getTalentTriggerNumReduce(levels: Record<number, number>, role?: HRole): number {
let total = 0;
if (!levels) return total;
for (const key in levels) {
const lv = levels[key];
if (lv <= 0) continue;
const cfg = TalentSet[key];
if (!cfg || !cfg.t_num_reduce_per_lv) continue;
if (cfg.role !== undefined && cfg.role !== role) continue;
total += cfg.t_num_reduce_per_lv * lv;
}
return total;
}
/**
* 聚合指定职业英雄的 L5 额外触发概率(百分点)。
* @param levels 天赋等级表
* @param role 英雄职业
* @returns 额外触发 1 次的概率(百分点,可超过 100 由消费端自行 clamp)
*/
export function getTalentExtraTriggerChance(levels: Record<number, number>, role?: HRole): number {
let total = 0;
if (!levels) return total;
for (const key in levels) {
const lv = levels[key];
if (lv <= 0) continue;
const cfg = TalentSet[key];
if (!cfg || !cfg.extra_trigger_chance_per_lv) continue;
if (cfg.role !== undefined && cfg.role !== role) continue;
total += cfg.extra_trigger_chance_per_lv * lv;
}
return total;
}
/**
* 将天赋加成写入英雄属性模型(统一由 HeroAttrsComp.applyTalent 调用)。
*
* 结算规则:
* - Percent 模式:以基础快照为基数(ap→base_ap,hp_max→base_hp,defense→def_base),按百分比放大后累加;
* - Flat 模式:直接按属性名动态累加(概率/抗性类为百分点,defense 为固定点数)。
*
* Why: percent 必须用基础快照而非当前 ap/hp_max 作基数——当前值已含驻场光环与成长增量,
* 若以当前值为基数会与光环乘区叠加成乘法,导致数值膨胀失控。
*
* @param model 英雄属性模型(满足 ITalentTarget 结构)
* @param levels 天赋等级表(smc.getTalentLevels())
* @param role 英雄职业(职业过滤;无 role 的怪物由调用方跳过)
*/
export function applyTalentBonuses(model: ITalentTarget, levels: Record<number, number>, role?: HRole): void {
const bonuses = getTalentBonusesForRole(levels, role);
if (bonuses.length === 0) return;
// percent 模式的 defense 基数:取调用方传入的当前 defense(Hero.load/applyHeroLevel 已在调用前重置为 hero.def)
const defBase = model.defense;
for (const b of bonuses) {
if (b.mode === TalentValueMode.Percent) {
switch (b.attr) {
case Attrs.ap:
model.ap += Math.floor(model.base_ap * b.value / 100);
break;
case Attrs.hp_max:
model.hp_max += Math.floor(model.base_hp * b.value / 100);
model.hp += Math.floor(model.base_hp * b.value / 100);
break;
case Attrs.defense:
model.defense += Math.floor(defBase * b.value / 100);
break;
default:
model[b.attr] = (typeof model[b.attr] === "number" ? model[b.attr] : 0) + b.value;
break;
}
} else {
// flat:概率/抗性类百分点累加,defense 固定点数累加
model[b.attr] = (typeof model[b.attr] === "number" ? model[b.attr] : 0) + b.value;
}
}
}