Files
healer-man/src/game/romNativeRecovery.integration.test.ts
T
2026-10-04 13:13:35 -04:00

155 lines
7.2 KiB
TypeScript

import { beforeEach, describe, expect, it } from "vitest";
import { PLAYER_AURA_ENTITY_ID, auraAdjustedCharacterStats, useCombatStore } from "./combatStore";
import { clearMobRuntimeRegistry } from "./mobRuntimeRegistry";
import { clearPartyRuntimeRegistry } from "./partyRuntimeRegistry";
import { advanceDungeonPartyCombat, resetPartyRuntimeClock } from "./partyRuntime";
import { createConfiguredPartyMember, usePartyStore } from "./partyStore";
import { deriveCharacterStats } from "./wotlkStats";
import { romNativePeriodicManaEffectDuring, romNativeRecoveryPulseAmounts } from "./romNativeRecovery";
const ACTIVE_FIVE = ["rom-knight", "rom-priest", "rom-scout", "rom-rogue", "rom-mage"] as const;
describe("native RoM recovery runtime", () => {
beforeEach(() => {
clearMobRuntimeRegistry();
clearPartyRuntimeRegistry();
resetPartyRuntimeClock();
useCombatStore.getState().initializeCharacter({ classId: "priest", level: 30 });
usePartyStore.getState().initializeParty("native-recovery-tests", 30, useCombatStore.getState().health);
});
it("pulses source HP and Mana on native player builds while ignoring the WoW mana-spend lockout", () => {
for (const classId of ACTIVE_FIVE) {
useCombatStore.getState().initializeCharacter({
classId,
raceId: "rom-human",
gender: "male",
level: 30,
statRuleset: "rom",
});
const initialized = useCombatStore.getState();
const nativeBase = deriveCharacterStats(
classId, 30, initialized.gearStats, initialized.raceId, {}, initialized.secondaryClassId,
initialized.gender, "rom",
);
expect(nativeBase.romNative, classId).toBeDefined();
const startAt = Date.now() + 50_000;
useCombatStore.setState((state) => ({
health: Math.max(1, state.maxHealth - 100),
resourcePools: { ...state.resourcePools, ...(nativeBase.romNative!.usesMana ? { mana: 0 } : {}) },
lastResourceSpentAt: { ...state.lastResourceSpentAt, mana: startAt + 1_500 },
}));
const startState = useCombatStore.getState();
const startHealth = startState.health;
const startMana = startState.resourcePools.mana ?? 0;
useCombatStore.getState().tick(1, startAt + 1_000);
const beforePulse = useCombatStore.getState();
const pulseAt = startAt + 2_000;
const livePulseAuras = beforePulse.auras.filter((aura) => (
aura.expiresAt === null || aura.expiresAt > pulseAt
));
const nativeAtPulse = auraAdjustedCharacterStats(
nativeBase,
livePulseAuras,
PLAYER_AURA_ENTITY_ID,
pulseAt,
).romNative!;
const pulse = romNativeRecoveryPulseAmounts(nativeAtPulse, "player", false);
const manaOverTime = romNativePeriodicManaEffectDuring(
beforePulse.auras.filter((aura) => aura.targetId === PLAYER_AURA_ENTITY_ID),
startAt + 1_000,
pulseAt,
beforePulse.maxResourcePools.mana ?? beforePulse.maxResource,
);
useCombatStore.getState().tick(1, pulseAt);
const recovered = useCombatStore.getState();
expect(recovered.health).toBeCloseTo(Math.min(recovered.maxHealth, startHealth + pulse.health), 6);
if (nativeBase.romNative!.usesMana) {
expect(recovered.resourcePools.mana).toBeCloseTo(
Math.min(recovered.maxResourcePools.mana!, startMana + pulse.mana + manaOverTime), 6,
);
} else {
expect(recovered.maxResourcePools.mana ?? 0).toBe(0);
}
expect(recovered.romNativeRecoveryClock?.elapsedMs).toBe(0);
}
});
it("uses the owned-NPC six-second pulse and party-wide combat gate, even while stopped", () => {
const configured = createConfiguredPartyMember({
name: "Native Recovery Priest",
classId: "rom-priest",
raceId: "rom-human",
specialization: "Primary",
role: "healer",
statRuleset: "rom",
}, 30, 0, "native-recovery-party-member");
const member = {
...configured,
health: configured.maxHealth - 100,
resourcePools: { ...configured.resourcePools, mana: 0 },
nextActionAt: Number.MAX_SAFE_INTEGER,
};
usePartyStore.getState().initializeParty("native-recovery-party", 30, useCombatStore.getState().health, "healer", 1, "rom");
usePartyStore.setState({ members: [member], command: "stop", activeMobId: null });
useCombatStore.getState().synchronizeActors();
const baseStats = deriveCharacterStats(
member.classId, member.level, member.gearStats, member.raceId, {}, member.secondaryClassId,
member.gender, "rom",
);
const startAt = Date.now() + 100_000;
advanceDungeonPartyCombat(startAt);
for (let second = 1; second <= 5; second += 1) advanceDungeonPartyCombat(startAt + second * 1_000);
const beforePulse = usePartyStore.getState().members[0]!;
expect(beforePulse.health).toBe(member.health);
expect(beforePulse.resourcePools.mana).toBe(0);
// Stop suppresses companion commands, but an engaged enemy still holds source attack mode.
useCombatStore.getState().registerMob("native-recovery-engaged-mob", {
name: "Recovery combat gate", maxHealth: 5_000, xpReward: 0,
});
useCombatStore.getState().damageMob("native-recovery-engaged-mob", 1, startAt + 5_500);
advanceDungeonPartyCombat(startAt + 6_000);
const inCombat = usePartyStore.getState().members[0]!;
expect(inCombat.health).toBe(member.health);
const expectedMana = romNativeRecoveryPulseAmounts(baseStats.romNative!, "party-npc", true).mana;
expect(inCombat.resourcePools.mana).toBeCloseTo(expectedMana, 6);
});
it("clears clock state on player and companion death, immediate revive, and party reset", () => {
useCombatStore.getState().initializeCharacter({
classId: "rom-priest", raceId: "rom-human", level: 30, statRuleset: "rom",
});
const startAt = Date.now() + 200_000;
useCombatStore.getState().tick(1, startAt);
expect(useCombatStore.getState().romNativeRecoveryClock?.elapsedMs).toBe(1_000);
useCombatStore.getState().damagePlayer(useCombatStore.getState().maxHealth * 10, "physical", 30, startAt + 100);
expect(useCombatStore.getState().romNativeRecoveryClock).toBeUndefined();
useCombatStore.getState().revivePlayer();
expect(useCombatStore.getState().romNativeRecoveryClock).toBeUndefined();
const configured = createConfiguredPartyMember({
name: "Clock reset companion",
classId: "rom-priest",
raceId: "rom-human",
specialization: "Primary",
role: "healer",
statRuleset: "rom",
}, 30, 0, "native-recovery-clock-reset");
const elapsed = {
ruleset: "rom" as const,
actorKind: "party-npc" as const,
elapsedMs: 5_000,
};
usePartyStore.setState({ members: [{ ...configured, romNativeRecoveryClock: elapsed }] });
usePartyStore.getState().damageMember(configured.id, configured.maxHealth * 10);
expect(usePartyStore.getState().members[0]?.romNativeRecoveryClock).toBeUndefined();
usePartyStore.getState().reviveMember(configured.id);
expect(usePartyStore.getState().members[0]?.romNativeRecoveryClock).toBeUndefined();
usePartyStore.getState().resetCombat(30, useCombatStore.getState().health);
expect(usePartyStore.getState().members.every((candidate) => !candidate.romNativeRecoveryClock)).toBe(true);
});
});