From 1e5017e28eec36b64c61ae4b3ce54e138992af9a Mon Sep 17 00:00:00 2001 From: richarjiang Date: Sun, 26 Apr 2026 17:04:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AF=B9=E6=8E=A5=E6=9C=80=E6=96=B0?= =?UTF-8?q?=E7=9A=84=E5=85=B3=E5=8D=A1=E5=B7=A5=E4=BD=9C=E6=B5=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/PageLoading.ts | 79 +-- assets/prefabs/PageLevel.prefab | 8 + assets/prefabs/PageLevel.ts | 373 +++++++---- assets/prefabs/PageWriteLevels.ts | 25 +- assets/prefabs/PassModal.prefab | 6 +- assets/prefabs/TimeoutModal.prefab | 761 ++++++++++++++++++----- assets/prefabs/TimeoutModal.ts | 162 +++++ assets/prefabs/TimeoutModal.ts.meta | 9 + assets/prefabs/WrongModal.prefab | 497 ++++++++++++--- assets/prefabs/WrongModal.ts | 111 ++++ assets/prefabs/WrongModal.ts.meta | 9 + assets/scripts/config/ApiConfig.ts | 4 +- assets/scripts/types/ApiTypes.ts | 61 +- assets/scripts/types/LevelTypes.ts | 57 +- assets/scripts/utils/AuthManager.ts | 29 +- assets/scripts/utils/LevelDataManager.ts | 412 ++++-------- 16 files changed, 1808 insertions(+), 795 deletions(-) create mode 100644 assets/prefabs/TimeoutModal.ts create mode 100644 assets/prefabs/TimeoutModal.ts.meta create mode 100644 assets/prefabs/WrongModal.ts create mode 100644 assets/prefabs/WrongModal.ts.meta diff --git a/assets/PageLoading.ts b/assets/PageLoading.ts index 7e598dc..f462812 100644 --- a/assets/PageLoading.ts +++ b/assets/PageLoading.ts @@ -3,7 +3,6 @@ import type { AssetManager } from 'cc'; import { ViewManager } from './scripts/core/ViewManager'; import { LevelDataManager } from './scripts/utils/LevelDataManager'; import { AuthManager } from './scripts/utils/AuthManager'; -import { StorageManager } from './scripts/utils/StorageManager'; import { ShareManager } from './scripts/utils/ShareManager'; import { WxSDK } from './scripts/utils/WxSDK'; const { ccclass, property } = _decorator; @@ -11,7 +10,7 @@ const { ccclass, property } = _decorator; /** * 页面加载组件 * 负责用户登录、预加载资源并显示加载进度 - * 登录与关卡数据加载并行执行以减少等待时间 + * 流程:登录 + game-data → 拿到 nextLevel → 加载首关图片 → 进入首页 */ @ccclass('PageLoading') export class PageLoading extends Component { @@ -34,15 +33,11 @@ export class PageLoading extends Component { this._updateStatusLabel('正在加载...'); - // 登录和关卡数据并行加载 - const [loginSuccess, levelSuccess] = await Promise.all([ - AuthManager.instance.initialize(), - LevelDataManager.instance.initialize((progress, message) => { - // 关卡加载占 0-80% 进度 - this._updateProgress(progress); - this._updateStatusLabel(message); - }), - ]); + // 阶段1: 登录 + 获取 game-data(含 nextLevel) + this._updateProgress(0); + this._updateStatusLabel('正在连接服务器...'); + + const loginSuccess = await AuthManager.instance.initialize(); if (loginSuccess) { console.log('[PageLoading] 用户登录成功'); @@ -50,22 +45,40 @@ export class PageLoading extends Component { console.warn('[PageLoading] 登录失败,继续离线模式'); } - if (!levelSuccess) { + this._updateProgress(0.2); + + // 阶段2: 加载首关图片(如果有 nextLevel) + const nextLevel = AuthManager.instance.nextLevel; + let levelSuccess = false; + + if (nextLevel) { + levelSuccess = await LevelDataManager.instance.initialize(nextLevel, (progress, message) => { + // 关卡图片加载占 20%-80% 进度 + this._updateProgress(0.2 + progress * 0.6); + this._updateStatusLabel(message); + }); + + if (!levelSuccess) { + this._updateStatusLabel('资源加载失败,请重新打开游戏'); + return; + } + } else if (loginSuccess) { + // nextLevel 为 null → 全部通关(或服务端无关卡) + console.log('[PageLoading] 全部通关或无可用关卡'); + this._updateProgress(0.8); + } else { + // 登录失败且没有 nextLevel this._updateStatusLabel('加载失败,请重新打开游戏'); return; } + // 阶段3: 加载字体分包 const fontSuccess = await this._loadFontBundle(); if (!fontSuccess) { this._updateStatusLabel('字体资源加载失败,请重新打开游戏'); return; } - // 登录 + 关卡数据都就绪后,用服务端进度覆盖本地进度 - if (loginSuccess) { - this._syncProgressFromServer(); - } - // 检测分享码:从微信启动参数中获取 const shareCode = WxSDK.getShareCodeFromLaunch(); if (shareCode && loginSuccess) { @@ -150,36 +163,4 @@ export class PageLoading extends Component { }); }); } - - /** - * 用服务端通关进度同步本地进度 - * 1. 根据 completedLevelIds 标记已通关关卡 - * 2. 更新 maxUnlockedLevelIndex - * 3. 将 currentLevelIndex 设为第一个未通关关卡 - */ - private _syncProgressFromServer(): void { - const completedIds = AuthManager.instance.completedLevelIds; - if (completedIds.length === 0) { - console.log('[PageLoading] 服务端无通关记录,使用本地进度'); - return; - } - - const maxCompletedIndex = LevelDataManager.instance.getMaxCompletedIndex(completedIds); - if (maxCompletedIndex < 0) { - return; - } - - const localMax = StorageManager.getMaxUnlockedLevelIndex(); - - // 取服务端和本地的较大值,防止进度回退 - if (maxCompletedIndex > localMax) { - StorageManager.onLevelCompleted(maxCompletedIndex); - console.log(`[PageLoading] 服务端进度同步:已通关到第 ${maxCompletedIndex + 1} 关`); - } - - // 根据关卡列表找到第一个未通关关卡,设为当前关卡 - const firstUncompleted = LevelDataManager.instance.getFirstUncompletedIndex(); - StorageManager.setCurrentLevelIndex(firstUncompleted); - console.log(`[PageLoading] 当前关卡设为第一个未通关: 第 ${firstUncompleted + 1} 关`); - } } diff --git a/assets/prefabs/PageLevel.prefab b/assets/prefabs/PageLevel.prefab index f3ed20d..72519db 100644 --- a/assets/prefabs/PageLevel.prefab +++ b/assets/prefabs/PageLevel.prefab @@ -7956,6 +7956,14 @@ "__uuid__": "29ff0bfc-d5cf-4ad1-b8cb-61bdfd4850ef", "__expectedType__": "cc.Prefab" }, + "wrongModalPrefab": { + "__uuid__": "455c7845-d090-4cd9-aeb4-1f5cad616bb5", + "__expectedType__": "cc.Prefab" + }, + "timeoutModalPrefab": { + "__uuid__": "e41c722f-f605-47f7-9ce4-abff0ed2020f", + "__expectedType__": "cc.Prefab" + }, "_id": "" }, { diff --git a/assets/prefabs/PageLevel.ts b/assets/prefabs/PageLevel.ts index d3c1cc1..8f019bb 100644 --- a/assets/prefabs/PageLevel.ts +++ b/assets/prefabs/PageLevel.ts @@ -1,7 +1,6 @@ import { _decorator, Node, EditBox, instantiate, Vec3, Button, Label, Sprite, SpriteFrame, AudioClip, AudioSource, Prefab } from 'cc'; 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 { StaminaManager } from 'db://assets/scripts/utils/StaminaManager'; import { WxSDK } from 'db://assets/scripts/utils/WxSDK'; import { LevelDataManager } from 'db://assets/scripts/utils/LevelDataManager'; @@ -10,13 +9,16 @@ import { RuntimeLevelConfig } from 'db://assets/scripts/types/LevelTypes'; import { ToastManager } from 'db://assets/scripts/utils/ToastManager'; import { ShareManager } from 'db://assets/scripts/utils/ShareManager'; import { PassModal } from 'db://assets/prefabs/PassModal'; -import { StaminaInfo } from 'db://assets/scripts/types/ApiTypes'; +import { WrongModal } from 'db://assets/prefabs/WrongModal'; +import { TimeoutModal } from 'db://assets/prefabs/TimeoutModal'; +import { StaminaInfo, NextLevelData } from 'db://assets/scripts/types/ApiTypes'; import { AchievementTitleManager } from 'db://assets/scripts/utils/AchievementTitleManager'; const { ccclass, property } = _decorator; /** * 关卡页面组件 * 继承 BaseView,实现页面生命周期 + * 关卡流程由服务端 NextLevelData 驱动,客户端不再维护关卡列表 */ @ccclass('PageLevel') export class PageLevel extends BaseView { @@ -29,9 +31,6 @@ export class PageLevel extends BaseView { /** 答案正确后展示包袱答案的停留时间 */ private static readonly PASS_MODAL_DELAY_MS = 2000; - /** 答案错误后清空输入的延迟,给失败音效和错误答案留出反馈时间 */ - private static readonly CLEAR_INPUT_DELAY_MS = 500; - // ========== 节点引用 ========== @property(Node) inputLayout: Node | null = null; @@ -88,17 +87,11 @@ export class PageLevel extends BaseView { @property(Label) liveLabel: Label | null = null; - /** 关卡标题标签,显示为“第 N 关” */ + /** 关卡标题标签,显示为"第 N 关" */ @property(Label) titleLevelLabel: Label | null = null; // ========== 配置属性 ========== - @property({ - min: 0, - tooltip: '当前关卡索引' - }) - currentLevelIndex: number = 0; - @property(AudioClip) clickAudio: AudioClip | null = null; @@ -111,6 +104,12 @@ export class PageLevel extends BaseView { @property(Prefab) passModalPrefab: Prefab | null = null; + @property(Prefab) + wrongModalPrefab: Prefab | null = null; + + @property(Prefab) + timeoutModalPrefab: Prefab | null = null; + // ========== 内部状态 ========== /** 当前创建的输入框节点数组 */ private _inputNodes: Node[] = []; @@ -157,12 +156,32 @@ export class PageLevel extends BaseView { /** 本次通关弹窗使用的已通关数量 */ private _passModalCompletedLevelCount: number | null = null; + /** 错误弹窗实例 */ + private _wrongModalNode: Node | null = null; + + /** 超时弹窗实例 */ + private _timeoutModalNode: Node | null = null; + /** 是否处于分享挑战模式 */ private _isShareMode: boolean = false; /** 体力恢复倒计时定时器 */ private _staminaTimerId: ReturnType | null = null; + // ========== 关卡驱动状态(NextLevel 驱动) ========== + + /** 当前关卡 ID */ + private _currentLevelId: string = ''; + + /** 当前关卡编号(仅显示用,来自 NextLevelData.level) */ + private _currentLevelNumber: number = 0; + + /** 下一关数据(来自 complete 接口返回),点击"下一关"时使用 */ + private _nextLevelData: NextLevelData | null = null; + + /** 分享模式下的关卡索引(仅分享模式使用) */ + private _shareLevelIndex: number = 0; + /** * 页面首次加载时调用 */ @@ -173,14 +192,20 @@ export class PageLevel extends BaseView { this._isShareMode = params?.shareMode === true; if (this._isShareMode) { - this.currentLevelIndex = 0; + this._shareLevelIndex = 0; console.log('[PageLevel] 进入分享挑战模式'); } else { - // 根据关卡列表找到第一个未通关的关卡 - this.currentLevelIndex = LevelDataManager.instance.getFirstUncompletedIndex(); - StorageManager.setCurrentLevelIndex(this.currentLevelIndex); - console.log(`[PageLevel] 进入第一个未通关关卡: 第 ${this.currentLevelIndex + 1} 关`); + // 从 AuthManager 获取首关数据(由 PageLoading → game-data 提供) + const nextLevel = AuthManager.instance.nextLevel; + if (nextLevel) { + this._currentLevelId = nextLevel.id; + this._currentLevelNumber = nextLevel.level; + console.log(`[PageLevel] 进入关卡: 第 ${nextLevel.level} 关 (${nextLevel.id})`); + } else { + console.warn('[PageLevel] 没有可用关卡'); + } } + this.updateStaminaLabel(); this.initIconSetting(); this.initUnlockButtons(); @@ -218,6 +243,8 @@ export class PageLevel extends BaseView { this.clearPunchBlocks(); this.stopCountdown(); this._closePassModal(); + this._closeWrongModal(); + this._closeTimeoutModal(); this._stopStaminaRecoverTimer(); // 清理事件监听 @@ -229,77 +256,85 @@ export class PageLevel extends BaseView { /** * 进入关卡并初始化 - * 1. 加载关卡图片资源 + * 1. 加载关卡图片资源(从缓存或 NextLevelData) * 2. 调用进入关卡接口(消耗体力,获取答案和线索) * 3. 启动倒计时 */ private async _enterAndInitLevel(): Promise { - // 先加载关卡图片资源 let config: RuntimeLevelConfig | null = null; if (this._isShareMode) { - config = await ShareManager.instance.ensureShareLevelReady(this.currentLevelIndex); + // 分享模式:使用 ShareManager 的关卡数据 + config = await ShareManager.instance.ensureShareLevelReady(this._shareLevelIndex); } else { - config = LevelDataManager.instance.getLevelConfig(this.currentLevelIndex); + // 正常模式:先尝试从缓存获取(PageLoading 初始化时已加载首关) + config = LevelDataManager.instance.getLevelConfig(this._currentLevelId); + if (!config) { - console.log(`[PageLevel] 关卡 ${this.currentLevelIndex + 1} 资源未缓存,开始加载...`); - config = await LevelDataManager.instance.ensureLevelReady(this.currentLevelIndex); + // 缓存未命中,从 nextLevel 数据加载(complete 返回的下一关) + const nextLevelData = this._nextLevelData ?? AuthManager.instance.nextLevel; + if (nextLevelData && nextLevelData.id === this._currentLevelId) { + console.log(`[PageLevel] 关卡 ${this._currentLevelId} 资源未缓存,开始加载...`); + config = await LevelDataManager.instance.ensureLevelReady(nextLevelData); + } } } if (!config) { - console.warn(`[PageLevel] 没有找到关卡配置,索引: ${this.currentLevelIndex}`); + console.warn(`[PageLevel] 没有找到关卡配置,ID: ${this._currentLevelId}`); return; } // 非分享模式下,调用进入关卡接口获取答案和线索 if (!this._isShareMode) { - const levelId = LevelDataManager.instance.getLevelId(this.currentLevelIndex); - if (levelId) { - const enterData = await StaminaManager.instance.enterLevel(levelId); - if (!enterData) { - // 进入关卡失败(可能是体力不足) - const stamina = StaminaManager.instance.getStamina(); - if (stamina.current <= 0) { - ToastManager.show('体力不足,请等待恢复'); - this._startStaminaRecoverTimer(); - } else { - ToastManager.show('进入关卡失败,请重试'); - } - this.updateStaminaLabel(); - return; + const enterData = await StaminaManager.instance.enterLevel(this._currentLevelId); + if (!enterData) { + // 进入关卡失败(可能是体力不足) + const stamina = StaminaManager.instance.getStamina(); + if (stamina.current <= 0) { + ToastManager.show('体力不足,请等待恢复'); + this._startStaminaRecoverTimer(); + } else { + ToastManager.show('进入关卡失败,请重试'); } - - // 提示用户消耗体力 - ToastManager.show(`消耗1点体力,剩余 ${enterData.stamina.current}/${this._getStaminaMax(enterData.stamina)}`); - - // 用 enter 接口返回的数据更新关卡配置(填充答案和线索) - LevelDataManager.instance.updateLevelDetails( - this.currentLevelIndex, - { - answer: enterData.answer, - image1Description: enterData.image1Description, - image2Description: enterData.image2Description, - punchline: enterData.punchline, - hint1: enterData.hint1, - hint2: enterData.hint2, - hint3: enterData.hint3, - } - ); - - // 重新获取更新后的配置 - config = LevelDataManager.instance.getLevelConfig(this.currentLevelIndex); - if (!config) { - console.error('[PageLevel] 更新关卡详情后获取配置失败'); - return; - } - - // 更新体力显示 this.updateStaminaLabel(); + return; + } + + // 提示用户消耗体力 + ToastManager.show(`消耗1点体力,剩余 ${enterData.stamina.current}/${this._getStaminaMax(enterData.stamina)}`); + + // 用 enter 接口返回的数据更新关卡配置(填充答案和线索) + LevelDataManager.instance.updateLevelDetails( + this._currentLevelId, + { + answer: enterData.answer, + image1Description: enterData.image1Description, + image2Description: enterData.image2Description, + punchline: enterData.punchline, + hint1: enterData.hint1, + hint2: enterData.hint2, + hint3: enterData.hint3, + } + ); + + // 重新获取更新后的配置 + config = LevelDataManager.instance.getLevelConfig(this._currentLevelId); + if (!config) { + console.error('[PageLevel] 更新关卡详情后获取配置失败'); + return; + } + + // 更新体力显示 + this.updateStaminaLabel(); + + // 预加载下一关图片(enter 返回的 preloadNextLevel) + if (enterData.preloadNextLevel) { + LevelDataManager.instance.preloadLevel(enterData.preloadNextLevel); } } - console.log(`[PageLevel] 初始化关卡 ${this.currentLevelIndex + 1}: ${config.name}`); + console.log(`[PageLevel] 初始化关卡 第${this._currentLevelNumber}关: ${config.name}`); this._applyLevelConfig(config); this.startCountdown(); } @@ -315,7 +350,7 @@ export class PageLevel extends BaseView { // 重置倒计时状态 this._isTimeUp = false; - this._countdown = 60; + this._countdown = config.timeLimit ?? 60; // 设置主图(图片1) this.setMainImage(config.spriteFrame1); @@ -355,17 +390,16 @@ export class PageLevel extends BaseView { // 更新倒计时显示 this.updateClockLabel(); - // 预加载下一关图片(静默加载,不阻塞) + // 分享模式下预加载下一关 if (this._isShareMode) { - const nextIndex = this.currentLevelIndex + 1; + const nextIndex = this._shareLevelIndex + 1; if (nextIndex < ShareManager.instance.getShareLevelCount()) { ShareManager.instance.ensureShareLevelReady(nextIndex).catch(() => {}); } - } else { - LevelDataManager.instance.preloadNextLevel(this.currentLevelIndex); } + // 正常模式的预加载在 enter 返回 preloadNextLevel 时已处理 - console.log(`[PageLevel] 初始化关卡 ${this.currentLevelIndex + 1}, 答案长度: ${Array.from(config.answer ?? '').length}`); + console.log(`[PageLevel] 初始化关卡 第${this._currentLevelNumber}关, 答案长度: ${Array.from(config.answer ?? '').length}`); } /** @@ -806,7 +840,7 @@ export class PageLevel extends BaseView { private updateTitleLevelLabel(): void { if (!this.titleLevelLabel) return; - this.titleLevelLabel.string = `第 ${this.currentLevelIndex + 1} 关`; + this.titleLevelLabel.string = `第 ${this._currentLevelNumber} 关`; } /** @@ -972,12 +1006,12 @@ export class PageLevel extends BaseView { * 开始倒计时 */ private startCountdown(): void { - this._countdown = 60; + // _countdown 已在 _applyLevelConfig 中根据 timeLimit 设置 this._isTimeUp = false; this._levelStartTime = Date.now(); this.updateClockLabel(); this.schedule(this.onCountdownTick, 1); - console.log('[PageLevel] 开始倒计时 60 秒'); + console.log(`[PageLevel] 开始倒计时 ${this._countdown} 秒`); } /** @@ -1018,7 +1052,7 @@ export class PageLevel extends BaseView { private onTimeUp(): void { console.log('[PageLevel] 倒计时结束!'); this.playFailSound(); - // 可以在这里添加游戏结束逻辑 + this._showTimeoutModal(); } // ========== 体力值相关方法 ========== @@ -1086,7 +1120,7 @@ export class PageLevel extends BaseView { this._stopStaminaRecoverTimer(); - if (newCurrent < currentStamina.max) { + if (newCurrent < currentMaxStamina) { this._startStaminaRecoverTimer(); } }, 1000); @@ -1151,22 +1185,26 @@ export class PageLevel extends BaseView { this._showPassModal(); } + /** + * 上报通关并获取下一关数据 + */ private reportLevelCompleted(levelId: string, timeSpent: number): void { if (!this._isShareMode) { - // 标记关卡为已通关(本地缓存),通关上报并行执行,不阻塞包袱展示节奏 - const wasCompleted = LevelDataManager.instance.isLevelCompleted(this.currentLevelIndex); - if (!wasCompleted) { - AuthManager.instance.addCompletedLevelCount(); - } + // 乐观更新通关计数(用于称号展示) + AuthManager.instance.addCompletedLevelCount(); this._passModalCompletedLevelCount = AuthManager.instance.completedLevelCount; - LevelDataManager.instance.markLevelCompleted(this.currentLevelIndex); + void StaminaManager.instance.completeLevel(levelId, timeSpent).then((result) => { if (result) { - if (!result.firstClear && !wasCompleted) { + // 保存 complete 返回的下一关数据 + this._nextLevelData = result.nextLevel; + + if (!result.firstClear) { + // 非首次通关,回退乐观更新 AuthManager.instance.addCompletedLevelCount(-1); this._passModalCompletedLevelCount = AuthManager.instance.completedLevelCount; } - console.log(`[PageLevel] 通关上报成功,首次通关: ${result.firstClear}`); + console.log(`[PageLevel] 通关上报成功,首次通关: ${result.firstClear}, 有下一关: ${!!result.nextLevel}`); } }); return; @@ -1215,13 +1253,13 @@ export class PageLevel extends BaseView { const passModal = modalNode.getComponent(PassModal); if (passModal) { passModal.setParams({ - levelIndex: this.currentLevelIndex + 1, + levelIndex: this._currentLevelNumber, titleInfo: AchievementTitleManager.getTitleInfo(this._getPassModalCompletedLevelCount()) }); passModal.setCallbacks({ onNextLevel: () => { this._closePassModal(); - this.nextLevel(); + this.goToNextLevel(); }, onShare: () => { // 分享后不关闭弹窗,用户可继续点击下一关 @@ -1251,6 +1289,127 @@ export class PageLevel extends BaseView { } } + /** + * 显示错误弹窗 + */ + private _showWrongModal(): void { + if (!this.wrongModalPrefab) { + console.warn('[PageLevel] wrongModalPrefab 未设置'); + return; + } + + // 如果弹窗已显示,不再重复创建 + if (this._wrongModalNode && this._wrongModalNode.isValid) { + return; + } + + const modalNode = instantiate(this.wrongModalPrefab); + modalNode.setPosition(PageLevel.ZERO_POS); + modalNode.setSiblingIndex(WrongModal.MODAL_Z_INDEX); + + const canvasNode = this.node.parent; + if (canvasNode) { + canvasNode.addChild(modalNode); + } else { + this.node.addChild(modalNode); + } + this._wrongModalNode = modalNode; + + const wrongModal = modalNode.getComponent(WrongModal); + if (wrongModal) { + wrongModal.setCallbacks({ + onContinue: () => { + this._closeWrongModal(); + this.clearInputText(); + } + }); + wrongModal.onViewLoad(); + wrongModal.onViewShow(); + } + + console.log('[PageLevel] 显示错误弹窗'); + } + + /** + * 关闭错误弹窗 + */ + private _closeWrongModal(): void { + if (this._wrongModalNode && this._wrongModalNode.isValid) { + this._wrongModalNode.destroy(); + this._wrongModalNode = null; + console.log('[PageLevel] 关闭错误弹窗'); + } + } + + /** + * 显示超时弹窗 + */ + private _showTimeoutModal(): void { + if (!this.timeoutModalPrefab) { + console.warn('[PageLevel] timeoutModalPrefab 未设置'); + return; + } + + // 如果弹窗已显示,不再重复创建 + if (this._timeoutModalNode && this._timeoutModalNode.isValid) { + return; + } + + const modalNode = instantiate(this.timeoutModalPrefab); + modalNode.setPosition(PageLevel.ZERO_POS); + modalNode.setSiblingIndex(TimeoutModal.MODAL_Z_INDEX); + + const canvasNode = this.node.parent; + if (canvasNode) { + canvasNode.addChild(modalNode); + } else { + this.node.addChild(modalNode); + } + this._timeoutModalNode = modalNode; + + const timeoutModal = modalNode.getComponent(TimeoutModal); + if (timeoutModal) { + timeoutModal.setParams({ + levelIndex: this._currentLevelNumber + }); + timeoutModal.setCallbacks({ + onShare: () => { + console.log('[PageLevel] 超时弹窗分享完成'); + }, + onRestart: () => { + this._closeTimeoutModal(); + this._enterAndInitLevel().catch(err => { + console.error('[PageLevel] 重新进入关卡失败:', err); + }); + }, + onHome: () => { + this._closeTimeoutModal(); + if (this._isShareMode) { + ShareManager.instance.clearShareMode(); + ViewManager.instance.replace('PageHome'); + } else { + ViewManager.instance.back(); + } + } + }); + timeoutModal.onViewLoad(); + timeoutModal.onViewShow(); + } + + console.log('[PageLevel] 显示超时弹窗'); + } + + /** + * 关闭超时弹窗 + */ + private _closeTimeoutModal(): void { + if (this._timeoutModalNode && this._timeoutModalNode.isValid) { + this._timeoutModalNode.destroy(); + this._timeoutModalNode = null; + console.log('[PageLevel] 关闭超时弹窗'); + } + } + /** * 显示错误提示 */ @@ -1263,32 +1422,21 @@ export class PageLevel extends BaseView { // 触发手机震动 WxSDK.vibrateLong(); - // 显示 Toast 提示 - ToastManager.show('答案错误,再试试吧!'); - - // 输入识别失败或答案错误后延迟清空,避免错误内容瞬间消失 - void this.delay(PageLevel.CLEAR_INPUT_DELAY_MS).then(() => { - if (!this._isTransitioning) { - this.clearInputText(); - } - }); + // 显示错误弹窗 + this._showWrongModal(); } /** * 进入下一关 + * 正常模式:使用 complete 返回的 nextLevel 数据 + * 分享模式:按索引递增 */ - private async nextLevel(): Promise { - // 标记当前关卡已通关 - if (!this._isShareMode) { - StorageManager.onLevelCompleted(this.currentLevelIndex); - LevelDataManager.instance.markLevelCompleted(this.currentLevelIndex); - } - - // 查找下一个未通关的关卡 + private async goToNextLevel(): Promise { if (this._isShareMode) { - this.currentLevelIndex++; + // 分享模式:按索引递增 + this._shareLevelIndex++; const totalLevels = ShareManager.instance.getShareLevelCount(); - if (this.currentLevelIndex >= totalLevels) { + if (this._shareLevelIndex >= totalLevels) { console.log('[PageLevel] 分享关卡全部完成'); this.stopCountdown(); ShareManager.instance.clearShareMode(); @@ -1296,20 +1444,23 @@ export class PageLevel extends BaseView { return; } } else { - const nextIndex = LevelDataManager.instance.getNextUncompletedIndex(this.currentLevelIndex); - if (nextIndex < 0) { - // 所有关卡全部通关 + // 正常模式:使用 complete 返回的 nextLevel + if (!this._nextLevelData) { + // 没有下一关 → 全部通关 console.log('[PageLevel] 恭喜通关!所有关卡已完成'); this.stopCountdown(); ViewManager.instance.back(); return; } - this.currentLevelIndex = nextIndex; - StorageManager.setCurrentLevelIndex(this.currentLevelIndex); + + // 切换到下一关 + this._currentLevelId = this._nextLevelData.id; + this._currentLevelNumber = this._nextLevelData.level; + this._nextLevelData = null; } // 重置并加载下一关(包含进入关卡接口调用) await this._enterAndInitLevel(); - console.log(`[PageLevel] 进入关卡 ${this.currentLevelIndex + 1}`); + console.log(`[PageLevel] 进入关卡 第${this._currentLevelNumber}关`); } } diff --git a/assets/prefabs/PageWriteLevels.ts b/assets/prefabs/PageWriteLevels.ts index bad459e..8b54341 100644 --- a/assets/prefabs/PageWriteLevels.ts +++ b/assets/prefabs/PageWriteLevels.ts @@ -183,7 +183,9 @@ export class PageWriteLevels extends BaseView { private _initLevelList(): void { this._clearList(); - this._levelCount = LevelDataManager.instance.getLevelCount(); + // TODO: LevelDataManager API 已重构为 NextLevel 驱动,此页面需要重新设计数据来源 + // this._levelCount = LevelDataManager.instance.getLevelCount(); + this._levelCount = 0; console.log('[PageWriteLevels] 关卡总数:', this._levelCount); if (this._levelCount === 0) { @@ -316,11 +318,11 @@ export class PageWriteLevels extends BaseView { /** * 异步加载关卡资源并刷新封面图和名称。 - * LevelDataManager 采用懒加载,初始化时只加载了第一关图片, - * 其余关卡通过 ensureLevelReady 按需加载。 + * TODO: LevelDataManager API 已重构为 NextLevel 驱动,此方法需要重新设计 */ private async _loadAndRefreshCover(item: Node, index: number): Promise { - const config = await LevelDataManager.instance.ensureLevelReady(index); + // const config = await LevelDataManager.instance.ensureLevelReady(index); + const config = null as any; // TODO: 需要适配新 API if (!config || !item.isValid) return; const levelCover = item.getChildByName('LevelCover'); @@ -548,14 +550,15 @@ export class PageWriteLevels extends BaseView { * 将选中的关卡索引转换为关卡 ID 数组 */ private _getSelectedLevelIds(): string[] { + // TODO: LevelDataManager API 已重构为 NextLevel 驱动,此方法需要重新设计 const ids: string[] = []; - const sortedIndices = Array.from(this._selectedIndices).sort((a, b) => a - b); - for (const index of sortedIndices) { - const config = LevelDataManager.instance.getLevelConfig(index); - if (config) { - ids.push(config.id); - } - } + // const sortedIndices = Array.from(this._selectedIndices).sort((a, b) => a - b); + // for (const index of sortedIndices) { + // const config = LevelDataManager.instance.getLevelConfig(index); + // if (config) { + // ids.push(config.id); + // } + // } return ids; } diff --git a/assets/prefabs/PassModal.prefab b/assets/prefabs/PassModal.prefab index 46865ff..8dc214d 100644 --- a/assets/prefabs/PassModal.prefab +++ b/assets/prefabs/PassModal.prefab @@ -1157,7 +1157,7 @@ }, "_contentSize": { "__type__": "cc.Size", - "width": 320, + "width": 426.25, "height": 100.8 }, "_anchorPoint": { @@ -1623,7 +1623,7 @@ }, "_contentSize": { "__type__": "cc.Size", - "width": 402.03125, + "width": 442.03125, "height": 75.6 }, "_anchorPoint": { @@ -3141,4 +3141,4 @@ "instance": null, "targetOverrides": null } -] +] \ No newline at end of file diff --git a/assets/prefabs/TimeoutModal.prefab b/assets/prefabs/TimeoutModal.prefab index 9e10c60..6021df0 100644 --- a/assets/prefabs/TimeoutModal.prefab +++ b/assets/prefabs/TimeoutModal.prefab @@ -20,19 +20,25 @@ "_children": [ { "__id__": 2 + }, + { + "__id__": 10 } ], "_active": true, "_components": [ { - "__id__": 98 + "__id__": 114 }, { - "__id__": 100 + "__id__": 116 + }, + { + "__id__": 118 } ], "_prefab": { - "__id__": 102 + "__id__": 120 }, "_lpos": { "__type__": "cc.Vec3", @@ -63,6 +69,181 @@ }, "_id": "" }, + { + "__type__": "cc.Node", + "_name": "BgMask", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 3 + }, + { + "__id__": 5 + }, + { + "__id__": 7 + } + ], + "_prefab": { + "__id__": 9 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": { + "__id__": 4 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 1080, + "height": 2160 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "4escl3UjlKyIlR7QRNpeX+" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": { + "__id__": 6 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 0, + "g": 0, + "b": 0, + "a": 121 + }, + "_spriteFrame": { + "__uuid__": "7d8f9b89-4fd1-4c9f-a3ab-38ec7cded7ca@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + "_type": 1, + "_fillType": 0, + "_sizeMode": 0, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_useGrayscale": false, + "_atlas": null, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "e7IdZ9rqVOw7BM2R+GoA4O" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": { + "__id__": 8 + }, + "_alignFlags": 45, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 0, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 40, + "_originalHeight": 36, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "edNixX0UBH7ZhdM+36Mtsy" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "924aSjHrBDI58vBHfvyR2b", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, { "__type__": "cc.Node", "_name": "dialogPanel", @@ -72,36 +253,36 @@ "__id__": 1 }, "_children": [ - { - "__id__": 3 - }, { "__id__": 11 }, { - "__id__": 17 + "__id__": 21 }, { - "__id__": 39 + "__id__": 27 }, { "__id__": 51 }, { - "__id__": 71 + "__id__": 63 + }, + { + "__id__": 85 } ], "_active": true, "_components": [ { - "__id__": 93 + "__id__": 109 }, { - "__id__": 95 + "__id__": 111 } ], "_prefab": { - "__id__": 97 + "__id__": 113 }, "_lpos": { "__type__": "cc.Vec3", @@ -138,23 +319,26 @@ "_objFlags": 0, "__editorExtras__": {}, "_parent": { - "__id__": 2 + "__id__": 10 }, "_children": [], "_active": true, "_components": [ { - "__id__": 4 + "__id__": 12 }, { - "__id__": 6 + "__id__": 14 }, { - "__id__": 8 + "__id__": 16 + }, + { + "__id__": 18 } ], "_prefab": { - "__id__": 10 + "__id__": 20 }, "_lpos": { "__type__": "cc.Vec3", @@ -191,11 +375,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 3 + "__id__": 11 }, "_enabled": true, "__prefab": { - "__id__": 5 + "__id__": 13 }, "_contentSize": { "__type__": "cc.Size", @@ -219,11 +403,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 3 + "__id__": 11 }, "_enabled": true, "__prefab": { - "__id__": 7 + "__id__": 15 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -264,11 +448,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 3 + "__id__": 11 }, "_enabled": true, "__prefab": { - "__id__": 9 + "__id__": 17 }, "_alignFlags": 33, "_target": null, @@ -294,6 +478,62 @@ "__type__": "cc.CompPrefabInfo", "fileId": "e6kTdMyd1C3ZK6dQGDNQYb" }, + { + "__type__": "cc.Button", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 11 + }, + "_enabled": true, + "__prefab": { + "__id__": 19 + }, + "clickEvents": [], + "_interactable": true, + "_transition": 3, + "_normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_hoverColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_pressedColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_normalSprite": null, + "_hoverSprite": null, + "_pressedSprite": null, + "_disabledSprite": null, + "_duration": 0.1, + "_zoomScale": 1.2, + "_target": null, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "45y0DHdKpBuKNEkMgDtEZ9" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -313,20 +553,20 @@ "_objFlags": 0, "__editorExtras__": {}, "_parent": { - "__id__": 2 + "__id__": 10 }, "_children": [], "_active": true, "_components": [ { - "__id__": 12 + "__id__": 22 }, { - "__id__": 14 + "__id__": 24 } ], "_prefab": { - "__id__": 16 + "__id__": 26 }, "_lpos": { "__type__": "cc.Vec3", @@ -363,11 +603,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 11 + "__id__": 21 }, "_enabled": true, "__prefab": { - "__id__": 13 + "__id__": 23 }, "_contentSize": { "__type__": "cc.Size", @@ -391,11 +631,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 11 + "__id__": 21 }, "_enabled": true, "__prefab": { - "__id__": 15 + "__id__": 25 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -475,30 +715,33 @@ "_objFlags": 0, "__editorExtras__": {}, "_parent": { - "__id__": 2 + "__id__": 10 }, "_children": [ { - "__id__": 18 - }, - { - "__id__": 26 - } - ], - "_active": true, - "_components": [ - { - "__id__": 32 - }, - { - "__id__": 34 + "__id__": 28 }, { "__id__": 36 } ], + "_active": true, + "_components": [ + { + "__id__": 42 + }, + { + "__id__": 44 + }, + { + "__id__": 46 + }, + { + "__id__": 48 + } + ], "_prefab": { - "__id__": 38 + "__id__": 50 }, "_lpos": { "__type__": "cc.Vec3", @@ -535,23 +778,23 @@ "_objFlags": 0, "__editorExtras__": {}, "_parent": { - "__id__": 17 + "__id__": 27 }, "_children": [], "_active": true, "_components": [ { - "__id__": 19 + "__id__": 29 }, { - "__id__": 21 + "__id__": 31 }, { - "__id__": 23 + "__id__": 33 } ], "_prefab": { - "__id__": 25 + "__id__": 35 }, "_lpos": { "__type__": "cc.Vec3", @@ -588,11 +831,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 18 + "__id__": 28 }, "_enabled": true, "__prefab": { - "__id__": 20 + "__id__": 30 }, "_contentSize": { "__type__": "cc.Size", @@ -616,11 +859,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 18 + "__id__": 28 }, "_enabled": true, "__prefab": { - "__id__": 22 + "__id__": 32 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -687,11 +930,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 18 + "__id__": 28 }, "_enabled": true, "__prefab": { - "__id__": 24 + "__id__": 34 }, "_alignFlags": 18, "_target": null, @@ -736,20 +979,20 @@ "_objFlags": 0, "__editorExtras__": {}, "_parent": { - "__id__": 17 + "__id__": 27 }, "_children": [], "_active": true, "_components": [ { - "__id__": 27 + "__id__": 37 }, { - "__id__": 29 + "__id__": 39 } ], "_prefab": { - "__id__": 31 + "__id__": 41 }, "_lpos": { "__type__": "cc.Vec3", @@ -786,11 +1029,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 26 + "__id__": 36 }, "_enabled": true, "__prefab": { - "__id__": 28 + "__id__": 38 }, "_contentSize": { "__type__": "cc.Size", @@ -814,11 +1057,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 26 + "__id__": 36 }, "_enabled": true, "__prefab": { - "__id__": 30 + "__id__": 40 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -872,11 +1115,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 17 + "__id__": 27 }, "_enabled": true, "__prefab": { - "__id__": 33 + "__id__": 43 }, "_contentSize": { "__type__": "cc.Size", @@ -900,11 +1143,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 17 + "__id__": 27 }, "_enabled": true, "__prefab": { - "__id__": 35 + "__id__": 45 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -945,11 +1188,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 17 + "__id__": 27 }, "_enabled": true, "__prefab": { - "__id__": 37 + "__id__": 47 }, "_opacity": 192, "_id": "" @@ -958,6 +1201,62 @@ "__type__": "cc.CompPrefabInfo", "fileId": "f7gu9ig4tNc4oEnw1gyXS4" }, + { + "__type__": "cc.Button", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 27 + }, + "_enabled": true, + "__prefab": { + "__id__": 49 + }, + "clickEvents": [], + "_interactable": true, + "_transition": 3, + "_normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_hoverColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_pressedColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_normalSprite": null, + "_hoverSprite": null, + "_pressedSprite": null, + "_disabledSprite": null, + "_duration": 0.1, + "_zoomScale": 1.2, + "_target": null, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "6bn48W0q9FW5R2RsqGeVQ7" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -977,24 +1276,24 @@ "_objFlags": 0, "__editorExtras__": {}, "_parent": { - "__id__": 2 + "__id__": 10 }, "_children": [ { - "__id__": 40 + "__id__": 52 } ], "_active": true, "_components": [ { - "__id__": 46 + "__id__": 58 }, { - "__id__": 48 + "__id__": 60 } ], "_prefab": { - "__id__": 50 + "__id__": 62 }, "_lpos": { "__type__": "cc.Vec3", @@ -1031,20 +1330,20 @@ "_objFlags": 0, "__editorExtras__": {}, "_parent": { - "__id__": 39 + "__id__": 51 }, "_children": [], "_active": true, "_components": [ { - "__id__": 41 + "__id__": 53 }, { - "__id__": 43 + "__id__": 55 } ], "_prefab": { - "__id__": 45 + "__id__": 57 }, "_lpos": { "__type__": "cc.Vec3", @@ -1081,11 +1380,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 40 + "__id__": 52 }, "_enabled": true, "__prefab": { - "__id__": 42 + "__id__": 54 }, "_contentSize": { "__type__": "cc.Size", @@ -1109,11 +1408,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 40 + "__id__": 52 }, "_enabled": true, "__prefab": { - "__id__": 44 + "__id__": 56 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1167,11 +1466,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 39 + "__id__": 51 }, "_enabled": true, "__prefab": { - "__id__": 47 + "__id__": 59 }, "_contentSize": { "__type__": "cc.Size", @@ -1195,11 +1494,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 39 + "__id__": 51 }, "_enabled": true, "__prefab": { - "__id__": 49 + "__id__": 61 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1253,27 +1552,30 @@ "_objFlags": 0, "__editorExtras__": {}, "_parent": { - "__id__": 2 + "__id__": 10 }, "_children": [ { - "__id__": 52 + "__id__": 64 }, { - "__id__": 60 + "__id__": 72 } ], "_active": true, "_components": [ { - "__id__": 66 + "__id__": 78 }, { - "__id__": 68 + "__id__": 80 + }, + { + "__id__": 82 } ], "_prefab": { - "__id__": 70 + "__id__": 84 }, "_lpos": { "__type__": "cc.Vec3", @@ -1310,23 +1612,23 @@ "_objFlags": 0, "__editorExtras__": {}, "_parent": { - "__id__": 51 + "__id__": 63 }, "_children": [], "_active": true, "_components": [ { - "__id__": 53 + "__id__": 65 }, { - "__id__": 55 + "__id__": 67 }, { - "__id__": 57 + "__id__": 69 } ], "_prefab": { - "__id__": 59 + "__id__": 71 }, "_lpos": { "__type__": "cc.Vec3", @@ -1363,11 +1665,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 52 + "__id__": 64 }, "_enabled": true, "__prefab": { - "__id__": 54 + "__id__": 66 }, "_contentSize": { "__type__": "cc.Size", @@ -1391,11 +1693,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 52 + "__id__": 64 }, "_enabled": true, "__prefab": { - "__id__": 56 + "__id__": 68 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1462,11 +1764,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 52 + "__id__": 64 }, "_enabled": true, "__prefab": { - "__id__": 58 + "__id__": 70 }, "_alignFlags": 18, "_target": null, @@ -1511,20 +1813,20 @@ "_objFlags": 0, "__editorExtras__": {}, "_parent": { - "__id__": 51 + "__id__": 63 }, "_children": [], "_active": true, "_components": [ { - "__id__": 61 + "__id__": 73 }, { - "__id__": 63 + "__id__": 75 } ], "_prefab": { - "__id__": 65 + "__id__": 77 }, "_lpos": { "__type__": "cc.Vec3", @@ -1561,11 +1863,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 60 + "__id__": 72 }, "_enabled": true, "__prefab": { - "__id__": 62 + "__id__": 74 }, "_contentSize": { "__type__": "cc.Size", @@ -1589,11 +1891,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 60 + "__id__": 72 }, "_enabled": true, "__prefab": { - "__id__": 64 + "__id__": 76 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1647,11 +1949,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 51 + "__id__": 63 }, "_enabled": true, "__prefab": { - "__id__": 67 + "__id__": 79 }, "_contentSize": { "__type__": "cc.Size", @@ -1675,11 +1977,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 51 + "__id__": 63 }, "_enabled": true, "__prefab": { - "__id__": 69 + "__id__": 81 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1714,6 +2016,62 @@ "__type__": "cc.CompPrefabInfo", "fileId": "deIXk3pwxOVqLs/mP5fa30" }, + { + "__type__": "cc.Button", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 63 + }, + "_enabled": true, + "__prefab": { + "__id__": 83 + }, + "clickEvents": [], + "_interactable": true, + "_transition": 3, + "_normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_hoverColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_pressedColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_normalSprite": null, + "_hoverSprite": null, + "_pressedSprite": null, + "_disabledSprite": null, + "_duration": 0.1, + "_zoomScale": 1.2, + "_target": null, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "07DYNWu3pMroj3lM2H6Oky" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -1733,30 +2091,33 @@ "_objFlags": 0, "__editorExtras__": {}, "_parent": { - "__id__": 2 + "__id__": 10 }, "_children": [ { - "__id__": 72 + "__id__": 86 }, { - "__id__": 80 + "__id__": 94 } ], "_active": true, "_components": [ { - "__id__": 86 + "__id__": 100 }, { - "__id__": 88 + "__id__": 102 }, { - "__id__": 90 + "__id__": 104 + }, + { + "__id__": 106 } ], "_prefab": { - "__id__": 92 + "__id__": 108 }, "_lpos": { "__type__": "cc.Vec3", @@ -1793,23 +2154,23 @@ "_objFlags": 0, "__editorExtras__": {}, "_parent": { - "__id__": 71 + "__id__": 85 }, "_children": [], "_active": true, "_components": [ { - "__id__": 73 + "__id__": 87 }, { - "__id__": 75 + "__id__": 89 }, { - "__id__": 77 + "__id__": 91 } ], "_prefab": { - "__id__": 79 + "__id__": 93 }, "_lpos": { "__type__": "cc.Vec3", @@ -1846,11 +2207,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 72 + "__id__": 86 }, "_enabled": true, "__prefab": { - "__id__": 74 + "__id__": 88 }, "_contentSize": { "__type__": "cc.Size", @@ -1874,11 +2235,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 72 + "__id__": 86 }, "_enabled": true, "__prefab": { - "__id__": 76 + "__id__": 90 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1945,11 +2306,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 72 + "__id__": 86 }, "_enabled": true, "__prefab": { - "__id__": 78 + "__id__": 92 }, "_alignFlags": 18, "_target": null, @@ -1994,20 +2355,20 @@ "_objFlags": 0, "__editorExtras__": {}, "_parent": { - "__id__": 71 + "__id__": 85 }, "_children": [], "_active": true, "_components": [ { - "__id__": 81 + "__id__": 95 }, { - "__id__": 83 + "__id__": 97 } ], "_prefab": { - "__id__": 85 + "__id__": 99 }, "_lpos": { "__type__": "cc.Vec3", @@ -2044,11 +2405,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 80 + "__id__": 94 }, "_enabled": true, "__prefab": { - "__id__": 82 + "__id__": 96 }, "_contentSize": { "__type__": "cc.Size", @@ -2072,11 +2433,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 80 + "__id__": 94 }, "_enabled": true, "__prefab": { - "__id__": 84 + "__id__": 98 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -2130,11 +2491,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 71 + "__id__": 85 }, "_enabled": true, "__prefab": { - "__id__": 87 + "__id__": 101 }, "_contentSize": { "__type__": "cc.Size", @@ -2158,11 +2519,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 71 + "__id__": 85 }, "_enabled": true, "__prefab": { - "__id__": 89 + "__id__": 103 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -2203,11 +2564,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 71 + "__id__": 85 }, "_enabled": true, "__prefab": { - "__id__": 91 + "__id__": 105 }, "_opacity": 192, "_id": "" @@ -2216,6 +2577,62 @@ "__type__": "cc.CompPrefabInfo", "fileId": "74REJyNs5N/poR72fDQT8g" }, + { + "__type__": "cc.Button", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 85 + }, + "_enabled": true, + "__prefab": { + "__id__": 107 + }, + "clickEvents": [], + "_interactable": true, + "_transition": 3, + "_normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_hoverColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_pressedColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_normalSprite": null, + "_hoverSprite": null, + "_pressedSprite": null, + "_disabledSprite": null, + "_duration": 0.1, + "_zoomScale": 1.2, + "_target": null, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "46SpqkOA9FJqWhHOJKiLaX" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -2235,11 +2652,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 2 + "__id__": 10 }, "_enabled": true, "__prefab": { - "__id__": 94 + "__id__": 110 }, "_contentSize": { "__type__": "cc.Size", @@ -2263,11 +2680,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 2 + "__id__": 10 }, "_enabled": true, "__prefab": { - "__id__": 96 + "__id__": 112 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -2325,7 +2742,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 99 + "__id__": 115 }, "_contentSize": { "__type__": "cc.Size", @@ -2353,7 +2770,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 101 + "__id__": 117 }, "_alignFlags": 45, "_target": null, @@ -2379,6 +2796,40 @@ "__type__": "cc.CompPrefabInfo", "fileId": "a29RxiIzdCmb3+pvtytcYa" }, + { + "__type__": "bdb18RzbvtFkr9nSFVYRe7F", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 1 + }, + "_enabled": true, + "__prefab": { + "__id__": 119 + }, + "animationNodes": [], + "backdropNode": null, + "openAnimationEnabled": true, + "openAnimationDuration": 0.36, + "closeBtn": { + "__id__": 11 + }, + "buttonShare": { + "__id__": 27 + }, + "buttonRestart": { + "__id__": 63 + }, + "buttonHome": { + "__id__": 85 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "934CARR2BBrLS546H9OcRm" + }, { "__type__": "cc.PrefabInfo", "root": { diff --git a/assets/prefabs/TimeoutModal.ts b/assets/prefabs/TimeoutModal.ts new file mode 100644 index 0000000..9dc8515 --- /dev/null +++ b/assets/prefabs/TimeoutModal.ts @@ -0,0 +1,162 @@ +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; + /** 点击返回主页 / 关闭按钮回调 */ + onHome?: () => void; +} + +interface TimeoutModalParams { + levelIndex?: number; +} + +/** + * 时间耗尽弹窗组件 + * 继承 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) + 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._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._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.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.buttonHome && this.buttonHome.isValid) { + this.buttonHome.off(Node.EventType.TOUCH_END, this._onHomeClick, this); + } + } + + /** + * 求助好友按钮点击 + */ + 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 _onHomeClick(): void { + console.log('[TimeoutModal] 点击返回主页'); + this._callbacks.onHome?.(); + } +} diff --git a/assets/prefabs/TimeoutModal.ts.meta b/assets/prefabs/TimeoutModal.ts.meta new file mode 100644 index 0000000..7f962a1 --- /dev/null +++ b/assets/prefabs/TimeoutModal.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "bdb18473-6efb-4592-bf67-48555845eec5", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/prefabs/WrongModal.prefab b/assets/prefabs/WrongModal.prefab index 35c7c36..ecbbd6d 100644 --- a/assets/prefabs/WrongModal.prefab +++ b/assets/prefabs/WrongModal.prefab @@ -20,19 +20,25 @@ "_children": [ { "__id__": 2 + }, + { + "__id__": 10 } ], "_active": true, "_components": [ { - "__id__": 56 + "__id__": 68 }, { - "__id__": 58 + "__id__": 70 + }, + { + "__id__": 72 } ], "_prefab": { - "__id__": 60 + "__id__": 74 }, "_lpos": { "__type__": "cc.Vec3", @@ -63,6 +69,181 @@ }, "_id": "" }, + { + "__type__": "cc.Node", + "_name": "BgMask", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 1 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 3 + }, + { + "__id__": 5 + }, + { + "__id__": 7 + } + ], + "_prefab": { + "__id__": 9 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": { + "__id__": 4 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 1080, + "height": 2160 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "88epfHEmhCcLE8ktlnygG1" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": { + "__id__": 6 + }, + "_customMaterial": null, + "_srcBlendFactor": 2, + "_dstBlendFactor": 4, + "_color": { + "__type__": "cc.Color", + "r": 0, + "g": 0, + "b": 0, + "a": 121 + }, + "_spriteFrame": { + "__uuid__": "7d8f9b89-4fd1-4c9f-a3ab-38ec7cded7ca@f9941", + "__expectedType__": "cc.SpriteFrame" + }, + "_type": 1, + "_fillType": 0, + "_sizeMode": 0, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_useGrayscale": false, + "_atlas": null, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "e4C6cPdxRB66+B4hmjydyn" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": { + "__id__": 8 + }, + "_alignFlags": 45, + "_target": null, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_horizontalCenter": 0, + "_verticalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 40, + "_originalHeight": 36, + "_alignMode": 2, + "_lockFlags": 0, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "61mt404VhKUq+sKTkX8CtT" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "cby61tEk5Iza0zINKbYqqt", + "instance": null, + "targetOverrides": null, + "nestedPrefabInstanceRoots": null + }, { "__type__": "cc.Node", "_name": "dialogPanel", @@ -72,33 +253,33 @@ "__id__": 1 }, "_children": [ - { - "__id__": 3 - }, { "__id__": 11 }, { - "__id__": 17 + "__id__": 21 }, { - "__id__": 23 + "__id__": 27 }, { - "__id__": 39 + "__id__": 33 + }, + { + "__id__": 51 } ], "_active": true, "_components": [ { - "__id__": 51 + "__id__": 63 }, { - "__id__": 53 + "__id__": 65 } ], "_prefab": { - "__id__": 55 + "__id__": 67 }, "_lpos": { "__type__": "cc.Vec3", @@ -135,23 +316,26 @@ "_objFlags": 0, "__editorExtras__": {}, "_parent": { - "__id__": 2 + "__id__": 10 }, "_children": [], "_active": true, "_components": [ { - "__id__": 4 + "__id__": 12 }, { - "__id__": 6 + "__id__": 14 }, { - "__id__": 8 + "__id__": 16 + }, + { + "__id__": 18 } ], "_prefab": { - "__id__": 10 + "__id__": 20 }, "_lpos": { "__type__": "cc.Vec3", @@ -188,11 +372,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 3 + "__id__": 11 }, "_enabled": true, "__prefab": { - "__id__": 5 + "__id__": 13 }, "_contentSize": { "__type__": "cc.Size", @@ -216,11 +400,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 3 + "__id__": 11 }, "_enabled": true, "__prefab": { - "__id__": 7 + "__id__": 15 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -261,11 +445,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 3 + "__id__": 11 }, "_enabled": true, "__prefab": { - "__id__": 9 + "__id__": 17 }, "_alignFlags": 33, "_target": null, @@ -291,6 +475,62 @@ "__type__": "cc.CompPrefabInfo", "fileId": "e6kTdMyd1C3ZK6dQGDNQYb" }, + { + "__type__": "cc.Button", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 11 + }, + "_enabled": true, + "__prefab": { + "__id__": 19 + }, + "clickEvents": [], + "_interactable": true, + "_transition": 3, + "_normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_hoverColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_pressedColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_normalSprite": null, + "_hoverSprite": null, + "_pressedSprite": null, + "_disabledSprite": null, + "_duration": 0.1, + "_zoomScale": 1.2, + "_target": null, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "3eNsK9o9tD8Zga0+Ucj722" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -310,20 +550,20 @@ "_objFlags": 0, "__editorExtras__": {}, "_parent": { - "__id__": 2 + "__id__": 10 }, "_children": [], "_active": true, "_components": [ { - "__id__": 12 + "__id__": 22 }, { - "__id__": 14 + "__id__": 24 } ], "_prefab": { - "__id__": 16 + "__id__": 26 }, "_lpos": { "__type__": "cc.Vec3", @@ -360,11 +600,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 11 + "__id__": 21 }, "_enabled": true, "__prefab": { - "__id__": 13 + "__id__": 23 }, "_contentSize": { "__type__": "cc.Size", @@ -388,11 +628,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 11 + "__id__": 21 }, "_enabled": true, "__prefab": { - "__id__": 15 + "__id__": 25 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -472,20 +712,20 @@ "_objFlags": 0, "__editorExtras__": {}, "_parent": { - "__id__": 2 + "__id__": 10 }, "_children": [], "_active": true, "_components": [ { - "__id__": 18 + "__id__": 28 }, { - "__id__": 20 + "__id__": 30 } ], "_prefab": { - "__id__": 22 + "__id__": 32 }, "_lpos": { "__type__": "cc.Vec3", @@ -522,11 +762,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 17 + "__id__": 27 }, "_enabled": true, "__prefab": { - "__id__": 19 + "__id__": 29 }, "_contentSize": { "__type__": "cc.Size", @@ -550,11 +790,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 17 + "__id__": 27 }, "_enabled": true, "__prefab": { - "__id__": 21 + "__id__": 31 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -634,27 +874,30 @@ "_objFlags": 0, "__editorExtras__": {}, "_parent": { - "__id__": 2 + "__id__": 10 }, "_children": [ { - "__id__": 24 + "__id__": 34 } ], "_active": true, "_components": [ { - "__id__": 32 + "__id__": 42 }, { - "__id__": 34 + "__id__": 44 }, { - "__id__": 36 + "__id__": 46 + }, + { + "__id__": 48 } ], "_prefab": { - "__id__": 38 + "__id__": 50 }, "_lpos": { "__type__": "cc.Vec3", @@ -691,23 +934,23 @@ "_objFlags": 0, "__editorExtras__": {}, "_parent": { - "__id__": 23 + "__id__": 33 }, "_children": [], "_active": true, "_components": [ { - "__id__": 25 + "__id__": 35 }, { - "__id__": 27 + "__id__": 37 }, { - "__id__": 29 + "__id__": 39 } ], "_prefab": { - "__id__": 31 + "__id__": 41 }, "_lpos": { "__type__": "cc.Vec3", @@ -744,11 +987,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 24 + "__id__": 34 }, "_enabled": true, "__prefab": { - "__id__": 26 + "__id__": 36 }, "_contentSize": { "__type__": "cc.Size", @@ -772,11 +1015,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 24 + "__id__": 34 }, "_enabled": true, "__prefab": { - "__id__": 28 + "__id__": 38 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -843,11 +1086,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 24 + "__id__": 34 }, "_enabled": true, "__prefab": { - "__id__": 30 + "__id__": 40 }, "_alignFlags": 18, "_target": null, @@ -892,11 +1135,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 23 + "__id__": 33 }, "_enabled": true, "__prefab": { - "__id__": 33 + "__id__": 43 }, "_contentSize": { "__type__": "cc.Size", @@ -920,11 +1163,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 23 + "__id__": 33 }, "_enabled": true, "__prefab": { - "__id__": 35 + "__id__": 45 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -965,11 +1208,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 23 + "__id__": 33 }, "_enabled": true, "__prefab": { - "__id__": 37 + "__id__": 47 }, "_alignFlags": 12, "_target": null, @@ -995,6 +1238,62 @@ "__type__": "cc.CompPrefabInfo", "fileId": "566NGbzlJOyLK5JELzf1Nj" }, + { + "__type__": "cc.Button", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 33 + }, + "_enabled": true, + "__prefab": { + "__id__": 49 + }, + "clickEvents": [], + "_interactable": true, + "_transition": 3, + "_normalColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_hoverColor": { + "__type__": "cc.Color", + "r": 211, + "g": 211, + "b": 211, + "a": 255 + }, + "_pressedColor": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_disabledColor": { + "__type__": "cc.Color", + "r": 124, + "g": 124, + "b": 124, + "a": 255 + }, + "_normalSprite": null, + "_hoverSprite": null, + "_pressedSprite": null, + "_disabledSprite": null, + "_duration": 0.1, + "_zoomScale": 1.2, + "_target": null, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "dcsuGJKVpOcoOngimD0/cU" + }, { "__type__": "cc.PrefabInfo", "root": { @@ -1014,24 +1313,24 @@ "_objFlags": 0, "__editorExtras__": {}, "_parent": { - "__id__": 2 + "__id__": 10 }, "_children": [ { - "__id__": 40 + "__id__": 52 } ], "_active": true, "_components": [ { - "__id__": 46 + "__id__": 58 }, { - "__id__": 48 + "__id__": 60 } ], "_prefab": { - "__id__": 50 + "__id__": 62 }, "_lpos": { "__type__": "cc.Vec3", @@ -1068,20 +1367,20 @@ "_objFlags": 0, "__editorExtras__": {}, "_parent": { - "__id__": 39 + "__id__": 51 }, "_children": [], "_active": true, "_components": [ { - "__id__": 41 + "__id__": 53 }, { - "__id__": 43 + "__id__": 55 } ], "_prefab": { - "__id__": 45 + "__id__": 57 }, "_lpos": { "__type__": "cc.Vec3", @@ -1118,11 +1417,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 40 + "__id__": 52 }, "_enabled": true, "__prefab": { - "__id__": 42 + "__id__": 54 }, "_contentSize": { "__type__": "cc.Size", @@ -1146,11 +1445,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 40 + "__id__": 52 }, "_enabled": true, "__prefab": { - "__id__": 44 + "__id__": 56 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1204,11 +1503,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 39 + "__id__": 51 }, "_enabled": true, "__prefab": { - "__id__": 47 + "__id__": 59 }, "_contentSize": { "__type__": "cc.Size", @@ -1232,11 +1531,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 39 + "__id__": 51 }, "_enabled": true, "__prefab": { - "__id__": 49 + "__id__": 61 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1290,11 +1589,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 2 + "__id__": 10 }, "_enabled": true, "__prefab": { - "__id__": 52 + "__id__": 64 }, "_contentSize": { "__type__": "cc.Size", @@ -1318,11 +1617,11 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 2 + "__id__": 10 }, "_enabled": true, "__prefab": { - "__id__": 54 + "__id__": 66 }, "_customMaterial": null, "_srcBlendFactor": 2, @@ -1380,7 +1679,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 57 + "__id__": 69 }, "_contentSize": { "__type__": "cc.Size", @@ -1408,7 +1707,7 @@ }, "_enabled": true, "__prefab": { - "__id__": 59 + "__id__": 71 }, "_alignFlags": 45, "_target": null, @@ -1434,6 +1733,34 @@ "__type__": "cc.CompPrefabInfo", "fileId": "a29RxiIzdCmb3+pvtytcYa" }, + { + "__type__": "972c5f8EOdJPqHaSEnzweVV", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 1 + }, + "_enabled": true, + "__prefab": { + "__id__": 73 + }, + "animationNodes": [], + "backdropNode": null, + "openAnimationEnabled": true, + "openAnimationDuration": 0.36, + "closeBtn": { + "__id__": 11 + }, + "buttonHint": { + "__id__": 33 + }, + "_id": "" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "c7ZV1N6ZxM8LkSfjwd1UFh" + }, { "__type__": "cc.PrefabInfo", "root": { diff --git a/assets/prefabs/WrongModal.ts b/assets/prefabs/WrongModal.ts new file mode 100644 index 0000000..4e8dc58 --- /dev/null +++ b/assets/prefabs/WrongModal.ts @@ -0,0 +1,111 @@ +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?.(); + } +} diff --git a/assets/prefabs/WrongModal.ts.meta b/assets/prefabs/WrongModal.ts.meta new file mode 100644 index 0000000..e930d1c --- /dev/null +++ b/assets/prefabs/WrongModal.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "972c57fc-10e7-493e-a1da-4849f3c1e555", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/scripts/config/ApiConfig.ts b/assets/scripts/config/ApiConfig.ts index 96bc9b8..ad6313b 100644 --- a/assets/scripts/config/ApiConfig.ts +++ b/assets/scripts/config/ApiConfig.ts @@ -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`, /** 分享相关 */ diff --git a/assets/scripts/types/ApiTypes.ts b/assets/scripts/types/ApiTypes.ts index 1b633d7..e0d3190 100644 --- a/assets/scripts/types/ApiTypes.ts +++ b/assets/scripts/types/ApiTypes.ts @@ -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; } /** 创建分享响应 */ diff --git a/assets/scripts/types/LevelTypes.ts b/assets/scripts/types/LevelTypes.ts index 4dfe4e0..b1de029 100644 --- a/assets/scripts/types/LevelTypes.ts +++ b/assets/scripts/types/LevelTypes.ts @@ -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; } diff --git a/assets/scripts/utils/AuthManager.ts b/assets/scripts/utils/AuthManager.ts index ad6e090..3610efb 100644 --- a/assets/scripts/utils/AuthManager.ts +++ b/assets/scripts/utils/AuthManager.ts @@ -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 { 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; - } } diff --git a/assets/scripts/utils/LevelDataManager.ts b/assets/scripts/utils/LevelDataManager.ts index 8a98eec..11e9b58 100644 --- a/assets/scripts/utils/LevelDataManager.ts +++ b/assets/scripts/utils/LevelDataManager.ts @@ -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 = new Map(); - - /** 是否已成功从 API 获取数据 */ - private _hasApiData: boolean = false; + /** 运行时关卡配置缓存(按 levelId 索引) */ + private _levelConfigs: Map = new Map(); /** 图片缓存:URL -> SpriteFrame */ private _imageCache: Map = new Map(); - /** 正在加载中的关卡索引集合 */ - private _loadingLevels: Set = new Set(); + /** 正在加载中的关卡 ID 集合 */ + private _loadingLevels: Set = 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 { - console.log('[LevelDataManager] 开始初始化'); + async initialize(nextLevel: NextLevelData, onProgress?: ProgressCallback): Promise { + 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 { - // 检查索引有效性 - 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 { + // 检查缓存 + 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 { - 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(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 { - 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 { + 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] 缓存已清除'); - } }