refactor(skillDesc): 重构技能描述渲染,支持全档位合并展示
修改了技能描述构建逻辑,新增showLocked参数实现全档位模式: 1. 英雄盒面板现在可以展示技能全部档位,已激活档正常高亮,未激活档位灰色弱化 2. 重构了buildReviveLine、buildSkillLines等核心函数,拆分逻辑适配多档位展示 3. 新增locked和nextValues字段支持未激活状态和未来档位数值拼接 4. 优化了富文本渲染规则,区分激活/未激活样式
This commit is contained in:
@@ -28,7 +28,7 @@
|
||||
* - SkillDescSet:结构化描述模板
|
||||
* - FieldSkillSet(驻场技能)、BuffList(计时 buff 默认值兜底)、Attrs(属性枚举)
|
||||
*/
|
||||
import { SkillTriggerName, SkillTriggerType, TriggerGrouped, LvFieldEntry, LvReviveEntry, LvBonusEntry, resolveTriggerByLv, resolveFieldByLv, resolveReviveByLv } from "./heroSet";
|
||||
import { SkillTriggerName, SkillTriggerType, TriggerGrouped, LvTriggerEntry, LvFieldEntry, LvReviveEntry, LvBonusEntry, resolveTriggerByLv, resolveFieldByLv, resolveReviveByLv } from "./heroSet";
|
||||
import { FieldSkillSet, mergeSkillParams, SkillConfig, SkillKind, SkillOverrides, SkillSet, TGroup } from "./SkillSet";
|
||||
import { FieldDescSet, FieldValueFmt, SkillDescSet } from "./SkillDescSet";
|
||||
import { BuffList } from "./BuffSet";
|
||||
@@ -66,6 +66,10 @@ interface ISkillLine {
|
||||
value: string;
|
||||
/** 数值后的文本(如 ",持续5秒" 之类尾部补充) */
|
||||
suffix: string;
|
||||
/** 尚未激活(档位等级高于当前等级):富文本整行灰色、不做高亮 */
|
||||
locked?: boolean;
|
||||
/** 同一技能未来档位的数值列表(全档位合并模式专用):渲染为 value 后灰色 "/v1/v2" 拼接 */
|
||||
nextValues?: string[];
|
||||
}
|
||||
|
||||
/** 需要遍历的触发技能类型列表(不含 Field 和 Revive,这两者结构不同需单独处理) */
|
||||
@@ -90,6 +94,9 @@ const RICH_VALUE_COLOR = "#27AE60";
|
||||
/** 富文本技能名高亮色(蓝色,与数值绿色区分,标识技能名「」片段) */
|
||||
const RICH_NAME_COLOR = "#2E86C1";
|
||||
|
||||
/** 富文本未激活档位整行灰色(低饱和,深色面板上可读但明显弱化) */
|
||||
const RICH_LOCKED_COLOR = "#95A5A6";
|
||||
|
||||
/** 技能名「」匹配模式(仅本模块行首/行中技能名片段使用) */
|
||||
const SKILL_NAME_PATTERN = /「([^」]*)」/g;
|
||||
|
||||
@@ -229,23 +236,69 @@ function buildFieldLine(head: string, uuid: number, showName: boolean = true): I
|
||||
/**
|
||||
* 复活(revive)结构化行:复刻战斗公式,高亮复活次数与回血百分比
|
||||
* 复活次数 = r_num + floor((lv - 1) * upr);回血百分比 = SkillSet[s_uuid].ap
|
||||
*
|
||||
* @param showLocked true 时列出全部档位并标激活状态(未激活档位次数按解锁等级预估);
|
||||
* false 时仅输出当前等级生效档
|
||||
*/
|
||||
function buildReviveLine(source: ISkillDescSource, showName: boolean = true): ISkillLine | null {
|
||||
function buildReviveLines(source: ISkillDescSource, showName: boolean = true, showLocked: boolean = false): ISkillLine[] {
|
||||
const lv = Math.max(1, source.lv ?? 1);
|
||||
const revive = resolveReviveByLv(source.revive, lv);
|
||||
if (!revive) return null;
|
||||
const base = SkillSet[revive.s_uuid];
|
||||
if (!base) return null;
|
||||
const maxCount = revive.r_num + Math.floor((lv - 1) * revive.upr);
|
||||
const hpPct = base.ap ?? 0;
|
||||
const entries = source.revive;
|
||||
if (!entries?.length) return [];
|
||||
|
||||
const tpl = SkillTriggerName[SkillTriggerType.Revive] ?? "复活";
|
||||
const nameSeg = showName ? `「${base.name}」 ` : "";
|
||||
// 高亮片段选回血百分比(数值中最关键的收益指标)
|
||||
return {
|
||||
prefix: `${tpl}:${nameSeg}可复活${maxCount}次,每次恢复`,
|
||||
value: `${hpPct}%`,
|
||||
suffix: "生命",
|
||||
const makeLine = (entry: { s_uuid: number; r_num: number; upr: number }, head: string, countLv: number, locked: boolean): ISkillLine | null => {
|
||||
const base = SkillSet[entry.s_uuid];
|
||||
if (!base) return null;
|
||||
const maxCount = entry.r_num + Math.floor((countLv - 1) * entry.upr);
|
||||
const hpPct = base.ap ?? 0;
|
||||
const nameSeg = showName ? `「${base.name}」 ` : "";
|
||||
// 高亮片段选回血百分比(数值中最关键的收益指标)
|
||||
return {
|
||||
prefix: `${head}${nameSeg}可复活${maxCount}次,每次恢复`,
|
||||
value: `${hpPct}%`,
|
||||
suffix: "生命",
|
||||
locked,
|
||||
};
|
||||
};
|
||||
|
||||
if (!showLocked) {
|
||||
const revive = resolveReviveByLv(entries, lv);
|
||||
if (!revive) return [];
|
||||
const line = makeLine(revive, `${tpl}:`, lv, false);
|
||||
return line ? [line] : [];
|
||||
}
|
||||
|
||||
// 全档位合并模式:每个 s_uuid 只出一行,等级前缀置前("Lv2: 复活 ...")
|
||||
const groups: Record<number, LvReviveEntry[]> = {};
|
||||
for (const e of entries) {
|
||||
(groups[e.s_uuid] ??= []).push(e);
|
||||
}
|
||||
const lines: ISkillLine[] = [];
|
||||
for (const key in groups) {
|
||||
const sorted = [...groups[Number(key)]].sort((a, b) => a.lv - b.lv);
|
||||
let active: LvReviveEntry | undefined;
|
||||
for (const e of sorted) { if (e.lv <= lv) active = e; }
|
||||
|
||||
if (active) {
|
||||
const line = makeLine(active, `Lv${active.lv}: ${tpl} `, lv, false);
|
||||
if (!line) continue;
|
||||
// 未来档回血数值以灰色追加(同 s_uuid 的 base.ap 通常相同,仅不同时才拼接)
|
||||
const nextVals: string[] = [];
|
||||
for (const e of sorted) {
|
||||
if (e.lv <= lv) continue;
|
||||
const v = `${SkillSet[e.s_uuid]?.ap ?? 0}%`;
|
||||
if (v !== line.value && !nextVals.includes(v)) nextVals.push(v);
|
||||
}
|
||||
if (nextVals.length) line.nextValues = nextVals;
|
||||
lines.push(line);
|
||||
} else {
|
||||
// 完全未激活:预览最低档,次数按解锁等级预估,整行灰色
|
||||
const first = sorted[0];
|
||||
const line = makeLine(first, `Lv${first.lv}: ${tpl} `, first.lv, true);
|
||||
if (line) lines.push(line);
|
||||
}
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
/** 额外属性加成(ap/hp)累计结构化行:高亮累计数值 */
|
||||
@@ -268,46 +321,110 @@ function buildEvolveBonusLines(source: ISkillDescSource): ISkillLine[] {
|
||||
/**
|
||||
* 构建全部能力结构化行(触发技能 + 驻场光环 + 复活 + 额外加成)。
|
||||
* 纯文本与富文本两个公开接口共用此结果,仅渲染阶段不同。
|
||||
* @param showName 是否在行中包含「技能名」片段(英雄盒面板传 false 隐藏技能名)
|
||||
*
|
||||
* @param showName 是否在行中包含「技能名」片段
|
||||
* @param showLocked true 时为全档位合并模式:每个技能只出一行——已激活档正常渲染且
|
||||
* 未来档数值灰色拼接在 value 后(nextValues);完全未激活的技能
|
||||
* 预览最低档、整行 locked 灰色、Lv 前缀置前。false 时仅输出当前生效档
|
||||
*/
|
||||
function buildSkillLines(source: ISkillDescSource, showName: boolean = true): ISkillLine[] {
|
||||
function buildSkillLines(source: ISkillDescSource, showName: boolean = true, showLocked: boolean = false): ISkillLine[] {
|
||||
const lines: ISkillLine[] = [];
|
||||
const lv = Math.max(1, source.lv ?? 1);
|
||||
|
||||
// ---- 6 种标准触发技能(按当前等级 resolve 后遍历) ----
|
||||
// ---- 6 种标准触发技能 ----
|
||||
for (const key of TRIGGER_KEYS) {
|
||||
const grouped = source[key] as TriggerGrouped | undefined;
|
||||
const arr = resolveTriggerByLv(grouped, lv);
|
||||
if (!arr.length) continue;
|
||||
if (!grouped) 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 (!showLocked) {
|
||||
// 仅当前等级生效档(resolve 取各 s_uuid ≤lv 的最大档)
|
||||
for (const item of resolveTriggerByLv(grouped, lv)) {
|
||||
const base = SkillSet[item.s_uuid];
|
||||
if (!base) continue;
|
||||
const skill = mergeSkillParams(base, item.overrides);
|
||||
const trigger = needCount ? `${name}×${item.t_num}` : name;
|
||||
const nameSeg = showName ? `「${base.name}」 ` : "";
|
||||
lines.push(buildEffectLine(`${trigger}:${nameSeg}`, skill));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 全档位合并模式:每个 s_uuid 只出一行,等级前缀置前("Lv1: 进击×5 ...")
|
||||
for (const sKey in grouped) {
|
||||
const entries = grouped[sKey];
|
||||
if (!entries?.length) continue;
|
||||
const base = SkillSet[Number(sKey)];
|
||||
if (!base) continue;
|
||||
const skill = mergeSkillParams(base, item.overrides);
|
||||
const trigger = needCount ? `${name}×${item.t_num}` : name;
|
||||
const sorted = [...entries].sort((a, b) => a.lv - b.lv);
|
||||
const nameSeg = showName ? `「${base.name}」 ` : "";
|
||||
lines.push(buildEffectLine(`${trigger}:${nameSeg}`, skill));
|
||||
const headOf = (e: LvTriggerEntry) => `Lv${e.lv}: ${needCount ? `${name}×${e.t_num}` : name} ${nameSeg}`;
|
||||
|
||||
// 取当前等级生效档(≤lv 的最大档)
|
||||
let active: LvTriggerEntry | undefined;
|
||||
for (const e of sorted) { if (e.lv <= lv) active = e; }
|
||||
|
||||
if (active) {
|
||||
const line = buildEffectLine(headOf(active), mergeSkillParams(base, active.overrides));
|
||||
// 未来档数值以灰色 "/v1/v2" 追加在高亮数值后(仅收集与当前不同的数值,避免重复)
|
||||
const nextVals: string[] = [];
|
||||
for (const e of sorted) {
|
||||
if (e.lv <= lv) continue;
|
||||
const v = buildEffectLine("", mergeSkillParams(base, e.overrides)).value;
|
||||
if (v && v !== line.value && !nextVals.includes(v)) nextVals.push(v);
|
||||
}
|
||||
if (nextVals.length) line.nextValues = nextVals;
|
||||
lines.push(line);
|
||||
} else {
|
||||
// 技能完全未激活:预览最低档效果,整行灰色
|
||||
const first = sorted[0];
|
||||
const line = buildEffectLine(headOf(first), mergeSkillParams(base, first.overrides));
|
||||
line.locked = true;
|
||||
lines.push(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 驻场光环(field):查 FieldDescSet 结构化渲染,value 数值高亮 ----
|
||||
const fieldUuids = resolveFieldByLv(source[SkillTriggerType.Field] as LvFieldEntry[] | undefined, lv);
|
||||
if (fieldUuids?.length) {
|
||||
const fieldEntries = source[SkillTriggerType.Field] as LvFieldEntry[] | undefined;
|
||||
if (fieldEntries?.length) {
|
||||
const tpl = SkillTriggerName[SkillTriggerType.Field] ?? "光环";
|
||||
for (const uuid of fieldUuids) {
|
||||
const line = buildFieldLine(`${tpl}:`, uuid, showName);
|
||||
if (line) lines.push(line);
|
||||
if (!showLocked) {
|
||||
for (const uuid of resolveFieldByLv(fieldEntries, lv)) {
|
||||
const line = buildFieldLine(`${tpl}:`, uuid, showName);
|
||||
if (line) lines.push(line);
|
||||
}
|
||||
} else {
|
||||
// 全档位合并模式:生效档(≤lv 最大档)正常显示,未来档整行灰色且 Lv 置前;
|
||||
// 被替换的历史档不显示(resolve 语义下升级后旧档光环失效)
|
||||
const sorted = [...fieldEntries].sort((a, b) => a.lv - b.lv);
|
||||
let activeTier: LvFieldEntry | undefined;
|
||||
for (const tier of sorted) { if (tier.lv <= lv) activeTier = tier; }
|
||||
if (activeTier) {
|
||||
for (const uuid of activeTier.uuids) {
|
||||
const line = buildFieldLine(`Lv${activeTier.lv}: ${tpl} `, uuid, showName);
|
||||
if (line) lines.push(line);
|
||||
}
|
||||
}
|
||||
for (const tier of sorted) {
|
||||
if (tier.lv <= lv) continue;
|
||||
for (const uuid of tier.uuids) {
|
||||
const line = buildFieldLine(`Lv${tier.lv}: ${tpl} `, uuid, showName);
|
||||
if (!line) continue;
|
||||
line.locked = true;
|
||||
lines.push(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 复活(revive)----
|
||||
const reviveLine = buildReviveLine(source, showName);
|
||||
if (reviveLine) lines.push(reviveLine);
|
||||
lines.push(...buildReviveLines(source, showName, showLocked));
|
||||
|
||||
// ---- evolve 额外属性加成(ap/hp)----
|
||||
// ---- 额外属性加成(ap/hp,按当前等级累计)----
|
||||
lines.push(...buildEvolveBonusLines(source));
|
||||
|
||||
return lines;
|
||||
@@ -326,20 +443,17 @@ export function buildSkillDesc(source: ISkillDescSource): string {
|
||||
|
||||
/**
|
||||
* 富文本描述(RichText 用,BBCode):技能名用 <color=#2E86C1> 蓝色、数值用 <color=#27AE60> 亮绿色高亮。
|
||||
* @param source 触发技能数据源(同 buildSkillDesc)
|
||||
* @param showName 是否包含「技能名」片段(英雄盒面板传 false 隐藏技能名,默认 true)
|
||||
* @returns 多行富文本串,技能名片段包裹 <color=#2E86C1>、数值片段包裹 <color=#27AE60>,用 \n 分隔
|
||||
* @param source 触发技能数据源(同 buildSkillDesc)
|
||||
* @param showName 是否包含「技能名」片段(默认 true)
|
||||
* @param showLocked true 时为全档位合并模式:每个技能只出一行,已激活档数值高亮、
|
||||
* 未来档数值灰色 "/v1/v2" 追加;完全未激活的技能整行灰色且 Lv 前缀置前
|
||||
* (英雄盒面板用);false 时仅输出当前等级生效档
|
||||
* @returns 多行富文本串,用 \n 分隔
|
||||
*/
|
||||
export function buildSkillDescRich(source: ISkillDescSource, showName: boolean = true): string {
|
||||
return buildSkillLines(source, showName)
|
||||
.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);
|
||||
})
|
||||
export function buildSkillDescRich(source: ISkillDescSource, showName: boolean = true, showLocked: boolean = false): string {
|
||||
return buildSkillLines(source, showName, showLocked)
|
||||
// 未激活行保持整行灰色,不再补充"持续X秒"数值高亮
|
||||
.map(l => l.locked ? renderLineRich(l) : highlightDuration(renderLineRich(l)))
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
@@ -401,13 +515,17 @@ function highlightDuration(text: string): string {
|
||||
|
||||
/**
|
||||
* 结构化行 → 富文本串(技能名蓝色、数值绿色)。
|
||||
* buildEquipRich / buildCardDescRich 共用,保证渲染规则唯一。
|
||||
* buildEquipRich / buildCardDescRich / buildSkillDescRich 共用,保证渲染规则唯一。
|
||||
* 未激活行(locked)整行灰色弱化、不做技能名/数值高亮。
|
||||
*/
|
||||
function renderLineRich(line: ISkillLine): string {
|
||||
if (line.locked) return `<color=${RICH_LOCKED_COLOR}>${line.prefix}${line.value}${line.suffix}</color>`;
|
||||
const prefix = line.prefix.replace(SKILL_NAME_PATTERN, `「<color=${RICH_NAME_COLOR}>$1</color>」`);
|
||||
// 未来档数值灰色追加在当前高亮数值后("15%/20%/30%"),suffix 仍位于整段数值之后
|
||||
const next = line.nextValues?.length ? `<color=${RICH_LOCKED_COLOR}>/${line.nextValues.join("/")}</color>` : "";
|
||||
return line.value
|
||||
? `${prefix}<color=${RICH_VALUE_COLOR}>${line.value}</color>${line.suffix}`
|
||||
: `${prefix}${line.suffix}`;
|
||||
? `${prefix}<color=${RICH_VALUE_COLOR}>${line.value}</color>${next}${line.suffix}`
|
||||
: `${prefix}${next}${line.suffix}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -438,10 +438,11 @@ export class HeroBoxComp extends CCComp {
|
||||
const finalCritDmg = this.model.getFinalCritDamage();
|
||||
this.setStatRow(this.crit_dmg_all_label, this.crit_dmg_plus_label, finalCritDmg, finalCritDmg - (this.model.crit_damage ?? 0), "%");
|
||||
|
||||
// ---- 技能描述(当前等级触发技能,读运行时 model 最终配置;数值富文本高亮) ----
|
||||
// ---- 技能描述(全档位合并:每技能一行,等级置前 + 已/未激活状态,数值富文本高亮) ----
|
||||
if (this.skill_label && this.skill_label.isValid) {
|
||||
// 缓存未变则跳过重设,避免 refresh 降频内 RichText 重复重解析排版;隐藏「技能名」仅保留效果描述
|
||||
const desc = buildSkillDescRich(this.model, false);
|
||||
// 缓存未变则跳过重设,避免 refresh 降频内 RichText 重复重解析排版
|
||||
// showName=false 不显示技能名仅保留效果;showLocked=true 合并全档位(未来档数值灰色拼接,未激活技能整行灰色)
|
||||
const desc = buildSkillDescRich(this.model, false, true);
|
||||
if (desc !== this.skillDescCache) {
|
||||
this.skillDescCache = desc;
|
||||
this.skill_label.string = desc;
|
||||
|
||||
Reference in New Issue
Block a user