Files
healer-man/src/game/wow335AbilityCatalog.test.ts
T
2026-09-30 13:25:55 -04:00

817 lines
36 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { abilityAtLevel, type AbilityDefinition, type AbilityEffect } from "./abilityCatalog";
import { aiHintsForAbility, targetingRulesForAbility } from "./abilityRules";
import {
WOW335_ABILITIES_BY_CLASS,
WOW335_TRIGGERED_ABILITIES_BY_SPELL_ID,
} from "./wow335AbilityCatalog";
import generatedData from "./wow335AbilityData.generated.json";
import { talentNodeById } from "./talentCatalog";
import { runtimeTalentNodeById } from "./talentRuntimeCatalog";
function byId(id: string): AbilityDefinition {
const ability = Object.values(WOW335_ABILITIES_BY_CLASS)
.flat()
.find((candidate) => candidate.id === id);
expect(ability, id).toBeDefined();
return ability!;
}
function effectsOfKind<K extends AbilityEffect["kind"]>(
ability: Pick<AbilityDefinition, "effects">,
kind: K,
): readonly Extract<AbilityEffect, { kind: K }>[] {
return ability.effects.filter(
(effect): effect is Extract<AbilityEffect, { kind: K }> => effect.kind === kind,
);
}
describe("WoW 3.3.5 executable effect mapping", () => {
it("uses the same legal talent-node set as the full catalog", () => {
const talentNodeIds = new Set(Object.values(generatedData.classes)
.flatMap((chains) => chains.flatMap((chain) => chain.talentNodeId ? [chain.talentNodeId] : [])));
for (const nodeId of talentNodeIds) {
expect(Boolean(runtimeTalentNodeById(nodeId)), nodeId).toBe(Boolean(talentNodeById(nodeId)));
}
});
it("preserves source spells that do not trigger a global cooldown", () => {
expect(byId("wow335-priest-inner-focus").gcdMs).toBe(0);
expect(byId("wow335-rogue-cold-blood").gcdMs).toBe(0);
expect(byId("wow335-warrior-shield-block").gcdMs).toBe(0);
expect(byId("wow335-priest-power-word-shield").gcdMs).toBe(1_500);
});
it("maps Stormstrike's generated weapon-hit children to their attacking hands", () => {
const stormstrike = abilityAtLevel(byId("wow335-shaman-stormstrike"), 80);
const chain = generatedData.classes.shaman.find((candidate) => candidate.name === "Stormstrike");
expect(stormstrike.dbcSpellId).toBe(17364);
expect(chain).toBeDefined();
const expectedHits = [32175, 32176].map((spellId) => {
const weaponEffect = chain!.ranks.find((rank) => rank.spellId === spellId)!
.effects.find((effect) => effect.effectType === 58)!;
expect(weaponEffect.coefficient).toBe(1);
return {
kind: "damage" as const,
coefficient: weaponEffect.coefficient,
school: "physical" as const,
weaponHand: spellId === 32175 ? "main-hand" as const : "off-hand" as const,
};
});
expect(effectsOfKind(stormstrike, "damage")).toEqual(expectedHits);
expect(stormstrike.effects).not.toEqual(expect.arrayContaining([
expect.objectContaining({ kind: "trigger-spell", spellId: 32175 }),
expect.objectContaining({ kind: "trigger-spell", spellId: 32176 }),
]));
expect(WOW335_TRIGGERED_ABILITIES_BY_SPELL_ID[32175]).toBeUndefined();
expect(WOW335_TRIGGERED_ABILITIES_BY_SPELL_ID[32176]).toBeUndefined();
expect(WOW335_ABILITIES_BY_CLASS.shaman.some((ability) => [32175, 32176].includes(ability.dbcSpellId ?? 0)))
.toBe(false);
});
it("keeps Bloodthirst's player cast, next-hit aura, and triggered heal on their source rows", () => {
const bloodthirst = abilityAtLevel(byId("wow335-warrior-bloodthirst"), 80);
expect(bloodthirst).toMatchObject({
dbcSpellId: 23881,
target: "hostile",
range: { min: 0, max: 5 },
cost: { resource: "rage", amount: 20 },
cooldownMs: 4_000,
});
expect(effectsOfKind(bloodthirst, "damage")).toHaveLength(1);
const bloodthirstAura = effectsOfKind(bloodthirst, "apply-aura")
.find((effect) => effect.sourceAuraType === 42);
expect(bloodthirstAura).toMatchObject({
recipient: "caster",
aura: {
durationMs: 8_000,
procs: [expect.objectContaining({
triggers: ["hit", "crit"],
attackKinds: ["melee"],
charges: 3,
action: { kind: "custom", id: "wow335-trigger-spell", data: { spellId: 23880 } },
})],
},
});
expect(WOW335_TRIGGERED_ABILITIES_BY_SPELL_ID[23880]?.effects).toEqual([
expect.objectContaining({ kind: "heal", percentMaxHealth: 0.01 }),
]);
});
it("selects Sweeping Strikes' player self-buff row instead of its damage child", () => {
const source = byId("wow335-warrior-sweeping-strikes");
const sweepingStrikes = abilityAtLevel(source, 80);
expect(source).toMatchObject({ unlockLevel: 30, sourceUnlockLevel: 30 });
expect(sweepingStrikes).toMatchObject({
dbcSpellId: 12328,
target: "self",
range: { min: 0, max: 0 },
cost: { resource: "rage", amount: 30 },
cooldownMs: 30_000,
});
expect(effectsOfKind(sweepingStrikes, "damage")).toEqual([]);
expect(effectsOfKind(sweepingStrikes, "apply-aura")).toEqual([
expect.objectContaining({
recipient: "caster",
aura: expect.objectContaining({
durationMs: 30_000,
procs: [expect.objectContaining({
triggers: ["hit", "crit"],
attackKinds: ["melee"],
charges: 5,
action: { kind: "custom", id: "wow335-sweeping-strikes-secondary-hit" },
})],
}),
}),
]);
});
it("maps Battle Shout and Commanding Shout to nearby party-wide buffs", () => {
for (const [abilityId, auraCount] of [
["wow335-warrior-battle-shout", 2],
["wow335-warrior-commanding-shout", 1],
] as const) {
const ability = abilityAtLevel(byId(abilityId), 80);
const auras = effectsOfKind(ability, "apply-aura");
expect(ability).toMatchObject({ target: "self", radius: 30 });
expect(targetingRulesForAbility(ability)).toMatchObject({ shape: "circle", radius: 30 });
expect(auras).toHaveLength(auraCount);
expect(auras.every((effect) => effect.scope === "party")).toBe(true);
}
});
it("maps Nature's Swiftness to one next-Nature-spell cast-time aura and its shared cooldown", () => {
const natureSwiftness = abilityAtLevel(byId("wow335-shaman-nature-s-swiftness"), 80);
const druidNatureSwiftness = abilityAtLevel(byId("wow335-druid-nature-s-swiftness"), 80);
const elementalMastery = abilityAtLevel(byId("wow335-shaman-elemental-mastery"), 80);
expect(natureSwiftness.cooldownGroup).toBe("shaman-nature-swiftness");
expect(elementalMastery.cooldownGroup).toBe("shaman-nature-swiftness");
expect(natureSwiftness.cooldownMs).toBe(180_000);
expect(druidNatureSwiftness.cooldownMs).toBe(180_000);
for (const ability of [natureSwiftness, druidNatureSwiftness]) {
const auraEffect = effectsOfKind(ability, "apply-aura")[0];
expect(auraEffect).toMatchObject({
recipient: "caster",
aura: {
durationMs: null,
utilityTags: ["wow335-next-nature-instant"],
},
});
if (auraEffect?.kind === "apply-aura") expect(auraEffect.aura.modifiers).toBeUndefined();
}
});
it("combines each Holy Nova damage rank with its matching healing payload", () => {
const nova = byId("wow335-priest-holy-nova");
expect(nova.ranks).toHaveLength(9);
for (const rank of nova.ranks ?? []) {
expect(effectsOfKind(rank, "damage")).toHaveLength(1);
expect(effectsOfKind(rank, "heal")).toHaveLength(1);
expect(rank.cost.amount).toBeGreaterThan(0);
}
expect(abilityAtLevel(nova, 80)).toMatchObject({
dbcSpellId: 48078,
target: "self",
radius: 10,
castMode: "instant",
});
});
it("keeps Holy Shock's cast wrapper while combining damage and healing ranks", () => {
const shock = byId("wow335-paladin-holy-shock");
expect(shock.ranks).toHaveLength(7);
for (const rank of shock.ranks ?? []) {
expect(effectsOfKind(rank, "damage")).toHaveLength(1);
expect(effectsOfKind(rank, "heal")).toHaveLength(1);
expect(rank.cost.amount).toBeGreaterThan(0);
expect(rank.cooldownMs).toBe(6_000);
}
expect(abilityAtLevel(shock, 80)).toMatchObject({
dbcSpellId: 48825,
target: "any",
range: { min: 0, max: 40 },
castMode: "instant",
gcdMs: 1_500,
cooldownMs: 6_000,
});
});
it("models Life Tap as a health cost and mana restoration, not a heal", () => {
const lifeTap = abilityAtLevel(byId("wow335-warlock-life-tap"), 80);
expect(lifeTap).toMatchObject({
dbcSpellId: 57946,
target: "self",
cost: { resource: "health", amount: 1_490 },
});
expect(effectsOfKind(lifeTap, "resource")).toContainEqual(
{ kind: "resource", resource: "mana", amount: 1_490 },
);
expect(effectsOfKind(lifeTap, "heal")).toEqual([]);
for (const id of [
"wow335-warlock-create-healthstone",
"wow335-warlock-create-soulstone",
"wow335-warlock-demonic-empowerment",
]) {
expect(effectsOfKind(abilityAtLevel(byId(id), 80), "heal"), id).toEqual([]);
}
});
it("maps Envenom to Deadly Poison dose damage and its poison-frequency buff", () => {
const envenom = abilityAtLevel(byId("wow335-rogue-envenom"), 80);
expect(effectsOfKind(envenom, "finisher-damage")).toEqual([
expect.objectContaining({
poison: expect.objectContaining({
maximumDoses: 5,
basePointsPerDose: 216,
attackPowerCoefficientPerDose: 0.09,
}),
}),
]);
expect(effectsOfKind(envenom, "apply-aura")).toEqual([
expect.objectContaining({
recipient: "caster",
aura: expect.objectContaining({
utilityTags: expect.arrayContaining(["rogue-envenom-poison-buff"]),
}),
}),
]);
});
it("maps the Ready DPS area actions to executable multi-target shapes", () => {
const multiShot = abilityAtLevel(byId("wow335-hunter-multi-shot"), 80);
expect(targetingRulesForAbility(multiShot)).toMatchObject({
shape: "chain",
maximumTargets: 3,
jumpRange: 10,
});
expect(aiHintsForAbility(multiShot).categories).toContain("area-damage");
const volley = abilityAtLevel(byId("wow335-hunter-volley"), 80);
expect(volley).toMatchObject({ target: "hostile", castMode: "channel", castTimeMs: 6_000, radius: 8 });
expect(targetingRulesForAbility(volley).shape).toBe("ground");
expect(effectsOfKind(volley, "dot")).toEqual([
expect.objectContaining({ ticks: 6, intervalMs: 1_000, tag: "volley" }),
]);
const fan = abilityAtLevel(byId("wow335-rogue-fan-of-knives"), 80);
expect(fan).toMatchObject({ target: "self", radius: 8 });
expect(targetingRulesForAbility(fan).shape).toBe("circle");
expect(aiHintsForAbility(fan).categories).toContain("area-damage");
});
it("gives every imported classic rank executable or explicit utility semantics", () => {
const effects = Object.values(WOW335_ABILITIES_BY_CLASS)
.flat()
.flatMap((ability) => ability.ranks?.flatMap((rank) => rank.effects) ?? ability.effects);
expect(effects.length).toBeGreaterThan(1_000);
expect(effectsOfKind({ effects }, "scripted")).toEqual([]);
expect(effectsOfKind({ effects }, "unsupported")).toEqual([]);
for (const effect of effectsOfKind({ effects }, "utility")) {
expect(effect.action).toMatch(/^(?:create-item|open-lock|portal|weapon-enchant|pickpocket|farsight|class-script)$/);
}
for (const ability of Object.values(WOW335_ABILITIES_BY_CLASS).flat()) {
if (ability.passive) continue;
for (const rank of ability.ranks ?? []) {
expect(rank.effects.length, `${ability.name} rank ${rank.rank}`).toBeGreaterThan(0);
const hasExecutableSemantics = rank.effects.some((effect) => {
if (["trigger-spell", "scripted", "unsupported"].includes(effect.kind)) return false;
if (effect.kind !== "apply-aura") return true;
return Boolean(
effect.control
|| effect.aura.modifiers?.length
|| effect.aura.absorbs?.length
|| effect.aura.procs?.length
|| effect.aura.utilityTags?.length
|| effect.aura.controlImmunities?.length
|| effect.aura.reflectSpellChance
|| effect.aura.redirectDamagePercent,
);
});
if (!hasExecutableSemantics && ability.talentNodeId) {
expect(rank.effects.some((effect) => effect.kind !== "scripted" && effect.kind !== "unsupported"),
`${ability.name} rank ${rank.rank}`).toBe(true);
} else {
expect(hasExecutableSemantics, `${ability.name} rank ${rank.rank}`).toBe(true);
}
}
}
});
it("does not promote triggered rank-zero child spells into player ranks", () => {
expect(byId("wow335-warrior-execute").ranks?.map((rank) => rank.spellId)).not.toContain(20647);
expect(byId("wow335-priest-divine-hymn").ranks?.map((rank) => rank.spellId)).toEqual([64843]);
expect(byId("wow335-death-knight-death-coil").ranks?.map((rank) => rank.spellId)).not.toContain(47632);
expect(byId("wow335-death-knight-death-coil")).toMatchObject({
target: "any",
effects: expect.arrayContaining([expect.objectContaining({ kind: "damage" })]),
});
});
it("freezes mana costs per learned rank while keeping form resources source-correct", () => {
expect(byId("wow335-mage-frostbolt").cost).toEqual({ resource: "mana", amount: 17 });
const pain = byId("wow335-priest-shadow-word-pain");
expect(abilityAtLevel(pain, 26)).toMatchObject({
dbcSpellId: 992,
cost: { resource: "mana", amount: 133 },
});
expect(abilityAtLevel(pain, 32)).toMatchObject({
dbcSpellId: 992,
cost: { resource: "mana", amount: 133 },
});
expect(abilityAtLevel(pain, 34)).toMatchObject({
dbcSpellId: 2767,
cost: { resource: "mana", amount: 188 },
});
expect(byId("wow335-druid-claw").cost).toMatchObject({ resource: "energy", amount: 45 });
expect(byId("wow335-druid-maul").cost).toMatchObject({ resource: "rage", amount: 15 });
});
it("never leaves an imported player mana cost tied to maximum mana", () => {
const manaRanks = Object.values(WOW335_ABILITIES_BY_CLASS)
.flat()
.flatMap((ability) => ability.ranks ?? [])
.filter((rank) => rank.cost.resource === "mana");
expect(manaRanks.length).toBeGreaterThan(100);
for (const rank of manaRanks) {
expect(rank.cost.percentage, `spell ${rank.spellId} rank ${rank.rank}`).not.toBe(true);
expect(Number.isFinite(rank.cost.amount), `spell ${rank.spellId} rank ${rank.rank}`).toBe(true);
}
});
it("provides mechanics for previously silent class scripts and hidden proc spells", () => {
expect(effectsOfKind(byId("wow335-warrior-slam"), "damage")).not.toEqual([]);
expect(effectsOfKind(byId("wow335-warrior-bloodthirst"), "damage")).toContainEqual(
expect.objectContaining({ coefficient: 1, basePoints: 50, bonusPowerCoefficient: 1 }),
);
expect(effectsOfKind(byId("wow335-warrior-bloodrage"), "resource")).toEqual([
{ kind: "resource", resource: "rage", amount: 20 },
]);
expect(effectsOfKind(byId("wow335-warrior-bloodrage"), "heal")).toEqual([]);
expect(effectsOfKind(abilityAtLevel(byId("wow335-warrior-execute"), 80), "damage")).toContainEqual(
expect.objectContaining({ basePoints: 1_456, coefficient: 1 }),
);
expect(effectsOfKind(byId("wow335-warrior-execute"), "execute")).toEqual([]);
expect(effectsOfKind(byId("wow335-warrior-execute"), "heal")).toEqual([]);
expect(effectsOfKind(byId("wow335-warrior-bladestorm"), "dot")).toEqual([
expect.objectContaining({ coefficient: 0.6, ticks: 6, intervalMs: 1_000 }),
]);
expect(byId("wow335-warrior-bladestorm")).toMatchObject({ target: "self", radius: 8 });
expect(effectsOfKind(byId("wow335-paladin-hammer-of-the-righteous"), "damage")).toEqual([
expect.objectContaining({ coefficient: 1.2 }),
]);
expect(effectsOfKind(byId("wow335-druid-swiftmend"), "heal")).toEqual([
expect.objectContaining({ coefficient: 2.2 }),
]);
expect(effectsOfKind(byId("wow335-rogue-mutilate"), "damage")).toContainEqual(
expect.objectContaining({ coefficient: 2 }),
);
expect(effectsOfKind(byId("wow335-rogue-mutilate"), "combo")).not.toEqual([]);
expect(effectsOfKind(byId("wow335-rogue-killing-spree"), "dot")).toEqual([]);
expect(effectsOfKind(byId("wow335-rogue-killing-spree"), "utility")).toContainEqual(
expect.objectContaining({ action: "class-script", durationMs: 2_000 }),
);
expect(byId("wow335-rogue-killing-spree").target).toBe("hostile");
expect(effectsOfKind(byId("wow335-warlock-conflagrate"), "damage")).toEqual([
expect.objectContaining({ coefficient: 0, school: "fire" }),
]);
expect(effectsOfKind(byId("wow335-death-knight-death-grip"), "pull")).not.toEqual([]);
expect(effectsOfKind(byId("wow335-death-knight-death-grip"), "taunt")).toEqual([
{ kind: "taunt", durationMs: 3_000 },
]);
expect(effectsOfKind(byId("wow335-death-knight-chains-of-ice"), "status")).toEqual([
{ kind: "status", status: "slow", durationMs: 10_000, magnitude: 0.95 },
]);
expect(effectsOfKind(byId("wow335-death-knight-pestilence"), "damage")).toEqual([]);
expect(effectsOfKind(byId("wow335-death-knight-pestilence"), "utility")).toContainEqual(
expect.objectContaining({ action: "class-script", effectType: 77 }),
);
expect(effectsOfKind(byId("wow335-shaman-windfury-weapon"), "apply-aura")[0]?.aura.procs).not.toEqual([]);
expect(effectsOfKind(byId("wow335-shaman-lightning-shield"), "apply-aura")[0]?.aura.procs).toEqual(expect.arrayContaining([
expect.objectContaining({ triggers: "damage-taken", charges: 3 }),
]));
});
it("models Penance as three healing or damaging pulses", () => {
const penance = byId("wow335-priest-penance");
expect(penance).toMatchObject({ castMode: "channel", castTimeMs: 2_000, channelTickMs: 1_000,
channelStartsWithPulse: true });
expect(penance.target).toBe("any");
expect(effectsOfKind(penance, "heal")).toEqual([
expect.objectContaining({ coefficient: 0.6 }),
]);
expect(effectsOfKind(penance, "hot")).toEqual([]);
expect(effectsOfKind(penance, "damage")).toEqual([
expect.objectContaining({ coefficient: 0.6 }),
]);
expect(effectsOfKind(penance, "dot")).toEqual([]);
});
it("keeps ground-channel damage children out of learned cast ranks", () => {
for (const id of ["wow335-mage-blizzard", "wow335-warlock-rain-of-fire", "wow335-druid-hurricane"]) {
const ability = byId(id);
expect(ability.ranks?.every((rank) => rank.castMode === "channel"), id).toBe(true);
expect(abilityAtLevel(ability, 80).castMode, id).toBe("channel");
}
const hurricane = abilityAtLevel(byId("wow335-druid-hurricane"), 80);
expect(hurricane).toMatchObject({ dbcSpellId: 48467, castTimeMs: 10_000,
channelTickMs: 1_000, channelPulses: 10 });
expect(hurricane.cost.amount).toBeGreaterThan(0);
});
it("makes Empower Rune Weapon activate all six runes from the player spell", () => {
const empowerRuneWeapon = abilityAtLevel(byId("wow335-death-knight-empower-rune-weapon"), 80);
expect(empowerRuneWeapon.dbcSpellId).toBe(47568);
expect(effectsOfKind(empowerRuneWeapon, "rune")).toEqual([
{ kind: "rune", action: "activate", count: 6 },
]);
expect(effectsOfKind(empowerRuneWeapon, "resource")).toEqual([
{ kind: "resource", resource: "runic-power", amount: 25 },
]);
});
it("models Death Knight source healing, rune, charge, and magic-shell effects", () => {
const deathCoil = abilityAtLevel(byId("wow335-death-knight-death-coil"), 80);
expect(deathCoil.target).toBe("any");
expect(effectsOfKind(deathCoil, "damage")).not.toEqual([]);
expect(effectsOfKind(deathCoil, "heal")).not.toEqual([]);
const deathStrike = abilityAtLevel(byId("wow335-death-knight-death-strike"), 80);
expect(effectsOfKind(deathStrike, "heal")).toEqual([
expect.objectContaining({ coefficient: 0, percentMaxHealth: 0.05 }),
]);
const antiMagicShell = abilityAtLevel(byId("wow335-death-knight-anti-magic-shell"), 80);
expect(effectsOfKind(antiMagicShell, "apply-aura")).toEqual([
expect.objectContaining({
recipient: "caster",
aura: expect.objectContaining({
durationMs: 5_000,
absorbs: [expect.objectContaining({
amount: 0.5,
fraction: 0.75,
schools: ["holy", "fire", "nature", "frost", "shadow", "arcane"],
})],
}),
}),
]);
const antiMagicZone = abilityAtLevel(byId("wow335-death-knight-anti-magic-zone"), 80);
expect(antiMagicZone).toMatchObject({
target: "self",
description: expect.stringContaining("75% of incoming magic damage"),
targeting: { shape: "ground", radius: 10, groundRange: 30 },
});
expect(effectsOfKind(antiMagicZone, "summon")).toEqual([
expect.objectContaining({
creatureId: 28306,
durationMs: 10_000,
antiMagicZone: {
radius: 10,
capacityBase: 10_000,
capacityPerAttackPower: 2,
absorbFraction: 0.75,
intervalMs: 250,
},
}),
]);
const boneShield = abilityAtLevel(byId("wow335-death-knight-bone-shield"), 80);
expect(effectsOfKind(boneShield, "apply-aura")).toEqual([
expect.objectContaining({ aura: expect.objectContaining({
durationMs: 300_000,
initialStacks: 4,
maxStacks: 4,
modifiers: expect.arrayContaining([
expect.objectContaining({ kind: "damage", direction: "taken", value: -0.2, perStack: false }),
expect.objectContaining({ kind: "damage", direction: "dealt", value: 0.02, perStack: false }),
]),
procs: [expect.objectContaining({
triggers: "damage-taken",
charges: 4,
action: { kind: "custom", id: "wow335-death-knight-bone-shield-charge" },
})],
}) }),
]);
expect(effectsOfKind(abilityAtLevel(byId("wow335-death-knight-blood-tap"), 80), "rune")).toEqual([
{ kind: "rune", action: "activate", count: 1, runeType: "blood", convertToDeathMs: 20_000 },
]);
expect(effectsOfKind(abilityAtLevel(byId("wow335-death-knight-deathchill"), 80), "apply-aura")).toEqual([
expect.objectContaining({
recipient: "caster",
aura: expect.objectContaining({ utilityTags: expect.arrayContaining(["wow335-deathchill-next-critical"]) }),
}),
]);
});
it("keeps every rank of imported channels in channel mode", () => {
const mismatches = Object.values(WOW335_ABILITIES_BY_CLASS).flat()
.filter((ability) => ability.castMode === "channel" && ability.ranks?.some((rank) => rank.castMode !== "channel"))
.map((ability) => `${ability.name}: ${ability.ranks?.filter((rank) => rank.castMode !== "channel")
.map((rank) => rank.spellId).join(", ")}`);
expect(mismatches).toEqual([]);
});
it("maps the Ready-five missing spell payloads without cast-time damage on self buffs", () => {
const devastate = abilityAtLevel(byId("wow335-warrior-devastate"), 80);
expect(effectsOfKind(devastate, "damage")).toEqual([
expect.objectContaining({ coefficient: 1, bonusPerSunderStack: 202 }),
]);
expect(effectsOfKind(devastate, "apply-aura")).toEqual([
expect.objectContaining({ aura: expect.objectContaining({ id: "wow335:warrior:sunder-armor", maxStacks: 5 }) }),
]);
const lastStand = byId("wow335-warrior-last-stand");
expect(lastStand.target).toBe("self");
expect(effectsOfKind(lastStand, "heal")).toEqual([]);
expect(effectsOfKind(lastStand, "apply-aura")).toEqual([
expect.objectContaining({ aura: expect.objectContaining({
modifiers: [{ kind: "stat", stat: "maximum-health", operation: "percent", value: 0.3 }],
}) }),
]);
expect(effectsOfKind(byId("wow335-priest-divine-hymn"), "heal")).toEqual([
expect.objectContaining({ scope: "party", maximumTargets: 3, radius: 40 }),
]);
expect(effectsOfKind(byId("wow335-hunter-chimera-shot"), "damage")).toEqual([
expect.objectContaining({ coefficient: 1.25 }),
]);
expect(effectsOfKind(abilityAtLevel(byId("wow335-mage-living-bomb"), 80), "dot")).toHaveLength(1);
const moltenArmor = abilityAtLevel(byId("wow335-mage-molten-armor"), 80);
expect(moltenArmor.target).toBe("self");
expect(effectsOfKind(moltenArmor, "damage")).toEqual([]);
expect(effectsOfKind(byId("wow335-rogue-shiv"), "combo")).toEqual([{ kind: "combo", amount: 1 }]);
expect(effectsOfKind(byId("wow335-rogue-cold-blood"), "apply-aura")).toEqual([
expect.objectContaining({ aura: expect.objectContaining({
utilityTags: ["next-offensive-critical"],
modifiers: [{ kind: "stat", stat: "critical-strike", operation: "flat", value: 100 }],
}) }),
]);
});
it("maps the remaining Ready-five cooldowns and defensive modifiers", () => {
expect(effectsOfKind(byId("wow335-warrior-last-stand"), "apply-aura")[0]?.aura.durationMs).toBe(20_000);
expect(byId("wow335-warrior-enraged-regeneration").target).toBe("self");
const innerFire = abilityAtLevel(byId("wow335-priest-inner-fire"), 80);
expect(effectsOfKind(innerFire, "apply-aura")[0]?.aura).toMatchObject({
maxStacks: 20, initialStacks: 20,
modifiers: [{ kind: "stat", stat: "armor", operation: "flat", value: 2440, perStack: false }],
});
const shadowfiend = byId("wow335-priest-shadowfiend");
expect(effectsOfKind(shadowfiend, "summon")).toHaveLength(1);
expect(effectsOfKind(shadowfiend, "resource")).toHaveLength(0);
expect(effectsOfKind(abilityAtLevel(byId("wow335-hunter-kill-shot"), 80), "damage"))
.toEqual([{ kind: "damage", coefficient: 2, bonusFlat: 325 }]);
const combustion = byId("wow335-mage-combustion");
expect(combustion.target).toBe("self");
expect(effectsOfKind(combustion, "damage")).toHaveLength(0);
expect(effectsOfKind(combustion, "apply-aura")[0]?.aura.utilityTags).toContain("fire-crit-stacking");
expect(effectsOfKind(byId("wow335-mage-mirror-image"), "summon")).toHaveLength(3);
const molten = abilityAtLevel(byId("wow335-mage-molten-armor"), 80);
expect(effectsOfKind(molten, "apply-aura").flatMap((effect) => effect.aura.modifiers ?? []))
.toEqual(expect.arrayContaining([
expect.objectContaining({ stat: "incoming-critical-strike", value: -5 }),
expect.objectContaining({ stat: "spirit-to-spell-crit-rating", value: 0.35 }),
]));
expect(effectsOfKind(abilityAtLevel(byId("wow335-rogue-evasion"), 80), "apply-aura")
.flatMap((effect) => effect.aura.modifiers ?? []))
.toEqual(expect.arrayContaining([expect.objectContaining({ stat: "incoming-ranged-hit", value: -25 })]));
});
it("keeps Blink's root and stun breaks without inventing target-relative teleport movement", () => {
const blink = byId("wow335-mage-blink");
expect(blink.dbcSpellId).toBe(1953);
expect(effectsOfKind(blink, "movement")).toEqual([]);
expect(effectsOfKind(blink, "apply-aura").map((effect) => effect.aura.controlImmunities))
.toEqual([["stun"], ["root"]]);
});
it("keeps triggered shield damage children out of learned Shaman ranks", () => {
const lightningShield = abilityAtLevel(byId("wow335-shaman-lightning-shield"), 80);
expect(lightningShield.dbcSpellId).toBe(49281);
expect(effectsOfKind(lightningShield, "damage")).toEqual([]);
expect(effectsOfKind(lightningShield, "apply-aura")[0]?.aura).toMatchObject({
exclusiveGroup: "shaman:elemental-shield",
procs: expect.arrayContaining([expect.objectContaining({ triggers: "damage-taken", charges: 3 })]),
});
const waterShield = abilityAtLevel(byId("wow335-shaman-water-shield"), 80);
expect(waterShield.dbcSpellId).toBe(57960);
expect(effectsOfKind(waterShield, "apply-aura").every((effect) => (
effect.aura.exclusiveGroup === "shaman:elemental-shield"
))).toBe(true);
const earthShield = byId("wow335-shaman-earth-shield");
const earthProc = effectsOfKind(earthShield, "apply-aura")
.flatMap((effect) => effect.aura.procs ?? [])
.find((proc) => proc.id.endsWith(":earth-shield"));
expect(earthProc).toMatchObject({
triggers: "damage-taken",
charges: 9,
internalCooldownMs: 3_000,
action: { kind: "heal", target: "aura-target" },
});
expect(effectsOfKind(earthShield, "apply-aura").every((effect) => (
effect.aura.exclusiveGroup === "shaman:earth-shield"
))).toBe(true);
});
it("models both offensive Spirit Wolves instead of the inert DBC summon marker", () => {
expect(effectsOfKind(byId("wow335-shaman-feral-spirit"), "summon")).toEqual([
expect.objectContaining({ creatureId: 29264, coefficient: 0.35, durationMs: 45_000 }),
expect.objectContaining({ creatureId: 29264, coefficient: 0.35, durationMs: 45_000 }),
]);
});
it("creates source-named stat and avoidance auras", () => {
const fortitude = effectsOfKind(byId("wow335-priest-power-word-fortitude"), "apply-aura")[0];
expect(fortitude).toMatchObject({
sourceEffectType: 6,
sourceAuraType: 29,
aura: {
id: "wow335:1243:effect:0:aura:29",
name: "Power Word: Fortitude (DBC 1243, Aura 29)",
disposition: "buff",
dispelCategory: "magic",
modifiers: [{ kind: "stat", stat: "stamina", operation: "flat", value: 3 }],
},
});
const evasion = effectsOfKind(byId("wow335-rogue-evasion"), "apply-aura")[0];
expect(evasion.aura.modifiers).toContainEqual({
kind: "stat",
stat: "dodge",
operation: "percent",
value: 0.5,
});
});
it("preserves permanent DBC toggles as non-expiring caster auras", () => {
const devotion = effectsOfKind(byId("wow335-paladin-devotion-aura"), "apply-aura")[0];
expect(devotion).toMatchObject({
recipient: "caster",
aura: {
disposition: "buff",
durationMs: null,
exclusiveGroup: "paladin:aura",
},
});
});
it("represents absorbs as named aura capacity instead of a generic shield", () => {
const shield = byId("wow335-priest-power-word-shield");
const aura = effectsOfKind(shield, "apply-aura").find((effect) => effect.sourceAuraType === 69);
expect(aura?.aura).toMatchObject({
disposition: "buff",
durationMs: 30_000,
absorbs: [{ amount: 44, perStack: false }],
});
expect(shield.effects.some((effect) => effect.kind === "shield")).toBe(false);
});
it("maps Paladin emergency cooldowns to their source-backed mechanics", () => {
const layOnHands = byId("wow335-paladin-lay-on-hands");
expect(effectsOfKind(layOnHands, "heal")).toContainEqual(expect.objectContaining({
coefficient: 0,
percentMaxHealth: 1,
}));
const handOfProtection = byId("wow335-paladin-hand-of-protection");
const immunity = effectsOfKind(handOfProtection, "apply-aura")
.find((effect) => effect.sourceAuraType === 39);
expect(immunity?.aura).toMatchObject({
durationMs: 6_000,
modifiers: [{
kind: "damage",
direction: "taken",
school: "physical",
operation: "percent",
value: -1,
}],
});
});
it("maps Paladin revive, healing riders, class-group blessings, and aura mechanics", () => {
const redemption = byId("wow335-paladin-redemption");
expect(redemption.target).toBe("friendly");
expect(effectsOfKind(redemption, "resurrection")).toContainEqual(expect.objectContaining({
flatHealth: 65,
}));
const divineStorm = byId("wow335-paladin-divine-storm");
expect(divineStorm.effects).toEqual(expect.arrayContaining([
expect.objectContaining({ kind: "damage", coefficient: 1.1, school: "physical" }),
expect.objectContaining({ kind: "heal", percentOfDamage: 0.25, scope: "party", maximumTargets: 3 }),
]));
const beacon = abilityAtLevel(byId("wow335-paladin-beacon-of-light"), 80);
expect(effectsOfKind(beacon, "apply-aura")[0]?.aura.utilityTags).toContain("beacon-of-light");
const greaterBlessing = byId("wow335-paladin-greater-blessing-of-kings");
expect(effectsOfKind(greaterBlessing, "apply-aura")[0]?.scope).toBe("party");
const avengersShield = byId("wow335-paladin-avenger-s-shield");
expect(avengersShield.targeting).toMatchObject({ shape: "chain", maximumTargets: 3, jumpRange: 10 });
const auraMastery = byId("wow335-paladin-aura-mastery");
expect(effectsOfKind(auraMastery, "apply-aura").every((effect) => (
effect.scope === "party" && effect.aura.utilityTags?.includes("wow335-aura-mastery-silence-interrupt")
))).toBe(true);
const divineFavor = byId("wow335-paladin-divine-favor");
expect(effectsOfKind(divineFavor, "apply-aura")[0]?.aura.utilityTags)
.toContain("wow335-divine-favor-next-holy-heal");
});
it("keeps periodic damage and healing auras out of the generic HUD strip", () => {
const pain = byId("wow335-priest-shadow-word-pain");
const painAura = effectsOfKind(pain, "apply-aura")
.find((effect) => effect.sourceAuraType === 3);
const renew = byId("wow335-priest-renew");
const renewAura = effectsOfKind(renew, "apply-aura")
.find((effect) => effect.sourceAuraType === 8);
expect(painAura?.aura).toMatchObject({
disposition: "debuff",
hideFromAuraStrip: true,
});
expect(renewAura?.aura).toMatchObject({
disposition: "buff",
hideFromAuraStrip: true,
});
expect(effectsOfKind(pain, "dot")).toHaveLength(1);
expect(effectsOfKind(renew, "hot")).toHaveLength(1);
});
it("maps cleanse, purge, and dual-purpose dispel categories", () => {
expect(effectsOfKind(byId("wow335-paladin-cleanse"), "dispel")).toEqual(expect.arrayContaining([
expect.objectContaining({ mode: "cleanse", relationship: "friendly", categories: ["magic"] }),
expect.objectContaining({ mode: "cleanse", relationship: "friendly", categories: ["disease"] }),
expect.objectContaining({ mode: "cleanse", relationship: "friendly", categories: ["poison"] }),
]));
expect(effectsOfKind(byId("wow335-shaman-purge"), "dispel")).toContainEqual(
expect.objectContaining({ mode: "purge", relationship: "hostile", categories: ["magic"] }),
);
expect(effectsOfKind(byId("wow335-priest-dispel-magic"), "dispel")).toContainEqual(
expect.objectContaining({ relationship: "target", categories: ["magic"] }),
);
});
it("maps interrupts, combo-point awards, and source-backed proc triggers", () => {
expect(effectsOfKind(byId("wow335-rogue-kick"), "interrupt")).toEqual([
expect.objectContaining({ lockoutMs: 5_000 }),
]);
expect(effectsOfKind(byId("wow335-rogue-sinister-strike"), "combo")).toEqual([
{ kind: "combo", amount: 1 },
]);
const seal = effectsOfKind(byId("wow335-paladin-seal-of-justice"), "apply-aura")
.find((effect) => effect.sourceAuraType === 42);
expect(seal?.aura.procs).toEqual([
expect.objectContaining({
triggers: "hit",
action: {
kind: "custom",
id: "classic-status",
data: { status: "stun", durationMs: 2_000 },
},
}),
]);
});
it("keeps fear as a named, dispellable control aura", () => {
const fear = effectsOfKind(byId("wow335-warlock-fear"), "apply-aura")
.find((effect) => effect.control?.kind === "fear");
expect(fear).toMatchObject({
control: { kind: "fear" },
aura: { disposition: "debuff", dispelCategory: "magic" },
});
});
it("maps Soulshatter to its source 50-yard self-centered threat reduction", () => {
const soulshatter = byId("wow335-warlock-soulshatter");
const source = generatedData.classes.warlock.find((chain) => chain.name === "Soulshatter")!;
expect(source.ranks[0]).toMatchObject({
spellId: 29858,
description: expect.stringContaining("Reduces threat by 50%"),
effects: [expect.objectContaining({ effectType: 3, basePoints: -50, radius: 50 })],
});
expect(soulshatter).toMatchObject({
dbcSpellId: 29858,
target: "self",
radius: 50,
});
expect(effectsOfKind(soulshatter, "threat")).toEqual([
{ kind: "threat", mode: "percent", amount: -50 },
]);
});
});