diff --git a/assets/script/game/common/config/HeroSkillDesc.ts b/assets/script/game/common/config/HeroSkillDesc.ts index ec12fc8e..44e21ca7 100644 --- a/assets/script/game/common/config/HeroSkillDesc.ts +++ b/assets/script/game/common/config/HeroSkillDesc.ts @@ -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 = {}; + 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):技能名用 蓝色、数值用 亮绿色高亮。 - * @param source 触发技能数据源(同 buildSkillDesc) - * @param showName 是否包含「技能名」片段(英雄盒面板传 false 隐藏技能名,默认 true) - * @returns 多行富文本串,技能名片段包裹 、数值片段包裹 ,用 \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, `「$1」`); - const text = l.value - ? `${prefix}${l.value}${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 `${line.prefix}${line.value}${line.suffix}`; const prefix = line.prefix.replace(SKILL_NAME_PATTERN, `「$1」`); + // 未来档数值灰色追加在当前高亮数值后("15%/20%/30%"),suffix 仍位于整段数值之后 + const next = line.nextValues?.length ? `/${line.nextValues.join("/")}` : ""; return line.value - ? `${prefix}${line.value}${line.suffix}` - : `${prefix}${line.suffix}`; + ? `${prefix}${line.value}${next}${line.suffix}` + : `${prefix}${next}${line.suffix}`; } /** diff --git a/assets/script/game/map/HeroBoxComp.ts b/assets/script/game/map/HeroBoxComp.ts index f219fff7..79cbb10f 100644 --- a/assets/script/game/map/HeroBoxComp.ts +++ b/assets/script/game/map/HeroBoxComp.ts @@ -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;