Files
mp-xieyingeng/软著源代码/06-关卡周边模块.md
2026-06-29 19:55:40 +08:00

771 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 梗中作乐 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;
}
}
```