← Back to the gamebug or idea?Terrapin Trail//Code BibleIIhow the game works, archive to van
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:

●assets/game/game.js : 1033–1041trace ↗
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); } },
MODS: each is display text plus an apply(s) that mutates stats or sets a boolean flag. applyMods() re-derives them from baseStats every toggle, which is why a mod must never spend cash.

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:

●assets/game/game.js : 1051–1060trace ↗
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; } },
VAN_PARTS: each part's buff(b) adds to an accumulator that recomputeBuffs() folds into s.buff and the shared CAP.

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/check that rolls dice must use the rnd/chance/pick/shuffle helpers, never Math.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.