7.4 KiB
Dice Rolling — Design Spec
Overview
Add a complete dice rolling system: server-side roll engine, shared real-time roll log panel, character sheet roll buttons for ability checks and attacks, advantage/disadvantage support, and a general-purpose dice roller.
1. Roll Engine (Server-Side)
Socket.IO Events
Client → Server:
roll:request—{ campaignId, characterId?, characterName?, type, dice, label, modifier?, advantage?, disadvantage? }type:'attack'|'ability-check'|'custom'dice: string expression like'1d20','2d6','1d8+1'label: human-readable description (e.g. "SHORTSWORD attack", "DEX check", "2d6")modifier: optional numeric modifier already included in the expression for display purposesadvantage/disadvantage: boolean, only meaningful for d20 rolls
Server → Campaign Room:
roll:result—{ id, campaignId, characterId, characterName, type, label, diceExpression, rolls, modifier, total, advantage, disadvantage, created_at }rolls: array of individual die results (e.g.[14]or[14, 8]for advantage)total: final computed result
Dice Expression Parser
Server-side parser that handles: NdX, NdX+M, NdX-M
N= number of dice (default 1 if omitted, e.g. "d20" = "1d20")X= die size (4, 6, 8, 10, 12, 20, 100)M= optional flat modifier
Examples: 1d20, 2d6, 1d8+1, d20-1, 4d6, 1d100
Invalid expressions return an error result with { error: "Couldn't parse: xyz" }.
Advantage / Disadvantage
For any roll where advantage or disadvantage is true:
- Roll the primary die (d20) twice instead of once
rollsarray contains both results:[14, 8]totaluses the higher (advantage) or lower (disadvantage) of the two, plus modifier- Display shows both dice with the chosen one highlighted
roll_log Table
| Column | Type | Notes |
|---|---|---|
| id | INTEGER | Primary key, autoincrement |
| campaign_id | INTEGER | FK to campaigns |
| character_id | INTEGER | Nullable (null for anonymous rolls) |
| character_name | TEXT | Display name (or "Roll") |
| type | TEXT | attack / ability-check / custom |
| label | TEXT | Human-readable description |
| dice_expression | TEXT | Original expression (e.g. "1d20") |
| rolls | TEXT | JSON array of die results |
| modifier | INTEGER | Flat modifier applied |
| total | INTEGER | Final result |
| advantage | INTEGER | 0 or 1 |
| disadvantage | INTEGER | 0 or 1 |
| created_at | TEXT | ISO timestamp |
2. Roll Log Panel
Layout
Collapsible side panel on the right of the campaign view (outside the character modal).
- Width: ~300px on desktop
- Mobile: full-width bottom drawer
- Collapsed state: thin strip (~40px) with a dice icon and the last roll result visible. Click to expand.
- Expanded state: full panel with header, input, and scrollable log
Panel Contents (top to bottom)
- Header: "Roll Log" title + collapse button
- General roller input: text input accepting dice expressions. Placeholder: "Roll dice... (e.g. 2d6+1)". Enter to roll. Rolls are anonymous (characterName = "Roll").
- Roll entries: scrollable list, newest at top. Last ~50 loaded from DB on campaign entry.
Roll Entry Card
Each roll displays as a small styled card:
- Top line: character name (bold, colored) + relative timestamp ("just now", "2m ago")
- Label: what was rolled ("SHORTSWORD attack", "DEX check", "2d6+1")
- Dice breakdown:
- Normal:
d20: [14] - With advantage:
d20: [14, 8] → 14(higher highlighted) - With disadvantage:
d20: [14, 8] → 8(lower highlighted) - Multiple dice:
2d6: [3, 5] = 8
- Normal:
- Modifier line (if any):
+ STR (+0) - Total: large, bold, highlighted number
- Pop-in animation: new cards slide in and briefly highlight when they arrive
Persistence
- Rolls saved to
roll_logtable - On entering a campaign, load last 50 rolls via
GET /api/campaigns/:id/rolls - New rolls arrive in real-time via Socket.IO
3. Roll Buttons on Character Sheet
Roll buttons appear in view mode only (not edit mode).
Ability Score Rolls
Each stat row in the StatsPanel gets a small dice button (🎲 or a styled button) on the right side (in the reserved rollSpace area).
- Click: rolls
1d20 + stat modifier - Label: "STR check", "DEX check", etc.
- Shift+click: advantage (rolls 2d20, takes higher)
- Ctrl/Cmd+click: disadvantage (rolls 2d20, takes lower)
Attack Rolls
Each attack line in AttackBlock gets a dice button on the right side.
- Click: rolls
1d20 + attack modifierfor the hit, then rolls the weapon's damage die. Two entries appear in the log: attack roll, then damage roll. - Label: "SHORTSWORD attack" for the d20, "SHORTSWORD damage" for the damage die
- Shift+click: advantage on the attack roll (damage is always normal)
- Ctrl/Cmd+click: disadvantage on the attack roll
Keyboard Modifier Hint
A small hint text at the bottom of the StatsPanel or in the roll log panel header: "Shift: advantage · Ctrl: disadvantage"
4. Data Flow
- Player clicks a roll button or types in the general roller
- Client emits
roll:requestvia Socket.IO with campaign ID, character info (if from sheet), dice expression, and advantage/disadvantage flags - Server parses the expression, generates random results, computes total
- Server saves to
roll_logtable - Server broadcasts
roll:resultto the campaign room - All clients in the room receive the result and prepend it to their roll log
- The new entry animates in with a pop-in effect
5. API Endpoints
GET /api/campaigns/:id/rolls— returns last 50 rolls for a campaign, newest first- Socket.IO handles all roll creation (no REST POST needed)
6. New/Modified Files
Server:
server/src/dice.ts— dice expression parser + rollerserver/src/routes/rolls.ts— GET endpoint for roll historyserver/src/db.ts— add roll_log tableserver/src/socket.ts— handle roll:request, generate result, broadcastserver/src/index.ts— register rolls route
Client:
client/src/components/RollLog.tsx— the side panel componentclient/src/components/RollLog.module.css— panel stylingclient/src/components/RollEntry.tsx— individual roll result cardclient/src/components/RollEntry.module.css— card styling + animationclient/src/components/DiceButton.tsx— small reusable dice roll buttonclient/src/components/DiceButton.module.css— button stylingclient/src/components/StatsPanel.tsx— add DiceButton to each stat rowclient/src/components/AttackBlock.tsx— add DiceButton to each attack lineclient/src/pages/CampaignView.tsx— add RollLog panel, handle roll socket eventsclient/src/pages/CampaignView.module.css— adjust layout for side panelclient/src/types.ts— add RollResult type
Out of Scope
- Animated 3D dice (future enhancement)
- "Roll as" character/NPC selector for general roller (future)
- "Drop lowest" or complex dice expressions
- Talent effects modifying rolls automatically
- Sound effects