Files
mp-xieyingeng/软著源代码/05-关卡核心逻辑.md
2026-06-29 19:55:40 +08:00

716 lines
28 KiB
Markdown
Raw Blame History

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