Files
healer-man/src/app/OnlineGroupCoordinator.tsx
T
2026-09-09 11:21:33 -04:00

123 lines
5.4 KiB
TypeScript

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<void>;
readonly enterDungeon: (dungeonId: string, role?: PartyRole) => boolean;
readonly installManastormRuntime: () => Promise<void>;
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<boolean> {
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<string>());
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;
}