/** * @file MissionComp.ts * @description 任务(关卡)核心控制组件(UI + 逻辑层) * * 职责: * 1. 管理单局游戏的 **完整生命周期**:初始化 → 小节循环(战斗→结算弹窗)→ 章节通关。 * 2. 在战斗阶段每帧更新战斗计时器、同步怪物数量、检测英雄全灭。 * 3. 驱动小节推进:SectionStart → 战斗 → VictoryComp 结算 → 下一节/下一关。 * 4. 处理关卡进度持久化(chapter / stage / section)。 * 5. 提供战斗结束后的结算弹窗入口(VictoryComp)。 * 6. (可选)内建性能监控面板,显示内存、帧率、实体数量等开发信息。 * * 关键设计: * - mission_start() 初始化所有游戏数据 → 恢复存档进度 → 进入第一小节。 * - 每节即一局:怪物全灭后弹 VictoryComp 结算。 * - 第 1~9 节胜利点"下一节"继续;第 10 节胜利显示"本关通关",确认退出并存档进下一关。 * - 每节完全独立:进入下一节时重派 MissionStart 触发三选一,英雄重新召唤。 * - cleanComponents() 在任务开始/结束时销毁所有英雄和技能 ECS 实体。 * - clearBattlePools() 回收对象池(Monster / Skill / Tooltip)。 * * 依赖: * - smc.mission —— 全局任务运行状态(play / pause / in_fight / stop_spawn_mon 等) * - smc.vmdata.mission_data —— 局内数据(金币 / 回合数 / 怪物数量 / 关卡进度等) * - StageSet —— 关卡配置与数值公式 * - UIID.Victory —— 结算弹窗 */ import { _decorator, Vec3, Animation, instantiate, Prefab, Node, NodeEventType, ProgressBar, Label, CCInteger, tween, v3, Tween, Widget, UIOpacity } 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 { SkillTriggerType } from "../common/config/heroSet"; import { HeroViewComp } from "../hero/HeroViewComp"; import { SkillTriggerHelper } from "../hero/SkillTriggerHelper"; import { UIID } from "../common/config/GameUIConfig"; import { SkillView } from "../skill/SkillView"; import { FacSet, FightSet } from "../common/config/GameSet"; import { HeroInfo } from "../common/config/heroSet"; import { mLogger } from "../common/Logger"; import { Monster } from "../hero/Mon"; import { Skill } from "../skill/Skill"; import { Tooltip } from "../skill/Tooltip"; import { Timer } from "db://oops-framework/core/common/timer/Timer"; import { FieldSkillType } from "../common/config/SkillSet"; import { FieldSkillHelper } from "../hero/FieldSkillHelper"; import { spawningEngine, WAVE_DURATION, BATTLE_TIMEOUT } from "./RogueConfig"; import { getProgressText, } from "../common/config/StageSet"; const { ccclass, property } = _decorator; /** 任务(关卡)生命周期阶段 */ export enum MissionPhase { None = 0, // 未初始化 PrepareStart = 1, // 准备开始阶段 (2s) Prepare = 2, // 准备阶段 (等待玩家点击开始) PrepareEnd = 3, // 准备结束阶段 (2s) BattleStart = 4, // 战斗开始阶段 (2s) Battle = 5, // 战斗阶段 (刷怪、战斗中) BattleEnd = 6, // 战斗结束阶段 (2s) Settle = 7, // 结算阶段(VictoryComp 弹窗,等待玩家点击下一节/下一关/退出) } /** 结算类型(驱动 VictoryComp 按钮分流与文案) */ type SettleType = "defeat" | "section" | "final"; //@todo 需要关注 当boss死亡的时候的动画播放完成后,需要触发事件,通知 MissionComp 进行奖励处理 /** * MissionComp —— 任务(关卡)核心控制器 * * 驱动单局游戏的完整流程:准备 → 战斗 → 小节结算 → 大关结算 → 章节结算。 * 管理战斗计时、怪物数量控制、英雄全灭检测和关卡进度推进。 */ @ccclass('MissionComp') @ecs.register('MissionComp', false) export class MissionComp extends CCComp { @property({ tooltip: "是否启用调试日志" }) private debugMode: boolean = true; @property({ tooltip: "是否显示战斗内存观测面板" }) private showMemoryPanel: boolean = false; // ======================== 配置参数 ======================== // ======================== 编辑器绑定节点 ======================== /** 开始战斗按钮 */ @property(Node) start_btn: Node = null! /** 战斗计时 Label(mm:ss) */ @property(Label) time_label: Label = null! @property(Node) tooltip: Node = null! /** 阶段名称映射表(用于 UI 显示) */ private static readonly PhaseNameMap: Record = { [MissionPhase.None]: "未开始", [MissionPhase.PrepareStart]: "准备开始", [MissionPhase.Prepare]: "准备阶段", [MissionPhase.PrepareEnd]: "准备结束", [MissionPhase.BattleStart]: "战斗开始", [MissionPhase.Battle]: "战斗中", [MissionPhase.BattleEnd]: "战斗结束", [MissionPhase.Settle]: "结算阶段", }; // ======================== 运行时状态 ======================== /** 本回合战斗已耗时(秒),正向计时,用于自适应难度评估 */ clearTime: number = 0 /** 剩余复活次数 */ revive_times: number = 1; /** 掉落奖励列表 */ rewards: any[] = [] /** 累计游戏数据 */ game_data: any = { exp: 0, gold: 0, diamond: 0 } /**秒计时 */ PhaseTime: Timer = new Timer(1) /** 回合间倒计时(秒) */ private waveCountdown: number = 0; /** 倒计时档位:快清场(压缩垃圾时间,奖励强 build) */ private static readonly COUNTDOWN_FAST = 2.5; /** 倒计时档位:普通 */ private static readonly COUNTDOWN_NORMAL = 4.0; /** 倒计时档位:有英雄死亡(保留满运营窗口:调整阵型/复活/买卡) */ private static readonly COUNTDOWN_FULL = 5.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; /** 是否为Boss回合 */ private isBossWave: boolean = false; /** 超时回合已在 onBattleTimeout 预扣留存分,BattleEnd 需跳过重复累加 */ private skipRemainScoreOnBattleEnd: boolean = false; /** 上回合英雄死亡数快照(BattleEnd 写入,供下一回合倒计时档位判定) */ private lastWaveDeathCount: number = 0; /** 上回合计时口径清场时间快照(含清场加速还原,供下一回合倒计时档位判定) */ private lastWaveClearTime: number = 0; /** 当前任务阶段 */ public currentPhase: MissionPhase = MissionPhase.None; /** 是否处于回合间倒计时状态 */ private isWaveCountdown: boolean = false; /** 本局是否胜利(open_Victory 时记录,供结算弹窗使用) */ private _victory: boolean = false; /** 本次结算类型(小节胜利/末关通关/失败,供结算弹窗按钮分流) */ private _settleType: SettleType = "defeat"; /** 末关通关时结算页展示的标题("本关通关"/"本章通关"),其余结算为空 */ private _settleTitle: string = ""; /** 结算弹窗延迟(秒):等待最后一个英雄/怪物死亡动画播完再切 BattleEnd */ private static readonly SETTLE_DELAY = 1.5; /** 结算是否已预约调度(防止 0.2s 同步周期内重复 scheduleOnce) */ private _settleScheduled: boolean = false; // ======================== 关卡进度状态 ======================== /** 当前章节号 */ public currentChapter: number = 1; /** 当前关卡号(1~10) */ public currentStage: number = 1; /** 当前小节号(1~10) */ public currentSection: number = 1; // ======================== ECS 查询匹配器(预缓存) ======================== /** 匹配拥有 HeroViewComp 的实体(英雄/怪物视图) */ private heroViewMatcher: any = null; /** 匹配拥有 SkillView 的实体(技能视图) */ private skillViewMatcher: any = null; /** 匹配拥有 HeroAttrsComp 的实体(英雄/怪物属性) */ private heroAttrsMatcher: any = null; // ======================== 生命周期 ======================== onLoad() { this.heroViewMatcher = ecs.allOf(HeroViewComp); this.skillViewMatcher = ecs.allOf(SkillView); this.heroAttrsMatcher = ecs.allOf(HeroAttrsComp); this.showMemoryPanel = false // 注册生命周期事件 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.on(GameEvent.NextSection, this.onNextSection, this) // 结算弹窗"再来一次"按钮重开本关 this.on(GameEvent.RestartMission, this.onRestartMission, this) this.start_btn?.on(NodeEventType.TOUCH_END, this.onStartFightBtnClick, this) // 开局免费三选一选完后自动进入战斗 oops.message.on("OpeningPicksDone", this.onOpeningPicksDone, this); this.removeMemoryPanel() } onAdded(args: any) { // 使用 scheduleOnce 将事件推迟到下一帧执行, // 确保所有关联组件(如 MissionCardComp 等)都已经完成其 onLoad 生命周期。 this.scheduleOnce(() => { oops.message.dispatchEvent(GameEvent.MissionStart, {}); this.mission_start(); }, 0); smc.map.MapView.scene.mapLayer.stopAnimations(); // smc.map.MapView.scene.mapLayer.node.getChildByName("fight").getChildByName("fbox").active = true; } onDestroy() { smc.map.MapView.scene.mapLayer.playAnimations() // smc.map.MapView.scene.mapLayer.node.getChildByName("fight").getChildByName("fbox").active = false; super.onDestroy(); if (this.start_btn && this.start_btn.isValid) { this.start_btn.off(NodeEventType.TOUCH_END, this.onStartFightBtnClick, this); } oops.message.off("OpeningPicksDone", this.onOpeningPicksDone, this); } /** * 帧更新: * - 非播放 / 暂停状态 → 跳过 * - 战斗中 → 同步怪物状态、更新计时器 */ protected update(dt: number): void { if (!smc.mission.play) return // 如果是暂停状态,且不在 BattleEnd 阶段(全灭时需要自动流转),才真正停止 update 逻辑 if (smc.mission.pause && this.currentPhase !== MissionPhase.BattleEnd) return // 回合间倒计时:PrepareStart 复用为倒计时阶段,5 秒后自动进入下一回合 if (this.currentPhase === MissionPhase.PrepareStart) { this.waveCountdown -= dt; this.updateCountdownUI(); if (this.waveCountdown <= 0) { this.autoNextPhase(); } return; } // 处理过渡阶段的计时 if (this.currentPhase === MissionPhase.PrepareEnd || this.currentPhase === MissionPhase.BattleStart || this.currentPhase === MissionPhase.BattleEnd) { if (this.PhaseTime.update(dt)) { this.autoNextPhase(); } } if (this.currentPhase === MissionPhase.Battle) { this.syncMonsterSpawnState(dt) if (smc.mission.stop_mon_action) return smc.vmdata.mission_data.fight_time += dt this.clearTime += dt this.update_time(); // 战斗超时兜底:僵局(坦克 vs 高血 Boss 互磨不死)时强制结算整局 if (this.clearTime >= BATTLE_TIMEOUT) { this.onBattleTimeout(); } } } // ======================== 时间显示 ======================== /** 更新时间/回合数显示(仅在秒数变化时更新以减少 Label 操作) */ update_time() { const remainSecond = Math.floor(smc.vmdata.mission_data.fight_time); if (remainSecond === this.lastTimeSecond) return; this.lastTimeSecond = remainSecond; let m = Math.floor(remainSecond / 60); let s = remainSecond % 60; let str = `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; if (str != this.lastTimeStr) { if (this.time_label && this.time_label.isValid) { this.time_label.string = str; } this.lastTimeStr = str; } } // ======================== 回合倒计时 ======================== /** 进入回合间倒计时:按上回合战况分档(快清场压缩垃圾时间,有死亡保留运营窗口) */ private startWaveCountdown() { this.waveCountdown = this.computeCountdown(); this.isWaveCountdown = true; this.updateCountdownUI(true); } /** 计算下一回合倒计时档位:读 BattleEnd 写入的快照(onNewWave 会清零 clearTime,不能现读) */ private computeCountdown(): number { if (this.lastWaveDeathCount > 0) return MissionComp.COUNTDOWN_FULL; if (this.lastWaveClearTime > 0 && this.lastWaveClearTime < WAVE_DURATION * 0.65) return MissionComp.COUNTDOWN_FAST; return MissionComp.COUNTDOWN_NORMAL; } /** 更新倒计时 UI(显示剩余秒数) */ private updateCountdownUI(force: boolean = false) { if (!this.isWaveCountdown) return; const remain = Math.max(0, Math.ceil(this.waveCountdown)); if (!force && remain === this.lastTimeSecond) return; this.lastTimeSecond = remain; } /** 结束回合间倒计时 */ private stopWaveCountdown() { this.isWaveCountdown = false; this.lastTimeSecond = -1; } // ======================== 奖励与广告 ======================== /** 奖励发放(预留) */ 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. 恢复关卡进度存档。 * 5. 进入准备阶段并显示 loading。 */ async mission_start() { this.unscheduleAllCallbacks(); this.cleanComponents(); this.data_init() this.loadStageProgress(); oops.message.dispatchEvent(GameEvent.FightReady) this.changePhase(MissionPhase.Prepare); // 派发首个 SectionStart,MissionMonComp 据此生成开局怪物 oops.message.dispatchEvent(GameEvent.SectionStart, { chapter: this.currentChapter, stage: this.currentStage, section: this.currentSection, }); let loading = this.node.getChildByName("loading") if (loading) { loading.active = true this.scheduleOnce(() => { loading.active = false }, 0.5) } // 播放战斗背景音乐,并稍微降低音量 oops.audio.volumeMusic = 0.5; oops.audio.playerMusicLoop("music/BATTLE"); } /** 更新开始按钮的状态显示 */ private updateStartBtnState() { if (!this.start_btn || !this.start_btn.isValid) return; const nobg = this.start_btn.getChildByName("nobg"); if (nobg) { // 只有在 Prepare 阶段且未暂停时,按钮才可点击,否则激活 nobg(显示不可点击状态) const canClick = this.currentPhase === MissionPhase.Prepare && smc.mission.play && !smc.mission.pause; nobg.active = !canClick; } } /** 播放阶段提示栏(Tooltip)动感切换动画 */ private playTooltipAnim(phaseName: string) { if (!this.tooltip || !this.tooltip.isValid) { console.warn("MissionComp: tooltip 节点未绑定或已失效,无法播放阶段提示动画!请在编辑器中将对应节点拖入 tooltip 属性中。"); return; } // 先激活节点,确保后续的组件刷新和渲染数据更新能正常生效 this.tooltip.active = true; // 禁用 Widget 组件,防止其在 LateUpdate 中覆盖 tween 的位置修改 const widget = this.tooltip.getComponent(Widget); if (widget) { widget.updateAlignment(); // 强制刷新一次布局,防止第一次激活时尺寸未初始化 widget.enabled = false; } const labNode = this.tooltip.getChildByName("lab"); if (labNode) { const label = labNode.getComponent(Label); if (label) { label.string = phaseName; label.updateRenderData(true); // 强制更新渲染数据,避免首次无文本 } } Tween.stopAllByTarget(this.tooltip); // 动感动画设计:右侧进入 -> 屏幕中央(带有轻微的弹跳和滑动) -> 左侧飞出 // 假设屏幕宽度适配下,1200是一个足够的屏幕外距离,适配横竖屏 const startPos = v3(1200, this.tooltip.position.y, this.tooltip.position.z); const centerPos = v3(0, this.tooltip.position.y, this.tooltip.position.z); const driftPos = v3(-50, this.tooltip.position.y, this.tooltip.position.z); // 在中央时的缓慢漂移 const endPos = v3(-1200, this.tooltip.position.y, this.tooltip.position.z); this.tooltip.setPosition(startPos); tween(this.tooltip) // 1. 从右侧快速飞入并带回弹效果 (0.5秒) .to(0.5, { position: centerPos }, { easing: "backOut" }) // 2. 在屏幕中央缓慢向左漂移,增强动感停留 (1.0秒) .to(1.0, { position: driftPos }, { easing: "sineInOut" }) // 3. 快速向左飞出并消失 (0.4秒) .to(0.4, { position: endPos }, { easing: "backIn" }) .call(() => { this.tooltip.active = false; }) .start(); } /** * 阶段切换核心方法(状态机) * 处理状态流转时所需的事件触发和全局标志位修改。 * @param targetPhase 目标阶段 */ private changePhase(targetPhase: MissionPhase) { if (this.currentPhase === targetPhase) return; const oldPhase = this.currentPhase; this.currentPhase = targetPhase; const phaseName = MissionComp.PhaseNameMap[targetPhase] || "未知"; // 播放状态切换提示栏动效(过滤掉 None、Prepare 准备阶段、Battle 战斗中阶段) if (targetPhase !== MissionPhase.None && targetPhase !== MissionPhase.Prepare && targetPhase !== MissionPhase.Battle) { this.playTooltipAnim(phaseName); } // 重置状态机的计时器 if (this.PhaseTime) { this.PhaseTime.reset(); } switch (targetPhase) { case MissionPhase.PrepareStart: smc.mission.in_fight = false; smc.vmdata.mission_data.in_fight = false; smc.mission.stop_spawn_mon = true; // 回合间倒计时:隐藏开始按钮(nobg 激活表示不可点击状态) if (this.start_btn && this.start_btn.isValid) { const nobg = this.start_btn.getChildByName("nobg"); if (nobg) nobg.active = true; } // 启动 5 秒倒计时,由 update 驱动自动进入下一回合 this.startWaveCountdown(); oops.message.dispatchEvent("PhasePrepareStart"); break; case MissionPhase.Prepare: if (this.start_btn && this.start_btn.isValid) { this.start_btn.active = true; const nobg = this.start_btn.getChildByName("nobg"); if (nobg) nobg.active = false; } break; case MissionPhase.PrepareEnd: // 不隐藏开始按钮 // 开闸刷怪:MissionMonComp 在本阶段启动分批释放,需解除暂停标志让第一批按时出场 smc.mission.stop_spawn_mon = false; oops.message.dispatchEvent("PhasePrepareEnd"); break; case MissionPhase.BattleStart: // 触发战斗开始技能(fstart) this.triggerHeroBattleSkills(); // Boss 预警:Boss 同步场立即登场(eta=0) if (this.isBossWave) { oops.message.dispatchEvent(GameEvent.BossWarning, { wave: this.currentWave, eta: 0, }); } break; case MissionPhase.Battle: smc.mission.stop_spawn_mon = false; smc.mission.in_fight = true; smc.vmdata.mission_data.in_fight = true; // 战斗阶段:不隐藏开始按钮,激活 nobg if (this.start_btn && this.start_btn.isValid) { const nobg = this.start_btn.getChildByName("nobg"); if (nobg) nobg.active = true; } oops.message.dispatchEvent(GameEvent.FightStart); break; case MissionPhase.BattleEnd: smc.mission.in_fight = false; smc.vmdata.mission_data.in_fight = false; smc.mission.stop_spawn_mon = true; if (smc.mission.play && !smc.mission.pause) { // 【评分系统 - 战绩分】每回合胜利加分 smc.vmdata.scores.wave_win_count++; // 【评分系统 - 战绩分】记录每回合结束时场上留存的敌人数量(扣分项) // 超时回合已在 onBattleTimeout 预扣,跳过防重 if (!this.skipRemainScoreOnBattleEnd) { smc.vmdata.scores.wave_remain_monsters += smc.vmdata.mission_data.mon_num; } this.skipRemainScoreOnBattleEnd = false; let allAlive = true; let hasHero = false; ecs.query(this.heroAttrsMatcher).forEach(entity => { const attrs = entity.get(HeroAttrsComp); if (attrs && attrs.fac === FacSet.HERO) { hasHero = true; } }); // 英雄为真实死亡(实体已销毁),本回合死亡数取死亡记录长度 const heroDeathCount = smc.mission.dead_heroes.length; if (heroDeathCount > 0) allAlive = false; // 快照供倒计时档位判定(onNewWave 会清零 clearTime,必须提前快照) this.lastWaveDeathCount = heroDeathCount; this.lastWaveClearTime = this.clearTime; // 【评分系统 - 战绩分】记录全员存活的胜利回合数(额外加分) if (hasHero && allAlive) { smc.vmdata.scores.wave_all_alive_count++; } // 【评分系统 - 战绩分】单场 Boss 战定胜负,通过即视为通关 smc.vmdata.scores.passed_wave_20 = true; } // 战斗结束阶段,按 FightSet.WAVE_HEAL_RATE 恢复所有英雄血量 this.healAllHeroes(); break; case MissionPhase.Settle: smc.mission.in_fight = false; smc.vmdata.mission_data.in_fight = false; smc.mission.stop_spawn_mon = true; // 不隐藏开始按钮 break; case MissionPhase.None: smc.mission.in_fight = false; smc.vmdata.mission_data.in_fight = false; smc.mission.stop_spawn_mon = false; if (this.start_btn && this.start_btn.isValid) { const nobg = this.start_btn.getChildByName("nobg"); if (nobg) nobg.active = true; } break; } // 阶段切换后更新按钮状态 this.updateStartBtnState(); } /** 自动流转到下一阶段(过渡状态结束时调用) */ private autoNextPhase() { switch (this.currentPhase) { case MissionPhase.PrepareStart: // 回合间倒计时结束,停止倒计时状态,直接进入 PrepareEnd(不再等待玩家点击) this.stopWaveCountdown(); this.changePhase(MissionPhase.PrepareEnd); break; case MissionPhase.PrepareEnd: this.changePhase(MissionPhase.BattleStart); break; case MissionPhase.BattleStart: this.changePhase(MissionPhase.Battle); break; case MissionPhase.BattleEnd: // BattleEnd 计时结束后,如果是因为全灭或手动调用的 fight_end,进入 Settle // 需要注意的是,open_Victory / fight_end 现在只需切换到 BattleEnd 即可,Settle 由这里自动接管 // 如果游戏正在运行(回合更迭),则自动进入 PrepareStart 阶段 if (smc.mission.play && !smc.mission.pause) { this.changePhase(MissionPhase.PrepareStart); } else { this.changePhase(MissionPhase.Settle); // 此时已经经过了 2s,可以真正执行结算弹窗或清理逻辑 if (smc.mission.play) { // 如果游戏还在运行中,说明是通过 open_Victory 进来的 smc.mission.pause = true; mLogger.log(this.debugMode, 'MissionComp', " autoNextPhase -> open_Victory logic", this.revive_times); oops.gui.open(UIID.Victory, { victory: this._victory, all_alive: this.checkAllHeroAlive(), rewards: this.rewards, game_data: this.game_data, can_revive: this.revive_times > 0, // 结算类型:小节胜利显示"下一节",末关通关显示"确认退出" + 通关标题 settle_type: this._settleType, settle_title: this._settleTitle, }); } else { // 如果 play 已经是 false,说明是通过 fight_end 进来的 this.cleanComponents(); this.clearBattlePools(); } } break; } } /** * 进入战斗: * - 恢复刷怪 * - 标记战斗中 * - 隐藏开始按钮 * - 分发 FightStart 事件 * - 触发英雄战斗开始技能 */ to_fight() { this.changePhase(MissionPhase.PrepareEnd); } /** * 进入准备阶段: * - 标记非战斗 * - 暂停刷怪 * - 显示开始按钮 */ private enterPreparePhase() { this.changePhase(MissionPhase.PrepareStart); } /** * 触发英雄的战斗开始技能 */ private triggerHeroBattleSkills() { let triggerCount = 1; triggerCount += FieldSkillHelper.getFieldSkillTotalValue(FieldSkillType.StartCount); triggerCount = Math.max(1, Math.floor(triggerCount)); ecs.query(this.heroAttrsMatcher).forEach(entity => { const attrs = entity.get(HeroAttrsComp); const view = entity.get(HeroViewComp); if (!attrs || !view || attrs.is_dead || attrs.fac !== FacSet.HERO) return; // 触发战斗开始技能 for (let i = 0; i < triggerCount; i++) { SkillTriggerHelper.trigger(SkillTriggerType.FStart, attrs, view); } }); } /** * 战斗结束阶段治疗所有英雄(包括墓地英雄),按 FightSet.WAVE_HEAL_RATE 恢复最大生命值比例 */ private healAllHeroes() { const healRateBoost = FieldSkillHelper.getFieldSkillTotalValue(FieldSkillType.WaveHeal); const finalHealRate = Math.min(1, FightSet.WAVE_HEAL_RATE + healRateBoost); ecs.query(this.heroAttrsMatcher).forEach(entity => { const attrs = entity.get(HeroAttrsComp); const view = entity.get(HeroViewComp); if (!attrs || !view || attrs.fac !== FacSet.HERO) return; // 计算恢复量:基于配置的百分比(如 70%)的最大生命值 const healAmount = Math.floor(attrs.hp_max * finalHealRate); // 应用恢复量,不超过最大生命值 attrs.hp = Math.min(attrs.hp_max, attrs.hp + healAmount); attrs.dirty_hp = true; // 重置复活次数,使得下回合可以继续复活 attrs.revived_count = 0; // 触发治疗动画,即使在墓地的英雄也触发(会在其当前位置播放) view.health(healAmount); }); } /** 开始战斗按钮点击回调 */ private onStartFightBtnClick() { if (!smc.mission.play) return; if (smc.mission.pause) return; if (this.currentPhase !== MissionPhase.Prepare) return; oops.audio.playEffect("music/button"); this.to_fight(); } /** 结算弹窗"下一节"事件:关闭弹窗并推进到下一小节 */ private onNextSection() { oops.gui.remove(UIID.Victory); this.advanceToNextSection(); } /** 结算弹窗"再来一次"事件:关闭弹窗并重开本关 */ private onRestartMission() { oops.gui.remove(UIID.Victory); this.restartCurrentMission(); } /** 开局免费三选一选完,自动进入战斗(等效于点击开始按钮) */ private onOpeningPicksDone() { if (this.currentPhase !== MissionPhase.Prepare) return; if (!smc.mission.play || smc.mission.pause) return; this.to_fight(); } /** * 打开结算弹窗: * - 暂停游戏 * - 按当前进度判定结算类型(小节胜利 / 末关通关 / 失败) * - 切入 BattleEnd,2s 后由 autoNextPhase 自动弹出 VictoryComp * * @param e 事件对象(未使用) * @param is_hero_dead 是否因英雄全灭触发 * @param victory 是否胜利(默认 false) */ open_Victory(e: any, is_hero_dead: boolean = false, victory: boolean = false) { // 战斗失败或胜利,标记暂停状态以切断回合流转逻辑 smc.mission.pause = true; // 记录胜利标记,供结算弹窗使用 this._victory = victory; this._settleTitle = ""; if (!victory) { // 失败:进度不变,结算窗走复活/退出流程 this._settleType = "defeat"; } else if (this.currentSection < 10) { // 第 1~9 节胜利:还有下一节,结算窗显示"下一节" this._settleType = "section"; } else { // 第 10 节胜利:本关最后一节,无"下一节"按钮,结算页显示通关标题、确认即退出 this._settleType = "final"; this._settleTitle = this.currentStage >= 10 ? "本章通关" : "本关通关"; // 存档直接推进到下一关第 1 小节(仅写存档,内存进度保留用于本局结算显示); // 第 10 关通关即本章全通,存档停留等待后续章节扩展 if (this.currentStage < 10) { smc.updateStageProgress({ chapter: this.currentChapter, stage: this.currentStage + 1, section: 1, }); if ((smc as any).updateCloudData) { (smc as any).updateCloudData(); } mLogger.log(this.debugMode, 'MissionComp', `[StageProgress] 本关通关,存档推进: ${this.currentChapter}-${this.currentStage + 1}-1`); } } // 直接切入 BattleEnd,2s 后由 autoNextPhase 自动流转弹窗结算 this.changePhase(MissionPhase.BattleEnd); } /** 战斗结束:延迟清理组件和对象池 */ fight_end() { // 这里只是强制清理关卡,为了防止重复弹窗,标记 play = false smc.mission.play = false this.changePhase(MissionPhase.BattleEnd); } /** * 任务结束(完全退出关卡): * - 取消所有延迟回调 * - 重置全局标志 * - 清理组件和对象池 * - 隐藏节点 */ mission_end() { this.unscheduleAllCallbacks(); smc.mission.play = false smc.mission.pause = false; this.changePhase(MissionPhase.None); this.cleanComponents() this.clearBattlePools() oops.gui.remove(UIID.Mission); } /** * 初始化全部局内数据: * - 全局运行标志 * - 战斗时间 / 怪物数量 / 金币 / 回合数 * - 奖励列表 / 复活次数 * - 性能监控基准值 */ data_init() { if (!this.PhaseTime) { this.PhaseTime = new Timer(1); } 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 this.clearTime = 0 smc.vmdata.mission_data.mon_num = 0 smc.vmdata.mission_data.level = 1 this.currentPhase = MissionPhase.None; this.currentWave = 1; this.isBossWave = false; this.skipRemainScoreOnBattleEnd = false; this.lastWaveDeathCount = 0; this.lastWaveClearTime = 0; 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; spawningEngine.reset(); // 重置所有的战局得分数据,防止上一局的数据污染 smc.resetScores(); smc.vmdata.mission_data.coin = Math.max(0, Math.floor(FightSet.INIT_COIN)); // 银币为局外持久货币,每局不重置,仅通过日常积攒,无需初始化 // 【评分系统 - 效率分】记录初始获得的金币收入 smc.vmdata.scores.gold_earned += smc.vmdata.mission_data.coin; } // ======================== 关卡进度管理 ======================== /** * 加载关卡进度存档 * 从 smc.data.stage_progress 恢复,缺省从 1-1-1 开始 */ private loadStageProgress() { const progress = smc.data.stage_progress; if (progress && progress.chapter >= 1 && progress.stage >= 1 && progress.section >= 1) { this.currentChapter = progress.chapter; this.currentStage = progress.stage; this.currentSection = progress.section; } else { this.currentChapter = 1; this.currentStage = 1; this.currentSection = 1; } // 加载完成后将权威进度镜像到 vmdata,驱动 HUD 实时显示 smc.syncStageProgressToView(); mLogger.log(this.debugMode, 'MissionComp', `[StageProgress] 加载进度: ${getProgressText(this.currentChapter, this.currentStage, this.currentSection)}`); } /** * 保存关卡进度存档 * 写入 smc.data.stage_progress 并镜像到 vmdata,同时标记云端同步 */ private saveStageProgress() { smc.updateStageProgress({ chapter: this.currentChapter, stage: this.currentStage, section: this.currentSection, }); // 标记数据脏,触发云端/本地同步 if ((smc as any).updateCloudData) { (smc as any).updateCloudData(); } mLogger.log(this.debugMode, 'MissionComp', `[StageProgress] 保存进度: ${getProgressText(this.currentChapter, this.currentStage, this.currentSection)}`); } /** * 推进到下一小节 * 结算弹窗保证:第 10 节胜利时不显示"下一节",不会触发本方法 */ private advanceToNextSection() { this.currentSection++; this.startNextSectionBattle(); } /** * 开始下一小节战斗: * 1. 清理上一节战场(英雄/技能实体 + 对象池)。 * 2. 重置局内数据(金币/计时/标志位)。 * 3. 重派 MissionStart 触发开局免费三选一(MissionCardComp 监听)。 * 4. 进入准备阶段并派发 SectionStart 生成新小节怪物。 * 5. 保存进度存档(精确到待挑战小节)。 */ private startNextSectionBattle() { this.cleanComponents(); this.clearBattlePools(); this.data_init(); this.changePhase(MissionPhase.Prepare); // 重新三选一:MissionCardComp 监听 MissionStart 重置开局免费抽卡 oops.message.dispatchEvent(GameEvent.MissionStart, {}); oops.message.dispatchEvent(GameEvent.FightReady); // 触发 SectionStart 事件,MissionMonComp 据此生成新小节怪物 oops.message.dispatchEvent(GameEvent.SectionStart, { chapter: this.currentChapter, stage: this.currentStage, section: this.currentSection, }); this.saveStageProgress(); mLogger.log(this.debugMode, 'MissionComp', `[StageProgress] 进入小节: ${getProgressText(this.currentChapter, this.currentStage, this.currentSection)}`); } /** * 重开本关:清理战场并重置数据,从当前小节重新开始 * 与 advanceToNextSection 的区别:不推进小节进度,保持当前 section 不变 */ private restartCurrentMission() { // 清理战场与局内数据 this.cleanComponents(); this.clearBattlePools(); this.data_init(); // 重置结算状态 this._victory = false; this._settleType = "defeat"; this._settleTitle = ""; // 进入准备阶段 this.changePhase(MissionPhase.Prepare); // 触发 MissionStart 重置开局免费抽卡 oops.message.dispatchEvent(GameEvent.MissionStart, {}); oops.message.dispatchEvent(GameEvent.FightReady); // 触发 SectionStart 重新生成当前小节怪物 oops.message.dispatchEvent(GameEvent.SectionStart, { chapter: this.currentChapter, stage: this.currentStage, section: this.currentSection, }); mLogger.log(this.debugMode, 'MissionComp', `[StageProgress] 重开本关: ${getProgressText(this.currentChapter, this.currentStage, this.currentSection)}`); } // ======================== 回合管理 ======================== /** * 新一回合事件回调: * 1. 进入准备阶段。 * 2. 更新当前回合数。 * 3. 刷新时间显示。 * * 注意:金币不再按回合固定发放,改为怪物死亡时掉落(见 HeroAtkSystem)。 * * @param event 事件名 * @param data { wave: number } */ private onNewWave(event: string, data: any) { const wave = Number(data?.wave ?? 0); if (wave <= 0) return; this.isBossWave = true; // 单场 Boss 战,恒为 Boss 战(超时阈值/预警走 Boss 档) if (wave > 1) { // 在新一回合到来时,先进入 BattleEnd,2秒后自动进入下一回合的准备阶段 this.changePhase(MissionPhase.BattleEnd); } else { // 第1回合不需要结束上一回合,延迟一点播放提示,避免被开始游戏的loading遮挡 this.scheduleOnce(() => { if (this.currentPhase === MissionPhase.Prepare) { this.playTooltipAnim(`第 ${wave} 回合`); } }, 0.5); } this.currentWave = wave; smc.vmdata.mission_data.level = wave; // 金币改为怪物死亡掉落(见 HeroAtkSystem),不再每回合固定发放 this.lastTimeSecond = -1; this.clearTime = 0; this.update_time(); } // ======================== 怪物数量管理 ======================== /** * 同步怪物刷新状态(降频执行,每 0.2 秒一次): * 1. 遍历所有 HeroAttrsComp 实体,统计怪物和英雄数量。 * 2. 检测英雄全灭 / 怪物全灭。 * * @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); // 怪物全灭检测:本小节怪物全灭 + 待刷队列空 → 弹结算窗(小节胜利,按钮按进度显示下一节/下一关) const pendingCount = smc.vmdata.mission_data.pending_mon_num || 0; if (monsterCount === 0 && pendingCount === 0 && smc.mission.play && !smc.mission.pause && this.currentPhase === MissionPhase.Battle) { // oops.message 全局事件总线:清屏庆祝(横幅/奖励由 BattleBannerComp 消费),需在结算前派发(clearTime 归零前取值) const allAlive = this.checkAllHeroAlive(); const fastClear = this.clearTime < WAVE_DURATION * 0.5 ? 2 : this.clearTime < WAVE_DURATION * 0.75 ? 1 : 0; oops.message.dispatchEvent(GameEvent.WaveClear, { wave: this.currentWave, clearTime: this.clearTime, allAlive, fastClear, }); // 小节胜利:延迟弹结算窗,等待死亡动画播放完 this.scheduleSettle(false, true); return; } smc.vmdata.mission_data.mon_num = monsterCount; } /** 检测场上英雄是否全员存活(清屏 Perfect 判定用,独立于评分统计逻辑) */ private checkAllHeroAlive(): boolean { let hasHero = false; ecs.query(this.heroAttrsMatcher).forEach(entity => { const attrs = entity.get(HeroAttrsComp); if (attrs && attrs.fac === FacSet.HERO) { hasHero = true; } }); // 英雄为真实死亡(实体已销毁),有死亡记录即非全员存活 return hasHero && smc.mission.dead_heroes.length === 0; } /** * 英雄全灭检测:若场上无存活英雄且处于战斗中,预约延迟结算(等待死亡动画播完再弹窗)。 * @param heroCount 当前存活英雄数量 */ private handleHeroWipe(heroCount: number) { if (heroCount > 0) return; if (!smc.mission.play || smc.mission.pause) return; if (this.currentPhase !== MissionPhase.Battle) return; this.scheduleSettle(true, false); } /** * 预约延迟结算:最后一个英雄/怪物死亡时,延迟 SETTLE_DELAY 秒等待死亡动画播完, * 再切入 BattleEnd(由 autoNextPhase 流转弹窗),避免结算窗立即盖住死亡表现。 * @param is_hero_dead 是否因英雄全灭触发 * @param victory 是否胜利 */ private scheduleSettle(is_hero_dead: boolean, victory: boolean) { if (this._settleScheduled) return; this._settleScheduled = true; this.scheduleOnce(() => { this._settleScheduled = false; // 延迟期间局内状态可能已变化(如复活、任务结束),兜底校验 if (!smc.mission.play || smc.mission.pause) return; if (this.currentPhase !== MissionPhase.Battle) return; // 胜利/失败统一走结算弹窗:open_Victory 内部按进度判定小节/大关/章节结算类型 this.open_Victory(null, is_hero_dead, victory); }, MissionComp.SETTLE_DELAY); } /** * 战斗超时强制结束(僵局兜底): * 1. 预扣留存分(残留怪销毁后 mon_num 会被下一次同步清零,BattleEnd 的累加会漏扣,故在此显式预扣并置防重标志)。 * 2. 销毁全部残留怪(不触发 MonDead、不掉金币——超时是对"清不掉"的惩罚)。 * 3. 一场定胜负,超时按怪物全灭同等处理——直接结算整局。 */ private onBattleTimeout() { // 防重入:只触发一次 if (this.currentPhase !== MissionPhase.Battle) return; smc.vmdata.scores.wave_remain_monsters += smc.vmdata.mission_data.mon_num; this.skipRemainScoreOnBattleEnd = true; ecs.query(this.heroAttrsMatcher).forEach(entity => { const attrs = entity.get(HeroAttrsComp); if (attrs && attrs.fac === FacSet.MON && !attrs.is_dead) { entity.destroy(); } }); smc.vmdata.mission_data.mon_num = 0; mLogger.log(this.debugMode, 'MissionComp', `[BattleTimeout] 超时强制结算`); // 超时按怪物全灭同等处理——视为胜利(但通常会有英雄死亡,all_alive 由结算时实时检测) this.open_Victory(null, false, true); } // ======================== 清理 ======================== /** 清理所有英雄和技能 ECS 实体 */ private cleanComponents() { if (!this.heroViewMatcher || !this.skillViewMatcher) return; 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/MON/BOSS 和 SKILL 根节点下的所有子节点 */ private clearBattleSceneNodes() { const scene = smc.map?.MapView?.scene; const layer = scene?.entityLayer?.node; if (!layer) return; for (const name of ["HERO", "MON", "BOSS", "SKILL"]) { const root = layer.getChildByName(name); if (root) { for (let i = root.children.length - 1; i >= 0; i--) { root.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 }; } // ======================== 性能监控面板 ======================== /** 性能监控相关代码 */ /** 初始化性能监控面板:在本组件节点下创建 Label */ private initMemoryPanel() { if (!this.showMemoryPanel) return; let panel = this.node.getChildByName("mem_panel"); if (!panel) { panel = new Node("mem_panel"); panel.parent = this.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.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.PhaseTime = null as any; this.heroViewMatcher = null; this.skillViewMatcher = null; this.heroAttrsMatcher = null; if (this.node && this.node.isValid) { this.node.destroy(); } } }