darkwatch/docs/specs/2026-04-09-dice-rolling-design.md
2026-04-09 01:13:49 -04:00

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 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