import { SpriteFrame, Texture2D, ImageAsset, assetManager } from 'cc'; import { RuntimeLevelConfig } from '../types/LevelTypes'; import { NextLevelData } from '../types/ApiTypes'; /** * 进度回调类型 * @param progress 进度值 0-1 * @param message 进度消息 */ export type ProgressCallback = (progress: number, message: string) => void; /** * 关卡数据管理器 * 单例模式,管理当前关卡和预加载关卡的图片资源 * 不再依赖全量关卡列表,由外部传入 NextLevelData 驱动 */ export class LevelDataManager { private static _instance: LevelDataManager | null = null; /** 运行时关卡配置缓存(按 levelId 索引) */ private _levelConfigs: Map = new Map(); /** 图片缓存:URL -> SpriteFrame */ private _imageCache: Map = new Map(); /** 正在加载中的关卡 ID 集合 */ private _loadingLevels: Set = new Set(); /** 是否已初始化 */ private _initialized: boolean = false; /** * 获取单例实例 */ static get instance(): LevelDataManager { if (!this._instance) { this._instance = new LevelDataManager(); } return this._instance; } private constructor() {} /** * 初始化:加载首关图片 * 由 PageLoading 在获取 game-data 后调用,传入 nextLevel 数据 * @param nextLevel 首关数据(来自 game-data 接口) * @param onProgress 进度回调 * @returns 是否初始化成功 */ async initialize(nextLevel: NextLevelData, onProgress?: ProgressCallback): Promise { console.log(`[LevelDataManager] 开始初始化,加载关卡 ${nextLevel.level}`); try { onProgress?.(0.3, '正在加载游戏必备资源...'); const config = await this._loadLevelFromData(nextLevel); if (!config) { console.error('[LevelDataManager] 初始化失败:图片加载失败'); onProgress?.(0.3, '资源加载失败,请重新打开游戏'); return false; } this._initialized = true; console.log('[LevelDataManager] 初始化完成'); onProgress?.(0.8, '游戏资源加载完成'); return true; } catch (error) { console.error('[LevelDataManager] 初始化失败:', error); onProgress?.(0.3, '网络异常,请重新打开游戏'); return false; } } /** * 是否已初始化 */ isInitialized(): boolean { return this._initialized; } /** * 获取指定关卡配置(按 ID) * @param levelId 关卡 ID */ getLevelConfig(levelId: string): RuntimeLevelConfig | null { return this._levelConfigs.get(levelId) ?? null; } /** * 用 enter 接口返回的数据更新运行时关卡配置(填充答案和线索) * @param levelId 关卡 ID * @param details enter 接口返回的详情 */ updateLevelDetails(levelId: string, details: { answer: string; image1Description: string | null; image2Description: string | null; punchline: string | null; hint1: string | null; hint2: string | null; hint3: string | null; }): void { const config = this._levelConfigs.get(levelId); if (!config) { console.warn(`[LevelDataManager] 关卡 ${levelId} 配置不存在,无法更新详情`); return; } this._levelConfigs.set(levelId, { ...config, answer: details.answer, image1Description: details.image1Description ?? config.image1Description, image2Description: details.image2Description ?? config.image2Description, punchline: details.punchline ?? config.punchline, clue1: details.hint1 ?? null, clue2: details.hint2 ?? null, clue3: details.hint3 ?? null, }); console.log(`[LevelDataManager] 关卡 ${levelId} 详情已更新`); } /** * 加载并缓存一个关卡(同步等待图片加载完成) * 用于 game-data 返回的 nextLevel 或 complete 返回的 nextLevel * @param data NextLevelData * @returns 加载好的 RuntimeLevelConfig,失败返回 null */ async ensureLevelReady(data: NextLevelData): Promise { // 检查缓存 const cached = this._levelConfigs.get(data.id); if (cached) { return cached; } // 检查是否正在加载 if (this._loadingLevels.has(data.id)) { console.log(`[LevelDataManager] 关卡 ${data.id} 正在加载中...`); return null; } return this._loadLevelFromData(data); } /** * 预加载关卡图片(静默加载,不阻塞) * 用于 enter 返回的 preloadNextLevel * @param data NextLevelData */ preloadLevel(data: NextLevelData): void { // 已缓存 if (this._levelConfigs.has(data.id)) { console.log(`[LevelDataManager] 关卡 ${data.id} 已加载`); return; } // 正在加载 if (this._loadingLevels.has(data.id)) { console.log(`[LevelDataManager] 关卡 ${data.id} 正在加载中`); return; } // 异步加载,不等待 console.log(`[LevelDataManager] 开始预加载关卡 ${data.id}...`); this._loadLevelFromData(data).catch(err => { console.error(`[LevelDataManager] 预加载关卡失败:`, err); }); } /** * 检查指定关卡图片是否已加载 * @param levelId 关卡 ID */ isLevelImageLoaded(levelId: string): boolean { return this._levelConfigs.has(levelId); } /** * 清除缓存 */ clearCache(): void { this._levelConfigs.clear(); this._loadingLevels.clear(); this._imageCache.clear(); this._initialized = false; console.log('[LevelDataManager] 缓存已清除'); } /** * 从 NextLevelData 加载图片并创建 RuntimeLevelConfig */ private async _loadLevelFromData(data: NextLevelData): Promise { this._loadingLevels.add(data.id); console.log(`[LevelDataManager] 开始加载关卡 ${data.id} 资源...`); try { const [spriteFrame1, spriteFrame2] = await Promise.all([ this._loadImage(data.image1Url), this._loadImage(data.image2Url), ]); if (!spriteFrame1) { console.error(`[LevelDataManager] 加载关卡 ${data.id} 图片1失败`); return null; } const config = this._createRuntimeConfig(data, spriteFrame1, spriteFrame2); this._levelConfigs.set(data.id, config); console.log(`[LevelDataManager] 关卡 ${data.id} 资源加载完成`); return config; } finally { this._loadingLevels.delete(data.id); } } /** * 创建运行时关卡配置 */ private _createRuntimeConfig(data: NextLevelData, spriteFrame1: SpriteFrame | null, spriteFrame2: SpriteFrame | null): RuntimeLevelConfig { return { id: data.id, name: `第${data.level}关`, spriteFrame1, spriteFrame2, image1Description: data.image1Description, image2Description: data.image2Description, punchline: data.punchline, clue1: data.hint1, clue2: data.hint2, clue3: data.hint3, answer: data.answer, completed: false, timeLimit: data.timeLimit, }; } /** * 加载远程图片为 SpriteFrame * @param url 图片 URL */ private async _loadImage(url: string): Promise { // 检查缓存 const cached = this._imageCache.get(url); if (cached) { return cached; } return new Promise((resolve) => { assetManager.loadRemote(url, (err, imageAsset) => { if (err) { console.error(`[LevelDataManager] 加载图片失败: ${url}`, err); resolve(null); return; } const texture = new Texture2D(); texture.image = imageAsset; const spriteFrame = new SpriteFrame(); spriteFrame.texture = texture; // 缓存 this._imageCache.set(url, spriteFrame); resolve(spriteFrame); }); }); } }