diff --git a/assets/script/game/common/HeroSkillText.ts b/assets/script/game/common/HeroSkillText.ts index 1f12f9e9..4c05f0a5 100644 --- a/assets/script/game/common/HeroSkillText.ts +++ b/assets/script/game/common/HeroSkillText.ts @@ -24,7 +24,7 @@ * HeroSkillDesc.ts 暂时保留兼容旧 UI 引用,待迁移完成后移除。 */ import { - HeroInfo, SkillTriggerType, SkillTriggerName, HSkillInfo, LvSkillEntry, LvTriggerEntry, + HeroInfo, SkillTriggerType, SkillTriggerName, HSkillInfo, LvTriggerEntry, TriggerGrouped, resolveSkillOverrides, resolveTriggerByLv, resolveFieldByLv, resolveReviveByLv, getGroupedTriggerName, getFieldSkillName, getReviveSkillName, } from "./config/heroSet"; @@ -145,38 +145,43 @@ function getHeroDisplayLv(heroUuid: number, configLv: number, fac: FacSet): numb * 输出一:英雄当前局外等级下,全部技能最大生效档的完整说明(富文本)。 * * 覆盖:普攻/大招(skills,resolve ≤lv 最大档)、5 类触发技能、驻场光环(field)、 - * 复活(revive)。每行 = 「技能名」+ 触发语境 + 模板渲染后的效果文本(数值高亮)。 + * 复活(revive)。每行 = 「技能名」:(或「技能名」+ 触发语境)+ 模板渲染后的效果文本(数值高亮), + * 不展示技能等级档号(当前档由英雄局外等级隐含决定)。 * - * @param heroUuid 英雄 uuid(HeroInfo key) - * @param showName 是否包含「技能名」片段(面板 true,紧凑卡片 false) - * @returns 多行富文本串,\n 分隔;英雄不存在时返回 "" + * 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): string { +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)} ` : ""); + const nameSeg = (n: string) => (showName && n ? skillNameRich(n) : ""); // ---- 普攻/大招(skills):resolve 当前等级生效档,渲染该档模板 ---- - for (const key in hero.skills) { - const hSkill = hero.skills[key]; + 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 最大档):取其 s_lv 与 desc;未配置 desc 的档位继承上一档模板 - let entry: LvSkillEntry | undefined; + // 生效档(≤lv 最大档)模板:未配置 desc 的档位继承基础/上一档模板 let inheritDesc = hSkill.desc; for (const e of hSkill.lv_entries ?? []) { - if (e.lv <= lv) { - if (!entry || e.lv > entry.lv) entry = e; - if (e.desc) inheritDesc = e.desc; - } + if (e.lv <= lv && e.desc) inheritDesc = e.desc; } - const sLv = entry?.s_lv ?? hSkill.lv; const text = inheritDesc ? renderDesc(inheritDesc, skill) : fallbackDesc(skill); - lines.push(`${nameSeg(hSkill.name ?? base.name)}Lv.${valueRich(sLv)}: ${text}`); + // 行首格式「技能名」:(不展示技能等级,等级由英雄局外等级隐含表达) + const head = nameSeg(hSkill.name ?? base.name); + lines.push(head ? `${head}: ${text}` : text); } // ---- 5 类标准触发技能:resolve 当前等级生效档 ---- @@ -339,33 +344,39 @@ function collectSkillTiers(hSkill: HSkillInfo): ISkillTierInfo[] { return tiers; } +/** 单个技能等级档位的展示行(结构化,供弹窗逐档渲染激活状态与描述) */ +export interface HeroSkillTierLine { + /** 技能等级(slv) */ + sLv: number; + /** 该档解锁/激活所需英雄等级 */ + heroLv: number; + /** 行文本:slv1=完整效果;slv2+=相对上一级提升(数值漂移/新增效果),富文本 */ + text: string; +} + /** - * 输出二:指定技能的各阶段说明(富文本)。 + * 输出二(结构化):指定技能各等级档位说明,按技能等级升序。 * - * 输出结构(按技能等级升序,通用支持任意档数): - * - slv1:完整能力 + 数值 +(英雄X级激活) - * - slv2+:相对上一级的提升(数值漂移/新增效果/计时 buff 增减)+(英雄Y级解锁) + * slv1 给完整能力 + 数值;slv2+ 给相对上一级的提升。每档附带解锁英雄等级, + * 调用方可自行对比英雄当前等级(smc.getHeroOuterLv)判定激活状态。 * - * 技能定位顺序:hero.skills[skillUuid](普攻/大招)→ 5 类触发技能组 → 未找到返回 ""。 + * 技能定位顺序:hero.skills[skillUuid](普攻/大招)→ 5 类触发技能组 → 未找到返回 []。 * Why: 同一底座技能可能被多个英雄以不同 overrides 引用,必须以英雄语境取档。 * * @param heroUuid 英雄 uuid(HeroInfo key) * @param skillUuid 技能 uuid(skills key 或触发组 s_uuid) - * @param showName 是否包含「技能名」片段 - * @returns 多行富文本串,\n 分隔;技能不属于该英雄时返回 "" + * @returns 档位行数组;技能不属于该英雄时返回 [] */ -export function buildHeroSkillTierDesc(heroUuid: number, skillUuid: number, showName: boolean = true): string { +export function buildHeroSkillTierLines(heroUuid: number, skillUuid: number): HeroSkillTierLine[] { const hero = HeroInfo[heroUuid]; - if (!hero) return ""; + if (!hero) return []; - let name = ""; let tiers: ISkillTierInfo[] = []; let triggerLabel = ""; const hSkill = hero.skills?.[skillUuid]; if (hSkill) { // 普攻/大招路径 - name = hSkill.name ?? SkillSet[skillUuid]?.name ?? ""; tiers = collectSkillTiers(hSkill); } else { // 触发技能路径:在 5 类触发组中定位 s_uuid @@ -374,9 +385,8 @@ export function buildHeroSkillTierDesc(heroUuid: number, skillUuid: number, show const entries = grouped?.[skillUuid]; if (!grouped || !entries?.length) continue; const base = SkillSet[skillUuid]; - if (!base) return ""; + if (!base) return []; const sorted = [...entries].sort((a, b) => (a.s_lv ?? 0) - (b.s_lv ?? 0)); - name = getGroupedTriggerName(grouped, skillUuid); let inherit: string | undefined; tiers = sorted.map((e: LvTriggerEntry, i: number) => { if (e.desc) inherit = e.desc; @@ -394,22 +404,57 @@ export function buildHeroSkillTierDesc(heroUuid: number, skillUuid: number, show break; } } - if (tiers.length === 0) return ""; + if (tiers.length === 0) return []; - const nameSeg = showName && name ? `${skillNameRich(name)} ` : ""; - const lines: string[] = []; + const lines: HeroSkillTierLine[] = []; for (let i = 0; i < tiers.length; i++) { const t = tiers[i]; - const head = `${nameSeg}Lv.${valueRich(t.sLv)}(英雄${valueRich(t.heroLv)}级${i === 0 ? "激活" : "解锁"})`; if (i === 0) { - // 技能 1 级:完整能力 + 数值(含激活英雄等级) + // 技能 1 级:完整能力 + 数值 const text = t.desc ? renderDesc(t.desc, t.skill) : fallbackDesc(t.skill); - lines.push(`${head}: ${triggerLabel ? `${triggerLabel}, ` : ""}${text}`); + lines.push({ sLv: t.sLv, heroLv: t.heroLv, text: triggerLabel ? `${triggerLabel}, ${text}` : text }); } else { - // 技能 2 级起:相对上一级的提升 + 解锁英雄等级 + // 技能 2 级起:相对上一级的提升 const diff = buildTierDiff(tiers[i - 1], t); - lines.push(`${head}: ${diff.length > 0 ? diff.join(",") : "无变化"}`); + lines.push({ sLv: t.sLv, heroLv: t.heroLv, text: diff.length > 0 ? diff.join(",") : "无变化" }); } } - return lines.join("\n"); + 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: typeof HeroInfo[number], skillUuid: number): string { + for (const type of TRIGGER_KEYS) { + const grouped = hero[type] as TriggerGrouped | undefined; + if (grouped?.[skillUuid]?.length) return getGroupedTriggerName(grouped, skillUuid); + } + return SkillSet[skillUuid]?.name ?? ""; } diff --git a/assets/script/game/common/HeroSkillText.ts.meta b/assets/script/game/common/HeroSkillText.ts.meta new file mode 100644 index 00000000..c77bb8c7 --- /dev/null +++ b/assets/script/game/common/HeroSkillText.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "481c93ea-b5e9-4f75-8676-c5ec48708c0b", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/script/game/map/CHeroComp.ts b/assets/script/game/map/CHeroComp.ts index 7549b85e..3f265995 100644 --- a/assets/script/game/map/CHeroComp.ts +++ b/assets/script/game/map/CHeroComp.ts @@ -20,7 +20,7 @@ import { _decorator, Node, Sprite, Label, RichText, NodeEventType, Vec3, Tween, import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp"; import { HeroInfo, HeroCardConfig } from "../common/config/heroSet"; -import { buildSkillDescMoreRich, ISkillDescSource } from "../common/config/HeroSkillDesc"; +import { buildHeroSkillDesc } from "../common/HeroSkillText"; import { oops } from "db://oops-framework/core/Oops"; import { GameEvent } from "../common/config/GameEvent"; import { outlineRich } from "../common/RichOutline"; @@ -270,11 +270,11 @@ export class CHeroComp extends CCComp { // 背景:按 card_lv 激活对应颜色子节点(1→green 2→blue 3→purple 4→red 5→yellow) this.updateBgNode(); - // 描述富文本:展示英雄当前局外等级的能力(buildSkillDescMoreRich) - // 与英雄信息弹窗同一套文案规则,杜绝手写文案与数值漂移 + // 描述富文本:展示英雄当前局外等级的能力(buildHeroSkillDesc 文本一) + // 行首「技能名」: + 效果文本;heroInfo 内联 desc 模板驱动,数值实时取合并后配置 // outlineRich 为整段富文本包裹描边,提升深色面板可读性 if (this.info_rich) { - const desc = hero ? buildSkillDescMoreRich(this.buildHeroDescSource(hero), false) : (this.cardData.info || ""); + const desc = hero ? buildHeroSkillDesc(this.cardData.uuid) : (this.cardData.info || ""); this.info_rich.string = outlineRich(desc); this.info_rich.node.active = !!desc; } @@ -451,30 +451,6 @@ export class CHeroComp extends CCComp { return model.hero_uuid ?? null; } - /** - * 构造英雄当前能力描述数据源(ISkillDescSource)。 - * lv 取英雄局外等级(与 level_label 同口径),由各 resolver 取当前等级生效档, - * 展示英雄当前实际能力信息。 - * @param hero HeroInfo 静态配置项 - * @returns 描述数据源 - */ - private buildHeroDescSource(hero: typeof HeroInfo[number]): ISkillDescSource { - return { - lv: Math.max(1, smc.getHeroOuterLv(hero.uuid)), - skills: hero.skills, - call: hero.call, - dead: hero.dead, - fstart: hero.fstart, - atking: hero.atking, - atked: hero.atked, - field: hero.field, - revive: hero.revive, - grow_type: hero.grow_type, - ap: hero.ap, - hp: hero.hp, - }; - } - /** * 为英雄图标加载 idle 动画剪辑,提取首帧 SpriteFrame 静止展示,零运行时开销。 * @param node 图标节点