1. 重构getFieldSkillName和getReviveSkillName函数,新增hero参数获取英雄自定义技能名 2. 为FieldEntry和LvReviveEntry新增name字段,支持配置英雄专属技能展示名 3. 更新buildHeroSkillDesc和resolveTriggerSkillName函数,使用新的名称获取逻辑 4. 为部分英雄配置添加专属技能名称,统一技能展示文本
561 lines
26 KiB
TypeScript
561 lines
26 KiB
TypeScript
/**
|
||
* @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 ?? "";
|
||
}
|