import { Game, game, sys } from 'cc'; import { ConfigTableData, TableNames } from './ConfigTableData'; import { Singleton } from '../../mx/tools/Singleton'; import { ConfigProjectData as Project } from './ConfigProjectData'; import { DEBUG, EDITOR } from 'cc/env'; import { GEvent } from '../../mx/module/event/GEvent'; import MTools from '../../mx/tools/MTools'; import { sdkConfig } from '../sdk/sdkConfig'; import type { IItemSaveData } from './GameDataEnums'; import { ItemNumType, OtherDataType, StatisticsType, StatusType, TaskType } from './GameDataEnums'; import { DailyShopDataManager } from '../manager/DailyShopDataManager'; import { EmolumentDataManager } from '../manager/EmolumentDataManager'; import { DailyWinStats } from '../manager/DailyWinStats'; import { MatchWinStats } from '../manager/MatchWinStats'; export type { IItemSaveData } from './GameDataEnums'; export { ConstNumType, ItemNumType, OtherDataType, StatisticsType, StatusType, TaskType, HERO_DEFAULT_EQUIP_ID } from './GameDataEnums'; /** * 游戏数据 */ export class GameData extends Singleton { /**是否首次登录(新玩家注册) */ get IsFirstLogin() { return this._IsFirstLogin; } /**今天是否首次登录 */ get IsFirstLoginToday() { return this._IsFirstLoginToday; } set IsFirstLoginToday(isFirst) { this._IsFirstLoginToday = isFirst } /**登录天数 */ get LoginDays() { return this._LoginDays; } /**离线时长(单位:秒) */ get OfflineTime() { return this._OfflineTime; } /**本次登录在线时长(单位:秒) */ get OnlineTime() { return this._OnlineTime; } /**当日累计在线时长(单位:秒) */ get OnlineTimeForToDay() { return this._OnlineTimeForToDay; } /**所有累计在线时长(单位:秒) */ get OnlineTimeForAll() { return this._OnlineTimeForAll; } /**当前登录时的日期 */ get CurrentLoginDate() { return this._CurrentLoginDate; } private _IsFirstLogin = false; private _IsFirstLoginToday = false; private _LoginDays = 0; private _OfflineTime = 0; private _OnlineTime = 0; private _OnlineTimeForToDay = 0; private _OnlineTimeForAll = 0; private _docName = "doc"; /**当前登录时的日期 */ private _CurrentLoginDate = -1; /**项目配表数据 */ table: ConfigTableData = ConfigTableData.getInstance(); /**项目动态逻辑数据 */ project: Project = Project.getInstance(); /**初始化 */ init(docName, userData) { // userData = '{"key":"","uid":"","playerTitleLevel":0,"dataVersion":14,"chapter":2,"level":1,"itemCount":["4_24","2_50","1_200","3_10","21_6","27_3","25_1","26_1","29_1"],"status":["2_2"],"statistics":["3_1766805850400","5_300","6_300","8_1766834348327","21_200","25_3","22_10","26_0","303_6","304_6","305_0","308_2","309_8","310_0","331_1","318_1766834643292"],"guide":[],"other":["37_","29_1&1&10&2&0|2&1&10&2&1|3&1&0&0&0|4&1&10&2&2|5&1&10&2&3|6&1&10&2&4|7&1&10&2&5|8&1&0&0&0|9&1&10&2&6|10&1&0&0&0|11&1&10&2&7|12&1&0&0&0|13&1&0&0&0|14&1&0&0&0|15&1&0&0&0|16&1&0&0&0|17&1&0&0&0|18&1&0&0&0|19&1&0&0&0","33_0&0&1","34_0&10&0&0&0&0&0","43_0&0&0&0&0","44_0&0&0&0&0","45_0&1&1&0&0&0&0&0&0&0&6&0&0&0","46_0&0&0&0&0&0&0&0&0&0&0&0&0&0","30_1|1#2|1","35_100&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0"],"onlyLocal":[]}' if (userData && typeof userData === "object" && !Array.isArray(userData) && userData["key"] && userData["key"] != "") { this._docName = userData["key"]; } if (docName != "") { this._docName = docName; } // SDK 可能在 init 前写入 uid,重建 Doc 时保留,避免被 initDoc 清掉 const prevUid = String((this.doc as Doc | undefined)?.uid ?? '').trim(); this.doc = new Doc(); this.initDoc(); if (prevUid) this.doc.uid = prevUid; let LocalStr = sys.localStorage.getItem(this._docName); ////本地测试时,调取的存档字符串,记得发布时注释 //LocalStr = `{"key":"","uid":"","playerTitleLevel":0,"dataVersion":17996,"chapter":40,"level":1,"itemCount":["4_30","2_50","1_4944","3_3386","21_1387","25_1299","27_271","26_463","29_413","31_327","24_429","33_47","22_474","28_131","32_77","35_97","36_72","34_35","106_155","30_19","99_2","23_20","37_118","38_66","39_11","97_1"],"status":["2_2","5_0","4_1"],"statistics":["3_1768306060819","5_118","6_250284","8_1767995850105","21_1094834","25_106","22_27075","26_68","303_6","304_6","305_0","308_0","309_3","330_26","310_17","331_0","318_1767959448067","312_10","23_1089890","27_106","316_0","311_0","336_4","4_25","7_1768334223735","314_0","315_0","317_0","319_0","324_0","332_0","322_1","323_1","327_1","334_0","326_0","325_0","24_23689","28_67","306_21300","320_2","333_5","329_0","328_1","337_5","335_0"],"guide":[],"other":["37_","29_1&18&10&2&0|2&16&10&2&3|3&11&0&2&6|4&16&10&1&-1|5&18&10&1&-1|6&16&10&1&-1|7&28&10&1&-1|8&11&0&1&-1|9&16&10&1&-1|10&11&0&1&-1|11&16&10&1&-1|12&11&0&2&5|13&24&0&1&-1|14&11&0&2&1|15&11&0&2&7|16&11&0&2&4|17&10&0&1&-1|18&10&0&1&-1|19&12&0&2&2","33_2&0","34_280&530&930&4500&7&3&0","43_0&0&0&0&0","44_0&0&0&0&0","45_0&0&0&0&0&0&0&0&0&0&0&0&0&0","46_0&0&0&0&0&0&0&0&0&0&0&0&0&0","30_1|1#2|1#3|3#4|2#5|6#6|6#7|6#8|4#9|8#10|6#11|3#12|11#13|8#14|12#15|4#16|23#17|6#18|10#19|10#10021|6#10022|1#10023|1#10024|1#10025|22#10001|4#10011|270#10012|2#10013|10#20|13#21|10#22|33#10002|1#1001|1#2001|1#1002|1#2002|1#1003|1#2003|1#1004|1#2004|1#23|13#1005|1#24|22#25|24#26|16#27|24#64|1#59|1#65|1#66|1#69|1#41|2#33|9#45|1#30|31#32|83#28|5#29|4#31|37#34|37#35|14#36|14#37|8#38|13#39|36#40|67","35_100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#98&1&1&0#32&1&0&0#0&0&0&0#0&0&0&0#0&0&0&0#34&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#40&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#31&0&0&0#58&0&0&0#45&0&0&0#0&0&0&0#0&0&0&0#32&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0","40_100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#73&1&1&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0","41_100&1&1&1#100&1&1&1#100&1&1&1#100&1&1&1#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0#0&0&0&0","32_1|10|0&24|7|1&9|10|1&8|10|1&69|5|1&55|9|1","31_3|2#5|3#6|2#7|3#8|2#9|7#10|5#11|2#12|9#13|6#14|8#15|2#16|18#17|3#18|8#19|9#10025|9#10001|3#10011|231#10013|5#20|12#21|7#22|29#23|9#1005|1#24|19#25|19#26|13#27|23#64|1#59|1#65|1#66|1#69|1#41|2#45|1#30|29#32|71#28|4#29|3#31|33#33|7#34|36#35|13#36|13#37|7#38|12#39|34#40|66","36_5&2|16&2","38_,1001,1002,1003,1004","39_,2001,2002,2003,2004","50_wuqi|2#daoju|2#wujin|2#baoxiang|2"],"onlyLocal":[],"failCount":0,"lastFailCount":0}` let local: Record | null = null; if (LocalStr) { try { const parsed = JSON.parse(LocalStr); if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { local = parsed as Record; } } catch (parseErr) { console.error('[GameData.init] 本地存档 JSON 解析失败', parseErr); local = null; } } let myRankId = local ? Math.floor(Number(local["rankId"]) || 0) : 0; let myDataVersion = local ? Math.floor(Number(local["dataVersion"]) || 0) : 0; let sd = null if (userData != null && userData !== "") { if (typeof userData === "string") { try { sd = JSON.parse(userData); } catch (parseErr) { console.error("[GameData.init] 存档 JSON 解析失败", parseErr); sd = null; } } else if (typeof userData === "object") { sd = userData as any; } if (sd) { const serverRankId = Math.floor(Number(sd["rankId"]) || 0); const serverDataVersion = Math.floor(Number(sd["dataVersion"]) || 0); // 云端段位更低,或段位相同但云端更旧 → 用本地,避免回档 if (serverRankId < myRankId || (serverRankId === myRankId && serverDataVersion < myDataVersion)) { sd = null; gg.sdk.reportDY("inLevel", `存档异常-使用本地数据${myRankId}|${serverRankId}`); let str = typeof userData === "string" ? userData : JSON.stringify(userData); if (str.length > 350) { str = str.substring(0, 350); } gg.sdk.reportDY("inLevel", `存档异常-远端数据|${str}`); let str2 = LocalStr; if (LocalStr && LocalStr.length > 350) { str2 = LocalStr.substring(0, 350); } gg.sdk.reportDY("inLevel", `存档异常-本地数据|${str2}`); } } } if (sd) { for (let key in this.doc) { if (Object.prototype.hasOwnProperty.call(sd, key) && sd[key] !== undefined) { this.doc[key] = sd[key]; } else if (local && Object.prototype.hasOwnProperty.call(local, key) && local[key] !== undefined) { // 云端缺字段(旧版上传)时保留本地,避免 userProvince 等被清空 this.doc[key] = local[key]; } } //上报数据 //恢复仅存本地的数据 this.doc.onlyLocal = Array.isArray(local?.onlyLocal) ? (local.onlyLocal as string[]).slice() : []; let _IsFirstLogin = this.getStatus(StatusType.IsFirstLogin) == 0 gg.sdk.reportDY("inLevel", ` -IsFirstLogin=${_IsFirstLogin}`); } else if (local) { for (let key in this.doc) { if (Object.prototype.hasOwnProperty.call(local, key) && local[key] !== undefined) this.doc[key] = local[key]; } //上报数据 let _IsFirstLogin = this.getStatus(StatusType.IsFirstLogin) == 0 gg.sdk.reportDY("inLevel", `首次登录本地-IsFirstLogin=${_IsFirstLogin}`); } this._sanitizeDocAfterLoad(); if (!this.doc.uid && prevUid) this.doc.uid = prevUid; this.project.repairRankOnDocLoad(); //首次登录 this._IsFirstLogin = this.getStatus(StatusType.IsFirstLogin) == 0 this.setStatus(StatusType.IsFirstLogin, 2); //今天首次登录 this.checkIsFirstLoginToday(); // 每日商店:登录时若已过凌晨则重置(对局中过零点不重置) DailyShopDataManager.applyDayResetIfNeeded(); //每天首次登录初始化逻辑 if (this._IsFirstLoginToday) { this.onFirstLoginToday(); } // this.saveToServer(); //开始计时 this.startTimer(); this.setStatistics(StatisticsType.LastLoginTime, this.getCurentTime2()); game.on(Game.EVENT_HIDE, this.onHide, this); sdkConfig.isInitGameData = true; } onFirstLoginToday() { this._LoginDays++; this.setStatistics(StatisticsType.LoginDays, this._LoginDays); this._OnlineTimeForToDay = 0; if (!this.IsFirstLogin) gg.data.setStatistics(StatisticsType.FailContinuousCountReset, 5) gg.data.setStatistics(StatisticsType.SuccessContinuousCount, 0) this.setStatistics(StatisticsType.ShopBuyGoldAdCount, 6) this.setStatistics(StatisticsType.ShopBuyItemAdCount, 6) this.setStatistics(StatisticsType.FreeBoxCount, 0) this.setStatistics(StatisticsType.FirstLoginWeight, 0) this.setStatistics(StatisticsType.TodayAdCount, 0) this.setStatus(StatusType.DayGiftGetFinish, 0); this.setStatus(StatusType.DeskGiftGetFinish, 0); if (this.getStatistics(StatisticsType.GreatGiftStatus) == 1) { gg.data.setStatistics(StatisticsType.GreatGiftStatus, 0) let day = this.getStatistics(StatisticsType.GreatGiftGetDay) gg.data.setStatistics(StatisticsType.GreatGiftGetDay, day + 1) } // let greatGiftGetDay = this.getStatistics(StatisticsType.GreatGiftGetDay) // gg.data.setStatistics(StatisticsType.GreatGiftGetDay, greatGiftGetDay + 1) //签到数据更新// let signData = this.getStringData(OtherDataType.SignData) if (signData) { let [day, status] = signData.split('&'); let curDay = Number(day); let curStatus = Number(status); if (curStatus > 0) { curDay++; if (curDay >= 14) { curDay = 0 } curStatus = 0; } this.setStringData(OtherDataType.SignData, curDay.toString() + '&' + curStatus.toString()) } this.setStatistics(StatisticsType.buyPowerAdCount, 0) this.setStatistics(StatisticsType.buyPowerMoneyCount, 0) this.setStatistics(StatisticsType.saodangNormalCount, 0) this.setStatistics(StatisticsType.saodangAdCount, 0) this.setStatistics(StatisticsType.ShopDiamondShareCount, 1) this.setStatistics(StatisticsType.SuccessShareGetRewardCount, 1) this.setStatistics(StatisticsType.BuyGoldShareCount, 1) //每日任务状态和进度重置 this.setStatistics(StatisticsType.DayActiveCount, 0) //重置副本进入次数,扫荡次数 this.resetReplicaEnterAndSweepCountsToday(); gg.data.setStringData(OtherDataType.DayTaskRewardGetData, '0&0&0&0&0') gg.data.setStringData(OtherDataType.DayTaskProgressData, '0&0&0&0&0&0&0&0&0&0&0&0&0&0') gg.data.setStringData(OtherDataType.DayActiveGetRewardData, '0&0&0&0&0&0&0&0&0&0&0&0&0&0') gg.data.setkeyKeyValueDayClear(OtherDataType.FreeUpgradeZombieFactory) //重置免费升级僵尸工厂次数重置 //重置副本扫荡次数 gg.data.getShopItemList() DailyShopDataManager.onCrossDay(); DailyWinStats.onCrossDay(); EmolumentDataManager.onCrossDay(); MatchWinStats.onCrossDay(); // gg.data.setStatistics(StatisticsType.PropModelLevel, 0); // gg.data.setStatistics(StatisticsType.BoxModelLevel, 0); // //抽卡挑战模式挑战等级 // gg.data.setStatistics(StatisticsType.GachaChallengeModeLevel, 0); GEvent.Ins.emit(GEvent.CrossDay); } /** Replica 表已移除 */ resetReplicaEnterAndSweepCountsToday(): void { } onHide() { if (gg.game.isInitAllData) { this.saveLocal(); this.saveToServer(); } } /** * 重置游戏数据 */ reset(callback: Function, tagStr: string = '') { this.clearServerData((res) => { if (res) { //白名单不能清除 let whiteUseruuid = this.getStringData(OtherDataType.BaiMingTianUUIid); this._IsFirstLogin = true; this._IsFirstLoginToday = true; this._LoginDays = 1; this._OfflineTime = 0; this._OnlineTime = 0; this._OnlineTimeForToDay = 0; this._OnlineTimeForAll = 0; sys.localStorage.clear(); // 必须新建 Doc:initDoc 不会清 userName/userProvince/rank* 等字段 this.doc = new Doc(); this.initDoc(); DailyShopDataManager.clearForReset(); // this.saveLocal(); this.setStringData(OtherDataType.BaiMingTianUUIid, whiteUseruuid); this.saveLocal(); callback && callback(true); } else { callback && callback(false); } }, tagStr); } private getSaveData() { let result = ""; let temp = this.deepClone(this.doc); temp.onlyLocal = [] // 昵称统一 Base64 后再上传,避免特殊字符破坏服务器存档;已是 Base64 不再处理(仅处理上传包,内存 doc 不动) if (typeof temp.userName === 'string' && temp.userName && !this._isBase64Text(temp.userName)) { temp.userName = this._utf8ToBase64(temp.userName); } result = JSON.stringify(temp); temp = null; return result; } /** 昵称是否已是 Base64(字符集+长度 4 的倍数;解出控制字符/替换符按明文处理) */ private _isBase64Text(s: string): boolean { if (!s || s.length % 4 !== 0) return false; if (!/^[A-Za-z0-9+/]+={0,2}$/.test(s)) return false; try { const bin = this._base64Decode(s); if (!bin) return false; for (let i = 0; i < bin.length; i++) { const c = bin.charCodeAt(i); if (c < 32 && c !== 9 && c !== 10 && c !== 13) return false; if (c === 127) return false; } return true; } catch { return false; } } /** Base64 → latin1 字符串(不依赖 atob,小游戏环境兜底);非法输入返回空串 */ private _base64Decode(b64: string): string { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; const lookup: Record = {}; for (let i = 0; i < chars.length; i++) lookup[chars.charAt(i)] = i; const clean = b64.replace(/=+$/, ''); let out = ''; let buffer = 0; let bits = 0; for (let i = 0; i < clean.length; i++) { const v = lookup[clean.charAt(i)]; if (v === undefined) return ''; buffer = (buffer << 6) | v; bits += 6; if (bits >= 8) { bits -= 8; out += String.fromCharCode((buffer >> bits) & 255); } } return out; } /** UTF-8 → Base64(无 btoa 环境走手写实现;编码失败返回原文避免丢昵称) */ private _utf8ToBase64(s: string): string { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; const bytes: number[] = []; for (let i = 0; i < s.length; i++) { let code = s.codePointAt(i) ?? 0; if (code > 0xffff) i++; // 跳过代理对低位 if (code < 0x80) { bytes.push(code); } else if (code < 0x800) { bytes.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f)); } else if (code < 0x10000) { bytes.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f)); } else { bytes.push( 0xf0 | (code >> 18), 0x80 | ((code >> 12) & 0x3f), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f), ); } } let out = ''; for (let i = 0; i < bytes.length; i += 3) { const b1 = bytes[i]; const b2 = i + 1 < bytes.length ? bytes[i + 1] : 0; const b3 = i + 2 < bytes.length ? bytes[i + 2] : 0; out += chars[b1 >> 2]; out += chars[((b1 & 3) << 4) | (b2 >> 4)]; out += i + 1 < bytes.length ? chars[((b2 & 15) << 2) | (b3 >> 6)] : '='; out += i + 2 < bytes.length ? chars[b3 & 63] : '='; } return out; } /** 读档后把数组/关键字段规范成安全类型,避免脏档导致运行时崩溃 */ private _sanitizeDocAfterLoad(): void { const d = this.doc as unknown as Record; const arrKeys = ['itemCount', 'status', 'statistics', 'guide', 'other', 'onlyLocal']; for (const k of arrKeys) { const v = d[k]; if (!Array.isArray(v)) { d[k] = []; } else { d[k] = (v as unknown[]).filter((x) => typeof x === 'string'); } } const strKeys = [ 'key', 'uid', 'userName', 'userAvatar', 'userProvince', 'matchWinStatsDate', 'dailyWinsDate', 'rankLastSyncDailyWinsDate', 'rankLastSyncProvince', 'rankLastSyncNick', 'rankLastSyncAvatar', 'rankLastSyncRegion', 'emolumentSnapDate', ]; for (const k of strKeys) { if (d[k] == null) d[k] = ''; else if (typeof d[k] !== 'string') d[k] = String(d[k]); } const numKeys = [ 'roleId', 'playerTitleLevel', 'dataVersion', 'rankId', 'rankStars', 'matchWinCount', 'matchLoseCount', 'totalBattleCount', 'matchWinStreak', 'matchLoseStreak', 'dailyWins', 'yesterdayWins', 'rankLastSyncDailyWins', 'rankUserAuthPrompted', 'rankSyncedOnce', 'rankLastSyncStars', 'emolumentSnapStars', 'emolumentSnapWins', 'gmAwaitEmolumentPop', 'headIconId', 'battleIllustratedGuideClicked', 'level', 'failCount', 'lastFailCount', ]; for (const k of numKeys) { const n = Number(d[k]); d[k] = Number.isFinite(n) ? n : 0; } if (!(this.doc.rankLastSyncDailyWins >= 0)) this.doc.rankLastSyncDailyWins = -1; if (!(this.doc.rankLastSyncStars >= 0)) this.doc.rankLastSyncStars = -1; if (!(this.doc.rankId > 0)) this.doc.rankId = 1; if (!(this.doc.level > 0)) this.doc.level = 1; } /**保存本地 */ saveLocal() { this.setStatistics(StatisticsType.TodayOnLineTime, this._OnlineTimeForToDay); this.setStatistics(StatisticsType.AllOnLineTime, this._OnlineTimeForAll); this.doc.dataVersion++; sys.localStorage.setItem(this._docName, JSON.stringify(this.doc)); } /**保存到服务器(部分必要数据备份) * @param callback 保存成功回调 * @param bForceSave 是否强制保存,默认false */ saveToServer(callback?: Function, bForceSave: boolean = false) { this.saveLocal(); if (DEBUG || EDITOR || sys.isBrowser) { if (callback) callback(false); return; } //1秒内不重复保存(强制保存除外);被节流时延迟补传并保留 callback if (MTools.beforeTimes(1000, "saveToServer") && !bForceSave) { if (callback) this._pendingServerCallbacks.push(callback); if (this._pendingServerTimer == null) { this._pendingServerTimer = setTimeout(() => { this._pendingServerTimer = null; const cbs = this._pendingServerCallbacks.splice(0, this._pendingServerCallbacks.length); this.saveToServer((ok: boolean) => { for (let i = 0; i < cbs.length; i++) cbs[i]?.(ok); }, false); }, 1100); } return; } let d = this.getSaveData(); if (bForceSave) { GEvent.Ins.emit(GEvent.SHOW_NET_UI); } ZYSDK.ZYSDK.setGameData(d, this.doc.rankId + "", this.OnlineTimeForAll, "", "", "", () => { GEvent.Ins.emit(GEvent.HIDE_NET_UI); this._forceSaveRetry = 0; if (this.doc.failCount != this.doc.lastFailCount) { gg.sdk.reportDY("inLevel", `存档失败次数-${this.doc.lastFailCount}`); } this.doc.failCount = this.doc.lastFailCount; if (callback) callback(true); }, () => { this.doc.lastFailCount++; if (bForceSave) { gg.ui.showToast("通关数据存档中"); this._forceSaveRetry++; // 有限次重试;callback 只在最终结果回调一次 if (this._forceSaveRetry <= 3) { const retry = this._forceSaveRetry; setTimeout(() => { this.saveToServer(callback, true); }, 800 * retry); return; } this._forceSaveRetry = 0; } GEvent.Ins.emit(GEvent.HIDE_NET_UI); if (callback) callback(false); }); } private _pendingServerCallbacks: Function[] = []; private _pendingServerTimer: ReturnType | null = null; private _forceSaveRetry = 0; clearServerData(callback?: Function, tagStr: string = '') { if (EDITOR || DEBUG || sys.isBrowser) { if (callback) callback(true); return; } gg.sdk.reportDY("inLevel", `存档重置-${tagStr}`); ZYSDK.ZYSDK.setGameData("", "", 0, "", "", "", () => { if (callback) callback(true); }, () => { if (callback) callback(false); }); } /**获取当前时间 */ getCurentTime() { return new Date().getTime() + 8 * 60 * 60 * 1000;//转换本地时间+8小时 } getCurentTime2() { return new Date().getTime()//转换本地时间+8小时 } /**获取当天剩余时间 */ getTodayResidualTime() { let dayTime = 24 * 60 * 60 * 1000; let curentTime = this.getCurentTime(); return dayTime - (curentTime % dayTime); } isSameWeek(date1, date2) { // 设置周起始日为周一(1表示周一,0表示周日) const weekStartDay = 1; // 可以根据需要调整为0(周日) // 计算两个日期是星期几(0-6,0表示周日) const dayOfWeek1 = date1.getDay(); const dayOfWeek2 = date2.getDay(); // 计算距离周起始日的天数差 const diff1 = dayOfWeek1 >= weekStartDay ? dayOfWeek1 - weekStartDay : 7 - (weekStartDay - dayOfWeek1); const diff2 = dayOfWeek2 >= weekStartDay ? dayOfWeek2 - weekStartDay : 7 - (weekStartDay - dayOfWeek2); // 设置为周起始日的日期 date1.setDate(date1.getDate() - diff1); date2.setDate(date2.getDate() - diff2); // 重置时间部分,只比较日期 date1.setHours(0, 0, 0, 0); date2.setHours(0, 0, 0, 0); // 比较两个周起始日是否相同 return date1.getTime() === date2.getTime(); } /** * 验证今天是否首次登录 */ private checkIsFirstLoginToday() { let lastLoginTime = this.getStatistics(StatisticsType.LastLoginTime); let currentTime = this.getCurentTime2(); //判断是不是同一天 const lastDate = new Date(lastLoginTime); const currentDate = new Date(currentTime); //已经转换为北京本地时间+8小时 // 检查是否是同一天(年、月、日相同) const isSameDay = lastDate.getFullYear() === currentDate.getFullYear() && lastDate.getMonth() === currentDate.getMonth() && lastDate.getDate() === currentDate.getDate(); this._IsFirstLoginToday = !isSameDay; //获取是不是同一周 let isWeek = this.isSameWeek(lastDate, currentDate) if (!isWeek) { gg.data.setStringData(OtherDataType.TaskRewardGetData, '0&0&0&0&0') //WeekActiveCount this.setStatistics(StatisticsType.WeekActiveCount, 0); } this._LoginDays = this.getStatistics(StatisticsType.LoginDays); this._OnlineTimeForAll = this.getStatistics(StatisticsType.AllOnLineTime); this._OnlineTimeForToDay = this.getStatistics(StatisticsType.TodayOnLineTime); //检测是不是第二周 //TaskRewardGetData是不是同一周 this._CurrentLoginDate = new Date().getDay(); //gg.data.setStringData(OtherDataType.TaskRewardGetData,'0&0&0&0&0') } /**验证跨天 */ public checkIsCrossDay() { let currentDay = new Date().getDay(); if (currentDay != this._CurrentLoginDate) { //跨天了 this._CurrentLoginDate = currentDay; this._IsFirstLoginToday = true; this._OnlineTimeForToDay = 0; this._LoginDays++; this.setStatistics(StatisticsType.LoginDays, this._LoginDays); this.setStatistics(StatisticsType.TodayFirstLoginTime, this.getCurentTime()); this.onFirstLoginToday(); } } /** * 开始计时 */ private startTimer() { this._OnlineTime = 1; // 登录立刻结算离线期间应得的体力 this.project.autoAddVigour(); if (!this._timer_flag) { this._timer_flag = setInterval(() => { this._OnlineTime++; this._OnlineTimeForToDay++; this._OnlineTimeForAll++; this.project.autoAddVigour(); if (this._OnlineTime % 30 == 0) { this.saveLocal(); this.saveToServer(); } }, 1000) } } private _timer_flag = null; //#region 存档数据 /**存档数据对象 */ doc: Doc = new Doc(); /**获取道具数量 */ getItemNum(itmeId) { let v = this.getDataFromArrayKeyValueArray(this.doc.itemCount, itmeId) return v == "" ? 0 : Number(v); } /**设置道具数量(警告:设置会覆盖道具的数量,请谨慎使用) */ setItemNum(itmeId, num: number) { if (isNaN(num)) { console.warn('[GameData] setItemNum NaN', itmeId, num); return } this.reportItemNumChange(itmeId, num); this.setKeyValueArray(this.doc.itemCount, itmeId, num); GEvent.Ins.emit(GEvent.UpdateItemNum, itmeId); } /**数量变动打点 */ reportItemNumChange(itmeId, num: number) { let v = this.getItemNum(itmeId); if (itmeId == ItemNumType.money) { let pArr = [50] for (let i = 100; i < 500; i += 50) { pArr.push(i); } for (let i = 500; i < 2000; i += 100) { pArr.push(i); } for (let i = 2000; i < 5000; i += 200) { pArr.push(i); } for (let i = 5000; i < 10000; i += 500) { pArr.push(i); } for (let i = 10000; i < 100000; i += 5000) { pArr.push(i); } for (let i = 100000; i < 500000; i += 10000) { pArr.push(i); } if (num > v) { //增加 this.addStatistics(StatisticsType.AccumulateGetCoin, num - v); let localIndex = this.getStatistics(StatisticsType.ReportIndexAccGetCoin); let money = this.getStatistics(StatisticsType.AccumulateGetCoin); for (let i = 0; i < pArr.length; i++) { if (money >= pArr[i] && localIndex < i) { localIndex = i; gg.sdk.reportDY("inLevel", `金币获取-累积${pArr[i]}`) } } this.setStatistics(StatisticsType.ReportIndexAccGetCoin, localIndex); } else if (num < v) { //减少 this.addStatistics(StatisticsType.AccumulateWasteCoin, v - num); let localIndex = this.getStatistics(StatisticsType.ReportIndexAccWasteCoin); let money = this.getStatistics(StatisticsType.AccumulateWasteCoin); for (let i = 0; i < pArr.length; i++) { if (money >= pArr[i] && localIndex < i) { localIndex = i; gg.sdk.reportDY("inLevel", `金币消耗-累积${pArr[i]}`) } } this.setStatistics(StatisticsType.ReportIndexAccWasteCoin, localIndex); } } else if (itmeId == ItemNumType.diamond) { let pArr = [10] for (let i = 20; i < 500; i += 20) { pArr.push(i); } for (let i = 500; i < 2000; i += 100) { pArr.push(i); } for (let i = 2000; i < 5000; i += 200) { pArr.push(i); } for (let i = 5000; i < 10000; i += 500) { pArr.push(i); } for (let i = 10000; i < 100000; i += 5000) { pArr.push(i); } for (let i = 100000; i < 500000; i += 10000) { pArr.push(i); } if (num > v) { //增加 this.addStatistics(StatisticsType.AccumulateGetDiamond, num - v); let localIndex = this.getStatistics(StatisticsType.ReportIndexAccGetDiamond); let diamond = this.getStatistics(StatisticsType.AccumulateGetDiamond); for (let i = 0; i < pArr.length; i++) { if (diamond >= pArr[i] && localIndex < i) { localIndex = i; gg.sdk.reportDY("inLevel", `钻石获取-累积${pArr[i]}`) } } this.setStatistics(StatisticsType.ReportIndexAccGetDiamond, localIndex); } else if (num < v) { //减少 this.addStatistics(StatisticsType.AccumulateWasteDiamond, v - num); let localIndex = this.getStatistics(StatisticsType.ReportIndexAccWasteDiamond); let diamond = this.getStatistics(StatisticsType.AccumulateWasteDiamond); for (let i = 0; i < pArr.length; i++) { if (diamond >= pArr[i] && localIndex < i) { localIndex = i; gg.sdk.reportDY("inLevel", `钻石消耗-累积${pArr[i]}`) } } this.setStatistics(StatisticsType.ReportIndexAccWasteDiamond, localIndex); } } } /**添加道具数量 */ addItemNum(itmeId, num: number) { let v = this.getItemNum(itmeId); this.setItemNum(itmeId, v + num); } /**减少道具数量 */ subItemNum(itmeId, num: number) { let v = this.getItemNum(itmeId); let count = v - num; if (count < 0) { count = 0; } this.setItemNum(itmeId, count); } /**获取所有道具 */ getAllItemNum(): IItemSaveData[] { let arr: IItemSaveData[] = []; for (let i = 0; i < this.doc.itemCount.length; i++) { let data = this.doc.itemCount[i].split("_"); arr.push({ id: Number(data[0]), count: Number(data[1]) }); } return arr; } /**获取当前金钱 */ getMoney() { return this.getItemNum(ItemNumType.money); } /**设置金钱数量 */ setMoney(num: number) { this.setItemNum(ItemNumType.money, Math.floor(num)); GEvent.Ins.emit(GEvent.UpdateMoney); } /**添加金钱数量 */ addMoney(num: number) { this.addItemNum(ItemNumType.money, Math.floor(num)); GEvent.Ins.emit(GEvent.UpdateMoney); } /**减少金钱数量 */ subMoney(num: number) { this.subItemNum(ItemNumType.money, Math.floor(num)); GEvent.Ins.emit(GEvent.UpdateMoney); } /**获取当前战力 */ getCombatPower() { return this.getItemNum(ItemNumType.combatPower); } /**设置战力数量 */ setCombatPower(num: number) { this.setItemNum(ItemNumType.combatPower, Math.floor(num)); GEvent.Ins.emit(GEvent.UpdateCombatPower); } /**添加战力数量 */ addCombatPower(num: number) { this.addItemNum(ItemNumType.combatPower, Math.floor(num)); GEvent.Ins.emit(GEvent.UpdateCombatPower); } /**减少战力数量 */ subCombatPower(num: number) { this.subItemNum(ItemNumType.combatPower, Math.floor(num)); GEvent.Ins.emit(GEvent.UpdateCombatPower); } /**获取当前钻石 */ getDiamond() { return this.getItemNum(ItemNumType.diamond); } /**设置钻石数量 */ setDiamond(num: number) { this.setItemNum(ItemNumType.diamond, Math.floor(num)); GEvent.Ins.emit(GEvent.UpdateDiamond); } /**添加钻石数量 */ addDiamond(num: number) { this.addItemNum(ItemNumType.diamond, Math.floor(num)); GEvent.Ins.emit(GEvent.UpdateDiamond); } /**减少钻石数量 */ subDiamond(num: number) { this.subItemNum(ItemNumType.diamond, Math.floor(num)); GEvent.Ins.emit(GEvent.UpdateDiamond); } /**获取当前体力 */ getVigour() { return this.getItemNum(ItemNumType.vigour); } /**设置体力数量 */ setVigour(num: number) { this.setItemNum(ItemNumType.vigour, Math.floor(num)); GEvent.Ins.emit(GEvent.UpdateVigour); } /**添加体力数量 */ addVigour(num: number) { this.addItemNum(ItemNumType.vigour, Math.floor(num)); GEvent.Ins.emit(GEvent.UpdateVigour); } /**减少体力数量 */ subVigour(num: number) { const cost = Math.floor(num); this.addDayTaskProgressData(TaskType.powerConsumption, cost); this.subItemNum(ItemNumType.vigour, cost); // 从满额首次降到未满时记下恢复锚点;进战斗等路径只走本方法,必须在此同步 this.project.syncAutoAddVigourTimeAfterConsume(cost); GEvent.Ins.emit(GEvent.UpdateVigour); // 推荐流复访托管:扣体力后刷新「体力回满」就绪时刻 try { gg.sdk?.storeFeedData?.(); } catch { /* ignore */ } } /**获取状态信息 */ getStatus(type: StatusType) { let v = this.getDataFromArrayKeyValueArray(this.doc.status, type) return v == "" ? 0 : Number(v); } /**设置状态信息 */ setStatus(type: StatusType, status: number) { this.setKeyValueArray(this.doc.status, type, status); } /**获取统计信息 */ getStatistics(type: StatisticsType) { let v = this.getDataFromArrayKeyValueArray(this.doc.statistics, type) return v == "" ? 0 : Number(v); } /**设置统计信息 */ setStatistics(type: StatisticsType, count: number) { this.setKeyValueArray(this.doc.statistics, type, count); } /**添加统计信息 */ addStatistics(type: StatisticsType, count: number) { let v = this.getStatistics(type); this.setStatistics(type, v + count); } /** 进战斗时累积挑战次数 +1,并写入本地存档 */ addTotalBattleCount(): void { const prev = Math.max(0, Math.floor(Number(this.doc.totalBattleCount) || 0)); this.doc.totalBattleCount = prev + 1; } /**保存数组数据 */ setArrayData(key: OtherDataType, arr: any[]) { this.setKeyValueArray(this.doc.other, key, arr.join("#")); } /**获取数组数据 */ getArrayData(key: OtherDataType) { let v = this.getDataFromArrayKeyValueArray(this.doc.other, key); return v ? v.split("#") : []; } /**转化为int数组 */ getArrayDataInt(key: OtherDataType) { let v = this.getDataFromArrayKeyValueArray(this.doc.other, key); let array = v ? v.split("#") : []; return array.map(Number); } /** * 保存键值对数据(额外接口) * @param key 您自己维护的key请保持唯一性 * @param value 存储的值字符串(“_”下划线是该接口保留的字符,请不要在value中包含下划线) */ setStringData(key: OtherDataType, value: string) { this.setKeyValueArray(this.doc.other, key, value); } /**获取键值对数据 */ getStringData(key: OtherDataType) { let v = this.getDataFromArrayKeyValueArray(this.doc.other, key); return v ? v : ""; } /** 保存同一个key下,多个key2的值 每日清空状态 */ setkeyKeyValueDayClear(key: OtherDataType, onlyLocal = false) { let saveArr = onlyLocal ? this.doc.onlyLocal : this.doc.other; this.setKeyValueArray(saveArr, key, ''); } /**保存同一个key下,多个key2的值(例如:XX角色多少级,key为角色等级的key,key2为角色id,value为等级) */ setkeyKeyValue(key: OtherDataType, key2: number | string, value: number | string, onlyLocal = false) { let saveArr = onlyLocal ? this.doc.onlyLocal : this.doc.other; let v = this.getDataFromArrayKeyValueArray(saveArr, key); let arr = v ? v.split("#") : []; let prefix = key2 + "|"; let foundIndex = -1; for (let i = 0; i < arr.length; i++) { if (arr[i].startsWith(prefix)) { foundIndex = i; break; } } if (foundIndex !== -1) { arr[foundIndex] = prefix + value; } else { arr.push(prefix + value); } this.setKeyValueArray(saveArr, key, arr.join("#")); } /**获取同一个key下,多个key2的值(例如:XX角色多少级,key为角色等级的key,key2为角色id,value为等级) */ getkeyKeyValue(key: OtherDataType, key2: number | string, onlyLocal = false) { const saveArr = onlyLocal ? this.doc.onlyLocal : this.doc.other; const v = this.getDataFromArrayKeyValueArray(saveArr, key); if (!v) return ""; const prefix = key2.toString() + '|'; const arr = v.split("#"); const found = arr.find(item => item.startsWith(prefix)); return found ? found.substring(prefix.length) : ""; } /** * 保存键值对数据(仅存本地存储,不会备份到服务器) * @param key 您自己维护的key请保持唯一性 * @param value 存储的值字符串(“_”下划线是该接口保留的字符,请不要在value中包含下划线) */ setStringDataOnlyLocal(key: OtherDataType, value: string) { this.setKeyValueArray(this.doc.onlyLocal, key, value); } /**获取键值对数据(仅存本地存储,不会备份到服务器) */ getStringDataOnlyLocal(key: OtherDataType) { let v = this.getDataFromArrayKeyValueArray(this.doc.onlyLocal, key); return v ? v : ""; } /**根据key获取键值对数组中的数据 */ private getDataFromArrayKeyValueArray(arr: string[], key: any): string { if (!Array.isArray(arr)) return ''; const keyStr = key.toString(); const prefix = keyStr + '_'; const found = arr.find(item => typeof item === 'string' && item.startsWith(prefix)); return found ? found.substring(prefix.length) : ''; } /**设置键值对数据中的数据 */ private setKeyValueArray(arr: string[], key: any, value: any): void { if (!Array.isArray(arr)) { console.warn('[GameData] setKeyValueArray 目标不是数组', key); return; } const keyStr = key.toString(); const prefix = keyStr + '_'; const newEntry = prefix + value; // 使用 findIndex 一次遍历找到匹配项的索引 const index = arr.findIndex(item => typeof item === 'string' && item.startsWith(prefix)); if (index !== -1) { arr[index] = newEntry; // 替换 } else { arr.push(newEntry); // 新增 } } /** * 获取引导状态 * @param key 您自己维护的key请保持唯一性 */ public getLocalGuideValue(key: string) { let v = this.getDataFromArrayKeyValueArray(this.doc.onlyLocal, key); return v ? v : 0; } /** * 设置引导状态 * @param key 您自己维护的key请保持唯一性 * @param value 存储的值字符串(“_”下划线是该接口保留的字符,请不要在value中包含下划线) */ public setLocalGuideValue(key: string, value: string) { this.setKeyValueArray(this.doc.onlyLocal, key, value); } /** * 深拷贝 * @param target 目标对象 */ private deepClone(target: T, visited = new WeakMap()): T { if (typeof target !== 'object' || target === null) { return target; } if (visited.has(target)) { return visited.get(target); } if (Array.isArray(target)) { let cloneArray: any[] = []; visited.set(target, cloneArray); for (let i = 0; i < target.length; i++) { cloneArray[i] = this.deepClone(target[i], visited); } return cloneArray as T; } let cloneObject: { [key: string]: any } = {}; visited.set(target, cloneObject); for (let key in target) { if (Object.prototype.hasOwnProperty.call(target, key)) { cloneObject[key] = this.deepClone((target as { [key: string]: any })[key], visited); } } return cloneObject as T; } //------------------------------------------------------------------------------------------------------------------ //获取商城列表(Shop 表已移除) public getShopItemList() { return []; } //获取打开宝箱的奖励数组(Box 表已移除) public getBoxRewardArray(_boxLevelData: unknown) { return []; } //获取特定碎片的数量(如通关奖励 104,5 → 展开为具体武器碎片) public getFragmentNumArray(fragmentStr) { return this._expandFragmentRewardStr(fragmentStr, true); } /** * 将「品质碎片箱」串展开为具体武器碎片 id 列表。 * @param fragmentStr 如 "104,5" 或 "104,52:105,12" * @param downgradeQuality 无对应品质时是否降级(通关奖励要降;开箱旧逻辑不降) */ private _expandFragmentRewardStr(fragmentStr: string, downgradeQuality: boolean): number[] { if (!fragmentStr) return []; const haveArr = gg.data.project.getHaveWeaponScrapItemIds(); const fragmentArrayAll: number[] = []; for (const data1 of fragmentStr.split(':')) { if (!data1) continue; const parts = data1.split(','); const boxItem = this.table.getItemData(Number(parts[0])); if (!boxItem) { console.log('_expandFragmentRewardStr: Item 缺失', parts[0]); continue; } let quality1 = boxItem.quality; const num1 = Number(parts[1]); if (!(num1 > 0)) continue; if (downgradeQuality) { while (quality1 > 0) { const hit = haveArr.some(id => this.table.getItemData(id)?.quality === quality1); if (hit) break; quality1--; } } const qualityArray1: number[] = []; const weightArray1: number[] = []; for (const goodID of haveArr) { const itemData = this.table.getItemData(goodID); if (!itemData || itemData.quality !== quality1) continue; qualityArray1.push(goodID); // Item 表无 weight:同品质碎片池等权 weightArray1.push(1); } fragmentArrayAll.push( ...MTools.getRandomByWeightArray(num1, qualityArray1, weightArray1), ); } return fragmentArrayAll; } //获取当前是不是可以武器升级 public getIsCanUpgradeWeapon() { const haveArr = gg.data.project.getHaveWeaponDataList(); for (let i = 0; i < haveArr.length; i++) { const t = haveArr[i]; const weaponRow = gg.data.table.getTableData(TableNames.Weapon, t.personalweapon || t.startweapon); const scrapId = weaponRow?.itemid; if (!scrapId) continue; const weaponSPNum = gg.data.getItemNum(scrapId); const needNum = gg.data.project.getWeaponFlagNumByLevel(t.id); if (weaponSPNum >= needNum) { return true; } } return false; } //获取玩家的钱够不够升级(Role 表已移除,按存档阶段粗判) public getIsCanUpgradeWeaponByMoney() { const myMoney = this.getItemNum(ItemNumType.money); return myMoney >= 100; } //商店是不是有免费的领取 public getIsCanGetFreeItem() { let freeSpendNum = this.getStatistics(StatisticsType.ShopBuyGoldAdCount) if (freeSpendNum >= 6) { return true } let freeSpendNum2 = this.getStatistics(StatisticsType.ShopBuyItemAdCount) if (freeSpendNum2 >= 6) { return true } if (gg.data.getDiamond() >= 200) { return true } return false } //获取玩家等级 public getPlayerLevel() { let curData = this.getStringData(OtherDataType.UserLevelupData) if (curData == '') { this.setStringData(OtherDataType.UserLevelupData, '0&10&0&0&0&0&0') curData = '0&10&0&0&0&0&0' } let curDataArr = curData.split('&') return Number(curDataArr[0]) } /**获取玩家攻击力系数*/ public getPlayerAtk() { let curData = this.getStringData(OtherDataType.UserLevelupData) if (curData == '') { this.setStringData(OtherDataType.UserLevelupData, '0&10&0&0&0&0&0') curData = '0&10&0&0&0&0&0' } let curDataArr = curData.split('&') let num = Number(curDataArr[1]); return num } /**获取玩家全局攻击力系数*/ public getPlayerAtkCoefficient() { let curData = this.getStringData(OtherDataType.UserLevelupData) if (curData == '') { this.setStringData(OtherDataType.UserLevelupData, '0&10&0&0&0&0&0') curData = '0&10&0&0&0&0&0' } let curDataArr = curData.split('&') let num = Number(curDataArr[1]); return num } /**获取玩家暴击系数*/ public getPlayerCritCoefficient() { let curData = this.getStringData(OtherDataType.UserLevelupData) if (curData == '') { this.setStringData(OtherDataType.UserLevelupData, '0&10&0&0&0&0&0') curData = '0&10&0&0&0&0&0' } let curDataArr = curData.split('&') return Number(curDataArr[2]) } /**获取玩家冷却系数*/ public getPlayerCoolCoefficient() { let curData = this.getStringData(OtherDataType.UserLevelupData) if (curData == '') { this.setStringData(OtherDataType.UserLevelupData, '0&10&0&0&0&0&0') curData = '0&10&0&0&0&0&0' } let curDataArr = curData.split('&') return Number(curDataArr[3]) } //获取当前宝箱等级配置 public getCurBoxLevelData(_boxType: number) { return null; } public getCurBoxLevel() { return 1; } public getCurBoxExp() { return [0, 0]; } //获取战力 //获取当前战力 public getCurPower() { let power = 100 //UserLevelupData let curData = this.getStringData(OtherDataType.UserLevelupData) if (curData == '') { return power } let curDataArr = curData.split('&') let attack = Number(curDataArr[1]) let dAttack = Number(curDataArr[2]) let lengque = Number(curDataArr[3]) power = 100 * (attack / 10) * (1 + dAttack / 10000) * (1 + lengque / 10000) //四舍五入不保留小数 power = Math.round(power) return power; } /** 获取战斗奖励加成比率(道具 id=98,para 为万分比) */ public getFightRewardRatio(): number { const id = 98; const row = this.table.getItemData(id); if (!row) { console.log('[GameData] getFightRewardRatio: Item 表缺少 id=', id); return 0; } const rateWan = Number(row.para); if (!Number.isFinite(rateWan)) { console.log('[GameData] getFightRewardRatio: Item id=', id, 'para 无效', row.para); return 0; } const num = this.getItemNum(id); return num * (rateWan / 10000); } // //每日任务进度 任务进度 // DayTaskProgressData = 45, // //每日活跃奖励领取情况 // DayActiveGetRewardData = 46, //周任务奖励领取状态设置 public setWeekTaskRewardGetData(index, rewardGet) { let rewardGetDataList = gg.data.getStringData(OtherDataType.TaskRewardGetData).split('&') rewardGetDataList[index] = rewardGet this.setStringData(OtherDataType.TaskRewardGetData, rewardGetDataList.join('&')) } public setDayTaskRewardGetData(index, rewardGet) { let rewardGetDataList = gg.data.getStringData(OtherDataType.DayTaskRewardGetData).split('&') rewardGetDataList[index] = rewardGet this.setStringData(OtherDataType.DayTaskRewardGetData, rewardGetDataList.join('&')) } public addDayTaskProgressData(_taskType: TaskType, _addPro = 1) { } public addDayTaskScore(_score = 1) { } public addWeekTaskScore(_score = 1) { } //活跃奖励进度领取状态设置 public setDayTaskProgressData(index, rewardGet) { let progressDataList = gg.data.getStringData(OtherDataType.DayActiveGetRewardData).split('&') if (isNaN(rewardGet)) { rewardGet = 1 } progressDataList[index] = rewardGet this.setStringData(OtherDataType.DayActiveGetRewardData, progressDataList.join('&')) } //判断任务是不是有奖励可以领取 public checkTaskRewardGetData() { let progressDataList = gg.data.getStringData(OtherDataType.TaskRewardGetData).split('&') let rewardGetDataList = gg.data.getStringData(OtherDataType.DayActiveGetRewardData).split('&') let dayTaskRewardGetData = gg.data.getStringData(OtherDataType.DayTaskRewardGetData).split('&') //y有1就返回true for (let i = 0; i < progressDataList.length; i++) { if (Number(progressDataList[i]) == 1) { return true } } //每日任务奖励领取情况 for (let i = 0; i < dayTaskRewardGetData.length; i++) { if (Number(dayTaskRewardGetData[i]) == 1) { return true } } //每日活跃奖励领取情况 for (let i = 0; i < rewardGetDataList.length; i++) { if (Number(rewardGetDataList[i]) == 1) { return true } } return false } /**初始化存档 */ private initDoc() { this.doc.key = ""; this.doc.uid = ""; this.doc.roleId = 1; this.doc.playerTitleLevel = 0; this.doc.dataVersion = 0; this.doc.rankId = 1; this.doc.rankStars = 1; this.doc.matchWinCount = 0; this.doc.matchLoseCount = 0; this.doc.totalBattleCount = 0; this.doc.matchWinStatsDate = ''; this.doc.matchWinStreak = 0; this.doc.matchLoseStreak = 0; this.doc.dailyWins = 0; this.doc.yesterdayWins = 0; this.doc.dailyWinsDate = ''; this.doc.rankLastSyncDailyWins = -1; this.doc.rankLastSyncDailyWinsDate = ''; this.doc.userName = ""; this.doc.userAvatar = ""; this.doc.userProvince = ""; this.doc.rankUserAuthPrompted = 0; this.doc.rankSyncedOnce = 0; this.doc.rankLastSyncStars = -1; this.doc.rankLastSyncProvince = ""; this.doc.rankLastSyncNick = ""; this.doc.rankLastSyncAvatar = ""; this.doc.rankLastSyncRegion = ""; this.doc.emolumentSnapStars = 0; this.doc.emolumentSnapWins = 0; this.doc.emolumentSnapDate = ''; this.doc.gmAwaitEmolumentPop = 0; this.doc.headIconId = 0; this.doc.battleIllustratedGuideClicked = 0; this.doc.level = 1; this.doc.itemCount = []; this.doc.status = []; this.doc.statistics = []; this.doc.guide = []; this.doc.other = []; this.doc.onlyLocal = []; this.doc.failCount = 0; this.doc.lastFailCount = 0; } //#endregion } /**存档数据结构 */ export class Doc { /**存档key */ key: string = ""; /**用户id */ uid: string = ""; /**角色id 男1 女 0 */ roleId: number = 1; /**玩家称号等级 */ playerTitleLevel: number = 0; /**版本号 */ dataVersion: number = 0; /** 当前段位(关卡解锁等进度;段位展示由 rankStars 推导) */ rankId: number = 1; /** 总星数(段位星数存档;段位 id 由 Rank.level 推导) */ rankStars: number = 1; /** 匹配页玩家胜场 */ matchWinCount: number = 0; /** 匹配页玩家负场 */ matchLoseCount: number = 0; /** 玩家累积挑战次数(每次进战斗 +1,跨天不清) */ totalBattleCount: number = 0; /** 匹配胜率统计日期 yyyyMMdd(跨天清零) */ matchWinStatsDate: string = ''; /** 匹配连续胜场(跨天不清;胜负互斥) */ matchWinStreak: number = 0; /** 匹配连续负场(跨天不清;胜负互斥) */ matchLoseStreak: number = 0; /** 今日胜场(每日胜场榜,跨天清零) */ dailyWins: number = 0; /** 昨日胜场 */ yesterdayWins: number = 0; /** 今日胜场所属日期 YYYYMMDD */ dailyWinsDate: string = ''; /** 上次同步的每日胜场榜分数 */ rankLastSyncDailyWins: number = -1; /** 上次同步每日胜场榜时的日期 YYYYMMDD */ rankLastSyncDailyWinsDate: string = ''; /** 排行榜昵称(授权写入为 UTF-8 Base64,读档/展示时解码) */ userName: string = ""; /** 排行榜头像 */ userAvatar: string = ""; /** 排行榜省份 */ userProvince: string = ""; /** 排行榜是否已弹过首次用户信息授权(1=已弹过,不再在打开时判断) */ rankUserAuthPrompted: number = 0; /** 是否已同步过排行榜分数 */ rankSyncedOnce: number = 0; /** 上次同步的省榜/总榜星数 */ rankLastSyncStars: number = -1; /** 上次同步的省份 */ rankLastSyncProvince: string = ""; /** 上次同步的排行榜昵称 */ rankLastSyncNick: string = ""; /** 上次同步的排行榜头像 */ rankLastSyncAvatar: string = ""; /** 上次同步的排行榜地区 */ rankLastSyncRegion: string = ""; /** 待领俸禄星级(总星数;0=无,领取后清零) */ emolumentSnapStars: number = 0; /** 待领俸禄胜场(领取后清零) */ emolumentSnapWins: number = 0; /** 待领俸禄快照日期 YYYYMMDD(等于今日=当日累计未到弹窗日;早于今日=可弹;隔日未上线仍保留) */ emolumentSnapDate: string = ''; /** GM:下次进 Home 弹俸禄(1=待弹,弹后清零) */ gmAwaitEmolumentPop: number = 0; /** 设置页选中头像 Headicon.id */ headIconId: number = 0; /** 是否已点击过战斗图鉴(1=已点击,不再显示 ui_illustrated 引导) */ battleIllustratedGuideClicked: number = 0; /**当前关卡 */ level: number = 1; /**所有道具数量 */ itemCount: string[] = []; /**存储所有状态 */ status: string[] = []; /**统计信息 */ statistics: string[] = []; /**引导状态 */ guide: string[] = []; /**其他数据 */ other: string[] = []; /**仅存本地缓存的数据,无需备份到服务器,数据丢失也没关系的数据 */ onlyLocal: string[] = []; /**上报失败次数 */ failCount: number = 0; /**上一次上报失败的次数 */ lastFailCount: number = 0; }