- The scene description shows during a battle only between rounds (while the first in the order is on turn), and always before and after it - Combatant details start closed in a battle: the spotlight has whoever is on turn, or on a narrow screen their own card opens - While a battle is under way, Add to the scene, Experience and Rewards fold to their titles - Button labels wrap inside their buttons instead of spilling out - Sticky turn order and side column follow the measured top bar height - Phone: quick stats no longer sits on Log out, incoming rolls clear the bottom bar, the damage bar no longer scrolls on its own - Docs updated; all packages at version 1.32.0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
271 lines
15 KiB
Markdown
271 lines
15 KiB
Markdown
# The Sunken Reach — D&D 5e Campaign Manager
|
||
|
||
A web app for a D&D 5e Dungeon Master and their players to run a campaign
|
||
together: campaign management, a guided dice-roll character builder, live
|
||
character sheets, a character panel on every page, weapons/inventory/spells,
|
||
monsters, and a real-time encounter/combat tracker.
|
||
|
||
## How it works
|
||
|
||
`/docs` describes the app as it currently behaves, one subject per file:
|
||
|
||
| Document | Covers |
|
||
|---|---|
|
||
| `docs/CHARACTERS.md` | Building a character, every number on the sheet, levelling up, rests, money, permissions |
|
||
| `docs/WEAPONS.md` | The weapon pool and library, equipping, weapon levels and how damage grows |
|
||
| `docs/SPELLS.md` | The spell list, who can use which spells, spell slots, the spell library |
|
||
| `docs/ITEMS.md` | The item pool and library, armor and other gear effects, what can be equipped, items for sale |
|
||
| `docs/ENCOUNTERS.md` | Battles and NPC quests, turns, rewards, trade, what players see |
|
||
| `docs/MONSTERS.md` | The monster list and built-in bestiary |
|
||
| `docs/NPCS.md` | Non-player characters, what they sell and the rates they change money at |
|
||
| `docs/CAMPAIGN_SETTINGS.md` | Every campaign setting: dice, ability scores, subclasses and multiclassing, auto-rolled attacks, XP, crits |
|
||
| `docs/SEEDS.md` | World seeds: ready-made campaigns a DM starts from a code, and how to add one |
|
||
|
||
`CLAUDE_BUILD_PROMPT.md`, `docs/GAME_RULES_REFERENCE.md` and
|
||
`docs/ROLL_YOUR_CHARACTER.md` are the original specifications the first
|
||
version was built from. The app has since moved on in places (gear levels,
|
||
the builder's story steps, picking a motivation); the documents above are the
|
||
current behaviour.
|
||
|
||
## Getting around
|
||
|
||
The same four destinations are always on screen: **Campaigns** (the list),
|
||
**Campaign** and **Party** (for the campaign you were last in, from any page,
|
||
including a character sheet or an encounter) and **Account**.
|
||
|
||
- On a wide screen they are links along the top bar, and the dice roller
|
||
floats at the bottom left. **A number with a dotted underline rolls when
|
||
clicked** (a skill, a save, an attack, damage), with the bonus already in
|
||
and advantage or disadvantage if the roller is set to it. The DM's "🎲 Call
|
||
for a roll…" rolls one check for the whole party.
|
||
- On a phone or small tablet (900px wide or less) they are a bar along the
|
||
bottom of the screen, with the **dice roller** as its last button. Nothing
|
||
floats over the page: the 🧙 character button sits in the top bar.
|
||
- The **🧙 character button** is on every page (floating at the bottom right
|
||
on a wide screen). It opens a panel with your character's numbers, saving
|
||
throws, skills and everything they hold; the DM can open it for any
|
||
character. "📋 Sheet" on an encounter card and 📋 on the party overview open
|
||
the same panel for that character, without leaving the page.
|
||
- **In an encounter** a strip across the top lists everyone in turn order and
|
||
stays in view; a click goes to that card. On a wide screen the stat block
|
||
of whoever is on turn stays in view beside the cards, and **📌 Pin** keeps
|
||
anyone else's there instead. For the DM, **N** is Next Turn and **S** opens
|
||
the sheet of whoever is on turn.
|
||
- The DM adds **several of a monster at once** (numbered for them), rolls
|
||
initiative for all the monsters and NPCs with one button, and can give a
|
||
**condition a number of rounds**, after which it comes off by itself.
|
||
Hovering over a condition shows its rule, and damaging someone who is
|
||
Concentrating shows the save to call for.
|
||
- The **campaign page shows one part at a time**, picked from a row of
|
||
buttons under its name: Overview (characters and encounters), NPCs,
|
||
Locations, Weapons, Items, Spells, Monsters, Notes and Settings for the DM;
|
||
Overview, Places and Notes for a player. Each button shows how many things
|
||
are in that part. On a phone the row is one line that scrolls sideways. The
|
||
part showing is in the address (`?view=weapons`), so it can be bookmarked
|
||
and the back button steps through the parts.
|
||
- **Search this campaign**, the box above that row, looks through everything
|
||
that has been added to the campaign at once: characters, encounters,
|
||
locations and notes, and for the DM also NPCs, weapons, items, spells and
|
||
monsters. It matches names and descriptions, every word typed, in any
|
||
order. A result opens that thing. Each of the DM's lists also has a search
|
||
box of its own.
|
||
- The DM's lists (NPCs, weapons, items, spells, monsters) are short rows: a
|
||
name and a few words. **Clicking a row opens a pop-out** with everything
|
||
about it and everything that can be changed. Adding one, by hand or from the
|
||
library, is a pop-out too. One added by hand opens its own pop-out, with an
|
||
**"+ Add another"** button at the top that goes straight back to an empty
|
||
form. A pop-out is a window over the page on a wide
|
||
screen and the whole screen on a phone; ✕, Escape or a click outside it
|
||
closes it.
|
||
- Every paged list has Prev and Next, and with three pages or more the page
|
||
number is a menu to **jump straight to any page**.
|
||
- The DM's **🛏 Rest…** button, beside "Characters" on the campaign page and
|
||
on the party overview, rests any of the party at once: a short rest says
|
||
which Hit Dice each character can roll and heals from what they rolled; a
|
||
long rest puts back HP, spell slots and Hit Dice.
|
||
- A caster's **spell slots** are on their sheet and in the character panel:
|
||
the player presses Use when they cast, and only the DM gives a slot back.
|
||
- Each encounter shows the DM **how hard it is** (Easy to Deadly, from the
|
||
monsters' challenge ratings against the party) and what it is worth in
|
||
**XP**, which one button shares out. A sheet shows XP and when a level is
|
||
due; levelling up stays the DM's call.
|
||
- A character's **condition and counters** are on the sheet and in the panel:
|
||
inspiration, exhaustion, conditions (which stay with the character from one
|
||
encounter to the next) and class counters such as Rage and Ki, which the
|
||
player spends and a rest gives back.
|
||
- Every class has its **full catalogue of official subclasses** (123, from the
|
||
2014 books and the 2024 Player’s Handbook), with features up to level 20.
|
||
One is picked in the builder (a die can suggest one) or in the level-up
|
||
checklist at level 2 or 3, and shown beside the class everywhere; the DM can
|
||
change it from the sheet, and switch a 2014 path to its 2024 update. Every
|
||
sheet shows **the road ahead**: what each level from 1 to 20 gives.
|
||
- **Class features that change a number are worked out**: Unarmored Defense
|
||
(barbarian, monk), Draconic Resilience, Fast Movement, Unarmored Movement,
|
||
Aura of Protection, Diamond Soul, Slippery Mind and Jack of All Trades are
|
||
counted on the sheet, by what is equipped (armor and shields are marked
|
||
items) and by level in the class that gives them.
|
||
- **Armor holds Dexterity back** as in the rules: all of it counts toward AC
|
||
in light armor, at most +2 in medium, none in heavy.
|
||
- **Battles that keep to the fight.** While a battle is under way the scene
|
||
description shows only between rounds, cards' details start closed (the
|
||
spotlight, or on a phone the card on turn, has them), and the DM's setup,
|
||
Experience and Rewards panels fold to their titles. Button labels wrap
|
||
inside their buttons, and on a phone quick stats no longer sits on Log out
|
||
nor incoming rolls on the bottom bar.
|
||
- **Stricter battles.** A spell with no slot of its level or higher left
|
||
cannot be cast (cantrips never need one), and a player changes the weapon
|
||
in hand only as the battle begins or on their own turn after attacking.
|
||
- **Free roam**, a third kind of encounter beside battles and quests: the DM
|
||
ticks the nearby locations and adds the characters who are there, and
|
||
each player sends their own character to a place to meet who is found there
|
||
and shop (a place's own shop, or an NPC's wares) where they stand.
|
||
- **Completed encounters are hidden** from the campaign's list until "Show
|
||
completed" is ticked.
|
||
- **Faster battles.** A colour-coded **turn-order track** (PCs blue, NPCs
|
||
green, monsters red) shows this round, a 🏁 flag where it ends, and the
|
||
next. Click a name (or press 1–9) to **target**. Whoever is on turn picks a
|
||
**weapon or spell**; the d20 is typed in or rolled and the app says **hit,
|
||
miss or critical**, or, for a spell, asks each target for its **saving
|
||
throw** against the caster's DC. The DM overrules anything and applies it
|
||
with one click. With **auto-roll** on, a monster's or NPC's turn opens with
|
||
its attack already aimed and rolled, to accept, edit or discard. A "What
|
||
now" line always says what the scene is waiting on.
|
||
- **Dice that roll.** Each die is drawn as its own shape, tumbles in and
|
||
lands on its result; a natural 20 glows. In a campaign the server makes
|
||
every roll and the **DM sees each one** as it lands, in the roller's
|
||
"Everyone's rolls" tab, beside the dice button, and on the encounter page.
|
||
A campaign can play with digital dice, real dice typed in (and checked), or
|
||
both, and can keep each player's rolls between them and the DM.
|
||
- **Multiclassing** (a campaign setting): the level-up checklist asks which
|
||
class gains the level, checks both classes' ability requirements, and works
|
||
out HP, Hit Dice, spell slots, counters and features by each class's own
|
||
level. The sheet suggests well-known multiclass routes from where the
|
||
character stands. Eldritch Knights and Arcane Tricksters now have their
|
||
spell slots.
|
||
- **43 species** (the 2014 core, the 2024 Aasimar and Goliath, and the species
|
||
of Monsters of the Multiverse) and **13 classes**, the Artificer included. A campaign can do without them: untick "Characters choose a
|
||
subclass" in its Settings and every class follows one path, unasked.
|
||
- **Campaign settings** come in four groups. **Characters**: starting gold
|
||
and gear, how ability scores are made (4d6, standard array or point buy),
|
||
subclasses, multiclassing. **Dice & rolls**: digital, physical or both, and
|
||
who sees a roll. **Combat**: auto-rolled monster and NPC attacks, the
|
||
critical hit rule, monster HP, the area penalty. **Progression**: XP
|
||
tracking (off for milestone levelling) and HP on a level up.
|
||
- **⬆ Level up…** (DM, beside Rest) approves a level for any of the party at
|
||
once. Each player gets a notice, sees what the level gives and makes their
|
||
own picks in the checklist: HP (the average, or a roll where the campaign
|
||
rolls), the class's new special abilities, a subclass, a Boon, spells and
|
||
the free weapon upgrade. A level waiting shows on the sheet and the
|
||
campaign page until it is taken; the DM sees who still has picks to make,
|
||
and can finish any of them.
|
||
- The character sheet is long, so it starts with a row of **section links**
|
||
that jump straight to that part of the page.
|
||
- On a small screen every table (weapons, items, spells, monsters, the party
|
||
overview) is shown as a stack of cards, one per row, with each figure
|
||
labelled, and rows of controls wrap rather than run off the side.
|
||
|
||
## Stack
|
||
|
||
- **Backend:** Node.js + Express, JavaScript, Prisma ORM (with the `pg` driver adapter), PostgreSQL, Socket.io, JWT auth (bcrypt + jsonwebtoken), zod validation.
|
||
- **Frontend:** React + Vite + TypeScript, React Router, TanStack Query, socket.io-client.
|
||
|
||
## Prerequisites
|
||
|
||
- Node.js 20.19+ (tested on Node 24)
|
||
- A running PostgreSQL instance (local install, Docker, or a hosted service like Neon/Supabase)
|
||
|
||
## First-time setup
|
||
|
||
```bash
|
||
npm install # installs both workspaces (backend + frontend)
|
||
|
||
# Backend: point at your Postgres instance and run migrations
|
||
cd backend
|
||
cp .env.example .env # edit DATABASE_URL and JWT_SECRET
|
||
npx prisma migrate dev --name init
|
||
npx prisma generate # migrate no longer does this itself
|
||
npx prisma db seed # adds the admin account if it is missing (see output for the login and the world codes)
|
||
|
||
# Frontend
|
||
cd ../frontend
|
||
cp .env.example .env # defaults to http://localhost:4000, adjust if needed
|
||
```
|
||
|
||
## Running in dev
|
||
|
||
In two terminals, from the repo root:
|
||
|
||
```bash
|
||
npm run dev:backend # Express + Socket.io on http://localhost:4000
|
||
npm run dev:frontend # Vite dev server, prints its own port (5173 by default)
|
||
```
|
||
|
||
Open the frontend URL, register an account (or log in with the seeded admin
|
||
account — see the seed script's console output for the credentials), and
|
||
start playing.
|
||
|
||
The database seed creates the admin account and nothing else. It only adds: an
|
||
existing account, and its password, is left alone, so it is safe to run on a
|
||
server with real data. `SEED_ADMIN_EMAIL`, `SEED_ADMIN_PASSWORD` and
|
||
`SEED_ADMIN_NAME` set the account it creates. `npx prisma db seed -- --reset`
|
||
wipes every user and campaign first.
|
||
|
||
## Worlds
|
||
|
||
Campaigns are created in the app: blank, or from a **world seed** by its
|
||
code. On the Campaigns page click "+ New Campaign" and enter a code under "Or
|
||
start from a ready-made world"; the whole world is built for you as its DM.
|
||
|
||
| Code | World |
|
||
|---|---|
|
||
| `ALDENF` | Aldenfell |
|
||
| `HOLLOW` | Hollowcrown: The Rift Between Worlds |
|
||
|
||
Each world is one file in `/backend/seeds`. Drop another file in and its code
|
||
works straight away, with no restart; `npm run seeds:check` (from the repo
|
||
root or `backend`) checks every file and says what is wrong with one.
|
||
`docs/SEEDS.md` has the format.
|
||
|
||
On a server, after pulling a new version, run `npm install` in the repo root
|
||
(when dependencies changed) and then `npm run deploy` in `backend`
|
||
and then restart the backend. It applies new migrations **and** regenerates
|
||
the Prisma client; without the second half the server keeps running with the
|
||
old data model and pages that use anything new fail to load (for example
|
||
"Could not load this encounter"). Change your password from the account page
|
||
("Account" in the navigation).
|
||
|
||
## Tests
|
||
|
||
```bash
|
||
cd backend
|
||
npm test
|
||
```
|
||
|
||
The calculation engine (`src/game/calculations.js`) and the world seeds
|
||
(`src/seeds`, which also checks every file in `/backend/seeds`) are unit
|
||
tested and run with no external dependencies. The route-level integration tests in
|
||
`src/routes/api.integration.test.js` need a real, migrated database reachable
|
||
via `DATABASE_URL` — they skip themselves gracefully (with a console warning)
|
||
if the DB isn't reachable, so `npm test` still passes in an environment with
|
||
no database configured.
|
||
|
||
## Project layout
|
||
|
||
```
|
||
/backend — Express + Prisma API, Socket.io server
|
||
prisma.config.js — where the Prisma CLI finds the schema, the database and the seed
|
||
/prisma — schema.prisma, migrations, seed.js (the admin account only)
|
||
/seeds — world seeds: one file per ready-made campaign, created by code
|
||
/seed-data — reference JSON (species/classes/skills/weapons, ported into
|
||
src/game/data/*.js and used directly by the calculation engine,
|
||
not stored as DB rows)
|
||
/src
|
||
/game — calculations.js (the ONLY place D&D formulas live) + buildFlow.js + levelUp.js
|
||
/middleware — auth + campaign/character authorization
|
||
/routes — auth, campaigns, characters, encounters, notes
|
||
/seeds — finds, checks and creates campaigns from the world seeds
|
||
/sockets — Socket.io room wiring
|
||
/frontend — React + Vite + TypeScript app
|
||
/docs — game rules & character-builder specs (reference material)
|
||
```
|