import { useEffect, useRef } from "react"; import { selectedCharacter, useShellStore } from "./shellStore"; import { useOnlineGroupStore } from "./onlineGroupStore"; import { classCanFillPartyRole, defaultPartyRoleForClass, type PartyRole, } from "../game/partyRoles"; import { isManastormPartySize, type ManastormPartySize } from "../game/manastormProgress"; import type { OnlineGroupActivity } from "./onlineGroupTypes"; import { useContentManagerSnapshot } from "../content/useContentManager"; const GROUP_POLL_MS = 2_500; const ACTIVITY_JOIN_WINDOW_MS = 10 * 60 * 1000; interface OnlineGroupActivityEntryDependencies { readonly installDungeonRuntime: () => Promise; readonly enterDungeon: (dungeonId: string, role?: PartyRole) => boolean; readonly installManastormRuntime: () => Promise; readonly enterManastorm: (role?: PartyRole, partySize?: ManastormPartySize, startingLevel?: number) => boolean; } const DEFAULT_ACTIVITY_ENTRY_DEPENDENCIES: OnlineGroupActivityEntryDependencies = { installDungeonRuntime: async () => { const { installDungeonShellRuntime } = await import("./dungeonShellRuntime"); installDungeonShellRuntime(); }, enterDungeon: (dungeonId, role) => useShellStore.getState().enterDungeon(dungeonId, role), installManastormRuntime: async () => { const { installManastormShellRuntime } = await import("./manastormShellRuntime"); installManastormShellRuntime(); }, enterManastorm: (role, partySize, startingLevel) => useShellStore.getState().enterManastorm( role, partySize, startingLevel, ), }; export async function enterOnlineGroupActivity( activity: OnlineGroupActivity, role: PartyRole, consumeActivity: (activityId: string) => void, dependencies: OnlineGroupActivityEntryDependencies = DEFAULT_ACTIVITY_ENTRY_DEPENDENCIES, ): Promise { let entered: boolean; if (activity.type === "dungeon") { await dependencies.installDungeonRuntime(); entered = dependencies.enterDungeon(activity.contentId, role); } else { await dependencies.installManastormRuntime(); const partySize = isManastormPartySize(activity.partySize) ? activity.partySize : 5; entered = dependencies.enterManastorm(role, partySize, activity.startingLevel ?? undefined); } if (entered) consumeActivity(activity.id); return entered; } export function OnlineGroupCoordinator() { const session = useShellStore((state) => state.session); const character = useShellStore(selectedCharacter); const phase = useShellStore((state) => state.phase); const snapshot = useOnlineGroupStore((state) => state.snapshot); const consumedActivityIds = useOnlineGroupStore((state) => state.consumedActivityIds); const refresh = useOnlineGroupStore((state) => state.refresh); const updateMember = useOnlineGroupStore((state) => state.updateMember); const consumeActivity = useOnlineGroupStore((state) => state.consumeActivity); const reset = useOnlineGroupStore((state) => state.reset); const { installedRevision } = useContentManagerSnapshot(); const pendingActivityIds = useRef(new Set()); useEffect(() => { if (!session || session.kind !== "account" || !session.accessToken) { reset(); return undefined; } void refresh(session); const timer = window.setInterval(() => void refresh(session, true), GROUP_POLL_MS); const onVisibility = () => { if (document.visibilityState === "visible") void refresh(session, true); }; document.addEventListener("visibilitychange", onVisibility); return () => { window.clearInterval(timer); document.removeEventListener("visibilitychange", onVisibility); }; }, [refresh, reset, session?.accessToken, session?.ownerId]); useEffect(() => { const group = snapshot.group; if (!session || !character || !group || group.activity) return; const member = group.members.find((candidate) => candidate.accountId === session.ownerId); if (!member) return; const role: PartyRole = member.role && classCanFillPartyRole(character.classId, member.role) ? member.role : defaultPartyRoleForClass(character.classId); const characterChanged = member.character?.id !== character.id || member.character?.level !== character.level || member.character?.classId !== character.classId; if (!characterChanged && member.role === role) return; void updateMember(session, character, role); }, [character?.classId, character?.id, character?.level, session?.ownerId, snapshot.group?.id, snapshot.group?.updatedAt, updateMember]); useEffect(() => { const activity = snapshot.group?.activity; if (!session || !activity || phase === "game" || consumedActivityIds.includes(activity.id)) return; if (Date.now() - activity.startedAt > ACTIVITY_JOIN_WINDOW_MS) return; const member = snapshot.group?.members.find((candidate) => candidate.accountId === session.ownerId); if (!member?.character || !member.role) return; if (pendingActivityIds.current.has(activity.id)) return; pendingActivityIds.current.add(activity.id); void enterOnlineGroupActivity(activity, member.role, consumeActivity) .catch((error) => { useShellStore.getState().setNotice(error instanceof Error ? error.message : "The group activity could not be opened."); }) .finally(() => { pendingActivityIds.current.delete(activity.id); }); }, [consumeActivity, consumedActivityIds, installedRevision, phase, session?.ownerId, snapshot.group?.activity?.id]); return null; }