joshuaandClaude Opus 5.5 57fba68a48 V1.21.0: a DM's toolkit: the battle at a glance, subclasses, character state, rolls, XP
Seven steps in one commit, 1.15.0 to 1.21.0. They change the same files
and were not saved apart, so they go in together.

- The battle at a glance (1.15). A strip across the top of an encounter
  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
  beside the cards, and Pin keeps anyone else's there. Saving throws,
  every skill and passive Perception, Investigation and Insight are on
  encounter cards and in the character panel, which "Sheet" on a card
  and a button on the party overview open. For the DM, N is Next Turn
  and S opens the sheet of whoever is on turn. The party overview shows
  passive scores and spell save DC.
- Subclasses (1.16). Three per class, picked in the level-up checklist
  at level 2 (cleric, druid, wizard) or 3, shown beside the class
  everywhere, and changed by the DM from the sheet: the old one's lines
  come out of Special Abilities and the new one's go in. Characters
  already at that level keep the path they were on.
- Character state (1.17). Inspiration, exhaustion and conditions on the
  sheet and in the panel, and counters for what a class does a set
  number of times (Rage, Ki, Channel Divinity...), which the player
  spends and a rest gives back. A long rest takes a level of exhaustion
  off. A character's conditions are their own now: they stay from one
  encounter to the next.
- Faster battles (1.18). Up to 20 of a monster added at once, numbered.
  One button rolls initiative for the monsters and NPCs. A condition can
  be given a number of rounds and then comes off by itself. Hovering
  over a condition shows its rule, and damaging someone who is
  Concentrating shows the save to call for.
- Rolls from the sheet (1.19). A number with a dotted underline rolls
  when clicked, with its bonus: skills, saves, ability checks, attacks,
  damage, spell dice, and a monster's attack, damage and abilities. The
  dice roller has advantage and disadvantage. "Call for a roll" rolls
  one check or save for the whole party. Rolls stay in the browser.
- XP and difficulty (1.20). An encounter shows the DM how hard it is,
  Trivial to Deadly, from the monsters' challenge ratings against the
  characters in it, and what it is worth in XP; one button shares that
  out, once. The sheet and the party overview show XP and when a level
  is due. Levelling up stays the DM's decision.
- Subclasses are optional per campaign (1.21). With "Characters choose a
  subclass" unticked in the campaign's Settings nobody is asked, each
  class follows its first subclass, and none is named.
- Conditions, exhaustion, counters and subclass features are reminders
  for the table: none of them changes a number on the sheet.
- The docs cover all of it, with generated tables of what each subclass
  adds.
- New migrations: a character's subclass; inspiration, exhaustion,
  conditions and spent counters on a character; the rounds left on a
  combatant's conditions; the XP awarded for an encounter; the
  campaign's subclass setting.
- All packages at version 1.21.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 17:36:27 +02:00
2026-09-22 16:27:53 +02:00

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/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 three subclasses, picked in the level-up checklist at level 2 or 3 and shown beside the class everywhere; the DM can change one from the sheet. A campaign can do without them: untick "Characters choose a subclass" in its Settings and every class follows one path, unasked.
  • ⬆ Level Up… on a sheet (DM) takes the character up a level with the whole checklist: HP, the class's new special abilities, a Boon, spells and the free weapon upgrade.
  • 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

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:

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

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)
S
Description
No description provided
Readme
7.1 MiB
Languages
JavaScript 66.3%
TypeScript 30.8%
CSS 2.9%