Add dice rolling design spec
This commit is contained in:
parent
2c73dd9ec4
commit
46f5227fa9
1 changed files with 174 additions and 0 deletions
174
docs/specs/2026-04-09-dice-rolling-design.md
Normal file
174
docs/specs/2026-04-09-dice-rolling-design.md
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
# 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 purposes
|
||||||
|
- `advantage`/`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
|
||||||
|
- `rolls` array contains both results: `[14, 8]`
|
||||||
|
- `total` uses 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)
|
||||||
|
|
||||||
|
1. **Header:** "Roll Log" title + collapse button
|
||||||
|
2. **General roller input:** text input accepting dice expressions. Placeholder: "Roll dice... (e.g. 2d6+1)". Enter to roll. Rolls are anonymous (characterName = "Roll").
|
||||||
|
3. **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`
|
||||||
|
- **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_log` table
|
||||||
|
- 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 modifier` for 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
|
||||||
|
|
||||||
|
1. Player clicks a roll button or types in the general roller
|
||||||
|
2. Client emits `roll:request` via Socket.IO with campaign ID, character info (if from sheet), dice expression, and advantage/disadvantage flags
|
||||||
|
3. Server parses the expression, generates random results, computes total
|
||||||
|
4. Server saves to `roll_log` table
|
||||||
|
5. Server broadcasts `roll:result` to the campaign room
|
||||||
|
6. All clients in the room receive the result and prepend it to their roll log
|
||||||
|
7. 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 + roller
|
||||||
|
- `server/src/routes/rolls.ts` — GET endpoint for roll history
|
||||||
|
- `server/src/db.ts` — add roll_log table
|
||||||
|
- `server/src/socket.ts` — handle roll:request, generate result, broadcast
|
||||||
|
- `server/src/index.ts` — register rolls route
|
||||||
|
|
||||||
|
**Client:**
|
||||||
|
|
||||||
|
- `client/src/components/RollLog.tsx` — the side panel component
|
||||||
|
- `client/src/components/RollLog.module.css` — panel styling
|
||||||
|
- `client/src/components/RollEntry.tsx` — individual roll result card
|
||||||
|
- `client/src/components/RollEntry.module.css` — card styling + animation
|
||||||
|
- `client/src/components/DiceButton.tsx` — small reusable dice roll button
|
||||||
|
- `client/src/components/DiceButton.module.css` — button styling
|
||||||
|
- `client/src/components/StatsPanel.tsx` — add DiceButton to each stat row
|
||||||
|
- `client/src/components/AttackBlock.tsx` — add DiceButton to each attack line
|
||||||
|
- `client/src/pages/CampaignView.tsx` — add RollLog panel, handle roll socket events
|
||||||
|
- `client/src/pages/CampaignView.module.css` — adjust layout for side panel
|
||||||
|
- `client/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
|
||||||
Loading…
Add table
Reference in a new issue