273 lines
8.6 KiB
TypeScript
273 lines
8.6 KiB
TypeScript
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<string, RuntimeLevelConfig> = new Map();
|
||
|
||
/** 图片缓存:URL -> SpriteFrame */
|
||
private _imageCache: Map<string, SpriteFrame> = new Map();
|
||
|
||
/** 正在加载中的关卡 ID 集合 */
|
||
private _loadingLevels: Set<string> = 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<boolean> {
|
||
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<RuntimeLevelConfig | null> {
|
||
// 检查缓存
|
||
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<RuntimeLevelConfig | null> {
|
||
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<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);
|
||
});
|
||
});
|
||
}
|
||
}
|