1. 新增SkillDescSet结构化技能描述配置表,实现配置驱动的文案生成 2. 改造HeroSkillDesc生成器,支持静态heroInfo和运行时HeroAttrsComp两种数据源 3. 为HeroAttrsComp新增evolve配置快照字段,用于展示等级成长属性 4. 在HeroBoxComp中集成技能描述UI组件,动态渲染当前英雄等级的技能效果 5. 新增设计文档说明方案细节,确保可维护性和可扩展性
242 lines
10 KiB
TypeScript
242 lines
10 KiB
TypeScript
/**
|
||
* @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].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 种标准触发技能,查 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");
|
||
}
|