/** * 镭射卡加权抽卡 — 5 张不重复,稀有度档位加权 + 第 5 张保底 * * 算法说明: * 1. 单张稀有度按 RARITY_WEIGHTS (50/30/15/4/1) 独立采样 * 2. 同档位内按 style.weight 二次加权选 style * 3. 5 张不重复(总池 45,远大于 5,无需去重惩罚) * 4. 第 5 张保底:若前 4 张全是 N,强制抽 R+(R/SR/SSR/UR 等概率) * 5. 纯 JS,无第三方依赖 * * 使用: * - drawGachaStyles(5) → 5 个 style 完整对象(含 grating_config + bg_prompt 等) * - drawGachaStyleIds(5) → 仅 5 个 style ID * * 边界: * - count <= 0 → 返回空数组 * - count > 池子大小 → 全部返回(顺序随机) * - Math.random 无 NaN/Inf 风险 */ import { LASER_STYLE_POOL } from './stylePool.js' /** @typedef {('N'|'R'|'SR'|'SSR'|'UR')} Rarity */ /** 档位权重(总和 100,代表单次抽卡的稀有度分布百分比) */ export const RARITY_WEIGHTS = /** @type {Record} */ ({ N: 50, R: 30, SR: 15, SSR: 4, UR: 1, }) /** 保底用:抽 R+ 时各档位等概率 */ const R_PLUS_BUCKETS = /** @type {Rarity[]} */ (['R', 'SR', 'SSR', 'UR']) /** * 加权随机选一个稀有度档位 * @returns {Rarity} */ export function pickRarity() { const total = Object.values(RARITY_WEIGHTS).reduce((a, b) => a + b, 0) let r = Math.random() * total for (const [rarity, w] of Object.entries(RARITY_WEIGHTS)) { r -= /** @type {number} */ (w) if (r <= 0) return /** @type {Rarity} */ (rarity) } return 'N' } /** * 在指定档位内按 style.weight 加权抽一个,且不与 exclude 重复 * @param {Rarity} rarity * @param {Set} exclude 已选 style id 集合 * @returns {import('./stylePool.js').LaserStyle | null} */ export function pickStyleInRarity(rarity, exclude) { const candidates = LASER_STYLE_POOL.filter( (s) => s.rarity === rarity && !exclude.has(s.id), ) if (candidates.length === 0) return null const sum = candidates.reduce((a, s) => a + (s.weight || 0), 0) || 1 let r = Math.random() * sum for (const s of candidates) { r -= s.weight || 0 if (r <= 0) return s } return candidates[candidates.length - 1] } /** * 加权抽卡:5 张不重复,保底 ≥1 张 R+ * @param {number} [count=5] 抽卡数量 * @returns {Array} */ export function drawGachaStyles(count = 5) { const n = Math.max(0, Math.floor(count)) if (n === 0) return [] if (n >= LASER_STYLE_POOL.length) { // 数量超过池子大小 → 全部返回(洗牌) const all = LASER_STYLE_POOL.slice() for (let i = all.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)) ;[all[i], all[j]] = [all[j], all[i]] } return all } const picked = /** @type {Array} */ ([]) const used = new Set() for (let i = 0; i < n; i++) { let rarity = pickRarity() // 第 n 张(最后一张)保底:前 n-1 张全是 N → 强制 R+ if (i === n - 1 && picked.length > 0 && picked.every((s) => s.rarity === 'N')) { rarity = R_PLUS_BUCKETS[Math.floor(Math.random() * R_PLUS_BUCKETS.length)] } let style = pickStyleInRarity(rarity, used) // 同档位被抽空 → 降级到任意可用档位 if (!style) { style = pickStyleInRarity('N', used) if (!style) { // 极端兜底:任意未选 style style = LASER_STYLE_POOL.find((s) => !used.has(s.id)) || null } } if (!style) break picked.push(style) used.add(style.id) } return picked } /** * 便捷函数:只返回 ID 列表 * @param {number} [count=5] * @returns {string[]} */ export function drawGachaStyleIds(count = 5) { return drawGachaStyles(count).map((s) => s.id) } /** * 工具:按 preset_id 反查 style(供调试 / 日志使用) * @param {string} id * @returns {import('./stylePool.js').LaserStyle | undefined} */ export function findStyleById(id) { return LASER_STYLE_POOL.find((s) => s.id === id) }