forked from phenom/healer-man
Update Healer Man server
This commit is contained in:
1 parent
f90ba7d989
commit
189b94d6b7
51 files changed
+3448
-154
No files matched your search
@@ -0,0 +1,144 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { abilitiesForCharacter, learnedAbilityForBuild } from "./abilityCatalog";
|
||||
import { isValidRomClassPair } from "../app/romDualClass";
|
||||
import { loadCanonicalBenchmarkBuilds } from "./benchmark/benchmarkBuilds";
|
||||
import { readinessStatusFor } from "./companionSpecReadiness";
|
||||
import { resolvedEquipmentLoadout } from "./heirloomEquipment";
|
||||
import { partyRolesForClass } from "./partyRoles";
|
||||
import { createConfiguredPartyMember, createDungeonParty, createSpectatorDungeonParty, usePartyStore } from "./partyStore";
|
||||
import { ROM_ORDINARY_ABILITIES_BY_CLASS } from "./romAbilityCatalog";
|
||||
import { ROM_FIVE_IDENTITIES } from "./romFiveIdentities";
|
||||
import { romDualClassBaseStats, romNativeClassBaseStats } from "./romStats";
|
||||
import { romGearProfile } from "./specializationCatalog";
|
||||
|
||||
const CASES = [
|
||||
{ classId: "rom-priest", role: "healer", layout: "caster" },
|
||||
{ classId: "rom-knight", role: "tank", layout: "weapon-shield" },
|
||||
{ classId: "rom-scout", role: "damage", layout: "ranged" },
|
||||
{ classId: "rom-rogue", role: "damage", layout: "dual-wield" },
|
||||
{ classId: "rom-mage", role: "damage", layout: "caster" },
|
||||
] as const;
|
||||
|
||||
describe("RoM five class validation scope", () => {
|
||||
it.each([10, 30, 60, 80])("keeps each secondary class at the primary level for stat growth at level %i", (level) => {
|
||||
for (const identity of ROM_FIVE_IDENTITIES) {
|
||||
const primary = romNativeClassBaseStats(identity.classId, level);
|
||||
const secondary = romNativeClassBaseStats(identity.secondaryClassId, level);
|
||||
const combined = romDualClassBaseStats(identity.classId, identity.secondaryClassId, level)!;
|
||||
for (const stat of Object.keys(primary) as (keyof typeof primary)[]) {
|
||||
expect(combined[stat]).toBeCloseTo(primary[stat] + secondary[stat] * 0.1, 6);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("builds the requested named class pairs and races without talents", async () => {
|
||||
const builds = await loadCanonicalBenchmarkBuilds(60, CASES.map((entry) => entry.classId));
|
||||
const selected = builds.filter((build) => CASES.some((entry) => (
|
||||
entry.classId === build.classId && entry.role === build.role
|
||||
)));
|
||||
expect(selected).toHaveLength(5);
|
||||
for (const build of selected) {
|
||||
expect(build.orientation).toBe("Primary");
|
||||
const identity = ROM_FIVE_IDENTITIES.find((entry) => entry.classId === build.classId && entry.role === build.role)!;
|
||||
expect(build).toMatchObject({
|
||||
characterName: identity.name,
|
||||
raceId: identity.raceId,
|
||||
secondaryClassId: identity.secondaryClassId,
|
||||
});
|
||||
expect(isValidRomClassPair(identity.raceId, identity.classId, identity.secondaryClassId)).toBe(true);
|
||||
expect(build.talentRanks).toEqual({});
|
||||
expect(readinessStatusFor(romGearProfile(build.classId, build.role).id)).toBe("needs-testing");
|
||||
}
|
||||
});
|
||||
|
||||
it("uses the same identities for live NPCs and unlocks their secondary class at level 10", () => {
|
||||
const found = new Map<string, (typeof ROM_FIVE_IDENTITIES)[number]>();
|
||||
for (let seed = 0; seed < 40; seed += 1) {
|
||||
for (const member of createSpectatorDungeonParty(`rom-pairs-${seed}`, 60, "rom")) {
|
||||
const identity = ROM_FIVE_IDENTITIES.find((entry) => entry.classId === member.classId && entry.role === member.role);
|
||||
if (!identity) continue;
|
||||
expect(member).toMatchObject({
|
||||
name: identity.name,
|
||||
raceId: identity.raceId,
|
||||
secondaryClassId: identity.secondaryClassId,
|
||||
talentRanks: {},
|
||||
});
|
||||
found.set(identity.classId, identity);
|
||||
}
|
||||
}
|
||||
expect(found.size).toBe(5);
|
||||
expect(new Set(ROM_FIVE_IDENTITIES.map((entry) => entry.name)).size).toBe(5);
|
||||
usePartyStore.getState().initializeSpectatorParty("rom-unlock", 1, "rom");
|
||||
expect(usePartyStore.getState().members.every((member) => member.secondaryClassId === null)).toBe(true);
|
||||
usePartyStore.getState().syncLevel(10);
|
||||
for (const member of usePartyStore.getState().members) {
|
||||
const identity = ROM_FIVE_IDENTITIES.find((entry) => entry.classId === member.classId && entry.role === member.role);
|
||||
if (identity) expect(member.secondaryClassId).toBe(identity.secondaryClassId);
|
||||
}
|
||||
const mage = ROM_FIVE_IDENTITIES.find((entry) => entry.classId === "rom-mage")!;
|
||||
expect(createConfiguredPartyMember({
|
||||
name: mage.name,
|
||||
classId: mage.classId,
|
||||
secondaryClassId: mage.secondaryClassId,
|
||||
raceId: mage.raceId,
|
||||
specialization: "Primary",
|
||||
role: mage.role,
|
||||
}, 60, 4, "rom-manual")).toMatchObject({
|
||||
name: "Durnik",
|
||||
raceId: "rom-dwarf",
|
||||
secondaryClassId: "rom-warlock",
|
||||
});
|
||||
});
|
||||
|
||||
it("adds general secondary skills to each pair's learned kit", () => {
|
||||
for (const identity of ROM_FIVE_IDENTITIES) {
|
||||
const abilities = abilitiesForCharacter(identity.classId, identity.secondaryClassId);
|
||||
expect(abilities.some((ability) => ability.classId === identity.secondaryClassId
|
||||
&& ability.romSkillGroup === "general")).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("uses ordinary learned abilities with no elite or talent-point gate", () => {
|
||||
for (const { classId, role } of CASES) {
|
||||
expect(partyRolesForClass(classId)).toContain(role);
|
||||
const abilities = abilitiesForCharacter(classId, null);
|
||||
expect(abilities.map((ability) => ability.id))
|
||||
.toEqual(ROM_ORDINARY_ABILITIES_BY_CLASS[classId].map((ability) => ability.id));
|
||||
expect(abilities.every((ability) => !ability.secondaryClassId
|
||||
&& ability.romSkillGroup !== "elite"
|
||||
&& !ability.talentNodeId)).toBe(true);
|
||||
for (const ability of abilities) {
|
||||
const learned = learnedAbilityForBuild(ability, 80, {});
|
||||
expect(learned?.id, ability.id).toBe(ability.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("equips the role defining weapons in the manual cohort", () => {
|
||||
for (const { classId, role, layout } of CASES) {
|
||||
expect(romGearProfile(classId, role).weaponLayout).toBe(layout);
|
||||
const loadout = resolvedEquipmentLoadout({}, [], {
|
||||
ownerId: `rom-five:${classId}`,
|
||||
ownerKind: "party",
|
||||
classId,
|
||||
specializationId: null,
|
||||
role,
|
||||
level: 60,
|
||||
});
|
||||
if (classId === "rom-knight") expect(loadout["off-hand"]?.armorType).toBe("shield");
|
||||
if (classId === "rom-scout") expect(loadout.ranged?.weaponType).toBe("bow");
|
||||
if (classId === "rom-rogue") {
|
||||
expect(loadout["main-hand"]?.weaponType).toBe("dagger");
|
||||
expect(loadout["off-hand"]?.weaponType).toBe("dagger");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("seats Knight only as a tank in RoM generated parties", () => {
|
||||
const parties = Array.from({ length: 40 }, (_, index) => createDungeonParty(`rom-knight-${index}`, 60, "healer", 5, "rom"));
|
||||
expect(parties.some((party) => party.some((member) => member.classId === "rom-knight"))).toBe(true);
|
||||
for (const party of parties) for (const member of party) {
|
||||
if (member.classId === "rom-knight") expect(member.role).toBe("tank");
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user