Files
healer-man/src/game/coaSourceScope.test.ts
T

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);
});
});