Update Healer Man server
This commit is contained in:
1 parent
39458d5de6
commit
6cc1b9217f
203 files changed
+133929
-31271
No files matched your search
@@ -0,0 +1,365 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { auraAdjustedCharacterStats, PLAYER_AURA_ENTITY_ID, useCombatStore } from "./combatStore";
|
||||
import { clearMobRuntimeRegistry } from "./mobRuntimeRegistry";
|
||||
import { clearPartyRuntimeRegistry } from "./partyRuntimeRegistry";
|
||||
import { ROM_FIVE_IDENTITIES } from "./romFiveIdentities";
|
||||
import { EMPTY_ITEM_STATS } from "./itemStats";
|
||||
import {
|
||||
createConfiguredPartyMember,
|
||||
createSpectatorDungeonParty,
|
||||
usePartyStore,
|
||||
} from "./partyStore";
|
||||
import type { InventoryItem } from "./lootTypes";
|
||||
import { deriveCharacterStats } from "./wotlkStats";
|
||||
import { resolvedEquipmentItems } from "./heirloomEquipment";
|
||||
|
||||
const LEVELS = [1, 9, 10, 30, 60, 79, 80] as const;
|
||||
const FIVE_IDENTITY_SPECTATOR_SEED = "rom-native-five-generated-60";
|
||||
|
||||
function nativePriestTestTrinket(): InventoryItem {
|
||||
const instanceId = "rom-native-priest-level-sync-trinket";
|
||||
return {
|
||||
instanceId,
|
||||
itemId: 990_001,
|
||||
itemKey: instanceId,
|
||||
name: "Native Priest level-sync test trinket",
|
||||
quality: "rare",
|
||||
inventorySlot: "trinket",
|
||||
armorType: null,
|
||||
weaponType: null,
|
||||
itemLevel: 30,
|
||||
sourceItemLevel: 30,
|
||||
stats: { ...EMPTY_ITEM_STATS, romNative: { stamina: 250, health: 500, mana: 180 } },
|
||||
quantity: 1,
|
||||
sourceBossKey: "integration-test",
|
||||
sourceBossName: "Integration Test",
|
||||
acquiredAt: 0,
|
||||
};
|
||||
}
|
||||
|
||||
describe("native RoM stats for the five live classes", () => {
|
||||
beforeEach(() => {
|
||||
clearMobRuntimeRegistry();
|
||||
clearPartyRuntimeRegistry();
|
||||
usePartyStore.setState({ members: [] });
|
||||
useCombatStore.getState().initializeCharacter({ classId: "rom-priest", level: 1 });
|
||||
});
|
||||
|
||||
it.each(LEVELS)("keeps player, generated, and configured actor stats aligned at level %i", (level) => {
|
||||
usePartyStore.getState().initializeSpectatorParty(FIVE_IDENTITY_SPECTATOR_SEED, level, "rom");
|
||||
const generatedMembers = usePartyStore.getState().members;
|
||||
expect(generatedMembers).toHaveLength(5);
|
||||
|
||||
for (const identity of ROM_FIVE_IDENTITIES) {
|
||||
const generated = generatedMembers.find((member) => member.name === identity.name);
|
||||
expect(generated, identity.name).toBeDefined();
|
||||
if (!generated) throw new Error(`Generated RoM party did not include ${identity.name}.`);
|
||||
const secondaryClassId = level >= 10 ? identity.secondaryClassId : null;
|
||||
expect(generated).toMatchObject({
|
||||
classId: identity.classId,
|
||||
raceId: identity.raceId,
|
||||
role: identity.role,
|
||||
level,
|
||||
secondaryClassId,
|
||||
statRuleset: "rom",
|
||||
});
|
||||
usePartyStore.setState({ members: generatedMembers });
|
||||
|
||||
useCombatStore.getState().initializeCharacter({
|
||||
classId: identity.classId,
|
||||
raceId: identity.raceId,
|
||||
gender: generated.gender,
|
||||
role: identity.role,
|
||||
specializationId: generated.specializationId,
|
||||
secondaryClassId,
|
||||
level,
|
||||
statRuleset: "rom",
|
||||
});
|
||||
useCombatStore.getState().synchronizeActors();
|
||||
|
||||
const playerState = useCombatStore.getState();
|
||||
const playerActor = playerState.actors[PLAYER_AURA_ENTITY_ID];
|
||||
const generatedActor = playerState.actors[generated.id];
|
||||
expect(playerState.statRuleset).toBe("rom");
|
||||
expect(playerActor).toMatchObject({ kind: "player", classId: identity.classId, statRuleset: "rom", level });
|
||||
expect(generatedActor).toMatchObject({
|
||||
kind: "companion", classId: identity.classId, statRuleset: "rom", level,
|
||||
});
|
||||
|
||||
const configured = createConfiguredPartyMember({
|
||||
name: identity.name,
|
||||
classId: identity.classId,
|
||||
secondaryClassId: identity.secondaryClassId,
|
||||
raceId: identity.raceId,
|
||||
gender: generated.gender,
|
||||
specialization: generated.specialization,
|
||||
specializationId: generated.specializationId ?? undefined,
|
||||
role: identity.role,
|
||||
statRuleset: "rom",
|
||||
}, level, 0, `rom-native-configured-${identity.classId}-${level}`);
|
||||
expect(configured).toMatchObject({ level, secondaryClassId, statRuleset: "rom" });
|
||||
|
||||
const playerGear = playerState.gearStats;
|
||||
expect(generated.gearStats).toEqual(playerGear);
|
||||
expect(configured.gearStats).toEqual(playerGear);
|
||||
const playerBaseNative = deriveCharacterStats(
|
||||
identity.classId, level, playerGear, identity.raceId, {}, secondaryClassId, generated.gender, "rom",
|
||||
).romNative!;
|
||||
const playerNative = auraAdjustedCharacterStats(
|
||||
deriveCharacterStats(
|
||||
identity.classId, level, playerGear, identity.raceId, {}, secondaryClassId, generated.gender, "rom",
|
||||
),
|
||||
playerState.auras,
|
||||
PLAYER_AURA_ENTITY_ID,
|
||||
).romNative!;
|
||||
const generatedNative = deriveCharacterStats(
|
||||
generated.classId, generated.level, generated.gearStats, generated.raceId, {},
|
||||
generated.secondaryClassId, generated.gender, generated.statRuleset,
|
||||
).romNative!;
|
||||
const configuredNative = deriveCharacterStats(
|
||||
configured.classId, configured.level, configured.gearStats, configured.raceId, {},
|
||||
configured.secondaryClassId, configured.gender, configured.statRuleset,
|
||||
).romNative!;
|
||||
expect(generatedNative).toEqual(playerBaseNative);
|
||||
expect(configuredNative).toEqual(playerBaseNative);
|
||||
expect(playerState.maxHealth).toBeCloseTo(playerBaseNative.health.max, 5);
|
||||
expect(generated.maxHealth).toBeCloseTo(generatedNative.health.max, 5);
|
||||
expect(generatedActor?.maxHealth).toBeCloseTo(generatedNative.health.max, 5);
|
||||
if (playerNative.usesMana) {
|
||||
expect(playerState.maxResourcePools.mana).toBeCloseTo(playerNative.mana.max, 5);
|
||||
expect(generated.resource).toBe(generated.maxResource);
|
||||
expect(generated.resourceMaximums?.mana).toBeCloseTo(generatedNative.mana.max, 5);
|
||||
expect(generatedActor?.resources.mana?.maximum).toBeCloseTo(generatedNative.mana.max, 5);
|
||||
expect(configured.resource).toBe(configured.maxResource);
|
||||
expect(configured.resourceMaximums?.mana).toBeCloseTo(configured.maxResource, 5);
|
||||
} else {
|
||||
expect(playerBaseNative.mana.max).toBe(0);
|
||||
expect(playerState.maxResourcePools.mana ?? 0).toBe(0);
|
||||
expect(generated.resourceMaximums?.mana ?? 0).toBe(0);
|
||||
}
|
||||
|
||||
usePartyStore.setState({ members: [configured] });
|
||||
useCombatStore.getState().synchronizeActors();
|
||||
const configuredActor = useCombatStore.getState().actors[configured.id];
|
||||
expect(configuredActor).toMatchObject({
|
||||
kind: "companion", classId: identity.classId, statRuleset: "rom", level,
|
||||
});
|
||||
expect(configuredActor?.maxHealth).toBeCloseTo(configuredNative.health.max, 5);
|
||||
if (configuredNative.usesMana) {
|
||||
expect(configuredActor?.resources.mana?.maximum).toBeCloseTo(configuredNative.mana.max, 5);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("applies the level 10 secondary unlock while party actors scale through level 80", () => {
|
||||
usePartyStore.getState().initializeSpectatorParty(FIVE_IDENTITY_SPECTATOR_SEED, 1, "rom");
|
||||
|
||||
for (const level of LEVELS) {
|
||||
usePartyStore.getState().syncLevel(level);
|
||||
useCombatStore.getState().synchronizeActors();
|
||||
for (const identity of ROM_FIVE_IDENTITIES) {
|
||||
const member = usePartyStore.getState().members.find((candidate) => candidate.name === identity.name)!;
|
||||
const secondaryClassId = level >= 10 ? identity.secondaryClassId : null;
|
||||
const native = deriveCharacterStats(
|
||||
identity.classId, level, member.gearStats, identity.raceId, {}, secondaryClassId, member.gender, "rom",
|
||||
).romNative!;
|
||||
const actor = useCombatStore.getState().actors[member.id];
|
||||
expect(member).toMatchObject({ level, secondaryClassId, statRuleset: "rom" });
|
||||
expect(member.maxHealth, `${identity.name} at level ${level}`).toBeCloseTo(native.health.max, 5);
|
||||
expect(member.health, `${identity.name} full health at level ${level}`).toBe(member.maxHealth);
|
||||
expect(actor, `${identity.name} actor at level ${level}`).toMatchObject({
|
||||
level, statRuleset: "rom", maxHealth: member.maxHealth,
|
||||
});
|
||||
if (native.usesMana) {
|
||||
expect(member.resourceMaximums?.mana).toBeCloseTo(native.mana.max, 5);
|
||||
expect(member.resourcePools.mana).toBe(member.resourceMaximums?.mana);
|
||||
expect(actor?.resources.mana?.maximum).toBeCloseTo(native.mana.max, 5);
|
||||
expect(actor?.resources.mana?.current).toBe(actor?.resources.mana?.maximum);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("retains wounded and defeated RoM members safely while their native caps grow", () => {
|
||||
usePartyStore.getState().initializeSpectatorParty(FIVE_IDENTITY_SPECTATOR_SEED, 30, "rom");
|
||||
const membersAt30 = usePartyStore.getState().members;
|
||||
const wounded = membersAt30.find((member) => member.name === "Serenna")!;
|
||||
const defeated = membersAt30.find((member) => member.name === "Rorik")!;
|
||||
const woundedHealthAt30 = Math.round(wounded.maxHealth * 0.43);
|
||||
const woundedManaAt30 = Math.round(wounded.maxResource * 0.37);
|
||||
usePartyStore.setState((state) => ({
|
||||
members: state.members.map((member) => member.id === wounded.id
|
||||
? {
|
||||
...member,
|
||||
health: woundedHealthAt30,
|
||||
resource: woundedManaAt30,
|
||||
resourcePools: { ...member.resourcePools, mana: woundedManaAt30 },
|
||||
}
|
||||
: member.id === defeated.id
|
||||
? { ...member, health: 0, status: "down" }
|
||||
: member),
|
||||
}));
|
||||
|
||||
usePartyStore.getState().syncLevel(60);
|
||||
const woundedAt60 = usePartyStore.getState().members.find((member) => member.id === wounded.id)!;
|
||||
const defeatedAt60 = usePartyStore.getState().members.find((member) => member.id === defeated.id)!;
|
||||
expect(woundedAt60.health).toBeLessThan(woundedAt60.maxHealth);
|
||||
expect(woundedAt60.health).toBeLessThanOrEqual(woundedAt60.maxHealth);
|
||||
expect(woundedAt60.health / woundedAt60.maxHealth)
|
||||
.toBeCloseTo(woundedHealthAt30 / wounded.maxHealth, 2);
|
||||
expect(woundedAt60.resource).toBeLessThan(woundedAt60.maxResource);
|
||||
expect(woundedAt60.resource).toBeLessThanOrEqual(woundedAt60.maxResource);
|
||||
expect(woundedAt60.resource / woundedAt60.maxResource)
|
||||
.toBeCloseTo(woundedManaAt30 / wounded.maxResource, 2);
|
||||
expect(defeatedAt60).toMatchObject({ health: 0, status: "down", level: 60 });
|
||||
});
|
||||
|
||||
it("keeps a configured Priest's full native Mana exact and bounds wounded pools when gear changes", () => {
|
||||
const identity = ROM_FIVE_IDENTITIES.find((candidate) => candidate.classId === "rom-priest")!;
|
||||
const member = createConfiguredPartyMember({
|
||||
name: identity.name,
|
||||
classId: identity.classId,
|
||||
secondaryClassId: identity.secondaryClassId,
|
||||
raceId: identity.raceId,
|
||||
gender: "male",
|
||||
specialization: "Primary",
|
||||
role: identity.role,
|
||||
statRuleset: "rom",
|
||||
}, 30, 0, "rom-native-priest-gear-sync");
|
||||
expect(member.resource).toBe(member.maxResource);
|
||||
expect(member.resourceMaximums?.mana).toBe(member.maxResource);
|
||||
const healthAt30 = Math.round(member.maxHealth * 0.43);
|
||||
const manaAt30 = Math.round(member.maxResource * 0.37);
|
||||
const wounded = {
|
||||
...member,
|
||||
health: healthAt30,
|
||||
resource: manaAt30,
|
||||
resourcePools: { ...member.resourcePools, mana: manaAt30 },
|
||||
};
|
||||
usePartyStore.setState({ members: [wounded] });
|
||||
const trinket = nativePriestTestTrinket();
|
||||
usePartyStore.getState().syncEquipment({ [member.id]: { trinket: trinket.instanceId } }, [trinket]);
|
||||
|
||||
const resized = usePartyStore.getState().members[0]!;
|
||||
const native = deriveCharacterStats(
|
||||
identity.classId, 30, resized.gearStats, identity.raceId, {}, identity.secondaryClassId, "male", "rom",
|
||||
).romNative!;
|
||||
expect(resized.maxHealth).toBeCloseTo(native.health.max, 5);
|
||||
expect(resized.health).toBeLessThanOrEqual(resized.maxHealth);
|
||||
expect(resized.health).toBeLessThan(resized.maxHealth);
|
||||
expect(resized.health / resized.maxHealth).toBeCloseTo(healthAt30 / member.maxHealth, 2);
|
||||
expect(resized.maxResource).toBeCloseTo(native.mana.max, 5);
|
||||
expect(resized.resourceMaximums?.mana).toBeCloseTo(native.mana.max, 5);
|
||||
expect(resized.resource).toBeLessThanOrEqual(resized.maxResource);
|
||||
expect(resized.resource).toBeLessThan(resized.maxResource);
|
||||
expect(resized.resource / resized.maxResource).toBeCloseTo(manaAt30 / member.maxResource, 2);
|
||||
});
|
||||
|
||||
it("selects native stats from the explicit mode, while omitted configured/player modes stay legacy WoW", () => {
|
||||
for (const identity of ROM_FIVE_IDENTITIES) {
|
||||
const configured = createConfiguredPartyMember({
|
||||
name: identity.name,
|
||||
classId: identity.classId,
|
||||
secondaryClassId: identity.secondaryClassId,
|
||||
raceId: identity.raceId,
|
||||
gender: "male",
|
||||
specialization: "Primary",
|
||||
role: identity.role,
|
||||
}, 30, 0, `rom-native-legacy-${identity.classId}`);
|
||||
expect(configured.statRuleset).toBe("wow");
|
||||
|
||||
usePartyStore.setState({ members: [configured] });
|
||||
useCombatStore.getState().initializeCharacter({
|
||||
classId: identity.classId,
|
||||
raceId: identity.raceId,
|
||||
gender: "male",
|
||||
role: identity.role,
|
||||
specializationId: configured.specializationId,
|
||||
secondaryClassId: identity.secondaryClassId,
|
||||
level: 30,
|
||||
});
|
||||
useCombatStore.getState().synchronizeActors();
|
||||
|
||||
const state = useCombatStore.getState();
|
||||
expect(state.statRuleset).toBe("wow");
|
||||
expect(state.actors[PLAYER_AURA_ENTITY_ID]?.statRuleset).toBe("wow");
|
||||
expect(state.actors[configured.id]?.statRuleset).toBe("wow");
|
||||
expect(deriveCharacterStats(
|
||||
identity.classId, 30, state.gearStats, identity.raceId, {}, identity.secondaryClassId, "male", "wow",
|
||||
).romNative).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("clamps configured and synchronized actor levels to the shared 1–80 range", () => {
|
||||
const identity = ROM_FIVE_IDENTITIES[0]!;
|
||||
const configured = (level: number) => createConfiguredPartyMember({
|
||||
name: identity.name,
|
||||
classId: identity.classId,
|
||||
secondaryClassId: identity.secondaryClassId,
|
||||
raceId: identity.raceId,
|
||||
gender: "male",
|
||||
specialization: "Primary",
|
||||
role: identity.role,
|
||||
statRuleset: "rom",
|
||||
}, level, 0, `rom-native-level-cap-${String(level)}`);
|
||||
|
||||
expect(configured(81)).toMatchObject({ level: 80, secondaryClassId: identity.secondaryClassId });
|
||||
for (const invalidLevel of [Number.NaN, Number.POSITIVE_INFINITY, 0]) {
|
||||
expect(configured(invalidLevel)).toMatchObject({ level: 1, secondaryClassId: null });
|
||||
}
|
||||
|
||||
usePartyStore.setState({ members: [configured(30)] });
|
||||
for (const level of [81, Number.NaN, Number.POSITIVE_INFINITY, 0]) {
|
||||
usePartyStore.getState().syncLevel(level);
|
||||
const member = usePartyStore.getState().members[0]!;
|
||||
expect(member.level).toBe(level === 81 ? 80 : 1);
|
||||
expect(member.secondaryClassId).toBe(level === 81 ? identity.secondaryClassId : null);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps Training gear capped and owner-kind parity intact at levels 79 and 80", () => {
|
||||
for (const identity of ROM_FIVE_IDENTITIES) {
|
||||
const snapshots = new Map<number, ReturnType<typeof resolvedEquipmentItems>>();
|
||||
for (const level of [79, 80]) {
|
||||
const secondaryClassId = identity.secondaryClassId;
|
||||
const specializationId = createConfiguredPartyMember({
|
||||
name: identity.name,
|
||||
classId: identity.classId,
|
||||
secondaryClassId,
|
||||
raceId: identity.raceId,
|
||||
gender: "male",
|
||||
specialization: "Primary",
|
||||
role: identity.role,
|
||||
statRuleset: "rom",
|
||||
}, level, 0, `rom-native-training-${identity.classId}-${level}`).specializationId;
|
||||
const context = {
|
||||
classId: identity.classId,
|
||||
secondaryClassId,
|
||||
specializationId,
|
||||
role: identity.role,
|
||||
level,
|
||||
} as const;
|
||||
const playerItems = resolvedEquipmentItems({}, [], {
|
||||
...context, ownerId: `training-player-${identity.classId}`, ownerKind: "player",
|
||||
});
|
||||
const partyItems = resolvedEquipmentItems({}, [], {
|
||||
...context, ownerId: `training-party-${identity.classId}`, ownerKind: "party",
|
||||
});
|
||||
expect(playerItems.length).toBeGreaterThan(0);
|
||||
expect(playerItems.every((item) => item.itemLevel === 79)).toBe(true);
|
||||
expect(partyItems.map((item) => item.itemLevel)).toEqual(playerItems.map((item) => item.itemLevel));
|
||||
expect(playerItems.every((item) => item.name.includes("Training")
|
||||
&& item.sourceBossName === "Training Armory" && item.quality === "heirloom")).toBe(true);
|
||||
expect(partyItems.map(({ instanceId: _instanceId, ...item }) => item)).toEqual(
|
||||
playerItems.map(({ instanceId: _instanceId, ...item }) => item),
|
||||
);
|
||||
snapshots.set(level, playerItems);
|
||||
}
|
||||
const at79 = snapshots.get(79)!;
|
||||
const at80 = snapshots.get(80)!;
|
||||
expect(at80.map(({ instanceId: _instanceId, ...item }) => item)).toEqual(
|
||||
at79.map(({ instanceId: _instanceId, ...item }) => item),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user