Files
healer-man/src/game/dpsMeterStore.ts
T
2026-09-12 21:14:41 -04:00

85 lines
4.1 KiB
TypeScript

import { create } from "zustand";
import { classById, CLASSES } from "../app/characterCatalog";
import { useShellStore } from "../app/shellStore";
import { useOnlineSessionStore } from "../app/onlineSessionStore";
import { PLAYER_AGGRO_ID } from "./aggro";
import { useCombatStore } from "./combatStore";
import { subscribeCombatEvents } from "./combatEvents";
import { advanceDpsMeter, dpsMeterSnapshot, emptyDpsMeter, recordDpsDamage, type DpsMeterActor } from "./dpsMeter";
import { onlinePlayerActorId } from "./onlineSessionRuntime";
import { usePartyStore } from "./partyStore";
import { useGameStore } from "./store";
export const useDpsMeterStore = create(() => emptyDpsMeter());
function meterActors(): DpsMeterActor[] {
const combat = useCombatStore.getState();
const online = useOnlineSessionStore.getState().snapshot;
return [
{ id: PLAYER_AGGRO_ID, name: useShellStore.getState().activeCharacter?.name ?? "You", color: classById(combat.classId).color },
...usePartyStore.getState().members.map((member) => ({ id: member.id, name: member.name, color: classById(member.classId).color })),
...(online?.players ?? []).filter((player) => player.accountId !== online?.localAccountId).map((player) => ({
id: onlinePlayerActorId(player.accountId), name: player.character?.name ?? player.username,
color: CLASSES.find((entry) => entry.id === player.character?.classId)?.color ?? "#d5c28b",
})),
];
}
export function dpsSourceActor(sourceId: string): DpsMeterActor | null {
const combat = useCombatStore.getState();
const localAccount = useOnlineSessionStore.getState().snapshot?.localAccountId;
const visited = new Set<string>();
let id = sourceId;
while (!visited.has(id)) {
visited.add(id);
if (localAccount && id === onlinePlayerActorId(localAccount)) id = PLAYER_AGGRO_ID;
const known = meterActors().find((actor) => actor.id === id);
if (known) return known;
const ownerId = combat.actors[id]?.ownerId ?? combat.summons.find((summon) => summon.id === id)?.ownerActorId;
if (!ownerId) return null;
id = ownerId;
}
return null;
}
export function currentDpsMeterSnapshot() {
return dpsMeterSnapshot(useDpsMeterStore.getState(), meterActors());
}
/** Owned by the gameplay runtime so hiding or moving the meter never loses damage. */
export function connectDpsMeter(): () => void {
useDpsMeterStore.setState(emptyDpsMeter(), true);
let suspendedPreviously = false;
const sample = () => {
const combat = useCombatStore.getState();
const game = useGameStore.getState();
const inCombat = Object.values(combat.mobs).some((mob) => mob.engaged && !mob.dead && !mob.despawned);
const sharedOnline = Boolean(useOnlineSessionStore.getState().snapshot);
const suspended = !sharedOnline && (game.paused || game.companionOpen || (typeof document !== "undefined" && document.hidden));
const state = useDpsMeterStore.getState();
const next = advanceDpsMeter(state, Date.now(), inCombat, suspendedPreviously);
suspendedPreviously = suspended;
if (next !== state) useDpsMeterStore.setState(next, true);
};
const unsubscribeEvents = subscribeCombatEvents((event) => {
if (event.kind !== "damage" || !useCombatStore.getState().mobs[event.targetActorId]) return;
const actor = dpsSourceActor(event.sourceActorId);
if (actor) useDpsMeterStore.setState(recordDpsDamage(useDpsMeterStore.getState(), event, actor), true);
});
const unsubscribeCombat = useCombatStore.subscribe((state, previous) => {
if (state.combatEvents !== previous.combatEvents && state.combatEvents.length === 0) {
useDpsMeterStore.setState(emptyDpsMeter(), true);
} else sample();
});
const unsubscribeGame = useGameStore.subscribe(sample);
if (typeof document !== "undefined") document.addEventListener("visibilitychange", sample);
const timer = setInterval(sample, 250);
sample();
return () => {
clearInterval(timer);
unsubscribeEvents(); unsubscribeCombat(); unsubscribeGame();
if (typeof document !== "undefined") document.removeEventListener("visibilitychange", sample);
useDpsMeterStore.setState(emptyDpsMeter(), true);
};
}