feat(map,hero): 实现英雄技能整组合并展示功能

重构英雄技能展示逻辑,支持将同一触发槽位的多个技能/光环词条合并为一个技能展示:
1. 新增触发组整组分档渲染接口,合并同槽位多技能档位数据
2. 新增组名获取函数,统一使用槽位首个自定义名称作为展示名
3. 调整图鉴面板技能列表,每个触发槽位仅展示一个合并后的图标
4. 升级技能弹窗渲染逻辑,支持整组模式下的合并说明展示
5. 优化注释与代码结构,统一技能展示口径
This commit is contained in:
pan
2026-09-04 14:15:45 +08:00
parent 4e0ef5bccc
commit db26865e03
5 changed files with 319 additions and 107 deletions

View File

@@ -14,6 +14,11 @@
* 2. buildHeroSkillTierDesc(heroUuid, skillUuid) —— 指定技能各阶段说明:
* slv1 给完整能力 + 激活英雄等级;slv2+ 给相对上一级的提升 + 解锁英雄等级
*
* 触发组口径:每个触发槽位(atking/dead/fstart/atked/call/field)整组视为一个技能,
* 组内多个 s_uuid / 光环词条合并为一个名称(组内首个自定义名)与一套说明统一展示:
* - buildHeroSkillDesc 中每组合并为一行(CHeroComp / HeroBoxComp 等直接消费)
* - buildHeroGroupTierLines(heroUuid, slot) 供弹窗按整组分档渲染(SinfoComp)
*
* 占位符规范(模板作者可用,渲染时数值片段自动绿色高亮):
* {target} 目标语义(自身/敌方/全体友方/N名队友…,与 SCastSystem 目标选取一致)
* {dur} 计时 buff 持续秒数(buff_duration 优先,回退 BuffList 默认)
@@ -24,9 +29,9 @@
* 卡牌/道具语境描述见 config/CardSkillText.ts。
*/
import {
HeroInfo, heroInfo, SkillTriggerType, SkillTriggerName, HSkillInfo, LvTriggerEntry,
HeroInfo, heroInfo, SkillTriggerType, SkillTriggerName, HSkillInfo, LvTriggerEntry, FieldEntry,
TriggerGrouped, resolveSkillOverrides, resolveTriggerByLv, resolveFieldByLv,
getGroupedTriggerName, getFieldSkillName,
getGroupedTriggerName, getFieldSkillName, getTriggerGroupName, getFieldGroupName,
} from "./config/heroSet";
import { SkillSet, mergeSkillParams, SkillConfig, SkillKind, TGroup, FieldSkillSet } from "./config/SkillSet";
import { BuffList } from "./config/BuffSet";
@@ -145,8 +150,9 @@ function getHeroDisplayLv(heroUuid: number, configLv: number, fac: FacSet): numb
* 输出一:英雄当前局外等级下,全部技能最大生效档的完整说明(富文本)。
*
* 覆盖:普攻/大招(skills,resolve ≤lv 最大档)、5 类触发技能、驻场光环(field)、
* 死亡触发(dead,含复活)。每行 = 「技能名」:(或「技能名」+ 触发语境)+ 模板渲染后的效果文本(数值高亮),
* 不展示技能等级档号(当前档由英雄局外等级隐含决定)。
* 死亡触发(dead,含复活)。触发槽位整组视为一个技能(组内多技能/词条合并为一行,
* 组名取组内首个自定义名),普攻/大招仍逐技能一行。每行 = 「技能名」:(或「技能名」+
* 触发语境)+ 模板渲染后的效果文本(数值高亮),不展示技能等级档号(当前档由英雄局外等级隐含决定)。
*
* Why 普攻默认隐藏:skills 首位为普攻(全英雄共有基础能力),展示场景通常只关心
* 特色技能(大招/触发/光环),普攻行信息冗余;需要完整展示时传 showNormal=true。
@@ -184,11 +190,16 @@ export function buildHeroSkillDesc(heroUuid: number, showName: boolean = true, s
lines.push(head ? `${head}: ${text}` : text);
}
// ---- 5 类标准触发技能:resolve 当前等级生效档 ----
// ---- 5 类触发技能:每个触发槽位整组视为一个技能,组内多技能效果合并为一行 ----
for (const type of TRIGGER_KEYS) {
const grouped = hero[type] as TriggerGrouped | undefined;
if (!grouped) continue;
for (const item of resolveTriggerByLv(grouped, lv)) {
const items = resolveTriggerByLv(grouped, lv);
if (items.length === 0) continue;
// 组内多个技能时,各效果片段带自身技能名区分
const headOf = (s_uuid: number) => (items.length > 1 ? skillNameRich(getGroupedTriggerName(grouped, s_uuid)) : "");
const parts: string[] = [];
for (const item of items) {
const base = SkillSet[item.s_uuid];
if (!base) continue;
const skill = mergeSkillParams(base, item.overrides);
@@ -204,31 +215,40 @@ export function buildHeroSkillDesc(heroUuid: number, showName: boolean = true, s
const trig = type === SkillTriggerType.Atking || type === SkillTriggerType.Atked
? `${SkillTriggerName[type]}${valueRich(item.t_num)}次`
: SkillTriggerName[type];
lines.push(`${nameSeg(getGroupedTriggerName(grouped, item.s_uuid))}${trig}, ${text}`);
parts.push(`${headOf(item.s_uuid)}${trig}, ${text}`);
}
// dead 组内的复活效果(revive_hp / revive_count)并入同一行
if (type === SkillTriggerType.Dead) {
for (const item of items) {
const reviveHp = item.overrides?.revive_hp;
const reviveCount = item.overrides?.revive_count ?? 0;
if (reviveHp === undefined || reviveCount <= 0) continue;
const text = `每回合可复活${valueRich(reviveCount)}次, 死亡后以${valueRich(`${Math.round(reviveHp)}%`)}生命原地重生`;
parts.push(`${headOf(item.s_uuid)}${SkillTriggerName[SkillTriggerType.Dead]}, ${text}`);
}
}
if (parts.length === 0) continue;
// 组名取组内首个自定义名(getTriggerGroupName)
lines.push(`${nameSeg(getTriggerGroupName(hero, type))}${parts.join(";")}`);
}
// ---- 驻场光环(field):查 FieldDescSet 按 type 渲染,数值实时高亮 ----
for (const entry of resolveFieldByLv(hero.field, lv)) {
// ---- 驻场光环(field):整组视为一个技能,组内多词条合并为一行 ----
const fieldItems = resolveFieldByLv(hero.field, lv);
const fieldParts: string[] = [];
for (const entry of fieldItems) {
const fs = FieldSkillSet[entry.uuid];
const desc = fs ? FieldDescSet[fs.type] : undefined;
if (!fs || !desc) continue;
const val = desc.fmt === FieldValueFmt.Percent
? `+${Math.round(entry.value * 100)}%`
: desc.fmt === FieldValueFmt.IntMinus ? `-${entry.value}` : `+${entry.value}`;
lines.push(`${nameSeg(getFieldSkillName(hero, entry.uuid))}${desc.verb}${valueRich(val)}`);
// 组内多个词条时带各自词条名区分
const head = fieldItems.length > 1 ? skillNameRich(entry.name ?? fs.name) : "";
fieldParts.push(`${head}${desc.verb}${valueRich(val)}`);
}
// ---- 复活(已纳入 dead 触发组):从 dead 配置提取 revive_hp / revive_count ----
for (const item of resolveTriggerByLv(hero.dead, lv)) {
const reviveHp = item.overrides?.revive_hp;
const reviveCount = item.overrides?.revive_count ?? 0;
if (reviveHp === undefined || reviveCount <= 0) continue;
const base = SkillSet[item.s_uuid];
if (!base) continue;
const skill = mergeSkillParams(base, item.overrides);
const text = `每回合可复活${valueRich(reviveCount)}次, 死亡后以${valueRich(`${Math.round(reviveHp)}%`)}生命原地重生`;
lines.push(`${nameSeg(getGroupedTriggerName(hero.dead!, item.s_uuid))}${SkillTriggerName[SkillTriggerType.Dead]}, ${text}`);
if (fieldParts.length > 0) {
// 组名取组内首个自定义名(getFieldGroupName)
lines.push(`${nameSeg(getFieldGroupName(hero))}${fieldParts.join(",")}`);
}
return lines.join("\n");
@@ -552,6 +572,104 @@ export function buildHeroSkillTierLines(heroUuid: number, skillUuid: number, slo
return buildTierLines(tiers, triggerLabel);
}
/**
* 驻场光环整组分档行(hero.field 全部词条合并展示)。
* 每档一行:新词条给完整效果(动词 + 数值),已有词条给数值提升;
* 组内仅 1 个词条时与单词条口径一致。档位配置了 desc(LvFieldEntry.desc)时优先采用。
*/
function collectFieldGroupTierLines(hero: heroInfo): HeroSkillTierLine[] {
const tiers = [...(hero.field ?? [])].sort((a, b) => a.lv - b.lv);
if (tiers.length === 0) return [];
// 组内词条数 >1 时每行带词条名区分
const uuidSet = new Set<number>();
for (const t of tiers) {
for (const u of t.uuids) uuidSet.add(u.uuid);
}
const multi = uuidSet.size > 1;
const lines: HeroSkillTierLine[] = [];
let prev: FieldEntry[] = [];
tiers.forEach((e, i) => {
const parts: string[] = [];
for (const entry of e.uuids) {
const fs = FieldSkillSet[entry.uuid];
const descCfg = fs ? FieldDescSet[fs.type] : undefined;
if (!fs || !descCfg) continue;
const head = multi ? `${entry.name ?? fs.name}: ` : "";
const before = prev.find(p => p.uuid === entry.uuid);
if (!before) {
// 新词条:完整效果(动词 + 数值高亮)
parts.push(`${head}${descCfg.verb}${buildFieldValueRich(entry.uuid, entry.value)}`);
} else if (before.value !== entry.value) {
// 已有词条:数值由旧值提升至新值
parts.push(`${head}${descCfg.verb}${buildFieldValueRich(entry.uuid, before.value)}提升至${buildFieldValueRich(entry.uuid, entry.value)}`);
}
}
lines.push({ sLv: i + 1, heroLv: e.lv, text: e.desc ?? parts.join("\n") });
prev = e.uuids;
});
return lines;
}
/** 多技能档位行按解锁英雄等级合并:同阈值档位文本换行拼接,sLv 取该阈值下最大档 */
function mergeTierLinesByHeroLv(groups: HeroSkillTierLine[][]): HeroSkillTierLine[] {
if (groups.length === 0) return [];
if (groups.length === 1) return groups[0];
const byLv = new Map<number, HeroSkillTierLine[]>();
for (const lines of groups) {
for (const line of lines) {
const bucket = byLv.get(line.heroLv);
if (bucket) bucket.push(line);
else byLv.set(line.heroLv, [line]);
}
}
return [...byLv.keys()].sort((a, b) => a - b).map(heroLv => {
const bucket = byLv.get(heroLv)!;
return {
sLv: Math.max(...bucket.map(l => l.sLv)),
heroLv,
text: bucket.map(l => l.text).join("\n"),
};
});
}
/**
* 输出二(触发组整体):指定触发槽位整组的各等级档位说明,按解锁英雄等级升序。
*
* 每个触发槽位(atking/dead/fstart/atked/call/field)整组视为一个技能:
* 组内各 s_uuid 分别生成分档行后按解锁英雄等级阈值合并(同阈值文本换行拼接),
* 组内多个技能时每行带各自技能名区分;驻场光环按词条维度合并。
* 每档附带解锁英雄等级,调用方对比 smc.getHeroOuterLv 判定激活状态。
*
* @param heroUuid 英雄 uuid(HeroInfo key)
* @param slot 触发槽位(field 表示驻场光环整组)
* @returns 合并后的档位行数组;该槽位未配置返回 []
*/
export function buildHeroGroupTierLines(heroUuid: number, slot: SkillTriggerType): HeroSkillTierLine[] {
const hero = HeroInfo[heroUuid];
if (!hero) return [];
if (slot === SkillTriggerType.Field) return collectFieldGroupTierLines(hero);
const grouped = hero[slot] as TriggerGrouped | undefined;
if (!grouped) return [];
const keys = Object.keys(grouped).map(Number);
const lineGroups: HeroSkillTierLine[][] = [];
for (const sUuid of keys) {
const hit = collectTriggerTiers(hero, slot, sUuid);
if (!hit) continue;
const lines = buildTierLines(hit.tiers, hit.label);
// 组内多个技能时每行带各自技能名区分
if (keys.length > 1) {
const name = getGroupedTriggerName(grouped, sUuid);
for (const line of lines) {
line.text = `${skillNameRich(name)}${line.text}`;
}
}
lineGroups.push(lines);
}
return mergeTierLinesByHeroLv(lineGroups);
}
/**
* 输出二:指定技能的各阶段说明(富文本整段)。
*