Files
healer-man/docs/NPC_SPELL_BLOCKING_2026-10-05.md
2026-10-06 22:25:34 -04:00

14 KiB
Raw Permalink Blame History

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 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 and 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.