forked from phenom/healer-man
Release Healer Man 0.1.37
This commit is contained in:
1 parent
03a4e3f51c
commit
361734bc41
41 files changed
+1714
-110
No files matched your search
@@ -5,6 +5,8 @@ import { useShellStore } from "../app/shellStore";
|
||||
import { emitControllerToken, setExternalControllerMovement } from "../input/controller";
|
||||
import { currentCompanionSnapshot, executeCompanionCommand, type DualScreenMessage } from "./dualScreenSync";
|
||||
import { createRateLimitedPublisher } from "./rateLimitedPublisher";
|
||||
import { useUiLayoutStore } from "../game/uiLayoutStore";
|
||||
import { useDpsMeterStore } from "../game/dpsMeterStore";
|
||||
|
||||
export function connectAuthoritativeDualScreen(
|
||||
channel: BroadcastChannel,
|
||||
@@ -30,6 +32,12 @@ export function connectAuthoritativeDualScreen(
|
||||
const unsubscribeParty = usePartyStore.subscribe(() => {
|
||||
if (companionReady) gamePublisher.request();
|
||||
});
|
||||
const unsubscribeUi = useUiLayoutStore.subscribe(() => {
|
||||
if (companionReady) gamePublisher.request();
|
||||
});
|
||||
const unsubscribeDps = useDpsMeterStore.subscribe(() => {
|
||||
if (companionReady) gamePublisher.request();
|
||||
});
|
||||
channel.onmessage = (event: MessageEvent<DualScreenMessage>) => {
|
||||
if (event.data.type === "companion-ready") {
|
||||
companionReady = true;
|
||||
@@ -52,7 +60,8 @@ export function connectAuthoritativeDualScreen(
|
||||
unsubscribeGame();
|
||||
unsubscribeCombat();
|
||||
unsubscribeParty();
|
||||
unsubscribeUi();
|
||||
unsubscribeDps();
|
||||
gamePublisher.dispose();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { connectAuthoritativeDualScreen } from "./authoritativeDualScreenRuntime";
|
||||
import { useUiLayoutStore } from "../game/uiLayoutStore";
|
||||
import { useDpsMeterStore } from "../game/dpsMeterStore";
|
||||
import { emptyDpsMeter } from "../game/dpsMeter";
|
||||
import type { DualScreenMessage } from "./dualScreenSync";
|
||||
|
||||
describe("dedicated companion DPS synchronization", () => {
|
||||
afterEach(() => {
|
||||
useUiLayoutStore.getState().finish(false);
|
||||
useDpsMeterStore.setState(emptyDpsMeter(), true);
|
||||
vi.unstubAllGlobals();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("publishes meter ticks and layout drafts, then unsubscribes on disconnect", () => {
|
||||
vi.useFakeTimers(); vi.setSystemTime(1_000);
|
||||
vi.stubGlobal("window", { setTimeout, clearTimeout, location: { search: "?display=top" } });
|
||||
const postMessage = vi.fn<(message: DualScreenMessage) => void>();
|
||||
const channel = { postMessage, onmessage: null } as unknown as BroadcastChannel;
|
||||
const disconnect = connectAuthoritativeDualScreen(channel, { data: { type: "companion-ready" } } as MessageEvent<DualScreenMessage>);
|
||||
try {
|
||||
useUiLayoutStore.getState().begin();
|
||||
useUiLayoutStore.getState().updateDraft((layout) => ({ ...layout, dpsMeterDisplay: "companion", elements: {
|
||||
...layout.elements, dps: { visible: false, position: { x: .4, y: .6 }, width: 300 },
|
||||
} }));
|
||||
vi.advanceTimersByTime(100);
|
||||
let latest = postMessage.mock.lastCall?.[0];
|
||||
expect(latest?.type).toBe("app-state");
|
||||
if (latest?.type !== "app-state") throw new Error("Missing companion snapshot");
|
||||
expect(latest.snapshot.game.dpsMeter).toMatchObject({ display: "companion", layout: {
|
||||
visible: false, position: { x: .4, y: .6 }, width: 300,
|
||||
} });
|
||||
|
||||
useDpsMeterStore.setState({ startedAt: 1_000, endedAt: null, updatedAt: 3_000, elapsedMs: 2_000,
|
||||
damage: { test: { id: "test", name: "Test", color: "#fff", total: 200 } } }, true);
|
||||
vi.advanceTimersByTime(100);
|
||||
latest = postMessage.mock.lastCall?.[0];
|
||||
if (latest?.type !== "app-state") throw new Error("Missing DPS snapshot");
|
||||
expect(latest.snapshot.game.dpsMeter.values).toMatchObject({ totalDamage: 200, totalDps: 100, durationSeconds: 2 });
|
||||
} finally { disconnect(); }
|
||||
postMessage.mockClear();
|
||||
useUiLayoutStore.getState().finish(false);
|
||||
useDpsMeterStore.setState(emptyDpsMeter(), true);
|
||||
vi.advanceTimersByTime(100);
|
||||
expect(postMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -26,6 +26,12 @@ import { abilityAtLevel, resourceProfileForClass, type AbilityTarget } from "../
|
||||
import type { DungeonId } from "../game/dungeonCatalog";
|
||||
import { useManastormStore } from "../game/manastormStore";
|
||||
import { resolveStagePresentation } from "../game/manastormStagePresentation";
|
||||
import { currentDpsMeterSnapshot } from "../game/dpsMeterStore";
|
||||
import type { DpsMeterSnapshot } from "../game/dpsMeter";
|
||||
import { activeUiLayout, useUiLayoutStore } from "../game/uiLayoutStore";
|
||||
import { normalizeHudPosition } from "../game/hudLayout";
|
||||
import { resolveDpsMeterDisplay, type HudElementLayout } from "../game/uiLayout";
|
||||
import { currentDisplayLayout } from "./displayLayout";
|
||||
|
||||
export { createDualScreenChannel } from "./dualScreenChannel";
|
||||
|
||||
@@ -89,6 +95,12 @@ export interface CompanionPartyObjectiveSnapshot {
|
||||
}
|
||||
|
||||
export interface CompanionGameSnapshot {
|
||||
dpsMeter: {
|
||||
values: DpsMeterSnapshot;
|
||||
layout: HudElementLayout;
|
||||
display: "main" | "companion";
|
||||
editing: boolean;
|
||||
};
|
||||
gameMode: GameMode;
|
||||
dungeonId: string;
|
||||
dungeonTitle: string;
|
||||
@@ -159,7 +171,8 @@ export type CompanionCommand =
|
||||
| { name: "cyclePartyMember"; direction: 1 | -1 }
|
||||
| { name: "setPartyCommand"; command: PartyCommand }
|
||||
| { name: "recallParty" }
|
||||
| { name: "resetAtEntrance" };
|
||||
| { name: "resetAtEntrance" }
|
||||
| { name: "editDpsMeter"; update: Partial<HudElementLayout> };
|
||||
|
||||
export type DualScreenMessage =
|
||||
| { type: "companion-ready" }
|
||||
@@ -180,6 +193,7 @@ export function currentCompanionSnapshot(): CompanionSnapshot {
|
||||
const combat = useCombatStore.getState();
|
||||
const party = usePartyStore.getState();
|
||||
const manastorm = useManastormStore.getState();
|
||||
const uiLayout = activeUiLayout();
|
||||
const dungeon = requireDungeonDefinition(game.activeDungeonId);
|
||||
const stagePresentation = resolveStagePresentation(game.gameMode, manastorm.currentEncounter, {
|
||||
title: dungeon.title,
|
||||
@@ -251,6 +265,12 @@ export function currentCompanionSnapshot(): CompanionSnapshot {
|
||||
draft: shell.draft,
|
||||
notice: shell.notice,
|
||||
game: {
|
||||
dpsMeter: {
|
||||
values: currentDpsMeterSnapshot(),
|
||||
layout: uiLayout.elements.dps,
|
||||
display: resolveDpsMeterDisplay(uiLayout, typeof window !== "undefined" && currentDisplayLayout() !== "single"),
|
||||
editing: game.overlay === "ui-layout",
|
||||
},
|
||||
gameMode: game.gameMode,
|
||||
dungeonId: dungeon.id,
|
||||
dungeonTitle: stagePresentation.title,
|
||||
@@ -322,6 +342,17 @@ export function executeCompanionCommand(command: CompanionCommand): void {
|
||||
const game = useGameStore.getState();
|
||||
const combat = useCombatStore.getState();
|
||||
switch (command.name) {
|
||||
case "editDpsMeter": {
|
||||
if (game.overlay !== "ui-layout" || !useUiLayoutStore.getState().draft) break;
|
||||
const update: Partial<HudElementLayout> = {};
|
||||
if (command.update.position) update.position = normalizeHudPosition(command.update.position, { x: .5, y: .5 });
|
||||
if (typeof command.update.width === "number" && Number.isFinite(command.update.width)) update.width = Math.round(Math.max(160, Math.min(600, command.update.width)));
|
||||
useUiLayoutStore.getState().updateDraft((layout) => ({ ...layout, elements: {
|
||||
...layout.elements, dps: { ...layout.elements.dps, ...update },
|
||||
} }));
|
||||
useUiLayoutStore.setState({ selectedElement: "dps" });
|
||||
break;
|
||||
}
|
||||
case "selectCharacter": shell.selectCharacter(command.characterId); break;
|
||||
case "openCreator": shell.openCreator(); break;
|
||||
case "cancelCreator": shell.cancelCreator(); break;
|
||||
|
||||
Reference in new issue
Block a user