The format
The adventure file
An adventure is a file you can send somebody. This is one, with every part of the format used at least once and a note on each. Copy it, change the words, and load it back through Import on the adventures page.
The example below is built by the same code the builder uses and read back by the same loader your own file goes through, every time this page renders. It is not a copy of the format kept in step by hand.
What each part is
- kind
- Always adventure. A file without it is not one.
- manifest
- Who wrote it, what it is called, and what it needs. formatVersion is what the loader keys its migrations on — leave it alone unless you know why you are changing it. requiresPack names the rulesets a table has to have loaded for the stat pointers below to resolve to anything.
- maps
- Every map, as a placement: a name, a grid, a picture and a scale. columns and rows are the grid squares are counted in. scale says what one square means, and null is an honest answer for a map nobody has measured.
- contents
- One record per map, holding that map's rooms, things and boundaries, keyed by mapId. C4 on the crypt is not C4 on the cave — each map owns its own coordinate space, which is why this is a list and not one big pile.
- contents[].regions
- Rooms. shape is a list of blocks, so a corridor that turns is one room with several of them — ["C4:C8", "D8:F8"] in the example. readAloud is read to the table; dmNotes never leaves your screen; publicName is what players are told instead of the real name; reveal decides when they are told anything at all.
- contents[].contents
- Things standing on squares — creatures, loot, exits, hazards, terrain. stat points at a creature in a pack, so a roll brings its numbers with it. drawFrom points at a chart id instead, so the square rolls for what is there when a table opens rather than being decided now.
- contents[].edges
- Boundaries between squares. There is no /N and no /W: the boundary north of C5 is the one south of C4, so every boundary is written as /S or /E. A run of them is C4/S..H4/S. The board's own outside edge is implicit and impassable and is never stored.
- links
- Where an exit goes. fromContentId names the exit it leaves from, because two maps can be joined in more than one place and a party standing on a square needs to know whether this door is the one to the crypt.
- rollTables
- Charts a square can roll on. A line with a stat brings numbers with it; a line without one is still a perfectly good line — a rumour or a bit of weather has no creature behind it.
- entry
- Which map the adventure starts on.
- provenance
- human or generated. What made this, recorded rather than guessed at later.
- lattices
- Reserved. Leave it as an empty list.
- edges
- Document-level boundaries. Leave it empty — walls live on contents[].edges, beside the map they are on.
Two rules worth repeating
Ids are minted once and never re-derived
A room called Lich's Tomb is r-lich-s-tomb forever, even after you rename it to the room with the sacks. Everything that points at a room points at its id, so changing one by hand detaches every note, every link and every square that referenced it — silently, and usually months later.
publicRef is the only identifier that may reach a player
id is a slug of the name a thing had when it was made, so sending it to a browser spells the twist in a network tab. If you are hand-writing records, give each one a publicRef that says nothing about what it is.
A complete file
{
"contents": [
{
"columns": 12,
"contents": [
{
"behaviours": [],
"dmCue": null,
"dmNotes": null,
"drawFrom": null,
"id": "c-ogre",
"kind": "creature",
"name": "Ogre",
"provenance": "human",
"publicName": "something enormous",
"publicRef": "p2",
"readAloud": null,
"reveal": "always",
"shape": [
"D5"
],
"stat": {
"packId": "dndmax.dogfood",
"selector": "ogre"
},
"tags": []
},
{
"behaviours": [],
"dmCue": null,
"dmNotes": null,
"drawFrom": "rt-creatures",
"id": "c-rotted-chest",
"kind": "loot",
"name": "Rotted chest",
"provenance": "human",
"publicName": null,
"publicRef": "p3",
"readAloud": null,
"reveal": "always",
"shape": [
"E6"
],
"stat": null,
"tags": []
},
{
"behaviours": [],
"dmCue": null,
"dmNotes": null,
"drawFrom": null,
"id": "c-iron-door",
"kind": "exit",
"name": "Iron door",
"provenance": "human",
"publicName": null,
"publicRef": "p4",
"readAloud": null,
"reveal": "always",
"shape": [
"L1"
],
"stat": null,
"tags": []
}
],
"edges": [
{
"id": "e-door-c4s",
"kind": "door",
"state": "closed",
"runs": [
{
"first": {
"x": 2,
"y": 3,
"side": "S"
},
"last": {
"x": 2,
"y": 3,
"side": "S"
}
}
],
"provenance": "human"
}
],
"mapId": "m-the-crypt",
"regions": [
{
"dmCue": null,
"dmNotes": "The bandit runs at half HP.",
"id": "r-the-corridor",
"name": "The corridor",
"provenance": "human",
"publicName": null,
"publicRef": "p1",
"readAloud": "Moss over flagstones, and something breathing further in.",
"reveal": "on-entry",
"shape": [
"C4:C8",
"D8:F8"
],
"tags": []
}
],
"rows": 8
},
{
"columns": 8,
"contents": [],
"edges": [],
"mapId": "m-the-cave",
"regions": [],
"rows": 6
}
],
"edges": [],
"entry": {
"placementId": "m-the-crypt"
},
"kind": "adventure",
"lattices": [],
"links": [
{
"id": "l-1",
"fromMapId": "m-the-crypt",
"toMapId": "m-the-cave",
"fromContentId": "c-iron-door"
}
],
"manifest": {
"authors": [
"You"
],
"engineMin": "0.1.0",
"formatVersion": 1,
"id": "adv-example",
"license": {
"attribution": "You",
"spdx": "CC0-1.0"
},
"name": "An Example Adventure",
"requiresPack": [
"dndmax.dogfood"
],
"version": "1.0.0"
},
"maps": [
{
"backdrop": {
"columns": 12,
"gridVerified": true,
"imageUrl": "/maps/library/castle-courtyard.webp",
"kind": "registered",
"licence": {
"attribution": "DMax original art",
"spdx": "CC0-1.0"
},
"nativeCellPx": 128,
"rows": 8
},
"columns": 12,
"id": "m-the-crypt",
"lattice": null,
"name": "The crypt",
"ports": [],
"provenance": "human",
"rows": 8,
"scale": {
"tier": "room",
"cell": {
"unit": "feet",
"magnitude": 5
}
},
"source": null,
"stack": null
},
{
"backdrop": null,
"columns": 8,
"id": "m-the-cave",
"lattice": null,
"name": "The cave",
"ports": [],
"provenance": "human",
"rows": 6,
"scale": null,
"source": null,
"stack": null
}
],
"provenance": "human",
"rollTables": [
{
"id": "rt-creatures",
"name": "Creatures",
"entries": [
{
"name": "Ogre",
"stat": {
"packId": "dndmax.dogfood",
"selector": "ogre"
}
},
{
"name": "A rumour about the miller",
"stat": null
}
]
}
]
}
Back to adventures