Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhXuLD2Z6hv3Wqeq9UL9K
26 KiB
Arcane Wasteland Character Creator
Website for creating characters for the Arcane Wasteland TTRPG, with a searchable rulebook and export to PDF or Word.
- Frontend: React (Vite), 8-step wizard, searchable rules page
- Backend: FastAPI, SQLite,
python-docxandreportlabfor the export - Everything runs in one container
Run it on the server
docker compose up -d --build
Then open http://localhost:8090 (the port is bound to the machine itself; see DEPLOY.md for running it on a server with its own user, nginx and HTTPS). Users, characters and presets are stored in ./db/aw.db.
Login and accounts
Everything (the whole API, rules, characters) is behind a login. The only public things are the login page itself and the app's JavaScript.
- Copy
.env.exampleto.envand setAW_REGISTRATION_CODE. People need this code once to create an account; with an empty code registration is disabled. - Open the site, choose "Create account", enter a username, a password (min. 8 characters) and the registration code.
- Sessions last 30 days (HttpOnly cookie, stored server-side). Logging out or changing the password ends them.
Wrong logins and wrong registration codes are throttled (10 failures per address, 8 per username, 5 wrong codes per address and 30 per hour for the whole server), so a short code is not trivially guessable, but a long one is still better. Passwords are stored as scrypt hashes.
HTTPS is a must when the site is reachable from the internet, otherwise passwords travel in plain text. Put the container behind a reverse proxy (Caddy, nginx, Traefik) that terminates TLS, pass the Host header through, and set AW_TRUST_PROXY=1 in .env so the app sees the real client address and marks the cookie Secure.
Admin tasks (list users, reset a password, delete a user):
docker compose exec awcc python -m app.admin list
docker compose exec awcc python -m app.admin passwd <username>
docker compose exec awcc python -m app.admin delete <username>
Characters and presets made before accounts existed are adopted automatically by the account that logs in from the same browser (the old secret keys are used once and then removed).
Edit the rules
All rule data lives in backend/data/rules.json: attributes, derived-stat formulas, skills, professions, classes, talents, mutations, implants, conditions, equipment and the texts of the rules page. Edit the file and reload the page; no rebuild is needed because the folder is mounted into the container.
Entries with "placeholder": true are placeholders and show a badge in the app. To remove the badge, fill in the entry and delete that line.
Note: Derived-stat formulas are in rules.json (derived[].formula = list of attributes whose mods are summed). Special rules that cannot be expressed there (profession attribute bonus, skill maximum, higher-of-two-attributes for skills, carry capacity) are mirrored in frontend/src/calc.js and backend/app/derive.py. When changing them, change both files.
Rules presets
Presets are named sets of creation numbers (attribute points, skill pool, talents, proficiency picks, unlock cost, caps ...). They are defined in rules.json under presets: each has an id, name, desc and a creation object that overrides keys of the global creation block (an empty object = the standard values). default_preset is the one new characters start with. The list of values that can be edited per character is tunables.
In the creator, the Concept page only has a "Rules preset" selector. Presets are created, edited, duplicated and deleted on the Presets tab. Built-in presets (from rules.json) are read-only; user presets are stored in the database (/api/presets) and visible to everyone, but only the browser that created one holds the key to change or delete it. The chosen numbers are copied into the character, so shared links and exports use the same numbers even if the preset is changed or deleted later. "Start over" keeps the chosen preset.
Skills, proficiency and expertise
creation.advanced_attr_percent: share of the attribute that counts for advanced skills (default 50, rounded down; 100 = full attribute like basic skills).creation.advanced_unlock_cost: pool points needed to unlock an advanced skill (free if the character's profession grants proficiency in it; proficiency picks do not unlock a skill).creation.free_proficiencies: free proficiency picks every character gets. Talents can add more with"proficiency_picks": N(see "Versatile Training").- Proficiency is +
proficiency_bonus; two picks on one skill (a profession proficiency counts as one) give Expertise, +expertise_bonus.
Equipment
equipment lists every item with id, name, cat (category), stat (damage / armor / type), kg and note. There are no starting-kit rules: players pick any items (with an amount) on the Equipment page and the total weight is checked against the carry capacity. Weights are approximate; for ranges in the source document the upper value is used.
Development
# Backend
cd backend && pip install -r requirements.txt
uvicorn app.main:app --reload --port 8000
# Frontend (second terminal, proxies /api to port 8000)
cd frontend && npm install && npm run dev
Saving and sharing
Characters are saved in your account ("My characters" page). Only the owner can see a character, change it or delete it. On "My characters" the Share button lets you add usernames (e.g. your DM): they get read-only access, see the character under "Shared with me" and can open it through /#creator/<ID>. Saving a viewed character creates their own copy. Use the JSON backup (Summary page) to move a character between servers.
Hover (or tap / focus) the dotted-underlined labels and the small "i" icons to see where a number comes from, e.g. how a skill's target number and maximum are derived, or what a derived stat is made of. The explanations are generated from the rules, so they follow presets and rules.json changes.
While creating, the full character sheet stays visible next to the wizard. Edits are shown as a difference (old → new, added / removed items) until you press Apply (or Apply & next); Discard drops them.
Each wizard page has a Reset this page button that clears only that page's data, plus Start over for the whole character.
Character sheet (A4)
The Summary page and the sheet tabs in the lobbies show the character as A4 pages: page 1 has name, attributes, derived stats and all skills (proficiency dots, attribute, points, total), page 2 has talents, mutations and implants, equipment, background and a ruled notes area. On the Summary page, Print / PDF (A4) opens just the pages in a clean window; choose "Save as PDF" in the print dialog (margins none, background graphics on if your browser asks). Hover or tap a value to see how it is made up (not printed). Light / Dark (in the lobby it sits above the sheet) switches the colours of the sheet on screen (remembered in the browser; printed pages are always light). The Word and PDF downloads on the Summary page are separate exports with their own layout.
Character portrait
On the Concept page Add picture takes a PNG, JPEG or WebP: move and zoom it inside the square window, Use this stores a small 256 px picture inside the character (so it is saved, shared, exported as JSON and loaded with it). The server checks and re-encodes it. The picture appears in the header of the character sheet (also when printing), as icon in the player bar of the lobby and as the token of the character on the map (in the player's colour ring). Only members of the lobby get the picture through /api/lobbies/{id}/portrait/{username}. The PDF / Word export from the server does not contain it yet.
The portrait sits next to the name and identity fields on page 1 of the sheet. The two ruled note areas ("Conditions & notes" on page 1, "Notes" on page 2) can be typed into on the Summary page and, for your own character, in the lobby (saved automatically through PUT /api/characters/{id}/notes, which only touches those two fields). They are included in print, PDF and Word.
Rules editor
Admins get an Editor tab to add, change and delete items, item categories, skills, skill groups and professions with forms. Admins are the usernames in AW_ADMINS (comma separated, set in .env); without that setting only the first account ever created is admin. Admins can also promote other users: the Users and admins card above the tabs lists all accounts with Make admin / Remove admin. Names in AW_ADMINS stay admin and can only be changed in .env; the last admin cannot be removed.
- Changes are saved in
db/rules_overrides.json(next to the database, so it is part of your backup). For each edited list it replaces the list fromrules.json; the shipped file is not touched. A previous copy is kept asrules_overrides.json.bak. "Reset to shipped" removes the override of one list. - New entries get their id from the name (lowercase,
_). Ids of existing entries cannot be changed, because characters refer to them. - The server checks every reference: an item needs an existing category, a skill a group and attributes, a profession existing skills. Removing a category, group or skill that is still used is refused. Removing an item, skill or profession that saved characters use asks for confirmation first (their data stays, the entry disappears from the sheets).
- Talents, mutations, implants, classes and the rules text are still edited in
rules.json. Players see changes after reloading the page.
Lobbies
The Lobbies tab groups a campaign. Whoever creates a lobby is its DM and gets an 8-character join code. Players join with the code and pick one of their saved characters (they can switch it later or leave).
The lobby page is built for desktop with three columns: the character sheet on the left (collapsible with the arrow), the Player Area in the middle and the chat at the right edge of the window.
- Chat (right): live chat for everyone, plus whispers to a single person (dropdown next to the message field). Whispers are only visible to sender and recipient, the DM does not see other people's whispers. The same feed shows the event log (joins, leaves, character switches, renames, new codes, can be hidden), dice rolls and skill checks. Names are shown in the player's colour. Everything in the lobby (chat, dice, difficulty, colours, sheets) updates live over a WebSocket (
/api/lobbies/{id}/ws); it only sends a short "something changed" note and the data is fetched through the normal API, so the visibility rules (whispers, hidden numbers) stay in one place. If the socket is blocked or drops, the page reconnects and meanwhile polls every few seconds. The chat keeps the last 1000 entries per lobby and allows 500 characters per message and 30 messages/rolls per minute. - Character bar (top of the Player Area): one card per player with the character, class and the player's colour. The DM clicks a card to show that character's sheet on the left (always the current saved version, refreshed every 15 s) and can enter a personal bonus / malus per character in the small number field. Players see their own sheet only and just a short card of the others. (The bar is also meant for initiative and enemies later.)
- Skill and attribute checks: players click an attribute or an unlocked skill on their own sheet. The server rolls a d100 (roll under): target = attribute or skill total + the DM's difficulty + the character's personal modifier. 01-05 is always a critical success, 96-100 always a critical failure. The chat and the table show the roll, the target and by how much it succeeded or failed. Combat is not handled specially: roll the normal skill and let the DM adjust with the modifiers.
- Difficulty (bar under the characters): the DM picks a level from the rules table (Trivial +60 ... Hellish -60) or a custom number. It applies to all checks until changed. With Hide numbers from players the players only see their roll and success / failure, not the target, difficulty or margin.
- Dice (below the table): d4 ... d100 with a count (1 to 10 dice). The server rolls, the dice tumble into the middle of the table in the colour of whoever threw them, and the result goes into the chat. With a person selected as recipient in the chat dropdown it is a private roll. DM rolls have three modes: Mystery (default: players see grey dice with "?" but not the result), Hidden (only the DM sees it) and Public.
- Sound: rolling plays
frontend/public/dice.mp3(the default dice sound) with a slight random pitch; every die has its own base pitch, from a little higher and lighter (d4) to a little lower and heavier (d100). Many dice layer the sound up to three times. Checks add a short cue after the dice: built-in tones for success and failure, and for a critical success or failure either a built-in tone or a sound the DM uploaded. The speaker button next to the dice mutes everything for yourself, the slider sets the volume (remembered per browser). Browsers only allow sound after the page was clicked once, and nothing plays while the tab is in the background. - DM sound settings (Lobby actions -> Sounds): replace the dice sound with an own MP3 (up to 1.5 MB, per lobby) or reset it to the default, upload sounds for Critical success and Critical failure (reset = built-in tone), and switch the critical sounds and the short success / failure tones on or off for the whole lobby. Test plays a sound locally.
- Own dice: with this switch on, a click on a die or a skill asks for the result of your real dice instead of rolling. The server only checks the range and marks the roll as "own dice" in the chat.
- Colours: every player has a different colour (lobby menu -> Your colour); the DM is gold.
- Lobby menu (top right): join code, rename, remove players, colour, switch character, leave or delete the lobby.
NPCs, enemies and creatures
The NPCs tab is a library of rules-based NPCs. It uses the same wizard as the character creator (attributes, skills, talents, mutations, equipment, portrait), with a free "NPC / creature" preset that has large pools and no profession requirement, plus a type (NPC, enemy, monster, creature, animal, robot), a default token size, a description, special abilities and attacks, and loot.
- NPCs are stored like characters (flag
npc) and are listed separately; they can be duplicated and shared with other DMs (read-only). - In the map editor and at the table, + From NPC library / + NPC places a token that takes the NPC's name, size and portrait. Plain + Token still works.
- The DM clicks an NPC token to see its sheet in the left column and can click attributes and skills to roll checks for it (shown in the chat as "NPC: Skill"). Players never see the NPC link or sheet.
Players and the DM show a green dot in the lobby while they have it open (online presence over the WebSocket; without WebSockets everyone appears offline).
Initiative and fights
The DM presses ⚔ Start fight in the lobby. Every player with a character joins automatically and rolls initiative (1d20 + the character's Initiative value, as in the rules). Right click a token (DM, full view) for a radial menu: add it to the fight, open its sheet, hide/reveal or remove an NPC token. Players who join later can be added with the + name buttons in the initiative bar.
The bar shows the order, the round and whose turn it is (the token pulses on the map). The DM can overwrite every number, reroll, remove entries, step ◀ / Next turn ▶ and End fight. Hidden NPCs are not shown to players. Fight start, joins and the end are logged in the chat feed.
The top of the lobby is one compact row: small buttons on the left (start fight, previous/next turn, end fight, ▾ to expand players and difficulty), the player bar – replaced by a scrollable initiative bar during a fight – and the lobby settings button. Players only see the name of whoever's turn it is; NPC names are not sent to players otherwise, and token names show on hover, selection or turn. The sheet and the chat can be resized by dragging the thin lines next to them (remembered per browser).
HP, conditions and fog of war
- HP and conditions: select a token (DM, full view) to edit its HP (−5/−1/+1/+5, max) and conditions from the rules (click raises the level 1 → 3 → off). NPC tokens from the library start with the NPC's Health value. Right click the token and use the HP and Cond. toggles of the radial menu to decide when players may see them; by default players see their own and other players' values but not NPCs'.
- Fog of war: with line of sight on, ground a player has already seen stays visible as a dimmed memory (Fog button; ↺ forgets it for everybody). The memory is a small mask per player and lobby, the dim picture is made on the server, so players never get pixels they have not seen.
- Sight range: the map's value is the default (map editor); the number next to the Sight button changes it live for the lobby (reset when you switch maps).
Sight groups and revealing far away areas
- Sight groups (Lobby actions, DM): players in the same group (A–D) share what they see; "Everyone shares sight" / "Everyone alone" are shortcuts. Tokens and ground seen by one member are visible to the whole group.
- 👁 Reveal (DM, line of sight on): click the spot the view starts from, then click where it ends (distance = range, direction = where it looks), pick the cone width and who sees it (everyone or chosen players), then Reveal. Walls block it like any other view. The area stays visible until you press Hide; afterwards it remains as dimmed memory (fog of war). Everything is computed on the server, so players only receive the pixels you showed them.
Maps, tokens and line of sight
- Maps tab (top navigation): all your maps with preview. Upload PNG, JPEG or WebP (up to 12 MB, 30 maps each; the server checks and re-encodes every image as WebP, max 4096 px per side, into
db/maps/). Edit opens the map editor, Share gives other users access by username (like characters: they can put the map on their own table or Copy it, but not change it), Duplicate copies a map. - Map editor: grid (pixels per square, token sizes are counted in squares), enemies and NPCs placed in advance (name, size, colour, Hidden from players, duplicate), and walls and doors for line of sight: tools Wall (click points, chains lines), Room (two corners), Door (two ends), Erase, Undo last; new lines snap to existing corners, optionally to grid corners. An optional sight range in squares limits how far characters see (0 = unlimited).
- Show on table (lobby, DM, Lobby actions): shows one of your or a shared map; the prepared tokens are copied into the lobby, so moving them during play never changes the map. Doors start closed. Every player has a token of their own. Without walls the players drag it onto the map themselves; with walls (line of sight) the DM places it from the tray.
- At the table: pan (drag the background), zoom (wheel, pinch, buttons) and Grid are personal. Players move only their own token; the DM moves everything, edits NPC tokens (click one), adds NPCs (+ NPC) and clicks doors to open or close them (Walls shows the layout in the full view).
- Line of sight is strict: the server calculates what each character can see from the walls and closed doors, cuts the map image to that area (everything else is black, only that piece is ever sent) and leaves out all other tokens. Walls are never sent to players. The DM chooses Full view or View as (exactly what that player gets) and can switch Sight on/off for the table.
Players only see a short card of the other players, never their sheets. Guessing join codes is rate limited. Deleting a character takes it out of the lobbies (the player can pick another); deleting a lobby does not delete characters.
Scenes and props
-
Scenes: every map you put on the table becomes a scene of the lobby. Switching to another map (or back, in Lobby actions → Scenes) first saves the current one - token positions, HP and conditions, doors, explored ground, revealed areas, sight settings and the running fight - and restores the other exactly as you left it. State also survives pauses, since the table lives in the database. Per lobby up to 12 scenes; rename, delete, Save now and Save a copy of the current table (checkpoint) are available. Only one scene runs at a time (no simultaneous split party).
-
Edit live: the map editor can be opened for the running map. Walls, grid and sight range apply to the game at once; the prepared tokens are only used the next time the map is put on the table.
-
Visibility in the editor: the 👁 button beside each prepared token toggles "hidden from players".
-
Props: decor tokens (chest, barrel, table, chair, bed, door, campfire, tree, rug, stairs, ... 19 built-in drawings, plus your own PNG/WebP uploads up to 3 MB, 40 per DM). Add them in the map editor (+ Prop) or on the table (+ Prop); drag like tokens, size width/height in squares, rotate by slider, 45° or 90° steps (also in the right click menu), hide from players. They are drawn below tokens; players cannot move them.
-
Prop library: the Maps page lists all props (built-in and your own) and lets you upload, rename and remove your own ones.
-
DM notes: NPC tokens and props have a "Notes (only you see these)" field (map editor and table; e.g. the contents of a chest). On the table click the token to edit; a 📝 marks tokens with notes and the text shows as tooltip. Players never receive notes.
-
Fog without walls: sight range and fog also work on maps without walls (circle around the token). Sight needs walls or a range above 0; the explored memory is now stored at up to 1024 px.
-
Map ping: everybody (players and DM) can point at a spot: double-click the map, or press 📍 Ping and click. All see a ring in the pinger's colour with their name, and hear a short ping (rate limited to one per second and person). The DM can switch the sound off or upload an MP3 for it under Lobby actions → Sounds → Map ping; players can still mute sounds for themselves.
API
All /api/* routes except /api/auth/me, /api/auth/register and /api/auth/login need the session cookie.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/auth/me |
Current user (or null) |
| POST | /api/auth/register / login / logout / password |
Accounts |
| GET | /api/rules |
Rule data including presets |
| GET / POST | /api/characters[?npc=1] |
Own and shared-with-me characters (or the NPC library) / create one |
| GET | /api/characters/{id}/portrait |
Portrait image (readers of the character) |
| POST / PUT / DELETE | /api/lobbies/{id}/combat |
Start a fight / add, remove, set, reroll, next, prev / end (DM) |
| GET | /api/lobbies/{id}/token-portrait/{token} |
Portrait of an NPC token (lobby members) |
| GET / PUT | /api/admin/users[/{username}] |
List accounts / make or remove an admin (admins) |
| GET | /api/characters/{id} |
Read (owner, shared users, or the DM of a lobby it is in) |
| POST / DELETE | /api/characters/{id}/shares[/{username}] |
Share / unshare (owner only) |
| PUT / DELETE | /api/characters/{id} |
Change / delete (owner only) |
| GET / POST | /api/lobbies |
Own and joined lobbies / create one (you are the DM) |
| POST | /api/lobbies/join |
Join with {code, character_id} |
| GET / PUT / DELETE | /api/lobbies/{id} |
Members and cards (DM sees all ids) / rename / delete (DM only) |
| POST | /api/lobbies/{id}/code |
New join code (DM only) |
| GET / POST | /api/maps |
Your maps and the ones shared with you / upload (raw image body, ?name=) |
| GET / PUT / DELETE | /api/maps/{id} |
Map with tokens and walls / change (name, grid, sight, tokens, walls) / delete |
| POST / DELETE | /api/maps/{id}/shares[/{username}] |
Share with a user / stop sharing |
| POST | /api/maps/{id}/copy |
Your own editable copy |
| GET | /api/maps/{id}/thumb |
Preview image |
| GET / PUT | /api/lobbies/{id}/map |
Map and tokens as the viewer may see them (?as=<player> for the DM) / DM: show map (map_id, null clears) |
| GET | /api/lobbies/{id}/map/view |
The map image for this viewer, cut to their line of sight (X-Crop = x,y,w,h) |
| PUT | /api/lobbies/{id}/doors/{wall} , /sight |
DM: open / close a door; line of sight on / off |
| GET / POST | /api/lobbies/{id}/scenes |
DM: saved scenes / save a copy of the table (name) |
| PUT / DELETE | /api/lobbies/{id}/scenes/{sid} |
DM: rename (name), save now (save) / delete |
| POST | /api/lobbies/{id}/ping |
Ping a spot (x, y); everybody gets a push |
| PUT | /api/lobbies/{id}/scene |
DM: switch to a scene (scene_id); the current one is saved first |
| GET / POST / DELETE | /api/props[/{id}] |
Your own prop images / upload (raw body, ?name=) / delete |
| GET | /api/props/{id}/image, /api/lobbies/{id}/prop-image/{pid} |
Prop image (owner / lobby members, DM's props) |
| POST / PUT / DELETE | /api/lobbies/{id}/tokens[/{tid}] |
Add NPC token (DM) / move (own token or DM) and edit (DM) / remove (DM) |
| WebSocket | /api/lobbies/{id}/ws |
Live notes {t: messages / lobby / sheet / map} for lobby members (cookie login, same origin) |
| PUT | /api/lobbies/{id}/sounds |
DM: switch result / critical sounds on or off |
| GET / POST / DELETE | /api/lobbies/{id}/sounds/{roll|crit_success|crit_failure} |
Custom lobby sound (MP3): everyone gets it / DM uploads (raw body) / DM resets |
| GET / POST | /api/lobbies/{id}/messages |
Chat and event log (?after=<id> to poll) / send, optional to = whisper |
| POST | /api/lobbies/{id}/rolls |
Roll {sides, count, to?, manual?, mode?} (d4-d20, d100; up to 10 dice). manual = results of real dice, mode (DM only) = hidden/mystery/public |
| POST | /api/lobbies/{id}/checks |
d100 check {kind: skill|attr, id, manual?} for the caller's lobby character |
| PUT | /api/lobbies/{id}/difficulty |
{label, mod, hidden} (DM only) |
| PUT | /api/lobbies/{id}/members/{username}/mod |
Personal bonus / malus {mod} (DM only) |
| PUT / DELETE | /api/lobbies/{id}/me |
Switch your character and / or {color} / leave |
| DELETE | /api/lobbies/{id}/members/{username} |
Remove a player (DM only) |
| POST | /api/presets |
Create a preset |
| PUT / DELETE | /api/presets/{id} |
Change / delete (owner only) |
| POST | /api/export/pdf or /docx |
Character JSON in the body, file in the response |