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); }); });