import { CASH_BASE } from '@scripts/constants/App'; import { sys } from 'cc'; import { ANDROID, IOS } from 'cc/env'; const STORAGE_ENCRYPTION_HEAD = 'a6072637'; const STORAGE_ENCRYPTION_PASSWORD = 'xbd4jl'; const STORAGE_ENCRYPTION_ENABLED = true; /** * 将数字格式化为财务展示格式:使用千位分隔符并保留两位小数。 * 例如:1234567.8 -> 1,234,567.80。 */ export function formatCurrency(value: number | string, decimal = 0): string { if (typeof value === 'string') value = Number(value); if (!Number.isFinite(value)) value = 0; return value.toLocaleString('en-US', { minimumFractionDigits: decimal, maximumFractionDigits: decimal, useGrouping: true, }); } export function formatSCNum(value: number | string) { return formatCurrency(value, 2); } export function formatGCNum(value: number | string) { return formatCurrency(value, 0); } export function formatCash(value: number) { value = Math.floor(value / CASH_BASE); return formatCurrency(value, 2); } const SHORT_MONTH_NAMES = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', ]; /** Converts `YYYY-MM-DD HH:mm:ss.SSS` into `Mon D, HH:mm`. */ export function formatShortDateTime(value: string): string { const match = value.match(/^(\d{4})-(\d{1,2})-(\d{1,2})\s+(\d{1,2}):(\d{2})/); if (!match) return ''; const month = Number(match[2]); const day = Number(match[3]); const hour = Number(match[4]); const minute = Number(match[5]); if (month < 1 || month > 12 || day < 1 || day > 31 || hour > 23 || minute > 59) return ''; return `${SHORT_MONTH_NAMES[month - 1]} ${day}, ${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`; } /** * 商品货币显示 * 小于 10,000 时展示完整数值;小于 1,000,000 时向下取整为 K; * 其余数值向下截取到 0.01M,并固定展示两位小数。 * @param value 商品数量 */ export function formatGoodsNum(value: number): string { if (!Number.isFinite(value)) value = 0; if (value < 10_000) { let decimal = value % 1 > 0 ? 2 : 0; return value.toLocaleString('en-US', { minimumFractionDigits: decimal, maximumFractionDigits: 20, useGrouping: true, }); } if (value < 1_000_000) { return `${Math.floor(value / 1_000)}K`; } return `${(Math.floor(value / 10_000) / 100).toFixed(2)}M`; } /** * 读取本地数据,并兼容 game_web_us2 写入的加密格式。 */ export function localStorageGet(key: string, defaultValue: T = '' as T): string | T { let value = sys.localStorage.getItem(key); if (value === null) return defaultValue; if (value.startsWith(STORAGE_ENCRYPTION_HEAD)) { try { value = decrypt(value.substring(STORAGE_ENCRYPTION_HEAD.length)); } catch { // 与原项目保持一致:解密失败时返回空字符串。 value = ''; } } return value; } /** * 加密并保存本地数据。空字符串不加密,行为与 game_web_us2 保持一致。 */ export function localStorageSet(key: string, value: unknown): void { let storageValue = value; if (STORAGE_ENCRYPTION_ENABLED && storageValue !== null && storageValue !== '') { const plainValue = typeof storageValue === 'string' ? storageValue : String(storageValue); storageValue = plainValue; try { storageValue = STORAGE_ENCRYPTION_HEAD + encrypt(plainValue); } catch { // 加密失败时沿用原值保存,兼容原项目行为。 } } sys.localStorage.setItem(key, String(storageValue)); } export function localStorageRemove(key: string): void { sys.localStorage.removeItem(key); } function encrypt(value: string, password = STORAGE_ENCRYPTION_PASSWORD): string { if (value === '') return ''; const source = encodeURIComponent(value); const encodedPassword = encodeURIComponent(password || STORAGE_ENCRYPTION_PASSWORD); if (encodedPassword.length === 0) return ''; let randomSeed = ''; for (let index = 0; index < encodedPassword.length; index += 1) { randomSeed += encodedPassword.charCodeAt(index).toString(); } const seedPosition = Math.floor(randomSeed.length / 5); const multiplier = Number.parseInt( randomSeed.charAt(seedPosition) + randomSeed.charAt(seedPosition * 2) + randomSeed.charAt(seedPosition * 3) + randomSeed.charAt(seedPosition * 4) + randomSeed.charAt(seedPosition * 5), 10, ); const increment = Math.ceil(encodedPassword.length / 2); const modulus = Math.pow(2, 31) - 1; if (multiplier < 2) return ''; let salt = Math.round(Math.random() * 1_000_000_000) % 100_000_000; randomSeed += salt; randomSeed = reduceSeed(randomSeed); let pseudoRandom = (multiplier * Number(randomSeed) + increment) % modulus; let encrypted = ''; for (let index = 0; index < source.length; index += 1) { const encryptedChar = source.charCodeAt(index) ^ Math.floor((pseudoRandom / modulus) * 255); encrypted += encryptedChar < 16 ? `0${encryptedChar.toString(16)}` : encryptedChar.toString(16); pseudoRandom = (multiplier * pseudoRandom + increment) % modulus; } let saltHex = salt.toString(16); while (saltHex.length < 8) saltHex = `0${saltHex}`; return encrypted + saltHex; } function decrypt(value: string, password = STORAGE_ENCRYPTION_PASSWORD): string { if (value === '') return ''; const encodedPassword = encodeURIComponent(password || STORAGE_ENCRYPTION_PASSWORD); if (value.length < 8 || encodedPassword.length === 0) return ''; let randomSeed = ''; for (let index = 0; index < encodedPassword.length; index += 1) { randomSeed += encodedPassword.charCodeAt(index).toString(); } const seedPosition = Math.floor(randomSeed.length / 5); const multiplier = Number.parseInt( randomSeed.charAt(seedPosition) + randomSeed.charAt(seedPosition * 2) + randomSeed.charAt(seedPosition * 3) + randomSeed.charAt(seedPosition * 4) + randomSeed.charAt(seedPosition * 5), 10, ); const increment = Math.round(encodedPassword.length / 2); const modulus = Math.pow(2, 31) - 1; const salt = Number.parseInt(value.substring(value.length - 8), 16); const encrypted = value.substring(0, value.length - 8); randomSeed += salt; randomSeed = reduceSeed(randomSeed); let pseudoRandom = (multiplier * Number(randomSeed) + increment) % modulus; let decrypted = ''; for (let index = 0; index < encrypted.length; index += 2) { const decryptedChar = Number.parseInt(encrypted.substring(index, index + 2), 16) ^ Math.floor((pseudoRandom / modulus) * 255); decrypted += String.fromCharCode(decryptedChar); pseudoRandom = (multiplier * pseudoRandom + increment) % modulus; } return decodeURIComponent(decrypted); } function reduceSeed(seed: string): string { let result = seed; while (result.length > 10) { let remaining = result; let total = 0; while (remaining.length > 0) { total += Number.parseInt(remaining.substring(0, 10), 10); remaining = remaining.substring(10); } result = total.toString(); } return result; } /** * 进入平台判定:0=普通浏览器 1=iOS壳包 2=Android壳包 3=PWA(桌面图标/TWA 进入) * 用于按来源差异化展示(如提现页官网引流提示:桌面图标进入的玩家不显示)。 * 壳包判定优先于 PWA;TWA 判定内联实现,避免 helpers→UAUtils 循环依赖。 */ export function getEnterPlatform(): 0 | 1 | 2 | 3 { if (ANDROID) return 2; if (IOS) return 1; return 0; } export function createUUID() { const chars: string[] = []; const hexDigits = '0123456789abcdef'; for (let index = 0; index < 36; index += 1) { chars[index] = hexDigits.charAt( Math.floor(Math.random() * 16), ); } // UUID version 4:0100 chars[14] = '4'; // RFC variant:10xx const variant = (Number.parseInt(chars[19], 16) & 0x3) | 0x8; chars[19] = hexDigits.charAt(variant); chars[8] = '-'; chars[13] = '-'; chars[18] = '-'; chars[23] = '-'; return chars.join(''); } export function getUrlParams(url: string) { const queryIndex = url.indexOf('?'); const params = {}; if (queryIndex >= 0) { url.slice(queryIndex + 1).split('&').forEach((entry) => { if (!entry) return; const separatorIndex = entry.indexOf('='); const rawKey = separatorIndex >= 0 ? entry.slice(0, separatorIndex) : entry; const rawValue = separatorIndex >= 0 ? entry.slice(separatorIndex + 1) : ''; try { const key = decodeURIComponent(rawKey.replace(/\+/g, ' ')); params[key] = decodeURIComponent(rawValue.replace(/\+/g, ' ')); } catch { // Preserve malformed values instead of crashing the native callback. params[rawKey] = rawValue; } }); } return params; } export function waitForSeconds(duration: number) { return new Promise((resolve, reject) => { setTimeout(resolve, duration * 1000) }) } export function formatMaskContent(content: string, prefix: number, suffix: number) { const startIdx = Math.min(prefix, content.length); const endIdx = content.length - suffix; const first = content.substring(0, startIdx); const last = content.substring(endIdx, content.length) return `${first}***${last}` } export function formatMaskNickName(nickName: string) { return formatMaskContent(nickName, 2, 1); } export function formatMaskEmail(email: string) { if (!email || email.length === 0) return ''; let index = email.indexOf('@'); if (index === -1) return formatMaskContent(email, 2, 3); return formatMaskContent(email, 2, email.length - index); } export function checkIsEmail(str: string): boolean { const s = str.trim(); if (!s) return false; const reg = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return reg.test(s); } export function safeFloor(num: number, decimal = 2) { let unit = Math.pow(10, decimal); return Math.floor(num * unit) / unit } export function formatMailTimeToMMDDYYYY(timeStr: string): string { const date = new Date(timeStr); if (isNaN(date.getTime())) { return ""; // 无效时间返回空 } const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); const year = date.getFullYear(); return `${month}-${day}-${year}`; } export function parseMailRewardStr(str: string) { const result = []; const itemStrList = str.split(";").filter(Boolean); for (const itemStr of itemStrList) { const parts = itemStr.split(","); if (parts.length !== 2) continue; const type = parts[0]; // 直接字符串,不转数字! const rawRewardStr = parts[1]; const rawReward = Number(rawRewardStr); if (isNaN(rawReward)) continue; if (rawReward == 0) continue; result.push({ type, reward: rawReward / 100, rawReward: rawRewardStr }); } return result; }