Appearance
How to Build: Aggro & Leashing
Combat doesn't start with a sword swing — it starts with an enemy noticing you. The aggro and leashing system is the gatekeeper that decides which enemies are fighting the player at any given moment. It uses the spatial hash for fast proximity checks, manages engagement state, handles auto-targeting, and makes sure enemies don't chase the player to the ends of the earth.
The rules
Before diving into code, let's establish the rules that govern engagement:
- Aggro radius: 2 hex tiles from the enemy's current position. Walk within 2 hexes, the enemy engages.
- Attack range: Depends on weapon type — 1 hex for daggers and swords, 2 hexes for greatswords. The player must be within range to land hits.
- Max simultaneous attackers: 32 enemies can be fighting the player at once.
- Leashing: If the player pulls an enemy too far from its spawn point (8 hex tiles), the enemy disengages and walks home.
- Kiting: The player can move during combat to reposition. This is how you manage large groups — pull a few at a time instead of aggroing everything.
Detecting aggro
The tickAggro system runs every game tick, before combat resolves. It uses the spatial hash map for fast proximity queries — no need to check every enemy in the world, just the ones near the player.
typescript
function tickAggro(world: World): voidThe system queries the spatial hash for entities near the player's position, filters to living enemies within the aggro radius (2 hex tiles), and flips their CombatState.inCombat to true. It caps engagement at COMBAT.MAX_SIMULTANEOUS_ATTACKERS.
On the player side, if any enemies are now engaged and the player isn't already in combat, the system sets the player's combat state and auto-targets the nearest engaged enemy. If all enemies have died or leashed, it clears the player's combat state.
This creates a smooth flow: walk into range, combat begins automatically. Walk away (or kill everything), combat ends automatically. The player never has to click "attack" — this is an auto-battler, after all.
Leash checking
Leashing prevents a frustrating pattern where the player could kite enemies indefinitely, dragging them across the entire map. When an engaged enemy gets too far from home, it gives up the chase.
typescript
function checkLeashing(world: World): voidThe system iterates over all in-combat enemies and measures the hex distance between their current position and their SpawnPosition. If the distance exceeds the leash threshold (8 hex tiles), the enemy's combat state clears and it receives a movement target back to its spawn point.
This also gives the player a tactical tool: if a fight is going badly, they can run. Once they're far enough away, enemies leash and the player can regen in peace. It rewards spatial awareness.
Auto-target selection
When the player's current target dies, we don't want combat to just... stop. The findNearestEngaged function picks the next target automatically.
typescript
function findNearestEngaged(player: Entity, world: World): Entity | nullIt queries all in-combat, living enemies and finds the nearest one within the player's weapon range. If no enemy is in range, it returns null — the player needs to close the distance before attacks can land.
This function is also called when the player first enters combat to select the initial target.
Attack range enforcement
The combat tick system (covered in its own guide) checks weapon range before resolving each player attack. If the current target is out of range — maybe the player moved, or the enemy leashed partway — the attack doesn't fire. Instead, the player should move toward the target.
This is where weapon range becomes a real gameplay choice. A dagger fighter (range 1) has to stay right next to their target. A greatsword fighter (range 2) has a bit more breathing room and can hit enemies that a dagger user can't reach without repositioning.
Relevant files
src/core/systems/tick-aggro.ts — Aggro detection and leashingWe now know how enemies engage and disengage. But what happens when someone's HP hits zero? Let's look at the death and level reset system.