import { _decorator, Node, EditBox, instantiate, Vec3, Button, Label, Sprite, SpriteFrame, AudioClip, AudioSource, UITransform, 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'; 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'; const { ccclass, property } = _decorator; /** * 关卡页面组件 * 继承 BaseView,实现页面生命周期 */ @ccclass('PageLevel') export class PageLevel extends BaseView { /** 静态常量:零位置 */ private static readonly ZERO_POS = new Vec3(0, 0, 0); // ========== 节点引用 ========== @property(Node) inputLayout: Node | null = null; @property(Node) submitButton: Node | null = null; @property(Node) inputTemplate: Node | null = null; @property(Node) actionNode: Node | null = null; @property(Node) iconSetting: Node | null = null; @property(Node) tipsLayout: Node | null = null; @property(Node) mainImage: Node | null = null; @property(Node) tipsItem1: Node | null = null; @property(Node) tipsItem2: Node | null = null; @property(Node) tipsItem3: Node | null = null; @property(Node) unLockItem2: Node | null = null; @property(Node) unLockItem3: Node | null = null; @property(Label) clockLabel: Label | null = null; /** 体力值显示标签(prefab 中序列化名为 liveLabel,保持兼容) */ @property(Label) liveLabel: Label | null = null; // ========== 配置属性 ========== @property({ min: 0, tooltip: '当前关卡索引' }) currentLevelIndex: number = 0; @property(AudioClip) clickAudio: AudioClip | null = null; @property(AudioClip) successAudio: AudioClip | null = null; @property(AudioClip) failAudio: AudioClip | null = null; @property(Prefab) passModalPrefab: Prefab | null = null; // ========== 内部状态 ========== /** 当前创建的输入框节点数组 */ private _inputNodes: Node[] = []; /** 倒计时剩余秒数 */ private _countdown: number = 60; /** 倒计时是否结束 */ private _isTimeUp: boolean = false; /** 当前关卡配置 */ private _currentConfig: RuntimeLevelConfig | null = null; /** 是否正在切换关卡(防止重复提交) */ private _isTransitioning: boolean = false; /** 是否正在解锁提示(防止双击重复触发) */ private _isUnlocking: boolean = false; /** 通关弹窗实例 */ private _passModalNode: Node | null = null; /** 是否处于分享挑战模式 */ private _isShareMode: boolean = false; /** 体力恢复倒计时定时器 */ private _staminaTimerId: ReturnType | null = null; /** * 页面首次加载时调用 */ onViewLoad(): void { console.log('[PageLevel] onViewLoad'); const params = this.getParams(); this._isShareMode = params?.shareMode === true; if (this._isShareMode) { this.currentLevelIndex = 0; console.log('[PageLevel] 进入分享挑战模式'); } else { // 根据关卡列表找到第一个未通关的关卡 this.currentLevelIndex = LevelDataManager.instance.getFirstUncompletedIndex(); StorageManager.setCurrentLevelIndex(this.currentLevelIndex); console.log(`[PageLevel] 进入第一个未通关关卡: 第 ${this.currentLevelIndex + 1} 关`); } this.updateStaminaLabel(); this.initIconSetting(); this.initUnlockButtons(); this.initSubmitButton(); // 异步加载关卡资源并调用进入关卡接口,完成后启动倒计时 this._enterAndInitLevel().catch(err => { console.error('[PageLevel] 进入关卡失败:', err); }); } /** * 页面每次显示时调用 */ onViewShow(): void { console.log('[PageLevel] onViewShow'); this.updateStaminaLabel(); this._startStaminaRecoverTimer(); } /** * 页面隐藏时调用 */ onViewHide(): void { console.log('[PageLevel] onViewHide'); this._stopStaminaRecoverTimer(); } /** * 页面销毁时调用 */ onViewDestroy(): void { console.log('[PageLevel] onViewDestroy'); this.clearInputNodes(); this.stopCountdown(); this._closePassModal(); this._stopStaminaRecoverTimer(); // 清理事件监听 this.iconSetting?.off(Node.EventType.TOUCH_END, this.onIconSettingClick, this); this.unLockItem2?.off(Node.EventType.TOUCH_END); this.unLockItem3?.off(Node.EventType.TOUCH_END); this.submitButton?.off(Node.EventType.TOUCH_END, this.onSubmitAnswer, this); } /** * 进入关卡并初始化 * 1. 加载关卡图片资源 * 2. 调用进入关卡接口(消耗体力,获取答案和线索) * 3. 启动倒计时 */ private async _enterAndInitLevel(): Promise { // 先加载关卡图片资源 let config: RuntimeLevelConfig | null = null; if (this._isShareMode) { config = await ShareManager.instance.ensureShareLevelReady(this.currentLevelIndex); } else { config = LevelDataManager.instance.getLevelConfig(this.currentLevelIndex); if (!config) { console.log(`[PageLevel] 关卡 ${this.currentLevelIndex + 1} 资源未缓存,开始加载...`); config = await LevelDataManager.instance.ensureLevelReady(this.currentLevelIndex); } } if (!config) { console.warn(`[PageLevel] 没有找到关卡配置,索引: ${this.currentLevelIndex}`); 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; } // 提示用户消耗体力 ToastManager.show(`消耗1点体力,剩余 ${enterData.stamina.current}/${enterData.stamina.max}`); // 用 enter 接口返回的数据更新关卡配置(填充答案和线索) LevelDataManager.instance.updateLevelDetails( this.currentLevelIndex, { answer: enterData.answer, hint1: enterData.hint1, hint2: enterData.hint2, hint3: enterData.hint3, } ); // 重新获取更新后的配置 config = LevelDataManager.instance.getLevelConfig(this.currentLevelIndex); if (!config) { console.error('[PageLevel] 更新关卡详情后获取配置失败'); return; } // 更新体力显示 this.updateStaminaLabel(); } } console.log(`[PageLevel] 初始化关卡 ${this.currentLevelIndex + 1}: ${config.name}`); this._applyLevelConfig(config); this.startCountdown(); } /** * 应用关卡配置(通用初始化逻辑) */ private _applyLevelConfig(config: RuntimeLevelConfig): void { this._currentConfig = config; // 重置关卡切换状态,允许再次提交 this._isTransitioning = false; // 重置倒计时状态 this._isTimeUp = false; this._countdown = 60; // 设置主图 this.setMainImage(config.spriteFrame); // 设置线索1(默认解锁,如果有的话) if (config.clue1) { this.setClue(1, config.clue1); } // 隐藏线索2、3 this.hideClue(2); this.hideClue(3); // 显示解锁按钮2、3 this.showUnlockButton(2); this.showUnlockButton(3); // 根据答案长度创建单个输入框 if (config.answer) { this.createSingleInput(config.answer.length); } // 更新倒计时显示 this.updateClockLabel(); // 预加载下一关图片(静默加载,不阻塞) if (this._isShareMode) { const nextIndex = this.currentLevelIndex + 1; if (nextIndex < ShareManager.instance.getShareLevelCount()) { ShareManager.instance.ensureShareLevelReady(nextIndex).catch(() => {}); } } else { LevelDataManager.instance.preloadNextLevel(this.currentLevelIndex); } console.log(`[PageLevel] 初始化关卡 ${this.currentLevelIndex + 1}, 答案长度: ${config.answer?.length ?? 0}`); } /** * 创建单个输入框 * @param answerLength 答案长度,用于设置 placeholder 和宽度 */ private createSingleInput(answerLength: number): void { if (!this.inputLayout || !this.inputTemplate) { console.error('[PageLevel] inputLayout 或 inputTemplate 未设置'); return; } // 清理现有输入框 this.clearInputNodes(); // 隐藏模板节点 this.inputTemplate.active = false; // 创建单个输入框 const inputNode = instantiate(this.inputTemplate); inputNode.active = true; inputNode.name = 'singleInput'; // 设置位置 inputNode.setPosition(PageLevel.ZERO_POS); // 获取 EditBox 组件 const editBox = inputNode.getComponent(EditBox); if (editBox) { // 设置 placeholder 提示 editBox.placeholder = `(${answerLength}个字)`; // 设置最大长度为答案长度 editBox.maxLength = answerLength; // 清空输入内容 editBox.string = ''; // 监听事件 editBox.node.on(EditBox.EventType.TEXT_CHANGED, this.onInputTextChanged, this); editBox.node.on(EditBox.EventType.EDITING_DID_ENDED, this.onInputEditingEnded, this); } // 动态调整输入框宽度 const uitransform = inputNode.getComponent(UITransform); let inputWidth = 200; if (uitransform) { // 每个字符约 60px,加上 padding inputWidth = Math.min(600, Math.max(200, answerLength * 60 + 40)); uitransform.setContentSize(inputWidth, 100); } // 调整下划线宽度与输入框一致 const underLine = inputNode.getChildByName('UnderLine'); if (underLine) { const underLineTransform = underLine.getComponent(UITransform); if (underLineTransform) { underLineTransform.setContentSize(inputWidth, underLineTransform.height); } } this.inputLayout.addChild(inputNode); this._inputNodes.push(inputNode); console.log(`[PageLevel] 创建单个输入框,答案长度: ${answerLength}`); } /** * 清理所有输入框节点 */ private clearInputNodes(): void { for (const node of this._inputNodes) { if (node.isValid) { const editBox = node.getComponent(EditBox); if (editBox) { editBox.node.off(EditBox.EventType.TEXT_CHANGED, this.onInputTextChanged, this); editBox.node.off(EditBox.EventType.EDITING_DID_ENDED, this.onInputEditingEnded, this); } node.destroy(); } } this._inputNodes = []; } /** * 获取所有输入框的值 */ getInputValues(): string[] { if (this._inputNodes.length === 0) return []; const editBox = this._inputNodes[0].getComponent(EditBox); const str = (editBox?.string ?? '').trim(); return [str]; } /** * 获取拼接后的答案字符串 */ getAnswer(): string { if (this._inputNodes.length === 0) return ''; const editBox = this._inputNodes[0].getComponent(EditBox); return (editBox?.string ?? '').trim(); } // ========== EditBox 事件回调 ========== /** * 输入框文本变化回调 */ private onInputTextChanged(_editBox: EditBox): void { console.log('[PageLevel] 输入内容变化'); } /** * 输入框编辑结束回调 */ private onInputEditingEnded(_editBox: EditBox): void { console.log('[PageLevel] 输入编辑结束'); } // ========== IconSetting 按钮相关 ========== /** * 初始化 IconSetting 按钮事件 */ private initIconSetting(): void { if (!this.iconSetting) { console.warn('[PageLevel] iconSetting 节点未设置'); return; } const button = this.iconSetting.getComponent(Button); if (!button) { console.warn('[PageLevel] iconSetting 节点缺少 Button 组件'); return; } this.iconSetting.on(Node.EventType.TOUCH_END, this.onIconSettingClick, this); console.log('[PageLevel] IconSetting 按钮事件已绑定'); } /** * IconSetting 按钮点击回调 */ private onIconSettingClick(): void { console.log('[PageLevel] IconSetting 点击,返回主页'); this.playClickSound(); // 分享模式下栈中没有 PageHome,需要清除分享状态并直接打开首页 if (this._isShareMode) { ShareManager.instance.clearShareMode(); ViewManager.instance.replace('PageHome'); } else { ViewManager.instance.back(); } } // ========== 线索相关方法 ========== /** * 获取线索节点 */ private getTipsItem(index: number): Node | null { switch (index) { case 1: return this.tipsItem1; case 2: return this.tipsItem2; case 3: return this.tipsItem3; default: return null; } } /** * 设置线索内容 */ private setClue(index: number, content: string): void { const tipsItem = this.getTipsItem(index); if (!tipsItem) return; // 查找 TipsLabel 节点:Content -> TipsLabel const contentNode = tipsItem.getChildByName('Content'); if (!contentNode) return; const tipsLabelNode = contentNode.getChildByName('TipsLabel'); if (!tipsLabelNode) return; const label = tipsLabelNode.getComponent(Label); if (label) { label.string = `提示 ${index}: ${content}`; console.log(`[PageLevel] 设置线索${index}: ${content}`); } } /** * 显示线索 */ private showClue(index: number): void { const tipsItem = this.getTipsItem(index); if (tipsItem) { tipsItem.active = true; console.log(`[PageLevel] 显示线索${index}`); } } /** * 隐藏线索 */ private hideClue(index: number): void { const tipsItem = this.getTipsItem(index); if (tipsItem) { tipsItem.active = false; console.log(`[PageLevel] 隐藏线索${index}`); } } /** * 显示解锁按钮 */ private showUnlockButton(index: number): void { const unlockItem = index === 2 ? this.unLockItem2 : this.unLockItem3; if (unlockItem) { unlockItem.active = true; console.log(`[PageLevel] 显示解锁按钮${index}`); } } /** * 隐藏解锁按钮 */ private hideUnlockButton(index: number): void { const unlockItem = index === 2 ? this.unLockItem2 : this.unLockItem3; if (unlockItem) { unlockItem.active = false; console.log(`[PageLevel] 隐藏解锁按钮${index}`); } } /** * 初始化解锁按钮事件 */ private initUnlockButtons(): void { // 解锁按钮2 if (this.unLockItem2) { this.unLockItem2.on(Node.EventType.TOUCH_END, () => this.onUnlockClue(2), this); } // 解锁按钮3 if (this.unLockItem3) { this.unLockItem3.on(Node.EventType.TOUCH_END, () => this.onUnlockClue(3), this); } console.log('[PageLevel] 解锁按钮事件已绑定'); } /** * 初始化提交按钮事件 */ private initSubmitButton(): void { if (!this.submitButton) { console.warn('[PageLevel] submitButton 节点未设置'); return; } this.submitButton.on(Node.EventType.TOUCH_END, this.onSubmitAnswer, this); console.log('[PageLevel] 提交按钮事件已绑定'); } /** * 点击解锁线索(观看激励视频广告后解锁) */ private async onUnlockClue(index: number): Promise { // 防止双击重复触发 if (this._isUnlocking) return; this._isUnlocking = true; try { // 检查线索是否存在 if (!this._currentConfig) return; const clueContent = index === 2 ? this._currentConfig.clue2 : this._currentConfig.clue3; if (!clueContent) { ToastManager.show('该提示暂未配置'); return; } // 调用微信激励视频广告 ToastManager.show('观看视频即可解锁提示'); const adWatched = await WxSDK.showRewardedVideoAd(); if (!adWatched) { ToastManager.show('需要看完视频才能解锁提示哦'); return; } this.playClickSound(); this.hideUnlockButton(index); this.showClue(index); this.setClue(index, clueContent); console.log(`[PageLevel] 通过观看广告解锁线索${index}`); } finally { this._isUnlocking = false; } } // ========== 主图相关方法 ========== /** * 设置主图 */ private setMainImage(spriteFrame: SpriteFrame | null): void { if (!this.mainImage) return; const sprite = this.mainImage.getComponent(Sprite); if (sprite && spriteFrame) { sprite.spriteFrame = spriteFrame; console.log('[PageLevel] 设置主图'); } } // ========== 音效相关方法 ========== /** * 播放音效(通用方法) */ private playSound(clip: AudioClip | null): void { if (!clip) return; const audioSource = this.node.getComponent(AudioSource); audioSource?.playOneShot(clip); } /** * 播放点击音效 */ private playClickSound(): void { this.playSound(this.clickAudio); } /** * 播放成功音效 */ private playSuccessSound(): void { this.playSound(this.successAudio); } /** * 播放失败音效 */ private playFailSound(): void { this.playSound(this.failAudio); } // ========== 倒计时相关方法 ========== /** * 开始倒计时 */ private startCountdown(): void { this._countdown = 60; this._isTimeUp = false; this.updateClockLabel(); this.schedule(this.onCountdownTick, 1); console.log('[PageLevel] 开始倒计时 60 秒'); } /** * 停止倒计时 */ private stopCountdown(): void { this.unschedule(this.onCountdownTick); } /** * 倒计时每秒回调 */ private onCountdownTick(): void { if (this._isTimeUp) return; this._countdown--; this.updateClockLabel(); if (this._countdown <= 0) { this._isTimeUp = true; this.stopCountdown(); this.onTimeUp(); } } /** * 更新倒计时显示 */ private updateClockLabel(): void { if (this.clockLabel) { this.clockLabel.string = `${this._countdown}s`; } } /** * 倒计时结束 */ private onTimeUp(): void { console.log('[PageLevel] 倒计时结束!'); this.playFailSound(); // 可以在这里添加游戏结束逻辑 } // ========== 体力值相关方法 ========== /** 上次显示的体力值,用于变更检测 */ private _lastDisplayedStamina: number = -1; /** * 更新体力值显示(仅值变化时更新 UI) */ private updateStaminaLabel(): void { if (this.liveLabel) { const stamina = StaminaManager.instance.getStamina(); if (stamina.current !== this._lastDisplayedStamina) { this.liveLabel.string = `x ${stamina.current}`; this._lastDisplayedStamina = stamina.current; } } } /** * 启动体力恢复倒计时 UI */ private _startStaminaRecoverTimer(): void { this._stopStaminaRecoverTimer(); const stamina = StaminaManager.instance.getStamina(); if (!stamina.nextRecoverAt || stamina.current >= stamina.max) { return; } const targetTime = new Date(stamina.nextRecoverAt).getTime(); if (isNaN(targetTime)) return; this._staminaTimerId = setInterval(() => { if (targetTime - Date.now() > 0) return; // 恢复一点体力 const currentStamina = StaminaManager.instance.getStamina(); const newCurrent = Math.min(currentStamina.current + 1, currentStamina.max); const newStamina: StaminaInfo = { ...currentStamina, current: newCurrent, nextRecoverAt: newCurrent < currentStamina.max ? new Date(Date.now() + 10 * 60 * 1000).toISOString() : null, }; StaminaManager.instance.updateStamina(newStamina); this.updateStaminaLabel(); this._stopStaminaRecoverTimer(); if (newCurrent < currentStamina.max) { this._startStaminaRecoverTimer(); } }, 1000); } /** * 停止体力恢复倒计时 */ private _stopStaminaRecoverTimer(): void { if (this._staminaTimerId !== null) { clearInterval(this._staminaTimerId); this._staminaTimerId = null; } } // ========== 答案提交与关卡切换 ========== /** * 提交答案 */ onSubmitAnswer(): void { if (!this._currentConfig) return; if (this._isTransitioning) return; const userAnswer = this.getAnswer(); console.log(`[PageLevel] 提交答案: ${userAnswer}, 正确答案: ${this._currentConfig.answer}`); if (userAnswer === this._currentConfig.answer) { // 答案正确,只播放成功音效(不播放点击音效,避免重合) this.showSuccess(); } else { // 答案错误 this.showError(); } } /** * 显示成功提示并上报通关 */ private async showSuccess(): Promise { console.log('[PageLevel] 答案正确!'); // 标记正在切换关卡,防止重复提交 this._isTransitioning = true; // 停止倒计时 this.stopCountdown(); // 播放成功音效 this.playSuccessSound(); const levelId = this._currentConfig?.id ?? ''; const timeSpent = 60 - this._countdown; if (!this._isShareMode) { // 上报通关耗时 const result = await StaminaManager.instance.completeLevel(levelId, timeSpent); if (result) { console.log(`[PageLevel] 通关上报成功,首次通关: ${result.firstClear}`); } // 标记关卡为已通关(本地缓存) LevelDataManager.instance.markLevelCompleted(this.currentLevelIndex); } else { // fire-and-forget: errors are logged inside reportLevelProgress void ShareManager.instance.reportLevelProgress(levelId, true, timeSpent); } // 显示通关弹窗 this._showPassModal(); } /** * 显示通关弹窗 * 将弹窗添加到 Canvas 根节点下(而非 PageLevel 子节点) * 这样 Widget 可以正确对齐到全屏 */ private _showPassModal(): void { if (!this.passModalPrefab) { console.warn('[PageLevel] passModalPrefab 未设置'); return; } // 如果弹窗已显示,不再重复创建 if (this._passModalNode && this._passModalNode.isValid) { return; } // 实例化弹窗 const modalNode = instantiate(this.passModalPrefab); modalNode.setPosition(PageLevel.ZERO_POS); modalNode.setSiblingIndex(PassModal.MODAL_Z_INDEX); // 找到 Canvas 根节点并添加弹窗 const canvasNode = this.node.parent; if (canvasNode) { canvasNode.addChild(modalNode); } else { this.node.addChild(modalNode); } this._passModalNode = modalNode; // 获取 PassModal 组件并设置回调 const passModal = modalNode.getComponent(PassModal); if (passModal) { passModal.setParams({ levelIndex: this.currentLevelIndex + 1 }); passModal.setCallbacks({ onNextLevel: () => { this._closePassModal(); this.nextLevel(); }, onShare: () => { // 分享后不关闭弹窗,用户可继续点击下一关 console.log('[PageLevel] 分享完成'); } }); // 手动调用 onViewLoad 和 onViewShow passModal.onViewLoad(); passModal.onViewShow(); } console.log('[PageLevel] 显示通关弹窗'); } /** * 关闭通关弹窗 */ private _closePassModal(): void { if (this._passModalNode && this._passModalNode.isValid) { this._passModalNode.destroy(); this._passModalNode = null; console.log('[PageLevel] 关闭通关弹窗'); } } /** * 显示错误提示 */ private showError(): void { console.log('[PageLevel] 答案错误!'); // 播放失败音效 this.playFailSound(); // 触发手机震动 WxSDK.vibrateLong(); // 显示 Toast 提示 ToastManager.show('答案错误,再试试吧!'); } /** * 进入下一关 */ private async nextLevel(): Promise { // 标记当前关卡已通关 if (!this._isShareMode) { StorageManager.onLevelCompleted(this.currentLevelIndex); LevelDataManager.instance.markLevelCompleted(this.currentLevelIndex); } // 查找下一个未通关的关卡 if (this._isShareMode) { this.currentLevelIndex++; const totalLevels = ShareManager.instance.getShareLevelCount(); if (this.currentLevelIndex >= totalLevels) { console.log('[PageLevel] 分享关卡全部完成'); this.stopCountdown(); ShareManager.instance.clearShareMode(); ViewManager.instance.replace('PageHome'); return; } } else { const nextIndex = LevelDataManager.instance.getNextUncompletedIndex(this.currentLevelIndex); if (nextIndex < 0) { // 所有关卡全部通关 console.log('[PageLevel] 恭喜通关!所有关卡已完成'); this.stopCountdown(); ViewManager.instance.back(); return; } this.currentLevelIndex = nextIndex; StorageManager.setCurrentLevelIndex(this.currentLevelIndex); } // 重置并加载下一关(包含进入关卡接口调用) await this._enterAndInitLevel(); console.log(`[PageLevel] 进入关卡 ${this.currentLevelIndex + 1}`); } }