refactor(skillDesc): 重构技能描述系统,统一技能命名规则

1.  移除旧的TriggerSkillNameSet,改为从HeroInfo配置读取技能展示名
2.  新增buildSkillTierDescRich方法,实现技能等级分段展示
3.  废弃heroInfo.infos字段,使用动态生成的技能描述替代
4.  新增技能名配置字段,统一主技能和触发技能的展示逻辑
This commit is contained in:
pan
2026-08-27 10:56:37 +08:00
parent 1375712f90
commit 04835e29f6
2 changed files with 227 additions and 198 deletions

View File

@@ -28,7 +28,7 @@
* - SkillDescSet:结构化描述模板
* - FieldSkillSet(驻场技能)、BuffList(计时 buff 默认值兜底)、Attrs(属性枚举)
*/
import { SkillTriggerName, SkillTriggerType, TriggerGrouped, LvTriggerEntry, LvFieldEntry, LvReviveEntry, LvBonusEntry, FieldEntry, resolveTriggerByLv, resolveFieldByLv, resolveReviveByLv, resolveSkillOverrides, getTriggerSkillName, HGrowType, calcGrowBonus, HSkillInfo } from "./heroSet";
import { SkillTriggerName, SkillTriggerType, TriggerGrouped, LvTriggerEntry, LvFieldEntry, LvReviveEntry, LvBonusEntry, FieldEntry, resolveTriggerByLv, resolveFieldByLv, resolveReviveByLv, resolveSkillOverrides, getGroupedTriggerName, getFieldSkillName, getReviveSkillName, HGrowType, calcGrowBonus, HSkillInfo } from "./heroSet";
import { FieldSkillSet, mergeSkillParams, SkillConfig, SkillKind, SkillOverrides, SkillSet, TGroup } from "./SkillSet";
import { FieldDescSet, FieldValueFmt, SkillDescSet } from "./SkillDescSet";
import { BuffList } from "./BuffSet";
@@ -383,7 +383,7 @@ function buildSkillLines(source: ISkillDescSource, showName: boolean = true, sho
const sLv = hSkill.lv_entries?.length
? (hSkill.lv_entries.filter(e => e.lv <= lv).pop()?.s_lv ?? hSkill.lv)
: hSkill.lv;
const nameSeg = showName ? `「${base.name}」 ` : "";
const nameSeg = showName ? `「${hSkill.name ?? base.name}」 ` : "";
lines.push(buildEffectLine(`Lv${sLv}: ${nameSeg}`, skill));
}
}
@@ -404,8 +404,8 @@ function buildSkillLines(source: ISkillDescSource, showName: boolean = true, sho
if (!base) continue;
const skill = mergeSkillParams(base, item.overrides);
const trigger = needCount ? `${name}×${item.t_num}` : name;
// 触发语境专属名称(如 atked.6301=不屈壁垒),未配置回退通用名
const nameSeg = showName ? `「${getTriggerSkillName(key, item.s_uuid)}」 ` : "";
// 触发技能名走 HeroInfo 语境配置(如 atked.6301=不屈壁垒),未配置回退 SkillSet 内部名
const nameSeg = showName ? `「${getGroupedTriggerName(grouped, item.s_uuid)}」 ` : "";
lines.push(buildEffectLine(`${trigger}:${nameSeg}`, skill));
}
continue;
@@ -418,7 +418,7 @@ function buildSkillLines(source: ISkillDescSource, showName: boolean = true, sho
const base = SkillSet[Number(sKey)];
if (!base) continue;
const sorted = [...entries].sort((a, b) => a.lv - b.lv);
const nameSeg = showName ? `「${getTriggerSkillName(key, Number(sKey))}」 ` : "";
const nameSeg = showName ? `「${getGroupedTriggerName(grouped, Number(sKey))}」 ` : "";
const headOf = (e: LvTriggerEntry) => `Lv${e.lv}: ${needCount ? `${name}×${e.t_num}` : name} ${nameSeg}`;
// 取当前等级生效档(≤lv 的最大档)
@@ -602,7 +602,7 @@ export function buildSkillDescMoreRich(source: ISkillDescSource, showName: boole
const sLv = hSkill.lv_entries?.length
? (hSkill.lv_entries.filter(e => e.lv <= lv).pop()?.s_lv ?? hSkill.lv)
: hSkill.lv;
const head = showName ? `「${base.name}」 Lv.${sLv}: ` : `Lv.${sLv}: `;
const head = showName ? `「${hSkill.name ?? base.name}」 Lv.${sLv}: ` : `Lv.${sLv}: `;
lines.push(effectRich(buildEffectLine(head, skill)));
}
}
@@ -620,8 +620,8 @@ export function buildSkillDescMoreRich(source: ISkillDescSource, showName: boole
const base = SkillSet[sUuid];
if (!base) continue;
const sorted = [...entries].sort((a, b) => a.lv - b.lv);
// 技能名走触发语境专属命名(如 atked.6301=不屈壁垒)
const skillName = getTriggerSkillName(key, sUuid);
// 技能名走 HeroInfo 触发语境配置(如 atked.6301=不屈壁垒)
const skillName = getGroupedTriggerName(grouped, sUuid);
// 当前生效档(≤lv 最大档),未激活则用最低档
let activeIdx = -1;
@@ -645,7 +645,7 @@ export function buildSkillDescMoreRich(source: ISkillDescSource, showName: boole
const fieldEntries = source[SkillTriggerType.Field] as LvFieldEntry[] | undefined;
if (fieldEntries?.length) {
for (const entry of resolveFieldByLv(fieldEntries, lv)) {
const baseName = getTriggerSkillName(SkillTriggerType.Field, entry.uuid);
const baseName = getFieldSkillName(entry.uuid);
const line = buildFieldLine("", entry, showName, baseName);
if (line) lines.push(effectRich(line));
}
@@ -655,7 +655,7 @@ export function buildSkillDescMoreRich(source: ISkillDescSource, showName: boole
// 血量百分比来自 SkillSet[s_uuid].ap(运行时口径),upr 是每级追加次数,不在此展示
const revive = resolveReviveByLv(source[SkillTriggerType.Revive] as LvReviveEntry[] | undefined, lv);
if (revive) {
const reviveName = getTriggerSkillName(SkillTriggerType.Revive, revive.s_uuid);
const reviveName = getReviveSkillName();
const reviveHpPct = SkillSet[revive.s_uuid]?.ap ?? 50;
lines.push(showName
? `${skillNameRich(reviveName)}: 每回合拥有${valueRich(revive.r_num)}次重生机会, 死亡后以${valueRich(`${Math.round(reviveHpPct)}%`)}血量原地重生`
@@ -757,3 +757,164 @@ export function buildCardDescRich(card: CardConfig): string {
return card.info || "";
}
/* =====================================================================================
* 按技能等级分段说明(升级引导 / 图鉴逐档预览)
*
* 与 buildSkillDesc(当前等级)/ buildSkillDescRich(全档位合并单行)并列的第三种模式:
* 每个技能按等级档位逐段输出 —— 技能 Lv1 给完整能力+数值,Lv2 起只给相对上一级的
* 「数值漂移 + 新功能」,让玩家一眼看清每一级"多得到了什么"。
* ===================================================================================*/
/** 参与"新功能/数值漂移"对比的展示字段(排除动画/弹道/特效等玩家无感字段) */
const DIFF_DISPLAY_KEYS: (keyof SkillOverrides)[] = [
"ap", "hit_count", "num", "summon_count", "crt", "frz", "stun", "para",
"bck", "bck_chance", "timed_buff_id", "buff_value", "buff_duration",
];
/** 概率/次数类展示字段的单位与称谓(新功能标记 + 数值漂移描述共用) */
const DIFF_FIELD_META: Partial<Record<keyof SkillOverrides, { 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: "秒" },
};
/** 一个技能的等级档位(统一普攻 lv_entries 与触发技能 LvTriggerEntry 两种结构) */
interface ISkillTier {
/** 技能等级(展示用,取 s_lv,缺省按档位序号) */
sLv: number;
/** 该档生效的英雄等级(解锁提示) */
heroLv: number;
/** 合并底座后的完整技能配置 */
skill: SkillConfig;
}
/** 收集普攻/主技能的全部等级档位(按 sLv 升序)。Lv1=基础档(底+基础 overrides),Lv2+=lv_entries 各档 */
function collectTiers(base: SkillConfig, baseOverrides: SkillOverrides | undefined,
entries: { lv: number; s_lv?: number; overrides?: SkillOverrides }[]): ISkillTier[] {
const tiers: ISkillTier[] = [];
// Lv1 = 基础档(底 + 基础 overrides)
const lv1Entry = entries.find(e => (e.s_lv ?? 0) === 1);
tiers.push({
sLv: lv1Entry?.s_lv ?? 1,
heroLv: lv1Entry?.lv ?? 1,
skill: mergeSkillParams(base, baseOverrides),
});
// Lv2+ = lv_entries 中 s_lv≥2 的档(与基础档浅合并,档位优先)
for (const e of entries) {
const sLv = e.s_lv ?? 0;
if (sLv < 2) continue;
tiers.push({ sLv, heroLv: e.lv, skill: mergeSkillParams(mergeSkillParams(base, baseOverrides), e.overrides) });
}
return tiers.sort((a, b) => a.sLv - b.sLv);
}
/** 相邻两档的 diff:数值漂移(ap 等差值)+ 新功能(上级没有的 timed_buff/暴击/冰冻等) */
function buildTierDiff(prev: SkillConfig, cur: SkillConfig): string[] {
const parts: string[] = [];
for (const key of DIFF_DISPLAY_KEYS) {
const pv = (prev[key as keyof SkillConfig] as number | undefined) ?? 0;
const cv = (cur[key as keyof SkillConfig] as number | undefined) ?? 0;
if (cv === pv) continue;
// 新功能:上级为 0/无,本档新增(含 timed_buff_id 新注入)
if (key === "timed_buff_id") {
if (cv > 0 && pv === 0) parts.push(`新增${valueRich("计时增益")}`);
continue;
}
const meta = DIFF_FIELD_META[key];
if (!meta) continue;
if (pv === 0 && cv > 0) {
parts.push(`新增${meta.label}${valueRich(`${cv}${meta.unit}`)}`);
} else {
const diff = cv - pv;
const sign = diff > 0 ? "+" : "";
parts.push(`${meta.label}${valueRich(`${sign}${diff}${meta.unit}`)}`);
}
}
return parts;
}
/** 单个技能的分段说明行(Lv1 完整 / Lv2+ diff),返回富文本行数组 */
function buildTierLines(name: string, tiers: ISkillTier[], headPrefix: string): string[] {
const lines: string[] = [];
for (let i = 0; i < tiers.length; i++) {
const t = tiers[i];
const head = `${skillNameRich(name)} ${valueRich(`Lv.${t.sLv}`)}${headPrefix}`;
if (i === 0) {
// 技能 1 级:完整能力 + 数值
const line = buildEffectLine("", t.skill);
const effect = `${line.prefix}${line.value ? valueRich(line.value) : ""}${line.suffix}`;
lines.push(`${head}: ${highlightDuration(effect)}`);
} else {
// 技能 2 级起:相对上一级的数值漂移 + 新功能
const diff = buildTierDiff(tiers[i - 1].skill, t.skill);
const unlock = t.heroLv > 1 ? `(英雄${valueRich(t.heroLv)}级解锁)` : "";
lines.push(diff.length > 0
? `${head}${unlock}: ${diff.join(",")}`
: `${head}${unlock}: 无变化`);
}
}
return lines;
}
/**
* 按技能等级分段输出说明(富文本)。
*
* 两种对外说明模式的第二种(第一种为 buildSkillDesc/buildSkillDescMoreRich 的"当前等级"模式):
* 每个技能逐档输出 —— Lv1 给完整能力与数值,Lv2 起给相对上一级的提升(数值漂移 + 新功能),
* 并标注解锁所需英雄等级。适合升级引导、图鉴逐档预览。
*
* @param source 英雄数据源(heroInfo 静态配置 或 HeroAttrsComp 运行时配置)
* @returns 多行富文本串,\n 分隔;按技能分组、组内按技能等级升序
*/
export function buildSkillTierDescRich(source: ISkillDescSource): string {
const lines: string[] = [];
// ---- 普攻/主技能(skills):lv_entries 分档 ----
if (source.skills) {
for (const key in source.skills) {
const hSkill = source.skills[key];
if (!hSkill) continue;
const base = SkillSet[hSkill.uuid];
if (!base) continue;
const entries = hSkill.lv_entries ?? [];
// 无分档的单档技能只出 Lv1 完整行
const tiers = collectTiers(base, hSkill.overrides, entries);
lines.push(...buildTierLines(hSkill.name ?? base.name, tiers, ""));
}
}
// ---- 触发技能(atking/atked/dead/fstart/call):LvTriggerEntry 分档 ----
for (const key of TRIGGER_KEYS) {
const grouped = source[key] as TriggerGrouped | undefined;
if (!grouped) continue;
const trigName = SkillTriggerName[key] ?? key;
const needCount = key === SkillTriggerType.Atking || key === SkillTriggerType.Atked;
for (const sKey in grouped) {
const entries = grouped[sKey];
if (!entries?.length) continue;
const base = SkillSet[Number(sKey)];
if (!base) continue;
const sorted = [...entries].sort((a, b) => (a.s_lv ?? 0) - (b.s_lv ?? 0));
const name = getGroupedTriggerName(grouped, Number(sKey));
// 触发技能以第 1 档为基础档,逐档 diff(t_num 变化并入新功能/数值漂移)
const tiers: ISkillTier[] = sorted.map((e, i) => ({
sLv: e.s_lv ?? (i + 1),
heroLv: e.lv,
skill: mergeSkillParams(base, e.overrides),
}));
const headPrefix = needCount && sorted[0] ? ` ${trigName}${valueRich(sorted[0].t_num)}次` : ` ${trigName}`;
lines.push(...buildTierLines(name, tiers, headPrefix));
}
}
return lines.join("\n");
}