NPC AI & Behaviour
Two layers: a compact engine-side state machine, and per-entity scripts that decide when to change state. The engine knows how to fight and walk; the script knows whether to.
The behaviour flags
Twelve states appear in NPC.cpp, held as flags rather than an exclusive enum, so combinations are legal:
NONE · FRIENDLY · MOVE_TO · WANDER_AROUND · FLEE · SNEAK · FIGHT · DISTANT · MAGIC · GO_HOME · LOOK_AROUND · STARE_AT
FIGHT and MAGIC/DISTANT are the two combat postures — melee closers versus casters and archers, who keep their distance.
The behaviour stack is the good idea
ARX_NPC_Behaviour_Stack / _UnStack push and pop the whole state bundle: behaviour flags, behaviour parameter, current target, movement mode, and the animation layers. On pop, the NPC re-launches pathfinding toward the restored target.
That single mechanism buys the world its sense of routine. A guard walking a patrol can be interrupted into FIGHT, then pop back to exactly where he was going and what he was playing — without the script author writing any resume logic. It is the cheapest possible version of "NPCs have lives," and it is why Arx's inhabitants read as people doing things rather than encounters waiting in a box.
Pathfinding: A* over an anchor graph
Navigation runs on a precomputed anchor graph (src/ai/Anchors.cpp) queried by PathFinder:
- Tunable heuristic,
0.0to0.5. At0.0only remaining distance to target counts (greedy, cheap, dumb); at the0.5default, traversed cost and remaining distance are weighted equally. - Cylinder constraint (radius, height) so a troll and a rat don't share a route.
- Three query types:
move(from, to),flee(from, danger, safeDistance), andwanderAround(from, radius)— fleeing is a first-class query, not a fight query run backwards.
Light is terrain
The pathfinder takes a stealth flag, and in stealth mode every node accumulates an illumination cost:
STEALTH_LIGHT_COST = 300
cost += 300 × intensity × mean(r,g,b) × falloff // per ignited light in range
Only currently ignited lights count. So dousing a torch does not merely change what NPCs can see — it rewrites the navigation graph, and sneaking NPCs re-route through the dark you just made. The same lighting state feeds player visibility (Stealth & Light) and enemy routing, from one authority. For 2002 that is a genuinely elegant piece of systems economy.
Combat AI: animation-level tactics
Inside FIGHT and within engagement range, an NPC in its fight-wait stance rolls to reposition:
| Situation | Trigger | Outcome |
|---|---|---|
Melee, inside TOLERANCE − 20 |
Always | Back up |
Melee, within TOLERANCE + 10 |
3% per check | 10% back up · 45% strafe left · 45% strafe right |
| Caster/ranged, inside 340 units | Always | Back up |
| Caster/ranged, beyond that | 15% per check | 33% back up · 33% strafe left · 33% strafe right |
| Player begins a strike | Every time | 20% back up · 40% strafe left · 40% strafe right |
That last row is the one worth stealing. IsPlayerStriking() makes the AI read the player's swing animation and react to it — the enemy is responding to a physical act, not to a stat or a cooldown. It is the AI-side counterpart to the manual-verb thesis, and it means charging a heavy blow visibly provokes evasion. Feint pressure, in 2002.
NPC combat stats
NPCs carry their own tohit, damages, critical, backstab_skill and absorb, resolved through the same ARX_EQUIPMENT_ComputeDamages path as the player, with one difference: NPC damage is multiplied by Random(0.5, 1.0) while the player's is multiplied by swing charge. The player's variance is earned; the NPC's is rolled. Curse reduces both an NPC's damage and its AC/absorb by 5% per caster level.
Per-class values, and the four dials that actually differentiate enemies, are in Bestiary & Enemy Design.
Factions and escalation
The "clever" script archetype adds a social layer the engine knows nothing about: NPCs join a group via SETGROUP, broadcast PLAYER_ENEMY to that group the first time they turn hostile, and call CALL_FOR_HELP to allies within 600 units while fighting or 1200 while fleeing. Attacking one goblin is attacking the goblins. Fleeing enemies also halve their own cowardice threshold each time, so a creature you rout twice returns willing to die.
The PLAYER_ENEMY broadcast carries no radius at all — it reaches every group member on the level instantly. Full mechanics in Factions & Groups.
Perception
Sight and hearing are shared with Stealth & Light: a ±110° cone, the 15 + Stealth × 0.1 light threshold with torch and 200-unit overrides, footsteps audible at 600 units and items at 800. Transitions fire DETECTPLAYER, UNDETECTPLAYER and HEAR (with distance) into the entity's script, so reaction is authored per creature while detection is uniform.
Assessment
The engine layer is small, general and cheap: flags, a stack, an A* with a cost surface, and a handful of animation rolls. Everything specific — factions, dialogue, whether a goblin flees at low health, whether a guard cares that you drew a weapon — lives in the scripts. This is the same "thin simulation, many scripts" architecture as the rest of the game.
The limitation: tactics are animation-level, so enemies strafe and back off but never flank, coordinate positioning, use terrain, or retreat to a chokepoint — the only group behaviour is the faction alarm above. Difficulty comes from stats and numbers, not from opposition intelligence. The strength: detection and routing are systemic and honest, so the player can reason about them — and the light-cost pathfinder means the player's tools genuinely change enemy behaviour rather than just enemy awareness.
Touchpoints
- → Stealth & Light: one lighting authority drives both player visibility and NPC routing
- → Combat: NPCs react to the player's strike animation; shared damage resolution
- → Simulation & Interaction: per-entity scripts supply all specific behaviour
- → Magic & Runes:
BEHAVIOUR_MAGICNPCs cast from the same spell system the player uses
What breaks if you remove it
The behaviour stack is what makes the city feel populated rather than staffed. Remove it and every NPC becomes a trigger; remove the light-cost pathing and the torch stops being a decision.