Files
pixelheros/assets/script/game/map/MSkillBoxComp.ts
pan e76cba7933 feat(map): 新增固定波次技能三选一弹窗系统
1.  新增MSkillBoxComp弹窗组件,实现固定波次触发的技能卡选择功能
2.  新增SkillBoxCardConfig配置与SkillBoxPool技能池,支持按波次配置技能
3.  重构MissionCardComp,将技能卡抽取改为固定波次弹窗触发
4.  扩展SingletonModuleComp与MissionComp,添加技能刷新次数持久化逻辑
5.  优化MissSkillsComp,新增SkillBox专属技能加载流程
6.  修复SkillBoxComp,支持自定义技能参数覆盖
7.  调整UIConfig与CardSet配置,适配新的技能卡流程
2026-06-03 16:36:22 +08:00

267 lines
10 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 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("广告功能即将上线");
}
}