Files
healer-man/src/game/dpsMeterStore.test.ts
T
2026-09-12 21:14:41 -04:00

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