import { assetManager } from 'cc'; import { GBundle } from './ConfigRes'; import { BattleType } from '../manager/ChapterDataManager'; /** * 进战斗前预载 home(priority=3);audio 为远程包(不计 30M 包体),运行时 loadBundle 从 CDN 拉取。 * loadBundle 只拉分包索引 config,不会把包内贴图/预制体载入内存; * 具体资源在 ensurePrefab / getNodeAsync 时按依赖图逐条加载。 */ const BATTLE_ENTRY_BUNDLES: string[] = [GBundle.Home, GBundle.Audio]; const JIUJIUWOYA_BUNDLE_GROUP: string[] = [GBundle.jiujiuwoya, GBundle.jiujiuwoyaCard]; /** 离开战斗时可卸载的分包(保留 home/audio 等大厅共用包) */ const BATTLE_RELEASE_BUNDLES: string[] = [ GBundle.tetriCards, GBundle.tetriWeapon, GBundle.tetriGame, GBundle.tetriMap, GBundle.BattleGameMonster, GBundle.jiujiuwoya, GBundle.jiujiuwoyaCard, ]; // 注意:故意不卸 battleGameWeapon——二次进关高频用 weapon1/hitEffect; // 若每次 removeBundle,体感会比「以前一次预载一堆」更慢。 /** 进大厅前需加载的分包(启动仅保留 main/first/comm) */ const HALL_BUNDLES: string[] = [ GBundle.Comm2, GBundle.Home, GBundle.Items, GBundle.Audio, ]; /** 进方块战斗前必须就绪的分包(openPanel 之前) */ const TETRI_BATTLE_CORE_BUNDLES: string[] = [ GBundle.tetriGame, GBundle.tetriMap, GBundle.tetriCards, GBundle.tetriWeapon, GBundle.BattleGameMonster, ]; /** * 战斗/大厅分包与战斗内增量资源加载。 * 目标:缩减启动与进局内峰值内存,怪物按波次再拉。 */ export class BattleResLoader { private static _bundleInflight = new Map>(); static loadBundle(bundleName: string): Promise { if (assetManager.getBundle(bundleName)) { return Promise.resolve(true); } const pending = this._bundleInflight.get(bundleName); if (pending) { return pending; } const task = new Promise((resolve) => { assetManager.loadBundle(bundleName, (err) => { if (err) { console.warn(`[BattleResLoader] loadBundle ${bundleName} 失败`, err); resolve(false); } else { resolve(true); } }); }); this._bundleInflight.set(bundleName, task); task.then( () => { this._bundleInflight.delete(bundleName); }, () => { this._bundleInflight.delete(bundleName); }, ); return task; } static async ensureBundles(bundleNames: string[]): Promise { await Promise.all(bundleNames.map((name) => this.loadBundle(name))); } /** 进局最小引导:注册 home;audio 远程包按需 loadBundle */ static async ensureBattleEntry(): Promise { await this.ensureBundles(BATTLE_ENTRY_BUNDLES); if (gg?.game?.CurentSelectBattleType === BattleType.OrangeWeaponMode_Ya) { await this.ensureBundles(JIUJIUWOYA_BUNDLE_GROUP); } } /** 打开战斗界面前:确保局内 UI/地图/卡牌等核心分包已 loadBundle */ static async ensureBattleCoreBundles(): Promise { if (gg?.game?.CurentSelectBattleType === BattleType.OrangeWeaponMode_Ya) { await this.ensureBundles(JIUJIUWOYA_BUNDLE_GROUP); return; } await this.ensureBundles(TETRI_BATTLE_CORE_BUNDLES); } /** @deprecated 请用 ensureBattleEntry;保留别名避免遗漏调用点 */ static async ensureBattleBundles(): Promise { return this.ensureBattleEntry(); } /** * 回大厅卸载战斗分包(releaseAll + removeBundle)。 * battleGameWeapon 不在列表中,避免二次进关冷拉武器。 */ static releaseBattleBundles(): void { for (let i = 0; i < BATTLE_RELEASE_BUNDLES.length; i++) { const bundleName = BATTLE_RELEASE_BUNDLES[i]; this._bundleInflight.delete(bundleName); const bundle = assetManager.getBundle(bundleName); if (!bundle) { continue; } try { bundle.releaseAll(); assetManager.removeBundle(bundle); } catch (e) { console.warn(`[BattleResLoader] releaseBattleBundles ${bundleName}`, e); } } } static ensureHallBundles(): Promise { return this.ensureBundles(HALL_BUNDLES); } /** 按需预加载怪物 prefab(仅缓存,不实例化) */ static preloadMonsterModels(models: string[] | Iterable): Promise { const names: string[] = []; const pushName = (m: unknown) => { if (typeof m === 'string' && m.length > 0) names.push(m); }; if (Array.isArray(models)) { for (let i = 0; i < models.length; i++) pushName(models[i]); } else if (models) { for (const m of models) pushName(m); } const unique = Array.from(new Set(names)); if (unique.length === 0) { return Promise.resolve(); } return Promise.all( unique.map((model) => gg.res.ensurePrefab(model, 'prefab/monster/', GBundle.BattleGameMonster)), ).then(() => undefined); } /** * 进关预热:仅 weapon1 子弹(首发武器)。 * 其它武器在三选一选中后再 {@link preloadWeaponCombatAssets}。 */ static async preloadWeapon1Bullet(): Promise { await this.preloadWeaponCombatAssets(1); } /** * 预加载单个武器的战斗资源: * - `prefab/武器/weaponN` 子弹 * - 武器表 `hitEffect`(格式 spine名|预制体名)→ `prefab/爆炸/预制体名` */ static async preloadWeaponCombatAssets(weaponIdOrData: number | ITableWeapon2 | null | undefined): Promise { if (!gg?.res) return; let weaponId = 0; let hitEffect = ''; if (typeof weaponIdOrData === 'number') { weaponId = weaponIdOrData; const conf = gg.data?.project?.getWeaponData?.(weaponId); hitEffect = conf?.hitEffect ?? ''; } else if (weaponIdOrData) { weaponId = weaponIdOrData.weaponid; hitEffect = weaponIdOrData.hitEffect ?? ''; } if (!Number.isFinite(weaponId) || weaponId <= 0) return; await this.loadBundle(GBundle.BattleGameWeapon); const tasks: Promise[] = [ gg.res.ensurePrefab(`weapon${weaponId}`, 'prefab/武器/', GBundle.BattleGameWeapon), ]; const bombName = this._parseHitEffectPrefabName(hitEffect); if (bombName) { tasks.push(gg.res.ensurePrefab(bombName, 'prefab/爆炸/', GBundle.BattleGameWeapon)); } await Promise.all(tasks); } /** 批量预热(如三选一全选) */ static preloadWeaponCombatAssetsBatch(weapons: Array): void { for (let i = 0; i < weapons.length; i++) { void this.preloadWeaponCombatAssets(weapons[i]); } } /** Boss 副本投掷物 / 爆炸特效(同步 getNode 前需已 ensure) */ static async preloadBossSkillAssets(): Promise { if (!gg?.res) return; await this.loadBundle(GBundle.BattleGameWeapon); await Promise.all([ gg.res.ensurePrefab('bossWeapon1', 'prefab/武器/', GBundle.BattleGameWeapon), gg.res.ensurePrefab('砖块爆炸', 'prefab/爆炸/', GBundle.BattleGameWeapon), ]); } /** hitEffect: `spine名|爆炸预制体名` → 取预制体名 */ private static _parseHitEffectPrefabName(hitEffect: string | null | undefined): string { if (!hitEffect || typeof hitEffect !== 'string') return ''; const parts = hitEffect.split('|'); const name = (parts.length >= 2 ? parts[1] : parts[0])?.trim(); return name || ''; } }