消消方块阵换皮表情
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.
 
 
 

1187 lines
42 KiB

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<string, any> = new Map<string, any>();
/**
* 进入大厅/主界面时清空节流 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<T>(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<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)) {
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<Vec3> {
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<Vec2> {
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;