import { describe, expect, it } from "vitest"; import { COA_CLASS_IDS, type CoaClassId } from "../app/characterCatalog"; import type { AbilityDefinition, AbilityEffect, AbilityRankDefinition } from "./abilityCatalog"; import { COA_ABILITIES_BY_CLASS, COA_CLASS_RESOURCES, COA_TRIGGERED_ABILITIES_BY_SPELL_ID, } from "./coaAbilityCatalog"; import { COA_PROGRESSION_CHAINS, COA_TREE_ENTRIES, coaClassIdFromDbcId, coaSpellById, } from "./coaLiveCatalog"; const abilities = Object.values(COA_ABILITIES_BY_CLASS).flat(); it("keeps Drakkari Barrier friendly and its retaliation in the damage-taken proc", () => { const barrier = COA_ABILITIES_BY_CLASS["witch-doctor"].find((ability) => ability.name === "Drakkari Barrier")!; expect(barrier.target).toBe("friendly"); for (const rank of barrier.ranks ?? []) { expect(rank.effects.some((effect) => effect.kind === "damage")).toBe(false); expect(rank.effects.some((effect) => effect.kind === "apply-aura" && effect.aura.procs?.some((proc) => proc.triggers === "damage-taken" && proc.action.kind === "damage"))).toBe(true); } }); const ranksFor = (ability: AbilityDefinition): readonly AbilityRankDefinition[] => ability.ranks ?? [{ rank: 1, spellId: ability.dbcSpellId, level: ability.unlockLevel, description: ability.description, castMode: ability.castMode, castTimeMs: ability.castTimeMs, cooldownMs: ability.cooldownMs, gcdMs: ability.gcdMs, cost: ability.cost, effects: ability.effects, }]; const ranks = abilities.flatMap((ability) => ranksFor(ability).map((rank) => ({ ability, rank }))); const byName = (classId: CoaClassId, name: string): AbilityDefinition => { const ability = COA_ABILITIES_BY_CLASS[classId].find((candidate) => candidate.name === name); if (!ability) throw new Error(`Missing ${classId} ability ${name}`); return ability; }; const rankEffectsForSpell = (spellId: number): readonly AbilityEffect[] => ranks .find(({ rank }) => rank.spellId === spellId)?.rank.effects ?? []; describe("Ascension Conquest ability catalog", () => { it("keeps Rejuvenating Venom healing in its cast proc", () => { const venom = byName("venomancer", "Rejuvenating Venom"); expect(venom.effects.some(effect => effect.kind === "heal")).toBe(false); expect(venom.effects.some(effect => effect.kind === "apply-aura" && effect.aura.procs?.some(proc => proc.action.kind === "custom" && proc.action.data?.spellId === 630869))).toBe(true); const payload = COA_TRIGGERED_ABILITIES_BY_SPELL_ID[630869]; expect(payload.effects.find(effect => effect.kind === "hot")).toMatchObject({ scope: "party", radius: 30, maximumTargets: 1 }); }); it("covers every class, source family, and playable rank", () => { expect(Object.keys(COA_ABILITIES_BY_CLASS).sort()).toEqual([...COA_CLASS_IDS].sort()); expect(Object.keys(COA_CLASS_RESOURCES).sort()).toEqual([...COA_CLASS_IDS].sort()); expect(abilities).toHaveLength(1_275); expect(ranks).toHaveLength(3_394); expect(new Set(ranks.map(({ rank }) => rank.spellId)).size).toBe(3_347); const runtimeSpellIds = new Set(ranks.map(({ rank }) => rank.spellId)); for (const chain of COA_PROGRESSION_CHAINS) { const classId = coaClassIdFromDbcId(chain.classId); expect(classId, chain.name).not.toBeNull(); for (const rank of chain.ranks) { expect(runtimeSpellIds.has(rank.spellId), `${classId}: ${chain.name} rank ${rank.rank}`).toBe(true); } } for (const entry of COA_TREE_ENTRIES.filter((candidate) => candidate.entryType === "Ability")) { const classId = coaClassIdFromDbcId(entry.classId); if (!classId) continue; for (const rank of entry.rankDescriptions) { expect(runtimeSpellIds.has(rank.spellId), `${classId}: ${entry.name} rank ${rank.rank}`).toBe(true); } } }); it("gives every family and rank finite, executable runtime data", () => { expect(new Set(abilities.map((ability) => ability.id)).size).toBe(abilities.length); for (const ability of abilities) { expect(ability.classId, ability.id).toBeTruthy(); expect(ability.name.length, ability.id).toBeGreaterThan(0); expect(ability.icon.startsWith("/assets/ui/spells/"), ability.id).toBe(true); expect(Number.isFinite(ability.range.min), ability.id).toBe(true); expect(Number.isFinite(ability.range.max), ability.id).toBe(true); expect(ability.range.max, ability.id).toBeGreaterThanOrEqual(ability.range.min); expect(ability.cooldownMs, ability.id).toBeGreaterThanOrEqual(0); for (const rank of ranksFor(ability)) { const label = `${ability.id} rank ${rank.rank} (${rank.spellId})`; expect(rank.spellId, label).toBeGreaterThan(0); expect(rank.level, label).toBeGreaterThan(0); expect(rank.description.length, label).toBeGreaterThan(0); expect(rank.castTimeMs, label).toBeGreaterThanOrEqual(0); expect(rank.cooldownMs, label).toBeGreaterThanOrEqual(0); expect(rank.gcdMs, label).toBeGreaterThanOrEqual(0); expect(Number.isFinite(rank.cost.amount), label).toBe(true); expect(rank.cost.amount, label).toBeGreaterThanOrEqual(0); expect(rank.effects.length, label).toBeGreaterThan(0); expect(rank.effects.some((effect) => effect.kind === "scripted" || effect.kind === "unsupported"), label).toBe(false); if (!ability.passive) { expect(rank.effects.some((effect) => effect.kind !== "utility" || effect.action !== "class-script"), label).toBe(true); } } } }); it("translates every standard DBC combat effect into its executable semantic", () => { const applyAuraTypes = new Set([0, 3, 6, 27, 35, 65, 119, 128, 129, 143, 190]); const triggerTypes = new Set([32, 64, 140, 141, 142, 148, 151, 160, 165, 169, 175, 178, 183]); const movementTypes = new Set([5, 29, 41, 42, 43, 79, 96, 98, 138, 144, 149]); for (const { ability, rank } of ranks) { const dbc = coaSpellById(rank.spellId)?.dbc; if (!dbc) continue; const kinds = new Set(rank.effects.map((effect) => effect.kind)); for (const effect of dbc.effects) { const label = `${ability.classId}: ${ability.name} rank ${rank.rank}, DBC ${effect.effectType}/${effect.auraType}`; if (effect.effectType === 2 || [17, 31, 58, 121].includes(effect.effectType)) expect(kinds.has("damage"), label).toBe(true); if ([10, 67, 136].includes(effect.effectType)) expect(kinds.has("heal"), label).toBe(true); if (applyAuraTypes.has(effect.effectType) && effect.auraType > 0) expect(kinds.has("apply-aura"), label).toBe(true); if (effect.auraType === 3 && effect.periodMs > 0) expect(kinds.has("dot"), label).toBe(true); if ([8, 20].includes(effect.auraType) && effect.periodMs > 0) expect(kinds.has("hot"), label).toBe(true); if (triggerTypes.has(effect.effectType) && effect.triggerSpellId > 0) expect(kinds.has("trigger-spell"), label).toBe(true); if (movementTypes.has(effect.effectType)) expect(kinds.has("movement"), label).toBe(true); if (effect.effectType === 38) expect(kinds.has("dispel"), label).toBe(true); if (effect.effectType === 68) expect(kinds.has("interrupt"), label).toBe(true); if ([18, 94, 113, 117].includes(effect.effectType)) expect(kinds.has("resurrection"), label).toBe(true); if ([192, 195].includes(effect.effectType)) expect(kinds.has("cooldown-reset"), label).toBe(true); } } }); it("preserves percentages, actual power pools, and shared cooldown groups", () => { const percentageCosts = ranks.filter(({ rank }) => rank.cost.percentage); expect(percentageCosts.length).toBeGreaterThan(1_500); expect(ranks.some(({ rank }) => rank.cost.resource === "health")).toBe(true); expect(ranks.some(({ rank }) => rank.cost.resource === "energy")).toBe(true); expect(ranks.some(({ rank }) => rank.cost.resource === "rage")).toBe(true); expect(ranks.some(({ rank }) => rank.cost.resource === "runic-power")).toBe(true); const ascension = byName("sun-cleric", "Solar Invocation: Ascension"); const revelation = byName("sun-cleric", "Solar Invocation: Revelation"); expect(ascension.cooldownGroup).toBe("coa:solar-invocation-spells"); expect(revelation.cooldownGroup).toBe(ascension.cooldownGroup); expect(revelation.target).toBe("any"); const profound = byName("templar", "Profound Enlightenment"); expect(profound.effects.filter((effect) => effect.kind === "cooldown-reset")).toEqual([ expect.objectContaining({ mode: "reduce", amountPercent: 0.5 }), expect.objectContaining({ mode: "reduce", amountPercent: 0.5 }), expect.objectContaining({ mode: "reduce", amountPercent: 0.5 }), ]); }); it("keeps representative healing, protection, control, proc, trigger, and reset spells functional", () => { expect(byName("sun-cleric", "Solar Invocation: Ascension")).toMatchObject({ target: "self", radius: 40 }); expect(byName("guardian", "Feats of Strength")).toMatchObject({ target: "friendly" }); expect(byName("knight-of-xoroth", "Black Shield").ranks?.every((rank) => rank.effects.some((effect) => effect.kind === "shield"))).toBe(true); expect(byName("witch-hunter", "Brand of the Unworthy").effects).toContainEqual(expect.objectContaining({ kind: "status", status: "sleep" })); expect(byName("necromancer", "Plaguestorm").ranks?.every((rank) => rank.effects.some((effect) => effect.kind === "dot"))).toBe(true); expect(byName("templar", "Profound Enlightenment").effects.filter((effect) => effect.kind === "cooldown-reset")).toHaveLength(3); expect(byName("sun-cleric", "New Day").effects.filter((effect) => effect.kind === "cooldown-reset")).toHaveLength(3); const gaze = byName("cultist", "Gaze of C'Thun"); expect(gaze.target).toBe("any"); expect(gaze.effects.some((effect) => effect.kind === "trigger-spell")).toBe(true); expect(gaze.effects.some((effect) => effect.kind === "damage")).toBe(true); expect(gaze.effects.some((effect) => effect.kind === "heal")).toBe(true); const razorice = byName("necromancer", "Razorice"); const procs = razorice.effects .filter((effect): effect is Extract => effect.kind === "apply-aura") .flatMap((effect) => effect.aura.procs ?? []); expect(procs.length).toBeGreaterThan(0); expect(procs.every((proc) => proc.action.kind === "damage" || proc.action.kind === "custom")).toBe(true); }); it("keeps every rank spell lookup executable, including recursively expanded trigger spells", () => { for (const { ability, rank } of ranks) { expect(rankEffectsForSpell(rank.spellId).length, `${ability.name} ${rank.spellId}`).toBeGreaterThan(0); } const gaze = byName("cultist", "Gaze of C'Thun"); expect(gaze.effects.filter((effect) => effect.kind === "trigger-spell")).toEqual(expect.arrayContaining([ expect.objectContaining({ spellId: 520334 }), expect.objectContaining({ spellId: 520335 }), ])); }); it("ships executable hidden payloads for proc-triggered DBC spell IDs", () => { // Geode Barrage now uses its ranked parent payload instead of hidden placeholder 803138. expect(Object.keys(COA_TRIGGERED_ABILITIES_BY_SPELL_ID)).toHaveLength(584); expect(COA_TRIGGERED_ABILITIES_BY_SPELL_ID[803138]).toBeUndefined(); expect(COA_TRIGGERED_ABILITIES_BY_SPELL_ID[800951]).toMatchObject({ name: "Battle Rhythm", effects: expect.arrayContaining([{ kind: "resource", amount: 5 }]), }); expect(Object.values(COA_TRIGGERED_ABILITIES_BY_SPELL_ID).filter((ability) => ( ability.effects.some((effect) => effect.kind !== "utility") ))).toHaveLength(545); for (const [spellId, ability] of Object.entries(COA_TRIGGERED_ABILITIES_BY_SPELL_ID)) { expect(ability.dbcSpellId, spellId).toBe(Number(spellId)); expect(ability.effects.length, spellId).toBeGreaterThan(0); expect(ability.effects.some((effect) => effect.kind === "scripted" || effect.kind === "unsupported"), spellId).toBe(false); } }); it("does not invent damage for proc auras or cooldown-only trigger payloads", () => { expect(COA_TRIGGERED_ABILITIES_BY_SPELL_ID[707821].effects.some((effect) => effect.kind === "damage")).toBe(false); expect(COA_TRIGGERED_ABILITIES_BY_SPELL_ID[807661].effects.every((effect) => effect.kind === "utility")).toBe(true); expect(COA_TRIGGERED_ABILITIES_BY_SPELL_ID[712682].effects).toContainEqual( expect.objectContaining({ kind: "trigger-spell", spellId: 712476, expanded: true }), ); }); });