forked from phenom/healer-man
223 lines
7.3 KiB
TypeScript
223 lines
7.3 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import type { AbilityDefinition } from "./abilityCatalog";
|
|
import {
|
|
ascensionSpellById,
|
|
coaSpellById,
|
|
type CoaProgressionChain,
|
|
} from "./coaLiveCatalog";
|
|
import { ascensionProgressionAbility } from "./coaAbilityCatalog";
|
|
|
|
const fixture = vi.hoisted(() => {
|
|
const makeEffect = (effectType: number, triggerSpellId = 0, basePoints = 1) => ({
|
|
index: 0,
|
|
effectType,
|
|
auraType: 0,
|
|
basePoints,
|
|
dieSides: 0,
|
|
pointsPerLevel: 0,
|
|
mechanic: 0,
|
|
implicitTargetA: 6,
|
|
implicitTargetB: 0,
|
|
radiusIndex: 0,
|
|
radius: 0,
|
|
periodMs: 0,
|
|
valueMultiplier: 0,
|
|
chainTargets: 0,
|
|
itemType: 0,
|
|
miscValue: 0,
|
|
miscValueB: 0,
|
|
triggerSpellId,
|
|
pointsPerCombo: 0,
|
|
coefficient: 0,
|
|
});
|
|
|
|
const makeDbc = (
|
|
id: number,
|
|
name: string,
|
|
effects: ReturnType<typeof makeEffect>[] = [],
|
|
startRecoveryTimeMs = 1_500,
|
|
) => ({
|
|
id,
|
|
name,
|
|
rankText: "Rank 1",
|
|
description: `${name} fixture description`,
|
|
categoryId: 0,
|
|
dispelType: 0,
|
|
mechanic: 0,
|
|
attributes: Array(8).fill(0),
|
|
castTimeIndex: 0,
|
|
castTimeMs: 0,
|
|
recoveryTimeMs: 0,
|
|
categoryRecoveryTimeMs: 0,
|
|
interruptFlags: 0,
|
|
auraInterruptFlags: 0,
|
|
channelInterruptFlags: 0,
|
|
procFlags: 0,
|
|
procChance: 0,
|
|
procCharges: 0,
|
|
maxLevel: 80,
|
|
baseLevel: 1,
|
|
spellLevel: 1,
|
|
durationIndex: 0,
|
|
durationMs: 0,
|
|
powerType: 0,
|
|
powerCost: 0,
|
|
powerCostPerLevel: 0,
|
|
powerCostPerSecond: 0,
|
|
powerCostPerSecondPerLevel: 0,
|
|
rangeIndex: 0,
|
|
rangeMin: 0,
|
|
rangeMax: 30,
|
|
projectileSpeed: 0,
|
|
stackAmount: 0,
|
|
equippedItemClass: 0,
|
|
equippedItemSubclassMask: 0,
|
|
equippedItemInventoryMask: 0,
|
|
iconId: 0,
|
|
powerCostPercentage: 0,
|
|
startRecoveryCategory: 0,
|
|
startRecoveryTimeMs,
|
|
maximumTargets: 0,
|
|
damageClass: 0,
|
|
preventionType: 0,
|
|
schoolMask: 1,
|
|
runeCostId: 0,
|
|
powerDisplayId: 0,
|
|
effects,
|
|
});
|
|
|
|
const makeLiveSpell = (id: number, name: string, dbc?: ReturnType<typeof makeDbc>) => ({
|
|
id,
|
|
classIds: [11, 27],
|
|
name,
|
|
iconBasename: "fixture",
|
|
level: 1,
|
|
rank: "Rank 1",
|
|
description: `${name} live description`,
|
|
sourceIds: [],
|
|
advancementType: "",
|
|
advancementTabId: 0,
|
|
talented: false,
|
|
excluded: false,
|
|
...(dbc ? { dbc } : {}),
|
|
});
|
|
|
|
const mainSpells = [
|
|
makeLiveSpell(900, "Shared Classic Root", makeDbc(900, "Classic Root", [], 700)),
|
|
makeLiveSpell(910, "Shared Trigger Root", makeDbc(910, "Classic Trigger Root", [makeEffect(64, 920)])),
|
|
makeLiveSpell(920, "Shared First Payload", makeDbc(920, "Classic First Payload", [makeEffect(64, 930)])),
|
|
makeLiveSpell(930, "Shared Nested Payload", makeDbc(930, "Classic Nested Payload", [makeEffect(2)])),
|
|
makeLiveSpell(940, "Shared Missing CoA Base", makeDbc(940, "Area52 Overlay Payload", [makeEffect(2)])),
|
|
{ ...makeLiveSpell(950, "Classic Only Root", makeDbc(950, "Classic Only Root", [makeEffect(2)])), classIds: [11] },
|
|
];
|
|
const coaOverrides = [
|
|
makeLiveSpell(900, "Shared CoA Root", makeDbc(900, "CoA Root", [], 0)),
|
|
makeLiveSpell(910, "Shared CoA Trigger Root", makeDbc(910, "CoA Trigger Root", [makeEffect(64, 920)])),
|
|
makeLiveSpell(920, "Shared CoA First Payload", makeDbc(920, "CoA First Payload", [makeEffect(64, 930)])),
|
|
makeLiveSpell(930, "Shared CoA Nested Payload", makeDbc(930, "CoA Nested Payload", [makeEffect(67)])),
|
|
makeLiveSpell(940, "Shared CoA Missing Base"),
|
|
];
|
|
const overlayPayload = makeDbc(940, "Area52 Overlay Payload", [makeEffect(2)]);
|
|
|
|
return {
|
|
liveData: {
|
|
source: {
|
|
builderUrl: "fixture",
|
|
databaseClassUrl: "fixture",
|
|
realmId: 1,
|
|
realmSlug: "fixture",
|
|
realmName: "Fixture",
|
|
maximumLevel: 80,
|
|
schemaVersion: {},
|
|
synchronizedAt: "2026-01-01T00:00:00.000Z",
|
|
spellDbcPath: "fixture/classic/Spell.dbc",
|
|
supportDbcDirectory: "fixture/support",
|
|
},
|
|
counts: {},
|
|
classes: [],
|
|
entries: [],
|
|
progressionChains: [],
|
|
spells: mainSpells,
|
|
coaSpellOverrides: coaOverrides,
|
|
},
|
|
triggeredCatalog: {
|
|
spells: [overlayPayload],
|
|
classicSpells: [overlayPayload],
|
|
},
|
|
};
|
|
});
|
|
|
|
vi.mock("./coaLiveData.generated.json", () => ({ default: fixture.liveData }));
|
|
vi.mock("./generated/coaTriggeredSpellCatalog.json", () => ({ default: fixture.triggeredCatalog }));
|
|
|
|
function chain(spellId: number): CoaProgressionChain {
|
|
return {
|
|
id: `scope-${spellId}`,
|
|
classId: 11,
|
|
name: `Scope ${spellId}`,
|
|
iconBasename: "fixture",
|
|
talented: false,
|
|
advancementType: "",
|
|
trainer: false,
|
|
ranks: [{ spellId, rank: 1, level: 1 }],
|
|
};
|
|
}
|
|
|
|
function ability(spellId: number, classId: "druid" | "sun-cleric"): AbilityDefinition {
|
|
const classic = classId === "druid";
|
|
const result = ascensionProgressionAbility(
|
|
chain(spellId),
|
|
classId,
|
|
"mana",
|
|
classic ? "ascension-progression" : "coa-progression",
|
|
);
|
|
if (!result) throw new Error(`Fixture spell ${spellId} did not produce an ability`);
|
|
return result;
|
|
}
|
|
|
|
describe("classic and CoA spell source scope", () => {
|
|
it("excludes classic-only records from CoA lookup", () => {
|
|
expect(ascensionSpellById(950)?.dbc?.name).toBe("Classic Only Root");
|
|
expect(coaSpellById(950)).toBeNull();
|
|
expect(ability(950, "druid").effects.some((effect) => effect.kind === "damage")).toBe(true);
|
|
expect(ability(950, "sun-cleric").effects.some((effect) => effect.kind === "damage")).toBe(false);
|
|
});
|
|
|
|
it("resolves a shared root spell through separate classic and CoA wrappers", () => {
|
|
expect(ascensionSpellById(900)?.dbc?.name).toBe("Classic Root");
|
|
expect(coaSpellById(900)?.dbc?.name).toBe("CoA Root");
|
|
|
|
expect(ability(900, "druid").ranks?.[0].gcdMs).toBe(700);
|
|
expect(ability(900, "sun-cleric").ranks?.[0].gcdMs).toBe(0);
|
|
});
|
|
|
|
it("keeps nested triggered payloads in the root spell's class scope", () => {
|
|
const classicEffects = ability(910, "druid").ranks?.[0].effects ?? [];
|
|
const coaEffects = ability(910, "sun-cleric").ranks?.[0].effects ?? [];
|
|
|
|
expect(classicEffects).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({ kind: "trigger-spell", spellId: 920 }),
|
|
expect.objectContaining({ kind: "trigger-spell", spellId: 930 }),
|
|
expect.objectContaining({ kind: "damage" }),
|
|
]));
|
|
expect(classicEffects.some((effect) => effect.kind === "heal")).toBe(false);
|
|
expect(coaEffects).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({ kind: "trigger-spell", spellId: 920 }),
|
|
expect.objectContaining({ kind: "trigger-spell", spellId: 930 }),
|
|
expect.objectContaining({ kind: "heal" }),
|
|
]));
|
|
expect(coaEffects.some((effect) => effect.kind === "damage")).toBe(false);
|
|
});
|
|
|
|
it("does not fall back to classic overlay DBC when a CoA override has no base row", () => {
|
|
expect(ascensionSpellById(940)?.dbc?.name).toBe("Area52 Overlay Payload");
|
|
expect(coaSpellById(940)).toMatchObject({ id: 940, name: "Shared CoA Missing Base" });
|
|
expect(coaSpellById(940)?.dbc).toBeUndefined();
|
|
|
|
const classicEffects = ability(940, "druid").ranks?.[0].effects ?? [];
|
|
const coaEffects = ability(940, "sun-cleric").ranks?.[0].effects ?? [];
|
|
expect(classicEffects.some((effect) => effect.kind === "damage")).toBe(true);
|
|
expect(coaEffects.some((effect) => effect.kind === "damage")).toBe(false);
|
|
});
|
|
});
|