Skip to content

How to Build: Health & Regen

Every RPG needs a way for things to live and die. Before we can build combat, enemy spawning, or any of the exciting systems that make up Phase 3, we need a foundation: a health system that tracks hit points for every combatant and passively regenerates the player between fights.

This guide walks through building an Entity Component System (ECS) health model with Koota traits, damage and healing actions, and a tick-based regen system that pauses during combat.

Why health needs its own system

It might seem like health could just be a number on the player entity, but it touches everything. Combat deals damage. Consumables restore it. The Heads-Up Display (HUD) reads it. Death detection checks it. Enemies need it too. By giving health a clean trait and a small set of actions, every other system can interact with hit points through a consistent interface.

Defining the traits

We need two traits. Health goes on every combatant — player and enemies alike. HpRegen is player-only, since enemies don't heal between fights.

typescript
type Health = {
  current: number
  max: number
}

type HpRegen = {
  ticksPerHeal: number   // how many game ticks between each heal
  healAmount: number      // HP restored per tick
  timer: number           // accumulator counting toward ticksPerHeal
}

The player entity gets both traits at spawn. Enemy entities only get Health — their max HP comes from their stat profile in shared game data, scaled by a difficulty factor.

Building the health actions

Three small functions form the public Application Programming Interface (API) that every other system calls into. Notice how they all clamp values — takeDamage floors at zero, heal caps at max, and isDead is a simple read.

typescript
function takeDamage(entity: Entity, amount: number): void

function heal(entity: Entity, amount: number): void

function isDead(entity: Entity): boolean

takeDamage reads the entity's Health, subtracts the amount, and clamps to zero. heal does the inverse, clamping at Health.max. isDead returns true when current has hit zero.

These are plain functions, not ECS systems. They get called from systems — combat calls takeDamage, consumables and regen call heal, and death detection calls isDead.

Tying max HP to the Hitpoints skill

Here's where things get interesting. The player's max HP isn't a static number — it equals their Hitpoints skill level. Every time the player gains a Hitpoints level, their max HP goes up by one, and they get a small +1 bump to current HP as a reward.

This creates a natural sense of progression. Early on, the player is fragile. As they fight and earn Hitpoints experience, they become steadily harder to kill.

Passive regen between fights

The tickHpRegen system runs every game tick (250 milliseconds at 4 ticks per second). It queries for the player entity and checks their CombatState. If the player is currently in combat, the system does nothing — regen only kicks in once all engaged enemies are dead and the player is walking between clusters.

typescript
function tickHpRegen(world: World): void

The system increments HpRegen.timer each tick. When the timer reaches ticksPerHeal, it calls heal for healAmount and resets the timer to zero. With default values of ticksPerHeal: 4 and healAmount: 1, the player recovers 1 HP per second while out of combat.

This is deliberately gentle. We want the player to occasionally dip low enough that potions matter, but not so low that every fight feels punishing. The regen rate can be tuned later alongside consumable pricing and enemy damage output.

Setting up enemy HP

When an enemy spawns, its max HP comes from the enemy profile in shared game data, multiplied by a scaling factor based on level depth.

typescript
function spawnEnemy(world: World, profileId: string, scaleFactor: number): Entity

The spawn action reads profile.baseStats.hp, multiplies by scaleFactor, and sets both Health.current and Health.max to that value. No regen trait — enemies are fire-and-forget until they die or respawn.

Where this fits in the tick order

The regen system runs after combat and consumables in the Phase 3 tick execution order. That way, if the player takes damage and a potion fires, regen doesn't also heal on the same tick. The full ordering is covered in the Combat Tick guide.

Relevant files

src/core/systems/tick-hp-regen.ts   — Passive regen system
src/core/actions/health.ts          — takeDamage, heal, isDead actions

We now have a health foundation that every combat system can build on. Next up: spawning enemies that will actually use takeDamage against the player.