Files
pixelheros/assets/script/game/common/HeroSkillText.ts
pan d704f3201d refactor(skillText): 重构技能描述生成逻辑,支持英雄自定义技能名
1. 重构getFieldSkillName和getReviveSkillName函数,新增hero参数获取英雄自定义技能名
2. 为FieldEntry和LvReviveEntry新增name字段,支持配置英雄专属技能展示名
3. 更新buildHeroSkillDesc和resolveTriggerSkillName函数,使用新的名称获取逻辑
4. 为部分英雄配置添加专属技能名称,统一技能展示文本
2026-08-28 17:07:20 +08:00

561 lines
26 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.
/**
* @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+ 给相对上一级的提升 + 解锁英雄等级
*
* 占位符规范(模板作者可用,渲染时数值片段自动绿色高亮):
* {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,
TriggerGrouped, resolveSkillOverrides, resolveTriggerByLv, resolveFieldByLv, resolveReviveByLv,
getGroupedTriggerName, getFieldSkillName, getReviveSkillName,
} 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 `<outline color=${RICH_VALUE_OUTLINE} width=1><color=${RICH_VALUE_COLOR}>${text}</color></outline>`;
}
/** 技能名富文本片段:「技能名」蜜桃橙填充 + 深褐描边 */
function skillNameRich(name: string): string {
return `<outline color=${RICH_NAME_OUTLINE} width=1><color=${RICH_NAME_COLOR}>「${name}」</color></outline>`;
}
/* ==================== 模板渲染基础 ==================== */
/** 需要遍历的触发技能类型(field/revive 结构不同,单独处理) */
const TRIGGER_KEYS: SkillTriggerType[] = [
SkillTriggerType.Call,
SkillTriggerType.Dead,
SkillTriggerType.FStart,
SkillTriggerType.Atking,
SkillTriggerType.Atked,
];
/** 占位符模式:{target}/{dur}/{buff_value}/{技能字段名} */
const PLACEHOLDER_PATTERN = /\{(\w+)\}/g;
/**
* 解析目标描述文本(与战斗 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<string, unknown>)[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)、
* 复活(revive)。每行 = 「技能名」:(或「技能名」+ 触发语境)+ 模板渲染后的效果文本(数值高亮),
* 不展示技能等级档号(当前档由英雄局外等级隐含决定)。
*
* 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 类标准触发技能:resolve 当前等级生效档 ----
for (const type of TRIGGER_KEYS) {
const grouped = hero[type] as TriggerGrouped | undefined;
if (!grouped) continue;
for (const item of resolveTriggerByLv(grouped, lv)) {
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);
// 进击/受击带触发次数,拼为"攻击3次";其余用触发类型简称
const trig = type === SkillTriggerType.Atking || type === SkillTriggerType.Atked
? `${SkillTriggerName[type]}${valueRich(item.t_num)}次`
: SkillTriggerName[type];
lines.push(`${nameSeg(getGroupedTriggerName(grouped, item.s_uuid))}${trig}, ${text}`);
}
}
// ---- 驻场光环(field):查 FieldDescSet 按 type 渲染,数值实时高亮 ----
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}`;
lines.push(`${nameSeg(getFieldSkillName(hero, entry.uuid))}${desc.verb}${valueRich(val)}`);
}
// ---- 复活(revive):次数复刻战斗公式 r_num + floor((lv-1)*upr) ----
const revive = resolveReviveByLv(hero.revive, lv);
if (revive) {
const count = revive.r_num + Math.floor((lv - 1) * revive.upr);
const hpPct = SkillSet[revive.s_uuid]?.ap ?? 0;
lines.push(`${nameSeg(getReviveSkillName(hero, revive.s_uuid))}每回合可复活${valueRich(count)}次, 死亡后以${valueRich(`${Math.round(hpPct)}%`)}生命原地重生`);
}
return lines.join("\n");
}
/* ==================== 输出二:指定技能各阶段(分档)说明 ==================== */
/** 一个技能等级档位的统一结构(普攻 lv_entries 与触发技能 LvTriggerEntry 归一) */
interface ISkillTierInfo {
/** 技能等级(展示用,取 s_lv,缺省按档位序号) */
sLv: number;
/** 该档生效的英雄等级(激活/解锁提示) */
heroLv: number;
/** 合并底座后的完整技能配置 */
skill: SkillConfig;
/** 该档描述模板(继承链解析后) */
desc?: 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<string, { label: string; unit: string }> = {
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<string, unknown>)[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,
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,
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;
}
/**
* 收集复活技能分档行(hero.revive 各英雄等级档,无 s_lv 概念,按档位序号编号)。
* slv1 给完整效果(复活次数 + 重生血量);slv2+ 给复活次数变化。
* 档位配置了 desc 时直接采用。
*/
function collectReviveTierLines(hero: heroInfo, sUuid: number): HeroSkillTierLine[] {
const matched = (hero.revive ?? [])
.filter(e => e.s_uuid === sUuid)
.sort((a, b) => a.lv - b.lv);
if (matched.length === 0) return [];
const lines: HeroSkillTierLine[] = [];
let prevCount: number | undefined;
matched.forEach((e, i) => {
// 复活次数复刻战斗公式:r_num + floor((英雄等级-1)*upr)
const count = e.r_num + Math.floor((e.lv - 1) * e.upr);
const hpPct = Math.round(SkillSet[e.s_uuid]?.ap ?? 0);
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) {
lines.push({ sLv: i + 1, heroLv: e.lv, text: `复活次数${valueRich(prevCount)}次提升至${valueRich(count)}次` });
} else {
lines.push({ sLv: i + 1, heroLv: e.lv, text: "无变化" });
}
prevCount = count;
});
return lines;
}
/** 单个技能等级档位的展示行(结构化,供弹窗逐档渲染激活状态与描述) */
export interface HeroSkillTierLine {
/** 技能等级(slv) */
sLv: number;
/** 该档解锁/激活所需英雄等级 */
heroLv: number;
/** 行文本:slv1=完整效果;slv2+=相对上一级提升(数值漂移/新增效果),富文本 */
text: string;
}
/**
* 输出二(结构化):指定技能各等级档位说明,按技能等级升序。
*
* slv1 给完整能力 + 数值;slv2+ 给相对上一级的提升。每档附带解锁英雄等级,
* 调用方可自行对比英雄当前等级(smc.getHeroOuterLv)判定激活状态。
*
* 技能定位顺序(覆盖英雄图鉴全部 5 类技能形态):
* 1. hero.skills[skillUuid] —— 普攻/大招
* 2. 5 类触发技能组 —— call/dead/fstart/atking/atked
* 3. hero.field —— 驻场光环词条(FieldSkillSet uuid)
* 4. hero.revive —— 复活技能(SkillSet uuid)
* 均未命中返回 []。
*
* Why: 同一底座技能可能被多个英雄以不同 overrides 引用,必须以英雄语境取档。
*
* @param heroUuid 英雄 uuid(HeroInfo key)
* @param skillUuid 技能 uuid(skills key / 触发组 s_uuid / FieldSkillSet uuid / 复活 SkillSet uuid)
* @returns 档位行数组;技能不属于该英雄时返回 []
*/
export function buildHeroSkillTierLines(heroUuid: number, skillUuid: number): HeroSkillTierLine[] {
const hero = HeroInfo[heroUuid];
if (!hero) return [];
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 grouped = hero[type] as TriggerGrouped | undefined;
const entries = grouped?.[skillUuid];
if (!grouped || !entries?.length) continue;
const base = SkillSet[skillUuid];
if (!base) return [];
const sorted = [...entries].sort((a, b) => (a.s_lv ?? 0) - (b.s_lv ?? 0));
let inherit: string | undefined;
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,
tNum: e.t_num,
};
});
triggerLabel = (type === SkillTriggerType.Atking || type === SkillTriggerType.Atked)
? `${SkillTriggerName[type]}${valueRich(sorted[0].t_num)}次`
: SkillTriggerName[type];
break;
}
}
// 光环/复活路径(无 ISkillTierInfo 中间结构,直接生成 HeroSkillTierLine)
if (tiers.length === 0) {
const fieldLines = collectFieldTierLines(hero, skillUuid);
if (fieldLines.length > 0) return fieldLines;
const reviveLines = collectReviveTierLines(hero, skillUuid);
if (reviveLines.length > 0) return reviveLines;
return [];
}
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 {
// 技能 2 级起:相对上一级的提升
const diff = buildTierDiff(tiers[i - 1], t);
lines.push({ sLv: t.sLv, heroLv: t.heroLv, text: diff.length > 0 ? diff.join(",") : "无变化" });
}
}
return lines;
}
/**
* 输出二:指定技能的各阶段说明(富文本整段)。
*
* 输出结构(按技能等级升序,通用支持任意档数):
* - 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);
}
// 复活技能:名称取英雄语境(LvReviveEntry.name 优先,回退固定称谓)
if (hero.revive?.some(e => e.s_uuid === skillUuid)) {
return getReviveSkillName(hero, skillUuid);
}
return SkillSet[skillUuid]?.name ?? "";
}