refactor(hero skill): 重构英雄技能描述生成逻辑

1.  抽离原HeroSkillDesc工具函数为独立HeroSkillText模块
2.  简化英雄局外技能描述生成逻辑,移除冗余的数据源构造步骤
3.  新增结构化技能档位数据接口,拆分整段文本输出与结构化输出能力
4.  优化默认隐藏普攻技能展示,减少冗余信息
This commit is contained in:
pan
2026-08-27 21:06:34 +08:00
parent d929a99172
commit 39cbb2eed4
3 changed files with 97 additions and 67 deletions

View File

@@ -24,7 +24,7 @@
* HeroSkillDesc.ts 暂时保留兼容旧 UI 引用,待迁移完成后移除。
*/
import {
HeroInfo, SkillTriggerType, SkillTriggerName, HSkillInfo, LvSkillEntry, LvTriggerEntry,
HeroInfo, SkillTriggerType, SkillTriggerName, HSkillInfo, LvTriggerEntry,
TriggerGrouped, resolveSkillOverrides, resolveTriggerByLv, resolveFieldByLv, resolveReviveByLv,
getGroupedTriggerName, getFieldSkillName, getReviveSkillName,
} from "./config/heroSet";
@@ -145,38 +145,43 @@ function getHeroDisplayLv(heroUuid: number, configLv: number, fac: FacSet): numb
* 输出一:英雄当前局外等级下,全部技能最大生效档的完整说明(富文本)。
*
* 覆盖:普攻/大招(skills,resolve ≤lv 最大档)、5 类触发技能、驻场光环(field)、
* 复活(revive)。每行 = 「技能名」+ 触发语境 + 模板渲染后的效果文本(数值高亮)。
* 复活(revive)。每行 = 「技能名」:(或「技能名」+ 触发语境)+ 模板渲染后的效果文本(数值高亮),
* 不展示技能等级档号(当前档由英雄局外等级隐含决定)。
*
* @param heroUuid 英雄 uuid(HeroInfo key)
* @param showName 是否包含「技能名」片段(面板 true,紧凑卡片 false)
* @returns 多行富文本串,\n 分隔;英雄不存在时返回 ""
* 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): string {
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)} ` : "");
const nameSeg = (n: string) => (showName && n ? skillNameRich(n) : "");
// ---- 普攻/大招(skills):resolve 当前等级生效档,渲染该档模板 ----
for (const key in hero.skills) {
const hSkill = hero.skills[key];
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 最大档):取其 s_lv 与 desc;未配置 desc 的档位继承上一档模板
let entry: LvSkillEntry | undefined;
// 生效档(≤lv 最大档)模板:未配置 desc 的档位继承基础/上一档模板
let inheritDesc = hSkill.desc;
for (const e of hSkill.lv_entries ?? []) {
if (e.lv <= lv) {
if (!entry || e.lv > entry.lv) entry = e;
if (e.desc) inheritDesc = e.desc;
}
if (e.lv <= lv && e.desc) inheritDesc = e.desc;
}
const sLv = entry?.s_lv ?? hSkill.lv;
const text = inheritDesc ? renderDesc(inheritDesc, skill) : fallbackDesc(skill);
lines.push(`${nameSeg(hSkill.name ?? base.name)}Lv.${valueRich(sLv)}: ${text}`);
// 行首格式「技能名」:(不展示技能等级,等级由英雄局外等级隐含表达)
const head = nameSeg(hSkill.name ?? base.name);
lines.push(head ? `${head}: ${text}` : text);
}
// ---- 5 类标准触发技能:resolve 当前等级生效档 ----
@@ -339,33 +344,39 @@ function collectSkillTiers(hSkill: HSkillInfo): ISkillTierInfo[] {
return tiers;
}
/** 单个技能等级档位的展示行(结构化,供弹窗逐档渲染激活状态与描述) */
export interface HeroSkillTierLine {
/** 技能等级(slv) */
sLv: number;
/** 该档解锁/激活所需英雄等级 */
heroLv: number;
/** 行文本:slv1=完整效果;slv2+=相对上一级提升(数值漂移/新增效果),富文本 */
text: string;
}
/**
* 输出二:指定技能的各阶段说明(富文本)。
* 输出二(结构化):指定技能各等级档位说明,按技能等级升序。
*
* 输出结构(按技能等级升序,通用支持任意档数):
* - slv1:完整能力 + 数值 +(英雄X级激活)
* - slv2+:相对上一级的提升(数值漂移/新增效果/计时 buff 增减)+(英雄Y级解锁)
* slv1 给完整能力 + 数值;slv2+ 给相对上一级的提升。每档附带解锁英雄等级,
* 调用方可自行对比英雄当前等级(smc.getHeroOuterLv)判定激活状态。
*
* 技能定位顺序:hero.skills[skillUuid](普攻/大招)→ 5 类触发技能组 → 未找到返回 ""。
* 技能定位顺序:hero.skills[skillUuid](普攻/大招)→ 5 类触发技能组 → 未找到返回 []。
* Why: 同一底座技能可能被多个英雄以不同 overrides 引用,必须以英雄语境取档。
*
* @param heroUuid 英雄 uuid(HeroInfo key)
* @param skillUuid 技能 uuid(skills key 或触发组 s_uuid)
* @param showName 是否包含「技能名」片段
* @returns 多行富文本串,\n 分隔;技能不属于该英雄时返回 ""
* @returns 档位行数组;技能不属于该英雄时返回 []
*/
export function buildHeroSkillTierDesc(heroUuid: number, skillUuid: number, showName: boolean = true): string {
export function buildHeroSkillTierLines(heroUuid: number, skillUuid: number): HeroSkillTierLine[] {
const hero = HeroInfo[heroUuid];
if (!hero) return "";
if (!hero) return [];
let name = "";
let tiers: ISkillTierInfo[] = [];
let triggerLabel = "";
const hSkill = hero.skills?.[skillUuid];
if (hSkill) {
// 普攻/大招路径
name = hSkill.name ?? SkillSet[skillUuid]?.name ?? "";
tiers = collectSkillTiers(hSkill);
} else {
// 触发技能路径:在 5 类触发组中定位 s_uuid
@@ -374,9 +385,8 @@ export function buildHeroSkillTierDesc(heroUuid: number, skillUuid: number, show
const entries = grouped?.[skillUuid];
if (!grouped || !entries?.length) continue;
const base = SkillSet[skillUuid];
if (!base) return "";
if (!base) return [];
const sorted = [...entries].sort((a, b) => (a.s_lv ?? 0) - (b.s_lv ?? 0));
name = getGroupedTriggerName(grouped, skillUuid);
let inherit: string | undefined;
tiers = sorted.map((e: LvTriggerEntry, i: number) => {
if (e.desc) inherit = e.desc;
@@ -394,22 +404,57 @@ export function buildHeroSkillTierDesc(heroUuid: number, skillUuid: number, show
break;
}
}
if (tiers.length === 0) return "";
if (tiers.length === 0) return [];
const nameSeg = showName && name ? `${skillNameRich(name)} ` : "";
const lines: string[] = [];
const lines: HeroSkillTierLine[] = [];
for (let i = 0; i < tiers.length; i++) {
const t = tiers[i];
const head = `${nameSeg}Lv.${valueRich(t.sLv)}(英雄${valueRich(t.heroLv)}级${i === 0 ? "激活" : "解锁"})`;
if (i === 0) {
// 技能 1 级:完整能力 + 数值(含激活英雄等级)
// 技能 1 级:完整能力 + 数值
const text = t.desc ? renderDesc(t.desc, t.skill) : fallbackDesc(t.skill);
lines.push(`${head}: ${triggerLabel ? `${triggerLabel}, ` : ""}${text}`);
lines.push({ sLv: t.sLv, heroLv: t.heroLv, text: triggerLabel ? `${triggerLabel}, ${text}` : text });
} else {
// 技能 2 级起:相对上一级的提升 + 解锁英雄等级
// 技能 2 级起:相对上一级的提升
const diff = buildTierDiff(tiers[i - 1], t);
lines.push(`${head}: ${diff.length > 0 ? diff.join(",") : "无变化"}`);
lines.push({ sLv: t.sLv, heroLv: t.heroLv, text: diff.length > 0 ? diff.join(",") : "无变化" });
}
}
return lines.join("\n");
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: typeof HeroInfo[number], skillUuid: number): string {
for (const type of TRIGGER_KEYS) {
const grouped = hero[type] as TriggerGrouped | undefined;
if (grouped?.[skillUuid]?.length) return getGroupedTriggerName(grouped, skillUuid);
}
return SkillSet[skillUuid]?.name ?? "";
}