- 删除已弃用的 compositor_client.go - 删除激光合成微服务代码 - 添加 gateway 合成控制器和测试文件 - 添加 Dify prompt 补丁脚本 Co-Authored-By: Claude <noreply@anthropic.com>
136 lines
3.8 KiB
JavaScript
136 lines
3.8 KiB
JavaScript
/**
|
|
* 镭射卡加权抽卡 — 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<Rarity, number>} */ ({
|
|
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<string>} 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<import('./stylePool.js').LaserStyle>}
|
|
*/
|
|
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<import('./stylePool.js').LaserStyle>} */ ([])
|
|
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)
|
|
}
|