Files
pixelheros/assets/script/game/map/MissionComp.ts
pan 5fe961846f feat(victory): add defeat reward gate by clear time and kill ratio
Defeat rewards now require surviving 15s and killing at least one
monster. Qualified defeats grant a fixed survive reward plus a
kill-ratio-scaled bonus; unqualified defeats yield nothing.
Victory rewards remain unchanged.
2026-09-11 15:57:21 +08:00

1321 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. 管理单局游戏的 **完整生命周期**:初始化 → 小节循环(战斗→结算弹窗)→ 章节通关。
* 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 { MissionMonCompComp } from "./MissionMonComp";
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, 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;
/** 倒计时档位:快清场(压缩垃圾时间,奖励强 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,
// 失败奖励判定数据:本节坚持时间 + 击杀统计(kills=已刷出-存活)
defeat_stats: {
clear_time: this.clearTime,
...this.querySectionKillStats(),
},
});
} 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 && this.currentPhase === MissionPhase.Battle) {
// 战斗中回合更迭:先进入 BattleEnd,2秒后自动进入下一回合的准备阶段。
// Why: NewWave 现在仅在 SectionStart(Prepare 阶段,等待三选一)时派发,
// 不加相位守卫会把"下一节/再来一次"从 Prepare 强切 BattleEnd,
// 导致玩家还没选英雄就自动开战,场上无英雄直接判负
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;
}
/**
* 查询本节击杀统计(转交结算弹窗做失败奖励判定)
* @returns { kills, total },查询不到 MissionMonComp 时返回 0/0
*/
private querySectionKillStats(): { kills: number; total: number } {
const entities = ecs.query(ecs.allOf(MissionMonCompComp));
const comp = entities[0]?.get(MissionMonCompComp);
if (!comp) return { kills: 0, total: 0 };
return comp.getSectionKillStats();
}
/**
* 英雄全灭检测:若场上无存活英雄且处于战斗中,预约延迟结算(等待死亡动画播完再弹窗)。
* @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();
}
}
}