Update Healer Man server
This commit is contained in:
1 parent
4c768fbec6
commit
b16821e92e
330 files changed
+35406
-1760
No files matched your search
@@ -0,0 +1,153 @@
|
||||
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" }),
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user