Helper.ts 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. import { sys } from 'cc';
  2. import { ANDROID, IOS } from 'cc/env';
  3. const STORAGE_ENCRYPTION_HEAD = 'a6072637';
  4. const STORAGE_ENCRYPTION_PASSWORD = 'xbd4jl';
  5. const STORAGE_ENCRYPTION_ENABLED = true;
  6. /**
  7. * 将数字格式化为财务展示格式:使用千位分隔符并保留两位小数。
  8. * 例如:1234567.8 -> 1,234,567.80。
  9. */
  10. export function formatCurrency(value: number | string, decimal = 0): string {
  11. if (typeof value === 'string') value = Number(value);
  12. if (!Number.isFinite(value)) value = 0;
  13. return value.toLocaleString('en-US', {
  14. minimumFractionDigits: decimal,
  15. maximumFractionDigits: decimal,
  16. useGrouping: true,
  17. });
  18. }
  19. export function formatSCNum(value: number | string) {
  20. return formatCurrency(value, 2);
  21. }
  22. export function formatGCNum(value: number | string) {
  23. return formatCurrency(value, 0);
  24. }
  25. /**
  26. * 商品货币显示
  27. * 小于 10,000 时展示完整数值;小于 1,000,000 时向下取整为 K;
  28. * 其余数值向下截取到 0.01M,并固定展示两位小数。
  29. * @param value 商品数量
  30. */
  31. export function formatGoodsNum(value: number): string {
  32. if (!Number.isFinite(value)) value = 0;
  33. if (value < 10_000) {
  34. let decimal = value % 1 > 0 ? 2 : 0;
  35. return value.toLocaleString('en-US', {
  36. minimumFractionDigits: decimal,
  37. maximumFractionDigits: 20,
  38. useGrouping: true,
  39. });
  40. }
  41. if (value < 1_000_000) {
  42. return `${Math.floor(value / 1_000)}K`;
  43. }
  44. return `${(Math.floor(value / 10_000) / 100).toFixed(2)}M`;
  45. }
  46. /**
  47. * 读取本地数据,并兼容 game_web_us2 写入的加密格式。
  48. */
  49. export function localStorageGet<T = string>(key: string, defaultValue: T = '' as T): string | T {
  50. let value = sys.localStorage.getItem(key);
  51. if (value === null) return defaultValue;
  52. if (value.startsWith(STORAGE_ENCRYPTION_HEAD)) {
  53. try {
  54. value = decrypt(value.substring(STORAGE_ENCRYPTION_HEAD.length));
  55. } catch {
  56. // 与原项目保持一致:解密失败时返回空字符串。
  57. value = '';
  58. }
  59. }
  60. return value;
  61. }
  62. /**
  63. * 加密并保存本地数据。空字符串不加密,行为与 game_web_us2 保持一致。
  64. */
  65. export function localStorageSet(key: string, value: unknown): void {
  66. let storageValue = value;
  67. if (STORAGE_ENCRYPTION_ENABLED && storageValue !== null && storageValue !== '') {
  68. const plainValue = typeof storageValue === 'string' ? storageValue : String(storageValue);
  69. storageValue = plainValue;
  70. try {
  71. storageValue = STORAGE_ENCRYPTION_HEAD + encrypt(plainValue);
  72. } catch {
  73. // 加密失败时沿用原值保存,兼容原项目行为。
  74. }
  75. }
  76. sys.localStorage.setItem(key, String(storageValue));
  77. }
  78. export function localStorageRemove(key: string): void {
  79. sys.localStorage.removeItem(key);
  80. }
  81. function encrypt(value: string, password = STORAGE_ENCRYPTION_PASSWORD): string {
  82. if (value === '') return '';
  83. const source = encodeURIComponent(value);
  84. const encodedPassword = encodeURIComponent(password || STORAGE_ENCRYPTION_PASSWORD);
  85. if (encodedPassword.length === 0) return '';
  86. let randomSeed = '';
  87. for (let index = 0; index < encodedPassword.length; index += 1) {
  88. randomSeed += encodedPassword.charCodeAt(index).toString();
  89. }
  90. const seedPosition = Math.floor(randomSeed.length / 5);
  91. const multiplier = Number.parseInt(
  92. randomSeed.charAt(seedPosition)
  93. + randomSeed.charAt(seedPosition * 2)
  94. + randomSeed.charAt(seedPosition * 3)
  95. + randomSeed.charAt(seedPosition * 4)
  96. + randomSeed.charAt(seedPosition * 5),
  97. 10,
  98. );
  99. const increment = Math.ceil(encodedPassword.length / 2);
  100. const modulus = Math.pow(2, 31) - 1;
  101. if (multiplier < 2) return '';
  102. let salt = Math.round(Math.random() * 1_000_000_000) % 100_000_000;
  103. randomSeed += salt;
  104. randomSeed = reduceSeed(randomSeed);
  105. let pseudoRandom = (multiplier * Number(randomSeed) + increment) % modulus;
  106. let encrypted = '';
  107. for (let index = 0; index < source.length; index += 1) {
  108. const encryptedChar = source.charCodeAt(index) ^ Math.floor((pseudoRandom / modulus) * 255);
  109. encrypted += encryptedChar < 16
  110. ? `0${encryptedChar.toString(16)}`
  111. : encryptedChar.toString(16);
  112. pseudoRandom = (multiplier * pseudoRandom + increment) % modulus;
  113. }
  114. let saltHex = salt.toString(16);
  115. while (saltHex.length < 8) saltHex = `0${saltHex}`;
  116. return encrypted + saltHex;
  117. }
  118. function decrypt(value: string, password = STORAGE_ENCRYPTION_PASSWORD): string {
  119. if (value === '') return '';
  120. const encodedPassword = encodeURIComponent(password || STORAGE_ENCRYPTION_PASSWORD);
  121. if (value.length < 8 || encodedPassword.length === 0) return '';
  122. let randomSeed = '';
  123. for (let index = 0; index < encodedPassword.length; index += 1) {
  124. randomSeed += encodedPassword.charCodeAt(index).toString();
  125. }
  126. const seedPosition = Math.floor(randomSeed.length / 5);
  127. const multiplier = Number.parseInt(
  128. randomSeed.charAt(seedPosition)
  129. + randomSeed.charAt(seedPosition * 2)
  130. + randomSeed.charAt(seedPosition * 3)
  131. + randomSeed.charAt(seedPosition * 4)
  132. + randomSeed.charAt(seedPosition * 5),
  133. 10,
  134. );
  135. const increment = Math.round(encodedPassword.length / 2);
  136. const modulus = Math.pow(2, 31) - 1;
  137. const salt = Number.parseInt(value.substring(value.length - 8), 16);
  138. const encrypted = value.substring(0, value.length - 8);
  139. randomSeed += salt;
  140. randomSeed = reduceSeed(randomSeed);
  141. let pseudoRandom = (multiplier * Number(randomSeed) + increment) % modulus;
  142. let decrypted = '';
  143. for (let index = 0; index < encrypted.length; index += 2) {
  144. const decryptedChar = Number.parseInt(encrypted.substring(index, index + 2), 16)
  145. ^ Math.floor((pseudoRandom / modulus) * 255);
  146. decrypted += String.fromCharCode(decryptedChar);
  147. pseudoRandom = (multiplier * pseudoRandom + increment) % modulus;
  148. }
  149. return decodeURIComponent(decrypted);
  150. }
  151. function reduceSeed(seed: string): string {
  152. let result = seed;
  153. while (result.length > 10) {
  154. let remaining = result;
  155. let total = 0;
  156. while (remaining.length > 0) {
  157. total += Number.parseInt(remaining.substring(0, 10), 10);
  158. remaining = remaining.substring(10);
  159. }
  160. result = total.toString();
  161. }
  162. return result;
  163. }
  164. /**
  165. * 进入平台判定:0=普通浏览器 1=iOS壳包 2=Android壳包 3=PWA(桌面图标/TWA 进入)
  166. * 用于按来源差异化展示(如提现页官网引流提示:桌面图标进入的玩家不显示)。
  167. * 壳包判定优先于 PWA;TWA 判定内联实现,避免 helpers→UAUtils 循环依赖。
  168. */
  169. export function getEnterPlatform(): 0 | 1 | 2 | 3 {
  170. if (ANDROID) return 2;
  171. if (IOS) return 1;
  172. return 0;
  173. }
  174. export function createUUID() {
  175. const chars: string[] = [];
  176. const hexDigits = '0123456789abcdef';
  177. for (let index = 0; index < 36; index += 1) {
  178. chars[index] = hexDigits.charAt(
  179. Math.floor(Math.random() * 16),
  180. );
  181. }
  182. // UUID version 4:0100
  183. chars[14] = '4';
  184. // RFC variant:10xx
  185. const variant = (Number.parseInt(chars[19], 16) & 0x3) | 0x8;
  186. chars[19] = hexDigits.charAt(variant);
  187. chars[8] = '-';
  188. chars[13] = '-';
  189. chars[18] = '-';
  190. chars[23] = '-';
  191. return chars.join('');
  192. }
  193. export function getUrlParams(url: string) {
  194. const queryIndex = url.indexOf('?');
  195. const params = {};
  196. if (queryIndex >= 0) {
  197. url.slice(queryIndex + 1).split('&').forEach((entry) => {
  198. if (!entry) return;
  199. const separatorIndex = entry.indexOf('=');
  200. const rawKey = separatorIndex >= 0 ? entry.slice(0, separatorIndex) : entry;
  201. const rawValue = separatorIndex >= 0 ? entry.slice(separatorIndex + 1) : '';
  202. try {
  203. const key = decodeURIComponent(rawKey.replace(/\+/g, ' '));
  204. params[key] = decodeURIComponent(rawValue.replace(/\+/g, ' '));
  205. } catch {
  206. // Preserve malformed values instead of crashing the native callback.
  207. params[rawKey] = rawValue;
  208. }
  209. });
  210. }
  211. return params;
  212. }
  213. export function waitForSeconds(duration: number) {
  214. return new Promise((resolve, reject) => {
  215. setTimeout(resolve, duration * 1000)
  216. })
  217. }
  218. export function formatMaskNickName(nickName: string, prefix = 2, suffix = 1) {
  219. const startIdx = Math.min(prefix, nickName.length);
  220. const endIdx = nickName.length - suffix;
  221. const first = nickName.substring(0, startIdx);
  222. const last = nickName.substring(endIdx, nickName.length)
  223. return `${first}***${last}`
  224. }