1066 lines
42 KiB
TypeScript
1066 lines
42 KiB
TypeScript
/**
|
||
* @file MissionCardComp.ts
|
||
* @description 卡牌系统核心控制器(战斗 UI 层 + 业务逻辑层)
|
||
*
|
||
* 职责:
|
||
* 1. **卡牌分发管理** —— 从卡池抽取 4 张卡,分发到 4 个 CardComp 槽位。
|
||
* 2. **卡池升级** —— 消耗金币提升卡池等级(poolLv),解锁更高稀有度的卡牌。
|
||
* 3. **金币费用管理** —— 抽卡费用(refreshCost)、升级费用(CardsUpSet)、
|
||
* 波次折扣(CARD_POOL_UPGRADE_DISCOUNT_PER_WAVE)的计算与扣除。
|
||
* 4. **英雄数量上限校验** —— 在 UseHeroCard 事件的 guard 阶段判断是否允许
|
||
* 再召唤英雄(含合成后腾位的特殊判断 canUseHeroCardByMerge)。
|
||
* 5. **场上英雄信息面板(HInfoComp 列表)同步** ——
|
||
* 英雄上场时实例化面板,死亡时移除,定时刷新属性显示。
|
||
* 6. **特殊卡执行** —— 处理英雄升级卡(SpecialUpgrade)和英雄刷新卡(SpecialRefresh)。
|
||
* 7. **准备/战斗阶段切换** —— 控制卡牌面板的 展开/收起 动画。
|
||
*
|
||
* 关键设计:
|
||
* - 4 个 CardComp 通过 cacheCardComps() 映射为有序数组 cardComps[],
|
||
* 之后所有分发、清空操作均通过此数组进行。
|
||
* - buildDrawCards() 保证每次抽取 4 张,不足时循环补齐。
|
||
* - 英雄上限校验(onUseHeroCard)采用 guard/cancel 模式:
|
||
* CardComp 发出 UseHeroCard 事件并传入 guard 对象,
|
||
* 本组件可通过 guard.cancel=true 阻止使用。
|
||
* - ensureHeroInfoPanel() 建立 EID → HInfoComp 的 Map 映射,
|
||
* 支持英雄合成升级后面板热更新。
|
||
*
|
||
* 依赖:
|
||
* - CardComp —— 单卡槽位
|
||
* - HInfoComp —— 英雄信息面板
|
||
* - CardSet 模块 —— 卡池配置、抽卡规则、特殊卡数据
|
||
* - HeroAttrsComp —— 英雄属性(合成校验 / 升级)
|
||
* - MissionHeroCompComp —— 获取合成规则(needCount / maxLv)
|
||
* - smc.vmdata.mission_data —— 局内数据(coin / hero_num / hero_max_num)
|
||
*/
|
||
import { mLogger } from "../common/Logger";
|
||
import { _decorator, instantiate, Label, Node, NodeEventType, Prefab, SpriteAtlas, 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 { CARD_POOL_INIT_LEVEL, CARD_POOL_MAX_LEVEL, CARD_POOL_UPGRADE_DISCOUNT_PER_WAVE, CardConfig, CardType, CardsUpSet, drawCardsByRule, getCardsByLv, SpecialRefreshCardList, SpecialRefreshHeroType, SpecialUpgradeCardList } from "../common/config/CardSet";
|
||
import { CardComp } from "./CardComp";
|
||
import { oops } from "db://oops-framework/core/Oops";
|
||
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
|
||
import { HInfoComp } from "./HInfoComp";
|
||
import { smc } from "../common/SingletonModuleComp";
|
||
import { HeroInfo, HType } from "../common/config/heroSet";
|
||
import { HeroViewComp } from "../hero/HeroViewComp";
|
||
import { FacSet } from "../common/config/GameSet";
|
||
import { MoveComp } from "../hero/MoveComp";
|
||
import { MissionHeroCompComp } from "./MissionHeroComp";
|
||
|
||
const { ccclass, property } = _decorator;
|
||
|
||
|
||
/**
|
||
* MissionCardComp —— 卡牌系统核心控制器
|
||
*
|
||
* 管理 4 个卡牌槽位的抽卡分发、卡池升级、金币费用、
|
||
* 英雄上限校验、场上英雄信息面板同步以及特殊卡执行。
|
||
*/
|
||
@ccclass('MissionCardComp')
|
||
@ecs.register('MissionCard', false)
|
||
export class MissionCardComp extends CCComp {
|
||
/** 是否启用调试日志 */
|
||
private debugMode: boolean = true;
|
||
/** 卡牌槽位宽度(像素),用于水平等距布局 */
|
||
private readonly cardWidth: number = 175;
|
||
/** 按钮正常缩放 */
|
||
private readonly buttonNormalScale: number = 1;
|
||
/** 按钮按下缩放 */
|
||
private readonly buttonPressScale: number = 0.94;
|
||
/** 按钮弹起缩放(峰值) */
|
||
private readonly buttonClickScale: number = 1.06;
|
||
/** 抽卡(刷新)费用 */
|
||
refreshCost: number = 1;
|
||
/** 卡牌面板展开/收起动画时长(秒) */
|
||
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!
|
||
/** 卡池升级按钮节点 */
|
||
@property(Node)
|
||
cards_up:Node = null!
|
||
/** 金币显示节点(含 icon + num 子节点) */
|
||
@property(Node)
|
||
coins_node:Node = null!
|
||
/** 卡池等级显示节点 */
|
||
@property(Node)
|
||
pool_lv_node:Node = null!
|
||
/** 场上英雄信息面板容器节点(HInfoComp 实例的父节点) */
|
||
@property(Node)
|
||
hero_info_node:Node = null!
|
||
/** 英雄信息面板预制体(每个英雄上场时实例化一份) */
|
||
@property(Prefab)
|
||
hero_info_prefab:Prefab=null!
|
||
/** 英雄数量显示节点(含 icon + num 子节点) */
|
||
@property(Node)
|
||
hero_num_node:Node=null!
|
||
|
||
// ======================== 运行时状态 ========================
|
||
|
||
/** 预留图集缓存(后续接入按钮/卡面图标时复用) */
|
||
private uiconsAtlas: SpriteAtlas | null = null;
|
||
/** 四个槽位对应的 CardComp 控制器缓存(有序数组) */
|
||
private cardComps: CardComp[] = [];
|
||
/** 当前卡池等级(仅影响抽卡来源,不直接改卡槽现有内容) */
|
||
private poolLv: number = CARD_POOL_INIT_LEVEL;
|
||
/** 英雄信息面板项间距(像素) */
|
||
private readonly heroInfoItemGap: number = 130;
|
||
/** 英雄信息面板项间额外间距(像素) */
|
||
private readonly heroInfoItemSpacing: number = 5;
|
||
/** 英雄信息面板同步计时器(降频刷新用) */
|
||
private heroInfoSyncTimer: number = 0;
|
||
/** 是否已缓存卡牌面板基准缩放 */
|
||
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);
|
||
/**
|
||
* 英雄信息面板映射:EID → { node, model, comp }
|
||
* 用于追踪每个出战英雄的面板实例和数据引用
|
||
*/
|
||
private heroInfoItems: Map<number, {
|
||
node: Node,
|
||
model: HeroAttrsComp,
|
||
comp: HInfoComp
|
||
}> = new Map();
|
||
// ======================== 生命周期 ========================
|
||
|
||
/**
|
||
* 组件加载:
|
||
* 1. 绑定生命周期事件和按钮交互事件。
|
||
* 2. 缓存 4 个 CardComp 子控制器引用。
|
||
* 3. 计算并设置槽位水平布局。
|
||
* 4. 初始化卡牌面板缩放参数。
|
||
* 5. 触发首次任务开始流程。
|
||
*/
|
||
onLoad() {
|
||
this.bindEvents();
|
||
this.cacheCardComps();
|
||
this.layoutCardSlots();
|
||
this.initCardsPanelPos();
|
||
this.onMissionStart();
|
||
mLogger.log(this.debugMode, "MissionCardComp", "onLoad init", {
|
||
slots: this.cardComps.length,
|
||
poolLv: this.poolLv
|
||
});
|
||
}
|
||
|
||
/** 组件销毁时解绑所有事件并清理英雄信息面板 */
|
||
onDestroy() {
|
||
this.unbindEvents();
|
||
this.clearHeroInfoPanels();
|
||
}
|
||
|
||
/** 外部初始化入口(由 CardController 调用) */
|
||
init(){
|
||
this.onMissionStart();
|
||
}
|
||
|
||
/**
|
||
* 任务开始:
|
||
* 1. 进入准备阶段(展开卡牌面板)。
|
||
* 2. 重置卡池等级为初始值。
|
||
* 3. 初始化局内数据(金币、英雄数量上限)。
|
||
* 4. 清空旧英雄信息面板和卡牌槽位。
|
||
* 5. 重置按钮状态和 UI 显示。
|
||
* 6. 执行首次抽卡并分发到 4 个槽位。
|
||
*/
|
||
onMissionStart() {
|
||
this.enterPreparePhase();
|
||
this.poolLv = CARD_POOL_INIT_LEVEL;
|
||
const missionData = this.getMissionData();
|
||
if (missionData) {
|
||
missionData.coin = Math.max(0, Math.floor(missionData.coin ?? 0));
|
||
missionData.hero_num = 0;
|
||
missionData.hero_max_num = 5;
|
||
missionData.hero_extend_max_num = 6;
|
||
}
|
||
this.clearHeroInfoPanels();
|
||
this.layoutCardSlots();
|
||
this.clearAllCards();
|
||
if (this.cards_up) {
|
||
this.cards_up.active = true;
|
||
}
|
||
this.resetButtonScale(this.cards_chou);
|
||
this.resetButtonScale(this.cards_up);
|
||
this.updateCoinAndCostUI();
|
||
this.updateHeroNumUI(false, false);
|
||
this.node.active = true;
|
||
const cards = this.buildDrawCards();
|
||
this.dispatchCardsToSlots(cards);
|
||
mLogger.log(this.debugMode, "MissionCardComp", "mission start", {
|
||
poolLv: this.poolLv
|
||
});
|
||
}
|
||
|
||
/** 任务结束:清空 4 槽 + 英雄面板并隐藏整个节点 */
|
||
onMissionEnd() {
|
||
this.clearAllCards();
|
||
this.clearHeroInfoPanels();
|
||
this.node.active = false;
|
||
}
|
||
|
||
start() {
|
||
}
|
||
|
||
/**
|
||
* 帧更新:每 0.15 秒刷新一次场上英雄信息面板(降频)。
|
||
* 检测已死亡 / 已失效的面板并移除,刷新存活面板属性。
|
||
*/
|
||
update(dt: number) {
|
||
this.heroInfoSyncTimer += dt;
|
||
if (this.heroInfoSyncTimer < 0.15) return;
|
||
this.heroInfoSyncTimer = 0;
|
||
this.refreshHeroInfoPanels();
|
||
}
|
||
|
||
|
||
|
||
/** 关闭面板(不销毁数据模型,仅隐藏) */
|
||
close() {
|
||
this.node.active = false;
|
||
}
|
||
|
||
// ======================== 事件绑定 ========================
|
||
|
||
/**
|
||
* 绑定所有事件监听:
|
||
* - 节点级事件:MissionStart / MissionEnd / NewWave / FightStart
|
||
* - 全局消息:CoinAdd / MasterCalled / HeroDead / UseHeroCard / UseSpecialCard
|
||
* - 按钮触控:抽卡(cards_chou)、升级(cards_up)
|
||
*/
|
||
private bindEvents() {
|
||
/** 生命周期事件(节点级) */
|
||
this.on(GameEvent.MissionStart, this.onMissionStart, this);
|
||
this.on(GameEvent.MissionEnd, this.onMissionEnd, this);
|
||
this.on(GameEvent.NewWave, this.onNewWave, this);
|
||
this.on(GameEvent.FightStart, this.onFightStart, 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.UseHeroCard, this.onUseHeroCard, this);
|
||
oops.message.on(GameEvent.UseSpecialCard, this.onUseSpecialCard, 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.cards_up?.on(NodeEventType.TOUCH_START, this.onUpgradeTouchStart, this);
|
||
this.cards_up?.on(NodeEventType.TOUCH_END, this.onUpgradeTouchEnd, this);
|
||
this.cards_up?.on(NodeEventType.TOUCH_CANCEL, this.onUpgradeTouchCancel, this);
|
||
}
|
||
// ======================== 事件回调 ========================
|
||
|
||
/**
|
||
* 金币变化事件回调:
|
||
* - syncOnly=true:仅同步 UI 显示(金币已被外部修改过)。
|
||
* - 否则:累加 delta 到 mission_data.coin 后刷新 UI。
|
||
*/
|
||
private onCoinAdd(event: string, args: any){
|
||
const payload = args ?? event;
|
||
if (payload?.syncOnly) {
|
||
this.updateCoinAndCostUI();
|
||
this.playCoinChangeAnim((payload?.delta ?? 0) > 0);
|
||
return;
|
||
}
|
||
const v = typeof payload === 'number' ? payload : (payload?.delta ?? payload?.value ?? 0);
|
||
if (v === 0) return;
|
||
this.setMissionCoin(this.getMissionCoin() + v);
|
||
this.updateCoinAndCostUI();
|
||
this.playCoinChangeAnim(v > 0);
|
||
}
|
||
|
||
/** 战斗开始:收起卡牌面板 */
|
||
private onFightStart() {
|
||
this.enterBattlePhase();
|
||
}
|
||
|
||
/** 新一波:展开面板 → 刷新费用 UI → 重新抽卡分发 */
|
||
private onNewWave() {
|
||
this.enterPreparePhase();
|
||
this.updateCoinAndCostUI();
|
||
this.layoutCardSlots();
|
||
const cards = this.buildDrawCards();
|
||
this.dispatchCardsToSlots(cards);
|
||
}
|
||
|
||
/** 解除按钮监听,避免节点销毁后回调泄漏 */
|
||
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.UseHeroCard, this.onUseHeroCard, this);
|
||
oops.message.off(GameEvent.UseSpecialCard, this.onUseSpecialCard, this);
|
||
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);
|
||
this.cards_up?.off(NodeEventType.TOUCH_START, this.onUpgradeTouchStart, this);
|
||
this.cards_up?.off(NodeEventType.TOUCH_END, this.onUpgradeTouchEnd, this);
|
||
this.cards_up?.off(NodeEventType.TOUCH_CANCEL, this.onUpgradeTouchCancel, 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();
|
||
this.ensureHeroInfoPanel(eid, model);
|
||
const after = this.getAliveHeroCount();
|
||
this.updateHeroNumUI(true, after > before);
|
||
}
|
||
|
||
/** 英雄死亡事件回调:刷新面板列表并更新英雄数量 UI */
|
||
private onHeroDead() {
|
||
this.refreshHeroInfoPanels();
|
||
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) {
|
||
const heroUuid = Number(payload?.uuid ?? 0);
|
||
const heroLv = Math.max(1, Math.floor(Number(payload?.hero_lv ?? 1)));
|
||
const cardLv = Math.max(1, Math.floor(Number(payload?.pool_lv ?? 1)));
|
||
// 检查是否可以通过合成腾出位置
|
||
if (this.canUseHeroCardByMerge(heroUuid, heroLv)) {
|
||
payload.cancel = false;
|
||
payload.reason = "";
|
||
return;
|
||
}
|
||
payload.cancel = true;
|
||
payload.reason = "hero_limit";
|
||
oops.gui.toast(`英雄已满 (${current}/${heroMax})`);
|
||
// this.playHeroNumDeniedAnim();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 判断新召唤的英雄是否能通过合成腾位:
|
||
* 场上同 UUID 同等级数量 + 1(新卡自身)>= 合成所需数量 → 可以合成。
|
||
*/
|
||
private canUseHeroCardByMerge(heroUuid: number, heroLv: number): boolean {
|
||
if (!heroUuid) return false;
|
||
const mergeRule = this.getMergeRule();
|
||
if (heroLv >= mergeRule.maxLv) return false;
|
||
const sameCount = this.countAliveHeroesByUuidAndLv(heroUuid, heroLv);
|
||
return sameCount + 1 >= mergeRule.needCount;
|
||
}
|
||
|
||
/** 统计场上同 UUID 同等级的存活英雄数量 */
|
||
private countAliveHeroesByUuidAndLv(heroUuid: number, heroLv: number): number {
|
||
let count = 0;
|
||
const actors = this.queryAliveHeroActors();
|
||
for (let i = 0; i < actors.length; i++) {
|
||
const model = actors[i].model;
|
||
if (!model) continue;
|
||
if (model.hero_uuid !== heroUuid) continue;
|
||
if (model.lv !== heroLv) continue;
|
||
count += 1;
|
||
}
|
||
return count;
|
||
}
|
||
|
||
/**
|
||
* 从 MissionHeroCompComp 实时读取合成规则。
|
||
* 通过 ECS 查询获取,避免硬编码与 MissionHeroComp 不一致。
|
||
* @returns { needCount: 合成所需数量, maxLv: 最大合成等级 }
|
||
*/
|
||
private getMergeRule(): { needCount: number, maxLv: number } {
|
||
let needCount = 3;
|
||
let maxLv = 2; // 兜底值改为2,与 MissionHeroComp 保持一致
|
||
ecs.query(ecs.allOf(MissionHeroCompComp)).forEach((entity: ecs.Entity) => {
|
||
const comp = entity.get(MissionHeroCompComp);
|
||
if (!comp) return;
|
||
needCount = comp.merge_need_count === 2 ? 2 : 3;
|
||
maxLv = Math.max(1, Math.floor(comp.merge_max_lv ?? 2));
|
||
});
|
||
return { needCount, maxLv };
|
||
}
|
||
|
||
/**
|
||
* 使用特殊卡事件回调:
|
||
* - SpecialUpgrade:随机选一个指定等级的英雄升级到目标等级。
|
||
* - 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 card = SpecialUpgradeCardList[uuid];
|
||
if (!card) return;
|
||
success = this.tryUpgradeOneHero(card.currentLv, card.targetLv);
|
||
if (!success) oops.gui.toast(`场上没有可从${card.currentLv}级升到${card.targetLv}级的英雄`);
|
||
} else if (type === CardType.SpecialRefresh) {
|
||
const card = SpecialRefreshCardList[uuid];
|
||
if (!card) return;
|
||
success = this.tryRefreshHeroCardsByEffect(card.refreshHeroType, card.refreshLv);
|
||
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() {
|
||
this.playButtonClickAnim(this.cards_chou, () => this.onClickDraw());
|
||
}
|
||
/** 抽卡按钮取消 → 恢复缩放 */
|
||
private onDrawTouchCancel() {
|
||
this.playButtonResetAnim(this.cards_chou);
|
||
}
|
||
/** 升级按钮按下反馈 */
|
||
private onUpgradeTouchStart() {
|
||
this.playButtonPressAnim(this.cards_up);
|
||
}
|
||
/** 升级按钮释放 → 执行升级逻辑 */
|
||
private onUpgradeTouchEnd() {
|
||
this.playButtonClickAnim(this.cards_up, () => this.onClickUpgrade());
|
||
}
|
||
/** 升级按钮取消 → 恢复缩放 */
|
||
private onUpgradeTouchCancel() {
|
||
this.playButtonResetAnim(this.cards_up);
|
||
}
|
||
|
||
/** 将四个卡槽节点映射为 CardComp,形成固定顺序控制数组 */
|
||
private cacheCardComps() {
|
||
const nodes = [this.card1, this.card2, this.card3, this.card4];
|
||
this.cardComps = nodes
|
||
.map(node => node?.getComponent(CardComp))
|
||
.filter((comp): comp is CardComp => !!comp);
|
||
}
|
||
|
||
// ======================== 核心业务:抽卡 & 升级 ========================
|
||
|
||
/**
|
||
* 抽卡按钮核心逻辑:
|
||
* 1. 检查金币是否足够 → 不够则 toast 提示。
|
||
* 2. 扣除费用、播放金币动画。
|
||
* 3. 重新布局槽位 → 从卡池构建 4 张卡 → 分发到槽位。
|
||
*/
|
||
private onClickDraw() {
|
||
const cost = this.getRefreshCost();
|
||
const currentCoin = this.getMissionCoin();
|
||
if (currentCoin < cost) {
|
||
oops.gui.toast(`金币不足,刷新需要${cost}`);
|
||
this.updateCoinAndCostUI();
|
||
mLogger.log(this.debugMode, "MissionCardComp", "draw coin not enough", {
|
||
currentCoin,
|
||
cost
|
||
});
|
||
return;
|
||
}
|
||
this.setMissionCoin(currentCoin - cost);
|
||
this.playCoinChangeAnim(false);
|
||
this.updateCoinAndCostUI();
|
||
mLogger.log(this.debugMode, "MissionCardComp", "click draw", {
|
||
poolLv: this.poolLv,
|
||
cost,
|
||
leftCoin: this.getMissionCoin()
|
||
});
|
||
this.layoutCardSlots();
|
||
const cards = this.buildDrawCards();
|
||
this.dispatchCardsToSlots(cards);
|
||
}
|
||
|
||
/** 升级按钮:仅提升卡池等级,卡槽是否更新由下一次抽卡触发 */
|
||
private onClickUpgrade() {
|
||
if (this.poolLv >= CARD_POOL_MAX_LEVEL) {
|
||
mLogger.log(this.debugMode, "MissionCardComp", "pool already max", this.poolLv);
|
||
return;
|
||
}
|
||
const cost = this.getUpgradeCost(this.poolLv);
|
||
const currentCoin = this.getMissionCoin();
|
||
if (currentCoin < cost) {
|
||
oops.gui.toast(`金币不足,升级需要${cost}`);
|
||
this.updateCoinAndCostUI();
|
||
mLogger.log(this.debugMode, "MissionCardComp", "pool upgrade coin not enough", {
|
||
poolLv: this.poolLv,
|
||
currentCoin,
|
||
cost
|
||
});
|
||
return;
|
||
}
|
||
this.setMissionCoin(currentCoin - cost);
|
||
this.poolLv += 1;
|
||
this.playCoinChangeAnim(false);
|
||
this.updateCoinAndCostUI();
|
||
mLogger.log(this.debugMode, "MissionCardComp", "pool level up", {
|
||
poolLv: this.poolLv,
|
||
cost,
|
||
leftCoin: this.getMissionCoin()
|
||
});
|
||
}
|
||
|
||
// ======================== 阶段切换 ========================
|
||
|
||
/** 缓存卡牌面板的基准缩放值(从场景初始状态读取,仅缓存一次) */
|
||
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);
|
||
}
|
||
|
||
/** 进入准备阶段:展开卡牌面板(立即恢复缩放,无动画) */
|
||
private enterPreparePhase() {
|
||
if (!this.cards_node || !this.cards_node.isValid) return;
|
||
this.initCardsPanelPos();
|
||
this.cards_node.active = true;
|
||
Tween.stopAllByTarget(this.cards_node);
|
||
this.cards_node.setScale(this.cardsShowScale);
|
||
}
|
||
|
||
private enterBattlePhase() {
|
||
if (!this.cards_node || !this.cards_node.isValid) return;
|
||
this.initCardsPanelPos();
|
||
this.cards_node.active = true;
|
||
Tween.stopAllByTarget(this.cards_node);
|
||
this.cards_node.setScale(this.cardsShowScale);
|
||
tween(this.cards_node)
|
||
.to(this.cardsPanelMoveDuration, {
|
||
scale: this.cardsHideScale
|
||
})
|
||
.start();
|
||
}
|
||
|
||
/** 构建本次抽卡结果,保证最终可分发4条数据 */
|
||
private buildDrawCards(): CardConfig[] {
|
||
const cards = getCardsByLv(this.poolLv);
|
||
/** 正常情况下直接取前4 */
|
||
if (cards.length >= 4) return cards.slice(0, 4);
|
||
/** 兜底:当返回不足4张时循环补齐,保证分发不缺位 */
|
||
const filled = [...cards];
|
||
while (filled.length < 4) {
|
||
const fallback = getCardsByLv(this.poolLv);
|
||
if (fallback.length === 0) break;
|
||
filled.push(fallback[filled.length % fallback.length]);
|
||
}
|
||
return filled;
|
||
}
|
||
|
||
private tryRefreshHeroCards(heroType?: HType, targetPoolLv?: number): boolean {
|
||
const cards = drawCardsByRule(this.poolLv, {
|
||
count: 4,
|
||
type: CardType.Hero,
|
||
heroType,
|
||
targetPoolLv
|
||
});
|
||
if (cards.length <= 0) return false;
|
||
this.layoutCardSlots();
|
||
this.dispatchCardsToSlots(cards.slice(0, 4));
|
||
return true;
|
||
}
|
||
|
||
private tryRefreshHeroCardsByEffect(refreshHeroType: SpecialRefreshHeroType, refreshLv: number): boolean {
|
||
const heroType = this.resolveRefreshHeroType(refreshHeroType);
|
||
const targetPoolLv = refreshLv > 0 ? refreshLv : undefined;
|
||
return this.tryRefreshHeroCards(heroType, targetPoolLv);
|
||
}
|
||
|
||
private resolveRefreshHeroType(refreshHeroType: SpecialRefreshHeroType): HType | undefined {
|
||
if (refreshHeroType === SpecialRefreshHeroType.Melee) return HType.Melee;
|
||
if (refreshHeroType === SpecialRefreshHeroType.Ranged) return HType.Long;
|
||
return undefined;
|
||
}
|
||
|
||
/** 全量分发给4槽;每个槽位是否接收由 CardComp 自己判断(锁定可跳过) */
|
||
private dispatchCardsToSlots(cards: CardConfig[]) {
|
||
for (let i = 0; i < this.cardComps.length; 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
|
||
});
|
||
}
|
||
}
|
||
|
||
/** 系统清空4槽(用于任务切换) */
|
||
private clearAllCards() {
|
||
this.cardComps.forEach(comp => comp.clearBySystem());
|
||
}
|
||
|
||
private layoutCardSlots() {
|
||
const count = this.cardComps.length;
|
||
if (count === 0) return;
|
||
const startX = -((count - 1) * this.cardWidth) / 2;
|
||
for (let i = 0; i < count; i++) {
|
||
const x = startX + i * this.cardWidth;
|
||
this.cardComps[i].setSlotPosition(x);
|
||
}
|
||
mLogger.log(this.debugMode, "MissionCardComp", "layout card slots", {
|
||
count,
|
||
cardWidth: this.cardWidth,
|
||
startX
|
||
});
|
||
}
|
||
|
||
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 canUpPool() {
|
||
if (this.poolLv >= CARD_POOL_MAX_LEVEL) return false;
|
||
const currentCoin = this.getMissionCoin();
|
||
return currentCoin >= this.getUpgradeCost(this.poolLv);
|
||
}
|
||
|
||
private canDrawCards() {
|
||
return this.getMissionCoin() >= this.getRefreshCost();
|
||
}
|
||
/** 更新升级按钮上的等级文案,反馈当前卡池层级 */
|
||
private updatePoolLvUI() {
|
||
if (this.cards_up) {
|
||
const nobg = this.cards_up.getChildByName("nobg");
|
||
if (nobg) {
|
||
nobg.active = !this.canUpPool();
|
||
}
|
||
const coinNode = this.cards_up.getChildByName("coin");
|
||
const label = coinNode?.getChildByName("num")?.getComponent(Label);
|
||
if (this.poolLv >= CARD_POOL_MAX_LEVEL) {
|
||
if (label) {
|
||
label.string = `0`;
|
||
}
|
||
} else {
|
||
if (label) {
|
||
label.string = `${this.getUpgradeCost(this.poolLv)}`;
|
||
}
|
||
}
|
||
}
|
||
if (this.pool_lv_node) {
|
||
this.pool_lv_node.active = true;
|
||
const lv = Math.max(CARD_POOL_INIT_LEVEL, Math.min(CARD_POOL_MAX_LEVEL, Math.floor(this.poolLv)));
|
||
this.pool_lv_node.getComponent(Label).string = `${lv}`;
|
||
const peak = 1.2
|
||
this.playHeroNumNodePop( this.pool_lv_node, peak);
|
||
}
|
||
mLogger.log(this.debugMode, "MissionCardComp", "pool lv ui update", {
|
||
poolLv: this.poolLv,
|
||
cost: this.getUpgradeCost(this.poolLv)
|
||
});
|
||
}
|
||
|
||
private updateDrawCostUI() {
|
||
if (!this.cards_chou) return;
|
||
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 = `${this.getRefreshCost()}`;
|
||
}
|
||
}
|
||
|
||
private updateCoinAndCostUI() {
|
||
this.updatePoolLvUI();
|
||
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);
|
||
}
|
||
|
||
private getUpgradeCost(lv: number): number {
|
||
const baseCost = Math.max(0, Math.floor(CardsUpSet[lv] ?? 0));
|
||
const completedWave = Math.max(0, this.getCurrentWave() - 1);
|
||
const discount = Math.max(0, Math.floor(CARD_POOL_UPGRADE_DISCOUNT_PER_WAVE)) * completedWave;
|
||
return Math.max(0, baseCost - discount);
|
||
}
|
||
|
||
private getRefreshCost(): number {
|
||
return Math.max(0, Math.floor(this.refreshCost));
|
||
}
|
||
|
||
private ensureHeroInfoPanel(eid: number, model: HeroAttrsComp) {
|
||
if (!this.hero_info_node || !this.hero_info_prefab) {
|
||
mLogger.error(this.debugMode, "MissionCardComp", "ensureHeroInfoPanel: missing hero_info_node or hero_info_prefab");
|
||
return;
|
||
}
|
||
this.hero_info_node.active = true;
|
||
const current = this.heroInfoItems.get(eid);
|
||
if (current) {
|
||
current.model = model;
|
||
current.comp.bindData(eid, model);
|
||
this.updateHeroInfoPanel(current);
|
||
return;
|
||
}
|
||
|
||
mLogger.log(this.debugMode, "MissionCardComp", "ensureHeroInfoPanel: creating new panel for eid", eid);
|
||
const node = instantiate(this.hero_info_prefab);
|
||
node.parent = this.hero_info_node;
|
||
node.active = true;
|
||
|
||
// 尝试两种方式获取组件,并输出日志
|
||
let comp = node.getComponent(HInfoComp) as any;
|
||
if (!comp) {
|
||
comp = node.getComponent("HInfoComp") as any;
|
||
}
|
||
|
||
if (!comp) {
|
||
mLogger.error(this.debugMode, "MissionCardComp", "ensureHeroInfoPanel: Failed to get HInfoComp from prefab!");
|
||
node.destroy();
|
||
return;
|
||
}
|
||
|
||
const item = {
|
||
node,
|
||
model,
|
||
comp
|
||
};
|
||
comp.bindData(eid, model);
|
||
this.heroInfoItems.set(eid, item);
|
||
this.relayoutHeroInfoPanels();
|
||
this.updateHeroInfoPanel(item);
|
||
|
||
mLogger.log(this.debugMode, "MissionCardComp", `ensureHeroInfoPanel: new panel created for eid ${eid}, final position:`, item.node.position);
|
||
}
|
||
|
||
private refreshHeroInfoPanels() {
|
||
const removeKeys: number[] = [];
|
||
this.heroInfoItems.forEach((item, eid) => {
|
||
if (!item.node || !item.node.isValid) {
|
||
removeKeys.push(eid);
|
||
return;
|
||
}
|
||
if (item.model?.is_dead) {
|
||
if (item.node.isValid) item.node.destroy();
|
||
removeKeys.push(eid);
|
||
return;
|
||
}
|
||
if (!item.comp.isModelAlive()) {
|
||
if (item.node.isValid) item.node.destroy();
|
||
removeKeys.push(eid);
|
||
return;
|
||
}
|
||
this.updateHeroInfoPanel(item);
|
||
});
|
||
for (let i = 0; i < removeKeys.length; i++) {
|
||
this.heroInfoItems.delete(removeKeys[i]);
|
||
}
|
||
this.relayoutHeroInfoPanels();
|
||
this.updateHeroNumUI(false, false);
|
||
}
|
||
|
||
private updateHeroInfoPanel(item: {
|
||
node: Node,
|
||
model: HeroAttrsComp,
|
||
comp: HInfoComp
|
||
}) {
|
||
item.comp.refresh();
|
||
}
|
||
|
||
private relayoutHeroInfoPanels() {
|
||
const sortedItems = [...this.heroInfoItems.values()].sort((a, b) => {
|
||
const aEnt = (a.model as any)?.ent as ecs.Entity | undefined;
|
||
const bEnt = (b.model as any)?.ent as ecs.Entity | undefined;
|
||
const aView = aEnt?.get(HeroViewComp);
|
||
const bView = bEnt?.get(HeroViewComp);
|
||
const aMove = aEnt?.get(MoveComp);
|
||
const bMove = bEnt?.get(MoveComp);
|
||
// 排序逻辑反转:适应 cc.Layout 的节点渲染顺序(先渲染/index小的在左边)
|
||
// 1. x 坐标越小(越靠后排)的,index 应该越小
|
||
const aFrontScore = aView?.node?.position?.x ?? -999999;
|
||
const bFrontScore = bView?.node?.position?.x ?? -999999;
|
||
if (aFrontScore !== bFrontScore) return aFrontScore - bFrontScore;
|
||
|
||
const aSpawnOrder = aMove?.spawnOrder ?? 0;
|
||
const bSpawnOrder = bMove?.spawnOrder ?? 0;
|
||
if (aSpawnOrder !== bSpawnOrder) return aSpawnOrder - bSpawnOrder;
|
||
|
||
const aEid = aEnt?.eid ?? 0;
|
||
const bEid = bEnt?.eid ?? 0;
|
||
return aEid - bEid;
|
||
});
|
||
|
||
for (let index = 0; index < sortedItems.length; index++) {
|
||
const item = sortedItems[index];
|
||
if (!item.node || !item.node.isValid) continue;
|
||
|
||
// 既然使用了 cc.Layout 进行自动排版,我们只需设置渲染顺序
|
||
// Layout 会自动根据 siblingIndex 对所有子节点重新排位
|
||
item.node.setSiblingIndex(index);
|
||
}
|
||
}
|
||
|
||
private clearHeroInfoPanels() {
|
||
this.heroInfoItems.forEach(item => {
|
||
if (item.node && item.node.isValid) {
|
||
item.node.destroy();
|
||
}
|
||
});
|
||
this.heroInfoItems.clear();
|
||
if (this.hero_info_node && this.hero_info_node.isValid) {
|
||
for (let i = this.hero_info_node.children.length - 1; i >= 0; i--) {
|
||
const child = this.hero_info_node.children[i];
|
||
if (child && child.isValid) child.destroy();
|
||
}
|
||
}
|
||
this.heroInfoSyncTimer = 0;
|
||
this.syncMissionHeroData(0);
|
||
this.updateHeroNumUI(false, false);
|
||
}
|
||
|
||
public setHeroMaxCount(max: number) {
|
||
const missionData = this.getMissionData();
|
||
if (!missionData) return;
|
||
const min = 5;
|
||
const limit = Math.max(min, missionData.hero_extend_max_num ?? 6);
|
||
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;
|
||
this.heroInfoItems.forEach(item => {
|
||
if (!item?.node || !item.node.isValid) return;
|
||
if (!item.comp?.isModelAlive()) return;
|
||
if (item.model?.is_dead) return;
|
||
count += 1;
|
||
});
|
||
return count;
|
||
}
|
||
|
||
private queryAliveHeroActors(): Array<{ model: HeroAttrsComp, view: HeroViewComp | null }> {
|
||
const actors: Array<{ 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({ model, view });
|
||
});
|
||
return actors;
|
||
}
|
||
|
||
private tryUpgradeOneHero(currentLv: number, targetLv: number): boolean {
|
||
const fromLv = Math.max(1, Math.floor(currentLv));
|
||
const toLv = Math.max(1, Math.floor(targetLv));
|
||
if (toLv <= fromLv) return false;
|
||
const candidates = this.queryAliveHeroActors().filter(item => item.model.lv === fromLv);
|
||
if (candidates.length === 0) return false;
|
||
const target = candidates[Math.floor(Math.random() * candidates.length)];
|
||
this.applyHeroLevel(target.model, toLv);
|
||
if (target.view) {
|
||
target.view.playBuff("buff_lvup");
|
||
}
|
||
return true;
|
||
}
|
||
|
||
private applyHeroLevel(model: HeroAttrsComp, targetLv: number) {
|
||
const hero = HeroInfo[model.hero_uuid];
|
||
if (!hero) return;
|
||
const nextLv = Math.max(1, Math.min(3, Math.floor(targetLv)));
|
||
const hpRate = model.hp_max > 0 ? model.hp / model.hp_max : 1;
|
||
model.lv = nextLv;
|
||
model.ap = hero.ap * nextLv;
|
||
model.hp_max = hero.hp * nextLv;
|
||
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 getMissionCoin(): number {
|
||
const missionData = this.getMissionData();
|
||
return Math.max(0, Math.floor(missionData?.coin ?? 0));
|
||
}
|
||
|
||
private getCurrentWave(): number {
|
||
const missionData = this.getMissionData();
|
||
return Math.max(1, Math.floor(missionData?.level ?? 1));
|
||
}
|
||
|
||
private setMissionCoin(value: number) {
|
||
const missionData = this.getMissionData();
|
||
if (!missionData) return;
|
||
missionData.coin = Math.max(0, Math.floor(value));
|
||
}
|
||
|
||
private getMissionHeroMaxNum(): number {
|
||
const missionData = this.getMissionData();
|
||
return Math.max(5, Math.floor(missionData?.hero_max_num ?? 5));
|
||
}
|
||
|
||
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.clearHeroInfoPanels();
|
||
this.resetButtonScale(this.cards_chou);
|
||
this.resetButtonScale(this.cards_up);
|
||
this.node.destroy();
|
||
}
|
||
}
|