import { _decorator, assetManager, Button, clamp01, Component, EventKeyboard, Input, input, instantiate, KeyCode, Label, macro, Node, Prefab, Sprite, sys, tween, v3, Vec3, view, Widget } from 'cc'; import { getHallWarmLongTailResPakage, getHallWarmResPakage, getHerosResPakage, getMainShellResPakage, GAudioPath, GBundle, GPath, ResPkgName, UIConfigs, UIID } from './ConfigRes'; import { ResPkgBuilder } from './ResPkgBuilder'; import { ItemData } from './ConfigProjectData'; import { GEvent } from '../../mx/module/event/GEvent'; import MTools from '../../mx/tools/MTools'; import { createMembershipSet, membershipAdd, membershipToArray, mapValuesToArray, type MembershipSet, } from '../../mx/tools/MembershipSet'; import { OtherDataType, StatisticsType, StatusType } from './GameData'; import { sdkConfig } from '../sdk/sdkConfig'; /** 用户信息 */ export interface IUserInfo { nickName: string; avatarUrl: string; gender: string; country: string; province: string; city: string; language: string; } import type { ProjectBattleHandle } from '../battle/runtime/BattleRuntime'; import { PopToastParam, PopToastType } from '../ui/PopToast/UIPopToast'; import { Player } from './Player'; import { DEV } from 'cc/env'; import { patchSpineSkeletonResetEnums } from '../../mx/module/lang/spineRuntimePatch'; import { BattleFlowMode } from '../../mx/module/battle/core/BattleFlowMode'; import { PreloadPredictor } from '../../mx/module/res/PreloadPredictor'; import { LoadScheduler } from '../../mx/module/res/LoadScheduler'; import { MemoryDowngrade } from './MemoryDowngrade'; import { AppLifecycle } from './AppLifecycle'; import { MemoryProfile } from './MemoryProfile'; import { PerfReport } from './PerfReport'; import { PerfLoadTimer } from './PerfLoadTimer'; import { BundleLifecycle } from '../../mx/module/res/BundleLifecycle'; import { ResourcePhase, ResourcePhaseGate } from '../../mx/module/res/ResourcePhaseGate'; import type { ResPakage } from '../../mx/module/res/ResourcesManager'; import { BattleAudio } from '../battle/audio/BattleAudio'; import { ChapterLevelBuilder } from '../battle/config/ChapterLevelBuilder'; import { RankDataManager } from '../manager/RankDataManager'; import { HeadIconManager } from '../manager/HeadIconManager'; import { RankBoardManager } from '../manager/RankBoardManager'; import { DailyShopDataManager } from '../manager/DailyShopDataManager'; import { EmolumentDataManager } from '../manager/EmolumentDataManager'; import { MatchWinStats } from '../manager/MatchWinStats'; import { AiBattleBridge } from '../battle/ai/bridge/AiBattleBridge'; import type { MatchGateHandlers } from '../ui/match/UImatch'; import { FrameFlowHost } from './FrameFlowHost'; import { awaitWithTimeout, loadWithRetry, LoadRecoveryConfig, type LoadResult } from '../../mx/module/res/LoadRecovery'; import { tryProactiveCacheTrimBeforePreload } from '../../mx/tools/CacheManagerPatch'; const { ccclass, property } = _decorator; /** 进战斗附加选项(匹配页门闩) */ export type BattleEnterOpts = { /** 匹配页已展示,跳过 loading 遮罩 */ skipLoading?: boolean; /** 匹配页自行关闭,closeAllPanel 时保留 match */ deferCloseMatch?: boolean; /** 战斗页 openPanel 结果 */ onBattleOpened?: (ok: boolean) => void; }; @ccclass('GameController') export class GameController extends Component { static instance: GameController = null; protected onLoad(): void { GameController.instance = this; this._frameFlow = new FrameFlowHost(); } protected onDestroy(): void { this._frameFlow?.dispose(); } /**游戏总时间 */ GameTime: number = 0; /**自己的称谓(主角称呼) */ SelfName: string = "男主"; UserInfo: IUserInfo = null; /**暂停秒赚 */ PauseSecondEarn: boolean = false; /**是否正在播放引导剧情 */ IsPlayingGuideStory: boolean = false; /**是否正在播放新闻 */ IsPlayingNews: boolean = false; /**是否显示障碍物 */ isShowObstacle: boolean = true; /**当前玩家血量 */ playerHp: number = 0; /**当前玩家银币数 */ playerCoinNum: number = 0; //玩家玩过了游戏到结算 IsPlayedGame: boolean = false; /** 本次回主页是否来自关卡战斗失败(UIBattleFail 领取后回大厅) */ returnedFromBattleFail = false; /** 失败回主页时对应的关卡章节(仅 returnedFromBattleFail 为 true 时有效) */ returnedFromBattleFailChapterId = 0; //弹出签到 IsShowSignPop: boolean = false; /**当前完成后的章节 */ finishChapterId: number = -1; /** * 怪物数量倍数 */ monsterNumScale: number = 1; /** GM:背包刷新仅从该 Weapon2 列表中随机(null=正常权重池) */ gmBagWeapon2Ids: number[] | null = null; /** GM:AI 主动 Battlebuff.id(null=每局匹配随机;有值则跨局常驻覆盖直至清除) */ gmAiActiveBuffIds: number[] | null = null; /** GM:AI 被动 Battlebuff.id(null=每局匹配随机;有值则跨局常驻覆盖直至清除) */ gmAiPassiveBuffIds: number[] | null = null; /** GM:固定战场 Level.id;0=首页随机 */ gmMapId = 0; /** GM:七十二变 roll 固定目标 Battlebuff.id;null=正常随机 */ gmRandomActiveFixBuffId: number | null = null; /** GM:己方营地不掉血(仅本次登录内存,不写档) */ gmLocalCampImmune = false; /** GM:敌方营地不掉血(仅本次登录内存,不写档) */ gmEnemyCampImmune = false; playerWeaponIdArray = [] playerSkillIdArray = [] playerSkillGroupIdArray = [] playerWeaponGroupIdArray = [] setPlayerSkillGroupIdArray = false setPlayerWeaponGroupIdArray = false afkTimeOne = 60 * 60 isFinishGuide = false; /**武器模式最大挑战次数 */ // WeaponCountMax = 5; // /**无尽模式最大挑战次数 */ // InfiniteChallengeCountMax = 20; // /**道具模式最大挑战次数 */ // PropModeCountMax = 20; // /**宝箱副本最大挑战次数 */ // BoxChallengeCountMax = 20; // /**抽卡挑战模式最大挑战次数 */ // GachaChallengeCountMax = 20; // /**boss挑战模式最大挑战次数 */ // BossChallengeCountMax = 20; isInitAllData: boolean = false /** 当前选中准备进入的战场 Level id */ CurentSelectLevelId: number = 1; /** @deprecated 使用 CurentSelectLevelId */ get CurentSelectChapterId(): number { return this.CurentSelectLevelId; } set CurentSelectChapterId(value: number) { this.CurentSelectLevelId = value; } /** 当前正在进行中的战斗(对局 runtime + 旧 API 兼容) */ CurentBattle: ProjectBattleHandle | null = null; /** 本地玩家上下文(PVP 时区分 local / remote) */ localPlayer: Player | null = null; guideStoryId: number = 1; guideGreatGift: boolean = false; /**第三章失败引导角色升级 */ thirdChapterFailRoleUpgrade: boolean = false; /**第三章失败引导武器升级 */ thirdChapterFailWeaponUpgrade: boolean = false; /***第三章胜利榴莲弹出 */ thirdChapterWinGreatGift: boolean = false; /** * 初始化游戏 */ init() { patchSpineSkeletonResetEnums(); gg.battle.init(); macro.ENABLE_MULTI_TOUCH = false this.GameTime = 0; gg.dot.init(); gg.data.project.syncBattleVigourCost(); gg.audio.MuteMusic = gg.data.getStatus(StatusType.MuteMusic) == 1; gg.audio.MuteEffect = gg.data.getStatus(StatusType.MuteEffect) == 1; GEvent.Ins.on(GEvent.UpdateItemNum, this.onItemUpdate, this); gg.ui.setPanelOpenedHook((panelId) => { const phase = ResourcePhaseGate.getInstance().phase; if (phase === ResourcePhase.Battle || phase === ResourcePhase.BattleExit) return; PreloadPredictor.getInstance().onPanelOpened(panelId); }); // this.playerWeaponIdArray.push(22); // let obj: IBattleSkill = { // weaponId: 22, // id: 2105, // type: 1, // limit_num: 10, // }; // this.playerSkillIdArray.push(obj); // obj = { // weaponId: 22, // id: 2153, // type: 1, // limit_num: 10, // }; // this.playerSkillIdArray.push(obj); this.unscheduleAllCallbacks(); //初始化每整秒更新计时器 this.schedule(() => { this.onSecond(); }, 1); gg.sdk.reportDY("inLevel", "进入-进入首页"); //首次登录 if (gg.data.IsFirstLogin) { gg.sdk.reportDY("inLevel", "进入-首次登录"); gg.sdk?.sdk30r?.onRegister(); //测试-设置体力金币钻石战力等 gg.data.setCombatPower(50); gg.data.setMoney(100); //初始化30 gg.data.setVigour(30); gg.data.setDiamond(10); //测试-送8个已装备 gg.data.project.setHaveWeaponIds([1, 2, 3, 4, 11, 12, 13, 14]); gg.data.project.setEquipWeaponStatus([1, 2, 3, 4, 11, 12, 13, 14]); gg.data.setStatistics(StatisticsType.NoDoublePurpleSkillCountActiveRefresh, 20); gg.data.setStatistics(StatisticsType.NoDoublePurpleSkillCount, 20); gg.data.setStatistics(StatisticsType.ShopBuyGoldAdCount, 6) gg.data.setStatistics(StatisticsType.ShopBuyItemAdCount, 6) gg.data.setStatistics(StatisticsType.FreeBoxCount, 0) gg.data.setStringData(OtherDataType.SignData, '0&0&1') gg.data.setStatistics(StatisticsType.ActiveRefreshPurpleCount, 2) gg.data.setStatistics(StatisticsType.NoDoublePurpleSkillInterval, 50) gg.data.setStringData(OtherDataType.UserLevelupData, '0&10&0&0&0&0&0') gg.data.setStringData(OtherDataType.TaskRewardGetData, '0&0&0&0&0') gg.data.setStringData(OtherDataType.DayTaskRewardGetData, '0&0&0&0&0') gg.data.setStringData(OtherDataType.DayTaskProgressData, '0&0&0&0&0&0&0&0&0&0&0&0&0&0') gg.data.setStringData(OtherDataType.DayActiveGetRewardData, '0&0&0&0&0&0&0&0&0&0&0&0&0&0') } else { gg.sdk.reportDY("inLevel", "进入-第" + gg.data.LoginDays + "天登录"); gg.sdk.sdk30r?.track('RE_ACTIVE', {}); } let arr = gg.data.project.getHaveWeaponIds(); if (arr.length == 0) { gg.data.project.setHaveWeaponIds([1, 2, 3, 4, 11, 12, 13, 14]); gg.data.project.setEquipWeaponStatus([1, 2, 3, 4, 11, 12, 13, 14]); } gg.data.saveToServer(); //今天首次登录 if (gg.data.IsFirstLoginToday) { } this._clearLegacyBattleSnapshot(); PerfReport.reportLoadRes(sdkConfig.resLoadFinishTime, sdkConfig.resLoadFinishTime); PerfReport.reportLoadSdk(sdkConfig.sdkLoadFinishTime, sdkConfig.sdkLoadFinishTime); void this._routeAfterInit(); this.isInitAllData = true // 获客/复访首屏就绪兜底上报(幂等);避免进景失败导致测不通「场景加载完成」 this.scheduleOnce(() => { gg.sdk.notifyFeedGameInteractive(); }, 0.2) } /** * 获取章节id(取服务器端控制的章节id) * @param chapterID 章节ID * @returns 章节数据 */ getChapter(chapterID) { if (gg.sdk.levelS.length > chapterID - 1) return gg.sdk.levelS[chapterID - 1]; return chapterID; } /**获取原始章节id */ getOriginChapterID(chapterID) { let idx = gg.sdk.levelS.findIndex((id) => id == chapterID); if (idx >= 0) return idx + 1; return chapterID; } /** * 抖音 inLevel 打点用战场 Level id。 * @param levelId 可选;传入则直接使用,不传则战斗中取当前关,否则取 CurentSelectLevelId */ getReportDYLevelLabel(levelId?: number): string { if (levelId != null) { return String(levelId); } const battle = this.CurentBattle; if (battle) { return String(battle.getReportDYLevelId?.() ?? this.CurentSelectLevelId ?? 1); } return String(this.CurentSelectLevelId ?? 1); } /** 战斗内 inLevel 打点前缀:章节{Level id}_{波次},波次最小为 1 */ getReportDYBattleLabel(waveNum?: number, levelId?: number): string { const wave = Math.max(1, waveNum ?? this.CurentBattle?.CurentWaveNum ?? 0); return `章节${this.getReportDYLevelLabel(levelId)}_${wave}`; } /** * 抖音 inLevel:累计挑战次数区间。 * 0~200 传当前值;201~250→201,251~300→251…;超过 1000 传 1001。 */ getReportDYBattleCount(): number { const count = Math.max(0, Math.floor(Number(gg.data.doc?.totalBattleCount) || 0)); if (count > 1000) { return 1001; } if (count <= 200) { return count; } return 201 + Math.floor((count - 201) / 50) * 50; } /** * @description 打点描述 挑战次数小于200场次不传波次 * @param curWaveNum 波次 * @returns */ getReportDYBattleCountLabel(curWaveNum: number): string { const waveNum = curWaveNum == 0 ? 1 : curWaveNum; const count = Math.max(0, Math.floor(Number(gg.data.doc?.totalBattleCount) || 0)); if (count <= 200) { return `章节${this.getReportDYBattleCount()}_1-波次${waveNum}`; } else { return `章节${this.getReportDYBattleCount()}_1`; } } /** * 抖音 inLevel:段位打点文案,格式「段位-{段位名}{星数}」,星数为总星对 5 取余(0 视为 5,最大 5)。 * @param stars 总星数,缺省为结算后玩家当前总星 */ getReportDYRankLabel(stars?: number): string { const totalStars = Math.max(1, Math.floor(Number(stars ?? RankDataManager.getPlayerRankStars()) || 0)); const row = RankDataManager.resolveTierByStars(totalStars); const rankId = row?.id ?? RankDataManager.getPlayerRankId(); let name = RankDataManager.resolveRankLevelIconName(rankId); if (!name) { name = RankDataManager.rankDisplayName(RankDataManager.getRankConfig(rankId)); } name = String(name ?? '').trim(); if (!name) { console.warn('[GameController] getReportDYRankLabel 段位名为空', rankId); } const star = RankDataManager.getStarModCount(totalStars); return `段位-${name}${star}`; } /** * 重置游戏 */ reset(tagStr: string = '') { gg.data.reset((res) => { if (res) { if (gg.sdk.pf) { assetManager.cacheManager.clearCache(); gg.sdk.restartMiniProgramSync(); } else { this.playerHp = 0; this.playerCoinNum = 0; this.isShowObstacle = true this.monsterNumScale = 1; this.gmBagWeapon2Ids = null; this.gmAiActiveBuffIds = null; this.gmAiPassiveBuffIds = null; this.gmMapId = 0; this.gmRandomActiveFixBuffId = null; this.gmLocalCampImmune = false; this.gmEnemyCampImmune = false; this.playerWeaponIdArray = [] this.playerSkillIdArray = [] this.playerSkillGroupIdArray = [] this.playerWeaponGroupIdArray = [] this.setPlayerSkillGroupIdArray = false this.setPlayerWeaponGroupIdArray = false this.unscheduleAllCallbacks(); GEvent.Ins.off(GEvent.UpdateItemNum, this.onItemUpdate, this); gg.ui.closeAllPanel(); // gg.task.init(); setTimeout(() => { this.init(); }, 10); } } else { gg.ui.showToast("重置游戏失败"); } }, tagStr); } /** * 每帧更新 * @param deltaTime */ update(deltaTime: number) { this.GameTime += deltaTime; gg.sdk.serverTime += deltaTime * 1000; } /** * 每秒更新逻辑 */ onSecond() { //游戏总整秒数 let gs = Math.floor(this.GameTime); let onlineTime = Math.floor(gg.data.OnlineTimeForAll); //每3分钟打点 if (Math.floor(onlineTime) % 60 == 0) { let index = Math.floor(onlineTime / 60); if (index == 1 || index == 2) { gg.sdk.reportDY("inLevel", `累积时长-${index}分钟`); } } if (Math.floor(onlineTime) % 180 == 0) { let index = Math.floor(onlineTime / 60); gg.sdk.reportDY("inLevel", `累积时长-${index}分钟`); } //每分钟执行的逻辑 if (gs % 60 == 0) { } GEvent.Ins.emit(GEvent.SecondUpdate); } /** * 物品更新 */ onItemUpdate() { } /** 获客 / 新号才在路由前 warm;老号冷启动进大厅不抢 silent 带宽 */ private _shouldWarmFrontierBeforeRoute(): boolean { if (!!gg.sdk?.shouldFeedEnterPlayScene?.()) return true; if (gg.data.IsFirstLogin) return true; return false; } /** 配表与存档就绪后的首屏路由:直接进入 Home */ private _routeAfterInit(): void { this.CurentSelectLevelId = 1; this.enterMain(); } /** 清除已下线的战斗快照本地数据 */ private _clearLegacyBattleSnapshot(): void { try { sys.localStorage.removeItem('mx_battle_snapshot_v1'); } catch { /* ignore */ } gg.data.setStringDataOnlyLocal(OtherDataType.BattleGameData, ''); gg.data.saveLocal(); } /** 防止 enterBattle / enterMain 重入导致双管道加载 */ private _battleEnterInflight = false; private _enterMainInflight = false; /** 退战后 BattleRes 仍保留(tier1/3 跨局复用) */ private _battleResRetained = false; private _frameFlow: FrameFlowHost = null; /** LD-HALL 场景:战斗退出 / 冷启动 */ private _hallLoadReason: 'battle_exit' | 'cold_boot' = 'cold_boot'; private _homeBundlePrefetch: Promise | null = null; private _herosResReady = false; /** 进战斗管道代数:异步完成时校验,防止过期回调在 teardown 后仍 load */ private _battleEnterSession = 0; /** 当前进战斗附加选项(匹配页门闩) */ private _enterOpts: BattleEnterOpts | null = null; /** 匹配页回传句柄 */ private _matchGateHandlers: MatchGateHandlers | null = null; /** 匹配进战失败回调 */ private _matchOnFail: (() => void) | null = null; /** 匹配链路是否跳过体力检查(首页已校验,匹配/加载期间不再弹购买体力) */ private _matchNotCheckPower = false; /** 首页开始→匹配:仅飞行动画,进战斗页成功后再扣体力 */ private _homeMatchDeferVigour = false; /** 局内 BattleGame 打开成功后执行 subVigour */ private _enterConsumeVigour = false; /** 回首页恢复连续失败次数,避免 load 挂死后再 enterMain 死循环 */ private _homeRecoverFails = 0; /** 开场剧情打不开时的兜底(与关闭开场剧情时一致:进第 1 章战斗) */ private _enterFirstLoginFallback(): void { this.CurentSelectLevelId = 1; const entered = this.enterBattle(true); if (entered !== 1) { this.enterMain(); } } /** * 进入主场景 * 须先结束战斗并等面板销毁,再释放 BattleRes;否则 Batcher2D 仍渲染已 decRef 的 SpriteFrame → getGFXTexture 空引用。 */ enterMain(_pageeID: UIID = UIID.Home) { if (gg.ui.hasPanel(UIID.Home) && !gg.battle.match && !this.CurentBattle) { EmolumentDataManager.tryPopPanelIfNeeded(); return; } if (this._enterMainInflight) return; this.showLoading(1, true); if (this._isColdBootEnterMain()) { void this._prefetchHomeBundle(); this._enterMainFromColdBoot(); return; } void this._enterMainAfterBattleTeardown(); } /** 冷启动进大厅:无战斗 UI / 对局,跳过 battle-exit 管道 */ private _isColdBootEnterMain(): boolean { return !gg.battle.match && !this.CurentBattle && !gg.ui.hasPanel(UIID.BattleGame); } private _enterMainFromColdBoot(): void { if (this._enterMainInflight) return; this._enterMainInflight = true; this._hallLoadReason = 'cold_boot'; AppLifecycle.setBattleKeepScreen(false); ResourcePhaseGate.getInstance().setPhase(ResourcePhase.Hall); this._enterMainReleaseBattleAndLoadHall(); } private _enterMainAfterBattleTeardown(): void { if (this._enterMainInflight) return; this._enterMainInflight = true; PreloadPredictor.getInstance().abortHallSilentDownload(); this._hallLoadReason = 'battle_exit'; void this._prefetchHomeBundle(); AppLifecycle.setBattleKeepScreen(false); ResourcePhaseGate.getInstance().setPhase(ResourcePhase.BattleExit); PerfReport.reportSessionEnd('battle_exit'); this._teardownOpenBattleUi(() => { gg.ui.clearAnimFlyNodes(); gg.ui.closeAllPanel().then(() => { gg.ui.clearAnimFlyNodes(); this._frameFlow.waitFrames(2, () => { this._endBattleSessionIfNeeded(); this._enterMainReleaseBattleAndLoadHall(); }); }).catch((e) => { console.error('[GameController] enterMain closeAllPanel', String(e)); this._finishEnterMainInflight(); }); }); } private _prefetchHomeBundle(): Promise { const homeReady = !!assetManager.getBundle(GBundle.Home); const commReady = !!assetManager.getBundle(GBundle.Comm); if (homeReady && commReady) { return Promise.resolve(); } if (this._homeBundlePrefetch && commReady) { return this._homeBundlePrefetch; } this._homeBundlePrefetch = Promise.all([ this._loadBundleIfNeeded(GBundle.Home), this._loadBundleIfNeeded(GBundle.Comm), ]).then(() => undefined); return this._homeBundlePrefetch; } private _loadBundleIfNeeded(bundleName: string): Promise { return BundleLifecycle.getInstance().ensureLoaded(bundleName).then(() => undefined); } private _loadHerosResIfNeeded(onComplete: (result?: LoadResult) => void): void { if (this._herosResReady) { onComplete({ ok: true, failed: [] }); return; } gg.res.loadResPakg(getHerosResPakage(), null, (result) => { if (result.ok) { this._herosResReady = true; } onComplete(result); }); } private _enterMainReleaseBattleAndLoadHall(): void { MemoryProfile.resolve(); const retainBattleRes = MemoryProfile.current.enableBattleResRetain; const life = BundleLifecycle.getInstance(); if (retainBattleRes) { // 跨局复用:用 battleRetain 钉住战斗分包,clearResPkg 只放资源引用不卸 zip const battleBundles = ['battleMap', 'battleGameMonster', 'battleHero', 'popBattle', 'battleGame']; for (let i = 0; i < battleBundles.length; i++) { life.acquire(battleBundles[i], 'battleRetain'); } gg.res.clearResPkg(ResPkgName.BattleRes); gg.res.purgeNodePoolForBundles(battleBundles); this._battleResRetained = true; } else { life.releaseOwner('battleRetain'); gg.res.clearResPkg(ResPkgName.BattleRes); gg.res.clearNodePool(); this._battleResRetained = false; life.releaseBattlePhase(); } this._invalidateBattlePreloadCache(); PreloadPredictor.getInstance().resetBattlePhase(); PreloadPredictor.getInstance().resetHallPhase(); BattleAudio.releaseBattleWarmup(); gg.audio.release(); sdkConfig.hallLoadTimer?.dispose(); sdkConfig.hallLoadTimer = PerfLoadTimer.start(); void this._prefetchHomeBundle().then(() => { tryProactiveCacheTrimBeforePreload(); let pending = 1; const onHallAssetsReady = (result?: LoadResult) => { if (result && !result.ok) { PerfReport.reportLoadFail('main_shell', result.failed.length); this.recoverHome('main_shell_load'); return; } pending--; if (pending > 0) return; PreloadPredictor.getInstance().markMainShellLoaded(); gg.ui.openPanel(UIID.Home).then((ok) => { if (!ok) { this.recoverHome('home_open'); return; } this._enterMainAfterHomeOpen(); }).catch((e) => { console.error('[GameController] enterMain open Home', String(e)); this.recoverHome('home_open'); }); }; gg.res.loadResPakg(getMainShellResPakage(), (c, t) => { this.onLoadingShow(c, t); if (DEV) { } }, onHallAssetsReady); }).catch((e) => { console.error('[GameController] enterMain home bundle', String(e)); this.recoverHome('home_bundle'); }); } private _enterMainAfterHomeOpen(): void { this._playHallBgm(); ResourcePhaseGate.getInstance().setPhase(ResourcePhase.Hall); PreloadPredictor.getInstance().warmHighPriorityHallTabs(); MemoryDowngrade.reset(); const hallTimer = sdkConfig.hallLoadTimer; sdkConfig.hallLoadTimer = null; if (hallTimer) { PerfReport.reportLoadHall( hallTimer.foregroundMs(), hallTimer.wallMs(), this._hallLoadReason, ); hallTimer.dispose(); } PerfReport.reportSessionEnd('hall'); this._homeRecoverFails = 0; this.onMainUiReady(); if (this._hallLoadReason === 'battle_exit') { DailyShopDataManager.popSettleShopPanelIfNeeded(); } this._finishEnterMainInflight(); } private _finishEnterMainInflight(): void { gg.game.forceHideLoading(); this._enterMainInflight = false; } /** * 先打开匹配页;预载+最短展示完成后进战斗,局内页打开后再关匹配页。 * 匹配页打不开则直接进战;进战失败走 onFail。 * @param opts.notCheckPower 跳过体力检查(与 enterBattle 一致) * @param opts.onFail 匹配或进战失败 */ enterBattleWithMatch(opts?: { notCheckPower?: boolean; onFail?: () => void }): void { void this._enterBattleWithMatch(opts); } /** 匹配页预载:表 + 神将包 + BattleGame 预制;战斗包只点火,不阻塞门闩 */ async preloadBattleForMatch(): Promise { PreloadPredictor.getInstance().abortHallSilentDownload(); LoadScheduler.getInstance().beginUserLoad(); const levelId = this.resolveBattleLevelId(); this.CurentSelectLevelId = levelId; try { await awaitWithTimeout( gg.data.table.ensureBattleTables(), LoadRecoveryConfig.dirTimeoutMs, 'battleTables', ); PreloadPredictor.getInstance().scheduleBattleWarm(levelId, 'interactive'); await Promise.all([ this._ensureHerosResForBattle(), this._ensureBattleGamePrefab(), ]); } catch (e) { console.error('[GameController] preloadBattleForMatch', String(e)); } finally { LoadScheduler.getInstance().endUserLoad(); } } /** * 解析本局 Level id:GM 固定 > 首次进战读 Const > 已选或随机。 */ resolveBattleLevelId(): number { const forced = Math.floor(Number(this.gmMapId) || 0); if (forced > 0) { return gg.data.project.clampBattleLevelId(forced); } if (RankDataManager.isFirstEnterBattle()) { return RankDataManager.readFirstEnterBattleLevelId(); } if (this.CurentSelectLevelId > 0) { return gg.data.project.clampBattleLevelId(this.CurentSelectLevelId); } return RankDataManager.pickRandomBattleLevelId(); } /** 确保 battleGame 分包与战斗页预制已加载 */ private _ensureBattleGamePrefab(): Promise { const path = GPath.UIPrefab + 'BattleGame'; return BundleLifecycle.getInstance().ensureLoaded(GBundle.BattleGame).then((bundle) => { if (!bundle) { console.error('[GameController] battleGame bundle failed'); return; } if (bundle.get(path, Prefab)) return; return new Promise((resolve) => { let done = false; const finish = (): void => { if (done) return; done = true; resolve(); }; const timer = setTimeout(() => { console.error('[GameController] BattleGame prefab timeout'); finish(); }, LoadRecoveryConfig.prefabTimeoutMs); bundle.load(path, Prefab, (loadErr) => { clearTimeout(timer); if (loadErr) { console.error('[GameController] BattleGame prefab failed', loadErr); } finish(); }); }); }); } private async _enterBattleWithMatch(opts?: { notCheckPower?: boolean; onFail?: () => void }): Promise { MatchWinStats.ensurePendingOpponentRank(); this._matchOnFail = opts?.onFail ?? null; this._matchNotCheckPower = !!opts?.notCheckPower; this._homeMatchDeferVigour = true; const ok = await gg.ui.openPanel(UIID.match, { data: { onGateReady: this._onMatchGateReady }, }); if (ok) return; const entered = this.enterBattle(this._matchNotCheckPower); if (entered !== 1) { this._homeMatchDeferVigour = false; this._enterConsumeVigour = false; this._matchOnFail?.(); } } private _onMatchGateReady = (handlers: MatchGateHandlers): void => { this._matchGateHandlers = handlers; const ret = this.enterBattle(this._matchNotCheckPower, { skipLoading: true, deferCloseMatch: true, onBattleOpened: this._onMatchBattleOpened, }); if (ret !== 1) { this._matchGateHandlers = null; this._homeMatchDeferVigour = false; this._enterConsumeVigour = false; handlers.notifyEnterFailed(); this._matchOnFail?.(); } }; private _onMatchBattleOpened = (battleOk: boolean): void => { const handlers = this._matchGateHandlers; this._matchGateHandlers = null; if (battleOk) { this._consumeEnterVigourIfNeeded(); handlers?.notifyBattleEntered(); this._matchOnFail = null; return; } this._enterConsumeVigour = false; handlers?.notifyEnterFailed(); const fail = this._matchOnFail; this._matchOnFail = null; fail?.(); }; /** * 进入战斗场景 * 重开时须先关掉旧 BattleGame 再 start:node.destroy 的 onDestroy 可能晚一帧, * UIBattleGame 会按 session 所有权决定是否 end,避免误清新对局。 * @param notCheckPower 跳过体力检查 * @param opts 匹配页门闩 */ enterBattle(notCheckPower = false, opts?: BattleEnterOpts) { if (this._battleEnterInflight) { return 0; } if (gg.data.project.getEquipWeaponIds().length < 8) { gg.ui.showToast(gg.lang.get("上阵武器不足8个")); return 0; } // 推荐流直玩预加载/获客进景:禁止被体力弹窗拦截,否则报不到 reportScene(7001) const feedLaunch = !!gg.sdk?.isFeedDirectPlayLaunch?.(); if (!notCheckPower && !feedLaunch && !gg.data.project.checkVigourEnough()) { gg.ui.openPanel(UIID.buyPower); return 0; } this._abortHallLevelPreload(); PreloadPredictor.getInstance().abortHallSilentDownload(); LoadScheduler.getInstance().beginUserLoad(); this._enterConsumeVigour = this._homeMatchDeferVigour || (!notCheckPower && !feedLaunch); this._homeMatchDeferVigour = false; if (feedLaunch && gg.sdk?.isFeedPreboot?.()) { gg.sdk.markFeedPendingVigourDeduct?.(); this._enterConsumeVigour = false; } this._enterOpts = opts ?? null; if (!opts?.skipLoading) { this.showLoading(1, true); } gg.audio.release(); ResourcePhaseGate.getInstance().setPhase(ResourcePhase.Battle); sdkConfig.battleLoadTimer?.dispose(); sdkConfig.battleLoadTimer = PerfLoadTimer.start(); sdkConfig.enterBattleTime = Date.now(); this._battleEnterInflight = true; const enterSession = ++this._battleEnterSession; const levelId = this.resolveBattleLevelId(); this.CurentSelectLevelId = levelId; RankDataManager.markBattleEntered(); const reportLevelId = gg.sdk.get30rLevelId(); gg.data.addTotalBattleCount(); gg.sdk.reportDY('inLevel', `章节${this.getReportDYBattleCount()}_1-进入`); gg.sdk.report30r('LEVEL_ENTER', { level_id: reportLevelId, stage_type: gg.sdk.get30rStageType(), action_param: reportLevelId, }); this._loadBattleResAfterUiTeardown(levelId, enterSession); return 1; } private _failBattleEnter(session: number, reason = 'battle_enter'): void { LoadScheduler.getInstance().endUserLoad(); this._enterConsumeVigour = false; this._homeMatchDeferVigour = false; const opened = this._enterOpts?.onBattleOpened; this._enterOpts = null; if (session === this._battleEnterSession) { this._battleEnterSession++; } opened?.(false); this.recoverHome(reason); } /** * 加载失败统一恢复:关 loading → 回可交互大厅(或提示重启)。 * @param reason 恢复原因(PF-RECOVER) * @param opts.restart Boot 关键路径失败时提示重启 */ recoverHome(reason: string, opts?: { restart?: boolean }): void { PerfReport.reportRecover(reason); PreloadPredictor.getInstance().abortHallSilentDownload(); LoadScheduler.getInstance().resetUserLoad(); gg.ui.abortInflightOpens(); this._releaseBattleRuntime(); AppLifecycle.setBattleKeepScreen(false); ResourcePhaseGate.getInstance().setPhase(ResourcePhase.Hall); this._battleEnterInflight = false; this._enterMainInflight = false; this._enterConsumeVigour = false; this._homeMatchDeferVigour = false; this.forceHideLoading(); this._closeMatchAfterRecover(); if (opts?.restart) { gg.ui.showToast('资源加载失败,请重启小游戏'); gg.sdk.restartMiniProgramSync(); return; } const homeUi = gg.ui.getPanelBase(UIID.Home); if (homeUi?.node?.isValid) { this._homeRecoverFails = 0; gg.ui.showToast('加载失败,请重试'); return; } this._homeRecoverFails++; if (this._homeRecoverFails >= 2) { gg.ui.showToast('资源加载失败,请重启小游戏'); gg.sdk.restartMiniProgramSync(); return; } gg.ui.showToast('加载失败,正在返回首页'); void this.enterMain(); } /** 进战失败时关掉匹配页并回传 onFail,避免一直停在匹配中 */ private _closeMatchAfterRecover(): void { const handlers = this._matchGateHandlers; this._matchGateHandlers = null; handlers?.notifyEnterFailed(); const fail = this._matchOnFail; this._matchOnFail = null; fail?.(); if (gg.ui.hasPanel(UIID.match)) { void gg.ui.closePanel(UIID.match); } } /** 局内页真正打开后再扣体力,匹配/加载失败不扣 */ private _consumeEnterVigourIfNeeded(): void { if (!this._enterConsumeVigour) return; this._enterConsumeVigour = false; const cost = gg.data.project.getBattleVigourCost(); if (!(cost > 0)) { console.error('[GameController] 进战扣体力数值非法', cost); return; } if (!gg.data.project.checkVigourEnough()) { console.error('[GameController] 进战成功但体力已不足,取消扣体力', gg.data.getVigour(), cost); return; } gg.data.subVigour(cost); } /** RuntimeLoadingGate 超时:避免永久遮罩 */ onLoadingWatchdogTimeout(): void { this.recoverHome('loading_watchdog'); } /** * 关战斗 UI:先释放 Spine/怪物视图并 end 对局,再关页。 * end 必须在 closeAllPanel 之前:Pop 层 Pause 先关会 release 暂停门闩并 resume, * 若对局未结束会解冻技能 Spine 并继续播「攻击」;同时 CurentBattle 须清空以免 resume。 */ private _teardownOpenBattleUi(done: () => void): void { this._releaseBattleRuntime(); this._frameFlow.waitFrames(1, done); } /** 先拆战斗视图再 end,避免 dispose 后 UI 仍同步 */ private _releaseBattleRuntime(): void { const battleUi = gg.ui.getPanelBase(UIID.BattleGame); if (battleUi?.node?.isValid) { battleUi.teardownBeforeDestroy(); } this._endBattleSessionIfNeeded(); } /** 结束对局并清空 CurentBattle */ private _endBattleSessionIfNeeded(): void { if (!gg.battle.match) return; gg.battle.end(); HeadIconManager.resetBattleSession(); this.CurentBattle = null; this.localPlayer = null; } /** 冷启动直进战斗未走 enterMain 时,须先载入 HerosRes 字形池供 HeroSkin / TextGrid 实例化 */ private async _ensureHerosResForBattle(): Promise { if (this._herosResReady && assetManager.getBundle(GBundle.Heros)) { return; } this._herosResReady = false; try { await awaitWithTimeout( new Promise((resolve) => { this._loadHerosResIfNeeded((result) => { if (!result?.ok) { console.error('[GameController] HerosRes 预载未完全成功', result?.failed); } resolve(); }); }), LoadRecoveryConfig.dirTimeoutMs, 'herosRes', ); } catch (e) { console.error('[GameController] HerosRes 等待超时', String(e)); } } /** * 关页并等 onDestroy 完成后再 loadResPakg,避免连关(1→2 章)时 * clearResPkg 卸 bundle 后仍从池中取出带失效贴图的 monsterTemplate。 */ private _loadBattleResAfterUiTeardown(levelId: number, enterSession: number): void { const entryWarm = Promise.all([ gg.data.table.ensureBattleTables(), this._ensureHerosResForBattle(), ]); this._teardownOpenBattleUi(() => { const keepIds = this._enterOpts?.deferCloseMatch ? [UIID.match] : []; gg.ui.closeAllPanel(keepIds).then(() => { gg.ui.clearAnimFlyNodes(); this._frameFlow.waitFrames(2, () => { this._endBattleSessionIfNeeded(); void entryWarm.then(() => { this._continueBattleResAfterWarm(levelId, enterSession); }).catch((e) => { console.error('[GameController] enterBattle warm', String(e)); this._continueBattleResAfterWarm(levelId, enterSession); }); }); }).catch((e) => { console.error('[GameController] enterBattle closeAllPanel', String(e)); this._failBattleEnter(enterSession); }); }); } /** * warm 等待结束(成功或超时)后走进战加载,禁止因预载超时卡在匹配页。 * @param levelId 关卡 * @param enterSession 进战世代 */ private _continueBattleResAfterWarm(levelId: number, enterSession: number): void { if (enterSession !== this._battleEnterSession) { this._battleEnterInflight = false; LoadScheduler.getInstance().endUserLoad(); return; } const mapID = ChapterLevelBuilder.getLevelMapId(levelId); const resPkg = ResPkgBuilder.buildBattlePackage(levelId, mapID, 0); this._loadBattleResAfterPanelsClosed(resPkg, levelId, enterSession); } private _loadBattleResAfterPanelsClosed( resPkg: ResPakage, levelId: number, enterSession: number, ): void { if (enterSession !== this._battleEnterSession) { this._battleEnterInflight = false; LoadScheduler.getInstance().endUserLoad(); return; } this._invalidateHallWarmPreload(); PreloadPredictor.getInstance().resetHallPhase(); if (MemoryProfile.current.enableTabBundleUnloadOnBattle) { BundleLifecycle.getInstance().releaseTabBundles(); } const predictor = PreloadPredictor.getInstance(); const warmReady = predictor.isBattleWarmReady(levelId); const warmInflight = predictor.isBattleWarmInflight(levelId); const retained = this._battleResRetained; if (warmReady) { const bundleNames = createMembershipSet(); for (const conf of resPkg.ress) { if (typeof conf.bundle === 'string' && conf.bundle) { membershipAdd(bundleNames, conf.bundle); } } gg.res.clearNodePool(); gg.res.purgeNodePoolForBundles(membershipToArray(bundleNames)); predictor.consumeBattleWarm(levelId); } else if (!warmInflight && !retained) { predictor.abortBattleWarmForChapter(levelId); this._purgeBattleResBeforeLoad(resPkg); } // 退战保留时禁止 fresh/evict,否则会把已下缓存冲掉再下一遍 const fresh = !warmReady && !warmInflight && !retained; this._battleResRetained = false; void loadWithRetry( () => new Promise((resolve, reject) => { // loadResPakg 无整体超时:弱网下个别回调丢失会让 Promise 永挂,进战门闩卡死 let settled = false; const timer = setTimeout(() => { if (settled) return; settled = true; reject(new Error(`battleRes timeout lv${levelId}`)); }, LoadRecoveryConfig.dirTimeoutMs); try { gg.res.loadResPakg(resPkg, (c, t) => { this.onLoadingShow(c, t); }, (result) => { if (settled) return; settled = true; clearTimeout(timer); resolve(result); }, fresh); } catch (e) { if (settled) return; settled = true; clearTimeout(timer); reject(e); } }), { label: `battleRes:lv${levelId}` }, ).then((result) => { if (enterSession !== this._battleEnterSession) { this._battleEnterInflight = false; LoadScheduler.getInstance().endUserLoad(); return; } if (!result.ok) { PerfReport.reportLoadFail(`battle_lv${levelId}`, result.failed.length); this._failBattleEnter(enterSession, 'battle_res_load'); return; } this._startBattleAfterResLoaded(); }).catch((e) => { console.error('[GameController] battleRes load', String(e)); if (enterSession !== this._battleEnterSession) { this._battleEnterInflight = false; LoadScheduler.getInstance().endUserLoad(); return; } this._failBattleEnter(enterSession, 'battle_res_timeout'); }); } private _startBattleAfterResLoaded(): void { LoadScheduler.getInstance().endUserLoad(); const levelId = gg.data.project.clampBattleLevelId(this.CurentSelectLevelId); const playerRankId = RankDataManager.getPlayerRankId(); const pendingRank = MatchWinStats.peekPendingOpponentRankId(); const pendingStars = MatchWinStats.peekPendingOpponentStars(); MatchWinStats.takePendingOpponentRankId(); // 开战按当前玩家携带重抽 AI buff(匹配页 ensurePending 可能过早建档导致 buff 恒空) const profile = AiBattleBridge.inst.buildMatchProfile(); if (profile && pendingRank > 0) { AiBattleBridge.inst.applyPendingOpponent(pendingRank, pendingStars); } let opponentRankId = pendingRank > 0 ? pendingRank : (profile?.opponentRankId ?? 0); if (opponentRankId <= 0) { opponentRankId = RankDataManager.pickOpponentRankId(playerRankId); } const runtime = gg.battle.start({ levelId, playerRankId, opponentRankId, mode: 'pvp', flowMode: BattleFlowMode.Continuous, }); if (!runtime) { this._failBattleEnter(this._battleEnterSession, 'battle_init'); gg.ui.showToast('战斗初始化失败'); return; } this.CurentBattle = runtime as ProjectBattleHandle; this.localPlayer = runtime.player ?? null; HeadIconManager.resetBattleSession(); AppLifecycle.setBattleKeepScreen(true); gg.ui.openPanel(UIID.BattleGame).then((ok) => { const opts = this._enterOpts; this._enterOpts = null; if (!ok) { AppLifecycle.setBattleKeepScreen(false); this._releaseBattleRuntime(); gg.game.flushDeferredLevelPreload(); gg.ui.showToast('战斗界面加载失败'); this._enterConsumeVigour = false; opts?.onBattleOpened?.(false); if (opts?.deferCloseMatch) { this.recoverHome('battle_ui_open'); return; } } else { if (!opts?.deferCloseMatch) { this._consumeEnterVigourIfNeeded(); } BattleAudio.playBgmBattle(); PreloadPredictor.getInstance().startBattleSilentDownload(levelId); opts?.onBattleOpened?.(true); } if (!opts?.skipLoading) { gg.game.hideLoading(); } this._battleEnterInflight = false; }).catch((e) => { console.error('[GameController] enterBattle open BattleGame', String(e)); const opts = this._enterOpts; this._enterOpts = null; this._enterConsumeVigour = false; this._battleEnterSession++; opts?.onBattleOpened?.(false); if (!opts?.skipLoading) { gg.game.hideLoading(); } this._battleEnterInflight = false; this._releaseBattleRuntime(); if (opts?.deferCloseMatch) { this.recoverHome('battle_ui_open'); } }); } onLoadingShow(c: number, t: number) { this.CurentLoadingProgress = clamp01(c / t); } CurentLoadingProgress: number = 0; /** * 获取所有秒赚 * @returns */ getAllProfit() { let num = 0; return Math.floor(num); } /** * 弹出道具不足弹窗 * @param skipUIidArr 需要跳转的UIID数组 * @param itemID 道具id * @param itemNum 道具数量 * @param showVideoAd 是否显示视频广告获取 * @returns */ showSkipPop(itemID = 0, itemNum = 1, showVideoAd = false, skipUIidArr: UIID[] = [], title = "道具不足") { gg.ui.openPanel(UIID.SkipPop, { data: { skipUIidArr: skipUIidArr, itemID: itemID, itemNum: itemNum, showVideoAd: showVideoAd, title: title } }); } getUserInfo() { gg.sdk.getUserInfo().then((info) => { this.UserInfo = info as IUserInfo; if (this.UserInfo) { gg.data.doc.uid = this.UserInfo.nickName; } }); } showMsgBox(content: string, title: string = "温馨提示", yesFun: Function = null, noFun: Function = null) { gg.ui.openPanel(UIID.MsgBox, { data: { title: title, content: content, yesFun: yesFun, noFun: noFun } }) } /** * 公用 PopToast 确认弹窗 * @param desc 描述 * @param type 类型(1=删除道具) * @param onOk 确定回调 * @param onCancel 取消回调 */ showPopToast( desc: string, type: PopToastType = PopToastType.DeleteItem, onOk?: () => void, onCancel?: () => void, ): void { gg.ui.openPanel(UIID.PopToast, { data: { desc, type, onOk, onCancel, } satisfies PopToastParam, }); } /**获取最大免费扫荡次数 */ getMaxFreeSweepTimes() { return 3; } /**根据奖励字符串解析奖励道具(只能解析固定格式的字符串:“道具id,道具数量|道具id,道具数量|...”) */ parseRewardStr(rewardStr: string) { let rewardArr = rewardStr.split("|"); let rewardList: ItemData[] = []; for (let i = 0; i < rewardArr.length; i++) { let str = rewardArr[i]; if (str == "") continue; let [id, num] = rewardArr[i].split(","); const itemId = Number(id); const itemNum = Number(num); if (!(itemId > 0) || !(itemNum > 0)) continue; let itemData = new ItemData(itemId, itemNum); itemData.staticData = gg.data.table.getItemData(itemId); if (!itemData.staticData) continue; itemData.isFragment = itemData.staticData.type == 8; rewardList.push(itemData); } return rewardList; } /**根据道具数组合并同类型道具 */ mergeSameTypeItem(itemList: ItemData[]) { let itemMap = new Map(); for (let i = 0; i < itemList.length; i++) { let item = itemList[i]; if (itemMap.has(item.id)) { itemMap.get(item.id).num += item.num; } else { itemMap.set(item.id, item); } } return mapValuesToArray(itemMap); } /**根据道具数组自动获得通用碎片转化的道具 */ autoMergeFragment(itemList: ItemData[]) { const result: ItemData[] = []; for (let i = 0; i < itemList.length; i++) { const item = itemList[i]; if (!item.staticData) item.staticData = gg.data.table.getItemData(item.id); if (item.staticData?.type === 4) { result.push(...this._expandType4Box(item.id, item.num)); } else { result.push(item); } } return this.mergeSameTypeItem(result); } /** type=4 品质碎片箱 → 具体武器碎片 */ private _expandType4Box(itemId: number, itemNum: number): ItemData[] { const boxRewardArray = gg.data.getFragmentNumArray(`${itemId},${itemNum}`); const statsArray = MTools.mergeToStatsArray(boxRewardArray); const result: ItemData[] = []; for (let i = 0; i < statsArray.length; i++) { const [idStr, numStr] = statsArray[i].split('|'); const id = Number(idStr); const num = Number(numStr); const item = new ItemData(id, num); item.id = id; item.num = num; item.staticData = gg.data.table.getItemData(id); result.push(item); } return result; } /** 单条奖励 id,num 展开为可入背包的 ItemData 列表(type 4/5/8 特殊处理) */ private _expandSingleRewardItem(itemId: number, itemNum: number): ItemData[] { const tableItem = gg.data.table.getItemData(itemId); if (!tableItem) { console.log('GameController._expandSingleRewardItem: Item 缺失', itemId); return []; } if (tableItem.type === 4) { return this._expandType4Box(itemId, itemNum); } if (tableItem.type === 5) { const item = new ItemData(itemId, itemNum); item.id = itemId; item.num = itemNum; item.staticData = tableItem; return [item]; } const item = new ItemData(itemId, itemNum); item.id = itemId; item.num = itemNum; item.staticData = tableItem; return [item]; } getRoleLv() { let curData = gg.data.getStringData(OtherDataType.UserLevelupData) let curDataArr = curData.split('&') let roleLevel = curDataArr[0] return Number(roleLevel); } /**根据奖励字符串显示获得奖励界面 */ showGetRewardByReardStr(rewardStr: string, callBack = null) { const itemsArray: ItemData[] = []; const rewardsStrArr = rewardStr.split("|"); for (let i = 0; i < rewardsStrArr.length; i++) { const signReward = rewardsStrArr[i]; if (!signReward) continue; const [id, num] = signReward.split(","); const itemId = Number(id); const itemNum = Number(num); if (!(itemId > 0) || !(itemNum > 0)) continue; itemsArray.push(...this._expandSingleRewardItem(itemId, itemNum)); } if (callBack) { gg.ui.openPanel(UIID.GetReward, { data: itemsArray, onClose: callBack }); } else { gg.ui.openPanel(UIID.GetReward, { data: itemsArray }); } } /**根据道具数组显示获得奖励界面 */ showGetRewardByItemList(items: ItemData[]) { const itemsArray: ItemData[] = []; for (let i = 0; i < items.length; i++) { const itemData = items[i]; if (!itemData.staticData) itemData.staticData = gg.data.table.getItemData(itemData.id); itemsArray.push(...this._expandSingleRewardItem(itemData.id, itemData.num)); } gg.ui.openPanel(UIID.GetReward, { data: itemsArray }); } showLoading(index: number = 2, immediate = false) { GEvent.Ins.emit(GEvent.SHOW_LOADING, index, immediate); } hideLoading() { GEvent.Ins.emit(GEvent.HIDE_LOADING); } /** 强制关闭 loading(ref 失配等意外时兜底) */ forceHideLoading() { GEvent.Ins.emit(GEvent.FORCE_HIDE_LOADING); } /**根据id跳转到指定页面 */ gotoPageById(id: number) { } isPreLoadLevelRes: boolean = false; private _pendingPreLoadLevelId: number = 0; private _runningPreLoadLevelId: number = 0; /** 战斗期间挂起的大厅 Level 预载,战斗结束后再执行 */ private _deferredPreLoadLevelId: number = 0; /** 上次完成预载的 Level(换关 invalidate 用) */ private _lastPreloadedLevelId: number = 0; private _invalidatePreloadedLevelRes(levelId: number): void { if (levelId <= 0) return; const levelData = gg.data.project.getLevelConfig(levelId); if (!levelData) return; const mapID = ChapterLevelBuilder.getLevelMapId(levelId); const resPkg = ResPkgBuilder.buildBattlePackage(levelId, mapID, 0); gg.res.invalidatePreloadForPackage(resPkg); } /** * 中止大厅侧 Level 预载(preLoadLevelRes)。 * Predictor 战斗 warm 在进关路径上 await 后复用,不在此处 abort。 */ private _abortHallLevelPreload(): void { const runningId = this._runningPreLoadLevelId; if (this.isPreLoadLevelRes || runningId > 0 || this._lastPreloadedLevelId > 0) { const levelId = runningId || this._lastPreloadedLevelId; if (levelId > 0) { this._invalidatePreloadedLevelRes(levelId); } } this.isPreLoadLevelRes = false; this._runningPreLoadLevelId = 0; this._pendingPreLoadLevelId = 0; } /** loadResPakg 前:清节点池 + invalidate + evict(fresh load 在 loadResPakg 内完成) */ private _purgeBattleResBeforeLoad(resPkg: ResPakage): void { const bundleNames = createMembershipSet(); for (const conf of resPkg.ress) { if (typeof conf.bundle === 'string' && conf.bundle) { membershipAdd(bundleNames, conf.bundle); } } gg.res.clearNodePool(); gg.res.purgeNodePoolForBundles(membershipToArray(bundleNames)); gg.res.invalidatePreloadForPackage(resPkg); } /** 大厅就绪后播放首页 BGM(新产品直开 Home,不经 MainUI.onInit) */ private _playHallBgm(): void { gg.audio.playMusic({ name: '首页背景音', path: GAudioPath.Bgm }); } /** Home 可交互后再静默下载战斗包与大厅弹窗(不阻塞、失败不回首页) */ onMainUiReady(): void { PreloadPredictor.getInstance().rescheduleHomeIdle(); void gg.equip.ensureTable(); void HeadIconManager.syncUnlocksFromProgressAsync(); // 只预拉总榜/日榜;定位权限放到玩家打开排行榜之后 RankBoardManager.warmupOnHallEnter(); // 推荐流复访托管:上报体力恢复就绪;订阅面板每进程最多拉起一次 gg.sdk.storeFeedData(); gg.sdk.PlatformFeedSubscribe(); ResourcePhaseGate.getInstance().setPhase(ResourcePhase.HallPreload); this._frameFlow.waitFrames(2, this._startHallSilentAfterReady); } private _startHallSilentAfterReady = (): void => { PreloadPredictor.getInstance().warmHighPriorityHallTabs(); PreloadPredictor.getInstance().startHallSilentDownload(); ResourcePhaseGate.getInstance().setPhase(ResourcePhase.Hall); }; /** 大厅 Level 预载是否已完成(skip beforeBattle 用) */ isLevelPreloaded(levelId: number): boolean { return this._lastPreloadedLevelId === levelId && !this.isPreLoadLevelRes; } /** @deprecated 兼容 PreloadPredictor */ isChapterPreloaded(chapterId: number): boolean { return this.isLevelPreloaded(chapterId); } /** 进战释放大厅弹窗预制(zip 常驻 popCommon);长尾一并卸 */ private _invalidateHallWarmPreload(): void { gg.res.invalidatePreloadForPackage(getHallWarmResPakage()); gg.res.invalidatePreloadForPackage(getHallWarmLongTailResPakage()); } /** 退战斗时清除上一关战斗预载登记 */ private _invalidateBattlePreloadCache(): void { const levelId = this._lastPreloadedLevelId || this.CurentSelectLevelId || 0; if (levelId <= 0) return; this._invalidatePreloadedLevelRes(levelId); this._lastPreloadedLevelId = 0; this._deferredPreLoadLevelId = 0; this._pendingPreLoadLevelId = 0; this._runningPreLoadLevelId = 0; this.isPreLoadLevelRes = false; } /** 进大厅后预载当前 Level 战斗资源(统一入口,避免与 enterHall 规则重复) */ private _preloadCurrentLevelRes(): void { const levelId = this.CurentSelectLevelId ?? 1; this.preLoadLevelRes(levelId); } /** 战斗结束、回到大厅后恢复挂起的 Level 预载 */ flushDeferredLevelPreload(): void { const levelId = this._deferredPreLoadLevelId; if (levelId <= 0) return; this._deferredPreLoadLevelId = 0; this.preLoadLevelRes(levelId); } /** @deprecated 兼容旧调用 */ flushDeferredChapterPreload(): void { this.flushDeferredLevelPreload(); } /** 预加载指定 Level 战斗资源 */ preLoadLevelRes(levelID: number) { return; if (this.isPreLoadLevelRes) { this._pendingPreLoadLevelId = levelID; return; } const levelData = gg.data.project.getLevelConfig(levelID); if (!levelData) { return; } if (this._lastPreloadedLevelId > 0 && this._lastPreloadedLevelId !== levelID) { this._invalidatePreloadedLevelRes(this._lastPreloadedLevelId); } this.isPreLoadLevelRes = true; this._runningPreLoadLevelId = levelID; this._pendingPreLoadLevelId = 0; const preloadTimer = PerfLoadTimer.start(); const mapID = ChapterLevelBuilder.getLevelMapId(levelID); const resPkg = ResPkgBuilder.buildBattlePackage(levelID, mapID, 0); gg.res.preLoadResPakg(resPkg, (c, t) => { this.onLoadingShow(c, t); }, () => { const finishedLevelId = this._runningPreLoadLevelId; if (finishedLevelId <= 0 || finishedLevelId !== levelID) { preloadTimer.dispose(); return; } PerfReport.reportPreloadChapter( finishedLevelId, preloadTimer.foregroundMs(), resPkg.ress.length, preloadTimer.wallMs(), ); preloadTimer.dispose(); this._lastPreloadedLevelId = finishedLevelId; this.isPreLoadLevelRes = false; const nextLevelId = this._pendingPreLoadLevelId; this._pendingPreLoadLevelId = 0; if (nextLevelId > 0 && nextLevelId !== finishedLevelId) { this.preLoadLevelRes(nextLevelId); } this._runningPreLoadLevelId = 0; }); } /** @deprecated 兼容旧调用 */ preLoadChapterRes(chapterID: number) { this.preLoadLevelRes(chapterID); } }