Lua for Game Development — Chapter 5: Combat Systems, Hitboxes, Stats, Buffs, and Abilities

A complete design and implementation of production-grade combat systems in Lua: stats, buffs, hitboxes, damage pipelines, cooldowns, abilities, and logs.

Combat systems are the backbone of many games:
RPGs, action games, shooters, platformers, MOBAs, and roguelikes.

This chapter explains how to implement real combat architecture in Lua:

  1. Entity stats & modifiers
  2. Damage pipeline
  3. Hitboxes & hurtboxes
  4. Buffs, debuffs, DOTs, HOTs
  5. Cooldowns & cast times
  6. Skill/ability system
  7. Combat logs & replay
  8. Network-sync-friendly updates

This is a complete combat architecture, not a toy example.

1. Entity Stats (Base + Modifiers)

Stats usually come from:

  • base stats
  • equipment
  • buffs
  • passive bonuses
  • level scaling
  • temporary effects

We design a stat system using:

  • Base values
  • Flat modifiers
  • Multiplicative modifiers

1.1 Stat Object

local Stat = {}
Stat.__index = Stat

function Stat.new(base)
  return setmetatable({
    base = base or 0,
    flat = 0,
    mult = 1.0,
  }, Stat)
end

function Stat:value()
  return (self.base + self.flat) * self.mult
end

return Stat

1.2 Example: Player Stats

local hp = Stat.new(100)
local atk = Stat.new(12)

atk.flat = atk.flat + 5     -- item bonus
atk.mult = atk.mult * 1.2   -- buff

print("Attack =", atk:value())  -- 20.4

This math model is used in:

  • Diablo
  • Path of Exile
  • Genshin Impact
  • Monster Hunter
  • WoW

2. Stats Component for Entities

local Stats = {}
Stats.__index = Stats

function Stats.new(cfg)
  local self = setmetatable({}, Stats)
  self.hp     = Stat.new(cfg.hp)
  self.attack = Stat.new(cfg.attack)
  self.def    = Stat.new(cfg.def)
  return self
end

function Stats:current_hp()
  return self.hp:value()
end

return Stats

Attach to an entity:

enemy:add("stats", Stats.new({hp=80, attack=10, def=2}))

3. Damage Pipeline (Industry Standard)

A proper damage pipeline handles:

  1. attacker stats
  2. skill modifiers
  3. hitbox/hurtbox
  4. defense
  5. critical hits
  6. elemental multipliers
  7. buffs & debuffs
  8. clamping to min/max
  9. post-processing callbacks

We’ll build a minimal version.

3.1 Damage Pipeline Processor

local Damage = {}

function Damage.compute(attacker, target, skill)
  local atk = attacker.stats.attack:value()
  local def = target.stats.def:value()

  local base = atk * skill.power

  local final = base - def
  if final < 1 then final = 1 end

  -- critical?
  if math.random() < skill.crit_chance then
    final = final * skill.crit_mult
  end

  return math.floor(final)
end

return Damage

3.2 Example skill data

local slash = {
  power = 1.5,
  crit_chance = 0.1,
  crit_mult = 2.0,
}

3.3 Apply damage

local dmg = Damage.compute(attacker, target, slash)
target.stats.hp.base = target.stats.hp.base - dmg

print("damage:", dmg)

4. Hitboxes & Hurtboxes

Two common models:

Discrete collision

For turn-based or tile games.

Continuous collision

For action/MOBA/platformers.

We’ll implement continuous AABB (axis-aligned bounding box) overlap.

4.1 Hitbox Module

local Hitbox = {}

function Hitbox.overlaps(a, b)
  return not (
    a.x + a.w < b.x or
    b.x + b.w < a.x or
    a.y + a.h < b.y or
    b.y + b.h < a.y
  )
end

return Hitbox

4.2 Example check

local hb1 = {x=0, y=0, w=32, h=32}
local hb2 = {x=20, y=10, w=32, h=32}

if Hitbox.overlaps(hb1, hb2) then
  print("Hit!")
end

5. Buffs & Debuffs

Buffs apply:

  • flat modifiers
  • multiplicative modifiers
  • event hooks
  • DOT (damage over time)
  • HOT (heal over time)

5.1 Buff Structure

local Buff = {}
Buff.__index = Buff

function Buff.new(cfg)
  return setmetatable({
    duration = cfg.duration,
    elapsed = 0,
    apply = cfg.apply,
    remove = cfg.remove,
    tick = cfg.tick,
  }, Buff)
end

function Buff:update(entity, dt)
  self.elapsed = self.elapsed + dt

  if self.tick then
    self.tick(entity, dt)
  end

  return self.elapsed >= self.duration
end

return Buff

5.2 Buff Manager

