Placeholders Overview

Placeholders are dynamic references to towns, dungeons, monsters, and NPCs. They let one campaign JSON file work for every party without you hardcoding names, and they keep the narration immersive instead of generic.

Why use placeholders? Campaign Cookbook generates a small roster of named towns (with shops and shopkeeper NPCs) for each party the first time it needs one - seeded from that party's own campaign seed, so it's reproducible but different per party. NPC placeholders ({nobleNPC}, etc.) work the same way. Dungeon flavor-text placeholders are the one exception - they only resolve to something specific if you've hand-authored an optional data/maps/locations.json file (see its README); otherwise they degrade to generic text. Either way, your campaign JSON never needs to hardcode a name.

How Placeholders Work

When Campaign Cookbook loads a campaign, it:

  1. Generates (or loads a previously-generated) roster of named towns and NPCs for this specific party+campaign, seeded from that party's campaign seed
  2. Requests dungeon flavor-text data via window.electronAPI.getMapData('dungeons'), which reads the optional data/maps/locations.json - there's no generation fallback for these
  3. Resolves each simple placeholder ({nearestBurg}, {capitalCity}, etc.) once and caches the result
  4. Resolves parameterized placeholders ({randomBurg:minPopulation=500}) on demand, then caches that specific parameter combination
  5. Falls back to fixed generic text for anything that couldn't be resolved - see below

Example

You write:

"The merchant was last seen in {nearestBurg}."

DM sees (the normal case - a town has been generated for this party):

"The merchant was last seen in Millhaven."

DM sees (generation unavailable for some reason, and no locations.json either):

"The merchant was last seen in the nearest settlement."

Available Placeholders

Settlement Placeholders

Placeholder Description Parameters
{nearestBurg} Closest settlement to starting location None
{randomBurg} Random settlement from the party's generated roster minPopulation, maxPopulation (sameState/withinRange accepted but non-functional - see Parameter Syntax)
{capitalCity} Capital city (highest population) None
{targetBurg} A second random settlement, distinct from {randomBurg}'s pick, resolved and cached once per campaign load None

Dungeon Placeholders

Placeholder Description Parameters
{nearestDungeon} Closest dungeon to starting location type, difficulty
{randomDungeon} Random dungeon flavor-text entry (not a real, playable dungeon - see {nearestDungeon}) type, difficulty (sameState accepted but non-functional)
{targetDungeon} A second random dungeon, distinct from {randomDungeon}'s pick, resolved and cached once per campaign load None
{dungeon:id} Not flavor text - opens a real, campaign-authored dungeons[] entry (fog-of-war rooms, seeded loot/encounters, Initiative Tracker hookup). See Campaign Structure for how to author one. None (the id itself selects the location)

Content Placeholders

Placeholder Description Parameters
{monster:Name} Monster stat block from the bundled bestiary, looked up by name count, variant (pipe-separated after the name, not comma-separated)

NPC Placeholders

These resolve to a real generated D&D 5e NPC - a name, plus a full clickable stat block (ability scores, class/background flavor, appropriate gear) the DM can pull up on demand. Each one is generated once per campaign session and cached, so the same noble is referenced consistently everywhere they're mentioned. They take no parameters:

Placeholder Archetype
{nobleNPC} Noble
{merchantNPC} Merchant
{cultistNPC} Cultist (religious archetype)
{priestNPC} Priest (religious archetype)
Example: "{nobleNPC}, a scholar of ancient history in {targetBurg}, has discovered disturbing information..." renders as something like "Lord Aldric Vane, a scholar of ancient history in Millhaven, has discovered disturbing information..." - click the name in the reading pane to see their full stat block. Falls back to generic text ("the local noble," etc.) only if the NPC generator script didn't load for some reason.

Numbered Tags: Multiple Distinct People

A bare {nobleNPC} always resolves to the same generated person everywhere it appears in your campaign - the same noble met in episode 1 is still that exact person if referenced again in episode 4. That's deliberate: a quest-giver or recurring contact the party returns to should stay one consistent identity. But not every noble in a 6-episode campaign is the same person - a numbered suffix creates a genuinely different, independently generated identity:

{nobleNPC} // identity #1 - e.g. the Duke who sends the party out in Episode 1 {nobleNPC_2} // identity #2 - a completely different noble, e.g. a rival lord met in Episode 3 {nobleNPC_3} // identity #3 - a third, distinct noble

Works the same way for all four archetypes ({merchantNPC_2}, {cultistNPC_2}, {priestNPC_2}, ...) - there's no fixed cap on how many you can use, and a campaign only ever generates the identities its own text actually references (no wasted generation for archetypes you never use).

The authoring rule: default to a different person (introduce a new number) unless your text gives a real reason to think it's the same one returning. If in doubt, a new number is almost always the safer, more narratively honest choice - reusing an identity implies real continuity ("this is the noble you already met"), which players will notice if it's wrong.

There's also an open-ended form, {randomNPC_<profession>} (e.g. {randomNPC_archmage}, {randomNPC_scholar}) - unlike the four fixed archetypes above, this one is not generated; it always resolves to plain generic text ("the archmage," "the scholar"). Use the four fixed archetypes when you want a real, clickable stat block; use randomNPC_ only when you just need a role mentioned in prose.

