226 lines
9.3 KiB
TypeScript
226 lines
9.3 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { abilityAtLevel, abilityById } from "./abilityCatalog";
|
|
import { WOW335_ABILITIES_BY_CLASS } from "./wow335AbilityCatalog";
|
|
import {
|
|
WOW335_MAGE_FROST_WARDING_ARMOR_ABILITIES,
|
|
WOW335_MAGE_FROST_WARDING_ARMOR_TAG,
|
|
WOW335_MAGE_FROST_WARDING_TALENT_ID,
|
|
WOW335_MAGE_FROST_WARDING_WARD_ABILITIES,
|
|
wow335MageFrostWardingArmorAuraEffect,
|
|
wow335MageFrostWardingProcPlan,
|
|
wow335MageFrostWardingProfile,
|
|
} from "./wow335MageFrostWardingRuntime";
|
|
|
|
const procEvent = (overrides: Partial<Parameters<typeof wow335MageFrostWardingProcPlan>[1]> = {}) => ({
|
|
wardOwnerClassId: "mage",
|
|
eventKind: "spell-damage",
|
|
wardAbilityId: WOW335_MAGE_FROST_WARDING_WARD_ABILITIES.fire.abilityId,
|
|
wardRankSpellId: 43010,
|
|
wardAuraActive: true,
|
|
damageSchool: "fire",
|
|
spellDamage: 200,
|
|
roll: 0,
|
|
...overrides,
|
|
});
|
|
|
|
describe("WoW 3.3.5a Mage Frost Warding", () => {
|
|
it("reads the generated rank chance, reduction, Mana refund, and armor/resistance values", () => {
|
|
expect(WOW335_MAGE_FROST_WARDING_TALENT_ID).toBe("talent-70");
|
|
expect([1, 2].map(wow335MageFrostWardingProfile)).toEqual([
|
|
{
|
|
rank: 1,
|
|
rankSpellId: 11189,
|
|
armorAndResistanceBonusPercent: 25,
|
|
wardProcChancePercent: 25,
|
|
wardDamageReductionPercent: 50,
|
|
manaRefundPerDamageReduced: 1,
|
|
},
|
|
{
|
|
rank: 2,
|
|
rankSpellId: 28332,
|
|
armorAndResistanceBonusPercent: 50,
|
|
wardProcChancePercent: 50,
|
|
wardDamageReductionPercent: 50,
|
|
manaRefundPerDamageReduced: 1,
|
|
},
|
|
]);
|
|
expect(wow335MageFrostWardingProfile(0)).toBeNull();
|
|
expect(wow335MageFrostWardingProfile(3)).toBeNull();
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
armor: "frost" as const,
|
|
rank: 1,
|
|
spellId: 7301,
|
|
expected: { armor: 250 },
|
|
},
|
|
{
|
|
armor: "frost" as const,
|
|
rank: 2,
|
|
spellId: 7301,
|
|
expected: { armor: 300 },
|
|
},
|
|
{
|
|
armor: "ice" as const,
|
|
rank: 1,
|
|
spellId: 43008,
|
|
expected: { armor: 1162.5, "resistance:frost": 50 },
|
|
},
|
|
{
|
|
armor: "ice" as const,
|
|
rank: 2,
|
|
spellId: 43008,
|
|
expected: { armor: 1395, "resistance:frost": 60 },
|
|
},
|
|
])("scales only the exact local $armor armor aura at Frost Warding rank $rank", ({ armor, rank, spellId, expected }) => {
|
|
const row = WOW335_MAGE_FROST_WARDING_ARMOR_ABILITIES[armor];
|
|
const baseAbility = abilityById(row.abilityId);
|
|
if (!baseAbility) throw new Error(`Missing local ${row.name} ability.`);
|
|
const ability = abilityAtLevel(baseAbility, 80);
|
|
expect(ability.dbcSpellId).toBe(spellId);
|
|
const armorEffects = ability.effects.filter((effect) => effect.kind === "apply-aura"
|
|
&& effect.sourceAuraType === 22);
|
|
expect(armorEffects.length).toBeGreaterThan(0);
|
|
|
|
const actual: Record<string, number> = {};
|
|
for (const effect of armorEffects) {
|
|
if (effect.kind !== "apply-aura") continue;
|
|
const applied = wow335MageFrostWardingArmorAuraEffect(ability, rank, effect);
|
|
expect(applied.aura.utilityTags).toContain(WOW335_MAGE_FROST_WARDING_ARMOR_TAG);
|
|
for (const modifier of applied.aura.modifiers ?? []) {
|
|
if (modifier.kind === "stat" && (modifier.stat === "armor" || modifier.stat.startsWith("resistance:"))) {
|
|
actual[modifier.stat] = (actual[modifier.stat] ?? 0) + modifier.value;
|
|
}
|
|
}
|
|
}
|
|
expect(actual).toEqual(expected);
|
|
});
|
|
|
|
it("requires a learned rank and the exact local Mage Frost/Ice Armor source aura", () => {
|
|
const baseAbility = abilityById(WOW335_MAGE_FROST_WARDING_ARMOR_ABILITIES.ice.abilityId)!;
|
|
const ability = abilityAtLevel(baseAbility, 80);
|
|
const effect = ability.effects.find((candidate) => candidate.kind === "apply-aura"
|
|
&& candidate.sourceAuraType === 22)!;
|
|
if (effect.kind !== "apply-aura") throw new Error("Ice Armor has no stat aura.");
|
|
expect(wow335MageFrostWardingArmorAuraEffect(ability, 0, effect)).toBe(effect);
|
|
expect(wow335MageFrostWardingArmorAuraEffect({ ...ability, classId: "warlock" }, 2, effect)).toBe(effect);
|
|
expect(wow335MageFrostWardingArmorAuraEffect({ ...ability, dbcSpellId: 12345 }, 2, effect)).toBe(effect);
|
|
const wrongAuraType = { ...effect, sourceAuraType: 42 };
|
|
expect(wow335MageFrostWardingArmorAuraEffect(ability, 2, wrongAuraType)).toBe(wrongAuraType);
|
|
expect(wow335MageFrostWardingArmorAuraEffect(ability, 2, {
|
|
...effect,
|
|
sourceAuraType: 42,
|
|
})).toMatchObject({ sourceAuraType: 42 });
|
|
expect(wow335MageFrostWardingArmorAuraEffect(ability, 2, {
|
|
...effect,
|
|
aura: {
|
|
...effect.aura,
|
|
modifiers: [
|
|
{ kind: "stat", stat: "armor", operation: "flat", value: 0 },
|
|
{ kind: "stat", stat: "resistance:frost", operation: "flat", value: -5 },
|
|
{ kind: "stat", stat: "spell-power", operation: "flat", value: 25 },
|
|
],
|
|
},
|
|
})).toMatchObject({ aura: { modifiers: [
|
|
{ kind: "stat", stat: "armor", value: 0 },
|
|
{ kind: "stat", stat: "resistance:frost", value: -5 },
|
|
{ kind: "stat", stat: "spell-power", value: 25 },
|
|
] } });
|
|
});
|
|
|
|
it("keeps the ward school selector separate from rank-specific chance", () => {
|
|
expect(WOW335_MAGE_FROST_WARDING_WARD_ABILITIES.fire.school).toBe("fire");
|
|
expect(WOW335_MAGE_FROST_WARDING_WARD_ABILITIES.frost.school).toBe("frost");
|
|
expect(wow335MageFrostWardingProfile(1)?.wardProcChancePercent).toBe(25);
|
|
expect(wow335MageFrostWardingProfile(2)?.wardProcChancePercent).toBe(50);
|
|
|
|
const frostEvent = procEvent({
|
|
wardAbilityId: WOW335_MAGE_FROST_WARDING_WARD_ABILITIES.frost.abilityId,
|
|
wardRankSpellId: 43012,
|
|
damageSchool: "frost",
|
|
roll: 0.249,
|
|
});
|
|
expect(wow335MageFrostWardingProcPlan(1, frostEvent)?.wardAbilityId)
|
|
.toBe(WOW335_MAGE_FROST_WARDING_WARD_ABILITIES.frost.abilityId);
|
|
expect(wow335MageFrostWardingProcPlan(1, { ...frostEvent, damageSchool: "fire" })).toBeNull();
|
|
});
|
|
|
|
it("binds its supported Fire and Frost Wards to local Mage Spell.dbc ability rows", () => {
|
|
const mageRows = new Map(WOW335_ABILITIES_BY_CLASS.mage.map((ability) => [ability.id, ability]));
|
|
expect(mageRows.get(WOW335_MAGE_FROST_WARDING_WARD_ABILITIES.fire.abilityId)?.name).toBe("Fire Ward");
|
|
expect(mageRows.get(WOW335_MAGE_FROST_WARDING_WARD_ABILITIES.frost.abilityId)?.name).toBe("Frost Ward");
|
|
|
|
for (const ward of Object.values(WOW335_MAGE_FROST_WARDING_WARD_ABILITIES)) {
|
|
const row = mageRows.get(ward.abilityId);
|
|
expect(row?.source).toBe("wow335-progression");
|
|
expect(row?.ranks?.map((rank) => rank.spellId)).toEqual(ward.rankSpellIds);
|
|
}
|
|
|
|
const otherClassRows = new Map(WOW335_ABILITIES_BY_CLASS.warlock.map((ability) => [ability.name, ability]));
|
|
expect(otherClassRows.get("Shadow Ward")?.id).toBe("wow335-warlock-shadow-ward");
|
|
expect(mageRows.get("wow335-mage-arcane-ward")).toBeUndefined();
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
rank: 1,
|
|
event: procEvent({ roll: 0.249 }),
|
|
expectedRankSpellId: 11189,
|
|
},
|
|
{
|
|
rank: 2,
|
|
event: procEvent({ roll: 0.499 }),
|
|
expectedRankSpellId: 28332,
|
|
},
|
|
{
|
|
rank: 2,
|
|
event: procEvent({
|
|
wardAbilityId: WOW335_MAGE_FROST_WARDING_WARD_ABILITIES.frost.abilityId,
|
|
wardRankSpellId: 43012,
|
|
damageSchool: "Frost",
|
|
roll: 0.2,
|
|
}),
|
|
expectedRankSpellId: 28332,
|
|
},
|
|
])("plans a matching ward proc at the generated rank chance", ({ rank, event, expectedRankSpellId }) => {
|
|
expect(wow335MageFrostWardingProcPlan(rank, event)).toEqual({
|
|
rankSpellId: expectedRankSpellId,
|
|
wardAbilityId: event.wardAbilityId,
|
|
wardDamageReductionPercent: 50,
|
|
manaRefundPerDamageReduced: 1,
|
|
});
|
|
});
|
|
|
|
it("uses an exclusive upper roll boundary for 25% and 50% proc chances", () => {
|
|
expect(wow335MageFrostWardingProcPlan(1, procEvent({ roll: 0.25 }))).toBeNull();
|
|
expect(wow335MageFrostWardingProcPlan(2, procEvent({ roll: 0.5 }))).toBeNull();
|
|
});
|
|
|
|
it("requires the Mage owner, a learned rank, an active matching ward, and positive spell damage", () => {
|
|
expect(wow335MageFrostWardingProcPlan(2, procEvent({ wardOwnerClassId: "warlock" }))).toBeNull();
|
|
expect(wow335MageFrostWardingProcPlan(0, procEvent())).toBeNull();
|
|
expect(wow335MageFrostWardingProcPlan(2, procEvent({ wardAuraActive: false }))).toBeNull();
|
|
expect(wow335MageFrostWardingProcPlan(2, procEvent({ wardRankSpellId: 43012 }))).toBeNull();
|
|
expect(wow335MageFrostWardingProcPlan(2, procEvent({ damageSchool: "frost" }))).toBeNull();
|
|
expect(wow335MageFrostWardingProcPlan(2, procEvent({ eventKind: "weapon-damage" }))).toBeNull();
|
|
expect(wow335MageFrostWardingProcPlan(2, procEvent({ spellDamage: 0 }))).toBeNull();
|
|
expect(wow335MageFrostWardingProcPlan(2, procEvent({ spellDamage: -1 }))).toBeNull();
|
|
expect(wow335MageFrostWardingProcPlan(2, procEvent({ roll: Number.NaN }))).toBeNull();
|
|
expect(wow335MageFrostWardingProcPlan(2, procEvent({ roll: 1 }))).toBeNull();
|
|
});
|
|
|
|
it("does not synthesize support for the tooltip's absent Mage Arcane/Shadow Ward rows", () => {
|
|
expect(wow335MageFrostWardingProcPlan(2, procEvent({
|
|
wardAbilityId: "wow335-warlock-shadow-ward",
|
|
wardRankSpellId: 47891,
|
|
damageSchool: "shadow",
|
|
}))).toBeNull();
|
|
expect(wow335MageFrostWardingProcPlan(2, procEvent({
|
|
wardAbilityId: "wow335-mage-arcane-ward",
|
|
wardRankSpellId: 43020,
|
|
damageSchool: "arcane",
|
|
}))).toBeNull();
|
|
});
|
|
});
|