forked from phenom/healer-man
65 lines
4.0 KiB
TypeScript
65 lines
4.0 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { PLAYER_AGGRO_ID } from "./aggro";
|
|
import { useCombatStore } from "./combatStore";
|
|
import { appendCombatEvent } from "./combatEvents";
|
|
import { connectDpsMeter, currentDpsMeterSnapshot, useDpsMeterStore } from "./dpsMeterStore";
|
|
import { useGameStore } from "./store";
|
|
import { useUiLayoutStore } from "./uiLayoutStore";
|
|
import { currentCompanionSnapshot, executeCompanionCommand } from "../platform/dualScreenSync";
|
|
|
|
describe("live DPS tracking and companion editor", () => {
|
|
let disconnect: () => void;
|
|
beforeEach(() => {
|
|
vi.useFakeTimers(); vi.setSystemTime(1_000);
|
|
useGameStore.setState({ overlay: null, paused: false, companionOpen: false });
|
|
useCombatStore.getState().initializeCharacter({ classId: "mage", level: 5 });
|
|
useCombatStore.getState().registerMob("dps-target", { name: "Target", maxHealth: 1_000_000 });
|
|
disconnect = connectDpsMeter();
|
|
});
|
|
afterEach(() => { disconnect(); useUiLayoutStore.getState().finish(false); vi.useRealTimers(); });
|
|
|
|
it("tracks real damage while hidden, caps overkill, freezes on defeat, and clears on reset", () => {
|
|
useUiLayoutStore.getState().begin();
|
|
useUiLayoutStore.getState().updateDraft((layout) => ({ ...layout, elements: { ...layout.elements, dps: { ...layout.elements.dps, visible: false } } }));
|
|
useCombatStore.getState().damageMob("dps-target", 100, Date.now());
|
|
vi.advanceTimersByTime(2_000);
|
|
expect(currentDpsMeterSnapshot()).toMatchObject({ totalDamage: 100, totalDps: 50 });
|
|
const remaining = useCombatStore.getState().mobs["dps-target"].health;
|
|
useCombatStore.getState().damageMob("dps-target", 2_000_000, Date.now());
|
|
expect(currentDpsMeterSnapshot()).toMatchObject({ active: false, totalDamage: 100 + remaining });
|
|
vi.advanceTimersByTime(10_000);
|
|
expect(currentDpsMeterSnapshot().durationSeconds).toBe(2);
|
|
useCombatStore.getState().resetEncounter();
|
|
expect(useDpsMeterStore.getState().startedAt).toBeNull();
|
|
});
|
|
it("merges summon damage into its owner and excludes hostile and friendly-target damage", () => {
|
|
const actors = useCombatStore.getState().actors;
|
|
useCombatStore.setState({ actors: { ...actors, "meter-pet": { ...actors[PLAYER_AGGRO_ID], id: "meter-pet", kind: "pet", ownerId: PLAYER_AGGRO_ID } } });
|
|
for (const [id, sourceActorId, targetActorId] of [[1, "meter-pet", "dps-target"], [2, "dps-target", PLAYER_AGGRO_ID], [3, PLAYER_AGGRO_ID, PLAYER_AGGRO_ID]] as const) {
|
|
appendCombatEvent([], { occurredAt: Date.now(), kind: "damage", sourceActorId, targetActorId,
|
|
rawAmount: 100, effectiveAmount: 80, overheal: 0, absorbed: 20, critical: false }, id);
|
|
}
|
|
expect(currentDpsMeterSnapshot().totalDamage).toBe(80);
|
|
expect(currentDpsMeterSnapshot().rows.find((row) => row.total > 0)?.id).toBe(PLAYER_AGGRO_ID);
|
|
});
|
|
it("does not dilute DPS while a local game menu pauses combat", () => {
|
|
useCombatStore.getState().damageMob("dps-target", 100, Date.now());
|
|
vi.advanceTimersByTime(1_000);
|
|
useGameStore.setState({ paused: true, overlay: "ui-layout" });
|
|
vi.advanceTimersByTime(10_000);
|
|
expect(currentDpsMeterSnapshot()).toMatchObject({ durationSeconds: 1, totalDps: 100 });
|
|
useGameStore.setState({ paused: false, overlay: null });
|
|
vi.advanceTimersByTime(1_000);
|
|
expect(currentDpsMeterSnapshot()).toMatchObject({ durationSeconds: 2, totalDps: 50 });
|
|
});
|
|
it("publishes draft visibility and accepts lower-screen movement only while editing", () => {
|
|
useUiLayoutStore.getState().begin();
|
|
const command = { name: "editDpsMeter", update: { position: { x: .7, y: .8 }, width: 300 } } as const;
|
|
executeCompanionCommand(command);
|
|
expect(useUiLayoutStore.getState().draft?.elements.dps.position).toBeNull();
|
|
useGameStore.setState({ overlay: "ui-layout" });
|
|
executeCompanionCommand(command);
|
|
expect(currentCompanionSnapshot().game.dpsMeter).toMatchObject({ editing: true, layout: { position: { x: .7, y: .8 }, width: 300 } });
|
|
});
|
|
});
|