Update Healer Man server

This commit is contained in:
phenom committed 2026-09-23 16:14:19 -04:00
1 parent f90ba7d989
commit 189b94d6b7
51 files changed
+3448 -154

No files matched your search

+144
View File
@@ -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");
}
});
});