feat: dice roller improved

This commit is contained in:
2026-02-28 16:49:59 +08:00
parent 66aaaa96e1
commit cc10cea31d
12 changed files with 736 additions and 40 deletions
@@ -0,0 +1,136 @@
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 rolls: DieResult[] = pool.dice.map((die) => ({
sides: die.sides,
value: rollDie(die.sides),
isNegative: die.isNegative,
}));
// 应用修饰符
const { keptRolls, droppedRolls } = applyModifier(rolls, pool.modifier);
// 计算小计
const subtotal = keptRolls.reduce((sum, roll) => {
return sum + (roll.isNegative ? -roll.value : roll.value);
}, 0);
return {
pool,
rolls,
keptRolls,
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;
if (modifier.type === "kh") {
const kept = sorted.slice(-count);
const dropped = sorted.slice(0, -count);
return { keptRolls: kept, droppedRolls: dropped };
} else {
const kept = sorted.slice(0, -count);
const dropped = sorted.slice(-count);
return { keptRolls: kept, droppedRolls: dropped };
}
}
case "kl": // Keep Lowest - 保留最小的 N 个
case "dl": { // Drop Lowest - 丢弃最小的 N 个
const count = modifier.count;
if (modifier.type === "kl") {
const kept = sorted.slice(0, count);
const dropped = sorted.slice(count);
return { keptRolls: kept, droppedRolls: dropped };
} else {
const kept = sorted.slice(count);
const dropped = sorted.slice(0, count);
return { keptRolls: kept, droppedRolls: dropped };
}
}
}
}
/**
* 执行完整的掷骰
*/
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);
// 生成详细表达式 - 使用 HTML 格式
const parts: string[] = [];
for (const result of poolResults) {
const diceCount = result.pool.dice.filter((d) => d.sides > 0).length;
const fixedValue = result.pool.dice.find((d) => d.sides === 0)?.value || 0;
const modStr = result.pool.modifier
? `${result.pool.modifier.type}${result.pool.modifier.count}`
: "";
// 骰子部分
let dicePart: string;
if (diceCount > 0) {
const sides = result.pool.dice[0]?.sides || 0;
dicePart = `${diceCount}d${sides}${modStr}`;
} else {
dicePart = `${fixedValue}`;
}
// 构建骰子结果 HTML
const rollSpans = result.rolls.map((roll) => {
const isKept = result.keptRolls.some(
(kept) => kept === roll
);
if (isKept) {
return `<strong>[${roll.value}]</strong>`;
} else {
return `<span class="text-gray-400">[${roll.value}]</span>`;
}
});
const rollsStr = rollSpans.join(" ");
parts.push(`${dicePart} ${rollsStr} = <strong>${result.subtotal}</strong>`);
}
const detail = parts.join(" + ");
return {
pools: poolResults,
total,
detail,
};
}