local BuffManager = {}
BuffManager.__index = BuffManager

function BuffManager.new()
  return setmetatable({list = {}}, BuffManager)
end

function BuffManager:add(entity, buff)
  table.insert(self.list, {entity=entity, buff=buff})
  if buff.apply then buff.apply(entity) end
end

function BuffManager:update(dt)
  local remaining = {}
  for _, item in ipairs(self.list) do
    if not item.buff:update(item.entity, dt) then
      table.insert(remaining, item)
    else
      if item.buff.remove then item.buff.remove(item.entity) end
    end
  end
  self.list = remaining
end

return BuffManager

5.3 Example buffs

Attack boost buff

local atk_buff = Buff.new({
  duration = 3,
  apply = function(e) e.stats.attack.flat = e.stats.attack.flat + 5 end,
  remove = function(e) e.stats.attack.flat = e.stats.attack.flat - 5 end,
})

Damage over time (burn)

local burn = Buff.new({
  duration = 2,
  tick = function(e, dt)
    e.stats.hp.base = e.stats.hp.base - 5 * dt
  end
})

6. Cooldowns & Cast Times

Every modern game wants this:

  • abilities with cooldown
  • abilities with cast bar
  • skills interrupted mid-cast

6.1 Ability Base Class

local Ability = {}
Ability.__index = Ability

function Ability.new(cfg)
  return setmetatable({
    name = cfg.name,
    cooldown = cfg.cooldown,
    cast = cfg.cast or 0,
    on_cast = cfg.on_cast,
    on_fire = cfg.on_fire,
    timer = 0,
  }, Ability)
end

function Ability:use(entity)
  if self.timer > 0 then return false end

  if self.cast > 0 then
    entity.casting = {ability=self, time=self.cast}
    if self.on_cast then self.on_cast(entity) end
  else
    self:on_fire(entity)
  end

  self.timer = self.cooldown
  return true
end

function Ability:update(dt)
  if self.timer > 0 then
    self.timer = math.max(0, self.timer - dt)
  end
end

return Ability

6.2 Example abilities

Fireball

local fireball = Ability.new({
  name = "fireball",
  cooldown = 3,
  cast = 1.2,
  on_cast = function(e)
    print("Casting fireball...")
  end,
  on_fire = function(e)
    print("Fireball launched!")
  end
})

6.3 Cast-bar update (FPS loop)

if entity.casting then
  entity.casting.time = entity.casting.time - dt
  if entity.casting.time <= 0 then
    entity.casting.ability:on_fire(entity)
    entity.casting = nil
  end
end

7. Combat Log System (Replay-Friendly)

Log-based engines allow:

  • replay
  • rollback
  • debugging
  • analytics

7.1 Combat Log

local Log = {entries = {}}

function Log.record(event, data)
  table.insert(Log.entries, {event=event, data=data})
end

return Log

7.2 Log damage

Log.record("damage", {
  target = target.id,
  amount = dmg,
  skill = "slash"
})

8. Network-Sync-Friendly Combat (Authoritative Design)

Combat must be deterministic when possible.

Key rules:

  • Use integers for damage
  • Do not use math.random() unsafely
  • Move RNG to server only
  • Keep combat log authoritative
  • Always run update(dt) deterministically

Lua’s simplicity helps keep gameplay deterministic.

9. Full Combat Demo (Everything Together)

This is a full combat round, combining:

  • stats
  • damage
  • hitboxes
  • abilities
  • buffs
  • logging
local attacker = Entity.new(1)
local target   = Entity.new(2)

attacker:add("stats", Stats.new({hp=100, attack=20, def=2}))
target:add("stats",   Stats.new({hp=80, attack=12, def=5}))

local slash = {power=1.3, crit_chance=0.1, crit_mult=2}

if Hitbox.overlaps(attacker.hb, target.hb) then
  local dmg = Damage.compute(attacker, target, slash)
  target.stats.hp.base = target.stats.hp.base - dmg

  BuffManager:add(target, burn)

  Log.record("hit", {from=1, to=2, dmg=dmg})
end

This is the foundation of many real combat engines.

10. Summary of Chapter 5

You now understand:

  • Robust stat systems
  • Modifiers (flat/multiplicative)
  • Damage pipelines
  • AABB hit detection
  • Buff/debuff architecture
  • DOT/HOT mechanics
  • Ability cooldowns
  • Cast-bar system
  • Combat logging
  • How to compose combat logic cleanly

This is a real, production-ready combat architecture that can power an RPG, ARPG, action game, strategy game, or MOBA in Lua.

Keep Reading

Follow the engineering thread

Get the next practical Birdor note, or browse the archive for related systems, tooling, and architecture work.

Join newsletter Browse articles