Louis built a turn-based combat engine from scratch at a startup, and the problem that nearly broke it wasn’t rendering or netcode — it was designer velocity. Abilities were being tweaked several times per playtest, and hardcoded implementations meant the designer waited on him to write and test every change.
So he flipped the question: instead of “what does character #123 do?”, ask “what are abilities made of?” The answer was three primitives:
- Effects — the thing that happens (deal damage, apply a status condition, heal), as real classes
- Value evaluators — where numbers come from (“150% of the owner’s attack”)
- Condition evaluators — the dynamic part (“is the target below half health?”, “did this crit?”)
An ability became an ordered list of if this condition, do this action with this value, stored in MongoDB, with status conditions as table rows rather than classes. The state machine shrank to one job: walk the list and execute it. The designer started authoring and balancing abilities himself, no engineer in the loop.
Then the engine half of the story went sideways:
- The engine was one big state machine with smaller machines nested inside — one per effect, per ability, per turn phase.
- Straightforward damage flowed fine. Counters, reactions, interrupts and death-triggered effects broke the boundaries, and two machines would fight over the same transition.
- An event-bus/pub-sub rewrite failed on ordering: a plain event bus promises nothing about what resolves first, and cascades folded back on themselves until battles soft-locked mid-resolution.
- He stabilised it by making each effect its own sub-state-machine, with hard limits on trigger nesting. It handled the edge cases; it was also “a messy stack of heavily guarded, overlapping state machines that only I fully understood.”
The line worth keeping: flexibility breeds complexity, and complexity breeds bugs. Moving abilities into data was right — the cost landed in control flow, not content. What he’d build today is the small fix that a plain event bus never gave him: an event-driven state machine where events queue state changes and the machine only advances once the queue drains. That buys ordering and a clear “everything’s settled” moment to inspect from, which is most of what debugging ability systems actually requires.