You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
AdLike2048/assets/script/game/GameController.ts

1705 lines
64 KiB

3 weeks ago
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';
1 week ago
import { PopCoinToastParam } from '../ui/PopCoinToast/UIPopCoinToast';
3 weeks ago
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:本机开局灌入 CarryStore 的 Battlebuff.id(null=不灌;开战 clear 后再写入) */
gmCarryBuffIds: number[] | null = null;
3 weeks ago
/** 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.gmCarryBuffIds = null;
3 weeks ago
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<void> | 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<void> {
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<void> {
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<void> {
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<void> {
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<void>((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<void> {
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<void> {
if (this._herosResReady && assetManager.getBundle(GBundle.Heros)) {
return;
}
this._herosResReady = false;
try {
await awaitWithTimeout(
new Promise<void>((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<string>();
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<LoadResult>((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 {
3 days ago
MatchWinStats.markBattleStarted(opponentRankId);
3 weeks ago
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,
});
}
1 week ago
/**
* 金币不足确认弹窗:确定先看广告,成功后再回调。
* @param desc 描述
* @param onOk 广告成功后的确定回调
* @param onCancel 取消回调
*/
showPopCoinToast(desc: string, onOk?: () => void, onCancel?: () => void): void {
gg.ui.openPanel(UIID.PopCoinToast, {
data: {
desc,
onOk,
onCancel,
} satisfies PopCoinToastParam,
});
}
3 weeks ago
/**获取最大免费扫荡次数 */
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<number, ItemData>();
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<string>();
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);
}
}