- Implement character creation functions to handle adventurer setup. - Add validation for adventurer name and selection of starting items. - Introduce new items in sample content pack: Flint and Steel, Pouch, Wax Sealing Kit, and Backpack. - Create tests for character creation functionality to ensure valid options and error handling. ✨Feature: implement dice rolling mechanics and tests - Add functions for rolling various dice types (d3, d6, 2d6, d66) with validation. - Implement modifier application with clamping for roll results. - Create tests to verify correct behavior of dice rolling functions. ✨Feature: add table lookup functionality and tests - Implement table lookup and resolution logic for defined tables. - Support for modified ranges and fallback to nearest entries. - Create tests to ensure correct table entry resolution based on roll results.
114 lines
2.6 KiB
TypeScript
114 lines
2.6 KiB
TypeScript
import type { DiceKind, RollResult } from "@/types/rules";
|
|
|
|
export type DiceRoller = (sides: number) => number;
|
|
|
|
const defaultDiceRoller: DiceRoller = (sides) =>
|
|
Math.floor(Math.random() * sides) + 1;
|
|
|
|
function assertRollInRange(value: number, sides: number) {
|
|
if (!Number.isInteger(value) || value < 1 || value > sides) {
|
|
throw new Error(`Dice roller returned invalid value ${value} for d${sides}.`);
|
|
}
|
|
}
|
|
|
|
function clampTotal(total: number, min: number, max: number) {
|
|
return Math.max(min, Math.min(max, total));
|
|
}
|
|
|
|
export function rollD3(roller: DiceRoller = defaultDiceRoller): RollResult {
|
|
const value = roller(6);
|
|
assertRollInRange(value, 6);
|
|
const total = Math.ceil(value / 2);
|
|
|
|
return {
|
|
diceKind: "d3",
|
|
rolls: [value],
|
|
total,
|
|
modifiedTotal: total,
|
|
};
|
|
}
|
|
|
|
export function rollD6(roller: DiceRoller = defaultDiceRoller): RollResult {
|
|
const value = roller(6);
|
|
assertRollInRange(value, 6);
|
|
|
|
return {
|
|
diceKind: "d6",
|
|
rolls: [value],
|
|
total: value,
|
|
modifiedTotal: value,
|
|
};
|
|
}
|
|
|
|
export function roll2D6(roller: DiceRoller = defaultDiceRoller): RollResult {
|
|
const first = roller(6);
|
|
const second = roller(6);
|
|
assertRollInRange(first, 6);
|
|
assertRollInRange(second, 6);
|
|
const total = first + second;
|
|
|
|
return {
|
|
diceKind: "2d6",
|
|
rolls: [first, second],
|
|
primary: first,
|
|
secondary: second,
|
|
total,
|
|
modifiedTotal: total,
|
|
};
|
|
}
|
|
|
|
export function rollD66(roller: DiceRoller = defaultDiceRoller): RollResult {
|
|
const primary = roller(6);
|
|
const secondary = roller(6);
|
|
assertRollInRange(primary, 6);
|
|
assertRollInRange(secondary, 6);
|
|
const total = primary * 10 + secondary;
|
|
|
|
return {
|
|
diceKind: "d66",
|
|
rolls: [primary, secondary],
|
|
primary,
|
|
secondary,
|
|
total,
|
|
modifiedTotal: total,
|
|
};
|
|
}
|
|
|
|
export function applyRollModifier(
|
|
roll: RollResult,
|
|
modifier = 0,
|
|
limits?: { min: number; max: number },
|
|
): RollResult {
|
|
if (roll.total === undefined) {
|
|
throw new Error("Cannot apply a modifier to a roll without a total.");
|
|
}
|
|
|
|
const nextTotal = roll.total + modifier;
|
|
const boundedTotal = limits
|
|
? clampTotal(nextTotal, limits.min, limits.max)
|
|
: nextTotal;
|
|
|
|
return {
|
|
...roll,
|
|
modifier,
|
|
modifiedTotal: boundedTotal,
|
|
clamped: limits ? boundedTotal !== nextTotal : false,
|
|
};
|
|
}
|
|
|
|
export function rollDice(
|
|
diceKind: DiceKind,
|
|
roller: DiceRoller = defaultDiceRoller,
|
|
): RollResult {
|
|
switch (diceKind) {
|
|
case "d3":
|
|
return rollD3(roller);
|
|
case "d6":
|
|
return rollD6(roller);
|
|
case "2d6":
|
|
return roll2D6(roller);
|
|
case "d66":
|
|
return rollD66(roller);
|
|
}
|
|
}
|