# 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`, `subclassesEnabled`, `multiclassEnabled`, `diceMode`, `autoRollMonsters`, `autoRollNpcs`, `trackXp`, `hpOnLevelUp`, `abilityScoreMethod`, `rollVisibility`, `hideMonsterRolls`, `critRule`. 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.