Enemy Aggro System
Contents
SummaryDetectionEnemy SelectionThreat Knowledge Threat DensityRecoveryPlayer ManipulationGame FilesSummary
This article covers how enemies decide who to focus on; how they discover players, pick a target, and change targets when someone starts shooting them. It does NOT cover when enemies actually get to attack; that's the Combat Token System, which governs attack pacing and turn-taking.
Every enemy stores its current target in the Enemy blackboard key. All behavior tree distance checks, attack targeting, and positioning decisions reference this key. If you are not an enemy's Enemy, your position is irrelevant to its AI — you could be standing on its head and it won't care unless you're its target.
The system operates in three layers, in order:
Detection — How enemies discover players exist (vision, hearing, damage, chain alerts). An enemy can't target what it doesn't know about.
Enemy Selection — From known targets, who does the enemy focus on? A utility-based scoring system with decision stickiness that re-evaluates every second.
Threat Knowledge — Incoming damage can override the selection system, forcing target swaps via RangeThreat/MeleeThreat interrupts. This is the "aggro stealing" layer — and the part players have the most direct control over.
Detection
Before an enemy can target a player, it must first detect them. The Advanced Enemy Selection pipeline feeds from a set of Sensors and Observers that populate the enemy's known-targets list. An enemy that hasn't detected you will not target you, regardless of distance or damage.
Detection Sensors
The pipeline includes 6 sensor types, defined in {SERVER}/ssl/ai/enemyselection/advancedenemyselection/sensorsobservers/:
Sensor | How It Works |
|---|---|
Vision | Line-of-sight detection within a per-enemy aggro radius. Range varies enormously: Spore Mine = 20m, Tyranid Warrior Sniper = 115m, Zoanthrope = 140m. Requires unobstructed sightline |
Hearing | Sound-based detection. Gunfire, abilities, and movement generate sound that can alert enemies outside visual range |
Damage | Taking damage from a previously unknown actor instantly reveals them. You always become known to an enemy you shoot, even outside its vision range |
AI Target | Script-assigned targets from mission events. The game director can force an enemy to know about specific players (e.g., boss encounters, scripted ambushes) |
Boss Sixth Sense | Bosses can detect players regardless of line of sight. Prevents players from "hiding" from boss enemies |
Player Got Detected | Chain detection — when one enemy spots a player, nearby allies become aware too. This is how an entire group turns on you after one enemy sees you |
Detection Propagation
Once an enemy detects a player, it can spread awareness to allies through two mechanisms:
Aggro Scream — an active alert broadcast:
Scream Delay: 0.6 seconds after detection
Scream Duration: 2 seconds
Cooldown: 20 seconds
When an enemy spots a player, it waits 0.6s then "screams" for 2s, alerting nearby allies. The 20s cooldown prevents constant re-broadcasting. This is the primary chain-detection mechanism — one Termagant spots you, screams, and the whole group turns.
Aggro Brattle — passive ambient awareness:
Update Period: 1 second
A continuous background check that propagates awareness periodically. Less dramatic than scream but ensures nearby enemies eventually learn about detected players even without an active alert.
Per-Enemy Detection Ranges
From individual enemy writeups, confirmed vision/aggro detection ranges:
Enemy | Detection Range | Notes |
|---|---|---|
Spore Mine | 20m | Speeds up from 4.5 to 6.5 m/s on alert |
Acid Mine | 20m | Same as Spore Mine |
Tyranid Warrior (Sniper) | 115m | Huge detection range matches sniper role |
Zoanthrope | 140m | Largest confirmed detection range |
Chaos Cultist | Squad-based | 25m squad formation search radius |
Most enemies use default detection ranges that we haven't individually confirmed — the values above are the ones explicitly documented in enemy writeups.
Detection Implications
You can avoid aggro entirely by staying outside detection range — but the Damage sensor means any shot you take immediately reveals you
Chain detection means stealth is fragile — one enemy's scream alerts the group within 0.6s
Bosses can't be hidden from — the Sixth Sense sensor bypasses all stealth
Detection is prerequisite, not targeting — being detected adds you to the known-targets pool, but Enemy Selection (Layer 2) still decides who the enemy actually focuses on
Enemy Selection
Once detected, potential targets enter the Advanced Enemy Selection pipeline — a utility-based system that scores each known target and picks the highest. The Enemy blackboard key is set to the winner.
Selection Pipeline Components
The pipeline runs through these controllers:
Controller | Role |
|---|---|
Selection | Runs every 1 second; evaluates all potential targets ( |
Known Enemies | Maintains list of actors the enemy is aware of (fed by Layer 1 sensors) |
Exposed Enemies | Tracks which actors are currently visible/exposed |
Vision History | Remembers previously seen actors (persistence after breaking line of sight) |
Aggro Registrator | Registers aggro state changes |
Blackboard Updater | Pushes selection result into the |
Domain Restriction | Constrains selection to valid domain (prevents targeting across arenas, etc.) |
Raw Damage Received | Tracks damage from each actor; decay rate = 10 |
Default Target Scoring
The default enemy selection utility (utilityfunctiondefaultenemyselection.sso) scores each potential target as:
Score = CanBeAiTarget × min(DecisionStickiness, Priority) × TargetDesirabilityWhere Priority is a weighted average of:
Base Aggressiveness (weight: 10) — primary factor, based on the
UtilityFunctionAggressivenesssubfunctionPlayer Contribution (weight: 1) — minor factor from
UtilityFunctionEnemySelectionPlayerContribution
The heavy 10:1 weight toward aggressiveness means the base scoring dominates over player contribution tracking.
Decision Stickiness
Enemies don't instantly swap to a new target every cycle. The UtilityFunctionDecisionStickiness module provides a bias toward keeping the current target:
Stickiness Duration: 7 seconds — after selecting a target, that target gets a bonus score for 7s
Stickiness Fade Duration: 3 seconds — the bonus fades over 3s after the 7s expires
This means an enemy typically holds its current target for ~7-10 seconds before being open to swapping, unless a threat module (Layer 3) forces an override.
Melee Enemy Selection
Melee-specialized enemies use utilityfunctionmeleeenemyselection.sso, which adds:
Path validation — target must have a valid navigation path (unreachable targets score 0)
Melee Aggressiveness override — replaces base aggressiveness with
UtilityFunctionAggressivenessMelee
Power-Level-Based Selection
Some enemies use utilityfunctionenemyselectionbypowerlevel.sso, which factors in:
Target power level — spline from 0→0.1 to 100→1.0, meaning higher-power targets are preferred
Distance decay — "Decay Closer" spline from 0→0.1 to 80→1.0 (closer = lower score, favoring distant targets)
Decision stickiness — 7s duration, 3s fade (same as default)
Player contribution — inverted (NOT player contribution), meaning enemies using this profile are LESS likely to target the player who's been contributing most
Threat Knowledge
This is the "aggro stealing" layer. While Layer 2 runs its selection cycle every second, Layer 3 can interrupt that process — incoming damage sets blackboard keys (RangeThreat, MeleeThreat) that force target swaps mid-cycle.
Every enemy includes one or more AI Knowledge Modules that track incoming damage and fire when thresholds are exceeded. These are the core defaults — individual enemies can (and frequently do) override every parameter.
Default RangeThreat
Parameter | Value |
|---|---|
Blackboard Key |
|
Time Window | 2 seconds |
Damage Threshold | 2 |
Distance | 0–30m |
Damage Types | bullet, heavybullet, shotgunbullet, explosivebullet, chargedplasma, plasma, melta, laser, gunstrike, volkite, volkiteexp |
Filter |
|
When an enemy takes 2+ qualifying damage within 2 seconds from a player within 30m, the RangeThreat blackboard key is set to that player. This is the primary mechanism for "stealing aggro" with ranged fire.
Key exclusions from the default filter: Melee damage, fire/burn damage, generic explosive damage, and player perk damage (dmgtypeplayer_perk) do NOT trigger RangeThreat by default. Shooting is the intended trigger.
Default RangeFarThreat
Parameter | Value |
|---|---|
Blackboard Key |
|
Time Window | 2 seconds |
Damage Threshold | 4 |
Distance | 30–50m |
A separate module for players engaging from farther away. Higher threshold (4 vs 2) — you need to deal more sustained damage from range to register as a threat. No damage type filter is specified (only distance filtering), which means it may accept all damage types at this range.
Default MeleeThreat
Parameter | Value |
|---|---|
Blackboard Key |
|
Time Window | 1.5 seconds |
Damage Threshold | 6 |
Update Period | 0.1 seconds |
Damage Types | melee, melee_push, contact |
Melee threat requires substantially more damage (6 vs 2 for ranged) in a shorter window (1.5s vs 2s). This makes ranged fire significantly more effective at pulling aggro than melee attacks. The high threshold means weak melee hits won't trigger a target swap.
TopDamageThreat
Parameter | Value |
|---|---|
Update Period | 0.2 seconds |
Time Window | 1 second |
Filters | Targetable actors only, ignores friendly fire |
A global module that tracks who is dealing the most damage overall within a 1-second rolling window. Used by some enemy selection utility functions as a factor in target prioritization (not directly tied to a blackboard key like the others).
Per-Enemy Overrides
Most enemies override the default thresholds. Here's a compiled reference of every enemy whose threat parameters we've confirmed differ from defaults:
Enemy | Type | Threshold | Window | Distance | Notes |
|---|---|---|---|---|---|
Biovore | Range | 20 | 5s | — | Extremely high threshold; hard to pull aggro with chip damage |
Helbrute | Range | 15 | 6s | — | Also requires first event >=3s old, last <=2s old; ignores current target |
Helbrute | Melee | — | 5s | — | First event >=3s old, last <=1s old; ignores current target |
Rubric Marine (Flamer) | Range | 15 | 2s | 40m max | High threshold for a common enemy |
Rubric Marine (Flamer) | Melee | 10 | 1.8s | — | |
Rubric Marine (Bolter) | Range | Default | 1.3s | Unlimited | Shorter window than default |
Tzaangor Spearman | Range | 10 raw | 1.8s | Unlimited | Uses raw (pre-mitigation) damage |
Tzaangor Spearman | Melee | 5 | 1.8s | — | |
Tzaangor | Range | 3 raw | Default | — | Very sensitive (default module) |
Tzaangor | Melee | 1 raw | 3s | — | Extremely sensitive — any melee hit triggers |
Tyranid Warrior (Sniper) | Range | 8 | Default | 110m max | High threshold but huge detection range |
Tyranid Warrior (Sniper) | Melee | 7 | 2s | — | |
Tyranid Warrior (Whip) | Range | 5 | 1.5s | 150m max | |
Tyranid Warrior (Whip) | Melee | 4 raw | 1.5s | — | Uses raw damage |
Zoanthrope | Range | 3 | 1.5s | — | Only filtered types (bullet, plasma, etc. — NOT melee, fire, explosive) |
Neurothrope | Range | 3 | 1.5s | — | Same as Zoanthrope |
Lesser Sorcerer | Range | None | — | — | No RangeThreat module at all; melee-only recovery |
Lesser Sorcerer | Melee | 9 | 1.5s | — | |
Ravener | Melee | 1 | 3s | — | useRelativeValues = False; any hit triggers |
Hormagaunt | — | — | — | — | No recovery system at all |
Termagant | — | — | — | — | No recovery system; uses IsScared flee behavior instead |
Chaos Cultist | — | — | — | — | No recovery; squad suicide/panic instead |
"Raw" vs "Modified" damage: Some enemies check checkRawDamage = true, meaning pre-mitigation damage counts. Others check post-mitigation. This matters when enemies have damage sensitivity states active — raw-checking enemies are easier to trigger threat on while they're in damage reduction states.
Threat Density
The floatvalueproviderthreatdensity.sso assigns threat weights by enemy classification. This is used by the AI to evaluate how dangerous a group of enemies is in an area:
Classification | Weight |
|---|---|
FODDER | 0.2 |
COMMON | 0.25 |
ELITE | 0.5 |
SPECIAL | 1.0 |
MINIBOSS | 1.0 |
BOSS | 1.0 |
Fodder enemies (Hormagaunts, Cultists) contribute very little to threat density. Elites (Terminators, Warriors) contribute half. Specials, Minibosses, and Bosses all have equal maximum weight.
Recovery
When a threat module sets RangeThreat or MeleeThreat, the enemy's behavior tree enters its Recovery subtree. Recovery is an interrupt system; it can pause the enemy's current action (if AllowInterruptions is true) and execute a response.
Recovery responses vary wildly by enemy, but common patterns include:
Response Type | Examples | Effect on Aggro |
|---|---|---|
Dodge + Counter | Neurothrope, Tyranid Warriors | Evades then retaliates; often sets threat source as new |
Shield/Stance Swap | Zoanthrope (Bolster Defense) | Changes defensive posture; 7.7s cooldown between swaps |
Teleport | Helbrute (Phase 2), Lesser Sorcerer | Repositions to threat source or away from it |
Block + Counter | Tzaangor (shield block), Lesser Sorcerer | Absorbs hit then counterattacks; can set attacker as new target |
Reposition | Biovore (unanchor), Rubric Marines | Moves to new position; target swap depends on implementation |
Escalation | Hierophant (cooldown reduction) | Becomes MORE dangerous; reduces shooting cooldown by 15s |
Flee/Panic | Termagant (IsScared), Chaos Cultist (suicide) | Disengages combat entirely |
Critical insight: Recovery often makes enemies MORE dangerous, not less. The Helbrute's ranged threat response reduces its shooting cooldown. The Hierophant gets faster. Triggering recovery carelessly can escalate the fight.
Player Manipulation
Avoiding Detection
Stay outside an enemy's vision radius to avoid being added to its known-targets list
But the Damage sensor means any shot reveals you instantly — there's no stealth sniping
Chain detection (Player Got Detected sensor + Aggro Scream) means one alert spreads to the group in ~0.6s
Bosses have Sixth Sense — they can't be hidden from at all
Stealing Aggro (Layer 3 Overrides)
Ranged fire is king — RangeThreat has a lower threshold (2 damage) than MeleeThreat (6 damage). A few shots steal aggro faster than sustained melee
Stay within 30m for default RangeThreat. Beyond 30m you hit the RangeFarThreat module which requires double the damage (4 threshold)
Use the right damage types — bullet, plasma, melta, laser, volkite all work. Fire and generic explosive do NOT trigger RangeThreat by default
Enemies hold targets for ~7-10 seconds (decision stickiness). Brief damage won't instantly pull an enemy off a teammate — you need to sustain pressure or deal enough to force a threat module override
Off-Screen Safety
The frustum aggro penalty (0.6x for enemies behind you) means enemies you can't see are 40% less likely to get attack tokens. See the Combat Token System for the full scoring breakdown and how forced tokens bypass this entirely.
High-Value Target Enemies
Some enemies using power-level-based selection actively prefer higher-power targets. Players running high power-level loadouts will attract more aggro from these enemies.
Per-Enemy Threat Exploitation
Biovore (20 dmg/5s): Extremely hard to pull aggro with ranged fire alone. Close to 25m and stay there for 2s to force unanchor instead
Helbrute (15 dmg/6s with age requirement): The first-event-age requirement (>=3s old) means burst damage won't trigger threat — you need sustained fire over 3+ seconds
Tzaangor (1 raw melee): Absurdly sensitive to melee; any melee contact pulls aggro immediately
Zoanthrope/Neurothrope (3 dmg/1.5s): Sensitive but ONLY to ranged weapon types. Melee, fire, and explosive don't trigger shield swap
Lesser Sorcerer: Has NO RangeThreat module. You cannot pull aggro by shooting — only melee (9 damage in 1.5s) triggers recovery
Hormagaunt/Termagant/Cultist: No recovery at all. Threat modules may still update the
Enemykey through the selection pipeline, but there's no behavioral interrupt
The Recovery Trap
Forcing recovery can backfire:
Don't trigger Helbrute ranged threat recovery carelessly — it reduces its shooting cooldown by 15s
Hierophant recovery makes it shoot faster
Neurothrope recovery leads to a forced HEAVY counter-attack (Psychic Leech or Warp Ball)
Tzaangor Spearman: every 3rd recovery triggers Explosive Throw or Taunt (escalation)
Plan your threat-pulling around whether the recovery response is something your team can handle.
Game Files
{SERVER}/ssl/ai/enemy_selection/advanced_enemy_selection/sensors_observers/— all detection sensors (vision, hearing, damage,ai_target,boss_sixth_sense,player_got_detected){SERVER}/ssl/ai/detection/rules/server/detection_rule_aggro_scream.sso— aggro scream propagation{SERVER}/ssl/ai/detection/rules/server/detection_rule_aggro_brattle.sso— aggro brattle (ambient detection){SERVER}/ssl/ai/enemy_selection/advanced_enemy_selection/— full enemy selection pipeline (controllers, sensors, utility functions){SERVER}/ssl/ai/behaviour/value_providers/object_value_provider_enemy.sso— theEnemyblackboard binding{SERVER}/ssl/ai/knowledge/damage/ai_knowldege_module_range_threat.sso— default RangeThreat module{SERVER}/ssl/ai/knowledge/damage/ai_knowldege_module_range_far_threat.sso— RangeFarThreat module (30-50m band){SERVER}/ssl/ai/knowledge/damage/ai_knowldege_module_melee_threat.sso— default MeleeThreat module{SERVER}/ssl/ai/knowledge/damage/ai_knowledge_module_top_damage_threat.sso— TopDamageThreat (who's dealing the most overall){SERVER}/ssl/ai/behaviour/value_providers/float_value_provider_threat_density.sso— threat density by enemy classification