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

230 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`.