feat(map): 新增固定波次技能三选一弹窗系统
1. 新增MSkillBoxComp弹窗组件,实现固定波次触发的技能卡选择功能 2. 新增SkillBoxCardConfig配置与SkillBoxPool技能池,支持按波次配置技能 3. 重构MissionCardComp,将技能卡抽取改为固定波次弹窗触发 4. 扩展SingletonModuleComp与MissionComp,添加技能刷新次数持久化逻辑 5. 优化MissSkillsComp,新增SkillBox专属技能加载流程 6. 修复SkillBoxComp,支持自定义技能参数覆盖 7. 调整UIConfig与CardSet配置,适配新的技能卡流程
This commit is contained in:
266
assets/script/game/map/MSkillBoxComp.ts
Normal file
266
assets/script/game/map/MSkillBoxComp.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
/**
|
||||
* @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("广告功能即将上线");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user