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.
1188 lines
42 KiB
1188 lines
42 KiB
|
1 week ago
|
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;
|
||
|
|
|