Files
healer-man/src/app/onlineGroupStore.ts
T
2026-09-20 16:46:17 -04:00

183 lines
5.8 KiB
TypeScript

import { create } from "zustand";
import type { CharacterProfile, PlayerSession } from "./types";
import type { PartyRole } from "../game/partyRoles";
import type { PartySize } from "../game/partyRoles";
import {
clearOnlineGroupActivity,
fetchOnlineGroup,
inviteOnlineGroupPlayer,
leaveOnlineGroup,
removeOnlineGroupPlayer,
respondToOnlineGroupInvite,
startOnlineGroupActivity,
updateOnlineGroupMember,
} from "./onlineAccountClient";
import type { OnlineGroupActivity, OnlineGroupSnapshot, StartOnlineGroupActivityInput } from "./onlineGroupTypes";
const EMPTY_SNAPSHOT: OnlineGroupSnapshot = Object.freeze({ group: null, invitations: Object.freeze([]) });
interface OnlineGroupState {
snapshot: OnlineGroupSnapshot;
loading: boolean;
busy: boolean;
error: string;
fillWithAi: boolean;
activeActivity: OnlineGroupActivity | null;
consumedActivityIds: readonly string[];
refresh: (session: PlayerSession, silent?: boolean) => Promise<void>;
invite: (session: PlayerSession, username: string) => Promise<boolean>;
respond: (session: PlayerSession, inviteId: string, accept: boolean) => Promise<boolean>;
leave: (session: PlayerSession) => Promise<boolean>;
remove: (session: PlayerSession, accountId: string) => Promise<boolean>;
updateMember: (session: PlayerSession, character: CharacterProfile, role: PartyRole) => Promise<boolean>;
startActivity: (session: PlayerSession, input: Omit<StartOnlineGroupActivityInput, "fillWithAi"> & {
readonly fillWithAi?: boolean;
}) => Promise<boolean>;
clearActivity: (session: PlayerSession) => Promise<boolean>;
setFillWithAi: (fillWithAi: boolean) => void;
consumeActivity: (activityId: string) => void;
reset: () => void;
}
function messageFor(error: unknown): string {
return error instanceof Error ? error.message : "The online group could not be updated.";
}
export const useOnlineGroupStore = create<OnlineGroupState>((set, get) => ({
snapshot: EMPTY_SNAPSHOT,
loading: false,
busy: false,
error: "",
fillWithAi: true,
activeActivity: null,
consumedActivityIds: [],
refresh: async (session, silent = false) => {
if (session.kind !== "account" || !session.accessToken) {
set({ snapshot: EMPTY_SNAPSHOT, loading: false, busy: false, error: "" });
return;
}
if (!silent) set({ loading: true });
try {
const snapshot = await fetchOnlineGroup(session);
set({ snapshot, loading: false, error: "" });
} catch (error) {
set({ loading: false, error: messageFor(error) });
}
},
invite: async (session, username) => {
set({ busy: true, error: "" });
try {
const snapshot = await inviteOnlineGroupPlayer(session, username);
set({ snapshot, busy: false, activeActivity: null });
return true;
} catch (error) {
set({ busy: false, error: messageFor(error) });
return false;
}
},
respond: async (session, inviteId, accept) => {
set({ busy: true, error: "" });
try {
const snapshot = await respondToOnlineGroupInvite(session, inviteId, accept);
set({ snapshot, busy: false, activeActivity: null });
return true;
} catch (error) {
set({ busy: false, error: messageFor(error) });
return false;
}
},
leave: async (session) => {
set({ busy: true, error: "" });
try {
const snapshot = await leaveOnlineGroup(session);
set({ snapshot, busy: false, activeActivity: null });
return true;
} catch (error) {
set({ busy: false, error: messageFor(error) });
return false;
}
},
remove: async (session, accountId) => {
set({ busy: true, error: "" });
try {
const snapshot = await removeOnlineGroupPlayer(session, accountId);
set({ snapshot, busy: false, activeActivity: null });
return true;
} catch (error) {
set({ busy: false, error: messageFor(error) });
return false;
}
},
updateMember: async (session, character, role) => {
set({ busy: true, error: "" });
try {
const snapshot = await updateOnlineGroupMember(session, character, role);
set({ snapshot, busy: false, activeActivity: null });
return true;
} catch (error) {
set({ busy: false, error: messageFor(error) });
return false;
}
},
startActivity: async (session, input) => {
set({ busy: true, error: "" });
try {
const snapshot = await startOnlineGroupActivity(session, {
...input,
fillWithAi: input.fillWithAi ?? get().fillWithAi,
});
set({ snapshot, busy: false });
return true;
} catch (error) {
set({ busy: false, error: messageFor(error) });
return false;
}
},
clearActivity: async (session) => {
set({ busy: true, error: "" });
try {
const snapshot = await clearOnlineGroupActivity(session);
set({ snapshot, busy: false, activeActivity: null });
return true;
} catch (error) {
set({ busy: false, error: messageFor(error) });
return false;
}
},
setFillWithAi: (fillWithAi) => set({ fillWithAi }),
consumeActivity: (activityId) => set((state) => {
if (state.consumedActivityIds.includes(activityId)) return state;
return {
activeActivity: state.snapshot.group?.activity?.id === activityId
? state.snapshot.group.activity
: state.activeActivity,
consumedActivityIds: [...state.consumedActivityIds.slice(-7), activityId],
};
}),
reset: () => set({
snapshot: EMPTY_SNAPSHOT,
loading: false,
busy: false,
error: "",
activeActivity: null,
consumedActivityIds: [],
}),
}));
export function onlineGroupHumanCount(): number {
return useOnlineGroupStore.getState().snapshot.group?.members.length ?? 1;
}
export function onlineGroupAiPartySize(activityPartySize: number): PartySize {
return Math.max(1, Math.min(5, activityPartySize - Math.max(0, onlineGroupHumanCount() - 1))) as PartySize;
}