forked from phenom/healer-man
85 lines
4.1 KiB
TypeScript
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);
|
|
};
|
|
}
|