diff --git a/assets/PageLoading.ts b/assets/PageLoading.ts index d4d59f5..0959401 100644 --- a/assets/PageLoading.ts +++ b/assets/PageLoading.ts @@ -1,5 +1,6 @@ -import { _decorator, Component, ProgressBar } from 'cc'; +import { _decorator, Component, ProgressBar, Label } from 'cc'; import { ViewManager } from './scripts/core/ViewManager'; +import { LevelDataManager } from './scripts/utils/LevelDataManager'; const { ccclass, property } = _decorator; /** @@ -11,6 +12,9 @@ export class PageLoading extends Component { @property(ProgressBar) progressBar: ProgressBar | null = null; + @property(Label) + statusLabel: Label | null = null; + start() { this._startPreload(); } @@ -18,16 +22,22 @@ export class PageLoading extends Component { /** * 开始预加载 */ - private _startPreload(): void { + private async _startPreload(): Promise { // 初始化进度条 if (this.progressBar) { this.progressBar.progress = 0; } - // 预加载 PageHome + // 阶段1: 初始化 LevelDataManager (0-80%) + await LevelDataManager.instance.initialize((progress, message) => { + this._updateProgress(progress); + this._updateStatusLabel(message); + }); + + // 阶段2: 预加载 PageHome (80-100%) ViewManager.instance.preload('PageHome', (progress) => { - this._updateProgress(progress); + this._updateProgress(0.8 + progress * 0.2); }, () => { this._onPreloadComplete(); @@ -44,12 +54,22 @@ export class PageLoading extends Component { } } + /** + * 更新状态标签 + */ + private _updateStatusLabel(message: string): void { + if (this.statusLabel) { + this.statusLabel.string = message; + } + } + /** * 预加载完成回调 */ private _onPreloadComplete(): void { // 确保进度条显示完成 this._updateProgress(1); + this._updateStatusLabel('加载完成'); // 打开 PageHome ViewManager.instance.open('PageHome', { diff --git a/assets/prefabs/PageLevel.ts b/assets/prefabs/PageLevel.ts index d01c92f..c76660d 100644 --- a/assets/prefabs/PageLevel.ts +++ b/assets/prefabs/PageLevel.ts @@ -3,6 +3,8 @@ import { BaseView } from 'db://assets/scripts/core/BaseView'; import { ViewManager } from 'db://assets/scripts/core/ViewManager'; import { StorageManager } from 'db://assets/scripts/utils/StorageManager'; import { WxSDK } from 'db://assets/scripts/utils/WxSDK'; +import { LevelDataManager } from 'db://assets/scripts/utils/LevelDataManager'; +import { RuntimeLevelConfig } from 'db://assets/scripts/types/LevelTypes'; const { ccclass, property } = _decorator; /** @@ -104,6 +106,9 @@ export class PageLevel extends BaseView { /** 倒计时是否结束 */ private _isTimeUp: boolean = false; + /** 当前关卡配置(API 或本地) */ + private _currentConfig: RuntimeLevelConfig | null = null; + /** * 页面首次加载时调用 */ @@ -145,18 +150,67 @@ export class PageLevel extends BaseView { * 初始化关卡 */ private initLevel(): void { - const config = this.levelConfigs[this.currentLevelIndex]; + // 优先使用 API 数据 + if (LevelDataManager.instance.hasApiData()) { + this._initFromApiConfig(); + } else { + this._initFromLocalConfig(); + } + } + + /** + * 从 API 配置初始化关卡 + */ + private _initFromApiConfig(): void { + const config = LevelDataManager.instance.getLevelConfig(this.currentLevelIndex); if (!config) { - console.warn('[PageLevel] 没有找到关卡配置'); + console.warn('[PageLevel] 没有找到 API 关卡配置'); + // 降级到本地配置 + this._initFromLocalConfig(); return; } + console.log(`[PageLevel] 使用 API 配置初始化关卡 ${this.currentLevelIndex + 1}: ${config.name}`); + this._applyLevelConfig(config); + } + + /** + * 从本地配置初始化关卡 + */ + private _initFromLocalConfig(): void { + const config = this.levelConfigs[this.currentLevelIndex]; + if (!config) { + console.warn('[PageLevel] 没有找到本地关卡配置'); + return; + } + + // 创建 RuntimeLevelConfig 从本地配置 + const runtimeConfig: RuntimeLevelConfig = { + id: this.currentLevelIndex, + name: `关卡 ${this.currentLevelIndex + 1}`, + spriteFrame: config.mainImage, + clue1: config.clue1, + clue2: config.clue2, + clue3: config.clue3, + answer: config.answer + }; + + console.log(`[PageLevel] 使用本地配置初始化关卡 ${this.currentLevelIndex + 1}`); + this._applyLevelConfig(runtimeConfig); + } + + /** + * 应用关卡配置(通用初始化逻辑) + */ + private _applyLevelConfig(config: RuntimeLevelConfig): void { + this._currentConfig = config; + // 重置倒计时状态 this._isTimeUp = false; this._countdown = 60; // 设置主图 - this.setMainImage(config.mainImage); + this.setMainImage(config.spriteFrame); // 设置线索1(默认解锁) this.setClue(1, config.clue1); @@ -170,13 +224,12 @@ export class PageLevel extends BaseView { this.showUnlockButton(3); // 根据答案长度创建单个输入框 - const answerLength = config.answer.length; - this.createSingleInput(answerLength); + this.createSingleInput(config.answer.length); // 更新倒计时显示 this.updateClockLabel(); - console.log(`[PageLevel] 初始化关卡 ${this.currentLevelIndex + 1}, 答案长度: ${answerLength}`); + console.log(`[PageLevel] 初始化关卡 ${this.currentLevelIndex + 1}, 答案长度: ${config.answer.length}`); } /** @@ -455,9 +508,8 @@ export class PageLevel extends BaseView { this.showClue(index); // 设置线索内容 - const config = this.levelConfigs[this.currentLevelIndex]; - if (config) { - const clueContent = index === 2 ? config.clue2 : config.clue3; + if (this._currentConfig) { + const clueContent = index === 2 ? this._currentConfig.clue2 : this._currentConfig.clue3; this.setClue(index, clueContent); } @@ -615,13 +667,12 @@ export class PageLevel extends BaseView { * 提交答案 */ onSubmitAnswer(): void { - const config = this.levelConfigs[this.currentLevelIndex]; - if (!config) return; + if (!this._currentConfig) return; const userAnswer = this.getAnswer(); - console.log(`[PageLevel] 提交答案: ${userAnswer}, 正确答案: ${config.answer}`); + console.log(`[PageLevel] 提交答案: ${userAnswer}, 正确答案: ${this._currentConfig.answer}`); - if (userAnswer === config.answer) { + if (userAnswer === this._currentConfig.answer) { // 答案正确 this.playClickSound(); this.showSuccess(); @@ -671,7 +722,13 @@ export class PageLevel extends BaseView { private nextLevel(): void { this.currentLevelIndex++; - if (this.currentLevelIndex >= this.levelConfigs.length) { + // 检查是否还有关卡 + const manager = LevelDataManager.instance; + const totalLevels = manager.hasApiData() + ? manager.getLevelCount() + : this.levelConfigs.length; + + if (this.currentLevelIndex >= totalLevels) { // 所有关卡完成 console.log('[PageLevel] 恭喜通关!'); this.stopCountdown(); diff --git a/assets/scripts/types/LevelTypes.ts b/assets/scripts/types/LevelTypes.ts new file mode 100644 index 0000000..bf94b90 --- /dev/null +++ b/assets/scripts/types/LevelTypes.ts @@ -0,0 +1,53 @@ +import { SpriteFrame } from 'cc'; + +/** + * API 返回的单个关卡数据结构 + */ +export interface ApiLevelData { + /** 关卡 ID */ + id: number; + /** 关卡名称 */ + name: string; + /** 主图 URL */ + imageUrl: string; + /** 线索1(映射到 clue1) */ + hint1: string; + /** 线索2(映射到 clue2) */ + hint2: string; + /** 线索3(映射到 clue3) */ + hint3: string; + /** 答案 */ + answer: string; +} + +/** + * API 响应结构 + */ +export interface ApiResponse { + /** 状态码,0 表示成功 */ + code: number; + /** 响应消息 */ + message: string; + /** 关卡数据数组 */ + data: ApiLevelData[]; +} + +/** + * 运行时关卡配置(包含已加载的图片) + */ +export interface RuntimeLevelConfig { + /** 关卡 ID */ + id: number; + /** 关卡名称 */ + name: string; + /** 主图 SpriteFrame(可能为 null 如果加载失败) */ + spriteFrame: SpriteFrame | null; + /** 线索1 */ + clue1: string; + /** 线索2 */ + clue2: string; + /** 线索3 */ + clue3: string; + /** 答案 */ + answer: string; +} diff --git a/assets/scripts/utils/HttpUtil.ts b/assets/scripts/utils/HttpUtil.ts new file mode 100644 index 0000000..9cdbc05 --- /dev/null +++ b/assets/scripts/utils/HttpUtil.ts @@ -0,0 +1,75 @@ +/** + * HTTP 请求工具类 + * 封装 XMLHttpRequest,支持 GET/POST 请求 + */ +export class HttpUtil { + /** + * 发送 GET 请求 + * @param url 请求 URL + * @param timeout 超时时间(毫秒),默认 10000 + * @returns Promise + */ + static get(url: string, timeout: number = 10000): Promise { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + + xhr.open('GET', url, true); + xhr.timeout = timeout; + xhr.responseType = 'json'; + + xhr.onload = () => { + if (xhr.status >= 200 && xhr.status < 300) { + resolve(xhr.response as T); + } else { + reject(new Error(`HTTP 错误: ${xhr.status}`)); + } + }; + + xhr.onerror = () => { + reject(new Error('网络请求失败')); + }; + + xhr.ontimeout = () => { + reject(new Error('请求超时')); + }; + + xhr.send(); + }); + } + + /** + * 发送 POST 请求 + * @param url 请求 URL + * @param data 请求体数据 + * @param timeout 超时时间(毫秒),默认 10000 + * @returns Promise + */ + static post(url: string, data: object, timeout: number = 10000): Promise { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + + xhr.open('POST', url, true); + xhr.timeout = timeout; + xhr.responseType = 'json'; + xhr.setRequestHeader('Content-Type', 'application/json'); + + xhr.onload = () => { + if (xhr.status >= 200 && xhr.status < 300) { + resolve(xhr.response as T); + } else { + reject(new Error(`HTTP 错误: ${xhr.status}`)); + } + }; + + xhr.onerror = () => { + reject(new Error('网络请求失败')); + }; + + xhr.ontimeout = () => { + reject(new Error('请求超时')); + }; + + xhr.send(JSON.stringify(data)); + }); + } +} diff --git a/assets/scripts/utils/LevelDataManager.ts b/assets/scripts/utils/LevelDataManager.ts new file mode 100644 index 0000000..dadd12f --- /dev/null +++ b/assets/scripts/utils/LevelDataManager.ts @@ -0,0 +1,207 @@ +import { SpriteFrame, Texture2D, ImageAsset, assetManager } from 'cc'; +import { HttpUtil } from './HttpUtil'; +import { ApiLevelData, ApiResponse, RuntimeLevelConfig } from '../types/LevelTypes'; + +/** + * 进度回调类型 + * @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_URL = 'https://ilookai.cn/api/v1/wechat-game/levels'; + + /** 请求超时时间(毫秒) */ + private readonly REQUEST_TIMEOUT = 8000; + + /** 运行时关卡配置缓存 */ + private _levelConfigs: RuntimeLevelConfig[] = []; + + /** 是否已成功从 API 获取数据 */ + private _hasApiData: boolean = false; + + /** 图片缓存:URL -> SpriteFrame */ + private _imageCache: Map = new Map(); + + /** + * 获取单例实例 + */ + 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 { + console.log('[LevelDataManager] 开始初始化'); + + try { + // 阶段1: 获取 API 数据 (0-20%) + onProgress?.(0, '正在获取关卡数据...'); + const apiData = await this._fetchApiData(); + + if (!apiData || apiData.length === 0) { + console.warn('[LevelDataManager] API 返回空数据'); + onProgress?.(0.2, 'API 数据为空,使用本地配置'); + return false; + } + + console.log(`[LevelDataManager] 获取到 ${apiData.length} 个关卡数据`); + onProgress?.(0.2, `获取到 ${apiData.length} 个关卡`); + + // 阶段2: 预加载所有图片 (20-80%) + const configs = await this._preloadImages(apiData, (progress) => { + onProgress?.(0.2 + progress * 0.6, '正在加载关卡资源...'); + }); + + this._levelConfigs = configs; + this._hasApiData = true; + + console.log('[LevelDataManager] 初始化完成'); + onProgress?.(0.8, '关卡资源加载完成'); + + return true; + } catch (error) { + console.error('[LevelDataManager] 初始化失败:', error); + onProgress?.(0.2, '获取数据失败,使用本地配置'); + return false; + } + } + + /** + * 获取指定关卡配置 + * @param index 关卡索引 + */ + getLevelConfig(index: number): RuntimeLevelConfig | null { + if (index < 0 || index >= this._levelConfigs.length) { + return null; + } + return this._levelConfigs[index]; + } + + /** + * 获取关卡总数 + */ + getLevelCount(): number { + return this._levelConfigs.length; + } + + /** + * 检查是否有 API 数据 + */ + hasApiData(): boolean { + return this._hasApiData && this._levelConfigs.length > 0; + } + + /** + * 从 API 获取关卡数据 + */ + private async _fetchApiData(): Promise { + try { + const response = await HttpUtil.get(this.API_URL, this.REQUEST_TIMEOUT); + + if (response.code !== 0) { + console.warn(`[LevelDataManager] API 返回错误码: ${response.code}, 消息: ${response.message}`); + return null; + } + + return response.data; + } catch (error) { + console.error('[LevelDataManager] API 请求失败:', error); + return null; + } + } + + /** + * 预加载所有图片 + * @param apiData API 返回的关卡数据 + * @param onProgress 进度回调 + */ + private async _preloadImages( + apiData: ApiLevelData[], + onProgress?: (progress: number) => void + ): Promise { + const configs: RuntimeLevelConfig[] = []; + const total = apiData.length; + + for (let i = 0; i < total; i++) { + const data = apiData[i]; + const spriteFrame = await this._loadImage(data.imageUrl); + + configs.push({ + id: data.id, + name: data.name, + spriteFrame: spriteFrame, + clue1: data.hint1, + clue2: data.hint2, + clue3: data.hint3, + answer: data.answer + }); + + onProgress?.((i + 1) / total); + } + + return configs; + } + + /** + * 加载远程图片为 SpriteFrame + * @param url 图片 URL + */ + private async _loadImage(url: string): Promise { + // 检查缓存 + if (this._imageCache.has(url)) { + return this._imageCache.get(url)!; + } + + return new Promise((resolve) => { + assetManager.loadRemote(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._levelConfigs = []; + this._hasApiData = false; + this._imageCache.clear(); + console.log('[LevelDataManager] 缓存已清除'); + } +}