darkwatch/docs/HANDBOOK.md
2026-04-12 01:46:46 -04:00

333 lines
16 KiB
Markdown

# Darkwatch Handbook
**Last updated:** 2026-04-12
A reference for everyone working on or with Darkwatch — whether you're a DM discovering the tool, a developer joining the project, or a collaborator adding features.
## Table of Contents
- [Part 1: Product Overview](#part-1-product-overview)
- [What is Darkwatch?](#what-is-darkwatch)
- [Who is it for?](#who-is-it-for)
- [How a session works](#how-a-session-works)
- [Features](#features)
- [What's coming next](#whats-coming-next)
- [Known limitations](#known-limitations)
- [Part 2: Technical Guide](#part-2-technical-guide)
- [Stack](#stack)
- [Running locally](#running-locally)
- [Repo structure](#repo-structure)
- [Key patterns](#key-patterns)
- [Development workflow](#development-workflow)
---
# Part 1: Product Overview
## What is Darkwatch?
Darkwatch is a real-time digital session companion for [Shadowdark RPG](https://www.thearcanelibrary.com/pages/shadowdark). It runs in a web browser — the DM opens a campaign and players join via invite link. Everything stays in sync automatically: HP changes, dice rolls, spell slots, atmosphere effects, and combat all update live for everyone at the table.
It's not a virtual tabletop (no maps or tokens). It's the digital layer that sits alongside your physical table: character sheets, dice, a torch timer, initiative tracking, and the right amount of atmosphere.
## Who is it for?
- **Dungeon Masters** running Shadowdark campaigns who want a real-time view of the whole party without paper shuffling
- **Players** who want a clean digital character sheet that stays synced without everyone shouting HP updates across the table
- **Groups** playing in-person, online, or hybrid — it works in any browser
## How a session works
1. The DM creates a campaign and gets an invite link
2. Players click the link and join with their account (or create one)
3. The DM sees all characters in a compact card grid; players see their own full character sheet
4. Dice rolls, HP changes, spell casts, and atmosphere effects all sync in real time
5. When combat starts, the DM opens the initiative tracker — everyone rolls, the tracker resolves who goes first, and the DM advances turns
Sessions are stateful: pausing mid-combat and resuming later works because combat state is saved to the database.
## Features
### Characters
Each character has the full Shadowdark stat block: STR, DEX, CON, INT, WIS, CHA, HP, AC, XP, gold, gear slots, and currency. Stats are click-to-edit inline. AC and attacks are auto-calculated from stats and talents.
**Character creation wizard** walks through four steps: name/class/ancestry → roll 3d6 for each stat → background/alignment/deity → review. HP, starting gold, gear slots, and character title are all derived automatically at the end.
**Talents** can be added with name and effect (e.g. "+2 STR", "+1 gear slot"). They apply to the relevant derived values immediately.
**Avatars** use DiceBear with a style picker. Each character also has a color used for their dice and name display.
### Dice Rolling
Dice roll in 3D with animation. Results are predetermined on the server (not client-side) so they're trustworthy. Dice match the character's color. Results appear in a roll log that all players can see.
Supported: d4, d6, d8, d10, d12, d20, d100. Advantage and disadvantage are supported (roll two dice, take higher/lower). Long-press the roll button on mobile to trigger advantage or disadvantage.
### Spellcasting
34 Tier 1 and Tier 2 spells for Wizard and Priest classes. Casting rolls a d20 against a difficulty; failure causes exhaustion (one spell slot lost). The spell list shows current/max slots and highlights exhausted spells.
**Wizard mishaps:** On a critical fail, a d12 mishap table is rolled automatically. The effect is applied and can be undone if the DM made an error.
**Priest penance:** Failed casts trigger the Shadowdark penance mechanic. Rest recovers all spell slots.
### Torch Timer
A 60-minute countdown synced across all clients. The DM controls start/stop/reset. When the torch goes out, everyone knows.
### Luck Tokens
Each character has a luck token toggle. Players and the DM can both see and toggle luck token state.
### Atmosphere Effects
The DM can layer atmosphere effects that all players see in real time:
- **Fog** — drifting radial-gradient layers with adjustable intensity; fades in smoothly when enabled
- **Fire** — Three.js GLSL shader with a realistic colour ramp (deep red tips → orange → amber → warm yellow core), per-column height variation for a ragged silhouette, and ember glow at the base
- **Rain** — translucent blue-white drops that increase in density with intensity; at high intensity the angle tilts slightly, simulating wind-driven rain
- **Snow** — gentle white flakes with size variation for depth; drift naturally as they fall; density scales with intensity
- **Embers** — small slow-drifting sparks with flickering opacity; more numerous and finer than rain
Each effect has an independent intensity slider. Atmosphere is DM-only to control; all players see the active effects.
### Initiative Tracker
Shadowdark uses team-based initiative — the whole party acts together, then all enemies act together. Darkwatch implements this directly:
**Rolling phase:** Each player clicks "Roll Initiative" for their character. The server takes the highest individual roll as the party's result. The DM rolls once for all enemies. Either side can roll in any order.
**Active phase:** Whichever side rolled higher acts first (ties go to the party). The DM clicks "Next Turn" to flip sides. The round counter increments each time the turn passes back to the party.
**Enemy management (DM only):** Enemies are added at combat start with a name and max HP. HP is tracked inline and visible only to the DM — players see enemy names but no HP values. Enemies can be added or removed mid-combat.
Combat state persists to the database. If the server restarts mid-fight, the tracker reappears when players rejoin.
### Death Timer
Shadowdark's dying mechanic is fully implemented. When a character drops to 0 HP they enter a dying state:
- The server rolls 1d4 + CON modifier (minimum 1) to determine how many rounds the character has left
- The character card gains a pulsing red border and shows a 💀 countdown (e.g. 💀 3)
- Each time the party's turn begins in the initiative tracker, all dying timers tick down by 1
- The DM can click **Roll Recovery** in the initiative tracker — a d20 is rolled server-side; 18 or higher lets the character stand at 1 HP
- If the timer reaches 0 the character is marked permanently dead (card goes grey, HP is locked)
- The DM can click **Revive** on a dead character's card to bring them back at 1 HP
Healing a dying character above 0 HP at any point immediately clears the dying state with no recovery roll needed.
### DM View
The DM sees all characters in a compact three-column card grid showing: HP (current/max), AC, luck token state, torch timer, and all stat modifiers at a glance. The full character detail is one click away.
## What's coming next
Features currently planned or in progress:
- **Talent roll UI** — roll 2d6 on the class talent table when leveling up; currently talents are added manually
- **Conditions / status effects** — poisoned, stunned, etc. shown on character cards (dying is already tracked internally)
- **Per-enemy dice rolls** — DM triggers attack/damage rolls from individual enemies in the tracker
- **Party loot / shared inventory** — campaign-level shared item pool, DM-managed
- **Creature gallery / NPC system** — saved enemy stat blocks, quick-add to initiative tracker
- **Initiative enhancements** — turn timer per combatant, re-roll initiative mid-combat, individual initiative mode (each combatant rolls separately, as opposed to team-based; needed for non-Shadowdark systems)
- **Google / social login** — OAuth alongside the existing email/password flow
- **Ambient audio** — optional background sound; low priority
Longer-term ideas: multi-system support (Cairn, Knave, Cyberpunk RED), profile image uploads, metrics and analytics, and API documentation.
## Known limitations
- **Shadowdark only (for now)** — rules, stat blocks, and spell lists are Shadowdark-specific. Multi-system support is planned but not started.
- **No map or token support** — Darkwatch is a companion tool, not a full VTT.
- **Single combat displayed** — the initiative tracker shows one combat at a time. The backend supports multiple simultaneous combats (rare edge case) but the UI only shows the first.
- **No offline mode** — everything requires a server connection. There's no local-only or offline fallback.
- **No profile image uploads** — avatars use DiceBear. Custom image uploads are not yet supported.
---
# Part 2: Technical Guide
## Stack
| Layer | Technology |
|---|---|
| Client | React 18 + Vite + TypeScript + CSS Modules |
| Server | Express + Socket.IO + TypeScript (tsx runner) |
| Database | MariaDB 11 (Docker container `darkwatch-maria`, port 3307) |
| DB client | mysql2/promise |
| Real-time | Socket.IO (rooms per campaign: `campaign:{id}`) |
| Auth | JWT in httpOnly cookies |
| 3D dice | @react-three/fiber + cannon-es (Ammo.js physics) |
| Particles | tsParticles (rain/embers), Three.js (fire) |
| Avatars | DiceBear |
## Running locally
**Prerequisites:** Node 18+, Docker
**1. Start the database**
```bash
docker start darkwatch-maria
```
If the container doesn't exist yet, it was created with:
```bash
docker run -d --name darkwatch-maria \
-e MYSQL_DATABASE=darkwatch \
-e MYSQL_USER=darkwatch \
-e MYSQL_PASSWORD=darkwatch \
-e MYSQL_ROOT_PASSWORD=root \
-p 3307:3306 mariadb:11
```
**2. Set up environment**
Create `server/.env` with these variables:
```
DB_HOST=127.0.0.1
DB_PORT=3307
DB_NAME=darkwatch
DB_USER=darkwatch
DB_PASSWORD=darkwatch
JWT_SECRET=devsecret
CLIENT_URL=http://localhost:5173
PORT=3000
NODE_ENV=development
```
**3. Start the server**
```bash
cd server
npm install
npm run dev
```
The server runs migrations automatically on startup, then listens on `http://localhost:3000`.
**4. Start the client**
```bash
cd client
npm install
npm run dev -- --host
```
The client runs on `http://localhost:5173` (or the next available port). The `--host` flag exposes it on the local network for device testing.
**Dev seed accounts** (created automatically on first server startup):
- DM: `dm@darkwatch.test` / `password`
- Player: `player@darkwatch.test` / `password`
## Repo structure
```
shadowdark/
├── client/ # React + Vite frontend
│ ├── src/
│ │ ├── components/ # Reusable UI components
│ │ ├── pages/ # Route-level pages (CampaignView, Login, etc.)
│ │ ├── context/ # React context (AuthContext)
│ │ ├── lib/ # Utility libraries
│ │ ├── types.ts # Shared TypeScript types
│ │ └── socket.ts # Socket.IO singleton
│ └── public/ # Static assets (dice themes, etc.)
├── site/ # Static marketing brochure (no build step)
│ ├── index.html # Single-page brochure
│ ├── style.css # All brochure styles
│ ├── main.js # Smooth scroll + hero parallax
│ ├── screenshots.js # Playwright script — regenerates screenshots
│ └── assets/screenshots/ # Committed PNGs used by the brochure
├── server/
│ ├── src/
│ │ ├── routes/ # Express REST routes + socket handler files
│ │ ├── index.ts # Express app setup
│ │ ├── socket.ts # Socket.IO setup, connection handler
│ │ ├── db.ts # MariaDB pool
│ │ ├── dice.ts # Server-side dice rolling
│ │ ├── auth.ts # JWT helpers
│ │ └── migrate.ts # Migration runner
│ └── migrations/ # Numbered SQL migration files (001_, 002_, ...)
├── docs/
│ ├── ROADMAP.md # Feature backlog and ideas
│ ├── HANDBOOK.md # This file
│ ├── specs/ # Design specs (brainstorming output)
│ └── plans/ # Implementation plans
├── .claude/
│ └── skills/ # Project-local Claude skills
├── CHANGELOG.md # Version history
└── CLAUDE.md # Claude Code instructions for this project
```
## Key patterns
### Socket rooms
Every campaign has a Socket.IO room named `campaign:{id}`. Clients join on page load:
```typescript
socket.emit("join-campaign", String(campaignId));
```
Server handlers broadcast to the room:
```typescript
io.to(`campaign:${campaignId}`).emit("event-name", data);
```
The initiative tracker uses a role-aware pattern: `socket.to(room)` sends to all except the emitting socket; `socket.emit()` sends back to the emitter. This lets the DM receive full data while players receive HP-stripped data in one operation.
### CSS Modules and variables
All components use CSS Modules (`.module.css` files co-located with the component). Global CSS variables are defined in `client/src/theme.css`:
Key variables: `--gold`, `--gold-rgb`, `--bg-modal`, `--bg-input`, `--text-primary`, `--text-secondary`, `--text-tertiary`, `--btn-active-text`, `--danger`, `--danger-rgb`, `--font-display`.
Always use CSS variables for colors — never hardcode hex values in component CSS.
### Database migrations
Migrations live in `server/migrations/` as numbered SQL files: `001_initial_schema.sql`, `002_spells.sql`, `003_combat_state.sql`, etc. The migration runner (`server/src/migrate.ts`) tracks applied migrations in a `_migrations` table and runs new ones on server startup.
To add a migration: create the next numbered file. It runs automatically on the next server restart.
### Auth and roles
Auth uses JWT stored in httpOnly cookies. The token contains `{ userId }`. On socket connection, the auth middleware reads the cookie and populates `socket.data.user`.
Campaign roles (DM or player) are stored in the `campaign_members` table. Role enforcement happens:
- REST routes: middleware checks `campaign_members` before handling requests
- Socket handlers: `checkDM()` queries `campaign_members` for DM-only events
- Frontend: `role === "dm"` gates DM-only UI elements — role comes from `AuthContext` and is passed as a prop to campaign view components
### All imports use `.js` extensions
The project uses ES modules. Even when importing `.tsx` or `.ts` files, use `.js` in the import path:
```typescript
import InitiativeTracker from "../components/InitiativeTracker.js";
import type { CombatState } from "../types.js";
```
Vite and tsx resolve `.js` to the TypeScript source at build/dev time.
## Development workflow
We use a structured approach: **brainstorm → spec → plan → implement**.
1. **Brainstorm** (`/brainstorm` skill) — collaborative design session; produces a spec doc
> Skills are Claude Code slash commands defined in `.claude/skills/` — invoke them inside a Claude Code session.
2. **Spec** saved to `docs/specs/YYYY-MM-DD-feature-design.md`
3. **Plan** (`/writing-plans` skill) — detailed implementation plan with code in every step
4. **Plan** saved to `docs/plans/YYYY-MM-DD-feature.md`
5. **Implement** — subagent-driven development, one task at a time with spec + quality reviews
All specs and plans are committed to the repo. Check `docs/superpowers/` to understand why things were built the way they were.
**Project-local Claude skills** live in `.claude/skills/`. See `CLAUDE.md` for the list of available skills and how to invoke them.
**Technical backlog** (DB migration tooling, containerization, Kubernetes, metrics infrastructure) is tracked in `docs/ROADMAP.md`.