Release Healer Man 0.1.6
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user