V1.12.0: worlds from a code, shops and cash in the seeds, up-to-date packages
- A DM can start a campaign from a ready-made world by typing its six-character code: "+ New Campaign" on the Campaigns page, then "Or start from a ready-made world". The whole world is built in one go with them as its DM: locations, weapons, items, spells, monsters, NPCs, encounters and notes. ALDENF is Aldenfell, HOLLOW is Hollowcrown. - A world is one file in /backend/seeds. Drop another in and its code works straight away, with no restart. "npm run seeds:check" lists every world, or what is wrong with one: a misspelt field, a name used twice, an encounter pointing at something the file does not have. A file with a mistake in it is never offered. docs/SEEDS.md has the format. - A seed can now hold everything a campaign can: gear effects, an NPC's wares and exchange rate, things for sale at an encounter, and rewards in silver, copper and cash. - The database seed creates the admin account and nothing else. The Aldenfell and Hollowcrown seed commands are gone: worlds come from codes. - Hollowcrown follows the V3 book: 4 starting items and 3 things equipped, the book's effects on every weapon and item, armor as items (library, modern and magic), worn gear at the book's levels, damage types on monsters, ten shop scenes with prices and stock, five money changers, Meridian loot as cash and mixed hoards as gold, silver and copper. - Aldenfell: the Shield and Leather Armor change AC, Mireille and the Founding Fair stalls sell things, and the book's missing scenes are in (E5, E7, E15, E16, E18, E23-24, E26, the Drowned Shrine, Sevrisk's Hollow, a wyvern pass). The locket is found with Investigation, as the book has it. - Packages are up to date: Prisma 7 (with the pg driver adapter and a prisma.config.js that holds the database URL), Express 5, zod 4, dotenv 18, jest 30, bcrypt 6 and TypeScript 7. "npm audit" finds nothing. Install scripts are approved for Prisma and bcrypt. On a server: "npm install", then "npm run deploy". - The frontend's five lint warnings are fixed; useAuth and usePagination have files of their own. - All packages at version 1.12.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+233
@@ -0,0 +1,233 @@
|
||||
# World Seeds
|
||||
|
||||
A world seed is a ready-made campaign: locations, weapons, items, spells,
|
||||
monsters, NPCs with their wares, encounters with their rewards, and notes. A
|
||||
DM starts a campaign from one by typing its code.
|
||||
|
||||
This describes the code as it stands. Related: [ITEMS.md](ITEMS.md),
|
||||
[WEAPONS.md](WEAPONS.md), [SPELLS.md](SPELLS.md), [MONSTERS.md](MONSTERS.md),
|
||||
[NPCS.md](NPCS.md), [ENCOUNTERS.md](ENCOUNTERS.md).
|
||||
|
||||
## Contents
|
||||
|
||||
1. [The basics](#1-the-basics)
|
||||
2. [Starting a campaign from a code](#2-starting-a-campaign-from-a-code)
|
||||
3. [Adding a world](#3-adding-a-world)
|
||||
4. [What a seed file holds](#4-what-a-seed-file-holds)
|
||||
5. [Checking a seed](#5-checking-a-seed)
|
||||
6. [The database seed](#6-the-database-seed)
|
||||
7. [The worlds that come with the app](#7-the-worlds-that-come-with-the-app)
|
||||
|
||||
---
|
||||
|
||||
## 1. The basics
|
||||
|
||||
- Every `.js` file in `/backend/seeds` is one world. The file says what its
|
||||
**code** is: six capital letters or digits.
|
||||
- Anyone who is logged in and knows a code can create that world as a new
|
||||
campaign. They become its DM and owner, exactly as with a blank campaign.
|
||||
- The same code can be used any number of times. Each use makes a separate
|
||||
campaign with its own invite code; nothing is shared between copies.
|
||||
- A seed only says what a campaign **starts** as. Once created, the campaign
|
||||
is the DM's to change, and changing the seed file later does not touch
|
||||
campaigns already made from it.
|
||||
- The folder is read each time a code is entered, so a new file is live as
|
||||
soon as it is on the server. No restart, no migration.
|
||||
|
||||
## 2. Starting a campaign from a code
|
||||
|
||||
On the **Campaigns** page, click **+ New Campaign**. Under the form for a
|
||||
blank campaign is **Or start from a ready-made world**: type the code and
|
||||
click **Create From Code**. Capitals and spaces do not matter (`aldenf`,
|
||||
`ALDENF` and `ald-enf` are the same code).
|
||||
|
||||
The whole world is created in one go, and you land on its campaign page. If
|
||||
anything in it fails, nothing is created.
|
||||
|
||||
| Reply | Meaning |
|
||||
|---|---|
|
||||
| "A world code is 6 letters or numbers" | What was typed is not six characters long |
|
||||
| "There is no world with that code" | No valid seed file has that code |
|
||||
|
||||
The API is `POST /campaigns/from-seed` with `{ "code": "ALDENF" }`. It
|
||||
answers with the new campaign, as `POST /campaigns` does.
|
||||
|
||||
## 3. Adding a world
|
||||
|
||||
1. Write the world as a file in `/backend/seeds`, for example
|
||||
`/backend/seeds/my-world.js` (section 4).
|
||||
2. Run `npm run seeds:check` (section 5) and fix whatever it reports.
|
||||
3. Put the file on the server. The code works straight away.
|
||||
|
||||
A file with a mistake in it is never offered: its code simply answers "There
|
||||
is no world with that code" until the file is fixed. Two files cannot share a
|
||||
code; the one whose file name comes first keeps it.
|
||||
|
||||
To retire a world, delete its file. Campaigns already made from it stay.
|
||||
|
||||
## 4. What a seed file holds
|
||||
|
||||
A seed file is a JavaScript module that exports one object. Only `code` and
|
||||
`campaign.name` are required; every list can be left out.
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
code: 'MYWRLD',
|
||||
campaign: { name: 'My World', description: '...', startingGold: 20, startingItemCount: 2, startingSpellCount: 2 },
|
||||
locations: [{ name: 'The Old Mill', description: '...' }],
|
||||
weapons: [
|
||||
'dagger', // a copy from the site library
|
||||
{ library: 'longsword', isStartingOption: true }, // a copy, with changes
|
||||
{ name: 'Millstone Hammer', damageDice: '2d6', damageType: 'bludgeoning', attackAbility: 'STR', minLevel: 3 },
|
||||
],
|
||||
items: [
|
||||
{ library: 'shield', isStartingOption: true },
|
||||
{ name: 'Miller’s Charm', description: '...', effects: [{ stat: 'SKILL_PERCEPTION', amount: 1 }] },
|
||||
],
|
||||
spells: [{ library: 'fire-bolt', isStartingOption: true }, 'fireball'],
|
||||
monsters: ['goblin', { name: 'Mill Wraith', ac: 13, maxHp: 30, attackBonus: 4, damageDice: '1d8+2 necrotic' }],
|
||||
npcs: [
|
||||
{
|
||||
name: 'Old Hob', role: 'Miller', disposition: 'FRIENDLY', description: '...', dmNotes: '...',
|
||||
wares: [{ item: 'Shield', price: 10 }, { weapon: 'Dagger', price: 2, stock: 3 }],
|
||||
},
|
||||
],
|
||||
encounters: [
|
||||
{
|
||||
name: 'The Mill at Night', mode: 'BATTLE', location: 'The Old Mill', description: '...', dmNotes: '...',
|
||||
monsters: [['Goblin', 3], ['Mill Wraith', 1, 'The Miller’s Wife', 40]],
|
||||
npcs: ['Old Hob'],
|
||||
rewards: [{ gold: 25 }, { silver: 40 }, { weapon: 'Millstone Hammer', note: 'Behind the wheel' }],
|
||||
},
|
||||
],
|
||||
sharedNotes: ['Welcome to My World...'],
|
||||
dmNotes: ['The secret of the mill...'],
|
||||
};
|
||||
```
|
||||
|
||||
The file is ordinary JavaScript, so it can build its lists however it likes
|
||||
(the Hollowcrown seed adds the whole spell library with a `map`).
|
||||
|
||||
### Top level
|
||||
|
||||
| Field | What it is |
|
||||
|---|---|
|
||||
| `code` | Six capital letters or digits. What a DM types |
|
||||
| `campaign` | The campaign's settings: `name`, and optionally `description`, `startingGold`, `startingItemCount`, `startingSpellCount`, `skipMotivationRoll`, `showMonsterHp`, `areaAttackAcPenalty`, `maxEquipped`, `maxEquippedWeapons`. Anything left out takes the app's default |
|
||||
| `locations` | `{ name, description }` |
|
||||
| `weapons`, `items`, `spells`, `monsters` | The pools. Each entry is a row or a library copy (below) |
|
||||
| `npcs` | NPC rows, each optionally with `wares` |
|
||||
| `encounters` | In the order they should list |
|
||||
| `sharedNotes`, `dmNotes` | Note bodies, in reading order: the first is shown at the top. Shared notes are for everyone, DM notes for the DM |
|
||||
|
||||
### Pool entries
|
||||
|
||||
An entry in `weapons`, `items`, `spells` or `monsters` is one of:
|
||||
|
||||
- **A library key** such as `'longsword'`: a copy of that entry from the
|
||||
site's built-in library (`src/game/data/*Library.js`).
|
||||
- **A library copy with changes**: `{ library: 'longsword', isStartingOption: true, minLevel: 2 }`.
|
||||
Any field of the row can be changed, including its name.
|
||||
- **A full row**, with the same fields the app's own forms have:
|
||||
|
||||
| Pool | Fields (required in bold) |
|
||||
|---|---|
|
||||
| Weapon | **`name`**, **`damageDice`**, **`damageType`**, **`attackAbility`** (`STR`, `DEX` or `FINESSE`), `properties`, `rangeNormal`, `rangeLong`, `damageScaling` (1 to 3), `minLevel` (1 to 9), `isStartingOption`, `effects`, `effectNote` |
|
||||
| Item | **`name`**, `description`, `damageDice`, `damageScaling`, `minLevel`, `isStartingOption`, `effects`, `effectNote` |
|
||||
| Spell | **`name`**, **`level`** (0 to 9), `damageDice`, `school`, `castingTime`, `range`, `duration`, `description`, `isStartingOption`, `revives` |
|
||||
| Monster | **`name`**, **`ac`**, **`maxHp`**, `speed`, `strScore` ... `chaScore`, `attackBonus`, `damageDice` (may name its type: `"1d6+2 necrotic"`), `challengeRating`, `specialTrait`, `notes` |
|
||||
|
||||
`effects` is a list of `{ stat, amount }`, as described in
|
||||
[ITEMS.md](ITEMS.md): armor is an item with an `AC` effect.
|
||||
|
||||
Within a pool a name can only be used once, because everything else in the
|
||||
file points at pool entries **by name**.
|
||||
|
||||
### NPCs
|
||||
|
||||
**`name`**, `role`, `disposition` (`FRIENDLY`, `NEUTRAL` or `HOSTILE`),
|
||||
`description` (what players see), `dmNotes`, and for those who might fight
|
||||
`ac`, `maxHp`, `attackBonus`, `damageDice`.
|
||||
|
||||
- **A money changer** has `exchangeGold` and `exchangeCash` together:
|
||||
`exchangeGold: 1, exchangeCash: 50` is 1 gold for $50, either way.
|
||||
- **A shopkeeper** has `wares`, a list of things for sale (below).
|
||||
|
||||
### Encounters
|
||||
|
||||
| Field | What it is |
|
||||
|---|---|
|
||||
| **`name`** | |
|
||||
| `mode` | `BATTLE` (the default) or `QUEST`. Buying and money changing only happen in a quest |
|
||||
| `description` | The scene, shown to the players in it |
|
||||
| `dmNotes` | DM only |
|
||||
| `location` | A location's name |
|
||||
| `monsters` | `[monster name, how many, display name, max HP]`. The last two are optional; more than one gets "#1", "#2"... |
|
||||
| `npcs` | NPC names, or `{ name, hp }` to set the hit points for this scene |
|
||||
| `rewards` | Lined up unawarded (below) |
|
||||
| `wares` | For sale at the scene itself, with nobody behind it: a market, a vending machine |
|
||||
|
||||
A **reward** is exactly one of `gold`, `silver`, `copper`, `cash` (an amount),
|
||||
`weapon` or `item` (a name), with an optional `note`. A weapon or item can also
|
||||
have `quantity` and `level`.
|
||||
|
||||
A **ware** is `{ weapon | item, price }` with optional `currency` (`GOLD`, the
|
||||
default, `SILVER`, `COPPER` or `CASH`), `level` and `stock` (left out: no
|
||||
limit).
|
||||
|
||||
A reward's or ware's `level` defaults to the entry's own starting level, and
|
||||
cannot be below it.
|
||||
|
||||
### Limits
|
||||
|
||||
Text fields have the same length limits as the app's forms (for example 4,000
|
||||
characters for an encounter's DM notes and 10,000 for a note), so that
|
||||
everything a seed creates can still be edited on screen. The checker says
|
||||
which field is over.
|
||||
|
||||
## 5. Checking a seed
|
||||
|
||||
```bash
|
||||
npm run seeds:check
|
||||
```
|
||||
|
||||
lists every file in `/backend/seeds` with its code and what it holds, or
|
||||
everything wrong with it:
|
||||
|
||||
```
|
||||
✓ aldenfell.js — code ALDENF — Aldenfell
|
||||
24 weapons (16 starting), 31 items (10 starting), 45 spells (30 starting), 21 locations
|
||||
15 monsters, 13 NPCs (8 wares), 38 encounters (40 rewards, 9 wares), 23 notes
|
||||
✗ my-world.js
|
||||
npcs.0.desciption: Unrecognized key: "desciption"
|
||||
Encounter "The Mill at Night": there is no monster called "Gobblin"
|
||||
```
|
||||
|
||||
It checks the things that would otherwise only show up as a half-working
|
||||
campaign:
|
||||
|
||||
- A field name that is not one of the fields above (a typo).
|
||||
- A library key that is not in the library.
|
||||
- The same name twice in one pool.
|
||||
- An encounter, reward or ware that names a location, monster, NPC, weapon or
|
||||
item the file does not have.
|
||||
- A reward that is not exactly one thing, or gear handed out below its
|
||||
starting level.
|
||||
- An exchange rate with only one half.
|
||||
|
||||
`npm test` runs the same check on every seed in the folder.
|
||||
|
||||
## 6. The database seed
|
||||
|
||||
`npx prisma db seed` creates the admin account if it is missing, and nothing
|
||||
else. It never adds a campaign: worlds come from codes, in the app. See the
|
||||
README for the account it creates and `--reset`.
|
||||
|
||||
## 7. The worlds that come with the app
|
||||
|
||||
| Code | World | From |
|
||||
|---|---|---|
|
||||
| `ALDENF` | Aldenfell | The Aldenfell Campaign Book |
|
||||
| `HOLLOW` | Hollowcrown: The Rift Between Worlds | The Hollowcrown Dungeon Master's Book (V3) |
|
||||
|
||||
Change a world's code by editing `code` in its file.
|
||||
Reference in New Issue
Block a user