Files
joshuaandClaude Opus 5.5 4bd301182b V1.32.0: battles that keep to the fight
- 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>
2026-10-19 08:19:48 +02:00

271 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)
```