Homebrew by hand
Write packs in a plain text editor, check them against the JSON schemas and load them into the app.
Everything the in-app forms can do, and several things they cannot, can be written in an ordinary text editor. A pack is one JSON file. If you are comfortable with JSON, this is the fastest way to write a lot of content, to copy a pattern across twenty entries, or to use features the forms have no box for, such as charges, requirements and conditional rules.
This page assumes you know what JSON is and nothing else. By the end you will have written a pack, checked it, and loaded it into the app.
What you need
- A text editor that understands JSON. Visual Studio Code, the JetBrains editors, Sublime Text and Neovim all do, and all can use a JSON Schema to catch mistakes as you type. Notepad will work, but will not tell you when you have dropped a comma.
- The JSON schema files below.
- A way to get the finished file onto your device: a cloud drive, AirDrop, email, a USB cable.
- Optional, but recommended: the sample pack, one of each kind of entry, used as the running example on this page.
Get the JSON schemas
A JSON schema describes the shape of a file: which fields exist, which are required and what type each one has. Give one to your editor and it will underline a misspelled field, offer to complete one and explain what each one means.
These three files describe a pack. They are published here, straight from the engine’s own source:
content-compendium.schema.jsonA whole pack file: the top-level fields and the list of entries. Start here.common.schema.jsonThe shared pieces: rules, modifiers, predicates, timers, charges. The pack schema points into it.content-doc.schema.jsonA single entry's doc on its own, for a file that holds exactly one.
They refer to each other by name, so if you download them keep all three in the same folder. You only
ever point your editor at content-compendium.schema.json; it finds the others itself.
Point your editor at the schema
You do not put anything in the pack file itself. Instead you tell the editor “files that end in
.comp.json follow this schema”.
Visual Studio Code. Open your user or workspace settings (settings.json) and add:
{
"json.schemas": [
{
"fileMatch": ["*.comp.json"],
"url": "https://crawler-vault-marketing.web.app/schema/content-compendium.schema.json"
}
]
}JetBrains editors (IntelliJ IDEA, WebStorm and the rest). Open Settings → Languages & Frameworks →
Schemas and DTDs → JSON Schema Mappings, add a mapping, paste the address of
content-compendium.schema.json as the schema, choose JSON Schema version 2020-12 and add the file name
pattern *.comp.json.
Anything else. Editors that use the JSON language server accept the same json.schemas setting
shown above. If your editor wants a local file, download the three schema files into one folder and give it
the path to content-compendium.schema.json.
What the schema catches, and what it does not
The schema knows the shape of a pack. It will catch:
- a misspelled field (
predicte,modifer), or a field in the wrong place; - a missing required field (every entry needs an
idand adoc, every modifier atarget,op,valueandlabel); - the wrong type: a number where text goes, a list where an object goes;
- an operator or time unit the game does not have.
It cannot know the meaning of what you wrote. It will happily accept a bonus aimed at a stat that does not exist, a tag the game has never heard of, or a reference to a skill nobody published. For those, run Check… in the app (see Checking your work).
The file
A pack is a JSON file ending in .comp.json (the comp is for compendium). When the app exports a pack it
names the file after the pack’s identifier, and you may as well do the same. The smallest useful pack is:
{
"repo": "my-pack",
"entries": []
}
Fill in the rest of the details and it looks like this:
{
"repo": "crawler-samples",
"name": "Crawler Samples",
"source": "Crawler Vault help: Homebrew by hand",
"version": "1.0",
"description": "One of each kind of entry, written by hand. Copy it, rename it, break it.",
"authors": ["Crawler Vault"],
"license": "CC0-1.0",
"system": "dccarl",
"entries": []
}
| Field | Required | What it is |
|---|---|---|
repo | yes | The pack’s permanent identifier. See below. |
entries | yes | The list of content. |
name | What the pack is called on screen. Free to change. | |
description | A sentence shown under the name. | |
authors | A list of names, because packs get co-written and handed on. | |
source | Where the ideas came from. | |
license | What anyone else may do with the pack. Use an SPDX identifier such as CC-BY-4.0 or MIT where one fits. Leaving this blank is the wrong default for something meant to be shared, and Check will say so. | |
licenseUrl, copyright | The licence’s address, and the copyright line. | |
version | Stamped onto every reference the pack publishes. See Versions. | |
system | Which game the pack is for: dccarl. | |
minSystemVersion | The oldest version of the game’s definition the pack works with. |
The repo identifier
repo is half of every reference to your content: a skill in your pack is your-repo/the-skill-id.
That has three consequences.
- It is lowercase letters and digits, in segments joined by
.or-:crawler-samples,dave.boom,table-3. No spaces, capitals or underscores. - It must be unique. Two packs with the same identifier would publish references that resolve into each
other’s content. It also must not be one of the official books (
dcc-core). - Never change it once anyone is using the pack. A crawler holding
old-name/thingwill not find it undernew-name.
A plain word like homebrew is a poor choice: you may not be the only person who thought of it. Something
with your table’s or your own name in it is better.
Anatomy of an entry
Each item in entries has an id and a doc. Here is a ring, complete:
{
"id": "ring-of-pep",
"doc": {
"name": "Ring of Pep",
"tags": ["trait:accessory", "trait:magical"],
"rules": [
{ "kind": "Note", "text": "While worn, your Strength is 2 higher." },
{
"kind": "Modifier",
"modifier": {
"target": { "kind": "exact", "path": "stats.str.score" },
"op": "Add",
"value": "2",
"label": "Ring of Pep",
"type": "item",
"predicate": { "kind": "tag", "tag": "item:equipped" }
}
}
]
}
}idis what the rest of the game refers to the entry by. Use lowercase letters, digits and hyphens. It must be unique within the pack, and it never changes once a crawler owns one. (A crawler holds a reference torepo/id, and renaming the id breaks it.)docis the content itself. Its fields:
| Field | What it is |
|---|---|
name | What a player reads. |
tags | Labels that tell the game what this is and where it goes. See Tags. |
rules | What it does. A list of rules, in order. |
rollDescriptors | Dice lines it declares, such as a skill’s damage. |
charges | Limited uses, like a wand’s. See Charges, timers and requirements. |
timers | How long it lasts, and its cooldown. |
requirements | What a crawler must be before taking it. |
meta | Notes for tools: mana cost, range. Never changes what the entry does. |
Every field is optional. An entry with only a name is perfectly legal, and so is a piece of gear that
does nothing but exist, which describes most of what a crawler picks up.
version(next toid, not insidedoc) is optional and covered under Versions.
Tags
Tags are how the game finds things. A tag is a short word with a colon in it, like skill:spell or
trait:head. They decide which tab an entry lands on, which slot gear fits, and whether a buff is internal or
external.
Which tags for which kind
| Kind of entry | Tags |
|---|---|
| Gear | One tag per place it can be worn: trait:head, trait:torso (or trait:armor), trait:arms, trait:hands, trait:legs, trait:feet, trait:holdable, trait:accessory. Add trait:two-handed for two hands’ worth, trait:stackable for ammunition and rations, and trait:magical for flavour. |
| Any skill | skill, so it appears on the Skills tab, and a kind: skill:attack, skill:spell or skill:utility. |
| Skill extras | skill:passive, skill:interrupt, stat:str (or int, con, dex, cha) for the stat it is rolled with, damage:fire and so on for what it deals, attack:melee / attack:ranged, and skill:target:self, skill:target:person, skill:target:object or skill:target:area. |
| Buff | buff:internal or buff:external. |
| Debuff | debuff: plus a name of your own, such as debuff:my-pack.soggy-socks. Two debuffs with the same name apply once, unless they also carry effect:stackable. effect:requires-action means it takes an Action to switch on. |
| Race | race and race: plus its id. |
| Class | class and class: plus its id. |
Gear’s slots are read from the tags, so tagging an item is how you say where it goes. An item that fits two places carries two tags. The bandanna that goes on your head or your accessory slot carries both.
The vocabulary is checked
The game declares the tags it knows. A tag outside that list is reported by Check, so a typo does not silently do nothing. Two kinds of tag are open-ended and need no declaration, as long as they start with one of these prefixes:
applies-to:, attack:, buff:, check:, class:, debuff:, effect:, grants:, item:,
opposed-by:, race:, skill:favored:, spell:, stat:, target:, trait:, situation:,
self:condition:
So trait:cursed-by-a-cat is fine and house:banned is a problem. The tags that are fixed in advance are the
skill, class, race and similar words above, the thirteen damage types, and the three ways to answer them:
- Damage types:
acid,bludgeoning,electric,fire,force,holy,ice,necrotic,piercing,poison,psychic,slashingandsonic, writtendamage:fire. - Resistances:
resist:fire,immune:fireandvulnerable:fire, for any of those thirteen. Resistance halves the damage, vulnerability doubles it and immunity takes it to nothing.resist:wateris an error.
Rules
rules is the list of things an entry does. Each rule is an object with a kind. The four you will use
nearly all the time are Note, Modifier, GrantTag and GrantEntry.
Note
Words for the player to read. This is how the books are written, and how a rule the sheet cannot compute is still recorded.
{ "kind": "Note", "text": "Melee Attack, Str. Put something heavy in a sock. Swing the sock." }
A note can wait for a condition with a predicate. It appears only while the condition is true:
{
"kind": "Note",
"text": "Rank 5: +1d4 base damage.",
"predicate": { "kind": "expr", "expr": "@source.rank >= 5" }
}
The field is called predicate. Do not write when, which is what the game’s own definition uses
and is easy to mix up. The schema rejects it, which is the point of having the schema.
Modifier
A modifier changes a number. It is the heart of a ring, a race or a buff.
{
"kind": "Modifier",
"modifier": {
"target": { "kind": "exact", "path": "stats.str.score" },
"op": "Add",
"value": "2",
"label": "Ring of Pep",
"type": "item",
"predicate": { "kind": "tag", "tag": "item:equipped" }
}
}
| Field | What it does |
|---|---|
target | What to change. Usually { "kind": "exact", "path": "..." }. A { "kind": "pattern", "glob": "skill.*.rank" } hits every attribute that matches, where * stands for exactly one segment of the path. |
op | What to do to it. See below. |
value | How much, written as an expression. Always a string: "2", "-1", "@character.floor". |
label | What the sheet shows in the breakdown when a player asks “why is my Strength 8?”. |
type | Where it comes from: item, race, class, status and so on. Leave it out for an untyped modifier. In this game modifiers stack. |
predicate | When it applies. Left out, it always does. |
priority | Explicit precedence when two modifiers compete. Ties fall to the order they were written, later winning. Races and classes use it so that a class’s changes beat a race’s. |
The operators:
op | Does |
|---|---|
Add | Adds the value. A negative value subtracts. By far the most common. |
Multiply | Multiplies a plain number: "0.5" halves a Speed. |
Override | Sets it to the value, whatever it was. |
Upgrade | Sets it to at least the value: a floor rather than a cap. |
Downgrade | Sets it to at most the value. |
AddDice | Adds dice to a dice line: "dice(1, 6)". A damage line takes this. |
MultiplyBaseDice | Multiplies the rolled base damage before anything is added. |
MultiplyTotalDice | Multiplies the finished damage: “x2 the total”. |
UpgradeRank, DowngradeRank, SetDieCount, UpgradeDieSize, GrantFlag and ClearFlag also exist for
corners of the system most packs never reach.
The numbers you can change in Dungeon Crawler Carl:
| Path | What it is |
|---|---|
stats.str.score, stats.int.score, stats.con.score, stats.dex.score, stats.cha.score | A stat’s enhanced value: base plus everything on you. Aim at .score, never .base; the base is the number the player writes and nothing else may touch it. The modifier (.mod) follows from the score on its own. |
defense.armor | Armor. Adds to the Damage Resistance total. |
defense.dr.buffs | Damage reduction from buffs. Use this for a potion or a skill, and leave armor to armor. |
evasion.buffs | Added to Evade. |
movement.speed, movement.step | Speed and Step, after everything. |
check.modifier | Added to every Check. Soggy Socks use it. |
skill.<id>.rank | A skill’s rank. skill.*.rank is every skill the crawler has. |
skill.<id>.max-rank | The highest rank a skill can reach. |
skill.<id>.damage | A skill’s damage line (dice). |
skill.<id>.extra-mana | Extra mana a skill costs. |
debuff.endOfRound.damage | Damage taken at the end of each round (dice). |
regen.hp.perMinute, regen.mana.perMinute | Regeneration. |
body.arms, body.hands, body.legs, body.feet, body.heads | How many of each a crawler has, which sets how many things they can hold or wear. |
character.size | The crawler’s size. Races set it with Override and a value like "#size:large". |
A path that does not exist would do nothing at all, silently, which is the worst kind of bug. That is what Check is for: it reports a modifier aimed at an attribute the game does not have.
GrantTag
Gives the crawler a tag while the entry applies. This is how resistances work:
{
"kind": "GrantTag",
"tag": "resist:fire",
"predicate": { "kind": "tag", "tag": "item:equipped" }
}
A race grants origin:earth or origin:alien the same way, and a class grants its class-type: tags.
GrantEntry
Hands the crawler another piece of content: a skill, a spell, a buff. The crawler then has it, just as if they had learned it.
{
"kind": "GrantEntry",
"ref": { "repo": "crawler-samples", "id": "chefs-kiss" },
"bind": { "rank": "0" },
"retain": true
}
| Field | What it does |
|---|---|
ref | Which entry: its repo and id. A version is optional, and leaving it out means “whatever the pack has”. |
bind | The entry’s starting state, as expressions. { "rank": "7" } starts a granted skill at rank 7. |
appliesWhile | For gear: "EQUIPPED" (the default) or "CARRIED" for something that applies while it is merely in the pack. |
predicate | When the grant holds. It is withdrawn when this stops being true. |
retain | true means that what was granted stays the crawler’s own when the grant ends. Without it, it goes away. |
Granting says what the crawler gets. How good it is belongs to a modifier on the rank, as in the Line Cook below. Keeping the two apart lets a grant be skipped for a crawler who already has the skill, while the bonus still applies.
ref must point at an entry that exists in a pack that is switched on. Check reports a grant whose target nothing
publishes.
Predicates and expressions
Two small languages sit inside the quotation marks.
A predicate says when. It is an object with a kind:
kind | Means |
|---|---|
tag | The crawler has this tag. { "kind": "tag", "tag": "item:equipped" } is the one for gear. |
expr | This expression is true. { "kind": "expr", "expr": "@source.rank >= 5" } |
and, or | All, or any, of the predicates in of. |
not | The opposite of the predicate in of. |
always | Always. The same as leaving it out. |
item:equipped is a tag the game works out for you from the crawler’s slots. Gating everything an item grants on
it is what makes a ring in a pocket do nothing.
An expression says how much, or whether. The forms the shipped content uses are:
| Write | To mean |
|---|---|
2, -1 | A number. |
@stats.str.mod | The value of a number on the crawler. Any path from the table above works, plus @character.floor and @character.level. |
@source.rank | The rank of the entry this rule belongs to. A skill’s rules read this a lot. |
@source.charges | A counter on the entry (see charges, below). |
a + b, a - b, a * b | Arithmetic. |
a >= b, a < b | Comparisons. |
cond ? x : y | Choose one: @source.rank >= 5 ? dice(1, 4) : 0. |
dice(1, 6) | One six-sided die. dice(2, 8) is two eight-siders. |
flat(@character.floor) | A plain number added to a dice line. |
hasTag('trait:two-handed') | Whether the crawler has a tag. |
Writing each kind of entry
The sample pack has one of each. Every block below is copied from it unchanged.
Gear
Gear is a name, tags saying where it goes, and rules gated on item:equipped. The Ring of Pep above adds to
a stat. These oven mitts do two things at once: they add armor and make the wearer resist fire.
{
"id": "oven-mitts",
"doc": {
"name": "Oven Mitts of Mild Immolation",
"tags": ["trait:hands", "trait:magical"],
"rules": [
{ "kind": "Note", "text": "Fire does half damage to you while these are on. Your hands, anyway." },
{
"kind": "Modifier",
"modifier": {
"target": { "kind": "exact", "path": "defense.armor" },
"op": "Add",
"value": "1",
"label": "Oven Mitts of Mild Immolation",
"type": "item",
"predicate": { "kind": "tag", "tag": "item:equipped" }
}
},
{
"kind": "GrantTag",
"tag": "resist:fire",
"predicate": { "kind": "tag", "tag": "item:equipped" }
}
]
}
}Both rules are gated, so the mitts do nothing from a pocket. type is item on the bonus so the sheet
credits the right source. trait:hands is what makes the Hands slot offer them.
An attack skill
An attack is a skill with skill:attack, a stat tag and a rollDescriptors line for its damage:
{
"id": "sock-slap",
"doc": {
"name": "Sock Slap",
"tags": [
"skill",
"skill:attack",
"skill:attack:melee",
"attack:melee",
"skill:weapon",
"damage:bludgeoning",
"stat:str"
],
"rules": [
{ "kind": "Note", "text": "Melee Attack, Str. Put something heavy in a sock. Swing the sock." },
{
"kind": "Note",
"text": "Rank 5: +1d4 base damage.",
"predicate": { "kind": "expr", "expr": "@source.rank >= 5" }
}
],
"rollDescriptors": [
{
"id": "damage",
"dice": [{ "count": 1, "faces": 4 }],
"bonus": "@stats.str.mod + (@source.rank >= 5 ? dice(1, 4) : 0)",
"tags": ["damage:bludgeoning"]
}
]
}
}rollDescriptorsdeclares a dice line with anidofdamage.diceis the base dice (countdice withfacessides);bonusis an expression added to it, andtagsis its damage type.- Here
bonusadds the Strength modifier, and+1d4once the skill is rank 5. A rank-gated step inbonusis how “Rank 5: +1d4 base damage” gets into the numbers, and the matchingNoteis how it gets onto the page for the player. - The stat the damage uses (
@stats.str.mod) is separate from the stat the check is rolled with (stat:str). A shuriken is thrown with Dexterity and hurts with Strength.
A spell
A spell is the same shape with skill:spell, and costs mana. The cost and range are not rules, they are notes
in meta:
{
"id": "chefs-kiss",
"doc": {
"name": "Chef's Kiss",
"tags": ["skill", "skill:spell", "stat:int", "damage:fire", "skill:target:person"],
"rules": [
{ "kind": "Note", "text": "A flick of the fingers and something is, regrettably, on fire." },
{
"kind": "Note",
"text": "Rank 5: +1d6 base damage.",
"predicate": { "kind": "expr", "expr": "@source.rank >= 5" }
}
],
"rollDescriptors": [
{
"id": "damage",
"dice": [{ "count": 1, "faces": 6 }],
"bonus": "@stats.int.mod + (@source.rank >= 5 ? dice(1, 6) : 0)",
"tags": ["damage:fire"]
}
],
"meta": { "dccarl.mana-cost": "8", "dccarl.range": "30" }
}
}dccarl.mana-cost is the cost in mana, as text. dccarl.range is "self" or a distance in feet. A passive spell
carries skill:passive, and one you can cast on someone else’s turn carries skill:interrupt. To give a spell
a duration or a cooldown, use timers (below).
A utility skill
A skill that does not hit anything is just notes, gated by rank:
{
"id": "lucky-socks",
"doc": {
"name": "Lucky Socks",
"tags": ["skill", "skill:utility", "skill:passive", "stat:dex"],
"rules": [
{ "kind": "Note", "text": "You never lose a sock. Nobody knows how." },
{
"kind": "Note",
"text": "Rank 5: you can always find a clean pair when you need one.",
"predicate": { "kind": "expr", "expr": "@source.rank >= 5" }
}
]
}
}skill:utility puts it with the utility skills, and stat:dex is the stat its checks use. Drop the stat: tag for a skill
that is not rolled at all.
A buff
A buff is tagged buff:internal or buff:external, changes a number and says how long it lasts:
{
"id": "sugar-rush",
"doc": {
"name": "Sugar Rush",
"tags": ["buff:internal"],
"rules": [
{ "kind": "Note", "text": "Your Speed is 5 higher until the crash." },
{
"kind": "Modifier",
"modifier": {
"target": { "kind": "exact", "path": "movement.speed" },
"op": "Add",
"value": "5",
"label": "Sugar Rush"
}
}
],
"timers": [{ "key": "duration", "amount": 3, "unit": "rounds" }]
}
}Internal buffs start switched on. External buffs wait for the player and only three may be on at once. There is no
item:equipped predicate here: a buff applies whenever it is switched on.
A debuff
A debuff’s tag is debuff: plus a name of your own. The convention, which the forms follow, is to put your pack’s
identifier in the name so two packs cannot collide:
{
"id": "soggy-socks",
"doc": {
"name": "Soggy Socks",
"tags": ["debuff:crawler-samples.soggy-socks"],
"rules": [
{ "kind": "Note", "text": "-1 to every Check. Squelch." },
{
"kind": "Modifier",
"modifier": {
"target": { "kind": "exact", "path": "check.modifier" },
"op": "Add",
"value": "-1",
"label": "Soggy Socks",
"type": "status"
}
}
],
"timers": [{ "key": "duration", "text": "until you change your socks" }]
}
}The duration here is words rather than a number. When it can be counted down, give an amount and a unit
instead (see timers, below). Add "effect:stackable" to the tags if two of them should pile up.
A race
A race grants an origin, changes stats and can carry any other rules. Races are tagged race and race: plus
their id:
{
"id": "gutter-goblin",
"doc": {
"name": "Gutter Goblin",
"tags": ["race", "race:gutter-goblin"],
"rules": [
{ "kind": "GrantTag", "tag": "origin:alien" },
{ "kind": "Note", "text": "Small, quick and extremely hard to get rid of." },
{
"kind": "Modifier",
"modifier": {
"target": { "kind": "exact", "path": "stats.dex.score" },
"op": "Add",
"value": "2",
"label": "Gutter Goblin",
"type": "race"
}
},
{
"kind": "Modifier",
"modifier": {
"target": { "kind": "exact", "path": "stats.cha.score" },
"op": "Add",
"value": "-1",
"label": "Gutter Goblin",
"type": "race"
}
}
]
}
}The official races are built from Build Points, and the in-app builder keeps its choices in meta. A race written
by hand needs none of that. Open it in the builder later and each rule appears as a benefit (or a custom one), so nothing is lost.
A class
A class is tagged class and class: plus its id, grants a class-type: tag (the official types are arcanist,
barbarian, bard, cleric, druid, fighter, mage, merchant, monk, necromancer, paladin and rogue)
and, typically, grants skills. This Line Cook grants Chef’s Kiss, then raises its rank:
{
"id": "line-cook",
"doc": {
"name": "Line Cook",
"tags": ["class", "class:line-cook"],
"rules": [
{ "kind": "GrantTag", "tag": "class-type:merchant" },
{ "kind": "Note", "text": "You have survived the dinner rush. A dungeon is only a longer shift." },
{
"kind": "Modifier",
"modifier": {
"target": { "kind": "exact", "path": "stats.con.score" },
"op": "Add",
"value": "3",
"label": "Line Cook",
"type": "class",
"priority": 1
}
},
{
"kind": "GrantEntry",
"ref": { "repo": "crawler-samples", "id": "chefs-kiss" },
"bind": { "rank": "0" },
"retain": true
},
{
"kind": "Modifier",
"modifier": {
"target": { "kind": "exact", "path": "skill.chefs-kiss.rank" },
"op": "Add",
"value": "3",
"label": "Line Cook",
"type": "class",
"priority": 1
}
}
]
}
}Look at the two halves of the skill. The GrantEntry gives the crawler Chef’s Kiss at rank 0, and retain says that
if the class is ever changed, they keep it. The second Modifier adds three ranks to it while they are a Line Cook.
priority makes a class’s changes win over a race’s.
Charges, timers and requirements
These three fields sit in doc, beside rules.
Charges
A charge is a counter: arrows in a quiver, castings of a ring.
"charges": [
{ "key": "doses", "max": "3", "resetOn": [] }
]
keynames it, and is also what the sheet calls it. It is read in the entry’s rules as@source.doses.maxis an expression, so"@stats.int.mod"is a legal maximum and follows whoever holds the thing.resetOnlists the rests that refill it. This game has two:rest:safe-roomandencounter. An empty list, like the one above, is a consumable that nothing refills.
A note can react to the counter. This line appears only on a spent item:
{
"kind": "Note",
"text": "Empty.",
"predicate": { "kind": "expr", "expr": "@source.doses <= 0" }
}
Charges count uses of one thing. For how many of a thing you have, tag it trait:stackable and the item gets a
quantity.
Timers
A timer says how long an effect lasts, or how long before it can be used again.
"timers": [
{ "key": "duration", "amount": 3, "unit": "rounds" },
{ "key": "cooldown", "text": "once per floor" }
]
key is duration or cooldown. Give it an amount with a unit (seconds, rounds, minutes, hours or
days) and the sheet counts it down. Give it text and it is shown as words. An amount without a unit is
an error. Both together is allowed (“2 rounds or 20 seconds”): the count is for tracking and the words are for reading.
Requirements
A requirement says what a crawler must be before they can take the entry. Each has the words a player sees when they fall short, and a test written as an expression:
"requirements": [
{
"text": "Needs a Strength or Dexterity skill at rank 5",
"when": "maxOf(entries(tag('stat:str') || tag('stat:dex')), 'rank') >= 5"
}
]
entries(...) is the crawler’s own list of skills and other content, and 'rank' is the rank they trained it to, which is
not what their gear gives them. To test a stat, read @stats.str.base rather than the enhanced value, so a bonus
the entry itself grants can never qualify the crawler for it. A requirement the game cannot decide counts as unmet.
The official Hobgoblin race uses these; open it in the Compendium to see.
Versions and updates
Each reference to your content can carry a version: crawler-samples/ring-of-pep@1.0. The pack’s top-level version is stamped onto
every entry, and an entry may state its own version beside id if it has moved on without the rest.
A crawler carries a copy of what it picked up and remembers the version it came from. When you change an entry, change the version too. Crawlers carrying the old one are then offered the update under Updates in their menu. Change the content and leave the version alone, and the game calls it drifted: it knows the text no longer matches, but cannot tell anyone there is anything to apply.
{
"id": "ring-of-pep",
"version": "1.1",
"doc": { "name": "Ring of Pep" }
}
(The forms do this for you: every save in the app stamps a new version.)
Checking your work
There are three layers, from cheapest to most thorough. Use all of them.
- Your editor and the schema catch shape errors as you type: a misspelled field, a missing
label, a timer in an unknown unit. Red squiggles mean something is wrong before you ever save. - Check… in the app catches meaning errors: it reads the whole pack with the game’s own rules and reports what cannot work. Import the pack, open its ⋮ menu and choose Check…. Problems are bugs. “Worth saying” is a pack that works but is missing a licence or an author.
- Try it on a crawler. Add the gear, learn the skill, switch the buff on, and tap the ⓘ beside the number it should have changed. If the breakdown does not mention your entry, it is not doing anything.
The usual mistakes
| You wrote | What Check says | What to do |
|---|---|---|
"path": "stats.strr.score" | 'ring-of-pep' rule 2 ('Ring of Pep') targets unknown attribute 'stats.strr.score' | Fix the spelling. Paths are in the table above. |
"tag": "resist:water", or "house:banned" in tags | declares undeclared tag 'resist:water' | Use a declared tag, or one that starts with an open prefix. |
"when": {…} on a note | 'entries[2].doc.rules[1].when' is not a field anything reads | The field is predicate. |
A GrantEntry to an entry nobody has | grants 'dcc-core/nope', which nothing here publishes | Fix the repo and id, or publish the target. |
Two entries with the same id | compendium 'x' declares 'ring-of-pep' more than once | Give each its own. |
A kind that is not a rule kind ("Shout") | The file cannot be read at all, and Check names the position. | Use Note, Modifier, GrantTag or GrantEntry. |
"repo": "My Pack" | repo 'My Pack' is not a namespace… | Lowercase letters, digits, - and . only. |
Two others are silent, and only testing finds them. A bonus aimed at .base instead of .score applies, but where a
player cannot see it. A gear rule without the item:equipped predicate applies from a pocket.
Getting your pack into the app
- Save the file with a name that ends in
.comp.json. - Get it onto the device: AirDrop it, put it in a cloud drive, email it to yourself or copy it across with a cable.
- In the app, open the Homebrew tab, tap the Import a pack icon and choose the file.
- If a pack with the same
repois already there, the app asks whether to Replace it or Keep both. Replace is what you want when you are working on a pack and have re-saved it. - Open the pack, run Check… and fix whatever it reports.
Importing does not check every entry for you, so run Check. If an entry turns out to be wrong, you can edit it in the app without leaving, Edit as text… puts you back in JSON.
Writing in both places
The app and your editor are working on the same file, so nothing stops you using both. The usual loop is:
- Make the pack in the app and add an entry or two with the forms.
- Export… it, and open the file in your editor.
- Write twenty more entries by copying the first one.
- Import it again, and choose Replace. Crawlers carrying the old versions are offered the updates.
The complete sample pack
Here is the whole sample pack, the same file as the download.
crawler-samples.comp.json
{
"repo": "crawler-samples",
"name": "Crawler Samples",
"source": "Crawler Vault help: Homebrew by hand",
"version": "1.0",
"description": "One of each kind of entry, written by hand. Copy it, rename it, break it.",
"authors": ["Crawler Vault"],
"license": "CC0-1.0",
"system": "dccarl",
"entries": [
{
"id": "ring-of-pep",
"doc": {
"name": "Ring of Pep",
"tags": ["trait:accessory", "trait:magical"],
"rules": [
{ "kind": "Note", "text": "While worn, your Strength is 2 higher." },
{
"kind": "Modifier",
"modifier": {
"target": { "kind": "exact", "path": "stats.str.score" },
"op": "Add",
"value": "2",
"label": "Ring of Pep",
"type": "item",
"predicate": { "kind": "tag", "tag": "item:equipped" }
}
}
]
}
},
{
"id": "oven-mitts",
"doc": {
"name": "Oven Mitts of Mild Immolation",
"tags": ["trait:hands", "trait:magical"],
"rules": [
{ "kind": "Note", "text": "Fire does half damage to you while these are on. Your hands, anyway." },
{
"kind": "Modifier",
"modifier": {
"target": { "kind": "exact", "path": "defense.armor" },
"op": "Add",
"value": "1",
"label": "Oven Mitts of Mild Immolation",
"type": "item",
"predicate": { "kind": "tag", "tag": "item:equipped" }
}
},
{
"kind": "GrantTag",
"tag": "resist:fire",
"predicate": { "kind": "tag", "tag": "item:equipped" }
}
]
}
},
{
"id": "sock-slap",
"doc": {
"name": "Sock Slap",
"tags": [
"skill",
"skill:attack",
"skill:attack:melee",
"attack:melee",
"skill:weapon",
"damage:bludgeoning",
"stat:str"
],
"rules": [
{ "kind": "Note", "text": "Melee Attack, Str. Put something heavy in a sock. Swing the sock." },
{
"kind": "Note",
"text": "Rank 5: +1d4 base damage.",
"predicate": { "kind": "expr", "expr": "@source.rank >= 5" }
}
],
"rollDescriptors": [
{
"id": "damage",
"dice": [{ "count": 1, "faces": 4 }],
"bonus": "@stats.str.mod + (@source.rank >= 5 ? dice(1, 4) : 0)",
"tags": ["damage:bludgeoning"]
}
]
}
},
{
"id": "chefs-kiss",
"doc": {
"name": "Chef's Kiss",
"tags": ["skill", "skill:spell", "stat:int", "damage:fire", "skill:target:person"],
"rules": [
{ "kind": "Note", "text": "A flick of the fingers and something is, regrettably, on fire." },
{
"kind": "Note",
"text": "Rank 5: +1d6 base damage.",
"predicate": { "kind": "expr", "expr": "@source.rank >= 5" }
}
],
"rollDescriptors": [
{
"id": "damage",
"dice": [{ "count": 1, "faces": 6 }],
"bonus": "@stats.int.mod + (@source.rank >= 5 ? dice(1, 6) : 0)",
"tags": ["damage:fire"]
}
],
"meta": { "dccarl.mana-cost": "8", "dccarl.range": "30" }
}
},
{
"id": "lucky-socks",
"doc": {
"name": "Lucky Socks",
"tags": ["skill", "skill:utility", "skill:passive", "stat:dex"],
"rules": [
{ "kind": "Note", "text": "You never lose a sock. Nobody knows how." },
{
"kind": "Note",
"text": "Rank 5: you can always find a clean pair when you need one.",
"predicate": { "kind": "expr", "expr": "@source.rank >= 5" }
}
]
}
},
{
"id": "sugar-rush",
"doc": {
"name": "Sugar Rush",
"tags": ["buff:internal"],
"rules": [
{ "kind": "Note", "text": "Your Speed is 5 higher until the crash." },
{
"kind": "Modifier",
"modifier": {
"target": { "kind": "exact", "path": "movement.speed" },
"op": "Add",
"value": "5",
"label": "Sugar Rush"
}
}
],
"timers": [{ "key": "duration", "amount": 3, "unit": "rounds" }]
}
},
{
"id": "soggy-socks",
"doc": {
"name": "Soggy Socks",
"tags": ["debuff:crawler-samples.soggy-socks"],
"rules": [
{ "kind": "Note", "text": "-1 to every Check. Squelch." },
{
"kind": "Modifier",
"modifier": {
"target": { "kind": "exact", "path": "check.modifier" },
"op": "Add",
"value": "-1",
"label": "Soggy Socks",
"type": "status"
}
}
],
"timers": [{ "key": "duration", "text": "until you change your socks" }]
}
},
{
"id": "gutter-goblin",
"doc": {
"name": "Gutter Goblin",
"tags": ["race", "race:gutter-goblin"],
"rules": [
{ "kind": "GrantTag", "tag": "origin:alien" },
{ "kind": "Note", "text": "Small, quick and extremely hard to get rid of." },
{
"kind": "Modifier",
"modifier": {
"target": { "kind": "exact", "path": "stats.dex.score" },
"op": "Add",
"value": "2",
"label": "Gutter Goblin",
"type": "race"
}
},
{
"kind": "Modifier",
"modifier": {
"target": { "kind": "exact", "path": "stats.cha.score" },
"op": "Add",
"value": "-1",
"label": "Gutter Goblin",
"type": "race"
}
}
]
}
},
{
"id": "line-cook",
"doc": {
"name": "Line Cook",
"tags": ["class", "class:line-cook"],
"rules": [
{ "kind": "GrantTag", "tag": "class-type:merchant" },
{ "kind": "Note", "text": "You have survived the dinner rush. A dungeon is only a longer shift." },
{
"kind": "Modifier",
"modifier": {
"target": { "kind": "exact", "path": "stats.con.score" },
"op": "Add",
"value": "3",
"label": "Line Cook",
"type": "class",
"priority": 1
}
},
{
"kind": "GrantEntry",
"ref": { "repo": "crawler-samples", "id": "chefs-kiss" },
"bind": { "rank": "0" },
"retain": true
},
{
"kind": "Modifier",
"modifier": {
"target": { "kind": "exact", "path": "skill.chefs-kiss.rank" },
"op": "Add",
"value": "3",
"label": "Line Cook",
"type": "class",
"priority": 1
}
}
]
}
}
]
}
What is not covered here
This page is about content: the things that go into a pack. It does not cover writing a new system definition, which is what decides the tags, stats and paths above. The official books also use some rules this page does not describe (choice sets, granted entities and per-entry state). The best source of examples is the Compendium in the app, which shows the real thing.