Files
pixelheros/assets/script/game/map/MissionComp.ts
panw e87be79792 fix(mission): 修复波次更迭与战斗结束阶段的逻辑冲突
调整 PhaseTime 计时器间隔为 1 秒,避免更新过于频繁。
在 update 逻辑中,当游戏暂停且当前阶段不是 BattleEnd 时才完全停止,确保战斗结束动画能正常播放并自动流转。
修改波次更迭逻辑:新波次到来时先进入 BattleEnd 阶段,播放结束技能后自动进入准备阶段,避免与 open_Victory 的结算流程冲突。
在 open_Victory 中立即标记暂停,以切断波次自动流转。
2026-04-14 17:39:14 +08:00

827 lines
32 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 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, CCInteger } 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 { 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 { CardInitCoins } from "../common/config/CardSet";
import { Timer } from "db://oops-framework/core/common/timer/Timer";
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 // 结算阶段
}
//@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;
// ======================== 配置参数 ========================
/** 怪物数量上限(超过后暂停刷怪) */
private maxMonsterCount: number = 5;
/** 怪物数量恢复阈值(降至此值以下恢复刷怪) */
private resumeMonsterCount: number = 3;
/** 新一波金币奖励基础值 */
private prepareBaseCoinReward: number = 100;
/** 每一波金币增长值 */
private prepareCoinWaveGrow: number = 1;
/** 金币奖励上限 */
private prepareCoinRewardCap: number = 500;
/** 卡池升级波次配置:达到对应波次时,推送卡池升级事件 */
@property({ type: [CCInteger], tooltip: "卡池升级波次配置,例如 [10, 20] 表示第10波升到2级第20波升到3级" })
cardPoolUpgradeWaves: number[] = [5, 10];
// ======================== 编辑器绑定节点 ========================
/** 开始战斗按钮 */
@property(Node)
start_btn:Node = null!
/** 时间/波数显示节点 */
@property(Node)
time_node:Node = null!
/** 阶段名称映射表(用于 UI 显示) */
private static readonly PhaseNameMap: Record<MissionPhase, string> = {
[MissionPhase.None]: "未开始",
[MissionPhase.PrepareStart]: "准备开始",
[MissionPhase.Prepare]: "准备阶段",
[MissionPhase.PrepareEnd]: "准备结束",
[MissionPhase.BattleStart]: "战斗开始",
[MissionPhase.Battle]: "战斗中",
[MissionPhase.BattleEnd]: "战斗结束",
[MissionPhase.Settle]: "结算阶段"
};
// ======================== 运行时状态 ========================
/** 战斗倒计时(秒) */
FightTime:number = FightSet.FiIGHT_TIME
/** 剩余复活次数 */
revive_times: number = 1;
/** 掉落奖励列表 */
rewards:any[]=[]
/** 累计游戏数据 */
game_data:any={
exp:0,
gold:0,
diamond:0
}
/**秒计时 */
PhaseTime:Timer= new Timer(1)
/** 上一次显示的时间字符串(避免重复设置) */
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;
/** 当前任务阶段 */
public currentPhase: MissionPhase = MissionPhase.None;
// ======================== 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
// 如果是暂停状态,且不在 BattleEnd 阶段(全灭时需要播放完 fend 技能动画并自动流转),才真正停止 update 逻辑
if(smc.mission.pause && this.currentPhase !== MissionPhase.BattleEnd) return
// 处理过渡阶段的计时
if (this.currentPhase === MissionPhase.PrepareStart ||
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.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.changePhase(MissionPhase.Prepare)
let loading=this.node.parent.getChildByName("loading")
loading.active=true
this.scheduleOnce(()=>{
loading.active=false
},0.5)
}
/**
* 阶段切换核心方法(状态机)
* 处理状态流转时所需的事件触发和全局标志位修改。
* @param targetPhase 目标阶段
*/
private changePhase(targetPhase: MissionPhase) {
if (this.currentPhase === targetPhase) return;
const oldPhase = this.currentPhase;
this.currentPhase = targetPhase;
// 更新阶段显示 UI
if (this.time_node && this.time_node.isValid) {
const phaseNode = this.time_node.getChildByPath("Phase/Label");
if (phaseNode) {
const label = phaseNode.getComponent(Label);
if (label) {
label.string = MissionComp.PhaseNameMap[targetPhase] || "未知";
}
}
}
// 重置状态机的计时器
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;
if (this.start_btn && this.start_btn.isValid) this.start_btn.active = false;
break;
case MissionPhase.Prepare:
if (this.start_btn && this.start_btn.isValid) this.start_btn.active = true;
break;
case MissionPhase.PrepareEnd:
if (this.start_btn && this.start_btn.isValid) this.start_btn.active = false;
break;
case MissionPhase.BattleStart:
// 触发战斗开始技能fstart
this.triggerHeroBattleSkills(true);
break;
case MissionPhase.Battle:
smc.mission.stop_spawn_mon = false;
smc.mission.in_fight = true;
smc.vmdata.mission_data.in_fight = 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;
// 触发战斗结束技能fend
this.triggerHeroBattleSkills(false);
break;
case MissionPhase.Settle:
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 = false;
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) this.start_btn.active = false;
break;
}
}
/** 自动流转到下一阶段(过渡状态结束时调用) */
private autoNextPhase() {
switch (this.currentPhase) {
case MissionPhase.PrepareStart:
this.changePhase(MissionPhase.Prepare);
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: false,
rewards: this.rewards,
game_data: this.game_data,
can_revive: this.revive_times > 0
});
} else {
// 如果 play 已经是 false说明是通过 fight_end 进来的
this.cleanComponents();
this.clearBattlePools();
}
}
break;
}
}
/**
* 进入战斗:
* - 恢复刷怪
* - 标记战斗中
* - 隐藏开始按钮
* - 分发 FightStart 事件
* - 触发英雄战斗开始技能
*/
to_fight(){
this.changePhase(MissionPhase.PrepareEnd);
}
/**
* 进入准备阶段:
* - 标记非战斗
* - 暂停刷怪
* - 显示开始按钮
* - 触发英雄战斗结束技能
*/
private enterPreparePhase() {
this.changePhase(MissionPhase.PrepareStart);
}
/**
* 触发英雄的战斗开始/结束技能
* @param isStart 是否为战斗开始
*/
private triggerHeroBattleSkills(isStart: boolean) {
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;
const info = HeroInfo[attrs.hero_uuid];
if (!info) return;
const skillUuid = isStart ? info.fstart : info.fend;
if (skillUuid) {
oops.message.dispatchEvent(GameEvent.TriggerSkill, {
s_uuid: skillUuid,
heroAttrs: attrs,
heroView: view,
triggerType: isStart ? 'fstart' : 'fend'
});
}
});
}
/** 开始战斗按钮点击回调 */
private onStartFightBtnClick() {
if (!smc.mission.play) return;
if (smc.mission.pause) return;
if (this.currentPhase !== MissionPhase.Prepare) return;
this.to_fight();
}
/**
* 打开结算弹窗:
* - 暂停游戏
* - 打开 VictoryComp 弹窗
*
* @param e 事件对象(未使用)
* @param is_hero_dead 是否因英雄全灭触发
*/
open_Victory(e:any,is_hero_dead: boolean = false){
// 战斗失败或胜利,标记暂停状态以切断波次流转逻辑
smc.mission.pause = true;
// 直接切入 BattleEnd触发 fend 表现
// 倒计时逻辑已在 update 中由 PhaseTime 接管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()
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.currentPhase = MissionPhase.None;
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;
// 在新一波到来时,先进入 BattleEnd触发上一波的战斗结束技能 (fend)2秒后自动进入下一波的准备阶段
this.changePhase(MissionPhase.BattleEnd);
this.currentWave = wave;
smc.vmdata.mission_data.level = wave;
this.grantPrepareCoinByWave(wave);
this.lastTimeSecond = -1;
this.update_time();
// 检查并推送卡池升级事件
this.checkCardPoolUpgrade(wave);
}
/** 检查是否达到卡池升级波次,并推送升级事件 */
private checkCardPoolUpgrade(wave: number) {
if (!this.cardPoolUpgradeWaves || this.cardPoolUpgradeWaves.length === 0) return;
const upgradeIndex = this.cardPoolUpgradeWaves.indexOf(wave);
if (upgradeIndex !== -1) {
// 根据配置的索引,计算目标等级(初始等级 + index + 1
// 例如 index=0对应等级为2index=1对应等级为3
const targetLv = upgradeIndex + 2;
oops.message.dispatchEvent(GameEvent.CardPoolUpgrade, { wave, targetLv });
mLogger.log(this.debugMode, 'MissionComp', "card pool upgrade event pushed", { wave, targetLv });
}
}
/**
* 按波数发放金币奖励:
* 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 (this.currentPhase !== MissionPhase.Battle) return;
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();
}
}