131 lines
6.8 KiB
TypeScript
131 lines
6.8 KiB
TypeScript
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<string, typeof ROM_ELITE_ABILITIES[number][]>();
|
|
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 },
|
|
]);
|
|
});
|
|
});
|