import { Vec3, Node, tween, misc, Vec2, lerp, v2, v3, sp, UIOpacity, assetManager, Sprite, SpriteFrame, AssetManager, Texture2D, isValid, UITransform, JsonAsset, sys } from "cc"; import { LZ4Lib } from "../../libs/LZ4Lib"; export default class MTools { //#region 格式化工具 /** * 中文数字单位(4位一级) */ public static chineseUnit = [``, `万`, `亿`, `兆`, `京`, `垓`, `秭`, `穰`, `沟`, `涧`, `正`, `载`, `极`, `恒河沙`, `阿僧祇`, `那由他`, `不可思议`, `无量大数`]; /** * 格式化中文单位的数字字符串 * @param param 参数 * @param param.num 需要格式化的数字或者字符串类型的数字 * @param param.fixed 保留小数位数 * @param param.fixedUnit 保留单位精确度(比如:123456,默认是12.3456万,传“亿”,则为:0.00123456亿,亿以上是什么单位还是什么单位) * @param param.limitUnit 强制使用fixedUnit字段指定的单位(无论数值多少均转化为此单位) * @param param.limitLen 限定使用单位的最小位数:例:limitLen:12,即12位以下不使用单位格式化,12位以上每超出一个单位提升一个单位,这是与fixedUnit类似的需求,不过这个字段是用位数做,更具体 * @param param.char 用于分隔较长的数字的分隔符,不传或传空字符串则不使用分隔符 * @param param.charLen 使用分隔符的位数,默认4位 * @param param.unitLen 使用单位的位数,即每个单位多少位数字,默认4位一个单位 * @param param.units 指定单位数组,可用来替换掉默认的中文单位数组 * @returns */ public static formatChineseUnit({ num, fixed = 2, fixedUnit = "", limitUnit = false, limitLen = 4, char = "", charLen = 4, unitLen = 4, units = [] }) { ////解析原始数字字符串 let result = ``;//定义结果 let unit = "";//单位 let left, right = ""; let numStr = this.scientificToDecimalString(num + ``); let arr = numStr.split("."); left = arr[0];//整数部分 if (arr.length > 1) right = arr[1];//小数部分 let index = Math.floor((left.length - 1) / unitLen); if (units.length == 0) units = this.chineseUnit; if (gg.lang.Current == 'en') { units = ["", "K", "M", "B", "T"] unitLen = 3; } ////是否指定了单位 if (fixedUnit != "") { //重新计算单位index let idx = units.indexOf(fixedUnit); if (limitUnit) {//无论是否超出指定单位固定使用指定的单位 if (idx >= 0) index = idx; } else {//否则只有超出指定单位后才使用指定单位 if (idx >= 0 && index > idx) index = idx; } } //计算整数位数 let intCount = left.length - unitLen * index; if (limitLen > 0) {//如果限制了多少位以下不使用单位,则重新计算单位索引和整数位位数 index = 0; if (left.length <= limitLen) { intCount = left.length; } else { let l = left.length; while (l > limitLen) { index++; l = l - unitLen; } intCount = left.length - unitLen * index; } } //计算总位数 let count = intCount + fixed; if (intCount > 0) {//如果整数位数>0,则正常计算 for (let i = 0; i < count; i++) { if (i < left.length) result += left[i]; if (i == intCount - 1 && fixed > 0) { result += `.`; } if (i >= left.length) { let s = "0"; if (i - left.length < right.length) { s = right[i - left.length]; result += s; } } } } else {//否则根据单位得出的整数位数补0 let absCount = Math.abs(intCount); count = index * unitLen; result = `0.`; for (let i = 0; i < count; i++) { let s = "0"; if (i >= absCount) { if (i - absCount < left.length) s = left[i - absCount]; else if (i - absCount - left.length < right.length) s = right[i - absCount - left.length]; } result += s; } } ////限定了按位数使用分隔符 if (char != "") { arr = result.split("."); left = arr[0];//整数部分 right = ""; if (arr.length > 1) right = arr[1];//小数部分 result = ""; let yushu = left.length % charLen; for (let i = 0; i < left.length; i++) { result += left[i]; if ((i == yushu - 1 || ((i + 1) - yushu) % charLen == 0) && i != left.length - 1) result += char; } if (right.length > 0) result += "."; for (let i = 0; i < right.length; i++) { result += right[i]; if (i != 0 && (i + 1) % charLen == 0 && i != right.length - 1) result += char; } } if (result.includes(".")) { result = result.replace(/0+$/, ''); result = result.replace(/\.$/, ''); } // if (result[result.length - 1] == ".") // result = result.replace(".", ""); //得到单位 let idx = index % units.length; let pCount = Math.floor(index / units.length); unit = units[idx]; if (pCount > 0) { unit += units[units.length - 1]; for (let i = 1; i < pCount; i++) { unit += "+"; } } result += unit; return result; } /** * 将科学计数法表示的字符串转换为完整数字字符串 * @param sciNum 科学计数法字符串 (e.g., "1.2e+13") * @returns 完整数字字符串 (e.g., "12000000000000") */ public static scientificToDecimalString(sciNum: string): string { // 分离底数和指数 const [basePart, expPart] = sciNum.toLowerCase().split('e'); if (!expPart) return sciNum; // 非科学计数法 const exponent = parseInt(expPart, 10); if (isNaN(exponent)) return sciNum; // 无效指数 // 分离整数和小数部分 let [integer = '', decimal = ''] = basePart.split('.'); // 处理负数 let sign = ''; if (integer.startsWith('-')) { sign = '-'; integer = integer.slice(1); } // 正指数处理(向右移动小数点) if (exponent > 0) { const totalLength = integer.length + decimal.length; const moveCount = Math.min(exponent, decimal.length); // 移动小数点后的部分 integer += decimal.slice(0, moveCount); decimal = decimal.slice(moveCount); // 需要补充的零位数 const zerosNeeded = exponent - moveCount; if (zerosNeeded > 0) { integer += '0'.repeat(zerosNeeded); } } // 负指数处理(向左移动小数点) else if (exponent < 0) { const absExponent = Math.abs(exponent); const zerosNeeded = Math.max(0, absExponent - integer.length); // 添加前置零 if (zerosNeeded > 0) { decimal = '0'.repeat(zerosNeeded) + integer + decimal; integer = '0'; } else { const movePoint = integer.length - absExponent; decimal = integer.slice(movePoint) + decimal; integer = integer.slice(0, movePoint); } } // 处理前导零(保留单个零) if (integer === '') integer = '0'; integer = integer.replace(/^0+(?=\d)/, '') || '0'; // 组合结果 return sign + integer + (decimal ? `.${decimal}` : ''); } /** * 对单位为秒的时间生成格式化时间字符串 * @param sec 时间s * @param format 格式化字符串 * @example * // 当format为string时,会以format中的最大时间单位进行格式化 * Tool.formatTimeString(3601, "m:s"); // 60:1 * Tool.formatTimeString(3601, "mm:ss"); // 60:01 * Tool.formatTimeString(3601, "hh:mm:ss"); // 01:00:01 * * // 当format为object时,会以传入的sec计算最大的时间单位,并选择format对应的字符串进行格式化 * Tool.formatTimeString(100, { * S: "s秒", * M: "m分s秒", * H: "h时m分s秒", * D: "d天h时m分s秒" * }); // 1分40秒 * Tool.formatTimeString(100000, { * S: "s秒", * M: "m分s秒", * H: "h时m分s秒", * D: "d天h时m分s秒" * }); // 1天3时46分40秒 */ public static formatTimeString(sec: number, format: string | { "S": string; "M": string; "H": string; "D": string } = "hh:mm:ss"): string { let seconds: number = Math.floor(sec); let minutes: number = Math.floor(seconds / 60); let hours: number = Math.floor(seconds / 3600); let days: number = Math.floor(seconds / 86400); let maxUnit = 0 let result: string = ""; if (typeof format === "string") { // 查询格式化字符串中最大的单位 result = format; if (/d/i.test(format)) { maxUnit = 3; } else if (/h/i.test(format)) { maxUnit = 2; } else if (/m/i.test(format)) { maxUnit = 1; } } else { // 以传入的数值判断最大单位 if (days > 0) { maxUnit = 3; result = format.D; } else if (hours > 0) { maxUnit = 2; result = format.H; } else if (minutes > 0) { maxUnit = 1; result = format.M; } else { maxUnit = 0; result = format.S; } } if (maxUnit > 0) { seconds %= 60; } if (maxUnit > 1) { minutes %= 60; } if (maxUnit > 2) { hours %= 24; } let data = { d: days, hh: hours < 10 ? `0${hours}` : `${hours}`, h: hours, mm: minutes < 10 ? `0${minutes}` : `${minutes}`, m: minutes, ss: seconds < 10 ? `0${seconds}` : `${seconds}`, s: seconds }; result = result.toLowerCase(); for (const key in data) { const value = data[key]; result = result.replace(new RegExp(key, "g"), value); } return result; } /** * 时间格式化 * @param date 时间对象 * @param fmt 格式化字符(yyyy-MM-dd hh:mm:ss S) */ public static format(date: Date, fmt: string) { var o: any = { "M+": date.getMonth() + 1, // 月份 "d+": date.getDate(), // 日 "h+": date.getHours(), // 小时 "m+": date.getMinutes(), // 分 "s+": date.getSeconds(), // 秒 "q+": Math.floor((date.getMonth() + 3) / 3), // 季度 "S": date.getMilliseconds() // 毫秒 }; if (/(y+)/.test(fmt)) { fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length)); } for (var k in o) { if (new RegExp("(" + k + ")").test(fmt)) { fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length))); } } return fmt; } /** * 超高性能:仅格式化时分秒 * @param time 时间(默认单位秒,isMillisecond字段传true,会视该字段为毫秒) * @param format 格式化字符串 * @param isMillisecond 是否毫秒 * @returns */ public static formatHMS(time: number, format: string = 'HH:mm:ss', isMillisecond: boolean = false): string { const totalSeconds = isMillisecond ? Math.floor(time / 1000) : time; const h = Math.floor(totalSeconds / 3600) % 24; const m = Math.floor(totalSeconds / 60) % 60; const s = totalSeconds % 60; const pad = (n: number) => (n < 10 ? '0' + n : `${n}`); // 使用正则表达式全局替换,确保所有匹配项都被替换 let result = format; result = result.replace(/HH/g, pad(h)); result = result.replace(/mm/g, pad(m)); result = result.replace(/ss/g, pad(s)); return result; } //#endregion //#region 时间工具 /** * 冷却节流:在 time ms 内对同一 flag 再次调用返回 true(应跳过),首次/冷却结束后返回 false(应执行)。 * * 正确用法(二选一,勿取反搞反): * - 跳过:`if (MTools.beforeTimes(200, "btn")) return;` * - 执行:`if (!MTools.beforeTimes(200, "sfx")) { play(); }` * * 错误示例(曾导致音效/秒伤 UI 几乎不更新): * - `return beforeTimes(...)` 当作「应播放」 * - `if (!beforeTimes(...)) return` 当作「冷却内跳过」 * * flag 必须按用途拆开(如按 weaponId / 音效名),禁止多系统共用一个 key。 */ public static beforeTimes(time: number, flag = "default") { let tIdex = this._time_before_map.get(flag); if (tIdex) { return true; } let t = setTimeout(() => { clearTimeout(t); this._time_before_map.set(flag, null); }, time); this._time_before_map.set(flag, t); return false; } private static _time_before_map: Map = new Map(); /** * 进入大厅/主界面时清空节流 Map,并取消尚未触发的定时器。 * 否则带 monster.uuid 等动态 key 会长期占槽(超时后 value 为 null 仍保留 key)。 */ public static clearBeforeTimesMapForEnterHall(): void { for (const h of this._time_before_map.values()) { if (h != null) { clearTimeout(h as any); } } this._time_before_map.clear(); } /** * 延迟执行 * @param time 时间(秒) * @returns */ public static delay(time: number) { return new Promise((resolve) => { setTimeout(() => { resolve(null); }, time * 1000); }); } //#endregion //#region spine工具 /** * 播放spine动画 * @param sk spine组件 * @param name 动画名称 * @param callback 播完回调 * @param timeScale 时间缩放 * @param loop 是否循环 */ public static playSpine(sk: sp.Skeleton, name: string, callback: Function = null, timeScale = 1, loop = false) { sk.setCompleteListener(() => { sk.setCompleteListener(null); if (callback) callback(); }); sk.timeScale = timeScale; sk.setAnimation(0, name, loop); } //#endregion //#region 数组工具 /** * 获取随机数组成员 * @param array 目标数组 */ public static getRandomValueInArray(array: any[]): any { let newArray = array[Math.floor(Math.random() * array.length)]; return newArray; } //获取随机值 /** * 获取随机值 * @param limitMin 最小值 * @param limitMax 最大值 * @returns */ public static getRandomValue(limitMin: number, limitMax: number): number { return Math.floor(Math.random() * (limitMax - limitMin + 1) + limitMin) } /** * 根据权重抽奖 * @param lotteryitems 抽奖列表 * @param nameStr 权重字段名 */ public static lottery(lotteryitems, nameStr = "weight") { let resultItem = null; let allWeight = 0; for (let i = 0; i < lotteryitems.length; i++) { let x = lotteryitems[i]; allWeight += x[nameStr]; } let r = Math.random() * allWeight; let weight = 0; for (let i = 0; i < lotteryitems.length; i++) { let x = lotteryitems[i]; if (r >= weight && r < weight + x[nameStr]) { resultItem = x; break; } weight += x[nameStr]; } return resultItem; } /** * 从数组中随机获取count个不同的元素 * @param arr 原数组 * @param count 个数 * @returns */ public static getRandomElements(arr: T[], count: number): T[] { if (count > arr.length) { throw new Error('Count cannot be greater than the array length'); } let shuffled = arr.slice(0); // Clone the array for (let i = shuffled.length - 1; i > 0; i--) { let j = Math.floor(Math.random() * (i + 1)); // Random index from 0 to i [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; // Swap elements } return shuffled.slice(0, count); } //#endregion //#region 对象工具 /** * 深拷贝 * @param target 目标对象 */ public static 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)) { const 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; } const cloneObject: { [key: string]: any } = {}; visited.set(target, cloneObject); for (const 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; } //#endregion //#region 缓动工具 /** * 二阶贝塞尔曲线运动 * @param target 目标 * @param duration 时间 * @param c1 起始点 * @param c2 控制点 * @param to 终点 * @param opts 自定义选项 * @returns */ public static bezierTo(target: any, duration: number, c1: Vec3, c2: Vec3, to: Vec3, opts: any) { opts = opts || Object.create(null); opts.onUpdate = (arg: Vec3, ratio: number) => { target.position = this.twoBezier(ratio, c1, c2, to); }; return tween(target).to(duration, { position: to }, opts); } /** * @desc 二阶贝塞尔 * @param {number} t 当前百分比 * @param {} p1 起点坐标 * @param {} cp 控制点 * @param {} p2 终点坐标 * @returns {any} */ public static twoBezier(t: number, p1: Vec3, cp: Vec3, p2: Vec3) { let x = (1 - t) * (1 - t) * p1.x + 2 * t * (1 - t) * cp.x + t * t * p2.x; let y = (1 - t) * (1 - t) * p1.y + 2 * t * (1 - t) * cp.y + t * t * p2.y; return v3(x, y, 0); } /** * 跳跃到指定位置 * @param to 目标位置 * @param height 跳跃高度 * @returns */ public static jumpToV3(to: Vec3, height: number): Readonly { return { value: to, progress: this.jumpToProgress(3, height) } as any as Vec3; } /** * 跳跃到指定位置 * @param to 目标位置 * @param height 跳跃高度 * @returns */ public static jumpToV2(to: Vec3, height: number): Readonly { return { value: to, progress: this.jumpToProgress(2, height) } as any as Vec2; } /** * 黑屏过场 * @param time 时间 * @param callback 回调 * @returns */ public static tweenBlackScene(maskNode: Node, time: number, callback: Function = null) { return new Promise((resolve) => { tween(maskNode) .to(time * 0.4, { opacity: 255 }) .call(() => { if (callback) callback(); resolve(true); }) .delay(time * 0.2) .to(time * 0.4, { opacity: 0 }) .call(() => { maskNode.removeFromParent(); }) .start(); }) } //#endregion //#region 数学工具 /** * 计算指定容器下适配目标等比缩放的尺寸 * @param contentWidth 容器宽 * @param contentHeight 容器高 * @param targetWidth 目标宽 * @param targetHeight 目标高 * @returns 返回适配后的尺寸 */ public static adaptSize(contentWidth: number, contentHeight: number, targetWidth: number, targetHeight: number) { let result = v2(); let scale = 1; let isWidth = false; if (contentWidth >= contentHeight) { if (targetWidth > targetHeight && contentWidth / contentHeight < targetWidth / targetHeight) { isWidth = true; } } else { if (targetWidth > targetHeight && contentWidth / contentHeight > targetWidth / targetHeight) { isWidth = true; } } if (isWidth) { scale = contentWidth / targetWidth; } else { scale = contentHeight / targetHeight; } result.x = targetWidth * scale; result.y = targetHeight * scale; return result; } /** * 旋转向量一定角度后得到新的向量 * @param vector 原向量 * @param angle 旋转角度 * @returns */ public static rotateVector(vector, angle) { // 将角度转换为弧度 let radians = misc.degreesToRadians(angle); // 计算旋转后的向量 let x = vector.x * Math.cos(radians) - vector.y * Math.sin(radians); let y = vector.x * Math.sin(radians) + vector.y * Math.cos(radians); return v2(x, y); } //#endregion //#region 图片工具 /** * 获取默认图片(像素转SpriteFrame) * @param r 红色通道 * @param g 绿色通道 * @param b 蓝色通道 * @param a 透明度通道 */ public static getDefaultSpriteFrame(r = 255, g = 255, b = 255, a = 255) { let buffer = Uint8Array.from([r, g, b, a]); let spriteFrame = new SpriteFrame(); //默认一张白色纹理 let tex = new Texture2D(); // /包含 RGBA 四通道的 32 位整形像素格式:RGBA8888。 一字节8位 tex.reset({ width: 1, height: 1, format: Texture2D.PixelFormat.RGBA8888, mipmapLevel: 0 }); tex.uploadData(buffer, 0, 0); // 更新 0 级 Mipmap。 tex.updateImage(); spriteFrame.texture = tex; spriteFrame.packable = false; return spriteFrame; } //#endregion //#region 私有方法 /** * 跳跃到指定位置 * @param max 最大跳跃次数 * @param height 跳跃高度 * @returns */ private static jumpToProgress(max: number, height: number): (from: number, to: number, cur: number, pcs: number) => number { let i = max; let heightSqrt = Math.sqrt(height); return (from: number, to: number, cur: number, pcs: number) => { // 使用序列耦合区分xyz轴: 1: x, 2: y, 3: z if (i >= max) i = 1; else i++; // let rsl = from + (to - from) * pcs; // lerp let rsl = lerp(from, to, pcs); if (i === 2) { // y轴的增量算法 let du = Math.abs(1 - pcs * 2); // [0,1] > [1,0,1] rsl += height - Math.pow(heightSqrt * du, 2); } return rsl; }; } public static getRandomByWeightArray(num, qualityArray, weightArray) { //num = 8 //qualityArray = [22,24] // weightArray = [1,1] //根据权重随机获取num个元素,可以重复 //return [22,22,24,24,22,24,22,24] //转数字 qualityArray = qualityArray.map(Number) weightArray = weightArray.map(Number) if (qualityArray.length !== weightArray.length) { console.log('qualityArray and weightArray must have the same length') return [] } if (num <= 0 || qualityArray.length === 0) { return []; } const result = []; // 计算总权重 const totalWeight = weightArray.reduce((sum, weight) => sum + weight, 0); for (let i = 0; i < num; i++) { // 生成随机数 const random = Math.random() * totalWeight; // 根据权重选择元素 let weightSum = 0; for (let j = 0; j < weightArray.length; j++) { weightSum += weightArray[j]; if (random <= weightSum) { result.push(qualityArray[j]); break; } } } return result; } //长数组 [21, 25, 25, 25, 21, 25, 21, 21, 25, 22]合并为['21|4', '25|4', '22|1'] public static mergeToStatsArray(arr) { const countMap = new Map(); // 统计每个数字出现的次数 for (const num of arr) { countMap.set(num, (countMap.get(num) || 0) + 1); } // 转换为目标格式 const result = []; for (const [num, count] of countMap) { result.push(`${num}|${count}`); } return result; } public static getNodeToTargetPos(curNode, targetNode) { //目标节点的本地坐标 let targetLocalPos = targetNode.getPosition() //目标节点的世界坐标 let targetWorldPos = targetNode.parent.getComponent(UITransform).convertToWorldSpaceAR(targetLocalPos) //目标节点相对于当前节点父节点的本地坐标 let localPos = curNode.parent.getComponent(UITransform).convertToNodeSpaceAR(targetWorldPos) return localPos } public static setNodeToTargetPos(curNode, targetNode) { //目标节点的本地坐标 if (isValid(curNode)) { } else { return v2(0, 0) } if (isValid(targetNode) && isValid(targetNode.parent)) { } else { return v2(0, 0) } let targetLocalPos = targetNode.getPosition() //目标节点的世界坐标 let targetWorldPos = targetNode.parent.getComponent(UITransform).convertToWorldSpaceAR(targetLocalPos) //目标节点相对于当前节点父节点的本地坐标 let localPos = curNode.parent.getComponent(UITransform).convertToNodeSpaceAR(targetWorldPos) curNode.setPosition(localPos) } public static stringToAsciiUint8Array(str: string): Uint8Array { const uint8Array = new Uint8Array(str.length); for (let i = 0; i < str.length; i++) { uint8Array[i] = str.charCodeAt(i) & 0xFF; // 只取低8位 } return uint8Array; } public static buildStringFromUint8Array(uint8Array: Uint8Array): string { let result = ''; for (let i = 0; i < uint8Array.length; i++) { result += String.fromCharCode(uint8Array[i]); } return result; } /** * 压缩JSON数据的方法 */ public static compressJsonData(mergeJson) { try { // 方法1:从文件加载JSON(如果JSON文件在resources目录下) // const jsonAsset = await loadRes('path/to/your/json.json', JsonAsset); // 方法2:直接使用JSON对象 // const jsonData = { // "chapterId": 1, // "waveNum": 3, // "obstacles": [ // {"type": "Cone", "x": 100, "y": 200}, // {"type": "Wall", "x": 300, "y": 400} // ] // }; let func = (jsonData2) => { // 将JSON对象转换为字符串 let jsonString = JSON.stringify(jsonData2); // 将字符串转换为Uint8Array let textEncoder = new TextEncoder(); let uint8Array = textEncoder.encode(jsonString); // let uint8Array = this.stringToAsciiUint8Array(jsonString) // let uint8Array = this.utf8StringToUint8Array(jsonString) // 调用LZ4压缩 let compressedData = LZ4Lib.lz4.compress(uint8Array); // let timestamp2 = Date.now(); // this.saveUint8ArrayToFile(compressedData, `compressed_data_${timestamp2}.bin`); // return console.log('原始数据大小:', uint8Array.length, '字节'); console.log('压缩后数据大小:', compressedData); console.log('压缩后数据大小长度:', compressedData.length); console.log('压缩率:', ((1 - compressedData.length / uint8Array.length) * 100).toFixed(2) + '%'); // const decoder = new TextDecoder("utf-8"); // let strDecoded = decoder.decode(compressedData,{ // stream: true // }); let strDecoded = this.buildStringFromUint8Array(compressedData) console.log('解码后数据', strDecoded); console.log('解码后数据长度', strDecoded.length); let str = ZYSDK.ZYSDK.b64encode(strDecoded); // let str = Base64.encode(compressedData) console.log('编码后数据', str); // 保存压缩后的数据到文件 // 使用时间戳作为文件名的一部分,避免覆盖 // compressedData = textEncoder.encode(str); // compressedData = this.stringToAsciiUint8Array(str) compressedData = this.stringToAsciiUint8Array(str) const timestamp = Date.now(); this.saveUint8ArrayToFile(compressedData, `compressed_data_${timestamp}.bin`); } func(mergeJson) } catch (error) { console.error('JSON压缩失败:', error); } } public static decompressJsonData(strConfig) { try { // 兼容两种 base64 解码方式: // 1) 旧链路:ZYSDK.ZYSDK.b64decode // 2) 构建/服务端标准 base64 字节流 let uint8Array: Uint8Array = null; try { let strData = ZYSDK.ZYSDK.b64decode(strConfig); console.log('解码后数据长度==', strData?.length ?? 0); uint8Array = this.stringToAsciiUint8Array(strData); } catch (e) { console.warn('ZYSDK b64decode失败,尝试标准Base64解码', e); } if (!uint8Array || uint8Array.length <= 0) { uint8Array = this.base64ToAsciiUint8Array(strConfig); console.log('标准Base64解码后长度==', uint8Array?.length ?? 0); } // const timestamp = Date.now(); // this.saveUint8ArrayToFile(uint8Array, `compressed_data_${timestamp}.bin`); // return // console.log('解码后uint8Array数据长度==',uint8Array.length); const decompressedData = LZ4Lib.lz4.decompress(uint8Array); // const decoder = new TextDecoder("utf-8"); // const decompressedString = decoder.decode(decompressedData, { // stream: true // }); let decompressedString = this.uint8ArrayToUtf8String(decompressedData) const originalJson = JSON.parse(decompressedString); // console.log('解压后的数据:', originalJson); return originalJson; } catch (error) { console.error('JSON解压失败:', error); return null; } } /**将Base64字符串解码成ASCII字节流(每字节 0~255) */ public static base64ToAsciiUint8Array(base64Str: string): Uint8Array { const clean = (base64Str || '').replace(/\s/g, ''); if (!clean) return new Uint8Array(0); let binary = ''; if (typeof atob !== 'undefined') { binary = atob(clean); } else { // atob 不可用时,使用纯 JS base64 解码 const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; let str = clean; let output = ''; let i = 0; while (i < str.length) { const enc1 = chars.indexOf(str.charAt(i++)); const enc2 = chars.indexOf(str.charAt(i++)); const enc3 = chars.indexOf(str.charAt(i++)); const enc4 = chars.indexOf(str.charAt(i++)); const chr1 = (enc1 << 2) | (enc2 >> 4); const chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); const chr3 = ((enc3 & 3) << 6) | enc4; output += String.fromCharCode(chr1); if (enc3 !== 64) output += String.fromCharCode(chr2); if (enc4 !== 64) output += String.fromCharCode(chr3); } binary = output; } return this.stringToAsciiUint8Array(binary); } /** * 将Uint8Array数据保存为文件 * @param uint8Array 要保存的Uint8Array数据 * @param fileName 文件名 */ public static saveUint8ArrayToFile(uint8Array: Uint8Array, fileName: string): void { // 方式1:浏览器环境下的下载方式 if (typeof document !== 'undefined' && typeof Blob !== 'undefined' && typeof URL !== 'undefined') { const blob = new Blob([uint8Array], { type: 'application/octet-stream' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = fileName; // 模拟点击下载 document.body.appendChild(a); a.click(); // 清理 setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 100); console.log(`文件 ${fileName} 已开始下载`); } // // 方式2:Cocos Creator 环境 // else if (typeof cc !== 'undefined') { // // 检查是否为原生平台 // if (cc.sys && cc.sys.isNative) { // // 原生平台需要使用文件系统API // const fs = require('fs'); // const path = require('path'); // // 获取可写目录 // const writablePath = cc.sys.localStorageDirectory; // const filePath = path.join(writablePath, fileName); // try { // // 将Uint8Array转换为Buffer并写入文件 // fs.writeFileSync(filePath, Buffer.from(uint8Array)); // console.log(`文件已保存到原生平台:${filePath}`); // } catch (error) { // console.error('原生平台文件保存失败:', error); // } // } // // Web平台但没有document对象的情况 // else { // console.warn('当前环境不支持直接保存文件,请考虑其他存储方式'); // // 可以选择存储到localStorage(对于小数据) // if (typeof localStorage !== 'undefined') { // const base64 = this.uint8ArrayToBase64(uint8Array); // localStorage.setItem('compressed_data', base64); // console.log('数据已存储到localStorage'); // } // } // } // // 方式3:Node.js环境(如构建脚本) // else if (typeof require !== 'undefined') { // try { // const fs = require('fs'); // const path = require('path'); // const filePath = path.join(process.cwd(), fileName); // fs.writeFileSync(filePath, Buffer.from(uint8Array)); // console.log(`文件已保存到:${filePath}`); // } catch (error) { // console.error('Node.js环境下文件保存失败:', error); // } // } } /** * 将Uint8Array转换为Base64字符串 * @param uint8Array Uint8Array数据 * @returns Base64字符串 */ public static uint8ArrayToBase64(uint8Array: Uint8Array): string { let binary = ''; const len = uint8Array.byteLength; for (let i = 0; i < len; i++) { binary += String.fromCharCode(uint8Array[i]); } // 使用环境中可用的btoa方法 if (typeof btoa !== 'undefined') { return btoa(binary); } // // Node.js环境 // else if (typeof Buffer !== 'undefined') { // return Buffer.from(binary).toString('base64'); // } throw new Error('当前环境不支持Base64编码'); } /** * 将UTF-8字符串转换为Uint8Array * @param str UTF-8字符串 * @returns Uint8Array */ public static utf8StringToUint8Array(str: string): Uint8Array { // 兼容处理:手动实现UTF-8编码 let utf8 = []; for (let i = 0; i < str.length; i++) { let charcode = str.charCodeAt(i); if (charcode < 0x80) { utf8.push(charcode); } else if (charcode < 0x800) { utf8.push(0xc0 | (charcode >> 6), 0x80 | (charcode & 0x3f)); } else if (charcode < 0xd800 || charcode >= 0xe000) { utf8.push(0xe0 | (charcode >> 12), 0x80 | ((charcode >> 6) & 0x3f), 0x80 | (charcode & 0x3f)); } else { // 处理代理对 i++; charcode = ((charcode & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff) + 0x10000; utf8.push( 0xf0 | (charcode >> 18), 0x80 | ((charcode >> 12) & 0x3f), 0x80 | ((charcode >> 6) & 0x3f), 0x80 | (charcode & 0x3f) ); } } return new Uint8Array(utf8); } /** * 将Uint8Array转换为UTF-8字符串 * @param uint8Array Uint8Array数据 * @returns UTF-8字符串 */ public static uint8ArrayToUtf8String(uint8Array: Uint8Array): string { // 兼容处理:手动实现UTF-8解码 let str = ''; let i = 0; while (i < uint8Array.length) { let byte1 = uint8Array[i]; if (byte1 < 0x80) { // 单字节字符 str += String.fromCharCode(byte1); i++; } else if (byte1 < 0xe0) { // 双字节字符 let byte2 = uint8Array[i + 1]; str += String.fromCharCode(((byte1 & 0x1f) << 6) | (byte2 & 0x3f)); i += 2; } else if (byte1 < 0xf0) { // 三字节字符 let byte2 = uint8Array[i + 1]; let byte3 = uint8Array[i + 2]; str += String.fromCharCode(((byte1 & 0x0f) << 12) | ((byte2 & 0x3f) << 6) | (byte3 & 0x3f)); i += 3; } else { // 四字节字符 let byte2 = uint8Array[i + 1]; let byte3 = uint8Array[i + 2]; let byte4 = uint8Array[i + 3]; let codePoint = ((byte1 & 0x07) << 18) | ((byte2 & 0x3f) << 12) | ((byte3 & 0x3f) << 6) | (byte4 & 0x3f); // 转换为代理对 codePoint -= 0x10000; let highSurrogate = (codePoint >> 10) + 0xd800; let lowSurrogate = (codePoint & 0x3ff) + 0xdc00; str += String.fromCharCode(highSurrogate, lowSurrogate); i += 4; } } return str; } //#endregion /** * 获取一个稳定的中文名字(适合长期标识用户,使用浏览器指纹方式) * @returns 稳定的中文名字 */ static async getStableName() { const fp = await FingerprintJS.load(); const { visitorId } = await fp.get(); const hashValue = parseInt(visitorId.substring(0, 8), 16); console.log(visitorId); console.log(hashValue); return hashValue + ''; } //拷贝文本 static CopyTextEvent(copyStr: string) { if (sys.os == "Android") { // setTimeout(() => { // jsb.reflection.callStaticMethod("com/cocos/game/AppActivity", "JavaCopy", "(Ljava/lang/String;)V", copyStr); // }, 100); } else { const el = document.createElement('textarea'); el.value = copyStr; // Prevent keyboard from showing on mobile el.setAttribute('readonly', ''); //el.style.contain = 'strict'; el.style.position = 'absolute'; el.style.left = '-9999px'; el.style.fontSize = '12pt'; // Prevent zooming on iOS const selection = getSelection()!; let originalRange; if (selection.rangeCount > 0) { originalRange = selection.getRangeAt(0); } document.body.appendChild(el); el.select(); // Explicit selection workaround for iOS el.selectionStart = 0; el.selectionEnd = copyStr.length; let success = false; try { success = document.execCommand('copy'); } catch (err) { } document.body.removeChild(el); if (originalRange) { selection.removeAllRanges(); selection.addRange(originalRange); } } } } declare var FingerprintJS: any;