155 lines
7.2 KiB
TypeScript
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);
|
|
});
|
|
});
|