Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhXuLD2Z6hv3Wqeq9UL9K
125 lines
11 KiB
Markdown
125 lines
11 KiB
Markdown
# 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-docx` and `reportlab` for the export
|
||
- Everything runs in **one** container
|
||
|
||
## Run it on the server
|
||
|
||
```bash
|
||
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.
|
||
|
||
1. Copy `.env.example` to `.env` and set `AW_REGISTRATION_CODE`. People need this code **once** to create an account; with an empty code registration is disabled.
|
||
2. Open the site, choose "Create account", enter a username, a password (min. 8 characters) and the registration code.
|
||
3. 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):
|
||
|
||
```bash
|
||
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_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
|
||
|
||
```bash
|
||
# 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. **Print / save as 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). In a lobby, **A4 sheet / Wide** switches between the compact page and the full-width table view (your choice is remembered). The Word and PDF downloads on the Summary page are separate exports with their own layout.
|
||
|
||
### 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: the main area on the left, the chat as a column at the right edge of the window and a dice hotbar at the bottom centre of the main area.
|
||
|
||
- **Chat** (right column): live chat for everyone, plus **whispers** to a single person (pick them in the 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); it can be hidden with a checkbox. The chat polls the server every few seconds, keeps the last 1000 entries per lobby and allows 500 characters per message and 30 messages per minute.
|
||
- **Dice** (bottom centre): d4, d6, d8, d10, d12, d20 and d100, with a count (1 to 10 dice). A click rolls on the server (so nobody can cheat), the dice tumble into a tray above the hotbar for everybody in the lobby and the result is posted in the chat. Rolls follow the chat's recipient dropdown: with a person selected it is a private roll only that person (and you) can see. **The DM's rolls are hidden from the players** (the hotbar says "Hidden (only you)"); the DM can still pick a player as recipient to show them a roll.
|
||
- **Party** (tab): member cards (name, concept, class, profession). The DM also finds the join code, rename, remove player and delete lobby here.
|
||
- **One sheet tab per character** (next to Party): the DM gets a tab for every character in the lobby (read-only, always the current saved version, refreshed every 15 s); a player gets "My character". Hover or tap a value to see how it is made up. "Open in the Character tab" jumps to the full editor (for the DM it is a read-only look, "Save" would create the DM's own copy).
|
||
|
||
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.
|
||
|
||
## 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` | Own and shared-with-me characters / create one |
|
||
| 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/lobbies/{id}/messages` | Chat and event log (`?after=<id>` to poll) / send, optional `to` = whisper |
|
||
| POST | `/api/lobbies/{id}/rolls` | Roll `{sides, count, to?}` (d4–d20, d100; up to 10 dice), result goes into the chat |
|
||
| PUT / DELETE | `/api/lobbies/{id}/me` | Switch your character / 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 |
|