Reel 04 · 6 min read
Data-driven design
Content lives in catalogs; the engine reads them generically. Adding content is adding a data entry.
The top of game.js is a stack of catalogs — plain arrays and objects holding all the game's content. The engine reads them generically, so most "add a feature" tasks are "add an entry."
The catalogs
| Catalog | What it holds |
|---|---|
ROLES |
playable characters + starting stats + taping/dose traits |
TOURS |
the 10 tour years, each a list of real STOPS |
VANMATES |
the crew roster: archetype id, name variants, trait, perk |
LOT_ITEMS |
Shakedown vendors (apply(s) returns a line) |
HUSTLES |
work-the-lot money-makers (need(s), run(s)) |
EVENTS |
road events: title, text, choices[]; optional needs gate |
MODS |
builder perks (set stats/flags, never cash) |
VAN_PARTS |
installable upgrades; each contributes to a buff object |
CONSUMABLES |
stash items (use(s) or auto-trigger) |
PEAKS |
per-role once-per-run surge |
ACHIEVEMENTS |
badges (check(ctx)) |
The generic-consumption pattern
Each catalog entry carries a tiny behavior closure with a fixed contract, and the engine calls it without knowing the specifics. A role seeds stats; a mod sets flags:
1033 var MODS = [ 1034 { id: "iron_stomach", name: "Iron Stomach", desc: "Bad clams, cursed burritos -- nothing touches you.", apply: function (s) { s.flags.ironStomach = true; } }, 1035 { id: "silver_tongue", name: "Silver Tongue", desc: "You talk your way in anywhere. +12 starting Lot Cred.", apply: function (s) { s.cred = clamp(s.cred + 12, 0, 100); } }, 1036 { id: "lead_foot", name: "Lead Foot", desc: "You drive like you're being chased. Legs go faster; the van pays for it.", apply: function (s) { s.flags.leadFoot = true; } }, 1037 { id: "night_owl", name: "Night Owl", desc: "Your best thinking happens at 3 a.m. Your Peak hits even harder.", apply: function (s) { s.flags.nightOwl = true; } }, 1038 { id: "golden_ears", name: "Golden Ears", desc: "You hear the whole mix. +8 quality on every tape you pull.", apply: function (s) { s.flags.goldenEars = true; } }, 1039 { id: "pack_rat", name: "Pack Rat", desc: "You start with a random extra piece of van gear in your stash.", apply: function (s) { s.flags.packRat = true; } }, 1040 { id: "been_there", name: "Been There", desc: "Whatever's in the snack, you've had worse. Immune to bad trips.", apply: function (s) { s.doseImmune = true; } }, 1041 { id: "grease_monkey", name: "Grease Monkey", desc: "You keep the bus alive with spit and wire. Van starts +15.", apply: function (s) { s.van = clamp(s.van + 15, 0, 100); } },
Van parts follow the same idea, but their closure contributes to a shared buff accumulator rather than mutating state directly — the economy chapter covers why:
1051 var VAN_PARTS = [ 1052 { id: "big_tank", slot: "tank", name: "Aux Fuel Tank", icon: "⛽", desc: "+8 max Gas.", price: 16, buff: function (b) { b.gasCap += 8; } }, 1053 { id: "roof_rack", slot: "rack", name: "Roof-Rack Larder", icon: "📦", desc: "+12 max Grub.", price: 12, buff: function (b) { b.grubCap += 12; } }, 1054 { id: "hd_shocks", slot: "tires", name: "Heavy-Duty Shocks", icon: "🛞", desc: "Van wears 3 less every leg.", price: 18, buff: function (b) { b.wearReduce += 3; } }, 1055 { id: "rebuilt_engine", slot: "engine", name: "Rebuilt Engine", icon: "🔧", desc: "Van wears 4 less; climbs the long grades clean.", price: 26, buff: function (b) { b.wearReduce += 4; } }, 1056 { id: "pa_rig", slot: "sound", name: "In-Van PA Rig", icon: "🔊", desc: "Every show gives +4 extra Vibes.", price: 20, buff: function (b) { b.showVibes += 4; } }, 1057 { id: "stealth_paint", slot: "paint", name: "Matte \"Nobody Here\" Paint", icon: "🎨", desc: "Security never looks twice. Taping is far safer.", price: 22, buff: function (b) { b.bustGuard += 6; } }, 1058 { id: "nakamichi", slot: "deck", name: "Nakamichi Dragon Deck", icon: "🎛", desc: "+10 quality on every tape.", price: 30, buff: function (b) { b.tapeQuality += 10; } }, 1059 { id: "disco_ball", slot: "charm", name: "Dashboard Disco Ball", icon: "🪩", desc: "Purely cosmetic. +3 Vibes at every show anyway.", price: 8, buff: function (b) { b.showVibes += 3; } }, 1060 { id: "cb_radio", slot: "radio", name: "CB Radio", icon: "📻", desc: "You hear about the smokeys ahead. Fewer nasty road surprises.", price: 14, buff: function (b) { b.roadLuck += 1; } },
Why this matters
Three consequences fall out of the pattern, all of them worth internalizing before you touch the code:
- Uniqueness is enforced by a test. Every catalog's
ids must be unique — a test asserts it, so you can trust ids as stable keys. - Randomness must be seedable. Any
apply/run/checkthat rolls dice must use thernd/chance/pick/shufflehelpers, neverMath.random, or seeded playthroughs diverge (see testing). - Prefer a data entry over new code. A new event, item, van-mate, or achievement is almost always an array entry the existing systems already understand. Reach for new control flow only when the contract genuinely doesn't fit.
Key takeaways
- Content is catalogs; behavior is a small closure per entry with a fixed contract (
apply/run/use/buff/check). - The engine consumes them generically, so adding content rarely means adding control flow.
- Ids are unique (test-enforced), and any randomness inside an entry must be seedable.