feat: 接入激励视频

This commit is contained in:
richarjiang
2026-06-29 19:55:40 +08:00
parent 411ca8c772
commit e2fd4eeda3
16 changed files with 9215 additions and 73 deletions

View File

@@ -0,0 +1,715 @@
# 梗中作乐 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;
/**
* 通关赞美 spinepose 节点)档位:[最小通关数, 动画名],倒序匹配。
* 1-5 关 → "1"6-10 关 → "2"11+ 关 → "3"。
* count <= 0 不会匹配任何档位,自然不播放。
*/
private static readonly POSE_TIERS: ReadonlyArray<readonly [number, string]> = [
[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 positionprefab 摆放位),动画结束后用来回归 */
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<typeof setTimeout> | null = null;
/** pose 赞美延迟播放定时器;等待通关动画与成功音效结束后再触发 */
private _posePraiseDelayTimer: ReturnType<typeof setTimeout> | null = null;
/** pose 赞美延迟播放序号,用于取消 resources.load 异步回调里的过期播放 */
private _posePraiseSequenceId: number = 0;
/** good.mp3 加载缓存 */
private _goodAudioClip: AudioClip | null = null;
/** good.mp3 加载中的 Promise避免重复请求 resources */
private _goodAudioLoadPromise: Promise<AudioClip | null> | 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<typeof setInterval> | 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<string, SubmitShareLevel> = 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<void> {
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}`);
```