feat: dice macros — persisted named rolls (Grimjaw attack, fire damage…)
CI / frontend (push) Successful in 31s
CI / rust (push) Successful in 5m41s

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.
This commit is contained in:
itsamejms
2026-09-07 10:28:04 +01:00
parent 00178e603d
commit 630a025e77
+65
View File
@@ -24,12 +24,22 @@ function rollDie(sides: number): number {
// with a runnable self-check (scripts/check-dice.ts). The Mode + DieResult // with a runnable self-check (scripts/check-dice.ts). The Mode + DieResult
// types are imported from there too. // types are imported from there too.
interface Macro {
label: string;
notation: string;
}
export function DiceRoller() { export function DiceRoller() {
const [input, setInput] = usePersistentState<string>("dice.input", "1d20"); const [input, setInput] = usePersistentState<string>("dice.input", "1d20");
const [results, setResults] = usePersistentState<DieResult[]>("dice.history", []); const [results, setResults] = usePersistentState<DieResult[]>("dice.history", []);
const [lastTotal, setLastTotal] = useState<number | null>(null); const [lastTotal, setLastTotal] = useState<number | null>(null);
const [lastBreakdown, setLastBreakdown] = useState<DieResult | null>(null); const [lastBreakdown, setLastBreakdown] = useState<DieResult | null>(null);
const [mode, setMode] = usePersistentState<Mode>("dice.mode", "normal"); const [mode, setMode] = usePersistentState<Mode>("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<Macro[]>("dice.macros", []);
const [macroLabel, setMacroLabel] = useState("");
const { addToast } = useToast(); const { addToast } = useToast();
const roll = useCallback( 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 ( return (
<div className="flex flex-col gap-3 h-full"> <div className="flex flex-col gap-3 h-full">
{/* Big result display */} {/* Big result display */}
@@ -209,6 +231,49 @@ export function DiceRoller() {
})} })}
</div> </div>
{/* Macros — the DM's named rolls. Same relabel path as templates. */}
{macros.length > 0 && (
<div className="flex flex-wrap gap-1.5 items-center">
<span className="text-[var(--color-text-dim)] text-[10px] uppercase tracking-wider">Macros</span>
{macros.map((m, i) => (
<span key={i} className="flex items-center rounded-lg bg-[var(--color-gold-glow)] border border-[var(--color-border-glass)] overflow-hidden">
<button
onClick={() => applyTemplate(m)}
className="px-2.5 py-1 text-xs text-[var(--color-gold-bright)] hover:bg-[var(--color-bg-card)] transition-colors cursor-pointer"
title={`${m.label} — ${m.notation}`}
>
{m.label}
</button>
<button
onClick={() => setMacros((prev) => prev.filter((_, j) => j !== i))}
className="px-1.5 py-1 text-[10px] text-[var(--color-text-dim)] hover:text-[var(--color-danger)] cursor-pointer transition-colors"
title="Delete macro"
aria-label={`Delete macro ${m.label}`}
>
×
</button>
</span>
))}
</div>
)}
<div className="flex gap-1.5">
<input
className="flex-1 rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-subtle)] px-2.5 py-1 text-xs text-[var(--color-text-primary)] placeholder-[var(--color-text-dim)] focus:outline-none focus:border-[var(--color-gold-bright)]"
value={macroLabel}
onChange={(e) => 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"
/>
<button
onClick={addMacro}
className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] px-2.5 py-1 text-xs text-[var(--color-text-secondary)] hover:border-[var(--color-gold-bright)] hover:text-[var(--color-gold-bright)] transition-colors cursor-pointer"
title={`Save ${input} as a named roll`}
>
+ macro
</button>
</div>
{/* History */} {/* History */}
<div className="flex-1 overflow-y-auto"> <div className="flex-1 overflow-y-auto">
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">