feat: 对接最新的关卡工作流

This commit is contained in:
richarjiang
2026-04-26 17:04:47 +08:00
parent 5074706115
commit 1e5017e28e
16 changed files with 1808 additions and 795 deletions

View File

@@ -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;
}
}

View File

@@ -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] 缓存已清除');
}
}