import { describe, expect, it } from "vitest"; import generated from "./romPlayerCatalog.generated.json"; import audit from "../../scripts/runewaker-pipeline/snapshots/runewaker-player-skill-audit.json"; import { abilitiesForCharacter, abilityAtLevel, defaultActionBarForCharacter, } from "./abilityCatalog"; import { ROM_ALL_ABILITIES, ROM_ELITE_ABILITIES } from "./romAbilityCatalog"; import { romDualClassBaseStats, romNativeClassBaseStats } from "./romStats"; import { EMPTY_ITEM_STATS } from "./itemStats"; import { deriveCharacterStats } from "./wotlkStats"; const RACE_MATRIX = { "rom-human": ["rom-warrior", "rom-scout", "rom-rogue", "rom-mage", "rom-priest", "rom-knight"], "rom-elf": ["rom-warrior", "rom-scout", "rom-rogue", "rom-mage", "rom-warden", "rom-druid"], "rom-dwarf": ["rom-warrior", "rom-rogue", "rom-mage", "rom-priest", "rom-warlock", "rom-champion"], }; const ELITE_LEVELS = [15, 20, 25, 30, 35, 40, 45, 50, 60, 70]; describe("generated RuneWaker player catalog", () => { it("imports the exact released roster and race eligibility without Duelist", () => { expect(generated.canonicalClasses.map((entry) => entry.name)).toEqual([ "Warrior", "Scout", "Rogue", "Mage", "Priest", "Knight", "Warden", "Druid", "Warlock", "Champion", ]); expect(generated.canonicalClasses.some((entry) => entry.name === "Duelist")).toBe(false); expect(generated.raceMatrix).toEqual(RACE_MATRIX); expect(generated.eliteLevels).toEqual(ELITE_LEVELS); expect(audit.coverage).toMatchObject({ releasedClasses: 10, duelistExcluded: true, races: 3, orderedPairs: 66, eliteMilestones: 660, ordinaryFamilies: 269, iconResolved: 929, uniqueNativeIcons: 918, unresolvedVisible: 0, }); expect(audit.hashes.interfaceFdb).toMatch(/^[a-f0-9]{64}$/); }); it("resolves every ordered pair to all ten executable elite milestones", () => { expect(ROM_ELITE_ABILITIES).toHaveLength(660); const pairGroups = new Map(); for (const ability of ROM_ELITE_ABILITIES) { const key = `${ability.classId}:${ability.secondaryClassId}`; pairGroups.set(key, [...(pairGroups.get(key) ?? []), ability]); } expect(pairGroups.size).toBe(66); for (const elites of pairGroups.values()) { expect(elites.map((ability) => ability.unlockLevel)).toEqual(ELITE_LEVELS); expect(elites.every((ability) => ability.source === "runewaker" && ability.effects.length > 0)).toBe(true); } expect(ROM_ALL_ABILITIES).toHaveLength(929); expect(ROM_ALL_ABILITIES.every((ability) => ( /^\/assets\/ui\/rom-spells\/\d+\.png$/.test(ability.icon) && ability.icon === generated.skills.find((skill) => skill.id === ability.id)?.icon ))).toBe(true); expect(generated.skills.every((ability) => ( ability.iconResolution === "native-interface-fdb" && /^\/assets\/ui\/rom-spells\/\d+\.png$/.test(ability.icon) ))).toBe(true); }); it("composes only primary, secondary-general, and active-orientation elite skills", () => { const warriorScout = abilitiesForCharacter("rom-warrior", "rom-scout"); const scoutWarrior = abilitiesForCharacter("rom-scout", "rom-warrior"); expect(warriorScout.some((ability) => ability.classId === "rom-scout" && ability.romSkillGroup === "primary")).toBe(false); expect(warriorScout.some((ability) => ability.classId === "rom-scout" && ability.romSkillGroup === "general")).toBe(true); expect(warriorScout.filter((ability) => ability.secondaryClassId)).toHaveLength(10); expect(warriorScout.filter((ability) => ability.secondaryClassId).every((ability) => ( ability.classId === "rom-warrior" && ability.secondaryClassId === "rom-scout" ))).toBe(true); expect(scoutWarrior.filter((ability) => ability.secondaryClassId).every((ability) => ( ability.classId === "rom-scout" && ability.secondaryClassId === "rom-warrior" ))).toBe(true); expect(defaultActionBarForCharacter("rom-warrior", "rom-scout", 14).some((id) => id.includes("-elite-"))).toBe(false); expect(defaultActionBarForCharacter("rom-warrior", "rom-scout", 15).filter((id) => id.includes("-elite-"))).toHaveLength(1); }); it("selects the highest eligible source rank automatically", () => { const ranked = ROM_ALL_ABILITIES.find((ability) => (ability.ranks?.length ?? 0) > 1)!; const eligible = ranked.ranks!.filter((rank) => rank.level <= 80).at(-1)!; expect(abilityAtLevel(ranked, 80).dbcSpellId).toBe(eligible.spellId); }); it("preserves every source rank and its full flat or percentage cost", () => { const normalizedCosts = (rows: readonly { resource: string; amount: number; percentage?: boolean }[]) => rows.map((cost) => ({ resource: cost.resource, amount: cost.amount, ...(cost.percentage ? { percentage: true } : {}), })); const generatedRanks = generated.skills.reduce((sum, ability) => sum + ability.ranks.length, 0); expect(generatedRanks).toBe(934); expect(Math.max(...generated.skills.flatMap((ability) => ( ability.costs.filter((cost) => !cost.percentage).map((cost) => cost.amount) )))).toBe(400); for (const source of generated.skills) { const runtime = ROM_ALL_ABILITIES.find((ability) => ability.id === source.id)!; expect(runtime.costs, `${source.name} base costs`).toEqual(normalizedCosts(source.costs)); expect(runtime.ranks).toHaveLength(source.ranks.length); for (const [index, sourceRank] of source.ranks.entries()) { expect(runtime.ranks?.[index]?.spellId, `${source.name} rank ${sourceRank.rank}`).toBe(sourceRank.sourceSkillId); expect(runtime.ranks?.[index]?.costs, `${source.name} rank ${sourceRank.rank} costs`).toEqual(normalizedCosts(sourceRank.costs)); } } }); it("adds exactly ten percent of the secondary class base stats", () => { const primary = romNativeClassBaseStats("rom-warrior", 40); const secondary = romNativeClassBaseStats("rom-scout", 40); const combined = romDualClassBaseStats("rom-warrior", "rom-scout", 40)!; for (const stat of Object.keys(primary) as (keyof typeof primary)[]) { expect(combined[stat]).toBeCloseTo(primary[stat] + secondary[stat] * 0.1, 8); } }); it("gives level-one healers native mana pools and imports their real spell costs", () => { expect(deriveCharacterStats("rom-priest", 1, EMPTY_ITEM_STATS, "rom-human").mana).toBe(103); expect(deriveCharacterStats("rom-druid", 1, EMPTY_ITEM_STATS, "rom-elf").mana).toBe(86); expect(generated.skills.find((ability) => ability.id === "rom-priest-general-1")?.costs).toEqual([ { resource: "mana", amount: 12, percentage: false }, ]); expect(generated.skills.find((ability) => ability.id === "rom-druid-primary-0")?.costs).toEqual([ { resource: "mana", amount: 12, percentage: false }, ]); }); });