feat: 对接最新的关卡工作流
This commit is contained in:
@@ -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} 关`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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<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;
|
||||
|
||||
/**
|
||||
* 页面首次加载时调用
|
||||
*/
|
||||
@@ -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<void> {
|
||||
// 先加载关卡图片资源
|
||||
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<void> {
|
||||
// 标记当前关卡已通关
|
||||
if (!this._isShareMode) {
|
||||
StorageManager.onLevelCompleted(this.currentLevelIndex);
|
||||
LevelDataManager.instance.markLevelCompleted(this.currentLevelIndex);
|
||||
}
|
||||
|
||||
// 查找下一个未通关的关卡
|
||||
private async goToNextLevel(): Promise<void> {
|
||||
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}关`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<void> {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
162
assets/prefabs/TimeoutModal.ts
Normal file
162
assets/prefabs/TimeoutModal.ts
Normal file
@@ -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?.();
|
||||
}
|
||||
}
|
||||
9
assets/prefabs/TimeoutModal.ts.meta
Normal file
9
assets/prefabs/TimeoutModal.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "bdb18473-6efb-4592-bf67-48555845eec5",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -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": {
|
||||
|
||||
111
assets/prefabs/WrongModal.ts
Normal file
111
assets/prefabs/WrongModal.ts
Normal file
@@ -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?.();
|
||||
}
|
||||
}
|
||||
9
assets/prefabs/WrongModal.ts.meta
Normal file
9
assets/prefabs/WrongModal.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "972c57fc-10e7-493e-a1da-4849f3c1e555",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -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`,
|
||||
/** 分享相关 */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/** 创建分享响应 */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<number, RuntimeLevelConfig> = new Map();
|
||||
|
||||
/** 是否已成功从 API 获取数据 */
|
||||
private _hasApiData: boolean = false;
|
||||
/** 运行时关卡配置缓存(按 levelId 索引) */
|
||||
private _levelConfigs: Map<string, RuntimeLevelConfig> = new Map();
|
||||
|
||||
/** 图片缓存:URL -> SpriteFrame */
|
||||
private _imageCache: Map<string, SpriteFrame> = new Map();
|
||||
|
||||
/** 正在加载中的关卡索引集合 */
|
||||
private _loadingLevels: Set<number> = new Set();
|
||||
/** 正在加载中的关卡 ID 集合 */
|
||||
private _loadingLevels: Set<string> = 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<boolean> {
|
||||
console.log('[LevelDataManager] 开始初始化');
|
||||
async initialize(nextLevel: NextLevelData, onProgress?: ProgressCallback): Promise<boolean> {
|
||||
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<RuntimeLevelConfig | null> {
|
||||
// 检查索引有效性
|
||||
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<RuntimeLevelConfig | null> {
|
||||
// 检查缓存
|
||||
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<ApiLevelData[] | null> {
|
||||
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<ApiResponse>(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<void> {
|
||||
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<RuntimeLevelConfig | null> {
|
||||
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] 缓存已清除');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user