A real-time companion app for the storytelling board game Legacy of Dragonholt. It keeps the shared state of a campaign in sync across every player's screen:
- The Party — character cards with species, background, a stamina tracker and a full sheet: ranked skills, traits, conditions, inventory and notes.
- Gold — the party's coin, with a Split loot action that divides a haul evenly across the named heroes and logs the breakdown in the journal.
- Fame — the party's reputation and deeds.
- The Calendar — the current day and time of day, with one-tap advancement.
- Quests — pledged errands with notes, dependency tracking and active/completed filtering; flips between a list and a left-to-right DAG view showing which quests are unblocked, locked or done.
- People — the cast met along the way, tagged ally / foe / neutral / unknown, with location and notes.
- Locations — the map ledger of towns, wilds and ruins, tagged rumored / visited / home / lost, with region and notes; flips between a list and a hand-drawn parchment map with draggable pins.
- Keywords — the log of story keywords and notes recorded along the way.
- The Chronicle — an A1–Z8 grid of story entries, with progress, chapter navigation and filtering.
- The Dice roller — quick d4–d100 buttons, a full dice-notation parser
(
2d6+3,3d4+2d6+1, …) and named macros (Longsword attack, Sneak) saved per device for one-tap repeat rolls. Every roll is broadcast live so the whole table sees the same dice. - Ambient soundscape — a synth-only ambience picker (tavern, forest, dungeon, storm, sea). Picking a scene plays it for every connected player; volume and mute are per device. No audio assets — every scene is built procedurally with Web Audio.
- Reactions — a docked pill of six quick emoji (applause, crit, "oh no", battle, "down they go", cheers). Tapping one fires off a floating emoji that every connected player sees rise up their screen for a couple of seconds; nothing is persisted.
The header also offers a campaign menu with one-click Download backup (every panel saved to a single JSON file), Restore from backup, a GM notebook — a private scratchpad for the storyteller, kept local to the device that wrote it and never broadcast to the table — a Session recap view, a one-screen "where the party stands" with stat cards and the most recent journal entries, with a Copy-as-text button for dropping a handout into the table's group chat — and a lore generator that rolls fresh tavern names, NPCs and weather reports when the table is stuck on what to call the next inn.
Built as a React app (powered by Vite) with an Express + Socket.IO backend. Every change is broadcast over Socket.IO so all connected players stay in sync instantly.
A guided tour of every panel — with screenshots — is published to GitHub
Pages from the docs/ folder. Browse it locally by opening
docs/features/index.md, or read the live site at
https://it-rec.github.io/lod-gui/.
This project uses Bun as both its package manager
and its runtime for the backend dev server. The version is pinned in
.bun-version and mirrored in the packageManager and
engines.bun fields of package.json.
Install Bun (curl -fsSL https://bun.sh/install | bash),
then install dependencies with bun install.
Vite still builds the React app and Vitest still runs the tests — Bun just executes them.
Always use
bun run <script>. Barebun testwould invoke Bun's built-in test runner instead of thetestscript — we use Vitest here for its React Testing Library integration, sobun run testis what you want.
Runs the Vite dev server at http://localhost:3000.
Requests to /api and /socket.io are proxied to the backend on
http://localhost:3080.
Runs the backend server (Express + Socket.IO) on port 3080 under Bun
with --watch, which restarts on changes to server/ and its imports.
If MongoDB is not reachable the server logs a warning and falls back to an
in-memory store, so the app is fully usable for local development and demos
without a database. It keeps retrying in the background and switches to the
database transparently once it becomes reachable. Set MONGO_URL to point at a
database for real persistence.
| Variable | Default | Purpose |
|---|---|---|
PORT |
3080 |
Port the backend listens on. |
MONGO_URL |
mongodb://localhost:27017/... |
MongoDB connection string. Falls back to an in-memory store when unreachable. |
MONGO_RECONNECT_MS |
10000 |
Delay between background attempts to reach MongoDB after a failed startup connection. |
ALLOWED_ORIGINS |
(none) | Comma-separated CORS allowlist, or * for any origin. CORS is off by default — the client is served same-origin in production and proxied through Vite in development. |
Builds the app for production into the build/ folder. The Express server
(server/app.js) serves this directory and provides the SPA fallback.
Serves the production build locally for previewing.
Runs the test suite with Vitest.
Runs ESLint over the project.
src/— React source (Vite entry:src/index.jsx).components/— feature panels and shared UI components.hooks/—useGameChannel(per-channel data + realtime sync) anduseConnection.socket/— the shared Socket.IO connection.shared.js— the game id andgamePath()REST-path helper.- design tokens live in
src/index.scss; component styles in*.module.scss.
shared/collections.json— collection/channel names, the single source of truth imported by both the client (src/shared.js) and the server.public/— static assets copied verbatim to the build root.index.html— Vite HTML entry (at project root).server/— Express + Socket.IO backend with a MongoDB / in-memory store. Realtime updates are scoped to a per-game Socket.IO room (server/rooms.js).vite.config.mjs— Vite configuration (dev server proxy, build output, tests).