/** * Rolls a die of a configurable number of sides and holds the most recent * result, exposing both that value and a roll stream a host can fire itself. * * Embed several as `` * and each writes its own roll back to the host's cell, so the host can total * them or read them independently. * * @hashtags dice, random, roll, d20, generator * @keywords roll dice, random number, die, d6, d20, randomize, chance, * generate a number, pick a number, random integer, shuffle */ import { action, computed, Default, NAME, pattern, Stream, UI, type VNode, Writable, } from "commonfabric"; export interface DiceRollerInput { /** The most recent roll. A host passes its own cell to read it back. */ value?: Writable>; /** How many faces the die has; a roll lands between 1 and this. */ sides?: Writable>; /** Caption shown under the rolled number. */ label?: Writable>; } export interface DiceRollerOutput { [NAME]: string; [UI]: VNode; value: number; sides: number; roll: Stream<{ sides?: number }>; } /** `sides` reduced to a usable face count, whatever the caller supplied. */ const faceCount = (sides: number | undefined): number => { const floored = Math.floor(Number(sides)); return Number.isFinite(floored) && floored > 0 ? floored : 6; }; export const DiceRoller = pattern( ({ value, sides, label }) => { const roll = action((event: { sides?: number }) => { const faces = faceCount(event?.sides ?? sides.get()); value.set(Math.floor(Math.random() * faces) + 1); }); const caption = computed(() => `${label.get()} (d${faceCount(sides.get())})` ); return { [NAME]: computed(() => `${label.get()}: ${value.get() ?? 1}`), [UI]: (
{value}
{caption} Roll
), value, sides, roll, }; }, ); export default DiceRoller;