Compare commits
4 Commits
f99bc12f52
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a62eb2319c | ||
|
|
9899f696b2 | ||
|
|
45bb6b35ae | ||
|
|
b05ef71368 |
@@ -474,6 +474,14 @@
|
||||
"__uuid__": "8611dbdc-4749-49f1-97ca-aedae3d16320",
|
||||
"__expectedType__": "cc.Prefab"
|
||||
},
|
||||
"toastPrefab": {
|
||||
"__uuid__": "cff2809d-6daa-4749-a911-bb99e97b4b54",
|
||||
"__expectedType__": "cc.Prefab"
|
||||
},
|
||||
"passModalPrefab": {
|
||||
"__uuid__": "29ff0bfc-d5cf-4ad1-b8cb-61bdfd4850ef",
|
||||
"__expectedType__": "cc.Prefab"
|
||||
},
|
||||
"_id": "c2b3nbzv9JuZmP2jxQyN72"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { _decorator, Component, Prefab } from 'cc';
|
||||
import { ViewManager } from './scripts/core/ViewManager';
|
||||
import { ToastManager } from './scripts/utils/ToastManager';
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
@@ -14,6 +15,9 @@ export class main extends Component {
|
||||
@property({ type: Prefab, tooltip: '关卡页面预制体' })
|
||||
pageLevelPrefab: Prefab | null = null;
|
||||
|
||||
@property({ type: Prefab, tooltip: 'Toast 预制体' })
|
||||
toastPrefab: Prefab | null = null;
|
||||
|
||||
/**
|
||||
* onLoad 比 start 更早执行
|
||||
* 确保 ViewManager 在 PageLoading.start() 之前初始化
|
||||
@@ -45,5 +49,10 @@ export class main extends Component {
|
||||
zIndex: 1
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化 Toast 管理器
|
||||
if (this.toastPrefab) {
|
||||
ToastManager.instance.init(this.toastPrefab, this.node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,10 +404,7 @@
|
||||
"b": 255,
|
||||
"a": 255
|
||||
},
|
||||
"_spriteFrame": {
|
||||
"__uuid__": "388a4fd2-4c46-46ae-b796-10ab85c39e04@f9941",
|
||||
"__expectedType__": "cc.SpriteFrame"
|
||||
},
|
||||
"_spriteFrame": null,
|
||||
"_type": 0,
|
||||
"_fillType": 0,
|
||||
"_sizeMode": 0,
|
||||
@@ -6274,6 +6271,10 @@
|
||||
"__uuid__": "be83ca42-3579-46e8-821f-7a0f0b9d8464",
|
||||
"__expectedType__": "cc.AudioClip"
|
||||
},
|
||||
"passModalPrefab": {
|
||||
"__uuid__": "29ff0bfc-d5cf-4ad1-b8cb-61bdfd4850ef",
|
||||
"__expectedType__": "cc.Prefab"
|
||||
},
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { _decorator, Node, EditBox, instantiate, Vec3, Button, Label, Sprite, SpriteFrame, AudioClip, AudioSource, UITransform } from 'cc';
|
||||
import { _decorator, Node, EditBox, instantiate, Vec3, Button, Label, Sprite, SpriteFrame, AudioClip, AudioSource, UITransform, Prefab } from 'cc';
|
||||
import { BaseView } from 'db://assets/scripts/core/BaseView';
|
||||
import { ViewManager } from 'db://assets/scripts/core/ViewManager';
|
||||
import { StorageManager } from 'db://assets/scripts/utils/StorageManager';
|
||||
import { WxSDK } from 'db://assets/scripts/utils/WxSDK';
|
||||
import { LevelDataManager } from 'db://assets/scripts/utils/LevelDataManager';
|
||||
import { RuntimeLevelConfig } from 'db://assets/scripts/types/LevelTypes';
|
||||
import { ToastManager } from 'db://assets/scripts/utils/ToastManager';
|
||||
import { PassModal } from 'db://assets/prefabs/PassModal';
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
@@ -13,6 +15,9 @@ const { ccclass, property } = _decorator;
|
||||
*/
|
||||
@ccclass('PageLevel')
|
||||
export class PageLevel extends BaseView {
|
||||
/** 静态常量:零位置 */
|
||||
private static readonly ZERO_POS = new Vec3(0, 0, 0);
|
||||
|
||||
// ========== 节点引用 ==========
|
||||
@property(Node)
|
||||
inputLayout: Node | null = null;
|
||||
@@ -72,6 +77,9 @@ export class PageLevel extends BaseView {
|
||||
@property(AudioClip)
|
||||
failAudio: AudioClip | null = null;
|
||||
|
||||
@property(Prefab)
|
||||
passModalPrefab: Prefab | null = null;
|
||||
|
||||
// ========== 内部状态 ==========
|
||||
/** 当前创建的输入框节点数组 */
|
||||
private _inputNodes: Node[] = [];
|
||||
@@ -88,17 +96,28 @@ export class PageLevel extends BaseView {
|
||||
/** 是否正在切换关卡(防止重复提交) */
|
||||
private _isTransitioning: boolean = false;
|
||||
|
||||
/** 通关弹窗实例 */
|
||||
private _passModalNode: Node | null = null;
|
||||
|
||||
/**
|
||||
* 页面首次加载时调用
|
||||
*/
|
||||
onViewLoad(): void {
|
||||
console.log('[PageLevel] onViewLoad');
|
||||
// 从本地存储恢复关卡进度
|
||||
this.currentLevelIndex = StorageManager.getCurrentLevelIndex();
|
||||
console.log(`[PageLevel] 恢复关卡进度: 第 ${this.currentLevelIndex + 1} 关`);
|
||||
this.updateLiveLabel();
|
||||
this.initLevel();
|
||||
this.initIconSetting();
|
||||
this.initUnlockButtons();
|
||||
this.initSubmitButton();
|
||||
|
||||
// 异步加载关卡资源,完成后启动倒计时
|
||||
this.initLevel().then(() => {
|
||||
this.startCountdown();
|
||||
}).catch(err => {
|
||||
console.error('[PageLevel] 加载关卡失败:', err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,13 +142,22 @@ export class PageLevel extends BaseView {
|
||||
console.log('[PageLevel] onViewDestroy');
|
||||
this.clearInputNodes();
|
||||
this.stopCountdown();
|
||||
this._closePassModal();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化关卡(从 API 数据加载)
|
||||
* 初始化关卡(从 API 数据加载,异步确保资源就绪)
|
||||
*/
|
||||
private initLevel(): void {
|
||||
const config = LevelDataManager.instance.getLevelConfig(this.currentLevelIndex);
|
||||
private async initLevel(): Promise<void> {
|
||||
// 先尝试从缓存获取
|
||||
let config = LevelDataManager.instance.getLevelConfig(this.currentLevelIndex);
|
||||
|
||||
if (!config) {
|
||||
// 缓存中没有,异步加载
|
||||
console.log(`[PageLevel] 关卡 ${this.currentLevelIndex + 1} 资源未缓存,开始加载...`);
|
||||
config = await LevelDataManager.instance.ensureLevelReady(this.currentLevelIndex);
|
||||
}
|
||||
|
||||
if (!config) {
|
||||
console.warn(`[PageLevel] 没有找到关卡配置,索引: ${this.currentLevelIndex}`);
|
||||
return;
|
||||
@@ -172,6 +200,9 @@ export class PageLevel extends BaseView {
|
||||
// 更新倒计时显示
|
||||
this.updateClockLabel();
|
||||
|
||||
// 预加载下一关图片(静默加载,不阻塞)
|
||||
LevelDataManager.instance.preloadNextLevel(this.currentLevelIndex);
|
||||
|
||||
console.log(`[PageLevel] 初始化关卡 ${this.currentLevelIndex + 1}, 答案长度: ${config.answer.length}`);
|
||||
}
|
||||
|
||||
@@ -197,7 +228,7 @@ export class PageLevel extends BaseView {
|
||||
inputNode.name = 'singleInput';
|
||||
|
||||
// 设置位置
|
||||
inputNode.setPosition(new Vec3(0, 0, 0));
|
||||
inputNode.setPosition(PageLevel.ZERO_POS);
|
||||
|
||||
// 获取 EditBox 组件
|
||||
const editBox = inputNode.getComponent(EditBox);
|
||||
@@ -644,10 +675,71 @@ export class PageLevel extends BaseView {
|
||||
// 通关奖励:增加一颗生命值
|
||||
this.addLife();
|
||||
|
||||
// 延迟后进入下一关
|
||||
this.scheduleOnce(() => {
|
||||
// 显示通关弹窗
|
||||
this._showPassModal();
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示通关弹窗
|
||||
* 将弹窗添加到 Canvas 根节点下(而非 PageLevel 子节点)
|
||||
* 这样 Widget 可以正确对齐到全屏
|
||||
*/
|
||||
private _showPassModal(): void {
|
||||
if (!this.passModalPrefab) {
|
||||
console.warn('[PageLevel] passModalPrefab 未设置');
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果弹窗已显示,不再重复创建
|
||||
if (this._passModalNode && this._passModalNode.isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 实例化弹窗
|
||||
const modalNode = instantiate(this.passModalPrefab);
|
||||
modalNode.setPosition(PageLevel.ZERO_POS);
|
||||
modalNode.setSiblingIndex(PassModal.MODAL_Z_INDEX);
|
||||
|
||||
// 找到 Canvas 根节点并添加弹窗
|
||||
const canvasNode = this.node.parent;
|
||||
if (canvasNode) {
|
||||
canvasNode.addChild(modalNode);
|
||||
} else {
|
||||
this.node.addChild(modalNode);
|
||||
}
|
||||
this._passModalNode = modalNode;
|
||||
|
||||
// 获取 PassModal 组件并设置回调
|
||||
const passModal = modalNode.getComponent(PassModal);
|
||||
if (passModal) {
|
||||
passModal.setParams({ levelIndex: this.currentLevelIndex + 1 });
|
||||
passModal.setCallbacks({
|
||||
onNextLevel: () => {
|
||||
this._closePassModal();
|
||||
this.nextLevel();
|
||||
}, 1.0);
|
||||
},
|
||||
onShare: () => {
|
||||
// 分享后不关闭弹窗,用户可继续点击下一关
|
||||
console.log('[PageLevel] 分享完成');
|
||||
}
|
||||
});
|
||||
// 手动调用 onViewLoad 和 onViewShow
|
||||
passModal.onViewLoad();
|
||||
passModal.onViewShow();
|
||||
}
|
||||
|
||||
console.log('[PageLevel] 显示通关弹窗');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭通关弹窗
|
||||
*/
|
||||
private _closePassModal(): void {
|
||||
if (this._passModalNode && this._passModalNode.isValid) {
|
||||
this._passModalNode.destroy();
|
||||
this._passModalNode = null;
|
||||
console.log('[PageLevel] 关闭通关弹窗');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -661,12 +753,18 @@ export class PageLevel extends BaseView {
|
||||
|
||||
// 触发手机震动
|
||||
WxSDK.vibrateLong();
|
||||
|
||||
// 显示 Toast 提示
|
||||
ToastManager.show('答案错误,再试试吧!');
|
||||
}
|
||||
|
||||
/**
|
||||
* 进入下一关
|
||||
*/
|
||||
private nextLevel(): void {
|
||||
private async nextLevel(): Promise<void> {
|
||||
// 保存当前关卡进度
|
||||
StorageManager.onLevelCompleted(this.currentLevelIndex);
|
||||
|
||||
this.currentLevelIndex++;
|
||||
|
||||
// 检查是否还有关卡
|
||||
@@ -681,7 +779,7 @@ export class PageLevel extends BaseView {
|
||||
}
|
||||
|
||||
// 重置并加载下一关,重新开始倒计时
|
||||
this.initLevel();
|
||||
await this.initLevel();
|
||||
this.startCountdown();
|
||||
console.log(`[PageLevel] 进入关卡 ${this.currentLevelIndex + 1}`);
|
||||
}
|
||||
|
||||
1853
assets/prefabs/PassModal.prefab
Normal file
1853
assets/prefabs/PassModal.prefab
Normal file
File diff suppressed because it is too large
Load Diff
13
assets/prefabs/PassModal.prefab.meta
Normal file
13
assets/prefabs/PassModal.prefab.meta
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"ver": "1.1.50",
|
||||
"importer": "prefab",
|
||||
"imported": true,
|
||||
"uuid": "29ff0bfc-d5cf-4ad1-b8cb-61bdfd4850ef",
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {
|
||||
"syncNodeName": "PassModal"
|
||||
}
|
||||
}
|
||||
154
assets/prefabs/PassModal.ts
Normal file
154
assets/prefabs/PassModal.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { _decorator, Node, Label, AudioClip, AudioSource, view, UITransform, Size } from 'cc';
|
||||
import { BaseView } from 'db://assets/scripts/core/BaseView';
|
||||
import { WxSDK } from 'db://assets/scripts/utils/WxSDK';
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
* PassModal 回调接口
|
||||
*/
|
||||
export interface PassModalCallbacks {
|
||||
/** 点击下一关回调 */
|
||||
onNextLevel?: () => void;
|
||||
/** 点击分享回调 */
|
||||
onShare?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通关弹窗组件
|
||||
* 继承 BaseView,显示通关成功弹窗,提供"下一关"和"分享给好友"两个按钮
|
||||
*/
|
||||
@ccclass('PassModal')
|
||||
export class PassModal extends BaseView {
|
||||
/** 静态常量:弹窗层级 */
|
||||
public static readonly MODAL_Z_INDEX = 999;
|
||||
|
||||
/** 下一关按钮 */
|
||||
@property(Node)
|
||||
nextLevelButton: Node | null = null;
|
||||
|
||||
/** 分享按钮 */
|
||||
@property(Node)
|
||||
shareButton: Node | null = null;
|
||||
|
||||
/** 提示Label(如 +1 生命) */
|
||||
@property(Label)
|
||||
tipLabel: Label | null = null;
|
||||
|
||||
/** 通关音效 */
|
||||
@property(AudioClip)
|
||||
successAudio: AudioClip | null = null;
|
||||
|
||||
/** 回调函数 */
|
||||
private _callbacks: PassModalCallbacks = {};
|
||||
|
||||
/** 缓存的屏幕尺寸 */
|
||||
private _screenSize: Size | null = null;
|
||||
|
||||
/**
|
||||
* 设置回调函数
|
||||
*/
|
||||
setCallbacks(callbacks: PassModalCallbacks): void {
|
||||
this._callbacks = callbacks;
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面首次加载时调用
|
||||
*/
|
||||
onViewLoad(): void {
|
||||
console.log('[PassModal] onViewLoad');
|
||||
this._bindButtonEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面每次显示时调用
|
||||
*/
|
||||
onViewShow(): void {
|
||||
this._updateWidget();
|
||||
this._playSuccessSound();
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面销毁时调用
|
||||
*/
|
||||
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.nextLevelButton) {
|
||||
this.nextLevelButton.on(Node.EventType.TOUCH_END, this._onNextLevelClick, this);
|
||||
}
|
||||
if (this.shareButton) {
|
||||
this.shareButton.on(Node.EventType.TOUCH_END, this._onShareClick, this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解除按钮事件绑定
|
||||
*/
|
||||
private _unbindButtonEvents(): void {
|
||||
// 节点可能在销毁过程中已被置空,需要检查 isValid
|
||||
if (this.nextLevelButton && this.nextLevelButton.isValid) {
|
||||
this.nextLevelButton.off(Node.EventType.TOUCH_END, this._onNextLevelClick, this);
|
||||
}
|
||||
if (this.shareButton && this.shareButton.isValid) {
|
||||
this.shareButton.off(Node.EventType.TOUCH_END, this._onShareClick, this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 播放通关音效
|
||||
*/
|
||||
private _playSuccessSound(): void {
|
||||
if (!this.successAudio) {
|
||||
return;
|
||||
}
|
||||
|
||||
const audioSource = this.node.getComponent(AudioSource);
|
||||
if (audioSource) {
|
||||
audioSource.playOneShot(this.successAudio);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下一关按钮点击
|
||||
*/
|
||||
private _onNextLevelClick(): void {
|
||||
console.log('[PassModal] 点击下一关');
|
||||
this._callbacks.onNextLevel?.();
|
||||
}
|
||||
|
||||
/**
|
||||
* 分享按钮点击
|
||||
*/
|
||||
private _onShareClick(): void {
|
||||
console.log('[PassModal] 点击分享');
|
||||
|
||||
// 调用微信分享
|
||||
WxSDK.shareAppMessage({
|
||||
title: '快来一起玩这款游戏吧',
|
||||
query: `level=${this._params?.levelIndex ?? 1}`
|
||||
});
|
||||
|
||||
this._callbacks.onShare?.();
|
||||
}
|
||||
}
|
||||
9
assets/prefabs/PassModal.ts.meta
Normal file
9
assets/prefabs/PassModal.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "c08bfab3-d14b-4398-bf27-afde6770b665",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
425
assets/prefabs/Toast.prefab
Normal file
425
assets/prefabs/Toast.prefab
Normal file
@@ -0,0 +1,425 @@
|
||||
[
|
||||
{
|
||||
"__type__": "cc.Prefab",
|
||||
"_name": "Toast",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"_native": "",
|
||||
"data": {
|
||||
"__id__": 1
|
||||
},
|
||||
"optimizationPolicy": 0,
|
||||
"persistent": false
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "Toast",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"_parent": null,
|
||||
"_children": [
|
||||
{
|
||||
"__id__": 2
|
||||
},
|
||||
{
|
||||
"__id__": 8
|
||||
}
|
||||
],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 14
|
||||
},
|
||||
{
|
||||
"__id__": 16
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 18
|
||||
},
|
||||
"_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.Node",
|
||||
"_name": "Bg",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"_parent": {
|
||||
"__id__": 1
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 3
|
||||
},
|
||||
{
|
||||
"__id__": 5
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 7
|
||||
},
|
||||
"_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": 600,
|
||||
"height": 100.2
|
||||
},
|
||||
"_anchorPoint": {
|
||||
"__type__": "cc.Vec2",
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
"fileId": "eb34X6nKREI5sW484fCbxJ"
|
||||
},
|
||||
{
|
||||
"__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": 81
|
||||
},
|
||||
"_spriteFrame": {
|
||||
"__uuid__": "7d8f9b89-4fd1-4c9f-a3ab-38ec7cded7ca@f9941",
|
||||
"__expectedType__": "cc.SpriteFrame"
|
||||
},
|
||||
"_type": 0,
|
||||
"_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": "ccLTaXNAxNN5+PbNTb0/4j"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.PrefabInfo",
|
||||
"root": {
|
||||
"__id__": 1
|
||||
},
|
||||
"asset": {
|
||||
"__id__": 0
|
||||
},
|
||||
"fileId": "bbXTIr965Gx7L1svMvPPuy",
|
||||
"instance": null,
|
||||
"targetOverrides": null,
|
||||
"nestedPrefabInstanceRoots": null
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "Content",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"_parent": {
|
||||
"__id__": 1
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 9
|
||||
},
|
||||
{
|
||||
"__id__": 11
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 13
|
||||
},
|
||||
"_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__": 8
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 10
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 240,
|
||||
"height": 50.4
|
||||
},
|
||||
"_anchorPoint": {
|
||||
"__type__": "cc.Vec2",
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
"fileId": "7fiEDGRW9OSKl45B3f5V9U"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Label",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 8
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 12
|
||||
},
|
||||
"_customMaterial": null,
|
||||
"_srcBlendFactor": 2,
|
||||
"_dstBlendFactor": 4,
|
||||
"_color": {
|
||||
"__type__": "cc.Color",
|
||||
"r": 255,
|
||||
"g": 255,
|
||||
"b": 255,
|
||||
"a": 255
|
||||
},
|
||||
"_string": "这是一个提示",
|
||||
"_horizontalAlign": 1,
|
||||
"_verticalAlign": 1,
|
||||
"_actualFontSize": 40,
|
||||
"_fontSize": 40,
|
||||
"_fontFamily": "Arial",
|
||||
"_lineHeight": 40,
|
||||
"_overflow": 0,
|
||||
"_enableWrapText": true,
|
||||
"_font": null,
|
||||
"_isSystemFontUsed": true,
|
||||
"_spacingX": 0,
|
||||
"_isItalic": false,
|
||||
"_isBold": false,
|
||||
"_isUnderline": false,
|
||||
"_underlineHeight": 2,
|
||||
"_cacheMode": 0,
|
||||
"_enableOutline": false,
|
||||
"_outlineColor": {
|
||||
"__type__": "cc.Color",
|
||||
"r": 0,
|
||||
"g": 0,
|
||||
"b": 0,
|
||||
"a": 255
|
||||
},
|
||||
"_outlineWidth": 2,
|
||||
"_enableShadow": false,
|
||||
"_shadowColor": {
|
||||
"__type__": "cc.Color",
|
||||
"r": 0,
|
||||
"g": 0,
|
||||
"b": 0,
|
||||
"a": 255
|
||||
},
|
||||
"_shadowOffset": {
|
||||
"__type__": "cc.Vec2",
|
||||
"x": 2,
|
||||
"y": 2
|
||||
},
|
||||
"_shadowBlur": 2,
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
"fileId": "7c1OW3o5JEg7hHN4XokTfS"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.PrefabInfo",
|
||||
"root": {
|
||||
"__id__": 1
|
||||
},
|
||||
"asset": {
|
||||
"__id__": 0
|
||||
},
|
||||
"fileId": "67pbvD85ZOx75N+pmBJOuz",
|
||||
"instance": null,
|
||||
"targetOverrides": null,
|
||||
"nestedPrefabInstanceRoots": null
|
||||
},
|
||||
{
|
||||
"__type__": "cc.UITransform",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 1
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 15
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 1080,
|
||||
"height": 2160
|
||||
},
|
||||
"_anchorPoint": {
|
||||
"__type__": "cc.Vec2",
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
"fileId": "5aGKm9R8JK3o+0MgSZhQCU"
|
||||
},
|
||||
{
|
||||
"__type__": "64bceXnkCpDdZDL3fRWV2/3",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 1
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 17
|
||||
},
|
||||
"contentLabel": {
|
||||
"__id__": 11
|
||||
},
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
"fileId": "f9bSb/raBCPIvJVb+jhlpc"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.PrefabInfo",
|
||||
"root": {
|
||||
"__id__": 1
|
||||
},
|
||||
"asset": {
|
||||
"__id__": 0
|
||||
},
|
||||
"fileId": "c46/YsCPVOJYA4mWEpNYRx",
|
||||
"instance": null,
|
||||
"targetOverrides": null
|
||||
}
|
||||
]
|
||||
13
assets/prefabs/Toast.prefab.meta
Normal file
13
assets/prefabs/Toast.prefab.meta
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"ver": "1.1.50",
|
||||
"importer": "prefab",
|
||||
"imported": true,
|
||||
"uuid": "cff2809d-6daa-4749-a911-bb99e97b4b54",
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {
|
||||
"syncNodeName": "Toast"
|
||||
}
|
||||
}
|
||||
49
assets/prefabs/Toast.ts
Normal file
49
assets/prefabs/Toast.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { _decorator, Component, Label, tween, UIOpacity } from 'cc';
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
@ccclass('Toast')
|
||||
export class Toast extends Component {
|
||||
@property(Label)
|
||||
contentLabel: Label | null = null;
|
||||
|
||||
private _uiOpacity: UIOpacity | null = null;
|
||||
|
||||
onLoad() {
|
||||
// 获取或添加 UIOpacity 组件用于透明度动画
|
||||
this._uiOpacity = this.node.getComponent(UIOpacity);
|
||||
if (!this._uiOpacity) {
|
||||
this._uiOpacity = this.node.addComponent(UIOpacity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示 Toast
|
||||
* @param content 提示内容
|
||||
* @param duration 显示时长(毫秒),默认 2000ms
|
||||
*/
|
||||
show(content: string, duration: number = 2000): void {
|
||||
if (this.contentLabel) {
|
||||
this.contentLabel.string = content;
|
||||
}
|
||||
|
||||
// 重置透明度
|
||||
this._uiOpacity!.opacity = 255;
|
||||
|
||||
// 延迟后执行渐隐动画
|
||||
this.scheduleOnce(() => {
|
||||
this._fadeOut();
|
||||
}, duration / 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 渐隐动画并销毁
|
||||
*/
|
||||
private _fadeOut(): void {
|
||||
tween(this._uiOpacity!)
|
||||
.to(0.3, { opacity: 0 })
|
||||
.call(() => {
|
||||
this.node.destroy();
|
||||
})
|
||||
.start();
|
||||
}
|
||||
}
|
||||
9
assets/prefabs/Toast.ts.meta
Normal file
9
assets/prefabs/Toast.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "64bce5e7-902a-4375-90cb-ddf456576ff7",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
BIN
assets/resources/images/pageLevel/ButtonBg.png
Normal file
BIN
assets/resources/images/pageLevel/ButtonBg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
134
assets/resources/images/pageLevel/ButtonBg.png.meta
Normal file
134
assets/resources/images/pageLevel/ButtonBg.png.meta
Normal file
@@ -0,0 +1,134 @@
|
||||
{
|
||||
"ver": "1.0.27",
|
||||
"importer": "image",
|
||||
"imported": true,
|
||||
"uuid": "e1267c1b-ceb0-4483-b36d-bc9cb4d2fd26",
|
||||
"files": [
|
||||
".json",
|
||||
".png"
|
||||
],
|
||||
"subMetas": {
|
||||
"6c48a": {
|
||||
"importer": "texture",
|
||||
"uuid": "e1267c1b-ceb0-4483-b36d-bc9cb4d2fd26@6c48a",
|
||||
"displayName": "ButtonBg",
|
||||
"id": "6c48a",
|
||||
"name": "texture",
|
||||
"userData": {
|
||||
"wrapModeS": "clamp-to-edge",
|
||||
"wrapModeT": "clamp-to-edge",
|
||||
"imageUuidOrDatabaseUri": "e1267c1b-ceb0-4483-b36d-bc9cb4d2fd26",
|
||||
"isUuid": true,
|
||||
"visible": false,
|
||||
"minfilter": "linear",
|
||||
"magfilter": "linear",
|
||||
"mipfilter": "none",
|
||||
"anisotropy": 0
|
||||
},
|
||||
"ver": "1.0.22",
|
||||
"imported": true,
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {}
|
||||
},
|
||||
"f9941": {
|
||||
"importer": "sprite-frame",
|
||||
"uuid": "e1267c1b-ceb0-4483-b36d-bc9cb4d2fd26@f9941",
|
||||
"displayName": "ButtonBg",
|
||||
"id": "f9941",
|
||||
"name": "spriteFrame",
|
||||
"userData": {
|
||||
"trimThreshold": 1,
|
||||
"rotated": false,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0.5,
|
||||
"trimX": 1,
|
||||
"trimY": 0,
|
||||
"width": 306,
|
||||
"height": 77,
|
||||
"rawWidth": 308,
|
||||
"rawHeight": 78,
|
||||
"borderTop": 0,
|
||||
"borderBottom": 0,
|
||||
"borderLeft": 0,
|
||||
"borderRight": 0,
|
||||
"packable": true,
|
||||
"pixelsToUnit": 100,
|
||||
"pivotX": 0.5,
|
||||
"pivotY": 0.5,
|
||||
"meshType": 0,
|
||||
"vertices": {
|
||||
"rawPosition": [
|
||||
-153,
|
||||
-38.5,
|
||||
0,
|
||||
153,
|
||||
-38.5,
|
||||
0,
|
||||
-153,
|
||||
38.5,
|
||||
0,
|
||||
153,
|
||||
38.5,
|
||||
0
|
||||
],
|
||||
"indexes": [
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
2,
|
||||
1,
|
||||
3
|
||||
],
|
||||
"uv": [
|
||||
1,
|
||||
78,
|
||||
307,
|
||||
78,
|
||||
1,
|
||||
1,
|
||||
307,
|
||||
1
|
||||
],
|
||||
"nuv": [
|
||||
0.003246753246753247,
|
||||
0.01282051282051282,
|
||||
0.9967532467532467,
|
||||
0.01282051282051282,
|
||||
0.003246753246753247,
|
||||
1,
|
||||
0.9967532467532467,
|
||||
1
|
||||
],
|
||||
"minPos": [
|
||||
-153,
|
||||
-38.5,
|
||||
0
|
||||
],
|
||||
"maxPos": [
|
||||
153,
|
||||
38.5,
|
||||
0
|
||||
]
|
||||
},
|
||||
"isUuid": true,
|
||||
"imageUuidOrDatabaseUri": "e1267c1b-ceb0-4483-b36d-bc9cb4d2fd26@6c48a",
|
||||
"atlasUuid": "",
|
||||
"trimType": "auto"
|
||||
},
|
||||
"ver": "1.0.12",
|
||||
"imported": true,
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {}
|
||||
}
|
||||
},
|
||||
"userData": {
|
||||
"type": "sprite-frame",
|
||||
"fixAlphaTransparencyArtifacts": false,
|
||||
"hasAlpha": true,
|
||||
"redirect": "e1267c1b-ceb0-4483-b36d-bc9cb4d2fd26@6c48a"
|
||||
}
|
||||
}
|
||||
BIN
assets/resources/images/pageLevel/test.png
Normal file
BIN
assets/resources/images/pageLevel/test.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
134
assets/resources/images/pageLevel/test.png.meta
Normal file
134
assets/resources/images/pageLevel/test.png.meta
Normal file
@@ -0,0 +1,134 @@
|
||||
{
|
||||
"ver": "1.0.27",
|
||||
"importer": "image",
|
||||
"imported": true,
|
||||
"uuid": "d46acd4d-66e2-423b-8015-334ff99dd9f1",
|
||||
"files": [
|
||||
".json",
|
||||
".png"
|
||||
],
|
||||
"subMetas": {
|
||||
"6c48a": {
|
||||
"importer": "texture",
|
||||
"uuid": "d46acd4d-66e2-423b-8015-334ff99dd9f1@6c48a",
|
||||
"displayName": "test",
|
||||
"id": "6c48a",
|
||||
"name": "texture",
|
||||
"userData": {
|
||||
"wrapModeS": "clamp-to-edge",
|
||||
"wrapModeT": "clamp-to-edge",
|
||||
"imageUuidOrDatabaseUri": "d46acd4d-66e2-423b-8015-334ff99dd9f1",
|
||||
"isUuid": true,
|
||||
"visible": false,
|
||||
"minfilter": "linear",
|
||||
"magfilter": "linear",
|
||||
"mipfilter": "none",
|
||||
"anisotropy": 0
|
||||
},
|
||||
"ver": "1.0.22",
|
||||
"imported": true,
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {}
|
||||
},
|
||||
"f9941": {
|
||||
"importer": "sprite-frame",
|
||||
"uuid": "d46acd4d-66e2-423b-8015-334ff99dd9f1@f9941",
|
||||
"displayName": "test",
|
||||
"id": "f9941",
|
||||
"name": "spriteFrame",
|
||||
"userData": {
|
||||
"trimThreshold": 1,
|
||||
"rotated": false,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"trimX": 0,
|
||||
"trimY": 0,
|
||||
"width": 536,
|
||||
"height": 548,
|
||||
"rawWidth": 536,
|
||||
"rawHeight": 548,
|
||||
"borderTop": 0,
|
||||
"borderBottom": 0,
|
||||
"borderLeft": 0,
|
||||
"borderRight": 0,
|
||||
"packable": true,
|
||||
"pixelsToUnit": 100,
|
||||
"pivotX": 0.5,
|
||||
"pivotY": 0.5,
|
||||
"meshType": 0,
|
||||
"vertices": {
|
||||
"rawPosition": [
|
||||
-268,
|
||||
-274,
|
||||
0,
|
||||
268,
|
||||
-274,
|
||||
0,
|
||||
-268,
|
||||
274,
|
||||
0,
|
||||
268,
|
||||
274,
|
||||
0
|
||||
],
|
||||
"indexes": [
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
2,
|
||||
1,
|
||||
3
|
||||
],
|
||||
"uv": [
|
||||
0,
|
||||
548,
|
||||
536,
|
||||
548,
|
||||
0,
|
||||
0,
|
||||
536,
|
||||
0
|
||||
],
|
||||
"nuv": [
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"minPos": [
|
||||
-268,
|
||||
-274,
|
||||
0
|
||||
],
|
||||
"maxPos": [
|
||||
268,
|
||||
274,
|
||||
0
|
||||
]
|
||||
},
|
||||
"isUuid": true,
|
||||
"imageUuidOrDatabaseUri": "d46acd4d-66e2-423b-8015-334ff99dd9f1@6c48a",
|
||||
"atlasUuid": "",
|
||||
"trimType": "auto"
|
||||
},
|
||||
"ver": "1.0.12",
|
||||
"imported": true,
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {}
|
||||
}
|
||||
},
|
||||
"userData": {
|
||||
"type": "sprite-frame",
|
||||
"fixAlphaTransparencyArtifacts": false,
|
||||
"hasAlpha": false,
|
||||
"redirect": "d46acd4d-66e2-423b-8015-334ff99dd9f1@6c48a"
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ export type ProgressCallback = (progress: number, message: string) => void;
|
||||
|
||||
/**
|
||||
* 关卡数据管理器
|
||||
* 单例模式,负责从 API 获取关卡数据并预加载图片
|
||||
* 单例模式,负责从 API 获取关卡数据并按需加载图片
|
||||
*/
|
||||
export class LevelDataManager {
|
||||
private static _instance: LevelDataManager | null = null;
|
||||
@@ -22,8 +22,11 @@ export class LevelDataManager {
|
||||
/** 请求超时时间(毫秒) */
|
||||
private readonly REQUEST_TIMEOUT = 8000;
|
||||
|
||||
/** 运行时关卡配置缓存 */
|
||||
private _levelConfigs: RuntimeLevelConfig[] = [];
|
||||
/** API 返回的原始关卡数据 */
|
||||
private _apiData: ApiLevelData[] = [];
|
||||
|
||||
/** 运行时关卡配置缓存(按需填充) */
|
||||
private _levelConfigs: Map<number, RuntimeLevelConfig> = new Map();
|
||||
|
||||
/** 是否已成功从 API 获取数据 */
|
||||
private _hasApiData: boolean = false;
|
||||
@@ -31,6 +34,9 @@ export class LevelDataManager {
|
||||
/** 图片缓存:URL -> SpriteFrame */
|
||||
private _imageCache: Map<string, SpriteFrame> = new Map();
|
||||
|
||||
/** 正在加载中的关卡索引集合 */
|
||||
private _loadingLevels: Set<number> = new Set();
|
||||
|
||||
/**
|
||||
* 获取单例实例
|
||||
*/
|
||||
@@ -47,7 +53,7 @@ export class LevelDataManager {
|
||||
private constructor() {}
|
||||
|
||||
/**
|
||||
* 初始化:从 API 获取数据并预加载图片
|
||||
* 初始化:从 API 获取数据并预加载第一关图片
|
||||
* @param onProgress 进度回调
|
||||
* @returns 是否初始化成功
|
||||
*/
|
||||
@@ -55,34 +61,37 @@ export class LevelDataManager {
|
||||
console.log('[LevelDataManager] 开始初始化');
|
||||
|
||||
try {
|
||||
// 阶段1: 获取 API 数据 (0-20%)
|
||||
// 阶段1: 获取 API 数据 (0-30%)
|
||||
onProgress?.(0, '正在获取关卡数据...');
|
||||
const apiData = await this._fetchApiData();
|
||||
|
||||
if (!apiData || apiData.length === 0) {
|
||||
console.warn('[LevelDataManager] API 返回空数据');
|
||||
onProgress?.(0.2, 'API 数据为空,使用本地配置');
|
||||
onProgress?.(0.3, 'API 数据为空,使用本地配置');
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`[LevelDataManager] 获取到 ${apiData.length} 个关卡数据`);
|
||||
onProgress?.(0.2, `获取到 ${apiData.length} 个关卡`);
|
||||
|
||||
// 阶段2: 预加载所有图片 (20-80%)
|
||||
const configs = await this._preloadImages(apiData, (progress) => {
|
||||
onProgress?.(0.2 + progress * 0.6, '正在加载关卡资源...');
|
||||
});
|
||||
|
||||
this._levelConfigs = configs;
|
||||
this._apiData = apiData;
|
||||
this._hasApiData = true;
|
||||
onProgress?.(0.3, `获取到 ${apiData.length} 个关卡`);
|
||||
|
||||
console.log('[LevelDataManager] 初始化完成');
|
||||
onProgress?.(0.8, '关卡资源加载完成');
|
||||
// 阶段2: 只预加载第一关图片 (30-80%)
|
||||
const firstLevel = apiData[0];
|
||||
onProgress?.(0.3, '正在加载第一关资源...');
|
||||
|
||||
const spriteFrame = await this._loadImage(firstLevel.imageUrl);
|
||||
if (spriteFrame) {
|
||||
this._levelConfigs.set(0, this._createRuntimeConfig(firstLevel, spriteFrame));
|
||||
}
|
||||
|
||||
console.log('[LevelDataManager] 初始化完成,第一关资源已加载');
|
||||
onProgress?.(0.8, '第一关资源加载完成');
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[LevelDataManager] 初始化失败:', error);
|
||||
onProgress?.(0.2, '获取数据失败,使用本地配置');
|
||||
onProgress?.(0.3, '获取数据失败,使用本地配置');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -92,24 +101,107 @@ export class LevelDataManager {
|
||||
* @param index 关卡索引
|
||||
*/
|
||||
getLevelConfig(index: number): RuntimeLevelConfig | null {
|
||||
if (index < 0 || index >= this._levelConfigs.length) {
|
||||
return null;
|
||||
}
|
||||
return this._levelConfigs[index];
|
||||
return this._levelConfigs.get(index) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取关卡总数
|
||||
*/
|
||||
getLevelCount(): number {
|
||||
return this._levelConfigs.length;
|
||||
return this._apiData.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有 API 数据
|
||||
*/
|
||||
hasApiData(): boolean {
|
||||
return this._hasApiData && this._levelConfigs.length > 0;
|
||||
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 spriteFrame = await this._loadImage(data.imageUrl);
|
||||
this._loadingLevels.delete(index);
|
||||
|
||||
if (!spriteFrame) {
|
||||
console.error(`[LevelDataManager] 加载关卡 ${index} 图片失败`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const config = this._createRuntimeConfig(data, spriteFrame);
|
||||
this._levelConfigs.set(index, config);
|
||||
console.log(`[LevelDataManager] 关卡 ${index} 资源加载完成`);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 预加载下一关图片(静默加载,不阻塞)
|
||||
* 在进入当前关卡后调用,提前加载下一关资源
|
||||
* @param currentIndex 当前关卡索引
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
// 检查是否正在加载
|
||||
if (this._loadingLevels.has(nextIndex)) {
|
||||
console.log(`[LevelDataManager] 下一关 ${nextIndex} 正在加载中`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 异步加载,不等待
|
||||
console.log(`[LevelDataManager] 开始预加载下一关 ${nextIndex}...`);
|
||||
this.ensureLevelReady(nextIndex).catch(err => {
|
||||
console.error(`[LevelDataManager] 预加载下一关失败:`, err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,22 +224,12 @@ export class LevelDataManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 预加载所有图片
|
||||
* @param apiData API 返回的关卡数据
|
||||
* @param onProgress 进度回调
|
||||
* 创建运行时关卡配置
|
||||
* @param data API 关卡数据
|
||||
* @param spriteFrame 已加载的精灵帧
|
||||
*/
|
||||
private async _preloadImages(
|
||||
apiData: ApiLevelData[],
|
||||
onProgress?: (progress: number) => void
|
||||
): Promise<RuntimeLevelConfig[]> {
|
||||
const configs: RuntimeLevelConfig[] = [];
|
||||
const total = apiData.length;
|
||||
|
||||
for (let i = 0; i < total; i++) {
|
||||
const data = apiData[i];
|
||||
const spriteFrame = await this._loadImage(data.imageUrl);
|
||||
|
||||
configs.push({
|
||||
private _createRuntimeConfig(data: ApiLevelData, spriteFrame: SpriteFrame | null): RuntimeLevelConfig {
|
||||
return {
|
||||
id: data.id,
|
||||
name: `第${data.level}关`,
|
||||
spriteFrame: spriteFrame,
|
||||
@@ -155,12 +237,7 @@ export class LevelDataManager {
|
||||
clue2: data.hint2,
|
||||
clue3: data.hint3,
|
||||
answer: data.answer
|
||||
});
|
||||
|
||||
onProgress?.((i + 1) / total);
|
||||
}
|
||||
|
||||
return configs;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -169,8 +246,9 @@ export class LevelDataManager {
|
||||
*/
|
||||
private async _loadImage(url: string): Promise<SpriteFrame | null> {
|
||||
// 检查缓存
|
||||
if (this._imageCache.has(url)) {
|
||||
return this._imageCache.get(url)!;
|
||||
const cached = this._imageCache.get(url);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
@@ -199,7 +277,9 @@ export class LevelDataManager {
|
||||
* 清除缓存
|
||||
*/
|
||||
clearCache(): void {
|
||||
this._levelConfigs = [];
|
||||
this._apiData = [];
|
||||
this._levelConfigs.clear();
|
||||
this._loadingLevels.clear();
|
||||
this._hasApiData = false;
|
||||
this._imageCache.clear();
|
||||
console.log('[LevelDataManager] 缓存已清除');
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import { sys } from 'cc';
|
||||
|
||||
/**
|
||||
* 用户进度数据结构
|
||||
*/
|
||||
interface UserProgress {
|
||||
/** 当前关卡索引(0-based) */
|
||||
currentLevelIndex: number;
|
||||
/** 已通关的最高关卡索引 */
|
||||
maxUnlockedLevelIndex: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 本地存储管理器
|
||||
* 统一管理用户数据的本地持久化存储
|
||||
@@ -8,12 +18,24 @@ export class StorageManager {
|
||||
/** 生命值存储键 */
|
||||
private static readonly KEY_LIVES = 'game_lives';
|
||||
|
||||
/** 用户进度存储键 */
|
||||
private static readonly KEY_PROGRESS = 'game_progress';
|
||||
|
||||
/** 默认生命值 */
|
||||
private static readonly DEFAULT_LIVES = 10;
|
||||
|
||||
/** 最小生命值 */
|
||||
private static readonly MIN_LIVES = 0;
|
||||
|
||||
/** 默认进度 */
|
||||
private static readonly DEFAULT_PROGRESS: UserProgress = {
|
||||
currentLevelIndex: 0,
|
||||
maxUnlockedLevelIndex: 0
|
||||
};
|
||||
|
||||
/** 进度缓存(避免重复读取 localStorage) */
|
||||
private static _progressCache: UserProgress | null = null;
|
||||
|
||||
// ==================== 生命值管理 ====================
|
||||
|
||||
/**
|
||||
@@ -84,4 +106,134 @@ export class StorageManager {
|
||||
StorageManager.setLives(StorageManager.DEFAULT_LIVES);
|
||||
console.log('[StorageManager] 生命值已重置为默认值');
|
||||
}
|
||||
|
||||
// ==================== 关卡进度管理 ====================
|
||||
|
||||
/**
|
||||
* 获取用户进度数据(带缓存)
|
||||
* @returns 用户进度对象的副本
|
||||
*/
|
||||
private static _getProgress(): UserProgress {
|
||||
// 返回缓存副本
|
||||
if (StorageManager._progressCache !== null) {
|
||||
return { ...StorageManager._progressCache };
|
||||
}
|
||||
|
||||
const stored = sys.localStorage.getItem(StorageManager.KEY_PROGRESS);
|
||||
if (stored === null || stored === '') {
|
||||
// 新用户,返回默认进度
|
||||
StorageManager._progressCache = { ...StorageManager.DEFAULT_PROGRESS };
|
||||
return { ...StorageManager._progressCache };
|
||||
}
|
||||
|
||||
try {
|
||||
const progress = JSON.parse(stored) as UserProgress;
|
||||
// 验证数据有效性
|
||||
if (typeof progress.currentLevelIndex !== 'number' ||
|
||||
typeof progress.maxUnlockedLevelIndex !== 'number' ||
|
||||
progress.currentLevelIndex < 0 ||
|
||||
progress.maxUnlockedLevelIndex < 0) {
|
||||
console.warn('[StorageManager] 进度数据无效,使用默认值');
|
||||
StorageManager._progressCache = { ...StorageManager.DEFAULT_PROGRESS };
|
||||
} else {
|
||||
StorageManager._progressCache = progress;
|
||||
}
|
||||
return { ...StorageManager._progressCache };
|
||||
} catch (e) {
|
||||
console.warn('[StorageManager] 解析进度数据失败,使用默认值');
|
||||
StorageManager._progressCache = { ...StorageManager.DEFAULT_PROGRESS };
|
||||
return { ...StorageManager._progressCache };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存用户进度数据
|
||||
* @param progress 进度对象
|
||||
*/
|
||||
private static _saveProgress(progress: UserProgress): void {
|
||||
StorageManager._progressCache = progress;
|
||||
sys.localStorage.setItem(StorageManager.KEY_PROGRESS, JSON.stringify(progress));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前关卡索引
|
||||
* @returns 当前关卡索引(0-based)
|
||||
*/
|
||||
static getCurrentLevelIndex(): number {
|
||||
return StorageManager._getProgress().currentLevelIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前关卡索引
|
||||
* @param index 关卡索引
|
||||
*/
|
||||
static setCurrentLevelIndex(index: number): void {
|
||||
if (index < 0) {
|
||||
console.warn('[StorageManager] 关卡索引不能为负数');
|
||||
return;
|
||||
}
|
||||
const progress = StorageManager._getProgress();
|
||||
progress.currentLevelIndex = index;
|
||||
StorageManager._saveProgress(progress);
|
||||
console.log(`[StorageManager] 当前关卡已更新: ${progress.currentLevelIndex}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已解锁的最高关卡索引
|
||||
* @returns 最高关卡索引(0-based)
|
||||
*/
|
||||
static getMaxUnlockedLevelIndex(): number {
|
||||
return StorageManager._getProgress().maxUnlockedLevelIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通关后更新进度
|
||||
* 当玩家通关第 N 关后,设置当前关卡为 N+1,解锁关卡更新为 max(N, 已解锁)
|
||||
* @param completedLevelIndex 刚通关的关卡索引
|
||||
*/
|
||||
static onLevelCompleted(completedLevelIndex: number): void {
|
||||
if (completedLevelIndex < 0) {
|
||||
console.warn('[StorageManager] 通关关卡索引不能为负数');
|
||||
return;
|
||||
}
|
||||
const progress = StorageManager._getProgress();
|
||||
const nextLevelIndex = completedLevelIndex + 1;
|
||||
|
||||
// 更新当前关卡为下一关
|
||||
progress.currentLevelIndex = nextLevelIndex;
|
||||
|
||||
// 更新最高解锁关卡
|
||||
progress.maxUnlockedLevelIndex = Math.max(progress.maxUnlockedLevelIndex, completedLevelIndex);
|
||||
|
||||
StorageManager._saveProgress(progress);
|
||||
console.log(`[StorageManager] 通关第 ${completedLevelIndex + 1} 关,下一关: ${nextLevelIndex + 1}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查指定关卡是否已解锁
|
||||
* @param levelIndex 关卡索引
|
||||
* @returns 是否已解锁
|
||||
*/
|
||||
static isLevelUnlocked(levelIndex: number): boolean {
|
||||
const progress = StorageManager._getProgress();
|
||||
return levelIndex <= progress.maxUnlockedLevelIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置所有进度
|
||||
*/
|
||||
static resetProgress(): void {
|
||||
StorageManager._progressCache = null;
|
||||
sys.localStorage.removeItem(StorageManager.KEY_PROGRESS);
|
||||
console.log('[StorageManager] 进度已重置');
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置所有数据(生命值 + 进度)
|
||||
*/
|
||||
static resetAll(): void {
|
||||
StorageManager.resetLives();
|
||||
StorageManager.resetProgress();
|
||||
console.log('[StorageManager] 所有数据已重置');
|
||||
}
|
||||
}
|
||||
|
||||
58
assets/scripts/utils/ToastManager.ts
Normal file
58
assets/scripts/utils/ToastManager.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Node, Prefab, instantiate, find } from 'cc';
|
||||
|
||||
/**
|
||||
* Toast 管理器
|
||||
* 单例模式,统一管理 Toast 提示的显示
|
||||
*/
|
||||
export class ToastManager {
|
||||
private static _instance: ToastManager | null = null;
|
||||
private _prefab: Prefab | null = null;
|
||||
private _container: Node | null = null;
|
||||
|
||||
static get instance(): ToastManager {
|
||||
if (!this._instance) {
|
||||
this._instance = new ToastManager();
|
||||
}
|
||||
return this._instance;
|
||||
}
|
||||
|
||||
private constructor() {}
|
||||
|
||||
/**
|
||||
* 初始化 Toast 管理器
|
||||
* @param prefab Toast 预制体
|
||||
* @param container Toast 容器节点(默认为 Canvas)
|
||||
*/
|
||||
init(prefab: Prefab, container?: Node): void {
|
||||
this._prefab = prefab;
|
||||
this._container = container ?? find('Canvas');
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示 Toast 提示
|
||||
* @param content 提示内容
|
||||
* @param duration 显示时长(毫秒),默认 2000ms
|
||||
*/
|
||||
show(content: string, duration: number = 2000): void {
|
||||
if (!this._prefab || !this._container) {
|
||||
console.error('[ToastManager] 未初始化,请先调用 init()');
|
||||
return;
|
||||
}
|
||||
|
||||
const node = instantiate(this._prefab);
|
||||
this._container.addChild(node);
|
||||
|
||||
// 动态获取 Toast 组件
|
||||
const toast = node.getComponent('Toast') as any;
|
||||
if (toast && typeof toast.show === 'function') {
|
||||
toast.show(content, duration);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 静态快捷方法
|
||||
*/
|
||||
static show(content: string, duration: number = 2000): void {
|
||||
ToastManager.instance.show(content, duration);
|
||||
}
|
||||
}
|
||||
9
assets/scripts/utils/ToastManager.ts.meta
Normal file
9
assets/scripts/utils/ToastManager.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "0ea3615a-071b-4938-81aa-be8615bd5322",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
Reference in New Issue
Block a user