Files
pixelheros/assets/script/game/map/SinfoComp.ts
pan 434ab91f81 feat(SinfoComp): 新增信息弹窗关闭按钮功能
1. 给SInfoBox预制体添加关闭按钮配置项
2. 新增close_btn节点绑定与自动兜底查找逻辑
3. 重构全局触摸关闭逻辑,改为精确判断触点位置
4. 分离弹窗自身点击和关闭按钮点击的事件处理
5. 统一添加按钮点击音效播放逻辑
2026-08-27 14:52:50 +08:00

376 lines
14 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. 展示 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, 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 { 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;
/** 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;
/** 关闭按钮节点 */
@property({ type: Node, displayName: "关闭按钮" })
private close_btn: 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);
}
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);
}
// 注销全局触摸监听
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 onCloseClick(event: EventTouch) {
// 阻止冒泡,避免同时触发 onSelfTouch / onGlobalTouch 双重关闭
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() {
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() {
if (this.node && this.node.isValid) {
this.node.destroy();
}
}
}