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.

Ashfield Inferno — multiple wardens firing simultaneously with fire, ice, and gold sparkle particle effects visible
Ashfield Inferno: Bombard fire impacts, Frostweaver ice sparkles, Sylvan gold trails — every effect spawned from the same particle registry with different dictionary definitions.

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.

Bombard tower firing with explosion impact on a Deep Forest path, enemies approaching
A Bombard warden fires on incoming enemies. The explosion, the trail, and the impact burst are all ParticleRegistry definitions — the Bombard’s config just references them by ID.

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.

Frostweaver ice effects and fire-engulfed enemies on a dual-biome level
Frostweaver ice sparkles and fire impacts on the same battlefield. Two different warden types, two different particle definitions, same registry system.

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.

Fire tower with fireball projectiles hitting an enemy column on volcanic terrain
Volcanic Badlands: a fire warden’s reach circle, fireball projectiles mid-flight, and an enemy column taking hits. Every visual element here — the fire trail, the impact burst, the reach circle glow — is a registry entry.

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

Multiple warden types deployed on volcanic terrain with green poison projectile and fire effects
Eight warden types, each defined as a dictionary. Deadeye tower (bottom-left), Frostweavers, Bombard, Stinger with poison projectile — all instantiated from registry definitions, no warden-specific code.

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.