421 lines
13 KiB
TypeScript
421 lines
13 KiB
TypeScript
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<number, RuntimeLevelConfig> = new Map();
|
||
|
||
/** 是否已成功从 API 获取数据 */
|
||
private _hasApiData: boolean = false;
|
||
|
||
/** 图片缓存:URL -> SpriteFrame */
|
||
private _imageCache: Map<string, SpriteFrame> = new Map();
|
||
|
||
/** 正在加载中的关卡索引集合 */
|
||
private _loadingLevels: Set<number> = 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<boolean> {
|
||
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<RuntimeLevelConfig | null> {
|
||
// 检查索引有效性
|
||
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<ApiLevelData[] | null> {
|
||
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<ApiResponse>(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<void> {
|
||
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<SpriteFrame | null> {
|
||
// 检查缓存
|
||
const cached = this._imageCache.get(url);
|
||
if (cached) {
|
||
return cached;
|
||
}
|
||
|
||
return new Promise((resolve) => {
|
||
assetManager.loadRemote<ImageAsset>(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] 缓存已清除');
|
||
}
|
||
}
|