feat: 进入关卡时 toast 提示体力消耗,修复 StorageManager 接口位置和 WxSDK 访问级别
- 进入关卡成功后显示 toast 提示消耗体力及剩余体力 - 将 StorageManager 中 UserInfo 接口移至模块顶层,修复嵌套接口语法问题 - WxSDK.getWx() 改为 static 公开方法,便于外部调用
This commit is contained in:
@@ -2,13 +2,14 @@ import { _decorator, Node, EditBox, instantiate, Vec3, Button, Label, Sprite, Sp
|
||||
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 { UserAssetsManager } from 'db://assets/scripts/utils/UserAssetsManager';
|
||||
import { StaminaManager } from 'db://assets/scripts/utils/StaminaManager';
|
||||
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 { ShareManager } from 'db://assets/scripts/utils/ShareManager';
|
||||
import { PassModal } from 'db://assets/prefabs/PassModal';
|
||||
import { StaminaInfo } from 'db://assets/scripts/types/ApiTypes';
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/**
|
||||
@@ -60,7 +61,7 @@ export class PageLevel extends BaseView {
|
||||
@property(Label)
|
||||
clockLabel: Label | null = null;
|
||||
|
||||
/** 积分显示标签(prefab 中序列化名为 liveLabel,保持兼容) */
|
||||
/** 体力值显示标签(prefab 中序列化名为 liveLabel,保持兼容) */
|
||||
@property(Label)
|
||||
liveLabel: Label | null = null;
|
||||
|
||||
@@ -99,7 +100,7 @@ export class PageLevel extends BaseView {
|
||||
/** 是否正在切换关卡(防止重复提交) */
|
||||
private _isTransitioning: boolean = false;
|
||||
|
||||
/** 是否正在解锁提示(防止双击重复消耗积分) */
|
||||
/** 是否正在解锁提示(防止双击重复触发) */
|
||||
private _isUnlocking: boolean = false;
|
||||
|
||||
/** 通关弹窗实例 */
|
||||
@@ -108,6 +109,9 @@ export class PageLevel extends BaseView {
|
||||
/** 是否处于分享挑战模式 */
|
||||
private _isShareMode: boolean = false;
|
||||
|
||||
/** 体力恢复倒计时定时器 */
|
||||
private _staminaTimerId: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
/**
|
||||
* 页面首次加载时调用
|
||||
*/
|
||||
@@ -125,16 +129,14 @@ export class PageLevel extends BaseView {
|
||||
this.currentLevelIndex = StorageManager.getCurrentLevelIndex();
|
||||
console.log(`[PageLevel] 恢复关卡进度: 第 ${this.currentLevelIndex + 1} 关`);
|
||||
}
|
||||
this.updatePointsLabel();
|
||||
this.updateStaminaLabel();
|
||||
this.initIconSetting();
|
||||
this.initUnlockButtons();
|
||||
this.initSubmitButton();
|
||||
|
||||
// 异步加载关卡资源,完成后启动倒计时
|
||||
this.initLevel().then(() => {
|
||||
this.startCountdown();
|
||||
}).catch(err => {
|
||||
console.error('[PageLevel] 加载关卡失败:', err);
|
||||
// 异步加载关卡资源并调用进入关卡接口,完成后启动倒计时
|
||||
this._enterAndInitLevel().catch(err => {
|
||||
console.error('[PageLevel] 进入关卡失败:', err);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -143,7 +145,8 @@ export class PageLevel extends BaseView {
|
||||
*/
|
||||
onViewShow(): void {
|
||||
console.log('[PageLevel] onViewShow');
|
||||
this.updatePointsLabel();
|
||||
this.updateStaminaLabel();
|
||||
this._startStaminaRecoverTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,6 +154,7 @@ export class PageLevel extends BaseView {
|
||||
*/
|
||||
onViewHide(): void {
|
||||
console.log('[PageLevel] onViewHide');
|
||||
this._stopStaminaRecoverTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -161,6 +165,7 @@ export class PageLevel extends BaseView {
|
||||
this.clearInputNodes();
|
||||
this.stopCountdown();
|
||||
this._closePassModal();
|
||||
this._stopStaminaRecoverTimer();
|
||||
|
||||
// 清理事件监听
|
||||
this.iconSetting?.off(Node.EventType.TOUCH_END, this.onIconSettingClick, this);
|
||||
@@ -170,16 +175,18 @@ export class PageLevel extends BaseView {
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化关卡(从 API 数据加载,异步确保资源就绪)
|
||||
* 进入关卡并初始化
|
||||
* 1. 加载关卡图片资源
|
||||
* 2. 调用进入关卡接口(消耗体力,获取答案和线索)
|
||||
* 3. 启动倒计时
|
||||
*/
|
||||
private async initLevel(): Promise<void> {
|
||||
private async _enterAndInitLevel(): Promise<void> {
|
||||
// 先加载关卡图片资源
|
||||
let config: RuntimeLevelConfig | null = null;
|
||||
|
||||
if (this._isShareMode) {
|
||||
// 分享模式:从 ShareManager 获取关卡
|
||||
config = await ShareManager.instance.ensureShareLevelReady(this.currentLevelIndex);
|
||||
} else {
|
||||
// 正常模式:先尝试缓存,再异步加载
|
||||
config = LevelDataManager.instance.getLevelConfig(this.currentLevelIndex);
|
||||
if (!config) {
|
||||
console.log(`[PageLevel] 关卡 ${this.currentLevelIndex + 1} 资源未缓存,开始加载...`);
|
||||
@@ -192,8 +199,53 @@ export class PageLevel extends BaseView {
|
||||
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;
|
||||
}
|
||||
|
||||
// 提示用户消耗体力
|
||||
ToastManager.show(`消耗1点体力,剩余 ${enterData.stamina.current}/${enterData.stamina.max}`);
|
||||
|
||||
// 用 enter 接口返回的数据更新关卡配置(填充答案和线索)
|
||||
LevelDataManager.instance.updateLevelDetails(
|
||||
this.currentLevelIndex,
|
||||
{
|
||||
answer: enterData.answer,
|
||||
hint1: enterData.hint1,
|
||||
hint2: enterData.hint2,
|
||||
hint3: enterData.hint3,
|
||||
}
|
||||
);
|
||||
|
||||
// 重新获取更新后的配置
|
||||
config = LevelDataManager.instance.getLevelConfig(this.currentLevelIndex);
|
||||
if (!config) {
|
||||
console.error('[PageLevel] 更新关卡详情后获取配置失败');
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新体力显示
|
||||
this.updateStaminaLabel();
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[PageLevel] 初始化关卡 ${this.currentLevelIndex + 1}: ${config.name}`);
|
||||
this._applyLevelConfig(config);
|
||||
this.startCountdown();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -212,8 +264,10 @@ export class PageLevel extends BaseView {
|
||||
// 设置主图
|
||||
this.setMainImage(config.spriteFrame);
|
||||
|
||||
// 设置线索1(默认解锁)
|
||||
this.setClue(1, config.clue1);
|
||||
// 设置线索1(默认解锁,如果有的话)
|
||||
if (config.clue1) {
|
||||
this.setClue(1, config.clue1);
|
||||
}
|
||||
|
||||
// 隐藏线索2、3
|
||||
this.hideClue(2);
|
||||
@@ -224,7 +278,9 @@ export class PageLevel extends BaseView {
|
||||
this.showUnlockButton(3);
|
||||
|
||||
// 根据答案长度创建单个输入框
|
||||
this.createSingleInput(config.answer.length);
|
||||
if (config.answer) {
|
||||
this.createSingleInput(config.answer.length);
|
||||
}
|
||||
|
||||
// 更新倒计时显示
|
||||
this.updateClockLabel();
|
||||
@@ -239,7 +295,7 @@ export class PageLevel extends BaseView {
|
||||
LevelDataManager.instance.preloadNextLevel(this.currentLevelIndex);
|
||||
}
|
||||
|
||||
console.log(`[PageLevel] 初始化关卡 ${this.currentLevelIndex + 1}, 答案长度: ${config.answer.length}`);
|
||||
console.log(`[PageLevel] 初始化关卡 ${this.currentLevelIndex + 1}, 答案长度: ${config.answer?.length ?? 0}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -313,6 +369,11 @@ export class PageLevel extends BaseView {
|
||||
private clearInputNodes(): void {
|
||||
for (const node of this._inputNodes) {
|
||||
if (node.isValid) {
|
||||
const editBox = node.getComponent(EditBox);
|
||||
if (editBox) {
|
||||
editBox.node.off(EditBox.EventType.TEXT_CHANGED, this.onInputTextChanged, this);
|
||||
editBox.node.off(EditBox.EventType.EDITING_DID_ENDED, this.onInputEditingEnded, this);
|
||||
}
|
||||
node.destroy();
|
||||
}
|
||||
}
|
||||
@@ -501,38 +562,38 @@ export class PageLevel extends BaseView {
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击解锁线索
|
||||
* 点击解锁线索(观看激励视频广告后解锁)
|
||||
*/
|
||||
private async onUnlockClue(index: number): Promise<void> {
|
||||
// 防止双击重复消耗
|
||||
// 防止双击重复触发
|
||||
if (this._isUnlocking) return;
|
||||
|
||||
if (!this.hasPoints()) {
|
||||
ToastManager.show('积分不足,无法解锁提示!');
|
||||
return;
|
||||
}
|
||||
|
||||
this._isUnlocking = true;
|
||||
|
||||
try {
|
||||
const levelId = this._currentConfig?.id;
|
||||
const success = await UserAssetsManager.instance.consumePoint(levelId, index);
|
||||
if (!success) {
|
||||
ToastManager.show('积分不足,无法解锁提示!');
|
||||
// 检查线索是否存在
|
||||
if (!this._currentConfig) return;
|
||||
|
||||
const clueContent = index === 2 ? this._currentConfig.clue2 : this._currentConfig.clue3;
|
||||
if (!clueContent) {
|
||||
ToastManager.show('该提示暂未配置');
|
||||
return;
|
||||
}
|
||||
|
||||
// 调用微信激励视频广告
|
||||
ToastManager.show('观看视频即可解锁提示');
|
||||
const adWatched = await WxSDK.showRewardedVideoAd();
|
||||
if (!adWatched) {
|
||||
ToastManager.show('需要看完视频才能解锁提示哦');
|
||||
return;
|
||||
}
|
||||
|
||||
this.updatePointsLabel();
|
||||
this.playClickSound();
|
||||
this.hideUnlockButton(index);
|
||||
this.showClue(index);
|
||||
this.setClue(index, clueContent);
|
||||
|
||||
if (this._currentConfig) {
|
||||
const clueContent = index === 2 ? this._currentConfig.clue2 : this._currentConfig.clue3;
|
||||
this.setClue(index, clueContent);
|
||||
}
|
||||
|
||||
console.log(`[PageLevel] 解锁线索${index}`);
|
||||
console.log(`[PageLevel] 通过观看广告解锁线索${index}`);
|
||||
} finally {
|
||||
this._isUnlocking = false;
|
||||
}
|
||||
@@ -639,17 +700,70 @@ export class PageLevel extends BaseView {
|
||||
// 可以在这里添加游戏结束逻辑
|
||||
}
|
||||
|
||||
// ========== 积分相关方法 ==========
|
||||
// ========== 体力值相关方法 ==========
|
||||
|
||||
private updatePointsLabel(): void {
|
||||
/** 上次显示的体力值,用于变更检测 */
|
||||
private _lastDisplayedStamina: number = -1;
|
||||
|
||||
/**
|
||||
* 更新体力值显示(仅值变化时更新 UI)
|
||||
*/
|
||||
private updateStaminaLabel(): void {
|
||||
if (this.liveLabel) {
|
||||
const points = StorageManager.getPoints();
|
||||
this.liveLabel.string = `x ${points}`;
|
||||
const stamina = StaminaManager.instance.getStamina();
|
||||
if (stamina.current !== this._lastDisplayedStamina) {
|
||||
this.liveLabel.string = `x ${stamina.current}`;
|
||||
this._lastDisplayedStamina = stamina.current;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private hasPoints(): boolean {
|
||||
return StorageManager.hasPoints();
|
||||
/**
|
||||
* 启动体力恢复倒计时 UI
|
||||
*/
|
||||
private _startStaminaRecoverTimer(): void {
|
||||
this._stopStaminaRecoverTimer();
|
||||
|
||||
const stamina = StaminaManager.instance.getStamina();
|
||||
if (!stamina.nextRecoverAt || stamina.current >= stamina.max) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetTime = new Date(stamina.nextRecoverAt).getTime();
|
||||
if (isNaN(targetTime)) return;
|
||||
|
||||
this._staminaTimerId = setInterval(() => {
|
||||
if (targetTime - Date.now() > 0) return;
|
||||
|
||||
// 恢复一点体力
|
||||
const currentStamina = StaminaManager.instance.getStamina();
|
||||
const newCurrent = Math.min(currentStamina.current + 1, currentStamina.max);
|
||||
const newStamina: StaminaInfo = {
|
||||
...currentStamina,
|
||||
current: newCurrent,
|
||||
nextRecoverAt: newCurrent < currentStamina.max
|
||||
? new Date(Date.now() + 10 * 60 * 1000).toISOString()
|
||||
: null,
|
||||
};
|
||||
StaminaManager.instance.updateStamina(newStamina);
|
||||
this.updateStaminaLabel();
|
||||
|
||||
this._stopStaminaRecoverTimer();
|
||||
|
||||
if (newCurrent < currentStamina.max) {
|
||||
this._startStaminaRecoverTimer();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止体力恢复倒计时
|
||||
*/
|
||||
private _stopStaminaRecoverTimer(): void {
|
||||
if (this._staminaTimerId !== null) {
|
||||
clearInterval(this._staminaTimerId);
|
||||
this._staminaTimerId = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 答案提交与关卡切换 ==========
|
||||
@@ -674,7 +788,7 @@ export class PageLevel extends BaseView {
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示成功提示
|
||||
* 显示成功提示并上报通关
|
||||
*/
|
||||
private async showSuccess(): Promise<void> {
|
||||
console.log('[PageLevel] 答案正确!');
|
||||
@@ -692,8 +806,13 @@ export class PageLevel extends BaseView {
|
||||
const timeSpent = 60 - this._countdown;
|
||||
|
||||
if (!this._isShareMode) {
|
||||
await UserAssetsManager.instance.earnPoint(levelId, timeSpent);
|
||||
this.updatePointsLabel();
|
||||
// 上报通关耗时
|
||||
const result = await StaminaManager.instance.completeLevel(levelId, timeSpent);
|
||||
if (result) {
|
||||
console.log(`[PageLevel] 通关上报成功,首次通关: ${result.firstClear}`);
|
||||
}
|
||||
// 标记关卡为已通关(本地缓存)
|
||||
LevelDataManager.instance.markLevelCompleted(this.currentLevelIndex);
|
||||
} else {
|
||||
// fire-and-forget: errors are logged inside reportLevelProgress
|
||||
void ShareManager.instance.reportLevelProgress(levelId, true, timeSpent);
|
||||
@@ -812,11 +931,8 @@ export class PageLevel extends BaseView {
|
||||
return;
|
||||
}
|
||||
|
||||
// 重置并加载下一关,重新开始倒计时
|
||||
await this.initLevel();
|
||||
this.startCountdown();
|
||||
// 重置并加载下一关(包含进入关卡接口调用)
|
||||
await this._enterAndInitLevel();
|
||||
console.log(`[PageLevel] 进入关卡 ${this.currentLevelIndex + 1}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ export class PassModal extends BaseView {
|
||||
@property(Node)
|
||||
shareButton: Node | null = null;
|
||||
|
||||
/** 提示Label(如 +1 生命) */
|
||||
/** 提示Label(如 恭喜通关) */
|
||||
@property(Label)
|
||||
tipLabel: Label | null = null;
|
||||
|
||||
|
||||
@@ -8,27 +8,38 @@ export const API_BASE = 'https://ilookai.cn/api/v1';
|
||||
|
||||
/** API 端点 */
|
||||
export const API_ENDPOINTS = {
|
||||
/** 微信登录 */
|
||||
WX_LOGIN: `${API_BASE}/auth/wx-login`,
|
||||
USER_ASSETS: `${API_BASE}/user/assets`,
|
||||
USER_ASSETS_CONSUME: `${API_BASE}/user/assets/consume`,
|
||||
USER_ASSETS_EARN: `${API_BASE}/user/assets/earn`,
|
||||
/** 用户资料(含实时体力) */
|
||||
USER_PROFILE: `${API_BASE}/user/profile`,
|
||||
/** 游戏数据(体力 + 通关进度) */
|
||||
USER_GAME_DATA: `${API_BASE}/user/game-data`,
|
||||
LEVELS: `${API_BASE}/wechat-game/levels`,
|
||||
/** 关卡列表 */
|
||||
LEVELS: `${API_BASE}/levels`,
|
||||
/** 游戏配置 */
|
||||
GAME_CONFIGS: `${API_BASE}/game-configs`,
|
||||
/** 分享相关 */
|
||||
SHARE_CREATE: `${API_BASE}/share`,
|
||||
SHARE_PROGRESS: `${API_BASE}/share/progress`,
|
||||
/** 用户信息 */
|
||||
USER_INFO: `${API_BASE}/user/info`,
|
||||
} as const;
|
||||
|
||||
/** 构建加入分享的 URL */
|
||||
export function getLevelEnterUrl(levelId: string): string {
|
||||
return `${API_BASE}/levels/${levelId}/enter`;
|
||||
}
|
||||
|
||||
export function getLevelCompleteUrl(levelId: string): string {
|
||||
return `${API_BASE}/levels/${levelId}/complete`;
|
||||
}
|
||||
|
||||
export function getShareJoinUrl(code: string): string {
|
||||
return `${API_BASE}/share/${code}/join`;
|
||||
}
|
||||
|
||||
/** 积分操作原因 */
|
||||
export const POINT_REASONS = {
|
||||
HINT_UNLOCK: 'hint_unlock',
|
||||
LEVEL_COMPLETE: 'level_complete',
|
||||
} as const;
|
||||
export function getGameConfigUrl(key: string): string {
|
||||
return `${API_BASE}/game-configs/${key}`;
|
||||
}
|
||||
|
||||
/** 请求超时时间(毫秒) */
|
||||
export const API_TIMEOUT = {
|
||||
|
||||
@@ -7,6 +7,17 @@ export interface ApiEnvelope<T> {
|
||||
success: boolean;
|
||||
data: T | null;
|
||||
message: string | null;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
/** 体力值信息 */
|
||||
export interface StaminaInfo {
|
||||
/** 当前体力值(已计算恢复) */
|
||||
current: number;
|
||||
/** 体力上限,固定为 5 */
|
||||
max: number;
|
||||
/** 下一点体力恢复的时间(ISO 8601),满体力时为 null */
|
||||
nextRecoverAt: string | null;
|
||||
}
|
||||
|
||||
/** 登录响应数据 */
|
||||
@@ -15,21 +26,64 @@ export interface WxLoginData {
|
||||
user: {
|
||||
id: string;
|
||||
nickname: string | null;
|
||||
points: number;
|
||||
stamina: number;
|
||||
};
|
||||
}
|
||||
|
||||
/** 积分响应数据 */
|
||||
export interface UserAssetsData {
|
||||
points: number;
|
||||
/** 用户资料响应数据 */
|
||||
export interface UserProfileData {
|
||||
id: string;
|
||||
nickname: string | null;
|
||||
stamina: StaminaInfo;
|
||||
}
|
||||
|
||||
/** 游戏数据响应(Loading 页面) */
|
||||
export interface GameData {
|
||||
user: { id: string; points: number };
|
||||
user: {
|
||||
id: string;
|
||||
stamina: StaminaInfo;
|
||||
};
|
||||
completedLevelIds: string[];
|
||||
}
|
||||
|
||||
/** 关卡列表项 */
|
||||
export interface LevelListItem {
|
||||
id: string;
|
||||
level: number;
|
||||
imageUrl: string;
|
||||
answer: string | null;
|
||||
hint1: string | null;
|
||||
hint2: string | null;
|
||||
hint3: string | null;
|
||||
completed: boolean;
|
||||
timeSpent: number | null;
|
||||
}
|
||||
|
||||
/** 关卡列表响应 */
|
||||
export interface LevelListData {
|
||||
levels: LevelListItem[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
/** 进入关卡响应 */
|
||||
export interface EnterLevelData {
|
||||
id: string;
|
||||
level: number;
|
||||
imageUrl: string;
|
||||
answer: string;
|
||||
hint1: string | null;
|
||||
hint2: string | null;
|
||||
hint3: string | null;
|
||||
stamina: StaminaInfo;
|
||||
}
|
||||
|
||||
/** 通关上报响应 */
|
||||
export interface CompleteLevelData {
|
||||
firstClear: boolean;
|
||||
levelId: string;
|
||||
timeSpent: number;
|
||||
}
|
||||
|
||||
/** 创建分享响应 */
|
||||
export interface CreateShareData {
|
||||
shareCode: string;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { SpriteFrame } from 'cc';
|
||||
|
||||
/**
|
||||
* API 返回的单个关卡数据结构
|
||||
* API 返回的单个关卡数据结构(关卡列表)
|
||||
*/
|
||||
export interface ApiLevelData {
|
||||
/** 关卡 ID (UUID) */
|
||||
@@ -10,20 +10,22 @@ export interface ApiLevelData {
|
||||
level: number;
|
||||
/** 主图 URL */
|
||||
imageUrl: string;
|
||||
/** 线索1 */
|
||||
hint1: string;
|
||||
/** 线索2 */
|
||||
hint2: string;
|
||||
/** 线索3 */
|
||||
hint3: string;
|
||||
/** 答案 */
|
||||
answer: string;
|
||||
/** 排序 */
|
||||
sortOrder: number;
|
||||
/** 线索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 响应结构
|
||||
* API 响应结构(关卡列表)
|
||||
*/
|
||||
export interface ApiResponse {
|
||||
/** 是否成功 */
|
||||
@@ -47,12 +49,14 @@ export interface RuntimeLevelConfig {
|
||||
name: string;
|
||||
/** 主图 SpriteFrame(可能为 null 如果加载失败) */
|
||||
spriteFrame: SpriteFrame | null;
|
||||
/** 线索1 */
|
||||
clue1: string;
|
||||
/** 线索2 */
|
||||
clue2: string;
|
||||
/** 线索3 */
|
||||
clue3: string;
|
||||
/** 答案 */
|
||||
answer: string;
|
||||
/** 线索1(未通关时为 null,进入关卡后由 enter 接口获取) */
|
||||
clue1: string | null;
|
||||
/** 线索2(未通关时为 null) */
|
||||
clue2: string | null;
|
||||
/** 线索3(未通关时为 null) */
|
||||
clue3: string | null;
|
||||
/** 答案(未通关时为 null,进入关卡后由 enter 接口获取) */
|
||||
answer: string | null;
|
||||
/** 是否已通关 */
|
||||
completed: boolean;
|
||||
}
|
||||
|
||||
@@ -15,8 +15,6 @@ export class AuthManager {
|
||||
private _isLoggedIn: boolean = false;
|
||||
/** 服务端返回的已完成关卡 ID(登录后暂存,等 LevelDataManager 就绪后同步) */
|
||||
private _completedLevelIds: string[] = [];
|
||||
/** 服务端返回的已完成关卡 ID(登录后暂存,等 LevelDataManager 就绪后同步) */
|
||||
private _completedLevelIds: string[] = [];
|
||||
|
||||
static get instance(): AuthManager {
|
||||
if (!this._instance) {
|
||||
@@ -88,12 +86,14 @@ export class AuthManager {
|
||||
|
||||
this._userId = user.id;
|
||||
this._isLoggedIn = true;
|
||||
StorageManager.setPoints(user.points);
|
||||
|
||||
// 获取通关进度
|
||||
await this.fetchCompletedLevels();
|
||||
// 登录响应中 stamina 是原始数值(不含实时恢复),先存储默认体力
|
||||
// 后续通过 game-data 接口获取完整 StaminaInfo
|
||||
console.log(`[AuthManager] 登录成功,用户: ${user.id},体力: ${user.stamina}`);
|
||||
|
||||
// 获取通关进度和完整体力信息
|
||||
await this.fetchGameData();
|
||||
|
||||
console.log(`[AuthManager] 登录成功,用户: ${user.id},积分: ${user.points}`);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('[AuthManager] 登录异常:', err);
|
||||
@@ -102,45 +102,42 @@ export class AuthManager {
|
||||
}
|
||||
|
||||
private async validateToken(): Promise<boolean> {
|
||||
try {
|
||||
const response = await HttpUtil.get<ApiEnvelope<GameData>>(
|
||||
API_ENDPOINTS.USER_GAME_DATA,
|
||||
API_TIMEOUT.SHORT
|
||||
);
|
||||
const gameData = await this._fetchGameData();
|
||||
if (!gameData) return false;
|
||||
|
||||
if (!response.success || !response.data) {
|
||||
return false;
|
||||
}
|
||||
this._userId = gameData.user.id;
|
||||
this._isLoggedIn = true;
|
||||
StorageManager.setStamina(gameData.user.stamina);
|
||||
this._completedLevelIds = gameData.completedLevelIds;
|
||||
|
||||
this._userId = response.data.user.id;
|
||||
this._isLoggedIn = true;
|
||||
StorageManager.setPoints(response.data.user.points);
|
||||
this._completedLevelIds = response.data.completedLevelIds;
|
||||
console.log(`[AuthManager] Token 验证成功,体力: ${gameData.user.stamina.current}/${gameData.user.stamina.max},已完成: ${this._completedLevelIds.length} 关`);
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log(`[AuthManager] Token 验证成功,积分: ${response.data.user.points},已完成: ${this._completedLevelIds.length} 关`);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
/**
|
||||
* 登录成功后获取游戏数据(体力 + 通关进度)
|
||||
*/
|
||||
private async fetchGameData(): Promise<void> {
|
||||
const gameData = await this._fetchGameData();
|
||||
if (gameData) {
|
||||
this._completedLevelIds = gameData.completedLevelIds;
|
||||
StorageManager.setStamina(gameData.user.stamina);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录成功后获取通关进度
|
||||
* 从服务端获取游戏数据(共用方法)
|
||||
*/
|
||||
private async fetchCompletedLevels(): Promise<void> {
|
||||
private async _fetchGameData(): Promise<GameData | null> {
|
||||
try {
|
||||
const response = await HttpUtil.get<ApiEnvelope<GameData>>(
|
||||
API_ENDPOINTS.USER_GAME_DATA,
|
||||
API_TIMEOUT.SHORT
|
||||
);
|
||||
|
||||
if (response.success && response.data) {
|
||||
this._completedLevelIds = response.data.completedLevelIds;
|
||||
// 同步最新积分
|
||||
StorageManager.setPoints(response.data.user.points);
|
||||
}
|
||||
return (response.success && response.data) ? response.data : null;
|
||||
} catch {
|
||||
console.warn('[AuthManager] 获取通关进度失败');
|
||||
console.warn('[AuthManager] 获取游戏数据失败');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,45 @@ export class LevelDataManager {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记指定关卡为已通关(本地缓存更新)
|
||||
* @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
|
||||
@@ -191,6 +230,27 @@ export class LevelDataManager {
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用 enter 接口返回的数据更新运行时关卡配置(填充答案和线索)
|
||||
*/
|
||||
updateLevelDetails(index: number, details: { answer: string; hint1: string | null; hint2: string | null; hint3: string | null }): void {
|
||||
const config = this._levelConfigs.get(index);
|
||||
if (!config) {
|
||||
console.warn(`[LevelDataManager] 关卡 ${index} 配置不存在,无法更新详情`);
|
||||
return;
|
||||
}
|
||||
|
||||
this._levelConfigs.set(index, {
|
||||
...config,
|
||||
answer: details.answer,
|
||||
clue1: details.hint1 ?? null,
|
||||
clue2: details.hint2 ?? null,
|
||||
clue3: details.hint3 ?? null,
|
||||
});
|
||||
|
||||
console.log(`[LevelDataManager] 关卡 ${index} 详情已更新`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预加载下一关图片(静默加载,不阻塞)
|
||||
* 在进入当前关卡后调用,提前加载下一关资源
|
||||
@@ -278,7 +338,8 @@ export class LevelDataManager {
|
||||
clue1: data.hint1,
|
||||
clue2: data.hint2,
|
||||
clue3: data.hint3,
|
||||
answer: data.answer
|
||||
answer: data.answer,
|
||||
completed: data.completed,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
110
assets/scripts/utils/StaminaManager.ts
Normal file
110
assets/scripts/utils/StaminaManager.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { HttpUtil } from './HttpUtil';
|
||||
import { StorageManager } from './StorageManager';
|
||||
import { AuthManager } from './AuthManager';
|
||||
import { API_TIMEOUT, getLevelEnterUrl, getLevelCompleteUrl } from '../config/ApiConfig';
|
||||
import { ApiEnvelope, StaminaInfo, EnterLevelData, CompleteLevelData } from '../types/ApiTypes';
|
||||
|
||||
/**
|
||||
* 体力值管理器
|
||||
* 单例模式,负责体力值的服务端同步、进入关卡和通关上报
|
||||
* 以服务端为准,本地 StorageManager 作为缓存
|
||||
*/
|
||||
export class StaminaManager {
|
||||
private static _instance: StaminaManager | null = null;
|
||||
|
||||
static get instance(): StaminaManager {
|
||||
if (!this._instance) {
|
||||
this._instance = new StaminaManager();
|
||||
}
|
||||
return this._instance;
|
||||
}
|
||||
|
||||
private constructor() {}
|
||||
|
||||
/**
|
||||
* 获取当前体力信息(从本地缓存)
|
||||
*/
|
||||
getStamina(): StaminaInfo {
|
||||
return StorageManager.getStamina();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新本地缓存的体力信息
|
||||
* @param stamina 服务端返回的体力信息
|
||||
*/
|
||||
updateStamina(stamina: StaminaInfo): void {
|
||||
StorageManager.setStamina(stamina);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查当前是否有足够的体力
|
||||
*/
|
||||
hasStamina(): boolean {
|
||||
return StorageManager.hasStamina();
|
||||
}
|
||||
|
||||
/**
|
||||
* 进入关卡
|
||||
* 消耗 1 点体力(未通关关卡),获取完整关卡详情(含答案和线索)
|
||||
* @param levelId 关卡 ID
|
||||
* @returns 关卡详情,失败时返回 null
|
||||
*/
|
||||
async enterLevel(levelId: string): Promise<EnterLevelData | null> {
|
||||
if (!AuthManager.instance.isLoggedIn) {
|
||||
console.warn('[StaminaManager] 未登录,无法进入关卡');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await HttpUtil.post<ApiEnvelope<EnterLevelData>>(
|
||||
getLevelEnterUrl(levelId),
|
||||
{},
|
||||
API_TIMEOUT.DEFAULT
|
||||
);
|
||||
|
||||
if (response.success && response.data) {
|
||||
StorageManager.setStamina(response.data.stamina);
|
||||
console.log(`[StaminaManager] 进入关卡 ${levelId},体力: ${response.data.stamina.current}/${response.data.stamina.max}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
console.warn('[StaminaManager] 进入关卡失败:', response.message);
|
||||
return null;
|
||||
} catch (err) {
|
||||
console.error('[StaminaManager] 进入关卡请求失败:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通关上报
|
||||
* @param levelId 关卡 ID
|
||||
* @param timeSpent 通关耗时(秒)
|
||||
* @returns 通关响应,失败时返回 null
|
||||
*/
|
||||
async completeLevel(levelId: string, timeSpent: number): Promise<CompleteLevelData | null> {
|
||||
if (!AuthManager.instance.isLoggedIn) {
|
||||
console.warn('[StaminaManager] 未登录,无法上报通关');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await HttpUtil.post<ApiEnvelope<CompleteLevelData>>(
|
||||
getLevelCompleteUrl(levelId),
|
||||
{ timeSpent },
|
||||
API_TIMEOUT.DEFAULT
|
||||
);
|
||||
|
||||
if (response.success && response.data) {
|
||||
console.log(`[StaminaManager] 通关上报成功: ${levelId}, 首次: ${response.data.firstClear}, 耗时: ${response.data.timeSpent}s`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
console.warn('[StaminaManager] 通关上报失败:', response.message);
|
||||
return null;
|
||||
} catch (err) {
|
||||
console.error('[StaminaManager] 通关上报请求失败:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "ddba71db-75ed-468d-ac99-b4632c0b2ae4",
|
||||
"uuid": "7fb84423-af68-468c-a17b-7bdd195eb815",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { sys } from 'cc';
|
||||
import { StaminaInfo } from '../types/ApiTypes';
|
||||
|
||||
/**
|
||||
* 用户进度数据结构
|
||||
@@ -10,13 +11,21 @@ interface UserProgress {
|
||||
maxUnlockedLevelIndex: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户信息结构
|
||||
*/
|
||||
interface UserInfo {
|
||||
avatarUrl: string;
|
||||
nickName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 本地存储管理器
|
||||
* 统一管理用户数据的本地持久化存储
|
||||
*/
|
||||
export class StorageManager {
|
||||
/** 积分存储键 */
|
||||
private static readonly KEY_POINTS = 'game_points';
|
||||
/** 体力值存储键 */
|
||||
private static readonly KEY_STAMINA = 'game_stamina';
|
||||
|
||||
/** 用户进度存储键 */
|
||||
private static readonly KEY_PROGRESS = 'game_progress';
|
||||
@@ -27,11 +36,12 @@ export class StorageManager {
|
||||
/** 用户信息存储键 */
|
||||
private static readonly KEY_USER_INFO = 'user_info';
|
||||
|
||||
/** 默认积分 */
|
||||
private static readonly DEFAULT_POINTS = 10;
|
||||
|
||||
/** 最小积分 */
|
||||
private static readonly MIN_POINTS = 0;
|
||||
/** 默认体力值 */
|
||||
private static readonly DEFAULT_STAMINA: StaminaInfo = {
|
||||
current: 5,
|
||||
max: 5,
|
||||
nextRecoverAt: null,
|
||||
};
|
||||
|
||||
/** 默认进度 */
|
||||
private static readonly DEFAULT_PROGRESS: UserProgress = {
|
||||
@@ -42,75 +52,52 @@ export class StorageManager {
|
||||
/** 进度缓存(避免重复读取 localStorage) */
|
||||
private static _progressCache: UserProgress | null = null;
|
||||
|
||||
// ==================== 积分管理 ====================
|
||||
/** 体力缓存(避免重复 JSON 解析) */
|
||||
private static _staminaCache: StaminaInfo | null = null;
|
||||
|
||||
// ==================== 体力值管理 ====================
|
||||
|
||||
/**
|
||||
* 获取当前积分
|
||||
* @returns 当前积分,新用户返回默认值 10
|
||||
* 获取当前体力信息(带内存缓存,避免重复 JSON 解析)
|
||||
*/
|
||||
static getPoints(): number {
|
||||
const stored = sys.localStorage.getItem(StorageManager.KEY_POINTS);
|
||||
static getStamina(): StaminaInfo {
|
||||
if (StorageManager._staminaCache) {
|
||||
return { ...StorageManager._staminaCache };
|
||||
}
|
||||
|
||||
const stored = sys.localStorage.getItem(StorageManager.KEY_STAMINA);
|
||||
if (stored === null || stored === '') {
|
||||
// 新用户,设置默认值
|
||||
StorageManager.setPoints(StorageManager.DEFAULT_POINTS);
|
||||
return StorageManager.DEFAULT_POINTS;
|
||||
StorageManager.setStamina(StorageManager.DEFAULT_STAMINA);
|
||||
return { ...StorageManager.DEFAULT_STAMINA };
|
||||
}
|
||||
const points = parseInt(stored, 10);
|
||||
// 防止异常数据
|
||||
if (isNaN(points) || points < 0) {
|
||||
StorageManager.setPoints(StorageManager.DEFAULT_POINTS);
|
||||
return StorageManager.DEFAULT_POINTS;
|
||||
try {
|
||||
const stamina = JSON.parse(stored) as StaminaInfo;
|
||||
if (typeof stamina.current !== 'number' || typeof stamina.max !== 'number') {
|
||||
StorageManager.setStamina(StorageManager.DEFAULT_STAMINA);
|
||||
return { ...StorageManager.DEFAULT_STAMINA };
|
||||
}
|
||||
StorageManager._staminaCache = stamina;
|
||||
return { ...stamina };
|
||||
} catch {
|
||||
StorageManager.setStamina(StorageManager.DEFAULT_STAMINA);
|
||||
return { ...StorageManager.DEFAULT_STAMINA };
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置积分
|
||||
* @param points 积分
|
||||
* 设置体力信息(同时更新缓存)
|
||||
*/
|
||||
static setPoints(points: number): void {
|
||||
const validPoints = Math.max(StorageManager.MIN_POINTS, points);
|
||||
sys.localStorage.setItem(StorageManager.KEY_POINTS, validPoints.toString());
|
||||
console.log(`[StorageManager] 积分已更新: ${validPoints}`);
|
||||
static setStamina(stamina: StaminaInfo): void {
|
||||
StorageManager._staminaCache = stamina;
|
||||
sys.localStorage.setItem(StorageManager.KEY_STAMINA, JSON.stringify(stamina));
|
||||
console.log(`[StorageManager] 体力已更新: ${stamina.current}/${stamina.max}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 消耗一个积分
|
||||
* @returns 是否消耗成功(积分不足时返回 false)
|
||||
* 检查是否有足够的体力
|
||||
*/
|
||||
static consumePoint(): boolean {
|
||||
const currentPoints = StorageManager.getPoints();
|
||||
if (currentPoints <= 0) {
|
||||
console.warn('[StorageManager] 积分不足,无法消耗');
|
||||
return false;
|
||||
}
|
||||
StorageManager.setPoints(currentPoints - 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加一个积分
|
||||
*/
|
||||
static addPoint(): void {
|
||||
const currentPoints = StorageManager.getPoints();
|
||||
StorageManager.setPoints(currentPoints + 1);
|
||||
console.log(`[StorageManager] 获得一个积分,当前积分: ${currentPoints + 1}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有足够的积分
|
||||
* @returns 是否有积分
|
||||
*/
|
||||
static hasPoints(): boolean {
|
||||
return StorageManager.getPoints() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置积分为默认值
|
||||
*/
|
||||
static resetPoints(): void {
|
||||
StorageManager.setPoints(StorageManager.DEFAULT_POINTS);
|
||||
console.log('[StorageManager] 积分已重置为默认值');
|
||||
static hasStamina(): boolean {
|
||||
return StorageManager.getStamina().current > 0;
|
||||
}
|
||||
|
||||
// ==================== 认证 Token 管理 ====================
|
||||
@@ -261,10 +248,10 @@ export class StorageManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置所有数据(积分 + 进度)
|
||||
* 重置所有数据(体力 + 进度)
|
||||
*/
|
||||
static resetAll(): void {
|
||||
StorageManager.resetPoints();
|
||||
StorageManager.setStamina(StorageManager.DEFAULT_STAMINA);
|
||||
StorageManager.resetProgress();
|
||||
StorageManager.clearToken();
|
||||
StorageManager.clearUserInfo();
|
||||
@@ -273,14 +260,6 @@ export class StorageManager {
|
||||
|
||||
// ==================== 用户信息管理 ====================
|
||||
|
||||
/**
|
||||
* 用户信息结构
|
||||
*/
|
||||
interface UserInfo {
|
||||
avatarUrl: string;
|
||||
nickName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存用户信息(头像、昵称)
|
||||
* @param userInfo 用户信息对象
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
import { HttpUtil } from './HttpUtil';
|
||||
import { StorageManager } from './StorageManager';
|
||||
import { AuthManager } from './AuthManager';
|
||||
import { API_ENDPOINTS, API_TIMEOUT, POINT_REASONS } from '../config/ApiConfig';
|
||||
import { ApiEnvelope, UserAssetsData } from '../types/ApiTypes';
|
||||
|
||||
/**
|
||||
* 用户资产管理器
|
||||
* 单例模式,负责积分的服务端同步
|
||||
* 以服务端为准,本地 StorageManager 作为缓存
|
||||
*/
|
||||
export class UserAssetsManager {
|
||||
private static _instance: UserAssetsManager | null = null;
|
||||
|
||||
static get instance(): UserAssetsManager {
|
||||
if (!this._instance) {
|
||||
this._instance = new UserAssetsManager();
|
||||
}
|
||||
return this._instance;
|
||||
}
|
||||
|
||||
private constructor() {}
|
||||
|
||||
/**
|
||||
* 从服务端获取最新积分
|
||||
*/
|
||||
async fetchPoints(): Promise<number> {
|
||||
if (!AuthManager.instance.isLoggedIn) {
|
||||
return StorageManager.getPoints();
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await HttpUtil.get<ApiEnvelope<UserAssetsData>>(
|
||||
API_ENDPOINTS.USER_ASSETS,
|
||||
API_TIMEOUT.SHORT
|
||||
);
|
||||
|
||||
if (response.success && response.data) {
|
||||
StorageManager.setPoints(response.data.points);
|
||||
return response.data.points;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[UserAssetsManager] 获取积分失败:', err);
|
||||
}
|
||||
|
||||
return StorageManager.getPoints();
|
||||
}
|
||||
|
||||
/**
|
||||
* 消耗积分(解锁提示)
|
||||
* @returns 是否消耗成功
|
||||
*/
|
||||
async consumePoint(levelId?: string, hintIndex?: number): Promise<boolean> {
|
||||
if (!StorageManager.hasPoints()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!AuthManager.instance.isLoggedIn) {
|
||||
return StorageManager.consumePoint();
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await HttpUtil.post<ApiEnvelope<UserAssetsData>>(
|
||||
API_ENDPOINTS.USER_ASSETS_CONSUME,
|
||||
{
|
||||
reason: POINT_REASONS.HINT_UNLOCK,
|
||||
levelId,
|
||||
hintIndex,
|
||||
},
|
||||
API_TIMEOUT.SHORT
|
||||
);
|
||||
|
||||
if (response.success && response.data) {
|
||||
StorageManager.setPoints(response.data.points);
|
||||
return true;
|
||||
} else {
|
||||
console.warn('[UserAssetsManager] 消耗积分失败:', response.message);
|
||||
return false;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[UserAssetsManager] 消耗积分请求失败,降级本地处理:', err);
|
||||
return StorageManager.consumePoint();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得积分(通关奖励)
|
||||
* @param levelId 关卡ID
|
||||
* @param timeSpent 通关耗时(秒)
|
||||
* @returns 获得后的积分数
|
||||
*/
|
||||
async earnPoint(levelId: string, timeSpent: number): Promise<number> {
|
||||
if (!AuthManager.instance.isLoggedIn) {
|
||||
StorageManager.addPoint();
|
||||
return StorageManager.getPoints();
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await HttpUtil.post<ApiEnvelope<UserAssetsData>>(
|
||||
API_ENDPOINTS.USER_ASSETS_EARN,
|
||||
{
|
||||
reason: POINT_REASONS.LEVEL_COMPLETE,
|
||||
levelId,
|
||||
timeSpent,
|
||||
},
|
||||
API_TIMEOUT.SHORT
|
||||
);
|
||||
|
||||
if (response.success && response.data) {
|
||||
StorageManager.setPoints(response.data.points);
|
||||
return response.data.points;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[UserAssetsManager] 获得积分请求失败,降级本地处理:', err);
|
||||
}
|
||||
|
||||
StorageManager.addPoint();
|
||||
return StorageManager.getPoints();
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ export class WxSDK {
|
||||
/**
|
||||
* 获取 wx 全局对象(仅微信环境下可用)
|
||||
*/
|
||||
private static getWx(): any {
|
||||
static getWx(): any {
|
||||
if (!WxSDK.isWechat()) return null;
|
||||
return typeof wx !== 'undefined' ? wx : null;
|
||||
}
|
||||
@@ -217,6 +217,84 @@ export class WxSDK {
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 激励视频广告 ====================
|
||||
|
||||
/** 激励视频广告实例(复用) */
|
||||
private static _rewardedVideoAd: any = null;
|
||||
|
||||
/**
|
||||
* 展示激励视频广告
|
||||
* 用户看完广告后返回 true,中途退出或失败返回 false
|
||||
* 非微信环境直接返回 true(开发模式直接通过)
|
||||
* @param adUnitId 广告单元 ID(默认使用项目配置的 ID)
|
||||
* @returns Promise<boolean> 是否看完广告
|
||||
*/
|
||||
static showRewardedVideoAd(adUnitId: string = ''): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const wxApi = WxSDK.getWx();
|
||||
if (!wxApi) {
|
||||
console.log('[WxSDK] 非微信环境,跳过激励视频广告');
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof wxApi.createRewardedVideoAd !== 'function') {
|
||||
console.warn('[WxSDK] 当前微信版本不支持激励视频广告');
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 复用或创建广告实例
|
||||
if (!WxSDK._rewardedVideoAd) {
|
||||
WxSDK._rewardedVideoAd = wxApi.createRewardedVideoAd({
|
||||
adUnitId: adUnitId,
|
||||
});
|
||||
}
|
||||
|
||||
const ad = WxSDK._rewardedVideoAd;
|
||||
|
||||
// 定义关闭回调(一次性)
|
||||
const onClose = (res: any) => {
|
||||
ad.offClose(onClose);
|
||||
if (res && res.isEnded) {
|
||||
console.log('[WxSDK] 激励视频广告观看完成');
|
||||
resolve(true);
|
||||
} else {
|
||||
console.log('[WxSDK] 激励视频广告中途退出');
|
||||
resolve(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 定义错误回调(一次性)
|
||||
const onError = (err: any) => {
|
||||
ad.offError(onError);
|
||||
ad.offClose(onClose);
|
||||
console.error('[WxSDK] 激励视频广告错误:', err);
|
||||
resolve(false);
|
||||
};
|
||||
|
||||
ad.onClose(onClose);
|
||||
ad.onError(onError);
|
||||
|
||||
// 先尝试 show,如果广告未加载则先 load
|
||||
ad.show().catch(() => {
|
||||
ad.load().then(() => ad.show()).catch((loadErr: any) => {
|
||||
ad.offClose(onClose);
|
||||
ad.offError(onError);
|
||||
console.error('[WxSDK] 激励视频广告加载失败:', loadErr);
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[WxSDK] 激励视频广告异常:', err);
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 启动参数 ====================
|
||||
|
||||
/**
|
||||
* 从启动参数中获取分享码
|
||||
* @returns 分享码,不存在则返回 null
|
||||
|
||||
Reference in New Issue
Block a user