/** * @file HeroSkillText.ts * @description 英雄技能文案生成器(heroInfo 内联描述模板驱动) * * 英雄语境描述文案层(原 HeroSkillDesc 已由本模块 + CardSkillText 替代并移除): * - 描述模板直接配置在 heroInfo(HSkillInfo.desc / LvSkillEntry.desc / LvTriggerEntry.desc), * "同一底座技能、不同英雄语境"的表述差异在英雄配置处表达; * - 数值永远从 mergeSkillParams 合并后的实时配置取值(占位符渲染), * 改 overrides 数值文案自动同步,杜绝"改数值忘改文案"漂移; * - 英雄当前等级取自 SingletonModuleComp 的局外成长等级(smc.getHeroOuterLv)。 * * 两个输出(均为富文本 BBCode,RichText 直接可用): * 1. buildHeroSkillDesc(heroUuid) —— 当前局外等级下各技能最大生效档的完整说明 * 2. buildHeroSkillTierDesc(heroUuid, skillUuid) —— 指定技能各阶段说明: * slv1 给完整能力 + 激活英雄等级;slv2+ 给相对上一级的提升 + 解锁英雄等级 * * 触发组口径:每个触发槽位(atking/dead/fstart/atked/call/field)整组视为一个技能, * 组内多个 s_uuid / 光环词条合并为一个名称(组内首个自定义名)与一套说明统一展示: * - buildHeroSkillDesc 中每组合并为一行(CHeroComp / HeroBoxComp 等直接消费) * - buildHeroGroupTierLines(heroUuid, slot) 供弹窗按整组分档渲染(SinfoComp) * * 占位符规范(模板作者可用,渲染时数值片段自动绿色高亮): * {target} 目标语义(自身/敌方/全体友方/N名队友…,与 SCastSystem 目标选取一致) * {dur} 计时 buff 持续秒数(buff_duration 优先,回退 BuffList 默认) * {buff_value} 计时 buff 修饰数值(tick 类取绝对值展示,modifiers 类保留正负号) * {字段名} 合并后技能配置数值(ap/para/frz/stun/crt/bck/hit_count/num/summon_count…) * * 依赖:heroSet(配置与 resolver)、SkillSet/BuffSet/SkillDescSet(底座)、smc(局外等级)。 * 卡牌/道具语境描述见 config/CardSkillText.ts。 */ import { HeroInfo, heroInfo, SkillTriggerType, SkillTriggerName, HSkillInfo, LvTriggerEntry, FieldEntry, TriggerGrouped, resolveSkillOverrides, resolveTriggerByLv, resolveFieldByLv, getGroupedTriggerName, getFieldSkillName, getTriggerGroupName, getFieldGroupName, } from "./config/heroSet"; import { SkillSet, mergeSkillParams, SkillConfig, SkillKind, TGroup, FieldSkillSet } from "./config/SkillSet"; import { BuffList } from "./config/BuffSet"; import { FieldDescSet, FieldValueFmt } from "./config/SkillDescSet"; import { FacSet } from "./config/GameSet"; import { smc } from "./SingletonModuleComp"; /* ==================== 富文本片段(与 CardSkillText 同配色,保证各面板视觉一致) ==================== */ /** 富文本数值高亮色(鲜亮绿色) */ const RICH_VALUE_COLOR = "#3CE66E"; /** 数值描边色(深绿,衬托亮绿填充) */ const RICH_VALUE_OUTLINE = "#145A32"; /** 技能名高亮色(蜜桃橙) */ const RICH_NAME_COLOR = "#F8C471"; /** 技能名描边色(深褐) */ const RICH_NAME_OUTLINE = "#6E2C00"; /** 数值富文本片段:亮绿填充 + 深绿描边 */ function valueRich(text: string | number): string { return `${text}`; } /** 技能名富文本片段:「技能名」蜜桃橙填充 + 深褐描边 */ function skillNameRich(name: string): string { return `「${name}」`; } /* ==================== 模板渲染基础 ==================== */ /** 需要遍历的触发技能类型(field 结构不同,单独处理;复活已纳入 dead) */ const TRIGGER_KEYS: SkillTriggerType[] = [ SkillTriggerType.Call, SkillTriggerType.Dead, SkillTriggerType.FStart, SkillTriggerType.Atking, SkillTriggerType.Atked, ]; /** 占位符模式:{target}/{dur}/{buff_value}/{技能字段名} */ const PLACEHOLDER_PATTERN = /\{(\w+)\}/g; /** * 构建触发语境短语(整组/单技能行首共用)。 * atking/atked 为周期循环触发(运行时按次数取模判定),拼为"每攻击N次"/"每受击N次" * 表达循环语义(N=1 时退化为"每次攻击"/"每次受击");其余类型直接用触发名(召唤/死亡后/回合开始)。 */ function buildTriggerLabel(type: SkillTriggerType, tNum: number): string { if (type === SkillTriggerType.Atking || type === SkillTriggerType.Atked) { return tNum <= 1 ? `每次${SkillTriggerName[type]}` : `每${SkillTriggerName[type]}${valueRich(tNum)}次`; } return SkillTriggerName[type]; } /** * 解析目标描述文本(与战斗 SCastSystem 目标选取语义严格一致) */ function buildTargetDesc(skill: SkillConfig): string { switch (skill.TGroup) { case TGroup.Self: return "自身"; case TGroup.Enemy: return "敌方"; case TGroup.Ally: case TGroup.All: return "全体友方"; case TGroup.Team: { const n = Math.max(1, Math.floor(skill.hit_count ?? 1)); // 治疗类命中低血量队友 if (skill.kind === SkillKind.Heal) { return n > 1 ? `${n}名低血量队友` : "低血量队友"; } return `${n}名队友`; } default: return "全体友方"; } } /** 计时 buff 生效时长(overrides.buff_duration 优先,回退 BuffList 默认) */ function getBuffDuration(skill: SkillConfig): number { if (skill.timed_buff_id === undefined) return 0; return skill.buff_duration ?? BuffList[skill.timed_buff_id]?.duration ?? 0; } /** 计时 buff 修饰数值(tick 类伤害取绝对值展示;modifiers 类保留正负号表达增减) */ function getBuffValue(skill: SkillConfig): number { if (skill.timed_buff_id === undefined) return skill.buff_value ?? 0; if (skill.buff_value !== undefined) return skill.buff_value; const buff = BuffList[skill.timed_buff_id]; if (!buff) return 0; if (buff.modifiers?.length) return buff.modifiers[0].value; if (buff.tick) return Math.abs(buff.tick.damage_or_heal); return 0; } /** * 渲染 heroInfo 内联描述模板:数值占位符替换为合并后配置实时值并绿色高亮。 * Why: 数值不写死在模板里,overrides 调整档位数值时文案自动同步。 * @param tmpl 描述模板(含占位符) * @param skill mergeSkillParams 合并后的技能配置 */ function renderDesc(tmpl: string, skill: SkillConfig): string { return tmpl.replace(PLACEHOLDER_PATTERN, (raw, key: string) => { if (key === "target") return buildTargetDesc(skill); if (key === "dur") return valueRich(getBuffDuration(skill)); if (key === "buff_value") return valueRich(getBuffValue(skill)); const v = (skill as unknown as Record)[key]; // 未定义数值字段按 0 渲染,避免面板出现 undefined(模板与配置不匹配的兜底) return v === undefined ? valueRich(0) : valueRich(String(v)); }); } /** 无模板时兜底:回退底座技能的 info 备注 */ function fallbackDesc(skill: SkillConfig): string { return String(skill.info ?? ""); } /** 取英雄展示等级:英雄用局外成长等级(smc),怪物无成长直接用配置 lv */ function getHeroDisplayLv(heroUuid: number, configLv: number, fac: FacSet): number { if (fac !== FacSet.HERO) return Math.max(1, configLv); return Math.max(1, smc.getHeroOuterLv(heroUuid)); } /* ==================== 输出一:当前等级最大生效档说明 ==================== */ /** * 输出一:英雄当前局外等级下,全部技能最大生效档的完整说明(富文本)。 * * 覆盖:普攻/大招(skills,resolve ≤lv 最大档)、5 类触发技能、驻场光环(field)、 * 死亡触发(dead,含复活)。触发槽位整组视为一个技能(组内多技能/词条合并为一行, * 组名取组内首个自定义名),普攻/大招仍逐技能一行。每行 = 「技能名」:(或「技能名」+ * 触发语境)+ 模板渲染后的效果文本(数值高亮),不展示技能等级档号(当前档由英雄局外等级隐含决定)。 * * Why 普攻默认隐藏:skills 首位为普攻(全英雄共有基础能力),展示场景通常只关心 * 特色技能(大招/触发/光环),普攻行信息冗余;需要完整展示时传 showNormal=true。 * * @param heroUuid 英雄 uuid(HeroInfo key) * @param showName 是否包含「技能名」片段(面板 true,紧凑卡片 false) * @param showNormal 是否展示普攻(skills 首位技能),默认 false * @returns 多行富文本串,\n 分隔;英雄不存在时返回 "" */ export function buildHeroSkillDesc(heroUuid: number, showName: boolean = true, showNormal: boolean = false): string { const hero = HeroInfo[heroUuid]; if (!hero) return ""; const lv = getHeroDisplayLv(heroUuid, hero.lv, hero.fac); const lines: string[] = []; const nameSeg = (n: string) => (showName && n ? skillNameRich(n) : ""); // ---- 普攻/大招(skills):resolve 当前等级生效档,渲染该档模板 ---- const skillKeys = Object.keys(hero.skills); for (let i = 0; i < skillKeys.length; i++) { // 首位技能 = 普攻,默认跳过(基础能力不占描述篇幅) if (i === 0 && !showNormal) continue; const hSkill = hero.skills[skillKeys[i]]; if (!hSkill) continue; const base = SkillSet[hSkill.uuid]; if (!base) continue; const skill = mergeSkillParams(base, resolveSkillOverrides(hSkill, lv)); // 生效档(≤lv 最大档)模板:未配置 desc 的档位继承基础/上一档模板 let inheritDesc = hSkill.desc; for (const e of hSkill.lv_entries ?? []) { if (e.lv <= lv && e.desc) inheritDesc = e.desc; } const text = inheritDesc ? renderDesc(inheritDesc, skill) : fallbackDesc(skill); // 行首格式「技能名」:(不展示技能等级,等级由英雄局外等级隐含表达) const head = nameSeg(hSkill.name ?? base.name); lines.push(head ? `${head}: ${text}` : text); } // ---- 5 类触发技能:每个触发槽位整组视为一个技能,组内多技能效果合并为一行 ---- for (const type of TRIGGER_KEYS) { const grouped = hero[type] as TriggerGrouped | undefined; if (!grouped) continue; const items = resolveTriggerByLv(grouped, lv); if (items.length === 0) continue; const parts: string[] = []; for (const item of items) { const base = SkillSet[item.s_uuid]; if (!base) continue; const skill = mergeSkillParams(base, item.overrides); // 档位模板继承链:组内 ≤lv 档依次覆盖(后配置的生效),缺省回退第 1 档 const entries = grouped[item.s_uuid] ?? []; let tmpl: string | undefined; for (const e of entries) { if (e.lv <= lv && e.desc) tmpl = e.desc; } tmpl = tmpl ?? entries[0]?.desc; const text = tmpl ? renderDesc(tmpl, skill) : fallbackDesc(skill); // 触发语境:"每攻击N次"循环语义(buildTriggerLabel),逗号衔接效果文本 const trig = buildTriggerLabel(type, item.t_num); parts.push(`${trig}, ${text}`); } // dead 组内的复活效果(revive_hp / revive_count)并入同一行 if (type === SkillTriggerType.Dead) { for (const item of items) { const reviveHp = item.overrides?.revive_hp; const reviveCount = item.overrides?.revive_count ?? 0; if (reviveHp === undefined || reviveCount <= 0) continue; const text = `每回合可复活${valueRich(reviveCount)}次, 死亡后以${valueRich(`${Math.round(reviveHp)}%`)}生命原地重生`; parts.push(`${SkillTriggerName[SkillTriggerType.Dead]}, ${text}`); } } if (parts.length === 0) continue; // 组名取组内首个自定义名(getTriggerGroupName) lines.push(`${nameSeg(getTriggerGroupName(hero, type))}${parts.join(";")}`); } // ---- 驻场光环(field):整组视为一个技能,组内多词条合并为一行 ---- const fieldParts: string[] = []; for (const entry of resolveFieldByLv(hero.field, lv)) { const fs = FieldSkillSet[entry.uuid]; const desc = fs ? FieldDescSet[fs.type] : undefined; if (!fs || !desc) continue; const val = desc.fmt === FieldValueFmt.Percent ? `+${Math.round(entry.value * 100)}%` : desc.fmt === FieldValueFmt.IntMinus ? `-${entry.value}` : `+${entry.value}`; fieldParts.push(`${desc.verb}${valueRich(val)}`); } if (fieldParts.length > 0) { // 组名取组内首个自定义名(getFieldGroupName) lines.push(`${nameSeg(getFieldGroupName(hero))}${fieldParts.join(",")}`); } return lines.join("\n"); } /* ==================== 输出二:指定技能各阶段(分档)说明 ==================== */ /** 一个技能等级档位的统一结构(普攻 lv_entries 与触发技能 LvTriggerEntry 归一) */ interface ISkillTierInfo { /** 技能等级(展示用,取 s_lv,缺省按档位序号) */ sLv: number; /** 该档生效的英雄等级(激活/解锁提示) */ heroLv: number; /** 合并底座后的完整技能配置 */ skill: SkillConfig; /** 该档描述模板(继承链解析后) */ desc?: string; /** 该档显式配置的描述模板(未走继承链):配置后提升档直接展示该模板,优先于自动 diff */ ownDesc?: string; /** 触发技能档位的触发次数(普攻无) */ tNum: number; } /** 相邻档 diff 参与展示的字段(排除动画/弹道/特效等玩家无感字段),t_num 为触发技能专有 */ const DIFF_KEYS: string[] = [ "ap", "hit_count", "num", "summon_count", "crt", "frz", "stun", "para", "bck", "bck_chance", "buff_duration", "buff_value", "t_num", "timed_buff_id", ]; /** diff 字段的称谓与单位("新增/漂移"描述共用) */ const DIFF_META: Record = { ap: { label: "伤害", unit: "%" }, hit_count: { label: "命中", unit: "次" }, num: { label: "双发", unit: "次" }, summon_count: { label: "召唤数", unit: "" }, crt: { label: "暴击", unit: "%" }, frz: { label: "冰冻", unit: "%" }, stun: { label: "击晕", unit: "%" }, para: { label: "麻痹", unit: "%" }, bck: { label: "击退", unit: "" }, bck_chance: { label: "击退概率", unit: "%" }, buff_duration: { label: "持续", unit: "秒" }, buff_value: { label: "增益强度", unit: "" }, t_num: { label: "触发次数", unit: "次" }, }; /** 读取档位某展示字段的实时值(t_num/buff_value 走专用取值) */ function getTierValue(tier: ISkillTierInfo, key: string): number { if (key === "t_num") return tier.tNum; if (key === "buff_value") return getBuffValue(tier.skill); const v = (tier.skill as unknown as Record)[key]; return typeof v === "number" ? v : 0; } /** * 相邻两档 diff:数值漂移(差值)+ 新功能(上级为 0 本级新增)+ 计时 buff 增减。 * Why: slv2+ 只描述"多得到了什么",让玩家一眼看清每级提升。 */ function buildTierDiff(prev: ISkillTierInfo, cur: ISkillTierInfo): string[] { const parts: string[] = []; for (const key of DIFF_KEYS) { // 计时 buff 增减单独处理:展示 buff 名而非内部 id if (key === "timed_buff_id") { const pb = prev.skill.timed_buff_id ?? 0; const cb = cur.skill.timed_buff_id ?? 0; if (pb === cb) continue; if (!pb && cb) parts.push(`新增${valueRich(BuffList[cb]?.name ?? "计时增益")}`); else if (pb && !cb) parts.push(`移除${valueRich(BuffList[pb]?.name ?? "计时增益")}`); else parts.push(`${valueRich(BuffList[pb]?.name ?? "计时增益")}→${valueRich(BuffList[cb]?.name ?? "计时增益")}`); continue; } const meta = DIFF_META[key]; if (!meta) continue; const pv = getTierValue(prev, key); const cv = getTierValue(cur, key); if (cv === pv) continue; if (pv === 0 && cv > 0) { parts.push(`新增${meta.label}${valueRich(`${cv}${meta.unit}`)}`); } else { const d = cv - pv; parts.push(`${meta.label}${valueRich(`${d > 0 ? "+" : ""}${d}${meta.unit}`)}`); } } return parts; } /** 收集普攻/大招(skills)的全部等级档位(slv 升序,模板走继承链) */ function collectSkillTiers(hSkill: HSkillInfo): ISkillTierInfo[] { const base = SkillSet[hSkill.uuid]; if (!base) return []; const entries = [...(hSkill.lv_entries ?? [])].sort((a, b) => (a.s_lv ?? 0) - (b.s_lv ?? 0)); const tiers: ISkillTierInfo[] = []; // slv1 = 基础档(底座 + 基础 overrides),激活等级取 s_lv≤1 档的 lv(缺省 1) const lv1 = entries.find(e => (e.s_lv ?? 0) <= 1); tiers.push({ sLv: lv1?.s_lv ?? 1, heroLv: lv1?.lv ?? 1, skill: mergeSkillParams(base, hSkill.overrides), desc: lv1?.desc ?? hSkill.desc, ownDesc: lv1?.desc ?? hSkill.desc, tNum: 0, }); // slv2+ = lv_entries 各档(与基础 overrides 浅合并,档位字段优先) for (const e of entries) { if ((e.s_lv ?? 0) < 2) continue; tiers.push({ sLv: e.s_lv ?? 0, heroLv: e.lv, skill: mergeSkillParams(mergeSkillParams(base, hSkill.overrides), e.overrides), desc: e.desc, ownDesc: e.desc, tNum: 0, }); } // 模板继承链:未配置 desc 的档位沿用上一档模板(机制不变仅数值变化的档位免配) let inherit: string | undefined; for (const t of tiers) { if (t.desc) inherit = t.desc; else t.desc = inherit; } return tiers; } /** 驻场光环词条数值展示片段(与 buildHeroSkillDesc 口径一致:比例/整数/减免) */ function buildFieldValueRich(fieldUuid: number, value: number): string { const fs = FieldSkillSet[fieldUuid]; const desc = fs ? FieldDescSet[fs.type] : undefined; if (!desc) return valueRich(value); const val = desc.fmt === FieldValueFmt.Percent ? `+${Math.round(value * 100)}%` : desc.fmt === FieldValueFmt.IntMinus ? `-${value}` : `+${value}`; return valueRich(val); } /** * 收集驻场光环词条分档行(hero.field 各英雄等级档,无 s_lv 概念,按档位序号编号)。 * slv1 给完整效果(动词 + 数值);slv2+ 给数值提升描述。 * 档位配置了 desc 时直接采用(策划自定义表述优先)。 */ function collectFieldTierLines(hero: heroInfo, fieldUuid: number): HeroSkillTierLine[] { const matched = (hero.field ?? []) .filter(e => e.uuids.some(u => u.uuid === fieldUuid)) .sort((a, b) => a.lv - b.lv); if (matched.length === 0) return []; const fs = FieldSkillSet[fieldUuid]; const descCfg = fs ? FieldDescSet[fs.type] : undefined; const verb = descCfg?.verb ?? ""; const lines: HeroSkillTierLine[] = []; let prevValue: number | undefined; matched.forEach((e, i) => { const entry = e.uuids.find(u => u.uuid === fieldUuid)!; if (e.desc) { // 配置描述优先(策划自定义表述) lines.push({ sLv: i + 1, heroLv: e.lv, text: e.desc }); } else if (i === 0 || !descCfg) { // 首档:完整效果(动词 + 数值高亮);未入表类型回退词条 info lines.push({ sLv: i + 1, heroLv: e.lv, text: descCfg ? `${verb}${buildFieldValueRich(fieldUuid, entry.value)}` : (fs?.info ?? "") }); } else { // 提升档:数值由旧值提升至新值 lines.push({ sLv: i + 1, heroLv: e.lv, text: `${verb}${buildFieldValueRich(fieldUuid, prevValue!)}提升至${buildFieldValueRich(fieldUuid, entry.value)}`, }); } prevValue = entry.value; }); return lines; } /** * 收集复活技能分档行(已纳入 dead 触发组,从 dead[skillUuid] 提取 revive_hp / revive_count)。 * slv1 给完整效果(复活次数 + 重生血量);slv2+ 给复活次数/血量变化。 * 档位配置了 desc 时直接采用。 */ function collectReviveTierLines(hero: heroInfo, sUuid: number): HeroSkillTierLine[] { const entries = hero.dead?.[sUuid]; if (!entries?.length) return []; // 筛选带复活参数的档位,按 lv 升序 const matched = entries .filter(e => e.overrides?.revive_hp !== undefined && (e.overrides?.revive_count ?? 0) > 0) .sort((a, b) => a.lv - b.lv); if (matched.length === 0) return []; const lines: HeroSkillTierLine[] = []; let prevCount: number | undefined; let prevHp: number | undefined; matched.forEach((e, i) => { const count = e.overrides!.revive_count!; const hpPct = Math.round(e.overrides!.revive_hp!); if (e.desc) { lines.push({ sLv: i + 1, heroLv: e.lv, text: e.desc }); } else if (i === 0) { lines.push({ sLv: 1, heroLv: e.lv, text: `每回合可复活${valueRich(count)}次, 死亡后以${valueRich(`${hpPct}%`)}生命原地重生` }); } else if (prevCount !== undefined && (count !== prevCount || hpPct !== prevHp)) { const parts: string[] = []; if (count !== prevCount) parts.push(`复活次数${valueRich(prevCount)}次提升至${valueRich(count)}次`); if (hpPct !== prevHp) parts.push(`回血比例${valueRich(prevHp)}%提升至${valueRich(hpPct)}%`); lines.push({ sLv: i + 1, heroLv: e.lv, text: parts.join(",") }); } else { lines.push({ sLv: i + 1, heroLv: e.lv, text: "无变化" }); } prevCount = count; prevHp = hpPct; }); return lines; } /** 单个技能等级档位的展示行(结构化,供弹窗逐档渲染激活状态与描述) */ export interface HeroSkillTierLine { /** 技能等级(slv) */ sLv: number; /** 该档解锁/激活所需英雄等级 */ heroLv: number; /** 行文本:slv1=完整效果;slv2+=相对上一级提升(数值漂移/新增效果),富文本 */ text: string; } /** * 收集指定触发槽位的触发技能档位与触发语境词;未命中(组不存在/无该 s_uuid/无底座)返回 null。 */ function collectTriggerTiers(hero: heroInfo, type: SkillTriggerType, skillUuid: number): { tiers: ISkillTierInfo[]; label: string } | null { const grouped = hero[type] as TriggerGrouped | undefined; const entries = grouped?.[skillUuid]; if (!grouped || !entries?.length) return null; const base = SkillSet[skillUuid]; if (!base) return null; const sorted = [...entries].sort((a, b) => (a.s_lv ?? 0) - (b.s_lv ?? 0)); let inherit: string | undefined; const tiers = sorted.map((e: LvTriggerEntry, i: number) => { if (e.desc) inherit = e.desc; return { sLv: e.s_lv ?? (i + 1), heroLv: e.lv, skill: mergeSkillParams(base, e.overrides), desc: e.desc ?? inherit, ownDesc: e.desc, tNum: e.t_num, }; }); const label = buildTriggerLabel(type, sorted[0].t_num); return { tiers, label }; } /** 档位 → 展示行:slv1 完整能力 + 数值;slv2+ 显式配置 desc 时直接渲染该模板,否则相对上一级的提升 */ function buildTierLines(tiers: ISkillTierInfo[], triggerLabel: string): HeroSkillTierLine[] { const lines: HeroSkillTierLine[] = []; for (let i = 0; i < tiers.length; i++) { const t = tiers[i]; if (i === 0) { // 技能 1 级:完整能力 + 数值 const text = t.desc ? renderDesc(t.desc, t.skill) : fallbackDesc(t.skill); lines.push({ sLv: t.sLv, heroLv: t.heroLv, text: triggerLabel ? `${triggerLabel}, ${text}` : text }); } else if (t.ownDesc) { // 提升档:该档显式配置了模板(如护盾/治疗等 diff 标签语义不达的技能),策划表述优先 lines.push({ sLv: t.sLv, heroLv: t.heroLv, text: renderDesc(t.ownDesc, t.skill) }); } else { // 提升档:未配置模板,给相对上一级的提升 const diff = buildTierDiff(tiers[i - 1], t); lines.push({ sLv: t.sLv, heroLv: t.heroLv, text: diff.length > 0 ? diff.join(",") : "无变化" }); } } return lines; } /** * 输出二(结构化):指定技能各等级档位说明,按技能等级升序。 * * slv1 给完整能力 + 数值;slv2+ 给相对上一级的提升。每档附带解锁英雄等级, * 调用方可自行对比英雄当前等级(smc.getHeroOuterLv)判定激活状态。 * * 技能定位顺序(覆盖英雄图鉴全部 5 类技能形态): * 1. hero.skills[skillUuid] —— 普攻/大招 * 2. 5 类触发技能组 —— call/dead/fstart/atking/atked * 3. hero.field —— 驻场光环词条(FieldSkillSet uuid) * 4. hero.dead 中的复活技能 —— 已纳入 dead 触发组(revive_hp / revive_count) * 均未命中返回 []。 * * Why: 同一底座技能可能被多个英雄以不同 overrides 引用,必须以英雄语境取档; * 同一底座还可能同时是普攻与触发技能(如 5002 的 6004),传 slot 指定槽位 * 可避免自动定位命中错误分组(激活等级/档位随之错位)。 * * @param heroUuid 英雄 uuid(HeroInfo key) * @param skillUuid 技能 uuid(skills key / 触发组 s_uuid / FieldSkillSet uuid / 复活 SkillSet uuid) * @param slot 技能所属槽位(可空):指定时仅在该槽位查找;缺省按上述顺序自动定位 * @param minLv 最低展示档(默认 1):传 2 时裁剪掉 sLv<2 的档,仅保留 lv2+ 档位 * (普攻强化档作为独立技能展示的场景,如 heroSet 普攻 lv2 档单独出图标) * @returns 档位行数组;技能不属于该英雄时返回 [] */ export function buildHeroSkillTierLines(heroUuid: number, skillUuid: number, slot?: SkillTriggerType, minLv: number = 1): HeroSkillTierLine[] { const hero = HeroInfo[heroUuid]; if (!hero) return []; // 槽位指定:光环结构特殊直接生成行;触发组走统一档位结构(复活已纳入 dead) if (slot === SkillTriggerType.Field) return collectFieldTierLines(hero, skillUuid); if (slot !== undefined) { const hit = collectTriggerTiers(hero, slot, skillUuid); return hit ? buildTierLines(hit.tiers, hit.label) : []; } // 自动定位:普攻/大招优先,其次 5 类触发组,最后光环/复活 let tiers: ISkillTierInfo[] = []; let triggerLabel = ""; const hSkill = hero.skills?.[skillUuid]; if (hSkill) { // 普攻/大招路径 tiers = collectSkillTiers(hSkill); } else { // 触发技能路径:在 5 类触发组中定位 s_uuid for (const type of TRIGGER_KEYS) { const hit = collectTriggerTiers(hero, type, skillUuid); if (hit) { tiers = hit.tiers; triggerLabel = hit.label; break; } } } // 光环路径(无 ISkillTierInfo 中间结构,直接生成 HeroSkillTierLine) if (tiers.length === 0) { const fieldLines = collectFieldTierLines(hero, skillUuid); if (fieldLines.length > 0) return minLv > 1 ? fieldLines.filter(l => l.sLv >= minLv) : fieldLines; // 复活已纳入 dead 触发组,通过 dead 路径处理 return []; } const lines = buildTierLines(tiers, triggerLabel); return minLv > 1 ? lines.filter(l => l.sLv >= minLv) : lines; } /** * 驻场光环整组分档行(hero.field 全部词条合并展示)。 * 每档一行:新词条给完整效果(动词 + 数值),已有词条给数值提升; * 组内仅 1 个词条时与单词条口径一致。档位配置了 desc(LvFieldEntry.desc)时优先采用。 */ function collectFieldGroupTierLines(hero: heroInfo): HeroSkillTierLine[] { const tiers = [...(hero.field ?? [])].sort((a, b) => a.lv - b.lv); if (tiers.length === 0) return []; const lines: HeroSkillTierLine[] = []; let prev: FieldEntry[] = []; tiers.forEach((e, i) => { const parts: string[] = []; for (const entry of e.uuids) { const fs = FieldSkillSet[entry.uuid]; const descCfg = fs ? FieldDescSet[fs.type] : undefined; if (!fs || !descCfg) continue; const before = prev.find(p => p.uuid === entry.uuid); if (!before) { // 新词条:完整效果(动词 + 数值高亮) parts.push(`${descCfg.verb}${buildFieldValueRich(entry.uuid, entry.value)}`); } else if (before.value !== entry.value) { // 已有词条:数值由旧值提升至新值 parts.push(`${descCfg.verb}${buildFieldValueRich(entry.uuid, before.value)}提升至${buildFieldValueRich(entry.uuid, entry.value)}`); } } lines.push({ sLv: i + 1, heroLv: e.lv, text: e.desc ?? parts.join("\n") }); prev = e.uuids; }); return lines; } /** 多技能档位行按解锁英雄等级合并:同阈值档位文本换行拼接,sLv 取该阈值下最大档 */ function mergeTierLinesByHeroLv(groups: HeroSkillTierLine[][]): HeroSkillTierLine[] { if (groups.length === 0) return []; if (groups.length === 1) return groups[0]; const byLv = new Map(); for (const lines of groups) { for (const line of lines) { const bucket = byLv.get(line.heroLv); if (bucket) bucket.push(line); else byLv.set(line.heroLv, [line]); } } return [...byLv.keys()].sort((a, b) => a - b).map(heroLv => { const bucket = byLv.get(heroLv)!; return { sLv: Math.max(...bucket.map(l => l.sLv)), heroLv, text: bucket.map(l => l.text).join("\n"), }; }); } /** * 输出二(触发组整体):指定触发槽位整组的各等级档位说明,按解锁英雄等级升序。 * * 每个触发槽位(atking/dead/fstart/atked/call/field)整组视为一个技能: * 组内各 s_uuid 分别生成分档行后按解锁英雄等级阈值合并(同阈值文本换行拼接), * 说明内只保留组名与效果文本,不再显示组内子技能名;驻场光环按词条维度合并。 * 每档附带解锁英雄等级,调用方对比 smc.getHeroOuterLv 判定激活状态。 * * @param heroUuid 英雄 uuid(HeroInfo key) * @param slot 触发槽位(field 表示驻场光环整组) * @returns 合并后的档位行数组;该槽位未配置返回 [] */ export function buildHeroGroupTierLines(heroUuid: number, slot: SkillTriggerType): HeroSkillTierLine[] { const hero = HeroInfo[heroUuid]; if (!hero) return []; if (slot === SkillTriggerType.Field) return collectFieldGroupTierLines(hero); const grouped = hero[slot] as TriggerGrouped | undefined; if (!grouped) return []; const lineGroups: HeroSkillTierLine[][] = []; for (const sUuid of Object.keys(grouped).map(Number)) { const hit = collectTriggerTiers(hero, slot, sUuid); if (!hit) continue; lineGroups.push(buildTierLines(hit.tiers, hit.label)); } return mergeTierLinesByHeroLv(lineGroups); } /** * 输出二:指定技能的各阶段说明(富文本整段)。 * * 输出结构(按技能等级升序,通用支持任意档数): * - slv1:完整能力 + 数值 +(英雄X级激活) * - slv2+:相对上一级的提升(数值漂移/新增效果/计时 buff 增减)+(英雄Y级解锁) * * @param heroUuid 英雄 uuid(HeroInfo key) * @param skillUuid 技能 uuid(skills key 或触发组 s_uuid) * @param showName 是否包含「技能名」片段 * @returns 多行富文本串,\n 分隔;技能不属于该英雄时返回 "" */ export function buildHeroSkillTierDesc(heroUuid: number, skillUuid: number, showName: boolean = true): string { const hero = HeroInfo[heroUuid]; if (!hero) return ""; const tiers = buildHeroSkillTierLines(heroUuid, skillUuid); if (tiers.length === 0) return ""; // 技能名:大招取 HSkillInfo.name,触发技能取组名,缺省回退底座名 const hSkill = hero.skills?.[skillUuid]; const name = hSkill ? (hSkill.name ?? SkillSet[skillUuid]?.name ?? "") : resolveTriggerSkillName(hero, skillUuid); const nameSeg = showName && name ? `${skillNameRich(name)} ` : ""; return tiers .map((t, i) => `${nameSeg}Lv.${valueRich(t.sLv)}(英雄${valueRich(t.heroLv)}级${i === 0 ? "激活" : "解锁"}): ${t.text}`) .join("\n"); } /** 在英雄技能配置中解析技能对外展示名(触发组名 / 光环词条名 / 复活名 / 底座名) */ function resolveTriggerSkillName(hero: heroInfo, skillUuid: number): string { for (const type of TRIGGER_KEYS) { const grouped = hero[type] as TriggerGrouped | undefined; if (grouped?.[skillUuid]?.length) return getGroupedTriggerName(grouped, skillUuid); } // 驻场光环:词条名取英雄语境(FieldEntry.name 优先,回退 FieldSkillSet 底座名) if (hero.field?.some(e => e.uuids.some(u => u.uuid === skillUuid))) { return getFieldSkillName(hero, skillUuid); } // 复活技能:名称取英雄语境(dead 组名,回退底座名) if (hero.dead?.[skillUuid]?.length) { return getGroupedTriggerName(hero.dead, skillUuid); } return SkillSet[skillUuid]?.name ?? ""; }