feat: 接入激励视频
This commit is contained in:
770
软著源代码/06-关卡周边模块.md
Normal file
770
软著源代码/06-关卡周边模块.md
Normal file
@@ -0,0 +1,770 @@
|
||||
# 梗中作乐 V1.0 - 源代码文档(第 6 部分)
|
||||
|
||||
软件名称:梗中作乐
|
||||
版本号:V1.0
|
||||
|
||||
---
|
||||
|
||||
## 第 6 部分:关卡周边模块
|
||||
|
||||
本部分包含关卡数据缓存、答错弹窗、超时弹窗、通关关卡列表四个与关卡流程紧密关联的模块。
|
||||
|
||||
### 6.1 关卡数据管理器 (assets/scripts/utils/LevelDataManager.ts)
|
||||
|
||||
```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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 答错弹窗 (assets/prefabs/WrongModal.ts)
|
||||
|
||||
```typescript
|
||||
import { _decorator, Node, view, UITransform, Size } from 'cc';
|
||||
import { BaseModal } from 'db://assets/scripts/core/BaseModal';
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* WrongModal 回调接口
|
||||
*/
|
||||
export interface WrongModalCallbacks {
|
||||
/** 点击继续挑战 / 关闭按钮回调 */
|
||||
onContinue?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 答案错误弹窗组件
|
||||
* 继承 BaseModal,显示答案错误提示,提供"继续挑战"和关闭按钮
|
||||
*/
|
||||
@ccclass('WrongModal')
|
||||
export class WrongModal extends BaseModal {
|
||||
/** 静态常量:弹窗层级 */
|
||||
public static readonly MODAL_Z_INDEX = 999;
|
||||
|
||||
/** 关闭按钮 */
|
||||
@property(Node)
|
||||
closeBtn: Node | null = null;
|
||||
|
||||
/** 继续挑战按钮 */
|
||||
@property(Node)
|
||||
buttonHint: Node | null = null;
|
||||
|
||||
/** 回调函数 */
|
||||
private _callbacks: WrongModalCallbacks = {};
|
||||
|
||||
/** 缓存的屏幕尺寸 */
|
||||
private _screenSize: Size | null = null;
|
||||
|
||||
/**
|
||||
* 设置回调函数
|
||||
*/
|
||||
setCallbacks(callbacks: WrongModalCallbacks): void {
|
||||
this._callbacks = callbacks;
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面首次加载时调用
|
||||
*/
|
||||
onViewLoad(): void {
|
||||
console.log('[WrongModal] onViewLoad');
|
||||
this._bindButtonEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面每次显示时调用
|
||||
*/
|
||||
onViewShow(): void {
|
||||
super.onViewShow();
|
||||
this._updateWidget();
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面销毁时调用
|
||||
*/
|
||||
onViewDestroy(): void {
|
||||
this._unbindButtonEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置弹窗尺寸为全屏
|
||||
*/
|
||||
private _updateWidget(): void {
|
||||
if (!this._screenSize) {
|
||||
this._screenSize = view.getVisibleSize();
|
||||
}
|
||||
|
||||
const uiTransform = this.node.getComponent(UITransform);
|
||||
if (uiTransform) {
|
||||
uiTransform.setContentSize(this._screenSize.width, this._screenSize.height);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定按钮事件
|
||||
*/
|
||||
private _bindButtonEvents(): void {
|
||||
if (this.closeBtn) {
|
||||
this.closeBtn.on(Node.EventType.TOUCH_END, this._onContinueClick, this);
|
||||
}
|
||||
if (this.buttonHint) {
|
||||
this.buttonHint.on(Node.EventType.TOUCH_END, this._onContinueClick, this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解除按钮事件绑定
|
||||
*/
|
||||
private _unbindButtonEvents(): void {
|
||||
if (this.closeBtn && this.closeBtn.isValid) {
|
||||
this.closeBtn.off(Node.EventType.TOUCH_END, this._onContinueClick, this);
|
||||
}
|
||||
if (this.buttonHint && this.buttonHint.isValid) {
|
||||
this.buttonHint.off(Node.EventType.TOUCH_END, this._onContinueClick, this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 继续挑战 / 关闭按钮点击
|
||||
*/
|
||||
private _onContinueClick(): void {
|
||||
console.log('[WrongModal] 点击继续挑战');
|
||||
this._callbacks.onContinue?.();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 超时弹窗 (assets/prefabs/TimeoutModal.ts)
|
||||
|
||||
```typescript
|
||||
import { _decorator, Node, view, UITransform, Size } from 'cc';
|
||||
import { BaseModal } from 'db://assets/scripts/core/BaseModal';
|
||||
import { WxSDK } from 'db://assets/scripts/utils/WxSDK';
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* TimeoutModal 回调接口
|
||||
*/
|
||||
export interface TimeoutModalCallbacks {
|
||||
/** 点击求助好友回调 */
|
||||
onShare?: () => void;
|
||||
/** 点击再次挑战回调 */
|
||||
onRestart?: () => void;
|
||||
/** 点击下一题回调 */
|
||||
onNext?: () => void;
|
||||
/** 点击返回主页 / 关闭按钮回调 */
|
||||
onHome?: () => void;
|
||||
}
|
||||
|
||||
interface TimeoutModalParams {
|
||||
levelIndex?: number;
|
||||
shareMode?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 时间耗尽弹窗组件
|
||||
* 继承 BaseModal,显示倒计时结束提示,提供"求助好友"、"再次挑战"和"返回主页"三个按钮
|
||||
*/
|
||||
@ccclass('TimeoutModal')
|
||||
export class TimeoutModal extends BaseModal {
|
||||
/** 静态常量:弹窗层级 */
|
||||
public static readonly MODAL_Z_INDEX = 999;
|
||||
|
||||
/** 关闭按钮 */
|
||||
@property(Node)
|
||||
closeBtn: Node | null = null;
|
||||
|
||||
/** 求助好友按钮 */
|
||||
@property(Node)
|
||||
buttonShare: Node | null = null;
|
||||
|
||||
/** 再次挑战按钮 */
|
||||
@property(Node)
|
||||
buttonRestart: Node | null = null;
|
||||
|
||||
/** 下一题按钮(分享挑战超时时显示) */
|
||||
@property(Node)
|
||||
buttonNext: Node | null = null;
|
||||
|
||||
/** 返回主页按钮 */
|
||||
@property(Node)
|
||||
buttonHome: Node | null = null;
|
||||
|
||||
/** 回调函数 */
|
||||
private _callbacks: TimeoutModalCallbacks = {};
|
||||
|
||||
/** 缓存的屏幕尺寸 */
|
||||
private _screenSize: Size | null = null;
|
||||
|
||||
/**
|
||||
* 设置回调函数
|
||||
*/
|
||||
setCallbacks(callbacks: TimeoutModalCallbacks): void {
|
||||
this._callbacks = callbacks;
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面首次加载时调用
|
||||
*/
|
||||
onViewLoad(): void {
|
||||
console.log('[TimeoutModal] onViewLoad');
|
||||
this._resolveNodes();
|
||||
this._bindButtonEvents();
|
||||
this._refreshModeButtons();
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面每次显示时调用
|
||||
*/
|
||||
onViewShow(): void {
|
||||
super.onViewShow();
|
||||
this._updateWidget();
|
||||
this._refreshModeButtons();
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面销毁时调用
|
||||
*/
|
||||
onViewDestroy(): void {
|
||||
this._unbindButtonEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置弹窗尺寸为全屏
|
||||
*/
|
||||
private _updateWidget(): void {
|
||||
if (!this._screenSize) {
|
||||
this._screenSize = view.getVisibleSize();
|
||||
}
|
||||
|
||||
const uiTransform = this.node.getComponent(UITransform);
|
||||
if (uiTransform) {
|
||||
uiTransform.setContentSize(this._screenSize.width, this._screenSize.height);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定按钮事件
|
||||
*/
|
||||
private _bindButtonEvents(): void {
|
||||
if (this.closeBtn) {
|
||||
this.closeBtn.on(Node.EventType.TOUCH_END, this._onHomeClick, this);
|
||||
}
|
||||
if (this.buttonShare) {
|
||||
this.buttonShare.on(Node.EventType.TOUCH_END, this._onShareClick, this);
|
||||
}
|
||||
if (this.buttonRestart) {
|
||||
this.buttonRestart.on(Node.EventType.TOUCH_END, this._onRestartClick, this);
|
||||
}
|
||||
if (this.buttonNext) {
|
||||
this.buttonNext.on(Node.EventType.TOUCH_END, this._onNextClick, this);
|
||||
}
|
||||
if (this.buttonHome) {
|
||||
this.buttonHome.on(Node.EventType.TOUCH_END, this._onHomeClick, this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解除按钮事件绑定
|
||||
*/
|
||||
private _unbindButtonEvents(): void {
|
||||
if (this.closeBtn && this.closeBtn.isValid) {
|
||||
this.closeBtn.off(Node.EventType.TOUCH_END, this._onHomeClick, this);
|
||||
}
|
||||
if (this.buttonShare && this.buttonShare.isValid) {
|
||||
this.buttonShare.off(Node.EventType.TOUCH_END, this._onShareClick, this);
|
||||
}
|
||||
if (this.buttonRestart && this.buttonRestart.isValid) {
|
||||
this.buttonRestart.off(Node.EventType.TOUCH_END, this._onRestartClick, this);
|
||||
}
|
||||
if (this.buttonNext && this.buttonNext.isValid) {
|
||||
this.buttonNext.off(Node.EventType.TOUCH_END, this._onNextClick, this);
|
||||
}
|
||||
if (this.buttonHome && this.buttonHome.isValid) {
|
||||
this.buttonHome.off(Node.EventType.TOUCH_END, this._onHomeClick, this);
|
||||
}
|
||||
}
|
||||
|
||||
private _resolveNodes(): void {
|
||||
this.closeBtn = this.closeBtn ?? this._findChild(this.node, 'closeBtn');
|
||||
this.buttonShare = this.buttonShare ?? this._findChild(this.node, 'ButtonShare');
|
||||
this.buttonRestart = this.buttonRestart ?? this._findChild(this.node, 'ButtonRestart');
|
||||
this.buttonNext = this.buttonNext ?? this._findChild(this.node, 'ButtonNext');
|
||||
this.buttonHome = this.buttonHome ?? this._findChild(this.node, 'ButtonHome');
|
||||
}
|
||||
|
||||
private _refreshModeButtons(): void {
|
||||
const isShareMode = this._params?.shareMode === true;
|
||||
if (this.buttonRestart) {
|
||||
this.buttonRestart.active = !isShareMode;
|
||||
}
|
||||
if (this.buttonNext) {
|
||||
this.buttonNext.active = isShareMode;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 求助好友按钮点击
|
||||
*/
|
||||
private _onShareClick(): void {
|
||||
console.log('[TimeoutModal] 点击求助好友');
|
||||
|
||||
WxSDK.shareAppMessage({
|
||||
title: '这道题太难了,快来帮帮我!',
|
||||
query: `level=${this._params?.levelIndex ?? 1}`
|
||||
});
|
||||
|
||||
this._callbacks.onShare?.();
|
||||
}
|
||||
|
||||
/**
|
||||
* 再次挑战按钮点击
|
||||
*/
|
||||
private _onRestartClick(): void {
|
||||
console.log('[TimeoutModal] 点击再次挑战');
|
||||
this._callbacks.onRestart?.();
|
||||
}
|
||||
|
||||
/**
|
||||
* 下一题按钮点击
|
||||
*/
|
||||
private _onNextClick(): void {
|
||||
console.log('[TimeoutModal] 点击下一题');
|
||||
this._callbacks.onNext?.();
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回主页 / 关闭按钮点击
|
||||
*/
|
||||
private _onHomeClick(): void {
|
||||
console.log('[TimeoutModal] 点击返回主页');
|
||||
this._callbacks.onHome?.();
|
||||
}
|
||||
|
||||
private _findChild(root: Node, nodeName: string): Node | null {
|
||||
if (root.name === nodeName) {
|
||||
return root;
|
||||
}
|
||||
|
||||
for (const child of root.children) {
|
||||
const found = this._findChild(child, nodeName);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 通关关卡列表管理器 (assets/scripts/utils/CompletedLevelsManager.ts)
|
||||
|
||||
```typescript
|
||||
import { SpriteFrame, Texture2D, ImageAsset, assetManager } from 'cc';
|
||||
import { HttpUtil } from './HttpUtil';
|
||||
import { API_ENDPOINTS, API_TIMEOUT } from '../config/ApiConfig';
|
||||
import { ApiEnvelope, CompletedLevel } from '../types/ApiTypes';
|
||||
|
||||
/**
|
||||
* 已通关关卡管理器
|
||||
* 单例模式,负责拉取当前用户所有已通关关卡 + 封面图缓存
|
||||
* 适用于「成就墙」「关卡回看」「出题 / 预览」等场景
|
||||
*/
|
||||
export class CompletedLevelsManager {
|
||||
private static _instance: CompletedLevelsManager | null = null;
|
||||
|
||||
/** 关卡数据按服务端返回顺序缓存 */
|
||||
private _levels: CompletedLevel[] = [];
|
||||
|
||||
/** 是否已经成功拉取过一次 */
|
||||
private _loaded: boolean = false;
|
||||
|
||||
/** 图片缓存:URL -> SpriteFrame */
|
||||
private _imageCache: Map<string, SpriteFrame> = new Map();
|
||||
|
||||
/** 正在进行中的请求,用于去重并发调用 */
|
||||
private _inflight: Promise<CompletedLevel[] | null> | null = null;
|
||||
|
||||
static get instance(): CompletedLevelsManager {
|
||||
if (!this._instance) {
|
||||
this._instance = new CompletedLevelsManager();
|
||||
}
|
||||
return this._instance;
|
||||
}
|
||||
|
||||
private constructor() {}
|
||||
|
||||
/**
|
||||
* 获取已缓存的关卡列表(需先 fetch 或 ensureLoaded)
|
||||
*/
|
||||
get levels(): CompletedLevel[] {
|
||||
return this._levels;
|
||||
}
|
||||
|
||||
get count(): number {
|
||||
return this._levels.length;
|
||||
}
|
||||
|
||||
isLoaded(): boolean {
|
||||
return this._loaded;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按索引(0-based)获取关卡,越界返回 null
|
||||
*/
|
||||
getByIndex(index: number): CompletedLevel | null {
|
||||
if (index < 0 || index >= this._levels.length) return null;
|
||||
return this._levels[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取并缓存已通关关卡列表
|
||||
* - forceRefresh=true 强制重新请求
|
||||
* - 并发调用会共享同一次请求
|
||||
*/
|
||||
async fetch(forceRefresh: boolean = false): Promise<CompletedLevel[] | null> {
|
||||
if (!forceRefresh && this._loaded) {
|
||||
return this._levels;
|
||||
}
|
||||
|
||||
if (this._inflight) {
|
||||
return this._inflight;
|
||||
}
|
||||
|
||||
this._inflight = this._doFetch();
|
||||
try {
|
||||
return await this._inflight;
|
||||
} finally {
|
||||
this._inflight = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async _doFetch(): Promise<CompletedLevel[] | null> {
|
||||
try {
|
||||
const response = await HttpUtil.get<ApiEnvelope<CompletedLevel[]>>(
|
||||
API_ENDPOINTS.COMPLETED_LEVELS,
|
||||
API_TIMEOUT.DEFAULT,
|
||||
);
|
||||
|
||||
if (!response.success || !response.data) {
|
||||
console.error('[CompletedLevelsManager] 拉取失败:', response.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
this._levels = response.data;
|
||||
this._loaded = true;
|
||||
console.log(`[CompletedLevelsManager] 拉取成功,共 ${this._levels.length} 关`);
|
||||
return this._levels;
|
||||
} catch (err) {
|
||||
console.error('[CompletedLevelsManager] 拉取异常:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按图片 URL 加载并缓存 SpriteFrame
|
||||
* 已缓存直接返回
|
||||
*/
|
||||
loadImage(url: string): Promise<SpriteFrame | null> {
|
||||
if (!url) return Promise.resolve(null);
|
||||
|
||||
const cached = this._imageCache.get(url);
|
||||
if (cached) {
|
||||
return Promise.resolve(cached);
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
assetManager.loadRemote<ImageAsset>(url, (err, imageAsset) => {
|
||||
if (err) {
|
||||
console.error('[CompletedLevelsManager] 加载图片失败:', 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** 清除缓存(登出等场景) */
|
||||
clear(): void {
|
||||
this._levels = [];
|
||||
this._loaded = false;
|
||||
this._imageCache.clear();
|
||||
this._inflight = null;
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user