Release Healer Man 0.1.37

This commit is contained in:
phenom committed 2026-09-12 21:14:41 -04:00
1 parent 03a4e3f51c
commit 361734bc41
41 files changed
+1714 -110

No files matched your search

+10 -1
View File
@@ -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();
};
}
+48
View File
@@ -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();
});
});
+32 -1
View File
@@ -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;