import { _decorator, Component, Node, math } from 'cc'; import { Player } from './Player'; const { ccclass, property } = _decorator; export enum ICON_TYPE { SYMBOL_FREE = 0, SYMBOL_WILD = 1, SYMBOL_COW = 2, SYMBOL_EAGLE = 3, SYMBOL_LEOPARD = 4, SYMBOL_WOLF = 5, SYMBOL_ELK = 6, SYMBOL_A = 7, SYMBOL_K = 8, SYMBOL_Q = 9, SYMBOL_J = 10, SYMBOL_10 = 11, SYMBOL_9 = 12, } @ccclass('GameConstant') export class GameConstant extends Component { public static readonly GAMEID = 69; public static readonly HTTP_HOST = "https://test.pgn-nmu2nd.com/"; public static readonly MAX_ROW: number = 4; public static readonly MAX_COL: number = 6; public static readonly MAX_BET_TIMES: number = 40; public static readonly COL_ITEM_LEN: number = 12; public static readonly MAX_ICON_TYPE: number = 13; public static readonly ROLL_FAST_SPEED: number = 5200; public static readonly ROLL_NORMAL_SPEED: number = 4300; public static readonly SPECIAL_ICON: number = 0; public static readonly ICON_TYPE = ICON_TYPE; public static readonly AUTO_TYPE_COUNT = [-1, 500, 300, 100, 50, 30]; public static readonly ICON_NORMAL_SPRITE_NAME: string[] = [ "01_normal", "02_normal", "03_normal", "04_normal", "05_normal", "06_normal", "07_normal", "08_normal", "09_normal", "10_normal", "11_normal", "12_normal", "S13_normal" ]; public static readonly ICON_BLURRED_SPRITE_NAME: string[] = [ "01_blurred", "02_blurred", "03_blurred", "04_blurred", "05_blurred", "06_blurred", "07_blurred", "08_blurred", "09_blurred", "10_blurred", "11_blurred", "12_blurred", "S13_blurred" ]; public static readonly RESULT_TITLE = ["nice", "mega", "superb", "sensational"]; public static readonly WILD_TIMES = [1, 2, 3, 5]; public static readonly ICON_TYPE_TIMES = [ [80,60,60,40,40,20,20,10,10,10,10], [200,100,100,80,80,40,40,30,30,20,20], [250,200,200,120,120,80,80,60,60,40,40], [300,250,250,200,200,120,120,100,100,80,80] ]; public static readonly ICON_TYPE_PROB = [ [1000, 0, 100, 100, 100, 1200, 1200, 1200, 1200, 1400, 1400, 1600, 1600], [1000, 860, 1200, 1200, 1200, 100, 100, 1200, 1200, 1400, 1400, 1600, 1600], [100, 860, 1000, 1000, 1000, 1000, 1000, 100, 100, 1400, 1400, 1600, 1600], [500, 120, 900, 900, 900, 900, 900, 1200, 1200, 100, 100, 100, 100], [400, 120, 800, 800, 800, 800, 800, 1200, 1200, 1400, 1400, 1600, 1600], [400, 120, 700, 700, 700, 700, 700, 1200, 1200, 1400, 1400, 1600, 1600], ]; public static NO_TIMES_ICON_TYPE = [ [[8, 9, 10, 11, 12, 7],[8, 9, 10, 11, 12, 7],[8, 9, 10, 11, 12, 7],[8, 9, 10, 11, 12, 7]], [[2, 3, 4, 5, 6, 7],[2, 3, 4, 5, 6, 7],[2, 3, 4, 5, 6, 7],[2, 3, 4, 5, 6, 7]] ]; public static TEST_ICON_TYPE = [ [[8, 9, 10, 11, 12, 0],[0, 9, 10, 11, 12, 7],[8, 9, 0, 11, 12, 7],[8, 9, 10, 11, 12, 7]], // [2, 3, 4, 5, 6, 7],[2, 3, 4, 5, 6, 7],[2, 3, 4, 5, 6, 7],[2, 3, 4, 5, 6, 7] // [[2,2,2,2,2,2],[2,2,2,2,2,2],[2,2,2,2,2,2],[2,2,2,2,2,2]] ]; public static readonly ENTER_FREE_COUNTS = [1, 1, 1, 1, 1];//[5, 8, 15, 25, 100] public static readonly bets = [100, 0.2, 0.3, 0.4, 0.5]; public static readonly ENTER_TYPE_PROB = [10, 0]; public static readonly ENTER_FREE_COUNTS_PROB = [100,30,20,5]; public static readonly ENTER_ADD_FREE_COUNTS_PROB = [100,30,20,10,5]; // 通用权重随机工具函数 // =========================== private static weightedRandom(weights: number[]): number { const total = weights.reduce((a, b) => a + b, 0); let rand = Math.random() * total; for (let i = 0; i < weights.length; i++) { if (rand < weights[i]) return i; rand -= weights[i]; } return weights.length - 1; } // =========================== // 是否进入小环节(根据权重) // =========================== private static shouldEnterFreeRound(): boolean { const idx = this.weightedRandom(this.ENTER_TYPE_PROB); return idx === 1; // 1代表进入 } // =========================== // 获取小环节图标数量 // =========================== private static getFreeRoundIconCount(isAddFree: boolean): number { const prob = isAddFree ? this.ENTER_ADD_FREE_COUNTS_PROB : this.ENTER_FREE_COUNTS_PROB; const idx = this.weightedRandom(prob); const base = isAddFree ? 2 : 3; return base + idx; // 范围:小环节 3~6,追加时 2~6 } // ==================== // 1️⃣ 生成随机矩阵 // ==================== // =========================== // =========================== // 生成图形函数(支持小环节) // =========================== public static generateGrid(isTest = false, isFreeRound = false, isAddFree = false): number[][] { const grid: number[][] = []; for (let r = 0; r < GameConstant.MAX_ROW; r++) grid[r] = []; // 计算是否要插入小环节图标 let freeIconPositions: { r: number, c: number }[] = []; if (isFreeRound || isAddFree) { const iconCount = this.getFreeRoundIconCount(isAddFree); const availableCols = Array.from({ length: GameConstant.MAX_COL }, (_, i) => i); const selectedCols = []; // 每列最多一个小环节 while (selectedCols.length < iconCount && availableCols.length > 0) { const idx = Math.floor(Math.random() * availableCols.length); selectedCols.push(availableCols.splice(idx, 1)[0]); } // 为每个选中列随机一行放置小环节图标(0) for (const c of selectedCols) { const r = Math.floor(Math.random() * GameConstant.MAX_ROW); freeIconPositions.push({ r, c }); } } // 生成普通图标 for (let c = 0; c < GameConstant.MAX_COL; c++) { const colProb = GameConstant.ICON_TYPE_PROB[c]; const total = colProb.reduce((a, b) => a + b, 0); for (let r = 0; r < GameConstant.MAX_ROW; r++) { // 若该位置是小环节图标,则直接放0 if (freeIconPositions.some(p => p.r === r && p.c === c)) { grid[r][c] = 0; continue; } // 否则按普通权重随机(剔除0图标) let rand = Math.floor(Math.random() * (total - colProb[0])); let acc = 0, type = 1; for (let i = 1; i < colProb.length; i++) { acc += colProb[i]; if (rand < acc) { type = i; break; } } grid[r][c] = isTest ? GameConstant.TEST_ICON_TYPE[0][r][c] : type; } } return grid; } // ==================== // 2️⃣ 生成百搭倍数矩阵 // ==================== public static generateWildTimes(grid: number[][], isInFree = false): number[][] { const rows = grid.length; const cols = grid[0].length; const wildTimes: number[][] = Array.from({ length: rows }, () => Array(cols).fill(1)); for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { if (grid[r][c] === 1) { // 小环节中才会随机倍数,否则始终为 1 if (isInFree) { const randTimes = GameConstant.WILD_TIMES[Math.floor(Math.random() * GameConstant.WILD_TIMES.length) + 1]; wildTimes[r][c] = randTimes; } else { wildTimes[r][c] = 1; } } } } return wildTimes; } // ==================== // 3️⃣ 计算连线(含百搭规则) // ==================== public static calcSlotLinesWithWildTimes(grid: number[][], wildsTimes: number[][], betScore) { const ROWS = grid.length; const COLS = grid[0].length; const idx = (r: number, c: number) => r * COLS + c; const toRC = (i: number) => [Math.floor(i / COLS), i % COLS]; const canConnect = (a: number, b: number, baseType: number | null) => { if (a === 0 || b === 0) return false; if (b === 1) return true; if (a === 1) return !baseType || b === baseType; return b === a; }; function extendFrom(row: number, col: number, symbol: number, path: number[], baseType: number | null) { const results: any[] = []; if (col >= COLS - 1) { results.push({ path, resolvedType: baseType }); return results; } const nextCol = col + 1; for (let nr = 0; nr < ROWS; nr++) { const nextSym = grid[nr][nextCol]; if (nextSym === 0) continue; let currentBase = baseType; if (!currentBase && symbol !== 1) currentBase = symbol; if (!canConnect(symbol, nextSym, currentBase)) continue; let nextBase = currentBase; if (nextSym !== 1) nextBase = nextSym; const newPath = [...path, idx(nr, nextCol)]; results.push(...extendFrom(nr, nextCol, nextSym, newPath, nextBase)); } if (results.length === 0) results.push({ path, resolvedType: baseType }); return results; } const allLines: any[] = []; var m_lottery_size: number = 0; for (let r = 0; r < ROWS; r++) { const sym = grid[r][0]; if (sym === 0) continue; const startIdx = idx(r, 0); const branches = extendFrom(r, 0, sym, [startIdx], sym === 1 ? null : sym); for (const b of branches) { if (b.path.length >= 3) { if (b.resolvedType === null) { for (const i of b.path) { const [rr, cc] = toRC(i); const s = grid[rr][cc]; if (s !== 1 && s !== 0) { b.resolvedType = s; break; } } } if (b.resolvedType) allLines.push(b); } } } const m_line_icon: number[][] = []; const m_line_size: number[] = []; const m_line_icon_active = Array.from({ length: ROWS }, () => Array(COLS).fill(0)); const m_line_times: number[] = []; const m_line_index: number[][] = []; for (const line of allLines) { const path = line.path; const iconType = line.resolvedType; const icons = path.map(i => { const [r, c] = toRC(i); return grid[r][c]; }); const indexes = path.map(i => i); const size = path.length; for (const i of path) { const [r, c] = toRC(i); m_line_icon_active[r][c] = 1; } let times = 0; if (size >= 3 && size <= 6 && iconType >= 2 && iconType <= 12) { times = GameConstant.ICON_TYPE_TIMES[size - 3][iconType - 2]; } let wildMultiplier = 1; for (const i of path) { const [r, c] = toRC(i); if (grid[r][c] === 1 && wildsTimes[r][c] > 1) { wildMultiplier *= wildsTimes[r][c]; } } times *= wildMultiplier; m_line_icon.push(icons); m_line_size.push(size); m_line_times.push(times); m_line_index.push(indexes); } m_lottery_size = m_line_times.length > 0 ? (m_line_times.reduce((pre, cur)=>{ return pre + cur; }) * betScore) : 0; return { m_line_icon, m_line_size, m_line_icon_active, m_line_times, m_line_index, m_lottery_size }; } // ==================== // 4️⃣ 判断是否进入小环节 // ==================== public static checkSpecialRound(grid: number[][], isInFree = false) { let zeroCount = 0; for (const row of grid) { for (const val of row) { if (val === 0) zeroCount++; } } let m_isSpecialGame = 0; let m_freeCount = 0; let m_freeCountAdd = 0; if (!isInFree) { // 普通模式中:≥3个0触发小环节,获得对应次数 if (zeroCount >= 3) { m_isSpecialGame = 1; const idx = Math.min(zeroCount - 2, GameConstant.ENTER_FREE_COUNTS.length - 1); m_freeCount = GameConstant.ENTER_FREE_COUNTS[idx]; } } else { // 小环节中:若有2个0以上则追加次数 if (zeroCount >= 2) { m_isSpecialGame = 1; const idx = Math.min(zeroCount - 2, GameConstant.ENTER_FREE_COUNTS.length - 1); m_freeCountAdd = GameConstant.ENTER_FREE_COUNTS[idx]; } } return { m_isSpecialGame, m_freeCount, m_freeCountAdd }; } // ==================== // 5️⃣ 主流程控制器 // ==================== public static playRound(betScore) { const results: any[] = []; let isFree = false; // 是否处于免费 let remainingFree = 0; // 剩余免费次数 let totalFreeCount = 0; // 累计免费次数 let hasReachedMaxFree = false; // 是否到达上限 let totalTimes = 0; // 累计倍数 const MAX_TRIES = 100; let tempIndex = 10; do { let m_desk_data: number[][] = []; let m_wildTimes_data: number[][] = []; let lineResult: any; let specialResult: any; let tryCount = 0; let accepted = false; while (tryCount < MAX_TRIES && !accepted) { tryCount++; // === 生成图形 & 倍数 === const shouldEnterFree = !isFree && this.shouldEnterFreeRound(); const isAddFree = isFree && shouldEnterFree; m_desk_data = this.generateGrid(tempIndex++ == 0, shouldEnterFree, isAddFree); m_wildTimes_data = this.generateWildTimes(m_desk_data, isFree); // === 计算连线 & 小环节 === lineResult = this.calcSlotLinesWithWildTimes(m_desk_data, m_wildTimes_data, betScore); specialResult = this.checkSpecialRound(m_desk_data, isFree); const hasLineWin = (lineResult.m_line_icon && lineResult.m_line_icon.length > 0); const addsFree = isFree && specialResult.m_freeCountAdd > 0; // 达到上限后禁止追加 if (hasReachedMaxFree && addsFree) continue; // 触发或追加免费必须无连线 if (addsFree && hasLineWin) continue; // 超出100上限 if (!hasReachedMaxFree && isFree && (totalFreeCount + specialResult.m_freeCountAdd > 100)) { hasReachedMaxFree = true; } accepted = true; } // ❗保底逻辑 if (!accepted) { m_desk_data = this.NO_TIMES_ICON_TYPE[0]; m_wildTimes_data = this.generateWildTimes(m_desk_data, isFree); lineResult = this.calcSlotLinesWithWildTimes(m_desk_data, m_wildTimes_data, betScore); specialResult = { m_isSpecialGame: 0, m_freeCount: 0, m_freeCountAdd: 0 }; if (lineResult && 'm_lottery_size' in lineResult) lineResult.m_lottery_size = 0; } const triggeredFree = specialResult.m_isSpecialGame && !isFree; const addFree = isFree && specialResult.m_isSpecialGame && specialResult.m_freeCountAdd > 0; // 触发或追加免费时不中奖 if (triggeredFree || addFree) { if (lineResult && 'm_lottery_size' in lineResult) lineResult.m_lottery_size = 0; } // === 更新免费次数逻辑 === if (specialResult.m_isSpecialGame) { if (isFree) { if (!hasReachedMaxFree) { remainingFree += specialResult.m_freeCountAdd; totalFreeCount += specialResult.m_freeCountAdd; if (totalFreeCount >= 100) { totalFreeCount = 100; hasReachedMaxFree = true; } } } else { remainingFree = specialResult.m_freeCount; totalFreeCount = specialResult.m_freeCount; if (totalFreeCount >= 100) { totalFreeCount = 100; hasReachedMaxFree = true; } isFree = true; } } // ✅ 在这里再扣除一次免费次数(只对已经在免费状态的轮次) if (isFree && !specialResult.m_isSpecialGame) { remainingFree--; } // === 累计倍数 === if (lineResult && typeof lineResult.m_lottery_size === "number") { totalTimes += (lineResult.m_lottery_size / betScore); } const roundResult = { ...lineResult, ...specialResult, m_desk_data, m_wildTimes_data, m_curTotalFreeCount: remainingFree, tryCount, }; results.push(roundResult); } while (isFree && remainingFree > 0); return { results, totalFreeCount, totalTimes }; } public static getRandomNumber(min, max) { return Math.floor(math.random() * (max - min)) + min; } /** * 通用数字格式化函数 * * @param value 数字值 * @param options 可选配置 * - decimals: 保留的小数位数(默认 2) * - useGrouping: 是否使用千分位分隔符(默认 true) * - trimZero: 是否去掉小数点后的多余 0(默认 true) * - hideDecimalIfZero: 若小数部分为 0 是否隐藏(默认 true) * * @returns 格式化后的字符串 */ public static formatNumber( value: number, options?: { decimals?: number; // 保留小数位 useGrouping?: boolean; // 是否使用千分位 trimZero?: boolean; // 是否去除多余的 0 hideDecimalIfZero?: boolean; // 小数为 0 是否隐藏 currency?: string; //'USD' | 'MYR' | 'EUR' | 'GBP' | 'BRL'; // 货币类型 showCurrencyBefore?: boolean; // 货币符号是否在前面(默认 true) } ): string { if (isNaN(value)) return '0'; const { decimals = 2, useGrouping = true, trimZero = true, hideDecimalIfZero = true, currency = Player.getInstance().currencyType || 'USD', showCurrencyBefore = true, } = options || {}; // 定义货币符号映射表 const currencySymbols: Record = { USD: '$', // 美元 MYR: 'RM', // 林吉特(马来西亚) EUR: '€', // 欧元 GBP: '£', // 英镑 BRL: 'R$', // 巴西雷亚尔 }; const symbol = currencySymbols[currency] || ''; // 保留指定小数位 let str = value.toFixed(decimals); let [integerPart, decimalPart] = str.split('.'); // 千分位格式化 if (useGrouping) { integerPart = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ','); } // 去除无意义的尾部 0 if (trimZero && decimalPart) { decimalPart = decimalPart.replace(/0+$/, ''); } // 如果小数部分为空或全为 0 且允许隐藏 if (hideDecimalIfZero && (!decimalPart || parseInt(decimalPart) === 0)) { return showCurrencyBefore ? `${symbol}${integerPart}` : `${integerPart}${symbol}`; } // 拼接完整结果 const formatted = `${integerPart}.${decimalPart}`; return showCurrencyBefore ? `${symbol}${formatted}` : `${formatted}${symbol}`; } /** * 通用 HTTP 请求方法 * @param type 请求类型:"GET" 或 "POST" * @param url 请求地址 * @param params 请求参数对象(GET时为查询参数,POST时为请求体) * @param isJson 是否使用 JSON 格式(仅对 POST 有效,默认 true) * @param timeout 超时时间(毫秒,默认 10000) */ public static httpRequest( type: 'GET' | 'POST', url: string, params?: Record, isJson: boolean = true, timeout: number = 10000 ): Promise { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); if (type === 'GET' && params) { // 拼接 GET 参数 const query = Object.keys(params) .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`) .join('&'); url += (url.includes('?') ? '&' : '?') + query; } console.log(url); xhr.open(type, url, true); xhr.timeout = timeout; if (type === 'POST') { if (isJson) { xhr.setRequestHeader('Content-Type', 'application/json'); } else { xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); } } xhr.onreadystatechange = () => { if (xhr.readyState === 4) { if (xhr.status >= 200 && xhr.status < 300) { try { const response = JSON.parse(xhr.responseText); resolve(response); } catch { resolve(xhr.responseText as any); } } else { reject(new Error(`HTTP ${type} 请求失败:状态码 ${xhr.status}`)); } } }; xhr.onerror = (e) => { console.error(`HTTP ${type} 请求网络错误:`, e); reject(new Error(`HTTP ${type} 请求网络错误`)); } xhr.ontimeout = () => reject(new Error(`HTTP ${type} 请求超时`)); if (type === 'POST') { const body = isJson ? JSON.stringify(params || {}) : Object.keys(params || {}) .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params![key])}`) .join('&'); xhr.send(body); } else { xhr.send(); } }); } }