import type { AbilityDefinition } from "./abilityCatalog"; import type { ActiveAura } from "./combatAuras"; export type EnemyControlImmunityMechanic = "charm" | "confuse" | "fear" | "sleep" | "stun"; export type EnemyControlCastBlockMechanic = Extract< EnemyControlImmunityMechanic, "confuse" | "fear" | "stun" >; export interface EnemyControlImmunityPurgeProof { readonly sourceProfile: "wow335"; readonly sourceSpellId: 18499 | 46924 | 49039; /** Mechanic immunities proven by the exact selected source aura/effect. */ readonly grantsMechanics: readonly EnemyControlImmunityMechanic[]; /** Every named shout aura below has spell-level Mechanic=FEAR. */ readonly purgesNamedShoutSpellMechanics: readonly "fear"[]; /** Core cast-prevention bypasses proven for this selected spell. */ readonly usableWhileControlledBy: readonly EnemyControlCastBlockMechanic[]; /** Explicit source flags that permit casts through a control aura without purging it. */ readonly directCastBypasses: readonly EnemyControlCastBlockMechanic[]; } const IMMUNITY_PURGES_EFFECT = 0x00008000; const NAMED_SHOUT_AURA_IDS = new Set([ "enemy-control:source-spell-5246", "enemy-control:source-spell-19134", "enemy-control:source-spell-20511", "enemy-control:source-spell-29544", ]); const VERIFIED_PROOFS = new WeakSet(); type ExactApplyAura = Extract; function isExactApplyAura( effect: AbilityDefinition["effects"][number] | undefined, expected: { readonly index: number; readonly spellId: 18499 | 46924 | 49039; readonly auraType: number; readonly miscValue: number; readonly basePoints: number; readonly dieSides: number; readonly triggerSpellId?: number; }, ): effect is ExactApplyAura { return effect?.kind === "apply-aura" && effect.sourceEffectType === 6 && effect.sourceAuraType === expected.auraType && effect.sourceBasePoints === expected.basePoints && effect.sourceDieSides === expected.dieSides && effect.sourcePointsPerLevel === 0 && effect.miscValue === expected.miscValue && effect.triggerSpellId === (expected.triggerSpellId ?? 0) && effect.recipient === "caster" && effect.aura.id === `wow335:${expected.spellId}:effect:${expected.index}:aura:${expected.auraType}`; } function verifiedProof( proof: EnemyControlImmunityPurgeProof, ): EnemyControlImmunityPurgeProof { const frozen = Object.freeze(proof); VERIFIED_PROOFS.add(frozen); return frozen; } function defenseMatches( ability: AbilityDefinition, expected: { readonly spellId: 18499 | 46924 | 49039; readonly classId: "warrior" | "death-knight"; readonly attributes0: number; readonly attributes1: number; readonly attributes3: number; readonly attributes5: number; readonly damageClass: number; readonly schoolMask: number; }, ): boolean { const defense = ability.wowSpellDefense; return ability.dbcSpellId === expected.spellId && ability.classId === expected.classId && ability.source === "wow335-progression" && defense?.sourceProfile === "wow335" && defense.damageClass === expected.damageClass && defense.schoolMask === expected.schoolMask && defense.attributes0 === expected.attributes0 && defense.attributes1 === expected.attributes1 && defense.attributes3 === expected.attributes3 && defense.attributes5 === expected.attributes5 && defense.directDamageFromEffects === false && Number.isInteger(defense.attributes1) && Number.isInteger(defense.attributes5) && (defense.attributes1 & IMMUNITY_PURGES_EFFECT) !== 0; } function berserkerRageProof(ability: AbilityDefinition): EnemyControlImmunityPurgeProof | undefined { if (ability.id !== "wow335-warrior-berserker-rage" || ability.name !== "Berserker Rage" || ability.target !== "self" || ability.effects.length !== 3 || !defenseMatches(ability, { spellId: 18499, classId: "warrior", attributes0: 327696, attributes1: 32768, attributes3: 0, attributes5: 0, damageClass: 0, schoolMask: 1, })) return undefined; const [fear, sap, incapacitate] = ability.effects; if (!isExactApplyAura(fear, { index: 0, spellId: 18499, auraType: 77, miscValue: 5, basePoints: -1, dieSides: 1, }) || !fear.aura.controlImmunities?.includes("fear") || !isExactApplyAura(sap, { index: 1, spellId: 18499, auraType: 77, miscValue: 14, basePoints: -1, dieSides: 1, }) || !isExactApplyAura(incapacitate, { index: 2, spellId: 18499, auraType: 77, miscValue: 30, basePoints: 0, dieSides: 0, })) return undefined; return verifiedProof({ sourceProfile: "wow335", sourceSpellId: 18499, grantsMechanics: Object.freeze(["fear"]), purgesNamedShoutSpellMechanics: Object.freeze(["fear"]), usableWhileControlledBy: Object.freeze(["fear"]), directCastBypasses: Object.freeze([]), }); } function bladestormProof(ability: AbilityDefinition): EnemyControlImmunityPurgeProof | undefined { if (ability.id !== "wow335-warrior-bladestorm" || ability.name !== "Bladestorm" || ability.target !== "self" || ability.effects.length !== 4 || !defenseMatches(ability, { spellId: 46924, classId: "warrior", attributes0: 327696, attributes1: 32784, attributes3: 1024, attributes5: 512, damageClass: 2, schoolMask: 1, })) return undefined; const sourceAuras = ability.effects.filter((effect) => effect.kind === "apply-aura"); if (sourceAuras.length !== 3 || ability.effects.filter((effect) => effect.kind === "dot").length !== 1 || ability.effects.some((effect) => effect.kind !== "apply-aura" && effect.kind !== "dot")) return undefined; const [periodic, immunity, dummy] = sourceAuras; const immunityMechanics = immunity.kind === "apply-aura" ? immunity.aura.controlImmunities ?? [] : []; if (!isExactApplyAura(periodic, { index: 0, spellId: 46924, auraType: 23, miscValue: 0, basePoints: -1, dieSides: 1, triggerSpellId: 50622, }) || !isExactApplyAura(immunity, { index: 1, spellId: 46924, auraType: 147, miscValue: 1733, basePoints: -1, dieSides: 1, }) || !immunityMechanics.includes("fear") || !immunityMechanics.includes("stun") || !isExactApplyAura(dummy, { index: 2, spellId: 46924, auraType: 263, miscValue: 0, basePoints: -1, dieSides: 1, })) return undefined; return verifiedProof({ sourceProfile: "wow335", sourceSpellId: 46924, grantsMechanics: Object.freeze(["fear", "stun"]), purgesNamedShoutSpellMechanics: Object.freeze(["fear"]), usableWhileControlledBy: Object.freeze([]), directCastBypasses: Object.freeze([]), }); } function lichborneProof(ability: AbilityDefinition): EnemyControlImmunityPurgeProof | undefined { if (ability.id !== "wow335-death-knight-lichborne" || ability.name !== "Lichborne" || ability.target !== "self" || ability.effects.length !== 3 || !defenseMatches(ability, { spellId: 49039, classId: "death-knight", attributes0: 262144, attributes1: 163872, attributes3: 0, attributes5: 393224, damageClass: 0, schoolMask: 32, })) return undefined; const [charm, fear, sleep] = ability.effects; if (!isExactApplyAura(charm, { index: 0, spellId: 49039, auraType: 77, miscValue: 1, basePoints: 0, dieSides: 1, }) || !isExactApplyAura(fear, { index: 1, spellId: 49039, auraType: 77, miscValue: 5, basePoints: 0, dieSides: 0, }) || !fear.aura.controlImmunities?.includes("fear") || !isExactApplyAura(sleep, { index: 2, spellId: 49039, auraType: 77, miscValue: 10, basePoints: 0, dieSides: 0, }) || !sleep.aura.controlImmunities?.includes("sleep")) return undefined; return verifiedProof({ sourceProfile: "wow335", sourceSpellId: 49039, grantsMechanics: Object.freeze(["charm", "fear", "sleep"]), purgesNamedShoutSpellMechanics: Object.freeze(["fear"]), usableWhileControlledBy: Object.freeze(["fear", "stun", "confuse"]), directCastBypasses: Object.freeze(["fear", "stun", "confuse"]), }); } /** Returns a source-pinned purge/cast proof only for the three audited WoW 3.3.5a rows. */ export function enemyControlImmunityPurgeProofForAbility( ability: AbilityDefinition | null | undefined, ): EnemyControlImmunityPurgeProof | undefined { if (!ability) return undefined; if (ability.dbcSpellId === 18499) return berserkerRageProof(ability); if (ability.dbcSpellId === 46924) return bladestormProof(ability); if (ability.dbcSpellId === 49039) return lichborneProof(ability); return undefined; } /** Whether the pinned source cast rules permit this selected spell during one control mechanic. */ export function enemyControlAbilityUsableWhileControlled( ability: AbilityDefinition | null | undefined, mechanic: EnemyControlImmunityMechanic, ): boolean { return enemyControlImmunityPurgeProofForAbility(ability)?.usableWhileControlledBy .some((allowed) => allowed === mechanic) ?? false; } const NON_ACTION_BLOCKING_CONTROL_MECHANICS = new Set(["root", "slow", "interrupt"]); /** * Berserker Rage and Lichborne can use the Fear-mechanic 20511 child while it * applies a stun effect. Lichborne's exact row also permits separate stun and * confuse states; those auras stay active because the row grants no such purge. */ export function enemyControlAbilityCanEscapeAuras( ability: AbilityDefinition | null | undefined, mechanic: EnemyControlCastBlockMechanic, auras: readonly ActiveAura[], targetId: string, now: number, ): boolean { const proof = enemyControlImmunityPurgeProofForAbility(ability); if (!proof || !Number.isFinite(now)) return false; const activeControlAuras = auras.filter((aura) => aura.targetId === targetId && (aura.expiresAt === null || aura.expiresAt > now) && aura.definition.id.startsWith("enemy-control:")); if (!activeControlAuras.length) return false; const allPreventingAurasAreCovered = activeControlAuras.every((aura) => { const id = aura.definition.id; if (NAMED_SHOUT_AURA_IDS.has(id)) { return proof.purgesNamedShoutSpellMechanics.includes("fear"); } const suffix = id.slice("enemy-control:".length); if (suffix.startsWith("source-spell-")) return false; const sourceMechanic = suffix.split(":").at(-1); return sourceMechanic !== undefined && (NON_ACTION_BLOCKING_CONTROL_MECHANICS.has(sourceMechanic) || proof.directCastBypasses.some((allowed) => allowed === sourceMechanic)); }); if (!allPreventingAurasAreCovered) return false; if (mechanic === "fear") { const canPurgeNamedShout = proof.purgesNamedShoutSpellMechanics.includes("fear") && activeControlAuras.some((aura) => NAMED_SHOUT_AURA_IDS.has(aura.definition.id)); const canBypassGenericFear = proof.directCastBypasses.includes("fear") && activeControlAuras.some((aura) => { const suffix = aura.definition.id.slice("enemy-control:".length); return !NAMED_SHOUT_AURA_IDS.has(aura.definition.id) && !suffix.startsWith("source-spell-") && suffix.split(":").at(-1) === "fear"; }); return enemyControlAbilityUsableWhileControlled(ability, "fear") && (canPurgeNamedShout || canBypassGenericFear); } if (mechanic === "stun") { const canCastThroughSourceChild = activeControlAuras.some( (aura) => aura.definition.id === "enemy-control:source-spell-20511", ) && (enemyControlAbilityUsableWhileControlled(ability, "fear") || enemyControlAbilityUsableWhileControlled(ability, "stun")); const canCastThroughDirectStun = proof.directCastBypasses.includes("stun") && activeControlAuras.some((aura) => { const suffix = aura.definition.id.slice("enemy-control:".length); return !suffix.startsWith("source-spell-") && suffix.split(":").at(-1) === "stun"; }); return canCastThroughSourceChild || canCastThroughDirectStun; } return mechanic === "confuse" && proof.directCastBypasses.includes("confuse") && activeControlAuras.some((aura) => aura.definition.id.slice("enemy-control:".length) .split(":").at(-1) === "confuse"); } export interface EnemyControlShoutAuraPurgeResult { readonly auras: readonly ActiveAura[]; readonly removed: readonly ActiveAura[]; } /** Remove only active, same-target source shout auras covered by a proven FEAR purge. */ export function purgeNamedEnemyControlShoutAuras( auras: readonly ActiveAura[], targetId: string, proof: EnemyControlImmunityPurgeProof | undefined, now: number, ): EnemyControlShoutAuraPurgeResult { if (!proof || proof.sourceProfile !== "wow335" || !VERIFIED_PROOFS.has(proof) || !proof.purgesNamedShoutSpellMechanics.includes("fear") || !Number.isFinite(now)) return { auras, removed: [] }; const removed: ActiveAura[] = []; const retained = auras.filter((aura) => { const isActive = aura.expiresAt === null || aura.expiresAt > now; const isNamedShout = NAMED_SHOUT_AURA_IDS.has(aura.definition.id); if (isActive && isNamedShout && aura.targetId === targetId) { removed.push(aura); return false; } return true; }); return { auras: retained, removed }; }