Files
pixelheros/assets/script/game/map/MissionComp.ts
panFD be6af4d157 chore(map): lower inter-wave countdown duration to 1s
调整节间过场倒计时的固定时长,从2.5秒改为1秒,加快关卡节间推进节奏
2026-09-13 11:50:12 +08:00

1284 lines
55 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. 管理单关(一关一局)的 **完整生命周期**:初始化 → 5 节自动连战 → 本关结算。
* 2. 在战斗阶段每帧更新战斗计时器、同步怪物数量、检测英雄全灭 / 怪物全灭。
* 3. 驱动节间自动推进:节间不弹结算,背景滚动过渡 + 英雄归位 + 怪物从右侧跑入,自动开下一节。
* 4. 处理关卡进度持久化(只记到关,节恒从 1 开始)。
* 5. 第 5 节(Boss 节)胜利后弹结算(VictoryComp);英雄全灭判负弹结算。
*
* 关键设计:
* - mission_start() 初始化所有游戏数据 → 恢复存档进度(大关)→ 进入第 1 节。
* - 每节不清场、不重新三选一:英雄保留并自动归位到左侧编队,节间仅做一次过场推进。
* - 节间过场:BattleEnd 判定非末节 → 进入 PrepareStart(in_fight=false)触发英雄编队归位,
* 同时播放背景滚动,倒计时结束自动进入下一节。
* - 第 5 节胜利 → open_Victory(final) 弹结算,确认退出并存档进下一关。
* - cleanComponents() / clearBattlePools() 仅在整关开始/结束时执行,节间不调用。
*/
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 { MoveSystem } from "../hero/MoveComp";
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 { MissionMonCompComp } from "./MissionMonComp";
import {
getProgressText,
getMonsterLv,
BOSS_SECTION,
SECTIONS_PER_STAGE,
} 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" | "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, string> = {
[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;
/** 节间过场倒计时档位:统一时长(一关一局节间自动推进,节奏紧凑) */
private static readonly COUNTDOWN_TRANSITION = 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;
/** 当前回合数(= 当前小节号,驱动 HUD 回合/进度显示) */
private currentWave: number = 0;
/** 当前节是否包含 Boss(驱动 Boss 预警) */
private isBossWave: boolean = false;
/** 超时回合已在 onBattleTimeout 预扣留存分,BattleEnd 需跳过重复累加 */
private skipRemainScoreOnBattleEnd: boolean = false;
/** 上节清场时间快照(含清场加速还原,供评分统计) */
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~SECTIONS_PER_STAGE) */
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.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();
}
onDestroy() {
// 解除节间过场移动动画锁,避免泄漏到下一关
MoveSystem.transitionMoveLock = false;
// 退出关卡时恢复地图背景动画
smc.map.MapView.scene.mapLayer.playAnimations()
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 = MissionComp.COUNTDOWN_TRANSITION;
this.isWaveCountdown = true;
this.updateCountdownUI(true);
}
/** 更新倒计时 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;
// 过场结束,停掉背景滚动(战斗中保持静止)
smc.map?.MapView?.scene?.mapLayer?.stopAnimations();
// 解除英雄移动动画锁:归位后恢复待机
MoveSystem.transitionMoveLock = false;
}
// ======================== 奖励与广告 ========================
/** 奖励发放(预留) */
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/PrepareStart/PrepareEnd 准备阶段、Battle 战斗中阶段;
// 回合提示统一由 BattleStart 的"第 X 回合开始"承担,准备相关阶段不弹提示)
if (targetPhase !== MissionPhase.None &&
targetPhase !== MissionPhase.Prepare &&
targetPhase !== MissionPhase.PrepareStart &&
targetPhase !== MissionPhase.PrepareEnd &&
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();
// 回合开始提示:"第 X 回合开始"(准备阶段不显示,战斗开始才播)
this.playTooltipAnim(`第 ${this.currentWave} 回合开始`);
// 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 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;
// 快照清场时间供评分统计(onNewWave 会清零 clearTime,必须提前快照)
this.lastWaveClearTime = this.clearTime;
// 【评分系统 - 战绩分】记录全员存活的胜利节数(额外加分)
if (hasHero && heroDeathCount === 0) {
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:
// 节间过场倒计时结束:推进到下一节、停背景滚动、派发 SectionStart 刷怪,进入 PrepareEnd 自动开战
this.stopWaveCountdown();
this.advanceToNextSection();
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 计时结束后分流:
// - 局仍在进行(节间清场,非末节)→ 进入 PrepareStart 过节间过场,自动连战下一节
// - 已暂停(open_Victory 结算:末节通关/失败)→ 进入 Settle 弹结算
if (smc.mission.play && !smc.mission.pause) {
this.changePhase(MissionPhase.PrepareStart);
} else {
this.changePhase(MissionPhase.Settle);
if (smc.mission.play) {
// 通过 open_Victory 进来:暂停并弹结算(通关/失败)
smc.mission.pause = true;
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,
// 结算类型:final=通关(确认退出),defeat=失败(再来一次/退出)
settle_type: this._settleType,
settle_title: this._settleTitle,
// 关卡难度度量(= 大关号,驱动胜利奖励随进度增长)
mon_lv: getMonsterLv(this.currentStage),
// 失败奖励判定数据:整局坚持时间 + 整局击杀统计(kills=跨回合累计击杀,total=整局已刷出)
defeat_stats: this.queryMissionKillStats(),
});
} else {
// play 已为 false(fight_end 强制结束)→ 直接清理
this.cleanComponents();
this.clearBattlePools();
}
}
break;
}
}
/**
* 进入战斗:
* - 恢复刷怪
* - 标记战斗中
* - 隐藏开始按钮
* - 分发 FightStart 事件
* - 触发英雄战斗开始技能
*/
to_fight() {
this.changePhase(MissionPhase.PrepareEnd);
}
/**
* 触发英雄的战斗开始技能
*/
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 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();
}
/**
* 打开结算弹窗(一关一局:仅整关结束时触发——Boss 节通关 / 英雄全灭判负 / 超时):
* - 暂停游戏、切断节间流转
* - 胜利即 final(通关),存档推进到下一关;失败即 defeat(进度不变,可复活/再来一次)
* - 切入 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 {
// 通关(Boss 节胜利):结算窗显示通关标题,确认退出并存档进下一关
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.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();
// 重置整局刷怪统计(跨回合累计的已刷出总数),回合间不重置
ecs.query(ecs.allOf(MissionMonCompComp)).forEach(e => e.get(MissionMonCompComp).resetMissionStats());
smc.vmdata.mission_data.coin = Math.max(0, Math.floor(FightSet.INIT_COIN));
// 银币为局外持久货币,每局不重置,仅通过日常积攒,无需初始化
// 【评分系统 - 效率分】记录初始获得的金币收入
smc.vmdata.scores.gold_earned += smc.vmdata.mission_data.coin;
}
// ======================== 关卡进度管理 ========================
/**
* 加载关卡进度存档(一关一局:只恢复大关,节恒从 1 开始)
* 从 smc.data.stage_progress 恢复章节/大关,缺省从 1-1-1 开始
*/
private loadStageProgress() {
const progress = smc.data.stage_progress;
if (progress && progress.chapter >= 1 && progress.stage >= 1) {
this.currentChapter = progress.chapter;
this.currentStage = progress.stage;
} else {
this.currentChapter = 1;
this.currentStage = 1;
}
// 一关一局:进入关卡始终从第 1 节开打
this.currentSection = 1;
// 加载完成后将权威进度镜像到 vmdata,驱动 HUD 实时显示
smc.syncStageProgressToView();
mLogger.log(this.debugMode, 'MissionComp',
`[StageProgress] 加载进度: ${getProgressText(this.currentChapter, this.currentStage, this.currentSection)}`);
}
/**
* 推进到下一小节(节间自动推进):
* 1. 节号 +1,派发 SectionStart 触发 MissionMonComp 生成下一节怪物(从右侧跑入)。
* 2. 不清理英雄/技能实体、不重置局内数据、不重新三选一——英雄保留并自动归位(编队排队)。
* 3. 进度实时镜像到 HUD,但不写持久存档(存档只在整关通关时推进到下一关)。
*
* 调用时机:PrepareStart 过场倒计时结束(autoNextPhase)。
* 保证:仅 1~4 节清场后触发,末节(BOSS_SECTION)已走 open_Victory 结算,不会推进。
*/
private advanceToNextSection() {
this.currentSection++;
// 节号实时同步到 HUD(内存进度,不持久化;整关通关时才统一写大关存档)。
// 直接写 vmdata 视图层而非走 syncStageProgressToView(后者从 data 读,而 data 仍停留在大关起点)
const view = smc.vmdata.stage_progress;
view.chapter = this.currentChapter;
view.stage = this.currentStage;
view.section = this.currentSection;
oops.message.dispatchEvent(GameEvent.STAGE_PROGRESS_UPDATE);
// 触发 SectionStart 事件,MissionMonComp 据此生成下一节怪物(从右侧跑入)
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 节重新开始
* Why: 一关一局,失败"再来一次"即整关重打(节恒从 1 开始)
*/
private restartCurrentMission() {
// 清理战场与局内数据
this.cleanComponents();
this.clearBattlePools();
this.data_init();
// 重置结算状态
this._victory = false;
this._settleType = "defeat";
this._settleTitle = "";
// 一关一局:重开从第 1 节开始
this.currentSection = 1;
// 进入准备阶段
this.changePhase(MissionPhase.Prepare);
// 触发 MissionStart 重置开局免费抽卡
oops.message.dispatchEvent(GameEvent.MissionStart, {});
oops.message.dispatchEvent(GameEvent.FightReady);
// 触发 SectionStart 重新生成本关第 1 节怪物
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)}`);
}
// ======================== 回合管理 ========================
/**
* 新一回合事件回调(SectionStart 时由 MissionMonComp 派发,wave = 回合号):
* 1. 更新当前回合号与 HUD 进度。
* 2. 按回合配置判定是否含 Boss(驱动 Boss 预警)。
*
* 注意:回合推进在 autoNextPhase(PrepareStart) 完成,此处仅同步显示与 Boss 预警;
* "第 X 回合开始"提示改在 BattleStart(战斗开始)时播放,准备阶段不显示。
*
* @param event 事件名
* @param data { wave: number, bossWave?: boolean }
*/
private onNewWave(event: string, data: any) {
const wave = Number(data?.wave ?? 0);
if (wave <= 0) return;
// Boss 预警:本回合怪物列表含 Boss 时置位(Boss 回合 Boss 延迟登场,BattleStart 时发预警)
this.isBossWave = !!data?.bossWave;
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,
});
if (this.currentSection >= BOSS_SECTION) {
// Boss 节(末节)清场 → 本关通关,延迟弹结算(等死亡动画播完)
this.scheduleSettle(false, true);
} else {
// 非末节清场 → 立即启动节间过场(背景滚动 + 英雄归位同帧启动),自动连战下一节
this.beginSectionTransition();
}
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;
}
/**
* 查询整局(整关 5 回合)击杀统计与坚持时间,供结算弹窗做失败奖励判定。
* Why: 一关一局后,失败奖励按整局累计口径计算,不再只看当前回合——
* clear_time 用整关战斗总时长 fight_time(跨回合累计,整关重置一次);
* kills 用 scores 跨回合累计的分类击杀数之和(melee/remote/elite/boss);
* total 用整局已刷出怪物总数(MissionMonComp 跨回合累计)。
*/
private queryMissionKillStats(): { clear_time: number; kills: number; total: number } {
const s = smc.vmdata.scores;
const kills = (s.melee_kill_count || 0) + (s.remote_kill_count || 0)
+ (s.elite_kill_count || 0) + (s.boss_kill_count || 0);
const entities = ecs.query(ecs.allOf(MissionMonCompComp));
const comp = entities[0]?.get(MissionMonCompComp);
const total = comp ? comp.getTotalSpawnedCount() : kills;
return {
clear_time: smc.vmdata.mission_data.fight_time,
kills,
total: Math.max(total, kills),
};
}
/**
* 英雄全灭检测:若场上无存活英雄且处于战斗中,预约延迟结算(等待死亡动画播完再弹窗)。
* @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. 立即切出战斗(in_fight=false)→ 英雄触发编队排队、自动归位到左侧站位。
* 2. 同时恢复地图背景滚动,表现玩家推进(背景与英雄归位同帧启动,避免一先一后)。
* 3. 切 BattleEnd,2s 后由 autoNextPhase 进入 PrepareStart 过场倒计时,倒计时结束自动连战下一节。
*
* Why: 不等 SETTLE_DELAY 死亡动画,让"归位 + 背景滚动"在清场瞬间就启动,过渡更连贯。
*/
private beginSectionTransition() {
smc.mission.in_fight = false;
smc.vmdata.mission_data.in_fight = false;
// 恢复地图背景滚动(与英雄归位同步启动,2 倍速表现推进)
smc.map?.MapView?.scene?.mapLayer?.playAnimations();
// 过场期间英雄保持移动动画:即使已归位也持续行军,地图停才回待机
MoveSystem.transitionMoveLock = true;
this.changePhase(MissionPhase.BattleEnd);
}
/**
* 战斗超时强制结束(僵局兜底):
* 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();
}
}
}