Reserved Location Placeholders

These four always resolve to the same fixed generic phrase - there's no generation or lookup behind them yet, they exist as stable slots for quest-hub location text seen across several bundled campaigns' questGiver/location fields. They take no parameters:

Placeholder Always resolves to
{targetLocation} "the target location"
{nearestTemple} "a nearby temple"
{randomRuin} "ancient ruins in the region"
{randomTower} "an old tower in the region"

Simple vs Parameterized Placeholders

Simple Placeholders

No parameters. Just wrap the placeholder name in curly braces:

{nearestBurg}
{capitalCity}
{nearestDungeon}

Parameterized Placeholders

Include parameters after a colon to filter location selection:

{randomBurg:minPopulation=500}
{randomBurg:minPopulation=500,maxPopulation=2000}
{nearestDungeon:type=Cave,difficulty=hard}
Syntax Rules:
  • Parameters follow the placeholder name after a colon :
  • Multiple parameters separated by commas ,
  • No spaces around = or ,
  • Boolean values: true or false
  • Numbers: 50, 1000
  • Strings: Cave, hard

Common Parameters

sameState and withinRange are not currently functional. Both were written for the sibling product's live map (which tracks state/province and real map-distance data); Campaign Cookbook's generated towns and hand-authored locations.json format don't supply that data, so these are silently ignored rather than filtering anything. See Parameter Syntax for the full explanation. minPopulation/ maxPopulation, below, are the ones that actually work.

minPopulation / maxPopulation

Filters settlements by population size.

{randomBurg:minPopulation=1000} // Large city
{randomBurg:maxPopulation=300} // Small village
{randomBurg:minPopulation=500,maxPopulation=2000} // Medium town

Use case: Match settlement size to story needs (village vs city).

type (Dungeons)

Filters dungeons by type.

{nearestDungeon:type=Cave}
{nearestDungeon:type=Ancient-Crypt}
{randomDungeon:type=Sewer}

Valid Types: Cave, Underground-Ruins, Sewer, Ancient-Crypt, Monster-Lair, Ruin

Use case: Match dungeon aesthetics to quest theme (crypts for undead, caves for bandits).

difficulty (Dungeons)

Filters dungeons by difficulty level.

{nearestDungeon:difficulty=easy}
{nearestDungeon:difficulty=hard}
{randomDungeon:difficulty=deadly}

Valid Difficulties: easy, medium, hard, deadly

Use case: Match dungeon challenge to party level.

Consistency & Caching

Each unique placeholder + parameters combination is resolved once and cached for the entire campaign.

Caching Behavior

Placeholder First Use Second Use
{randomBurg:maxPopulation=300} Resolves to "Millhaven" Returns "Millhaven" (cached)
{randomBurg:minPopulation=1000} Resolves to "Eastport" Returns "Eastport" (cached)

Different parameters = different cached value

Why is this good? Caching ensures location names stay consistent throughout your campaign. If Scene 2 mentions "Millhaven", Scene 10 will also reference "Millhaven" - not a different random town.

Fallback Behavior

Three distinct situations produce fallback text, and it's worth telling them apart:

Town/NPC generation didn't run for some reason (rare - this only happens if the generator script failed to load). Every location and NPC placeholder degrades to specific generic text, verified exactly against useFallbackPlaceholders():

No hand-authored data/maps/locations.json (the common, out-of-the-box case - dungeon flavor text has no generation fallback the way towns do):

A parameterized placeholder's filters match nothing. Only that specific placeholder falls back, to a shorter generic phrase:

Important: If you see fallback text for a location placeholder, it usually means your filters are too strict - try relaxing minPopulation/maxPopulation. If you see it for {nearestDungeon}/{randomDungeon}, it likely just means you haven't authored a locations.json yet.

Usage Examples

Basic Settlement Reference

{ "readAloud": "<div class='read-aloud'><p>Welcome to {nearestBurg}, a bustling settlement.</p></div>" }

Local Travel

{ "readAloud": "<div class='read-aloud'><p>The road leads from {nearestBurg} to {randomBurg:maxPopulation=300}.</p></div>" }

Filtered Destination

{ "readAloud": "<div class='read-aloud'><p>You must travel to {randomBurg:minPopulation=1000}, a large city.</p></div>" }

Dungeon Reference

{ "readAloud": "<div class='read-aloud'><p>The cultists hide in {nearestDungeon:type=Ancient-Crypt,difficulty=hard}.</p></div>" }

Best Practices

Always Use Placeholders for Locations

✓ Good: "Travel to {nearestBurg}"
✗ Bad: "Travel to Millhaven"

A hardcoded name assumes every party's generated roster happens to include a town by that exact name - it won't.

Keep Adventures Small Early On

✓ Good: {randomBurg:maxPopulation=300}
✗ Risk: {randomBurg} // Could resolve to a major city

See Scope Control for the full strategy.

Match Population to Context

✓ Good: "A small village: {randomBurg:maxPopulation=200}"
✗ Bad: "A small village: {randomBurg}" // Might select a city

Match Dungeon Type to Story

✓ Good: "The undead lair: {nearestDungeon:type=Ancient-Crypt}"
✗ Bad: "The undead lair: {nearestDungeon}" // Might select a cave

Next Steps