Utils.ts 46 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423
  1. import { UITransform, Vec2, Vec3,Node, _decorator, find, director, Color, UIOpacity, tween, Label, Camera, view, Quat, math } from "cc";
  2. const { ccclass, property } = _decorator;
  3. import sensitiveArray from '../extend/SensitiveWords';
  4. @ccclass("Utils")
  5. export class Utils {
  6. /**
  7. * 验证字符串是否是空的
  8. * @s 字符串
  9. */
  10. public static isNull(s: string) {
  11. if(s == ""
  12. || s == null
  13. || s == undefined){
  14. return true;
  15. }
  16. if (typeof s === 'string'){
  17. let re = new RegExp("^[ ]+$");
  18. return re.test(s);
  19. }else{
  20. return false;
  21. }
  22. }
  23. /**
  24. * 合并多个字典
  25. * @param args
  26. */
  27. public static merge(...args) {
  28. let mergeFn = <T, U> (arg1: T, arg2: U) : (T & U) =>{
  29. let res = {} as (T & U);
  30. res = Object.assign(arg1, arg2);
  31. return res;
  32. };
  33. let nDict = {};
  34. args.forEach(obj => {
  35. nDict = mergeFn(nDict,obj);
  36. });
  37. return nDict;
  38. }
  39. /**
  40. * 排除合并对象,但排除'reward'和'dec'属性(如果e中已存在)
  41. * mergeWithExceptions(e, pData, ['reward', 'dec']);
  42. * @param target
  43. * @param source
  44. * @param exclude
  45. * @returns
  46. */
  47. public static mergeWithExceptions(target: any, source: any, exclude: string[]) {
  48. Object.keys(source).forEach(key => {
  49. if (!exclude.includes(key) || !(key in target)) {
  50. target[key] = source[key];
  51. }
  52. });
  53. return target;
  54. }
  55. /**
  56. * 深度拷贝
  57. * @param {any} sObj 拷贝的对象
  58. * @returns
  59. */
  60. public static clone(sObj: any) {
  61. if (sObj === null || typeof sObj !== "object") {
  62. return sObj;
  63. }
  64. let s: { [key: string]: any } = {};
  65. if (sObj.constructor === Array) {
  66. s = [];
  67. }
  68. for (let i in sObj) {
  69. if (sObj.hasOwnProperty(i)) {
  70. s[i] = this.clone(sObj[i]);
  71. }
  72. }
  73. return s;
  74. }
  75. /**
  76. * 转换数字
  77. */
  78. public static formatUnits(num: number): string {
  79. if (num >= 1e8) {
  80. return (num / 1e8).toFixed(1) + '亿';
  81. } else if (num >= 1e7) {
  82. return (num / 1e7).toFixed(1) + '千万';
  83. } else if (num >= 1e6) {
  84. return (num / 1e6).toFixed(1) + '百万';
  85. } else if (num >= 1e4) {
  86. return (num / 1e4).toFixed(1) + '万';
  87. } else if (num >= 1000) {
  88. return (num / 1000).toFixed(1) + 'k';
  89. } else {
  90. return num.toString();
  91. }
  92. }
  93. /**
  94. * 将object转化为数组
  95. * @param { any} srcObj
  96. * @returns
  97. */
  98. public static objectToArray(srcObj: { [key: string]: any }) {
  99. let resultArr: any[] = [];
  100. // to array
  101. for (let key in srcObj) {
  102. if (!srcObj.hasOwnProperty(key)) {
  103. continue;
  104. }
  105. resultArr.push(srcObj[key]);
  106. }
  107. return resultArr;
  108. }
  109. /**
  110. * !#zh 将数组转化为object。
  111. */
  112. /**
  113. * 将数组转化为object。
  114. * @param { any} srcObj
  115. * @param { string} objectKey
  116. * @returns
  117. */
  118. public static arrayToObject(srcObj: any, objectKey: string) {
  119. let resultObj: { [key: string]: any } = {};
  120. // to object
  121. for (var key in srcObj) {
  122. if (!srcObj.hasOwnProperty(key) || !srcObj[key][objectKey]) {
  123. continue;
  124. }
  125. resultObj[srcObj[key][objectKey]] = srcObj[key];
  126. }
  127. return resultObj;
  128. }
  129. /**
  130. * 根据权重,计算随机内容
  131. * @param {arrany} weightArr
  132. * @param {number} totalWeight 权重
  133. * @returns
  134. */
  135. public static getWeightRandIndex(weightArr: [], totalWeight: number) {
  136. let randWeight: number = Math.floor(Math.random() * totalWeight);
  137. let sum: number = 0;
  138. for (var weightIndex: number = 0; weightIndex < weightArr.length; weightIndex++) {
  139. sum += weightArr[weightIndex];
  140. if (randWeight < sum) {
  141. break;
  142. }
  143. }
  144. return weightIndex;
  145. }
  146. /**
  147. * 从n个数中获取m个随机数
  148. * @param {Number} n 总数
  149. * @param {Number} m 获取数
  150. * @returns {Array} array 获取数列
  151. */
  152. public static getRandomNFromM(n: number, m: number) {
  153. let array: any[] = [];
  154. let intRd: number = 0;
  155. let count: number = 0;
  156. while (count < m) {
  157. if (count >= n + 1) {
  158. break;
  159. }
  160. intRd = this.getRandomInt(0, n);
  161. var flag = 0;
  162. for (var i = 0; i < count; i++) {
  163. if (array[i] === intRd) {
  164. flag = 1;
  165. break;
  166. }
  167. }
  168. if (flag === 0) {
  169. array[count] = intRd;
  170. count++;
  171. }
  172. }
  173. return array;
  174. }
  175. /**
  176. * 获取随机整数
  177. * @param {Number} min 最小值
  178. * @param {Number} max 最大值
  179. * @returns
  180. */
  181. public static getRandomInt(min: number, max: number) {
  182. let r: number = Math.random();
  183. let rr: number = r * (max - min + 1) + min;
  184. return Math.floor(rr);
  185. }
  186. /**
  187. * 获取字符串长度
  188. * @param {string} render
  189. * @returns
  190. */
  191. public static getStringLength(render: string) {
  192. let strArr: string = render;
  193. let len: number = 0;
  194. for (let i: number = 0, n = strArr.length; i < n; i++) {
  195. let val: number = strArr.charCodeAt(i);
  196. if (val <= 255) {
  197. len = len + 1;
  198. } else {
  199. len = len + 2;
  200. }
  201. }
  202. return Math.ceil(len / 2);
  203. }
  204. /**
  205. * 要从一个数组模型中随机取出 n 个元素
  206. * @param arr
  207. * @param n
  208. * @returns 返回一个新的数组
  209. */
  210. public static getRandomElements<T>(arr: T[], n: number): T[] {
  211. if (n <= 0) return []; // 如果 n 小于等于 0,返回空数组
  212. //复制数组以避免修改原数组
  213. const copy = [...arr];
  214. //Fisher-Yates 洗牌算法
  215. for (let i = copy.length - 1; i > 0; i--) {
  216. const j = Math.floor(Math.random() * (i + 1));
  217. [copy[i], copy[j]] = [copy[j], copy[i]];
  218. }
  219. //如果 n 超过数组长度,返回乱序后的整个数组
  220. if (n >= copy.length) {
  221. return copy;
  222. }
  223. //返回前 n 个元素
  224. return copy.slice(0, n);
  225. }
  226. /**
  227. * 判断传入的参数是否为空的Object。数组或undefined会返回false
  228. * @param obj
  229. */
  230. public static isEmptyObject(obj: any) {
  231. let result: boolean = true;
  232. if (obj && obj.constructor === Object) {
  233. for (var key in obj) {
  234. if (obj.hasOwnProperty(key)) {
  235. result = false;
  236. break;
  237. }
  238. }
  239. } else {
  240. result = false;
  241. }
  242. return result;
  243. }
  244. /**
  245. * 判断是否是新的一天
  246. * @param {Object|Number} dateValue 时间对象 todo MessageCenter 与 pve 相关的时间存储建议改为 Date 类型
  247. * @returns {boolean}
  248. */
  249. public static isNewDay(dateValue: any) {
  250. // todo:是否需要判断时区?
  251. var oldDate: any = new Date(dateValue);
  252. var curDate: any = new Date();
  253. var oldYear = oldDate.getYear();
  254. var oldMonth = oldDate.getMonth();
  255. var oldDay = oldDate.getDate();
  256. var curYear = curDate.getYear();
  257. var curMonth = curDate.getMonth();
  258. var curDay = curDate.getDate();
  259. if (curYear > oldYear) {
  260. return true;
  261. } else {
  262. if (curMonth > oldMonth) {
  263. return true;
  264. } else {
  265. if (curDay > oldDay) {
  266. return true;
  267. }
  268. }
  269. }
  270. return false;
  271. }
  272. /**
  273. * 获取对象属性数量
  274. * @param {object}o 对象
  275. * @returns
  276. */
  277. public static getPropertyCount(o: Object) {
  278. var n, count = 0;
  279. for (n in o) {
  280. if (o.hasOwnProperty(n)) {
  281. count++;
  282. }
  283. }
  284. return count;
  285. }
  286. /**
  287. * 返回一个差异化数组(将array中diff里的值去掉)
  288. * @param array
  289. * @param diff
  290. */
  291. public static difference(array: [], diff: any) {
  292. let result: any[] = [];
  293. if (array.constructor !== Array || diff.constructor !== Array) {
  294. return result;
  295. }
  296. let length = array.length;
  297. for (let i: number = 0; i < length; i++) {
  298. if (diff.indexOf(array[i]) === -1) {
  299. result.push(array[i]);
  300. }
  301. }
  302. return result;
  303. }
  304. public static _stringToArray(string: string) {
  305. // 用于判断emoji的正则们
  306. var rsAstralRange = '\\ud800-\\udfff';
  307. var rsZWJ = '\\u200d';
  308. var rsVarRange = '\\ufe0e\\ufe0f';
  309. var rsComboMarksRange = '\\u0300-\\u036f';
  310. var reComboHalfMarksRange = '\\ufe20-\\ufe2f';
  311. var rsComboSymbolsRange = '\\u20d0-\\u20ff';
  312. var rsComboRange = rsComboMarksRange + reComboHalfMarksRange + rsComboSymbolsRange;
  313. var reHasUnicode = RegExp('[' + rsZWJ + rsAstralRange + rsComboRange + rsVarRange + ']');
  314. var rsFitz = '\\ud83c[\\udffb-\\udfff]';
  315. var rsOptVar = '[' + rsVarRange + ']?';
  316. var rsCombo = '[' + rsComboRange + ']';
  317. var rsModifier = '(?:' + rsCombo + '|' + rsFitz + ')';
  318. var reOptMod = rsModifier + '?';
  319. var rsAstral = '[' + rsAstralRange + ']';
  320. var rsNonAstral = '[^' + rsAstralRange + ']';
  321. var rsRegional = '(?:\\ud83c[\\udde6-\\uddff]){2}';
  322. var rsSurrPair = '[\\ud800-\\udbff][\\udc00-\\udfff]';
  323. var rsOptJoin = '(?:' + rsZWJ + '(?:' + [rsNonAstral, rsRegional, rsSurrPair].join('|') + ')' + rsOptVar + reOptMod + ')*';
  324. var rsSeq = rsOptVar + reOptMod + rsOptJoin;
  325. var rsSymbol = '(?:' + [rsNonAstral + rsCombo + '?', rsCombo, rsRegional, rsSurrPair, rsAstral].join('|') + ')';
  326. var reUnicode = RegExp(rsFitz + '(?=' + rsFitz + ')|' + rsSymbol + rsSeq, 'g');
  327. var hasUnicode = function (val: any) {
  328. return reHasUnicode.test(val);
  329. };
  330. var unicodeToArray = function (val: any) {
  331. return val.match(reUnicode) || [];
  332. };
  333. var asciiToArray = function (val: any) {
  334. return val.split('');
  335. };
  336. return hasUnicode(string) ? unicodeToArray(string) : asciiToArray(string);
  337. }
  338. // 模拟传msg的uuid
  339. public static simulationUUID() {
  340. function s4() {
  341. return Math.floor((1 + Math.random()) * 0x10000)
  342. .toString(16)
  343. .substring(1);
  344. }
  345. return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
  346. s4() + '-' + s4() + s4() + s4();
  347. }
  348. public static trim(str: string) {
  349. return str.replace(/(^\s*)|(\s*$)/g, "");
  350. }
  351. /**
  352. * 判断当前时间是否在有效时间内
  353. * @param {String|Number} start 起始时间。带有时区信息
  354. * @param {String|Number} end 结束时间。带有时区信息
  355. */
  356. public static isNowValid(start: any, end: any) {
  357. var startTime = new Date(start);
  358. var endTime = new Date(end);
  359. var result = false;
  360. if (startTime.getDate() + '' !== 'NaN' && endTime.getDate() + '' !== 'NaN') {
  361. var curDate = new Date();
  362. result = curDate < endTime && curDate > startTime;
  363. }
  364. return result;
  365. }
  366. /**
  367. * 返回相隔天数
  368. * @param start
  369. * @param end
  370. * @returns
  371. */
  372. public static getDeltaDays(start: any, end: any) {
  373. start = new Date(start);
  374. end = new Date(end);
  375. let startYear: number = start.getFullYear();
  376. let startMonth: number = start.getMonth() + 1;
  377. let startDate: number = start.getDate();
  378. let endYear: number = end.getFullYear();
  379. let endMonth: number = end.getMonth() + 1;
  380. let endDate: number = end.getDate();
  381. start = new Date(startYear + '/' + startMonth + '/' + startDate + ' GMT+0800').getTime();
  382. end = new Date(endYear + '/' + endMonth + '/' + endDate + ' GMT+0800').getTime();
  383. let deltaTime = end - start;
  384. return Math.floor(deltaTime / (24 * 60 * 60 * 1000));
  385. }
  386. /**
  387. * 获取数组最小值
  388. * @param array 数组
  389. * @returns
  390. */
  391. public static getMin(array: number[]) {
  392. let result: number = null!;
  393. if (array.constructor === Array) {
  394. let length = array.length;
  395. for (let i = 0; i < length; i++) {
  396. if (i === 0) {
  397. result = Number(array[0]);
  398. } else {
  399. result = result > Number(array[i]) ? Number(array[i]) : result;
  400. }
  401. }
  402. }
  403. return result;
  404. }
  405. /**
  406. * 格式化两位小数点
  407. * @param time
  408. * @returns
  409. */
  410. public static formatTwoDigits(time: number) {
  411. //@ts-ignore
  412. return (Array(2).join(0) + time).slice(-2);
  413. }
  414. /**
  415. * 根据格式返回时间
  416. * @param date 时间
  417. * @param fmt 格式
  418. * @returns
  419. */
  420. public static formatDate(date: Date, fmt: string) {
  421. let o: any = {
  422. "M+": date.getMonth() + 1, //月份
  423. "d+": date.getDate(), //日
  424. "h+": date.getHours(), //小时
  425. "m+": date.getMinutes(), //分
  426. "s+": date.getSeconds(), //秒
  427. "q+": Math.floor((date.getMonth() + 3) / 3), //季度
  428. "S": date.getMilliseconds() //毫秒
  429. };
  430. if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
  431. for (let k in o)
  432. if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
  433. return fmt;
  434. }
  435. /**
  436. * 获取格式化后的日期(不含小时分秒)
  437. */
  438. public static getDay() {
  439. let date: Date = new Date();
  440. return date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate();
  441. }
  442. /**
  443. * 格式化名字,XXX...
  444. * @param {string} name 需要格式化的字符串
  445. * @param {number}limit
  446. * @returns {string} 返回格式化后的字符串XXX...
  447. */
  448. public static formatName(name: string, limit: number) {
  449. limit = limit || 6;
  450. var nameArray = this._stringToArray(name);
  451. var str = '';
  452. var length = nameArray.length;
  453. if (length > limit) {
  454. for (var i = 0; i < limit; i++) {
  455. str += nameArray[i];
  456. }
  457. str += '...';
  458. } else {
  459. str = name;
  460. }
  461. return str;
  462. }
  463. /**
  464. * 格式化钱数,超过10000 转换位 10K 10000K 转换为 10M
  465. * @param {number}money 需要被格式化的数值
  466. * @returns {string}返回 被格式化的数值
  467. */
  468. public static formatMoney(money: number) {
  469. let arrUnit: string[] = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'B', 'N', 'D'];
  470. let strValue: string = '';
  471. for (let idx: number = 0; idx < arrUnit.length; idx++) {
  472. if (money >= 10000) {
  473. money /= 1000;
  474. } else {
  475. strValue = Math.floor(money) + arrUnit[idx];
  476. break;
  477. }
  478. }
  479. if (strValue === '') {
  480. strValue = Math.floor(money) + 'U'; //超过最大值就加个U
  481. }
  482. return strValue;
  483. }
  484. /**
  485. * 开始展示文字
  486. * @param lable 文本
  487. * @param words 播放的文字
  488. * @param cb 播放完成回调
  489. * @param cbbS 回调延迟
  490. * @param delay 延迟逐字播放
  491. * @param s 一个字的播放速度/秒
  492. */
  493. public static verbatim(lable: Label,words: string, cb?: Function,cbbS:number = 0,delay: number = 0,s: number = 0.1,){
  494. if (!words.hasOwnProperty('length'))return;
  495. lable.node.active = true;
  496. lable.unscheduleAllCallbacks();
  497. let f = function(){
  498. let arr = words.replace(/ /g,"").split('');
  499. var step = 0;
  500. var allWords: string = "";
  501. let fun = ()=> {
  502. allWords += arr[step];
  503. lable.string = allWords;
  504. if(++step >= arr.length) {
  505. lable.unschedule(fun);
  506. let cbFun = ()=>{cb?.()};
  507. cbbS > 0 ? (lable.scheduleOnce(cbFun, cbbS)) : cbFun();
  508. }
  509. };
  510. lable.schedule(fun,s,Number.MAX_SAFE_INTEGER);
  511. };
  512. delay > 0 ? (lable.scheduleOnce(f.bind(this),delay)) : f();
  513. }
  514. /**
  515. * 格式化数值
  516. * @param {number}value 需要被格式化的数值
  517. * @returns {string}返回 被格式化的数值
  518. */
  519. public static formatValue(value: number) {
  520. let arrUnit: string[] = [];
  521. let strValue: string = '';
  522. for (let i = 0; i < 26; i++) {
  523. arrUnit.push(String.fromCharCode(97 + i));
  524. }
  525. for (let idx: number = 0; idx < arrUnit.length; idx++) {
  526. if (value >= 10000) {
  527. value /= 1000;
  528. } else {
  529. strValue = Math.floor(value) + arrUnit[idx];
  530. break;
  531. }
  532. }
  533. return strValue;
  534. }
  535. /**
  536. * 根据剩余秒数格式化剩余时间 返回 HH:MM:SS
  537. * @param {Number} leftSec
  538. */
  539. public static formatTimeForSecond(leftSec: number, withoutSeconds: boolean = false) {
  540. let timeStr: string = '';
  541. let sec: number = leftSec % 60;
  542. let leftMin: number = Math.floor(leftSec / 60);
  543. leftMin = leftMin < 0 ? 0 : leftMin;
  544. let hour: number = Math.floor(leftMin / 60);
  545. let min: number = leftMin % 60;
  546. if (hour > 0) {
  547. timeStr += hour > 9 ? hour.toString() : '0' + hour;
  548. timeStr += ':';
  549. } else {
  550. timeStr += '00:';
  551. }
  552. timeStr += min > 9 ? min.toString() : '0' + min;
  553. if (!withoutSeconds) {
  554. timeStr += ':';
  555. timeStr += sec > 9 ? sec.toString() : '0' + sec;
  556. }
  557. return timeStr;
  558. }
  559. /**
  560. * 计算 3D 空间中两点之间的欧几里得距离
  561. * @param a 第一个点
  562. * @param b 第二个点
  563. * @returns 两点之间的距离
  564. */
  565. public static distance(a: Vec3, b: Vec3): number {
  566. const dx = b.x - a.x;
  567. const dy = b.y - a.y;
  568. const dz = b.z - a.z;
  569. return Math.sqrt(dx * dx + dy * dy + dz * dz);
  570. }
  571. /**
  572. * 根据剩余毫秒数格式化剩余时间 返回 HH:MM:SS
  573. *
  574. * @param {Number} ms
  575. */
  576. public static formatTimeForMillisecond(ms: number): Object {
  577. let second: number = Math.floor(ms / 1000 % 60);
  578. let minute: number = Math.floor(ms / 1000 / 60 % 60);
  579. let hour: number = Math.floor(ms / 1000 / 60 / 60);
  580. return { 'hour': hour, 'minute': minute, 'second': second };
  581. }
  582. /**
  583. * 格式化时间戳字符串
  584. * @param timestamp 1740006560000
  585. * @returns 输出2025-02-20 05:09:20
  586. */
  587. public static formatTimestamp(timestamp: number): string {
  588. const date = new Date(timestamp);
  589. //获取年月日时分秒
  590. const year = date.getFullYear();
  591. //月份从 0 开始,需要 +1
  592. const month = String(date.getMonth() + 1).padStart(2, '0');
  593. const day = String(date.getDate()).padStart(2, '0');
  594. const hours = String(date.getHours()).padStart(2, '0');
  595. const minutes = String(date.getMinutes()).padStart(2, '0');
  596. const seconds = String(date.getSeconds()).padStart(2, '0');
  597. //拼接成 YYYY-MM-DD HH:MM:SS 格式
  598. return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
  599. }
  600. /**
  601. * 随机乱序数组
  602. * @param array
  603. * @returns
  604. */
  605. public static randomArray(array) {
  606. // 使用 Fisher-Yates Shuffle 算法
  607. for (let i = array.length - 1; i > 0; i--) {
  608. const j = Math.floor(Math.random() * (i + 1));
  609. // 交换元素
  610. [array[i], array[j]] = [array[j], array[i]];
  611. }
  612. return array;
  613. }
  614. /**
  615. * 获得开始和结束两者之间相隔分钟数
  616. *
  617. * @static
  618. * @param {number} start
  619. * @param {number} end
  620. * @memberof utils
  621. */
  622. public static getOffsetMimutes(start: number, end: number) {
  623. let offSetTime: number = end - start;
  624. let minute: number = Math.floor((offSetTime % (1000 * 60 * 60)) / (1000 * 60));
  625. return minute;
  626. }
  627. /**
  628. * 获取随机小数
  629. * @param {Number} min 最小值
  630. * @param {Number} max 最大值
  631. * @returns
  632. */
  633. public static getRandomFloat(min: number, max: number) {
  634. return Math.random() * (max - min) + min;
  635. }
  636. /**
  637. * 返回指定小数位的数值
  638. * @param {number} num
  639. * @param {number} idx
  640. */
  641. public static formatNumToFixed(num: number, idx: number = 0) {
  642. return Number(num.toFixed(idx));
  643. }
  644. /**
  645. * 用于数值到达另外一个目标数值之间进行平滑过渡运动效果
  646. * @param {number} targetValue 目标数值
  647. * @param {number} curValue 当前数值
  648. * @param {number} ratio 过渡比率
  649. * @returns
  650. */
  651. public static lerp(targetValue: number, curValue: number, ratio: number = 0.25) {
  652. let v: number = curValue;
  653. if (targetValue > curValue) {
  654. v = curValue + (targetValue - curValue) * ratio;
  655. } else if (targetValue < curValue) {
  656. v = curValue - (curValue - targetValue) * ratio;
  657. }
  658. return v;
  659. }
  660. /**
  661. * 数据解密
  662. * @param {String} str
  663. */
  664. public static decrypt(b64Data: string) {
  665. if(b64Data == null || b64Data == undefined){
  666. return "";
  667. }
  668. let n: number = 6;
  669. if (b64Data.length % 2 === 0) {
  670. n = 7;
  671. }
  672. let decodeData = '';
  673. for (var idx = 0; idx < b64Data.length - n; idx += 2) {
  674. decodeData += b64Data[idx + 1];
  675. decodeData += b64Data[idx];
  676. }
  677. decodeData += b64Data.slice(b64Data.length - n + 1);
  678. decodeData = this._base64Decode(decodeData);
  679. return decodeData;
  680. }
  681. /**
  682. * 数据加密
  683. * @param {String} str
  684. */
  685. public static encrypt(str: string) {
  686. if(str == null || str == undefined){
  687. return "";
  688. }
  689. let b64Data = this._base64encode(str);
  690. let n: number = 6;
  691. if (b64Data.length % 2 === 0) {
  692. n = 7;
  693. }
  694. let encodeData: string = '';
  695. for (let idx = 0; idx < (b64Data.length - n + 1) / 2; idx++) {
  696. encodeData += b64Data[2 * idx + 1];
  697. encodeData += b64Data[2 * idx];
  698. }
  699. encodeData += b64Data.slice(b64Data.length - n + 1);
  700. return encodeData;
  701. }
  702. //public method for encoding
  703. /**
  704. * base64加密
  705. * @param {string}input
  706. * @returns
  707. */
  708. private static _base64encode(input: string) {
  709. let keyStr: string = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
  710. let output: string = "", chr1: number, chr2: number, chr3: number, enc1: number, enc2: number, enc3: number, enc4: number, i: number = 0;
  711. input = this._utf8Encode(input);
  712. while (i < input.length) {
  713. chr1 = input.charCodeAt(i++);
  714. chr2 = input.charCodeAt(i++);
  715. chr3 = input.charCodeAt(i++);
  716. enc1 = chr1 >> 2;
  717. enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
  718. enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
  719. enc4 = chr3 & 63;
  720. if (isNaN(chr2)) {
  721. enc3 = enc4 = 64;
  722. } else if (isNaN(chr3)) {
  723. enc4 = 64;
  724. }
  725. output = output +
  726. keyStr.charAt(enc1) + keyStr.charAt(enc2) +
  727. keyStr.charAt(enc3) + keyStr.charAt(enc4);
  728. }
  729. return output;
  730. }
  731. /**
  732. * utf-8 加密
  733. * @param string
  734. * @returns
  735. */
  736. private static _utf8Encode(string: string) {
  737. string = string.replace(/\r\n/g, "\n");
  738. let utftext: string = "";
  739. for (let n: number = 0; n < string.length; n++) {
  740. let c: number = string.charCodeAt(n);
  741. if (c < 128) {
  742. utftext += String.fromCharCode(c);
  743. } else if ((c > 127) && (c < 2048)) {
  744. utftext += String.fromCharCode((c >> 6) | 192);
  745. utftext += String.fromCharCode((c & 63) | 128);
  746. } else {
  747. utftext += String.fromCharCode((c >> 12) | 224);
  748. utftext += String.fromCharCode(((c >> 6) & 63) | 128);
  749. utftext += String.fromCharCode((c & 63) | 128);
  750. }
  751. }
  752. return utftext;
  753. }
  754. /**
  755. * utf-8解密
  756. * @param utftext
  757. * @returns
  758. */
  759. private static _utf8Decode(utftext: string) {
  760. let string = "";
  761. let i: number = 0;
  762. let c: number = 0;
  763. let c1: number = 0;
  764. let c2: number = 0;
  765. let c3: number = 0;
  766. while (i < utftext.length) {
  767. c = utftext.charCodeAt(i);
  768. if (c < 128) {
  769. string += String.fromCharCode(c);
  770. i++;
  771. } else if ((c > 191) && (c < 224)) {
  772. c2 = utftext.charCodeAt(i + 1);
  773. string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
  774. i += 2;
  775. } else {
  776. c2 = utftext.charCodeAt(i + 1);
  777. c3 = utftext.charCodeAt(i + 2);
  778. string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
  779. i += 3;
  780. }
  781. }
  782. return string;
  783. }
  784. /**
  785. * base64解密
  786. * @param {string}input 解密字符串
  787. * @returns
  788. */
  789. private static _base64Decode(input: string) {
  790. let keyStr: string = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
  791. let output: string = "";
  792. let chr1: number;
  793. let chr2: number;
  794. let chr3: number;
  795. let enc1: number;
  796. let enc2: number;
  797. let enc3: number;
  798. let enc4: number;
  799. let i: number = 0;
  800. input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
  801. while (i < input.length) {
  802. enc1 = keyStr.indexOf(input.charAt(i++));
  803. enc2 = keyStr.indexOf(input.charAt(i++));
  804. enc3 = keyStr.indexOf(input.charAt(i++));
  805. enc4 = keyStr.indexOf(input.charAt(i++));
  806. chr1 = (enc1 << 2) | (enc2 >> 4);
  807. chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
  808. chr3 = ((enc3 & 3) << 6) | enc4;
  809. output = output + String.fromCharCode(chr1);
  810. if (enc3 != 64) {
  811. output = output + String.fromCharCode(chr2);
  812. }
  813. if (enc4 != 64) {
  814. output = output + String.fromCharCode(chr3);
  815. }
  816. }
  817. output = this._utf8Decode(output);
  818. return output;
  819. }
  820. /**
  821. * 页面渐隐渐显动画
  822. * @param n 节点
  823. * @param isApper 是否是出现
  824. * @param restore 是否恢复255显示的状态
  825. * @param cb 执行完回调
  826. */
  827. public static pageAnim(n: Node,isApper: boolean = true,cb?:Function){
  828. let uiop:UIOpacity = n.getComponent(UIOpacity);
  829. if(!n || !uiop){
  830. cb?.();
  831. }else{
  832. uiop.opacity = isApper ? 25 : 255;
  833. let toOpacity: number = isApper ? 255 : 25;
  834. tween(uiop)
  835. .to(0.4,{opacity:toOpacity})
  836. .call(function(){
  837. cb?.();
  838. }.bind(this))
  839. .start();
  840. }
  841. }
  842. /**
  843. * 将数组(array)拆分成多个 size 长度的区块,并将这些区块组成一个新数组
  844. * @param {Array}array
  845. * @param {number}size
  846. * @returns
  847. */
  848. public static chunk(array: any[], size: number) {
  849. var length = array === null ? 0 : array.length;
  850. if (!length || size < 1) {
  851. return [];
  852. }
  853. let result = [];
  854. while (array.length > size) {
  855. result.push(array.slice(0, size));
  856. array = array.slice(size);
  857. }
  858. result.push(array);
  859. return result;
  860. }
  861. /**
  862. * 一个字符串首字母大写
  863. */
  864. public static upperCase(str){
  865. if(this.isNull(str))return "";
  866. return str.replace(/^\w/, (c) => c.toUpperCase());
  867. }
  868. /**
  869. * 查询一个节点下的子节点
  870. */
  871. public static findName(root: Node, name: string) : Node{
  872. let child: Node;
  873. if(name.indexOf("/") != -1) {
  874. child = find(name, root);
  875. }else {
  876. if(!root) {root = director.getScene();}
  877. if (root.name === name) {
  878. child = root;
  879. } else {
  880. child = this.findChild(name, root);
  881. }
  882. }
  883. if(child) {
  884. return child;
  885. } else {
  886. console.log("没有找到指定的Node, node name ==", name);
  887. return null;
  888. }
  889. }
  890. private static findChild(name: string, parent: Node): Node {
  891. let child: Node = parent.getChildByName(name);
  892. if (child) {
  893. return child;
  894. } else {
  895. let children: Node[] = parent.children;
  896. for (let i = 0; i < children.length; i++) {
  897. child = this.findChild(name, children[i]);
  898. if (child) {
  899. return child;
  900. }
  901. }
  902. }
  903. return null;
  904. }
  905. /**
  906. * 处理万为单位
  907. * @param num 数值
  908. * @param point 保留小数点
  909. * @param s 是否去掉无用的0
  910. * @returns
  911. */
  912. public static numUnit(num: number,point: number = 0,f: boolean = true): string {
  913. let n: number = num;
  914. let unit: number = 10000;
  915. if (n > unit) {
  916. if(point == 0){
  917. n = Math.ceil(n / unit);
  918. return n.toString() + "万";
  919. }else{
  920. let s: string = (n / unit).toFixed(point);
  921. if(f){
  922. return this.removeZeros(s) + "万";
  923. }else{
  924. return s + "万";
  925. }
  926. }
  927. }else{
  928. return Math.ceil(num).toString();
  929. }
  930. }
  931. /**
  932. * 格式化数字:
  933. * * 如果小数部分全是 0(如 38.0 或 38.00),去掉小数部分,返回整数。
  934. * 如果是小数(如 38.1 或 38.01),保留两位小数。
  935. * @param num 输入的数字
  936. * @returns 格式化后的数字
  937. */
  938. public static formatNumber(num: number): number | string {
  939. // 如果 num 是 undefined 或 null,返回 0 或空字符串
  940. if (num == undefined || num == null) return 0;
  941. //将数字转换为字符串
  942. const numStr = num.toString();
  943. //判断小数部分是否全是 0 如果小数部分全是 0,去掉小数部分并返回整数
  944. if (numStr.includes('.') && /\.0+$/.test(numStr)) {
  945. return parseInt(numStr, 10);
  946. } else {
  947. //否则保留两位小数
  948. const fixedNum = num.toFixed(2);
  949. //如果小数部分全是 0,去掉小数部分
  950. if(fixedNum.endsWith(".00")) {
  951. return parseInt(fixedNum, 10);
  952. }
  953. return parseFloat(fixedNum);
  954. }
  955. }
  956. /**
  957. * 去掉小数点后无用的0
  958. * @param numberString 字符串呢
  959. * @returns
  960. */
  961. public static removeZeros(numberString: string): string {
  962. const trimmedString = numberString.trim(); // 去除首尾空格
  963. const decimalIndex = trimmedString.indexOf('.');
  964. if (decimalIndex !== -1) {
  965. let endIndex = trimmedString.length - 1;
  966. while (trimmedString[endIndex] === '0') {
  967. endIndex--;
  968. }
  969. if (trimmedString[endIndex] === '.') {
  970. endIndex--; // 如果小数点后面全是零,也去掉小数点
  971. }
  972. return trimmedString.slice(0, endIndex + 1);
  973. }
  974. return trimmedString;
  975. }
  976. /**
  977. * 数组移除某一个元素
  978. */
  979. public static remove(arr,param){
  980. let index = arr.indexOf(param)
  981. if (index > -1) {
  982. arr.splice(index, 1)
  983. }
  984. }
  985. /**
  986. * 16进制的颜色
  987. * @param hexColor
  988. * @returns
  989. */
  990. public static hexColor(hexColor) {
  991. const hex = hexColor.replace(/^#?/, "0x");
  992. const c = parseInt(hex);
  993. const r = c >> 16;
  994. const g = (65280 & c) >> 8;
  995. const b = 255 & c;
  996. return new Color(r, g, b, 255);
  997. };
  998. /**
  999. * 检查是否有铭感词
  1000. * @param str 文字
  1001. * @returns 返回是否有铭感词
  1002. */
  1003. public static filtion(str): boolean{
  1004. return sensitiveArray.some(word => {
  1005. //忽略大小写匹配
  1006. const lowerCaseStr = str.toLowerCase();
  1007. const lowerCaseWord = word.toLowerCase();
  1008. return lowerCaseStr.includes(lowerCaseWord);
  1009. });
  1010. }
  1011. /**
  1012. * 计算两点间的距离
  1013. */
  1014. public static pDistance(localPos: Vec3,tarPos: Vec3): number {
  1015. let dx = localPos.x - tarPos.x;
  1016. let dy = localPos.y - tarPos.y;
  1017. let dis = Math.sqrt(dx * dx + dy * dy);
  1018. return dis;
  1019. }
  1020. /**
  1021. * 计算两点之间的绝对距离
  1022. */
  1023. public static pAbsDistance(a: Vec3,b: Vec3): number {
  1024. let p: number = Math.abs(a.x - b.x);
  1025. let k: number = Math.abs(a.y - b.y);
  1026. return p + k;
  1027. }
  1028. /**
  1029. * 角度转向量
  1030. * @param angle
  1031. * @returns
  1032. */
  1033. public static angle_to_vector (angle: number): Vec2 {
  1034. // tan = sin / cos 将传入的角度转为弧度
  1035. let radian = this.angle_to_radian(angle);
  1036. // 算出cos,sin和tan
  1037. let cos = Math.cos(radian);// 邻边 / 斜边
  1038. let sin = Math.sin(radian);// 对边 / 斜边
  1039. let tan = sin / cos;// 对边 / 邻边
  1040. //结合在一起并归一化
  1041. let vec = new Vec2(cos, sin).normalize();
  1042. //返回向量
  1043. return(vec);
  1044. }
  1045. /**
  1046. * 向量转角度
  1047. * @param vector
  1048. * @returns
  1049. */
  1050. public static vector_to_angle (vector: Vec2): number {
  1051. //将传入的向量归一化
  1052. let dir = vector.normalize();
  1053. //计算出目标角度的弧度
  1054. let radian = dir.signAngle(new Vec2(1, 0));
  1055. //把弧度计算成角度
  1056. let angle = -this.radian_to_angle(radian);
  1057. //返回角度
  1058. return(angle);
  1059. }
  1060. /**
  1061. * 角度转弧度
  1062. * @param angle
  1063. * @returns
  1064. */
  1065. public static angle_to_radian (angle: number): number {
  1066. //角度转弧度公式 π / 180 * 角度 计算出弧度
  1067. let radian = Math.PI / 180 * angle;
  1068. //返回弧度
  1069. return(radian);
  1070. }
  1071. /**
  1072. * 弧度转角度
  1073. * @param radian
  1074. * @returns
  1075. */
  1076. public static radian_to_angle (radian: number): number {
  1077. //弧度转角度公式 180 / π * 弧度 计算出角度
  1078. let angle = 180 / Math.PI * radian;
  1079. //返回角度
  1080. return(angle);
  1081. }
  1082. /**
  1083. * 计算弧度
  1084. * @param start
  1085. * @param end
  1086. */
  1087. public static getAngle(start: Vec3, end: Vec3) {
  1088. //两点的x、y值
  1089. var x = end.x - start.x;
  1090. var y = end.y - start.y;
  1091. var hypotenuse = Math.sqrt(x * x + y * y);
  1092. //斜边长度
  1093. var cos = x / hypotenuse;
  1094. var radian = Math.acos(cos);
  1095. //求出弧度
  1096. var angle = 180 / (Math.PI / radian);
  1097. //用弧度算出角度
  1098. if (y < 0) {
  1099. angle = 0 - angle;
  1100. }else if (y == 0 && x < 0) {
  1101. angle = 180;
  1102. }
  1103. return angle;
  1104. }
  1105. /**
  1106. * 扣血转化成字符串
  1107. * @param number
  1108. * @returns
  1109. */
  1110. public static numberToString(value: number | string) {
  1111. console.log("当前的值: " + value);
  1112. const num = Number(value);
  1113. if (isNaN(num)) return "0";
  1114. //特殊处理:小于1百万显示完整数字
  1115. if(num < 100000) {
  1116. return Math.floor(num).toString();
  1117. }
  1118. console.log("转换过后的值: " + this.formatBigNumber(num, ["", "K", "M", "B"]));
  1119. //使用标准格式化中文单位
  1120. return this.formatBigNumber(num, ["", "K", "M", "B"]);
  1121. }
  1122. /**
  1123. * 基本用法(自动中文单位)
  1124. console.log(formatChineseBigNumber(998989)); // "998989"
  1125. console.log(formatChineseBigNumber(24326755)); // "2432.67万"
  1126. console.log(formatChineseBigNumber(958753111)); // "9.58亿"
  1127. console.log(formatChineseBigNumber(9587531115987));// "9.58万亿"
  1128. //高级用法(自定义单位)
  1129. console.log(formatBigNumber(1500, ["", "K", "M"], 1000)); // "1.5K"
  1130. console.log(formatBigNumber(2500000, ["", "K", "M"], 1000)); // "2.5M"
  1131. * 格式化大数字带单位
  1132. * @param value 要格式化的数字或字符串
  1133. * @param customUnits 可选的自定义单位数组(从大到小排序)
  1134. * @param customK 可选的进制基数(默认10000)
  1135. * @returns 格式化后的字符串
  1136. */
  1137. public static formatBigNumber(
  1138. value: number | string,
  1139. customUnits?: string[],
  1140. customK: number = 10000
  1141. ): string {
  1142. const num = Number(value);
  1143. if (isNaN(num)) return "0";
  1144. const defaultUnits = ["", "万", "亿", "万亿"];
  1145. const units = customUnits || defaultUnits;
  1146. const k = customK;
  1147. //小于1万的直接返回原数字
  1148. if(num < k) {
  1149. return num % 1 === 0 ? num.toString() : num.toFixed(2);
  1150. }
  1151. //计算单位
  1152. const i = Math.floor(Math.log(num) / Math.log(k));
  1153. const unitIndex = Math.min(i, units.length - 1);
  1154. const divided = num / Math.pow(k, unitIndex);
  1155. // 智能确定小数位数
  1156. let decimalPlaces = 2;
  1157. if (divided >= 1000) decimalPlaces = 0;
  1158. else if (divided >= 100) decimalPlaces = 1;
  1159. // 格式化并去除无效的.00
  1160. let formatted = divided.toFixed(decimalPlaces);
  1161. if(formatted.endsWith(".00")) {
  1162. formatted = formatted.slice(0, -3);
  1163. }else if (formatted.endsWith("0") && decimalPlaces > 0) {
  1164. //处理类似1032.50万 -> 1032.5万的情况
  1165. formatted = formatted.replace(/0+$/, "").replace(/\.$/, "");
  1166. }
  1167. return formatted + units[unitIndex];
  1168. }
  1169. /**
  1170. * 将某个节点下的坐标转移到另外一个节点
  1171. * @param fromNode 坐标所在的节点
  1172. * @param toNode 目标节点
  1173. * @returns 转换后的坐标值
  1174. */
  1175. public static convertPosition(fromNode: Node, toNode: Node): Vec3 {
  1176. let pos: Vec3 = fromNode.position.clone();
  1177. // 将 pos 转为世界坐标系下的坐标
  1178. const worldPos: Vec3 = fromNode.parent.getComponent(UITransform).convertToWorldSpaceAR(pos);
  1179. // 将世界坐标系下的坐标转为目标节点的局部坐标系下的坐标
  1180. const localPos: Vec3 = toNode.getComponent(UITransform).convertToNodeSpaceAR(worldPos);
  1181. return localPos;
  1182. }
  1183. /**
  1184. * 以敌人的中心点 攻击范围为半径 产生随机坐标
  1185. * @param center 中心点
  1186. * @param radius 半径
  1187. * @returns
  1188. */
  1189. public static randomPointGenerator(center: Vec3,radius: number){
  1190. //随机角度
  1191. let angle = Math.random() * Math.PI * 2;
  1192. //随机距离
  1193. let distance = Math.sqrt(Math.random()) * radius;
  1194. //根据极坐标转换成笛卡尔坐标
  1195. const x = center.x + distance * Math.cos(angle);
  1196. const y = center.y + distance * Math.sin(angle);
  1197. return new Vec3(x,y,1);
  1198. }
  1199. /**
  1200. * 将某个节点上的坐标转移到另外一个节点
  1201. * @param fromNode 坐标所在的节点
  1202. * @param toNode 目标节点
  1203. * @returns 转换后的坐标值
  1204. */
  1205. public static convertPositionPos(fromNode: Node,pos: Vec3, toNode: Node): Vec3 {
  1206. let nPos: Vec3 = pos.clone();
  1207. // 将 pos 转为世界坐标系下的坐标
  1208. const worldPos: Vec3 = fromNode.parent.getComponent(UITransform).convertToWorldSpaceAR(nPos);
  1209. // 将世界坐标系下的坐标转为目标节点的局部坐标系下的坐标
  1210. const localPos: Vec3 = toNode.getComponent(UITransform).convertToNodeSpaceAR(worldPos);
  1211. return localPos;
  1212. }
  1213. /**
  1214. * 获取屏幕中心对应的3D世界坐标
  1215. * @param camera 摄像机
  1216. * @param groundY 地面高度
  1217. * @returns
  1218. */
  1219. public static getScreenCenterWorldPos(camera: Camera,groundY:number = 0): Vec3 {
  1220. if (!camera) return Vec3.ZERO;
  1221. //获取屏幕中心坐标
  1222. const screenSize = view.getVisibleSize();
  1223. const centerX = screenSize.width / 2;
  1224. const centerY = screenSize.height / 2;
  1225. //生成从屏幕中心发出的射线
  1226. const ray = camera.screenPointToRay(centerX, centerY);
  1227. //计算射线与特定平面的交点(假设地面Y=0)
  1228. const distance = (groundY - ray.o.y) / ray.d.y;
  1229. // 4. 返回交点坐标
  1230. return new Vec3(
  1231. ray.o.x + ray.d.x * distance,
  1232. ray.o.y + ray.d.y * distance,
  1233. ray.o.z + ray.d.z * distance
  1234. );
  1235. }
  1236. /**
  1237. * 让相机平滑看向目标节点
  1238. * @param selfNode 设置方向的节点
  1239. * @param targetNode 目标节点
  1240. * @param duration 平滑过渡时间(秒),默认0.3秒
  1241. */
  1242. public static lookAtNode(selfNode: Node, targetNode: Node, duration: number = 0.3): void {
  1243. if(!selfNode || !targetNode) return;
  1244. //计算水平方向(忽略Y轴差异)
  1245. const targetPos = targetNode.worldPosition;
  1246. const selfPos = selfNode.worldPosition;
  1247. const direction = new Vec3();
  1248. Vec3.subtract(direction, targetPos, selfPos);
  1249. //关键修复:将Y轴归零计算水平方向
  1250. const horizontalDir = new Vec3(direction.x, 0, direction.z).normalize();
  1251. //计算垂直角度(仅Y轴差异)
  1252. const distance = Vec3.distance(targetPos, selfPos);
  1253. const heightDiff = targetPos.y - selfPos.y;
  1254. const verticalAngle = math.toRadian(math.clamp(
  1255. Math.atan2(heightDiff, Math.sqrt(direction.x * direction.x + direction.z * direction.z)) * 180 / Math.PI,
  1256. -89, 89 // 限制在±89度内防止万向节锁
  1257. ));
  1258. //构建最终旋转(先水平后垂直)
  1259. const targetRotation = new Quat();
  1260. Quat.fromViewUp(targetRotation, horizontalDir, Vec3.UP);
  1261. //添加垂直旋转
  1262. const verticalRot = new Quat();
  1263. Quat.fromEuler(verticalRot, verticalAngle, 0, 0);
  1264. Quat.multiply(targetRotation, targetRotation, verticalRot);
  1265. //应用旋转
  1266. if(duration <= 0) {
  1267. selfNode.setRotation(targetRotation);
  1268. }else{
  1269. const startRotation = selfNode.rotation.clone();
  1270. tween(selfNode)
  1271. .to(duration, { rotation: targetRotation }, {
  1272. onUpdate: (_, ratio: number) => {
  1273. const currentRot = new Quat();
  1274. Quat.slerp(currentRot, startRotation, targetRotation, ratio);
  1275. selfNode.setRotation(currentRot);
  1276. }
  1277. })
  1278. .start();
  1279. }
  1280. }
  1281. /**
  1282. * 根据A,B两个坐标点 和抛物线的弧度 来计算中心点坐标
  1283. */
  1284. public static calculateParabolaCenter(start: Vec3, end: Vec3){
  1285. // 计算两点之间的水平距离
  1286. const deltaX = end.x - start.x;
  1287. // 将控制点的 x 坐标设置为两点的中点
  1288. const controlX = (start.x + end.x) / 2;
  1289. // 计算抛物线的最高点,使其位于两点之间的中间位置 可以根据需要调整最高点的位置
  1290. const highestY = Math.max(start.y, end.y) + Math.abs(deltaX) / 4;
  1291. // 计算控制点的 y 坐标
  1292. const controlY = highestY;
  1293. //返回抛物线的中心坐标点
  1294. return new Vec3(controlX, controlY);
  1295. }
  1296. /**
  1297. * 在UI坐标系内生成安全的随机点
  1298. * @param uiParent 父节点(UITransform)
  1299. * @param center 中心点(Vec3) - 基于父节点的本地坐标
  1300. * @param radius 半径(Number) - 单位为像素
  1301. */
  1302. public static randomUIPointGenerator(uiParent: Node, center: Vec3, radius: number): Vec3 {
  1303. // 将像素半径转换为世界坐标比例
  1304. const uiSize = uiParent.getComponent(UITransform).contentSize;
  1305. const maxRadius = Math.min(
  1306. uiSize.width / 2 - Math.abs(center.x),
  1307. uiSize.height / 2 - Math.abs(center.y)
  1308. );
  1309. const safeRadius = Math.min(radius, maxRadius);
  1310. //生成随机角度和距离(距离使用平方根使分布均匀)
  1311. const angle = Math.random() * Math.PI * 2;
  1312. const distance = Math.sqrt(Math.random()) * safeRadius;
  1313. //转换为本地坐标
  1314. return new Vec3(
  1315. center.x + distance * Math.cos(angle),
  1316. center.y + distance * Math.sin(angle),
  1317. 0 // UI节点z轴设为0
  1318. );
  1319. }
  1320. /**
  1321. * 生成一个随机颜色值的函数
  1322. */
  1323. public static getRandomColor() {
  1324. let r = Math.floor(Math.random() * 256);
  1325. let g = Math.floor(Math.random() * 256);
  1326. let b = Math.floor(Math.random() * 256);
  1327. return new Color(r, g, b);
  1328. }
  1329. }
  1330. /**
  1331. * //摄像机正前方是跟随枪
  1332. const targetPos = Game.I.player.node.worldPosition;
  1333. const playerRotation = Game.I.player.node.worldRotation;
  1334. const offset = new Vec3(0, 2, -8); // 上方2单位,后方5单位
  1335. // 计算世界空间偏移
  1336. const worldOffset = Vec3.transformQuat(new Vec3(),
  1337. offset,
  1338. playerRotation
  1339. );
  1340. Game.I.camera.node.worldPosition = Vec3.add(new Vec3(), targetPos, worldOffset);
  1341. Game.I.camera.node.lookAt(targetPos);
  1342. let crossPos: Vec3 = Utils.getScreenCenterWorldPos(Game.I.camera);
  1343. const direction = Vec3.subtract(new Vec3(), crossPos, gun.node.worldPosition);
  1344. direction.normalize();
  1345. Game.I.camera.node.forward = direction;
  1346. */