Phase 4: interactive dice & sheet rolling
- Dice notation: exploding (Nd6!) and reroll-below (Nd6r2), capped + tested - Degrees of success / roll-vs-DC (PF2e ±10 + nat 20/1 steps; 5e crit on nat 20/1) - Global roll tray (shared store + RollTray in layout) with animated result + degree - Roll from the character sheet: skills, saves, and attack to-hit/damage are clickable and post to the tray + history (rollAndShow/rollCheck helpers) - Saved roll macros per campaign (persisted) on the Dice page 10 new unit tests (notation explode/reroll, degrees), interactive-dice e2e. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import { useState } from 'react';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import type { Character } from '@/lib/schemas';
|
||||
import type { AbilityKey, CharacterRulesInput, ProficiencyRank } from '@/lib/rules';
|
||||
import type { AbilityKey, CharacterRulesInput, ProficiencyRank, SystemId } from '@/lib/rules';
|
||||
import { getSystem, ABILITY_ABBR } from '@/lib/rules';
|
||||
import { charactersRepo } from '@/lib/db/repositories';
|
||||
import { PF2E_SAVES } from '@/lib/rules/pf2e/skills';
|
||||
import { useDebouncedCallback } from '@/lib/useDebouncedCallback';
|
||||
import { rollCheck } from '@/lib/useRoll';
|
||||
import { Page } from '@/components/ui/Page';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input, Select } from '@/components/ui/Input';
|
||||
@@ -151,6 +152,7 @@ export function CharacterSheet({ character }: { character: Character }) {
|
||||
rank={(c.saveRanks[s.ability] as ProficiencyRank) ?? 'untrained'}
|
||||
ranks={ranks}
|
||||
onRank={(rank) => update({ saveRanks: { ...c.saveRanks, [s.ability]: rank } })}
|
||||
system={c.system}
|
||||
/>
|
||||
))
|
||||
: ABILITIES.map((a) => (
|
||||
@@ -161,6 +163,7 @@ export function CharacterSheet({ character }: { character: Character }) {
|
||||
rank={(c.saveRanks[a] as ProficiencyRank) ?? 'untrained'}
|
||||
ranks={ranks}
|
||||
onRank={(rank) => update({ saveRanks: { ...c.saveRanks, [a]: rank } })}
|
||||
system={c.system}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -178,6 +181,7 @@ export function CharacterSheet({ character }: { character: Character }) {
|
||||
rank={(c.skillRanks[s.key] as ProficiencyRank) ?? 'untrained'}
|
||||
ranks={ranks}
|
||||
onRank={(rank) => update({ skillRanks: { ...c.skillRanks, [s.key]: rank } })}
|
||||
system={c.system}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -256,16 +260,24 @@ function RankRow({
|
||||
rank,
|
||||
ranks,
|
||||
onRank,
|
||||
system,
|
||||
}: {
|
||||
label: string;
|
||||
modifier: number;
|
||||
rank: ProficiencyRank;
|
||||
ranks: ProficiencyRank[];
|
||||
onRank: (r: ProficiencyRank) => void;
|
||||
system: SystemId;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 rounded-md border border-line bg-panel px-3 py-1.5">
|
||||
<span className="w-8 font-display text-lg font-semibold text-accent">{formatModifier(modifier)}</span>
|
||||
<button
|
||||
onClick={() => rollCheck(modifier, label, { system })}
|
||||
className="w-9 rounded font-display text-lg font-semibold text-accent hover:bg-accent/10"
|
||||
title={`Roll ${label}`}
|
||||
>
|
||||
{formatModifier(modifier)}
|
||||
</button>
|
||||
<span className="flex-1 truncate text-sm text-ink">{label}</span>
|
||||
<Select className="w-auto py-1 text-xs" value={rank} onChange={(e) => onRank(e.target.value as ProficiencyRank)}>
|
||||
{ranks.map((r) => (
|
||||
|
||||
Reference in New Issue
Block a user