diff --git a/docs/specs/2026-04-09-dice-rolling-design.md b/docs/specs/2026-04-09-dice-rolling-design.md new file mode 100644 index 0000000..7a76e25 --- /dev/null +++ b/docs/specs/2026-04-09-dice-rolling-design.md @@ -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