# Darkwatch Handbook

**Last updated:** 2026-04-11

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** — a dark gradient overlay with adjustable intensity
- **Fire** — Three.js particle simulation, full screen
- **Rain / Embers** — tsParticles overlay

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.

### 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
- **Death timer** — when a character is dying: 1d4 + CON rounds to live, d20 each turn to rise at 1 HP
- **Conditions / status effects** — poisoned, stunned, dying, etc. shown on character cards
- **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

Longer-term ideas: multi-system support (Cairn, Knave, Cyberpunk RED), metrics and analytics, API documentation, and a public marketing site.

## 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.)
├── 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
│   └── superpowers/
│       ├── 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/superpowers/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/superpowers/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.
