Why Combat Logic Must Be Centralized in a Tactical RPG

Combat systems often feel simple right up until they do not.
Early on, an attack is just a few steps. Pick a target. Play an animation. Subtract health. If health hits zero, remove the unit. That shape is easy to ship, and for a while it feels perfectly reasonable.
Then the game grows a little. One attack hits multiple targets. Another applies a status. A third one has special damage rules. You add armor, crits, resistances, reactions, or some kind of on-hit effect. Suddenly attack logic is not one thing anymore. It is a rule domain.
That is where a lot of tactics code starts drifting apart.
One action script calculates damage. A projectile decides who got hit. A unit script mutates its own health. Somewhere else, another system notices the target died and tries to react. The animation timing starts implying rule timing. The UI starts showing outcome hints based on its own partial version of the same logic. Nothing looks obviously broken at first, but the system stops having a single place where combat meaning is decided.
That is the real problem.
The issue is not just duplication. It is that combat stops having an authority.
Once that happens, every new combat mechanic becomes more expensive than it should be. Universal features stop feeling universal. Crit chance has to be threaded through multiple action types. Resistance rules land in some attacks but not others. A melee strike and a ranged hit that should behave the same begin producing slightly different results because they are not actually going through the same interpretation step.
This is the point where combat bugs start getting annoying in a very specific way. Not dramatic crashes. Just slow rule drift.
An enemy dies to Fireball but not to a bow shot with the same final damage. A splash attack applies a status correctly, but a direct strike forgets to. One action triggers defeat evaluation at the right moment, another only gets there because some later cleanup pass happened to catch it.
Those bugs are painful because they are telling you the same thing: combat meaning is being assembled from too many places.
What helped here was drawing a harder line between actions and combat.
An action should describe combat intent.
A combat layer should interpret that intent.
That sounds small, but it changes the whole shape of the codebase.
Action -> build combat request -> combat resolver -> combat result -> runtime applies outcome
With that boundary in place, actions stop deciding what combat means. They decide what is being attempted, who is acting, which target was chosen, and what base parameters are being passed in.
The combat layer then takes over the part that actually needs to stay consistent:
- who is affected
- how damage is interpreted
- what secondary effects are produced
- which units die
- what structured result the rest of the battle should react to
That split matters because actions and combat are not the same responsibility.
An action is about behavior.
Combat is about consequence.
If those stay fused together, every attack-capable script becomes its own little combat engine.
A small example shows the difference pretty clearly.
The fragile version usually looks something like this:
func execute_attack(target: Unit) -> void:
target.health -= damage_amount
if target.health <= 0:
target.die()
That is fine for a prototype. It is not fine for a growing system, because now the action owns damage interpretation, defeat logic, and timing assumptions all at once.
The stronger version pushes combat through one place:
var request := ActionRequest.new()
request.attacker = unit
request.target = target
request.damage_amount = damage_amount
var result := CombatResolver.resolve(request)
CombatResolver.apply_result(result)
That buys you more than tidier code.
It gives combat rules a real home. New mechanics have somewhere to go. Results can be logged and tested as structured output instead of guessed from scattered side effects. The rest of the runtime can react to a normalized result instead of trying to infer what just happened from half a dozen unrelated scripts.
It also makes debugging less miserable.
When combat resolution is centralized, you can ask one useful question: "What result did the combat layer produce?" When combat resolution is scattered, debugging turns into archaeology. You are tracing damage through actions, units, VFX timing, status handlers, and post-hit callbacks, trying to figure out where combat was really interpreted.
And this is worth doing earlier than people usually think.
You do not need a giant combat model before you centralize. In fact, it is better to centralize while the rules are still simple. That way crits, armor, resistances, multi-hit effects, chained kills, and triggered events grow into an existing boundary instead of forcing a rewrite later.
If you let every action interpret combat on its own, you are not building one combat system. You are building several smaller ones and hoping they keep agreeing.
Actions should describe combat intent.
The combat layer should decide what that intent means.
If you want the bigger picture, I go deeper into how this combat boundary connects to action execution, battle state updates, and long-term framework growth in the full tactical RPG architecture course.

