import { SpriteFrame, Texture2D, ImageAsset, assetManager } from 'cc'; import { HttpUtil } from './HttpUtil'; import { ApiLevelData, ApiResponse, RuntimeLevelConfig } from '../types/LevelTypes'; import { API_ENDPOINTS, API_TIMEOUT } from '../config/ApiConfig'; /** * 进度回调类型 * @param progress 进度值 0-1 * @param message 进度消息 */ export type ProgressCallback = (progress: number, message: string) => void; /** * 关卡数据管理器 * 单例模式,负责从 API 获取关卡数据并按需加载图片 */ export class LevelDataManager { private static _instance: LevelDataManager | null = null; /** API 请求重试次数 */ private readonly API_RETRY_COUNT = 2; /** API 返回的原始关卡数据 */ private _apiData: ApiLevelData[] = []; /** 运行时关卡配置缓存(按需填充) */ private _levelConfigs: Map = new Map(); /** 是否已成功从 API 获取数据 */ private _hasApiData: boolean = false; /** 图片缓存:URL -> SpriteFrame */ private _imageCache: Map = new Map(); /** 正在加载中的关卡索引集合 */ private _loadingLevels: Set = new Set(); /** * 获取单例实例 */ static get instance(): LevelDataManager { if (!this._instance) { this._instance = new LevelDataManager(); } return this._instance; } /** * 私有构造函数 */ private constructor() {} /** * 初始化:从 API 获取数据并预加载第一关图片 * @param onProgress 进度回调 * @returns 是否初始化成功 */ async initialize(onProgress?: ProgressCallback): Promise { console.log('[LevelDataManager] 开始初始化'); try { // 阶段1: 获取 API 数据 (0-30%) onProgress?.(0, '正在请求服务端数据...'); const apiData = await this._fetchApiData(onProgress); if (!apiData || apiData.length === 0) { console.warn('[LevelDataManager] API 返回空数据'); onProgress?.(0.3, '网络异常,请重新打开游戏'); return false; } console.log(`[LevelDataManager] 获取到 ${apiData.length} 个关卡数据`); this._apiData = apiData; this._hasApiData = true; onProgress?.(0.3, `获取到 ${apiData.length} 个关卡`); // 阶段2: 只预加载第一关图片 (30-80%) const firstLevel = apiData[0]; onProgress?.(0.3, '正在加载游戏必备资源...'); const spriteFrame = await this._loadImage(firstLevel.imageUrl); if (spriteFrame) { this._levelConfigs.set(0, this._createRuntimeConfig(firstLevel, spriteFrame)); } console.log('[LevelDataManager] 初始化完成,第一关资源已加载'); onProgress?.(0.8, '游戏资源加载完成'); return true; } catch (error) { console.error('[LevelDataManager] 初始化失败:', error); onProgress?.(0.3, '网络异常,请重新打开游戏'); return false; } } /** * 获取指定关卡配置 * @param index 关卡索引 */ getLevelConfig(index: number): RuntimeLevelConfig | null { return this._levelConfigs.get(index) ?? null; } /** * 获取关卡总数 */ getLevelCount(): number { return this._apiData.length; } /** * 获取指定索引的关卡 ID * @param index 关卡索引 */ getLevelId(index: number): string | null { if (index < 0 || index >= this._apiData.length) { return null; } return this._apiData[index].id; } /** * 检查指定索引的关卡是否已通关 * @param index 关卡索引 */ isLevelCompleted(index: number): boolean { if (index < 0 || index >= this._apiData.length) { return false; } return this._apiData[index].completed; } /** * 获取第一个未通关的关卡索引 * 遍历关卡列表,返回第一个 completed === false 的索引 * 如果全部通关,返回最后一关的索引 * @returns 第一个未通关关卡索引(0-based) */ getFirstUncompletedIndex(): number { for (let i = 0; i < this._apiData.length; i++) { if (!this._apiData[i].completed) { return i; } } // 全部通关,返回最后一关 return Math.max(0, this._apiData.length - 1); } /** * 获取指定索引之后第一个未通关的关卡索引 * @param afterIndex 从该索引之后开始查找(不含该索引) * @returns 下一个未通关关卡索引,如果后续全部通关则返回 -1 */ getNextUncompletedIndex(afterIndex: number): number { for (let i = afterIndex + 1; i < this._apiData.length; i++) { if (!this._apiData[i].completed) { return i; } } return -1; } /** * 标记指定关卡为已通关(本地缓存更新) * @param index 关卡索引 */ markLevelCompleted(index: number): void { if (index < 0 || index >= this._apiData.length) { return; } this._apiData[index].completed = true; // 同时更新运行时配置的 completed 状态 const config = this._levelConfigs.get(index); if (config) { this._levelConfigs.set(index, { ...config, completed: true }); } } /** * 根据已完成的关卡 ID 列表,计算最高已完成关卡索引 * @param completedLevelIds 服务端返回的已完成关卡 ID * @returns 最高已完成关卡的索引(0-based),无匹配返回 -1 */ getMaxCompletedIndex(completedLevelIds: string[]): number { if (!this._hasApiData || completedLevelIds.length === 0) { return -1; } const completedSet = new Set(completedLevelIds); let maxIndex = -1; for (let i = 0; i < this._apiData.length; i++) { if (completedSet.has(this._apiData[i].id)) { maxIndex = i; } } return maxIndex; } /** * 检查是否有 API 数据 */ hasApiData(): boolean { return this._hasApiData && this._apiData.length > 0; } /** * 检查指定关卡图片是否已加载 * @param index 关卡索引 */ isLevelImageLoaded(index: number): boolean { return this._levelConfigs.has(index); } /** * 确保指定关卡资源已准备好 * 如果资源未加载,会立即加载 * @param index 关卡索引 * @returns 加载的关卡配置,失败返回 null */ async ensureLevelReady(index: number): Promise { // 检查索引有效性 if (index < 0 || index >= this._apiData.length) { console.warn(`[LevelDataManager] 关卡索引无效: ${index}`); return null; } // 检查缓存 const cached = this._levelConfigs.get(index); if (cached) { return cached; } // 检查是否正在加载 if (this._loadingLevels.has(index)) { console.log(`[LevelDataManager] 关卡 ${index} 正在加载中...`); return null; } // 开始加载 this._loadingLevels.add(index); console.log(`[LevelDataManager] 开始加载关卡 ${index} 资源...`); const data = this._apiData[index]; const spriteFrame = await this._loadImage(data.imageUrl); this._loadingLevels.delete(index); if (!spriteFrame) { console.error(`[LevelDataManager] 加载关卡 ${index} 图片失败`); return null; } const config = this._createRuntimeConfig(data, spriteFrame); this._levelConfigs.set(index, config); console.log(`[LevelDataManager] 关卡 ${index} 资源加载完成`); return config; } /** * 用 enter 接口返回的数据更新运行时关卡配置(填充答案和线索) */ updateLevelDetails(index: number, details: { answer: string; hint1: string | null; hint2: string | null; hint3: string | null }): void { const config = this._levelConfigs.get(index); if (!config) { console.warn(`[LevelDataManager] 关卡 ${index} 配置不存在,无法更新详情`); return; } this._levelConfigs.set(index, { ...config, answer: details.answer, clue1: details.hint1 ?? null, clue2: details.hint2 ?? null, clue3: details.hint3 ?? null, }); console.log(`[LevelDataManager] 关卡 ${index} 详情已更新`); } /** * 预加载下一关图片(静默加载,不阻塞) * 在进入当前关卡后调用,提前加载下一关资源 * @param currentIndex 当前关卡索引 */ preloadNextLevel(currentIndex: number): void { const nextIndex = currentIndex + 1; // 检查是否有下一关 if (nextIndex >= this._apiData.length) { console.log('[LevelDataManager] 没有下一关了'); return; } // 检查是否已加载 if (this._levelConfigs.has(nextIndex)) { console.log(`[LevelDataManager] 下一关 ${nextIndex} 已加载`); return; } // 检查是否正在加载 if (this._loadingLevels.has(nextIndex)) { console.log(`[LevelDataManager] 下一关 ${nextIndex} 正在加载中`); return; } // 异步加载,不等待 console.log(`[LevelDataManager] 开始预加载下一关 ${nextIndex}...`); this.ensureLevelReady(nextIndex).catch(err => { console.error(`[LevelDataManager] 预加载下一关失败:`, err); }); } /** * 从 API 获取关卡数据(带重试机制) * @param onProgress 进度回调 */ private async _fetchApiData(onProgress?: ProgressCallback): Promise { let lastError: Error | null = null; for (let attempt = 1; attempt <= this.API_RETRY_COUNT; attempt++) { const progress = (attempt - 1) / this.API_RETRY_COUNT * 0.3; try { onProgress?.(progress, `正在请求服务端数据 (第${attempt}次)...`); const response = await HttpUtil.get(API_ENDPOINTS.LEVELS, API_TIMEOUT.DEFAULT); if (!response.success) { console.warn(`[LevelDataManager] API 返回失败, 消息: ${response.message}`); lastError = new Error(response.message || 'API 返回失败'); } else { return response.data.levels; } } catch (error) { console.warn(`[LevelDataManager] 第${attempt}次请求失败:`, error); lastError = error as Error; } // 重试逻辑(无论是 response.success 为 false 还是抛出异常) if (attempt < this.API_RETRY_COUNT) { onProgress?.(progress + 0.05, `请求失败,正在重试...`); await this._delay(1000); } } console.error('[LevelDataManager] API 请求重试全部失败:', lastError); return null; } private _delay(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } /** * 创建运行时关卡配置 * @param data API 关卡数据 * @param spriteFrame 已加载的精灵帧 */ private _createRuntimeConfig(data: ApiLevelData, spriteFrame: SpriteFrame | null): RuntimeLevelConfig { return { id: data.id, name: `第${data.level}关`, spriteFrame: spriteFrame, clue1: data.hint1, clue2: data.hint2, clue3: data.hint3, answer: data.answer, completed: data.completed, }; } /** * 加载远程图片为 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); }); }); } /** * 清除缓存 */ clearCache(): void { this._apiData = []; this._levelConfigs.clear(); this._loadingLevels.clear(); this._hasApiData = false; this._imageCache.clear(); console.log('[LevelDataManager] 缓存已清除'); } }