feat: 对接最新的关卡工作流
This commit is contained in:
@@ -12,10 +12,8 @@ export const API_ENDPOINTS = {
|
||||
WX_LOGIN: `${API_BASE}/auth/wx-login`,
|
||||
/** 用户资料(含实时体力) */
|
||||
USER_PROFILE: `${API_BASE}/user/profile`,
|
||||
/** 游戏数据(体力 + 通关进度) */
|
||||
/** 游戏数据(体力 + 通关进度 + 下一关) */
|
||||
USER_GAME_DATA: `${API_BASE}/user/game-data`,
|
||||
/** 关卡列表 */
|
||||
LEVELS: `${API_BASE}/levels`,
|
||||
/** 游戏配置 */
|
||||
GAME_CONFIGS: `${API_BASE}/game-configs`,
|
||||
/** 分享相关 */
|
||||
|
||||
@@ -20,6 +20,34 @@ export interface StaminaInfo {
|
||||
nextRecoverAt: string | null;
|
||||
}
|
||||
|
||||
/** 下一关完整数据(多个接口共用) */
|
||||
export interface NextLevelData {
|
||||
/** 关卡 ID */
|
||||
id: string;
|
||||
/** 关卡编号(sortOrder) */
|
||||
level: number;
|
||||
/** 图片1 URL */
|
||||
image1Url: string;
|
||||
/** 图片1 文本说明 */
|
||||
image1Description: string | null;
|
||||
/** 图片2 URL */
|
||||
image2Url: string;
|
||||
/** 图片2 文本说明 */
|
||||
image2Description: string | null;
|
||||
/** 答案 */
|
||||
answer: string;
|
||||
/** 谐音梗说明 */
|
||||
punchline: string | null;
|
||||
/** 线索1 */
|
||||
hint1: string | null;
|
||||
/** 线索2 */
|
||||
hint2: string | null;
|
||||
/** 线索3 */
|
||||
hint3: string | null;
|
||||
/** 限时(秒),null 表示不限时 */
|
||||
timeLimit: number | null;
|
||||
}
|
||||
|
||||
/** 登录响应数据 */
|
||||
export interface WxLoginData {
|
||||
token: string;
|
||||
@@ -43,31 +71,10 @@ export interface GameData {
|
||||
id: string;
|
||||
stamina: StaminaInfo;
|
||||
};
|
||||
completedLevelIds: string[];
|
||||
completedLevelCount?: number;
|
||||
}
|
||||
|
||||
/** 关卡列表项 */
|
||||
export interface LevelListItem {
|
||||
id: string;
|
||||
level: number;
|
||||
image1Url: string;
|
||||
image1Description: string | null;
|
||||
image2Url: string;
|
||||
image2Description: string | null;
|
||||
answer: string | null;
|
||||
punchline: string | null;
|
||||
hint1: string | null;
|
||||
hint2: string | null;
|
||||
hint3: string | null;
|
||||
completed: boolean;
|
||||
timeSpent: number | null;
|
||||
}
|
||||
|
||||
/** 关卡列表响应 */
|
||||
export interface LevelListData {
|
||||
levels: LevelListItem[];
|
||||
total: number;
|
||||
/** 已通关的关卡数量 */
|
||||
completedLevelCount: number;
|
||||
/** 下一个待通关的关卡(全部通关时为 null) */
|
||||
nextLevel: NextLevelData | null;
|
||||
}
|
||||
|
||||
/** 进入关卡响应 */
|
||||
@@ -84,6 +91,8 @@ export interface EnterLevelData {
|
||||
hint2: string | null;
|
||||
hint3: string | null;
|
||||
stamina: StaminaInfo;
|
||||
/** 预加载的下一关数据(无下一关时为 null) */
|
||||
preloadNextLevel: NextLevelData | null;
|
||||
}
|
||||
|
||||
/** 通关上报响应 */
|
||||
@@ -91,6 +100,8 @@ export interface CompleteLevelData {
|
||||
firstClear: boolean;
|
||||
levelId: string;
|
||||
timeSpent: number;
|
||||
/** 下一个待通关的关卡(全部通关时为 null) */
|
||||
nextLevel: NextLevelData | null;
|
||||
}
|
||||
|
||||
/** 创建分享响应 */
|
||||
|
||||
@@ -1,52 +1,5 @@
|
||||
import { SpriteFrame } from 'cc';
|
||||
|
||||
/**
|
||||
* API 返回的单个关卡数据结构(关卡列表)
|
||||
*/
|
||||
export interface ApiLevelData {
|
||||
/** 关卡 ID (UUID) */
|
||||
id: string;
|
||||
/** 关卡序号 */
|
||||
level: number;
|
||||
/** 图片1 URL */
|
||||
image1Url: string;
|
||||
/** 图片1 文本说明 */
|
||||
image1Description: string | null;
|
||||
/** 图片2 URL */
|
||||
image2Url: string;
|
||||
/** 图片2 文本说明 */
|
||||
image2Description: string | null;
|
||||
/** 谐音梗说明(仅通关后返回,未通关为 null) */
|
||||
punchline: string | null;
|
||||
/** 线索1(未通关时为 null) */
|
||||
hint1: string | null;
|
||||
/** 线索2(未通关时为 null) */
|
||||
hint2: string | null;
|
||||
/** 线索3(未通关时为 null) */
|
||||
hint3: string | null;
|
||||
/** 答案(未通关时为 null) */
|
||||
answer: string | null;
|
||||
/** 是否已通关 */
|
||||
completed: boolean;
|
||||
/** 通关时长(秒),未通关时为 null */
|
||||
timeSpent: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* API 响应结构(关卡列表)
|
||||
*/
|
||||
export interface ApiResponse {
|
||||
/** 是否成功 */
|
||||
success: boolean;
|
||||
/** 响应消息 */
|
||||
message: string | null;
|
||||
/** 响应数据 */
|
||||
data: {
|
||||
levels: ApiLevelData[];
|
||||
total: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行时关卡配置(包含已加载的图片)
|
||||
*/
|
||||
@@ -65,14 +18,16 @@ export interface RuntimeLevelConfig {
|
||||
image2Description: string | null;
|
||||
/** 谐音梗说明 */
|
||||
punchline: string | null;
|
||||
/** 线索1(未通关时为 null,进入关卡后由 enter 接口获取) */
|
||||
/** 线索1 */
|
||||
clue1: string | null;
|
||||
/** 线索2(未通关时为 null) */
|
||||
/** 线索2 */
|
||||
clue2: string | null;
|
||||
/** 线索3(未通关时为 null) */
|
||||
/** 线索3 */
|
||||
clue3: string | null;
|
||||
/** 答案(未通关时为 null,进入关卡后由 enter 接口获取) */
|
||||
/** 答案 */
|
||||
answer: string | null;
|
||||
/** 是否已通关 */
|
||||
completed: boolean;
|
||||
/** 限时(秒),null 表示不限时 */
|
||||
timeLimit: number | null;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { HttpUtil } from './HttpUtil';
|
||||
import { StorageManager } from './StorageManager';
|
||||
import { WxSDK } from './WxSDK';
|
||||
import { API_ENDPOINTS, API_TIMEOUT } from '../config/ApiConfig';
|
||||
import { ApiEnvelope, WxLoginData, GameData } from '../types/ApiTypes';
|
||||
import { ApiEnvelope, WxLoginData, GameData, NextLevelData } from '../types/ApiTypes';
|
||||
|
||||
/**
|
||||
* 认证管理器
|
||||
@@ -13,10 +13,10 @@ export class AuthManager {
|
||||
|
||||
private _userId: string = '';
|
||||
private _isLoggedIn: boolean = false;
|
||||
/** 服务端返回的已完成关卡 ID(登录后暂存,等 LevelDataManager 就绪后同步) */
|
||||
private _completedLevelIds: string[] = [];
|
||||
/** 服务端返回的已完成关卡数量,用于称号体系计算 */
|
||||
private _completedLevelCount: number = 0;
|
||||
/** game-data 返回的下一关数据,供 PageLoading 传给 LevelDataManager */
|
||||
private _nextLevel: NextLevelData | null = null;
|
||||
|
||||
static get instance(): AuthManager {
|
||||
if (!this._instance) {
|
||||
@@ -35,14 +35,15 @@ export class AuthManager {
|
||||
return this._userId;
|
||||
}
|
||||
|
||||
get completedLevelIds(): string[] {
|
||||
return this._completedLevelIds;
|
||||
}
|
||||
|
||||
get completedLevelCount(): number {
|
||||
return this._completedLevelCount;
|
||||
}
|
||||
|
||||
/** 获取 game-data 返回的下一关数据 */
|
||||
get nextLevel(): NextLevelData | null {
|
||||
return this._nextLevel;
|
||||
}
|
||||
|
||||
addCompletedLevelCount(delta: number = 1): void {
|
||||
this._completedLevelCount = Math.max(0, this._completedLevelCount + delta);
|
||||
}
|
||||
@@ -118,21 +119,21 @@ export class AuthManager {
|
||||
this._userId = gameData.user.id;
|
||||
this._isLoggedIn = true;
|
||||
StorageManager.setStamina(gameData.user.stamina);
|
||||
this._completedLevelIds = gameData.completedLevelIds;
|
||||
this._completedLevelCount = this._resolveCompletedLevelCount(gameData);
|
||||
this._completedLevelCount = gameData.completedLevelCount;
|
||||
this._nextLevel = gameData.nextLevel;
|
||||
|
||||
console.log(`[AuthManager] Token 验证成功,体力: ${gameData.user.stamina.current}/${gameData.user.stamina.max},已完成: ${this._completedLevelCount} 关`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录成功后获取游戏数据(体力 + 通关进度)
|
||||
* 登录成功后获取游戏数据(体力 + 通关进度 + 下一关)
|
||||
*/
|
||||
private async fetchGameData(): Promise<void> {
|
||||
const gameData = await this._fetchGameData();
|
||||
if (gameData) {
|
||||
this._completedLevelIds = gameData.completedLevelIds;
|
||||
this._completedLevelCount = this._resolveCompletedLevelCount(gameData);
|
||||
this._completedLevelCount = gameData.completedLevelCount;
|
||||
this._nextLevel = gameData.nextLevel;
|
||||
StorageManager.setStamina(gameData.user.stamina);
|
||||
}
|
||||
}
|
||||
@@ -152,8 +153,4 @@ export class AuthManager {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private _resolveCompletedLevelCount(gameData: GameData): number {
|
||||
return gameData.completedLevelCount ?? gameData.completedLevelIds.length;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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';
|
||||
import { RuntimeLevelConfig } from '../types/LevelTypes';
|
||||
import { NextLevelData } from '../types/ApiTypes';
|
||||
|
||||
/**
|
||||
* 进度回调类型
|
||||
@@ -12,28 +11,23 @@ export type ProgressCallback = (progress: number, message: string) => void;
|
||||
|
||||
/**
|
||||
* 关卡数据管理器
|
||||
* 单例模式,负责从 API 获取关卡数据并按需加载图片
|
||||
* 单例模式,管理当前关卡和预加载关卡的图片资源
|
||||
* 不再依赖全量关卡列表,由外部传入 NextLevelData 驱动
|
||||
*/
|
||||
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;
|
||||
/** 运行时关卡配置缓存(按 levelId 索引) */
|
||||
private _levelConfigs: Map<string, RuntimeLevelConfig> = new Map();
|
||||
|
||||
/** 图片缓存:URL -> SpriteFrame */
|
||||
private _imageCache: Map<string, SpriteFrame> = new Map();
|
||||
|
||||
/** 正在加载中的关卡索引集合 */
|
||||
private _loadingLevels: Set<number> = new Set();
|
||||
/** 正在加载中的关卡 ID 集合 */
|
||||
private _loadingLevels: Set<string> = new Set();
|
||||
|
||||
/** 是否已初始化 */
|
||||
private _initialized: boolean = false;
|
||||
|
||||
/**
|
||||
* 获取单例实例
|
||||
@@ -45,46 +39,30 @@ export class LevelDataManager {
|
||||
return this._instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 私有构造函数
|
||||
*/
|
||||
private constructor() {}
|
||||
|
||||
/**
|
||||
* 初始化:从 API 获取数据并预加载第一关图片
|
||||
* 初始化:加载首关图片
|
||||
* 由 PageLoading 在获取 game-data 后调用,传入 nextLevel 数据
|
||||
* @param nextLevel 首关数据(来自 game-data 接口)
|
||||
* @param onProgress 进度回调
|
||||
* @returns 是否初始化成功
|
||||
*/
|
||||
async initialize(onProgress?: ProgressCallback): Promise<boolean> {
|
||||
console.log('[LevelDataManager] 开始初始化');
|
||||
async initialize(nextLevel: NextLevelData, onProgress?: ProgressCallback): Promise<boolean> {
|
||||
console.log(`[LevelDataManager] 开始初始化,加载关卡 ${nextLevel.level}`);
|
||||
|
||||
try {
|
||||
// 阶段1: 获取 API 数据 (0-30%)
|
||||
onProgress?.(0, '正在请求服务端数据...');
|
||||
const apiData = await this._fetchApiData(onProgress);
|
||||
onProgress?.(0.3, '正在加载游戏必备资源...');
|
||||
|
||||
if (!apiData || apiData.length === 0) {
|
||||
console.warn('[LevelDataManager] API 返回空数据');
|
||||
onProgress?.(0.3, '网络异常,请重新打开游戏');
|
||||
const config = await this._loadLevelFromData(nextLevel);
|
||||
if (!config) {
|
||||
console.error('[LevelDataManager] 初始化失败:图片加载失败');
|
||||
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] 初始化完成,第一关资源已加载');
|
||||
this._initialized = true;
|
||||
console.log('[LevelDataManager] 初始化完成');
|
||||
onProgress?.(0.8, '游戏资源加载完成');
|
||||
|
||||
return true;
|
||||
@@ -96,185 +74,41 @@ export class LevelDataManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定关卡配置
|
||||
* @param index 关卡索引
|
||||
* 是否已初始化
|
||||
*/
|
||||
getLevelConfig(index: number): RuntimeLevelConfig | null {
|
||||
return this._levelConfigs.get(index) ?? null;
|
||||
isInitialized(): boolean {
|
||||
return this._initialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取关卡总数
|
||||
* 获取指定关卡配置(按 ID)
|
||||
* @param levelId 关卡 ID
|
||||
*/
|
||||
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;
|
||||
getLevelConfig(levelId: string): RuntimeLevelConfig | null {
|
||||
return this._levelConfigs.get(levelId) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用 enter 接口返回的数据更新运行时关卡配置(填充答案和线索)
|
||||
* @param levelId 关卡 ID
|
||||
* @param details enter 接口返回的详情
|
||||
*/
|
||||
updateLevelDetails(index: number, 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(index);
|
||||
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] 关卡 ${index} 配置不存在,无法更新详情`);
|
||||
console.warn(`[LevelDataManager] 关卡 ${levelId} 配置不存在,无法更新详情`);
|
||||
return;
|
||||
}
|
||||
|
||||
this._levelConfigs.set(index, {
|
||||
this._levelConfigs.set(levelId, {
|
||||
...config,
|
||||
answer: details.answer,
|
||||
image1Description: details.image1Description ?? config.image1Description,
|
||||
@@ -285,95 +119,112 @@ export class LevelDataManager {
|
||||
clue3: details.hint3 ?? null,
|
||||
});
|
||||
|
||||
console.log(`[LevelDataManager] 关卡 ${index} 详情已更新`);
|
||||
console.log(`[LevelDataManager] 关卡 ${levelId} 详情已更新`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预加载下一关图片(静默加载,不阻塞)
|
||||
* 在进入当前关卡后调用,提前加载下一关资源
|
||||
* @param currentIndex 当前关卡索引
|
||||
* 加载并缓存一个关卡(同步等待图片加载完成)
|
||||
* 用于 game-data 返回的 nextLevel 或 complete 返回的 nextLevel
|
||||
* @param data NextLevelData
|
||||
* @returns 加载好的 RuntimeLevelConfig,失败返回 null
|
||||
*/
|
||||
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;
|
||||
async ensureLevelReady(data: NextLevelData): Promise<RuntimeLevelConfig | null> {
|
||||
// 检查缓存
|
||||
const cached = this._levelConfigs.get(data.id);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// 检查是否正在加载
|
||||
if (this._loadingLevels.has(nextIndex)) {
|
||||
console.log(`[LevelDataManager] 下一关 ${nextIndex} 正在加载中`);
|
||||
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] 开始预加载下一关 ${nextIndex}...`);
|
||||
this.ensureLevelReady(nextIndex).catch(err => {
|
||||
console.error(`[LevelDataManager] 预加载下一关失败:`, err);
|
||||
console.log(`[LevelDataManager] 开始预加载关卡 ${data.id}...`);
|
||||
this._loadLevelFromData(data).catch(err => {
|
||||
console.error(`[LevelDataManager] 预加载关卡失败:`, err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 API 获取关卡数据(带重试机制)
|
||||
* @param onProgress 进度回调
|
||||
* 检查指定关卡图片是否已加载
|
||||
* @param levelId 关卡 ID
|
||||
*/
|
||||
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;
|
||||
isLevelImageLoaded(levelId: string): boolean {
|
||||
return this._levelConfigs.has(levelId);
|
||||
}
|
||||
|
||||
private _delay(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
/**
|
||||
* 清除缓存
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建运行时关卡配置
|
||||
* @param data API 关卡数据
|
||||
* @param spriteFrame1 已加载的图片1精灵帧
|
||||
* @param spriteFrame2 已加载的图片2精灵帧
|
||||
*/
|
||||
private _createRuntimeConfig(data: ApiLevelData, spriteFrame1: SpriteFrame | null, spriteFrame2: SpriteFrame | null): RuntimeLevelConfig {
|
||||
private _createRuntimeConfig(data: NextLevelData, spriteFrame1: SpriteFrame | null, spriteFrame2: SpriteFrame | null): RuntimeLevelConfig {
|
||||
return {
|
||||
id: data.id,
|
||||
name: `第${data.level}关`,
|
||||
spriteFrame1: spriteFrame1,
|
||||
spriteFrame2: spriteFrame2,
|
||||
spriteFrame1,
|
||||
spriteFrame2,
|
||||
image1Description: data.image1Description,
|
||||
image2Description: data.image2Description,
|
||||
punchline: data.punchline,
|
||||
@@ -381,7 +232,8 @@ export class LevelDataManager {
|
||||
clue2: data.hint2,
|
||||
clue3: data.hint3,
|
||||
answer: data.answer,
|
||||
completed: data.completed,
|
||||
completed: false,
|
||||
timeLimit: data.timeLimit,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -417,16 +269,4 @@ export class LevelDataManager {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除缓存
|
||||
*/
|
||||
clearCache(): void {
|
||||
this._apiData = [];
|
||||
this._levelConfigs.clear();
|
||||
this._loadingLevels.clear();
|
||||
this._hasApiData = false;
|
||||
this._imageCache.clear();
|
||||
console.log('[LevelDataManager] 缓存已清除');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user