- 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.
111 lines
3.0 KiB
TypeScript
111 lines
3.0 KiB
TypeScript
import type { TableDefinition, TableEntry } from "@/types/content";
|
|
import type { RollResult } from "@/types/rules";
|
|
|
|
import { applyRollModifier, rollDice, type DiceRoller } from "./dice";
|
|
|
|
export type TableLookupResult = {
|
|
tableId: string;
|
|
entryKey: string;
|
|
roll: RollResult;
|
|
entry: TableEntry;
|
|
};
|
|
|
|
function getNumericEntryValue(entry: TableEntry) {
|
|
if (entry.d66 !== undefined) {
|
|
return entry.d66;
|
|
}
|
|
|
|
if (entry.exact !== undefined) {
|
|
return entry.exact;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function getEntryBounds(entries: TableEntry[]) {
|
|
const values = entries
|
|
.flatMap((entry) => {
|
|
if (entry.min !== undefined && entry.max !== undefined) {
|
|
return [entry.min, entry.max];
|
|
}
|
|
|
|
const exactValue = getNumericEntryValue(entry);
|
|
return exactValue !== undefined ? [exactValue] : [];
|
|
})
|
|
.sort((a, b) => a - b);
|
|
|
|
if (values.length === 0) {
|
|
throw new Error("Cannot compute table bounds for an empty numeric table.");
|
|
}
|
|
|
|
return { min: values[0], max: values[values.length - 1] };
|
|
}
|
|
|
|
function matchesEntry(entry: TableEntry, lookupValue: number) {
|
|
if (entry.min !== undefined && entry.max !== undefined) {
|
|
return lookupValue >= entry.min && lookupValue <= entry.max;
|
|
}
|
|
|
|
const exactValue = getNumericEntryValue(entry);
|
|
return exactValue === lookupValue;
|
|
}
|
|
|
|
function findNearestEntry(entries: TableEntry[], lookupValue: number) {
|
|
return entries
|
|
.map((entry) => ({ entry, value: getNumericEntryValue(entry) }))
|
|
.filter((candidate): candidate is { entry: TableEntry; value: number } => {
|
|
return candidate.value !== undefined;
|
|
})
|
|
.sort((left, right) => {
|
|
const distanceDelta =
|
|
Math.abs(left.value - lookupValue) - Math.abs(right.value - lookupValue);
|
|
|
|
return distanceDelta !== 0 ? distanceDelta : left.value - right.value;
|
|
})[0]?.entry;
|
|
}
|
|
|
|
export function resolveTableEntry(
|
|
table: TableDefinition,
|
|
roll: RollResult,
|
|
): TableLookupResult {
|
|
const lookupValue = roll.modifiedTotal ?? roll.total;
|
|
|
|
if (lookupValue === undefined) {
|
|
throw new Error(`Cannot resolve table ${table.code} without a roll total.`);
|
|
}
|
|
|
|
const matchingEntry = table.entries.find((entry) => matchesEntry(entry, lookupValue));
|
|
const fallbackEntry = matchingEntry ?? findNearestEntry(table.entries, lookupValue);
|
|
|
|
if (!fallbackEntry) {
|
|
throw new Error(`No table entry found for ${table.code} with value ${lookupValue}.`);
|
|
}
|
|
|
|
return {
|
|
tableId: table.id,
|
|
entryKey: fallbackEntry.key,
|
|
roll,
|
|
entry: fallbackEntry,
|
|
};
|
|
}
|
|
|
|
export function lookupTable(
|
|
table: TableDefinition,
|
|
options?: {
|
|
modifier?: number;
|
|
roller?: DiceRoller;
|
|
},
|
|
): TableLookupResult {
|
|
const rawRoll = rollDice(table.diceKind, options?.roller);
|
|
const roll =
|
|
options?.modifier !== undefined
|
|
? applyRollModifier(
|
|
rawRoll,
|
|
options.modifier,
|
|
table.usesModifiedRangesRule ? getEntryBounds(table.entries) : undefined,
|
|
)
|
|
: rawRoll;
|
|
|
|
return resolveTableEntry(table, roll);
|
|
}
|