/** * @file MSkillBoxComp.ts * @description 固定波次触发的「技能卡三选一」弹窗组件(UI 视图层 + 流程控制) * * 职责: * 1. 由 MissionCardComp 在 NewWave 事件且当前波次 ∈ {1, 5, 10, 15, 20} 时 * 通过 `oops.gui.open(UIID.SkillBox, { wave, poolLv })` 弹出。 * 2. 从 SkillBoxPool[wave] 抽 3 张卡,渲染到 card1 / card2 / card3 三个 CardComp 槽位。 * 3. 玩家点中其中一张卡后,直接以该卡为 payload 分发 UseSkillCard 事件, * 由 MissSkillsComp 接管并实例化 SkillBoxComp(走 SkillBoxCardConfig 路径)。 * 4. 提供「刷新」按钮(扣除 refreshRemain 一次)和「看广告刷新」按钮(回调留空, * 待接入广告 SDK 后会把 adRefreshRemain 累加 +3)。 * * 关键设计: * - **强制必选**:不绑定关闭按钮,玩家只能点中 3 张卡之一才能关闭弹窗。 * - **免费领取**:cost 强制 0,dispatchEvent(UseSkillCard) 不走 CoinAdd 扣费。 * - **次数持久化**:refreshRemain / adRefreshRemain 存于 smc.vmdata.mission_data, * 跨波次保留,每局 mission 开始时由 MissionComp.data_init() 重置为 1/0。 * * 依赖: * - CardComp —— 复用其渲染/交互逻辑(免费版:card_cost=0) * - SkillBoxPool / getSkillBoxCards (CardSet) —— 5 级硬编码技能池 * - GameEvent.UseSkillCard —— 技能使用事件 * - UIID.SkillBox (GameUIConfig) —— 弹窗 prefab 路径 * - smc.vmdata.mission_data —— 刷新次数持久化 */ import { mLogger } from "../common/Logger"; import { _decorator, Label, Node, Button } 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 { GameEvent } from "../common/config/GameEvent"; import { oops } from "db://oops-framework/core/Oops"; import { smc } from "../common/SingletonModuleComp"; import { getSkillBoxCards, SkillBoxCardConfig } from "../common/config/CardSet"; import { UIID } from "../common/config/GameUIConfig"; import { CardComp } from "./CardComp"; const { ccclass, property } = _decorator; /** * MSkillBoxComp —— 技能三选一弹窗控制器 * * 由 oops.gui.open(UIID.SkillBox, { wave, poolLv }) 实例化。 */ @ccclass('MSkillBoxComp') @ecs.register('MSkillBoxComp', false) export class MSkillBoxComp extends CCComp { /** 是否启用调试日志 */ private debugMode: boolean = true; // ======================== 编辑器绑定节点 ======================== /** 卡牌槽位 1 节点(需挂 CardComp) */ @property(Node) card1: Node = null! /** 卡牌槽位 2 节点(需挂 CardComp) */ @property(Node) card2: Node = null! /** 卡牌槽位 3 节点(需挂 CardComp) */ @property(Node) card3: Node = null! /** 刷新按钮(消耗 1 次 refreshRemain 重抽 3 张) */ @property(Node) refreshBtn: Node = null! /** 看广告刷新按钮(回调留空,后续接入广告 SDK 后累加 adRefreshRemain +3) */ @property(Node) adRefreshBtn: Node = null! /** 刷新次数显示标签(可选,显示 "1/1" 形式) */ @property(Label) refreshCountLabel: Label = null! // ======================== 运行时状态 ======================== /** 当前波次(1/5/10/15/20) */ private currentWave: number = 0; /** 当前卡池等级(预留扩展,目前未使用) */ private currentPoolLv: number = 1; /** 3 个 CardComp 子控制器引用(有序) */ private cardComps: CardComp[] = []; /** 是否已派发 UseSkillCard(防重复触发关闭) */ private hasPicked: boolean = false; // ======================== 生命周期 ======================== onLoad() { // 监听任务结束,自动关闭弹窗(避免玩家关游戏时残留) oops.message.on(GameEvent.MissionEnd, this.onMissionEnd, this); this.bindEvents(); } onAdded(args: { wave?: number; poolLv?: number }) { this.currentWave = Math.max(0, Math.floor(Number(args?.wave ?? 0))); this.currentPoolLv = Math.max(1, Math.floor(Number(args?.poolLv ?? 1))); this.hasPicked = false; this.cacheCardComps(); this.rollAndRender(); this.refreshRefreshCountUI(); mLogger.log(this.debugMode, "MSkillBoxComp", "opened", { wave: this.currentWave, poolLv: this.currentPoolLv, refreshRemain: this.getRefreshRemain(), adRefreshRemain: this.getAdRefreshRemain(), }); } onDestroy() { super.onDestroy(); this.unbindEvents(); oops.message.off(GameEvent.MissionEnd, this.onMissionEnd, this); } onMissionEnd() { // 任务结束时强制关闭弹窗 oops.gui.remove(UIID.SkillBox); } init() { // 弹窗组件无需额外 init,onAdded 阶段完成所有初始化 } update(dt: number) { // 弹窗无帧更新逻辑 } reset() { // ECS 组件移除时清理 } // ======================== 内部:CardComp 缓存 ======================== /** * 缓存 3 个卡槽上的 CardComp 引用。 * 与 MissionCardComp.cacheCardComps() 同源实现,确保即开即用。 */ private cacheCardComps() { this.cardComps = []; const slots: (Node | null)[] = [this.card1, this.card2, this.card3]; for (let i = 0; i < slots.length; i++) { const node = slots[i]; if (!node) continue; const comp = node.getComponent(CardComp) || node.addComponent(CardComp); this.cardComps.push(comp); } } // ======================== 内部:抽卡与渲染 ======================== /** * 从 SkillBoxPool[currentWave] 抽 3 张并渲染到 3 个卡槽。 * 渲染时强制 cost=0,触发 free 路径。 */ private rollAndRender() { const cards = getSkillBoxCards(this.currentWave, 3); for (let i = 0; i < this.cardComps.length; i++) { const comp = this.cardComps[i]; if (!comp) continue; if (i < cards.length) { comp.applyDrawCard(cards[i]); comp.card_cost = 0; // 强制免费 } else { comp.clearBySystem(); } } } /** 重新抽 3 张(玩家点 refreshBtn) */ private reroll() { if (this.hasPicked) return; this.rollAndRender(); mLogger.log(this.debugMode, "MSkillBoxComp", "reroll", { refreshRemain: this.getRefreshRemain(), adRefreshRemain: this.getAdRefreshRemain(), }); } // ======================== 内部:刷新次数持久化 ======================== /** 读取当前总可用刷新次数(普通 + 广告奖励) */ private getRefreshRemain(): number { const d = smc.vmdata?.mission_data; if (!d) return 0; return Math.max(0, Math.floor(Number(d.skill_box_refresh_remain ?? 0))); } /** 读取广告奖励次数(待接入 SDK 后使用) */ private getAdRefreshRemain(): number { const d = smc.vmdata?.mission_data; if (!d) return 0; return Math.max(0, Math.floor(Number(d.skill_box_ad_refresh_remain ?? 0))); } /** 消耗一次刷新(优先用普通次数,再用广告奖励) */ private consumeRefresh(): boolean { const d = smc.vmdata?.mission_data; if (!d) return false; const remain = this.getRefreshRemain(); const adRemain = this.getAdRefreshRemain(); if (remain + adRemain <= 0) return false; if (remain > 0) { d.skill_box_refresh_remain = remain - 1; } else { d.skill_box_ad_refresh_remain = adRemain - 1; } return true; } /** 同步刷新次数显示 */ private refreshRefreshCountUI() { if (!this.refreshCountLabel) return; const total = this.getRefreshRemain() + this.getAdRefreshRemain(); this.refreshCountLabel.string = `${total}`; } // ======================== 内部:玩家选择 ======================== /** * 监听每个 CardComp 的 UseSkillCard 派发,以关闭弹窗。 * 由于 CardComp.useCard 内部已经 dispatchEvent(UseSkillCard, payload), * 这里只需监听事件并在识别为本弹窗的卡时关闭。 */ private onSkillCardUsed(event: string, args: any) { if (this.hasPicked) return; const payload = args ?? event; if (!payload) return; const uuid = Number(payload?.uuid ?? 0); // 仅处理本弹窗抽出的 SkillBox 卡(uuid >= 9000) if (uuid < 9000) return; this.hasPicked = true; mLogger.log(this.debugMode, "MSkillBoxComp", "player picked skill card", { uuid }); oops.gui.remove(UIID.SkillBox); } // ======================== 按钮事件 ======================== private bindEvents() { this.refreshBtn?.on(Button.EventType.CLICK, this.onRefreshClick, this); this.adRefreshBtn?.on(Button.EventType.CLICK, this.onAdRefreshClick, this); oops.message.on(GameEvent.UseSkillCard, this.onSkillCardUsed, this); } private unbindEvents() { this.refreshBtn?.off(Button.EventType.CLICK, this.onRefreshClick, this); this.adRefreshBtn?.off(Button.EventType.CLICK, this.onAdRefreshClick, this); oops.message.off(GameEvent.UseSkillCard, this.onSkillCardUsed, this); } private onRefreshClick() { if (this.hasPicked) return; if (this.getRefreshRemain() + this.getAdRefreshRemain() <= 0) { oops.gui.toast("刷新次数已用完,请观看广告获取"); return; } if (!this.consumeRefresh()) return; this.reroll(); this.refreshRefreshCountUI(); } private onAdRefreshClick() { // TODO: 接入广告 SDK 后,在广告播放成功回调中: // smc.vmdata.mission_data.skill_box_ad_refresh_remain += 3; // this.refreshRefreshCountUI(); // 当前为占位实现,仅打印日志提示 mLogger.log(this.debugMode, "MSkillBoxComp", "onAdRefreshClick", "TODO: 接入广告 SDK,成功后 adRefreshRemain += 3"); oops.gui.toast("广告功能即将上线"); } }