/** * @file MissionComp.ts * @description 任务(关卡)核心控制组件(UI + 逻辑层) * * 职责: * 1. 管理单局游戏的 **完整生命周期**:初始化 → 准备阶段 → 战斗阶段 → 结算。 * 2. 在战斗阶段每帧更新战斗计时器、同步怪物数量、检测英雄全灭。 * 3. 管理怪物数量阈值(暂停 / 恢复刷怪的上下限)。 * 4. 处理新一波事件(NewWave),进入准备阶段并发放金币奖励。 * 5. 提供战斗结束后的结算弹窗入口(VictoryComp)。 * 6. (可选)内建性能监控面板,显示内存、帧率、实体数量等开发信息。 * * 关键设计: * - mission_start() 初始化所有游戏数据 → 进入准备阶段 → 显示 loading。 * - 准备阶段(enterPreparePhase):停止刷怪,显示开始按钮。 * - 战斗阶段(to_fight):开始刷怪,隐藏按钮,由 update 驱动。 * - 怪物数量管理采用 max/resume 双阈值: * * 超过 max → 暂停刷怪(stop_spawn_mon=true) * * 降至 resume 以下 → 恢复刷怪 * - cleanComponents() 在任务开始/结束时销毁所有英雄和技能 ECS 实体。 * - clearBattlePools() 回收对象池(Monster / Skill / Tooltip)。 * * 依赖: * - smc.mission —— 全局任务运行状态(play / pause / in_fight / stop_spawn_mon 等) * - smc.vmdata.mission_data —— 局内数据(金币 / 波数 / 怪物数量等) * - FightSet —— 战斗常量配置 * - CardInitCoins —— 初始金币数 * - UIID.Victory —— 结算弹窗 */ import { _decorator, Vec3,Animation, instantiate, Prefab, Node, NodeEventType, ProgressBar, Label } 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 { smc } from "../common/SingletonModuleComp"; import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops"; import { HeroAttrsComp } from "../hero/HeroAttrsComp"; import { GameEvent } from "../common/config/GameEvent"; import { HeroViewComp } from "../hero/HeroViewComp"; import { UIID } from "../common/config/GameUIConfig"; import { SkillView } from "../skill/SkillView"; import { FacSet, FightSet } from "../common/config/GameSet"; import { mLogger } from "../common/Logger"; import { Monster } from "../hero/Mon"; import { Skill } from "../skill/Skill"; import { Tooltip } from "../skill/Tooltip"; import { CardInitCoins } from "../common/config/CardSet"; const { ccclass, property } = _decorator; //@todo 需要关注 当boss死亡的时候的动画播放完成后,需要触发事件,通知 MissionComp 进行奖励处理 /** * MissionComp —— 任务(关卡)核心控制器 * * 驱动单局游戏的完整流程:准备 → 战斗 → 结算。 * 管理战斗计时、怪物数量控制、英雄全灭检测和金币奖励发放。 */ @ccclass('MissionComp') @ecs.register('MissionComp', false) export class MissionComp extends CCComp { @property({ tooltip: "是否启用调试日志" }) private debugMode: boolean = false; @property({ tooltip: "是否显示战斗内存观测面板" }) private showMemoryPanel: boolean = false; // ======================== 配置参数 ======================== /** 怪物数量上限(超过后暂停刷怪) */ private maxMonsterCount: number = 5; /** 怪物数量恢复阈值(降至此值以下恢复刷怪) */ private resumeMonsterCount: number = 3; /** 新一波金币奖励基础值 */ private prepareBaseCoinReward: number = 100; /** 每一波金币增长值 */ private prepareCoinWaveGrow: number = 1; /** 金币奖励上限 */ private prepareCoinRewardCap: number = 500; // ======================== 编辑器绑定节点 ======================== /** 开始战斗按钮 */ @property(Node) start_btn:Node = null! /** 时间/波数显示节点 */ @property(Node) time_node:Node = null! // ======================== 运行时状态 ======================== /** 战斗倒计时(秒) */ FightTime:number = FightSet.FiIGHT_TIME /** 剩余复活次数 */ revive_times: number = 1; /** 掉落奖励列表 */ rewards:any[]=[] /** 累计游戏数据 */ game_data:any={ exp:0, gold:0, diamond:0 } /** 上一次显示的时间字符串(避免重复设置) */ private lastTimeStr: string = ""; /** 上一次显示的秒数(避免重复计算) */ private lastTimeSecond: number = -1; /** 性能监控面板 Label 引用 */ private memoryLabel: Label | null = null; /** 性能监控刷新计时器 */ private memoryRefreshTimer: number = 0; /** 上一次性能文本(避免重复渲染) */ private lastMemoryText: string = ""; /** 帧间隔累加(用于计算平均 FPS) */ private perfDtAcc: number = 0; /** 帧数计数 */ private perfFrameCount: number = 0; /** 初始堆内存基准值(MB) */ private heapBaseMB: number = -1; /** 堆内存峰值(MB) */ private heapPeakMB: number = 0; /** 堆内存增长趋势(MB/分钟) */ private heapTrendPerMinMB: number = 0; /** 趋势计算计时器 */ private heapTrendTimer: number = 0; /** 趋势计算基准(MB) */ private heapTrendBaseMB: number = -1; /** 怪物数量同步计时器(降低同步频率) */ private monsterCountSyncTimer: number = 0; /** 当前波数 */ private currentWave: number = 0; /** 上一次发放金币奖励的波数(防止重复发放) */ private lastPrepareCoinWave: number = 0; // ======================== ECS 查询匹配器(预缓存) ======================== /** 匹配拥有 HeroViewComp 的实体(英雄/怪物视图) */ private readonly heroViewMatcher = ecs.allOf(HeroViewComp); /** 匹配拥有 SkillView 的实体(技能视图) */ private readonly skillViewMatcher = ecs.allOf(SkillView); /** 匹配拥有 HeroAttrsComp 的实体(英雄/怪物属性) */ private readonly heroAttrsMatcher = ecs.allOf(HeroAttrsComp); // ======================== 生命周期 ======================== onLoad(){ this.showMemoryPanel = false // 注册生命周期事件 this.on(GameEvent.MissionStart,this.mission_start,this) this.on(GameEvent.MissionEnd,this.mission_end,this) this.on(GameEvent.NewWave,this.onNewWave,this) this.on(GameEvent.DO_AD_BACK,this.do_ad,this) this.start_btn?.on(NodeEventType.TOUCH_END, this.onStartFightBtnClick, this) this.removeMemoryPanel() } onDestroy(){ this.start_btn?.off(NodeEventType.TOUCH_END, this.onStartFightBtnClick, this) } /** * 帧更新: * - 非播放 / 暂停状态 → 跳过 * - 战斗中 → 同步怪物状态、更新计时器 */ protected update(dt: number): void { if(!smc.mission.play) return if(smc.mission.pause) return if(smc.mission.in_fight){ this.syncMonsterSpawnState(dt) if(smc.mission.stop_mon_action) return smc.vmdata.mission_data.fight_time+=dt this.FightTime-=dt this.update_time(); } } // ======================== 时间显示 ======================== /** 更新时间/波数显示(仅在秒数变化时更新以减少 Label 操作) */ update_time(){ const time = Math.max(0, this.FightTime); const remainSecond = Math.floor(time); if (remainSecond === this.lastTimeSecond) return; this.lastTimeSecond = remainSecond; let m = Math.floor(remainSecond / 60); let s = remainSecond % 60; const wave = Math.max(1, this.currentWave || smc.vmdata.mission_data.level || 1); let str = `W${wave.toString().padStart(2, '0')} ${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; if(str != this.lastTimeStr){ this.time_node.getChildByName("time").getComponent(Label).string = str; this.lastTimeStr = str; } } // ======================== 奖励与广告 ======================== /** 奖励发放(预留) */ do_reward(){ } /** * 广告回调处理: * 成功 → 增加刷新次数;失败 → 分发失败事件。 */ do_ad(){ if(this.ad_back()){ oops.message.dispatchEvent(GameEvent.AD_BACK_TRUE) smc.vmdata.mission_data.refresh_count+=FightSet.MORE_RC }else{ oops.message.dispatchEvent(GameEvent.AD_BACK_FALSE) } } /** 广告观看结果(预留,默认返回 true) */ ad_back(){ return true } // ======================== 任务生命周期 ======================== /** * 任务开始: * 1. 取消上一局延迟回调。 * 2. 清理残留实体。 * 3. 初始化全部局内数据。 * 4. 分发 FightReady 事件。 * 5. 进入准备阶段并显示 loading。 */ async mission_start(){ this.unscheduleAllCallbacks(); this.cleanComponents(); this.node.active=true this.data_init() oops.message.dispatchEvent(GameEvent.FightReady) this.enterPreparePhase() let loading=this.node.parent.getChildByName("loading") loading.active=true this.scheduleOnce(()=>{ loading.active=false },0.5) } /** * 进入战斗: * - 恢复刷怪 * - 标记战斗中 * - 隐藏开始按钮 * - 分发 FightStart 事件 */ to_fight(){ smc.mission.stop_spawn_mon = false; smc.mission.in_fight=true smc.vmdata.mission_data.in_fight = true if (this.start_btn && this.start_btn.isValid) this.start_btn.active = false; oops.message.dispatchEvent(GameEvent.FightStart) } /** * 进入准备阶段: * - 标记非战斗 * - 暂停刷怪 * - 显示开始按钮 */ private enterPreparePhase() { smc.mission.in_fight = false; smc.vmdata.mission_data.in_fight = false smc.mission.stop_spawn_mon = true; if (this.start_btn && this.start_btn.isValid) this.start_btn.active = true; } /** 开始战斗按钮点击回调 */ private onStartFightBtnClick() { if (!smc.mission.play) return; if (smc.mission.pause) return; if (smc.mission.in_fight) return; this.to_fight(); } /** * 打开结算弹窗: * - 暂停游戏 * - 打开 VictoryComp 弹窗 * * @param e 事件对象(未使用) * @param is_hero_dead 是否因英雄全灭触发 */ open_Victory(e:any,is_hero_dead: boolean = false){ smc.mission.pause = true; mLogger.log(this.debugMode, 'MissionComp', " open_Victory",is_hero_dead,this.revive_times) oops.gui.open(UIID.Victory,{ victory:false, rewards:this.rewards, game_data:this.game_data, can_revive: is_hero_dead && this.revive_times > 0 }) } /** 战斗结束:延迟清理组件和对象池 */ fight_end(){ this.scheduleOnce(() => { smc.mission.play=false this.cleanComponents() this.clearBattlePools() }, 0.5) } /** * 任务结束(完全退出关卡): * - 取消所有延迟回调 * - 重置全局标志 * - 清理组件和对象池 * - 隐藏节点 */ mission_end(){ this.unscheduleAllCallbacks(); smc.mission.play=false smc.mission.pause = false; smc.mission.in_fight = false; smc.vmdata.mission_data.in_fight = false if (this.start_btn && this.start_btn.isValid) this.start_btn.active = false; this.cleanComponents() this.clearBattlePools() this.node.active=false } /** * 初始化全部局内数据: * - 全局运行标志 * - 战斗时间 / 怪物数量 / 金币 / 波数 * - 奖励列表 / 复活次数 * - 性能监控基准值 */ data_init(){ smc.mission.play = true; smc.mission.pause = false; smc.mission.stop_mon_action = false; smc.mission.stop_spawn_mon = false; smc.vmdata.mission_data.in_fight=false smc.vmdata.mission_data.fight_time=0 smc.vmdata.mission_data.mon_num=0 smc.vmdata.mission_data.level = 1 smc.vmdata.mission_data.mon_max = Math.max(1, Math.floor(this.maxMonsterCount)) this.currentWave = 1; this.FightTime=FightSet.FiIGHT_TIME this.rewards=[] this.revive_times = 1; this.lastTimeStr = ""; this.lastTimeSecond = -1; this.memoryRefreshTimer = 0; this.lastMemoryText = ""; this.perfDtAcc = 0; this.perfFrameCount = 0; this.heapBaseMB = -1; this.heapPeakMB = 0; this.heapTrendPerMinMB = 0; this.heapTrendTimer = 0; this.heapTrendBaseMB = -1; this.monsterCountSyncTimer = 0; this.lastPrepareCoinWave = 0; smc.vmdata.mission_data.coin = Math.max(0, Math.floor(CardInitCoins)); } // ======================== 波次管理 ======================== /** * 新一波事件回调: * 1. 进入准备阶段。 * 2. 更新当前波数。 * 3. 发放本波金币奖励。 * 4. 刷新时间显示。 * * @param event 事件名 * @param data { wave: number } */ private onNewWave(event: string, data: any) { const wave = Number(data?.wave ?? 0); if (wave <= 0) return; this.enterPreparePhase(); this.currentWave = wave; smc.vmdata.mission_data.level = wave; this.grantPrepareCoinByWave(wave); this.lastTimeSecond = -1; this.update_time(); } /** * 按波数发放金币奖励: * reward = min(cap, base + (wave - 1) × grow) * 仅在波数首次到达时发放,防止重复。 * * @param wave 当前波数 */ private grantPrepareCoinByWave(wave: number) { if (wave <= 0) return; if (wave <= this.lastPrepareCoinWave) return; const base = Math.max(0, Math.floor(this.prepareBaseCoinReward)); const grow = Math.max(0, Math.floor(this.prepareCoinWaveGrow)); const cap = Math.max(0, Math.floor(this.prepareCoinRewardCap)); const reward = Math.min(cap, base + (wave - 1) * grow); if (reward <= 0) { this.lastPrepareCoinWave = wave; return; } smc.vmdata.mission_data.coin = Math.max(0, Math.floor((smc.vmdata.mission_data.coin ?? 0) + reward)); this.lastPrepareCoinWave = wave; oops.message.dispatchEvent(GameEvent.CoinAdd, { delta: reward, syncOnly: true }); mLogger.log(this.debugMode, 'MissionComp', "prepare coin reward", { wave, reward, coin: smc.vmdata.mission_data.coin }); } // ======================== 怪物数量管理 ======================== /** * 获取怪物数量阈值配置。 * @returns { max: 刷怪上限, resume: 恢复刷怪阈值 } */ private getMonsterThresholds(): { max: number; resume: number } { const max = Math.max(1, Math.floor(this.maxMonsterCount)); const resume = Math.min(max - 1, Math.max(0, Math.floor(this.resumeMonsterCount))); return { max, resume }; } /** * 同步怪物刷新状态(降频执行,每 0.2 秒一次): * 1. 遍历所有 HeroAttrsComp 实体,统计怪物和英雄数量。 * 2. 检测英雄全灭。 * 3. 根据 max/resume 阈值切换 stop_spawn_mon 状态。 * * @param dt 帧间隔 */ private syncMonsterSpawnState(dt: number) { this.monsterCountSyncTimer += dt; if (dt > 0 && this.monsterCountSyncTimer < 0.2) return; this.monsterCountSyncTimer = 0; let monsterCount = 0; let heroCount = 0; ecs.query(this.heroAttrsMatcher).forEach(entity => { const attrs = entity.get(HeroAttrsComp); if (!attrs || attrs.is_dead) return; if (attrs.fac === FacSet.MON) { monsterCount += 1; return; } if (attrs.fac === FacSet.HERO) { heroCount += 1; } }); this.handleHeroWipe(heroCount); smc.vmdata.mission_data.mon_num = monsterCount; const { max, resume } = this.getMonsterThresholds(); smc.vmdata.mission_data.mon_max = max; const stopSpawn = !!smc.mission.stop_spawn_mon; if (stopSpawn) { // 降至恢复阈值以下 → 恢复刷怪 if (monsterCount <= resume) smc.mission.stop_spawn_mon = false; return; } // 超过上限 → 暂停刷怪 if (monsterCount >= max) smc.mission.stop_spawn_mon = true; } /** * 英雄全灭检测:若场上无存活英雄且处于战斗中,触发结算弹窗。 * @param heroCount 当前存活英雄数量 */ private handleHeroWipe(heroCount: number) { if (heroCount > 0) return; if (!smc.mission.play || smc.mission.pause) return; if (!smc.mission.in_fight) return; smc.mission.in_fight = false; smc.vmdata.mission_data.in_fight = false; this.open_Victory(null, true); } // ======================== 清理 ======================== /** 清理所有英雄和技能 ECS 实体 */ private cleanComponents() { const heroEntities: ecs.Entity[] = []; ecs.query(this.heroViewMatcher).forEach(entity => { heroEntities.push(entity); }); heroEntities.forEach(entity => { entity.destroy(); }); const skillEntities: ecs.Entity[] = []; ecs.query(this.skillViewMatcher).forEach(entity => { skillEntities.push(entity); }); skillEntities.forEach(entity => { entity.destroy(); }); } /** 回收所有战斗对象池(Monster / Skill / Tooltip)并清理场景节点 */ private clearBattlePools() { Monster.clearPools(); Skill.clearPools(); Tooltip.clearPool(); this.clearBattleSceneNodes(); } /** 清理战斗场景中的 HERO 和 SKILL 根节点下的所有子节点 */ private clearBattleSceneNodes() { const scene = smc.map?.MapView?.scene; const layer = scene?.entityLayer?.node; if (!layer) return; const heroRoot = layer.getChildByName("HERO"); const skillRoot = layer.getChildByName("SKILL"); if (heroRoot) { for (let i = heroRoot.children.length - 1; i >= 0; i--) { heroRoot.children[i].destroy(); } } if (skillRoot) { for (let i = skillRoot.children.length - 1; i >= 0; i--) { skillRoot.children[i].destroy(); } } } /** 获取战斗层的英雄和技能节点数量(用于性能监控) */ private getBattleLayerNodeCount() { const scene = smc.map?.MapView?.scene; const layer = scene?.entityLayer?.node; if (!layer) return { heroNodes: 0, skillNodes: 0 }; const heroRoot = layer.getChildByName("HERO"); const skillRoot = layer.getChildByName("SKILL"); return { heroNodes: heroRoot?.children.length || 0, skillNodes: skillRoot?.children.length || 0 }; } // ======================== 性能监控面板 ======================== /** 性能监控相关代码 */ /** 初始化性能监控面板:在 time_node 下创建 Label */ private initMemoryPanel() { if (!this.showMemoryPanel || !this.time_node) return; let panel = this.time_node.getChildByName("mem_panel"); if (!panel) { panel = new Node("mem_panel"); panel.parent = this.time_node; panel.setPosition(0, -32, 0); } let label = panel.getComponent(Label); if (!label) { label = panel.addComponent(Label); } label.fontSize = 16; label.lineHeight = 20; this.memoryLabel = label; } /** 移除性能监控面板 */ private removeMemoryPanel() { const panel = this.time_node?.getChildByName("mem_panel"); if (panel) { panel.destroy(); } this.memoryLabel = null; this.lastMemoryText = ""; } /** * 更新性能监控面板内容(每 0.5 秒一次): * 显示 堆内存 / 增长趋势 / 帧率 / 实体数量 / 对象池状态 等信息。 */ private updateMemoryPanel(dt: number) { if (!this.showMemoryPanel || !this.memoryLabel) return; this.perfDtAcc += dt; this.perfFrameCount += 1; this.memoryRefreshTimer += dt; if (this.memoryRefreshTimer < 0.5) return; this.memoryRefreshTimer = 0; let heroCount = 0; ecs.query(this.heroViewMatcher).forEach(() => { heroCount++; }); let skillCount = 0; ecs.query(this.skillViewMatcher).forEach(() => { skillCount++; }); const monPool = Monster.getPoolStats(); const skillPool = Skill.getPoolStats(); const tooltipPool = Tooltip.getPoolStats(); const layerNodes = this.getBattleLayerNodeCount(); const perf = (globalThis as any).performance; const heapBytes = perf && perf.memory ? perf.memory.usedJSHeapSize : 0; let heapMB = heapBytes > 0 ? heapBytes / 1024 / 1024 : -1; if (heapMB > 0 && this.heapBaseMB < 0) { this.heapBaseMB = heapMB; this.heapPeakMB = heapMB; this.heapTrendBaseMB = heapMB; this.heapTrendTimer = 0; } if (heapMB > this.heapPeakMB) { this.heapPeakMB = heapMB; } this.heapTrendTimer += 0.5; if (heapMB > 0 && this.heapTrendBaseMB > 0 && this.heapTrendTimer >= 10) { const deltaMB = heapMB - this.heapTrendBaseMB; this.heapTrendPerMinMB = (deltaMB / this.heapTrendTimer) * 60; this.heapTrendBaseMB = heapMB; this.heapTrendTimer = 0; } const heapText = heapMB > 0 ? heapMB.toFixed(1) : "N/A"; const heapDeltaText = this.heapBaseMB > 0 && heapMB > 0 ? (heapMB - this.heapBaseMB).toFixed(1) : "N/A"; const heapPeakText = this.heapPeakMB > 0 ? this.heapPeakMB.toFixed(1) : "N/A"; const avgDt = this.perfFrameCount > 0 ? this.perfDtAcc / this.perfFrameCount : 0; const fps = avgDt > 0 ? 1 / avgDt : 0; this.perfDtAcc = 0; this.perfFrameCount = 0; const text = `Heap:${heapText}MB Δ:${heapDeltaText} Peak:${heapPeakText}\n` + `Trend:${this.heapTrendPerMinMB.toFixed(2)}MB/min\n` + `Perf dt:${(avgDt * 1000).toFixed(1)}ms fps:${fps.toFixed(1)}\n` + `Ent H:${heroCount} S:${skillCount} N:${layerNodes.heroNodes}/${layerNodes.skillNodes}\n` + `Pool M:${monPool.total}(${monPool.paths}) K:${skillPool.total}(${skillPool.paths}) T:${tooltipPool.total}`; if (text === this.lastMemoryText) return; this.lastMemoryText = text; this.memoryLabel.string = text; } /** ECS 组件移除时销毁节点 */ reset() { this.node.destroy(); } }