feat(UI): 添加技能信息弹窗功能
1. 新增UIID.SInfo枚举项与弹窗配置 2. 为技能图标组件添加点击弹窗逻辑 3. 实现完整的技能信息弹窗UI组件,支持展示技能详情、等级提升说明 4. 支持点击外部区域关闭弹窗,防止重复绑定点击事件
This commit is contained in:
@@ -23,6 +23,8 @@ export enum UIID {
|
||||
// Heros,
|
||||
// Talents,
|
||||
Mission,
|
||||
/** 技能信息弹窗(技能小图标点击弹出) */
|
||||
SInfo,
|
||||
}
|
||||
|
||||
/** 打开界面方式的配置数据 */
|
||||
@@ -38,5 +40,6 @@ export var UIConfigData: { [key: number]: UIConfig } = {
|
||||
// [UIID.Heros]: { layer: LayerType.UI, prefab: "gui/element/heros" },
|
||||
// [UIID.Talents]: { layer: LayerType.UI, prefab: "gui/element/talents" },
|
||||
[UIID.Mission]: { layer: LayerType.UI, prefab: "gui/element/mission" },
|
||||
[UIID.SInfo]: { layer: LayerType.PopUp, prefab: "gui/element/SInfoBox" },
|
||||
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import { _decorator, Node, Sprite, Label } from "cc";
|
||||
import { _decorator, Node, Sprite, Label, NodeEventType } 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 } from "../common/config/CardSet";
|
||||
import { EquipPoolList } from "../common/config/EquipSet";
|
||||
import { SkillSet, FieldSkillSet } from "../common/config/SkillSet";
|
||||
import { FieldEntry } from "../common/config/heroSet";
|
||||
import { UIID } from "../common/config/GameUIConfig";
|
||||
import { smc } from "../common/SingletonModuleComp";
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
@@ -53,6 +55,42 @@ export class SSkillBoxComp extends CCComp {
|
||||
/** 英雄图鉴模式下的图标来源:skill=SkillSet,field=FieldSkillSet;undefined 表示装备卡模式 */
|
||||
private hero_icon_source?: "skill" | "field";
|
||||
|
||||
/** 点击事件是否已注册(防止重复绑定) */
|
||||
private _clickRegistered: boolean = false;
|
||||
|
||||
/**
|
||||
* 注册点击事件(幂等,可安全重复调用)
|
||||
*/
|
||||
private registerClick() {
|
||||
if (this._clickRegistered) return;
|
||||
this._clickRegistered = true;
|
||||
this.node.on(NodeEventType.TOUCH_END, this.onClick, this);
|
||||
}
|
||||
|
||||
/** 点击:弹出技能信息框 */
|
||||
private onClick() {
|
||||
oops.audio.playEffect("music/button");
|
||||
oops.gui.open(UIID.SInfo, {
|
||||
uuid: this.s_uuid,
|
||||
card_lv: this.card_lv,
|
||||
card_icon: this.card_icon,
|
||||
skill_name: this.resolveSkillName(),
|
||||
field: this.field,
|
||||
trigger_type: this.trigger_type,
|
||||
});
|
||||
}
|
||||
|
||||
onLoad() {
|
||||
this.registerClick();
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
super.onDestroy();
|
||||
if (this.node && this.node.isValid) {
|
||||
this.node.off(NodeEventType.TOUCH_END, this.onClick, this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化技能卡效果:
|
||||
* 1. 从 EquipPoolList 查询装备卡的触发配置(含 trigger_type)。
|
||||
|
||||
@@ -1,20 +1,338 @@
|
||||
import { _decorator, Node, Sprite, Label } from "cc";
|
||||
/**
|
||||
* @file SinfoComp.ts
|
||||
* @description 技能信息弹窗组件(UI 视图层)
|
||||
*
|
||||
* 职责:
|
||||
* 1. 展示技能小图标(SSkillBoxComp)点击后的详细信息弹窗。
|
||||
* 2. 展示技能名称、当前等级、图标、主功能说明。
|
||||
* 3. 展示 2 级 / 3 级 激活状态(已激活显示"已激活",未激活显示"x级激活")
|
||||
* 及相对上一档的能力提升说明。
|
||||
* 4. 点击弹窗外部区域关闭。
|
||||
*
|
||||
* 使用(oops.gui 模块):
|
||||
* oops.gui.open(UIID.SInfo, {
|
||||
* uuid: 8701, // 卡牌/技能 UUID
|
||||
* card_lv: 1, // 当前等级
|
||||
* card_icon: "xxx", // 图标ID(可选,缺省自动解析)
|
||||
* skill_name: "战后恢复", // 名称(可选)
|
||||
* field: [...], // 驻场词条(装备卡)
|
||||
* trigger_type: 3, // 触发类型
|
||||
* });
|
||||
*
|
||||
* 依赖:
|
||||
* - oops.gui(UI 生命周期)—— open / removeByNode
|
||||
* - SkillSet / FieldSkillSet(技能/光环静态配置)
|
||||
* - SkillCardList / EquipPoolList(卡牌静态配置)
|
||||
* - HeroSkillDesc(buildCardDescRich 富文本描述生成)
|
||||
*/
|
||||
import { _decorator, Node, Label, RichText, NodeEventType, Sprite, Color, input, Input, EventTouch } 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 { EquipPoolList } from "../common/config/EquipSet";
|
||||
import { FieldEntry } from "../common/config/heroSet";
|
||||
import { buildCardDescRich } from "../common/config/HeroSkillDesc";
|
||||
import { smc } from "../common/SingletonModuleComp";
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* 技能小图标组件
|
||||
*
|
||||
* 由 MissSkillsComp.addSkill() 实例化并初始化。
|
||||
* 在战场上以图标 + 剩余次数的形式呈现。
|
||||
*/
|
||||
/** SInfo 弹窗初始化参数 */
|
||||
export interface SInfoParams {
|
||||
/** 卡牌/技能 UUID */
|
||||
uuid: number;
|
||||
/** 当前等级 */
|
||||
card_lv: number;
|
||||
/** 图标ID(可选,优先级最高) */
|
||||
card_icon?: string;
|
||||
/** 技能名称(可选) */
|
||||
skill_name?: string;
|
||||
/** 驻场词条(装备卡专用) */
|
||||
field?: FieldEntry[];
|
||||
/** 触发类型 */
|
||||
trigger_type?: CardTriggerType;
|
||||
}
|
||||
|
||||
/** 等级提升信息(用于 2 级 / 3 级面板) */
|
||||
interface LvUpgradeInfo {
|
||||
/** 是否已激活 */
|
||||
unlocked: boolean;
|
||||
/** 等级标签文本(如 "Lv.2") */
|
||||
lvText: string;
|
||||
/** 激活状态文本("已激活" 或 "2级激活") */
|
||||
statusText: string;
|
||||
/** 能力提升富文本描述 */
|
||||
desc: 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;
|
||||
|
||||
/** 2级激活状态标签 */
|
||||
@property({ type: Label, displayName: "2级激活状态" })
|
||||
private lv2_status: Label = null;
|
||||
|
||||
/** 2级提升说明(富文本) */
|
||||
@property({ type: RichText, displayName: "2级提升说明" })
|
||||
private lv2_desc: RichText = null;
|
||||
|
||||
/** 3级激活状态标签 */
|
||||
@property({ type: Label, displayName: "3级激活状态" })
|
||||
private lv3_status: Label = null;
|
||||
|
||||
/** 3级提升说明(富文本) */
|
||||
@property({ type: RichText, displayName: "3级提升说明" })
|
||||
private lv3_desc: RichText = null;
|
||||
|
||||
// ======================== 运行时状态 ========================
|
||||
|
||||
/** 弹窗参数 */
|
||||
private params: SInfoParams | null = null;
|
||||
|
||||
// ======================== 生命周期 ========================
|
||||
|
||||
onLoad() {
|
||||
// 点击弹窗自身(BlockInputEvents 拦截)不关闭,仅点击外部遮罩关闭
|
||||
this.node.on(NodeEventType.TOUCH_END, this.onSelfTouch, this);
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
super.onDestroy();
|
||||
if (this.node && this.node.isValid) {
|
||||
this.node.off(NodeEventType.TOUCH_END, this.onSelfTouch, 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(() => {
|
||||
input.on(Input.EventType.TOUCH_END, this.onGlobalTouch, this);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/** 点击弹窗自身:阻止冒泡,不关闭 */
|
||||
private onSelfTouch(event: EventTouch) {
|
||||
event.propagationStopped = true;
|
||||
}
|
||||
|
||||
/** 全局触摸结束:点击弹窗外部关闭 */
|
||||
private onGlobalTouch() {
|
||||
// 若点击落在弹窗内部(被 BlockInputEvents 拦截)则不会走到这里
|
||||
this.close();
|
||||
}
|
||||
|
||||
/** 关闭弹窗 */
|
||||
private close() {
|
||||
oops.gui.removeByNode(this.node);
|
||||
}
|
||||
|
||||
// ======================== 渲染逻辑 ========================
|
||||
|
||||
/** 主渲染入口 */
|
||||
private render() {
|
||||
if (!this.params) return;
|
||||
const p = this.params;
|
||||
|
||||
// ---- 图标 ----
|
||||
this.renderIcon(p);
|
||||
|
||||
// ---- 名称 ----
|
||||
const name = this.resolveName(p);
|
||||
this.setLabel(this.name_label, name);
|
||||
|
||||
// ---- 当前等级 ----
|
||||
this.setLabel(this.lv_label, `Lv.${p.card_lv}`);
|
||||
|
||||
// ---- 主功能说明 ----
|
||||
this.renderMainInfo(p);
|
||||
|
||||
// ---- 2 级 / 3 级 激活信息 ----
|
||||
const lv2Info = this.buildLvUpgradeInfo(p, 2);
|
||||
const lv3Info = this.buildLvUpgradeInfo(p, 3);
|
||||
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;
|
||||
// 装备卡:从 EquipPoolList 查 icon
|
||||
const equip = EquipPoolList.find(c => c.uuid === p.uuid);
|
||||
if (equip?.icon) return equip.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;
|
||||
return SkillSet[skillUuid]?.icon;
|
||||
}
|
||||
|
||||
/** 解析技能名称 */
|
||||
private resolveName(p: SInfoParams): string {
|
||||
if (p.skill_name) return p.skill_name;
|
||||
const equip = EquipPoolList.find(c => c.uuid === p.uuid);
|
||||
if (equip?.name) return equip.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;
|
||||
return SkillSet[skillUuid]?.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 equip = EquipPoolList.find(c => c.uuid === p.uuid);
|
||||
if (equip) return equip;
|
||||
// 其次从技能卡池查
|
||||
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
|
||||
card_lv: p.card_lv,
|
||||
icon: p.card_icon,
|
||||
name: p.skill_name,
|
||||
field: p.field,
|
||||
trigger_type: p.trigger_type,
|
||||
} as CardConfig;
|
||||
}
|
||||
|
||||
/** 解析主功能说明纯文本(兜底) */
|
||||
private resolveInfoText(p: SInfoParams): string {
|
||||
const equip = EquipPoolList.find(c => c.uuid === p.uuid);
|
||||
if (equip?.info) return equip.info;
|
||||
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.card_lv >= targetLv;
|
||||
const lvText = `Lv.${targetLv}`;
|
||||
const statusText = unlocked ? "已激活" : `${targetLv}级激活`;
|
||||
|
||||
// 能力提升描述:从对应等级的装备卡配置中提取
|
||||
const desc = this.buildLvDesc(p, targetLv);
|
||||
return { unlocked, lvText, statusText, desc };
|
||||
}
|
||||
|
||||
/** 构建指定等级的提升描述 */
|
||||
private buildLvDesc(p: SInfoParams, targetLv: number): string {
|
||||
// 从装备卡池中查找同名更高档位配置
|
||||
const currentEquip = EquipPoolList.find(c => c.uuid === p.uuid);
|
||||
if (currentEquip) {
|
||||
const higher = EquipPoolList.find(c =>
|
||||
c.name === currentEquip.name && c.card_lv === targetLv
|
||||
);
|
||||
if (higher) {
|
||||
return buildCardDescRich(higher);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
// 技能卡:从技能卡池查找同技能更高档位
|
||||
const currentCard = SkillCardList.find(c => c.uuid === p.uuid || c.skill === p.uuid);
|
||||
if (currentCard) {
|
||||
const skillUuid = currentCard.skill ?? currentCard.uuid;
|
||||
const higher = SkillCardList.find(c =>
|
||||
(c.skill ?? c.uuid) === skillUuid && c.card_lv === targetLv
|
||||
);
|
||||
if (higher) {
|
||||
return buildCardDescRich(higher);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/** 渲染单个等级面板 */
|
||||
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.desc || (info.unlocked ? "" : "未解锁");
|
||||
}
|
||||
}
|
||||
|
||||
/** 设置 Label 文本 */
|
||||
private setLabel(label: Label | null, text: string) {
|
||||
if (label && label.isValid) {
|
||||
label.string = text;
|
||||
}
|
||||
}
|
||||
|
||||
/** ECS 组件移除时销毁节点 */
|
||||
reset() {
|
||||
|
||||
Reference in New Issue
Block a user