# 梗中作乐 V1.0 - 源代码文档(第 5 部分) 软件名称:梗中作乐 版本号:V1.0 --- ## 第 5 部分:关卡核心逻辑 本部分展示关卡页 PageLevel 的核心实现:包含视图绑定、关卡数据加载、答题判定与通关流程驱动的关键代码。 ### 5.1 关卡页 (assets/prefabs/PageLevel.ts,节选 1-700 行) ```typescript import { _decorator, Node, EditBox, instantiate, Vec3, Button, Label, Sprite, SpriteFrame, AudioClip, AudioSource, Prefab, EffectAsset, UITransform, UIOpacity, ProgressBar, tween, Tween, Color, Layout, Widget, sp, view, resources } from 'cc'; import { BaseView } from 'db://assets/scripts/core/BaseView'; import { ViewManager } from 'db://assets/scripts/core/ViewManager'; import { StaminaManager } from 'db://assets/scripts/utils/StaminaManager'; import { WxSDK, getUserProfile, type WxUserInfo } from 'db://assets/scripts/utils/WxSDK'; import { LevelDataManager } from 'db://assets/scripts/utils/LevelDataManager'; import { AuthManager } from 'db://assets/scripts/utils/AuthManager'; 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 { StorageManager } from 'db://assets/scripts/utils/StorageManager'; import { HttpUtil } from 'db://assets/scripts/utils/HttpUtil'; import { API_ENDPOINTS, API_TIMEOUT } from 'db://assets/scripts/config/ApiConfig'; import { WrongModal } from 'db://assets/prefabs/WrongModal'; import { TimeoutModal } from 'db://assets/prefabs/TimeoutModal'; import { CommonModal } from 'db://assets/prefabs/CommonModal'; import { ApiEnvelope, StaminaInfo, NextLevelData, SubmitShareLevel } from 'db://assets/scripts/types/ApiTypes'; import { AchievementTitleManager, AchievementTitleInfo } from 'db://assets/scripts/utils/AchievementTitleManager'; import { AchievementTitleAnimator } from 'db://assets/scripts/utils/AchievementTitleAnimator'; import { applyRoundedCorner } from 'db://assets/scripts/utils/roundedMaterial.utils'; import { AudioManager } from 'db://assets/scripts/utils/AudioManager'; const { ccclass, property } = _decorator; /** * 关卡页面组件 * 继承 BaseView,实现页面生命周期 * 关卡流程由服务端 NextLevelData 驱动,客户端不再维护关卡列表 */ @ccclass('PageLevel') export class PageLevel extends BaseView { /** 静态常量:零位置 */ private static readonly ZERO_POS = new Vec3(0, 0, 0); /** 解锁线索按钮默认文案 */ private static readonly UNLOCK_BUTTON_CLUE_TEXT = '查看线索'; /** 线索全部查看后的按钮文案 */ private static readonly UNLOCK_BUTTON_ANSWER_TEXT = '查看答案'; /** 默认体力上限,服务端未返回 max 时使用 */ private static readonly DEFAULT_STAMINA_MAX = 50; /** 答案正确后到弹出通关弹窗之间的停留时间(不论是否有谐音梗都保持一致) */ private static readonly PASS_MODAL_DELAY_MS = 800; /** 图片2描述默认文案 */ private static readonly DEFAULT_IMAGE2_DESCRIPTION = '这是什么?'; /** 线索解锁出现动画时长(ms) */ private static readonly CLUE_APPEAR_DURATION = 0.3; /** 线索解锁出现动画起始缩放 */ private static readonly CLUE_APPEAR_START_SCALE = 0.8; /** 倒计时进入紧迫状态的阈值(秒,≤ 该值开始警示) */ private static readonly CLOCK_URGENT_THRESHOLD = 10; /** 紧迫状态下倒计时字体颜色(红) */ private static readonly CLOCK_URGENT_COLOR = new Color(230, 60, 60, 255); /** 倒计时 tick 脉冲峰值缩放 */ private static readonly CLOCK_PULSE_PEAK_SCALE = 1.3; /** 倒计时 tick 脉冲单向时长(放大、回落各一半) */ private static readonly CLOCK_PULSE_HALF_DURATION = 0.15; /** 谐音梗揭示动画:InputLayout 位移、divider 淡入时长 */ private static readonly PUNCH_REVEAL_DURATION = 0.3; /** 谐音梗揭示动画:punchLayout 出现的起始缩放 */ private static readonly PUNCH_REVEAL_START_SCALE = 0.85; /** 谐音梗揭示动画:punchLayout 在 InputLayout 动起来后再出现的延迟(让动画有节奏) */ private static readonly PUNCH_REVEAL_DELAY = 0.1; /** 分享模式只展示提示 1 时,固定放回 TipsLayout 顶部,避免 Layout 把它排到底部被下一题按钮遮挡 */ private static readonly SHARE_MODE_TIP1_Y = 120; /** 分享模式底部按钮默认文案 */ private static readonly SHARE_NEXT_BUTTON_TEXT = '下一题'; /** 分享模式最后一题提交文案 */ private static readonly SHARE_SUBMIT_BUTTON_TEXT = '提交答案'; /** PassNode 滑入 / 滑出动画时长(秒) */ private static readonly PASS_NODE_SLIDE_DURATION = 0.4; /** BottomLayout / TipsLayout 透明度淡入淡出动画时长(秒) */ private static readonly BOTTOM_LAYER_FADE_DURATION = 0.25; /** 彩带 spine 动画名(一次播放) */ private static readonly CAIDAI_ANIMATION_NAME = 'open'; /** pose 赞美音效 resources 路径(assets/resources/audios/good.mp3) */ private static readonly GOOD_AUDIO_RESOURCE_PATH = 'audios/good'; /** pose 赞美段提前量:比通关动效 / 成功音效结束点提前 1 秒启动 */ private static readonly POSE_PRAISE_ADVANCE_SECONDS = 1.5; /** * 通关赞美 spine(pose 节点)档位:[最小通关数, 动画名],倒序匹配。 * 1-5 关 → "1",6-10 关 → "2",11+ 关 → "3"。 * count <= 0 不会匹配任何档位,自然不播放。 */ private static readonly POSE_TIERS: ReadonlyArray = [ [11, '3'], [6, '2'], [1, '1'], ]; // ========== 节点引用 ========== @property(Node) inputLayout: Node | null = null; @property(Node) punchLayout: Node | null = null; /** Action 区域内 InputLayout 与 punchLayout 之间的分割线节点(prefab 中的 border_dashline_wht) */ @property(Node) punchDivider: 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) mainImage2: Node | null = null; @property(Label) image1DescLabel: Label | null = null; @property(Label) image2DescLabel: Label | null = null; @property(Node) tipsItem1: Node | null = null; @property(Node) tipsItem2: Node | null = null; @property(Node) tipsItem3: Node | null = null; @property(Node) unLockTipsBtn: Node | null = null; @property(Node) addTimeBtn: Node | null = null; @property(Label) clockLabel: Label | null = null; /** 体力值显示标签(prefab 中序列化名为 liveLabel,保持兼容) */ @property(Label) liveLabel: Label | null = null; /** 关卡标题标签,显示为"第 N 关" */ @property(Label) titleLevelLabel: Label | null = null; /** 普通模式背景 */ @property(Node) bgNode: Node | null = null; /** 分享 / PK 模式背景 */ @property(Node) pkBgNode: Node | null = null; /** 普通模式标题容器 */ @property(Node) titleLevelNode: Node | null = null; /** 分享 / PK 模式标题容器 */ @property(Node) pkTitleLevelNode: Node | null = null; /** 分享 / PK 模式标题标签 */ @property(Label) pkTitleLevelLabel: Label | null = null; /** 普通模式体力区域容器 */ @property(Node) liveNode: Node | null = null; /** 分享 / PK 模式进度容器 */ @property(Node) pkLevelProgressNode: Node | null = null; /** 分享 / PK 模式进度标签 */ @property(Label) pkLevelProgressLabel: Label | null = null; /** 普通模式底部按钮区域 */ @property(Node) bottomLayoutNode: Node | null = null; /** 分享 / PK 模式底部下一题按钮 */ @property(Node) pkNextLevelButton: Node | null = null; // ========== PassNode(通关后展示)========== /** PassNode 根节点:用户通关后从屏幕左侧滑入 */ @property(Node) passNode: Node | null = null; /** PassNode 内的成就体系容器(TitlteLevel)。分享模式下整体隐藏 */ @property(Node) passTitleLevelNode: Node | null = null; /** 称号文案 Label(如「冷场小白1级」) */ @property(Label) passTitleLevelLabel: Label | null = null; /** 称号进度条 */ @property(ProgressBar) passTitleProgressBar: ProgressBar | null = null; /** 进度提示 Label(如「还差3题,解锁新成就等级」) */ @property(Label) passProgressLabel: Label | null = null; /** 进度条上跟随移动的 anchor 节点(其下 Label 显示百分比) */ @property(Node) passProgressAnchor: Node | null = null; /** 通关页「下一关 / 提交答案」按钮 */ @property(Node) passNextLevelButton: Node | null = null; /** 通关页「分享」按钮 */ @property(Node) passShareButton: Node | null = null; /** 彩带 spine 根节点(与 PassNode 同级,挂在 PageLevel 下) */ @property(Node) caidaiNode: Node | null = null; /** 彩带 sp.Skeleton 组件,用于播放 "open" 动画 */ @property(sp.Skeleton) caidaiSkeleton: sp.Skeleton | null = null; /** 通关赞美 spine 节点(PassNode 子节点,根据本场通关数选择 1/2/3 动画) */ @property(Node) poseNode: Node | null = null; /** 通关赞美 sp.Skeleton 组件,动画名 "1" / "2" / "3",loop=false */ @property(sp.Skeleton) poseSkeleton: sp.Skeleton | null = null; // ========== 配置属性 ========== @property(AudioClip) clickAudio: AudioClip | null = null; @property(AudioClip) successAudio: AudioClip | null = null; @property(AudioClip) failAudio: AudioClip | null = null; @property(Prefab) wrongModalPrefab: Prefab | null = null; @property(Prefab) timeoutModalPrefab: Prefab | null = null; @property(Prefab) commonModalPrefab: Prefab | null = null; /** 主图圆角材质 EffectAsset */ @property(EffectAsset) roundedSpriteEffect: EffectAsset | null = null; /** 主图圆角半径比例(相对于短边,0-0.5) */ @property mainImageCornerRadius: number = 0.1; // ========== 内部状态 ========== /** 当前创建的输入框节点数组 */ private _inputNodes: Node[] = []; /** InputLayout 中默认放置的输入框模板节点 */ private _inputTemplateNode: Node | null = null; /** 当前创建的包袱展示块节点数组 */ private _punchBlockNodes: Node[] = []; /** punchLayout 中默认放置的展示块模板节点 */ private _punchBlockTemplateNode: Node | null = null; /** 是否正在同步输入格内容,避免设置文本时重复触发事件 */ private _isSyncingInputText: boolean = false; /** 最近一次自动提交的答案,避免填满后重复提交同一内容 */ private _lastAutoSubmittedAnswer: string = ''; /** 当前正在编辑的输入格索引 */ private _editingInputIndex: number = -1; /** 倒计时剩余秒数 */ private _countdown: number = 60; /** clockLabel 非紧迫状态下的原始颜色(首次渲染时懒记录,避免 hardcode prefab 颜色) */ private _clockLabelNormalColor: Color | null = null; /** InputLayout 原始位置(prefab 中的初始 _lpos,作为"有梗揭示"的目标位置) */ private _inputLayoutOriginalPos: Vec3 | null = null; /** punchLayout 原始位置(prefab 中的初始 _lpos,作为"有梗揭示"的目标位置) */ private _punchLayoutOriginalPos: Vec3 | null = null; /** "无梗居中态"下 InputLayout 的 Y 坐标:InputLayout 原始 Y 与 punchLayout 原始 Y 的中点 */ private _inputLayoutCenteredY: number | null = null; /** 关卡开始时间戳(ms),用于准确计算耗时 */ private _levelStartTime: number = 0; /** 倒计时是否结束 */ private _isTimeUp: boolean = false; /** 当前关卡配置 */ private _currentConfig: RuntimeLevelConfig | null = null; /** 是否正在切换关卡(防止重复提交) */ private _isTransitioning: boolean = false; /** 是否正在解锁提示(防止双击重复触发) */ private _isUnlocking: boolean = false; /** 下一个待解锁的线索序号(2 或 3),超过 3 表示全部已解锁 */ private _nextClueIndex: number = 2; /** 通关页(PassNode)当前是否已展示 */ private _isPassNodeShown: boolean = false; /** PassNode 进出动画是否在进行中(防止重入) */ private _isPassNodeAnimating: boolean = false; /** PassNode 原始 local position(prefab 摆放位),动画结束后用来回归 */ private _passNodeOriginalPos: Vec3 | null = null; /** PassNode Widget 初始启用状态;进出场动画期间临时关闭,避免激活首帧回写位置 */ private _passNodeWidgetOriginalEnabled: boolean | null = null; /** 通关页所用「已通关数量」(业务数据,给成就体系展示用) */ private _passCompletedLevelCount: number | null = null; /** * 本次进入 PageLevel 后累计通关数(onViewLoad / _reinitLevelSession 时归零)。 * 普通模式 / 分享模式都计入;用于驱动 pose 赞美动画档位。 */ private _sessionPassCount: number = 0; /** pose Spine 隐藏延时定时器(setTimeout 句柄);切关 / 关页时需清理避免穿屏触发 */ private _poseHideTimer: ReturnType | null = null; /** pose 赞美延迟播放定时器;等待通关动画与成功音效结束后再触发 */ private _posePraiseDelayTimer: ReturnType | null = null; /** pose 赞美延迟播放序号,用于取消 resources.load 异步回调里的过期播放 */ private _posePraiseSequenceId: number = 0; /** good.mp3 加载缓存 */ private _goodAudioClip: AudioClip | null = null; /** good.mp3 加载中的 Promise,避免重复请求 resources */ private _goodAudioLoadPromise: Promise | null = null; /** 通关页动画起点(通关前)的已通关数量;为 null 表示不播跨称号过渡 */ private _passPreviousCompletedLevelCount: number | null = null; /** 通关页称号 / 进度条动画工具,惰性初始化 */ private _titleAnimator: AchievementTitleAnimator | null = null; /** 错误弹窗实例 */ private _wrongModalNode: Node | null = null; /** 超时弹窗实例 */ private _timeoutModalNode: Node | null = null; /** 通用确认弹窗实例 */ private _commonModalNode: Node | null = null; /** 是否处于分享挑战模式 */ private _isShareMode: boolean = false; /** * 当前 PageLevel 实例所处分享挑战的 shareCode 缓存。 * PageLevel 注册时 cache: true,复用同一个实例。 * 当用户在后台切换好友分享卡片时,ShareManager.shareCode 会发生变化, * onViewShow 通过对比这个值与最新的 ShareManager.shareCode 来判断是否需要 _reinitLevelSession。 */ private _activeShareCode: string | null = null; /** 体力恢复倒计时定时器 */ 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; /** 分享模式下每关最终提交内容,等整场结束后一次性提交 */ private _shareSubmissions: Map = new Map(); /** 是否正在提交分享挑战结果 */ private _isSubmittingShareResult: boolean = false; /** 本场分享挑战是否已经尝试拉取过用户头像昵称,避免结算流程重复弹窗 */ private _hasRequestedShareUserInfo: boolean = false; /** * 页面首次加载时调用 */ onViewLoad(): void { console.log('[PageLevel] onViewLoad'); // 本次进入答题页的会话通关数归零;普通 / 分享模式都重新开始计数 this._sessionPassCount = 0; // 必须在任何可能改动 InputLayout/punchLayout 位置的逻辑之前记录原始位置 this._captureActionOriginalPositions(); const params = this.getParams(); this._isShareMode = params?.shareMode === true; if (this._isShareMode) { this._shareLevelIndex = 0; this._shareSubmissions.clear(); this._isSubmittingShareResult = false; this._hasRequestedShareUserInfo = false; this._activeShareCode = ShareManager.instance.shareCode; console.log('[PageLevel] 进入分享挑战模式'); } else { this._activeShareCode = null; // 从 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._refreshModeUI(); this.updateStaminaLabel(); this.initIconSetting(); this.initUnlockButtons(); this.initSubmitButton(); this.initPkNextLevelButton(); this._initPassNodeState(); // 异步加载关卡资源并调用进入关卡接口,完成后启动倒计时 this._enterAndInitLevel().catch(err => { console.error('[PageLevel] 进入关卡失败:', err); }); } /** * 页面每次显示时调用 */ onViewShow(): void { console.log('[PageLevel] onViewShow'); // PageLevel 注册时 cache: true,缓存实例会被复用。 // 必须根据本次进入时携带的 params 重新派生 _isShareMode, // 否则上一场分享挑战的状态会残留到下一次主线挑战进入。 const params = this.getParams(); const desiredShareMode = params?.shareMode === true; // 当前 ShareManager 中的 shareCode(可能因为后台切到新的分享卡片而变化) const latestShareCode = ShareManager.instance.shareCode; const modeChanged = desiredShareMode !== this._isShareMode; // 同样是分享模式,但 ShareManager 中的 shareCode 已经换了一份题单 —— 也必须重建会话 const shareCodeChanged = desiredShareMode && latestShareCode !== this._activeShareCode; if (modeChanged || shareCodeChanged) { console.log( `[PageLevel] 检测到模式/分享码切换 mode:${this._isShareMode}->${desiredShareMode} ` + `code:${this._activeShareCode}->${latestShareCode},重新初始化关卡会话`, ); this._reinitLevelSession(desiredShareMode); return; } // 上一次离场时如果停留在「答对后通关流程」(_isTransitioning=true 由 showSuccess 置位、 // 而 _applyLevelConfig 才会重置),缓存的 PageLevel 实例会保留完成态: // - 输入格已填入正确答案 // - 提交按钮被 _isTransitioning 锁住,无法重新提交 // - 倒计时已停、谐音梗已揭示 // 此时玩家从首页再次进入会看到一个无法操作的"死局"。必须把会话推进到下一关。 // 注意:这只可能在主线模式发生 —— 分享模式下点 iconSetting / PassNode 的入口 // 都会调用 ShareManager.clearShareMode + ViewManager.replace,再次进入会被 // 上面的 modeChanged / shareCodeChanged 分支拦截走 _reinitLevelSession。 if (this._isTransitioning) { console.log('[PageLevel] 上次离场时停留在通关后状态,自动推进到下一关'); this._resetPassNode(); this._closeWrongModal(); this._closeTimeoutModal(); this._closeCommonModal(); void this.goToNextLevel(); return; } this._refreshModeUI(); this.updateStaminaLabel(); if (!this._isShareMode) { this._startStaminaRecoverTimer(); } } /** * 跨模式切换时(例如分享挑战 → 主线挑战)重置会话状态并重新加载关卡。 * 仅在 onViewShow 检测到模式发生变化时调用,避免对正常的同模式连续作答产生副作用。 */ private _reinitLevelSession(shareMode: boolean): void { this._isShareMode = shareMode; // 跨模式切换视为新的"本次进入"会话,赞美动画从最低档位重新开始 this._sessionPassCount = 0; // 上一场可能遗留的弹窗 / 倒计时一并清掉,避免主线模式还看到分享态弹窗 this._resetPassNode(); this._closeWrongModal(); this._closeTimeoutModal(); this._closeCommonModal(); this.stopCountdown(); this._stopStaminaRecoverTimer(); // 复位分享态相关字段(无论切换到哪个模式,分享态都不应残留) this._shareLevelIndex = 0; this._shareSubmissions.clear(); this._isSubmittingShareResult = false; this._hasRequestedShareUserInfo = false; if (this._isShareMode) { this._activeShareCode = ShareManager.instance.shareCode; console.log(`[PageLevel] 切换到分享挑战模式 (shareCode=${this._activeShareCode})`); } else { this._activeShareCode = null; // 主线模式:从 AuthManager 拉取最新的 nextLevel this._nextLevelData = null; const nextLevel = AuthManager.instance.nextLevel; if (nextLevel) { this._currentLevelId = nextLevel.id; this._currentLevelNumber = nextLevel.level; console.log(`[PageLevel] 切换到主线挑战,进入关卡: 第 ${nextLevel.level} 关 (${nextLevel.id})`); } else { this._currentLevelId = ''; this._currentLevelNumber = 0; console.warn('[PageLevel] 切换到主线挑战,但没有可用关卡'); } } this._refreshModeUI(); this.updateStaminaLabel(); if (!this._isShareMode) { this._startStaminaRecoverTimer(); } // 异步加载关卡资源并调用进入关卡接口,完成后启动倒计时 this._enterAndInitLevel().catch(err => { console.error('[PageLevel] 模式切换后重新进入关卡失败:', err); }); } /** * 页面隐藏时调用 */ onViewHide(): void { console.log('[PageLevel] onViewHide'); this._stopStaminaRecoverTimer(); } /** * 页面销毁时调用 */ onViewDestroy(): void { console.log('[PageLevel] onViewDestroy'); this.clearInputNodes(); this.clearPunchBlocks(); this.stopCountdown(); this._resetPassNode(); this._closeWrongModal(); this._closeTimeoutModal(); this._closeCommonModal(); this._stopStaminaRecoverTimer(); // 清理事件监听 this.iconSetting?.off(Node.EventType.TOUCH_END, this.onIconSettingClick, this); this.unLockTipsBtn?.off(Node.EventType.TOUCH_END); this.addTimeBtn?.off(Node.EventType.TOUCH_END); this.submitButton?.off(Node.EventType.TOUCH_END, this.onSubmitAnswer, this); this.pkNextLevelButton?.off(Node.EventType.TOUCH_END, this.onPkNextLevelClick, this); } /** * 进入关卡并初始化 * 1. 加载关卡图片资源(从缓存或 NextLevelData) * 2. 调用进入关卡接口(消耗体力,获取答案和线索) * 3. 启动倒计时 */ private async _enterAndInitLevel(): Promise { let config: RuntimeLevelConfig | null = null; if (this._isShareMode) { // 分享模式:使用 ShareManager 的关卡数据 config = await ShareManager.instance.ensureShareLevelReady(this._shareLevelIndex); } else { // 正常模式:先尝试从缓存获取(PageLoading 初始化时已加载首关) config = LevelDataManager.instance.getLevelConfig(this._currentLevelId); if (!config) { // 缓存未命中,从 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] 没有找到关卡配置,ID: ${this._currentLevelId}`); return; } // 非分享模式下,调用进入关卡接口获取答案和线索 if (!this._isShareMode) { 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('进入关卡失败,请重试'); } this.updateStaminaLabel(); return; } // 用 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._currentLevelNumber}关: ${config.name}`); ```