14 KiB
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_BLOCKEDis set (attributes[3] & 0x8).- AzerothCore's computed
SPELL_ATTR0_CU_DIRECT_DAMAGEis absent (customAttributes & 0x100is zero). - Neither
SPELL_ATTR3_ALWAYS_HIT(attributes[3] & 0x40000) norSPELL_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 flagSPELL_ATTR0_CU_REQ_CASTER_BEHIND_TARGET(customAttributes & 0x20000), that aura does not bypass the hit-table facing check. - Creature template
flags_extradoes not includeCREATURE_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.
classifyEnemyNpcSpellBlockPhasereturnsdirect-damageonly with positive direct-effect evidence, a melee/ranged class, and physical school bit; it returnshit-tableonly when explicit custom attributes show the direct flag is absent and the blockable-spell attribute is present.enemyNpcSpellBlockChanceAfterSkilltakes the attacker's actual weapon skill and defender level. This avoids inventing a weapon requirement or guessing skill from spell DBC equipment masks.canEnemyNpcBlockSpellapplies 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.