158 lines
7.3 KiB
TypeScript
158 lines
7.3 KiB
TypeScript
import { useEffect, useState, type FormEvent } from "react";
|
|
import { classById } from "../app/characterCatalog";
|
|
import { selectedCharacter, useShellStore } from "../app/shellStore";
|
|
import { useOnlineGroupStore } from "../app/onlineGroupStore";
|
|
import {
|
|
classCanFillPartyRole,
|
|
partyRolesForClass,
|
|
PARTY_ROLE_LABELS,
|
|
type PartyRole,
|
|
} from "../game/partyRoles";
|
|
|
|
export function OnlineGroupPanel() {
|
|
const [username, setUsername] = useState("");
|
|
const [panelOpen, setPanelOpen] = useState(false);
|
|
const session = useShellStore((state) => state.session);
|
|
const character = useShellStore(selectedCharacter);
|
|
const snapshot = useOnlineGroupStore((state) => state.snapshot);
|
|
const busy = useOnlineGroupStore((state) => state.busy);
|
|
const error = useOnlineGroupStore((state) => state.error);
|
|
const fillWithAi = useOnlineGroupStore((state) => state.fillWithAi);
|
|
const invite = useOnlineGroupStore((state) => state.invite);
|
|
const respond = useOnlineGroupStore((state) => state.respond);
|
|
const leave = useOnlineGroupStore((state) => state.leave);
|
|
const remove = useOnlineGroupStore((state) => state.remove);
|
|
const updateMember = useOnlineGroupStore((state) => state.updateMember);
|
|
const clearActivity = useOnlineGroupStore((state) => state.clearActivity);
|
|
const setFillWithAi = useOnlineGroupStore((state) => state.setFillWithAi);
|
|
|
|
useEffect(() => {
|
|
if (snapshot.invitations.length > 0) setPanelOpen(true);
|
|
}, [snapshot.invitations.length]);
|
|
|
|
if (!session || session.kind !== "account" || !session.accessToken) return null;
|
|
const group = snapshot.group;
|
|
const currentMember = group?.members.find((member) => member.accountId === session.ownerId) ?? null;
|
|
const leader = group?.leaderAccountId === session.ownerId;
|
|
const roles = character ? partyRolesForClass(character.classId) : [];
|
|
const readyCount = group?.members.filter((member) => member.character && member.role).length ?? 0;
|
|
|
|
const submitInvite = async (event: FormEvent) => {
|
|
event.preventDefault();
|
|
if (!username.trim()) return;
|
|
if (await invite(session, username.trim())) setUsername("");
|
|
};
|
|
const chooseRole = (role: PartyRole) => {
|
|
if (character && classCanFillPartyRole(character.classId, role)) void updateMember(session, character, role);
|
|
};
|
|
|
|
return (
|
|
<details className="online-group-panel" open={panelOpen} onToggle={(event) => setPanelOpen(event.currentTarget.open)}>
|
|
<summary>
|
|
<span aria-hidden="true">◎</span>
|
|
<strong>Online group</strong>
|
|
<small>{group ? `${group.members.length} / 5 players · ${readyCount} ready` : "Optional co-op"}</small>
|
|
</summary>
|
|
<div className="online-group-panel__body">
|
|
{snapshot.invitations.length > 0 && (
|
|
<section className="online-group-invites" aria-label="Group invitations">
|
|
<strong>Invitations</strong>
|
|
{snapshot.invitations.map((invitation) => (
|
|
<div key={invitation.id}>
|
|
<span><b>{invitation.inviterUsername}</b><small>wants you in their group</small></span>
|
|
<button type="button" disabled={busy} onClick={() => void respond(session, invitation.id, true)}>Accept</button>
|
|
<button type="button" disabled={busy} onClick={() => void respond(session, invitation.id, false)}>Decline</button>
|
|
</div>
|
|
))}
|
|
</section>
|
|
)}
|
|
|
|
{group && (
|
|
<section className="online-group-roster" aria-label="Online group roster">
|
|
<header><strong>Party roster</strong><small>{leader ? "You are the leader" : "Waiting for the leader"}</small></header>
|
|
{group.members.map((member) => {
|
|
const memberClass = member.character ? classById(member.character.classId as Parameters<typeof classById>[0]) : null;
|
|
return (
|
|
<div className="online-group-member" key={member.accountId}>
|
|
<span className={member.character && member.role ? "is-ready" : ""} aria-hidden="true" />
|
|
<p>
|
|
<strong>{member.username}{group.leaderAccountId === member.accountId ? " ★" : ""}</strong>
|
|
<small>{member.character
|
|
? `${member.character.name} · Level ${member.character.level} ${memberClass?.name ?? member.character.classId}`
|
|
: "Choosing a character"}</small>
|
|
</p>
|
|
<b>{member.role ? PARTY_ROLE_LABELS[member.role] : "Not ready"}</b>
|
|
{leader && member.accountId !== session.ownerId && (
|
|
<button type="button" disabled={busy} onClick={() => void remove(session, member.accountId)}>Remove</button>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</section>
|
|
)}
|
|
|
|
{group && character && currentMember && (
|
|
<section className="online-group-role" aria-label="Your group role">
|
|
<strong>Your role</strong>
|
|
<div>
|
|
{roles.map((role) => (
|
|
<button
|
|
type="button"
|
|
key={role}
|
|
className={currentMember.role === role ? "is-active" : ""}
|
|
disabled={busy || Boolean(group.activity)}
|
|
aria-pressed={currentMember.role === role}
|
|
onClick={() => chooseRole(role)}
|
|
>
|
|
{PARTY_ROLE_LABELS[role]}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{group?.activity && (
|
|
<div className="online-group-activity" role="status">
|
|
<span>
|
|
<strong>Launching {group.activity.type === "dungeon" ? "dungeon" : "Manastorm"}</strong>
|
|
<small>{group.activity.fillWithAi ? `Party filled to ${group.activity.partySize} with AI` : `${group.activity.partySize} players only`}</small>
|
|
</span>
|
|
{leader && <button type="button" disabled={busy} onClick={() => void clearActivity(session)}>Reset</button>}
|
|
</div>
|
|
)}
|
|
|
|
{leader && group && !group.activity && (
|
|
<label className="online-group-ai-toggle">
|
|
<input type="checkbox" checked={fillWithAi} onChange={(event) => setFillWithAi(event.target.checked)} />
|
|
<span><strong>Fill open slots with AI</strong><small>Turn off to enter with only grouped players.</small></span>
|
|
</label>
|
|
)}
|
|
|
|
{(!group || leader) && (
|
|
<form className="online-group-invite-form" onSubmit={submitInvite}>
|
|
<label htmlFor="online-group-username">Invite account name</label>
|
|
<div>
|
|
<input
|
|
id="online-group-username"
|
|
value={username}
|
|
maxLength={20}
|
|
autoComplete="off"
|
|
placeholder="PlayerName"
|
|
onChange={(event) => setUsername(event.target.value)}
|
|
/>
|
|
<button type="submit" disabled={busy || !username.trim() || (group?.members.length ?? 0) >= 5}>Invite</button>
|
|
</div>
|
|
</form>
|
|
)}
|
|
|
|
{group && (
|
|
<button className="online-group-leave" type="button" disabled={busy} onClick={() => void leave(session)}>
|
|
{leader && group.members.length > 1 ? "Leave and transfer leadership" : "Leave group"}
|
|
</button>
|
|
)}
|
|
{error && <p className="online-group-error" role="alert">{error}</p>}
|
|
</div>
|
|
</details>
|
|
);
|
|
}
|