230 lines
14 KiB
Markdown
230 lines
14 KiB
Markdown
# 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`.
|