1. 替换单个Label词条为RichText多行展示 2. 统一使用buildEquipRich和buildSkillDescRich生成带高亮的词条文本 3. 调整prefab的UI元素尺寸适配新的富文本展示 4. 新增showName参数适配不同场景的技能名显示需求
476 lines
21 KiB
TypeScript
476 lines
21 KiB
TypeScript
/**
|
||
* @file HeroSkillDesc.ts
|
||
* @description 英雄技能/能力描述的【唯一文字表达标准层】
|
||
*
|
||
* 设计目标:
|
||
* heroSet / SkillSet 中的 info / infos 仅作"基础备注",不再承担面板展示。
|
||
* 所有功能性描述(触发技能、驻场光环、复活、等级额外属性加成)统一由本模块
|
||
* 依据结构化配置动态拼出,数值永远来自 mergeSkillParams 合并后的实时配置,
|
||
* 杜绝"改数值忘改文案"的漂移。
|
||
*
|
||
* 输出(双格式,供 UI 按需取用):
|
||
* - buildSkillDesc(source) → 纯文本(Label 用),数值无颜色
|
||
* - buildSkillDescRich(source) → 富文本(RichText 用,BBCode),技能名蓝色、数值绿色高亮
|
||
* 两者共用同一套结构化行构建逻辑,仅渲染阶段区分,保证纯/富文本内容严格一致。
|
||
*
|
||
* 通用性(纯配置驱动):
|
||
* 1. 触发框架:按触发类型遍历(SkillSet + overrides,不动)
|
||
* 2. 效果文案:查 SkillDescSet 结构化模板渲染
|
||
* 3. 目标/属性名:由本模块统一语义化(target/hit_count、ATTR_NAME)
|
||
*
|
||
* 数据源(入参抽象 ISkillDescSource):
|
||
* heroInfo(静态 1 级配置)与 HeroAttrsComp(运行时应用 evolve 后的当前等级配置)
|
||
* 字段结构一致,天然都满足。HeroBoxComp 传 model 显示当前等级;卡池/商店传 heroInfo 显示 1 级。
|
||
*
|
||
* 依赖:
|
||
* - heroSet :SkillTriggerName、SkillTriggerType、HeroEvolve
|
||
* - SkillSet :SkillSet、mergeSkillParams、SkillOverrides、SkillKind、TGroup
|
||
* - SkillDescSet:结构化描述模板
|
||
* - FieldSkillSet(驻场技能)、BuffList(计时 buff 默认值兜底)、Attrs(属性枚举)
|
||
*/
|
||
import { HeroEvolve, SkillTriggerName, SkillTriggerType } from "./heroSet";
|
||
import { FieldSkillSet, mergeSkillParams, SkillConfig, SkillKind, SkillOverrides, SkillSet, TGroup } from "./SkillSet";
|
||
import { FieldDescSet, FieldValueFmt, SkillDescSet } from "./SkillDescSet";
|
||
import { BuffList } from "./BuffSet";
|
||
import { Attrs } from "./HeroAttrs";
|
||
import { CardConfig } from "./CardSet";
|
||
|
||
/**
|
||
* 生成器入参窄接口:只约束实际读取的字段。
|
||
* heroInfo 与 HeroAttrsComp 的对应字段结构完全一致,天然都满足此接口。
|
||
*/
|
||
export interface ISkillDescSource {
|
||
/** 当前等级(用于计算 revive 实际复活次数、evolve 累计加成;静态 heroInfo 恒为 1) */
|
||
lv?: number;
|
||
call?: { s_uuid: number; t_num: number; overrides?: SkillOverrides }[];
|
||
dead?: { s_uuid: number; t_num: number; overrides?: SkillOverrides }[];
|
||
fstart?: { s_uuid: number; t_num: number; overrides?: SkillOverrides }[];
|
||
fend?: { s_uuid: number; t_num: number; overrides?: SkillOverrides }[];
|
||
atking?: { s_uuid: number; t_num: number; overrides?: SkillOverrides }[];
|
||
atked?: { s_uuid: number; t_num: number; overrides?: SkillOverrides }[];
|
||
field?: number[];
|
||
revive?: { s_uuid: number; r_num: number; upr: number };
|
||
/** 等级进化配置(heroInfo 自带;HeroAttrsComp 记录快照),用于累计 ap/hp 额外加成文案 */
|
||
evolve?: Record<number, HeroEvolve>;
|
||
}
|
||
|
||
/**
|
||
* 一条能力的结构化行:数值片段单独抽出,供富文本高亮与纯文本复用。
|
||
* 渲染结果 = prefix + value(可高亮) + suffix。
|
||
*/
|
||
interface ISkillLine {
|
||
/** 数值前的文本(含触发条件、技能名、目标、动词等) */
|
||
prefix: string;
|
||
/** 需要高亮的核心数值文本(如 "15%"、"4次"、"+30"),无数值时为 "" */
|
||
value: string;
|
||
/** 数值后的文本(如 ",持续5秒" 之类尾部补充) */
|
||
suffix: string;
|
||
}
|
||
|
||
/** 需要遍历的触发技能类型列表(不含 Field 和 Revive,这两者结构不同需单独处理) */
|
||
const TRIGGER_KEYS: SkillTriggerType[] = [
|
||
SkillTriggerType.Call,
|
||
SkillTriggerType.Dead,
|
||
SkillTriggerType.FStart,
|
||
SkillTriggerType.FEnd,
|
||
SkillTriggerType.Atking,
|
||
SkillTriggerType.Atked,
|
||
];
|
||
|
||
/** 属性标准中文名映射(统一称谓,供 evolve 额外加成文案使用),仅覆盖 ap_bonus/hp_bonus 用到的属性 */
|
||
const ATTR_NAME: Partial<Record<Attrs, string>> = {
|
||
[Attrs.ap]: "攻击力",
|
||
[Attrs.hp_max]: "最大生命",
|
||
};
|
||
|
||
/** 富文本数值高亮色(亮绿色,与项目绿色系同源,深色面板上清晰可读) */
|
||
const RICH_VALUE_COLOR = "#27AE60";
|
||
|
||
/** 富文本技能名高亮色(蓝色,与数值绿色区分,标识技能名「」片段) */
|
||
const RICH_NAME_COLOR = "#2E86C1";
|
||
|
||
/** 技能名「」匹配模式(仅本模块行首/行中技能名片段使用) */
|
||
const SKILL_NAME_PATTERN = /「([^」]*)」/g;
|
||
|
||
/** "持续X秒"匹配模式(卡片描述后处理:把持续时间数值也高亮) */
|
||
const DURATION_PATTERN = /持续(\d+)秒/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));
|
||
// 治疗类命中低血量队友(SCastSystem.pickHealTargetsByMostMissingHp)
|
||
if (skill.kind === SkillKind.Heal) {
|
||
return n > 1 ? `${n}名低血量队友` : "低血量队友";
|
||
}
|
||
return `${n}名队友`;
|
||
}
|
||
default:
|
||
return "全体友方";
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 占位符模板替换,并把"核心数值片段"用 \x01...\x02 标记,便于上层拆分高亮。
|
||
*
|
||
* Why: 模板中 {value}{unit} 是玩家最关心的数值。这里在渲染时将其单独包裹,
|
||
* 使富文本可对其单独上色,纯文本则剥离标记后原样拼接,保证两种输出内容一致。
|
||
*
|
||
* @returns 含 \x01hlValue\x02 标记的完整文本
|
||
*/
|
||
function renderTemplate(templ: string, vars: Record<string, string | number>, hlValue: string): string {
|
||
let out = templ;
|
||
for (const key of Object.keys(vars)) {
|
||
out = out.split(`{${key}}`).join(String(vars[key]));
|
||
}
|
||
// 用 \x01...\x02 包裹数值片段作为标记,便于上层拆分为 prefix/value/suffix
|
||
return out.split(hlValue).join(`\x01${hlValue}\x02`);
|
||
}
|
||
|
||
/**
|
||
* 由一行模板渲染结果构造结构化行(剥离 \x01\x02 标记,把数值片段拆到 value 字段)
|
||
* @param head 行首(触发条件:技能名 )
|
||
* @param rendered 已用 \x01...\x02 标记数值片段的效果文本
|
||
*/
|
||
function makeLine(head: string, rendered: string): ISkillLine {
|
||
const start = rendered.indexOf("\x01");
|
||
const end = rendered.indexOf("\x02");
|
||
if (start >= 0 && end > start) {
|
||
// 恰好含一个数值片段:prefix=行首+数值前文本,value=数值,suffix=数值后文本
|
||
return {
|
||
prefix: `${head}${rendered.slice(0, start)}`,
|
||
value: rendered.slice(start + 1, end),
|
||
suffix: rendered.slice(end + 1),
|
||
};
|
||
}
|
||
// 无数值片段(未命中模板或兜底 info):剥离残留标记,整行作为 prefix,不高亮
|
||
return { prefix: `${head}${rendered.replace(/[\x01\x02]/g, "")}`, value: "", suffix: "" };
|
||
}
|
||
|
||
/**
|
||
* 根据合并后的技能配置构造结构化效果行(查 SkillDescSet 模板渲染 + 数值高亮抽取)
|
||
*
|
||
* 计时模式默认值兜底:
|
||
* overrides 仅给 timed_buff_id 而未覆写 buff_value / buff_duration 时,
|
||
* 从 BuffList[timed_buff_id] 取默认值,保证 1 级基础档文案完整。
|
||
*/
|
||
function buildEffectLine(head: string, skill: SkillConfig): ISkillLine {
|
||
const desc = SkillDescSet[skill.uuid];
|
||
// 未入表技能回退基础备注 info,无法抽出数值,整行不高亮
|
||
if (!desc) return { prefix: `${head}${String(skill.info ?? "")}`, value: "", suffix: "" };
|
||
|
||
const buff = skill.timed_buff_id !== undefined ? BuffList[skill.timed_buff_id] : undefined;
|
||
const isTimed = buff !== undefined && !!desc.templTimed;
|
||
const templ = isTimed ? desc.templTimed! : desc.templPerm;
|
||
const valueField = isTimed ? (desc.timedField ?? desc.permField) : desc.permField;
|
||
|
||
// 计时模式默认值兜底:buff_value 未覆写时用 BuffList 默认修饰值
|
||
let value = (skill[valueField as keyof SkillConfig] as number | undefined);
|
||
if (value === undefined && isTimed) {
|
||
value = buff!.modifiers?.[0]?.value ?? buff!.tick?.damage_or_heal ?? 0;
|
||
}
|
||
value = value ?? 0;
|
||
const dur = skill.buff_duration ?? buff?.duration ?? 0;
|
||
|
||
// 高亮片段 = 数值 + 单位(如 "15%"、"4次"、"+30"),是玩家视觉焦点
|
||
const hlValue = `${value}${desc.unit}`;
|
||
const rendered = renderTemplate(templ, {
|
||
target: buildTargetDesc(skill),
|
||
value: value,
|
||
unit: desc.unit,
|
||
verb: desc.verb,
|
||
dur: dur,
|
||
}, hlValue);
|
||
return makeLine(head, rendered);
|
||
}
|
||
|
||
/**
|
||
* 驻场光环(field)结构化行:查 FieldDescSet 按 type 渲染,value 实时取数值并高亮。
|
||
*
|
||
* Why: FieldSkillConfig.info 是手写文案,改 value 易漂移。这里以 FieldSkillType
|
||
* 为 key 查标准动词,数值按 fmt(比例/整数/减免)从 value 实时渲染。
|
||
*
|
||
* @param showName 是否在行中包含「技能名」片段(英雄面板需要,装备商店不需要)
|
||
*/
|
||
function buildFieldLine(head: string, uuid: number, showName: boolean = true): ISkillLine | null {
|
||
const fs = FieldSkillSet[uuid];
|
||
if (!fs) return null;
|
||
const desc = FieldDescSet[fs.type];
|
||
const nameSeg = showName ? `「${fs.name}」 ` : "";
|
||
// 未入表类型回退基础备注 info,整行不高亮
|
||
if (!desc) return { prefix: `${head}${nameSeg}${fs.info}`, value: "", suffix: "" };
|
||
|
||
// 数值按格式渲染为高亮片段
|
||
let valueText: string;
|
||
switch (desc.fmt) {
|
||
case FieldValueFmt.Percent:
|
||
valueText = `+${Math.round(fs.value * 100)}%`;
|
||
break;
|
||
case FieldValueFmt.IntMinus:
|
||
valueText = `-${fs.value}`;
|
||
break;
|
||
case FieldValueFmt.Int:
|
||
default:
|
||
valueText = `+${fs.value}`;
|
||
break;
|
||
}
|
||
return { prefix: `${head}${nameSeg}${desc.verb}`, value: valueText, suffix: "" };
|
||
}
|
||
|
||
/**
|
||
* 复活(revive)结构化行:复刻战斗公式,高亮复活次数与回血百分比
|
||
* 复活次数 = r_num + floor((lv - 1) * upr);回血百分比 = SkillSet[s_uuid].ap
|
||
*/
|
||
function buildReviveLine(source: ISkillDescSource): ISkillLine | null {
|
||
const revive = source.revive;
|
||
if (!revive) return null;
|
||
const base = SkillSet[revive.s_uuid];
|
||
if (!base) return null;
|
||
const lv = Math.max(1, source.lv ?? 1);
|
||
const maxCount = revive.r_num + Math.floor((lv - 1) * revive.upr);
|
||
const hpPct = base.ap ?? 0;
|
||
const tpl = SkillTriggerName[SkillTriggerType.Revive] ?? "复活";
|
||
// 高亮片段选回血百分比(数值中最关键的收益指标)
|
||
return {
|
||
prefix: `${tpl}:「${base.name}」 可复活${maxCount}次,每次恢复`,
|
||
value: `${hpPct}%`,
|
||
suffix: "生命",
|
||
};
|
||
}
|
||
|
||
/** evolve 额外属性加成(ap/hp)累计结构化行:高亮累计数值 */
|
||
function buildEvolveBonusLines(source: ISkillDescSource): ISkillLine[] {
|
||
const evolve = source.evolve;
|
||
if (!evolve) return [];
|
||
const lv = Math.max(1, source.lv ?? 1);
|
||
let apTotal = 0;
|
||
let hpTotal = 0;
|
||
for (let elv = 2; elv <= lv; elv++) {
|
||
const evo = evolve[elv];
|
||
if (!evo) continue;
|
||
apTotal += evo.ap_bonus ?? 0;
|
||
hpTotal += evo.hp_bonus ?? 0;
|
||
}
|
||
const lines: ISkillLine[] = [];
|
||
if (apTotal !== 0) lines.push({ prefix: "成长:额外", value: `+${apTotal}`, suffix: ATTR_NAME[Attrs.ap] ?? "攻击力" });
|
||
if (hpTotal !== 0) lines.push({ prefix: "成长:额外", value: `+${hpTotal}`, suffix: ATTR_NAME[Attrs.hp_max] ?? "最大生命" });
|
||
return lines;
|
||
}
|
||
|
||
/**
|
||
* 构建全部能力结构化行(触发技能 + 驻场光环 + 复活 + evolve 加成)。
|
||
* 纯文本与富文本两个公开接口共用此结果,仅渲染阶段不同。
|
||
*/
|
||
function buildSkillLines(source: ISkillDescSource): ISkillLine[] {
|
||
const lines: ISkillLine[] = [];
|
||
|
||
// ---- 6 种标准触发技能 ----
|
||
for (const key of TRIGGER_KEYS) {
|
||
const arr = source[key] as { s_uuid: number; t_num: number; overrides?: SkillOverrides }[] | undefined;
|
||
if (!arr?.length) continue;
|
||
// 触发类型简称(召唤/亡语/起手/生息/进击/受击)
|
||
const name = SkillTriggerName[key] ?? key;
|
||
// 仅进击/受击带触发次数,拼为"×n";其余简称直接用
|
||
const needCount = key === SkillTriggerType.Atking || key === SkillTriggerType.Atked;
|
||
for (const item of arr) {
|
||
const base = SkillSet[item.s_uuid];
|
||
if (!base) continue;
|
||
const skill = mergeSkillParams(base, item.overrides);
|
||
const trigger = needCount ? `${name}×${item.t_num}` : name;
|
||
lines.push(buildEffectLine(`${trigger}:「${base.name}」 `, skill));
|
||
}
|
||
}
|
||
|
||
// ---- 驻场光环(field):查 FieldDescSet 结构化渲染,value 数值高亮 ----
|
||
const fieldUuids = source[SkillTriggerType.Field] as number[] | undefined;
|
||
if (fieldUuids?.length) {
|
||
const tpl = SkillTriggerName[SkillTriggerType.Field] ?? "光环";
|
||
for (const uuid of fieldUuids) {
|
||
const line = buildFieldLine(`${tpl}:`, uuid, true);
|
||
if (line) lines.push(line);
|
||
}
|
||
}
|
||
|
||
// ---- 复活(revive)----
|
||
const reviveLine = buildReviveLine(source);
|
||
if (reviveLine) lines.push(reviveLine);
|
||
|
||
// ---- evolve 额外属性加成(ap/hp)----
|
||
lines.push(...buildEvolveBonusLines(source));
|
||
|
||
return lines;
|
||
}
|
||
|
||
/**
|
||
* 纯文本描述(Label 用):数值无颜色。
|
||
* @param source 触发技能数据源(heroInfo 静态配置 或 HeroAttrsComp 运行时当前等级配置)
|
||
* @returns 多行纯文本,每行一条能力,用 \n 分隔
|
||
*/
|
||
export function buildSkillDesc(source: ISkillDescSource): string {
|
||
return buildSkillLines(source)
|
||
.map(l => `${l.prefix}${l.value}${l.suffix}`)
|
||
.join("\n");
|
||
}
|
||
|
||
/**
|
||
* 富文本描述(RichText 用,BBCode):技能名用 <color=#2E86C1> 蓝色、数值用 <color=#27AE60> 亮绿色高亮。
|
||
* @param source 触发技能数据源(同 buildSkillDesc)
|
||
* @returns 多行富文本串,技能名片段包裹 <color=#2E86C1>、数值片段包裹 <color=#27AE60>,用 \n 分隔
|
||
*/
|
||
export function buildSkillDescRich(source: ISkillDescSource): string {
|
||
return buildSkillLines(source)
|
||
.map(l => {
|
||
const prefix = l.prefix.replace(SKILL_NAME_PATTERN, `「<color=${RICH_NAME_COLOR}>$1</color>」`);
|
||
const text = l.value
|
||
? `${prefix}<color=${RICH_VALUE_COLOR}>${l.value}</color>${l.suffix}`
|
||
: `${prefix}${l.suffix}`;
|
||
// 持续时间数值同样高亮(计时 buff 的"持续X秒")
|
||
return highlightDuration(text);
|
||
})
|
||
.join("\n");
|
||
}
|
||
|
||
/**
|
||
* 单个技能详情富文本(IBox 弹窗用):展示 CD/持续时间 + 结构化效果描述。
|
||
* @param skillUuid SkillSet 技能 UUID
|
||
* @param overrides 可选覆写参数(如药水卡自定义档位)
|
||
* @param lv 技能等级(仅用于展示,不影响数值计算)
|
||
* @returns 多行富文本串:第一行 CD/持续时间,第二行效果描述
|
||
*/
|
||
export function buildSingleSkillRich(skillUuid: number, overrides?: SkillOverrides, lv: number = 1): string {
|
||
const base = SkillSet[skillUuid];
|
||
if (!base) return "未知技能";
|
||
const skill = mergeSkillParams(base, overrides);
|
||
// SkillConfig 未定义 cd 字段,冷却时间由外部运行时数据传入;计时模式展示持续时间
|
||
const dur = skill.buff_duration ?? (skill.timed_buff_id !== undefined ? BuffList[skill.timed_buff_id]?.duration : undefined) ?? 0;
|
||
const cdTag = dur > 0 ? `<color=${RICH_VALUE_COLOR}>持续:${dur}s</color>` : "";
|
||
const nameTag = `「<color=${RICH_NAME_COLOR}>${skill.name}</color>」 Lv.${lv}`;
|
||
|
||
// 复用结构化效果行,head 为空避免重复触发类型前缀
|
||
const line = buildEffectLine("", skill);
|
||
const prefix = line.prefix.replace(SKILL_NAME_PATTERN, `「<color=${RICH_NAME_COLOR}>$1</color>」`);
|
||
const effect = line.value
|
||
? `${prefix}<color=${RICH_VALUE_COLOR}>${line.value}</color>${line.suffix}`
|
||
: `${prefix}${line.suffix}`;
|
||
|
||
return cdTag ? `${nameTag} ${cdTag}\n${effect}` : `${nameTag}\n${effect}`;
|
||
}
|
||
|
||
/**
|
||
* 装备/驻场光环详情富文本:展示全部词条,数值绿色高亮。
|
||
* @param fieldUuids FieldSkillSet 驻场光环 UUID 数组
|
||
* @param showName 是否包含「技能名」片段(英雄面板 true,装备商店 false)
|
||
* @returns 多行富文本串,每行一个词条
|
||
*/
|
||
export function buildEquipRich(fieldUuids: number[], showName: boolean = false): string {
|
||
if (!fieldUuids?.length) return "无词条";
|
||
const lines: string[] = [];
|
||
for (const uuid of fieldUuids) {
|
||
const line = buildFieldLine("", uuid, showName);
|
||
if (!line) continue;
|
||
const prefix = line.prefix.replace(SKILL_NAME_PATTERN, `「<color=${RICH_NAME_COLOR}>$1</color>」`);
|
||
const text = line.value
|
||
? `${prefix}<color=${RICH_VALUE_COLOR}>${line.value}</color>${line.suffix}`
|
||
: `${prefix}${line.suffix}`;
|
||
lines.push(text);
|
||
}
|
||
return lines.length > 0 ? lines.join("\n") : "无词条";
|
||
}
|
||
|
||
/**
|
||
* 把"持续X秒"中的 X 也包裹绿色高亮(卡片描述后处理)。
|
||
* Why: 计时模板 {dur} 不在 \x01\x02 高亮标记内,renderLineRich 仅高亮主数值;
|
||
* 持续时间对药水/计时 buff 同样关键,这里补充高亮,保证视觉一致。
|
||
*/
|
||
function highlightDuration(text: string): string {
|
||
return text.replace(DURATION_PATTERN, `持续<color=${RICH_VALUE_COLOR}>$1</color>秒`);
|
||
}
|
||
|
||
/**
|
||
* 结构化行 → 富文本串(技能名蓝色、数值绿色)。
|
||
* buildEquipRich / buildCardDescRich 共用,保证渲染规则唯一。
|
||
*/
|
||
function renderLineRich(line: ISkillLine): string {
|
||
const prefix = line.prefix.replace(SKILL_NAME_PATTERN, `「<color=${RICH_NAME_COLOR}>$1</color>」`);
|
||
return line.value
|
||
? `${prefix}<color=${RICH_VALUE_COLOR}>${line.value}</color>${line.suffix}`
|
||
: `${prefix}${line.suffix}`;
|
||
}
|
||
|
||
/**
|
||
* 扫描合并后技能的附加概率字段(crt/frz/stun/bck),非零的拼为高亮后缀。
|
||
* Why: 模板不再硬编码"有概率击晕"等文案,由实际配置驱动,避免描述与数值漂移。
|
||
* @returns 形如 ",30%击晕,20%冰冻";全为 0 时返回 ""
|
||
*/
|
||
function buildChanceSuffix(skill: SkillConfig): string {
|
||
const items: string[] = [];
|
||
const push = (val: number | undefined, label: string) => {
|
||
if (val && val > 0) items.push(`附加<color=${RICH_VALUE_COLOR}>${val}</color>%${label}`);
|
||
};
|
||
push(skill.crt, "暴击");
|
||
push(skill.frz, "冰冻");
|
||
push(skill.stun, "击晕");
|
||
push(skill.bck, "击退");
|
||
return items.length > 0 ? `,${items.join(",")}` : "";
|
||
}
|
||
|
||
/**
|
||
* 商店卡片描述富文本(SCardComp / ItemListComp 用,BBCode)。
|
||
*
|
||
* Why: 卡片 info 为手写文案,与 SkillSet/BuffList 数值易漂移。
|
||
* 本函数从 CardConfig.skill + overrides 合并后的实时配置生成描述,
|
||
* 数值随档位(如高级药水 buff_value)自动同步,数值绿色高亮、技能名蓝色。
|
||
*
|
||
* 描述规则:
|
||
* - 药品/技能卡(card.skill 有效)→ 查 SkillDescSet 模板渲染(计时模式取 BuffList 默认值兜底);
|
||
* 未入表技能回退 SkillSet.info / card.info,整行不高亮。
|
||
* - 装备卡(card.field 有效)→ 复用 buildEquipRich 逐词条渲染。
|
||
* - 触发节奏(t_times/t_inv)融入单行描述:定时触发"每M秒释放一次「技能名」,xxx,共N次";即时触发仅显示效果。
|
||
*
|
||
* @param card 卡片配置(SCardSet 技能卡 / ICardSet 药品卡 / EquipSet 装备卡)
|
||
* @returns 多行富文本串;无可描述内容时回退 card.info
|
||
*/
|
||
export function buildCardDescRich(card: CardConfig): string {
|
||
// 装备卡:逐词条渲染驻场光环
|
||
if (card.field && card.field.length > 0) {
|
||
return buildEquipRich(card.field);
|
||
}
|
||
|
||
// 药品/技能卡:合并覆写后按 SkillDescSet 模板渲染
|
||
const base = card.skill ? SkillSet[card.skill] : undefined;
|
||
if (base) {
|
||
const skill = mergeSkillParams(base, card.overrides);
|
||
const line = buildEffectLine("", skill);
|
||
// 未入表技能回退链:SkillSet.info → card.info
|
||
if (!line.value && !SkillDescSet[skill.uuid]) {
|
||
return String(skill.info || card.info || "");
|
||
}
|
||
|
||
// 触发节奏融入描述:定时触发"每M秒释放一次「技能名」,xxx,共N次";即时触发直接描述
|
||
const tTimes = card.t_times ?? 1;
|
||
const tInv = card.t_inv ?? 0;
|
||
const isInstant = card.is_inst || tInv <= 0;
|
||
// 附加概率后缀(暴击/冰冻/击晕/击退),由实际配置驱动
|
||
const suffix = buildChanceSuffix(skill);
|
||
const rich = highlightDuration(renderLineRich(line)) + suffix;
|
||
|
||
if (isInstant) {
|
||
return rich;
|
||
}
|
||
|
||
const rhythm = `每<color=${RICH_VALUE_COLOR}>${tInv}</color>秒释放一次「<color=${RICH_NAME_COLOR}>${skill.name}</color>」`;
|
||
const times = `,共<color=${RICH_VALUE_COLOR}>${tTimes}</color>次`;
|
||
return `${rhythm},${rich}${times}`;
|
||
}
|
||
|
||
return card.info || "";
|
||
}
|