Introduces `plainDetail` to the dice engine to provide a non-HTML version of the roll results. This allows components to display the roll breakdown without needing to parse or render HTML.
145 lines
3.9 KiB
TypeScript
145 lines
3.9 KiB
TypeScript
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(`<strong>${sign}${roll.value}</strong>`);
|
|
textSpans.push(`${sign}${roll.value}`);
|
|
} else if (isKept) {
|
|
htmlSpans.push(`<strong>[${roll.value}]</strong>`);
|
|
textSpans.push(`[${roll.value}]`);
|
|
} else {
|
|
htmlSpans.push(`<span class="text-gray-400">[${roll.value}]</span>`);
|
|
textSpans.push(`[${roll.value}]`);
|
|
}
|
|
}
|
|
}
|
|
|
|
const rollHtml = htmlSpans.join(" + ");
|
|
const rollText = textSpans.join(" + ");
|
|
const detail = `<strong>${total}</strong> = ${rollHtml}`;
|
|
const plainDetail = `${total} = ${rollText}`;
|
|
|
|
return {
|
|
pools: poolResults,
|
|
total,
|
|
detail,
|
|
plainDetail,
|
|
};
|
|
}
|