Appearance
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): booleantakeDamage 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): voidThe 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): EntityThe 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 actionsWe now have a health foundation that every combat system can build on. Next up: spawning enemies that will actually use takeDamage against the player.