3 Commits

Author SHA1 Message Date
panw
2a8ab3265d chore: 添加游戏数据同步脚本和配置的元数据文件
- 新增 GameDataSync.ts 的元数据文件,用于 TypeScript 脚本导入管理
- 新增 hearthstone-battlegrounds.md 的元数据文件,用于文本配置文件导入管理
2026-04-28 16:20:54 +08:00
panw
c078c929ce refactor: 移除未使用的引导和英雄添加方法
清理 SingletonModuleComp 中未实际使用的代码,包括 finishGuide、addHero 和 error 方法,以保持代码简洁并减少维护负担。
2026-04-28 16:20:11 +08:00
panw
9baddd5462 feat(数据同步): 重构云端数据同步机制,引入防抖与本地缓存
- 新增 GameDataSync 类,封装数据同步逻辑,支持防抖与时间戳冲突解决
- 重构 SingletonModuleComp 的云端同步方法,统一调用 GameDataSync
- 优化 TalentsComp 天赋升级流程,使用新的同步机制
- 添加本地缓存支持,提升离线体验与数据恢复能力
2026-04-28 16:15:48 +08:00
6 changed files with 203 additions and 91 deletions

View File

@@ -0,0 +1,143 @@
import { sys } from "cc";
import { WxCloudApi } from "../wx_clound_client_api/WxCloudApi";
import { mLogger } from "./Logger";
import { smc, GameDate, CloudData } from "./SingletonModuleComp";
export class GameDataSync {
private debugMode: boolean = false;
private _localDataDirty: boolean = false;
private _lastSyncTime: number = 0;
private _syncTimerId: any = null;
private readonly LOCAL_STORAGE_KEY = "Heros_GameData_Local";
/** 标记数据为脏,并更新时间戳,然后保存到本地 */
public markDataDirty() {
this._localDataDirty = true;
this.saveToLocal();
// 尝试触发异步同步
this.tryAsyncCloudSync();
}
/** 同步数据到本地 localStorage */
private saveToLocal() {
try {
const data = smc.getGameDate();
data.timestamp = Date.now(); // 更新时间戳
sys.localStorage.setItem(this.LOCAL_STORAGE_KEY, JSON.stringify(data));
} catch (error) {
mLogger.error(this.debugMode, 'GameDataSync', '保存本地数据失败:', error);
}
}
/** 从本地 localStorage 读取数据 */
private loadFromLocal(): GameDate | null {
try {
const str = sys.localStorage.getItem(this.LOCAL_STORAGE_KEY);
if (str) {
return JSON.parse(str) as GameDate;
}
} catch (error) {
mLogger.error(this.debugMode, 'GameDataSync', '读取本地数据失败:', error);
}
return null;
}
/**
* 判断是否为微信客户端
*/
public isWxClient(): boolean {
return sys.platform === sys.Platform.WECHAT_GAME;
}
public updateCloudData() {
this.markDataDirty();
return true;
}
/** 尝试异步同步云端数据带有防抖Debounce保护 */
private tryAsyncCloudSync() {
if (!this.isWxClient()) return;
// 如果当前有同步在等待,清除之前的定时器
if (this._syncTimerId !== null) {
clearTimeout(this._syncTimerId);
}
// 防抖:延迟 3 秒同步,期间多次操作合并为一次同步请求
this._syncTimerId = setTimeout(() => {
this._syncTimerId = null;
this.executeCloudSync();
}, 3000);
}
/** 实际执行云端同步 */
private executeCloudSync() {
if (!this._localDataDirty) return;
let gameData = smc.getGameDate();
// 保证云端存一份时间戳,供下次登录对比
gameData.timestamp = Date.now();
WxCloudApi.save(gameData).then((result) => {
if (result.result.code === 200) {
mLogger.log(this.debugMode, 'GameDataSync', "静默云端保存成功", result.result);
// 同步成功,清除脏标记
this._localDataDirty = false;
this._lastSyncTime = Date.now();
} else {
mLogger.warn(this.debugMode, 'GameDataSync', `[GameDataSync]: 静默同步失败(等待下次重试): ${result.result.msg}`);
// 失败了不清除脏标记,下次有变化或定时器检查时会再次重试
}
}).catch((error) => {
mLogger.error(this.debugMode, 'GameDataSync', `[GameDataSync]: 静默同步异常(等待下次重试):`, error);
});
}
public getCloudData() {
const localData = this.loadFromLocal();
// 未登录微信云端前,先用本地数据顶上(让玩家秒进游戏)
if (localData && !this.isWxClient()) {
smc.overrideLocalDataWithRemote({ data: localData });
return;
}
WxCloudApi.get().then(async (result) => {
if(result.result.code === 200) {
let cloudData = result.result.data as CloudData;
mLogger.log(this.debugMode, 'GameDataSync', `[GameDataSync]: 获取游戏云端数据成功:`, cloudData);
// 冲突解决:基于时间戳比较(云端时间戳 > 本地时间戳 则覆盖)
let cloudTs = cloudData?.data?.timestamp || 0;
let localTs = localData?.timestamp || 0;
if (!localData || cloudTs > localTs) {
mLogger.log(this.debugMode, 'GameDataSync', `[GameDataSync]: 云端数据更新 (Cloud: ${cloudTs} > Local: ${localTs}), 执行覆盖。`);
smc.overrideLocalDataWithRemote(cloudData);
this.saveToLocal(); // 同步到本地
} else {
mLogger.log(this.debugMode, 'GameDataSync', `[GameDataSync]: 本地数据更新 (Local: ${localTs} >= Cloud: ${cloudTs}), 使用本地数据,触发强制云同步。`);
smc.overrideLocalDataWithRemote({ data: localData });
// 本地数据较新,需要强制推送到云端以保证多端一致
this._localDataDirty = true;
this.executeCloudSync();
}
return true
} else {
mLogger.warn(this.debugMode, 'GameDataSync', `[GameDataSync]: 获取游戏云端数据失败,使用本地缓存兜底。`);
if (localData) {
smc.overrideLocalDataWithRemote({ data: localData });
}
return false
}
}).catch((error) => {
mLogger.error(this.debugMode, 'GameDataSync', `[GameDataSync]: 获取游戏云端数据异常:`, error);
if (localData) {
smc.overrideLocalDataWithRemote({ data: localData });
}
});
}
}
export const gameDataSync = new GameDataSync();

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "03eb3891-3801-415d-a889-e05a1f304e5a",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -1,3 +1,4 @@
import { sys } from "cc";
import { VM } from "../../../../extensions/oops-plugin-framework/assets/libs/model-view/ViewModel"; import { VM } from "../../../../extensions/oops-plugin-framework/assets/libs/model-view/ViewModel";
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS"; import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { Initialize } from "../initialize/Initialize"; import { Initialize } from "../initialize/Initialize";
@@ -8,15 +9,17 @@ import { GameEvent } from "./config/GameEvent";
import { GameScoreStats } from "./config/HeroAttrs"; import { GameScoreStats } from "./config/HeroAttrs";
import { mLogger } from "./Logger"; import { mLogger } from "./Logger";
import { TalentType } from "./config/TalentSet"; import { TalentType } from "./config/TalentSet";
import { gameDataSync } from "./GameDataSync";
/** /**
* 用远程数据覆盖本地数据(统一方法) * 用远程数据覆盖本地数据(统一方法)
* @param remoteData 远程数据(云端或本地调试) * @param remoteData 远程数据(云端或本地调试)
*/ */
interface GameDate{ export interface GameDate{
gold:number, gold:number,
heros:any, heros:any,
fight_hero:number, fight_hero:number,
timestamp?: number, // 用于比对本地与云端数据的最新状态
collection?: { collection?: {
talents: Partial<Record<TalentType, number>>, talents: Partial<Record<TalentType, number>>,
player_level: number, player_level: number,
@@ -26,7 +29,7 @@ interface GameDate{
friend?: {uuid:0,count:0}, friend?: {uuid:0,count:0},
} }
} }
interface CloudData { export interface CloudData {
openid: string; openid: string;
data: GameDate; data: GameDate;
} }
@@ -212,117 +215,74 @@ export class SingletonModuleComp extends ecs.Comp {
/** /**
* 判断是否为微信客户端 * 判断是否为微信客户端
*/ */
private isWxClient(): boolean { isWxClient(): boolean {
// 检查是否存在微信API return gameDataSync.isWxClient();
return typeof wx !== 'undefined' && typeof (wx as any).getSystemInfoSync === 'function';
}
finishGuide(index:number){
smc.guides[index]=1
//存储到远程服务器 后续再添加
} }
updateCloudData(){ updateCloudData(){
return gameDataSync.updateCloudData();
}
let gemeDate=this.getGameDate()
WxCloudApi.save(gemeDate).then((result) => {
mLogger.log(this.debugMode, 'SMC', '云端保存')
if(result.result.code === 200) {
mLogger.log(this.debugMode, 'SMC', "保存成功",result.result)
return true
} else {
mLogger.warn(this.debugMode, 'SMC', `[SMC]: 游戏数据增加失败: ${result.result.msg}`);
return false
}
}).catch((error) => {
mLogger.error(this.debugMode, 'SMC', `[SMC]: 增加游戏数据异常:`, error);
return false
});
return true
}
getCloudData(){ getCloudData(){
WxCloudApi.get().then(async (result) => { gameDataSync.getCloudData();
if(result.result.code === 200) {
let data=result.result.data
mLogger.log(this.debugMode, 'SMC', `[SMC]: 获取游戏数据成功:`, result.result);
this.overrideLocalDataWithRemote(data)
return true
} else {
mLogger.warn(this.debugMode, 'SMC', `[SMC]: 游戏数据增加失败`);
return false
}
}).catch((error) => {
mLogger.error(this.debugMode, 'SMC', `[SMC]: 获取游戏数据异常:`, error);
});
} }
public async overrideLocalDataWithRemote(CloudData) { public async overrideLocalDataWithRemote(cloudData: any) {
try { try {
// 直接覆盖基础游戏数据 // 直接覆盖基础游戏数据
if (CloudData.openid) { if (cloudData.openid) {
this.openid=CloudData.openid this.openid = cloudData.openid;
} }
// 直接覆盖出战英雄配置 // 直接覆盖出战英雄配置
if (CloudData.data) { if (cloudData.data) {
if(CloudData.data.gold) this.vmdata.gold=CloudData.data.gold const data = cloudData.data;
if(CloudData.data.heros) this.heros=CloudData.data.heros // 同步金币
if(CloudData.data.fight_hero) this.fight_hero=CloudData.data.fight_hero if (data.gold !== undefined) {
this.vmdata.gold = data.gold;
}
// 同步英雄
if (data.heros && Array.isArray(data.heros)) {
this.heros = data.heros;
}
// 同步出战英雄
if (data.fight_hero !== undefined) {
this.fight_hero = data.fight_hero;
}
// 恢复收集记录 // 恢复收集记录
if(CloudData.data.collection) { if (data.collection) {
this.collection = CloudData.data.collection; const remoteCol = data.collection;
if (remoteCol.talents) this.collection.talents = remoteCol.talents;
if (typeof remoteCol.player_level === 'number') this.collection.player_level = remoteCol.player_level;
if (typeof remoteCol.player_exp === 'number') this.collection.player_exp = remoteCol.player_exp;
if (typeof remoteCol.talent_points === 'number') this.collection.talent_points = remoteCol.talent_points;
} }
} }
// 触发UI更新
oops.message.dispatchEvent(GameEvent.GOLD_UPDATE);
} catch (error) { } catch (error) {
mLogger.error(this.debugMode, 'SMC', `[SMC]: 数据覆盖失败:`, error); mLogger.error(this.debugMode, 'SMC', `[SMC]: 数据覆盖失败:`, error);
} }
} }
getGameDate(){ getGameDate(){
return { let data: GameDate = {
gold:this.vmdata.gold, gold: this.vmdata.gold,
heros:this.heros, heros: this.heros,
fight_hero:this.fight_hero, fight_hero: this.fight_hero,
collection: this.collection collection: this.collection,
} timestamp: Date.now() // 每次获取当前数据结构时都附带最新的时间戳
} };
addHero(hero_uuid:number){ return data;
if(this.heros.indexOf(hero_uuid)==-1){
this.heros.push(hero_uuid)
}
if(this.isWxClient()){
let res = this.updateCloudData()
if (res){
return true
}else{
// 同步不成功删除uuid
this.heros.splice(this.heros.indexOf(hero_uuid), 1);
oops.gui.toast("数据同步失败,已回滚操作");
return false
}
}
return true
} }
updateGold(gold:number, is_sync: boolean = true){ updateGold(gold:number, is_sync: boolean = true){
this.vmdata.gold += gold; this.vmdata.gold += gold;
if(this.isWxClient() && is_sync){ if (is_sync) {
let res = this.updateCloudData() gameDataSync.markDataDirty();
if (res){
oops.message.dispatchEvent(GameEvent.GOLD_UPDATE)
return true
}else{
this.vmdata.gold -= gold
return false
}
} }
oops.message.dispatchEvent(GameEvent.GOLD_UPDATE) oops.message.dispatchEvent(GameEvent.GOLD_UPDATE)
return true return true
} }
error(){
oops.gui.toast("数据处理异常,请重试或重新登录")
}
} }
export var smc: SingletonModuleComp = ecs.getSingleton(SingletonModuleComp); export var smc: SingletonModuleComp = ecs.getSingleton(SingletonModuleComp);

