import type { DicePool, DicePoolResult, DieResult, RollResult } from "./types"; import { tokenize, parse } from "./parser"; /** * 掷骰子 */ export function rollDie(sides: number): number { if (sides === 0) return 0; // 固定数字 return Math.floor(Math.random() * sides) + 1; } /** * 掷骰池 */ export function rollDicePool(pool: DicePool): DicePoolResult { // 分离骰子和固定数字 const diceToRoll = pool.dice.filter((die) => die.sides > 0); const fixedNumbers = pool.dice.filter((die) => die.sides === 0); // 只掷真正的骰子 const rolls: DieResult[] = diceToRoll.map((die) => ({ sides: die.sides, value: rollDie(die.sides), isNegative: die.isNegative, })); // 添加固定数字 const allRolls: DieResult[] = [ ...rolls, ...fixedNumbers.map((die) => ({ sides: 0, value: die.value, isNegative: die.isNegative, })), ]; // 应用修饰符(只对有面数的骰子生效) const { keptRolls, droppedRolls } = applyModifier(rolls, pool.modifier); // 固定数字总是保留 const finalKeptRolls = [ ...keptRolls, ...allRolls.filter((r) => r.sides === 0), ]; // 计算小计 const subtotal = finalKeptRolls.reduce((sum, roll) => { return sum + (roll.isNegative ? -roll.value : roll.value); }, 0); return { pool, rolls: allRolls, keptRolls: finalKeptRolls, droppedRolls, subtotal, }; } /** * 应用修饰符 */ export function applyModifier( rolls: DieResult[], modifier: DicePool["modifier"], ): { keptRolls: DieResult[]; droppedRolls: DieResult[] } { if (!modifier) { return { keptRolls: rolls, droppedRolls: [] }; } // 按值排序 const sorted = [...rolls].sort((a, b) => a.value - b.value); switch (modifier.type) { case "kh": // Keep Highest - 保留最大的 N 个 case "dh": { // Drop Highest - 丢弃最大的 N 个 const count = modifier.count; const higher = sorted.slice(-count); const rest = sorted.slice(0, -count); if (modifier.type === "kh") { return { keptRolls: higher, droppedRolls: rest }; } else { return { keptRolls: rest, droppedRolls: higher }; } } case "kl": // Keep Lowest - 保留最小的 N 个 case "dl": { // Drop Lowest - 丢弃最小的 N 个 const count = modifier.count; const lower = sorted.slice(0, count); const rest = sorted.slice(count); if (modifier.type === "kl") { return { keptRolls: lower, droppedRolls: rest }; } else { return { keptRolls: rest, droppedRolls: lower }; } } } } /** * 执行完整的掷骰 */ export function roll(formula: string): RollResult { const tokens = tokenize(formula); const pools = parse(tokens); const poolResults = pools.map((pool) => rollDicePool(pool)); const total = poolResults.reduce((sum, result) => sum + result.subtotal, 0); // Generate detail expressions (both HTML and plain text) const htmlSpans: string[] = []; const textSpans: string[] = []; for (const result of poolResults) { for (const roll of result.rolls) { const isKept = result.keptRolls.some((kept) => kept === roll); if (roll.sides === 0) { const sign = roll.isNegative ? "-" : ""; htmlSpans.push(`${sign}${roll.value}`); textSpans.push(`${sign}${roll.value}`); } else if (isKept) { htmlSpans.push(`[${roll.value}]`); textSpans.push(`[${roll.value}]`); } else { htmlSpans.push(`[${roll.value}]`); textSpans.push(`[${roll.value}]`); } } } const rollHtml = htmlSpans.join(" + "); const rollText = textSpans.join(" + "); const detail = `${total} = ${rollHtml}`; const plainDetail = `${total} = ${rollText}`; return { pools: poolResults, total, detail, plainDetail, }; }