You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
AdLike2048/assets/script/game/GameData.ts

1476 lines
61 KiB

3 weeks ago
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&
let local: Record<string, unknown> | null = null;
if (LocalStr) {
try {
const parsed = JSON.parse(LocalStr);
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
local = parsed as Record<string, unknown>;
}
} 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 {
3 weeks ago
const bin = this._base64Decode(s);
if (!bin) return false;
3 weeks ago
for (let i = 0; i < bin.length; i++) {
const c = bin.charCodeAt(i);
if (c < 32 && c !== 9 && c !== 10 && c !== 13) return false;
3 weeks ago
if (c === 127) return false;
3 weeks ago
}
return true;
} catch {
return false;
}
}
3 weeks ago
/** Base64 → latin1 字符串(不依赖 atob,小游戏环境兜底);非法输入返回空串 */
private _base64Decode(b64: string): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
const lookup: Record<string, number> = {};
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;
}
3 weeks ago
/** 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<string, unknown>;
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<typeof setTimeout> | 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<T>(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);
1 week ago
// Item 表无 weight:同品质碎片池等权
weightArray1.push(1);
3 weeks ago
}
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<ITableWeapon>(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;
}