Files
pixelheros/assets/script/game/common/config/HeroSkillDesc.ts
panFD 04f39217ac feat(heroBox): 实现当前等级技能描述面板功能
1. 新增SkillDescSet结构化技能描述配置表,实现配置驱动的文案生成
2. 改造HeroSkillDesc生成器,支持静态heroInfo和运行时HeroAttrsComp两种数据源
3. 为HeroAttrsComp新增evolve配置快照字段,用于展示等级成长属性
4. 在HeroBoxComp中集成技能描述UI组件,动态渲染当前英雄等级的技能效果
5. 新增设计文档说明方案细节,确保可维护性和可扩展性
2026-07-30 22:30:26 +08:00

242 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file HeroSkillDesc.ts
* @description 英雄技能/能力描述的【唯一文字表达标准层】
*
* 设计目标:
* heroSet / SkillSet 中的 info / infos 仅作"基础备注",不再承担面板展示。
* 所有功能性描述(触发技能、驻场光环、复活、等级额外属性加成)统一由本模块
* 依据结构化配置动态拼出,数值永远来自 mergeSkillParams 合并后的实时配置,
* 杜绝"改数值忘改文案"的漂移。
*
* 通用性(纯配置驱动):
* 1. 触发框架按触发类型遍历SkillSet + overrides不动
* 2. 效果文案:查 SkillDescSet 结构化模板渲染
* 3. 目标/单位/属性名由本模块统一语义化target/hit_count、ATTR_NAME
*
* 数据源(入参抽象 ISkillDescSource
* heroInfo静态 1 级配置)与 HeroAttrsComp运行时应用 evolve 后的当前等级配置)
* 字段结构一致天然都满足。HeroBoxComp 传 model 显示当前等级;卡池/商店传 heroInfo 显示 1 级。
*
* 依赖:
* - heroSet :SkillTriggerDesc、SkillTriggerType、HeroEvolve
* - SkillSet :SkillSet、mergeSkillParams、SkillOverrides、SkillKind、TGroup
* - SkillDescSet:结构化描述模板
* - FieldSkillSet驻场技能、BuffList计时 buff 默认值兜底、Attrs属性枚举
*/
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";
/**
* 生成器入参窄接口:只约束实际读取的字段。
* 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>;
}
/**
* 需要遍历的触发技能类型列表(不含 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<Record<Attrs, string>> = {
[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, string | number>): 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].valuedur=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 种标准触发技能,查 SkillSet + mergeSkillParams + SkillDescSet 拼行。
* 2. 处理 field驻场光环读 FieldSkillSet 的 type+value 结构化配置。
* 3. 处理 revive复活体现复活次数与回血百分比。
* 4. 处理 evolve 额外属性加成ap_bonus/hp_bonus累计文案。
*
* @param source 触发技能数据源heroInfo 静态配置 或 HeroAttrsComp 运行时当前等级配置)
* @returns 多行描述文本,每行一条能力,用 \n 分隔
*/
export function buildSkillDesc(source: ISkillDescSource): string {
const lines: string[] = [];
// ---- 第一步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 tpl = SkillTriggerDesc[key] ?? key;
for (const item of arr) {
const base = SkillSet[item.s_uuid];
if (!base) continue;
const skill = mergeSkillParams(base, item.overrides);
const trigger = tpl.replace("n", String(item.t_num));
lines.push(`${trigger}:${base.name} ${buildEffectDesc(skill)}`);
}
}
// ---- 第二步驻场光环field----
const fieldUuids = source[SkillTriggerType.Field] as number[] | undefined;
if (fieldUuids?.length) {
const tpl = SkillTriggerDesc[SkillTriggerType.Field] ?? "场上存活";
for (const uuid of fieldUuids) {
const fs = FieldSkillSet[uuid];
if (fs) lines.push(`${tpl}:${fs.name} ${fs.info}`);
}
}
// ---- 第三步复活revive----
const reviveLine = buildReviveDesc(source);
if (reviveLine) lines.push(reviveLine);
// ---- 第四步evolve 额外属性加成ap/hp----
lines.push(...buildEvolveBonusLines(source));
return lines.join("\n");
}