Files
healer-man/src/game/enemyControlImmunityPurge.ts
T
2026-10-07 13:05:04 -04:00

330 lines
13 KiB
TypeScript

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<object>();
type ExactApplyAura = Extract<AbilityDefinition["effects"][number], { kind: "apply-aura" }>;
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 };
}