154 lines
6.7 KiB
TypeScript
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" }),
|
|
]);
|
|
});
|
|
});
|