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

@@ -486,19 +486,21 @@ export class HerosListComp extends CCComp {
// ======================== 技能图标列表 ========================
/** 图鉴面板展示的触发技能类型(heroInfo 触发配置:死亡/战斗开始/光环/攻击后/受击) */
/** 图鉴面板展示的触发技能槽位(按触发时机排序:召唤→战斗开始→攻击后→受击后→死亡→常驻光环) */
private static readonly PANEL_TRIGGER_TYPES: SkillTriggerType[] = [
SkillTriggerType.Dead,
SkillTriggerType.Call,
SkillTriggerType.FStart,
SkillTriggerType.Field,
SkillTriggerType.Atking,
SkillTriggerType.Atked,
SkillTriggerType.Dead,
SkillTriggerType.Field,
]
/**
* 刷新当前英雄的技能图标列表:
* 1. 大招:skills[1] 及以后(首个为普攻,跳过)。
* 2. 触发技能:Dead/FStart/Field/Atking/Atked(复活已纳入 dead)。
* 2. 触发技能:每个触发槽位(Call/FStart/Atking/Atked/Dead/Field)整组视为一个技能,
* 只出一个图标,组内多技能/词条合并展示(复活已纳入 dead)。
* 以局外等级(getHeroOuterLv)为当前等级,未达解锁档的显示 locked,
* 已激活的显示当前等级对应的技能等级。
*/
@@ -522,63 +524,76 @@ export class HerosListComp extends CCComp {
this.addSkillIcon(hSkill.uuid, sLv, false, false)
})
// ---- 触发技能:按 PANEL_TRIGGER_TYPES 顺序追加 ----
// ---- 触发技能:每个触发槽位整组一个图标,按 PANEL_TRIGGER_TYPES 顺序追加 ----
for (const type of HerosListComp.PANEL_TRIGGER_TYPES) {
if (type === SkillTriggerType.Field) {
this.addFieldIcons(hero[SkillTriggerType.Field], lv)
this.addFieldGroupIcon(hero[SkillTriggerType.Field], lv)
} else {
this.addTriggerIcons(hero[type] as TriggerGrouped | undefined, lv, type)
this.addTriggerGroupIcon(hero[type] as TriggerGrouped | undefined, lv, type)
}
}
}
/**
* 实例化技能小图标并初始化
* @param s_uuid 技能 UUID(SkillSet 或 FieldSkillSet)
* @param s_lv 当前等级对应的技能等级(locked 时为解锁预览档)
* @param locked 当前等级未激活 → 显示锁定遮罩
* @param isField true 表示驻场光环词条(图标取 FieldSkillSet)
* @param slot 技能所属槽位(普攻/大招传 undefined;触发/光环传对应类型,
* 同一底座不同槽位视为不同技能,弹窗按槽位精确取档;复活已纳入 dead)
* @param s_uuid 技能 UUID(SkillSet 或 FieldSkillSet;整组模式下取组内首个,仅用于图标)
* @param s_lv 当前等级对应的技能等级(locked 时为解锁预览档)
* @param locked 当前等级未激活 → 显示锁定遮罩
* @param isField true 表示驻场光环词条(图标取 FieldSkillSet)
* @param slot 技能所属槽位(普攻/大招传 undefined;触发/光环传对应类型,
* 同一底座不同槽位视为不同技能,弹窗按槽位取档;复活已纳入 dead)
* @param isGroup 整组模式:该图标代表整个触发槽位,组名取组内首个自定义名,
* 弹窗按整组合并渲染(组内多技能/词条统一说明)
*/
private addSkillIcon(s_uuid: number, s_lv: number, locked: boolean, isField: boolean, slot?: SkillTriggerType) {
private addSkillIcon(s_uuid: number, s_lv: number, locked: boolean, isField: boolean, slot?: SkillTriggerType, isGroup: boolean = false) {
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, slot)
comp.initHeroSkill(s_uuid, s_lv, locked, isField, this.huuid, slot, isGroup)
this.skills_node.addChild(node)
}
/**
* 触发技能(dead/fstart/atking/atked):按 s_uuid 分组,取 ≤lv 最大档判定激活;
* 未激活时预览最低档(与 resolveTriggerByLv 同语义)
* @param type 触发槽位(随图标透传,区分与普攻同底座的触发技能)
* 触发技能槽位(call/fstart/atking/atked/dead)整组一个图标:
* 组内多 s_uuid 合并展示,图标/等级取组内首个 s_uuid 的生效档(未激活预览最低档);
* 整组激活判定:组内任一 s_uuid 存在 ≤lv 生效档即视为已激活(与 resolveTriggerByLv 同语义)
* @param type 触发槽位(随图标透传,弹窗按整组取档)
*/
private addTriggerIcons(grouped: TriggerGrouped | undefined, lv: number, type: SkillTriggerType) {
private addTriggerGroupIcon(grouped: TriggerGrouped | undefined, lv: number, type: SkillTriggerType) {
if (!grouped) return
for (const key in grouped) {
const entries = grouped[key]
if (!entries?.length) continue
const sorted = [...entries].sort((a, b) => a.lv - b.lv)
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, type)
const keys = Object.keys(grouped).map(Number).filter(k => grouped[k]?.length)
if (keys.length === 0) return
// 图标/等级取组内首个 s_uuid 的生效档
const sorted = [...grouped[keys[0]]].sort((a, b) => a.lv - b.lv)
let active: LvTriggerEntry | undefined
for (const e of sorted) { if (e.lv <= lv) active = e }
// 整组激活判定:组内任一 s_uuid 存在生效档即激活
let anyActive = !!active
for (const key of keys) {
if (anyActive) break
for (const e of grouped[key] ?? []) {
if (e.lv <= lv) { anyActive = true; break }
}
}
const pick = active ?? sorted[0]
this.addSkillIcon(keys[0], pick.s_lv ?? 1, !anyActive, false, type, true)
}
/** 驻场光环:未激活取最低档预览,激活取 ≤lv 最大档的词条列表 */
private addFieldIcons(field: LvFieldEntry[] | undefined, lv: number) {
/**
* 驻场光环槽位整组一个图标:组内多词条合并展示,
* 图标取生效档(未激活预览最低档)的首个词条
*/
private addFieldGroupIcon(field: LvFieldEntry[] | undefined, lv: number) {
if (!field?.length) return
const sorted = [...field].sort((a, b) => a.lv - b.lv)
let active: LvFieldEntry | undefined
for (const e of sorted) { if (e.lv <= lv) active = e }
const pick = active ?? sorted[0]
for (const entry of pick.uuids) {
// 词条档位无 s_lv 概念,技能等级恒为 1(等级标签展示技能等级而非解锁的英雄等级)
this.addSkillIcon(entry.uuid, 1, !active, true, SkillTriggerType.Field)
}
const firstUuid = pick.uuids[0]?.uuid
if (!firstUuid) return
// 词条档位无 s_lv 概念,技能等级恒为 1(等级标签展示技能等级而非解锁的英雄等级)
this.addSkillIcon(firstUuid, 1, !active, true, SkillTriggerType.Field, true)
}
/** 复活技能已纳入 dead 触发组,通过 dead 配置展示(revive_hp / revive_count),无需单独处理 */

View File

@@ -6,7 +6,7 @@ import { CardTriggerType } from "../common/config/CardSet";
import { SkillSet, FieldSkillSet } from "../common/config/SkillSet";
import {
FieldEntry, HeroInfo, TriggerGrouped, SkillTriggerType,
getGroupedTriggerName, getFieldSkillName,
getGroupedTriggerName, getFieldSkillName, getTriggerGroupName, getFieldGroupName,
} from "../common/config/heroSet";
import { UIID } from "../common/config/GameUIConfig";
import { smc } from "../common/SingletonModuleComp";
@@ -45,6 +45,9 @@ export class SSkillBoxComp extends CCComp {
/** 技能所属槽位(英雄图鉴模式):同一底座可同时为普攻与触发技能(如 5002 的 6004),
* 弹窗按槽位精确取档,避免自动定位命中错误分组 */
private hero_slot?: SkillTriggerType;
/** 整组模式(英雄图鉴):true 时该图标代表整个触发槽位(atking/dead/fstart/atked/call/field),
* 组内多技能/词条合并为一个名称与一套说明展示,弹窗按整组取档 */
private hero_group: boolean = false;
/** 卡牌等级 */
private rarity: number = 1;
/** 卡牌自定义图标ID(优先级最高,未定义则按 trigger_type 自动取) */
@@ -84,6 +87,8 @@ export class SSkillBoxComp extends CCComp {
hero_uuid: this.hero_uuid || undefined,
// 槽位:同一底座不同槽位视为不同技能(激活等级/档位各自独立)
hero_slot: this.hero_uuid ? this.hero_slot : undefined,
// 整组模式:弹窗按整个触发槽位合并渲染(组内多技能/词条统一说明)
hero_group: this.hero_uuid ? this.hero_group : undefined,
});
}
@@ -101,17 +106,21 @@ export class SSkillBoxComp extends CCComp {
/**
* 初始化英雄技能图标(英雄图鉴面板专用):
* 图标按 s_uuid 直接取 SkillSet / FieldSkillSet 配置,不走 EquipPoolList。
* @param s_uuid 技能 UUID(大招/触发技能指向 SkillSet,光环词条指向 FieldSkillSet)
* @param s_uuid 技能 UUID(大招/触发技能指向 SkillSet,光环词条指向 FieldSkillSet;
* 整组模式下取组内首个 s_uuid/词条,仅用于图标与兜底名)
* @param s_lv 当前英雄等级下生效的技能等级(显示在等级标签)
* @param locked 当前等级未激活时显示锁定遮罩
* @param isField true 时图标取 FieldSkillSet(驻场光环词条)
* @param heroUuid 所属英雄 UUID(点击弹窗按英雄技能分档描述渲染)
* @param slot 技能所属槽位(普攻/大招传 undefined;触发/光环传对应类型,复活已纳入 dead)
* @param isGroup 整组模式:该图标代表整个触发槽位,组名取组内首个自定义名,
* 弹窗按整组合并渲染(组内多技能/词条统一说明)
*/
initHeroSkill(s_uuid: number, s_lv: number, locked: boolean, isField: boolean = false, heroUuid: number = 0, slot?: SkillTriggerType) {
initHeroSkill(s_uuid: number, s_lv: number, locked: boolean, isField: boolean = false, heroUuid: number = 0, slot?: SkillTriggerType, isGroup: boolean = false) {
this.s_uuid = s_uuid;
this.hero_uuid = heroUuid;
this.hero_slot = slot;
this.hero_group = isGroup;
this.rarity = s_lv;
this.hero_icon_source = isField ? "field" : "skill";
this.updateUI();
@@ -163,6 +172,7 @@ export class SSkillBoxComp extends CCComp {
/**
* 英雄图鉴模式:按技能 uuid 在英雄配置中定位,取英雄语境展示名。
* 整组模式优先:整个触发槽位取统一组名(组内首个自定义名);
* 覆盖普攻/大招(skills)、5 类触发技能、驻场光环(field);复活已纳入 dead 触发组。
* 指定槽位时仅在对应槽位查找(同一底座可同时是普攻与触发技能,需精确区分);
* 未命中返回空串,由调用方回退底座名。
@@ -170,6 +180,12 @@ export class SSkillBoxComp extends CCComp {
private resolveHeroSkillName(): string {
const hero = HeroInfo[this.hero_uuid];
if (!hero) return "";
// 整组模式:整个触发槽位一个技能,取组名(组内首个自定义名)
if (this.hero_group && this.hero_slot !== undefined) {
return this.hero_slot === SkillTriggerType.Field
? getFieldGroupName(hero)
: getTriggerGroupName(hero, this.hero_slot);
}
// 槽位指定:光环/触发组各自取值,未命中直接回退(复活已纳入 dead)
if (this.hero_slot !== undefined) {
if (this.hero_slot === SkillTriggerType.Field) {

View File

@@ -24,8 +24,9 @@
* - SkillSet / FieldSkillSet(技能/光环静态配置)
* - SkillCardList(卡牌静态配置)
* - CardSkillText(buildCardDescRich 富文本描述生成,卡牌模式)
* - HeroSkillText(buildHeroSkillTierLines 技能分档描述,英雄技能模式,
* 覆盖普攻/大招、触发技能、驻场光环词条;复活已纳入 dead 触发组)
* - HeroSkillText(buildHeroSkillTierLines 技能分档描述 / buildHeroGroupTierLines
* 触发组整组分档描述,英雄技能模式;触发槽位整组视为一个技能,组内多技能/词条
* 合并为一个名称与一套说明统一展示;复活已纳入 dead 触发组)
*/
import { _decorator, Node, Label, RichText, NodeEventType, Sprite, Color, input, Input, EventTouch, UITransform, Vec3 } from "cc";
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
@@ -36,7 +37,7 @@ import { SkillSet, FieldSkillSet } from "../common/config/SkillSet";
import { SkillCardList } from "../common/config/SCardSet";
import { FieldEntry, SkillTriggerType } from "../common/config/heroSet";
import { buildCardDescRich } from "../common/config/CardSkillText";
import { buildHeroSkillTierLines, HeroSkillTierLine } from "../common/HeroSkillText";
import { buildHeroSkillTierLines, buildHeroGroupTierLines, HeroSkillTierLine } from "../common/HeroSkillText";
import { smc } from "../common/SingletonModuleComp";
const { ccclass, property } = _decorator;
@@ -60,6 +61,9 @@ export interface SInfoParams {
/** 技能所属槽位(英雄图鉴模式):同一底座可同时为普攻与触发技能(如 5002 的 6004),
* 指定后仅在该槽位取档,激活等级/档位按槽位独立判定 */
hero_slot?: SkillTriggerType;
/** 触发组整体模式(英雄图鉴):true 时按 hero_slot 整组渲染,
* 组内多技能/词条合并为一套分档说明统一展示(每个触发槽位视为一个技能) */
hero_group?: boolean;
}
/** 等级提升信息(用于 2 级 / 3 级面板) */
@@ -243,13 +247,16 @@ export class SinfoComp extends CCComp {
/**
* 英雄技能模式渲染(文本二:单个技能各阶段说明)。
* - hero_group:触发槽位整组视为一个技能,组内多技能/词条档位按解锁英雄等级合并展示
* - main_info:技能 slv1 完整描述
* - lv2_desc:slv2 相对 slv1 的提升;lv3_desc:slv3 及以后档(合并展示)
* - lv2/lv3_status:按英雄局外等级(smc.getHeroOuterLv)对比档位解锁等级判定激活
* @returns true 已渲染(拦截通用卡牌逻辑);false 技能无分档数据,回退通用卡牌逻辑
*/
private renderHeroSkill(p: SInfoParams): boolean {
const tiers = buildHeroSkillTierLines(p.hero_uuid!, p.uuid, p.hero_slot);
const tiers = (p.hero_group && p.hero_slot !== undefined)
? buildHeroGroupTierLines(p.hero_uuid!, p.hero_slot)
: buildHeroSkillTierLines(p.hero_uuid!, p.uuid, p.hero_slot);
if (tiers.length === 0) return false;
// 图标 / 名称 / 当前技能等级与通用逻辑一致(skill_lv 传的就是当前生效 s_lv)