diff --git a/assets/script/game/common/config/HeroSkillDesc.ts b/assets/script/game/common/config/HeroSkillDesc.ts index bdeb5b6f..471b995f 100644 --- a/assets/script/game/common/config/HeroSkillDesc.ts +++ b/assets/script/game/common/config/HeroSkillDesc.ts @@ -1,125 +1,227 @@ /** * @file HeroSkillDesc.ts - * @description 英雄技能描述生成器 + * @description 英雄技能/能力描述的【唯一文字表达标准层】 * - * 职责: - * 将英雄配置(heroInfo)中的特殊触发技能转换为可读的描述文本, - * 供 HInfoComp 的 info_node 显示。每行一个技能,用换行符连接。 + * 设计目标: + * heroSet / SkillSet 中的 info / infos 仅作"基础备注",不再承担面板展示。 + * 所有功能性描述(触发技能、驻场光环、复活、等级额外属性加成)统一由本模块 + * 依据结构化配置动态拼出,数值永远来自 mergeSkillParams 合并后的实时配置, + * 杜绝"改数值忘改文案"的漂移。 * - * 生成格式: - * {触发条件}:{技能名} {效果描述} - * 例: "受击2次:护盾 护盾2次" - * 例: "攻击1次:攻击强化 全体友方攻击力提升5点,持续1次" - * 例: "场上存活:攻击加成 英雄攻击力+20%" + * 通用性(纯配置驱动): + * 1. 触发框架:按触发类型遍历(SkillSet + overrides,不动) + * 2. 效果文案:查 SkillDescSet 结构化模板渲染 + * 3. 目标/单位/属性名:由本模块统一语义化(target/hit_count、ATTR_NAME) + * + * 数据源(入参抽象 ISkillDescSource): + * heroInfo(静态 1 级配置)与 HeroAttrsComp(运行时应用 evolve 后的当前等级配置) + * 字段结构一致,天然都满足。HeroBoxComp 传 model 显示当前等级;卡池/商店传 heroInfo 显示 1 级。 * * 依赖: - * - heroSet :SkillTriggerDesc(触发条件模板)、SkillTriggerType(触发类型枚举)、heroInfo - * - SkillSet :SkillSet(技能基础配置)、mergeSkillParams(合并覆盖参数)、SkillKind(技能类型枚举) - * - FieldSkillSet(驻场技能配置) + * - heroSet :SkillTriggerDesc、SkillTriggerType、HeroEvolve + * - SkillSet :SkillSet、mergeSkillParams、SkillOverrides、SkillKind、TGroup + * - SkillDescSet:结构化描述模板 + * - FieldSkillSet(驻场技能)、BuffList(计时 buff 默认值兜底)、Attrs(属性枚举) */ -import { heroInfo, SkillTriggerDesc, SkillTriggerType } from "./heroSet"; -import { FieldSkillSet, mergeSkillParams, SkillKind, SkillOverrides, SkillSet } from "./SkillSet"; +import { HeroEvolve, SkillTriggerDesc, SkillTriggerType } from "./heroSet"; +import { FieldSkillSet, mergeSkillParams, SkillConfig, SkillKind, SkillOverrides, SkillSet, TGroup } from "./SkillSet"; +import { SkillDescSet } from "./SkillDescSet"; +import { BuffList } from "./BuffSet"; +import { Attrs } from "./HeroAttrs"; /** - * 需要遍历的触发技能类型列表(不含 Field 和 Revive,这两者结构不同需单独处理) - * Field:number[](驻场技能 uuid 列表) - * Revive:{ s_uuid, r_num, upr }(单个对象) + * 生成器入参窄接口:只约束实际读取的字段。 + * heroInfo 与 HeroAttrsComp 的对应字段结构完全一致,天然都满足此接口。 */ -const TRIGGER_KEYS: SkillTriggerType[] = [ - SkillTriggerType.Call, // 召唤时触发 - SkillTriggerType.Dead, // 死亡时触发 - SkillTriggerType.FStart, // 战斗开始时触发 - SkillTriggerType.FEnd, // 战斗结束时触发 - SkillTriggerType.Atking, // 攻击后触发 - SkillTriggerType.Atked, // 受击后触发 -]; - -/** - * 根据合并后的技能配置生成效果描述文本 - * - * 按 SkillKind 分类输出: - * - Heal :"治疗伙伴{ap}" - * - Shield :"护盾{ap}次"(ap 表示可抵挡次数) - * - Gold :"金币+{gold}" - * - Support :直接使用技能自身的 info 字段(如"全体友方攻击力提升5点,持续1次") - * - Damage :"伤害{ap}%" + 可选的暴击/冰冻/击退/多段附加描述 - * - * @param skill 经过 mergeSkillParams 合并 overrides 后的完整技能配置 - * @returns 效果描述字符串,各部分用空格连接 - */ -function buildEffectDesc(skill: ReturnType): string { - const parts: string[] = []; - const kind = skill.kind; - - if (kind === SkillKind.Heal) { - // 治疗类:ap 为治疗量 - parts.push(`治疗伙伴${skill.ap}`); - } else if (kind === SkillKind.Shield) { - // 护盾类:ap 为可抵挡的攻击次数 - parts.push(`护盾${skill.ap}次`); - } else if (kind === SkillKind.Gold) { - // 金币类:gold 为获取金币数 - parts.push(`金币+${skill.gold ?? 0}`); - } else if (kind === SkillKind.Support) { - // 辅助类(buff):直接使用技能的 info 描述文案 - parts.push(String(skill.info)); - } else if (kind === SkillKind.Damage || kind === undefined) { - // 伤害类(含无 kind 的默认技能):ap 为攻击力百分比 - parts.push(`伤害${skill.ap}%`); - // 多段伤害 - if (skill.hit_count > 1) parts.push(`${skill.hit_count}段`); - // 附加暴击率 - if (skill.crt) parts.push(`暴击+${skill.crt}%`); - // 附加冰冻概率 - if (skill.frz) parts.push(`冰冻+${skill.frz}%`); - if (skill.stun) parts.push(`击晕+${skill.stun}%`); - // 附加击退概率 - if (skill.bck) parts.push(`击退+${skill.bck}%`); - } - - return parts.join(" "); +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; } /** - * 将英雄的特殊触发技能配置转换为可读描述文本 + * 需要遍历的触发技能类型列表(不含 Field 和 Revive,这两者结构不同需单独处理) + */ +const TRIGGER_KEYS: SkillTriggerType[] = [ + SkillTriggerType.Call, + SkillTriggerType.Dead, + SkillTriggerType.FStart, + SkillTriggerType.FEnd, + SkillTriggerType.Atking, + SkillTriggerType.Atked, +]; + +/** + * 属性标准中文名映射(文字表达标准层的统一称谓,供 evolve 额外加成文案使用) + * 仅覆盖 evolve.ap_bonus / hp_bonus 用到的属性;其余按需补充。 + */ +const ATTR_NAME: Partial> = { + [Attrs.ap]: "攻击力", + [Attrs.hp_max]: "最大生命", +}; + +/** + * 解析目标描述文本(与战斗 SCastSystem 目标选取语义严格一致) + * + * Why: Team 下 hit_count 表示"从全队中选取 N 个目标"(随机/血量最低), + * 并非全体。全体友方仅在 Ally / All 或未限定数量的 Team 群体光环语义下使用。 + * + * @param skill 合并 overrides 后的技能配置 + * @returns 目标短语,如 "自身"/"随机2名队友"/"血量最低队友"/"全体友方"/"敌方" + */ +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 "全体友方"; + } +} + +/** + * 占位符模板替换 + */ +function renderTemplate(templ: string, vars: Record): string { + let out = templ; + for (const key of Object.keys(vars)) { + out = out.split(`{${key}}`).join(String(vars[key])); + } + return out; +} + +/** + * 根据合并后的技能配置生成效果描述文本(查 SkillDescSet 结构化模板渲染) + * + * 计时模式默认值兜底: + * overrides 仅给 timed_buff_id 而未覆写 buff_value / buff_duration 时, + * 从 BuffList[timed_buff_id] 取默认值(value=modifiers[0].value,dur=duration), + * 保证 1 级基础档文案也完整(如占卜师 1 级"攻击力+30,持续5秒")。 + */ +function buildEffectDesc(skill: SkillConfig): string { + const desc = SkillDescSet[skill.uuid]; + // 未入表技能回退基础备注 info,保证未迁移技能不报错 + if (!desc) return String(skill.info ?? ""); + + 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; + + // 持续时间:overrides 覆写优先,否则用 BuffList 默认 duration + const dur = skill.buff_duration ?? buff?.duration ?? 0; + + return renderTemplate(templ, { + target: buildTargetDesc(skill), + value: value, + unit: desc.unit, + verb: desc.verb, + dur: dur, + }); +} + +/** + * 计算当前等级下 revive 的实际复活次数与回血百分比,生成完整复活文案 + * + * 与战斗 HeroAtkSystem 语义一致: + * 复活次数 = r_num + floor((lv - 1) * upr) + * 回血百分比 = SkillSet[revive.s_uuid].ap + */ +function buildReviveDesc(source: ISkillDescSource): string { + const revive = source.revive; + if (!revive) return ""; + const base = SkillSet[revive.s_uuid]; + if (!base) return ""; + 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 = SkillTriggerDesc[SkillTriggerType.Revive] ?? "复活时"; + return `${tpl}:${base.name} 可复活${maxCount}次,每次恢复${hpPct}%生命`; +} + +/** + * 累计当前等级已生效的 evolve 额外属性加成(ap_bonus / hp_bonus),生成文案行 + * + * Why: evolve 的属性加成不体现在触发技能配置里,若不平铺成文字,面板上该部分 + * 能力会静默丢失。这里按 lv 从 2 级累计到当前等级,逐条输出。 + */ +function buildEvolveBonusLines(source: ISkillDescSource): string[] { + 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: string[] = []; + if (apTotal !== 0) lines.push(`成长:额外+${apTotal}${ATTR_NAME[Attrs.ap] ?? "攻击力"}`); + if (hpTotal !== 0) lines.push(`成长:额外+${hpTotal}${ATTR_NAME[Attrs.hp_max] ?? "最大生命"}`); + return lines; +} + +/** + * 将英雄的触发技能/能力配置转换为标准可读描述文本 * * 处理流程: - * 1. 遍历 6 种触发类型(call/dead/fstart/fend/atking/atked),从 hero[key] 取出 - * {s_uuid, t_num, overrides?}[],通过 s_uuid 查 SkillSet 获取技能基础配置, - * 用 mergeSkillParams 合并 overrides 得到实际参数,再拼成一行描述。 - * 2. 单独处理 field(驻场技能):从 FieldSkillSet 查找配置,输出光环效果。 - * 3. 单独处理 revive(复活技能):结构与上述不同,是单个对象而非数组。 + * 1. 遍历 6 种标准触发技能,查 SkillSet + mergeSkillParams + SkillDescSet 拼行。 + * 2. 处理 field(驻场光环):读 FieldSkillSet 的 type+value 结构化配置。 + * 3. 处理 revive(复活):体现复活次数与回血百分比。 + * 4. 处理 evolve 额外属性加成(ap_bonus/hp_bonus)累计文案。 * - * 输出示例(5006 疾风刺客): - * "攻击1次:攻击强化 全体友方攻击力提升5点,持续1次 - * 死亡时:攻击强化 全体友方攻击力提升8点,持续1次" - * - * @param hero 英雄静态配置(HeroInfo 中的条目) - * @returns 多行技能描述文本,每行一个技能,用 \n 分隔 + * @param source 触发技能数据源(heroInfo 静态配置 或 HeroAttrsComp 运行时当前等级配置) + * @returns 多行描述文本,每行一条能力,用 \n 分隔 */ -export function buildSkillDesc(hero: heroInfo): string { +export function buildSkillDesc(source: ISkillDescSource): string { const lines: string[] = []; - // ---- 第一步:处理 6 种标准触发技能(结构均为 {s_uuid, t_num, overrides?}[]) ---- + // ---- 第一步:6 种标准触发技能 ---- for (const key of TRIGGER_KEYS) { - const arr = hero[key] as { s_uuid: number; t_num: number; overrides?: SkillOverrides }[] | undefined; + const arr = source[key] as { s_uuid: number; t_num: number; overrides?: SkillOverrides }[] | undefined; if (!arr?.length) continue; - // 从 SkillTriggerDesc 取模板,如 "攻击n次"、"受击n次"、"召唤时" 等 const tpl = SkillTriggerDesc[key] ?? key; for (const item of arr) { - // 通过 s_uuid 查找技能基础配置 const base = SkillSet[item.s_uuid]; if (!base) continue; - // 合并 overrides 得到实际技能参数(ap/hit_count/crt/frz/stun/bck 等可能被覆盖) const skill = mergeSkillParams(base, item.overrides); - // 将模板中的 "n" 替换为实际触发次数 const trigger = tpl.replace("n", String(item.t_num)); lines.push(`${trigger}:${base.name} ${buildEffectDesc(skill)}`); } } - // ---- 第二步:处理驻场技能(field)—— 结构为 number[],uuid 指向 FieldSkillSet ---- - const fieldUuids = hero[SkillTriggerType.Field] as number[] | undefined; + // ---- 第二步:驻场光环(field)---- + const fieldUuids = source[SkillTriggerType.Field] as number[] | undefined; if (fieldUuids?.length) { const tpl = SkillTriggerDesc[SkillTriggerType.Field] ?? "场上存活"; for (const uuid of fieldUuids) { @@ -128,16 +230,12 @@ export function buildSkillDesc(hero: heroInfo): string { } } - // ---- 第三步:处理复活技能(revive)—— 结构为 {s_uuid, r_num, upr} 单个对象 ---- - const revive = hero[SkillTriggerType.Revive] as { s_uuid: number; r_num: number; upr: number } | undefined; - if (revive) { - const base = SkillSet[revive.s_uuid]; - if (base) { - const skill = mergeSkillParams(base); - const tpl = SkillTriggerDesc[SkillTriggerType.Revive] ?? "复活时"; - lines.push(`${tpl} : ${base.name} ${buildEffectDesc(skill)}`); - } - } + // ---- 第三步:复活(revive)---- + const reviveLine = buildReviveDesc(source); + if (reviveLine) lines.push(reviveLine); + + // ---- 第四步:evolve 额外属性加成(ap/hp)---- + lines.push(...buildEvolveBonusLines(source)); return lines.join("\n"); } diff --git a/assets/script/game/common/config/SkillDescSet.ts b/assets/script/game/common/config/SkillDescSet.ts new file mode 100644 index 00000000..384d8162 --- /dev/null +++ b/assets/script/game/common/config/SkillDescSet.ts @@ -0,0 +1,102 @@ +/** + * @file SkillDescSet.ts + * @description 技能描述结构化配置表(方案 B:独立描述表,纯 UI 用) + * + * 职责: + * 为触发技能提供"结构化字段 + 占位符模板",供 HeroSkillDesc.buildSkillDesc + * 在合并 overrides 后动态拼出效果文案。杜绝 SkillConfig.info 写死数值导致的 + * "改数值忘改文案"漂移。 + * + * 关键设计: + * - 与 SkillSet 同 key(skill uuid),但只覆盖需要描述的触发技能; + * 普攻/怪物技能不入表,由生成器回退旧 info 兜底。 + * - 数值永远从 mergeSkillParams 后的实时 skill 取值(permField/timedField), + * overrides 改了数值,文案自动同步。 + * - 永久模式(overrides 直接改写 ap 等)与计时模式(overrides 走 timed_buff_id) + * 分别用 templPerm / templTimed 两套模板。 + * + * 模板占位符: + * {target} 目标(自身/随机N名队友/全体友方/敌方) + * {value} 取值(来自 permField 或 timedField 指向的字段) + * {unit} 单位(""/%/次) + * {verb} 效果动词(攻击力/暴击率/护盾…) + * {dur} 持续时间秒数(计时模式用,取 buff_duration) + */ +import { SkillOverrides } from "./SkillSet"; + +/** + * 单个技能的结构化描述配置 + */ +export interface SkillDescConfig { + /** 效果动词,如 "攻击力"/"暴击率"/"护盾" */ + verb: string; + /** 单位,如 ""/"%"/"次" */ + unit: string; + /** 永久模式取值字段(overrides 直接改写的字段),通常 "ap" */ + permField: keyof SkillOverrides; + /** 计时模式取值字段(overrides 走 timed_buff_id 时),通常 "buff_value" */ + timedField?: keyof SkillOverrides; + /** 永久模式文案模板 */ + templPerm: string; + /** 计时模式文案模板(含 {dur}),不填则该技能不支持计时模式 */ + templTimed?: string; +} + +/** + * 技能描述表,key = SkillSet 的技能 uuid + * 首批覆盖现役英雄涉及的触发技能;未入表技能由生成器回退 skill.info + */ +export const SkillDescSet: Record = { + // 6301 护盾:ap = 可格挡次数 + 6301: { + verb: "", unit: "次", permField: "ap", + templPerm: "{target}获得{value}{unit}格挡护盾", + }, + // 6302 治疗:ap = 攻击力百分比 + 6302: { + verb: "", unit: "", permField: "ap", + templPerm: "{target}回复攻击力{value}%生命", + }, + // 6401 攻击强化:永久 ap=攻击点;计时 buff_value=攻击点 + 6401: { + verb: "攻击力", unit: "", permField: "ap", timedField: "buff_value", + templPerm: "{target}{verb}+{value}", + templTimed: "{target}{verb}+{value},持续{dur}秒", + }, + // 6402 生命强化:ap = 最大生命点 + 6402: { + verb: "最大生命", unit: "", permField: "ap", + templPerm: "{target}{verb}+{value}", + }, + // 6403 暴击强化:永久 ap=暴击%;计时 buff_value=暴击% + 6403: { + verb: "暴击率", unit: "%", permField: "ap", timedField: "buff_value", + templPerm: "{target}{verb}+{value}{unit}", + templTimed: "{target}{verb}+{value}{unit},持续{dur}秒", + }, + // 6404 暴伤强化:ap = 暴击伤害% + 6404: { + verb: "暴击伤害", unit: "%", permField: "ap", + templPerm: "{target}{verb}+{value}{unit}", + }, + // 6405 击晕强化:ap = 击晕概率% + 6405: { + verb: "击晕概率", unit: "%", permField: "ap", + templPerm: "{target}{verb}+{value}{unit}", + }, + // 6408 穿刺强化:ap = 穿刺概率% + 6408: { + verb: "穿刺概率", unit: "%", permField: "ap", + templPerm: "{target}{verb}+{value}{unit}", + }, + // 6409 风怒强化:ap = 风怒次数 + 6409: { + verb: "风怒次数", unit: "次", permField: "ap", + templPerm: "{target}{verb}+{value}{unit}", + }, + // 6501 复活:ap = 复活生命值百分比 + 6501: { + verb: "", unit: "", permField: "ap", + templPerm: "{target}以{value}%生命复活", + }, +}; diff --git a/assets/script/game/common/config/SkillDescSet.ts.meta b/assets/script/game/common/config/SkillDescSet.ts.meta new file mode 100644 index 00000000..27f47e47 --- /dev/null +++ b/assets/script/game/common/config/SkillDescSet.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "d60c5899-0170-4269-bc3d-3bea21badd0d", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/script/game/hero/Hero.ts b/assets/script/game/hero/Hero.ts index 3359b71b..c11edb97 100644 --- a/assets/script/game/hero/Hero.ts +++ b/assets/script/game/hero/Hero.ts @@ -166,6 +166,8 @@ export class Hero extends ecs.Entity { // 应用等级进化配置(从 lv2 开始逐级覆盖) if (hero.evolve) { + // 记录 evolve 配置快照(只读引用),供 UI 计算 ap/hp 额外加成文案 + model.evolve = hero.evolve; for (let elv = 2; elv <= hero_lv; elv++) { const evo = hero.evolve[elv]; if (!evo) continue; diff --git a/assets/script/game/hero/HeroAttrsComp.ts b/assets/script/game/hero/HeroAttrsComp.ts index 5ca50cde..b93a9e22 100644 --- a/assets/script/game/hero/HeroAttrsComp.ts +++ b/assets/script/game/hero/HeroAttrsComp.ts @@ -1,5 +1,5 @@ import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; -import { HeroDisVal, HeroInfo, HSkillInfo, HType, SkillTriggerType } from "../common/config/heroSet"; +import { HeroDisVal, HeroInfo, HSkillInfo, HType, SkillTriggerType, HeroEvolve } from "../common/config/heroSet"; import { mLogger } from "../common/Logger"; import { FacSet, FightSet } from "../common/config/GameSet"; import { FieldSkillSet, FieldSkillType, SkillOverrides } from "../common/config/SkillSet"; @@ -52,6 +52,9 @@ export class HeroAttrsComp extends ecs.Comp { [SkillTriggerType.Field]?: number[]; [SkillTriggerType.Revive]?: { s_uuid: number, r_num: number, upr: number }; + /** 等级进化配置快照(引用 heroInfo.evolve,只读;供 UI 计算 ap/hp 额外加成文案) */ + evolve?: Record; + // ==================== 特殊属性 ==================== critical: number = 0; // 暴击率 critical_res: number = 0; // 暴击抗性 @@ -545,6 +548,7 @@ export class HeroAttrsComp extends ecs.Comp { this.atking = undefined; this.atked = undefined; this.revive = undefined; + this.evolve = undefined; this.critical = 0; this.critical_res = 0; this.freeze_chance = 0; diff --git a/assets/script/game/map/HeroBoxComp.ts b/assets/script/game/map/HeroBoxComp.ts index bad0090b..26e6824f 100644 --- a/assets/script/game/map/HeroBoxComp.ts +++ b/assets/script/game/map/HeroBoxComp.ts @@ -20,6 +20,7 @@ import { GameEvent } from "../common/config/GameEvent"; import { HeroInfo } from "../common/config/heroSet"; import { HeroAttrsComp } from "../hero/HeroAttrsComp"; import { getLvColor } from "../common/config/GameSet"; +import { buildSkillDesc } from "../common/config/HeroSkillDesc"; const { ccclass, property } = _decorator; @@ -52,6 +53,10 @@ export class HeroBoxComp extends CCComp { @property(Node) private noHero: Node = null; + /** 技能描述标签(多行,展示当前等级触发技能) */ + @property(Label) + private skill_label: Label = null; + // ======================== 战斗信息标签(总值 + 附加值) ======================== /** 攻击总值 */ @@ -321,6 +326,9 @@ export class HeroBoxComp extends CCComp { this.clearStatRow(this.freeze_all_label, this.freeze_plus_label); this.clearStatRow(this.puncture_all_label, this.puncture_plus_label); this.clearStatRow(this.crit_dmg_all_label, this.crit_dmg_plus_label); + if (this.skill_label && this.skill_label.isValid) { + this.skill_label.string = ""; + } } /** @@ -388,6 +396,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) { + this.skill_label.string = buildSkillDesc(this.model); + } + // ---- 图标(UUID 变化时重新加载首帧动画) ---- if (hero && heroUuid !== this.iconHeroUuid) { this.iconHeroUuid = heroUuid; diff --git a/docs/superpowers/specs/2026-07-30-herobox-skill-desc-design.md b/docs/superpowers/specs/2026-07-30-herobox-skill-desc-design.md new file mode 100644 index 00000000..e23e6348 --- /dev/null +++ b/docs/superpowers/specs/2026-07-30-herobox-skill-desc-design.md @@ -0,0 +1,118 @@ +# HeroBox 当前等级技能信息面板设计(方案 B:独立 SkillDescSet 表) + +## 背景与目标 + +HeroBoxComp 当前只展示数值属性(AP/HP/暴击/攻速…),没有技能信息区。需要在面板中加入**当前英雄当前等级**的技能描述。 + +关键诉求(用户已确认): +- 抛弃 `heroSet` 的手写 `infos[]` 文案,改用**结构化字段 + overrides 动态拼描述**,杜绝"数值改了忘改文案"的漂移。 +- 描述必须反映**运行时当前等级**(evolve 已应用的最终配置),而非 1 级静态配置。 +- 数据与展示分离,不侵入战斗配置(SkillSet/FieldSkillSet 一行不动)。 + +## 现状分析(已验证) + +- `HeroSkillDesc.buildSkillDesc(hero)` 已存在,且**触发框架已通用**:按触发类型遍历 → 查 `SkillSet` → `mergeSkillParams` 合并 overrides → 拼行。 +- 其 `buildEffectDesc` 按 `SkillKind` 硬编码 if-else,且 Support 类直接复用 `skill.info` 写死的数值文案 → **这是唯一不通用的点,也是漂移根因**。 +- `HeroAttrsComp` 运行时持有**应用 evolve 后的当前等级最终触发配置**(Hero.ts 中 base → 逐级 evolve 覆盖),字段结构与 `heroInfo` 完全一致。 +- `buildSkillDesc` 现有 2 个调用方(HersListComp:205、HInfoComp:448)都传静态 `heroInfo`(卡池/商店场景,无运行时 model)。**签名不能粗暴改成只收 model**。 + +## 方案 B 总体形态 + +三层都通用,纯配置驱动: + +1. **触发框架**:不动(已通用)。 +2. **技能数值**:不动(SkillSet + overrides)。 +3. **效果文案**:新增 `SkillDescSet` 独立表,把硬编码 if-else 变成"结构化配置 + 占位符模板"。 + +结果:`buildSkillDesc(source)` 成为纯渲染器。**新增英雄 = 纯配置,零代码**;新增技能 = 补一行 SkillDescSet 配置。 + +## 设计细节 + +### 1. 生成器入参抽象(兼容两种数据源) + +抽取窄接口,`heroInfo` 与 `HeroAttrsComp` 天然都满足(字段名、结构一致): + +```ts +export interface ISkillDescSource { + 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 }; +} +// buildSkillDesc(source: ISkillDescSource): string +``` + +- HeroBoxComp 传 `this.model` → 显示当前等级。 +- 卡池/商店传 `heroInfo` → 显示 1 级(无 model 场景),老调用方零改动。 + +### 2. SkillDescSet 结构化描述表(核心) + +新文件 `assets/script/game/common/config/SkillDescSet.ts`,与 SkillSet 同 key、纯 UI 用,只覆盖需要描述的触发技能(普攻/怪物技能跳过): + +```ts +export interface SkillDescConfig { + /** 效果动词,如 "攻击力"/"暴击率"/"护盾" */ + verb: string; + /** 单位,如 ""/"%"/"次" */ + unit: string; + /** 永久模式取值字段(overrides 直接改写),通常 "ap" */ + permField: keyof SkillOverrides; + /** 计时模式取值字段(overrides 走 timed_buff_id),通常 "buff_value" */ + timedField?: keyof SkillOverrides; + /** 永久模式文案模板 */ + templPerm: string; + /** 计时模式文案模板(含 {dur}),不填则该技能不支持计时模式 */ + templTimed?: string; +} +``` + +模板占位符:`{target}` `{value}` `{unit}` `{verb}` `{dur}`。 + +渲染逻辑(生成器内统一处理): +- `target`:TGroup.Self→"自身";Team+hit_count>1→"随机{hit_count}名队友";Team/Ally→"全体友方";Enemy→"敌方"。 +- 计时判定:合并后 skill 存在 `timed_buff_id` → 用 `templTimed`+`timedField`;否则 `templPerm`+`permField`。 +- `value`:从合并后 skill 对应字段取值;`dur`:`buff_duration` 值。 +- 查不到 SkillDescSet 条目 → 回退旧 `skill.info`(保证未迁移技能不报错、渐进迁移)。 + +### 3. HeroSkillDesc 改造 + +- 入参从 `heroInfo` 改为 `ISkillDescSource`。 +- 删除 `buildEffectDesc` 的 SkillKind 硬编码分支,改为查 `SkillDescSet[uuid]` 渲染。 +- `field`(驻场)继续直接读 `FieldSkillSet[uuid].info`(已结构化 type+value,无需改造)。 +- `revive` 用 6501 的模板渲染复活血量百分比。 + +### 4. HeroBoxComp 集成 + 迁移范围 + +**HeroBoxComp 改动**: +- 新增 `@property(Label)` 技能描述节点(单 Label 多行,与 HInfoComp 一致;放属性栏下方)。 +- `refresh()` 追加:`skill_label.string = buildSkillDesc(this.model)`。 +- `applyEmpty()` 清空该 label。 +- **展示范围**:仅触发技能(atking/atked/dead/fstart/fend/field/revive)。普攻不展示——AP/攻速已由数值栏体现,触发技能才是英雄身份核心。 + +**迁移范围(首批)**: +- SkillDescSet 首批覆盖现役英雄涉及的 ~10 个触发技能:6301/6302/6401/6402/6403/6404/6405/6501 + fstart 用的 7111(计时)。 +- `infos[]` 字段保留但不再在面板使用(卡池/详情页后续统一迁移,本次不动)。 + +## UI 取向(默认,可覆盖) + +- 格式:单个多行 Label(最省改动,与 HInfoComp 一致)。 +- 范围:仅触发技能,不含普攻。 + +## 涉及文件 + +| 文件 | 动作 | +|------|------| +| `assets/script/game/common/config/SkillDescSet.ts` | 新建 | +| `assets/script/game/common/config/HeroSkillDesc.ts` | 改造(入参抽象 + 查表渲染) | +| `assets/script/game/map/HeroBoxComp.ts` | 集成技能描述 Label | + +## 非目标(YAGNI) + +- 不改造 SkillSet/FieldSkillSet 结构。 +- 不做图标/富文本富 UI(本次纯文本)。 +- 不迁移卡池/详情页的 `infos[]` 展示。 +- 普攻技能不做描述。