View File

@@ -2,7 +2,7 @@
"ver": "1.0.1", "ver": "1.0.1",
"importer": "text", "importer": "text",
"imported": true, "imported": true,
"uuid": "fc76e3b0-5134-401a-b3ea-03251f9135ec", "uuid": "83563f4f-dcd3-4a6f-a2d8-735a643f593e",
"files": [ "files": [
".json" ".json"
], ],

View File

@@ -202,15 +202,15 @@ export class TalentsComp extends CCComp {
let points = collection.talent_points || 0; let points = collection.talent_points || 0;
if (points >= cost && currentLevel < 5) { if (points >= cost && currentLevel < 5) {
// 扣除天赋点 // 1. 扣除消耗
collection.talent_points -= cost; collection.talent_points -= cost;
// 增加天赋等级 // 2. 更新等级
collection.talents[talentId] = currentLevel + 1; collection.talents[talentId] = currentLevel + 1;
// 同步到云端 // 3. 同步数据(通过 SingletonModuleComp 新增的机制,这里会触发标记脏数据并自动尝试云端同步)
smc.updateCloudData(); smc.updateCloudData();
// 刷新界面 // 4. 刷新 UI
this.refreshUI(); this.refreshUI();
oops.gui.toast("天赋升级成功"); oops.gui.toast("天赋升级成功");