# Ordinary WoW NPC spell blocking — 2026-10-05 ## Result The historical AzerothCore source has two spell-block phases. They use different eligibility rules and different skill signs, so the pure helpers in [`src/game/enemyBlocking.ts`](../src/game/enemyBlocking.ts) keep them separate. The existing melee/ranged auto-attack baseline is unchanged. Native RoM NPCs, unknown sources, and character defenders do not use this ordinary-Creature spell path. The current imported spell metadata supports the post-hit direct-damage phase when source effects positively establish AzerothCore's `DIRECT_DAMAGE` custom flag. It does not infer that custom flag is absent from a Spell.dbc row. The separate hit-table full-block phase therefore requires an explicit AzerothCore-computed custom-attribute word and remains unavailable where that word has not been imported. ## Runtime implementation `scripts/generate-wow335-abilities.mjs` now retains the exact defense metadata on every imported rank. Its 239-field WoW client records use the existing six-column shift: damage class is field 219 and school mask is field 231. Raw Heroic Strike, Steady Shot and Arcane Shot records independently confirm those columns. Regeneration changed only this metadata and the generation timestamp; all previous spell effects, progression and costs remain identical. `wow335AbilityCatalog.ts` and the classic Ascension builder in `coaAbilityCatalog.ts` preserve those scalars. `abilityAtLevel` selects the rank's own metadata and clears it when that rank is unbound. `combatStore.ts` routes source-bound direct physical melee/ranged packets from both the player and real party companions through the ordinary NPC defender. `partyRuntime.ts` supplies the exact source damage class and selected spell rank to that resolver for blockable physical casts against ordinary source NPCs; those casts previously used a damage path without a hit-result context. The damage API uses the explicitly selected spell ID to look up that rank's defense metadata and leaves unknown IDs unbound. It resolves current NPC facing, active non-melee casts, control, source `flags_extra`, block auras and returning/evading state. Weapon skill uses the simulator's existing level-times-five abstraction; no weapon-skill progression or equipment requirement is introduced. Native RoM targets, guardians and character defenders retain their separate paths. Native RoM spells never acquire WoW metadata through their stat-ruleset choice. `combatResults.ts` adds an explicit `blockMode: post-hit`. The common hit/crit table contains no block entry for this mode. An independent `block` roll runs only after a physical melee/ranged hit lands, subtracting NPC block value after critical and armor mitigation. Crit-plus-block retains `outcome: crit`, `critical: true` and `blocked: true`; a regular blocked hit reports `block`. Structured outgoing damage events retain `blocked` and `blockedAmount`, so recorders can observe both properties without guessing from the latest result. Ordinary autoattacks retain their single table roll. Unknown source packets, periodic ticks, magical schools, source damage-class mismatches and defense-exception spells do not enter this new phase. Runtime full-hit blocking of non-direct spells, ignore-hit-direction aura imports and critical-block aura doubling remain source-integration boundaries. This pass does not implement the other spell-specific dodge/parry attribute exceptions. The level-80 cap, Ready-five policy and loot scope remain unchanged. ## Validation The ten directly affected Vitest files pass 134 checks: source helper gates, shared combat results, post-hit block distribution/order, per-rank metadata, stance/autoattack regression, native RoM exclusion, actual player and generated party spell casts, party AI/runtime and stat-ruleset regressions. The six new store integration cases prove Heroic Strike and a real Hunter companion's Steady Shot can critically hit and block simultaneously. They preserve exact event flags/amounts, source flags=0, actual selected rank versus unknown rank IDs, magical spell exclusion and rear/cast/control gates. Guaranteed crit and one-damage companion engagement are labeled synthetic fixtures; creature and spell fields remain real. NO_BLOCK is covered by the pure helper because no current reference template carries that flag. Ready-five passes seven additional Vitest and twenty Python checks. The final 8 GB TypeScript/Vite production build, pre/postbuild audits, compression and release-bundle verification pass; the subsequent TypeScript check includes the final integration test file. Independent Luna review found no concrete defect. Ordinary runtime callers have no modeled unsummoned Totem identity; that subtype must be exposed if such static dungeon actors are added. The frozen production copy has 5,964 files, 116,755,437 bytes and aggregate SHA-256 `864781d16c3c0a26457b6acd7a9bd31357116f856e6b27258b5b633d2f224fac`. Mutable dist stayed identical before/after the verified copy. The scoped one-boss browser run `20261005-202545227` clears Cellblock Rush in 03:18.0, Easy/Normal, with Hamhock (`creature-79098`) as its actual seeded objective. Required and killed IDs agree 1/1, all five actors finish alive and no death or resurrection recovery is observed. Nine effective heals total 2,801.9 HP. Six party-to-hostile blocks include two Revenge and two Sinister Strike spell hits, with event-level block amounts; one correlated Revenge result explicitly records the independent post-hit roll. No live crit-plus-block was captured; that behavior remains deterministic integration evidence. Snapshots sample the latest 200-event ring and are not a lossless stream. The frozen build and complete content manifest stay unchanged. All 2,124 served content objects match their hash addresses and manifest references. Root reviewed roster, entrance, route and native results screenshots: bodies and equipped silhouettes render, while close-party overlap, edge occlusion and compact-roster name clipping remain visible. Page errors are zero; the only console item is a favicon 404. Exact provenance, preserved failed entrance attempt and cleanup are in `docs/STOCKADE_SPELL_BLOCKING_PLAYTEST_2026-10-05.md`. ## Core spell gates `Unit::SpellHitResult` dispatches melee and ranged damage-class spells through `Unit::MeleeSpellHitResult`; magic and unclassified damage spells use another path. The spell damage-class values are 0 (none), 1 (magic), 2 (melee), and 3 (ranged). The normal physical school mask is bit `0x1`. The hit-table phase can return a full `SPELL_MISS_BLOCK` only when all of these source gates allow it: - Damage class is melee or ranged, so the spell uses `MeleeSpellHitResult`. - `SPELL_ATTR3_COMPLETELY_BLOCKED` is set (`attributes[3] & 0x8`). - AzerothCore's computed `SPELL_ATTR0_CU_DIRECT_DAMAGE` is absent (`customAttributes & 0x100` is zero). - Neither `SPELL_ATTR3_ALWAYS_HIT` (`attributes[3] & 0x40000`) nor `SPELL_ATTR0_NO_ACTIVE_DEFENSE` (`attributes[0] & 0x200000`) is set. - An attack from behind disables block unless the victim has `SPELL_AURA_IGNORE_HIT_DIRECTION`. If the spell has the core custom flag `SPELL_ATTR0_CU_REQ_CASTER_BEHIND_TARGET` (`customAttributes & 0x20000`), that aura does not bypass the hit-table facing check. - Creature template `flags_extra` does not include `CREATURE_FLAG_EXTRA_NO_BLOCK` (`0x10`), and the victim is not a Totem. - The skill-adjusted block chance remains positive, and the victim is not performing a non-melee spell cast or in `UNIT_STATE_CONTROLLED`. The hit-table roll runs after miss and mechanic-resist checks. Its block chance is truncated to integer hundredths before applying skill difference: `int32(blockChance * 100) - skillDiff * 4`, where `skillDiff = attackerWeaponSkill - defenderMaxSkill`. This subtracts 0.04 percentage points per skill point. AzerothCore uses level times five for the defender's max skill; ranged non-weapon spells also use attacker level times five, while other cases use the attacker's actual attack-type weapon skill. Apply the adjustment before checking whether chance is positive: a raw chance of zero can become positive when attacker skill is below defender maximum. The post-hit phase handles direct physical spell packets after they have landed. `SpellMgr` marks direct effects with `SPELL_ATTR0_CU_DIRECT_DAMAGE`; its effect list includes school damage (2), weapon damage without school (17), weapon percent damage (31), weapon damage (58), normalized weapon damage (121), and healing (10). For an incoming damage packet, `Unit::CalculateSpellDamageTaken` attempts this later block only when damage class is melee or ranged and the packet school includes physical bit `0x1`. It does not require `SPELL_ATTR3_COMPLETELY_BLOCKED`. The later `Unit::isSpellBlocked` path bypasses block for `NO_ACTIVE_DEFENSE` or `ALWAYS_HIT`; otherwise it requires a front-facing victim or an active ignore-hit-direction aura, no `NO_BLOCK` creature flag, a positive chance, and no non-melee cast or controlled state. It does not independently inspect `CU_REQ_CASTER_BEHIND_TARGET`. Its chance formula is the float expression `blockChance + (attackerWeaponSkill - defenderMaxSkill) * 0.04`, so higher attacker skill raises this post-hit block chance. A successful block subtracts the creature's block value from damage and can coexist with a critical hit. If the block consumes all damage, Spell.cpp can report the result as `SPELL_MISS_BLOCK`. Apply the skill adjustment before checking positivity here as well; a zero raw chance can become positive when attacker skill exceeds defender maximum. `SPELL_ATTR7_NO_ATTACK_DODGE` (`attributes[7] & 0x00800000`) and `SPELL_ATTR7_NO_ATTACK_PARRY` (`attributes[7] & 0x01000000`) suppress those avoidance results only; they do not disable block. School alone does not make a magic-class spell blockable, and ranged damage class does not by itself disable a source-authorized block. ## Spell metadata and helper contract `EnemyNpcSpellDefenseMetadata` mirrors the exact metadata fields added to source-backed ability ranks: `damageClass`, `schoolMask`, `attributes0`, `attributes3`, `directDamageFromEffects`, and `sourceProfile`. The helper also accepts optional `customAttributes` for callers that have a sourced or reconstructed AzerothCore custom word. Without that word, a non-direct spell classifies as `none`; a client DBC row alone cannot prove that SQL or server corrections did not set a custom flag. - `classifyEnemyNpcSpellBlockPhase` returns `direct-damage` only with positive direct-effect evidence, a melee/ranged class, and physical school bit; it returns `hit-table` only when explicit custom attributes show the direct flag is absent and the blockable-spell attribute is present. - `enemyNpcSpellBlockChanceAfterSkill` takes the attacker's actual weapon skill and defender level. This avoids inventing a weapon requirement or guessing skill from spell DBC equipment masks. - `canEnemyNpcBlockSpell` applies Totem, `NO_BLOCK`, direction, cast/control, and positive skill-adjusted chance gates for either phase. The source-profile type currently accepts `wow335` and `ascension`, matching the imported spell-rank metadata contract. The pinned Epoch Spell.dbc is available for adjacent aura and stance contracts, but this helper does not accept Epoch rank metadata until that profile is included in the importer contract. Two direct-effect candidates have exact local profile rows: | Spell | WoW 3.3.5a | Ascension patch-D | Epoch | | --- | --- | --- | --- | | Heroic Strike (78) | class 2, school 1, attrs0 327700, attrs3 1024, weapon damage effect 58 | class 2, school 1, attrs0 327700, attrs3 1024, weapon damage effect 58 | same as WoW 3.3.5a | | Steady Shot (56641) | class 3, school 1, attrs0 4259858, attrs3 0, direct effect 2 | class 3, school 1, attrs0 65554, attrs3 0, direct effects 2 and 31 | same as WoW 3.3.5a | These effects positively establish the direct custom flag under the historical core's SpellMgr effect rules. Ascension's Steady Shot has a different set of attribute words and a weapon-percent-damage effect, but retains ranged class and physical school. The helper does not hard-code these spell IDs. ## Pinned source locations Read-only historical core: `../wow-pvt-server-backups-2/azerothcore-wotlk`. | Source | Relevant code | | --- | --- | | `src/server/game/Entities/Unit/Unit.cpp` | `MeleeSpellHitResult` at 3330–3500; `isSpellBlocked` at 3276–3299; `CalculateSpellDamageTaken` at 1508–1640; dispatch by damage class at 3680–3770; `GetUnitBlockChance` at 3873–3895. | | `src/server/game/Spells/SpellInfo.h` | Core custom flags including `CU_DIRECT_DAMAGE = 0x100` and `CU_REQ_CASTER_BEHIND_TARGET = 0x20000` near 185–194. | | `src/server/shared/SharedDefines.h` | `NO_ACTIVE_DEFENSE = 0x200000`, `COMPLETELY_BLOCKED = 0x8`, `ALWAYS_HIT = 0x40000`, and damage-class values near lines 391, 484, 499, and 1552; no-dodge/no-parry attributes near 652–653. | | `src/server/game/Spells/SpellMgr.cpp` | Computes `CU_DIRECT_DAMAGE` for the listed effect types near 3210–3230. | | `src/server/game/Spells/Spell.cpp` | Applies post-hit block output after spell damage calculation near 2760–2820. | The source profile pins are already recorded by [`enemySourceAuras.ts`](../src/game/enemySourceAuras.ts) and [`enemyStances.ts`](../src/game/enemyStances.ts): Ascension patch-D Spell.dbc SHA-256 `2f77f18b91ffbae32a19568e4bc05ec78ee384b6ba8dd2a6e05f0e97abbb2dce`, and Epoch Spell.dbc SHA-256 `d5cce1a83550dcfa9eb2f0251dbb11fd24c272534b2b1a9b230924a44d817ab3`. The WoW 3.3.5a candidate rows above came from `../wow335a/Data/enUS/patch-enUS.MPQ` Spell.dbc. Their raw row fields are parsed by `scripts/generate-wow335-abilities.mjs` at DBC fields 4–11 (attributes), 213 (damage class), 225 (school), and 71–73 (effects), including the six-field schema shift for newer DBC records. The extracted stock Spell.dbc is 46,699,377 bytes, 46,583 rows and 239 fields, SHA-256 `9f0088da35c5ed0b5106cd20e422611489e435b74043e520e02221e0ce1b1af1`.