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 = { // ==================== 第 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, 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, 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, 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> = { [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> = { [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, 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, 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, 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, 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; } } }