There’s one design principle that runs through every system in Defense of Kyrath: if a value exists, it lives in data, not in code.
Warden stats, enemy behaviors, wave compositions, level configs, talent effects, particle effects, debuffs, challenge modifiers — all dictionaries, all resolved at runtime through registries. The game has 10 singleton registries, 100+ JSON config files, and hundreds of entity definitions. Zero hardcoded gameplay values.
This isn’t a novel idea — data-driven design goes back to id Software’s Quake defining entities in text files. What’s surprising is how far you can push it in a small project, and what falls out when you do.
The Newest Registry: Particle Effects
The particle system is a recent example.
Godot’s typical particle workflow means building nodes in the scene editor, tweaking values visually, saving as scene files. That works for a handful of effects. It doesn’t scale when the same fireball trail appears on multiple warden projectiles, effects need to spawn dynamically at runtime, and you want to add new effects without touching game logic.
Instead, every particle effect is a plain dictionary — emission shape, velocity, color gradient, gravity, lifetime. A fireball trail is 14 key-value pairs. A poison wisp that drifts upward is the same template with negative gravity and a green palette. Adding a frost impact for the Frostweaver? Twelve lines of dictionary, no other file changes. The system also supports overrides — take any registered effect, tweak a few properties at spawn time, get a variant without registering a new definition.
The Same Pattern, Everywhere
The particle registry is one of ten. Every major game system follows the same contract: register definitions at startup, look them up by string ID at runtime.
Wardens are the deepest example: ~60 parameters per definition covering combat, economy, projectile behavior, visual geometry, animations, audio, and progression curves. Each warden’s projectile_trail field is a ParticleRegistry ID — the warden doesn’t know what a fireball looks like, it just says “attach fireball_trail to my projectile” and the particle registry handles the rest. Systems reference each other by ID, never by implementation.
Enemies use the same pattern, plus a variant system: clone any enemy definition, override a few stats, and you get an elite version without a new class.
Waves are tree-structured compositions. A wave template can nest sub-waves, delays, and repeats into hierarchical groups. At runtime, the tree compiles down to a flat timeline of spawns with pre-computed timestamps.
Levels use a multi-layer config chain: base map → level definition → difficulty multiplier. A hard-mode variant of any level is a 5-line JSON override file. 17 base levels × 3 difficulties = 51 unique configurations from a compact set of configs.
Talents are stat multipliers keyed by string — all_warden_damage_mult, warden_range_mult:sniper, gold_earn_mult. The talent system doesn’t know what wardens exist. The warden system doesn’t know what talents exist. They communicate through loosely coupled string keys resolved at runtime.
Debuffs get their own registry because both sides of the battlefield use them. The DebuffRegistry defines slow, poison, webbed, weakened — each with duration, visual indicator, and display metadata. Wardens apply debuffs to enemies; enemies apply debuffs to wardens. Same definitions, same lookup, symmetric combat.
Edicts are challenge modifiers — toggleable rules that scale difficulty or grant boons. The EdictRegistry defines stat multipliers, behavioral flags (fog of war, silenced wardens, iron decree), and display metadata. At level load, the edict system applies all active modifiers centrally. The level doesn’t know which edicts are active; it just plays with whatever stats it’s given.
When Both Sides Share the Same Code
An unplanned but welcome consequence: wardens and enemies ended up using the same projectile implementation.
A single projectile class handles both directions of combat. When a Pyre warden fires a fireball at an enemy, the projectile carries damage, a damage type, and a ParticleRegistry trail ID. When a Hex Crone enemy fires a homing bolt at a warden, the projectile carries a DebuffRegistry ID and a different trail. Same class, same flight code.
Early on, only wardens fired projectiles. But because projectile behavior was already data-driven, extending the system to enemy attacks required no new projectile code. A Bog Matron boss that fires webbing projectiles to slow your wardens is six dictionary entries and a DebuffRegistry ID.
Data-driven design paying compound interest. The upfront investment in registries makes each subsequent system extension cheaper, because the extension is just more data.
What This Unlocks
Velocity. A new warden is a dictionary. A new enemy is a dictionary. A new particle effect is a dictionary. A new difficulty variant is 5 lines of JSON. None of these require touching game logic. This has played well with AI-assisted development — describe a new warden in plain language, get back a working definition.
Composability. Definitions are dictionaries. Dictionaries merge. The enemy variant system, the level config chain, and the edict stat resolver are all the same idea: take a base definition, layer modifications on top, get a new entity.
Debuggability. When something behaves wrong, you inspect the definition. “Why does this warden fire so slowly?” Check its definition in the registry. The data is the source of truth.
The tradeoff is indirection — string-keyed lookups trade compile-time safety for runtime flexibility. For a project with 10 registries and hundreds of definitions, it’s been worth it.
None of this is particularly clever. It’s straightforward infrastructure. But that’s sort of the point — the boring patterns are the ones that keep working quietly in the background while you focus on the interesting problems.