Files
mp-xieyingeng/assets/scripts/utils/LevelDataManager.ts
2026-04-19 14:19:13 +08:00

430 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 [spriteFrame1, spriteFrame2] = await Promise.all([
this._loadImage(firstLevel.image1Url),
this._loadImage(firstLevel.image2Url),
]);
this._levelConfigs.set(0, this._createRuntimeConfig(firstLevel, spriteFrame1, spriteFrame2));
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 [spriteFrame1, spriteFrame2] = await Promise.all([
this._loadImage(data.image1Url),
this._loadImage(data.image2Url),
]);
this._loadingLevels.delete(index);
if (!spriteFrame1) {
console.error(`[LevelDataManager] 加载关卡 ${index} 图片1失败`);
return null;
}
const config = this._createRuntimeConfig(data, spriteFrame1, spriteFrame2);
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 spriteFrame1 已加载的图片1精灵帧
* @param spriteFrame2 已加载的图片2精灵帧
*/
private _createRuntimeConfig(data: ApiLevelData, spriteFrame1: SpriteFrame | null, spriteFrame2: SpriteFrame | null): RuntimeLevelConfig {
return {
id: data.id,
name: `${data.level}`,
spriteFrame1: spriteFrame1,
spriteFrame2: spriteFrame2,
image1Description: data.image1Description,
image2Description: data.image2Description,
punchline: data.punchline,
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] 缓存已清除');
}
}