/** * @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(); } } }