import { UITransform, Vec2, Vec3,Node, _decorator, find, director, Color, UIOpacity, tween, Label, Camera, view, Quat, math } from "cc"; const { ccclass, property } = _decorator; import sensitiveArray from '../extend/SensitiveWords'; @ccclass("Utils") export class Utils { /** * 验证字符串是否是空的 * @s 字符串 */ public static isNull(s: string) { if(s == "" || s == null || s == undefined){ return true; } if (typeof s === 'string'){ let re = new RegExp("^[ ]+$"); return re.test(s); }else{ return false; } } /** * 合并多个字典 * @param args */ public static merge(...args) { let mergeFn = (arg1: T, arg2: U) : (T & U) =>{ let res = {} as (T & U); res = Object.assign(arg1, arg2); return res; }; let nDict = {}; args.forEach(obj => { nDict = mergeFn(nDict,obj); }); return nDict; } /** * 排除合并对象,但排除'reward'和'dec'属性(如果e中已存在) * mergeWithExceptions(e, pData, ['reward', 'dec']); * @param target * @param source * @param exclude * @returns */ public static mergeWithExceptions(target: any, source: any, exclude: string[]) { Object.keys(source).forEach(key => { if (!exclude.includes(key) || !(key in target)) { target[key] = source[key]; } }); return target; } /** * 深度拷贝 * @param {any} sObj 拷贝的对象 * @returns */ public static clone(sObj: any) { if (sObj === null || typeof sObj !== "object") { return sObj; } let s: { [key: string]: any } = {}; if (sObj.constructor === Array) { s = []; } for (let i in sObj) { if (sObj.hasOwnProperty(i)) { s[i] = this.clone(sObj[i]); } } return s; } /** * 转换数字 */ public static formatUnits(num: number): string { if (num >= 1e8) { return (num / 1e8).toFixed(1) + '亿'; } else if (num >= 1e7) { return (num / 1e7).toFixed(1) + '千万'; } else if (num >= 1e6) { return (num / 1e6).toFixed(1) + '百万'; } else if (num >= 1e4) { return (num / 1e4).toFixed(1) + '万'; } else if (num >= 1000) { return (num / 1000).toFixed(1) + 'k'; } else { return num.toString(); } } /** * 将object转化为数组 * @param { any} srcObj * @returns */ public static objectToArray(srcObj: { [key: string]: any }) { let resultArr: any[] = []; // to array for (let key in srcObj) { if (!srcObj.hasOwnProperty(key)) { continue; } resultArr.push(srcObj[key]); } return resultArr; } /** * !#zh 将数组转化为object。 */ /** * 将数组转化为object。 * @param { any} srcObj * @param { string} objectKey * @returns */ public static arrayToObject(srcObj: any, objectKey: string) { let resultObj: { [key: string]: any } = {}; // to object for (var key in srcObj) { if (!srcObj.hasOwnProperty(key) || !srcObj[key][objectKey]) { continue; } resultObj[srcObj[key][objectKey]] = srcObj[key]; } return resultObj; } /** * 根据权重,计算随机内容 * @param {arrany} weightArr * @param {number} totalWeight 权重 * @returns */ public static getWeightRandIndex(weightArr: [], totalWeight: number) { let randWeight: number = Math.floor(Math.random() * totalWeight); let sum: number = 0; for (var weightIndex: number = 0; weightIndex < weightArr.length; weightIndex++) { sum += weightArr[weightIndex]; if (randWeight < sum) { break; } } return weightIndex; } /** * 从n个数中获取m个随机数 * @param {Number} n 总数 * @param {Number} m 获取数 * @returns {Array} array 获取数列 */ public static getRandomNFromM(n: number, m: number) { let array: any[] = []; let intRd: number = 0; let count: number = 0; while (count < m) { if (count >= n + 1) { break; } intRd = this.getRandomInt(0, n); var flag = 0; for (var i = 0; i < count; i++) { if (array[i] === intRd) { flag = 1; break; } } if (flag === 0) { array[count] = intRd; count++; } } return array; } /** * 获取随机整数 * @param {Number} min 最小值 * @param {Number} max 最大值 * @returns */ public static getRandomInt(min: number, max: number) { let r: number = Math.random(); let rr: number = r * (max - min + 1) + min; return Math.floor(rr); } /** * 获取字符串长度 * @param {string} render * @returns */ public static getStringLength(render: string) { let strArr: string = render; let len: number = 0; for (let i: number = 0, n = strArr.length; i < n; i++) { let val: number = strArr.charCodeAt(i); if (val <= 255) { len = len + 1; } else { len = len + 2; } } return Math.ceil(len / 2); } /** * 要从一个数组模型中随机取出 n 个元素 * @param arr * @param n * @returns 返回一个新的数组 */ public static getRandomElements(arr: T[], n: number): T[] { if (n <= 0) return []; // 如果 n 小于等于 0,返回空数组 //复制数组以避免修改原数组 const copy = [...arr]; //Fisher-Yates 洗牌算法 for (let i = copy.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [copy[i], copy[j]] = [copy[j], copy[i]]; } //如果 n 超过数组长度,返回乱序后的整个数组 if (n >= copy.length) { return copy; } //返回前 n 个元素 return copy.slice(0, n); } /** * 判断传入的参数是否为空的Object。数组或undefined会返回false * @param obj */ public static isEmptyObject(obj: any) { let result: boolean = true; if (obj && obj.constructor === Object) { for (var key in obj) { if (obj.hasOwnProperty(key)) { result = false; break; } } } else { result = false; } return result; } /** * 判断是否是新的一天 * @param {Object|Number} dateValue 时间对象 todo MessageCenter 与 pve 相关的时间存储建议改为 Date 类型 * @returns {boolean} */ public static isNewDay(dateValue: any) { // todo:是否需要判断时区? var oldDate: any = new Date(dateValue); var curDate: any = new Date(); var oldYear = oldDate.getYear(); var oldMonth = oldDate.getMonth(); var oldDay = oldDate.getDate(); var curYear = curDate.getYear(); var curMonth = curDate.getMonth(); var curDay = curDate.getDate(); if (curYear > oldYear) { return true; } else { if (curMonth > oldMonth) { return true; } else { if (curDay > oldDay) { return true; } } } return false; } /** * 获取对象属性数量 * @param {object}o 对象 * @returns */ public static getPropertyCount(o: Object) { var n, count = 0; for (n in o) { if (o.hasOwnProperty(n)) { count++; } } return count; } /** * 返回一个差异化数组(将array中diff里的值去掉) * @param array * @param diff */ public static difference(array: [], diff: any) { let result: any[] = []; if (array.constructor !== Array || diff.constructor !== Array) { return result; } let length = array.length; for (let i: number = 0; i < length; i++) { if (diff.indexOf(array[i]) === -1) { result.push(array[i]); } } return result; } public static _stringToArray(string: string) { // 用于判断emoji的正则们 var rsAstralRange = '\\ud800-\\udfff'; var rsZWJ = '\\u200d'; var rsVarRange = '\\ufe0e\\ufe0f'; var rsComboMarksRange = '\\u0300-\\u036f'; var reComboHalfMarksRange = '\\ufe20-\\ufe2f'; var rsComboSymbolsRange = '\\u20d0-\\u20ff'; var rsComboRange = rsComboMarksRange + reComboHalfMarksRange + rsComboSymbolsRange; var reHasUnicode = RegExp('[' + rsZWJ + rsAstralRange + rsComboRange + rsVarRange + ']'); var rsFitz = '\\ud83c[\\udffb-\\udfff]'; var rsOptVar = '[' + rsVarRange + ']?'; var rsCombo = '[' + rsComboRange + ']'; var rsModifier = '(?:' + rsCombo + '|' + rsFitz + ')'; var reOptMod = rsModifier + '?'; var rsAstral = '[' + rsAstralRange + ']'; var rsNonAstral = '[^' + rsAstralRange + ']'; var rsRegional = '(?:\\ud83c[\\udde6-\\uddff]){2}'; var rsSurrPair = '[\\ud800-\\udbff][\\udc00-\\udfff]'; var rsOptJoin = '(?:' + rsZWJ + '(?:' + [rsNonAstral, rsRegional, rsSurrPair].join('|') + ')' + rsOptVar + reOptMod + ')*'; var rsSeq = rsOptVar + reOptMod + rsOptJoin; var rsSymbol = '(?:' + [rsNonAstral + rsCombo + '?', rsCombo, rsRegional, rsSurrPair, rsAstral].join('|') + ')'; var reUnicode = RegExp(rsFitz + '(?=' + rsFitz + ')|' + rsSymbol + rsSeq, 'g'); var hasUnicode = function (val: any) { return reHasUnicode.test(val); }; var unicodeToArray = function (val: any) { return val.match(reUnicode) || []; }; var asciiToArray = function (val: any) { return val.split(''); }; return hasUnicode(string) ? unicodeToArray(string) : asciiToArray(string); } // 模拟传msg的uuid public static simulationUUID() { function s4() { return Math.floor((1 + Math.random()) * 0x10000) .toString(16) .substring(1); } return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); } public static trim(str: string) { return str.replace(/(^\s*)|(\s*$)/g, ""); } /** * 判断当前时间是否在有效时间内 * @param {String|Number} start 起始时间。带有时区信息 * @param {String|Number} end 结束时间。带有时区信息 */ public static isNowValid(start: any, end: any) { var startTime = new Date(start); var endTime = new Date(end); var result = false; if (startTime.getDate() + '' !== 'NaN' && endTime.getDate() + '' !== 'NaN') { var curDate = new Date(); result = curDate < endTime && curDate > startTime; } return result; } /** * 返回相隔天数 * @param start * @param end * @returns */ public static getDeltaDays(start: any, end: any) { start = new Date(start); end = new Date(end); let startYear: number = start.getFullYear(); let startMonth: number = start.getMonth() + 1; let startDate: number = start.getDate(); let endYear: number = end.getFullYear(); let endMonth: number = end.getMonth() + 1; let endDate: number = end.getDate(); start = new Date(startYear + '/' + startMonth + '/' + startDate + ' GMT+0800').getTime(); end = new Date(endYear + '/' + endMonth + '/' + endDate + ' GMT+0800').getTime(); let deltaTime = end - start; return Math.floor(deltaTime / (24 * 60 * 60 * 1000)); } /** * 获取数组最小值 * @param array 数组 * @returns */ public static getMin(array: number[]) { let result: number = null!; if (array.constructor === Array) { let length = array.length; for (let i = 0; i < length; i++) { if (i === 0) { result = Number(array[0]); } else { result = result > Number(array[i]) ? Number(array[i]) : result; } } } return result; } /** * 格式化两位小数点 * @param time * @returns */ public static formatTwoDigits(time: number) { //@ts-ignore return (Array(2).join(0) + time).slice(-2); } /** * 根据格式返回时间 * @param date 时间 * @param fmt 格式 * @returns */ public static formatDate(date: Date, fmt: string) { let 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 (let 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; } /** * 获取格式化后的日期(不含小时分秒) */ public static getDay() { let date: Date = new Date(); return date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate(); } /** * 格式化名字,XXX... * @param {string} name 需要格式化的字符串 * @param {number}limit * @returns {string} 返回格式化后的字符串XXX... */ public static formatName(name: string, limit: number) { limit = limit || 6; var nameArray = this._stringToArray(name); var str = ''; var length = nameArray.length; if (length > limit) { for (var i = 0; i < limit; i++) { str += nameArray[i]; } str += '...'; } else { str = name; } return str; } /** * 格式化钱数,超过10000 转换位 10K 10000K 转换为 10M * @param {number}money 需要被格式化的数值 * @returns {string}返回 被格式化的数值 */ public static formatMoney(money: number) { let arrUnit: string[] = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'B', 'N', 'D']; let strValue: string = ''; for (let idx: number = 0; idx < arrUnit.length; idx++) { if (money >= 10000) { money /= 1000; } else { strValue = Math.floor(money) + arrUnit[idx]; break; } } if (strValue === '') { strValue = Math.floor(money) + 'U'; //超过最大值就加个U } return strValue; } /** * 开始展示文字 * @param lable 文本 * @param words 播放的文字 * @param cb 播放完成回调 * @param cbbS 回调延迟 * @param delay 延迟逐字播放 * @param s 一个字的播放速度/秒 */ public static verbatim(lable: Label,words: string, cb?: Function,cbbS:number = 0,delay: number = 0,s: number = 0.1,){ if (!words.hasOwnProperty('length'))return; lable.node.active = true; lable.unscheduleAllCallbacks(); let f = function(){ let arr = words.replace(/ /g,"").split(''); var step = 0; var allWords: string = ""; let fun = ()=> { allWords += arr[step]; lable.string = allWords; if(++step >= arr.length) { lable.unschedule(fun); let cbFun = ()=>{cb?.()}; cbbS > 0 ? (lable.scheduleOnce(cbFun, cbbS)) : cbFun(); } }; lable.schedule(fun,s,Number.MAX_SAFE_INTEGER); }; delay > 0 ? (lable.scheduleOnce(f.bind(this),delay)) : f(); } /** * 格式化数值 * @param {number}value 需要被格式化的数值 * @returns {string}返回 被格式化的数值 */ public static formatValue(value: number) { let arrUnit: string[] = []; let strValue: string = ''; for (let i = 0; i < 26; i++) { arrUnit.push(String.fromCharCode(97 + i)); } for (let idx: number = 0; idx < arrUnit.length; idx++) { if (value >= 10000) { value /= 1000; } else { strValue = Math.floor(value) + arrUnit[idx]; break; } } return strValue; } /** * 根据剩余秒数格式化剩余时间 返回 HH:MM:SS * @param {Number} leftSec */ public static formatTimeForSecond(leftSec: number, withoutSeconds: boolean = false) { let timeStr: string = ''; let sec: number = leftSec % 60; let leftMin: number = Math.floor(leftSec / 60); leftMin = leftMin < 0 ? 0 : leftMin; let hour: number = Math.floor(leftMin / 60); let min: number = leftMin % 60; if (hour > 0) { timeStr += hour > 9 ? hour.toString() : '0' + hour; timeStr += ':'; } else { timeStr += '00:'; } timeStr += min > 9 ? min.toString() : '0' + min; if (!withoutSeconds) { timeStr += ':'; timeStr += sec > 9 ? sec.toString() : '0' + sec; } return timeStr; } /** * 计算 3D 空间中两点之间的欧几里得距离 * @param a 第一个点 * @param b 第二个点 * @returns 两点之间的距离 */ public static distance(a: Vec3, b: Vec3): number { const dx = b.x - a.x; const dy = b.y - a.y; const dz = b.z - a.z; return Math.sqrt(dx * dx + dy * dy + dz * dz); } /** * 根据剩余毫秒数格式化剩余时间 返回 HH:MM:SS * * @param {Number} ms */ public static formatTimeForMillisecond(ms: number): Object { let second: number = Math.floor(ms / 1000 % 60); let minute: number = Math.floor(ms / 1000 / 60 % 60); let hour: number = Math.floor(ms / 1000 / 60 / 60); return { 'hour': hour, 'minute': minute, 'second': second }; } /** * 格式化时间戳字符串 * @param timestamp 1740006560000 * @returns 输出2025-02-20 05:09:20 */ public static formatTimestamp(timestamp: number): string { const date = new Date(timestamp); //获取年月日时分秒 const year = date.getFullYear(); //月份从 0 开始,需要 +1 const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); const seconds = String(date.getSeconds()).padStart(2, '0'); //拼接成 YYYY-MM-DD HH:MM:SS 格式 return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; } /** * 随机乱序数组 * @param array * @returns */ public static randomArray(array) { // 使用 Fisher-Yates Shuffle 算法 for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); // 交换元素 [array[i], array[j]] = [array[j], array[i]]; } return array; } /** * 获得开始和结束两者之间相隔分钟数 * * @static * @param {number} start * @param {number} end * @memberof utils */ public static getOffsetMimutes(start: number, end: number) { let offSetTime: number = end - start; let minute: number = Math.floor((offSetTime % (1000 * 60 * 60)) / (1000 * 60)); return minute; } /** * 获取随机小数 * @param {Number} min 最小值 * @param {Number} max 最大值 * @returns */ public static getRandomFloat(min: number, max: number) { return Math.random() * (max - min) + min; } /** * 返回指定小数位的数值 * @param {number} num * @param {number} idx */ public static formatNumToFixed(num: number, idx: number = 0) { return Number(num.toFixed(idx)); } /** * 用于数值到达另外一个目标数值之间进行平滑过渡运动效果 * @param {number} targetValue 目标数值 * @param {number} curValue 当前数值 * @param {number} ratio 过渡比率 * @returns */ public static lerp(targetValue: number, curValue: number, ratio: number = 0.25) { let v: number = curValue; if (targetValue > curValue) { v = curValue + (targetValue - curValue) * ratio; } else if (targetValue < curValue) { v = curValue - (curValue - targetValue) * ratio; } return v; } /** * 数据解密 * @param {String} str */ public static decrypt(b64Data: string) { if(b64Data == null || b64Data == undefined){ return ""; } let n: number = 6; if (b64Data.length % 2 === 0) { n = 7; } let decodeData = ''; for (var idx = 0; idx < b64Data.length - n; idx += 2) { decodeData += b64Data[idx + 1]; decodeData += b64Data[idx]; } decodeData += b64Data.slice(b64Data.length - n + 1); decodeData = this._base64Decode(decodeData); return decodeData; } /** * 数据加密 * @param {String} str */ public static encrypt(str: string) { if(str == null || str == undefined){ return ""; } let b64Data = this._base64encode(str); let n: number = 6; if (b64Data.length % 2 === 0) { n = 7; } let encodeData: string = ''; for (let idx = 0; idx < (b64Data.length - n + 1) / 2; idx++) { encodeData += b64Data[2 * idx + 1]; encodeData += b64Data[2 * idx]; } encodeData += b64Data.slice(b64Data.length - n + 1); return encodeData; } //public method for encoding /** * base64加密 * @param {string}input * @returns */ private static _base64encode(input: string) { let keyStr: string = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; let output: string = "", chr1: number, chr2: number, chr3: number, enc1: number, enc2: number, enc3: number, enc4: number, i: number = 0; input = this._utf8Encode(input); while (i < input.length) { chr1 = input.charCodeAt(i++); chr2 = input.charCodeAt(i++); chr3 = input.charCodeAt(i++); enc1 = chr1 >> 2; enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); enc4 = chr3 & 63; if (isNaN(chr2)) { enc3 = enc4 = 64; } else if (isNaN(chr3)) { enc4 = 64; } output = output + keyStr.charAt(enc1) + keyStr.charAt(enc2) + keyStr.charAt(enc3) + keyStr.charAt(enc4); } return output; } /** * utf-8 加密 * @param string * @returns */ private static _utf8Encode(string: string) { string = string.replace(/\r\n/g, "\n"); let utftext: string = ""; for (let n: number = 0; n < string.length; n++) { let c: number = string.charCodeAt(n); if (c < 128) { utftext += String.fromCharCode(c); } else if ((c > 127) && (c < 2048)) { utftext += String.fromCharCode((c >> 6) | 192); utftext += String.fromCharCode((c & 63) | 128); } else { utftext += String.fromCharCode((c >> 12) | 224); utftext += String.fromCharCode(((c >> 6) & 63) | 128); utftext += String.fromCharCode((c & 63) | 128); } } return utftext; } /** * utf-8解密 * @param utftext * @returns */ private static _utf8Decode(utftext: string) { let string = ""; let i: number = 0; let c: number = 0; let c1: number = 0; let c2: number = 0; let c3: number = 0; while (i < utftext.length) { c = utftext.charCodeAt(i); if (c < 128) { string += String.fromCharCode(c); i++; } else if ((c > 191) && (c < 224)) { c2 = utftext.charCodeAt(i + 1); string += String.fromCharCode(((c & 31) << 6) | (c2 & 63)); i += 2; } else { c2 = utftext.charCodeAt(i + 1); c3 = utftext.charCodeAt(i + 2); string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63)); i += 3; } } return string; } /** * base64解密 * @param {string}input 解密字符串 * @returns */ private static _base64Decode(input: string) { let keyStr: string = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; let output: string = ""; let chr1: number; let chr2: number; let chr3: number; let enc1: number; let enc2: number; let enc3: number; let enc4: number; let i: number = 0; input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); while (i < input.length) { enc1 = keyStr.indexOf(input.charAt(i++)); enc2 = keyStr.indexOf(input.charAt(i++)); enc3 = keyStr.indexOf(input.charAt(i++)); enc4 = keyStr.indexOf(input.charAt(i++)); chr1 = (enc1 << 2) | (enc2 >> 4); chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); chr3 = ((enc3 & 3) << 6) | enc4; output = output + String.fromCharCode(chr1); if (enc3 != 64) { output = output + String.fromCharCode(chr2); } if (enc4 != 64) { output = output + String.fromCharCode(chr3); } } output = this._utf8Decode(output); return output; } /** * 页面渐隐渐显动画 * @param n 节点 * @param isApper 是否是出现 * @param restore 是否恢复255显示的状态 * @param cb 执行完回调 */ public static pageAnim(n: Node,isApper: boolean = true,cb?:Function){ let uiop:UIOpacity = n.getComponent(UIOpacity); if(!n || !uiop){ cb?.(); }else{ uiop.opacity = isApper ? 25 : 255; let toOpacity: number = isApper ? 255 : 25; tween(uiop) .to(0.4,{opacity:toOpacity}) .call(function(){ cb?.(); }.bind(this)) .start(); } } /** * 将数组(array)拆分成多个 size 长度的区块,并将这些区块组成一个新数组 * @param {Array}array * @param {number}size * @returns */ public static chunk(array: any[], size: number) { var length = array === null ? 0 : array.length; if (!length || size < 1) { return []; } let result = []; while (array.length > size) { result.push(array.slice(0, size)); array = array.slice(size); } result.push(array); return result; } /** * 一个字符串首字母大写 */ public static upperCase(str){ if(this.isNull(str))return ""; return str.replace(/^\w/, (c) => c.toUpperCase()); } /** * 查询一个节点下的子节点 */ public static findName(root: Node, name: string) : Node{ let child: Node; if(name.indexOf("/") != -1) { child = find(name, root); }else { if(!root) {root = director.getScene();} if (root.name === name) { child = root; } else { child = this.findChild(name, root); } } if(child) { return child; } else { console.log("没有找到指定的Node, node name ==", name); return null; } } private static findChild(name: string, parent: Node): Node { let child: Node = parent.getChildByName(name); if (child) { return child; } else { let children: Node[] = parent.children; for (let i = 0; i < children.length; i++) { child = this.findChild(name, children[i]); if (child) { return child; } } } return null; } /** * 处理万为单位 * @param num 数值 * @param point 保留小数点 * @param s 是否去掉无用的0 * @returns */ public static numUnit(num: number,point: number = 0,f: boolean = true): string { let n: number = num; let unit: number = 10000; if (n > unit) { if(point == 0){ n = Math.ceil(n / unit); return n.toString() + "万"; }else{ let s: string = (n / unit).toFixed(point); if(f){ return this.removeZeros(s) + "万"; }else{ return s + "万"; } } }else{ return Math.ceil(num).toString(); } } /** * 格式化数字: * * 如果小数部分全是 0(如 38.0 或 38.00),去掉小数部分,返回整数。 * 如果是小数(如 38.1 或 38.01),保留两位小数。 * @param num 输入的数字 * @returns 格式化后的数字 */ public static formatNumber(num: number): number | string { // 如果 num 是 undefined 或 null,返回 0 或空字符串 if (num == undefined || num == null) return 0; //将数字转换为字符串 const numStr = num.toString(); //判断小数部分是否全是 0 如果小数部分全是 0,去掉小数部分并返回整数 if (numStr.includes('.') && /\.0+$/.test(numStr)) { return parseInt(numStr, 10); } else { //否则保留两位小数 const fixedNum = num.toFixed(2); //如果小数部分全是 0,去掉小数部分 if(fixedNum.endsWith(".00")) { return parseInt(fixedNum, 10); } return parseFloat(fixedNum); } } /** * 去掉小数点后无用的0 * @param numberString 字符串呢 * @returns */ public static removeZeros(numberString: string): string { const trimmedString = numberString.trim(); // 去除首尾空格 const decimalIndex = trimmedString.indexOf('.'); if (decimalIndex !== -1) { let endIndex = trimmedString.length - 1; while (trimmedString[endIndex] === '0') { endIndex--; } if (trimmedString[endIndex] === '.') { endIndex--; // 如果小数点后面全是零,也去掉小数点 } return trimmedString.slice(0, endIndex + 1); } return trimmedString; } /** * 数组移除某一个元素 */ public static remove(arr,param){ let index = arr.indexOf(param) if (index > -1) { arr.splice(index, 1) } } /** * 16进制的颜色 * @param hexColor * @returns */ public static hexColor(hexColor) { const hex = hexColor.replace(/^#?/, "0x"); const c = parseInt(hex); const r = c >> 16; const g = (65280 & c) >> 8; const b = 255 & c; return new Color(r, g, b, 255); }; /** * 检查是否有铭感词 * @param str 文字 * @returns 返回是否有铭感词 */ public static filtion(str): boolean{ return sensitiveArray.some(word => { //忽略大小写匹配 const lowerCaseStr = str.toLowerCase(); const lowerCaseWord = word.toLowerCase(); return lowerCaseStr.includes(lowerCaseWord); }); } /** * 计算两点间的距离 */ public static pDistance(localPos: Vec3,tarPos: Vec3): number { let dx = localPos.x - tarPos.x; let dy = localPos.y - tarPos.y; let dis = Math.sqrt(dx * dx + dy * dy); return dis; } /** * 计算两点之间的绝对距离 */ public static pAbsDistance(a: Vec3,b: Vec3): number { let p: number = Math.abs(a.x - b.x); let k: number = Math.abs(a.y - b.y); return p + k; } /** * 角度转向量 * @param angle * @returns */ public static angle_to_vector (angle: number): Vec2 { // tan = sin / cos 将传入的角度转为弧度 let radian = this.angle_to_radian(angle); // 算出cos,sin和tan let cos = Math.cos(radian);// 邻边 / 斜边 let sin = Math.sin(radian);// 对边 / 斜边 let tan = sin / cos;// 对边 / 邻边 //结合在一起并归一化 let vec = new Vec2(cos, sin).normalize(); //返回向量 return(vec); } /** * 向量转角度 * @param vector * @returns */ public static vector_to_angle (vector: Vec2): number { //将传入的向量归一化 let dir = vector.normalize(); //计算出目标角度的弧度 let radian = dir.signAngle(new Vec2(1, 0)); //把弧度计算成角度 let angle = -this.radian_to_angle(radian); //返回角度 return(angle); } /** * 角度转弧度 * @param angle * @returns */ public static angle_to_radian (angle: number): number { //角度转弧度公式 π / 180 * 角度 计算出弧度 let radian = Math.PI / 180 * angle; //返回弧度 return(radian); } /** * 弧度转角度 * @param radian * @returns */ public static radian_to_angle (radian: number): number { //弧度转角度公式 180 / π * 弧度 计算出角度 let angle = 180 / Math.PI * radian; //返回角度 return(angle); } /** * 计算弧度 * @param start * @param end */ public static getAngle(start: Vec3, end: Vec3) { //两点的x、y值 var x = end.x - start.x; var y = end.y - start.y; var hypotenuse = Math.sqrt(x * x + y * y); //斜边长度 var cos = x / hypotenuse; var radian = Math.acos(cos); //求出弧度 var angle = 180 / (Math.PI / radian); //用弧度算出角度 if (y < 0) { angle = 0 - angle; }else if (y == 0 && x < 0) { angle = 180; } return angle; } /** * 扣血转化成字符串 * @param number * @returns */ public static numberToString(value: number | string) { console.log("当前的值: " + value); const num = Number(value); if (isNaN(num)) return "0"; //特殊处理:小于1百万显示完整数字 if(num < 100000) { return Math.floor(num).toString(); } console.log("转换过后的值: " + this.formatBigNumber(num, ["", "K", "M", "B"])); //使用标准格式化中文单位 return this.formatBigNumber(num, ["", "K", "M", "B"]); } /** * 基本用法(自动中文单位) console.log(formatChineseBigNumber(998989)); // "998989" console.log(formatChineseBigNumber(24326755)); // "2432.67万" console.log(formatChineseBigNumber(958753111)); // "9.58亿" console.log(formatChineseBigNumber(9587531115987));// "9.58万亿" //高级用法(自定义单位) console.log(formatBigNumber(1500, ["", "K", "M"], 1000)); // "1.5K" console.log(formatBigNumber(2500000, ["", "K", "M"], 1000)); // "2.5M" * 格式化大数字带单位 * @param value 要格式化的数字或字符串 * @param customUnits 可选的自定义单位数组(从大到小排序) * @param customK 可选的进制基数(默认10000) * @returns 格式化后的字符串 */ public static formatBigNumber( value: number | string, customUnits?: string[], customK: number = 10000 ): string { const num = Number(value); if (isNaN(num)) return "0"; const defaultUnits = ["", "万", "亿", "万亿"]; const units = customUnits || defaultUnits; const k = customK; //小于1万的直接返回原数字 if(num < k) { return num % 1 === 0 ? num.toString() : num.toFixed(2); } //计算单位 const i = Math.floor(Math.log(num) / Math.log(k)); const unitIndex = Math.min(i, units.length - 1); const divided = num / Math.pow(k, unitIndex); // 智能确定小数位数 let decimalPlaces = 2; if (divided >= 1000) decimalPlaces = 0; else if (divided >= 100) decimalPlaces = 1; // 格式化并去除无效的.00 let formatted = divided.toFixed(decimalPlaces); if(formatted.endsWith(".00")) { formatted = formatted.slice(0, -3); }else if (formatted.endsWith("0") && decimalPlaces > 0) { //处理类似1032.50万 -> 1032.5万的情况 formatted = formatted.replace(/0+$/, "").replace(/\.$/, ""); } return formatted + units[unitIndex]; } /** * 将某个节点下的坐标转移到另外一个节点 * @param fromNode 坐标所在的节点 * @param toNode 目标节点 * @returns 转换后的坐标值 */ public static convertPosition(fromNode: Node, toNode: Node): Vec3 { let pos: Vec3 = fromNode.position.clone(); // 将 pos 转为世界坐标系下的坐标 const worldPos: Vec3 = fromNode.parent.getComponent(UITransform).convertToWorldSpaceAR(pos); // 将世界坐标系下的坐标转为目标节点的局部坐标系下的坐标 const localPos: Vec3 = toNode.getComponent(UITransform).convertToNodeSpaceAR(worldPos); return localPos; } /** * 以敌人的中心点 攻击范围为半径 产生随机坐标 * @param center 中心点 * @param radius 半径 * @returns */ public static randomPointGenerator(center: Vec3,radius: number){ //随机角度 let angle = Math.random() * Math.PI * 2; //随机距离 let distance = Math.sqrt(Math.random()) * radius; //根据极坐标转换成笛卡尔坐标 const x = center.x + distance * Math.cos(angle); const y = center.y + distance * Math.sin(angle); return new Vec3(x,y,1); } /** * 将某个节点上的坐标转移到另外一个节点 * @param fromNode 坐标所在的节点 * @param toNode 目标节点 * @returns 转换后的坐标值 */ public static convertPositionPos(fromNode: Node,pos: Vec3, toNode: Node): Vec3 { let nPos: Vec3 = pos.clone(); // 将 pos 转为世界坐标系下的坐标 const worldPos: Vec3 = fromNode.parent.getComponent(UITransform).convertToWorldSpaceAR(nPos); // 将世界坐标系下的坐标转为目标节点的局部坐标系下的坐标 const localPos: Vec3 = toNode.getComponent(UITransform).convertToNodeSpaceAR(worldPos); return localPos; } /** * 获取屏幕中心对应的3D世界坐标 * @param camera 摄像机 * @param groundY 地面高度 * @returns */ public static getScreenCenterWorldPos(camera: Camera,groundY:number = 0): Vec3 { if (!camera) return Vec3.ZERO; //获取屏幕中心坐标 const screenSize = view.getVisibleSize(); const centerX = screenSize.width / 2; const centerY = screenSize.height / 2; //生成从屏幕中心发出的射线 const ray = camera.screenPointToRay(centerX, centerY); //计算射线与特定平面的交点(假设地面Y=0) const distance = (groundY - ray.o.y) / ray.d.y; // 4. 返回交点坐标 return new Vec3( ray.o.x + ray.d.x * distance, ray.o.y + ray.d.y * distance, ray.o.z + ray.d.z * distance ); } /** * 让相机平滑看向目标节点 * @param selfNode 设置方向的节点 * @param targetNode 目标节点 * @param duration 平滑过渡时间(秒),默认0.3秒 */ public static lookAtNode(selfNode: Node, targetNode: Node, duration: number = 0.3): void { if(!selfNode || !targetNode) return; //计算水平方向(忽略Y轴差异) const targetPos = targetNode.worldPosition; const selfPos = selfNode.worldPosition; const direction = new Vec3(); Vec3.subtract(direction, targetPos, selfPos); //关键修复:将Y轴归零计算水平方向 const horizontalDir = new Vec3(direction.x, 0, direction.z).normalize(); //计算垂直角度(仅Y轴差异) const distance = Vec3.distance(targetPos, selfPos); const heightDiff = targetPos.y - selfPos.y; const verticalAngle = math.toRadian(math.clamp( Math.atan2(heightDiff, Math.sqrt(direction.x * direction.x + direction.z * direction.z)) * 180 / Math.PI, -89, 89 // 限制在±89度内防止万向节锁 )); //构建最终旋转(先水平后垂直) const targetRotation = new Quat(); Quat.fromViewUp(targetRotation, horizontalDir, Vec3.UP); //添加垂直旋转 const verticalRot = new Quat(); Quat.fromEuler(verticalRot, verticalAngle, 0, 0); Quat.multiply(targetRotation, targetRotation, verticalRot); //应用旋转 if(duration <= 0) { selfNode.setRotation(targetRotation); }else{ const startRotation = selfNode.rotation.clone(); tween(selfNode) .to(duration, { rotation: targetRotation }, { onUpdate: (_, ratio: number) => { const currentRot = new Quat(); Quat.slerp(currentRot, startRotation, targetRotation, ratio); selfNode.setRotation(currentRot); } }) .start(); } } /** * 根据A,B两个坐标点 和抛物线的弧度 来计算中心点坐标 */ public static calculateParabolaCenter(start: Vec3, end: Vec3){ // 计算两点之间的水平距离 const deltaX = end.x - start.x; // 将控制点的 x 坐标设置为两点的中点 const controlX = (start.x + end.x) / 2; // 计算抛物线的最高点,使其位于两点之间的中间位置 可以根据需要调整最高点的位置 const highestY = Math.max(start.y, end.y) + Math.abs(deltaX) / 4; // 计算控制点的 y 坐标 const controlY = highestY; //返回抛物线的中心坐标点 return new Vec3(controlX, controlY); } /** * 在UI坐标系内生成安全的随机点 * @param uiParent 父节点(UITransform) * @param center 中心点(Vec3) - 基于父节点的本地坐标 * @param radius 半径(Number) - 单位为像素 */ public static randomUIPointGenerator(uiParent: Node, center: Vec3, radius: number): Vec3 { // 将像素半径转换为世界坐标比例 const uiSize = uiParent.getComponent(UITransform).contentSize; const maxRadius = Math.min( uiSize.width / 2 - Math.abs(center.x), uiSize.height / 2 - Math.abs(center.y) ); const safeRadius = Math.min(radius, maxRadius); //生成随机角度和距离(距离使用平方根使分布均匀) const angle = Math.random() * Math.PI * 2; const distance = Math.sqrt(Math.random()) * safeRadius; //转换为本地坐标 return new Vec3( center.x + distance * Math.cos(angle), center.y + distance * Math.sin(angle), 0 // UI节点z轴设为0 ); } /** * 生成一个随机颜色值的函数 */ public static getRandomColor() { let r = Math.floor(Math.random() * 256); let g = Math.floor(Math.random() * 256); let b = Math.floor(Math.random() * 256); return new Color(r, g, b); } } /** * //摄像机正前方是跟随枪 const targetPos = Game.I.player.node.worldPosition; const playerRotation = Game.I.player.node.worldRotation; const offset = new Vec3(0, 2, -8); // 上方2单位,后方5单位 // 计算世界空间偏移 const worldOffset = Vec3.transformQuat(new Vec3(), offset, playerRotation ); Game.I.camera.node.worldPosition = Vec3.add(new Vec3(), targetPos, worldOffset); Game.I.camera.node.lookAt(targetPos); let crossPos: Vec3 = Utils.getScreenCenterWorldPos(Game.I.camera); const direction = Vec3.subtract(new Vec3(), crossPos, gun.node.worldPosition); direction.normalize(); Game.I.camera.node.forward = direction; */