feat(hero skill): add slot support for hero skill detail panel

1. 新增技能槽位参数,解决同底座技能(如普攻+触发技能)的分档定位问题
2. 新增2级/3级面板节点配置,自动隐藏无对应档位的面板
3. 重构英雄技能档位生成逻辑,支持按槽位精确查找技能配置
4. 优化技能弹窗渲染逻辑,传递槽位参数避免匹配错误分组
This commit is contained in:
panFD
2026-08-30 14:44:27 +08:00
parent e5c4849e96
commit e4530d6d1c
10 changed files with 2465 additions and 3734 deletions

View File

@@ -433,71 +433,34 @@ export interface HeroSkillTierLine {
}
/**
* 输出二(结构化):指定技能各等级档位说明,按技能等级升序。
*
* slv1 给完整能力 + 数值;slv2+ 给相对上一级的提升。每档附带解锁英雄等级,
* 调用方可自行对比英雄当前等级(smc.getHeroOuterLv)判定激活状态。
*
* 技能定位顺序(覆盖英雄图鉴全部 5 类技能形态):
* 1. hero.skills[skillUuid] —— 普攻/大招
* 2. 5 类触发技能组 —— call/dead/fstart/atking/atked
* 3. hero.field —— 驻场光环词条(FieldSkillSet uuid)
* 4. hero.revive —— 复活技能(SkillSet uuid)
* 均未命中返回 []。
*
* Why: 同一底座技能可能被多个英雄以不同 overrides 引用,必须以英雄语境取档。
*
* @param heroUuid 英雄 uuid(HeroInfo key)
* @param skillUuid 技能 uuid(skills key / 触发组 s_uuid / FieldSkillSet uuid / 复活 SkillSet uuid)
* @returns 档位行数组;技能不属于该英雄时返回 []
* 收集指定触发槽位的触发技能档位与触发语境词;未命中(组不存在/无该 s_uuid/无底座)返回 null。
*/
export function buildHeroSkillTierLines(heroUuid: number, skillUuid: number): HeroSkillTierLine[] {
const hero = HeroInfo[heroUuid];
if (!hero) return [];
let tiers: ISkillTierInfo[] = [];
let triggerLabel = "";
const hSkill = hero.skills?.[skillUuid];
if (hSkill) {
// 普攻/大招路径
tiers = collectSkillTiers(hSkill);
} else {
// 触发技能路径:在 5 类触发组中定位 s_uuid
for (const type of TRIGGER_KEYS) {
const grouped = hero[type] as TriggerGrouped | undefined;
const entries = grouped?.[skillUuid];
if (!grouped || !entries?.length) continue;
const base = SkillSet[skillUuid];
if (!base) return [];
const sorted = [...entries].sort((a, b) => (a.s_lv ?? 0) - (b.s_lv ?? 0));
let inherit: string | undefined;
tiers = sorted.map((e: LvTriggerEntry, i: number) => {
if (e.desc) inherit = e.desc;
return {
sLv: e.s_lv ?? (i + 1),
heroLv: e.lv,
skill: mergeSkillParams(base, e.overrides),
desc: e.desc ?? inherit,
tNum: e.t_num,
};
});
triggerLabel = (type === SkillTriggerType.Atking || type === SkillTriggerType.Atked)
? `${SkillTriggerName[type]}${valueRich(sorted[0].t_num)}次`
: SkillTriggerName[type];
break;
}
}
// 光环/复活路径(无 ISkillTierInfo 中间结构,直接生成 HeroSkillTierLine)
if (tiers.length === 0) {
const fieldLines = collectFieldTierLines(hero, skillUuid);
if (fieldLines.length > 0) return fieldLines;
const reviveLines = collectReviveTierLines(hero, skillUuid);
if (reviveLines.length > 0) return reviveLines;
return [];
}
function collectTriggerTiers(hero: heroInfo, type: SkillTriggerType, skillUuid: number): { tiers: ISkillTierInfo[]; label: string } | null {
const grouped = hero[type] as TriggerGrouped | undefined;
const entries = grouped?.[skillUuid];
if (!grouped || !entries?.length) return null;
const base = SkillSet[skillUuid];
if (!base) return null;
const sorted = [...entries].sort((a, b) => (a.s_lv ?? 0) - (b.s_lv ?? 0));
let inherit: string | undefined;
const tiers = sorted.map((e: LvTriggerEntry, i: number) => {
if (e.desc) inherit = e.desc;
return {
sLv: e.s_lv ?? (i + 1),
heroLv: e.lv,
skill: mergeSkillParams(base, e.overrides),
desc: e.desc ?? inherit,
tNum: e.t_num,
};
});
const label = (type === SkillTriggerType.Atking || type === SkillTriggerType.Atked)
? `${SkillTriggerName[type]}${valueRich(sorted[0].t_num)}次`
: SkillTriggerName[type];
return { tiers, label };
}
/** 档位 → 展示行:slv1 完整能力 + 数值;slv2+ 相对上一级的提升 */
function buildTierLines(tiers: ISkillTierInfo[], triggerLabel: string): HeroSkillTierLine[] {
const lines: HeroSkillTierLine[] = [];
for (let i = 0; i < tiers.length; i++) {
const t = tiers[i];
@@ -514,6 +477,71 @@ export function buildHeroSkillTierLines(heroUuid: number, skillUuid: number): He
return lines;
}
/**
* 输出二(结构化):指定技能各等级档位说明,按技能等级升序。
*
* slv1 给完整能力 + 数值;slv2+ 给相对上一级的提升。每档附带解锁英雄等级,
* 调用方可自行对比英雄当前等级(smc.getHeroOuterLv)判定激活状态。
*
* 技能定位顺序(覆盖英雄图鉴全部 5 类技能形态):
* 1. hero.skills[skillUuid] —— 普攻/大招
* 2. 5 类触发技能组 —— call/dead/fstart/atking/atked
* 3. hero.field —— 驻场光环词条(FieldSkillSet uuid)
* 4. hero.revive —— 复活技能(SkillSet uuid)
* 均未命中返回 []。
*
* Why: 同一底座技能可能被多个英雄以不同 overrides 引用,必须以英雄语境取档;
* 同一底座还可能同时是普攻与触发技能(如 5002 的 6004),传 slot 指定槽位
* 可避免自动定位命中错误分组(激活等级/档位随之错位)。
*
* @param heroUuid 英雄 uuid(HeroInfo key)
* @param skillUuid 技能 uuid(skills key / 触发组 s_uuid / FieldSkillSet uuid / 复活 SkillSet uuid)
* @param slot 技能所属槽位(可空):指定时仅在该槽位查找;缺省按上述顺序自动定位
* @returns 档位行数组;技能不属于该英雄时返回 []
*/
export function buildHeroSkillTierLines(heroUuid: number, skillUuid: number, slot?: SkillTriggerType): HeroSkillTierLine[] {
const hero = HeroInfo[heroUuid];
if (!hero) return [];
// 槽位指定:光环/复活结构特殊直接生成行;触发组走统一档位结构
if (slot === SkillTriggerType.Field) return collectFieldTierLines(hero, skillUuid);
if (slot === SkillTriggerType.Revive) return collectReviveTierLines(hero, skillUuid);
if (slot !== undefined) {
const hit = collectTriggerTiers(hero, slot, skillUuid);
return hit ? buildTierLines(hit.tiers, hit.label) : [];
}
// 自动定位:普攻/大招优先,其次 5 类触发组,最后光环/复活
let tiers: ISkillTierInfo[] = [];
let triggerLabel = "";
const hSkill = hero.skills?.[skillUuid];
if (hSkill) {
// 普攻/大招路径
tiers = collectSkillTiers(hSkill);
} else {
// 触发技能路径:在 5 类触发组中定位 s_uuid
for (const type of TRIGGER_KEYS) {
const hit = collectTriggerTiers(hero, type, skillUuid);
if (hit) {
tiers = hit.tiers;
triggerLabel = hit.label;
break;
}
}
}
// 光环/复活路径(无 ISkillTierInfo 中间结构,直接生成 HeroSkillTierLine)
if (tiers.length === 0) {
const fieldLines = collectFieldTierLines(hero, skillUuid);
if (fieldLines.length > 0) return fieldLines;
const reviveLines = collectReviveTierLines(hero, skillUuid);
if (reviveLines.length > 0) return reviveLines;
return [];
}
return buildTierLines(tiers, triggerLabel);
}
/**
* 输出二:指定技能的各阶段说明(富文本整段)。
*

View File

@@ -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"

View File

@@ -533,7 +533,7 @@ export class HerosListComp extends CCComp {
} else if (type === SkillTriggerType.Revive) {
this.addReviveIcon(hero[SkillTriggerType.Revive], lv)
} else {
this.addTriggerIcons(hero[type] as TriggerGrouped | undefined, lv)
this.addTriggerIcons(hero[type] as TriggerGrouped | undefined, lv, type)
}
}
}
@@ -544,21 +544,24 @@ export class HerosListComp extends CCComp {
* @param s_lv 当前等级对应的技能等级(locked 时为解锁预览档)
* @param locked 当前等级未激活 → 显示锁定遮罩
* @param isField true 表示驻场光环词条(图标取 FieldSkillSet)
* @param slot 技能所属槽位(普攻/大招传 undefined;触发/光环/复活传对应类型,
* 同一底座不同槽位视为不同技能,弹窗按槽位精确取档)
*/
private addSkillIcon(s_uuid: number, s_lv: number, locked: boolean, isField: boolean) {
private addSkillIcon(s_uuid: number, s_lv: number, locked: boolean, isField: boolean, slot?: SkillTriggerType) {
const node = instantiate(this.skill_icon_prefab)
node.name = `skill_${s_uuid}`
const comp = node.getComponent(SSkillBoxComp) || node.addComponent(SSkillBoxComp)
// 携带所属英雄,点击弹窗按英雄技能分档描述渲染
comp.initHeroSkill(s_uuid, s_lv, locked, isField, this.huuid)
// 携带所属英雄与槽位,点击弹窗按英雄技能分档描述渲染
comp.initHeroSkill(s_uuid, s_lv, locked, isField, this.huuid, slot)
this.skills_node.addChild(node)
}
/**
* 触发技能(dead/fstart/atking/atked):按 s_uuid 分组,取 ≤lv 最大档判定激活;
* 未激活时预览最低档(与 resolveTriggerByLv 同语义)
* @param type 触发槽位(随图标透传,区分与普攻同底座的触发技能)
*/
private addTriggerIcons(grouped: TriggerGrouped | undefined, lv: number) {
private addTriggerIcons(grouped: TriggerGrouped | undefined, lv: number, type: SkillTriggerType) {
if (!grouped) return
for (const key in grouped) {
const entries = grouped[key]
@@ -567,7 +570,7 @@ export class HerosListComp extends CCComp {
let active: LvTriggerEntry | undefined
for (const e of sorted) { if (e.lv <= lv) active = e }
const pick = active ?? sorted[0]
this.addSkillIcon(Number(key), pick.s_lv ?? 1, !active, false)
this.addSkillIcon(Number(key), pick.s_lv ?? 1, !active, false, type)
}
}
@@ -580,7 +583,7 @@ export class HerosListComp extends CCComp {
const pick = active ?? sorted[0]
for (const entry of pick.uuids) {
// 词条档位无 s_lv 概念,技能等级恒为 1(等级标签展示技能等级而非解锁的英雄等级)
this.addSkillIcon(entry.uuid, 1, !active, true)
this.addSkillIcon(entry.uuid, 1, !active, true, SkillTriggerType.Field)
}
}
@@ -592,7 +595,7 @@ export class HerosListComp extends CCComp {
for (const e of sorted) { if (e.lv <= lv) active = e }
const pick = active ?? sorted[0]
// 复活档位无 s_lv 概念,技能等级恒为 1(等级标签展示技能等级而非解锁的英雄等级)
this.addSkillIcon(pick.s_uuid, 1, !active, false)
this.addSkillIcon(pick.s_uuid, 1, !active, false, SkillTriggerType.Revive)
}
// ======================== 英雄动画 ========================

View File

@@ -46,6 +46,9 @@ export class SSkillBoxComp extends CCComp {
private s_uuid: number = 0;
/** 所属英雄 UUID(英雄图鉴模式,>0 时点击弹窗走英雄技能分档描述) */
private hero_uuid: number = 0;
/** 技能所属槽位(英雄图鉴模式):同一底座可同时为普攻与触发技能(如 5002 的 6004),
* 弹窗按槽位精确取档,避免自动定位命中错误分组 */
private hero_slot?: SkillTriggerType;
/** 卡牌等级 */
private card_lv: number = 1;
/** 卡牌自定义图标ID(优先级最高,未定义则按 trigger_type 自动取) */
@@ -83,6 +86,8 @@ export class SSkillBoxComp extends CCComp {
trigger_type: this.trigger_type,
// 英雄图鉴模式:携带所属英雄,弹窗按英雄技能分档描述渲染
hero_uuid: this.hero_uuid || undefined,
// 槽位:同一底座不同槽位视为不同技能(激活等级/档位各自独立)
hero_slot: this.hero_uuid ? this.hero_slot : undefined,
});
}
@@ -105,10 +110,12 @@ export class SSkillBoxComp extends CCComp {
* @param locked 当前等级未激活时显示锁定遮罩
* @param isField true 时图标取 FieldSkillSet(驻场光环词条)
* @param heroUuid 所属英雄 UUID(点击弹窗按英雄技能分档描述渲染)
* @param slot 技能所属槽位(普攻/大招传 undefined;触发/光环/复活传对应类型)
*/
initHeroSkill(s_uuid: number, s_lv: number, locked: boolean, isField: boolean = false, heroUuid: number = 0) {
initHeroSkill(s_uuid: number, s_lv: number, locked: boolean, isField: boolean = false, heroUuid: number = 0, slot?: SkillTriggerType) {
this.s_uuid = s_uuid;
this.hero_uuid = heroUuid;
this.hero_slot = slot;
this.card_lv = s_lv;
this.hero_icon_source = isField ? "field" : "skill";
this.updateUI();
@@ -182,11 +189,24 @@ export class SSkillBoxComp extends CCComp {
/**
* 英雄图鉴模式:按技能 uuid 在英雄配置中定位,取英雄语境展示名。
* 覆盖普攻/大招(skills)、5 类触发技能、驻场光环(field)、复活(revive)。
* 指定槽位时仅在对应槽位查找(同一底座可同时是普攻与触发技能,需精确区分);
* 未命中返回空串,由调用方回退底座名。
*/
private resolveHeroSkillName(): string {
const hero = HeroInfo[this.hero_uuid];
if (!hero) return "";
// 槽位指定:光环/复活/触发组各自取值,未命中直接回退
if (this.hero_slot !== undefined) {
if (this.hero_slot === SkillTriggerType.Field) {
return getFieldSkillName(hero, this.s_uuid);
}
if (this.hero_slot === SkillTriggerType.Revive) {
return getReviveSkillName(hero, this.s_uuid);
}
const grouped = hero[this.hero_slot] as TriggerGrouped | undefined;
if (grouped?.[this.s_uuid]?.length) return getGroupedTriggerName(grouped, this.s_uuid);
return "";
}
// 普攻/大招
const hSkill = hero.skills?.[this.s_uuid];
if (hSkill?.name) return hSkill.name;

View File

@@ -34,7 +34,7 @@ import { oops } from "db://oops-framework/core/Oops";
import { CardTriggerType, CardConfig } from "../common/config/CardSet";
import { SkillSet, FieldSkillSet } from "../common/config/SkillSet";
import { SkillCardList } from "../common/config/SCardSet";
import { FieldEntry } from "../common/config/heroSet";
import { FieldEntry, SkillTriggerType } from "../common/config/heroSet";
import { buildCardDescRich } from "../common/config/CardSkillText";
import { buildHeroSkillTierLines, HeroSkillTierLine } from "../common/HeroSkillText";
import { smc } from "../common/SingletonModuleComp";
@@ -57,6 +57,9 @@ export interface SInfoParams {
trigger_type?: CardTriggerType;
/** 所属英雄 UUID(英雄图鉴模式:按英雄技能分档描述渲染,优先级高于卡牌逻辑) */
hero_uuid?: number;
/** 技能所属槽位(英雄图鉴模式):同一底座可同时为普攻与触发技能(如 5002 的 6004),
* 指定后仅在该槽位取档,激活等级/档位按槽位独立判定 */
hero_slot?: SkillTriggerType;
}
/** 等级提升信息(用于 2 级 / 3 级面板) */
@@ -94,6 +97,10 @@ export class SinfoComp extends CCComp {
@property({ type: Label, displayName: "1级激活状态" })
private main_status: Label = null;
/** 2级面板节点(无 2 级档位的技能隐藏) */
@property({ type: Node, displayName: "2级面板" })
private lv2_node: Node = null;
/** 2级激活状态标签 */
@property({ type: Label, displayName: "2级激活状态" })
private lv2_status: Label = null;
@@ -102,6 +109,10 @@ export class SinfoComp extends CCComp {
@property({ type: RichText, displayName: "2级提升说明" })
private lv2_desc: RichText = null;
/** 3级面板节点(无 3 级及以上档位的技能隐藏) */
@property({ type: Node, displayName: "3级面板" })
private lv3_node: Node = null;
/** 3级激活状态标签 */
@property({ type: Label, displayName: "3级激活状态" })
private lv3_status: Label = null;
@@ -215,7 +226,7 @@ export class SinfoComp extends CCComp {
* @returns true 已渲染(拦截通用卡牌逻辑);false 技能无分档数据,回退通用卡牌逻辑
*/
private renderHeroSkill(p: SInfoParams): boolean {
const tiers = buildHeroSkillTierLines(p.hero_uuid!, p.uuid);
const tiers = buildHeroSkillTierLines(p.hero_uuid!, p.uuid, p.hero_slot);
if (tiers.length === 0) return false;
// 图标 / 名称 / 当前技能等级与通用逻辑一致(skill_lv 传的就是当前生效 s_lv)
@@ -237,6 +248,9 @@ export class SinfoComp extends CCComp {
// 技能 2 级 / 3 级(3 级以后档位合并进 3 级栏,多行展示)
const lv2 = tiers.find(t => t.sLv === 2);
const lv3Plus = tiers.filter(t => t.sLv >= 3);
// 无对应档位的技能隐藏 2 级 / 3 级面板节点
this.setPanelActive(this.lv2_node, !!lv2);
this.setPanelActive(this.lv3_node, lv3Plus.length > 0);
this.renderLvPanelFromTier(lv2, heroLv, this.lv2_status, this.lv2_desc);
const lv3Merged = lv3Plus.length
? { sLv: lv3Plus[0].sLv, heroLv: lv3Plus[0].heroLv, text: lv3Plus.map(t => t.text).join("\n") }
@@ -245,6 +259,13 @@ export class SinfoComp extends CCComp {
return true;
}
/** 设置等级面板节点显隐(无对应档位时隐藏) */
private setPanelActive(node: Node | null, active: boolean) {
if (node && node.isValid) {
node.active = active;
}
}
/** 按档位渲染单等级面板:激活状态按英雄等级对比档位解锁等级 */
private renderLvPanelFromTier(tier: HeroSkillTierLine | undefined, heroLv: number, statusLabel: Label | null, descRich: RichText | null) {
if (statusLabel && statusLabel.isValid) {