95 lines
3.6 KiB
TypeScript
95 lines
3.6 KiB
TypeScript
import { beforeEach, describe, expect, it } from "vitest";
|
|
import { PLAYER_AGGRO_ID } from "./aggro";
|
|
import { useCombatStore } from "./combatStore";
|
|
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
|
|
import { usePartyStore } from "./partyStore";
|
|
import { EMPTY_ITEM_STATS } from "./itemStats";
|
|
|
|
const SMASH_ID = "rom-knight-rom-rogue-elite-20";
|
|
|
|
function registerTarget(): void {
|
|
useCombatStore.getState().registerMob("smash-target", {
|
|
name: "Smash target",
|
|
maxHealth: 1_000_000,
|
|
xpReward: 0,
|
|
});
|
|
setMobPosition("smash-target", [2, 0, 0]);
|
|
useCombatStore.getState().selectMob("smash-target");
|
|
}
|
|
|
|
function playerDamageEvent(critical: boolean, id: number, sourceActorId = PLAYER_AGGRO_ID) {
|
|
return {
|
|
id,
|
|
occurredAt: id * 1_000,
|
|
kind: "damage" as const,
|
|
sourceActorId,
|
|
targetActorId: "smash-target",
|
|
abilityId: "rom-knight-primary-1",
|
|
school: "holy" as const,
|
|
rawAmount: 100,
|
|
effectiveAmount: 100,
|
|
overheal: 0,
|
|
absorbed: 0,
|
|
critical,
|
|
outcome: critical ? "crit" : "hit",
|
|
};
|
|
}
|
|
|
|
describe("RuneWaker Smash prerequisite", () => {
|
|
beforeEach(() => {
|
|
clearMobRuntimeRegistry();
|
|
useCombatStore.getState().initializeCharacter({
|
|
classId: "rom-knight",
|
|
secondaryClassId: "rom-rogue",
|
|
raceId: "rom-human",
|
|
level: 80,
|
|
xp: 0,
|
|
});
|
|
useCombatStore.getState().setPlayerPosition([0, 0, 0]);
|
|
usePartyStore.getState().initializeParty("rom-smash-test", 80, useCombatStore.getState().health);
|
|
registerTarget();
|
|
});
|
|
|
|
it("rejects Smash without a preceding player critical damage event without spending health or cooldown", () => {
|
|
const before = useCombatStore.getState();
|
|
const result = before.castAbility(SMASH_ID, undefined, 1_000);
|
|
|
|
expect(result).toMatchObject({ ok: false, reason: "condition-not-met", abilityId: SMASH_ID });
|
|
expect(useCombatStore.getState().health).toBe(before.health);
|
|
expect(useCombatStore.getState().cooldowns[SMASH_ID] ?? 0).toBe(0);
|
|
expect(useCombatStore.getState().mobs["smash-target"]?.health).toBe(1_000_000);
|
|
});
|
|
|
|
it("allows Smash after a real player critical hit and ignores the later cast event", () => {
|
|
useCombatStore.setState({
|
|
gearStats: { ...EMPTY_ITEM_STATS, criticalStrikeRating: 1_000_000, hitRating: 1_000_000 },
|
|
});
|
|
const holySeal = useCombatStore.getState().abilities.find((ability) => ability.id === "rom-knight-primary-1");
|
|
expect(holySeal).toBeDefined();
|
|
expect(useCombatStore.getState().castAbility(holySeal!.id, undefined, 2_000).ok).toBe(true);
|
|
expect(useCombatStore.getState().combatEvents.some((event) => (
|
|
event.kind === "damage"
|
|
&& event.sourceActorId === PLAYER_AGGRO_ID
|
|
&& event.critical
|
|
&& event.effectiveAmount > 0
|
|
))).toBe(true);
|
|
|
|
const result = useCombatStore.getState().castAbility(SMASH_ID, undefined, 4_000);
|
|
expect(result).toMatchObject({ ok: true, abilityId: SMASH_ID });
|
|
expect(useCombatStore.getState().cooldowns[SMASH_ID]).toBe(9_000);
|
|
});
|
|
|
|
it("requires the latest player damage event to be a landed crit", () => {
|
|
useCombatStore.setState({ combatEvents: [
|
|
playerDamageEvent(true, 1),
|
|
playerDamageEvent(false, 2),
|
|
] });
|
|
expect(useCombatStore.getState().castAbility(SMASH_ID, undefined, 3_000))
|
|
.toMatchObject({ ok: false, reason: "condition-not-met" });
|
|
|
|
useCombatStore.setState({ combatEvents: [playerDamageEvent(true, 1, "party-member")] });
|
|
expect(useCombatStore.getState().castAbility(SMASH_ID, undefined, 3_000))
|
|
.toMatchObject({ ok: false, reason: "condition-not-met" });
|
|
});
|
|
});
|