docs: add HANDBOOK.md with product overview and technical guide
This commit is contained in:
parent
ecc4da46ec
commit
d543b173b4
1 changed files with 294 additions and 0 deletions
294
docs/HANDBOOK.md
Normal file
294
docs/HANDBOOK.md
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
# 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 3-up 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**
|
||||
|
||||
Copy `server/.env.example` to `server/.env` (or ensure `server/.env` exists with DB credentials pointing to port 3307).
|
||||
|
||||
**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 (useful for testing on a phone via ngrok).
|
||||
|
||||
**Dev seed accounts:**
|
||||
- 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
|
||||
|
||||
### 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
|
||||
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.
|
||||
Loading…
Add table
Reference in a new issue