refactor(skill-text): 重构技能文案系统,拆分英雄/卡牌语境

1.  将原HeroSkillDesc拆分为CardSkillText(卡牌/道具通用)和HeroSkillText(英雄专属)
2.  移除废弃的HeroSkillDesc文件与相关引用
3.  统一技能文案生成逻辑,修复数值漂移问题
4.  调整英雄配置中技能解锁等级与分档规则
5.  优化技能弹窗UI与参数传递逻辑
This commit is contained in:
panFD
2026-08-28 08:36:39 +08:00
parent 423653c4c8
commit 10a6d18836
17 changed files with 1698 additions and 1782 deletions

View File

@@ -2,7 +2,7 @@
* @file HeroSkillText.ts
* @description 英雄技能文案生成器(heroInfo 内联描述模板驱动)
*
* 与 HeroSkillDesc(SkillDescSet 中央模板表)并列的新一代文案层:
* 英雄语境描述文案层(原 HeroSkillDesc 已由本模块 + CardSkillText 替代并移除):
* - 描述模板直接配置在 heroInfo(HSkillInfo.desc / LvSkillEntry.desc / LvTriggerEntry.desc),
* "同一底座技能、不同英雄语境"的表述差异在英雄配置处表达;
* - 数值永远从 mergeSkillParams 合并后的实时配置取值(占位符渲染),
@@ -21,10 +21,10 @@
* {字段名} 合并后技能配置数值(ap/para/frz/stun/crt/bck/hit_count/num/summon_count…)
*
* 依赖:heroSet(配置与 resolver)、SkillSet/BuffSet/SkillDescSet(底座)、smc(局外等级)。
* HeroSkillDesc.ts 暂时保留兼容旧 UI 引用,待迁移完成后移除。
* 卡牌/道具语境描述见 config/CardSkillText.ts。
*/
import {
HeroInfo, SkillTriggerType, SkillTriggerName, HSkillInfo, LvTriggerEntry,
HeroInfo, heroInfo, SkillTriggerType, SkillTriggerName, HSkillInfo, LvTriggerEntry,
TriggerGrouped, resolveSkillOverrides, resolveTriggerByLv, resolveFieldByLv, resolveReviveByLv,
getGroupedTriggerName, getFieldSkillName, getReviveSkillName,
} from "./config/heroSet";
@@ -34,7 +34,7 @@ import { FieldDescSet, FieldValueFmt } from "./config/SkillDescSet";
import { FacSet } from "./config/GameSet";
import { smc } from "./SingletonModuleComp";
/* ==================== 富文本片段(与 HeroSkillDesc 同配色,保证新旧面板视觉一致) ==================== */
/* ==================== 富文本片段(与 CardSkillText 同配色,保证各面板视觉一致) ==================== */
/** 富文本数值高亮色(鲜亮绿色) */
const RICH_VALUE_COLOR = "#3CE66E";
@@ -344,6 +344,84 @@ function collectSkillTiers(hSkill: HSkillInfo): ISkillTierInfo[] {
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) */
@@ -360,11 +438,17 @@ export interface HeroSkillTierLine {
* slv1 给完整能力 + 数值;slv2+ 给相对上一级的提升。每档附带解锁英雄等级,
* 调用方可自行对比英雄当前等级(smc.getHeroOuterLv)判定激活状态。
*
* 技能定位顺序:hero.skills[skillUuid](普攻/大招)→ 5 类触发技能组 → 未找到返回 []。
* 技能定位顺序(覆盖英雄图鉴全部 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)
* @param skillUuid 技能 uuid(skills key / 触发组 s_uuid / FieldSkillSet uuid / 复活 SkillSet uuid)
* @returns 档位行数组;技能不属于该英雄时返回 []
*/
export function buildHeroSkillTierLines(heroUuid: number, skillUuid: number): HeroSkillTierLine[] {
@@ -404,7 +488,15 @@ export function buildHeroSkillTierLines(heroUuid: number, skillUuid: number): He
break;
}
}
if (tiers.length === 0) return [];
// 光环/复活路径(无 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++) {
@@ -450,11 +542,19 @@ export function buildHeroSkillTierDesc(heroUuid: number, skillUuid: number, show
.join("\n");
}
/** 在英雄触发组中解析技能对外展示名(非触发技能返回底座名) */
function resolveTriggerSkillName(hero: typeof HeroInfo[number], skillUuid: number): string {
/** 在英雄技能配置中解析技能对外展示名(触发组名 / 光环词条名 / 复活名 / 底座名) */
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);
}
// 驻场光环:词条名取 FieldSkillSet
if (hero.field?.some(e => e.uuids.some(u => u.uuid === skillUuid))) {
return getFieldSkillName(skillUuid);
}
// 复活技能:统一复活称谓
if (hero.revive?.some(e => e.s_uuid === skillUuid)) {
return getReviveSkillName();
}
return SkillSet[skillUuid]?.name ?? "";
}