Files
pixelheros/assets/script/game/map/SinfoComp.ts
pan 22e91fc99d feat(hero-ui): 新增普攻lv2强化档独立展示功能
实现英雄图鉴中普攻lv2强化档的单独展示逻辑:
1. 为技能档位渲染添加minLv参数,支持裁剪低档位
2. 新增普攻lv2专属模式,图标/名称取lv1,弹窗仅展示lv2+档位
3. 自动为符合条件的普攻强化档生成独立技能图标
4. 适配等级显示偏移,配置Lv2显示为Lv1
2026-09-04 15:25:35 +08:00

492 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file SinfoComp.ts
* @description 技能信息弹窗组件(UI 视图层)
*
* 职责:
* 1. 展示技能小图标(SSkillBoxComp)点击后的详细信息弹窗。
* 2. 展示技能名称、当前等级、图标、主功能说明。
* 3. 展示 1 级 / 2 级 / 3 级 激活状态(已激活显示"已激活",未激活显示"x级激活")。
* 4. 点击弹窗外部区域关闭。
*
* 使用(oops.gui 模块):
* oops.gui.open(UIID.SInfo, {
* uuid: 8701, // 卡牌/技能 UUID
* skill_lv: 1, // 当前等级
* card_icon: "xxx", // 图标ID(可选,缺省自动解析)
* skill_name: "战后恢复", // 名称(可选)
* field: [...], // 驻场词条(装备卡)
* trigger_type: 3, // 触发类型
* hero_uuid: 5001, // 所属英雄(英雄图鉴模式:按英雄技能分档描述渲染)
* });
*
* 依赖:
* - oops.gui(UI 生命周期)—— open / removeByNode
* - SkillSet / FieldSkillSet(技能/光环静态配置)
* - SkillCardList(卡牌静态配置)
* - CardSkillText(buildCardDescRich 富文本描述生成,卡牌模式)
* - 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";
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
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, SkillTriggerType } from "../common/config/heroSet";
import { buildCardDescRich } from "../common/config/CardSkillText";
import { buildHeroSkillTierLines, buildHeroGroupTierLines, HeroSkillTierLine } from "../common/HeroSkillText";
import { smc } from "../common/SingletonModuleComp";
const { ccclass, property } = _decorator;
/** SInfo 弹窗初始化参数 */
export interface SInfoParams {
/** 卡牌/技能 UUID */
uuid: number;
/** 当前等级 */
skill_lv: number;
/** 图标ID(可选,优先级最高) */
card_icon?: string;
/** 技能名称(可选) */
skill_name?: string;
/** 驻场词条(装备卡专用) */
field?: FieldEntry[];
/** 触发类型 */
trigger_type?: CardTriggerType;
/** 所属英雄 UUID(英雄图鉴模式:按英雄技能分档描述渲染,优先级高于卡牌逻辑) */
hero_uuid?: number;
/** 技能所属槽位(英雄图鉴模式):同一底座可同时为普攻与触发技能(如 5002 的 6004),
* 指定后仅在该槽位取档,激活等级/档位按槽位独立判定 */
hero_slot?: SkillTriggerType;
/** 触发组整体模式(英雄图鉴):true 时按 hero_slot 整组渲染,
* 组内多技能/词条合并为一套分档说明统一展示(每个触发槽位视为一个技能) */
hero_group?: boolean;
/** 普攻 lv2+ 强化档专属模式(英雄图鉴):名称/图标取 lv1,只展示 lv2+ 档位说明
* (裁剪 lv1 档,隐藏 2/3 级面板);等级显示偏移一档(配置 Lv2 → 显示 Lv1,Lv3 → Lv2) */
hero_normal_lv2?: boolean;
}
/** 等级提升信息(用于 2 级 / 3 级面板) */
interface LvUpgradeInfo {
/** 是否已激活 */
unlocked: boolean;
/** 等级标签文本(如 "Lv.2") */
lvText: string;
/** 激活状态文本("已激活" 或 "2级激活") */
statusText: string;
}
@ccclass('SinfoComp')
@ecs.register('SinfoComp', false)
export class SinfoComp extends CCComp {
// ======================== 编辑器绑定节点 ========================
/** 技能图标节点 */
@property({ type: Node, displayName: "技能图标" })
private icon_node: Node = null;
/** 技能名称标签 */
@property({ type: Label, displayName: "技能名称" })
private name_label: Label = null;
/** 当前等级标签 */
@property({ type: Label, displayName: "当前等级" })
private lv_label: Label = null;
/** 主功能说明(富文本) */
@property({ type: RichText, displayName: "主功能说明" })
private main_info: RichText = null;
/** 1级激活状态标签 */
@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;
/** 2级提升说明(富文本) */
@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;
/** 3级提升说明(富文本) */
@property({ type: RichText, displayName: "3级提升说明" })
private lv3_desc: RichText = null;
/** 关闭按钮节点 */
@property({ type: Node, displayName: "关闭按钮" })
private close_btn: Node = null;
/** 全屏遮罩节点(铺满屏幕,点击弹窗主体以外区域关闭) */
@property({ type: Node, displayName: "遮罩节点" })
private mask_node: Node = null;
/** 技能未激活图标背景(技能未激活时显示,已激活隐藏) */
@property({ type: Node, displayName: "未激活图标背景" })
private inactive_bg: Node = null;
// ======================== 运行时状态 ========================
/** 弹窗参数 */
private params: SInfoParams | null = null;
// ======================== 生命周期 ========================
onLoad() {
// 点击弹窗自身:阻止冒泡,由全局触摸判定触点位置决定是否关闭
this.node.on(NodeEventType.TOUCH_END, this.onSelfTouch, this);
// 关闭按钮:编辑器未绑定时按节点名 "close" 兜底查找
if (!this.close_btn) {
this.close_btn = this.node.getChildByName("close");
}
this.close_btn?.on(NodeEventType.TOUCH_END, this.onCloseClick, this);
// 遮罩点击关闭:编辑器未绑定时按节点名 "mask" 兜底查找
if (!this.mask_node) {
this.mask_node = this.node.getChildByName("mask");
}
this.mask_node?.on(NodeEventType.TOUCH_END, this.onMaskClick, this);
}
onDisable() {
// 节点离场(含被 LayerManager 移入缓存池)即注销全局触摸,
// 防止残留监听导致 detached 节点再次响应关闭
this.unscheduleAllCallbacks();
input.off(Input.EventType.TOUCH_END, this.onGlobalTouch, this);
}
onDestroy() {
super.onDestroy();
if (this.node && this.node.isValid) {
this.node.off(NodeEventType.TOUCH_END, this.onSelfTouch, this);
}
if (this.close_btn && this.close_btn.isValid) {
this.close_btn.off(NodeEventType.TOUCH_END, this.onCloseClick, this);
}
if (this.mask_node && this.mask_node.isValid) {
this.mask_node.off(NodeEventType.TOUCH_END, this.onMaskClick, this);
}
// 注销全局触摸监听
input.off(Input.EventType.TOUCH_END, this.onGlobalTouch, this);
}
/**
* oops.gui 添加到舞台回调(框架约定入口)
* @param params 见 SInfoParams
*/
onAdded(params: SInfoParams) {
this.params = params;
this.render();
// 延迟一帧注册全局触摸,避免本次点击立即触发关闭;
// 回调内校验节点仍在舞台,防止同帧开关导致监听泄漏
this.scheduleOnce(() => {
if (this.node && this.node.isValid && this.node.activeInHierarchy) {
input.on(Input.EventType.TOUCH_END, this.onGlobalTouch, this);
}
}, 0);
}
/** 点击弹窗自身:阻止冒泡,不关闭 */
private onSelfTouch(event: EventTouch) {
event.propagationStopped = true;
}
/** 关闭按钮点击 */
private onCloseClick(event: EventTouch) {
// 阻止冒泡,避免同时触发 onSelfTouch / onGlobalTouch 双重关闭
event.propagationStopped = true;
oops.audio.playEffect("music/button");
this.close();
}
/** 遮罩点击:触点位于弹窗主体之外,直接关闭 */
private onMaskClick(event: EventTouch) {
event.propagationStopped = true;
oops.audio.playEffect("music/button");
this.close();
}
/** 全局触摸结束:触点落在弹窗包围盒外才关闭 */
private onGlobalTouch(event: EventTouch) {
if (!this.node || !this.node.isValid) return;
// 触点 UI 坐标 → 弹窗局部坐标,判定是否在弹窗内(convertToNodeSpaceAR 需要 Vec3)
const uit = this.node.getComponent(UITransform);
if (!uit) return;
const uiPos = event.getUILocation();
const localPos = uit.convertToNodeSpaceAR(new Vec3(uiPos.x, uiPos.y, 0));
const halfW = uit.contentSize.width * uit.anchorX;
const halfH = uit.contentSize.height * uit.anchorY;
const inBox =
localPos.x >= -halfW && localPos.x <= uit.contentSize.width - halfW &&
localPos.y >= -halfH && localPos.y <= uit.contentSize.height - halfH;
if (!inBox) {
this.close();
}
}
/** 关闭弹窗 */
private close() {
// 防御重复关闭:节点已被移除(如关闭按钮与全局触摸同帧先后触发,
// removeByNode 默认不销毁节点,此时 isValid 仍为 true 但 parent 为 null)
if (!this.node || !this.node.isValid || !this.node.parent) return;
oops.gui.removeByNode(this.node);
}
// ======================== 渲染逻辑 ========================
/**
* 英雄技能模式渲染(文本二:单个技能各阶段说明)。
* - 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 = (p.hero_group && p.hero_slot !== undefined)
? buildHeroGroupTierLines(p.hero_uuid!, p.hero_slot)
: buildHeroSkillTierLines(p.hero_uuid!, p.uuid, p.hero_slot, p.hero_normal_lv2 ? 2 : 1);
if (tiers.length === 0) return false;
// 图标 / 名称 / 当前技能等级与通用逻辑一致(skill_lv 传的就是当前生效 s_lv;
// 普攻 lv2+ 强化档等级偏移一档显示:配置 Lv2 → Lv1,Lv3 → Lv2)
this.renderIcon(p);
this.setLabel(this.name_label, this.resolveName(p));
this.setLabel(this.lv_label, `Lv.${p.hero_normal_lv2 ? Math.max(1, p.skill_lv - 1) : p.skill_lv}`);
// 主说明:普攻 lv2 专属模式只展示 lv2 档位文本(裁剪后首档即 lv2),否则 slv1 完整能力
if (this.main_info && this.main_info.isValid) {
this.main_info.string = tiers[0].text || "暂无描述";
}
// 英雄当前局外等级:激活判定基准
const heroLv = Math.max(1, smc.getHeroOuterLv(p.hero_uuid!));
// 未激活图标背景:首档未解锁时显示
this.updateInactiveBg(heroLv >= tiers[0].heroLv);
// 普攻 lv2 专属模式:等级/档位面板均由该档接管,隐藏 2/3 级面板(只展示 lv2 描述)
if (p.hero_normal_lv2) {
this.setPanelActive(this.lv2_node, false);
this.setPanelActive(this.lv3_node, false);
this.renderLvPanelFromTier(tiers[0], heroLv, this.main_status, null);
return true;
}
// 技能 1 级:激活状态与 2/3 级面板同一表示方式(描述已在 main_info 展示,此处仅渲染状态)
this.renderLvPanelFromTier(tiers[0], heroLv, this.main_status, null);
// 技能 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") }
: undefined;
this.renderLvPanelFromTier(lv3Merged, heroLv, this.lv3_status, this.lv3_desc);
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) {
const unlocked = !!tier && heroLv >= tier.heroLv;
statusLabel.string = unlocked ? "已激活" : (tier ? `${tier.heroLv}级解锁` : "");
// 已激活显示绿色,未激活显示灰色
statusLabel.color = unlocked
? new Color(0, 255, 10, 255)
: new Color(150, 150, 150, 255);
}
if (descRich && descRich.isValid) {
descRich.string = tier?.text ?? "";
}
}
/** 主渲染入口 */
private render() {
if (!this.params) return;
const p = this.params;
// ---- 英雄技能模式:按英雄语境渲染分档描述(文本二) ----
if (p.hero_uuid && this.renderHeroSkill(p)) return;
// ---- 图标 ----
this.renderIcon(p);
// ---- 名称 ----
const name = this.resolveName(p);
this.setLabel(this.name_label, name);
// ---- 当前等级 ----
this.setLabel(this.lv_label, `Lv.${p.skill_lv}`);
// ---- 主功能说明 ----
this.renderMainInfo(p);
// ---- 1 级 / 2 级 / 3 级 激活信息 ----
const lv1Info = this.buildLvUpgradeInfo(p, 1);
const lv2Info = this.buildLvUpgradeInfo(p, 2);
const lv3Info = this.buildLvUpgradeInfo(p, 3);
this.renderLvPanel(lv1Info, this.main_status, null);
this.renderLvPanel(lv2Info, this.lv2_status, this.lv2_desc);
this.renderLvPanel(lv3Info, this.lv3_status, this.lv3_desc);
}
/** 渲染技能图标 */
private renderIcon(p: SInfoParams) {
if (!this.icon_node || !this.icon_node.isValid) return;
const iconId = p.card_icon ?? this.resolveIconId(p);
let sprite = this.icon_node.getComponent(Sprite) || this.icon_node.addComponent(Sprite);
if (smc.uiconsAtlas && iconId) {
const frame = smc.uiconsAtlas.getSpriteFrame(iconId);
sprite.spriteFrame = frame || null;
} else {
sprite.spriteFrame = null;
}
}
/** 解析图标ID */
private resolveIconId(p: SInfoParams): string | undefined {
if (p.card_icon) return p.card_icon;
// 技能卡:从 SkillCardList 查 icon
const skillCard = SkillCardList.find(c => c.uuid === p.uuid || c.skill === p.uuid);
if (skillCard?.icon) return skillCard.icon;
// 驻场词条:从 FieldSkillSet 取
if (p.trigger_type === CardTriggerType.Field && p.field?.length) {
const fieldUuid = p.field[0].uuid;
return FieldSkillSet[fieldUuid]?.icon;
}
// 技能底座:从 SkillSet 取
const skillUuid = skillCard?.skill ?? p.uuid;
if (SkillSet[skillUuid]?.icon) return SkillSet[skillUuid]!.icon;
// 驻场光环词条:uuid 为 FieldSkillSet 词条(英雄图鉴光环弹窗)
return FieldSkillSet[p.uuid]?.icon;
}
/** 解析技能名称 */
private resolveName(p: SInfoParams): string {
if (p.skill_name) return p.skill_name;
const skillCard = SkillCardList.find(c => c.uuid === p.uuid || c.skill === p.uuid);
if (skillCard?.name) return skillCard.name;
if (p.trigger_type === CardTriggerType.Field && p.field?.length) {
const fieldUuid = p.field[0].uuid;
return FieldSkillSet[fieldUuid]?.name ?? "";
}
const skillUuid = skillCard?.skill ?? p.uuid;
if (SkillSet[skillUuid]?.name) return SkillSet[skillUuid]!.name;
// 驻场光环词条:名称取 FieldSkillSet(英雄图鉴光环弹窗)
return FieldSkillSet[p.uuid]?.name ?? "";
}
/** 渲染主功能说明 */
private renderMainInfo(p: SInfoParams) {
if (!this.main_info || !this.main_info.isValid) return;
// 构建 CardConfig 用于描述生成
const card = this.buildCardConfig(p);
if (card) {
this.main_info.string = buildCardDescRich(card);
} else {
// 兜底:直接展示配置中的 info 字段
const info = this.resolveInfoText(p);
this.main_info.string = info || "暂无描述";
}
}
/** 构建 CardConfig 供描述生成器使用 */
private buildCardConfig(p: SInfoParams): CardConfig | null {
// 优先从技能卡池查
const skillCard = SkillCardList.find(c => c.uuid === p.uuid || c.skill === p.uuid);
if (skillCard) return skillCard;
// 手动构建
return {
uuid: p.uuid,
type: 2, // CardType.Skill
cost: 0,
weight: 0,
kind: 2, // CKind.Skill
icon: p.card_icon,
name: p.skill_name,
field: p.field,
trigger_type: p.trigger_type,
} as CardConfig;
}
/** 解析主功能说明纯文本(兜底) */
private resolveInfoText(p: SInfoParams): string {
const skillCard = SkillCardList.find(c => c.uuid === p.uuid || c.skill === p.uuid);
if (skillCard?.info) return skillCard.info;
const skillUuid = skillCard?.skill ?? p.uuid;
return SkillSet[skillUuid]?.info ?? "";
}
/** 构建指定等级的提升信息 */
private buildLvUpgradeInfo(p: SInfoParams, targetLv: number): LvUpgradeInfo {
const unlocked = p.skill_lv >= targetLv;
const lvText = `Lv.${targetLv}`;
const statusText = unlocked ? "已激活" : `${targetLv}级激活`;
return { unlocked, lvText, statusText };
}
/** 渲染单个等级面板 */
private renderLvPanel(info: LvUpgradeInfo, statusLabel: Label | null, descRich: RichText | null) {
if (statusLabel && statusLabel.isValid) {
statusLabel.string = info.statusText;
// 已激活显示绿色,未激活显示灰色
statusLabel.color = info.unlocked
? new Color(0, 255, 10, 255)
: new Color(150, 150, 150, 255);
}
if (descRich && descRich.isValid) {
descRich.string = info.unlocked ? "" : "未解锁";
}
}
/** 设置 Label 文本 */
private setLabel(label: Label | null, text: string) {
if (label && label.isValid) {
label.string = text;
}
}
/**
* 更新未激活图标背景显隐
* @param unlocked 技能是否已激活
*/
private updateInactiveBg(unlocked: boolean) {
if (this.inactive_bg && this.inactive_bg.isValid) {
this.inactive_bg.active = !unlocked;
}
}
/** ECS 组件移除时销毁节点 */
reset() {
if (this.node && this.node.isValid) {
this.node.destroy();
}
}
}