/** AzerothCore CREATURE_FLAG_EXTRA_NO_BLOCK from CreatureData.h. */ export const CREATURE_FLAG_EXTRA_NO_BLOCK = 0x00000010; /** AzerothCore Spell.dbc damage-class values used by spell hit resolution. */ export const SPELL_DAMAGE_CLASS_MAGIC = 1; export const SPELL_DAMAGE_CLASS_MELEE = 2; export const SPELL_DAMAGE_CLASS_RANGED = 3; /** AzerothCore SpellSchoolMask bit for physical damage. */ export const SPELL_SCHOOL_MASK_NORMAL = 0x00000001; /** Source-backed spell attributes relevant to blocking ordinary creatures. */ export const SPELL_ATTR0_NO_ACTIVE_DEFENSE = 0x00200000; export const SPELL_ATTR3_COMPLETELY_BLOCKED = 0x00000008; export const SPELL_ATTR3_ALWAYS_HIT = 0x00040000; export const SPELL_ATTR0_CU_DIRECT_DAMAGE = 0x00000100; export const SPELL_ATTR0_CU_REQ_CASTER_BEHIND_TARGET = 0x00020000; export const SPELL_ATTR0_CU_BLOCKING_KNOWN_MASK = SPELL_ATTR0_CU_DIRECT_DAMAGE | SPELL_ATTR0_CU_REQ_CASTER_BEHIND_TARGET; export type EnemyNpcSpellBlockPhase = "none" | "hit-table" | "direct-damage"; export type EnemyNpcSpellSourceProfile = "wow335" | "ascension"; /** * The exact spell scalars needed by the ordinary WoW creature block paths. * `customAttributes` is optional because Spell.dbc does not store AzerothCore's * computed AttributesCu word. Its absence deliberately prevents inferring that * a non-direct spell is eligible for the hit-table full-block result. */ export interface EnemyNpcSpellDefenseMetadata { readonly damageClass: number; readonly schoolMask: number; readonly attributes0: number; /** Raw flags retained for source-proven immunity purge and controlled casting. */ readonly attributes1?: number; readonly attributes3: number; readonly attributes5?: number; readonly directDamageFromEffects: boolean; readonly sourceProfile: EnemyNpcSpellSourceProfile; /** Full AzerothCore AttributesCu word, only when imported/reconstructed exactly. */ readonly customAttributes?: number; /** Explicitly proven subset of AttributesCu when the full word is not known. */ readonly customAttributeEvidence?: { readonly knownMask: number; readonly value: number; }; } /** * Returns the narrowly audited Shield Bash, Kick and Pummel hit-table contract. Its two block- * relevant AttributesCu bits are sourced independently; this does not assert * that all other custom bits are zero. */ export function enemyNpcSourceSpellBlockMetadata( spellId: number, sourceProfile: EnemyNpcSpellSourceProfile, ): EnemyNpcSpellDefenseMetadata | undefined { if (![72, 1766, 6552].includes(spellId) || sourceProfile !== "wow335") return undefined; return Object.freeze({ damageClass: SPELL_DAMAGE_CLASS_MELEE, schoolMask: SPELL_SCHOOL_MASK_NORMAL, attributes0: 327696, attributes3: SPELL_ATTR3_COMPLETELY_BLOCKED, directDamageFromEffects: false, sourceProfile: "wow335", customAttributeEvidence: Object.freeze({ knownMask: SPELL_ATTR0_CU_BLOCKING_KNOWN_MASK, value: 0, }), }); } function customAttributeBit( spell: EnemyNpcSpellDefenseMetadata, bit: number, ): boolean | null { if (spell.customAttributes !== undefined) return (spell.customAttributes & bit) !== 0; const evidence = spell.customAttributeEvidence; if (!evidence || (evidence.knownMask & bit) !== bit) return null; return (evidence.value & bit) !== 0; } function isSpellBlockClassAndSchool(spell: EnemyNpcSpellDefenseMetadata): boolean { return (spell.damageClass === SPELL_DAMAGE_CLASS_MELEE || spell.damageClass === SPELL_DAMAGE_CLASS_RANGED) && (spell.schoolMask & SPELL_SCHOOL_MASK_NORMAL) !== 0; } /** * Classifies which AzerothCore block phase a spell can enter for ordinary * enemy Creatures. Direct-damage effects are a positive signal for the later * damage-reduction phase. Hit-table full blocks require the computed core * custom-attribute word so an absent CU_DIRECT_DAMAGE bit is never guessed * from a client DBC row alone. */ export function classifyEnemyNpcSpellBlockPhase( spell: EnemyNpcSpellDefenseMetadata, ): EnemyNpcSpellBlockPhase | null { if (!Number.isInteger(spell.damageClass) || spell.damageClass < 0 || spell.damageClass > 3 || !Number.isInteger(spell.schoolMask) || spell.schoolMask < 0 || spell.schoolMask > 0xFFFFFFFF || !Number.isInteger(spell.attributes0) || spell.attributes0 < 0 || spell.attributes0 > 0xFFFFFFFF || !Number.isInteger(spell.attributes3) || spell.attributes3 < 0 || spell.attributes3 > 0xFFFFFFFF || typeof spell.directDamageFromEffects !== "boolean" || (spell.sourceProfile !== "wow335" && spell.sourceProfile !== "ascension")) return null; const hasCustomAttributes = spell.customAttributes !== undefined; if (hasCustomAttributes && (!Number.isInteger(spell.customAttributes) || spell.customAttributes! < 0 || spell.customAttributes! > 0xFFFFFFFF)) return null; const customEvidence = spell.customAttributeEvidence; const invalidCustomEvidence = customEvidence && (!Number.isInteger(customEvidence.knownMask) || customEvidence.knownMask < 0 || customEvidence.knownMask > 0xFFFFFFFF || !Number.isInteger(customEvidence.value) || customEvidence.value < 0 || customEvidence.value > 0xFFFFFFFF || (((customEvidence.value & ~customEvidence.knownMask) >>> 0) !== 0) || (hasCustomAttributes && (((spell.customAttributes! & customEvidence.knownMask) >>> 0) !== customEvidence.value))); if (invalidCustomEvidence) return null; // Both source paths bypass blocking for these attributes. if ((spell.attributes0 & SPELL_ATTR0_NO_ACTIVE_DEFENSE) !== 0 || (spell.attributes3 & SPELL_ATTR3_ALWAYS_HIT) !== 0) return "none"; const directDamageAttribute = customAttributeBit(spell, SPELL_ATTR0_CU_DIRECT_DAMAGE); const hasDirectDamageAttribute = spell.directDamageFromEffects || directDamageAttribute === true; if (hasDirectDamageAttribute) { return isSpellBlockClassAndSchool(spell) ? "direct-damage" : "none"; } // AttributesCu is assembled by AzerothCore from effects/corrections/SQL and // is not a client Spell.dbc field. Without that exact word, fail closed. if (directDamageAttribute !== false) return "none"; if ((spell.damageClass === SPELL_DAMAGE_CLASS_MELEE || spell.damageClass === SPELL_DAMAGE_CLASS_RANGED) && (spell.attributes3 & SPELL_ATTR3_COMPLETELY_BLOCKED) !== 0) return "hit-table"; return "none"; } /** * Skill-adjusted chance for the two spell paths. The hit-table path truncates * the raw chance to integer hundredths and subtracts skill difference. The * later direct-damage path follows isSpellBlocked's float formula and adds * skill difference instead. */ export function enemyNpcSpellBlockChanceAfterSkill(input: { readonly phase: Exclude; readonly blockPercent: number; readonly attackerWeaponSkill: number; readonly defenderLevel: number; }): number | null { if (input.phase !== "hit-table" && input.phase !== "direct-damage") return null; if (!Number.isFinite(input.blockPercent) || input.blockPercent < 0 || !Number.isSafeInteger(input.attackerWeaponSkill) || input.attackerWeaponSkill < 0 || !Number.isInteger(input.defenderLevel) || input.defenderLevel < 1 || input.defenderLevel > 80) return null; const skillDiff = input.attackerWeaponSkill - input.defenderLevel * 5; if (input.phase === "direct-damage") { return Math.max(0, input.blockPercent + skillDiff * 0.04); } return Math.max(0, Math.trunc(input.blockPercent * 100) - skillDiff * 4) / 100; } /** * Applies source-backed hit/block gates for an ordinary enemy Creature spell. * Direct-damage spells use the post-hit physical reduction phase; explicitly * blockable non-direct spells use the separate hit-table phase. `attackerWeaponSkill` * is required because player weapon skill cannot be recovered from spell DBC * damage class or equipped-item requirements. */ export function canEnemyNpcBlockSpell(input: { readonly spell: EnemyNpcSpellDefenseMetadata; readonly blockPercent: number; readonly flagsExtra: number; readonly isTotem: boolean; readonly attackerWeaponSkill: number; readonly defenderLevel: number; readonly attackerIsBehind: boolean; readonly ignoresHitDirection: boolean; readonly isCastingNonMeleeSpell: boolean; readonly isControlled: boolean; }): boolean { if (!Number.isFinite(input.blockPercent) || input.blockPercent < 0 || !Number.isInteger(input.flagsExtra) || input.flagsExtra < 0 || input.flagsExtra > 0xFFFFFFFF || typeof input.isTotem !== "boolean" || !Number.isSafeInteger(input.attackerWeaponSkill) || input.attackerWeaponSkill < 0 || !Number.isInteger(input.defenderLevel) || input.defenderLevel < 1 || input.defenderLevel > 80 || typeof input.attackerIsBehind !== "boolean" || typeof input.ignoresHitDirection !== "boolean" || typeof input.isCastingNonMeleeSpell !== "boolean" || typeof input.isControlled !== "boolean") return false; const phase = classifyEnemyNpcSpellBlockPhase(input.spell); if (phase === null || phase === "none") return false; if (input.isTotem || (input.flagsExtra & CREATURE_FLAG_EXTRA_NO_BLOCK) !== 0) return false; if (input.isCastingNonMeleeSpell || input.isControlled) return false; // The hit-table path honors CU_REQ_CASTER_BEHIND_TARGET when checking the // ignore-hit-direction aura exception. The later direct-damage helper only // checks front-or-ignore-direction and has no separate spell-attribute gate. if (input.attackerIsBehind) { if (phase === "direct-damage") { if (!input.ignoresHitDirection) return false; } else { const requiresCasterBehindTarget = customAttributeBit( input.spell, SPELL_ATTR0_CU_REQ_CASTER_BEHIND_TARGET, ); if (!input.ignoresHitDirection || requiresCasterBehindTarget !== false) return false; } } const chanceAfterSkill = enemyNpcSpellBlockChanceAfterSkill({ phase, blockPercent: input.blockPercent, attackerWeaponSkill: input.attackerWeaponSkill, defenderLevel: input.defenderLevel, }); return chanceAfterSkill !== null && chanceAfterSkill > 0; } /** * Resolves a normal WoW creature's passive block chance and block value from * the historical AzerothCore formulas. `blockChanceAuraPercent` is the total * percentage-point amount from active SPELL_AURA_MOD_BLOCK_PERCENT auras. * Ordinary Creature objects in this AzerothCore branch leave UNIT_FIELD_STAT0 * at its zero-initialized value, so GetStat(STAT_STRENGTH) is 0 here. * * Returns null when the creature level is outside Healer Man's level-80 * source boundary, rather than clamping a missing or out-of-scope source row. */ export function resolveEnemyNpcBlockStats(input: { readonly level: number; readonly blockChanceAuraPercent: number; readonly isTotem: boolean; }): { readonly blockPercent: number; readonly blockValue: number } | null { if (!Number.isInteger(input.level) || input.level < 1 || input.level > 80) return null; if (!Number.isFinite(input.blockChanceAuraPercent)) return null; if (typeof input.isTotem !== "boolean") return null; const blockPercent = input.isTotem ? 0 : Math.max(0, 5 + input.blockChanceAuraPercent); return Object.freeze({ blockPercent, // Creature::GetShieldBlockValue(): level / 2 + uint32(GetStat(STR) / 20). // Ordinary creatures have zero STR in this core path; integer division is // observable for odd levels (for example, level 79 gives 39 block). blockValue: Math.trunc(input.level / 2), }); } export type EnemyNpcBlockAttack = "melee-auto" | "ranged-auto" | "spell" | "other"; /** * Resolves the ordinary weapon-hit chance after AzerothCore's skill adjustment. * The simulator maps attacker weapon skill to attacker level * 5; NPC defense * skill is the defender's effective level * 5. The returned chance preserves * AzerothCore's integer-hundredths truncation before the skill modifier. */ export function enemyNpcBlockChanceAfterSkill(input: { readonly blockPercent: number; readonly attackerLevel: number; readonly defenderLevel: number; }): number | null { if (!Number.isFinite(input.blockPercent)) return null; if (input.blockPercent <= 0) return 0; if (!Number.isInteger(input.attackerLevel) || input.attackerLevel < 1 || input.attackerLevel > 80) return null; if (!Number.isInteger(input.defenderLevel) || input.defenderLevel < 1 || input.defenderLevel > 80) return null; const attackerWeaponSkill = input.attackerLevel * 5; const defenderMaxSkill = input.defenderLevel * 5; const chanceHundredths = Math.trunc(input.blockPercent * 100) - 4 * (attackerWeaponSkill - defenderMaxSkill); return Math.max(0, chanceHundredths) / 100; } /** * Mirrors the explicit block gates supported for ordinary enemy weapon * auto-attacks. `blockPercent` is the raw pre-skill chance from * `resolveEnemyNpcBlockStats`; this applies the level adjustment only for the * positivity gate. Spell packets use the separate source-metadata helper * `canEnemyNpcBlockSpell`; spell and other packets fail closed in this helper. */ export function canEnemyNpcBlock(input: { readonly attack: EnemyNpcBlockAttack; readonly blockPercent: number; readonly flagsExtra: number; readonly isTotem: boolean; readonly attackerLevel: number; readonly defenderLevel: number; readonly attackerIsBehind: boolean; readonly ignoresHitDirection: boolean; readonly isCastingNonMeleeSpell: boolean; readonly isControlled: boolean; readonly isEvadingAttacks: boolean; }): boolean { if (!Number.isFinite(input.blockPercent) || input.blockPercent <= 0) return false; if (!Number.isInteger(input.flagsExtra) || typeof input.isTotem !== "boolean") return false; if (typeof input.attackerIsBehind !== "boolean" || typeof input.ignoresHitDirection !== "boolean" || typeof input.isCastingNonMeleeSpell !== "boolean" || typeof input.isControlled !== "boolean" || typeof input.isEvadingAttacks !== "boolean") return false; if (input.attack !== "melee-auto" && input.attack !== "ranged-auto") return false; const chanceAfterSkill = enemyNpcBlockChanceAfterSkill(input); if (chanceAfterSkill === null || chanceAfterSkill <= 0) return false; if (input.isTotem || (input.flagsExtra & CREATURE_FLAG_EXTRA_NO_BLOCK) !== 0) return false; if (input.isCastingNonMeleeSpell || input.isControlled) return false; // The ordinary weapon roll applies the facing gate to both melee and ranged // weapon attacks. It has no ranged-attack exclusion for blocking. if (input.attackerIsBehind && !input.ignoresHitDirection) return false; if (input.isEvadingAttacks) return false; return true; }