refactor(skill-text): 重构技能文案系统,拆分英雄/卡牌语境
1. 将原HeroSkillDesc拆分为CardSkillText(卡牌/道具通用)和HeroSkillText(英雄专属) 2. 移除废弃的HeroSkillDesc文件与相关引用 3. 统一技能文案生成逻辑,修复数值漂移问题 4. 调整英雄配置中技能解锁等级与分档规则 5. 优化技能弹窗UI与参数传递逻辑
This commit is contained in:
341
assets/script/game/common/config/CardSkillText.ts
Normal file
341
assets/script/game/common/config/CardSkillText.ts
Normal file
@@ -0,0 +1,341 @@
|
||||
/**
|
||||
* @file CardSkillText.ts
|
||||
* @description 卡牌/道具语境技能描述文案层(从已弃用的 HeroSkillDesc.ts 迁出)
|
||||
*
|
||||
* 职责:不依赖英雄语境的卡牌/道具描述富文本生成,数值永远来自
|
||||
* mergeSkillParams 合并后的实时配置,杜绝"改数值忘改文案"的漂移。
|
||||
*
|
||||
* 输出(均为富文本 BBCode,RichText 直接可用):
|
||||
* - buildCardDescRich(card) —— 商店卡片描述(技能卡/药品卡/装备卡统一入口)
|
||||
* - buildEquipRich(entries, showName) —— 装备/驻场光环词条详情
|
||||
* - buildSingleSkillRich(uuid, ov, lv) —— 单技能详情(CD/持续 + 效果,IBox 弹窗用)
|
||||
*
|
||||
* 英雄语境描述请使用 HeroSkillText.ts(heroInfo 内联模板驱动)。
|
||||
*
|
||||
* 依赖:
|
||||
* - SkillSet / FieldSkillSet(技能/光环静态配置与合并)
|
||||
* - SkillDescSet / FieldDescSet(结构化描述模板)
|
||||
* - BuffList(计时 buff 默认值兜底)
|
||||
* - CardConfig(卡牌统一配置结构)
|
||||
*/
|
||||
import { CardConfig } from "./CardSet";
|
||||
import { FieldSkillSet, mergeSkillParams, SkillConfig, SkillKind, SkillOverrides, SkillSet, TGroup } from "./SkillSet";
|
||||
import { FieldDescSet, FieldValueFmt, SkillDescSet, SkillDescConfig } from "./SkillDescSet";
|
||||
import { BuffList } from "./BuffSet";
|
||||
import { FieldEntry } from "./heroSet";
|
||||
|
||||
/**
|
||||
* 一条能力的结构化行:数值片段单独抽出,供富文本高亮与纯文本复用。
|
||||
* 渲染结果 = prefix + value(可高亮) + suffix。
|
||||
*/
|
||||
interface ISkillLine {
|
||||
/** 数值前的文本(含触发条件、技能名、目标、动词等) */
|
||||
prefix: string;
|
||||
/** 需要高亮的核心数值文本(如 "15%"、"4次"、"+30"),无数值时为 "" */
|
||||
value: string;
|
||||
/** 数值后的文本(如 ",持续5秒" 之类尾部补充) */
|
||||
suffix: string;
|
||||
}
|
||||
|
||||
/** 富文本数值高亮色(鲜亮绿色,深色面板上高亮醒目) */
|
||||
const RICH_VALUE_COLOR = "#3CE66E";
|
||||
|
||||
/** 数值描边色(深绿,衬托亮绿填充,与技能名橙填充+深褐描边同一设计思路) */
|
||||
const RICH_VALUE_OUTLINE = "#145A32";
|
||||
|
||||
/**
|
||||
* 数值富文本片段:亮绿填充 + 深绿描边。
|
||||
* 高亮数值明度高,深色描边增强边缘对比、提升深色面板上的锐度。
|
||||
* @param text 数值文本(含单位,如 "15%"、"4次")
|
||||
*/
|
||||
function valueRich(text: string | number): string {
|
||||
return `<outline color=${RICH_VALUE_OUTLINE} width=1><color=${RICH_VALUE_COLOR}>${text}</color></outline>`;
|
||||
}
|
||||
|
||||
/** 富文本技能名高亮色(蜜桃橙,暖色系,与面板绿/蓝/红/黄/紫均不冲突) */
|
||||
const RICH_NAME_COLOR = "#F8C471";
|
||||
|
||||
/** 技能名描边色(深褐,衬托蜜桃橙填充,深色面板上温馨醒目) */
|
||||
const RICH_NAME_OUTLINE = "#6E2C00";
|
||||
|
||||
/**
|
||||
* 技能名富文本片段:[技能名]整体蜜桃橙填充 + 深褐描边(含方括号)。
|
||||
* 括号与技能名同色同描边,正文/数值不描边,突出技能名视觉层级。
|
||||
* @param name 技能名(纯文本,不含括号)
|
||||
* @returns <outline><color>[技能名]</color></outline> 富文本片段
|
||||
*/
|
||||
function skillNameRich(name: string): string {
|
||||
return `<outline color=${RICH_NAME_OUTLINE} width=1><color=${RICH_NAME_COLOR}>[${name}]</color></outline>`;
|
||||
}
|
||||
|
||||
/** 技能名「」匹配模式(仅本模块行首/行中技能名片段使用) */
|
||||
const SKILL_NAME_PATTERN = /「([^」]*)」/g;
|
||||
|
||||
/** "持续X秒"匹配模式(卡片描述后处理:把持续时间数值也高亮) */
|
||||
const DURATION_PATTERN = /持续(\d+)秒/g;
|
||||
|
||||
/** 解析目标描述文本(与战斗 SCastSystem 目标选取语义严格一致) */
|
||||
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 "全体友方";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 占位符模板替换,并把"核心数值片段"用 \x01...\x02 标记,便于上层拆分高亮。
|
||||
*
|
||||
* Why: 模板中 {value}{unit} 是玩家最关心的数值。这里在渲染时将其单独包裹,
|
||||
* 使富文本可对其单独上色,保证高亮片段与模板数值严格一致。
|
||||
*
|
||||
* @returns 含 \x01hlValue\x02 标记的完整文本
|
||||
*/
|
||||
function renderTemplate(templ: string, vars: Record<string, string | number>, hlValue: string): string {
|
||||
let out = templ;
|
||||
for (const key of Object.keys(vars)) {
|
||||
out = out.split(`{${key}}`).join(String(vars[key]));
|
||||
}
|
||||
// 用 \x01...\x02 包裹数值片段作为标记,便于上层拆分为 prefix/value/suffix
|
||||
return out.split(hlValue).join(`\x01${hlValue}\x02`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 由一行模板渲染结果构造结构化行(剥离 \x01\x02 标记,把数值片段拆到 value 字段)
|
||||
* @param head 行首(触发条件:技能名 )
|
||||
* @param rendered 已用 \x01...\x02 标记数值片段的效果文本
|
||||
*/
|
||||
function makeLine(head: string, rendered: string): ISkillLine {
|
||||
const start = rendered.indexOf("\x01");
|
||||
const end = rendered.indexOf("\x02");
|
||||
if (start >= 0 && end > start) {
|
||||
// 恰好含一个数值片段:prefix=行首+数值前文本,value=数值,suffix=数值后文本
|
||||
return {
|
||||
prefix: `${head}${rendered.slice(0, start)}`,
|
||||
value: rendered.slice(start + 1, end),
|
||||
suffix: rendered.slice(end + 1),
|
||||
};
|
||||
}
|
||||
// 无数值片段(未命中模板或兜底 info):剥离残留标记,整行作为 prefix,不高亮
|
||||
return { prefix: `${head}${rendered.replace(/[\x01\x02]/g, "")}`, value: "", suffix: "" };
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据合并后的技能配置构造结构化效果行(查 SkillDescSet 模板渲染 + 数值高亮抽取)
|
||||
*
|
||||
* 计时模式默认值兜底:
|
||||
* overrides 仅给 timed_buff_id 而未覆写 buff_value / buff_duration 时,
|
||||
* 从 BuffList[timed_buff_id] 取默认值,保证 1 级基础档文案完整。
|
||||
*/
|
||||
function buildEffectLine(head: string, skill: SkillConfig, descOverride?: SkillDescConfig): ISkillLine {
|
||||
const desc = descOverride ?? SkillDescSet[skill.uuid];
|
||||
// 未入表技能回退基础备注 info,无法抽出数值,整行不高亮
|
||||
if (!desc) return { prefix: `${head}${String(skill.info ?? "")}`, value: "", suffix: "" };
|
||||
|
||||
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;
|
||||
const dur = skill.buff_duration ?? buff?.duration ?? 0;
|
||||
|
||||
// 高亮片段 = 数值 + 单位(如 "15%"、"4次"、"+30"),是玩家视觉焦点
|
||||
const hlValue = `${value}${desc.unit}`;
|
||||
const rendered = renderTemplate(templ, {
|
||||
target: buildTargetDesc(skill),
|
||||
value: value,
|
||||
unit: desc.unit,
|
||||
verb: desc.verb,
|
||||
dur: dur,
|
||||
}, hlValue);
|
||||
return makeLine(head, rendered);
|
||||
}
|
||||
|
||||
/**
|
||||
* 驻场光环(field)结构化行:查 FieldDescSet 按 type 渲染,value 实时取数值并高亮。
|
||||
*
|
||||
* Why: FieldSkillConfig.info 是手写文案,改 value 易漂移。这里以 FieldSkillType
|
||||
* 为 key 查标准动词,数值按 fmt(比例/整数/减免)从 value 实时渲染。
|
||||
*
|
||||
* @param showName 是否在行中包含「技能名」片段(英雄面板需要,装备商店不需要)
|
||||
*/
|
||||
function buildFieldLine(head: string, entry: FieldEntry, showName: boolean = true): ISkillLine | null {
|
||||
const fs = FieldSkillSet[entry.uuid];
|
||||
if (!fs) return null;
|
||||
const desc = FieldDescSet[fs.type];
|
||||
const nameSeg = showName ? `「${fs.name}」 ` : "";
|
||||
// 未入表类型回退基础备注 info,整行不高亮
|
||||
if (!desc) return { prefix: `${head}${nameSeg}${fs.info}`, value: "", suffix: "" };
|
||||
|
||||
// 数值按格式渲染为高亮片段(数值取自词条自身 value)
|
||||
let valueText: string;
|
||||
switch (desc.fmt) {
|
||||
case FieldValueFmt.Percent:
|
||||
valueText = `+${Math.round(entry.value * 100)}%`;
|
||||
break;
|
||||
case FieldValueFmt.IntMinus:
|
||||
valueText = `-${entry.value}`;
|
||||
break;
|
||||
case FieldValueFmt.Int:
|
||||
default:
|
||||
valueText = `+${entry.value}`;
|
||||
break;
|
||||
}
|
||||
return { prefix: `${head}${nameSeg}${desc.verb}`, value: valueText, suffix: "" };
|
||||
}
|
||||
|
||||
/**
|
||||
* 把"持续X秒"中的 X 也包裹绿色高亮(卡片描述后处理)。
|
||||
* Why: 计时模板 {dur} 不在 \x01\x02 高亮标记内,结构化行仅高亮主数值;
|
||||
* 持续时间对药水/计时 buff 同样关键,这里补充高亮,保证视觉一致。
|
||||
*/
|
||||
function highlightDuration(text: string): string {
|
||||
return text.replace(DURATION_PATTERN, `持续${valueRich("$1")}秒`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 结构化行 → 富文本串(技能名橙色、数值绿色)。
|
||||
* buildEquipRich / buildCardDescRich 共用,保证渲染规则唯一。
|
||||
*/
|
||||
function renderLineRich(line: ISkillLine): string {
|
||||
const prefix = line.prefix.replace(SKILL_NAME_PATTERN, (m, g1) => skillNameRich(g1));
|
||||
return line.value
|
||||
? `${prefix}${valueRich(line.value)}${line.suffix}`
|
||||
: `${prefix}${line.suffix}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描合并后技能的附加概率字段(crt/frz/stun/bck),非零的拼为高亮后缀。
|
||||
* Why: 模板不再硬编码"有概率击晕"等文案,由实际配置驱动,避免描述与数值漂移。
|
||||
* @returns 形如 ",30%击晕,20%冰冻";全为 0 时返回 ""
|
||||
*/
|
||||
function buildChanceSuffix(skill: SkillConfig): string {
|
||||
const items: string[] = [];
|
||||
const push = (val: number | undefined, label: string) => {
|
||||
if (val && val > 0) items.push(`附加${valueRich(val)}%${label}`);
|
||||
};
|
||||
push(skill.crt, "暴击");
|
||||
push(skill.frz, "冰冻");
|
||||
push(skill.stun, "击晕");
|
||||
push(skill.bck, "击退");
|
||||
return items.length > 0 ? `,${items.join(",")}` : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 单个技能详情富文本(IBox 弹窗用):展示 CD/持续时间 + 结构化效果描述。
|
||||
* @param skillUuid SkillSet 技能 UUID
|
||||
* @param overrides 可选覆写参数(如药水卡自定义档位)
|
||||
* @param lv 技能等级(仅用于展示,不影响数值计算)
|
||||
* @returns 多行富文本串:第一行名称/等级/持续时间,第二行效果描述
|
||||
*/
|
||||
export function buildSingleSkillRich(skillUuid: number, overrides?: SkillOverrides, lv: number = 1): string {
|
||||
const base = SkillSet[skillUuid];
|
||||
if (!base) return "未知技能";
|
||||
const skill = mergeSkillParams(base, overrides);
|
||||
// SkillConfig 未定义 cd 字段,冷却时间由外部运行时数据传入;计时模式展示持续时间
|
||||
const dur = skill.buff_duration ?? (skill.timed_buff_id !== undefined ? BuffList[skill.timed_buff_id]?.duration : undefined) ?? 0;
|
||||
const cdTag = dur > 0 ? valueRich(`持续:${dur}s`) : "";
|
||||
const nameTag = `${skillNameRich(skill.name)} Lv.${lv}`;
|
||||
|
||||
// 复用结构化效果行,head 为空避免重复触发类型前缀
|
||||
const line = buildEffectLine("", skill);
|
||||
const prefix = line.prefix.replace(SKILL_NAME_PATTERN, (m, g1) => skillNameRich(g1));
|
||||
const effect = line.value
|
||||
? `${prefix}${valueRich(line.value)}${line.suffix}`
|
||||
: `${prefix}${line.suffix}`;
|
||||
|
||||
return cdTag ? `${nameTag} ${cdTag}\n${effect}` : `${nameTag}\n${effect}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 装备/驻场光环详情富文本:展示全部词条,数值绿色高亮。
|
||||
* @param entries 驻场光环词条数组(uuid 指向 FieldSkillSet 底座,value 为生效数值)
|
||||
* @param showName 是否包含「技能名」片段(英雄面板 true,装备商店 false)
|
||||
* @returns 多行富文本串,每行一个词条
|
||||
*/
|
||||
export function buildEquipRich(entries: FieldEntry[], showName: boolean = false): string {
|
||||
if (!entries?.length) return "无词条";
|
||||
const lines: string[] = [];
|
||||
for (const entry of entries) {
|
||||
const line = buildFieldLine("", entry, showName);
|
||||
if (!line) continue;
|
||||
const prefix = line.prefix.replace(SKILL_NAME_PATTERN, (m, g1) => skillNameRich(g1));
|
||||
const text = line.value
|
||||
? `${prefix}${valueRich(line.value)}${line.suffix}`
|
||||
: `${prefix}${line.suffix}`;
|
||||
lines.push(text);
|
||||
}
|
||||
return lines.length > 0 ? lines.join("\n") : "无词条";
|
||||
}
|
||||
|
||||
/**
|
||||
* 商店卡片描述富文本(SCardComp / ItemListComp 用,BBCode)。
|
||||
*
|
||||
* Why: 卡片 info 为手写文案,与 SkillSet/BuffList 数值易漂移。
|
||||
* 本函数从 CardConfig.skill + overrides 合并后的实时配置生成描述,
|
||||
* 数值随档位(如高级药水 buff_value)自动同步,数值绿色高亮、技能名橙色。
|
||||
*
|
||||
* 描述规则:
|
||||
* - 药品/技能卡(card.skill 有效)→ 查 SkillDescSet 模板渲染(计时模式取 BuffList 默认值兜底);
|
||||
* 未入表技能回退 SkillSet.info / card.info,整行不高亮。
|
||||
* - 装备卡(card.field 有效)→ 复用 buildEquipRich 逐词条渲染。
|
||||
* - 触发节奏(t_times/t_inv)融入单行描述:定时触发"每M秒召唤K个「技能名」攻击敌人,xxx,共N次"(K=overrides.num+1);即时触发仅显示效果。
|
||||
*
|
||||
* @param card 卡片配置(SCardSet 技能卡 / ICardSet 药品卡 / EquipSet 装备卡)
|
||||
* @returns 多行富文本串;无可描述内容时回退 card.info
|
||||
*/
|
||||
export function buildCardDescRich(card: CardConfig): string {
|
||||
// 装备卡:逐词条渲染驻场光环
|
||||
if (card.field && card.field.length > 0) {
|
||||
return buildEquipRich(card.field);
|
||||
}
|
||||
|
||||
// 药品/技能卡:合并覆写后按 SkillDescSet 模板渲染
|
||||
const base = card.skill ? SkillSet[card.skill] : undefined;
|
||||
if (base) {
|
||||
const skill = mergeSkillParams(base, card.overrides);
|
||||
const line = buildEffectLine("", skill);
|
||||
// 未入表技能回退链:SkillSet.info → card.info
|
||||
if (!line.value && !SkillDescSet[skill.uuid]) {
|
||||
return String(skill.info || card.info || "");
|
||||
}
|
||||
|
||||
// 触发节奏融入描述:定时触发"每M秒释放一次「技能名」,xxx,共N次";即时触发直接描述
|
||||
const tTimes = card.t_times ?? 1;
|
||||
const tInv = card.t_inv ?? 0;
|
||||
const isInstant = card.is_inst || tInv <= 0;
|
||||
// 附加概率后缀(暴击/冰冻/击晕/击退),由实际配置驱动
|
||||
const suffix = buildChanceSuffix(skill);
|
||||
const rich = highlightDuration(renderLineRich(line)) + suffix;
|
||||
|
||||
if (isInstant) {
|
||||
return rich;
|
||||
}
|
||||
|
||||
const rhythm = `每${valueRich(tInv)}秒召唤${valueRich((card.overrides?.num ?? 0) + 1)}个${skillNameRich(skill.name)}攻击敌人`;
|
||||
const times = `, 共${valueRich(tTimes)}次`;
|
||||
return `${rhythm}, ${rich}${times}`;
|
||||
}
|
||||
|
||||
return card.info || "";
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "de462338-a674-4ccf-b621-bb67d0d9b901",
|
||||
"uuid": "9855de2d-2900-4e2d-831d-c9f177716a71",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
@@ -1,943 +0,0 @@
|
||||
/**
|
||||
* @file HeroSkillDesc.ts
|
||||
* @description 英雄技能/能力描述的【唯一文字表达标准层】
|
||||
*
|
||||
* @deprecated 已被 HeroSkillText.ts(heroInfo 内联 desc 模板驱动)取代。
|
||||
* 本文件暂时保留兼容旧 UI 引用(HerosListComp/CHeroComp/HeroBoxComp/HeroMoreCom/
|
||||
* SCardComp/ItemListComp/IBoxComp/EquipListComp/SinfoComp),待各面板迁移至
|
||||
* buildHeroSkillDesc / buildHeroSkillTierDesc 后移除。新代码请勿再引用本模块。
|
||||
*
|
||||
* 设计目标:
|
||||
* heroSet / SkillSet 中的 info / infos 仅作"基础备注",不再承担面板展示。
|
||||
* 所有功能性描述(触发技能、驻场光环、复活、等级额外属性加成)统一由本模块
|
||||
* 依据结构化配置动态拼出,数值永远来自 mergeSkillParams 合并后的实时配置,
|
||||
* 杜绝"改数值忘改文案"的漂移。
|
||||
*
|
||||
* 输出(双格式,供 UI 按需取用):
|
||||
* - buildSkillDesc(source) → 纯文本(Label 用),数值无颜色
|
||||
* - buildSkillDescRich(source) → 富文本(RichText 用,BBCode),技能名蓝色、数值绿色高亮
|
||||
* 两者共用同一套结构化行构建逻辑,仅渲染阶段区分,保证纯/富文本内容严格一致。
|
||||
*
|
||||
* 通用性(纯配置驱动):
|
||||
* 1. 触发框架:按触发类型遍历(SkillSet + overrides,不动)
|
||||
* 2. 效果文案:查 SkillDescSet 结构化模板渲染
|
||||
* 3. 目标/属性名:由本模块统一语义化(target/hit_count、ATTR_NAME)
|
||||
*
|
||||
* 数据源(入参抽象 ISkillDescSource):
|
||||
* heroInfo(静态 1 级配置)与 HeroAttrsComp(运行时应用 evolve 后的当前等级配置)
|
||||
* 字段结构一致,天然都满足。HeroBoxComp 传 model 显示当前等级;卡池/商店传 heroInfo 显示 1 级。
|
||||
*
|
||||
* 依赖:
|
||||
* - heroSet :SkillTriggerName、SkillTriggerType、TriggerGrouped/LvFieldEntry/LvReviveEntry/LvBonusEntry 及 resolver
|
||||
* - SkillSet :SkillSet、mergeSkillParams、SkillOverrides、SkillKind、TGroup
|
||||
* - SkillDescSet:结构化描述模板
|
||||
* - FieldSkillSet(驻场技能)、BuffList(计时 buff 默认值兜底)、Attrs(属性枚举)
|
||||
*/
|
||||
import { SkillTriggerName, SkillTriggerType, TriggerGrouped, LvTriggerEntry, LvFieldEntry, LvReviveEntry, LvBonusEntry, FieldEntry, resolveTriggerByLv, resolveFieldByLv, resolveReviveByLv, resolveSkillOverrides, getGroupedTriggerName, getFieldSkillName, getReviveSkillName, HGrowType, calcGrowBonus, HSkillInfo } from "./heroSet";
|
||||
import { FieldSkillSet, mergeSkillParams, SkillConfig, SkillKind, SkillOverrides, SkillSet, TGroup } from "./SkillSet";
|
||||
import { FieldDescSet, FieldValueFmt, SkillDescSet, SkillDescConfig } from "./SkillDescSet";
|
||||
import { BuffList } from "./BuffSet";
|
||||
import { Attrs } from "./HeroAttrs";
|
||||
import { CardConfig } from "./CardSet";
|
||||
|
||||
/**
|
||||
* 生成器入参窄接口:只约束实际读取的字段。
|
||||
* heroInfo 与 HeroAttrsComp 的对应字段结构完全一致(均为分组 + lv 数组),天然都满足此接口。
|
||||
*/
|
||||
export interface ISkillDescSource {
|
||||
/** 当前等级(用于 resolve 触发技能/光环/复活取档;静态 heroInfo 恒为 1) */
|
||||
lv?: number;
|
||||
/** 携带技能(普攻技能,支持 lv_entries 按等级分档 resolve) */
|
||||
skills?: Record<number, HSkillInfo>;
|
||||
call?: TriggerGrouped;
|
||||
dead?: TriggerGrouped;
|
||||
fstart?: TriggerGrouped;
|
||||
atking?: TriggerGrouped;
|
||||
atked?: TriggerGrouped;
|
||||
field?: LvFieldEntry[];
|
||||
revive?: LvReviveEntry[];
|
||||
/** 额外攻击/生命加成(按 lv 累加,用于成长文案) */
|
||||
ap_bonus?: LvBonusEntry[];
|
||||
hp_bonus?: LvBonusEntry[];
|
||||
/** 成长类型(决定每级 hp/ap 标准递增增量) */
|
||||
grow_type?: HGrowType;
|
||||
/** 基础攻击/生命(成长基数,线性公式 = 基础 × 系数 × (lv-1)) */
|
||||
ap?: number;
|
||||
hp?: number;
|
||||
/** 原始基础攻击/生命(HeroAttrsComp 场景优先用作成长基数,避免运行时 hp/ap 重复计成长) */
|
||||
base_ap?: number;
|
||||
base_hp?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 一条能力的结构化行:数值片段单独抽出,供富文本高亮与纯文本复用。
|
||||
* 渲染结果 = prefix + value(可高亮) + suffix。
|
||||
*/
|
||||
interface ISkillLine {
|
||||
/** 数值前的文本(含触发条件、技能名、目标、动词等) */
|
||||
prefix: string;
|
||||
/** 需要高亮的核心数值文本(如 "15%"、"4次"、"+30"),无数值时为 "" */
|
||||
value: string;
|
||||
/** 数值后的文本(如 ",持续5秒" 之类尾部补充) */
|
||||
suffix: string;
|
||||
/** 尚未激活(档位等级高于当前等级):富文本整行灰色、不做高亮 */
|
||||
locked?: boolean;
|
||||
/** 同一技能未来档位的数值列表(全档位合并模式专用):渲染为 value 后灰色 "/v1/v2" 拼接 */
|
||||
nextValues?: string[];
|
||||
}
|
||||
|
||||
/** 需要遍历的触发技能类型列表(不含 Field 和 Revive,这两者结构不同需单独处理) */
|
||||
const TRIGGER_KEYS: SkillTriggerType[] = [
|
||||
SkillTriggerType.Call,
|
||||
SkillTriggerType.Dead,
|
||||
SkillTriggerType.FStart,
|
||||
SkillTriggerType.Atking,
|
||||
SkillTriggerType.Atked,
|
||||
];
|
||||
|
||||
/** 属性标准中文名映射(统一称谓,供 evolve 额外加成文案使用),仅覆盖 ap_bonus/hp_bonus 用到的属性 */
|
||||
const ATTR_NAME: Partial<Record<Attrs, string>> = {
|
||||
[Attrs.ap]: "攻击力",
|
||||
[Attrs.hp_max]: "最大生命",
|
||||
};
|
||||
|
||||
/** 富文本数值高亮色(鲜亮绿色,深色面板上高亮醒目) */
|
||||
const RICH_VALUE_COLOR = "#3CE66E";
|
||||
|
||||
/** 数值描边色(深绿,衬托亮绿填充,与技能名橙填充+深褐描边同一设计思路) */
|
||||
const RICH_VALUE_OUTLINE = "#145A32";
|
||||
|
||||
/**
|
||||
* 数值富文本片段:亮绿填充 + 深绿描边。
|
||||
* 高亮数值明度高,深色描边增强边缘对比、提升深色面板上的锐度。
|
||||
* @param text 数值文本(含单位,如 "15%"、"4次")
|
||||
*/
|
||||
function valueRich(text: string | number): string {
|
||||
return `<outline color=${RICH_VALUE_OUTLINE} width=1><color=${RICH_VALUE_COLOR}>${text}</color></outline>`;
|
||||
}
|
||||
|
||||
/** 富文本技能名高亮色(蜜桃橙,暖色系,与面板绿/蓝/红/黄/紫均不冲突) */
|
||||
const RICH_NAME_COLOR = "#F8C471";
|
||||
|
||||
/** 技能名描边色(深褐,衬托蜜桃橙填充,深色面板上温馨醒目) */
|
||||
const RICH_NAME_OUTLINE = "#6E2C00";
|
||||
|
||||
/**
|
||||
* 技能名富文本片段:[技能名]整体蜜桃橙填充 + 深褐描边(含方括号)。
|
||||
* 括号与技能名同色同描边,正文/数值不描边,突出技能名视觉层级。
|
||||
* @param name 技能名(纯文本,不含括号)
|
||||
* @returns <outline><color>[技能名]</color></outline> 富文本片段
|
||||
*/
|
||||
function skillNameRich(name: string): string {
|
||||
return `<outline color=${RICH_NAME_OUTLINE} width=1><color=${RICH_NAME_COLOR}>[${name}]</color></outline>`;
|
||||
}
|
||||
|
||||
/** 富文本未激活档位整行灰色(低饱和,深色面板上可读但明显弱化) */
|
||||
const RICH_LOCKED_COLOR = "#95A5A6";
|
||||
|
||||
/** 技能名「」匹配模式(仅本模块行首/行中技能名片段使用) */
|
||||
const SKILL_NAME_PATTERN = /「([^」]*)」/g;
|
||||
|
||||
/** "持续X秒"匹配模式(卡片描述后处理:把持续时间数值也高亮) */
|
||||
const DURATION_PATTERN = /持续(\d+)秒/g;
|
||||
|
||||
/** 解析目标描述文本(与战斗 SCastSystem 目标选取语义严格一致) */
|
||||
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 "全体友方";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 占位符模板替换,并把"核心数值片段"用 \x01...\x02 标记,便于上层拆分高亮。
|
||||
*
|
||||
* Why: 模板中 {value}{unit} 是玩家最关心的数值。这里在渲染时将其单独包裹,
|
||||
* 使富文本可对其单独上色,纯文本则剥离标记后原样拼接,保证两种输出内容一致。
|
||||
*
|
||||
* @returns 含 \x01hlValue\x02 标记的完整文本
|
||||
*/
|
||||
function renderTemplate(templ: string, vars: Record<string, string | number>, hlValue: string): string {
|
||||
let out = templ;
|
||||
for (const key of Object.keys(vars)) {
|
||||
out = out.split(`{${key}}`).join(String(vars[key]));
|
||||
}
|
||||
// 用 \x01...\x02 包裹数值片段作为标记,便于上层拆分为 prefix/value/suffix
|
||||
return out.split(hlValue).join(`\x01${hlValue}\x02`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 由一行模板渲染结果构造结构化行(剥离 \x01\x02 标记,把数值片段拆到 value 字段)
|
||||
* @param head 行首(触发条件:技能名 )
|
||||
* @param rendered 已用 \x01...\x02 标记数值片段的效果文本
|
||||
*/
|
||||
function makeLine(head: string, rendered: string): ISkillLine {
|
||||
const start = rendered.indexOf("\x01");
|
||||
const end = rendered.indexOf("\x02");
|
||||
if (start >= 0 && end > start) {
|
||||
// 恰好含一个数值片段:prefix=行首+数值前文本,value=数值,suffix=数值后文本
|
||||
return {
|
||||
prefix: `${head}${rendered.slice(0, start)}`,
|
||||
value: rendered.slice(start + 1, end),
|
||||
suffix: rendered.slice(end + 1),
|
||||
};
|
||||
}
|
||||
// 无数值片段(未命中模板或兜底 info):剥离残留标记,整行作为 prefix,不高亮
|
||||
return { prefix: `${head}${rendered.replace(/[\x01\x02]/g, "")}`, value: "", suffix: "" };
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据合并后的技能配置构造结构化效果行(查 SkillDescSet 模板渲染 + 数值高亮抽取)
|
||||
*
|
||||
* 计时模式默认值兜底:
|
||||
* overrides 仅给 timed_buff_id 而未覆写 buff_value / buff_duration 时,
|
||||
* 从 BuffList[timed_buff_id] 取默认值,保证 1 级基础档文案完整。
|
||||
*/
|
||||
function buildEffectLine(head: string, skill: SkillConfig, descOverride?: SkillDescConfig): ISkillLine {
|
||||
const desc = descOverride ?? SkillDescSet[skill.uuid];
|
||||
// 未入表技能回退基础备注 info,无法抽出数值,整行不高亮
|
||||
if (!desc) return { prefix: `${head}${String(skill.info ?? "")}`, value: "", suffix: "" };
|
||||
|
||||
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;
|
||||
const dur = skill.buff_duration ?? buff?.duration ?? 0;
|
||||
|
||||
// 高亮片段 = 数值 + 单位(如 "15%"、"4次"、"+30"),是玩家视觉焦点
|
||||
const hlValue = `${value}${desc.unit}`;
|
||||
const rendered = renderTemplate(templ, {
|
||||
target: buildTargetDesc(skill),
|
||||
value: value,
|
||||
unit: desc.unit,
|
||||
verb: desc.verb,
|
||||
dur: dur,
|
||||
}, hlValue);
|
||||
return makeLine(head, rendered);
|
||||
}
|
||||
|
||||
/**
|
||||
* 驻场光环(field)结构化行:查 FieldDescSet 按 type 渲染,value 实时取数值并高亮。
|
||||
*
|
||||
* Why: FieldSkillConfig.info 是手写文案,改 value 易漂移。这里以 FieldSkillType
|
||||
* 为 key 查标准动词,数值按 fmt(比例/整数/减免)从 value 实时渲染。
|
||||
*
|
||||
* @param showName 是否在行中包含「技能名」片段(英雄面板需要,装备商店不需要)
|
||||
* @param nameOverride 技能名覆盖(英雄语境传入 heroSet 语境专属命名,缺省用 FieldSkillSet 通用名)
|
||||
*/
|
||||
function buildFieldLine(head: string, entry: FieldEntry, showName: boolean = true, nameOverride?: string): ISkillLine | null {
|
||||
const fs = FieldSkillSet[entry.uuid];
|
||||
if (!fs) return null;
|
||||
const desc = FieldDescSet[fs.type];
|
||||
const nameSeg = showName ? `「${nameOverride ?? fs.name}」 ` : "";
|
||||
// 未入表类型回退基础备注 info,整行不高亮
|
||||
if (!desc) return { prefix: `${head}${nameSeg}${fs.info}`, value: "", suffix: "" };
|
||||
|
||||
// 数值按格式渲染为高亮片段(数值取自词条自身 value)
|
||||
let valueText: string;
|
||||
switch (desc.fmt) {
|
||||
case FieldValueFmt.Percent:
|
||||
valueText = `+${Math.round(entry.value * 100)}%`;
|
||||
break;
|
||||
case FieldValueFmt.IntMinus:
|
||||
valueText = `-${entry.value}`;
|
||||
break;
|
||||
case FieldValueFmt.Int:
|
||||
default:
|
||||
valueText = `+${entry.value}`;
|
||||
break;
|
||||
}
|
||||
return { prefix: `${head}${nameSeg}${desc.verb}`, value: valueText, suffix: "" };
|
||||
}
|
||||
|
||||
/**
|
||||
* 复活(revive)结构化行:复刻战斗公式,高亮复活次数与回血百分比
|
||||
* 复活次数 = r_num + floor((lv - 1) * upr);回血百分比 = SkillSet[s_uuid].ap
|
||||
*
|
||||
* @param showLocked true 时列出全部档位并标激活状态(未激活档位次数按解锁等级预估);
|
||||
* false 时仅输出当前等级生效档
|
||||
*/
|
||||
function buildReviveLines(source: ISkillDescSource, showName: boolean = true, showLocked: boolean = false): ISkillLine[] {
|
||||
const lv = Math.max(1, source.lv ?? 1);
|
||||
const entries = source.revive;
|
||||
if (!entries?.length) return [];
|
||||
|
||||
const tpl = SkillTriggerName[SkillTriggerType.Revive] ?? "复活";
|
||||
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<number, LvReviveEntry[]> = {};
|
||||
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)累计结构化行:成长递增 + 一次性奖励,高亮累计数值 */
|
||||
function buildEvolveBonusLines(source: ISkillDescSource): ISkillLine[] {
|
||||
const lv = Math.max(1, source.lv ?? 1);
|
||||
// 成长线性增量:优先用原始基础值(base_*,HeroAttrsComp 场景),否则用配置 hp/ap;
|
||||
// 缺基础值时按 0 处理(如纯触发技能描述场景),避免运行时 hp/ap 重复计成长
|
||||
const baseHp = source.base_hp ?? source.hp ?? 0;
|
||||
const baseAp = source.base_ap ?? source.ap ?? 0;
|
||||
const grow = calcGrowBonus({ hp: baseHp, ap: baseAp, grow_type: source.grow_type }, lv);
|
||||
let apTotal = grow.ap;
|
||||
let hpTotal = grow.hp;
|
||||
if (source.ap_bonus) {
|
||||
for (const e of source.ap_bonus) { if (e.lv <= lv) apTotal += e.value; }
|
||||
}
|
||||
if (source.hp_bonus) {
|
||||
for (const e of source.hp_bonus) { if (e.lv <= lv) hpTotal += e.value; }
|
||||
}
|
||||
const lines: ISkillLine[] = [];
|
||||
if (apTotal !== 0) lines.push({ prefix: "成长:", value: `+${apTotal}`, suffix: ATTR_NAME[Attrs.ap] ?? "攻击力" });
|
||||
if (hpTotal !== 0) lines.push({ prefix: "成长:", value: `+${hpTotal}`, suffix: ATTR_NAME[Attrs.hp_max] ?? "最大生命" });
|
||||
return lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建全部能力结构化行(触发技能 + 驻场光环 + 复活 + 额外加成)。
|
||||
* 纯文本与富文本两个公开接口共用此结果,仅渲染阶段不同。
|
||||
*
|
||||
* @param showName 是否在行中包含「技能名」片段
|
||||
* @param showLocked true 时为全档位合并模式:每个技能只出一行——已激活档正常渲染且
|
||||
* 未来档数值灰色拼接在 value 后(nextValues);完全未激活的技能
|
||||
* 预览最低档、整行 locked 灰色、Lv 前缀置前。false 时仅输出当前生效档
|
||||
*/
|
||||
function buildSkillLines(source: ISkillDescSource, showName: boolean = true, showLocked: boolean = false): ISkillLine[] {
|
||||
const lines: ISkillLine[] = [];
|
||||
const lv = Math.max(1, source.lv ?? 1);
|
||||
|
||||
// ---- 普攻技能(skills):按英雄等级 resolve 分档 overrides ----
|
||||
if (source.skills) {
|
||||
for (const key in source.skills) {
|
||||
const hSkill = source.skills[key];
|
||||
if (!hSkill) continue;
|
||||
const base = SkillSet[hSkill.uuid];
|
||||
if (!base) continue;
|
||||
const overrides = resolveSkillOverrides(hSkill, lv);
|
||||
const skill = mergeSkillParams(base, overrides);
|
||||
// 技能等级展示:lv_entries 有档时取档位 s_lv,否则用运行时 lv
|
||||
const sLv = hSkill.lv_entries?.length
|
||||
? (hSkill.lv_entries.filter(e => e.lv <= lv).pop()?.s_lv ?? hSkill.lv)
|
||||
: hSkill.lv;
|
||||
const nameSeg = showName ? `「${hSkill.name ?? base.name}」 ` : "";
|
||||
lines.push(buildEffectLine(`Lv${sLv}: ${nameSeg}`, skill));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 6 种标准触发技能 ----
|
||||
for (const key of TRIGGER_KEYS) {
|
||||
const grouped = source[key] as TriggerGrouped | undefined;
|
||||
if (!grouped) continue;
|
||||
// 触发类型简称(召唤/亡语/起手/生息/进击/受击)
|
||||
const name = SkillTriggerName[key] ?? key;
|
||||
// 仅进击/受击带触发次数,拼为"×n";其余简称直接用
|
||||
const needCount = key === SkillTriggerType.Atking || key === SkillTriggerType.Atked;
|
||||
|
||||
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;
|
||||
// 触发技能名走 HeroInfo 语境配置(如 atked.6301=不屈壁垒),未配置回退 SkillSet 内部名
|
||||
const nameSeg = showName ? `「${getGroupedTriggerName(grouped, item.s_uuid)}」 ` : "";
|
||||
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 sorted = [...entries].sort((a, b) => a.lv - b.lv);
|
||||
const nameSeg = showName ? `「${getGroupedTriggerName(grouped, Number(sKey))}」 ` : "";
|
||||
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 fieldEntries = source[SkillTriggerType.Field] as LvFieldEntry[] | undefined;
|
||||
if (fieldEntries?.length) {
|
||||
const tpl = SkillTriggerName[SkillTriggerType.Field] ?? "光环";
|
||||
if (!showLocked) {
|
||||
for (const entry of resolveFieldByLv(fieldEntries, lv)) {
|
||||
const line = buildFieldLine(`${tpl}:`, entry, 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 entry of activeTier.uuids) {
|
||||
const line = buildFieldLine(`Lv${activeTier.lv}: ${tpl} `, entry, showName);
|
||||
if (line) lines.push(line);
|
||||
}
|
||||
}
|
||||
for (const tier of sorted) {
|
||||
if (tier.lv <= lv) continue;
|
||||
for (const entry of tier.uuids) {
|
||||
const line = buildFieldLine(`Lv${tier.lv}: ${tpl} `, entry, showName);
|
||||
if (!line) continue;
|
||||
line.locked = true;
|
||||
lines.push(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 复活(revive)----
|
||||
lines.push(...buildReviveLines(source, showName, showLocked));
|
||||
|
||||
// ---- 额外属性加成(ap/hp,按当前等级累计)----
|
||||
lines.push(...buildEvolveBonusLines(source));
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* 纯文本描述(Label 用):数值无颜色。
|
||||
* @param source 触发技能数据源(heroInfo 静态配置 或 HeroAttrsComp 运行时当前等级配置)
|
||||
* @returns 多行纯文本,每行一条能力,用 \n 分隔
|
||||
*/
|
||||
export function buildSkillDesc(source: ISkillDescSource): string {
|
||||
return buildSkillLines(source)
|
||||
.map(l => `${l.prefix}${l.value}${l.suffix}`)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* 富文本描述(RichText 用,BBCode):技能名用 <color=#2E86C1> 蓝色、数值用 <color=#27AE60> 亮绿色高亮。
|
||||
* @param source 触发技能数据源(同 buildSkillDesc)
|
||||
* @param showName 是否包含「技能名」片段(默认 true)
|
||||
* @param showLocked true 时为全档位合并模式:每个技能只出一行,已激活档数值高亮、
|
||||
* 未来档数值灰色 "/v1/v2" 追加;完全未激活的技能整行灰色且 Lv 前缀置前
|
||||
* (英雄盒面板用);false 时仅输出当前等级生效档
|
||||
* @returns 多行富文本串,用 \n 分隔
|
||||
*/
|
||||
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");
|
||||
}
|
||||
|
||||
/**
|
||||
* 单个技能详情富文本(IBox 弹窗用):展示 CD/持续时间 + 结构化效果描述。
|
||||
* @param skillUuid SkillSet 技能 UUID
|
||||
* @param overrides 可选覆写参数(如药水卡自定义档位)
|
||||
* @param lv 技能等级(仅用于展示,不影响数值计算)
|
||||
* @returns 多行富文本串:第一行 CD/持续时间,第二行效果描述
|
||||
*/
|
||||
export function buildSingleSkillRich(skillUuid: number, overrides?: SkillOverrides, lv: number = 1): string {
|
||||
const base = SkillSet[skillUuid];
|
||||
if (!base) return "未知技能";
|
||||
const skill = mergeSkillParams(base, overrides);
|
||||
// SkillConfig 未定义 cd 字段,冷却时间由外部运行时数据传入;计时模式展示持续时间
|
||||
const dur = skill.buff_duration ?? (skill.timed_buff_id !== undefined ? BuffList[skill.timed_buff_id]?.duration : undefined) ?? 0;
|
||||
const cdTag = dur > 0 ? valueRich(`持续:${dur}s`) : "";
|
||||
const nameTag = `${skillNameRich(skill.name)} Lv.${lv}`;
|
||||
|
||||
// 复用结构化效果行,head 为空避免重复触发类型前缀
|
||||
const line = buildEffectLine("", skill);
|
||||
const prefix = line.prefix.replace(SKILL_NAME_PATTERN, (m, g1) => skillNameRich(g1));
|
||||
const effect = line.value
|
||||
? `${prefix}${valueRich(line.value)}${line.suffix}`
|
||||
: `${prefix}${line.suffix}`;
|
||||
|
||||
return cdTag ? `${nameTag} ${cdTag}\n${effect}` : `${nameTag}\n${effect}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单个技能纯效果富文本(SInfo 提升面板用):不含技能名与 Lv 前缀,仅输出效果描述。
|
||||
* @param skillUuid SkillSet 技能 UUID
|
||||
* @param overrides 可选覆写参数
|
||||
* @param desc 可选 SkillDescConfig(heroSet 技能项自带说明,优先于 SkillDescSet)
|
||||
* @returns 单行富文本串:如 "造成攻击力<color=#27AE60>250%</color>的伤害"
|
||||
*/
|
||||
export function buildEffectOnlyRich(skillUuid: number, overrides?: SkillOverrides, desc?: SkillDescConfig): string {
|
||||
const base = SkillSet[skillUuid];
|
||||
if (!base) return "未知技能";
|
||||
const skill = mergeSkillParams(base, overrides);
|
||||
const line = buildEffectLine("", skill, desc);
|
||||
const prefix = line.prefix.replace(SKILL_NAME_PATTERN, (m, g1) => skillNameRich(g1));
|
||||
return line.value
|
||||
? `${prefix}${valueRich(line.value)}${line.suffix}`
|
||||
: `${prefix}${line.suffix}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 装备/驻场光环详情富文本:展示全部词条,数值绿色高亮。
|
||||
* @param entries 驻场光环词条数组(uuid 指向 FieldSkillSet 底座,value 为生效数值)
|
||||
* @param showName 是否包含「技能名」片段(英雄面板 true,装备商店 false)
|
||||
* @returns 多行富文本串,每行一个词条
|
||||
*/
|
||||
export function buildEquipRich(entries: FieldEntry[], showName: boolean = false): string {
|
||||
if (!entries?.length) return "无词条";
|
||||
const lines: string[] = [];
|
||||
for (const entry of entries) {
|
||||
const line = buildFieldLine("", entry, showName);
|
||||
if (!line) continue;
|
||||
const prefix = line.prefix.replace(SKILL_NAME_PATTERN, (m, g1) => skillNameRich(g1));
|
||||
const text = line.value
|
||||
? `${prefix}${valueRich(line.value)}${line.suffix}`
|
||||
: `${prefix}${line.suffix}`;
|
||||
lines.push(text);
|
||||
}
|
||||
return lines.length > 0 ? lines.join("\n") : "无词条";
|
||||
}
|
||||
|
||||
/**
|
||||
* 英雄信息弹窗技能描述富文本(HeroMoreCom 用)。
|
||||
*
|
||||
* 格式:
|
||||
* 「技能名」 Lv.当前技能等级:当前档技能效果
|
||||
*
|
||||
* 仅展示当前生效档位,不含升级提示;升级前后对比由升级确认框单独提供。
|
||||
* 数据源为运行时 HeroAttrsComp(lv 取当前英雄等级),逐触发类型逐技能渲染。
|
||||
* 技能等级取 LvTriggerEntry.s_lv,未标注时按档位序号(1/2/3...)兜底。
|
||||
*
|
||||
* @param source 英雄运行时数据源(含 lv 与触发技能分组)
|
||||
* @param showName 是否展示「技能名」片段(英雄信息弹窗 true,英雄卡池卡片 false)
|
||||
* @returns 多行富文本串,\n 分隔
|
||||
*/
|
||||
export function buildSkillDescMoreRich(source: ISkillDescSource, showName: boolean = true): string {
|
||||
const lv = Math.max(1, source.lv ?? 1);
|
||||
const lines: string[] = [];
|
||||
|
||||
// 单条效果 → 富文本(技能名蓝色、数值绿色高亮、持续秒数高亮)
|
||||
const effectRich = (line: ISkillLine): string => {
|
||||
const prefix = line.prefix.replace(SKILL_NAME_PATTERN, (m, g1) => skillNameRich(g1));
|
||||
const text = line.value
|
||||
? `${prefix}${valueRich(line.value)}${line.suffix}`
|
||||
: `${prefix}${line.suffix}`;
|
||||
return highlightDuration(text);
|
||||
};
|
||||
|
||||
// ---- 普攻技能(skills):按英雄等级 resolve 分档 overrides,展示当前生效档 ----
|
||||
if (source.skills) {
|
||||
for (const key in source.skills) {
|
||||
const hSkill = source.skills[key];
|
||||
if (!hSkill) continue;
|
||||
const base = SkillSet[hSkill.uuid];
|
||||
if (!base) continue;
|
||||
const overrides = resolveSkillOverrides(hSkill, lv);
|
||||
const skill = mergeSkillParams(base, overrides);
|
||||
// 技能等级展示:lv_entries 有档时取档位 s_lv,否则用运行时 lv
|
||||
const sLv = hSkill.lv_entries?.length
|
||||
? (hSkill.lv_entries.filter(e => e.lv <= lv).pop()?.s_lv ?? hSkill.lv)
|
||||
: hSkill.lv;
|
||||
const head = showName ? `「${hSkill.name ?? base.name}」 Lv.${sLv}: ` : `Lv.${sLv}: `;
|
||||
lines.push(effectRich(buildEffectLine(head, skill)));
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of TRIGGER_KEYS) {
|
||||
const grouped = source[key] as TriggerGrouped | undefined;
|
||||
if (!grouped) continue;
|
||||
const trigName = SkillTriggerName[key] ?? key;
|
||||
const needCount = key === SkillTriggerType.Atking || key === SkillTriggerType.Atked;
|
||||
|
||||
for (const sKey in grouped) {
|
||||
const entries = grouped[sKey];
|
||||
if (!entries?.length) continue;
|
||||
const sUuid = Number(sKey);
|
||||
const base = SkillSet[sUuid];
|
||||
if (!base) continue;
|
||||
const sorted = [...entries].sort((a, b) => a.lv - b.lv);
|
||||
// 技能名走 HeroInfo 触发语境配置(如 atked.6301=不屈壁垒)
|
||||
const skillName = getGroupedTriggerName(grouped, sUuid);
|
||||
|
||||
// 当前生效档(≤lv 最大档),未激活则用最低档
|
||||
let activeIdx = -1;
|
||||
for (let i = 0; i < sorted.length; i++) {
|
||||
if (sorted[i].lv <= lv) activeIdx = i;
|
||||
}
|
||||
const curIdx = activeIdx >= 0 ? activeIdx : 0;
|
||||
const cur = sorted[curIdx];
|
||||
const curSLv = cur.s_lv ?? (curIdx + 1);
|
||||
// 触发次数直接写为富文本(数字绿色高亮),如 "受伤3次"
|
||||
const curTrig = needCount
|
||||
? `${trigName}${valueRich(cur.t_num)}次`
|
||||
: trigName;
|
||||
// 技能名直接展示,不附加档位数字后缀(由 lv 字段控制生效档,玩家无需关注内部档号)
|
||||
const curHead = showName ? `「${skillName}」: ${curTrig}, ` : `${curTrig}, `;
|
||||
lines.push(effectRich(buildEffectLine(curHead, mergeSkillParams(base, cur.overrides))));
|
||||
}
|
||||
}
|
||||
|
||||
// 驻场光环(field):直接展示技能名,不附加档位数字后缀
|
||||
const fieldEntries = source[SkillTriggerType.Field] as LvFieldEntry[] | undefined;
|
||||
if (fieldEntries?.length) {
|
||||
for (const entry of resolveFieldByLv(fieldEntries, lv)) {
|
||||
const baseName = getFieldSkillName(entry.uuid);
|
||||
const line = buildFieldLine("", entry, showName, baseName);
|
||||
if (line) lines.push(effectRich(line));
|
||||
}
|
||||
}
|
||||
|
||||
// 重生(revive):每回合拥有 N 次重生机会,死亡后以 X% 血量原地重生
|
||||
// 血量百分比来自 SkillSet[s_uuid].ap(运行时口径),upr 是每级追加次数,不在此展示
|
||||
const revive = resolveReviveByLv(source[SkillTriggerType.Revive] as LvReviveEntry[] | undefined, lv);
|
||||
if (revive) {
|
||||
const reviveName = getReviveSkillName();
|
||||
const reviveHpPct = SkillSet[revive.s_uuid]?.ap ?? 50;
|
||||
lines.push(showName
|
||||
? `${skillNameRich(reviveName)}: 每回合拥有${valueRich(revive.r_num)}次重生机会, 死亡后以${valueRich(`${Math.round(reviveHpPct)}%`)}血量原地重生`
|
||||
: `每回合拥有${valueRich(revive.r_num)}次重生机会, 死亡后以${valueRich(`${Math.round(reviveHpPct)}%`)}血量原地重生`);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* 把"持续X秒"中的 X 也包裹绿色高亮(卡片描述后处理)。
|
||||
* Why: 计时模板 {dur} 不在 \x01\x02 高亮标记内,renderLineRich 仅高亮主数值;
|
||||
* 持续时间对药水/计时 buff 同样关键,这里补充高亮,保证视觉一致。
|
||||
*/
|
||||
function highlightDuration(text: string): string {
|
||||
return text.replace(DURATION_PATTERN, `持续${valueRich("$1")}秒`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 结构化行 → 富文本串(技能名蓝色、数值绿色)。
|
||||
* buildEquipRich / buildCardDescRich / buildSkillDescRich 共用,保证渲染规则唯一。
|
||||
* 未激活行(locked)整行灰色弱化、不做技能名/数值高亮。
|
||||
*/
|
||||
function renderLineRich(line: ISkillLine): string {
|
||||
if (line.locked) return `<color=${RICH_LOCKED_COLOR}>${line.prefix}${line.value}${line.suffix}</color>`;
|
||||
const prefix = line.prefix.replace(SKILL_NAME_PATTERN, (m, g1) => skillNameRich(g1));
|
||||
// 未来档数值灰色追加在当前高亮数值后("15%/20%/30%"),suffix 仍位于整段数值之后
|
||||
const next = line.nextValues?.length ? `<color=${RICH_LOCKED_COLOR}>/${line.nextValues.join("/")}</color>` : "";
|
||||
return line.value
|
||||
? `${prefix}${valueRich(line.value)}${next}${line.suffix}`
|
||||
: `${prefix}${next}${line.suffix}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描合并后技能的附加概率字段(crt/frz/stun/bck),非零的拼为高亮后缀。
|
||||
* Why: 模板不再硬编码"有概率击晕"等文案,由实际配置驱动,避免描述与数值漂移。
|
||||
* @returns 形如 ",30%击晕,20%冰冻";全为 0 时返回 ""
|
||||
*/
|
||||
function buildChanceSuffix(skill: SkillConfig): string {
|
||||
const items: string[] = [];
|
||||
const push = (val: number | undefined, label: string) => {
|
||||
if (val && val > 0) items.push(`附加${valueRich(val)}%${label}`);
|
||||
};
|
||||
push(skill.crt, "暴击");
|
||||
push(skill.frz, "冰冻");
|
||||
push(skill.stun, "击晕");
|
||||
push(skill.bck, "击退");
|
||||
return items.length > 0 ? `,${items.join(",")}` : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 商店卡片描述富文本(SCardComp / ItemListComp 用,BBCode)。
|
||||
*
|
||||
* Why: 卡片 info 为手写文案,与 SkillSet/BuffList 数值易漂移。
|
||||
* 本函数从 CardConfig.skill + overrides 合并后的实时配置生成描述,
|
||||
* 数值随档位(如高级药水 buff_value)自动同步,数值绿色高亮、技能名蓝色。
|
||||
*
|
||||
* 描述规则:
|
||||
* - 药品/技能卡(card.skill 有效)→ 查 SkillDescSet 模板渲染(计时模式取 BuffList 默认值兜底);
|
||||
* 未入表技能回退 SkillSet.info / card.info,整行不高亮。
|
||||
* - 装备卡(card.field 有效)→ 复用 buildEquipRich 逐词条渲染。
|
||||
* - 触发节奏(t_times/t_inv)融入单行描述:定时触发"每M秒召唤K个「技能名」攻击敌人,xxx,共N次"(K=overrides.num+1);即时触发仅显示效果。
|
||||
*
|
||||
* @param card 卡片配置(SCardSet 技能卡 / ICardSet 药品卡 / EquipSet 装备卡)
|
||||
* @returns 多行富文本串;无可描述内容时回退 card.info
|
||||
*/
|
||||
export function buildCardDescRich(card: CardConfig): string {
|
||||
// 装备卡:逐词条渲染驻场光环
|
||||
if (card.field && card.field.length > 0) {
|
||||
return buildEquipRich(card.field);
|
||||
}
|
||||
|
||||
// 药品/技能卡:合并覆写后按 SkillDescSet 模板渲染
|
||||
const base = card.skill ? SkillSet[card.skill] : undefined;
|
||||
if (base) {
|
||||
const skill = mergeSkillParams(base, card.overrides);
|
||||
const line = buildEffectLine("", skill);
|
||||
// 未入表技能回退链:SkillSet.info → card.info
|
||||
if (!line.value && !SkillDescSet[skill.uuid]) {
|
||||
return String(skill.info || card.info || "");
|
||||
}
|
||||
|
||||
// 触发节奏融入描述:定时触发"每M秒释放一次「技能名」,xxx,共N次";即时触发直接描述
|
||||
const tTimes = card.t_times ?? 1;
|
||||
const tInv = card.t_inv ?? 0;
|
||||
const isInstant = card.is_inst || tInv <= 0;
|
||||
// 附加概率后缀(暴击/冰冻/击晕/击退),由实际配置驱动
|
||||
const suffix = buildChanceSuffix(skill);
|
||||
const rich = highlightDuration(renderLineRich(line)) + suffix;
|
||||
|
||||
if (isInstant) {
|
||||
return rich;
|
||||
}
|
||||
|
||||
const rhythm = `每${valueRich(tInv)}秒召唤${valueRich((card.overrides?.num ?? 0) + 1)}个${skillNameRich(skill.name)}攻击敌人`;
|
||||
const times = `, 共${valueRich(tTimes)}次`;
|
||||
return `${rhythm}, ${rich}${times}`;
|
||||
}
|
||||
|
||||
return card.info || "";
|
||||
}
|
||||
|
||||
/* =====================================================================================
|
||||
* 按技能等级分段说明(升级引导 / 图鉴逐档预览)
|
||||
*
|
||||
* 与 buildSkillDesc(当前等级)/ buildSkillDescRich(全档位合并单行)并列的第三种模式:
|
||||
* 每个技能按等级档位逐段输出 —— 技能 Lv1 给完整能力+数值,Lv2 起只给相对上一级的
|
||||
* 「数值漂移 + 新功能」,让玩家一眼看清每一级"多得到了什么"。
|
||||
* ===================================================================================*/
|
||||
|
||||
/** 参与"新功能/数值漂移"对比的展示字段(排除动画/弹道/特效等玩家无感字段) */
|
||||
const DIFF_DISPLAY_KEYS: (keyof SkillOverrides)[] = [
|
||||
"ap", "hit_count", "num", "summon_count", "crt", "frz", "stun", "para",
|
||||
"bck", "bck_chance", "timed_buff_id", "buff_value", "buff_duration",
|
||||
];
|
||||
|
||||
/** 概率/次数类展示字段的单位与称谓(新功能标记 + 数值漂移描述共用) */
|
||||
const DIFF_FIELD_META: Partial<Record<keyof SkillOverrides, { label: string; unit: string }>> = {
|
||||
ap: { label: "伤害", unit: "%" },
|
||||
hit_count: { label: "命中", unit: "次" },
|
||||
num: { label: "双发", unit: "" },
|
||||
summon_count: { label: "召唤", unit: "" },
|
||||
crt: { label: "暴击", unit: "%" },
|
||||
frz: { label: "冰冻", unit: "%" },
|
||||
stun: { label: "击晕", unit: "%" },
|
||||
para: { label: "麻痹", unit: "%" },
|
||||
bck: { label: "击退", unit: "" },
|
||||
bck_chance: { label: "击退概率", unit: "%" },
|
||||
buff_duration: { label: "持续", unit: "秒" },
|
||||
};
|
||||
|
||||
/** 一个技能的等级档位(统一普攻 lv_entries 与触发技能 LvTriggerEntry 两种结构) */
|
||||
interface ISkillTier {
|
||||
/** 技能等级(展示用,取 s_lv,缺省按档位序号) */
|
||||
sLv: number;
|
||||
/** 该档生效的英雄等级(解锁提示) */
|
||||
heroLv: number;
|
||||
/** 合并底座后的完整技能配置 */
|
||||
skill: SkillConfig;
|
||||
}
|
||||
|
||||
/** 收集普攻/主技能的全部等级档位(按 sLv 升序)。Lv1=基础档(底+基础 overrides),Lv2+=lv_entries 各档 */
|
||||
function collectTiers(base: SkillConfig, baseOverrides: SkillOverrides | undefined,
|
||||
entries: { lv: number; s_lv?: number; overrides?: SkillOverrides }[]): ISkillTier[] {
|
||||
const tiers: ISkillTier[] = [];
|
||||
// Lv1 = 基础档(底 + 基础 overrides)
|
||||
const lv1Entry = entries.find(e => (e.s_lv ?? 0) === 1);
|
||||
tiers.push({
|
||||
sLv: lv1Entry?.s_lv ?? 1,
|
||||
heroLv: lv1Entry?.lv ?? 1,
|
||||
skill: mergeSkillParams(base, baseOverrides),
|
||||
});
|
||||
// Lv2+ = lv_entries 中 s_lv≥2 的档(与基础档浅合并,档位优先)
|
||||
for (const e of entries) {
|
||||
const sLv = e.s_lv ?? 0;
|
||||
if (sLv < 2) continue;
|
||||
tiers.push({ sLv, heroLv: e.lv, skill: mergeSkillParams(mergeSkillParams(base, baseOverrides), e.overrides) });
|
||||
}
|
||||
return tiers.sort((a, b) => a.sLv - b.sLv);
|
||||
}
|
||||
|
||||
/** 相邻两档的 diff:数值漂移(ap 等差值)+ 新功能(上级没有的 timed_buff/暴击/冰冻等) */
|
||||
function buildTierDiff(prev: SkillConfig, cur: SkillConfig): string[] {
|
||||
const parts: string[] = [];
|
||||
for (const key of DIFF_DISPLAY_KEYS) {
|
||||
const pv = (prev[key as keyof SkillConfig] as number | undefined) ?? 0;
|
||||
const cv = (cur[key as keyof SkillConfig] as number | undefined) ?? 0;
|
||||
if (cv === pv) continue;
|
||||
// 新功能:上级为 0/无,本档新增(含 timed_buff_id 新注入)
|
||||
if (key === "timed_buff_id") {
|
||||
if (cv > 0 && pv === 0) parts.push(`新增${valueRich("计时增益")}`);
|
||||
continue;
|
||||
}
|
||||
const meta = DIFF_FIELD_META[key];
|
||||
if (!meta) continue;
|
||||
if (pv === 0 && cv > 0) {
|
||||
parts.push(`新增${meta.label}${valueRich(`${cv}${meta.unit}`)}`);
|
||||
} else {
|
||||
const diff = cv - pv;
|
||||
const sign = diff > 0 ? "+" : "";
|
||||
parts.push(`${meta.label}${valueRich(`${sign}${diff}${meta.unit}`)}`);
|
||||
}
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
/** 单个技能的分段说明行(Lv1 完整 / Lv2+ diff),返回富文本行数组 */
|
||||
function buildTierLines(name: string, tiers: ISkillTier[], headPrefix: string): string[] {
|
||||
const lines: string[] = [];
|
||||
for (let i = 0; i < tiers.length; i++) {
|
||||
const t = tiers[i];
|
||||
const head = `${skillNameRich(name)} ${valueRich(`Lv.${t.sLv}`)}${headPrefix}`;
|
||||
if (i === 0) {
|
||||
// 技能 1 级:完整能力 + 数值
|
||||
const line = buildEffectLine("", t.skill);
|
||||
const effect = `${line.prefix}${line.value ? valueRich(line.value) : ""}${line.suffix}`;
|
||||
lines.push(`${head}: ${highlightDuration(effect)}`);
|
||||
} else {
|
||||
// 技能 2 级起:相对上一级的数值漂移 + 新功能
|
||||
const diff = buildTierDiff(tiers[i - 1].skill, t.skill);
|
||||
const unlock = t.heroLv > 1 ? `(英雄${valueRich(t.heroLv)}级解锁)` : "";
|
||||
lines.push(diff.length > 0
|
||||
? `${head}${unlock}: ${diff.join(",")}`
|
||||
: `${head}${unlock}: 无变化`);
|
||||
}
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按技能等级分段输出说明(富文本)。
|
||||
*
|
||||
* 两种对外说明模式的第二种(第一种为 buildSkillDesc/buildSkillDescMoreRich 的"当前等级"模式):
|
||||
* 每个技能逐档输出 —— Lv1 给完整能力与数值,Lv2 起给相对上一级的提升(数值漂移 + 新功能),
|
||||
* 并标注解锁所需英雄等级。适合升级引导、图鉴逐档预览。
|
||||
*
|
||||
* @param source 英雄数据源(heroInfo 静态配置 或 HeroAttrsComp 运行时配置)
|
||||
* @returns 多行富文本串,\n 分隔;按技能分组、组内按技能等级升序
|
||||
*/
|
||||
export function buildSkillTierDescRich(source: ISkillDescSource): string {
|
||||
const lines: string[] = [];
|
||||
|
||||
// ---- 普攻/主技能(skills):lv_entries 分档 ----
|
||||
if (source.skills) {
|
||||
for (const key in source.skills) {
|
||||
const hSkill = source.skills[key];
|
||||
if (!hSkill) continue;
|
||||
const base = SkillSet[hSkill.uuid];
|
||||
if (!base) continue;
|
||||
const entries = hSkill.lv_entries ?? [];
|
||||
// 无分档的单档技能只出 Lv1 完整行
|
||||
const tiers = collectTiers(base, hSkill.overrides, entries);
|
||||
lines.push(...buildTierLines(hSkill.name ?? base.name, tiers, ""));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 触发技能(atking/atked/dead/fstart/call):LvTriggerEntry 分档 ----
|
||||
for (const key of TRIGGER_KEYS) {
|
||||
const grouped = source[key] as TriggerGrouped | undefined;
|
||||
if (!grouped) continue;
|
||||
const trigName = SkillTriggerName[key] ?? key;
|
||||
const needCount = key === SkillTriggerType.Atking || key === SkillTriggerType.Atked;
|
||||
for (const sKey in grouped) {
|
||||
const entries = grouped[sKey];
|
||||
if (!entries?.length) continue;
|
||||
const base = SkillSet[Number(sKey)];
|
||||
if (!base) continue;
|
||||
const sorted = [...entries].sort((a, b) => (a.s_lv ?? 0) - (b.s_lv ?? 0));
|
||||
const name = getGroupedTriggerName(grouped, Number(sKey));
|
||||
// 触发技能以第 1 档为基础档,逐档 diff(t_num 变化并入新功能/数值漂移)
|
||||
const tiers: ISkillTier[] = sorted.map((e, i) => ({
|
||||
sLv: e.s_lv ?? (i + 1),
|
||||
heroLv: e.lv,
|
||||
skill: mergeSkillParams(base, e.overrides),
|
||||
}));
|
||||
const headPrefix = needCount && sorted[0] ? ` ${trigName}${valueRich(sorted[0].t_num)}次` : ` ${trigName}`;
|
||||
lines.push(...buildTierLines(name, tiers, headPrefix));
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
* @description 技能描述结构化配置表(方案 B:独立描述表,纯 UI 用)
|
||||
*
|
||||
* 职责:
|
||||
* 为触发技能提供"结构化字段 + 占位符模板",供 HeroSkillDesc.buildSkillDesc
|
||||
* 为触发技能提供"结构化字段 + 占位符模板",供 CardSkillText.buildCardDescRich
|
||||
* 在合并 overrides 后动态拼出效果文案。杜绝 SkillConfig.info 写死数值导致的
|
||||
* "改数值忘改文案"漂移。
|
||||
*
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { v3 } from "cc"
|
||||
import { v3 } from "cc"
|
||||
import { BoxSet, FacSet, FightSet } from "./GameSet"
|
||||
import { SkillOverrides, TGroup, SkillSet, FieldSkillSet, SkillConfig, SkillKind, FieldSkillConfig, FieldSkillType, mergeSkillParams } from "./SkillSet"
|
||||
import { BuffList, BuffConfig, ModOp } from "./BuffSet"
|
||||
@@ -286,7 +286,7 @@ export interface heroInfo {
|
||||
hp_bonus?: LvBonusEntry[]; // 额外生命加成(按 lv 升序,运行时累加 ≤英雄等级 的所有档)
|
||||
info: string; // 描述文案
|
||||
/**
|
||||
* @deprecated 已废弃:无任何 UI/运行时读取。技能提升说明由 HeroSkillDesc 依据
|
||||
* @deprecated 已废弃:无任何 UI/运行时读取。技能提升说明由 HeroSkillText 依据
|
||||
* 结构化配置动态生成(含数值漂移与新功能),请勿再填充此字段。
|
||||
*/
|
||||
infos?: string[];
|
||||
@@ -380,6 +380,8 @@ export interface FieldEntry {
|
||||
export interface LvFieldEntry {
|
||||
lv: number;
|
||||
uuids: FieldEntry[];
|
||||
/** 档位描述(可选,缺省由 HeroSkillText 按 FieldDescSet 自动生成) */
|
||||
desc?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -390,6 +392,8 @@ export interface LvReviveEntry {
|
||||
s_uuid: number;
|
||||
r_num: number;
|
||||
upr: number;
|
||||
/** 档位描述(可选,缺省由 HeroSkillText 自动生成) */
|
||||
desc?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -403,7 +407,7 @@ export interface LvBonusEntry {
|
||||
*=============英雄配置列表================
|
||||
* 几何王国复刻版 — 依据 英雄设计指引.md(A/B 级复刻英雄)配置
|
||||
*
|
||||
* 规则:lv2 解锁触发技能(atking/atked/fstart),lv3/5/7 大招走 lv_entries 分档成长,
|
||||
* 规则:lv3 解锁 atking/atked 触发技能,lv2 解锁 fstart,lv6/12 大招走 lv_entries 分档成长,
|
||||
* lv9 card_lv 4/5 配 field 光环,card_lv 3 配 ap_bonus/hp_bonus 基础属性+50%
|
||||
* 编号:本批复刻英雄集中于 5001~5009,card_lv 按配置(不遵循段位规则)
|
||||
*/
|
||||
@@ -422,17 +426,16 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
6203: {
|
||||
uuid: 6203, name: "能量波", lv: 1, cd: 8, ccd: 0,
|
||||
overrides: { ap: 250, bck: 30, para: 40, summon_count: 1 },
|
||||
desc: "释放{summon_count}道能量波,对{target}造成攻击力{ap}%伤害,击退{bck},{para}%概率麻痹",
|
||||
desc: "释放能量波,对{target}造成攻击力{ap}%伤害,击退{bck},{para}%概率麻痹",
|
||||
lv_entries: [
|
||||
{ lv: 3, s_lv: SkillLv.Lv2, overrides: { ap: 250, bck: 60, para: 40, summon_count: 1 } },
|
||||
{ lv: 5, s_lv: SkillLv.Lv3, overrides: { ap: 300, bck: 90, timed_buff_id: 7005, para: 50, summon_count: 2 }, desc: "释放{summon_count}道能量波,对{target}造成攻击力{ap}%伤害,击退{bck},{para}%概率麻痹,并使目标中毒(每0.5秒受到{buff_value}点毒伤,持续{dur}秒)" },
|
||||
{ lv: 7, s_lv: SkillLv.Lv4, overrides: { ap: 400, timed_buff_id: 7025, para: 70, summon_count: 3 }, desc: "释放{summon_count}道能量波,对{target}造成攻击力{ap}%伤害,击退{bck},{para}%概率麻痹,并使目标麻痹{dur}秒(技能CD减速50%)" },
|
||||
{ lv: 6, s_lv: SkillLv.Lv2, overrides: { ap: 250, bck: 60, para: 40, summon_count: 1 } },
|
||||
{ lv: 12, s_lv: SkillLv.Lv3, overrides: { ap: 300, bck: 90, timed_buff_id: 7005, para: 50, summon_count: 2 }, desc: "释放能量波,对{target}造成攻击力{ap}%伤害,击退{bck},{para}%概率麻痹,额外释放1次,并使目标中毒(每0.5秒受到{buff_value}点毒伤,持续{dur}秒)" },
|
||||
],
|
||||
},
|
||||
},
|
||||
atking: {
|
||||
6010: [
|
||||
{ lv: 2, s_lv: SkillLv.Lv1, name: "雷霆万钧", t_num: 3, overrides: { para: 100 }, desc: "释放紫魔球轰击{target},{para}%概率麻痹目标" },
|
||||
{ lv: 3, s_lv: SkillLv.Lv1, name: "雷霆万钧", t_num: 3, overrides: { para: 100 }, desc: "释放紫魔球轰击{target},{para}%概率麻痹目标" },
|
||||
],
|
||||
},
|
||||
field: [
|
||||
@@ -451,15 +454,14 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
overrides: { ap: 150 },
|
||||
desc: "斩出光闪,对{target}造成攻击力{ap}%伤害",
|
||||
lv_entries: [
|
||||
{ lv: 3, s_lv: SkillLv.Lv2, overrides: { ap: 150, timed_buff_id: 7007 }, desc: "斩出光闪,对{target}造成攻击力{ap}%伤害,并使目标出血(每0.5秒受到{buff_value}点伤害,持续{dur}秒)" },
|
||||
{ lv: 5, s_lv: SkillLv.Lv3, overrides: { ap: 180, hit_count: 8, num: 1 }, desc: "斩出光闪,对{target}连击{hit_count}次,每次造成攻击力{ap}%伤害,并双发{num}次" },
|
||||
{ lv: 7, s_lv: SkillLv.Lv4, overrides: { ap: 360, bck: 50 }, desc: "斩出光闪,对{target}造成攻击力{ap}%伤害,并击退{bck}" },
|
||||
{ lv: 6, s_lv: SkillLv.Lv2, overrides: { ap: 150, timed_buff_id: 7007 }, desc: "斩出光闪,对{target}造成攻击力{ap}%伤害,并使目标出血(每0.5秒受到{buff_value}点伤害,持续{dur}秒)" },
|
||||
{ lv: 12, s_lv: SkillLv.Lv3, overrides: { ap: 180, hit_count: 8, num: 1 }, desc: "斩出光闪,对{target}连击{hit_count}次,每次造成攻击力{ap}%伤害,并双发{num}次" },
|
||||
],
|
||||
},
|
||||
},
|
||||
atking: {
|
||||
6004: [
|
||||
{ lv: 2, s_lv: SkillLv.Lv1, name: "致命节奏", t_num: 3, overrides: { crt: 30 }, desc: "释放斩击,{crt}%概率造成暴击" },
|
||||
{ lv: 3, s_lv: SkillLv.Lv1, name: "致命节奏", t_num: 3, overrides: { crt: 30 }, desc: "释放斩击,{crt}%概率造成暴击" },
|
||||
],
|
||||
},
|
||||
field: [
|
||||
@@ -478,15 +480,14 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
overrides: { ap: 150, para: 40, summon_count: 1 },
|
||||
desc: "召唤落雷斩击{target},造成攻击力{ap}%伤害,{para}%概率麻痹",
|
||||
lv_entries: [
|
||||
{ lv: 3, s_lv: SkillLv.Lv2, overrides: { ap: 150, timed_buff_id: 7006, para: 40, summon_count: 1 }, desc: "召唤落雷斩击{target},造成攻击力{ap}%伤害,{para}%概率麻痹,并使目标攻击力{buff_value}%(持续{dur}秒)" },
|
||||
{ lv: 5, s_lv: SkillLv.Lv3, overrides: { ap: 180, timed_buff_id: 7006, buff_value: -15, para: 50, summon_count: 2 } },
|
||||
{ lv: 7, s_lv: SkillLv.Lv4, overrides: { ap: 200, hit_count: 6, bck: 40, para: 60, summon_count: 3 }, desc: "召唤落雷斩击{target},造成攻击力{ap}%伤害(命中{hit_count}次),{para}%概率麻痹,击退{bck}" },
|
||||
{ lv: 6, s_lv: SkillLv.Lv2, overrides: { ap: 150, timed_buff_id: 7006, para: 40, summon_count: 1 }, desc: "召唤落雷斩击{target},造成攻击力{ap}%伤害,{para}%概率麻痹,并使目标攻击力{buff_value}%(持续{dur}秒)" },
|
||||
{ lv: 12, s_lv: SkillLv.Lv3, overrides: { ap: 180, timed_buff_id: 7006, buff_value: -15, para: 50, summon_count: 2 } },
|
||||
],
|
||||
},
|
||||
},
|
||||
atked: {
|
||||
6301: [
|
||||
{ lv: 5, s_lv: SkillLv.Lv1, name: "罗生门", t_num: 4, overrides: { TGroup: TGroup.Self, ap: 2 }, desc: "{target}获得{ap}次格挡护盾" },
|
||||
{ lv: 3, s_lv: SkillLv.Lv1, name: "罗生门", t_num: 4, overrides: { TGroup: TGroup.Self, ap: 2 }, desc: "{target}获得{ap}次格挡护盾" },
|
||||
],
|
||||
},
|
||||
fstart: {
|
||||
@@ -510,15 +511,14 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
overrides: { ap: 150, summon_count: 1 },
|
||||
desc: "释放能量波,对{target}造成攻击力{ap}%伤害",
|
||||
lv_entries: [
|
||||
{ lv: 3, s_lv: SkillLv.Lv2, overrides: { ap: 150, frz: 15, summon_count: 1 }, desc: "释放能量波,对{target}造成攻击力{ap}%伤害,{frz}%概率冰冻" },
|
||||
{ lv: 5, s_lv: SkillLv.Lv3, overrides: { ap: 180, frz: 30, hit_count: 8, summon_count: 2 }, desc: "释放能量波,对{target}造成攻击力{ap}%伤害,{frz}%概率冰冻,命中{hit_count}次" },
|
||||
{ lv: 7, s_lv: SkillLv.Lv4, overrides: { ap: 200, timed_buff_id: 7003, summon_count: 3 }, desc: "释放{summon_count}道能量波,对{target}造成攻击力{ap}%伤害,并冰冻目标{dur}秒" },
|
||||
{ lv: 6, s_lv: SkillLv.Lv2, overrides: { ap: 150, frz: 15, summon_count: 1 }, desc: "释放能量波,对{target}造成攻击力{ap}%伤害,{frz}%概率冰冻" },
|
||||
{ lv: 12, s_lv: SkillLv.Lv3, overrides: { ap: 180, frz: 30, hit_count: 8, summon_count: 2 }, desc: "释放能量波,对{target}造成攻击力{ap}%伤害,{frz}%概率冰冻,命中{hit_count}次" },
|
||||
],
|
||||
},
|
||||
},
|
||||
atking: {
|
||||
6009: [
|
||||
{ lv: 2, s_lv: SkillLv.Lv1, name: "霜寒之触", t_num: 3, overrides: { timed_buff_id: 7003 }, desc: "释放寒气,冰冻{target}{dur}秒" },
|
||||
{ lv: 3, s_lv: SkillLv.Lv1, name: "霜寒之触", t_num: 3, overrides: { timed_buff_id: 7003 }, desc: "释放寒气,冰冻{target}{dur}秒" },
|
||||
],
|
||||
},
|
||||
field: [
|
||||
@@ -537,15 +537,14 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
overrides: { TGroup: TGroup.Self, ap: 3, summon_count: 1 },
|
||||
desc: "{target}获得{ap}次格挡护盾",
|
||||
lv_entries: [
|
||||
{ lv: 3, s_lv: SkillLv.Lv2, overrides: { TGroup: TGroup.Self, ap: 5, summon_count: 1 } },
|
||||
{ lv: 5, s_lv: SkillLv.Lv3, overrides: { TGroup: TGroup.Self, ap: 6, summon_count: 2 } },
|
||||
{ lv: 7, s_lv: SkillLv.Lv4, overrides: { TGroup: TGroup.Self, ap: 8, summon_count: 3 } },
|
||||
{ lv: 6, s_lv: SkillLv.Lv2, overrides: { TGroup: TGroup.Self, ap: 5, summon_count: 1 } },
|
||||
{ lv: 12, s_lv: SkillLv.Lv3, overrides: { TGroup: TGroup.Self, ap: 6, summon_count: 2 } },
|
||||
],
|
||||
},
|
||||
},
|
||||
atked: {
|
||||
6004: [
|
||||
{ lv: 5, s_lv: SkillLv.Lv1, name: "反击减攻", t_num: 4, overrides: { timed_buff_id: 7006 }, desc: "反击使攻击者攻击力{buff_value}%,持续{dur}秒" },
|
||||
{ lv: 3, s_lv: SkillLv.Lv1, name: "反击减攻", t_num: 4, overrides: { timed_buff_id: 7006 }, desc: "反击使攻击者攻击力{buff_value}%,持续{dur}秒" },
|
||||
],
|
||||
},
|
||||
fstart: {
|
||||
@@ -578,15 +577,14 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
overrides: { TGroup: TGroup.Team, ap: 300, summon_count: 1 },
|
||||
desc: "为{target}回复攻击力{ap}%生命",
|
||||
lv_entries: [
|
||||
{ lv: 3, s_lv: SkillLv.Lv2, overrides: { TGroup: TGroup.Team, ap: 400, summon_count: 1 } },
|
||||
{ lv: 5, s_lv: SkillLv.Lv3, overrides: { TGroup: TGroup.Team, ap: 450, summon_count: 2 } },
|
||||
{ lv: 7, s_lv: SkillLv.Lv4, overrides: { TGroup: TGroup.Team, ap: 450, timed_buff_id: 7010, summon_count: 3 }, desc: "为{target}回复攻击力{ap}%生命,并附加再生(每0.5秒回复{buff_value}点生命,持续{dur}秒)" },
|
||||
{ lv: 6, s_lv: SkillLv.Lv2, overrides: { TGroup: TGroup.Team, ap: 400, summon_count: 1 } },
|
||||
{ lv: 12, s_lv: SkillLv.Lv3, overrides: { TGroup: TGroup.Team, ap: 450, summon_count: 2 } },
|
||||
],
|
||||
},
|
||||
},
|
||||
atking: {
|
||||
6302: [
|
||||
{ lv: 5, s_lv: SkillLv.Lv1, name: "圣洁之锤", t_num: 3, overrides: { TGroup: TGroup.Self, ap: 100 }, desc: "释放圣光,为{target}回复攻击力{ap}%生命" },
|
||||
{ lv: 3, s_lv: SkillLv.Lv1, name: "圣洁之锤", t_num: 3, overrides: { TGroup: TGroup.Self, ap: 100 }, desc: "释放圣光,为{target}回复攻击力{ap}%生命" },
|
||||
],
|
||||
},
|
||||
fstart: {
|
||||
@@ -610,15 +608,14 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
overrides: { ap: 150, summon_count: 1 },
|
||||
desc: "斩出气刃,对{target}造成攻击力{ap}%伤害",
|
||||
lv_entries: [
|
||||
{ lv: 3, s_lv: SkillLv.Lv2, overrides: { ap: 150, timed_buff_id: 7002, summon_count: 1 }, desc: "斩出气刃,对{target}造成攻击力{ap}%伤害,并使目标易伤(受到伤害加深,持续{dur}秒)" },
|
||||
{ lv: 5, s_lv: SkillLv.Lv3, overrides: { ap: 180, bck: 60, timed_buff_id: 7002, buff_value: 20, summon_count: 2 }, desc: "斩出气刃,对{target}造成攻击力{ap}%伤害,击退{bck},并使目标易伤(持续{dur}秒)" },
|
||||
{ lv: 7, s_lv: SkillLv.Lv4, overrides: { ap: 220, bck: 80, summon_count: 3 }, desc: "斩出气刃,对{target}造成攻击力{ap}%伤害,击退{bck}" },
|
||||
{ lv: 6, s_lv: SkillLv.Lv2, overrides: { ap: 150, timed_buff_id: 7002, summon_count: 1 }, desc: "斩出气刃,对{target}造成攻击力{ap}%伤害,并使目标易伤(受到伤害加深,持续{dur}秒)" },
|
||||
{ lv: 12, s_lv: SkillLv.Lv3, overrides: { ap: 180, bck: 60, timed_buff_id: 7002, buff_value: 20, summon_count: 2 }, desc: "斩出气刃,对{target}造成攻击力{ap}%伤害,击退{bck},并使目标易伤(持续{dur}秒)" },
|
||||
],
|
||||
},
|
||||
},
|
||||
atking: {
|
||||
6004: [
|
||||
{ lv: 2, s_lv: SkillLv.Lv1, name: "雷霆一击", t_num: 3, overrides: { bck: 40 }, desc: "重击{target},击退{bck}" },
|
||||
{ lv: 3, s_lv: SkillLv.Lv1, name: "雷霆一击", t_num: 3, overrides: { bck: 40 }, desc: "重击{target},击退{bck}" },
|
||||
],
|
||||
},
|
||||
// card_lv 3:lv9 无光环,改为基础属性+50%
|
||||
@@ -637,15 +634,14 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
overrides: { ap: 300, summon_count: 1 },
|
||||
desc: "劈出开山斧,对{target}造成攻击力{ap}%伤害",
|
||||
lv_entries: [
|
||||
{ lv: 3, s_lv: SkillLv.Lv2, overrides: { ap: 300, stun: 20, summon_count: 1 }, desc: "劈出开山斧,对{target}造成攻击力{ap}%伤害,{stun}%概率击晕" },
|
||||
{ lv: 5, s_lv: SkillLv.Lv3, overrides: { ap: 350, stun: 35, hit_count: 8, summon_count: 2 }, desc: "劈出开山斧,对{target}造成攻击力{ap}%伤害,{stun}%概率击晕,命中{hit_count}次" },
|
||||
{ lv: 7, s_lv: SkillLv.Lv4, overrides: { ap: 400, timed_buff_id: 7004, summon_count: 3 }, desc: "劈出开山斧,对{target}造成攻击力{ap}%伤害,并击晕目标{dur}秒" },
|
||||
{ lv: 6, s_lv: SkillLv.Lv2, overrides: { ap: 300, stun: 20, summon_count: 1 }, desc: "劈出开山斧,对{target}造成攻击力{ap}%伤害,{stun}%概率击晕" },
|
||||
{ lv: 12, s_lv: SkillLv.Lv3, overrides: { ap: 350, stun: 35, hit_count: 8, summon_count: 2 }, desc: "劈出开山斧,对{target}造成攻击力{ap}%伤害,{stun}%概率击晕,命中{hit_count}次" },
|
||||
],
|
||||
},
|
||||
},
|
||||
atking: {
|
||||
6004: [
|
||||
{ lv: 2, s_lv: SkillLv.Lv1, name: "火眼金睛", t_num: 3, overrides: { crt: 25 }, desc: "释放斩击,{crt}%概率造成暴击" },
|
||||
{ lv: 3, s_lv: SkillLv.Lv1, name: "火眼金睛", t_num: 3, overrides: { crt: 25 }, desc: "释放斩击,{crt}%概率造成暴击" },
|
||||
],
|
||||
},
|
||||
field: [
|
||||
@@ -664,21 +660,20 @@ export const HeroInfo: Record<number, heroInfo> = {
|
||||
overrides: { ap: 150, summon_count: 1 },
|
||||
desc: "斩出气刃,对{target}造成攻击力{ap}%伤害",
|
||||
lv_entries: [
|
||||
{ lv: 3, s_lv: SkillLv.Lv2, overrides: { ap: 180, summon_count: 1 } },
|
||||
{ lv: 5, s_lv: SkillLv.Lv3, overrides: { ap: 200, summon_count: 2 } },
|
||||
{ lv: 7, s_lv: SkillLv.Lv4, overrides: { ap: 220, bck: 40, summon_count: 3 }, desc: "斩出气刃,对{target}造成攻击力{ap}%伤害,击退{bck}" },
|
||||
{ lv: 6, s_lv: SkillLv.Lv2, overrides: { ap: 180, summon_count: 1 } },
|
||||
{ lv: 12, s_lv: SkillLv.Lv3, overrides: { ap: 200, summon_count: 2 } },
|
||||
],
|
||||
},
|
||||
},
|
||||
atking: {
|
||||
6302: [
|
||||
{ lv: 5, s_lv: SkillLv.Lv1, name: "嗜血战斧", t_num: 4, overrides: { TGroup: TGroup.Self, ap: 120 }, desc: "释放嗜血斩击,为{target}回复攻击力{ap}%生命" },
|
||||
{ lv: 3, s_lv: SkillLv.Lv1, name: "嗜血战斧", t_num: 4, overrides: { TGroup: TGroup.Self, ap: 120 }, desc: "释放嗜血斩击,为{target}回复攻击力{ap}%生命" },
|
||||
],
|
||||
},
|
||||
atked: {
|
||||
// 坚硬皮肤:借 6513 防御爆发底座注入 7023 防御强化(+50%,可无限叠层)
|
||||
6513: [
|
||||
{ lv: 2, s_lv: SkillLv.Lv1, name: "坚硬皮肤", t_num: 2, overrides: { TGroup: TGroup.Self, timed_buff_id: 7023, buff_duration: 10 }, desc: "{target}防御提升{buff_value}%,持续{dur}秒" },
|
||||
{ lv: 3, s_lv: SkillLv.Lv1, name: "坚硬皮肤", t_num: 2, overrides: { TGroup: TGroup.Self, timed_buff_id: 7023, buff_duration: 10 }, desc: "{target}防御提升{buff_value}%,持续{dur}秒" },
|
||||
],
|
||||
},
|
||||
// card_lv 3:lv9 无光环,改为基础属性+50%
|
||||
|
||||
Reference in New Issue
Block a user