Files
healer-man/src/game/floatingCombatText.test.ts
T
2026-10-02 10:38:12 -04:00

154 lines
6.7 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { OnlineRelayEvent, OnlineSessionSnapshot } from "../app/onlineSessionTypes";
import { damageTextClassName } from "../ui/FloatingDamageText";
import { PLAYER_AGGRO_ID } from "./aggro";
import { useCombatStore } from "./combatStore";
import { appendCombatEvent, type CombatEventInput } from "./combatEvents";
import {
DAMAGE_TEXT_LIFETIME_MS,
MAX_DAMAGE_TEXT_PER_TARGET,
MAX_DAMAGE_TEXT_TOTAL,
clearDamageText,
connectFloatingCombatText,
damageTextCategory,
formatDamageText,
ingestRelayDamageText,
useDamageTextStore,
withSuppressedReplayDamageText,
} from "./floatingCombatText";
let disconnect: (() => void) | null = null;
function damage(id: number, overrides: Partial<CombatEventInput> = {}): void {
appendCombatEvent([], {
occurredAt: id * 100,
kind: "damage",
sourceActorId: PLAYER_AGGRO_ID,
targetActorId: "target",
abilityId: "priest-smite",
rawAmount: 500,
effectiveAmount: 18,
overheal: 0,
absorbed: 0,
critical: false,
...overrides,
}, id);
}
beforeEach(() => {
vi.useFakeTimers();
useCombatStore.getState().initializeCharacter({ classId: "priest", level: 20 });
useCombatStore.getState().registerMob("target", { name: "Target", maxHealth: 1_000 });
disconnect = connectFloatingCombatText();
});
afterEach(() => {
disconnect?.();
disconnect = null;
clearDamageText();
vi.useRealTimers();
});
describe("floating damage text", () => {
it("classifies own, ally, and incoming hits independently", () => {
const mobs = new Set(["target"]);
const party = new Set(["companion"]);
const online = new Set(["online-player:local", "online-player:friend"]);
expect(damageTextCategory(PLAYER_AGGRO_ID, "target", mobs, party, online, "online-player:local")).toBe("own");
expect(damageTextCategory("companion", "target", mobs, party, online, "online-player:local")).toBe("ally");
expect(damageTextCategory("online-player:friend", "target", mobs, party, online, "online-player:local")).toBe("ally");
expect(damageTextCategory("target", PLAYER_AGGRO_ID, mobs, party, online, "online-player:local")).toBe("incoming");
expect(damageTextCategory("target", "companion", mobs, party, online, "online-player:local")).toBe("incoming");
});
it("shows effective Smite damage, preserves crit, and expires after 1.5 seconds", () => {
damage(1, { effectiveAmount: 17.6, critical: true });
expect(useDamageTextStore.getState().byTarget.target).toEqual([
expect.objectContaining({ amount: 18, critical: true, category: "own" }),
]);
expect(formatDamageText(1_234.4)).toBe("1,234");
expect(damageTextClassName(useDamageTextStore.getState().byTarget.target[0]!)).toContain("is-critical");
vi.advanceTimersByTime(DAMAGE_TEXT_LIFETIME_MS);
expect(useDamageTextStore.getState().byTarget.target).toBeUndefined();
});
it("honors each switch and removes a category immediately when disabled", () => {
damage(1);
expect(useDamageTextStore.getState().byTarget.target).toHaveLength(1);
useCombatStore.getState().updateSettings({ showOwnDamageText: false, showIncomingDamageText: true });
expect(useDamageTextStore.getState().byTarget.target).toBeUndefined();
damage(2);
expect(useDamageTextStore.getState().byTarget.target).toBeUndefined();
damage(3, { sourceActorId: "target", targetActorId: PLAYER_AGGRO_ID });
expect(useDamageTextStore.getState().byTarget[PLAYER_AGGRO_ID]).toEqual([
expect.objectContaining({ category: "incoming", amount: 18 }),
]);
damage(4, { sourceActorId: "target", targetActorId: PLAYER_AGGRO_ID, effectiveAmount: 0 });
expect(useDamageTextStore.getState().byTarget[PLAYER_AGGRO_ID]).toHaveLength(1);
});
it("bounds bursts per target and across the scene", () => {
for (let id = 1; id <= 7; id += 1) damage(id);
expect(useDamageTextStore.getState().byTarget.target).toHaveLength(MAX_DAMAGE_TEXT_PER_TARGET);
const target = useCombatStore.getState().mobs.target;
useCombatStore.setState({ mobs: Object.fromEntries(Array.from({ length: 90 }, (_, index) => [`mob-${index}`, { ...target, id: `mob-${index}` }])) });
for (let id = 0; id < 90; id += 1) damage(100 + id, { targetActorId: `mob-${id}` });
expect(Object.values(useDamageTextStore.getState().byTarget).flat()).toHaveLength(MAX_DAMAGE_TEXT_TOTAL);
});
it("shows periodic and area hits on their own targets, including a killing hit", () => {
const first = useCombatStore.getState().mobs.target;
useCombatStore.setState({ mobs: {
target: { ...first, dead: true, health: 0 },
second: { ...first, id: "second", health: 50 },
} });
damage(1, { targetActorId: "target", abilityId: "priest-shadow-word-pain", effectiveAmount: 9 });
damage(2, { targetActorId: "second", abilityId: "priest-holy-nova", effectiveAmount: 14 });
expect(useDamageTextStore.getState().byTarget.target?.[0]?.amount).toBe(9);
expect(useDamageTextStore.getState().byTarget.second?.[0]?.amount).toBe(14);
});
it("uses one relayed online crit and suppresses damage generated during replay", () => {
useCombatStore.getState().updateSettings({ showAllyDamageText: true });
const snapshot = {
localAccountId: "local",
players: [{ accountId: "local" }, { accountId: "friend" }],
} as unknown as OnlineSessionSnapshot;
const relay = {
id: 40,
kind: "damage",
sourceAccountId: "friend",
sourceActorId: "online-player:friend",
targetActorId: "target",
effectiveAmount: 61,
critical: true,
} as OnlineRelayEvent;
withSuppressedReplayDamageText(() => damage(40, { sourceActorId: "online-player:friend", effectiveAmount: 61 }));
ingestRelayDamageText(relay, snapshot);
ingestRelayDamageText(relay, snapshot);
expect(useDamageTextStore.getState().byTarget.target).toEqual([
expect.objectContaining({ amount: 61, critical: true, category: "ally", key: "relay:40" }),
]);
});
it("maps relayed damage to the local player's party frame with its original crit", () => {
useCombatStore.getState().updateSettings({ showIncomingDamageText: true });
const snapshot = {
localAccountId: "local",
players: [{ accountId: "local" }, { accountId: "friend" }],
} as unknown as OnlineSessionSnapshot;
ingestRelayDamageText({
id: 41,
kind: "damage",
sourceAccountId: "friend",
sourceActorId: "target",
targetActorId: "online-player:local",
effectiveAmount: 36,
critical: true,
} as OnlineRelayEvent, snapshot);
expect(useDamageTextStore.getState().byTarget[PLAYER_AGGRO_ID]).toEqual([
expect.objectContaining({ amount: 36, critical: true, category: "incoming" }),
]);
});
});