Files
pixelheros/assets/script/game/map/MissionCardComp.ts
pan 2234b9021d feat(MissionCard): 添加英雄卡池关闭按钮功能
新增closeHeros节点绑定与点击回调,实现卡池收起逻辑,同步更新卡池显示状态与英雄信息面板。
优化进入准备阶段的交互表现,确保卡池初始状态与手动操作一致。
2026-07-21 16:12:38 +08:00

1295 lines
51 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 MissionCardComp.ts
* @description 卡牌系统核心控制器(战斗 UI 层 + 业务逻辑层)
*
* 职责:
* 1. **卡牌分发管理** —— 从卡池抽取 3 张卡,分发到 3 个 CardComp 槽位。
* 抽卡规则:场上已有英雄时按权重混合"对应英雄的升级卡" + "其他英雄卡" + "刷新卡"
* 场上无英雄时仅抽取英雄卡和刷新卡。
* 2. **金币费用管理** —— 抽卡费用refreshCost的扣除。
* 3. **英雄数量上限校验** —— 在 UseHeroCard 事件的 guard 阶段判断是否允许再召唤英雄。
* 4. **场上英雄信息面板HInfoComp 列表)同步** ——
* 英雄上场时实例化面板,死亡时移除,定时刷新属性显示。
* 5. **特殊卡执行** —— 处理英雄升级卡SpecialUpgrade按 UUID 精确升级)和
* 英雄刷新卡SpecialRefresh
* 6. **准备/战斗阶段切换** —— 控制卡牌面板的 展开/收起 动画。
*
* 关键设计:
* - 3 个 CardComp 通过 cacheCardComps() 映射为有序数组 cardComps[]
* 之后所有分发、清空操作均通过此数组进行。
* - buildDrawCards() 动态合并升级卡池和基础卡池后抽取 3 张,不足时循环补齐。
* - 英雄上限校验onUseHeroCard采用 guard/cancel 模式:
* CardComp 发出 UseHeroCard 事件并传入 guard 对象,
* 本组件可通过 guard.cancel=true 阻止使用。
*
* 历史:
* 旧版本曾包含"卡池等级poolLv"机制和"三合一合成腾位"判断,已全部移除:
* - 卡牌不再分级,所有英雄卡统一 lv1。
* - 英雄升级仅通过升级卡SpecialUpgrade触发按 UUID 精确升级场上对应英雄。
*
* 依赖:
* - CardComp —— 单卡槽位
* - HInfoComp —— 英雄信息面板
* - CardSet 模块 —— 卡池配置、抽卡规则、特殊卡数据
* - HeroAttrsComp —— 英雄属性(升级)
* - smc.vmdata.mission_data —— 局内数据coin / hero_num / hero_max_num
*/
import { mLogger } from "../common/Logger";
import { _decorator, Label, Node, NodeEventType, Tween, tween, 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 { GameEvent } from "../common/config/GameEvent";
import { CardConfig, CardPoolList, CardType, drawCardsByRule, SpecialRefreshCardList, SpecialRefreshHeroType, SpecialUpgradeCardList } from "../common/config/CardSet";
import { CardComp } from "./CardComp";
import { SCardComp } from "./SCardComp";
import { oops } from "db://oops-framework/core/Oops";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { smc } from "../common/SingletonModuleComp";
import { HeroInfo, HType } from "../common/config/heroSet";
import { HeroViewComp } from "../hero/HeroViewComp";
import { FacSet, FightSet } from "../common/config/GameSet";
import { MissionEconomy } from "./MissionEconomy";
import { UIID } from "../common/config/GameUIConfig";
const { ccclass, property } = _decorator;
/**
* MissionCardComp —— 卡牌系统核心控制器
*
* 管理 3 个卡牌槽位的抽卡分发、金币费用、
* 英雄上限校验、场上英雄信息面板同步以及特殊卡执行。
*/
@ccclass('MissionCardComp')
@ecs.register('MissionCard', false)
export class MissionCardComp extends CCComp {
/** 是否启用调试日志 */
private debugMode: boolean = false;
/** 卡牌槽位宽度(像素),用于水平等距布局 */
private readonly cardWidth: number = 175;
/** 按钮正常缩放 */
private readonly buttonNormalScale: number = 1;
/** 按钮按下缩放 */
private readonly buttonPressScale: number = 0.94;
/** 按钮弹起缩放(峰值) */
private readonly buttonClickScale: number = 1.06;
/** 抽卡(刷新)费用 */
refreshCost: number = FightSet.REFRESH_COST;
/** 卡牌面板展开/收起动画时长(秒) */
cardsPanelMoveDuration: number = 0.2;
// ======================== 编辑器绑定节点 ========================
/** 卡牌面板根节点(战斗阶段收起,准备阶段展开) */
@property(Node)
cards_node: Node = null!
/** 卡牌槽位 1 节点 */
@property(Node)
card1: Node = null!
/** 卡牌槽位 2 节点 */
@property(Node)
card2: Node = null!
/** 卡牌槽位 3 节点 */
@property(Node)
card3: Node = null!
/** 卡牌槽位 4 节点 */
@property(Node)
card4: Node = null!
/** 抽卡(刷新)按钮节点 */
@property(Node)
cards_chou: Node = null!
/** 英雄卡牌池cards_node显示隐藏 */
@property(Node)
showHeros: Node = null!
/** 关闭英雄卡池按钮(仅负责收起 cards_node */
@property(Node)
closeHeros: Node = null!
/** 卡池升级按钮节点(已废弃,保留节点引用防止 editor 报错) */
@property(Node)
cards_up: Node = null!
/** 金币显示节点(含 icon + num 子节点) */
@property(Node)
coins_node: Node = null!
/** 卡池等级显示节点(已废弃,保留节点引用) */
@property(Node)
pool_lv_node: Node = null!
/** 英雄数量显示节点(含 icon + num 子节点) */
@property(Node)
hero_num_node: Node = null!
/** 技能卡牌三选一弹窗节点 */
@property(Node)
skill_card_node: Node = null!
/**技能卡槽1节点 */
@property(Node)
skill_card1: Node = null!
/**技能卡槽2节点 */
@property(Node)
skill_card2: Node = null!
/**技能卡槽3节点 */
@property(Node)
skill_card3: Node = null!
/**技能刷新按钮节点 */
@property(Node)
skill_refresh: Node = null!
/**技能广告刷新按钮节点 */
@property(Node)
skill_ad_refresh: Node = null!
/**可用刷新数显示节点 */
@property(Node)
skill_refresh_num_node: Node = null!
// ======================== 运行时状态 ========================
/** 三个槽位对应的 CardComp 控制器缓存(有序数组) */
private cardComps: CardComp[] = [];
/** 技能卡槽控制器缓存 */
private skillCardComps: SCardComp[] = [];
/** 是否已缓存卡牌面板基准缩放 */
private hasCachedCardsBaseScale: boolean = false;
/** 卡牌面板基准缩放(从场景读取) */
private cardsBaseScale: Vec3 = new Vec3(1, 1, 1);
/** 卡牌面板展开态缩放 */
private cardsShowScale: Vec3 = new Vec3(1, 1, 1);
/** 卡牌面板收起态缩放scale=0 隐藏) */
private cardsHideScale: Vec3 = new Vec3(0, 0, 1);
/** 卡牌原始定位点 */
private cardsPos = [-220, 0, 220]
/**
* 必出升级卡的轮询索引:每次刷新按场上可升级英雄 UUID 升序轮询选 1 张。
* 每次 buildDrawCards 取模更新,保证多次刷新时升级卡依次切换。
*/
private upgradeRotationIndex: number = 0;
// ======================== 生命周期 ========================
/**
* 组件加载:
* 1. 绑定生命周期事件和按钮交互事件。
* 2. 缓存 3 个 CardComp 子控制器引用。
* 3. 计算并设置槽位水平布局。
* 4. 初始化卡牌面板缩放参数。
*/
onLoad() {
this.bindEvents();
this.cacheCardComps();
this.layoutCardSlots();
this.initCardsPanelPos();
mLogger.log(this.debugMode, "MissionCardComp", "onLoad init", {
slots: this.cardComps.length,
});
}
/** 组件销毁时解绑所有事件并清理英雄信息面板 */
onDestroy() {
super.onDestroy();
if (this.cards_chou && this.cards_chou.isValid) {
this.cards_chou.off(NodeEventType.TOUCH_START, this.onDrawTouchStart, this);
this.cards_chou.off(NodeEventType.TOUCH_END, this.onDrawTouchEnd, this);
this.cards_chou.off(NodeEventType.TOUCH_CANCEL, this.onDrawTouchCancel, this);
}
if (this.showHeros && this.showHeros.isValid) {
this.showHeros.off(NodeEventType.TOUCH_END, this.onShowHerosClick, this);
}
if (this.closeHeros && this.closeHeros.isValid) {
this.closeHeros.off(NodeEventType.TOUCH_END, this.onCloseHerosClick, this);
}
this.unbindEvents();
}
/** 外部初始化入口(由 CardController 调用) */
init() {
this.onMissionStart();
}
/**
* 任务开始:
* 1. 进入准备阶段(展开卡牌面板)。
* 2. 初始化局内数据(金币、英雄数量上限)。
* 3. 清空旧英雄信息面板和卡牌槽位。
* 4. 重置按钮状态和 UI 显示。
* 5. 执行首次抽卡并分发到 3 个槽位。
*/
onMissionStart() {
this.enterPreparePhase();
const missionData = this.getMissionData();
if (missionData) {
missionData.coin = Math.max(0, Math.floor(missionData.coin ?? 0));
missionData.hero_num = 0;
missionData.hero_max_num = FightSet.HERO_MAX_NUM;
missionData.hero_extend_max_num = FightSet.HERO_MAX_NUM + 1;
}
// 确保卡牌组件列表已被正确缓存
if (!this.cardComps || this.cardComps.length === 0) {
this.cacheCardComps();
}
this.layoutCardSlots();
this.clearAllCards();
this.resetButtonScale(this.cards_chou);
this.updateCoinAndCostUI();
this.updateHeroNumUI(false, false);
if (this.node && this.node.isValid) {
this.node.active = true;
}
const cards = this.buildDrawCards();
this.dispatchCardsToSlots(cards);
mLogger.log(this.debugMode, "MissionCardComp", "mission start");
}
/** 任务结束:清空 3 槽 + 英雄面板并隐藏整个节点 */
onMissionEnd() {
this.clearAllCards();
if (this.node && this.node.isValid) {
this.node.active = false;
}
}
start() {
}
/**
* 帧更新:每 0.15 秒刷新一次场上英雄信息面板(降频)。
* 检测已死亡 / 已失效的面板并移除,刷新存活面板属性。
*/
update(dt: number) {
}
/** 关闭面板(不销毁数据模型,仅隐藏) */
close() {
if (this.node && this.node.isValid) {
this.node.active = false;
}
}
// ======================== 事件绑定 ========================
/**
* 绑定所有事件监听:
* - 节点级事件MissionStart / MissionEnd / FightStart
* - 全局消息CoinAdd / MasterCalled / HeroDead / UseHeroCard / UseSpecialCard
* - 按钮触控抽卡cards_chou
*/
private bindEvents() {
/** 生命周期事件(节点级) */
this.on(GameEvent.MissionStart, this.onMissionStart, this);
this.on(GameEvent.MissionEnd, this.onMissionEnd, this);
this.on(GameEvent.FightStart, this.onFightStart, this);
this.on("PhasePrepareStart", this.onPhasePrepareStart, this);
oops.message.on(GameEvent.CoinAdd, this.onCoinAdd, this);
oops.message.on(GameEvent.MasterCalled, this.onMasterCalled, this);
oops.message.on(GameEvent.HeroDead, this.onHeroDead, this);
oops.message.on(GameEvent.HeroSell, this.onHeroSell, this);
oops.message.on(GameEvent.UseHeroCard, this.onUseHeroCard, this);
oops.message.on(GameEvent.UseSkillCard, this.onUseSkillCard, this);
oops.message.on(GameEvent.UseSpecialCard, this.onUseSpecialCard, this);
oops.message.on(GameEvent.ShowSmallTip, this.onShowSmallTip, this);
/** 按钮触控事件:抽卡 */
this.cards_chou?.on(NodeEventType.TOUCH_START, this.onDrawTouchStart, this);
this.cards_chou?.on(NodeEventType.TOUCH_END, this.onDrawTouchEnd, this);
this.cards_chou?.on(NodeEventType.TOUCH_CANCEL, this.onDrawTouchCancel, this);
/** 英雄卡池显示/隐藏切换按钮 */
this.showHeros?.on(NodeEventType.TOUCH_END, this.onShowHerosClick, this);
/** 关闭英雄卡池按钮 */
this.closeHeros?.on(NodeEventType.TOUCH_END, this.onCloseHerosClick, this);
/** 技能卡刷新按钮 */
this.skill_refresh?.on(NodeEventType.TOUCH_START, this.onSkillDrawTouchStart, this);
this.skill_refresh?.on(NodeEventType.TOUCH_END, this.onSkillDrawTouchEnd, this);
this.skill_refresh?.on(NodeEventType.TOUCH_CANCEL, this.onSkillDrawTouchCancel, this);
this.skill_ad_refresh?.on(NodeEventType.TOUCH_START, this.onSkillAdDrawTouchStart, this);
this.skill_ad_refresh?.on(NodeEventType.TOUCH_END, this.onSkillAdDrawTouchEnd, this);
this.skill_ad_refresh?.on(NodeEventType.TOUCH_CANCEL, this.onSkillAdDrawTouchCancel, this);
}
// ======================== 事件回调 ========================
/**
* 金币变化事件回调:
* 仅负责 UI 更新和动画表现。数据更新已由 MissionEconomy 统一处理。
*/
private onCoinAdd(event: string, args: any) {
const payload = args ?? event;
const v = typeof payload === 'number' ? payload : (payload?.delta ?? payload?.value ?? 0);
this.updateCoinAndCostUI();
if (v !== 0) {
this.playCoinChangeAnim(v > 0);
}
}
/** 战斗开始:保留卡牌面板,允许玩家在战斗阶段继续抽卡和召唤英雄 */
private onFightStart() {
this.enterBattlePhase();
// 第一次进入战斗阶段关闭guide4
if (!smc.finish_guides.includes(4)) {
smc.finish_guides.push(4);
oops.gui.remove(UIID.Guide4);
}
}
private onShowSmallTip(event: string, args: any) {
const type = args as string;
this.showSmallTip(type as any);
}
public showSmallTip(type: "refresh_coin" | "buy_coin" | "hero_full") {
let targetNode: Node | null = null;
switch (type) {
case "refresh_coin":
targetNode = this.cards_chou;
break;
case "buy_coin":
targetNode = this.coins_node;
break;
case "hero_full":
targetNode = this.hero_num_node;
break;
}
if (targetNode && targetNode.isValid) {
const tipNode = targetNode.getChildByName("smalltip");
if (tipNode) {
tipNode.active = true;
Tween.stopAllByTarget(tipNode);
// 设置初始状态:缩放为 0
tipNode.setScale(new Vec3(0, 0, 1));
tween(tipNode)
// 1. 弹出动画(微放大再回弹)
.to(0.15, { scale: new Vec3(1.1, 1.1, 1) }, { easing: 'quadOut' })
.to(0.05, { scale: new Vec3(1, 1, 1) })
// 2. 停留 1 秒
.delay(1)
// 3. 缩小消失动画
.to(0.15, { scale: new Vec3(0, 0, 1) }, { easing: 'quadIn' })
.call(() => {
if (tipNode && tipNode.isValid) tipNode.active = false;
})
.start();
}
}
}
private onPhasePrepareStart() {
this.updateHeroNumUI(true, true);
}
/**
* 技能卡入场动画:每个卡槽从场景基准位置下方 80px 处升起,
* 终止位置 = 场景编辑器位置(保持初始不做位移的约束),
* 多张依次错开 staggerDelay 形成"依次飞入"效果。
*/
private readonly skillEnterOffsetY: number = -80;
private readonly skillEnterDuration: number = 0.25;
private readonly skillEnterStagger: number = 0.08;
private playSkillCardEnterAnim() {
if (!this.skillCardComps || this.skillCardComps.length === 0) return;
for (let i = 0; i < this.skillCardComps.length; i++) {
const comp = this.skillCardComps[i];
if (!comp) continue;
const node = comp.node;
if (!node || !node.isValid) continue;
// 缓存场景编辑器位置作为动画终止点(初始不做位移)
const basePos = node.getPosition();
// 起始位置 = 基准位置下移 80px
node.setPosition(basePos.x, basePos.y + this.skillEnterOffsetY, basePos.z);
Tween.stopAllByTarget(node);
tween(node)
.delay(i * this.skillEnterStagger)
.to(this.skillEnterDuration, { position: basePos }, { easing: 'quadOut' })
.start();
}
}
private showSkillCardPopup() {
if (!this.skill_card_node) return;
this.skill_card_node.active = true;
// 先分发卡牌数据(让卡面内容就绪),再做入场动画
const cards = this.buildSkillDrawCards();
this.dispatchCardsToSkillSlots(cards);
this.playSkillCardEnterAnim();
// 首次弹出技能三选一的时候弹出guide2
if (!smc.finish_guides.includes(2)) {
oops.gui.open(UIID.Guide2);
}
}
private dispatchCardsToSkillSlots(cards: CardConfig[]) {
if (!this.skillCardComps) return;
for (let i = 0; i < this.skillCardComps.length; i++) {
if (this.skillCardComps[i]) {
this.skillCardComps[i].applyDrawCard(cards[i] ?? null);
}
}
}
private onUseSkillCard(event: string, args: any) {
// 选择技能后关闭弹窗
if (this.skill_card_node && this.skill_card_node.isValid) {
this.skill_card_node.active = false;
}
// 修复:独立判断 guide2 的关闭 和 guide3 的开启
if (!smc.finish_guides.includes(2)) {
smc.finish_guides.push(2);
oops.gui.remove(UIID.Guide2);
}
if (!smc.finish_guides.includes(3)) {
oops.gui.open(UIID.Guide3);
}
// 驻场技能可能影响刷新费用(如"刷新优惠"),延迟到下一帧刷新费用 UI
this.scheduleOnce(() => this.updateCoinAndCostUI(), 0);
}
/** 解除按钮监听,避免节点销毁后回调泄漏 */
private unbindEvents() {
oops.message.off(GameEvent.CoinAdd, this.onCoinAdd, this);
oops.message.off(GameEvent.MasterCalled, this.onMasterCalled, this);
oops.message.off(GameEvent.HeroDead, this.onHeroDead, this);
oops.message.off(GameEvent.HeroSell, this.onHeroSell, this);
oops.message.off(GameEvent.UseHeroCard, this.onUseHeroCard, this);
oops.message.off(GameEvent.UseSkillCard, this.onUseSkillCard, this);
oops.message.off(GameEvent.UseSpecialCard, this.onUseSpecialCard, this);
oops.message.off(GameEvent.ShowSmallTip, this.onShowSmallTip, this);
if (this.cards_chou && this.cards_chou.isValid) {
this.cards_chou.off(NodeEventType.TOUCH_START, this.onDrawTouchStart, this);
this.cards_chou.off(NodeEventType.TOUCH_END, this.onDrawTouchEnd, this);
this.cards_chou.off(NodeEventType.TOUCH_CANCEL, this.onDrawTouchCancel, this);
}
if (this.showHeros && this.showHeros.isValid) {
this.showHeros.off(NodeEventType.TOUCH_END, this.onShowHerosClick, this);
}
if (this.closeHeros && this.closeHeros.isValid) {
this.closeHeros.off(NodeEventType.TOUCH_END, this.onCloseHerosClick, this);
}
if (this.skill_refresh && this.skill_refresh.isValid) {
this.skill_refresh.off(NodeEventType.TOUCH_START, this.onSkillDrawTouchStart, this);
this.skill_refresh.off(NodeEventType.TOUCH_END, this.onSkillDrawTouchEnd, this);
this.skill_refresh.off(NodeEventType.TOUCH_CANCEL, this.onSkillDrawTouchCancel, this);
}
if (this.skill_ad_refresh && this.skill_ad_refresh.isValid) {
this.skill_ad_refresh.off(NodeEventType.TOUCH_START, this.onSkillAdDrawTouchStart, this);
this.skill_ad_refresh.off(NodeEventType.TOUCH_END, this.onSkillAdDrawTouchEnd, this);
this.skill_ad_refresh.off(NodeEventType.TOUCH_CANCEL, this.onSkillAdDrawTouchCancel, this);
}
}
/**
* 英雄上场事件回调MasterCalled
* 为新上场英雄创建或更新信息面板,并刷新英雄数量 UI。
*/
private onMasterCalled(event: string, args: any) {
const payload = args ?? event;
const eid = Number(payload?.eid ?? 0);
const model = payload?.model as HeroAttrsComp | undefined;
mLogger.log(this.debugMode, "MissionCardComp", "onMasterCalled received payload:", { eid, hasModel: !!model });
if (!eid || !model) return;
const before = this.getAliveHeroCount();
const after = this.getAliveHeroCount();
this.updateHeroNumUI(true, after > before);
// 第一次召唤英雄后关闭guide3
// 注:首个英雄召唤后战斗自动开始(见 MissionComp不再需要 Guide4 引导点击开始按钮
if (!smc.finish_guides.includes(3)) {
smc.finish_guides.push(3);
oops.gui.remove(UIID.Guide3);
}
}
/** 英雄死亡事件回调:刷新面板列表并更新英雄数量 UI */
private onHeroDead() {
this.updateHeroNumUI(true, false);
}
/** 英雄被出售事件回调:更新英雄数量 UI */
private onHeroSell() {
this.updateHeroNumUI(true, false);
}
/**
* 使用英雄卡的 guard 校验(由 CardComp 通过 UseHeroCard 事件调用):
* - 当前英雄数 < 上限 → 允许使用。
* - 已满 → 阻止使用cancel=true弹 toast。
*
* 注意:英雄不再支持合成腾位,满员时一律阻止。
*/
private onUseHeroCard(event: string, args: any) {
const payload = args ?? event;
if (!payload) return;
const current = this.getAliveHeroCount();
this.syncMissionHeroData(current);
const heroMax = this.getMissionHeroMaxNum();
if (current >= heroMax) {
payload.cancel = true;
payload.reason = "hero_limit";
this.showSmallTip("hero_full");
this.playHeroNumDeniedAnim();
}
}
/**
* 使用特殊卡事件回调:
* - SpecialUpgrade按卡牌携带的 target_hero_eid 精确升级场上对应英雄实体。
* target_hero_eid 缺失或实体不存在时升级失败。
* - SpecialRefresh按英雄类型重新抽取英雄卡。
*/
private onUseSpecialCard(event: string, args: any) {
const payload = args ?? event;
const uuid = Number(payload?.uuid ?? 0);
const type = Number(payload?.type ?? 0) as CardType;
if (!uuid) return;
let success = false;
if (type === CardType.SpecialUpgrade) {
const template = SpecialUpgradeCardList[uuid];
if (!template) return;
const targetHeroEid = Number(payload?.target_hero_eid ?? 0);
success = this.tryUpgradeHeroByEid(targetHeroEid);
if (!success) {
oops.gui.toast(`场上没有可升级的英雄`);
}
} else if (type === CardType.SpecialRefresh) {
const card = SpecialRefreshCardList[uuid];
if (!card) return;
success = this.tryRefreshHeroCardsByEffect(card.refreshHeroType);
if (!success) oops.gui.toast("当前卡池无符合条件的英雄卡");
}
mLogger.log(this.debugMode, "MissionCardComp", "use special card", {
uuid,
type,
success
});
}
// ======================== 按钮触控回调 ========================
/** 抽卡按钮按下反馈 */
private onDrawTouchStart() {
this.playButtonPressAnim(this.cards_chou);
}
/** 抽卡按钮释放 → 执行抽卡逻辑 */
private onDrawTouchEnd() {
oops.audio.playEffect("music/button");
this.playButtonClickAnim(this.cards_chou, () => this.onClickDraw());
}
/** 抽卡按钮取消 → 恢复缩放 */
private onDrawTouchCancel() {
this.playButtonResetAnim(this.cards_chou);
}
/**
* 英雄卡池显隐切换按钮回调:
* - 切换 cards_node 的 active 状态。
* - 显示时激活 showHeros 下的 "active" 子节点,隐藏时关闭该子节点。
*/
private onShowHerosClick() {
if (!this.cards_node || !this.cards_node.isValid) return;
oops.audio.playEffect("music/button");
const visible = !this.cards_node.active;
this.cards_node.active = visible;
const activeChild = this.showHeros?.getChildByName("active");
if (activeChild) {
activeChild.active = visible;
}
}
/**
* 关闭英雄卡池按钮回调:
* - 仅负责收起 cards_node不切换展开态。
* - 同步关闭 showHeros 下的 "active" 子节点,保持与 toggle 状态一致。
* - 同步关闭已打开的英雄信息面板HInfo避免卡池收起后信息面板孤立悬浮。
*/
private onCloseHerosClick() {
if (!this.cards_node || !this.cards_node.isValid) return;
oops.audio.playEffect("music/button");
this.cards_node.active = false;
const activeChild = this.showHeros?.getChildByName("active");
if (activeChild) {
activeChild.active = false;
}
// 关闭英雄卡池时同步关闭已打开的英雄信息面板
oops.gui.remove(UIID.HInfo);
}
// ======================== 技能抽卡按钮回调 ========================
private onSkillDrawTouchStart() {
this.playButtonPressAnim(this.skill_refresh);
}
private onSkillDrawTouchEnd() {
oops.audio.playEffect("music/button");
this.playButtonClickAnim(this.skill_refresh, () => this.onClickSkillRefresh());
}
private onSkillDrawTouchCancel() {
this.playButtonResetAnim(this.skill_refresh);
}
private onSkillAdDrawTouchStart() {
this.playButtonPressAnim(this.skill_ad_refresh);
}
private onSkillAdDrawTouchEnd() {
oops.audio.playEffect("music/button");
this.playButtonClickAnim(this.skill_ad_refresh, () => this.onClickSkillAdRefresh());
}
private onSkillAdDrawTouchCancel() {
this.playButtonResetAnim(this.skill_ad_refresh);
}
private onClickSkillRefresh() {
const cost = MissionEconomy.getRefreshCost(this.refreshCost);
const success = MissionEconomy.executeRefresh(this.refreshCost);
if (!success) {
this.showSmallTip("refresh_coin");
return;
}
const cards = this.buildSkillDrawCards();
this.dispatchCardsToSkillSlots(cards);
}
private onClickSkillAdRefresh() {
// TODO: 接入广告 SDK 逻辑,目前先直接刷新
const cards = this.buildSkillDrawCards();
this.dispatchCardsToSkillSlots(cards);
}
/** 将三个卡槽节点映射为 CardComp形成固定顺序控制数组 */
private cacheCardComps() {
if (this.card4) {
this.card4.active = false;
}
const nodes = [this.card1, this.card2, this.card3];
this.cardComps = nodes
.map(node => node?.getComponent(CardComp))
.filter((comp): comp is CardComp => !!comp);
const skillNodes = [this.skill_card1, this.skill_card2, this.skill_card3];
this.skillCardComps = skillNodes
.map(node => node?.getComponent(SCardComp))
.filter((comp): comp is SCardComp => !!comp);
}
// ======================== 核心业务:抽卡 ========================
/**
* 抽卡按钮核心逻辑:
* 1. 检查金币是否足够 → 不够则 toast 提示。
* 2. 扣除费用、播放金币动画。
* 3. 重新布局槽位 → 从卡池构建 3 张卡 → 分发到槽位。
*/
private onClickDraw() {
const cost = MissionEconomy.getRefreshCost(this.refreshCost);
const success = MissionEconomy.executeRefresh(this.refreshCost);
if (!success) {
this.showSmallTip("refresh_coin");
this.updateCoinAndCostUI();
mLogger.log(this.debugMode, "MissionCardComp", "draw coin not enough", {
currentCoin: MissionEconomy.getCoin(),
cost
});
return;
}
mLogger.log(this.debugMode, "MissionCardComp", "click draw", {
cost,
leftCoin: MissionEconomy.getCoin()
});
this.layoutCardSlots();
const cards = this.buildDrawCards();
this.dispatchCardsToSlots(cards);
}
// ======================== 阶段切换 ========================
/** 缓存卡牌面板的基准缩放值(从场景初始状态读取,仅缓存一次) */
private initCardsPanelPos() {
if (!this.cards_node || !this.cards_node.isValid) return;
if (!this.hasCachedCardsBaseScale) {
const scale = this.cards_node.scale;
this.cardsBaseScale = new Vec3(scale.x, scale.y, scale.z);
this.hasCachedCardsBaseScale = true;
}
this.cardsShowScale = new Vec3(this.cardsBaseScale.x, this.cardsBaseScale.y, this.cardsBaseScale.z);
this.cardsHideScale = new Vec3(0, 0, this.cardsBaseScale.z);
}
/**
* 进入准备阶段:
* - 先将卡牌面板置为收起态,激活 showHeros 按钮可见。
* - 再触发一次 onShowHerosClick等同玩家点击 showHeros 按钮),
* 让卡池以"按钮点击"的方式展开,保持与玩家手动点击一致的交互表现。
*/
private enterPreparePhase() {
if (!this.cards_node || !this.cards_node.isValid) return;
this.initCardsPanelPos();
// 先收起面板,确保后续 onShowHerosClick 能正确切换为展开
Tween.stopAllByTarget(this.cards_node);
this.cards_node.setScale(this.cardsShowScale);
this.cards_node.active = false;
// 显式激活「显示英雄卡池」按钮本身,让玩家可见可点
if (this.showHeros && this.showHeros.isValid) {
this.showHeros.active = true;
const showActive = this.showHeros.getChildByName("active");
if (showActive) showActive.active = false;
}
if (this.cards_chou && this.cards_chou.isValid) {
const nobg = this.cards_chou.getChildByName("nobg");
if (nobg) {
nobg.active = !this.canDrawCards();
}
}
// 触发一次 showHeros 按钮点击效果,自动展开卡池
this.onShowHerosClick();
}
private enterBattlePhase() {
if (!this.cards_node || !this.cards_node.isValid) return;
this.initCardsPanelPos();
// 战斗阶段允许抽卡nobg 按"金币是否足够"判断
if (this.cards_chou && this.cards_chou.isValid) {
const nobg = this.cards_chou.getChildByName("nobg");
if (nobg) {
nobg.active = !this.canDrawCards();
}
}
}
/**
* 构建本次抽卡结果,保证最终可分发 3 条数据。
*
* 抽卡规则:
* 1. **必出规则**:当场上有可升级英雄时,每次刷新必出 1 张升级卡,并放在返回数组首位(对应卡池最左侧槽位)。
* 多次刷新时,按场上英雄 target_hero_eid 升序循环切换升级卡upgradeRotationIndex
* 2. 其余 2 张从混合池(剩余升级卡 + 基础英雄卡)按权重抽取。
*
* 特殊规则:当场存活英雄数已达 HERO_MAX_NUM 时,全部出升级卡(若全部已满级则降级回混合池)。
*/
private buildDrawCards(): CardConfig[] {
const upgradeCards = this.buildHeroUpgradeCards();
const aliveHeroCount = this.getAliveHeroCount();
const heroMax = this.getMissionHeroMaxNum();
// 英雄已满员且有可升级的英雄 → 只出升级卡
if (aliveHeroCount >= heroMax && upgradeCards.length > 0) {
const picked = this.pickMixedCards(upgradeCards, 3, upgradeCards);
if (picked.length >= 3) return picked.slice(0, 3);
/** 兜底:不足 3 张时循环补齐 */
const filled = [...picked];
while (filled.length < 3) {
filled.push(upgradeCards[filled.length % upgradeCards.length]);
}
return filled;
}
const heroCards = CardPoolList.filter(c => c.type === CardType.Hero);
// 过滤掉场上已召唤英雄的普通英雄卡:已召唤的英雄只能通过升级卡升级,不再刷出普通卡
const aliveHeroUuids = this.getAliveHeroUuids();
const availableHeroCards = heroCards.filter(c => !aliveHeroUuids.has(c.uuid));
// 英雄池只刷英雄卡(含动态升级卡 + 未召唤英雄的普通卡)
const mixedPool: CardConfig[] = [...upgradeCards, ...availableHeroCards];
if (mixedPool.length === 0) return [];
const result: CardConfig[] = [];
const usedUpgradeTargets = new Set<number>();
const usedHeroUuids = new Set<number>();
// 必出 1 张升级卡,按 UUID 升序轮询,放在返回数组首位(对应最左侧卡槽)
if (upgradeCards.length > 0) {
const idx = this.upgradeRotationIndex % upgradeCards.length;
this.upgradeRotationIndex = (this.upgradeRotationIndex + 1) % upgradeCards.length;
const mandatory = upgradeCards[idx];
result.push(mandatory);
usedUpgradeTargets.add(mandatory.target_hero_eid ?? 0);
}
// 剩余张数从混合池抽取,排除已选升级卡 target_hero_eid 和已选英雄卡 uuid
const remainingCount = 3 - result.length;
if (remainingCount > 0) {
const remainingPool = mixedPool.filter(c => {
if (c.type === CardType.SpecialUpgrade && c.target_hero_eid) {
return !usedUpgradeTargets.has(c.target_hero_eid);
}
if (c.type === CardType.Hero) {
return !usedHeroUuids.has(c.uuid);
}
return true;
});
const rest = this.pickMixedCards(remainingPool, remainingCount, upgradeCards);
rest.forEach(c => {
result.push(c);
if (c.type === CardType.SpecialUpgrade && c.target_hero_eid) {
usedUpgradeTargets.add(c.target_hero_eid);
}
if (c.type === CardType.Hero) {
usedHeroUuids.add(c.uuid);
}
});
}
// 兜底:不足 3 张时从未使用的英雄卡中补齐,避免出现重复英雄
while (result.length < 3) {
const fillCard = availableHeroCards.find(c => !usedHeroUuids.has(c.uuid));
if (!fillCard) break;
result.push(fillCard);
usedHeroUuids.add(fillCard.uuid);
}
return result.slice(0, 3);
}
/**
* 从混合池中按权重抽取 n 张卡。
* 升级卡之间强制 unique同一个 target_hero_eid 只能出现一次)。
* 英雄卡之间强制 unique同一个 uuid 只能出现一次),避免同一次发牌出现重复英雄。
*/
private pickMixedCards(pool: CardConfig[], count: number, upgradeCards: CardConfig[]): CardConfig[] {
if (pool.length === 0 || count <= 0) return [];
const selected: CardConfig[] = [];
const usedUpgradeTargets = new Set<number>();
const usedHeroUuids = new Set<number>();
while (selected.length < count) {
const available = pool.filter(c => {
if (c.type === CardType.SpecialUpgrade && c.target_hero_eid) {
return !usedUpgradeTargets.has(c.target_hero_eid);
}
if (c.type === CardType.Hero) {
return !usedHeroUuids.has(c.uuid);
}
return true;
});
if (available.length === 0) break;
const pick = this.weightedPick(available);
if (!pick) break;
selected.push(pick);
if (pick.type === CardType.SpecialUpgrade && pick.target_hero_eid) {
usedUpgradeTargets.add(pick.target_hero_eid);
}
if (pick.type === CardType.Hero) {
usedHeroUuids.add(pick.uuid);
}
}
return selected;
}
/** 单次按权重抽取一张卡 */
private weightedPick(cards: CardConfig[]): CardConfig | null {
if (cards.length === 0) return null;
const totalWeight = cards.reduce((total, card) => total + (card.weight ?? 0), 0);
let random = Math.random() * totalWeight;
for (const card of cards) {
random -= (card.weight ?? 0);
if (random <= 0) return card;
}
return cards[cards.length - 1];
}
/**
* 扫描场上存活英雄,为每个英雄实体生成一张升级卡(按 eid 绑定)。
* - 已达 HERO_MAX_LV 的英雄不出卡。
* - 同 eid 只生成一张eid 本就唯一,不会重复)。
* - cost = 模板 cost + (当前等级 - 1) * BASE_COST等级越高升级越贵。
*/
private buildHeroUpgradeCards(): CardConfig[] {
const template = SpecialUpgradeCardList[7001];
if (!template) return [];
const actors = this.queryAliveHeroActors();
if (actors.length === 0) return [];
const result: CardConfig[] = [];
for (const actor of actors) {
const eid = actor.eid;
const lv = actor.model.lv;
if (lv >= FightSet.HERO_MAX_LV) continue; // 已达上限不再出升级卡
const dynamicCost = template.cost + (lv - 1) * FightSet.BASE_COST;
result.push({
...template,
cost: dynamicCost,
weight: template.weight,
target_hero_eid: eid,
hero_lv: lv + 1,
});
}
// 按 target_hero_eid 升序排序,保证返回顺序稳定(便于 buildDrawCards 按顺序轮询)
result.sort((a, b) => (a.target_hero_eid ?? 0) - (b.target_hero_eid ?? 0));
return result;
}
private buildSkillDrawCards(): CardConfig[] {
const currentWave = this.getCurrentWave();
// 使用明确规则的 drawCardsByRule指定只要 3 张技能卡,并且过滤对应 wave
const cards = drawCardsByRule(1, {
count: 3,
type: CardType.Skill,
wave: currentWave,
unique: true // 保证技能牌不重复
});
if (cards.length >= 3) return cards.slice(0, 3);
const filled = [...cards];
while (filled.length < 3) {
const fallback = drawCardsByRule(1, {
count: 3,
type: CardType.Skill,
wave: currentWave,
unique: true
});
if (fallback.length === 0) break;
// 如果池子数量不足,只能被迫允许重复,但尽量拿没被抽到的
const fPick = fallback.find(c => !filled.some(fc => fc.uuid === c.uuid));
if (fPick) {
filled.push(fPick);
} else {
filled.push(fallback[filled.length % fallback.length]);
}
}
return filled;
}
private tryRefreshHeroCards(heroType?: HType): boolean {
const cards = drawCardsByRule(1, {
count: 3,
type: CardType.Hero,
heroType,
unique: true, // 保证一次刷新内的英雄卡不重复
});
// 过滤掉场上已召唤英雄的普通英雄卡(已召唤的英雄不再重复刷出)
const aliveHeroUuids = this.getAliveHeroUuids();
const available = cards.filter(c => !aliveHeroUuids.has(c.uuid));
if (available.length <= 0) return false;
this.layoutCardSlots();
this.dispatchCardsToSlots(available.slice(0, 3));
return true;
}
private tryRefreshHeroCardsByEffect(refreshHeroType: SpecialRefreshHeroType): boolean {
const heroType = this.resolveRefreshHeroType(refreshHeroType);
return this.tryRefreshHeroCards(heroType);
}
private resolveRefreshHeroType(refreshHeroType: SpecialRefreshHeroType): HType | undefined {
if (refreshHeroType === SpecialRefreshHeroType.Melee) return HType.Melee;
if (refreshHeroType === SpecialRefreshHeroType.Ranged) return HType.Long;
return undefined;
}
/** 全量分发给 3 槽;每个槽位是否接收由 CardComp 自己判断(锁定可跳过) */
private dispatchCardsToSlots(cards: CardConfig[]) {
if (!this.cardComps) return;
for (let i = 0; i < this.cardComps.length; i++) {
if (this.cardComps[i]) {
const accepted = this.cardComps[i].applyDrawCard(cards[i] ?? null);
mLogger.log(this.debugMode, "MissionCardComp", "dispatch card", {
index: i,
card: cards[i]?.uuid ?? 0,
accepted
});
}
}
}
/** 系统清空 3 槽(用于任务切换) */
private clearAllCards() {
if (!this.cardComps) return;
this.cardComps.forEach(comp => {
if (comp) comp.clearBySystem();
});
if (this.skillCardComps) {
this.skillCardComps.forEach(comp => {
if (comp) comp.clearBySystem();
});
}
}
private layoutCardSlots() {
if (!this.cardComps) return;
const count = this.cardComps.length;
if (count === 0) return;
for (let i = 0; i < count; i++) {
if (this.cardComps[i]) {
this.cardComps[i].setSlotPosition(this.cardsPos[i]);
}
}
mLogger.log(this.debugMode, "MissionCardComp", "layout card slots", {
count,
cardWidth: this.cardWidth
});
}
private playButtonPressAnim(node: Node | null) {
this.playNodeScaleTo(node, this.buttonPressScale, 0.06);
}
private playButtonClickAnim(node: Node | null, onComplete: () => void) {
if (!node || !node.isValid) {
onComplete();
return;
}
this.playNodeScalePop(node, this.buttonClickScale, 0.05, 0.08, onComplete);
}
private playButtonResetAnim(node: Node | null) {
this.playNodeScaleTo(node, this.buttonNormalScale, 0.08);
}
private resetButtonScale(node: Node | null) {
if (!node || !node.isValid) return;
Tween.stopAllByTarget(node);
node.setScale(this.buttonNormalScale, this.buttonNormalScale, 1);
}
private canDrawCards() {
return MissionEconomy.getCoin() >= MissionEconomy.getRefreshCost(this.refreshCost);
}
private updateDrawCostUI() {
// 战斗阶段也允许抽卡nobg 统一按"金币是否足够"判断
if (this.cards_chou) {
const nobg = this.cards_chou.getChildByName("nobg");
if (nobg) {
nobg.active = !this.canDrawCards();
}
const coinNode = this.cards_chou.getChildByName("coin");
const numLabel = coinNode?.getChildByName("num")?.getComponent(Label);
if (numLabel) {
numLabel.string = `${MissionEconomy.getRefreshCost(this.refreshCost)}`;
}
}
if (this.skill_refresh) {
const nobg = this.skill_refresh.getChildByName("nobg");
if (nobg) {
nobg.active = !this.canDrawCards();
}
const coinNode = this.skill_refresh.getChildByName("coin");
const numLabel = coinNode?.getChildByName("num")?.getComponent(Label);
if (numLabel) {
numLabel.string = `${MissionEconomy.getRefreshCost(this.refreshCost)}`;
}
}
}
private updateCoinAndCostUI() {
this.updateDrawCostUI();
}
private playCoinChangeAnim(isIncrease: boolean) {
if (!this.coins_node || !this.coins_node.isValid) return;
const icon = this.coins_node.getChildByName("icon");
if (!icon || !icon.isValid) return;
const peak = isIncrease ? 1.2 : 1.2;
this.playHeroNumNodePop(icon, peak);
const num = this.coins_node.getChildByName("num");
if (!num || !num.isValid) return;
this.playHeroNumNodePop(num, peak);
}
public setHeroMaxCount(max: number) {
const missionData = this.getMissionData();
if (!missionData) return;
const min = FightSet.HERO_MAX_NUM;
const limit = Math.max(min, missionData.hero_extend_max_num ?? (FightSet.HERO_MAX_NUM + 1));
const next = Math.max(min, Math.min(limit, Math.floor(max || min)));
if (next === missionData.hero_max_num) return;
missionData.hero_max_num = next;
this.updateHeroNumUI(true, false);
}
public tryExpandHeroMax(add: number = 1): boolean {
const missionData = this.getMissionData();
if (!missionData) return false;
const before = this.getMissionHeroMaxNum();
const next = before + Math.max(0, Math.floor(add));
this.setHeroMaxCount(next);
return this.getMissionHeroMaxNum() > before;
}
public canUseHeroCard(): boolean {
return this.getAliveHeroCount() < this.getMissionHeroMaxNum();
}
private getAliveHeroCount(): number {
let count = 0;
ecs.query(ecs.allOf(HeroAttrsComp)).forEach((entity: ecs.Entity) => {
const model = entity.get(HeroAttrsComp);
if (model && model.fac === FacSet.HERO) {
count++;
}
});
return count;
}
private queryAliveHeroActors(): Array<{ eid: number, model: HeroAttrsComp, view: HeroViewComp | null }> {
const actors: Array<{ eid: number, model: HeroAttrsComp, view: HeroViewComp | null }> = [];
ecs.query(ecs.allOf(HeroAttrsComp)).forEach((entity: ecs.Entity) => {
const model = entity.get(HeroAttrsComp);
if (!model) return;
if (model.fac !== FacSet.HERO) return;
if (model.is_dead) return;
const view = entity.get(HeroViewComp);
actors.push({ eid: entity.eid, model, view });
});
return actors;
}
/**
* 获取场上所有存活英雄的 hero_uuid 集合。
* 用于抽卡时过滤:已召唤的英雄不再从普通英雄卡池中刷出,只能通过升级卡升级。
*/
private getAliveHeroUuids(): Set<number> {
const uuids = new Set<number>();
ecs.query(ecs.allOf(HeroAttrsComp)).forEach((entity: ecs.Entity) => {
const model = entity.get(HeroAttrsComp);
if (!model) return;
if (model.fac !== FacSet.HERO) return;
if (model.is_dead) return;
uuids.add(model.hero_uuid);
});
return uuids;
}
/**
* 按 eid 精确升级场上对应英雄实体。
*
* @param heroEid 要升级的英雄实体 eid
* @returns true = 升级成功
*/
private tryUpgradeHeroByEid(heroEid: number): boolean {
if (!heroEid) return false;
const actor = this.queryAliveHeroActors().find(item => item.eid === heroEid);
if (!actor) return false;
if (actor.model.lv >= FightSet.HERO_MAX_LV) return false;
const nextLv = Math.min(FightSet.HERO_MAX_LV, actor.model.lv + 1);
this.applyHeroLevel(actor.model, nextLv);
if (actor.view) {
actor.view.playBuff("buff_lvup");
}
return true;
}
/**
* 应用英雄等级变化(属性按 HERO_LV_MULTIPLIER 重新计算)。
* 沿用旧版本 SkillLvUp 的技能等级映射规则。
*/
private applyHeroLevel(model: HeroAttrsComp, targetLv: number) {
const hero = HeroInfo[model.hero_uuid];
if (!hero) return;
const nextLv = Math.max(1, Math.min(FightSet.HERO_MAX_LV, Math.floor(targetLv)));
const hpRate = model.hp_max > 0 ? model.hp / model.hp_max : 1;
model.lv = nextLv;
model.ap = hero.ap * Math.pow(FightSet.HERO_LV_MULTIPLIER, nextLv - 1);
model.hp_max = hero.hp * Math.pow(FightSet.HERO_LV_MULTIPLIER, nextLv - 1);
model.hp = Math.max(1, Math.floor(model.hp_max * Math.max(0, Math.min(1, hpRate))));
model.skills = {};
for (const key in hero.skills) {
const skill = hero.skills[key];
if (!skill) continue;
model.skills[skill.uuid] = { ...skill, lv: Math.max(0, skill.lv + nextLv - 2), ccd: 0 };
}
model.updateSkillDistanceCache();
model.dirty_hp = true;
oops.message.dispatchEvent(GameEvent.HeroLvUp, {
uuid: model.hero_uuid,
lv: nextLv
});
}
private updateHeroNumUI(animate: boolean, isIncrease: boolean) {
this.syncMissionHeroData();
if (!animate || !isIncrease) return;
this.playHeroNumGainAnim();
}
private playHeroNumGainAnim() {
if (!this.hero_num_node || !this.hero_num_node.isValid) return;
const iconNode = this.hero_num_node.getChildByName("icon");
const numNode = this.hero_num_node.getChildByName("num");
this.playHeroNumNodePop(iconNode, 1.2);
this.playHeroNumNodePop(numNode, 1.2);
}
private playHeroNumDeniedAnim() {
if (!this.hero_num_node || !this.hero_num_node.isValid) return;
const iconNode = this.hero_num_node.getChildByName("icon");
const numNode = this.hero_num_node.getChildByName("num");
this.playHeroNumNodePop(iconNode, 1.2);
this.playHeroNumNodePop(numNode, 1.2);
}
private playHeroNumNodePop(node: Node | null, scalePeak: number) {
this.playNodeScalePop(node, scalePeak, 0.08, 0.1);
}
private playNodeScaleTo(node: Node | null, scale: number, duration: number) {
if (!node || !node.isValid) return;
Tween.stopAllByTarget(node);
tween(node)
.to(duration, {
scale: new Vec3(scale, scale, 1)
})
.start();
}
private playNodeScalePop(node: Node | null, scalePeak: number, toPeakDuration: number, toNormalDuration: number, onPeak?: () => void) {
if (!node || !node.isValid) return;
Tween.stopAllByTarget(node);
node.setScale(1, 1, 1);
const seq = tween(node)
.to(toPeakDuration, { scale: new Vec3(scalePeak, scalePeak, 1) });
if (onPeak) {
seq.call(onPeak);
}
seq.to(toNormalDuration, { scale: new Vec3(1, 1, 1) })
.start();
}
private getMissionData(): any {
return smc?.vmdata?.mission_data ?? null;
}
private getMissionHeroNum(): number {
const missionData = this.getMissionData();
return Math.max(0, Math.floor(missionData?.hero_num ?? 0));
}
private getCurrentWave(): number {
const missionData = this.getMissionData();
return Math.max(1, Math.floor(missionData?.level ?? 1));
}
private getMissionHeroMaxNum(): number {
return FightSet.HERO_MAX_NUM
}
private syncMissionHeroData(count?: number) {
const missionData = this.getMissionData();
if (!missionData) return;
const safeCount = Math.max(0, Math.floor(count ?? this.getAliveHeroCount()));
missionData.hero_num = safeCount;
}
/** 视图对象通过 ecs.Entity.remove(ModuleViewComp) 删除组件是触发组件处理自定义释放逻辑 */
reset() {
this.resetButtonScale(this.cards_chou);
// 关键:在 reset/销毁 时将 Map 置空,彻底切断引用
this.cardComps = [] as any;
this.skillCardComps = [] as any;
if (this.node && this.node.isValid) {
this.node.destroy();
}
}
}