Utils.ts 46 KB

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