Packaging & Distribution
A campaign file on its own is not installable. To ship one — to a
friend, or to the Workshop — it needs a folder with a
manifest.json beside it, and that manifest is what the
campaign grid reads before anything is played.
Folder layout
One campaign is one folder containing exactly two files:
my-campaign/
manifest.json how it appears in the grid
campaign.json the campaign itself
Both names are required and neither is configurable. A folder missing either is skipped with a warning rather than half-loaded.
manifest.json
{
"id": "the-sunken-crypt",
"name": "The Sunken Crypt",
"levelRange": { "min": 3, "max": 6 },
"theme": "undead-horror",
"tags": ["dungeon", "undead", "investigation"],
"difficulty": "hard",
"enabled": true,
"minEngineVersion": "0.1.0"
}
| Field | Required | What it does |
|---|---|---|
id | yes | Unique across everything installed. On a collision the
first-seen wins and yours is skipped, so make it specific —
the-sunken-crypt, not crypt. It must
match the id inside campaign.json. |
name | yes | Shown on the campaign card. |
levelRange | recommended | { "min": n, "max": n }. Drives the level bracket a
DM filters by. |
difficulty | recommended | Free text, shown on the card. The bundled campaigns use
easy, medium, hard,
very-hard — matching them keeps the grid
readable. |
theme | optional | A short slug such as stealth-politics. Used for
grouping. |
tags | optional | Kept with the entry for search and future filtering. Nothing in the current UI renders them — they cost nothing and may start mattering, but do not rely on them being visible. |
enabled | optional | Set false to keep a campaign installed but hidden.
Absent means enabled. |
minEngineVersion | optional | See below. |
source | — | Ignored. The app sets this from where the pack was found. Writing it does nothing. |
minEngineVersion
If your campaign uses something a older build cannot do, say so:
"minEngineVersion": "0.2.0"
A player whose app is older is told “needs Campaign Cookbook 0.2.0 or newer” and the pack is not loaded. Without it, the same player gets a campaign that appears to install and then behaves oddly at the table, which is far harder for anyone to diagnose.
Plain dotted numbers only — 1.2.0. There is no range
syntax, no carets and no tildes. It answers one question
(is this new enough?) and you should not have to learn a
version grammar to answer it.
minEngineVersion locks players out of a campaign that would
have worked for them.
Sagas: several campaigns in order
For a multi-campaign story meant to be played in sequence, add a
saga.json at the top and put each campaign in its own
subfolder:
my-saga/
saga.json
part-one/
manifest.json
campaign.json
part-two/
manifest.json
campaign.json
{
"id": "the-long-dark",
"name": "The Long Dark",
"minEngineVersion": "0.1.0",
"campaigns": [
{ "id": "part-one", "order": 1 },
{ "id": "part-two", "order": 2 }
]
}
Each entry's id is the folder name. A
minEngineVersion on the saga applies to every member unless
that member sets its own.
Campaigns in one saga share a continuous world seed, so the towns, dungeons and NPCs a party met in part one are the same ones in part two. That is the reason to use a saga rather than shipping two unrelated campaigns.
What happens when it is installed
Every pack that did not come with the app is validated when it is found, not when it is played. A pack that fails never appears in the campaign grid at all — the alternative is a DM choosing it with players waiting and discovering the problem mid-scene.
Refused for any of:
campaign.jsonis unreadable or not valid JSON- it fails the same checks the
Testbed runs — a
nextScenepointing at a scene that does not exist, an unrecognised placeholder, a duplicate dungeon id, and so on minEngineVersionis newer than the installed app
Rejections are reported to the DM with the reason, so a pack that vanishes is never mistaken for one that failed to download. One bad pack does not stop the others loading.