From 630a025e774bf7f5b5532f45327254de681f155d Mon Sep 17 00:00:00 2001 From: itsamejms Date: Mon, 7 Sep 2026 10:28:04 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20dice=20macros=20=E2=80=94=20persisted?= =?UTF-8?q?=20named=20rolls=20(Grimjaw=20attack,=20fire=20damage=E2=80=A6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Type a notation in the input, name it, '+ macro' saves it. Chips roll with the template relabel path so history reads 'Attack (1d20+5) → 17'; × deletes. Roadmap 1.7. --- src/components/DiceRoller.tsx | 65 +++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/src/components/DiceRoller.tsx b/src/components/DiceRoller.tsx index 50ba809..d7a5353 100644 --- a/src/components/DiceRoller.tsx +++ b/src/components/DiceRoller.tsx @@ -24,12 +24,22 @@ function rollDie(sides: number): number { // with a runnable self-check (scripts/check-dice.ts). The Mode + DieResult // types are imported from there too. +interface Macro { + label: string; + notation: string; +} + export function DiceRoller() { const [input, setInput] = usePersistentState("dice.input", "1d20"); const [results, setResults] = usePersistentState("dice.history", []); const [lastTotal, setLastTotal] = useState(null); const [lastBreakdown, setLastBreakdown] = useState(null); const [mode, setMode] = usePersistentState("dice.mode", "normal"); + // ponytail: macros = the DM's saved attack/damage rolls. Persisted; the + // notation is snapshot at save time ("Grimjaw attack" → "1d20+5"), not a + // live reference to the input box. + const [macros, setMacros] = usePersistentState("dice.macros", []); + const [macroLabel, setMacroLabel] = useState(""); const { addToast } = useToast(); const roll = useCallback( @@ -120,6 +130,18 @@ export function DiceRoller() { }); } + function addMacro() { + const label = macroLabel.trim(); + const notation = input.trim(); + if (!label || !parseNotation(notation)) { + addToast("Type a name and a valid notation first", "info"); + return; + } + setMacros((prev) => [...prev, { label, notation }]); + setMacroLabel(""); + addToast(`Macro "${label}" saved (${notation})`, "success"); + } + return (
{/* Big result display */} @@ -209,6 +231,49 @@ export function DiceRoller() { })}
+ {/* Macros — the DM's named rolls. Same relabel path as templates. */} + {macros.length > 0 && ( +
+ Macros + {macros.map((m, i) => ( + + + + + ))} +
+ )} +
+ setMacroLabel(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && addMacro()} + placeholder={`save "${input}" as macro… (type a name)`} + aria-label="Name for a new dice macro" + /> + +
+ {/* History */}