diff --git a/.codex_tmp/sprite-load-error-probe.js b/.codex_tmp/sprite-load-error-probe.js new file mode 100644 index 00000000..7870ce59 --- /dev/null +++ b/.codex_tmp/sprite-load-error-probe.js @@ -0,0 +1,46 @@ +// 仅用于诊断测试包。复制到 build/wechatgame/ 后,在 game.js 中 +// __globalAdapter.adaptEngine() 后、require('./ccRequire') 前添加: +// require('./sprite-load-error-probe'); +// 本文件不会自动进入构建或修改资源生命周期;异常记录后继续抛出原错误。 +(function installSpriteLoadProbe() { + var proto = cc.Sprite.prototype; + var original = proto._applySpriteFrame; + if (original.__spriteLoadProbe) return; + + function probe() { + var frame = this._spriteFrame; + try { + return original.apply(this, arguments); + } catch (error) { + try { + var path = []; + for (var node = this.node; node; node = node.parent) { + path.unshift(node.name); + } + var texture = frame && frame.getTexture(); + console.error('[SpriteLoadError]', JSON.stringify({ + engineVersion: cc.ENGINE_VERSION, + nodePath: path.join('/'), + nodeValid: cc.isValid(this.node, true), + spriteValid: cc.isValid(this, true), + frameName: frame && frame.name, + frameUuid: frame && frame._uuid, + frameFlags: frame && frame._objFlags, + frameValid: cc.isValid(frame, true), + callbackTableNull: !!frame && frame._callbackTable === null, + textureUuid: texture && texture._uuid, + textureValid: cc.isValid(texture, true), + textureLoaded: !!texture && texture.loaded, + message: String(error) + })); + } catch (loggingError) { + console.error('[SpriteLoadError] diagnostic failed', loggingError); + } + throw error; + } + } + + probe.__spriteLoadProbe = true; + proto._applySpriteFrame = probe; + console.info('[SpriteLoadProbe] installed; engineVersion=' + cc.ENGINE_VERSION); +})(); diff --git a/assets/Script/GameManager.ts b/assets/Script/GameManager.ts index 0f51d743..8de0e5c9 100644 --- a/assets/Script/GameManager.ts +++ b/assets/Script/GameManager.ts @@ -273,7 +273,7 @@ export default class GameManager extends cc.Component { } } - console.log("_________________________abTests:", cc.fx.GameConfig.GM_INFO.abTests); + // console.log("_________________________abTests:", cc.fx.GameConfig.GM_INFO.abTests); } readUserData(retryCount = 0) { //@ts-ignore @@ -294,7 +294,7 @@ export default class GameManager extends cc.Component { const attemptUserInfo = () => { Utils.getUserInfo((data) => { if (data.code == 1) { // 假设返回数据中有 success 字段表示成功 - console.log("登錄", data); + // console.log("登錄", data); let systemInfo = null; if (cc.sys.platform === cc.sys.WECHAT_GAME) //@ts-ignore @@ -309,14 +309,14 @@ export default class GameManager extends cc.Component { cc.fx.StorageMessage.setStorage("openid", cc.fx.GameConfig.GM_INFO.openid); MiniGameSdk.API.tencent_SetOpenId(cc.fx.GameConfig.GM_INFO.openid); } - console.log("系统信息________:", data.data.version); + // console.log("系统信息________:", data.data.version); if (data.data.version) { let result = cc.fx.GameTool.compareVersion(cc.fx.GameConfig.GM_INFO.version, data.data.version); - console.log("版本号:", result); + // console.log("版本号:", result); if (result == 1 && systemInfo.platform == 'ios') { - console.log("ios版本号"); + // console.log("ios版本号"); cc.fx.GameConfig.GM_INFO.canIos = false; } else @@ -364,7 +364,7 @@ export default class GameManager extends cc.Component { if (data.data._id) { cc.fx.GameConfig.GM_INFO.uid = data.data._id; cc.fx.StorageMessage.setStorage("uid", data.data._id); - console.log("_________________用户UID", cc.fx.GameConfig.GM_INFO.uid); + // console.log("_________________用户UID", cc.fx.GameConfig.GM_INFO.uid); } if (data.data.onlyId) { cc.fx.GameConfig.GM_INFO.userId = data.data.onlyId; @@ -403,11 +403,11 @@ export default class GameManager extends cc.Component { if (data.data.userCat) { cc.fx.GameConfig.GM_INFO.useravatarCat = data.data.userCat; cc.fx.GameConfig.GM_INFO.useravatarCat = "cat_" + cc.fx.GameConfig.GM_INFO.useravatarCat; - console.log("我的猫:", cc.fx.GameConfig.GM_INFO.useravatarCat); + // console.log("我的猫:", cc.fx.GameConfig.GM_INFO.useravatarCat); } if (data.data.register_time) { this.register_time = data.data.register_time; - console.log("注册时间:", data.data.register_time); + // console.log("注册时间:", data.data.register_time); cc.fx.GameConfig.GM_INFO.firstTime = data.data.register_time; } else { @@ -429,18 +429,18 @@ export default class GameManager extends cc.Component { cc.fx.GameConfig.GM_INFO.address = data.data.address; } this.checkDailyQuests(); - console.log("服务器强制刷新状态__________", data.data.forcedUpdate); + // console.log("服务器强制刷新状态__________", data.data.forcedUpdate); //如果有连胜记录,就赋值 if (data.data.winStreak !== undefined && data.data.winStreak !== null) { - console.log("从服务器得到连胜", data.data.winStreak); + // console.log("从服务器得到连胜", data.data.winStreak); cc.fx.GameConfig.GM_INFO.winStreak = parseInt(data.data.winStreak); let winState = cc.fx.StorageMessage.getStorage("winState"); - console.log("本地连胜状态", winState); + // console.log("本地连胜状态", winState); let noCache = (winState == null || winState == undefined) && winState != false; if (data.data.forcedUpdate === true || cc.fx.GameConfig.GM_INFO.wucai_type === 1 || noCache) { - console.log("没有缓存进入这里 || 或者强制刷新"); + // console.log("没有缓存进入这里 || 或者强制刷新"); cc.fx.GameConfig.GM_INFO.winState = false; if (cc.fx.GameConfig.GM_INFO.winStreak >= 10) { cc.fx.GameConfig.GM_INFO.winState = true; @@ -448,15 +448,15 @@ export default class GameManager extends cc.Component { cc.fx.StorageMessage.setStorage("winState", cc.fx.GameConfig.GM_INFO.winState); } else { - console.log("以本地为主"); + // console.log("以本地为主"); cc.fx.GameConfig.GM_INFO.winState = winState; - console.log("连胜状态:", cc.fx.GameConfig.GM_INFO.winState); + // console.log("连胜状态:", cc.fx.GameConfig.GM_INFO.winState); if (winState == false) { if (cc.fx.GameConfig.GM_INFO.winStreak >= 10) { cc.fx.GameConfig.GM_INFO.winStreak = 0; Utils.setWinStreak((data) => { if (data.code == 1) { - console.log("连胜状态杀端清零成功"); + // console.log("连胜状态杀端清零成功"); } }) } @@ -474,10 +474,10 @@ export default class GameManager extends cc.Component { else { cc.fx.GameConfig.GM_INFO.winStreak = winStreak; } - console.log("调用连胜上传"); + // console.log("调用连胜上传"); Utils.setWinStreak((data) => { if (data.code == 1) { - console.log("设置连胜成功"); + // console.log("设置连胜成功"); } }) } @@ -498,7 +498,7 @@ export default class GameManager extends cc.Component { } if (data.data.isFirst === true) { - console.log("服务器中严格的新用户,发送注册事件和公共属性"); + // console.log("服务器中严格的新用户,发送注册事件和公共属性"); this.setFirstInfo(); } @@ -715,7 +715,7 @@ export default class GameManager extends cc.Component { return; } if (levelInfo.level > data.levelAmount) { - console.log("本地等级 大于服务器等级才走这里") + // console.log("本地等级 大于服务器等级才走这里") cc.fx.GameConfig.GM_INFO.level = levelInfo.level; cc.fx.GameConfig.GM_INFO.coin = coinInfo.coin; if (propInfo != undefined && propInfo != null) { @@ -731,7 +731,7 @@ export default class GameManager extends cc.Component { const timestamp = Date.now(); if (coinInfo.coin != data.coinAmount) { if ((coinInfo == undefined || coinInfo.coin == null) || data.forcedUpdate === true) { - console.log("没有缓存进入这里 || 或者强制刷新", data.forcedUpdate); + // console.log("没有缓存进入这里 || 或者强制刷新", data.forcedUpdate); cc.fx.GameConfig.GM_INFO.coin = data.coinAmount; let coinInfo = { "coin": cc.fx.GameConfig.GM_INFO.coin, "timestamp": timestamp }; cc.fx.StorageMessage.setStorage("coin", coinInfo); @@ -753,14 +753,14 @@ export default class GameManager extends cc.Component { } if (levelInfo.level != data.levelAmount) { - console.log("本地等级 大于服务器等级才走这里", levelInfo.level, data.levelAmount); + // console.log("本地等级 大于服务器等级才走这里", levelInfo.level, data.levelAmount); if ((levelInfo.level == null || levelInfo == undefined)) { cc.fx.GameConfig.GM_INFO.level = data.levelAmount; let levelInfo = { "level": cc.fx.GameConfig.GM_INFO.level, "timestamp": timestamp }; cc.fx.StorageMessage.setStorage("level", levelInfo); } else { - console.log("往服务器传输等级"); + // console.log("往服务器传输等级"); Utils.setUserLevel((data) => { }) } @@ -807,7 +807,7 @@ export default class GameManager extends cc.Component { cc.fx.GameConfig.GM_INFO.coin = data.coinAmount; cc.fx.StorageMessage.setStorage("prop", propInfo); if (serverLevelIsNewer) { - console.log("服务器关卡高于本地,金币和道具已使用服务器数据覆盖本地缓存", localLevel, data.levelAmount); + // console.log("服务器关卡高于本地,金币和道具已使用服务器数据覆盖本地缓存", localLevel, data.levelAmount); } // console.log("走的强制刷新", cc.fx.GameConfig.GM_INFO.hammerAmount) } @@ -854,7 +854,7 @@ export default class GameManager extends cc.Component { "magicAmount": data.magicAmount, "timestamp": timestamp, } - console.log("不应该走到这里的的-_----------------"); + // console.log("不应该走到这里的的-_----------------"); cc.fx.GameConfig.GM_INFO.freezeAmount = data.freezeAmount; cc.fx.GameConfig.GM_INFO.hammerAmount = data.hammerAmount; cc.fx.GameConfig.GM_INFO.magicAmount = data.magicAmount; @@ -1126,7 +1126,7 @@ export default class GameManager extends cc.Component { //@ts-ignore wx.onAddToFavorites(() => { - console.log("收藏成功"); + // console.log("收藏成功"); MiniGameSdk.API.tencent_ADD_TO_WISHLIST(); return { title: '收藏标题', diff --git a/assets/Script/JiaZai.ts b/assets/Script/JiaZai.ts index 324ebe97..bb0b65ff 100644 --- a/assets/Script/JiaZai.ts +++ b/assets/Script/JiaZai.ts @@ -39,6 +39,8 @@ export default class JiaZai extends cc.Component { if (!event) { return; } + // 新的主动操作取代尚未打开的按需弹窗;资源下载仍正常完成并写入缓存。 + this.homePopupOpenRequest++; try { cc.fx.GameTool.setVibrate("light", 1); } catch (error) { @@ -155,6 +157,10 @@ export default class JiaZai extends cc.Component { private _sRankRequestId: number = 0; private locatePositionAfterRank: boolean = false; private careerLoadGeneration: number = 0; + private homeGameplayWarmupPending = true; + private homeAutomaticTasks: Array<{ name: string; run: () => void; priority: number }> = []; + private homeAutomaticFlushScheduled = false; + private homePopupOpenRequest = 0; // 弹窗倒计时调度器 private heathScheduleCallback: Function = null; @property(cc.Node) @@ -341,7 +347,7 @@ export default class JiaZai extends cc.Component { cc.director.once(cc.Director.EVENT_AFTER_DRAW, releaseBundle); } setTimeout(releaseBundle, 100); - console.log("Career released for memory warning"); + // console.log("Career released for memory warning"); } private showCareerDisabledToast(): void { @@ -377,13 +383,13 @@ export default class JiaZai extends cc.Component { if (audioManager) { audioManager.playSceneMusic("HomeScene"); } - initProvinceLocator(); + this.queueHomeAutomaticTask("省份定位", () => initProvinceLocator()); // this.RankNode = null; this.isShow = true; this.shareIng = false; this.shareTime = 0; - // 先让 HomeScene 完成首帧,再开始游戏资源预热。 - this.scheduleOnce(() => this.startHomeGameplayWarmup(), 0.5); + // 首帧后先准备进关资源;首页非必要的自动任务尚不发出资源请求。 + this.scheduleOnce(() => this.startHomeGameplayWarmup(), 0); this.node.getChildByName("Snow").active = false; this.node.getChildByName("Career").active = !this.isCareerDisabledByMemoryWarning(); this.node.getChildByName("Snow").zIndex = 999; @@ -405,7 +411,7 @@ export default class JiaZai extends cc.Component { this.getShareInfo(); this.checkShare(); - this.closeAvatar(); + this.queueHomeAutomaticTask("首页头像", () => this.closeAvatar()); this.setShareInfo(); this.checkTasks(); if (!cc.fx.GameTool.maxLevel()) { @@ -417,13 +423,13 @@ export default class JiaZai extends cc.Component { this.checkAndSetPlayerPassLevel(); // this.checkDailyQuests(); // console.log("进入首页获取的share", cc.fx.GameConfig.GM_INFO.otherUid, cc.fx.GameConfig.GM_INFO.otherLevel); - console.log("是否是ios:", cc.fx.GameConfig.GM_INFO.canIos); + // console.log("是否是ios:", cc.fx.GameConfig.GM_INFO.canIos); const homeTop = this.node.getChildByName("Load").getChildByName("Top"); homeTop.getChildByName("shop").active = cc.fx.GameConfig.GM_INFO.canIos; const jungleButton = homeTop.getChildByName("jungle"); if (jungleButton) { - this.initializeJungleTreasureEntryFromLogin(); + this.queueHomeAutomaticTask("丛林宝藏初始化", () => this.initializeJungleTreasureEntryFromLogin(), 1); } @@ -433,7 +439,7 @@ export default class JiaZai extends cc.Component { this.newbieGift = false; this.setStarterPackHomeButtonVisible(false); this.winStreakShow = false; - this.checkStarter_pack(); + this.queueHomeAutomaticTask("新手礼包检查", () => this.checkStarter_pack(), 1); // Utils.getShopDouble(res => { // if (res.code === 1) { @@ -476,117 +482,10 @@ export default class JiaZai extends cc.Component { this.node.getChildByName("Load").getChildByName("New EditBox").active = true; } - // 预加载 活动 预制体 - if (!JiaZai.cachedActionPrefab) { - cc.assetManager.loadBundle('action_bundle', (err: Error, bundle: cc.AssetManager.Bundle) => { - if (err) { - cc.error(err.message || err); - return; - } - bundle.load('prefab/newbieGift', cc.Prefab, (err: Error, prefab: cc.Prefab) => { - if (err) { - cc.error(err.message || err); - return; - } - JiaZai.cachedActionPrefab = prefab; - }); - bundle.load('prefab/Avatar', cc.Prefab, (err: Error, prefab: cc.Prefab) => { - if (err) { - cc.error(err.message || err); - return; - } - JiaZai.cachedAvatarPrefab = prefab; - // console.log("头像预制体已经准备好"); - }); - bundle.load('prefab/dailyQuests', cc.Prefab, (err: Error, prefab: cc.Prefab) => { - if (err) { - cc.error(err.message || err); - return; - } - JiaZai.cachedDailyQuestsPrefab = prefab; - // console.log("每日任务预制体已经准备好"); - }); - bundle.load('prefab/winStreak', cc.Prefab, (err: Error, prefab: cc.Prefab) => { - if (err) { - cc.error(err.message || err); - return; - } - JiaZai.cachedWinStreakPrefab = prefab; - // console.log("连胜活动预制体已经准备好"); - }); - bundle.load('prefab/Share', cc.Prefab, (err: Error, prefab: cc.Prefab) => { - if (err) { - cc.error(err.message || err); - return; - } - JiaZai.cachedSharePrefab = prefab; - // console.log("分享预制体已经准备好"); - }); - - - }); - } - // 预加载 shop 预制体 - if (!JiaZai.cachedShopPrefab) { - cc.assetManager.loadBundle('shop', (err: Error, bundle: cc.AssetManager.Bundle) => { - if (err) { - cc.error(err.message || err); - return; - } - bundle.load('prefab/shop', cc.Prefab, (err: Error, prefab: cc.Prefab) => { - if (err) { - cc.error(err.message || err); - return; - } - JiaZai.cachedShopPrefab = prefab; - }); - bundle.load('prefab/RewardWindow', cc.Prefab, (err: Error, prefab: cc.Prefab) => { - if (err) { - cc.error(err.message || err); - return; - } - JiaZai.cachedRewardPrefab = prefab; - this.getOrder(); - }); - bundle.load('prefab/monthlyCard', cc.Prefab, (err: Error, prefab: cc.Prefab) => { - if (err) { - cc.error(err.message || err); - return; - } - JiaZai.cachedMonthlyCardPrefab = prefab; - if (!this.node || !cc.isValid(this.node)) { - return; - } - let top = this.node.getChildByName("Load").getChildByName("Top"); - //等级大于16开启 - if (cc.fx.GameConfig.GM_INFO.level >= 16 && cc.fx.GameConfig.GM_INFO.canIos) { - top.getChildByName("yicon").active = true; - this.monthH(); - } - else { - top.getChildByName("yicon").active = false; - } - }); - bundle.load('prefab/passCheck', cc.Prefab, (err: Error, prefab: cc.Prefab) => { - if (err) { - cc.error(err.message || err); - return; - } - JiaZai.cachedPassCheckPrefab = prefab; - - }); - - }); - } - else { - if (JiaZai.cachedRewardPrefab) { - this.getOrder(); - } - } - + this.preloadHomePopupPrefabs(); if (cc.fx.GameConfig.GM_INFO.otherLevel > 0) { - this.getSRank(false); + this.queueHomeAutomaticTask("首页排行榜", () => this.getSRank(false)); if (cc.fx.GameConfig.GM_INFO.first) { this.setFirstMusic(); cc.fx.GameConfig.GM_INFO.first = false; @@ -608,26 +507,24 @@ export default class JiaZai extends cc.Component { else { if (cc.fx.GameConfig.GM_INFO.first && cc.fx.GameConfig.GM_INFO.level == 0) { this.isShow = false; - this.getSRank(false); + this.queueHomeAutomaticTask("首页排行榜", () => this.getSRank(false)); //console.log("————————准备注册事件", cc.fx.GameConfig.GM_INFO.openid); this.setFirstMusic(); - cc.fx.AudioManager._instance.playEffect("zhuan1", null); - this.node.getChildByName("zhuanchang").active = true; - this.node.getChildByName("zhuanchang").getComponent(sp.Skeleton).setAnimation(1, "up", false); + this.playGameEntryTransition(); setTimeout(() => { cc.fx.GameConfig.GM_INFO.first = false; this.waitAndEnterGameScene(); - }, 1000); + }, cc.fx.GameConfig.shouldPrepareFirstGameplayEntry() ? 0 : 1000); } else { - this.getSRank(true); + this.queueHomeAutomaticTask("首页排行榜", () => this.getSRank(true)); } } //console.log("音乐开关", cc.fx.GameConfig.GM_INFO.musicOpen); // AudioManager._instance.playMusicGame(); // 从房间出来判断是否弹出通行证 - this.popUpPassCheck(); + this.queueHomeAutomaticTask("通行证自动弹窗", () => this.popUpPassCheck(), 1); this.onGames(); @@ -636,25 +533,197 @@ export default class JiaZai extends cc.Component { // }); } + private isHomeRuntimeAlive(): boolean { + const scene = cc.director.getScene(); + return cc.isValid(this, true) && !!this.node && cc.isValid(this.node, true) + && !!scene && scene.name === "HomeScene"; + } + + /** 仅用于首页自动任务的起点,不包装 loadBundle/load 的完成回调。 */ + private queueHomeAutomaticTask(name: string, run: () => void, priority = 0): void { + if (!this.isHomeRuntimeAlive()) return; + // 只调整尚未启动的任务;同优先级维持原顺序,不打断在途加载。 + const task = { name, run, priority }; + const index = this.homeAutomaticTasks.findIndex(item => item.priority < priority); + if (index < 0) this.homeAutomaticTasks.push(task); + else this.homeAutomaticTasks.splice(index, 0, task); + this.flushHomeAutomaticTasks(); + } + + private flushHomeAutomaticTasks(): void { + if (!this.isHomeRuntimeAlive() || this.homeGameplayWarmupPending + || this.homeAutomaticFlushScheduled || !this.homeAutomaticTasks.length) return; + this.homeAutomaticFlushScheduled = true; + this.scheduleOnce(() => { + this.homeAutomaticFlushScheduled = false; + if (!this.isHomeRuntimeAlive() || this.homeGameplayWarmupPending) return; + if (!cc.fx.GameConfig.isGameplayEntryPreparing()) { + const task = this.homeAutomaticTasks.shift(); + // console.log(`[首页加载顺序] 启动自动任务:${task.name}`); + try { task.run(); } + catch (error) { cc.warn(`首页自动任务失败:${task.name}`, error); } + } + this.flushHomeAutomaticTasks(); + }, cc.fx.GameConfig.isGameplayEntryPreparing() ? 0.1 : 0); + } + + private preloadHomePopupPrefabs(): void { + const entries: Array<[string, string, string, number]> = [ + // 第二阶段先准备补发窗口及其图片,成功后才允许开始补发。 + ["shop", "prefab/RewardWindow", "cachedRewardPrefab", 2], + ["action_bundle", "prefab/newbieGift", "cachedActionPrefab", 1], + ["shop", "prefab/monthlyCard", "cachedMonthlyCardPrefab", 1], + ["shop", "prefab/passCheck", "cachedPassCheckPrefab", 1], + ["shop", "prefab/shop", "cachedShopPrefab", 1], + ["action_bundle", "prefab/Avatar", "cachedAvatarPrefab", 0], + ["action_bundle", "prefab/dailyQuests", "cachedDailyQuestsPrefab", 0], + ["action_bundle", "prefab/winStreak", "cachedWinStreakPrefab", 0], + ["action_bundle", "prefab/Share", "cachedSharePrefab", 0], + ]; + for (const [bundleName, path, cacheKey, priority] of entries) { + this.queueHomeAutomaticTask(path, () => { + // 自动付费资源仍低于玩家点击(5)和进关资源(10)。 + this.loadHomePopupPrefab(bundleName, path, cacheKey, priority || -10).then(() => { + if (!this.isHomeRuntimeAlive()) return; + if (cacheKey === "cachedRewardPrefab") { + this.queueHomeAutomaticTask("订单补发检查", () => this.getOrder(), 2); + } else if (cacheKey === "cachedMonthlyCardPrefab") { + this.queueHomeAutomaticTask("月卡检查", () => { + if (cc.fx.GameConfig.GM_INFO.level >= 16 && cc.fx.GameConfig.GM_INFO.canIos) { + this.monthH(); + } + }, 1); + } + }, error => cc.warn(`首页预加载失败:${path}`, error)); + }, priority); + } + } + + private isHomeBundleReady(bundleName: string, visited: string[] = []): boolean { + const bundle = cc.assetManager.getBundle(bundleName); + if (!bundle) return false; + if (visited.indexOf(bundleName) >= 0) return true; + const next = visited.concat(bundleName); + return (bundle.deps || []).every(name => this.isHomeBundleReady(name, next)); + } + + /** loadBundle 只注册资源包;解析 Prefab 前必须等实际声明的跨包依赖就绪。 */ + private loadHomeBundleWithDependencies(bundleName: string, priority: number, + callback: (error: Error, bundle?: cc.AssetManager.Bundle) => void, visited: string[] = []): void { + let settled = false; + const finish = (error: Error, bundle?: cc.AssetManager.Bundle) => { + if (settled) return; + settled = true; + callback(error, bundle); + }; + const onBundle = (error: Error, bundle: cc.AssetManager.Bundle) => { + if (error || !bundle) { finish(error || new Error(`分包为空:${bundleName}`)); return; } + const next = visited.concat(bundleName); + const dependencies = (bundle.deps || []).filter(name => next.indexOf(name) < 0); + let remaining = dependencies.length; + const ready = () => { + if (cc.assetManager.getBundle(bundleName) !== bundle + || (!visited.length && !this.isHomeBundleReady(bundleName))) { + finish(new Error(`分包或依赖已被释放:${bundleName}`)); + return; + } + finish(null, bundle); + }; + if (!remaining) { ready(); return; } + dependencies.forEach(name => { + if (settled) return; + this.loadHomeBundleWithDependencies(name, priority, error => { + if (error) { finish(error); return; } + if (--remaining === 0) ready(); + }, next); + }); + }; + try { + const loaded = cc.assetManager.getBundle(bundleName); + if (loaded) onBundle(null, loaded); + else cc.assetManager.loadBundle(bundleName, { priority }, onBundle); + } catch (error) { + if (settled) throw error; + finish(error); + } + } + + private loadHomePopupPrefab(bundleName: string, path: string, cacheKey: string, + priority: number): Promise { + const cached: cc.Prefab = JiaZai[cacheKey]; + if (cached && cc.isValid(cached, true) && this.isHomeBundleReady(bundleName)) { + return Promise.resolve(cached); + } + return new Promise((resolve, reject) => { + this.loadHomeBundleWithDependencies(bundleName, priority, (error: Error, bundle: cc.AssetManager.Bundle) => { + if (error || !bundle) { reject(error || new Error(`分包为空:${bundleName}`)); return; } + cc.assetManager.loadAny({ path, type: cc.Prefab, bundle: bundleName }, { priority }, (error: Error, prefab: cc.Prefab) => { + if (error || !prefab || !cc.isValid(prefab, true) + || cc.assetManager.getBundle(bundleName) !== bundle || !this.isHomeBundleReady(bundleName)) { + reject(error || new Error(`预制体已失效:${bundleName}/${path}`)); + return; + } + // 即使首页已销毁,也完成资源缓存;只有后续 UI 操作检查场景存活。 + JiaZai[cacheKey] = prefab; + resolve(prefab); + }); + }); + }); + } + + /** 主动打开不等待自动队列;加载期间不锁住首页,也不操作进关的 Loading。 */ + private ensureHomePopupPrefab(bundleName: string, path: string, cacheKey: string, open: () => void): boolean { + if (!this.isHomeRuntimeAlive() || cc.fx.GameConfig.isGameplayEntryPreparing()) return false; + const requestId = ++this.homePopupOpenRequest; + const cached: cc.Prefab = JiaZai[cacheKey]; + if (cached && cc.isValid(cached, true) && this.isHomeBundleReady(bundleName)) return true; + // console.log(`[首页按需加载] 开始:${bundleName}/${path}`); + MiniGameSdk.API.showToast("界面资源加载中,请稍候"); + const canOpen = () => this.isHomeRuntimeAlive() && requestId === this.homePopupOpenRequest + && !cc.fx.GameConfig.isGameplayEntryPreparing(); + this.loadHomePopupPrefab(bundleName, path, cacheKey, 5).then(() => { + // console.log(`[首页按需加载] 完成:${bundleName}/${path}`); + if (canOpen()) open(); + }, error => { + cc.warn(`[首页按需加载] 失败:${bundleName}/${path}`, error); + if (canOpen()) MiniGameSdk.API.showToast("界面资源加载失败,请再次点击重试"); + }); + return false; + } + private startHomeGameplayWarmup(): void { if (!this.node || !cc.isValid(this.node, true)) return; const scene = cc.director.getScene(); if (!scene || scene.name !== "HomeScene") return; - cc.fx.GameTool.predownloadGameplayPopupSubpackages(); - cc.fx.GameConfig.warmupHomeBlockAssets().then(() => { - if (this.node && cc.isValid(this.node, true) && cc.director.getScene() === scene) { - return cc.fx.GameTool.warmupHomeGameplayPopupAssets(); - } - return false; - }, error => { + const config = cc.fx.GameConfig; + const finish = () => { + if (!this.isHomeRuntimeAlive() || !this.homeGameplayWarmupPending) return; + this.homeGameplayWarmupPending = false; + this.unschedule(fallback); + // console.log("[首页加载顺序] 进关预热阶段结束,允许首页自动任务逐帧启动"); + this.flushHomeAutomaticTasks(); + }; + // 只解除首页自动初始化的等待,不取消预热、不放宽正式进关的资源检查。 + const fallback = () => { + cc.warn("[首页加载顺序] 预热超过15秒,首页自动功能不再持续等待"); + finish(); + }; + // console.log("[首页加载顺序] 优先准备当前关资源,暂不启动首页自动预加载"); + this.scheduleOnce(fallback, 15); + // 独立启动,场景预取不再排在方块预热之后。 + const jobs = [ + config.timeGameplayStage("首页五个分包磁盘预下载", () => cc.fx.GameTool.predownloadGameplayPopupSubpackages()), + config.timeGameplayStage("首页当前关图片预热", () => config.warmupHomeBlockAssets()), + cc.fx.GameTool.shouldPreloadGameplayPopupInstances() + ? config.preloadGameplayScene() + // 低内存 iOS 仍只提前 loadBundle(Pause),不加载 Prefab 或 GameScene。 + : config.timeGameplayStage("首页Pause Bundle预热", () => cc.fx.GameTool.warmupHomeGameplayPopupAssets()), + ]; + Promise.all(jobs.map(job => job.catch(error => { cc.warn("HomeScene gameplay warmup failed:", error); - return false; - }).then(() => { - BlockAssetManager.instance.startBackgroundCache(); - }, error => { - // 弹窗预热失败不能阻断全量图片的后台磁盘缓存;进关与开窗仍有各自的重试兜底。 - cc.warn("HomeScene popup warmup failed:", error); + }))).then(() => { + finish(); BlockAssetManager.instance.startBackgroundCache(); }); } @@ -768,6 +837,8 @@ export default class JiaZai extends cc.Component { } onDestroy() { + this.homeAutomaticTasks = []; + this.homePopupOpenRequest++; this._sRankRequestId++; this.careerLoadGeneration++; this.unregisterCareerMemoryWarning(); @@ -892,7 +963,7 @@ export default class JiaZai extends cc.Component { kuandu = 35; } - console.log("寬度", kuandu); + // console.log("寬度", kuandu); NumberToImage.numberToImageNodesShop((cc.fx.GameConfig.GM_INFO.level + 1), kuandu, 15, "custom", this.level, true); if (this.level.children) { for (let i = 0; i < this.level.children.length; i++) { @@ -930,7 +1001,7 @@ export default class JiaZai extends cc.Component { this.uploadToCloud(cc.fx.GameConfig.GM_INFO.level + 1); // this.getMonthlyCardValidityDays(); - this.rewarded(); + this.queueHomeAutomaticTask("月卡奖励检查", () => this.rewarded(), 1); //判断过期 cc.fx.GameTool.checkExpiration(); @@ -953,7 +1024,7 @@ export default class JiaZai extends cc.Component { if (cc.fx.GameConfig.GM_INFO.level >= 16 && cc.fx.GameConfig.GM_INFO.canIos) { top.getChildByName("yicon").active = true; if (JiaZai.cachedMonthlyCardPrefab) { - this.monthH(); + this.queueHomeAutomaticTask("已缓存月卡检查", () => this.monthH(), 1); } } else { @@ -1117,7 +1188,7 @@ export default class JiaZai extends cc.Component { // 触摸开始 - 记录状态,真正移动时才渐隐 // ============================================ public _onTopFadeTouchStart(event?: cc.Event.EventTouch) { - console.log("触摸开始"); + // console.log("触摸开始"); this._topFadeTouching = true; } @@ -1127,10 +1198,10 @@ export default class JiaZai extends cc.Component { public _onTopFadeTouchMove(event?: cc.Event.EventTouch) { if (!this._topFadeTouching) { // END/CANCEL 后可能还有迟到的 MOVE,松手状态不能被它重新激活。 - console.log("已松手,忽略迟到的触摸移动"); + // console.log("已松手,忽略迟到的触摸移动"); return; } - console.log("触摸移动中"); + // console.log("触摸移动中"); if (!this._topFadeActive) { this._playTopFadeOut(); } @@ -1140,7 +1211,7 @@ export default class JiaZai extends cc.Component { // 触摸结束/取消 - 触发渐显 // ============================================ public _onTopFadeTouchEnd(event?: cc.Event.EventTouch) { - console.log("触摸结束/取消"); + // console.log("触摸结束/取消"); this._topFadeTouching = false; this._playTopFadeIn(); } @@ -1338,8 +1409,8 @@ export default class JiaZai extends cc.Component { Utils.getAvatarWithRetry(res => { if (res.code === 1) { if (!JiaZai.cachedAvatarPrefab) { - console.log('Avatar prefab is not loaded yet.'); - cc.assetManager.loadBundle('action_bundle', (err: Error, bundle: cc.AssetManager.Bundle) => { + // console.log('Avatar prefab is not loaded yet.'); + this.loadHomeBundleWithDependencies("action_bundle", 5, (err: Error, bundle: cc.AssetManager.Bundle) => { if (err) { cc.error(err.message || err); return; @@ -1708,7 +1779,7 @@ export default class JiaZai extends cc.Component { KVDataList: [ { key: 'level', value: String(level) } ], - success: () => console.log('✅ 数据上传成功'), + success: () => { /* console.log('✅ 数据上传成功') */ }, fail: (err) => console.error('❌ 上传失败', err) }); } @@ -1833,11 +1904,18 @@ export default class JiaZai extends cc.Component { // 5. 体力足够则加载并切换场景 // 6. 体力不足则弹出体力不足提示 // ============================================ + playGameEntryTransition(force: boolean = false): void { + if (!force && cc.fx.GameConfig.shouldPrepareFirstGameplayEntry()) { + this.openLoad(); + return; + } + cc.fx.AudioManager._instance.playEffect("zhuan1", null); + this.node.getChildByName("zhuanchang").active = true; + this.node.getChildByName("zhuanchang").getComponent(sp.Skeleton).setAnimation(1, "up", false); + } + startGame(event?: cc.Event) { this.vibrateButtonClick(event); - if (cc.fx.GameConfig.GM_INFO.otherLevel == 0) { - cc.fx.GameConfig.GM_INFO.allOutTradeNo = []; - } cc.fx.AudioManager._instance.playEffect("anniu_Big", null); let version = cc.fx.GameTool.getWechatGameVersion(); @@ -1874,27 +1952,23 @@ export default class JiaZai extends cc.Component { cc.fx.GameConfig.LEVEL_INFO_init(true, 0, false); } else { - cc.fx.AudioManager._instance.playEffect("zhuan1", null); - this.node.getChildByName("zhuanchang").active = true; - this.node.getChildByName("zhuanchang").getComponent(sp.Skeleton).setAnimation(1, "up", false); + this.playGameEntryTransition(); // cc.fx.GameConfig.GM_INFO.randomLevel = cc.fx.GameTool.getNextLevel() || 0; let rLevel = cc.fx.GameConfig.GM_INFO.randomLevel; cc.fx.StorageMessage.setStorage("randomLevel", rLevel); - console.log("保存玩家随机关卡", rLevel, cc.fx.GameConfig.GM_INFO.endLevelNum) + // console.log("保存玩家随机关卡", rLevel, cc.fx.GameConfig.GM_INFO.endLevelNum) Utils.setEndlessLevel(() => { }, rLevel, cc.fx.GameConfig.GM_INFO.endLevelNum); cc.fx.GameConfig.LEVEL_INFO_init(true, 1000, false, true); } } else { - cc.fx.AudioManager._instance.playEffect("zhuan1", null); - this.node.getChildByName("zhuanchang").active = true; - this.node.getChildByName("zhuanchang").getComponent(sp.Skeleton).setAnimation(1, "up", false); + this.playGameEntryTransition(); // cc.fx.GameConfig.GM_INFO.randomLevel = cc.fx.GameTool.getNextLevel() || 0; let rLevel = cc.fx.GameConfig.GM_INFO.randomLevel; cc.fx.StorageMessage.setStorage("randomLevel", rLevel); - console.log("保存玩家随机关卡", rLevel, cc.fx.GameConfig.GM_INFO.endLevelNum) + // console.log("保存玩家随机关卡", rLevel, cc.fx.GameConfig.GM_INFO.endLevelNum) Utils.setEndlessLevel(() => { }, rLevel, cc.fx.GameConfig.GM_INFO.endLevelNum); cc.fx.GameConfig.LEVEL_INFO_init(true, 1000, false, true); @@ -1920,9 +1994,7 @@ export default class JiaZai extends cc.Component { cc.fx.GameConfig.LEVEL_INFO_init(true, 0, false); } else { - cc.fx.AudioManager._instance.playEffect("zhuan1", null); - this.node.getChildByName("zhuanchang").active = true; - this.node.getChildByName("zhuanchang").getComponent(sp.Skeleton).setAnimation(1, "up", false); + this.playGameEntryTransition(); if (cc.fx.GameTool.maxLevel()) { MiniGameSdk.API.showToast("关卡每周更新,敬请期待!"); @@ -1939,9 +2011,7 @@ export default class JiaZai extends cc.Component { cc.fx.GameTool.requestSubscribe(); return; } - cc.fx.AudioManager._instance.playEffect("zhuan1", null); - this.node.getChildByName("zhuanchang").active = true; - this.node.getChildByName("zhuanchang").getComponent(sp.Skeleton).setAnimation(1, "up", false); + this.playGameEntryTransition(); this.isShow = false; @@ -1961,9 +2031,7 @@ export default class JiaZai extends cc.Component { cc.fx.GameConfig.LEVEL_INFO_init(true, 0, false); } else { - cc.fx.AudioManager._instance.playEffect("zhuan1", null); - this.node.getChildByName("zhuanchang").active = true; - this.node.getChildByName("zhuanchang").getComponent(sp.Skeleton).setAnimation(1, "up", false); + this.playGameEntryTransition(); if (cc.fx.GameTool.maxLevel()) { MiniGameSdk.API.showToast("关卡每周更新,敬请期待!"); @@ -1994,6 +2062,8 @@ export default class JiaZai extends cc.Component { if (!cc.fx.GameConfig.GM_INFO.canIos) { return; } + if (!this.ensureHomePopupPrefab("shop", "prefab/shop", "cachedShopPrefab", + () => this.openShop(undefined, customEventData))) return; let double = { gold_1: false, gold_2: false, @@ -2017,12 +2087,9 @@ export default class JiaZai extends cc.Component { if (!cc.fx.GameConfig.GM_INFO.canIos) { return; } - if (!JiaZai.cachedShopPrefab) { - cc.error('Shop prefab is not loaded yet.'); - return; - } Utils.getShopDouble(res => { + if (!this.isHomeRuntimeAlive() || cc.fx.GameConfig.isGameplayEntryPreparing()) return; if (res.code === 1) { cc.fx.GameConfig.GM_INFO.shopDouble = res.shopDouble; let eventName = ""; @@ -2041,9 +2108,9 @@ export default class JiaZai extends cc.Component { entry_point: btnName, // 按钮名称 } cc.fx.GameTool.shushu_Track("interface_exposure", dataInfo); - console.log("统计功能", dataInfo); + // console.log("统计功能", dataInfo); cc.fx.GameConfig.GM_INFO.shopDouble = res.data.shopDouble; - console.log("商店双倍", res.data.shopDouble); + // console.log("商店双倍", res.data.shopDouble); if (!this.shopNode) { // 第一次使用,创建节点 this.shopNode = cc.instantiate(JiaZai.cachedShopPrefab); @@ -2069,10 +2136,8 @@ export default class JiaZai extends cc.Component { //打开奖励弹窗 openRewardWindow(data, month?: number) { - if (!JiaZai.cachedRewardPrefab) { - cc.error('Reward prefab is not loaded yet.'); - return; - } + if (!this.ensureHomePopupPrefab("shop", "prefab/RewardWindow", "cachedRewardPrefab", + () => this.openRewardWindow(data, month))) return; if (!this.RewardNode) { // 第一次使用,创建节点 this.RewardNode = cc.instantiate(JiaZai.cachedRewardPrefab); @@ -2093,11 +2158,8 @@ export default class JiaZai extends cc.Component { } //打开月卡 openMonthlyCard(event?: cc.Event, customEventData?: string) { - if (!JiaZai.cachedMonthlyCardPrefab) { - cc.error('MonthlyCard prefab is not loaded yet.'); - console.log("月卡弹窗未加载"); - return; - } + if (!this.ensureHomePopupPrefab("shop", "prefab/monthlyCard", "cachedMonthlyCardPrefab", + () => this.openMonthlyCard(undefined, customEventData))) return; let eventName = ""; let btnName = "" @@ -2115,7 +2177,7 @@ export default class JiaZai extends cc.Component { entry_point: btnName, // 按钮名称 } cc.fx.GameTool.shushu_Track("interface_exposure", dataInfo); - console.log("统计功能", dataInfo); + // console.log("统计功能", dataInfo); // 弹窗缩放动画(放大回弹效果) - 三次回弹版 const playPopAnim = (node: cc.Node) => { @@ -2172,7 +2234,7 @@ export default class JiaZai extends cc.Component { cc.fx.GameConfig.GM_INFO.tasks.share.value += 1; cc.fx.GameTool.setDailyQuestInfo((data) => { if (data.code == 1) { - console.log("网络正常"); + // console.log("网络正常"); this.openDailyQuests(null, null, true); if (cc.fx.GameConfig.GM_INFO.tasks.share.value == cc.fx.GameConfig.GM_INFO.tasks.share.target) { let data = { @@ -2231,7 +2293,7 @@ export default class JiaZai extends cc.Component { } if (!JiaZai.cachedDailyQuestsPrefab) { // cc.error('Action prefab is not loaded yet.'); - cc.assetManager.loadBundle('action_bundle', (err: Error, bundle: cc.AssetManager.Bundle) => { + this.loadHomeBundleWithDependencies("action_bundle", 5, (err: Error, bundle: cc.AssetManager.Bundle) => { if (err) { cc.error(err.message || err); return; @@ -2482,6 +2544,8 @@ export default class JiaZai extends cc.Component { if (this.deferHomePopupWhileTransfer("order", () => this.getOrder())) { return; } + // 发货回调继续处理订单,但不能操作已销毁的首页或关闭进关的 Loading。 + const canUpdateHome = () => this.isHomeRuntimeAlive() && !cc.fx.GameConfig.isGameplayEntryPreparing(); //@ts-ignore if ((typeof wx !== 'undefined' && wx !== null) || (typeof tt !== 'undefined' && tt !== null)) { // 判断设备系统 @@ -2505,36 +2569,36 @@ export default class JiaZai extends cc.Component { if (cc.fx.GameConfig.GM_INFO.allOutTradeNo) { Utils.getShopDouble(res => { if (res.code === 1) { - console.log("获取到的shopDouble", res.data.shopDouble); + // console.log("获取到的shopDouble", res.data.shopDouble); cc.fx.GameConfig.GM_INFO.shopDouble = res.data.shopDouble; if (cc.fx.GameConfig.GM_INFO.allOutTradeNo.length != 0) { - console.log("有需要补发数据", cc.fx.GameConfig.GM_INFO.allOutTradeNo); + // console.log("有需要补发数据", cc.fx.GameConfig.GM_INFO.allOutTradeNo); // 递归函数,按顺序处理每个订单 const processOrder = (index: number) => { if (index >= cc.fx.GameConfig.GM_INFO.allOutTradeNo.length) { // 所有订单处理完成,清空数组 cc.fx.GameConfig.GM_INFO.allOutTradeNo = []; - console.log("补发逻辑全部完成,已清空allOutTradeNo数组"); + // console.log("补发逻辑全部完成,已清空allOutTradeNo数组"); return; // 所有订单处理完成 } let order = cc.fx.GameConfig.GM_INFO.allOutTradeNo[index]; - console.log("需要补发数据", order); + // console.log("需要补发数据", order); if (order.itemid == "gold_1" || order.itemid == "gold_2" || order.itemid == "gold_3" || order.itemid == "gold_4" || order.itemid == "gold_5" || order.itemid == "gold_6" || order.itemid == "unlimited_health_bundle_10" || order.itemid == "unlimited_health_bundle_20" - || order.itemid == "unlimited_health_bundle_30" || order.itemid == "month_Card" || "reborn_Gift" + || order.itemid == "unlimited_health_bundle_30" || order.itemid == "month_Card" || order.itemid == "reborn_Gift" || order.itemid == "starter_pack" || order.itemid == "battlepass" ) { - this.openLoad(); - console.log("补发名称:", order.itemid); + if (canUpdateHome()) this.openLoad(); + // console.log("补发名称:", order.itemid); let productId = order.itemid; Utils.setPayInfo( (res) => { - console.log("设置轮训结果:", res); - this.closeLoad(); + // console.log("设置轮训结果:", res); + if (canUpdateHome()) this.closeLoad(); if (res.code === 1) { - console.log("7.14_________正式发货"); + // console.log("7.14_________正式发货"); let coinTemp = 0; if (order.itemid == "gold_1") { coinTemp = 1200; } else if (order.itemid == "gold_2") { coinTemp = 8000; } @@ -2550,7 +2614,7 @@ export default class JiaZai extends cc.Component { cc.fx.GameConfig.GM_INFO.doubleCoin = 2; cc.fx.GameConfig.GM_INFO.hp_Max = 7; cc.fx.GameConfig.GM_INFO.hp = 7; - this.openLoad(); + if (canUpdateHome()) this.openLoad(); this.buyMonthCard(productId, order.outTradeNo); } else if (order.itemid == "reborn_Gift") { @@ -2569,7 +2633,7 @@ export default class JiaZai extends cc.Component { // MiniGameSdk.API.showToast(title); if (order.itemid != "month_Card") cc.fx.GameTool.shopBuy(productId, true); - console.log("充值成功获得金币"); + // console.log("充值成功获得金币"); let price = parseInt(order.goodsPrice) || 0; // const dataSuccess = { // outTradeNo: order.outTradeNo, @@ -2596,8 +2660,10 @@ export default class JiaZai extends cc.Component { } cc.fx.GameTool.shushu_Track("payment_fail", dataFail4); } - NumberToImage.numberToImageNodes5(cc.fx.GameConfig.GM_INFO.coin, 30, 15, "Black", this.coin, true); - this.updateCoin(); + if (canUpdateHome()) { + NumberToImage.numberToImageNodes5(cc.fx.GameConfig.GM_INFO.coin, 30, 15, "Black", this.coin, true); + this.updateCoin(); + } // 处理下一个订单 processOrder(index + 1); }, order.outTradeNo); @@ -2751,6 +2817,7 @@ export default class JiaZai extends cc.Component { // ============================================ // 打开通行证弹窗 openStarter_pack(event?: cc.Event, customEventData?: string) { + if (!this.isHomeRuntimeAlive() || cc.fx.GameConfig.isGameplayEntryPreparing()) return; this.vibrateButtonClick(event); if (!JiaZai.cachedActionPrefab) { @@ -2772,17 +2839,19 @@ export default class JiaZai extends cc.Component { cc.fx.GameTool.shushu_Track("interface_exposure", dataInfo); // cc.error('Action prefab is not loaded yet.'); - cc.assetManager.loadBundle('action_bundle', (err: Error, bundle: cc.AssetManager.Bundle) => { + this.loadHomeBundleWithDependencies("action_bundle", 5, (err: Error, bundle: cc.AssetManager.Bundle) => { if (err) { cc.error(err.message || err); return; } + if (!this.isHomeRuntimeAlive() || cc.fx.GameConfig.isGameplayEntryPreparing()) return; bundle.load('prefab/newbieGift', cc.Prefab, (err: Error, prefab: cc.Prefab) => { if (err) { cc.error(err.message || err); return; } JiaZai.cachedActionPrefab = prefab; + if (!this.isHomeRuntimeAlive() || cc.fx.GameConfig.isGameplayEntryPreparing()) return; if (!this.actionpNode) { // 第一次使用,创建节点 this.actionpNode = cc.instantiate(JiaZai.cachedActionPrefab); @@ -3280,6 +3349,7 @@ export default class JiaZai extends cc.Component { /** 预加载丛林宝藏分包;点击发生在加载过程中时,加载完成后自动打开。 */ private loadJungleTreasurePrefab(openAfterLoad: boolean = false) { + if (!this.isHomeRuntimeAlive() || cc.fx.GameConfig.isGameplayEntryPreparing()) return; if (openAfterLoad) { this.jungleTreasureOpenAfterLoad = true; } @@ -3287,7 +3357,8 @@ export default class JiaZai extends cc.Component { this.requestJungleTreasureData(this.jungleTreasureOpenAfterLoad); return; } - if (JiaZai.cachedJungleTreasurePrefab) { + if (JiaZai.cachedJungleTreasurePrefab && cc.isValid(JiaZai.cachedJungleTreasurePrefab, true) + && this.isHomeBundleReady("jungle_treasure")) { if (this.jungleTreasureOpenAfterLoad) { this.jungleTreasureOpenAfterLoad = false; this.showJungleTreasure(); @@ -3299,7 +3370,12 @@ export default class JiaZai extends cc.Component { } this.jungleTreasureLoading = true; - cc.assetManager.loadBundle("jungle_treasure", (bundleError: Error, bundle: cc.AssetManager.Bundle) => { + this.loadHomeBundleWithDependencies("jungle_treasure", 5, (bundleError: Error, bundle: cc.AssetManager.Bundle) => { + if (!this.isHomeRuntimeAlive() || cc.fx.GameConfig.isGameplayEntryPreparing()) { + this.jungleTreasureLoading = false; + this.jungleTreasureOpenAfterLoad = false; + return; + } if (bundleError || !bundle) { this.jungleTreasureLoading = false; this.jungleTreasureOpenAfterLoad = false; @@ -3309,14 +3385,19 @@ export default class JiaZai extends cc.Component { } bundle.load("prefab/jungle", cc.Prefab, (prefabError: Error, prefab: cc.Prefab) => { this.jungleTreasureLoading = false; - if (prefabError || !prefab) { + if (prefabError || !prefab || !cc.isValid(prefab, true) + || cc.assetManager.getBundle("jungle_treasure") !== bundle || !this.isHomeBundleReady("jungle_treasure")) { this.jungleTreasureOpenAfterLoad = false; - this.closeLoad(); + if (this.isHomeRuntimeAlive() && !cc.fx.GameConfig.isGameplayEntryPreparing()) this.closeLoad(); cc.error("加载 jungle_treasure/prefab/jungle 失败:", prefabError && (prefabError.message || prefabError)); return; } JiaZai.cachedJungleTreasurePrefab = prefab; + if (!this.isHomeRuntimeAlive() || cc.fx.GameConfig.isGameplayEntryPreparing()) { + this.jungleTreasureOpenAfterLoad = false; + return; + } if (this.jungleTreasureOpenAfterLoad) { this.jungleTreasureOpenAfterLoad = false; this.showJungleTreasure(); @@ -3327,6 +3408,7 @@ export default class JiaZai extends cc.Component { /** 创建或重新显示活动界面。 */ private showJungleTreasure() { + if (!this.isHomeRuntimeAlive() || cc.fx.GameConfig.isGameplayEntryPreparing()) return; if (!JiaZai.cachedJungleTreasurePrefab || !this.jungleTreasureServerData) { this.closeLoad(); return; @@ -3545,17 +3627,19 @@ export default class JiaZai extends cc.Component { } private loadJungleOverPrefab(): void { - const existingBundle = cc.assetManager.getBundle("jungle_treasure"); - if (existingBundle && JiaZai.cachedJungleOverPrefab) { + if (!this.isHomeRuntimeAlive() || cc.fx.GameConfig.isGameplayEntryPreparing()) return; + if (this.isHomeBundleReady("jungle_treasure") && JiaZai.cachedJungleOverPrefab + && cc.isValid(JiaZai.cachedJungleOverPrefab, true)) { this.showJungleOver(); return; } const loadPrefab = (bundle: cc.AssetManager.Bundle) => { bundle.load("prefab/jungleOver", cc.Prefab, (error: Error, prefab: cc.Prefab) => { - if (!this.node || !cc.isValid(this.node)) { + if (!this.isHomeRuntimeAlive() || cc.fx.GameConfig.isGameplayEntryPreparing()) { return; } - if (error || !prefab) { + if (error || !prefab || !cc.isValid(prefab, true) + || cc.assetManager.getBundle("jungle_treasure") !== bundle || !this.isHomeBundleReady("jungle_treasure")) { this.closeLoad(); MiniGameSdk.API.showToast("奖励界面加载失败,请重新进入游戏"); return; @@ -3564,12 +3648,8 @@ export default class JiaZai extends cc.Component { this.showJungleOver(); }); }; - if (existingBundle) { - loadPrefab(existingBundle); - return; - } - cc.assetManager.loadBundle("jungle_treasure", (error: Error, bundle: cc.AssetManager.Bundle) => { - if (!this.node || !cc.isValid(this.node)) { + this.loadHomeBundleWithDependencies("jungle_treasure", 5, (error: Error, bundle: cc.AssetManager.Bundle) => { + if (!this.isHomeRuntimeAlive() || cc.fx.GameConfig.isGameplayEntryPreparing()) { return; } if (error || !bundle) { @@ -3582,6 +3662,7 @@ export default class JiaZai extends cc.Component { } private showJungleOver(): void { + if (!this.isHomeRuntimeAlive() || cc.fx.GameConfig.isGameplayEntryPreparing()) return; if (!JiaZai.cachedJungleOverPrefab || !this.pendingJungleSettlement) { this.closeLoad(); return; @@ -3757,16 +3838,16 @@ export default class JiaZai extends cc.Component { // 3. 调用分享接口 // ============================================ openShare() { - console.log("打开加载2"); + // console.log("打开加载2"); this.openLoad(); let otherNickName = null; let otherAvatarUrl = null; let otherKuang = "0"; let otherLevel = cc.fx.GameConfig.GM_INFO.otherLevel; Utils.getUserInfoByID((data) => { - console.log("获取好友失败__________________", data); + // console.log("获取好友失败__________________", data); if (data.code == 1) { - console.log("获取好友信息:", data.data.name); + // console.log("获取好友信息:", data.data.name); if (data.data.name) { otherNickName = data.data.name; } @@ -3783,10 +3864,10 @@ export default class JiaZai extends cc.Component { otherLevel: otherLevel, } // 预加载 活动 预制体 - console.log("好友名字:", otherNickName); + // console.log("好友名字:", otherNickName); - if (!JiaZai.cachedActionPrefab) { - cc.assetManager.loadBundle('action_bundle', (err: Error, bundle: cc.AssetManager.Bundle) => { + if (!JiaZai.cachedSharePrefab) { + this.loadHomeBundleWithDependencies("action_bundle", 5, (err: Error, bundle: cc.AssetManager.Bundle) => { if (err) { cc.error(err.message || err); return; @@ -3797,7 +3878,7 @@ export default class JiaZai extends cc.Component { return; } JiaZai.cachedSharePrefab = prefab; - console.log("2222分享预制体已经准备好", shuju); + // console.log("2222分享预制体已经准备好", shuju); this.closeLoad(); if (!this.ShareNode) { this.ShareNode = cc.instantiate(JiaZai.cachedSharePrefab); @@ -3867,7 +3948,7 @@ export default class JiaZai extends cc.Component { } if (data.data != null && data.code == 0) { - console.log("未到期已领取"); + // console.log("未到期已领取"); } if (data.code == 1) { this.getcard.active = true; @@ -3964,7 +4045,7 @@ export default class JiaZai extends cc.Component { Utils.getStarter_pack((data) => { if (data.code == 1) { if (data.data.starter_pack == 0) { - console.log("用户没有触发新手礼包,准备触发,并且给弹窗"); + // console.log("用户没有触发新手礼包,准备触发,并且给弹窗"); this.winStreakShow = false; Utils.setStarter_pack((res) => { if (res.code == 1) { @@ -3975,7 +4056,7 @@ export default class JiaZai extends cc.Component { this.setStarterPackHomeButtonVisible(true); cc.fx.GameConfig.GM_INFO.starter_packTime = res.data.starter_pack / 1000; if (!this.actionpNode) this.openStarter_pack(); - console.log("打开新手礼包"); + // console.log("打开新手礼包"); const nowTime = Math.floor(Date.now() / 1000); let time = cc.fx.GameConfig.GM_INFO.starter_packTime - nowTime; if (time > 0) { @@ -3992,20 +4073,20 @@ export default class JiaZai extends cc.Component { } else if (data.data.starter_pack < timestamp) { this.winStreakShow = true; - console.log("用户新手礼包已经过期,不触发"); + // console.log("用户新手礼包已经过期,不触发"); this.newbieGift = false; if (this.Stamina) this.setStarterPackHomeButtonVisible(false); } else if (data.data.starter_packState == 1) { this.winStreakShow = true; - console.log("用户已经购买过新手礼包,不触发"); + // console.log("用户已经购买过新手礼包,不触发"); this.newbieGift = false; if (this.Stamina) this.setStarterPackHomeButtonVisible(false); } else { - console.log("用户在新手礼包时间范围内,给弹窗"); + // console.log("用户在新手礼包时间范围内,给弹窗"); cc.fx.GameConfig.GM_INFO.starter_packTime = data.data.starter_pack / 1000; const nowTime = Math.floor(Date.now() / 1000); let time = cc.fx.GameConfig.GM_INFO.starter_packTime - nowTime; @@ -4033,7 +4114,7 @@ export default class JiaZai extends cc.Component { this.setStarterPackHomeButtonVisible(true); cc.fx.StorageMessage.setStorage("Starter_pack", dateStr); if (!this.actionpNode) this.openStarter_pack(); - console.log("打开新手礼包"); + // console.log("打开新手礼包"); this.winStreakShow = false; } else { @@ -4041,7 +4122,7 @@ export default class JiaZai extends cc.Component { this.newbieGift = false; if (this.Stamina) this.setStarterPackHomeButtonVisible(true); - console.log("当天已经给过,不再弹窗新手礼包"); + // console.log("当天已经给过,不再弹窗新手礼包"); } } } @@ -4087,7 +4168,7 @@ export default class JiaZai extends cc.Component { level: level //被帮助关卡等级 } cc.fx.GameTool.shushu_Track("stage_help", eventData); //帮助通关 - this.openShare(); + this.queueHomeAutomaticTask("分享邀请弹窗", () => this.openShare()); } else { cc.fx.GameConfig.GM_INFO.otherLevel = 0; @@ -4178,7 +4259,7 @@ export default class JiaZai extends cc.Component { kuandu = 35; } - console.log("寬度", kuandu); + // console.log("寬度", kuandu); NumberToImage.numberToImageNodesShop((cc.fx.GameConfig.GM_INFO.level + 1), kuandu, 15, "custom", this.level, true); if (this.level.children) { @@ -4228,10 +4309,11 @@ export default class JiaZai extends cc.Component { }); // cc.fx.GameTool.changeCoin(6000); const jiazaiNode = cc.find("Canvas"); // 假设 JiaZai 挂在 Canvas 节点 - const jiazaiComp = jiazaiNode.getComponent(JiaZai); + const jiazaiComp = jiazaiNode && jiazaiNode.getComponent(JiaZai); if (jiazaiComp) { // console.log("获取到JiaZai组件", jiazaiComp); setTimeout(() => { + if (!jiazaiComp.isHomeRuntimeAlive() || cc.fx.GameConfig.isGameplayEntryPreparing()) return; jiazaiComp.setHealthInfo(false); jiazaiComp.startTimeCutDown(); jiazaiComp.updateCoin(); @@ -4245,7 +4327,7 @@ export default class JiaZai extends cc.Component { cc.fx.GameTool.getHealth(null); }) } - this.closeLoad(); + if (this.isHomeRuntimeAlive() && !cc.fx.GameConfig.isGameplayEntryPreparing()) this.closeLoad(); }, outTradeNo) // update (dt) {} } @@ -4294,7 +4376,7 @@ export default class JiaZai extends cc.Component { }) } else { - console.log("有任务列表判断红点", cc.fx.GameConfig.GM_INFO.tasks); + // console.log("有任务列表判断红点", cc.fx.GameConfig.GM_INFO.tasks); if (cc.fx.GameConfig.GM_INFO.tasks.levelPass.state == 1 || cc.fx.GameConfig.GM_INFO.tasks.share.state == 1 || cc.fx.GameConfig.GM_INFO.tasks.useEnergy.state == 1 || cc.fx.GameConfig.GM_INFO.tasks.useProp.state == 1 ) { @@ -4341,6 +4423,7 @@ export default class JiaZai extends cc.Component { // 何时调用:点击连胜活动按钮时调用 // ============================================ openWinStreak(event?: cc.Event, customEventData?: string) { + if (!this.isHomeRuntimeAlive() || cc.fx.GameConfig.isGameplayEntryPreparing()) return; this.vibrateButtonClick(event); let eventName = ""; let btnName = "" @@ -4361,17 +4444,19 @@ export default class JiaZai extends cc.Component { if (!JiaZai.cachedWinStreakPrefab) { // cc.error('Action prefab is not loaded yet.'); - cc.assetManager.loadBundle('action_bundle', (err: Error, bundle: cc.AssetManager.Bundle) => { + this.loadHomeBundleWithDependencies("action_bundle", 5, (err: Error, bundle: cc.AssetManager.Bundle) => { if (err) { cc.error(err.message || err); return; } + if (!this.isHomeRuntimeAlive() || cc.fx.GameConfig.isGameplayEntryPreparing()) return; bundle.load('prefab/winStreak', cc.Prefab, (err: Error, prefab: cc.Prefab) => { if (err) { cc.error(err.message || err); return; } JiaZai.cachedWinStreakPrefab = prefab; + if (!this.isHomeRuntimeAlive() || cc.fx.GameConfig.isGameplayEntryPreparing()) return; if (!this.winStreakNode) { // 第一次使用,创建节点 this.winStreakNode = cc.instantiate(JiaZai.cachedWinStreakPrefab); @@ -4453,7 +4538,7 @@ export default class JiaZai extends cc.Component { console.error("passCheckData[2] 不存在"); return; } - console.log("补发通行证", passCheckData) + // console.log("补发通行证", passCheckData) Utils.setPassCheckInfo((res) => { if (res.code === 1) { cc.fx.StorageMessage.setStorage("PassCheckActivate", null); @@ -4501,10 +4586,8 @@ export default class JiaZai extends cc.Component { // ============================================ openPassCheck(event?: cc.Event, customEventData?: string) { this.vibrateButtonClick(event); - if (!JiaZai.cachedPassCheckPrefab) { - console.error('PassCheck prefab is not loaded yet.') - return; - } + if (!this.ensureHomePopupPrefab("shop", "prefab/passCheck", "cachedPassCheckPrefab", + () => this.openPassCheck(undefined, customEventData))) return; let eventName = ""; let btnName = "" @@ -4528,8 +4611,9 @@ export default class JiaZai extends cc.Component { let closeLoadTimer = setTimeout(() => { if (!loadClosed) { loadClosed = true; + if (!this.isHomeRuntimeAlive() || cc.fx.GameConfig.isGameplayEntryPreparing()) return; MiniGameSdk.API.showToast("网络异常,获取通行证失败,请稍后重试"); - console.log("getPassCheckInfo 超时,强制关闭加载"); + // console.log("getPassCheckInfo 超时,强制关闭加载"); this.closeLoad(); } }, 5000); @@ -4537,14 +4621,15 @@ export default class JiaZai extends cc.Component { if (loadClosed) return; loadClosed = true; clearTimeout(closeLoadTimer); + if (!this.isHomeRuntimeAlive() || cc.fx.GameConfig.isGameplayEntryPreparing()) return; this.closeLoad(); passCheck = data.data.passCheck; if (typeof passCheck === 'string') { try { passCheck = JSON.parse(passCheck); - console.log("解析后的 passCheck:", passCheck); - console.log("解析后的 progressLevel:", passCheck[2].activate); + // console.log("解析后的 passCheck:", passCheck); + // console.log("解析后的 progressLevel:", passCheck[2].activate); } catch (e) { console.error("JSON 解析失败:", e); let endTiem = data.data.time + 30 * 24 * 60 * 60 * 1000; @@ -4627,7 +4712,7 @@ export default class JiaZai extends cc.Component { qinglishuju() { let passCheck = null; Utils.setPassCheckInfo(() => { - console.log("清空通行证", null); + // console.log("清空通行证", null); cc.fx.GameConfig.GM_INFO.playerPassLevel = 0; cc.fx.StorageMessage.setStorage("ProgressIndex", 0); cc.fx.StorageMessage.setStorage("PassCheckActivate", null); @@ -4692,7 +4777,7 @@ export default class JiaZai extends cc.Component { cc.fx.GameConfig.GM_INFO.getProgressLevel = 1; } if (Number(passCheckData[2].progressLevel) > 30) { - console.log("进度已满"); + // console.log("进度已满"); passCheckData[2].progressLevel = 30; passCheckData[2].progress = 4; return; @@ -4747,7 +4832,7 @@ export default class JiaZai extends cc.Component { // console.log("GameScene 保存通行证失败", result); } }, passCheckData); - console.log("通行证数据", passCheckData); + // console.log("通行证数据", passCheckData); } else { // console.log("获取通行证信息失败", res); @@ -4965,7 +5050,7 @@ export default class JiaZai extends cc.Component { // 检查免费奖励是否已领取(值为1表示未领取) const maxLength = Math.max(stage.free.length, stage.passCheck.length); for (let i = 0; i < maxLength; i++) { - console.log("检查玩家的奖励领取", stage.free[i], stage.activate, stage.passCheck[i]) + // console.log("检查玩家的奖励领取", stage.free[i], stage.activate, stage.passCheck[i]) if (i < stage.free.length && stage.free[i] == 1) { hasUnclaimedReward = true; break; @@ -5168,7 +5253,7 @@ export default class JiaZai extends cc.Component { if (data.role) { let role = data.role; let sortedArray = []; - console.log("_________________________得到接口返回", data); + // console.log("_________________________得到接口返回", data); sortedArray = Object.entries(role) //@ts-ignore .sort((a, b) => b[1] - a[1]) // 按数值从大到小排序 @@ -5381,7 +5466,7 @@ export default class JiaZai extends cc.Component { .to(0.2, { opacity: 255 }) .delay(0.2) .call(() => { - console.log("_________________________渲染完成"); + // console.log("_________________________渲染完成"); // this.node.getChildByName("Snow").active = true; }) .start() @@ -5394,7 +5479,7 @@ export default class JiaZai extends cc.Component { .to(0.2, { opacity: 255 }) .delay(0.2) .call(() => { - console.log("_________________________渲染完成"); + // console.log("_________________________渲染完成"); // this.node.getChildByName("Snow").active = true; }) .start() @@ -5411,22 +5496,18 @@ export default class JiaZai extends cc.Component { return; } const generation = ++this.careerLoadGeneration; - cc.assetManager.loadBundle("career", (err: Error, bundle: cc.AssetManager.Bundle) => { - if (err) { - cc.error(err.message || err); - return; - } - if (this.isCareerDisabledByMemoryWarning()) { - this.releaseCareerBundle(bundle); + const loadCareer = (dependencyError?: Error) => { + if (dependencyError) { + cc.error("加载排行依赖 action_bundle 失败:", dependencyError); return; } if (generation !== this.careerLoadGeneration || !this.node - || !cc.isValid(this.node)) { + || !cc.isValid(this.node) + || this.isCareerDisabledByMemoryWarning()) { return; } - - bundle.load('prefab/rankGame', cc.Prefab, (err: Error, prefab: cc.Prefab) => { + this.loadHomeBundleWithDependencies("career", 0, (err: Error, bundle: cc.AssetManager.Bundle) => { if (err) { cc.error(err.message || err); return; @@ -5440,14 +5521,36 @@ export default class JiaZai extends cc.Component { || !cc.isValid(this.node)) { return; } - this.RankNode = cc.instantiate(prefab); - if (isShow === true && this.isShow === true) { - if (callback) - callback(); - } + bundle.load('prefab/rankGame', cc.Prefab, (err: Error, prefab: cc.Prefab) => { + if (err) { + cc.error(err.message || err); + return; + } + if (this.isCareerDisabledByMemoryWarning()) { + this.releaseCareerBundle(bundle); + return; + } + if (generation !== this.careerLoadGeneration + || !this.node + || !cc.isValid(this.node)) { + return; + } + this.RankNode = cc.instantiate(prefab); + if (isShow === true && this.isShow === true) { + if (callback) + callback(); + } + + }); }); - }); + }; + // rankGame 的共享猫图被打包到 action_bundle,先等依赖包就绪再解析预制体。 + if (this.isHomeBundleReady("action_bundle")) { + loadCareer(); + } else { + this.loadHomeBundleWithDependencies("action_bundle", 0, loadCareer); + } } private applyStartUI2(top: cc.Node) { @@ -5583,7 +5686,7 @@ export default class JiaZai extends cc.Component { || !cc.isValid(careerList.node, true)) { return; } - console.log("解析地址:", success, data, error); + // console.log("解析地址:", success, data, error); if (success) { // console.log('详细城市信息:', data); top.getChildByName("posBtn").active = false; @@ -5761,10 +5864,10 @@ export default class JiaZai extends cc.Component { top.getChildByName("gachaGuang").active = false; this.openLoad(); if (data == null || data == "" || data == undefined) { - console.log("获取所有用户所得头像信息"); + // console.log("获取所有用户所得头像信息"); Utils.getAvatarWithRetry(res => { if (res.code === 1) { - console.log("获取到所有用户所得头像信息", res.data); + // console.log("获取到所有用户所得头像信息", res.data); let headArr = [] if (res.data.headArr != null && res.data.headArr != undefined && res.data.headArr.length > 0) headArr = cc.fx.GameTool.getAvatarData(res.data.headArr); @@ -5805,7 +5908,7 @@ export default class JiaZai extends cc.Component { cc.fx.GameTool.shushu_Track("interface_exposure", dataInfo); if (!JiaZai.cachedGachaPrefab) { // cc.error('Action prefab is not loaded yet.'); - cc.assetManager.loadBundle('gacha_bundle', (err: Error, bundle: cc.AssetManager.Bundle) => { + this.loadHomeBundleWithDependencies("gacha_bundle", 5, (err: Error, bundle: cc.AssetManager.Bundle) => { if (err) { cc.error(err.message || err); return; diff --git a/assets/Script/Map.ts b/assets/Script/Map.ts index 7d5e5bc0..f76c9a6e 100644 --- a/assets/Script/Map.ts +++ b/assets/Script/Map.ts @@ -14,7 +14,7 @@ import { MiniGameSdk } from "./Sdk/MiniGameSdk"; import Animation = cc.Animation; // import Revive from "./Revive"; import Utils from "./module/Pay/Utils"; -import { getABReviveCoinCost, MAX_REVIVE_COST_STEP } from "./module/Config/ReviveCostConfig"; +import { getReviveCoinCost, MAX_REVIVE_COST_STEP } from "./module/Config/ReviveCostConfig"; import Vine from "./prop/vine"; import BlockAssetManager from "./module/Tool/BlockAssetManager"; import SpawnGate, { SpawnGateBatchResult, SpawnGateConfig } from "./SpawnGate"; @@ -153,6 +153,10 @@ export default class MapConroler extends cc.Component { gameOver: boolean = false;//游戏结束状态 gameWin: boolean = false;//游戏胜利状态 gameStart: boolean = false;//游戏开始状态 + private waitingForFirstEntry = false; + private entryDownloadRelease: () => void = null; + private entryStartedAt = 0; + private mapPreparationStartedAt = 0; timeNumber: number = 0; //游戏时间 用于倒计时 count_Time: number = 0; //用于统计总游戏时长 add_Time: number = 0; //复活时间 @@ -298,15 +302,7 @@ export default class MapConroler extends cc.Component { this.mask.opacity = 0; this.node.getChildByName("Adhesive").zIndex = 500; cc.game.setFrameRate(63); - cc.fx.AudioManager._instance.playEffect("zhuan2", null); - this.node.parent.parent.parent.getChildByName("zhuanchang").active = true; - this.node.parent.parent.parent.getChildByName("zhuanchang").getComponent(sp.Skeleton).setAnimation(1, "down", false); - this.node.parent.parent.parent.getChildByName("zhuanchang").getComponent(sp.Skeleton).setCompleteListener((entry) => { - if (entry.animation.name === "down") { - // 动画播放结束后执行的逻辑 - this.node.parent.parent.parent.getChildByName("zhuanchang").active = false; - } - }); + this.beginGameEntry(); this.wall_Pass = []; this.blocks = []; this.blocks2 = []; @@ -369,10 +365,10 @@ export default class MapConroler extends cc.Component { if (SceneManager) { MapConroler._instance.SceneManager = SceneManager } else { - console.log("SceneManager 组件未找到"); + // console.log("SceneManager 组件未找到"); } } else { - console.log("SceneManager 节点未找到"); + // console.log("SceneManager 节点未找到"); } Utils.getCityRank((res) => { if (res.code === 1) { @@ -435,7 +431,7 @@ export default class MapConroler extends cc.Component { }, 1000); } else { - console.log("[Memory] low-memory iOS: skip HomeScene preload in Map"); + // console.log("[Memory] low-memory iOS: skip HomeScene preload in Map"); } // console.log("进入GameScene"); if (GameManager._instance) { @@ -455,6 +451,49 @@ export default class MapConroler extends cc.Component { // } // }); } + private beginGameEntry(): void { + this.entryStartedAt = cc.fx.GameConfig.gameplayEntryStartedAt || Date.now(); + this.mapPreparationStartedAt = Date.now(); + // console.log("[进关耗时][地图资源确认及生成] 开始"); + this.entryDownloadRelease = cc.fx.GameConfig.takeEntryDownloadRelease(); + this.waitingForFirstEntry = cc.fx.GameConfig.firstGameplayEntryPending + && cc.fx.GameTool.shouldPreloadGameplayPopupInstances(); + if (this.waitingForFirstEntry) { + this.node.parent.parent.parent.getChildByName("zhuanchang").active = true; + } else { + cc.fx.GameConfig.firstGameplayEntryPending = false; + this.playEntryReveal(); + } + } + + private playEntryReveal(onRevealed?: () => void): void { + const transition = this.node.parent.parent.parent.getChildByName("zhuanchang"); + cc.fx.AudioManager._instance.playEffect("zhuan2", null); + transition.active = true; + const skeleton = transition.getComponent(sp.Skeleton); + skeleton.setAnimation(1, "down", false); + skeleton.setCompleteListener((entry) => { + if (entry.animation.name === "down") { + transition.active = false; + if (onRevealed) onRevealed(); + } + }); + } + + private resumeEntryBackgroundDownloads(): void { + const release = this.entryDownloadRelease; + this.entryDownloadRelease = null; + if (release) release(); + } + + private finishFirstGameplayEntry(success: boolean): void { + if (!this.waitingForFirstEntry || !this.isMapRuntimeAlive()) return; + this.waitingForFirstEntry = false; + cc.fx.GameConfig.firstGameplayEntryPending = false; + if (success) cc.fx.GameConfig.firstGameplayEntryComplete = true; + this.playEntryReveal(() => this.showEnterNewModeIfNeeded()); + } + private async prepareBlockAssets(): Promise { const scene = cc.director.getScene(); const blocks = cc.fx.GameConfig.BLOCK_INFO; @@ -466,20 +505,28 @@ export default class MapConroler extends cc.Component { ready = await cc.fx.GameConfig.prepareBlockWindow(loading.open, "地图初始化"); } catch (error) { loading.close(); + this.resumeEntryBackgroundDownloads(); if (!isCurrent()) return; cc.fx.GameConfig.showBlockLoadError(error, () => this.prepareBlockAssets()); return; } loading.close(); - if (!isCurrent() || !ready) return; + if (!isCurrent() || !ready) { + this.resumeEntryBackgroundDownloads(); + return; + } BlockAssetManager.instance.activateScene(cc.fx.GameConfig.getCurrentLevelBlockDefinitions(), GAMEPLAY_ATLAS_PATHS); this.blockAssetsReady = true; this.initMap(); - this.showEnterNewModeIfNeeded(); + if (!this.waitingForFirstEntry) this.showEnterNewModeIfNeeded(); } onDestroy() { + this.resumeEntryBackgroundDownloads(); this.winDisposed = true; + if (this.waitingForFirstEntry) { + if (MapConroler._instance === this) cc.fx.GameConfig.firstGameplayEntryPending = false; + } if (MapConroler._instance === this) MapConroler._instance = null; if (this.SceneManager && this.SceneManager.finishDynamicPopupLoad) { this.SceneManager.finishDynamicPopupLoad("win"); @@ -561,7 +608,7 @@ export default class MapConroler extends cc.Component { winRoot.setPosition(0, 0); winRoot.setSiblingIndex(Math.min(3, gameRoot.childrenCount - 1)); // winRoot.scale = Number(cc.fx.GameConfig.GM_INFO.scale) || 1; - console.log("[PopupPrefab] instantiated inactive: win/prefab/Win"); + // console.log("[PopupPrefab] instantiated inactive: win/prefab/Win"); this.winRoot = winRoot; this.winPanel = winPanel; this.gameOverNode = gameOverNode; @@ -581,7 +628,7 @@ export default class MapConroler extends cc.Component { const loadedBundle = cc.assetManager.getBundle("win"); if (loadedBundle) { - console.log("[PopupBundle] already loaded: win"); + // console.log("[PopupBundle] already loaded: win"); loadPrefab(loadedBundle); return; } @@ -594,7 +641,7 @@ export default class MapConroler extends cc.Component { this.finishWinLoad(error); return; } - console.log("[PopupBundle] loadBundle complete: win"); + // console.log("[PopupBundle] loadBundle complete: win"); loadPrefab(bundle); }); } @@ -708,7 +755,7 @@ export default class MapConroler extends cc.Component { if (this.winLoading) return; this.winBundle = null; if (bundle && !retainBundle) this.releaseWinBundleAssets(bundle); - else if (bundle) console.log("[PopupBundle] retained win for next GameScene"); + else if (bundle) { /* console.log("[PopupBundle] retained win for next GameScene"); */ } } private releaseWinBundleAssets(bundle: cc.AssetManager.Bundle) { @@ -886,7 +933,7 @@ export default class MapConroler extends cc.Component { this.powerState = power; this.count_Time = Date.now(); //发送数数事件——进入关卡 - console.log("准备进入下一关,发送下一关进入"); + // console.log("准备进入下一关,发送下一关进入"); if ((cc.fx.GameConfig.GM_INFO.level + 1) == 1) { this.guideNode.getComponent(Animation).stop(); this.guideNode.active = false; @@ -1075,18 +1122,22 @@ export default class MapConroler extends cc.Component { let popupWarmupScheduled = false; const schedulePopupWarmup = () => { if (!wallReady || !blockReady || popupWarmupScheduled) return; + if (!this.isMapRuntimeAlive()) return; popupWarmupScheduled = true; + // console.log(`[进关耗时][地图资源确认及生成] 完成,独立耗时=${Date.now() - this.mapPreparationStartedAt}ms,从开始累计=${Date.now() - this.entryStartedAt}ms`); + this.resumeEntryBackgroundDownloads(); + if (this.waitingForFirstEntry) this.finishFirstGameplayEntry(true); this.scheduleOnce(() => { - if (!this.node || !cc.isValid(this.node)) return; + if (!this.isMapRuntimeAlive()) return; if (!this.SceneManager || !this.SceneManager.warmupGameplayPopupBundles) return; const preloadInstances = !!cc.fx.GameTool && !!cc.fx.GameTool.shouldPreloadGameplayPopupInstances && cc.fx.GameTool.shouldPreloadGameplayPopupInstances(); this.SceneManager.warmupGameplayPopupBundles( preloadInstances, - preloadInstances ? (onComplete: () => void) => { + preloadInstances ? (onComplete: (error?: Error) => void) => { if (!this.isMapRuntimeAlive()) return; - this.ensureWinLoaded(() => onComplete()); + this.ensureWinLoaded(error => onComplete(error)); } : null, ); }, 0); @@ -1146,6 +1197,7 @@ export default class MapConroler extends cc.Component { || !isPrefabArrayReady(loadedBlocks, 24) || !isPrefabArrayReady(loadedWalls, 12)) { cc.error("地图预制体资源恢复失败,返回加载场景"); + this.resumeEntryBackgroundDownloads(); cc.director.loadScene("LoadScene"); return; } @@ -2357,10 +2409,10 @@ export default class MapConroler extends cc.Component { if (this.wallArray[k].getChildByName("wall").getComponent("Wall").wall_Info.longAndShort != undefined || this.wallArray[k].getChildByName("wall").getComponent("Wall").wall_Info.colorArray != undefined) { - console.log("跳过长短门或者变色门"); + // console.log("跳过长短门或者变色门"); } else if (this.wallArray[k].getChildByName("wall").getComponent("Wall").wall_Info.toggle != undefined) { - console.log("跳过切换门"); + // console.log("跳过切换门"); } else { wallTemp.push(this.wallArray[k]); @@ -2950,7 +3002,7 @@ export default class MapConroler extends cc.Component { pushStick_block(blockPos, wall) { for (let i = 0; i < blockPos.length; i++) { if (this.mapBlocksWall[blockPos[i].x][blockPos[i].y]) { - console.log("放入:", blockPos[i].x, blockPos[i].y); + // console.log("放入:", blockPos[i].x, blockPos[i].y); } wall.stickBlock.push(this.mapBlocksWall[blockPos[i].x][blockPos[i].y]); } @@ -3458,7 +3510,7 @@ export default class MapConroler extends cc.Component { wallArray.push(this.jumpWallArray[i][j]); if (this.jumpWallArray[i][j].getChildByName("wall").getComponent("Wall").jumpNode) { for (let k = 0; k < wall.length; k++) { - console.log("跳完是空的:", this.jumpWallArray[i][j].getChildByName("wall").getComponent("Wall").wall_Info.num); + // console.log("跳完是空的:", this.jumpWallArray[i][j].getChildByName("wall").getComponent("Wall").wall_Info.num); this.jumpWallArray[i][j].getChildByName("wall").getComponent("Wall").changeJumpWall(false); } } @@ -3478,7 +3530,7 @@ export default class MapConroler extends cc.Component { for (let j = 0; j < toActivateNext.length; j++) { let index = toActivateNext[j]; wallArray[index].getChildByName("wall").getComponent("Wall").changeJumpWall(true); - console.log("跳完是有的:", wallArray[index].getChildByName("wall").getComponent("Wall").wall_Info.num); + // console.log("跳完是有的:", wallArray[index].getChildByName("wall").getComponent("Wall").wall_Info.num); } this.changeLength(wallArray); @@ -3669,7 +3721,7 @@ export default class MapConroler extends cc.Component { // console.log("尝试通过开关门:", wallArray[i].getComponent("Wall").open); } if (wallArray[i].getComponent("Wall").jump != undefined && wallArray[i].getComponent("Wall").jump != null) { - console.log("尝试通过跳跃门,但是门关着"); + // console.log("尝试通过跳跃门,但是门关着"); jg = false; return jg; } @@ -4144,7 +4196,7 @@ export default class MapConroler extends cc.Component { //双倍领取金币 getDoubleCoin() { cc.fx.GameConfig.GM_INFO.rewardAdCoin = 2; - console.log("领取双倍奖励", cc.fx.GameConfig.GM_INFO.rewardAdCoin); + // console.log("领取双倍奖励", cc.fx.GameConfig.GM_INFO.rewardAdCoin); } //播放双倍金币广告 @@ -4156,7 +4208,7 @@ export default class MapConroler extends cc.Component { var callback = function (isType) { if (isType) { // 领取双倍奖励 - console.log("领取双倍奖励"); + // console.log("领取双倍奖励"); this.getDoubleCoin(); } else { @@ -4196,9 +4248,9 @@ export default class MapConroler extends cc.Component { this.blockNum = this.blocks.length; - console.log("剩余数量:", this.blockNum, this.gameOver); + // console.log("剩余数量:", this.blockNum, this.gameOver); if (this.canCompleteLevel() && !this.gameWin && !this.gameOver) { - console.log("获胜了成功了!!!!!!") + // console.log("获胜了成功了!!!!!!") this.gameWin = true; this.gameStart = false; // 消除 Tween 此时可能仍在渲染,等结算层显示后再释放;onDestroy 仍作为兜底。 @@ -4215,7 +4267,7 @@ export default class MapConroler extends cc.Component { if (res.data[i].addLevel >= (cc.fx.GameConfig.GM_INFO.addLevel + 1)) { cc.fx.GameConfig.GM_INFO.cityRank = i + 1; this.cityRank = cc.fx.GameConfig.GM_INFO.cityRank; - console.log("2222我的排名:", cc.fx.GameConfig.GM_INFO.cityRank, "榜单里没有,从比较中得到"); + // console.log("2222我的排名:", cc.fx.GameConfig.GM_INFO.cityRank, "榜单里没有,从比较中得到"); break; } if (i == 0) { @@ -4236,7 +4288,7 @@ export default class MapConroler extends cc.Component { } this.cityRank = cc.fx.GameConfig.GM_INFO.cityRank; - console.log("我的排名:", cc.fx.GameConfig.GM_INFO.cityRank, "我不在绑,且比不过最后一名"); + // console.log("我的排名:", cc.fx.GameConfig.GM_INFO.cityRank, "我不在绑,且比不过最后一名"); } } } @@ -4263,14 +4315,14 @@ export default class MapConroler extends cc.Component { cc.fx.GameTool.setWinStreak("sucess"); cc.fx.GameConfig.GM_INFO.winState = true; cc.fx.StorageMessage.setStorage("winState", cc.fx.GameConfig.GM_INFO.winState); - console.log("本地连胜状态:", cc.fx.GameConfig.GM_INFO.winState); + // console.log("本地连胜状态:", cc.fx.GameConfig.GM_INFO.winState); } } else this.setOtherLevel(); if (cc.fx.GameConfig.GM_INFO.hp < cc.fx.GameConfig.GM_INFO.hp_Max) { - console.log("恢复一点体力", cc.fx.GameConfig.GM_INFO.level); + // console.log("恢复一点体力", cc.fx.GameConfig.GM_INFO.level); if (!this.powerState) { cc.fx.GameTool.setUserHealth(1, (data) => { }, false) @@ -4320,7 +4372,7 @@ export default class MapConroler extends cc.Component { if (data.code == 1) { cc.fx.GameConfig.GM_INFO.randomLevel = Number(data.data.userLevel) || cc.fx.GameTool.getNextLevel(); cc.fx.GameConfig.GM_INFO.endLevelNum = Number(data.data.endLevelNum) || 1; - console.log("获取无尽模式等级成功", cc.fx.GameConfig.GM_INFO.randomLevel, data); + // console.log("获取无尽模式等级成功", cc.fx.GameConfig.GM_INFO.randomLevel, data); } else { cc.fx.GameConfig.GM_INFO.randomLevel = cc.fx.GameTool.getNextLevel() || 0; @@ -4336,7 +4388,7 @@ export default class MapConroler extends cc.Component { cc.fx.GameConfig.GM_INFO.endLevelNum = cc.fx.GameConfig.GM_INFO.endLevelNum + 1; cc.fx.StorageMessage.setStorage("endLevelNum", cc.fx.GameConfig.GM_INFO.endLevelNum); - console.log("无尽模式保存下一关等级:", cc.fx.GameConfig.GM_INFO.randomLevel, cc.fx.GameConfig.GM_INFO.endLevelNum); + // console.log("无尽模式保存下一关等级:", cc.fx.GameConfig.GM_INFO.randomLevel, cc.fx.GameConfig.GM_INFO.endLevelNum); Utils.setEndlessLevel(() => { }, cc.fx.GameConfig.GM_INFO.randomLevel, cc.fx.GameConfig.GM_INFO.endLevelNum); } } @@ -4584,7 +4636,7 @@ export default class MapConroler extends cc.Component { winLevel2() { if (!this.isMapRuntimeAlive()) return; let recordInfinity = cc.fx.StorageMessage.getStorage("EndLevelPop"); - console.log("已经最大关卡房间 弹窗提示", recordInfinity, cc.fx.GameTool.maxLevel()) + // console.log("已经最大关卡房间 弹窗提示", recordInfinity, cc.fx.GameTool.maxLevel()) if (cc.fx.GameTool.maxLevel()) { if (recordInfinity == null || recordInfinity == "") { // 没有弹过窗 @@ -4623,7 +4675,7 @@ export default class MapConroler extends cc.Component { nextBtn.getComponent("btnControl").setTouch(false); this.clearLayerTwoWinCoinFly(); cc.fx.AudioManager._instance.playEffect("anniu_Big", null); - console.log("下一关", cc.fx.GameConfig.GM_INFO.level); + // console.log("下一关", cc.fx.GameConfig.GM_INFO.level); this.uploadToCloud(cc.fx.GameConfig.GM_INFO.level + 1); if (cc.fx.GameTool.maxLevel()) { // 正常游戏到最后一关时 先弹出提示 无尽关暂时都是从外面进入 @@ -4642,7 +4694,7 @@ export default class MapConroler extends cc.Component { if (data.code == 1) { cc.fx.GameConfig.GM_INFO.randomLevel = Number(data.data.userLevel) || cc.fx.GameTool.getNextLevel(); cc.fx.GameConfig.GM_INFO.endLevelNum = Number(data.data.endLevelNum) || 1; - console.log("获取无尽模式等级成功", cc.fx.GameConfig.GM_INFO.randomLevel, data); + // console.log("获取无尽模式等级成功", cc.fx.GameConfig.GM_INFO.randomLevel, data); } else { cc.fx.GameConfig.GM_INFO.randomLevel = rLevel || 0; @@ -4681,7 +4733,7 @@ export default class MapConroler extends cc.Component { // console.log("下一关"); if (this.new_item == 3) { if (cc.fx.GameConfig.GM_INFO.level + 1 == 8) { - console.log("新手引导开启"); + // console.log("新手引导开启"); cc.fx.GameConfig.GM_INFO.hammerFirst = true; } if (cc.fx.GameConfig.GM_INFO.level + 1 == 11) { @@ -4702,7 +4754,7 @@ export default class MapConroler extends cc.Component { this.node.parent.parent.parent.getChildByName("zhuanchang").getComponent(sp.Skeleton).setAnimation(1, "up", false); setTimeout(() => { - console.log("不该从这里进入关卡_____________________"); + // console.log("不该从这里进入关卡_____________________"); cc.fx.GameConfig.LEVEL_INFO_init(true, 0, false); }, 1200); } @@ -4759,7 +4811,7 @@ export default class MapConroler extends cc.Component { cc.fx.GameConfig.GM_INFO.getProgressLevel = 1; } if (Number(passCheckData[2].progressLevel) > 30) { - console.log("进度已满"); + // console.log("进度已满"); passCheckData[2].progressLevel = 30; passCheckData[2].progress = 4; return; @@ -4809,7 +4861,7 @@ export default class MapConroler extends cc.Component { // console.log("GameScene 保存通行证失败", result); } }, passCheckData); - console.log("通行证数据", passCheckData); + // console.log("通行证数据", passCheckData); } else { // console.log("获取通行证信息失败", res); @@ -4957,9 +5009,7 @@ export default class MapConroler extends cc.Component { } private getReviveCoinCost(): number { - const assignment = cc.fx.GameConfig.GM_INFO.abTestAssignments - && cc.fx.GameConfig.GM_INFO.abTestAssignments.layer_3; - return getABReviveCoinCost(assignment, cc.fx.GameConfig.GM_INFO.review); + return getReviveCoinCost(cc.fx.GameConfig.GM_INFO.review); } private updateReviveCoinDisplay(buyBtn: cc.Node) { @@ -4981,9 +5031,9 @@ export default class MapConroler extends cc.Component { //时间到了复活 reviewLevel(event, type) { if (!this.isMapRuntimeAlive()) return; - console.log("进入复活函数"); + // console.log("进入复活函数"); if (this.reviewState == true) { - console.log("在复活中,不可点击"); + // console.log("在复活中,不可点击"); return; } this.reviewState = true; @@ -5028,7 +5078,7 @@ export default class MapConroler extends cc.Component { } if (retryCount >= this.MAX_RETRY_COUNT) { - console.log("复活失败!!!!!!! 5次循环依然失败"); + // console.log("复活失败!!!!!!! 5次循环依然失败"); if (callback) callback(false); return; } @@ -5048,22 +5098,22 @@ export default class MapConroler extends cc.Component { //执行复活函数 runRewive(data) { if (!this.isMapRuntimeAlive()) return; - console.log("____________ 执行几次"); + // console.log("____________ 执行几次"); this.runReviveAndCheck(data, 0, (success) => { if (!this.isMapRuntimeAlive()) return; if (!success) { this.runMagic(true); - console.log("_____________复活失败后,使用魔法棒") + // console.log("_____________复活失败后,使用魔法棒") setTimeout(() => { if (!this.isMapRuntimeAlive()) return; - console.log("魔法棒执行完毕后:", this.blocks.length, this.gameOver, this.gameWin); + // console.log("魔法棒执行完毕后:", this.blocks.length, this.gameOver, this.gameWin); if (this.canCompleteLevel()) { - console.log("_____________魔法棒消除后游戏胜利,不再执行复活"); + // console.log("_____________魔法棒消除后游戏胜利,不再执行复活"); this.gameOver = false; this.nextLevel(1); return; } - console.log("_____________复活失败后,使用魔法棒,重新复活"); + // console.log("_____________复活失败后,使用魔法棒,重新复活"); this.runRewive(data); }, 1500); return; @@ -5240,7 +5290,7 @@ export default class MapConroler extends cc.Component { homeBtn(event, customEventData) { if (!this.isMapRuntimeAlive()) return; if (this.reviewState == true) { - console.log("在复活中,不可点击"); + // console.log("在复活中,不可点击"); return; } this.powerState = cc.fx.GameTool.getUserPowerTime(); @@ -5335,7 +5385,7 @@ export default class MapConroler extends cc.Component { KVDataList: [ { key: 'level', value: String(level) } ], - success: () => console.log('✅ 数据上传成功'), + success: () => { /* console.log('✅ 数据上传成功') */ }, fail: (err) => console.error('❌ 上传失败', err) }); } @@ -5499,7 +5549,7 @@ export default class MapConroler extends cc.Component { return; } if (blockOld != this.blockNum && type != "boom" && type != "time") { - console.log("Deadlock state changed before Lose opened; checking again"); + // console.log("Deadlock state changed before Lose opened; checking again"); this.gameOver = false; if (this.iceTrue() == false && !this.isPauseOpen() && !this.shopPause) { this.pause = false; @@ -6057,7 +6107,7 @@ export default class MapConroler extends cc.Component { //取消锤子道具 cancleHammer() { - console.log("取消锤子"); + // console.log("取消锤子"); let hammerBtn = this.node.parent.getChildByName("Bottom").getChildByName("destroyBtn"); this.hammerMask.active = false; @@ -6074,7 +6124,7 @@ export default class MapConroler extends cc.Component { // 如果是使用连胜免费锤子,则不隐藏按钮并恢复其状态 if (this.hammerSpecial) { - console.log("取消的是连胜免费锤子"); + // console.log("取消的是连胜免费锤子"); this.hammerSpecial = false; this.winStreakBtn.node.active = true; this.destroyBtn.node.getChildByName("mul10").opacity = 0; @@ -6179,10 +6229,10 @@ export default class MapConroler extends cc.Component { this.pause = true; wincoin.openShop(null); } else { - console.log("JiaZai 组件未找到"); + // console.log("JiaZai 组件未找到"); } } else { - console.log("JiaZai 节点未找到"); + // console.log("JiaZai 节点未找到"); } } @@ -6703,7 +6753,7 @@ export default class MapConroler extends cc.Component { } } else if (nomalArray.length == 1) { - console.log("消除一个普通方块"); + // console.log("消除一个普通方块"); nomalArray[0].getComponent("Block").eliminate(true); let time = 100; if (nomalArray[0].getComponent("Block").type == 1 || nomalArray[0].getComponent("Block").type == 9) { @@ -6743,7 +6793,7 @@ export default class MapConroler extends cc.Component { else { let count = 2; if (this.lockArray.length != 0) { - console.log("没有普通快,魔法消除一个带锁块"); + // console.log("没有普通快,魔法消除一个带锁块"); for (let i = 0; i < this.lockArray.length; i++) { // console.log("没有普通快,魔法消除一个带锁块"); this.lockArray[i].getComponent("Block").eliminate(true); @@ -6754,7 +6804,7 @@ export default class MapConroler extends cc.Component { } } if (count != 0 && this.lockArray2.length != 0) { - console.log("没有普通快,魔法消除一带锁块2"); + // console.log("没有普通快,魔法消除一带锁块2"); for (let i = 0; i < this.lockArray2.length; i++) { // console.log("没有普通快,魔法消除一个带锁块"); this.lockArray2[i].getComponent("Block").eliminate(true); @@ -6779,7 +6829,7 @@ export default class MapConroler extends cc.Component { // } if (type == true && count != 0) { - console.log("所有可消除的块都消除了,没办法了", this.blocks.length); + // console.log("所有可消除的块都消除了,没办法了", this.blocks.length); const available = this.blocks.filter(node => !this.isSpawnLockedNode(node)); if (available.length > 0) { let random = Math.floor(Math.random() * available.length); @@ -7203,7 +7253,7 @@ export default class MapConroler extends cc.Component { //判断失败原因 failReason() { - console.log("游戏失败,结束"); + // console.log("游戏失败,结束"); if (this.checkBlockLock() == true) { this.failLevel("blockLock"); } @@ -7645,7 +7695,7 @@ export default class MapConroler extends cc.Component { //设置帮助他人通关 setOtherLevel() { if (cc.fx.GameConfig.GM_INFO.otherLevel > 0) { - console.log("帮助他人通关"); + // console.log("帮助他人通关"); let level = cc.fx.GameConfig.GM_INFO.otherLevel; let uid = cc.fx.GameConfig.GM_INFO.otherUid; let eventData = { @@ -7656,7 +7706,7 @@ export default class MapConroler extends cc.Component { } cc.fx.GameTool.shushu_Track("stage_help", eventData); //帮助通关 Utils.shareLevel(level, uid, (data) => { - console.log("分享结果:", data); + // console.log("分享结果:", data); }); } } @@ -7768,7 +7818,7 @@ export default class MapConroler extends cc.Component { if (!this.hammer) { this.hammerSpecial = true; - console.log("1111使用特殊锤子", this.hammerSpecial); + // console.log("1111使用特殊锤子", this.hammerSpecial); this.node.parent.getChildByName("Mask").active = true; const timestamp = Date.now(); @@ -7861,12 +7911,12 @@ export default class MapConroler extends cc.Component { // 缓存内容:地址、授权时间戳、是否授权成功 // ============================================ runAuthorize() { - console.log("点击授权了"); + // console.log("点击授权了"); // 获取CareerList组件实例 MiniGameSdk.API.getWechatCityInfo((success, data, error) => { - console.log("解析地址:", success, data, error); + // console.log("解析地址:", success, data, error); if (success) { - console.log('详细城市信息:', data); + // console.log('详细城市信息:', data); if (data != null && data != "" && data != undefined) cc.fx.GameConfig.GM_INFO.address = data; let nowTime = Math.floor(Date.now() / 1000); let addressInfo = { @@ -8050,7 +8100,7 @@ export default class MapConroler extends cc.Component { //指定颜色门变换 else { for (let i = 0; i < this.buttonWall.length; i++) { - console.log(this.buttonWall[i].getChildByName("wall").getComponent("Wall").wall_Info.toggle); + // console.log(this.buttonWall[i].getChildByName("wall").getComponent("Wall").wall_Info.toggle); if (color == this.buttonWall[i].getChildByName("wall").getComponent("Wall").wall_Info.toggle) this.buttonWall[i].getChildByName("wall").getComponent("Wall").changeButtonWall(-1); } @@ -8077,7 +8127,7 @@ export default class MapConroler extends cc.Component { } if (Date.now() - this._blocksEmptyTime >= 5000) { this.gameStart = false; - console.log("保底机制:blocks为空超过5秒,强制胜利"); + // console.log("保底机制:blocks为空超过5秒,强制胜利"); let errorData = { error_type: "javascript", error_message: "胜利保底机制触发,方块全部消除且超过3秒,强制胜利。", diff --git a/assets/Script/SceneManager.ts b/assets/Script/SceneManager.ts index e8e9a858..075c6fae 100644 --- a/assets/Script/SceneManager.ts +++ b/assets/Script/SceneManager.ts @@ -207,7 +207,8 @@ export default class SceneManager extends cc.Component { warmupGameplayPopupBundles( preloadInstances: boolean = false, - preloadWin?: (onComplete: () => void) => void, + preloadWin?: (onComplete: (error?: Error) => void) => void, + onComplete?: (error?: Error) => void, ) { if (!this.isPopupRuntimeAlive() || this.popupBundleWarmupStarted) return; this.popupBundleWarmupStarted = true; @@ -216,21 +217,48 @@ export default class SceneManager extends cc.Component { const gameRoot = this.node && this.node.getChildByName("Game"); const requests = [ { popupName: "pause", bundleName: "pause", prefabPath: "prefab/Pause", parent: this.node }, - { popupName: "newMode", bundleName: "NewMode", prefabPath: "prefab/NewMode", parent: gameRoot }, { popupName: "win", bundleName: "win", prefabPath: "prefab/Win", parent: gameRoot }, - { popupName: "lose", bundleName: "lose", prefabPath: "prefab/Lose", parent: gameRoot }, { popupName: "propWindow", bundleName: "propWindow", prefabPath: "prefab/PropWindow", parent: gameRoot }, + { popupName: "newMode", bundleName: "NewMode", prefabPath: "prefab/NewMode", parent: gameRoot }, + { popupName: "lose", bundleName: "lose", prefabPath: "prefab/Lose", parent: gameRoot }, ]; + let warmupError: Error = null; const preloadNext = (index: number) => { if (!this.isPopupRuntimeAlive()) return; - if (index >= requests.length) return; + if (!cc.fx.GameTool.shouldPreloadGameplayPopupInstances()) { + this.popupBundleWarmupStarted = false; + if (onComplete) onComplete(); + return; + } + if (index >= requests.length) { + if (onComplete) { + if (warmupError) this.popupBundleWarmupStarted = false; + onComplete(warmupError); + } + return; + } const request = requests[index]; + const startedAt = Date.now(); + // console.log(`[进关耗时][弹窗预热/${request.bundleName}] 开始(不阻塞进关)`); + const logDone = (error?: Error) => { + // console.log(`[进关耗时][弹窗预热/${request.bundleName}] ${error ? "失败" : "完成"},独立耗时=${Date.now() - startedAt}ms`); + }; + // 即使资源全命中内存,也不要在同一帧连续实例化五个弹窗。 + const next = () => this.scheduleOnce(() => preloadNext(index + 1), 0); if (request.popupName === "win") { if (preloadWin) { - preloadWin(() => preloadNext(index + 1)); + preloadWin(error => { + if (!this.isPopupRuntimeAlive()) return; + logDone(error); + if (error) { + warmupError = error; + cc.warn("Popup instance warmup failed: win", error.message || error); + } + next(); + }); } else { - preloadNext(index + 1); + next(); } return; } @@ -241,11 +269,14 @@ export default class SceneManager extends cc.Component { request.parent, (error: Error) => { if (!this.isPopupRuntimeAlive()) return; + logDone(error); if (error) { + warmupError = error; cc.warn(`Popup instance warmup failed: ${request.bundleName}`, error.message || error); - this.releaseDynamicPopup(request.popupName, true); + // loadDynamicPopup 已清理失败资源。用户可能正共用此请求, + // 这里不能清除其输入拦截/重试状态。 } - preloadNext(index + 1); + next(); }, false, ); diff --git a/assets/Script/module/Config/GameConfig.ts b/assets/Script/module/Config/GameConfig.ts index 32e15d8f..ba61c913 100644 --- a/assets/Script/module/Config/GameConfig.ts +++ b/assets/Script/module/Config/GameConfig.ts @@ -4,6 +4,11 @@ import { MiniGameSdk } from "../../Sdk/MiniGameSdk"; import { GameTool } from "../Tool/GameTool"; import BlockAssetManager from "../Tool/BlockAssetManager"; +interface GameplayEntryPreparation { + startedAt: number; + downloads: Promise; + release: () => void; +} const { ccclass, property } = cc._decorator; @ccclass('GameConfig') @@ -74,6 +79,84 @@ export class GameConfig { private static sceneLoadRequest = 0; private static homeWarmupRequest = 0; private static cancelBlockLoading: () => void = null; + private static gameplayScenePreload: Promise = null; + private static gameplaySceneForegroundPreload: Promise = null; + private static pendingEntryDownloadRelease: () => void = null; + static gameplayEntryStartedAt = 0; + // 仅本次启动有效;地图就绪即可完成首次进关,不等待弹窗实例。 + static firstGameplayEntryComplete = false; + static firstGameplayEntryPending = false; + + /** 仅合并正在进行的预加载,不能把历史完成标记当作永久有效的场景缓存。 */ + static preloadGameplayScene(foreground = false): Promise { + const key = foreground ? "gameplaySceneForegroundPreload" : "gameplayScenePreload"; + if (this[key]) return this[key]; + const request = this.timeGameplayStage(foreground ? "GameScene前台文件准备" : "首页GameScene预取", + () => new Promise((resolve, reject) => { + const done = (error: Error) => error ? reject(error) : resolve(); + const bundle = cc.assetManager.bundles.find(bundle => !!bundle.getSceneInfo("GameScene")); + if (!bundle) { reject(new Error("GameScene 所属 Bundle 尚未加载")); return; } + // 不等待首页的低优先级任务;引擎复用已下载文件,前台请求使用 scene 调度配置。 + bundle.preloadScene("GameScene", { preset: "scene", priority: 10 }, done); + })); + this[key] = request; + const clear = () => { + if (this[key] === request) this[key] = null; + }; + request.then(clear, clear); + return request; + } + + static async timeGameplayStage(stage: string, task: () => Promise): Promise { + const start = Date.now(); + // console.log(`[进关耗时][${stage}] 开始`); + try { + const result = await task(); + // console.log(`[进关耗时][${stage}] ${result === false ? "未就绪" : "完成"},独立耗时=${Date.now() - start}ms`); + return result; + } catch (error) { + // console.log(`[进关耗时][${stage}] 失败,独立耗时=${Date.now() - start}ms`, error); + throw error; + } + } + + /** 点击进关即开始不依赖关卡 JSON 的任务,所有入口共用原有下载保障。 */ + static beginGameplayEntryPreparation(): GameplayEntryPreparation { + const startedAt = Date.now(); + this.gameplayEntryStartedAt = startedAt; + const previousRelease = this.takeEntryDownloadRelease(); + const releaseHold = BlockAssetManager.instance.holdBackgroundDownloads(); + if (previousRelease) previousRelease(); + const release = () => { + releaseHold(); + if (this.pendingEntryDownloadRelease === release) this.pendingEntryDownloadRelease = null; + }; + this.pendingEntryDownloadRelease = release; + // console.log("[进关耗时] 开始进关准备(各并行阶段耗时不能相加)"); + const downloads = this.timeGameplayStage("五个弹窗分包磁盘检查/下载", + () => GameTool.ensureGameplayPopupSubpackagesDownloaded()).catch(() => false); + // 不把文件预取作为门槛;必要资源就绪后 loadScene 立即正式加载并复用已有文件。 + this.preloadGameplayScene(true).catch(error => cc.warn("场景文件预取失败,正式加载时重试", error)); + return { startedAt, downloads, release }; + } + + /** 新 Map 接管自己的暂停凭证,旧 Map 销毁不能恢复新进关的后台派发。 */ + static takeEntryDownloadRelease(): () => void { + const release = this.pendingEntryDownloadRelease; + this.pendingEntryDownloadRelease = null; + return release; + } + + /** 首页仅用来判断能否启动自动预加载,不暂停或接管任何在途回调。 */ + static isGameplayEntryPreparing(): boolean { + return !!this.pendingEntryDownloadRelease; + } + + static shouldPrepareFirstGameplayEntry(): boolean { + const scene = cc.director.getScene(); + return !this.firstGameplayEntryComplete && !!scene && scene.name === "HomeScene" + && GameTool.shouldPreloadGameplayPopupInstances(); + } static getCurrentLevelBlockDefinitions(): any[] { const blocks = this.BLOCK_INFO && this.BLOCK_INFO[0] ? this.BLOCK_INFO[0] : []; @@ -175,59 +258,95 @@ export class GameConfig { } /** 所有正式进关入口都在弹窗分包及当前关图片就绪后才切换 GameScene。 */ - static enterGameSceneWhenReady(time = 0): void { + static enterGameSceneWhenReady(time = 0, preparation?: GameplayEntryPreparation): void { const id = ++this.sceneLoadRequest; const scene = cc.director.getScene(); const blocks = this.BLOCK_INFO; const isCurrent = () => id === this.sceneLoadRequest && cc.director.getScene() === scene && this.BLOCK_INFO === blocks; const loading = this.createBlockLoading(isCurrent); + const firstEntry = this.shouldPrepareFirstGameplayEntry(); + const entry = preparation || this.beginGameplayEntryPreparation(); + const { release, startedAt } = entry; - if (!GameTool.areGameplayPopupSubpackagesDownloaded()) loading.open(); - GameTool.ensureGameplayPopupSubpackagesDownloaded().then(popupReady => { + if (firstEntry || !GameTool.areGameplayPopupSubpackagesDownloaded()) loading.open(); + // 只等待磁盘分包与当前关图片,不等待后台场景预加载回调。 + Promise.all([ + entry.downloads, + this.timeGameplayStage("当前关方块+公共图集", () => this.prepareBlockWindow(loading.open)), + ]).then(([popupReady, ready]) => { if (!isCurrent()) { loading.close(); - return false; + release(); + return; } if (!popupReady) { loading.close(); + release(); const missing = GameTool.getMissingGameplayPopupSubpackages(); this.showBlockLoadError( new Error(`游戏弹窗分包下载失败: ${missing.join(", ")}`), () => this.enterGameSceneWhenReady(time), "游戏界面资源下载失败\n请检查网络,点击此处重试" ); - return false; + return; } - return this.prepareBlockWindow(loading.open); - }).then(ready => { + if (!ready) { + loading.close(); + release(); + return; + } + // console.log(`[进关耗时][进关门槛就绪] 从开始累计=${Date.now() - startedAt}ms`); loading.close(); - if (!ready || !isCurrent()) return; - cc.director.preloadScene("GameScene", error => { - if (!isCurrent()) return; - if (error) { - this.showBlockLoadError(error, () => this.enterGameSceneWhenReady(time)); + if (firstEntry) { + const home = cc.find("Canvas").getComponent("JiaZai"); + home.playGameEntryTransition(true); + } + const transitionStartedAt = Date.now(); + setTimeout(() => { + if (!isCurrent()) { + release(); return; } - setTimeout(() => { - if (!isCurrent()) return; - const retainPopupBundles = !!scene && scene.name === "GameScene" - && GameTool.beginGameplayPopupGameSceneReloadRetention(); - try { - cc.director.loadScene("GameScene", () => { - if (retainPopupBundles) { - GameTool.finishGameplayPopupGameSceneReloadRetention(); - } - }); - } catch (error) { + const retainPopupBundles = !!scene && scene.name === "GameScene" + && GameTool.beginGameplayPopupGameSceneReloadRetention(); + // console.log(`[进关耗时][转场等待] 独立耗时=${Date.now() - transitionStartedAt}ms`); + const sceneStartedAt = Date.now(); + // console.log("[进关耗时][GameScene正式加载及激活] 开始"); + try { + if (firstEntry) this.firstGameplayEntryPending = GameTool.shouldPreloadGameplayPopupInstances(); + const accepted = cc.director.loadScene("GameScene", (error: Error) => { if (retainPopupBundles) { GameTool.finishGameplayPopupGameSceneReloadRetention(); } - this.showBlockLoadError(error, () => this.enterGameSceneWhenReady(time)); + if (error) { + // console.log(`[进关耗时][GameScene正式加载及激活] 失败,独立耗时=${Date.now() - sceneStartedAt}ms`, error); + release(); + if (isCurrent()) { + this.firstGameplayEntryPending = false; + this.showBlockLoadError(error, () => this.enterGameSceneWhenReady(time)); + } + } else { + // console.log(`[进关耗时][GameScene正式加载及激活] 完成,独立耗时=${Date.now() - sceneStartedAt}ms,从开始累计=${Date.now() - startedAt}ms`); + } + }); + if (accepted === false) { + release(); + if (retainPopupBundles) GameTool.finishGameplayPopupGameSceneReloadRetention(); + this.firstGameplayEntryPending = false; + this.showBlockLoadError(new Error("GameScene 切换未启动"), () => this.enterGameSceneWhenReady(time)); } - }, time); - }); + } catch (error) { + release(); + if (firstEntry) this.firstGameplayEntryPending = false; + if (retainPopupBundles) { + GameTool.finishGameplayPopupGameSceneReloadRetention(); + } + this.showBlockLoadError(error, () => this.enterGameSceneWhenReady(time)); + } + }, firstEntry ? Math.max(time, 1000) : time); }).catch(error => { loading.close(); + release(); if (isCurrent()) this.showBlockLoadError(error, () => this.enterGameSceneWhenReady(time)); }); } @@ -543,7 +662,7 @@ export class GameConfig { vibrateOpen: true, //震动 coinnum: 0, //每局的金币数 paid_user: false, //是否是付费用户 - version: "1.0.47", //版本号 + version: "1.0.50", //版本号 shushu_DistinctId: "", //数数访客ID shushu_AccountId: "", //数数账号ID uid: "", //用户和后端唯一id @@ -671,6 +790,8 @@ export class GameConfig { var self = this; const requestId = ++this.levelLoadRequest; this.sceneLoadRequest++; + const previousEntryRelease = this.takeEntryDownloadRelease(); + if (previousEntryRelease) previousEntryRelease(); if (this.cancelBlockLoading) this.cancelBlockLoading(); let name = null; @@ -701,17 +822,30 @@ export class GameConfig { const scene = cc.director.getScene(); const isCurrent = () => requestId === this.levelLoadRequest && (!type || cc.director.getScene() === scene); + const firstEntryLoading = type && this.shouldPrepareFirstGameplayEntry() ? this.createBlockLoading(isCurrent) : null; + if (firstEntryLoading) firstEntryLoading.open(); + const preparation = type ? this.beginGameplayEntryPreparation() : null; + const configStartedAt = Date.now(); + // console.log(`[进关耗时][${name}/custom分包] 开始`); let bundleName = "custom"; // 替换为实际的分包名称 - cc.assetManager.loadBundle(bundleName, (err: Error, bundle: cc.AssetManager.Bundle) => { - if (!isCurrent()) return; + cc.assetManager.loadBundle(bundleName, { priority: 10 }, (err: Error, bundle: cc.AssetManager.Bundle) => { + // console.log(`[进关耗时][${name}/custom分包] ${err ? "失败" : "完成"},独立耗时=${Date.now() - configStartedAt}ms`); + if (!isCurrent()) { if (preparation) preparation.release(); return; } if (err) { + if (preparation) preparation.release(); + if (firstEntryLoading) firstEntryLoading.close(); if (type) this.showBlockLoadError(err, () => this.LEVEL_INFO_init(type, time, otherLevel, endless)); return; } - bundle.load(name, cc.JsonAsset, (err: Error, res: cc.JsonAsset) => { - if (!isCurrent()) return; + const jsonStartedAt = Date.now(); + // console.log(`[进关耗时][${name}/JSON] 开始`); + cc.assetManager.loadAny({ path: name, type: cc.JsonAsset, bundle: bundleName }, { priority: 10 }, (err: Error, res: cc.JsonAsset) => { + // console.log(`[进关耗时][${name}/JSON] ${err ? "失败" : "完成"},独立耗时=${Date.now() - jsonStartedAt}ms`); + if (!isCurrent()) { if (preparation) preparation.release(); return; } if (err) { + if (preparation) preparation.release(); + if (firstEntryLoading) firstEntryLoading.close(); if (bundle.getInfoWithPath(name, cc.JsonAsset)) { if (type) this.showBlockLoadError(err, () => this.LEVEL_INFO_init(type, time, otherLevel, endless)); return; @@ -727,7 +861,7 @@ export class GameConfig { return; } - self.applyLevelData(res.json, type, time, Number(name.substring("Json/level".length))); + self.applyLevelData(res.json, type, time, Number(name.substring("Json/level".length)), preparation); }); }); @@ -1013,13 +1147,13 @@ export class GameConfig { const bundleName = "custom"; const name = `Json/level${level}`; - cc.assetManager.loadBundle(bundleName, (err: Error, bundle: cc.AssetManager.Bundle) => { + cc.assetManager.loadBundle(bundleName, { priority: 10 }, (err: Error, bundle: cc.AssetManager.Bundle) => { if (err) { reject(new Error(`加载分包失败: ${err.message}`)); return; } - bundle.load(name, cc.JsonAsset, (err: Error, res: cc.JsonAsset) => { + cc.assetManager.loadAny({ path: name, type: cc.JsonAsset, bundle: bundleName }, { priority: 10 }, (err: Error, res: cc.JsonAsset) => { if (err) { reject(new Error(`加载关卡资源失败: ${err.message}`)); return; @@ -1111,7 +1245,7 @@ export class GameConfig { /** * 应用关卡数据 */ - static applyLevelData(jsonData: any, type: any, time: any, levelNumber = Number(jsonData.LEVEL_INFO[0].id)) { + static applyLevelData(jsonData: any, type: any, time: any, levelNumber = Number(jsonData.LEVEL_INFO[0].id), preparation?: GameplayEntryPreparation) { this.loadedLevelNumber = levelNumber; this.BLOCK_INFO = jsonData["BLOCK_INFO"]; this.LEVEL_INFO = jsonData["LEVEL_INFO"]; @@ -1119,7 +1253,7 @@ export class GameConfig { console.log("关卡配置加载完成"); if (type) { - this.enterGameSceneWhenReady(time); + this.enterGameSceneWhenReady(time, preparation); } } diff --git a/assets/Script/module/Config/ReviveCostConfig.ts b/assets/Script/module/Config/ReviveCostConfig.ts index a3393768..c9103128 100644 --- a/assets/Script/module/Config/ReviveCostConfig.ts +++ b/assets/Script/module/Config/ReviveCostConfig.ts @@ -1,25 +1,8 @@ -export const REVIVE_COST_AB_NAME = "revive_cost_ascend"; -export const MAX_REVIVE_COST_STEP = 5; +// 第三层实验结束,所有玩家统一采用原 C 组复活费用。 +const REVIVE_COSTS = [500, 700, 900, 1100, 1300, 1500]; +export const MAX_REVIVE_COST_STEP = REVIVE_COSTS.length - 1; -const DEFAULT_REVIVE_COSTS = [500]; -const REVIVE_COSTS_BY_GROUP = { - B: [500, 700, 900], - C: [500, 700, 900, 1100, 1300, 1500] -}; - -export function getABReviveCoinCost(assignment: any, reviveCount: any): number { - const isTargetExperiment = assignment - && String(assignment.layer) === "3" - && assignment.name === REVIVE_COST_AB_NAME - && assignment.enabled !== false; - const group = isTargetExperiment - ? String(assignment.group || "Default").toUpperCase() - : "DEFAULT"; - const costs = group === "B" - ? REVIVE_COSTS_BY_GROUP.B - : group === "C" - ? REVIVE_COSTS_BY_GROUP.C - : DEFAULT_REVIVE_COSTS; +export function getReviveCoinCost(reviveCount: any): number { const step = Math.max(0, Math.floor(Number(reviveCount) || 0)); - return costs[Math.min(step, costs.length - 1)]; + return REVIVE_COSTS[Math.min(step, MAX_REVIVE_COST_STEP)]; } diff --git a/assets/Script/module/Tool/BlockAssetManager.ts b/assets/Script/module/Tool/BlockAssetManager.ts index 781c529f..50592feb 100644 --- a/assets/Script/module/Tool/BlockAssetManager.ts +++ b/assets/Script/module/Tool/BlockAssetManager.ts @@ -63,6 +63,19 @@ export default class BlockAssetManager { private activeRequestId = 0; private homeWarmupRequestId = 0; private foregroundLoads = 0; + private entryDownloadHolds = 0; + + /** 只停止后台派发;已在途下载继续完成,进关所需文件仍可提优先级。 */ + holdBackgroundDownloads(): () => void { + this.entryDownloadHolds++; + let released = false; + return () => { + if (released) return; + released = true; + this.entryDownloadHolds--; + this.pumpDownloadQueue(); + }; + } private backgroundRunning = false; private backgroundTimer: any = null; private backgroundStarted = false; @@ -182,7 +195,9 @@ export default class BlockAssetManager { this.foregroundLoads++; this.releaseUnused(); try { + const versionStartedAt = Date.now(); await this.initialize(); + // console.log(`[进关耗时][${stage}/${currentLabel}/Block版本准备] 独立耗时=${Date.now() - versionStartedAt}ms`); if (id !== this.requestId) return false; this.ensureUserCacheFolder(); // console.log(`[Block图片][${stage}检查] ${currentLabel},版本=${this.version},共${paths.size}张`); @@ -190,13 +205,23 @@ export default class BlockAssetManager { // 公共图集全部进内存后才加载方块,不能混在同一批并发请求中。 const groups = [names.filter(isGameplayAtlas), names.filter(name => !isGameplayAtlas(name))]; for (const group of groups) { - for (let i = 0; i < group.length; i += 4) { - if (id !== this.requestId) return false; - const results = await Promise.all(group.slice(i, i + 4).map(name => - this.loadFrame(name).then(() => null, error => error))); - const error = results.find(result => result != null); - if (error) throw error; - } + const groupStartedAt = Date.now(); + const label = group === groups[0] ? "公共图集" : "方块图片"; + // console.log(`[进关耗时][${stage}/${currentLabel}/${label}] 开始,共${group.length}张,最多4个加载任务`); + let nextIndex = 0; + let failure: Error = null; + const worker = async () => { + while (id === this.requestId && !failure && nextIndex < group.length) { + const path = group[nextIndex++]; + try { await this.loadFrame(path); } + catch (error) { failure = error; } + } + }; + // 完成一张立即补一张;失败后停止领取,并等已在途任务结束再回滚窗口。 + await Promise.all(Array.from({ length: Math.min(4, group.length) }, () => worker())); + // console.log(`[进关耗时][${stage}/${currentLabel}/${label}] ${failure ? "失败" : id !== this.requestId ? "取消" : "完成"},独立耗时=${Date.now() - groupStartedAt}ms`); + if (failure) throw failure; + if (id !== this.requestId) return false; } if (id !== this.requestId) return false; this.committedPaths = paths; @@ -621,7 +646,7 @@ export default class BlockAssetManager { this.downloadQueue.sort((a, b) => b.priority - a.priority); while (this.activeFileDownloads < BlockAssetManager.FOREGROUND_DOWNLOAD_LIMIT) { const index = this.downloadQueue.findIndex(job => !job.background - || (this.foregroundLoads === 0 + || (this.foregroundLoads === 0 && this.entryDownloadHolds === 0 && this.activeBackgroundDownloads < BlockAssetManager.BACKGROUND_CONCURRENCY)); if (index < 0) return; const job = this.downloadQueue.splice(index, 1)[0]; @@ -752,7 +777,7 @@ export default class BlockAssetManager { let paused = false; const worker = async (batch: string[]) => { while (index < batch.length) { - while (this.foregroundLoads > 0) { + while (this.foregroundLoads > 0 || this.entryDownloadHolds > 0) { // if (!paused) console.log("[Block图片][后台缓存] 暂停派发,优先等待进关图片,已发出的请求继续完成"); paused = true; await this.delay(100); diff --git a/assets/Script/module/Tool/GameTool.ts b/assets/Script/module/Tool/GameTool.ts index 4334fbb6..c9ca125e 100644 --- a/assets/Script/module/Tool/GameTool.ts +++ b/assets/Script/module/Tool/GameTool.ts @@ -10,15 +10,15 @@ import Utils from "../Pay/Utils"; var GameTool = { MAX_NORMAL_LEVEL: 1940, MEMORY_WARNING_EVENT: "game-memory-warning", - GAMEPLAY_POPUP_SUBPACKAGES: ["pause", "NewMode", "propWindow", "lose", "win"], + GAMEPLAY_POPUP_SUBPACKAGES: ["pause", "win", "propWindow", "NewMode", "lose"], GAMEPLAY_POPUP_SUBPACKAGE_CONCURRENCY: 5, GAMEPLAY_POPUP_PREFAB_LOAD_CONCURRENCY: 2, GAMEPLAY_POPUP_PREFABS: [ { bundleName: "pause", prefabPath: "prefab/Pause" }, - { bundleName: "NewMode", prefabPath: "prefab/NewMode" }, - { bundleName: "propWindow", prefabPath: "prefab/PropWindow" }, - { bundleName: "lose", prefabPath: "prefab/Lose" }, { bundleName: "win", prefabPath: "prefab/Win" }, + { bundleName: "propWindow", prefabPath: "prefab/PropWindow" }, + { bundleName: "NewMode", prefabPath: "prefab/NewMode" }, + { bundleName: "lose", prefabPath: "prefab/Lose" }, ], _startTime: 0, _endTime: 0, @@ -2805,7 +2805,7 @@ var GameTool = { return Promise.resolve(false); } - // Pause 和 NewMode 可能刚进关就用到,优先于结算弹窗。 + // 按 Pause → Win → propWindow → NewMode → Lose 派发;并发下载的完成顺序由网络决定。 const pendingNames = this.getMissingGameplayPopupSubpackages(); if (pendingNames.length === 0) return Promise.resolve(true); @@ -2851,6 +2851,7 @@ var GameTool = { while (activeCount < this.GAMEPLAY_POPUP_SUBPACKAGE_CONCURRENCY && nextIndex < pendingNames.length) { const bundleName = pendingNames[nextIndex++]; + const startedAt = Date.now(); activeCount++; console.log(`[PopupSubpackage] predownload start: ${bundleName}`); try { @@ -2860,12 +2861,14 @@ var GameTool = { this._gameplayPopupSubpackageDownloaded[bundleName] = true; activeCount--; console.log(`[PopupSubpackage] predownload complete: ${bundleName}`); + // console.log(`[进关耗时][分包磁盘/${bundleName}] 完成,独立耗时=${Date.now() - startedAt}ms`); startNext(); }, fail: (error) => { failedCount++; activeCount--; console.warn(`[PopupSubpackage] predownload failed: ${bundleName}`, error); + // console.log(`[进关耗时][分包磁盘/${bundleName}] 失败,独立耗时=${Date.now() - startedAt}ms`); startNext(); }, }); diff --git a/assets/UI/UI/pop/gift.png b/assets/UI/UI/pop/gift.png new file mode 100644 index 00000000..d42d146b Binary files /dev/null and b/assets/UI/UI/pop/gift.png differ diff --git a/assets/shop/img/close.png.meta b/assets/UI/UI/pop/gift.png.meta similarity index 64% rename from assets/shop/img/close.png.meta rename to assets/UI/UI/pop/gift.png.meta index 5db806cf..1f0167f2 100644 --- a/assets/shop/img/close.png.meta +++ b/assets/UI/UI/pop/gift.png.meta @@ -1,6 +1,6 @@ { "ver": "2.3.7", - "uuid": "04b9cd54-0c2d-4174-acf8-50f0482c25bb", + "uuid": "fd4333cc-99b4-4151-a2e4-8e6ac3a34398", "importer": "texture", "type": "sprite", "wrapMode": "clamp", @@ -8,15 +8,15 @@ "premultiplyAlpha": false, "genMipmaps": false, "packable": true, - "width": 133, - "height": 133, + "width": 595, + "height": 361, "platformSettings": {}, "subMetas": { - "close": { + "gift": { "ver": "1.0.6", - "uuid": "d98518f7-b04d-4552-adf6-c225d74c4ca4", + "uuid": "ec42b7cd-0033-4248-b5a7-4ad45321ee3e", "importer": "sprite-frame", - "rawTextureUuid": "04b9cd54-0c2d-4174-acf8-50f0482c25bb", + "rawTextureUuid": "fd4333cc-99b4-4151-a2e4-8e6ac3a34398", "trimType": "auto", "trimThreshold": 1, "rotated": false, @@ -24,10 +24,10 @@ "offsetY": 0, "trimX": 0, "trimY": 0, - "width": 133, - "height": 133, - "rawWidth": 133, - "rawHeight": 133, + "width": 595, + "height": 361, + "rawWidth": 595, + "rawHeight": 361, "borderTop": 0, "borderBottom": 0, "borderLeft": 0, diff --git a/assets/UI/UI/pop/passCheckWindow.png b/assets/UI/UI/pop/passCheckWindow.png new file mode 100644 index 00000000..c7a362a2 Binary files /dev/null and b/assets/UI/UI/pop/passCheckWindow.png differ diff --git a/assets/UI/UI/pop/passCheckWindow.png.meta b/assets/UI/UI/pop/passCheckWindow.png.meta new file mode 100644 index 00000000..5d5faf28 --- /dev/null +++ b/assets/UI/UI/pop/passCheckWindow.png.meta @@ -0,0 +1,38 @@ +{ + "ver": "2.3.7", + "uuid": "3907c00d-466b-41d0-8e59-7391e880e66a", + "importer": "texture", + "type": "sprite", + "wrapMode": "clamp", + "filterMode": "bilinear", + "premultiplyAlpha": false, + "genMipmaps": false, + "packable": true, + "width": 902, + "height": 962, + "platformSettings": {}, + "subMetas": { + "passCheckWindow": { + "ver": "1.0.6", + "uuid": "c5785ec6-3ced-4449-8b01-c59cb4c787d3", + "importer": "sprite-frame", + "rawTextureUuid": "3907c00d-466b-41d0-8e59-7391e880e66a", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0, + "trimX": 0, + "trimY": 0, + "width": 902, + "height": 962, + "rawWidth": 902, + "rawHeight": 962, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "subMetas": {} + } + } +} \ No newline at end of file diff --git a/assets/UI/UI/pop/tongpiaozi.png b/assets/UI/UI/pop/tongpiaozi.png new file mode 100644 index 00000000..f0ee76e3 Binary files /dev/null and b/assets/UI/UI/pop/tongpiaozi.png differ diff --git a/assets/shop/img/chaozhi.png.meta b/assets/UI/UI/pop/tongpiaozi.png.meta similarity index 64% rename from assets/shop/img/chaozhi.png.meta rename to assets/UI/UI/pop/tongpiaozi.png.meta index c0b0d8c1..bba575b7 100644 --- a/assets/shop/img/chaozhi.png.meta +++ b/assets/UI/UI/pop/tongpiaozi.png.meta @@ -1,6 +1,6 @@ { "ver": "2.3.7", - "uuid": "d93b4f3c-ecfa-480a-92ed-3804685a6260", + "uuid": "ece6cfdd-4c54-41dd-97b3-099f2fc7a865", "importer": "texture", "type": "sprite", "wrapMode": "clamp", @@ -8,15 +8,15 @@ "premultiplyAlpha": false, "genMipmaps": false, "packable": true, - "width": 182, - "height": 113, + "width": 617, + "height": 195, "platformSettings": {}, "subMetas": { - "chaozhi": { + "tongpiaozi": { "ver": "1.0.6", - "uuid": "5cfc4f5b-2d0c-45db-ae4e-e1a59a86f78e", + "uuid": "681edcca-144b-430b-8a11-57965309cace", "importer": "sprite-frame", - "rawTextureUuid": "d93b4f3c-ecfa-480a-92ed-3804685a6260", + "rawTextureUuid": "ece6cfdd-4c54-41dd-97b3-099f2fc7a865", "trimType": "auto", "trimThreshold": 1, "rotated": false, @@ -24,10 +24,10 @@ "offsetY": 0, "trimX": 0, "trimY": 0, - "width": 182, - "height": 113, - "rawWidth": 182, - "rawHeight": 113, + "width": 617, + "height": 195, + "rawWidth": 617, + "rawHeight": 195, "borderTop": 0, "borderBottom": 0, "borderLeft": 0, diff --git a/assets/shop/img/chaozhi.png b/assets/shop/img/chaozhi.png deleted file mode 100644 index 16cd0064..00000000 Binary files a/assets/shop/img/chaozhi.png and /dev/null differ diff --git a/assets/shop/img/close.png b/assets/shop/img/close.png deleted file mode 100644 index 5daf1d0b..00000000 Binary files a/assets/shop/img/close.png and /dev/null differ diff --git a/assets/shop/img/passCheck/bg.jpg b/assets/shop/img/passCheck/bg.jpg index 8c076d37..1941a684 100644 Binary files a/assets/shop/img/passCheck/bg.jpg and b/assets/shop/img/passCheck/bg.jpg differ diff --git a/assets/shop/img/passCheck/svipBg.png b/assets/shop/img/passCheck/svipBg.png new file mode 100644 index 00000000..2d73c2f2 Binary files /dev/null and b/assets/shop/img/passCheck/svipBg.png differ diff --git a/assets/shop/img/passCheck/svipBg.png.meta b/assets/shop/img/passCheck/svipBg.png.meta new file mode 100644 index 00000000..a5976f52 --- /dev/null +++ b/assets/shop/img/passCheck/svipBg.png.meta @@ -0,0 +1,38 @@ +{ + "ver": "2.3.7", + "uuid": "5e2f63b4-fac4-439a-adb3-f83c322fc71d", + "importer": "texture", + "type": "sprite", + "wrapMode": "clamp", + "filterMode": "bilinear", + "premultiplyAlpha": false, + "genMipmaps": false, + "packable": true, + "width": 422, + "height": 840, + "platformSettings": {}, + "subMetas": { + "svipBg": { + "ver": "1.0.6", + "uuid": "9d56eda6-cc33-4acc-b0cb-4911913c0434", + "importer": "sprite-frame", + "rawTextureUuid": "5e2f63b4-fac4-439a-adb3-f83c322fc71d", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": 0.5, + "trimX": 1, + "trimY": 1, + "width": 420, + "height": 837, + "rawWidth": 422, + "rawHeight": 840, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "subMetas": {} + } + } +} \ No newline at end of file diff --git a/assets/shop/img/passCheck/vipBg.png b/assets/shop/img/passCheck/vipBg.png new file mode 100644 index 00000000..36231ebf Binary files /dev/null and b/assets/shop/img/passCheck/vipBg.png differ diff --git a/assets/shop/img/passCheck/vipBg.png.meta b/assets/shop/img/passCheck/vipBg.png.meta new file mode 100644 index 00000000..b4412c4e --- /dev/null +++ b/assets/shop/img/passCheck/vipBg.png.meta @@ -0,0 +1,38 @@ +{ + "ver": "2.3.7", + "uuid": "11f1e773-7180-43c1-a312-37cb5be62351", + "importer": "texture", + "type": "sprite", + "wrapMode": "clamp", + "filterMode": "bilinear", + "premultiplyAlpha": false, + "genMipmaps": false, + "packable": true, + "width": 362, + "height": 840, + "platformSettings": {}, + "subMetas": { + "vipBg": { + "ver": "1.0.6", + "uuid": "411f03f2-6876-43e9-9210-9db62c6cb83d", + "importer": "sprite-frame", + "rawTextureUuid": "11f1e773-7180-43c1-a312-37cb5be62351", + "trimType": "auto", + "trimThreshold": 1, + "rotated": false, + "offsetX": 0, + "offsetY": -0.5, + "trimX": 1, + "trimY": 1, + "width": 360, + "height": 839, + "rawWidth": 362, + "rawHeight": 840, + "borderTop": 0, + "borderBottom": 0, + "borderLeft": 0, + "borderRight": 0, + "subMetas": {} + } + } +} \ No newline at end of file diff --git a/assets/shop/prefab/buyActivate.prefab b/assets/shop/prefab/buyActivate.prefab index 6420ba4f..69164c4e 100644 --- a/assets/shop/prefab/buyActivate.prefab +++ b/assets/shop/prefab/buyActivate.prefab @@ -27,14 +27,14 @@ "_active": true, "_components": [ { - "__id__": 43 + "__id__": 37 }, { - "__id__": 44 + "__id__": 38 } ], "_prefab": { - "__id__": 45 + "__id__": 39 }, "_opacity": 255, "_color": { @@ -221,7 +221,7 @@ "_active": true, "_components": [], "_prefab": { - "__id__": 42 + "__id__": 36 }, "_opacity": 255, "_color": { @@ -401,18 +401,12 @@ }, { "__id__": 24 - }, - { - "__id__": 35 - }, - { - "__id__": 38 } ], "_active": true, "_components": [], "_prefab": { - "__id__": 41 + "__id__": 35 }, "_opacity": 255, "_color": { @@ -1396,222 +1390,6 @@ "fileId": "69JY4zjddDobAm6Ict7fHl", "sync": false }, - { - "__type__": "cc.Node", - "_name": "New Label", - "_objFlags": 0, - "_parent": { - "__id__": 10 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 36 - } - ], - "_prefab": { - "__id__": 37 - }, - "_opacity": 255, - "_color": { - "__type__": "cc.Color", - "r": 78, - "g": 63, - "b": 58, - "a": 255 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 200, - "height": 50.4 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_trs": { - "__type__": "TypedArray", - "ctor": "Float64Array", - "array": [ - -102.871, - -476.863, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 1 - ] - }, - "_eulerAngles": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_skewX": 0, - "_skewY": 0, - "_is3DNode": false, - "_groupIndex": 0, - "groupIndex": 0, - "_id": "" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "node": { - "__id__": 35 - }, - "_enabled": true, - "_materials": [ - { - "__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432" - } - ], - "_srcBlendFactor": 770, - "_dstBlendFactor": 771, - "_string": "剩余时间:", - "_N$string": "剩余时间:", - "_fontSize": 40, - "_lineHeight": 40, - "_enableWrapText": true, - "_N$file": null, - "_isSystemFontUsed": true, - "_spacingX": 0, - "_batchAsBitmap": false, - "_styleFlags": 1, - "_underlineHeight": 0, - "_N$horizontalAlign": 1, - "_N$verticalAlign": 1, - "_N$fontFamily": "Arial", - "_N$overflow": 0, - "_N$cacheMode": 1, - "_id": "" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "d1+V4aL+xAwaRfosOJkSJL", - "sync": false - }, - { - "__type__": "cc.Node", - "_name": "countdown", - "_objFlags": 0, - "_parent": { - "__id__": 10 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 39 - } - ], - "_prefab": { - "__id__": 40 - }, - "_opacity": 255, - "_color": { - "__type__": "cc.Color", - "r": 185, - "g": 74, - "b": 49, - "a": 255 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 208.98, - "height": 50.4 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_trs": { - "__type__": "TypedArray", - "ctor": "Float64Array", - "array": [ - 100.803, - -478.667, - 0, - 0, - 0, - 0, - 1, - 1, - 1, - 1 - ] - }, - "_eulerAngles": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_skewX": 0, - "_skewY": 0, - "_is3DNode": false, - "_groupIndex": 0, - "groupIndex": 0, - "_id": "" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "node": { - "__id__": 38 - }, - "_enabled": true, - "_materials": [ - { - "__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432" - } - ], - "_srcBlendFactor": 770, - "_dstBlendFactor": 771, - "_string": "00天00小时", - "_N$string": "00天00小时", - "_fontSize": 40, - "_lineHeight": 40, - "_enableWrapText": true, - "_N$file": null, - "_isSystemFontUsed": true, - "_spacingX": 0, - "_batchAsBitmap": false, - "_styleFlags": 1, - "_underlineHeight": 0, - "_N$horizontalAlign": 1, - "_N$verticalAlign": 1, - "_N$fontFamily": "Arial", - "_N$overflow": 0, - "_N$cacheMode": 0, - "_id": "" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "44dwejFr9B9L66kUp3WWX2", - "sync": false - }, { "__type__": "cc.PrefabInfo", "root": { @@ -1675,9 +1453,6 @@ "buyBtn": { "__id__": 31 }, - "countdown": { - "__id__": 39 - }, "_id": "" }, { diff --git a/assets/shop/prefab/onMCReward.prefab b/assets/shop/prefab/onMCReward.prefab index 1b433858..e031a7d3 100644 --- a/assets/shop/prefab/onMCReward.prefab +++ b/assets/shop/prefab/onMCReward.prefab @@ -265,8 +265,8 @@ }, "_contentSize": { "__type__": "cc.Size", - "width": 914, - "height": 1347 + "width": 1008, + "height": 546 }, "_anchorPoint": { "__type__": "cc.Vec2", diff --git a/assets/shop/script/buyActivate.ts b/assets/shop/script/buyActivate.ts index 11863bcf..be49ff81 100644 --- a/assets/shop/script/buyActivate.ts +++ b/assets/shop/script/buyActivate.ts @@ -8,8 +8,6 @@ export default class buyActivate extends cc.Component { @property(cc.Button) buyBtn: cc.Button = null; - @property(cc.Label) - countdown: cc.Label = null; private callback: Function = null; @@ -25,7 +23,7 @@ export default class buyActivate extends cc.Component { this.qiutBtn.node.on('click', this.onQuitBtnClick, this); this.buyBtn.node.on('click', this.onBuyBtnClick, this); - this.countdown.string = timeStr; + // this.countdown.string = timeStr; } // updateCountdown(str: string) { // this.countdown.string = str; diff --git a/server/laf-cloud/tests/revive-cost-ab.test.mjs b/server/laf-cloud/tests/revive-cost-ab.test.mjs index 5506d46b..9943c209 100644 --- a/server/laf-cloud/tests/revive-cost-ab.test.mjs +++ b/server/laf-cloud/tests/revive-cost-ab.test.mjs @@ -1,48 +1,30 @@ import test from "node:test"; import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import ts from "typescript"; const configUrl = new URL("../../../assets/Script/module/Config/ReviveCostConfig.ts", import.meta.url); -const { getABReviveCoinCost, MAX_REVIVE_COST_STEP, REVIVE_COST_AB_NAME } = await import(configUrl); - -function assignment(group, overrides = {}) { - return { - experimentId: "exp_layer_3_003", - layer: "3", - name: REVIVE_COST_AB_NAME, - enabled: true, - group, - ...overrides, - }; -} - -test("Default and group A always cost 500", () => { - for (const group of ["Default", "A"]) { - assert.deepEqual( - Array.from({ length: 9 }, (_, count) => getABReviveCoinCost(assignment(group), count)), - Array(9).fill(500), - ); - } +const { outputText } = ts.transpileModule(await readFile(configUrl, "utf8"), { + compilerOptions: { module: ts.ModuleKind.ESNext }, }); +const moduleUrl = "data:text/javascript;base64," + Buffer.from(outputText).toString("base64"); +const { getReviveCoinCost, MAX_REVIVE_COST_STEP } = await import(moduleUrl); -test("group B ascends 500, 700, 900 and stays at 900", () => { +test("all players use the settled C curve, capped at 1500", () => { assert.deepEqual( - Array.from({ length: 7 }, (_, count) => getABReviveCoinCost(assignment("B"), count)), - [500, 700, 900, 900, 900, 900, 900], - ); -}); - -test("group C ascends through 1500 and stays at 1500", () => { - assert.deepEqual( - Array.from({ length: 8 }, (_, count) => getABReviveCoinCost(assignment("C"), count)), + Array.from({ length: 8 }, (_, count) => getReviveCoinCost(count)), [500, 700, 900, 1100, 1300, 1500, 1500, 1500], ); assert.equal(MAX_REVIVE_COST_STEP, 5); + assert.equal(getReviveCoinCost(100), 1500); }); -test("disabled, missing, or unrelated assignments use the 500 fallback", () => { - assert.equal(getABReviveCoinCost(null, 5), 500); - assert.equal(getABReviveCoinCost(assignment("C", { enabled: false }), 5), 500); - assert.equal(getABReviveCoinCost(assignment("C", { layer: "2" }), 5), 500); - assert.equal(getABReviveCoinCost(assignment("C", { name: "another_experiment" }), 5), 500); - assert.equal(getABReviveCoinCost(assignment("D"), 5), 500); +test("revive counts are normalized and restarting at zero costs 500", () => { + for (const count of [undefined, null, "invalid", -1, 0]) { + assert.equal(getReviveCoinCost(count), 500); + } + assert.equal(getReviveCoinCost("2"), 900); + assert.equal(getReviveCoinCost(1.9), 700); + assert.equal(getReviveCoinCost(5), 1500); + assert.equal(getReviveCoinCost(0), 500); }); diff --git a/settings/project.json b/settings/project.json index 0ece24f4..e85c5a06 100644 --- a/settings/project.json +++ b/settings/project.json @@ -1,5 +1,5 @@ { - "last-module-event-record-time": 1788250629444, + "last-module-event-record-time": 1788855989096, "group-list": [ "default", "Map" diff --git a/tools/test-block-assets.cjs b/tools/test-block-assets.cjs index 924fa0de..11d9aec7 100644 --- a/tools/test-block-assets.cjs +++ b/tools/test-block-assets.cjs @@ -372,6 +372,8 @@ test('home warmup cannot cancel entry when its level JSON arrives late', async ( cc.fx.GameTool.shouldPreloadGameplayPopupInstances = () => true; cc.fx.GameTool.areGameplayPopupSubpackagesDownloaded = () => true; cc.fx.GameTool.ensureGameplayPopupSubpackagesDownloaded = () => Promise.resolve(true); + cc.fx.GameTool.warmupHomeGameplayPopupAssets = () => Promise.resolve(true); + state.scene.controller.playGameEntryTransition = force => assert.equal(force, true); Config.applyLevelData(level(50), false, 0, 50); state.hold.add('Json/level50'); state.hold.add('door'); @@ -383,6 +385,9 @@ test('home warmup cannot cancel entry when its level JSON arrives late', async ( await tick(); state.deferred.get('door')(); await warmup; + await waitFor(() => [...state.timers.values()].some(timer => timer.delay === 1000), 'first entry transition is ready'); + assert.deepEqual(state.scenes, [], 'scene launch waits for the transition'); + fireTimer(state, 1000); await waitFor(() => state.scenes.length > 0, 'formal entry must still launch GameScene'); assert.deepEqual(state.scenes, ['GameScene']); assert.equal(state.errors.length, 0); @@ -997,6 +1002,55 @@ test('failed foreground preparation also releases the background pause', async t assert.equal(manager.foregroundLoads, 0); }); +test('foreground rolling pool fills a free slot without waiting for the slowest image of a batch', async t => { + const { manager, state } = fixture(); + const names = manager.paths.filter(name => /^9color[0-5]$/.test(name)); + const blocks = names.map(name => { const [, color, block] = name.match(/^(\d+)color(\d+)$/); return { color: +color, block: +block }; }); + for (const name of names) state.hold.add('download:' + name); + t.after(() => releaseHeldDownloads(state)); + const ready = manager.prepareWindow(blocks, {}); + await waitFor(() => state.fileRequests.length === 4, 'first four foreground downloads start'); + state.deferred.get('download:' + names[1])(); + await waitFor(() => state.fileRequests.includes(names[4]), 'a fifth image starts before the first slow image finishes'); + assert.equal(state.activeDownloads.size, 4); + assert.ok(state.activeDownloads.has(names[0])); + releaseHeldDownloads(state); + assert.equal(await ready, true); +}); + +test('foreground rolling pool stops issuing work on failure and drains pending loads before cleanup', async () => { + const { manager } = fixture(); + const names = manager.paths.filter(name => /^9color[0-5]$/.test(name)); + const blocks = names.map(name => { const [, color, block] = name.match(/^(\d+)color(\d+)$/); return { color: +color, block: +block }; }); + const pending = []; + manager.loadFrame = name => new Promise((resolve, reject) => pending.push({ name, resolve, reject })); + const result = assert.rejects(manager.prepareWindow(blocks, {}), /broken/); + await waitFor(() => pending.length === 4, 'four workers start'); + pending[0].reject(new Error('broken')); await tick(); + assert.equal(manager.foregroundLoads, 1, 'pause remains held while other image jobs are still running'); + pending.slice(1).forEach(job => job.resolve()); await result; + assert.equal(pending.length, 4); assert.equal(manager.foregroundLoads, 0); +}); + +test('entry hold keeps bulk workers paused after foreground decoding until map handoff releases it', async t => { + const { manager, state } = fixture(); + manager.paths = manager.paths.slice(0, 12).concat('9color22'); + manager.delay = ms => ms === 100 ? tick() : Promise.resolve(); + const release = manager.holdBackgroundDownloads(); + t.after(release); + const background = manager.cacheAllFiles(); + await tick(); + assert.equal(state.fileRequests.length, 0); + assert.equal(await manager.prepareWindow([{ block: 22, color: 9 }], {}), true); + await tick(); + assert.deepEqual(state.fileRequests, ['9color22'], 'bulk downloads cannot resume at the end of prepareWindow'); + assert.equal(manager.foregroundLoads, 0); + release(); + assert.equal((await background).complete, true); + assert.equal(manager.entryDownloadHolds, 0); + assert.ok(manager.paths.every(name => state.disk.has(`http://usr/Block/1.0.0/${name}.png`))); +}); + test('Block scheduler gives foreground two extra slots while all eight background requests remain pending', async t => { const { manager, state, cc } = fixture(); manager.delay = ms => ms === 100 ? tick() : Promise.resolve(); diff --git a/tools/test-career-loading.cjs b/tools/test-career-loading.cjs new file mode 100644 index 00000000..e1b6040b --- /dev/null +++ b/tools/test-career-loading.cjs @@ -0,0 +1,173 @@ +// Run: node --test tools/test-career-loading.cjs +// Set JIAZAI_TEST_REF=HEAD to reproduce the regression against committed source. +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const vm = require('node:vm'); +const ts = require(process.env.TYPESCRIPT_PATH || 'typescript'); +const root = path.resolve(__dirname, '..'); +const filename = 'assets/Script/JiaZai.ts'; +const source = process.env.JIAZAI_TEST_REF + ? require('node:child_process').execFileSync('git', ['show', `${process.env.JIAZAI_TEST_REF}:${filename}`], { cwd: root, encoding: 'utf8' }) + : fs.readFileSync(path.join(root, filename), 'utf8'); +const parsed = ts.createSourceFile(filename, source, ts.ScriptTarget.Latest, true); +const declaration = parsed.statements.find(node => ts.isClassDeclaration(node) && node.name.text === 'JiaZai'); +const methods = ['LoadCareer', 'getSRank', 'isHomeBundleReady', 'loadHomeBundleWithDependencies'].map(name => { + const method = declaration.members.find(node => ts.isMethodDeclaration(node) && node.name.getText(parsed) === name); + assert.ok(method, `JiaZai.${name} must exist`); + return method.getText(parsed); +}); +const compiled = ts.transpileModule(`class Subject { ${methods.join('\n')} }\nSubject;`, { + compilerOptions: { target: ts.ScriptTarget.ES2017 }, +}).outputText; + +function fixture(cachedAction = false) { + const state = { bundles: new Map(), bundleLoads: [], prefabLoads: [], errors: [], requests: 0, instances: 0, + lowMemory: false, dependencies: {}, bundleErrors: {} }; + const pendingAction = []; + const action = { name: 'action_bundle' }; + if (cachedAction) state.bundles.set(action.name, action); + const cc = { + Prefab: class {}, + warn: (...args) => state.errors.push(args.map(String).join(' ')), + isValid: node => !!node && node.valid !== false, + error: (...args) => state.errors.push(args.map(String).join(' ')), + instantiate: prefab => { state.instances++; return { prefab }; }, + assetManager: { + getBundle: name => state.bundles.get(name), + loadBundle(name, options, done) { + state.bundleLoads.push(name); + if (state.bundleErrors[name]) return done(state.bundleErrors[name]); + if (name === 'action_bundle') return pendingAction.push(done); + const bundle = { + name, deps: state.dependencies[name] || [], + load(asset, type, loaded) { + state.prefabLoads.push(asset); + assert.equal(type, cc.Prefab); + // Model Cocos refusing to resolve cross-bundle prefab dependencies. + if (!state.bundles.has('action_bundle')) { + return loaded(new Error('Please load bundle action_bundle first')); + } + loaded(null, { asset }); + }, + }; + state.bundles.set(name, bundle); + done(null, bundle); + }, + }, + }; + const Subject = vm.runInNewContext(compiled, { cc, Utils: { getSRank: () => { state.requests++; } } }); + const subject = Object.assign(new Subject(), { + node: { valid: true }, RankNode: null, isShow: true, + careerLoadGeneration: 0, _sRankRequestId: 0, + isCareerDisabledByMemoryWarning: () => state.lowMemory, + }); + return { + subject, state, + finishAction(error = null) { + assert.ok(pendingAction.length, 'action_bundle must have a pending load'); + if (!error) state.bundles.set(action.name, action); + for (const done of pendingAction.splice(0)) done(error, error ? undefined : action); + }, + }; +} + +test('cold first entry waits for action_bundle before loading rankGame and requesting ranks', () => { + const { subject, state, finishAction } = fixture(); + subject.getSRank(true); + assert.deepEqual(state.bundleLoads, ['action_bundle']); + assert.deepEqual(state.prefabLoads, []); + assert.equal(state.instances, 0); + assert.equal(state.requests, 0); + finishAction(); + assert.deepEqual(state.bundleLoads, ['action_bundle', 'career']); + assert.deepEqual(state.prefabLoads, ['prefab/rankGame']); + assert.equal(state.instances, 1); + assert.equal(state.requests, 1); + assert.deepEqual(state.errors, []); +}); + +test('cached action_bundle allows the rank load to continue immediately', () => { + const { subject, state } = fixture(true); + subject.getSRank(true); + assert.deepEqual(state.bundleLoads, ['career']); + assert.deepEqual(state.prefabLoads, ['prefab/rankGame']); + assert.equal(state.instances, 1); + assert.equal(state.requests, 1); + assert.deepEqual(state.errors, []); +}); + +test('failed action_bundle load reports the error and does not continue', () => { + const { subject, state, finishAction } = fixture(); + subject.getSRank(true); + finishAction(new Error('dependency download failed')); + assert.deepEqual(state.bundleLoads, ['action_bundle']); + assert.deepEqual(state.prefabLoads, []); + assert.equal(state.instances, 0); + assert.equal(state.requests, 0); + assert.match(state.errors.join('\n'), /dependency download failed/); +}); + +for (const [name, invalidate] of [ + ['destroyed node', ({ subject }) => { subject.node.valid = false; }], + ['missing node', ({ subject }) => { subject.node = null; }], + ['new load generation', ({ subject }) => { subject.careerLoadGeneration++; }], + ['memory warning', ({ state }) => { state.lowMemory = true; }], +]) { + test(`action_bundle completion cannot continue after ${name}`, () => { + const context = fixture(); + context.subject.getSRank(true); + invalidate(context); + context.finishAction(); + assert.deepEqual(context.state.bundleLoads, ['action_bundle']); + assert.deepEqual(context.state.prefabLoads, []); + assert.equal(context.state.instances, 0); + assert.equal(context.state.requests, 0); + }); +} + +test('getSRank(false) still only preloads; showing the preloaded rank requests once', () => { + const { subject, state, finishAction } = fixture(); + subject.getSRank(false); + assert.equal(state.requests, 0); + finishAction(); + assert.equal(state.instances, 1); + assert.equal(state.requests, 0); + subject.getSRank(true); + assert.equal(state.requests, 1); + assert.equal(state.instances, 1); + assert.deepEqual(state.bundleLoads, ['action_bundle', 'career']); +}); + +test('memory warning before entry prevents all rank loading', () => { + const { subject, state } = fixture(); + state.lowMemory = true; + subject.getSRank(true); + assert.deepEqual(state.bundleLoads, []); + assert.equal(state.requests, 0); +}); + +test('cached action_bundle still waits for its own declared dependencies before Career', () => { + const { subject, state } = fixture(true); + state.bundles.get('action_bundle').deps = ['shop']; + state.dependencies.shop = ['UI']; + subject.getSRank(true); + assert.deepEqual(state.bundleLoads, ['shop', 'UI', 'career']); + assert.equal(state.instances, 1); + assert.deepEqual(state.errors, []); +}); + +for (const owner of ['action_bundle', 'career']) { + test(`${owner} dependency failure never parses or instantiates Career`, () => { + const { subject, state } = fixture(true); + if (owner === 'action_bundle') state.bundles.get(owner).deps = ['shop']; + else state.dependencies.career = ['shop']; + state.bundleErrors.shop = new Error('shop dependency offline'); + subject.getSRank(true); + assert.deepEqual(state.prefabLoads, []); + assert.equal(state.instances, 0); + assert.equal(state.requests, 0); + assert.match(state.errors.join('\n'), /shop dependency offline/); + }); +} diff --git a/tools/test-first-game-entry.cjs b/tools/test-first-game-entry.cjs new file mode 100644 index 00000000..b4d3c50a --- /dev/null +++ b/tools/test-first-game-entry.cjs @@ -0,0 +1,859 @@ +// Run: node --test tools/test-first-game-entry.cjs +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const vm = require('node:vm'); +const ts = require('typescript'); +const root = path.resolve(__dirname, '..'); + +function parse(file) { + return ts.createSourceFile(file, fs.readFileSync(path.join(root, file), 'utf8'), ts.ScriptTarget.Latest, true); +} +function loadClass(file, names, globals) { + const source = parse(file); + const declaration = source.statements.find(ts.isClassDeclaration); + const members = names.map(name => { + const member = declaration.members.find(node => node.name && node.name.getText(source) === name); + assert.ok(member, `${file}: ${name}`); + return member.getText(source); + }); + const code = `class ${declaration.name.text} { ${members.join('\n')} }\n${declaration.name.text};`; + return vm.runInNewContext(ts.transpileModule(code, { + compilerOptions: { target: ts.ScriptTarget.ES2017 }, + }).outputText, globals); +} +const flush = async () => { for (let i = 0; i < 15; i++) await Promise.resolve(); }; +function deferred() { + let resolve, reject; + const promise = new Promise((yes, no) => { resolve = yes; reject = no; }); + return { promise, resolve, reject }; +} + +function fixture() { + const state = { scene: { name: 'HomeScene' }, eligible: true, events: [], timers: [], errors: [], preloads: [], holds: 0, + downloads: deferred(), blocks: deferred(), bundleCallbacks: [], jsonCallbacks: [], logs: [] }; + const controller = { + openLoad() { state.loading = true; }, + closeLoad() { state.loading = false; }, + playGameEntryTransition(force) { assert.equal(force, true); state.events.push('up'); }, + }; + const cc = { + warn() {}, isValid: value => !!value && !value.destroyed, fx: {}, + find: () => ({ getComponent: () => controller }), + sys: { platform: 'web', WECHAT_GAME: 'wechat' }, + assetManager: { loadBundle(name, options, done) { assert.equal(options.priority, 10); state.bundleCallbacks.push(done); }, + loadAny(request, options, done) { + assert.equal(request.bundle, 'custom'); assert.match(request.path, /^Json\/level/); + assert.equal(options.priority, 10); state.jsonCallbacks.push(done); + }, bundles: { find: () => ({ preloadScene(name, options, done) { + assert.equal(options.preset, 'scene'); assert.equal(options.priority, 10); + state.preloads.push(done); + } }) } }, + director: { + getScene: () => state.scene, + preloadScene: (name, progress, done) => state.preloads.push(done), + loadScene: (name, done) => { + if (state.sceneError) { done(state.sceneError); return true; } + if (state.rejected) return false; + state.scene = { name }; state.events.push('loadScene'); done(null); return true; + }, + }, + }; + const GameTool = { + maxLevel: () => false, + shouldPreloadGameplayPopupInstances: () => state.eligible, + areGameplayPopupSubpackagesDownloaded: () => true, + ensureGameplayPopupSubpackagesDownloaded: () => { state.events.push('download-check'); return state.downloads.promise; }, + warmupHomeGameplayPopupAssets: () => { state.events.push('home-popups'); return Promise.resolve(true); }, + predownloadGameplayPopupSubpackages: () => state.events.push('disk'), + getMissingGameplayPopupSubpackages: () => ['pause'], + beginGameplayPopupGameSceneReloadRetention: () => state.eligible, + finishGameplayPopupGameSceneReloadRetention: () => state.events.push('retention-finished'), + }; + const BlockAssetManager = { instance: { + holdBackgroundDownloads() { + state.holds++; let released = false; + return () => { if (!released) { released = true; state.holds--; } }; + }, + startBackgroundCache: () => state.events.push('background'), + } }; + const Config = loadClass('assets/Script/module/Config/GameConfig.ts', [ + 'firstGameplayEntryComplete', 'firstGameplayEntryPending', 'sceneLoadRequest', 'cancelBlockLoading', + 'gameplayScenePreload', 'gameplaySceneForegroundPreload', 'pendingEntryDownloadRelease', 'takeEntryDownloadRelease', 'preloadGameplayScene', + 'timeGameplayStage', 'beginGameplayEntryPreparation', 'gameplayEntryStartedAt', 'isGameplayEntryPreparing', + 'levelLoadRequest', 'LEVEL_INFO_init', 'applyLevelData', 'loadLevelFromBundle', + 'shouldPrepareFirstGameplayEntry', 'createBlockLoading', 'enterGameSceneWhenReady', + ], { cc, GameTool, BlockAssetManager, console: { log: (...args) => state.logs.push(args.join(' ')) }, setTimeout: (done, delay) => state.timers.push({ done, delay }) }); + Config.BLOCK_INFO = []; + Config.GM_INFO = { level: 0, otherLevel: 0 }; + Config.prepareBlockWindow = () => { state.events.push('blocks'); return state.blocks.promise; }; + Config.warmupHomeBlockAssets = () => state.blocks.promise; + Config.showBlockLoadError = (error, retry) => state.errors.push({ error, retry }); + cc.fx = { GameConfig: Config, GameTool }; + return { state, cc, Config, BlockAssetManager }; +} + +async function resourcesReady(state) { + state.downloads.resolve(true); state.blocks.resolve(true); state.preloads.at(-1)(null); await flush(); +} + +test('entry prepares disk packages, blocks and scene in parallel; popup Prefabs never gate it', async () => { + const { state, Config } = fixture(); + Config.enterGameSceneWhenReady(); + assert.equal(state.loading, true); + assert.equal(state.preloads.length, 1); + assert.ok(state.events.includes('blocks')); + assert.equal(state.holds, 1); + await resourcesReady(state); + assert.equal(state.events.includes('home-popups'), false); + assert.equal(state.loading, false); + assert.equal(state.timers[0].delay, 1000, 'preserve the existing transition animation duration'); + state.timers[0].done(); + assert.equal(Config.firstGameplayEntryPending, true); + assert.equal(Config.firstGameplayEntryComplete, false); + assert.equal(state.holds, 1, 'scene launch is not map readiness'); + Config.takeEntryDownloadRelease()(); + assert.equal(state.holds, 0); +}); + +for (const mode of ['low', 'later-home', 'restart']) { + test(`${mode}: no popup barrier and caller transition delay remains unchanged`, async () => { + const { state, Config } = fixture(); + if (mode === 'low') state.eligible = false; + if (mode === 'later-home') Config.firstGameplayEntryComplete = true; + if (mode === 'restart') state.scene.name = 'GameScene'; + Config.enterGameSceneWhenReady(27); await resourcesReady(state); + assert.equal(state.timers[0].delay, 27); state.timers[0].done(); + assert.equal(Config.firstGameplayEntryPending, false); + assert.equal(state.events.includes('home-popups'), false); + assert.equal(state.events.includes('retention-finished'), mode === 'restart'); + }); +} + +for (const stage of ['downloads', 'blocks', 'loadScene', 'rejected']) { + test(`${stage} failure releases background hold and permits retry`, async () => { + const { state, Config } = fixture(); + Config.enterGameSceneWhenReady(); + state.downloads.resolve(stage !== 'downloads'); + if (stage === 'blocks') state.blocks.reject(new Error('blocks')); else state.blocks.resolve(true); + state.preloads[0](stage === 'preload' ? new Error('preload') : null); await flush(); + if (stage === 'loadScene') state.sceneError = new Error('scene'); + if (stage === 'rejected') state.rejected = true; + if (state.timers.length) state.timers[0].done(); + assert.equal(state.errors.length, 1); assert.equal(state.holds, 0); + assert.equal(Config.firstGameplayEntryComplete, false); + state.sceneError = null; state.rejected = false; + state.blocks = deferred(); state.downloads = deferred(); + state.errors[0].retry(); await resourcesReady(state); state.timers.at(-1).done(); + assert.equal(state.scene.name, 'GameScene'); assert.equal(state.holds, 1); + }); +} + +test('foreground scene preparation does not wait for the lower-priority Home request', async () => { + const { state, Config } = fixture(); + const early = Config.preloadGameplayScene(); Config.enterGameSceneWhenReady(); + assert.equal(state.preloads.length, 2); await resourcesReady(state); + state.timers[0].done(); assert.equal(state.scene.name, 'GameScene'); + state.preloads[0](null); await early; + Config.preloadGameplayScene(); assert.equal(state.preloads.length, 3); state.preloads[2](null); +}); + +test('superseded or departed entry cannot close a newer Loading or leak its hold', async () => { + const { state, Config } = fixture(); + Config.enterGameSceneWhenReady(); const old = state.blocks; + state.blocks = deferred(); Config.enterGameSceneWhenReady(); + assert.equal(state.holds, 1); + old.resolve(true); state.downloads.resolve(true); state.preloads[0](null); await flush(); + assert.equal(state.loading, true); assert.equal(state.holds, 1); + state.scene = { name: 'OtherScene' }; state.blocks.resolve(true); await flush(); + assert.equal(state.holds, 0); assert.equal(state.events.includes('up'), false); +}); + +test('old Map release cannot unpause a new entry', async () => { + const { state, Config } = fixture(); + Config.enterGameSceneWhenReady(); await resourcesReady(state); state.timers[0].done(); + const oldMapRelease = Config.takeEntryDownloadRelease(); + Config.enterGameSceneWhenReady(); assert.equal(state.holds, 2); + oldMapRelease(); oldMapRelease(); assert.equal(state.holds, 1); + Config.takeEntryDownloadRelease()(); assert.equal(state.holds, 0); +}); + +for (const high of [true, false]) { + test(`Home idle policy high=${high}: only high tier preloads GameScene, no high popup load`, async () => { + const { state, cc, Config, BlockAssetManager } = fixture(); state.eligible = high; + const Home = loadClass('assets/Script/JiaZai.ts', ['startHomeGameplayWarmup'], { cc, BlockAssetManager, console: { log() {} } }); + Object.assign(new Home(), { node: {}, homeGameplayWarmupPending: true, isHomeRuntimeAlive: () => true, scheduleOnce() {}, unschedule() {}, flushHomeAutomaticTasks() {} }).startHomeGameplayWarmup(); + assert.equal(state.events[0], 'disk'); + assert.equal(state.preloads.length, high ? 1 : 0, 'scene starts before block warmup completes'); + state.blocks.resolve(true); await flush(); + assert.equal(state.preloads.length, high ? 1 : 0); + if (high) { state.preloads[0](null); await flush(); } + assert.equal(state.events.includes('home-popups'), !high, 'low helper only warms Pause Bundle'); + assert.ok(state.events.includes('background')); + }); +} + +function mapFixture() { + const context = fixture(); const { state, cc, Config } = context; + state.scene.name = 'GameScene'; state.popupCallbacks = []; state.frames = []; + const Manager = loadClass('assets/Script/SceneManager.ts', ['warmupGameplayPopupBundles'], { cc }); + const manager = Object.assign(new Manager(), { + node: { getChildByName: () => ({}) }, isPopupRuntimeAlive: () => !state.destroyed, + scheduleOnce: done => state.frames.push(done), + loadDynamicPopup(name, bundle, prefab, parent, done, showLoading) { + assert.equal(showLoading, false); state.popupCallbacks.push({ name, done }); + }, + releaseDynamicPopup: () => assert.fail('warmup failure must not clear a concurrent user open/retry'), + }); + const Map = loadClass('assets/Script/Map.ts', [ + 'beginGameEntry', 'finishFirstGameplayEntry', 'resumeEntryBackgroundDownloads', 'onDestroy', + ], { cc }); + const map = Object.assign(new Map(), { + node: { parent: { parent: { parent: { getChildByName: () => ({ active: false }) } } } }, + SceneManager: manager, isMapRuntimeAlive: () => !state.destroyed, + ensureWinLoaded: done => state.popupCallbacks.push({ name: 'win', done }), + playEntryReveal: done => { state.events.push('down'); state.revealed = done; }, + showEnterNewModeIfNeeded: () => state.events.push('new-mode'), scheduleOnce: done => state.frames.push(done), + releaseWinBundle: () => {}, releaseBlockAssets: () => {}, + }); + Map._instance = map; + Config.firstGameplayEntryPending = true; + Config.pendingEntryDownloadRelease = context.BlockAssetManager.instance.holdBackgroundDownloads(); map.beginGameEntry(); + const source = parse('assets/Script/Map.ts'); let initializer; + function visit(node) { + if (ts.isVariableDeclaration(node) && node.name.getText(source) === 'schedulePopupWarmup') initializer = node.initializer.getText(source); + ts.forEachChild(node, visit); + } + visit(source); assert.ok(initializer); + const code = ts.transpileModule(`(function () { + let wallReady = false, blockReady = false, popupWarmupScheduled = false; + const schedulePopupWarmup = ${initializer}; + return { wall() { wallReady = true; schedulePopupWarmup(); }, block() { blockReady = true; schedulePopupWarmup(); } }; + })`, { compilerOptions: { target: ts.ScriptTarget.ES2017 } }).outputText; + return { ...context, map, ready: vm.runInNewContext(code, { cc }).call(map) }; +} + +test('map readiness reveals immediately; popup warmup follows pause, win, propWindow, newMode, lose across frames', () => { + const { state, ready, Config } = mapFixture(); + ready.wall(); assert.equal(state.holds, 1); assert.equal(state.events.includes('down'), false); + ready.block(); assert.equal(state.holds, 0); assert.equal(Config.firstGameplayEntryComplete, true); + assert.equal(state.events.at(-1), 'down'); assert.equal(state.popupCallbacks.length, 0); + const order = []; + for (let i = 0; i < 5; i++) { + state.frames.shift()(); const pending = state.popupCallbacks.shift(); order.push(pending.name); + pending.done(null); assert.equal(state.popupCallbacks.length, 0, 'next popup must wait another frame'); + } + assert.deepEqual(order, ['pause', 'win', 'propWindow', 'newMode', 'lose']); + state.revealed(); assert.equal(state.events.at(-1), 'new-mode'); +}); + +for (const failure of ['pause', 'win']) { + test(`${failure} warmup failure does not block gameplay or remaining popups`, () => { + const { state, ready, Config } = mapFixture(); ready.wall(); ready.block(); + for (let i = 0; i < 5; i++) { + state.frames.shift()(); const pending = state.popupCallbacks.shift(); + pending.done(pending.name === failure ? new Error('warmup') : null); + } + assert.equal(Config.firstGameplayEntryComplete, true); assert.equal(state.errors.length, 0); + }); +} + +for (const reason of ['memory', 'destroy']) { + test(`${reason} stops subsequent popup warmup even without a completion callback`, () => { + const { state, ready } = mapFixture(); ready.wall(); ready.block(); state.frames.shift()(); + if (reason === 'memory') state.eligible = false; else state.destroyed = true; + state.popupCallbacks.shift().done(null); + while (state.frames.length) state.frames.shift()(); + assert.equal(state.popupCallbacks.length, 0); assert.equal(state.holds, 0); + }); +} + +test('background hold blocks only queued background work and supports nested/idempotent release', () => { + const Manager = loadClass('assets/Script/module/Tool/BlockAssetManager.ts', [ + 'entryDownloadHolds', 'holdBackgroundDownloads', 'pumpDownloadQueue', + ], {}); + Manager.FOREGROUND_DOWNLOAD_LIMIT = 10; Manager.BACKGROUND_CONCURRENCY = 8; + const started = []; + const manager = Object.assign(new Manager(), { foregroundLoads: 0, activeFileDownloads: 0, + activeBackgroundDownloads: 0, downloadQueue: [], startUserDownload: job => started.push(job.path) }); + const a = manager.holdBackgroundDownloads(), b = manager.holdBackgroundDownloads(); + manager.downloadQueue = [{ path: 'bulk', priority: -1, background: true }, { path: 'current', priority: 1, background: false }]; + manager.pumpDownloadQueue(); assert.deepEqual(started, ['current']); + a(); a(); assert.deepEqual(started, ['current']); b(); assert.deepEqual(started, ['current', 'bulk']); + assert.equal(manager.entryDownloadHolds, 0); +}); + +test('Map destruction before readiness releases its hold; late map callbacks cannot warm popups', () => { + const { state, map, ready, Config } = mapFixture(); + assert.equal(state.holds, 1); + state.destroyed = true; map.onDestroy(); map.onDestroy(); + ready.wall(); ready.block(); + assert.equal(state.holds, 0); assert.equal(state.frames.length, 0); + assert.equal(Config.firstGameplayEntryPending, false); +}); + +test('memory downgrade before map readiness still reveals the map but skips Prefab warmup', () => { + const { state, ready, Config } = mapFixture(); state.eligible = false; + ready.wall(); assert.equal(state.events.includes('down'), false); + ready.block(); state.frames.shift()(); + assert.equal(Config.firstGameplayEntryComplete, true); + assert.equal(state.holds, 0); assert.equal(state.events.at(-1), 'down'); + assert.equal(state.popupCallbacks.length, 0); +}); + +test('JSON pending does not delay package downloads or foreground scene preparation; no duplicate entry task', async () => { + const { state, Config } = fixture(); Config.LEVEL_INFO_init(true, 0, false); + assert.equal(state.preloads.length, 1); assert.equal(state.holds, 1); + assert.ok(state.events.includes('download-check')); assert.equal(state.events.includes('blocks'), false); + state.bundleCallbacks[0](null, {}); + assert.equal(state.events.includes('blocks'), false); + state.jsonCallbacks[0](null, { json: { BLOCK_INFO: [[]], LEVEL_INFO: [{ id: 1 }], WALL_INFO: [] } }); + assert.equal(state.preloads.length, 1); assert.equal(state.events.filter(e => e === 'download-check').length, 1); + state.downloads.resolve(true); state.blocks.resolve(true); await flush(); + state.timers[0].done(); assert.equal(state.scene.name, 'GameScene', 'must launch while foreground prefetch is still pending'); + state.preloads[0](null); await flush(); + assert.equal(state.logs.some(line => line.includes('[进关耗时]')), false, 'entry timing logs are disabled'); +}); + +for (const failure of ['custom', 'json', 'departed']) { + test(`${failure} during early preparation releases the pause and cannot launch a stale scene`, () => { + const { state, Config } = fixture(); Config.LEVEL_INFO_init(true, 0, false); + if (failure === 'departed') state.scene = { name: 'OtherScene' }; + state.bundleCallbacks[0](failure === 'custom' ? new Error('custom') : null, { + getInfoWithPath: () => ({}) }); + if (failure === 'json') state.jsonCallbacks[0](new Error('json')); + assert.equal(state.holds, 0); assert.equal(state.timers.length, 0); + }); +} + +test('prefetch failure does not fail the entry: the actual scene load owns retry/error reporting', async () => { + const { state, Config } = fixture(); Config.enterGameSceneWhenReady(); + state.preloads[0](new Error('prefetch')); await flush(); assert.equal(state.errors.length, 0); + state.downloads.resolve(true); state.blocks.resolve(true); await flush(); state.timers[0].done(); + assert.equal(state.scene.name, 'GameScene'); assert.equal(state.errors.length, 0); +}); + +test('late custom callback from an older entry cannot release the newer entry hold', () => { + const { state, Config } = fixture(); + Config.LEVEL_INFO_init(true, 0, false); Config.LEVEL_INFO_init(true, 0, false); + assert.equal(state.holds, 1); + state.bundleCallbacks[0](new Error('stale')); + assert.equal(state.holds, 1); assert.equal(state.errors.length, 0); + state.bundleCallbacks[1](new Error('current')); + assert.equal(state.holds, 0); assert.equal(state.errors.length, 1); +}); + +test('entry preparation leaves Home resource scheduling untouched', () => { + const { state, cc, Config } = fixture(); + cc.find = () => assert.fail('entry preparation must not take control of Home resource tasks'); + const entry = Config.beginGameplayEntryPreparation(); + assert.equal(state.holds, 1); + assert.ok(state.events.includes('download-check')); + assert.equal(state.preloads.length, 1); + entry.release(); + assert.equal(state.holds, 0); +}); + +function homeFixture(high = true) { + const context = fixture(); + const { state, cc, Config, BlockAssetManager } = context; + Object.assign(state, { eligible: high, homeFrames: [], homeBundleLoads: [], homePrefabLoads: [], + bundles: new Map(), toasts: [], opened: [], automatic: [], bundleDependencies: {}, + delayedBundles: new Set(), pendingBundles: [], warnings: [] }); + Object.assign(Config.GM_INFO, { level: 20, canIos: true, first: false, otherLevel: 0 }); + cc.Prefab = class {}; + cc.game = { setFrameRate() {} }; + cc.assetManager.getBundle = name => state.bundles.get(name); + cc.assetManager.loadBundle = (name, options, done) => { + state.homeBundleLoads.push({ name, priority: options.priority }); + const finish = (error = null) => { + if (error) return done(error); + let bundle = state.bundles.get(name); + if (!bundle) { + bundle = { name, deps: state.bundleDependencies[name] || [], + load: (path, type, loaded) => { + assert.equal(type, cc.Prefab); + state.homePrefabLoads.push({ path, done: loaded, priority: options.priority }); + } }; + state.bundles.set(name, bundle); + } + done(null, bundle); + }; + if (state.delayedBundles.has(name)) state.pendingBundles.push({ name, finish }); + else finish(); + }; + cc.assetManager.loadAny = (request, options, done) => { + assert.ok(state.bundles.get(request.bundle), 'loadBundle must finish before loading its Prefab'); + assert.equal(request.type, cc.Prefab); + state.homePrefabLoads.push({ path: request.path, done, priority: options.priority }); + }; + cc.fx.GameTool.predownloadGameplayPopupSubpackages = () => { + state.events.push('disk'); return state.downloads.promise; + }; + cc.fx.GameTool.getWechatGameVersion = () => '正式版'; + cc.fx.GameTool.shushu_Track = () => {}; + cc.warn = (...args) => state.warnings.push(args); + cc.error = (...args) => state.warnings.push(args); + const globals = { cc, BlockAssetManager, console: { log() {}, error() {} }, LQCollideSystem: {}, + initProvinceLocator: () => state.automatic.push('province'), + wx: { getLaunchOptionsSync: () => ({ query: state.launchQuery || {} }), getSystemInfoSync: () => ({ platform: 'android' }) }, + MiniGameSdk: { API: { showToast: text => state.toasts.push(text) } }, + NumberToImage: { numberToImageNodes5: () => state.automatic.push('coin-images') }, + setTimeout: (done, delay) => state.timers.push({ done, delay }), + Utils: { getShopDouble: callback => { state.shopResponse = callback; } } }; + const Home = loadClass('assets/Script/JiaZai.ts', [ + 'homeGameplayWarmupPending', 'homeAutomaticTasks', 'homeAutomaticFlushScheduled', 'homePopupOpenRequest', + 'isHomeRuntimeAlive', 'queueHomeAutomaticTask', 'flushHomeAutomaticTasks', 'startHomeGameplayWarmup', + 'preloadHomePopupPrefabs', 'loadHomePopupPrefab', 'ensureHomePopupPrefab', 'onLoad', + 'isHomeBundleReady', 'loadHomeBundleWithDependencies', + 'openShop', 'openMonthlyCard', 'openRewardWindow', 'openPassCheck', 'getShareInfo', + 'getOrder', 'buyMonthCard', 'openStarter_pack', 'startGame', + 'loadJungleTreasurePrefab', 'showJungleTreasure', 'loadJungleOverPrefab', 'showJungleOver', + ], globals); + const node = { getChildByName: () => node, addChild() {} }; + const home = Object.assign(new Home(), { + node, scheduleOnce: (done, delay) => state.homeFrames.push({ done, delay }), + unschedule: done => { state.homeFrames = state.homeFrames.filter(item => item.done !== done); }, + isCareerDisabledByMemoryWarning: () => false, vibrateButtonClick() {}, + deferHomePopupWhileTransfer: () => false, + }); + for (const name of ['ensureLoadingOnTop', 'registerCareerMemoryWarning', 'updateWucaiTransferVisibility', + 'applyStartUI2', 'refreshStaminaDisplay', 'closeLoad', 'checkShare', 'closeAvatar', 'setShareInfo', + 'checkTasks', 'specialLevelShow', 'endlessLevelShow', 'passCheckBuyState', 'checkAndSetPlayerPassLevel', + 'initializeJungleTreasureEntryFromLogin', 'setStarterPackHomeButtonVisible', 'checkStarter_pack', + 'getSRank', 'popUpPassCheck', 'onGames', 'getOrder', 'monthH']) { + home[name] = () => state.automatic.push(name); + } + return { ...context, Home, home, globals, + finishBundle(name, error = null) { + const index = state.pendingBundles.findIndex(item => item.name === name); + assert.ok(index >= 0, `pending dependency: ${name}`); + state.pendingBundles.splice(index, 1)[0].finish(error); + }, + frame() { + const index = state.homeFrames.findIndex(item => item.delay < 1); + assert.ok(index >= 0, 'one queued frame must exist'); + state.homeFrames.splice(index, 1)[0].done(); + }, + async ready() { + state.downloads.resolve(true); state.blocks.resolve(true); + for (const done of state.preloads) done(null); + await flush(); + }, + }; +} + +for (const cached of [false, true]) { + test(`Home startup cached=${cached}: gameplay preparation precedes optional Prefabs and Career`, async () => { + const f = homeFixture(); + if (cached) { + const request = f.home.loadHomePopupPrefab('shop', 'prefab/shop', 'cachedShopPrefab', -10); + f.state.homePrefabLoads.shift().done(null, {}); await request; + f.state.homeBundleLoads = []; + } + f.home.onLoad(); + assert.equal(f.state.homeBundleLoads.length, 0); + assert.equal(f.state.automatic.includes('getSRank'), false); + assert.equal(f.state.automatic.includes('closeAvatar'), false); + f.frame(); + assert.ok(f.state.events.includes('disk')); + assert.equal(f.state.preloads.length, 1); + f.state.downloads.resolve(true); f.state.preloads[0](null); await flush(); + assert.equal(f.state.homePrefabLoads.length, 0, 'block preparation is still pending'); + f.state.blocks.resolve(true); await flush(); + while (f.state.homeFrames.some(item => item.delay < 1)) f.frame(); + assert.ok(f.state.homePrefabLoads.some(item => item.path === 'prefab/Share')); + assert.equal(f.state.homePrefabLoads.some(item => item.path === 'prefab/shop'), !cached); + assert.ok(f.state.automatic.includes('getSRank')); + }); +} + +test('Home automatic tasks wait through entry, resume on failure, and stop on scene departure', async () => { + const f = homeFixture(); + f.home.queueHomeAutomaticTask('one', () => f.state.automatic.push('one')); + f.home.queueHomeAutomaticTask('two', () => f.state.automatic.push('two')); + f.home.startHomeGameplayWarmup(); + const entry = f.Config.beginGameplayEntryPreparation(); + await f.ready(); f.frame(); + assert.deepEqual(f.state.automatic, []); + entry.release(); f.frame(); + assert.deepEqual(f.state.automatic, ['one']); + f.state.scene = { name: 'GameScene' }; f.home.node.destroyed = true; + f.frame(); + assert.deepEqual(f.state.automatic, ['one']); +}); + +for (const failure of ['rejection', 'timeout']) { + test(`Home warmup ${failure} does not leave automatic features waiting forever`, async () => { + const f = homeFixture(); + f.home.queueHomeAutomaticTask('home', () => f.state.automatic.push('home')); + f.home.startHomeGameplayWarmup(); + if (failure === 'timeout') f.state.homeFrames.find(item => item.delay === 15).done(); + else { f.state.blocks.reject(new Error('offline')); await f.ready(); } + f.frame(); + assert.deepEqual(f.state.automatic, ['home']); + assert.equal(f.Config.isGameplayEntryPreparing(), false); + }); +} + +test('only unstarted automatic tasks are deferred; an in-flight Prefab still completes and caches after Home destruction', async () => { + const f = homeFixture(); + const request = f.home.loadHomePopupPrefab('shop', 'prefab/shop', 'cachedShopPrefab', -10); + f.home.node.destroyed = true; f.state.scene = { name: 'GameScene' }; + const prefab = {}; + f.state.homePrefabLoads[0].done(null, prefab); + assert.equal(await request, prefab); + assert.equal(f.Home.cachedShopPrefab, prefab); +}); + +test('a cached shop does not skip the three other missing shop Prefabs', async () => { + const f = homeFixture(); + f.home.loadHomePopupPrefab('shop', 'prefab/shop', 'cachedShopPrefab', -10); + f.state.homePrefabLoads.shift().done(null, {}); await flush(); + f.home.homeGameplayWarmupPending = false; + f.home.preloadHomePopupPrefabs(); + while (f.state.homeFrames.some(item => item.delay < 1)) f.frame(); + const requests = f.state.homePrefabLoads.map(item => item.path); + assert.equal(requests.includes('prefab/shop'), false); + for (const name of ['RewardWindow', 'monthlyCard', 'passCheck']) assert.ok(requests.includes('prefab/' + name)); + for (const request of f.state.homePrefabLoads) { + const paid = ['newbieGift', 'monthlyCard', 'passCheck'].some(name => request.path === 'prefab/' + name); + assert.equal(request.priority, request.path === 'prefab/RewardWindow' ? 2 : paid ? 1 : -10); + } +}); + +for (const method of ['openShop', 'openMonthlyCard', 'openRewardWindow', 'openPassCheck']) { + test(`${method}: a user request loads immediately during warmup, then opens only after its Prefab is ready`, async () => { + const f = homeFixture(); + f.home.queueHomeAutomaticTask('background', () => assert.fail('automatic task started too early')); + f.home[method] = () => f.state.opened.push(method); + f.Home.prototype[method].call(f.home); + assert.equal(f.state.homePrefabLoads.length, 1); + assert.equal(f.state.homePrefabLoads[0].priority, 5); + assert.deepEqual(f.state.opened, []); + f.state.homePrefabLoads[0].done(null, {}); await flush(); + assert.deepEqual(f.state.opened, [method]); + assert.equal(f.home.homeGameplayWarmupPending, true); + }); +} + +test('late manual popup completion cannot open over entry or touch its Loading, but remains cached', async () => { + const f = homeFixture(); + f.home.ensureHomePopupPrefab('shop', 'prefab/shop', 'cachedShopPrefab', () => assert.fail('late popup opened')); + const entry = f.Config.beginGameplayEntryPreparation(); f.state.loading = true; + f.state.homePrefabLoads[0].done(null, {}); await flush(); + assert.ok(f.Home.cachedShopPrefab); + assert.equal(f.state.loading, true); + entry.release(); +}); + +test('failed manual popup load can be retried and never instantiates a missing asset', async () => { + const f = homeFixture(); + const open = () => f.state.opened.push('shop'); + f.home.ensureHomePopupPrefab('shop', 'prefab/shop', 'cachedShopPrefab', open); + f.state.homePrefabLoads[0].done(new Error('offline')); await flush(); + assert.equal(f.state.opened.length, 0); + f.home.ensureHomePopupPrefab('shop', 'prefab/shop', 'cachedShopPrefab', open); + f.state.homePrefabLoads[1].done(null, {}); await flush(); + assert.deepEqual(f.state.opened, ['shop']); +}); + +test('a bundle released before Prefab completion is never installed into the Home cache', async () => { + const f = homeFixture(); + const request = f.home.loadHomePopupPrefab('shop', 'prefab/shop', 'cachedShopPrefab', 5); + f.state.bundles.delete('shop'); + f.state.homePrefabLoads[0].done(null, {}); + await assert.rejects(request, /预制体已失效/); + assert.equal(f.Home.cachedShopPrefab, undefined); +}); + +test('share entry selects the actual level immediately but defers the automatic invitation window', () => { + const f = homeFixture(); + f.cc.sys.platform = 'wechat'; f.Config.GM_INFO.uid = 'self'; + f.state.launchQuery = { level: '51', uid: 'friend' }; + f.home.openShare = () => f.state.opened.push('share'); + f.home.getShareInfo(); + assert.equal(f.Config.GM_INFO.otherLevel, 51); + assert.equal(f.Config.GM_INFO.otherUid, 'friend'); + assert.equal(f.state.opened.length, 0); + f.home.homeGameplayWarmupPending = false; f.home.flushHomeAutomaticTasks(); f.frame(); + assert.deepEqual(f.state.opened, ['share']); +}); + +test('newer user popup requests supersede old UI callbacks without losing either Prefab cache', async () => { + const f = homeFixture(); + f.home.ensureHomePopupPrefab('shop', 'prefab/shop', 'cachedShopPrefab', () => f.state.opened.push('shop')); + f.home.ensureHomePopupPrefab('shop', 'prefab/monthlyCard', 'cachedMonthlyCardPrefab', () => f.state.opened.push('card')); + f.state.homePrefabLoads[0].done(null, {}); f.state.homePrefabLoads[1].done(null, {}); await flush(); + assert.deepEqual(f.state.opened, ['card']); + assert.ok(f.Home.cachedShopPrefab && f.Home.cachedMonthlyCardPrefab); +}); + +test('Home level warmup uses the same priority-aware JSON API as manual entry', async () => { + const { state, Config } = fixture(); + const request = Config.loadLevelFromBundle(51); + assert.equal(state.jsonCallbacks.length, 0); + state.bundleCallbacks[0](null, {}); + const json = { BLOCK_INFO: [[]] }; + state.jsonCallbacks[0](null, { json }); + assert.equal(await request, json); +}); + +for (const high of [false, true]) { + test(`Home tier high=${high}: RewardWindow loads first and order recovery waits for its success`, async () => { + const f = homeFixture(high); + f.home.onLoad(); f.frame(); + assert.equal(f.state.automatic.includes('getOrder'), false, 'phase one still has precedence'); + await f.ready(); f.state.automatic = []; + f.frame(); + assert.deepEqual(f.state.automatic, []); + assert.equal(f.state.homePrefabLoads.length, 1); + assert.equal(f.state.homePrefabLoads[0].path, 'prefab/RewardWindow'); + assert.equal(f.state.homePrefabLoads[0].priority, 2); + while (f.state.homeFrames.some(item => item.delay < 1)) f.frame(); + const paths = f.state.homePrefabLoads.map(item => item.path); + assert.deepEqual(paths.slice(0, 5), ['RewardWindow', 'newbieGift', 'monthlyCard', 'passCheck', 'shop'].map(n => 'prefab/' + n)); + assert.equal(f.state.automatic.includes('getOrder'), false, 'no recovery while reward images are pending'); + f.state.homePrefabLoads.find(item => item.path === 'prefab/RewardWindow').done(null, {}); + await flush(); + while (f.state.homeFrames.some(item => item.delay < 1)) f.frame(); + assert.equal(f.state.automatic.filter(name => name === 'getOrder').length, 1); + assert.ok(f.state.automatic.indexOf('checkStarter_pack') < f.state.automatic.indexOf('closeAvatar')); + }); +} + +test('RewardWindow failure leaves recovery unstarted and the pending orders untouched; a later preload can retry', async () => { + const f = homeFixture(); + const pending = [{ itemid: 'gold_1', outTradeNo: 'pending-order' }]; + f.Config.GM_INFO.allOutTradeNo = pending; + f.home.homeGameplayWarmupPending = false; + f.home.preloadHomePopupPrefabs(); f.frame(); + f.state.homePrefabLoads[0].done(new Error('reward images offline'), null); await flush(); + while (f.state.homeFrames.some(item => item.delay < 1)) f.frame(); + assert.equal(f.state.automatic.includes('getOrder'), false); + assert.equal(f.Config.GM_INFO.allOutTradeNo, pending); + f.home.preloadHomePopupPrefabs(); f.frame(); + const rewards = f.state.homePrefabLoads.filter(item => item.path === 'prefab/RewardWindow'); + assert.equal(rewards.length, 2); + rewards[1].done(null, {}); await flush(); f.frame(); + assert.equal(f.state.automatic.filter(name => name === 'getOrder').length, 1); +}); + +for (const departed of [false, true]) { + test(`late RewardWindow success does not start recovery over game entry, departed=${departed}`, async () => { + const f = homeFixture(); + f.home.homeGameplayWarmupPending = false; + f.home.preloadHomePopupPrefabs(); f.frame(); + f.Config.isGameplayEntryPreparing = () => true; + if (departed) { f.home.node.destroyed = true; f.state.scene = { name: 'GameScene' }; } + const prefab = {}; f.state.homePrefabLoads[0].done(null, prefab); await flush(); + f.frame(); + assert.equal(f.Home.cachedRewardPrefab, prefab); + assert.equal(f.state.automatic.includes('getOrder'), false); + if (!departed) { + f.Config.isGameplayEntryPreparing = () => false; + f.frame(); + assert.equal(f.state.automatic.filter(name => name === 'getOrder').length, 1); + assert.equal(f.state.automatic[0], 'getOrder', 'recovery still has priority over unstarted gift tasks'); + } + }); +} + +test('Home priorities reorder only unstarted tasks and keep equal priorities in insertion order', () => { + const f = homeFixture(); + for (const [name, priority] of [['normal', 0], ['paid-a', 1], ['order', 2], ['paid-b', 1]]) { + f.home.queueHomeAutomaticTask(name, () => f.state.automatic.push(name), priority); + } + f.home.homeGameplayWarmupPending = false; f.home.flushHomeAutomaticTasks(); + while (f.state.homeFrames.some(item => item.delay < 1)) f.frame(); + assert.deepEqual(f.state.automatic, ['order', 'paid-a', 'paid-b', 'normal']); +}); + +test('Home Prefab waits for nested bundle dependencies and forwards request priority', async () => { + const f = homeFixture(); + f.state.bundleDependencies = { action_bundle: ['shop'], shop: ['UI'] }; + f.state.delayedBundles.add('UI'); + const request = f.home.loadHomePopupPrefab('action_bundle', 'prefab/newbieGift', 'cachedActionPrefab', 5); + assert.deepEqual(f.state.homeBundleLoads.map(item => item.name), ['action_bundle', 'shop', 'UI']); + assert.ok(f.state.homeBundleLoads.every(item => item.priority === 5)); + assert.equal(f.state.homePrefabLoads.length, 0, 'registered root does not mean its dependencies are ready'); + f.finishBundle('UI'); + assert.equal(f.state.homePrefabLoads.length, 1); + const prefab = {}; f.state.homePrefabLoads[0].done(null, prefab); + assert.equal(await request, prefab); +}); + +test('failed Home dependency prevents Prefab parsing; the next request can retry', async () => { + const f = homeFixture(); + f.state.bundleDependencies = { shop: ['UI'] }; f.state.delayedBundles.add('UI'); + const request = f.home.loadHomePopupPrefab('shop', 'prefab/RewardWindow', 'cachedRewardPrefab', 1); + const failed = assert.rejects(request, /dependency offline/); + f.finishBundle('UI', new Error('dependency offline')); await failed; + assert.equal(f.state.homePrefabLoads.length, 0); + assert.equal(f.Home.cachedRewardPrefab, undefined); + const retry = f.home.loadHomePopupPrefab('shop', 'prefab/RewardWindow', 'cachedRewardPrefab', 5); + f.finishBundle('UI'); const prefab = {}; + f.state.homePrefabLoads[0].done(null, prefab); assert.equal(await retry, prefab); +}); + +test('Home cache fast path cannot bypass a missing dependency', async () => { + const f = homeFixture(); + f.state.bundles.set('shop', { name: 'shop', deps: ['UI'] }); + f.Home.cachedShopPrefab = {}; f.state.delayedBundles.add('UI'); + const opened = f.home.ensureHomePopupPrefab('shop', 'prefab/shop', 'cachedShopPrefab', () => f.state.opened.push('shop')); + assert.equal(opened, false); assert.deepEqual(f.state.opened, []); + assert.equal(f.state.homePrefabLoads.length, 0); + f.finishBundle('UI'); f.state.homePrefabLoads[0].done(null, {}); await flush(); + assert.deepEqual(f.state.opened, ['shop']); +}); + +test('Home dependency removed during Prefab loading cannot leave a usable-looking cache', async () => { + const f = homeFixture(); f.state.bundleDependencies = { shop: ['UI'] }; + const request = f.home.loadHomePopupPrefab('shop', 'prefab/shop', 'cachedShopPrefab', 1); + const failed = assert.rejects(request, /预制体已失效/); + f.state.bundles.delete('UI'); f.state.homePrefabLoads[0].done(null, {}); await failed; + assert.equal(f.Home.cachedShopPrefab, undefined); +}); + +test('Home dependency traversal handles repeated and cyclic bundle references', async () => { + const f = homeFixture(); f.state.bundleDependencies = { shop: ['action_bundle'], action_bundle: ['shop'] }; + const request = f.home.loadHomePopupPrefab('shop', 'prefab/shop', 'cachedShopPrefab', 1); + assert.deepEqual(f.state.homeBundleLoads.map(item => item.name), ['shop', 'action_bundle']); + f.state.homePrefabLoads[0].done(null, {}); await request; +}); + +for (const stage of ['dependency', 'prefab']) { + test(`newbie gift callback at ${stage} cannot create nodes after Home destruction`, () => { + const f = homeFixture(); + f.state.bundleDependencies = { action_bundle: ['shop'] }; f.state.delayedBundles.add('shop'); + f.cc.instantiate = () => assert.fail('late gift created a node'); + f.Home.prototype.openStarter_pack.call(f.home); + if (stage === 'prefab') f.finishBundle('shop'); + f.home.node.destroyed = true; f.state.scene = { name: 'GameScene' }; + if (stage === 'dependency') { + f.finishBundle('shop'); assert.equal(f.state.homePrefabLoads.length, 0); + } else { + const prefab = {}; f.state.homePrefabLoads[0].done(null, prefab); + assert.equal(f.Home.cachedActionPrefab, prefab, 'completed asset can still be cached'); + } + }); +} + +for (const stage of ['not-started', 'shop-request', 'first-order']) { + test(`startGame preserves pending orders while recovery is ${stage}`, () => { + const f = homeFixture(); f.cc.sys.platform = 'wechat'; + const orders = [{ itemid: 'gold_1', outTradeNo: 'order-a', goodsPrice: 600 }, + { itemid: 'gold_2', outTradeNo: 'order-b', goodsPrice: 3600 }]; + f.Config.GM_INFO.allOutTradeNo = orders; f.Config.GM_INFO.hp = 1; + f.cc.fx.AudioManager = { _instance: { playEffect() {} } }; + f.cc.fx.GameTool.getUserPowerTime = () => false; + f.globals.wx.offShow = f.globals.wx.offHide = () => {}; + f.home.node.getComponent = () => ({ _touch: true, setTouch() {} }); + f.home.playGameEntryTransition = () => {}; + let entries = 0; + f.Config.LEVEL_INFO_init = () => { entries++; f.Config.isGameplayEntryPreparing = () => true; }; + const requests = [], grants = [], callbacks = []; + f.globals.Utils.setPayInfo = (done, order) => { requests.push(order); callbacks.push(done); }; + f.globals.MiniGameSdk.API.yinli_Pay = () => {}; + f.cc.fx.GameTool.shopBuy = id => grants.push(id); + f.home.openLoad = f.home.closeLoad = f.home.updateCoin = () => {}; + if (stage !== 'not-started') f.Home.prototype.getOrder.call(f.home); + if (stage === 'first-order') f.state.shopResponse({ code: 1, data: { shopDouble: {} } }); + f.home.startGame(); + assert.equal(entries, 1); + assert.equal(f.Config.GM_INFO.allOutTradeNo, orders, 'entering gameplay must not discard the recovery list'); + assert.equal(f.Config.GM_INFO.allOutTradeNo.length, 2); + f.home.node.destroyed = true; f.state.scene = { name: 'GameScene' }; + if (stage === 'not-started') { + assert.deepEqual(requests, []); + return; + } + if (stage === 'shop-request') f.state.shopResponse({ code: 1, data: { shopDouble: {} } }); + while (callbacks.length) callbacks.shift()({ code: 1 }); + assert.deepEqual(requests, ['order-a', 'order-b']); + assert.deepEqual(grants, ['gold_1', 'gold_2']); + assert.equal(f.Config.GM_INFO.allOutTradeNo.length, 0); + }); +} + +for (const departed of [false, true]) { + test(`order recovery callback preserves fulfillment without touching entry/old Home UI, departed=${departed}`, () => { + const f = homeFixture(); f.cc.sys.platform = 'wechat'; + f.Config.GM_INFO.allOutTradeNo = [{ itemid: 'gold_1', outTradeNo: 'paid-order', goodsPrice: 600 }]; + let payDone, uiCalls = 0; const grants = []; + f.globals.Utils.setPayInfo = done => { payDone = done; }; + f.globals.MiniGameSdk.API.yinli_Pay = () => {}; + f.cc.fx.GameTool.shopBuy = (id, compensate) => grants.push({ id, compensate }); + f.home.openLoad = f.home.closeLoad = f.home.updateCoin = () => uiCalls++; + f.Home.prototype.getOrder.call(f.home); + f.state.shopResponse({ code: 1, data: { shopDouble: {} } }); + assert.equal(uiCalls, 1); + if (departed) { f.home.node.destroyed = true; f.state.scene = { name: 'GameScene' }; } + else f.Config.isGameplayEntryPreparing = () => true; + payDone({ code: 1 }); + assert.deepEqual(grants, [{ id: 'gold_1', compensate: true }]); + assert.equal(uiCalls, 1, 'late payment must not close entry Loading or update old coin nodes'); + assert.equal(f.state.automatic.includes('coin-images'), false); + assert.equal(f.Config.GM_INFO.allOutTradeNo.length, 0); + }); +} + +test('monthly-card fulfillment does not dereference a missing Canvas during scene replacement', async () => { + const f = homeFixture(); let complete, grants = 0; + f.globals.Utils.setMonthlyCard = (type, done) => { complete = done; }; + f.cc.fx.GameTool.shopBuy = () => grants++; + f.cc.fx.GameTool.getMonthlyCardValidityDays = () => Promise.resolve({ days: 30, time: Date.now() }); + f.cc.fx.GameTool.setUserHealth = () => {}; + f.cc.fx.StorageMessage = { setStorage() {} }; + f.Home.prototype.buyMonthCard.call(f.home, 'month_Card', 'month-order'); + f.cc.find = () => null; f.state.scene = { name: 'GameScene' }; f.home.node.destroyed = true; + complete({ code: 1 }); await flush(); + assert.equal(grants, 1); + assert.equal(f.state.automatic.includes('closeLoad'), false); +}); + +for (const departed of [false, true]) { + for (const stage of ['dependency', 'prefab']) { + for (const failed of [false, true]) { + test(`jungle late ${stage} callback cannot touch Home/entry UI, departed=${departed}, failed=${failed}`, () => { + const f = homeFixture(); + f.home.jungleTreasureServerData = {}; + f.state.bundleDependencies = { jungle_treasure: ['UI'] }; f.state.delayedBundles.add('UI'); + f.home.showJungleTreasure = () => f.state.opened.push('jungle'); + f.home.loadJungleTreasurePrefab(true); + if (stage === 'prefab') f.finishBundle('UI'); + if (departed) { f.home.node.destroyed = true; f.state.scene = { name: 'GameScene' }; } + else f.Config.isGameplayEntryPreparing = () => true; + const error = failed ? new Error('offline') : null; + if (stage === 'dependency') { + f.finishBundle('UI', error); + assert.equal(f.state.homePrefabLoads.length, 0); + } else { + const prefab = failed ? null : {}; + f.state.homePrefabLoads[0].done(error, prefab); + if (!failed) assert.equal(f.Home.cachedJungleTreasurePrefab, prefab); + } + assert.equal(f.home.jungleTreasureLoading, false); + assert.equal(f.home.jungleTreasureOpenAfterLoad, false); + assert.deepEqual(f.state.opened, []); + assert.equal(f.state.automatic.includes('closeLoad'), false); + }); + } + } +} + +test('cached jungle settlement bundle still waits for its missing dependencies', () => { + const f = homeFixture(); + f.state.bundles.set('jungle_treasure', { deps: ['UI'], load: (path, type, done) => { + f.state.homePrefabLoads.push({ path, done }); + } }); + f.Home.cachedJungleOverPrefab = {}; f.state.delayedBundles.add('UI'); + f.home.showJungleOver = () => f.state.opened.push('settlement'); + f.home.loadJungleOverPrefab(); + assert.equal(f.state.homePrefabLoads.length, 0); assert.deepEqual(f.state.opened, []); + f.finishBundle('UI'); + assert.equal(f.state.homePrefabLoads[0].path, 'prefab/jungleOver'); + f.state.homePrefabLoads[0].done(null, {}); + assert.deepEqual(f.state.opened, ['settlement']); +}); diff --git a/tools/verify-popup-preload-policy.cjs b/tools/verify-popup-preload-policy.cjs index e20b3b33..7a7ebfb9 100644 --- a/tools/verify-popup-preload-policy.cjs +++ b/tools/verify-popup-preload-policy.cjs @@ -9,6 +9,7 @@ const source = ts.transpileModule( ).outputText; const subpackageRequests = []; +const bundleRequests = []; const bundles = new Map(); let activePrefabLoads = 0; let maxPrefabLoads = 0; @@ -33,6 +34,7 @@ const cc = { getBundle: name => bundles.get(name) || null, removeBundle(bundle) { bundles.delete(bundle.name); }, loadBundle(name, callback) { + bundleRequests.push(name); activePrefabLoads++; maxPrefabLoads = Math.max(maxPrefabLoads, activePrefabLoads); const bundle = { @@ -64,10 +66,14 @@ cc.fx.GameTool = gameTool; (async () => { const diskReady = gameTool.predownloadGameplayPopupSubpackages(false); assert.equal(subpackageRequests.length, 5, 'all five disk downloads should start together'); + const expectedOrder = ['pause', 'win', 'propWindow', 'NewMode', 'lose']; + assert.deepEqual(subpackageRequests.map(request => request.name), expectedOrder, + 'disk requests must follow the gameplay warmup order'); subpackageRequests.forEach(request => request.success()); assert.equal(await diskReady, true); assert.equal(await gameTool.warmupHomeGameplayPopupAssets(), true); + assert.deepEqual(bundleRequests, expectedOrder, 'Bundle requests must use the same order'); assert.equal(maxPrefabLoads, 2, 'Prefab decode concurrency must stay at two'); assert.equal(instantiateCalls, 0, 'HomeScene must not instantiate popup nodes');