# Enemy control hit and immunity follow-up — 2026-10-06 ## Finding There is no complete, source-bounded generic hit helper to add for shout control. The pinned rows do establish ordinary miss, resist, and deflect gates for the rows whose `DamageClass` is Magic, but AzerothCore computes those outcomes from the live caster, defender, full `Spell`, world miss-rate policy, and random roll. The None-class children take a different branch and do not receive those Magic hit rolls. A fixed miss percentage or a helper that accepts only a spell ID and `CharacterDefender` would not match the source. Historical behavior below is pinned to read-only AzerothCore `e822d718322289dd036fb2daf8867ba41283bf9a`. The active HealerMan runtime and generated data remain authoritative for implementation. The earlier [shout speed and immunity audit](ENEMY_SHOUT_SPEED_IMMUNITY_2026-10-06.md) records the client-profile hashes and full eight-word attribute tuples for these four spells. ## Pinned shout hit branches All four spells have `SchoolMask=1` (Normal/physical) and spell-level `Mechanic=5` (Fear). The spell damage classes differ by row and profile: | Spell | Ascension | Epoch | Core hit branch | | --- | --- | --- | --- | | 5246 parent | Magic | Magic | `MagicSpellHitResult` in both profiles | | 19134 parent | Magic | Magic | `MagicSpellHitResult` in both profiles | | 20511 child | Magic | None | Magic roll in Ascension; aura-only None handling in Epoch | | 29544 child | None | None | Aura-only None handling in both profiles | The `Unit::SpellHitResult(Unit*, Spell const*, bool)` overload first checks whole-spell and effect immunities, then dispatches by `DamageClass` (`Unit.cpp:3706–3790`). A Magic-class shout therefore reaches `MagicSpellHitResult` despite its physical school. That routine (`Unit.cpp:3503–3634`) calculates miss from relative caster/defender levels, configured miss multipliers, caster spell modifiers and hit bonuses, defender hit-taken and area-avoidance bonuses, and player hit-taken rating. It then adds mechanic and eligible debuff resistance. The extra binary-school resistance term does not apply to these Normal-school rows. Deflect is possible when the target is in the caster's front arc (or ignores hit direction), is not stunned, and has a deflect aura. The routines also depend on actor types and state such as vehicles, trigger creatures, and dead non-player targets. These are source-representable gates only when the runtime can provide the relevant actors and spell context. The core call receives the original caster as `this`, the actual `Unit*` victim, and the active `Spell*`; `Spell::AddUnitTarget` calls it with `m_originalCaster` (`Spell.cpp:2293–2367`). A faithful integration needs the caster's target-relative level, hit and spell-modifier data, relevant owner/totem/vehicle state, and the defender's level, active immunities, mechanic/debuff resistance, hit-taken and area-avoidance data, facing, stun state, deflect/reflect auras, and evade/death state. It also needs the source row and the configured miss-rate policy. The current shout contract does not carry that information, and area recipients can include actor types outside player/party `CharacterDefender` snapshots. For a None-class row, the source first performs its immunity checks, then returns a successful hit for a nonzero spell family or for aura-only effects (`Unit.cpp:3761–3790`). Child 20511 and 29544 are aura-only in both pins, so their None-class profiles have no ordinary Magic miss, resist, or deflect roll. Preserve this difference: a generic roll on every triggered child would add an unsupported Epoch 20511 and 29544 outcome. The triggered child remains tied to the parent's explicit victim. The core records the parent's hit result per target, and runs target effects only for a successful hit (`Spell.cpp:2583–2637`); effect 0's trigger therefore runs only for the successful explicit-victim target. The current runtime validates that explicit victim's eligibility, range, and line of sight before selecting the child, then applies it directly in [combatStore.ts](../src/game/combatStore.ts) around the `triggered-shout` branch. Root integration should preserve that victim binding and place any supported parent hit gate before child execution. ## Whole-spell control immunity and active-aura purge The shout rows set spell-level Fear, so the core checks Fear mechanic immunity against the whole spell before per-effect immunity (`Unit.cpp:10032–10120`). This rejects the parent and its riders as a whole. Child 20511 remains a Fear-mechanic spell even though its first aura is stun: fear immunity rejects the whole child; stun-state immunity is narrower and may strip the stun effect while leaving Ascension's separate physical-damage-taken rider. Epoch 20511 has no separate rider. These distinctions match the existing shout audit. The local generated WoW 3.3.5a ability JSON was parsed and filtered by spell ID. Its imported Warrior rows identify Berserker Rage 18499 and Bladestorm 46924 as immunity candidates. The generator now preserves raw `attributes1` and `attributes5` alongside the selected spell-defense metadata, and apply-aura effects retain raw base-points, die-sides, and points-per-level scalars. I read the corresponding raw Spell.dbc fields from the local `patch-enUS.MPQ` to establish the purge bit rather than inferring it from tooltips: | Imported spell | Relevant generated effect | Raw `Attributes1` | Source result | | --- | --- | ---: | --- | | 18499 Berserker Rage | Aura 77, mechanic immunity value 5 (Fear) | `32768` (`0x00008000`) | Grants Fear immunity and carries the purge flag | | 46924 Bladestorm | Aura 147, mechanic-immunity mask value 1733; calculated amount is zero | `32784` (`0x00008010`) | Carries the purge flag; the pinned core maps this zero-amount mask to Fear and Stun immunity | | 49039 Lichborne | Aura 77, mechanic immunity value 5 (Fear) | `163872` (`0x00028020`) | Grants Fear immunity and carries the purge flag | | 48792 Icebound Fortitude | Aura 77, mechanic immunity value 12 (Stun) | `0` | Stun immunity, but this source row does not prove active-aura purge | The selected rows used by the runtime proof also require these exact raw values: | Spell | Attributes0 | Attributes1 | Attributes3 | Attributes5 | Raw immunity effect | | --- | ---: | ---: | ---: | ---: | --- | | 18499 Berserker Rage | `327696` | `32768` | `0` | `0` | Effect 0: aura 77, misc 5 (Fear), raw base points -1, die sides 1 | | 46924 Bladestorm | `327696` | `32784` | `1024` | `512` | Effect 1: aura 147, misc 1733, raw base points -1, die sides 1 | `SPELL_ATTR1_IMMUNITY_PURGES_EFFECT` is `0x00008000` in `SharedDefines.h:422`. On applying an immunity aura with a mechanic mask, `SpellInfo::ApplyAllSpellImmunitiesTo` removes existing auras matching that mask when the flag is set (`SpellInfo.cpp:2488–2537`). The pinned core maps Bladestorm's 1733 mask with zero calculated amount to a mask that includes both Fear and Stun (`SpellInfo.cpp:2355–2377`). Thus Berserker Rage and Bladestorm are concrete imported examples of active shout removal: Berserker Rage covers Fear; Bladestorm's sourced mask covers Fear and Stun. The shout rows themselves do not set this purge attribute. The cast exception is narrower than the aura grant. Berserker Rage has no `Attributes5` allow-while-stunned/fleeing/confused bit, yet its Fear aura and purge flag let the core's `Spell::CheckCasterAuras` ignore active Fear-mechanic auras during its cast check (`Spell.cpp:6830–6978`). That includes the `20511` child: although its effect is stun, its applied aura carries spell-level Mechanic=FEAR, so the core skips that whole aura before the stun-state check. A standalone stun aura remains blocking. Bladestorm has `Attributes5=512` (`EXTRA_INITIAL_PERIOD`), not an allow-while-stunned or allow-while-fleeing flag; it cannot start while feared or stunned in this source path, even though its applied aura later grants both immunities. ## Evidence and implementation boundary The ignored [parsed source audit artifact](../artifacts/enemy-control-followup-2026-10-06/source-audit.json) retains the parsed generated rows, raw Spell.dbc attribute words, and raw effect fields for the cited WoW 3.3.5a spells. The bounded helper in [enemyControlImmunityPurge.ts](../src/game/enemyControlImmunityPurge.ts) accepts only the exact selected WoW rows and fails closed on unknown profiles, IDs, raw flags, or aura scalars. It distinguishes direct Fear from Bladestorm's Fear-and-Stun grant, records that only Berserker Rage is usable while feared, and scopes cleanup to the four named shout source auras. Since each shout has spell-level Mechanic=FEAR, the Fear purge removes the named 20511 child aura as a whole, including any rider bundled into that runtime aura. A separate generic stun remains active and can keep the target controlled; Icebound Fortitude stays outside the purge path because it has no raw purge flag. Party AI uses the same selected Berserker Rage proof to escape a named shout Fear, or its Fear-mechanic 20511 stun. It preserves learned-rank selection, target validity, cooldown, GCD, and costs. The context check requires active action-blocking control auras on that companion to be covered by the named source shout proof, so an unrelated generic stun blocks the escape. The broad Magic hit/resist/deflect helper remains unsupported because the full source inputs are absent. Focused verification passed: the immunity helper, direct-grant integration, and party escape integration tests (19 tests across 3 files). No build, release check, or live run was performed in this scoped task.