@jgengine/react 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/hooks.d.ts CHANGED
@@ -1,10 +1,13 @@
1
+ import { AxisChannel, type AxisChannelConfig } from "@jgengine/core/input/axisInput";
1
2
  import type { GameContext } from "@jgengine/core/runtime/gameContext";
2
3
  import type { AbilityKit, AbilitySlotSnapshot } from "@jgengine/core/combat/abilityKit";
3
4
  import type { EventMeter } from "@jgengine/core/stats/eventMeter";
4
5
  import type { GameEvents } from "@jgengine/core/game/events";
5
6
  import type { FeedEntry } from "@jgengine/core/game/feed";
6
7
  import type { QuestInstance } from "@jgengine/core/game/quest";
7
- import type { FriendEntry, PartyMemberEntry, PresenceInfo } from "@jgengine/core/game/social";
8
+ import type { ChatMessage } from "@jgengine/core/game/chat";
9
+ import type { FriendEntry, FriendRequestEntry, PartyInviteEntry, PartyMemberEntry, PresenceInfo, WorldInvite } from "@jgengine/core/game/social";
10
+ import { type MatchFilter, type SessionListing } from "@jgengine/core/multiplayer/matchmaking";
8
11
  import type { LeaderboardScope } from "@jgengine/core/game/leaderboard";
9
12
  import type { InventorySlot } from "@jgengine/core/inventory/inventoryModel";
10
13
  import type { StatValue } from "@jgengine/core/scene/entityStats";
@@ -40,6 +43,30 @@ export declare function useQuestJournal(): QuestInstance[];
40
43
  export declare function useFriends(): FriendEntry[];
41
44
  export declare function useParty(): PartyMemberEntry[];
42
45
  export declare function usePresence(userId: string): PresenceInfo;
46
+ export declare function useWorldInvites(): WorldInvite[];
47
+ export declare function useChat(channelId: string, options?: {
48
+ limit?: number;
49
+ }): ChatMessage[];
50
+ export declare function useFriendRequests(): FriendRequestEntry[];
51
+ export declare function usePartyInvites(): PartyInviteEntry[];
52
+ export interface WorldBrowserState {
53
+ listings: SessionListing[];
54
+ loading: boolean;
55
+ error: string | null;
56
+ refresh(): void;
57
+ }
58
+ /**
59
+ * Polls a host-supplied session fetcher (e.g. createWsBackend().browse) and
60
+ * filters through matchmaking's browseSessions. fetchSessions must be
61
+ * identity-stable (wrap in useCallback at the call site) or every render
62
+ * refetches.
63
+ */
64
+ export declare function useWorldBrowser(options: {
65
+ fetchSessions: () => Promise<readonly SessionListing[]>;
66
+ filter?: MatchFilter;
67
+ limit?: number;
68
+ refreshMs?: number;
69
+ }): WorldBrowserState;
43
70
  export declare function useRoster(userId?: string): readonly RosterEntry[];
44
71
  export declare function useLeaderboard(stat: string, options: {
45
72
  scope: LeaderboardScope;
@@ -66,3 +93,25 @@ export interface EventMeterView {
66
93
  ready: boolean;
67
94
  }
68
95
  export declare function useEventMeter(meter: EventMeter, options?: AbilitySlotBindingOptions): EventMeterView;
96
+ type HeldKeyEventTarget = Pick<Window, "addEventListener" | "removeEventListener">;
97
+ export declare function createHeldKeyTracker(target: HeldKeyEventTarget): {
98
+ isDown: (code: string) => boolean;
99
+ dispose: () => void;
100
+ };
101
+ /**
102
+ * Held-key predicate backed by window keydown/keyup/blur listeners (blur clears held state so a
103
+ * released-off-window key doesn't stick). SSR-safe: listeners attach in an effect, never at module
104
+ * scope. The returned predicate is stable across renders.
105
+ */
106
+ export declare function useHeldKeys(): (code: string) => boolean;
107
+ export interface UseAxisChannelResult {
108
+ channel: AxisChannel;
109
+ isDown: (code: string) => boolean;
110
+ }
111
+ /**
112
+ * Wires useHeldKeys into a fresh AxisChannel, ready for a per-frame `channel.sample(dt, isDown)`.
113
+ * The channel is recreated when `config` identity changes, so pass a stable config (useMemo/module
114
+ * constant at the call site) unless a rebind is intended.
115
+ */
116
+ export declare function useAxisChannel(config: AxisChannelConfig): UseAxisChannelResult;
117
+ export {};
package/dist/hooks.js CHANGED
@@ -1,4 +1,6 @@
1
- import { useEffect, useMemo, useState, useSyncExternalStore } from "react";
1
+ import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
2
+ import { AxisChannel } from "@jgengine/core/input/axisInput";
3
+ import { browseSessions, } from "@jgengine/core/multiplayer/matchmaking";
2
4
  import { resolveActivePrompt, } from "@jgengine/core/interaction/proximityPrompt";
3
5
  import { useGameContext } from "./provider.js";
4
6
  export function useGameStore(selector) {
@@ -60,6 +62,63 @@ export function useParty() {
60
62
  export function usePresence(userId) {
61
63
  return useGameStore((ctx) => ctx.game.social.presence.get(userId));
62
64
  }
65
+ export function useWorldInvites() {
66
+ return useGameStore((ctx) => ctx.game.social.worldInvites.listFor(ctx.player.userId));
67
+ }
68
+ export function useChat(channelId, options) {
69
+ const limit = options?.limit ?? 50;
70
+ return useGameStore((ctx) => ctx.game.chat.history(channelId, { limit, viewerUserId: ctx.player.userId }));
71
+ }
72
+ export function useFriendRequests() {
73
+ return useGameStore((ctx) => ctx.game.social.friends.requestsFor(ctx.player.userId));
74
+ }
75
+ export function usePartyInvites() {
76
+ return useGameStore((ctx) => ctx.game.social.party.invitesFor(ctx.player.userId));
77
+ }
78
+ /**
79
+ * Polls a host-supplied session fetcher (e.g. createWsBackend().browse) and
80
+ * filters through matchmaking's browseSessions. fetchSessions must be
81
+ * identity-stable (wrap in useCallback at the call site) or every render
82
+ * refetches.
83
+ */
84
+ export function useWorldBrowser(options) {
85
+ const { fetchSessions, refreshMs } = options;
86
+ const [raw, setRaw] = useState([]);
87
+ const [loading, setLoading] = useState(true);
88
+ const [error, setError] = useState(null);
89
+ const [refreshTick, setRefreshTick] = useState(0);
90
+ useEffect(() => {
91
+ let cancelled = false;
92
+ setLoading(true);
93
+ fetchSessions()
94
+ .then((listings) => {
95
+ if (cancelled)
96
+ return;
97
+ setRaw(listings);
98
+ setError(null);
99
+ setLoading(false);
100
+ })
101
+ .catch((cause) => {
102
+ if (cancelled)
103
+ return;
104
+ setError(cause instanceof Error ? cause.message : "failed to browse sessions");
105
+ setLoading(false);
106
+ });
107
+ return () => {
108
+ cancelled = true;
109
+ };
110
+ }, [fetchSessions, refreshTick]);
111
+ useEffect(() => {
112
+ if (refreshMs === undefined || refreshMs <= 0)
113
+ return undefined;
114
+ const id = setInterval(() => setRefreshTick((tick) => tick + 1), refreshMs);
115
+ return () => clearInterval(id);
116
+ }, [refreshMs]);
117
+ const filter = options.filter;
118
+ const limit = options.limit;
119
+ const listings = useMemo(() => browseSessions(raw, filter, limit === undefined ? {} : { limit }), [raw, filter, limit]);
120
+ return useMemo(() => ({ listings, loading, error, refresh: () => setRefreshTick((tick) => tick + 1) }), [listings, loading, error]);
121
+ }
63
122
  export function useRoster(userId) {
64
123
  return useGameStore((ctx) => ctx.game.roster.list(userId ?? ctx.player.userId));
65
124
  }
@@ -118,3 +177,50 @@ export function useEventMeter(meter, options) {
118
177
  useEngineHeartbeat(options?.intervalMs ?? 80);
119
178
  return { value: meter.value(), fraction: meter.fraction(), tier: meter.tier(), ready: meter.ready() };
120
179
  }
180
+ export function createHeldKeyTracker(target) {
181
+ const held = new Set();
182
+ const onKeyDown = (event) => held.add(event.code);
183
+ const onKeyUp = (event) => held.delete(event.code);
184
+ const onBlur = () => held.clear();
185
+ target.addEventListener("keydown", onKeyDown);
186
+ target.addEventListener("keyup", onKeyUp);
187
+ target.addEventListener("blur", onBlur);
188
+ return {
189
+ isDown: (code) => held.has(code),
190
+ dispose: () => {
191
+ target.removeEventListener("keydown", onKeyDown);
192
+ target.removeEventListener("keyup", onKeyUp);
193
+ target.removeEventListener("blur", onBlur);
194
+ held.clear();
195
+ },
196
+ };
197
+ }
198
+ /**
199
+ * Held-key predicate backed by window keydown/keyup/blur listeners (blur clears held state so a
200
+ * released-off-window key doesn't stick). SSR-safe: listeners attach in an effect, never at module
201
+ * scope. The returned predicate is stable across renders.
202
+ */
203
+ export function useHeldKeys() {
204
+ const trackerRef = useRef(null);
205
+ useEffect(() => {
206
+ if (typeof window === "undefined")
207
+ return undefined;
208
+ const tracker = createHeldKeyTracker(window);
209
+ trackerRef.current = tracker;
210
+ return () => {
211
+ tracker.dispose();
212
+ trackerRef.current = null;
213
+ };
214
+ }, []);
215
+ return useCallback((code) => trackerRef.current?.isDown(code) ?? false, []);
216
+ }
217
+ /**
218
+ * Wires useHeldKeys into a fresh AxisChannel, ready for a per-frame `channel.sample(dt, isDown)`.
219
+ * The channel is recreated when `config` identity changes, so pass a stable config (useMemo/module
220
+ * constant at the call site) unless a rebind is intended.
221
+ */
222
+ export function useAxisChannel(config) {
223
+ const isDown = useHeldKeys();
224
+ const channel = useMemo(() => new AxisChannel(config), [config]);
225
+ return { channel, isDown };
226
+ }
@@ -0,0 +1,65 @@
1
+ import { type ReactNode } from "react";
2
+ import { type AuthSession, type PlayerIdentity } from "@jgengine/core/multiplayer/identity";
3
+ export interface IdentitySource {
4
+ session: AuthSession | null;
5
+ isLoading: boolean;
6
+ signOut?: () => void;
7
+ }
8
+ export interface ClerkUserShape {
9
+ id: string;
10
+ fullName?: string | null;
11
+ username?: string | null;
12
+ imageUrl?: string | null;
13
+ primaryEmailAddress?: {
14
+ emailAddress: string;
15
+ } | null;
16
+ createdAt?: Date | null;
17
+ lastSignInAt?: Date | null;
18
+ }
19
+ export interface ClerkUserState {
20
+ isLoaded: boolean;
21
+ isSignedIn: boolean | undefined;
22
+ user: ClerkUserShape | null | undefined;
23
+ }
24
+ export declare function clerkIdentity(state: ClerkUserState, options?: {
25
+ signOut?: () => void;
26
+ }): IdentitySource;
27
+ export interface BetterAuthUserShape {
28
+ id: string;
29
+ name?: string | null;
30
+ email?: string | null;
31
+ image?: string | null;
32
+ }
33
+ export interface BetterAuthSessionState {
34
+ data: {
35
+ user: BetterAuthUserShape;
36
+ } | null | undefined;
37
+ isPending: boolean;
38
+ }
39
+ export declare function betterAuthIdentity(state: BetterAuthSessionState, options?: {
40
+ signOut?: () => void;
41
+ }): IdentitySource;
42
+ export declare function guestIdentity(seed?: string): IdentitySource;
43
+ export declare function GameIdentityProvider({ source, children, }: {
44
+ source: IdentitySource;
45
+ children?: ReactNode;
46
+ }): import("react").JSX.Element;
47
+ export declare function useSession(): IdentitySource;
48
+ export declare function useAuthedPlayer(options?: {
49
+ guestSeed?: string;
50
+ }): PlayerIdentity | null;
51
+ export declare function RequireSession({ fallback, loading, children, }: {
52
+ fallback?: ReactNode;
53
+ loading?: ReactNode;
54
+ children?: ReactNode;
55
+ }): import("react").JSX.Element;
56
+ export declare function UserBadge({ className, avatarClassName, nameClassName, renderBadge, }: {
57
+ className?: string;
58
+ avatarClassName?: string;
59
+ nameClassName?: string;
60
+ renderBadge?: (session: AuthSession) => ReactNode;
61
+ }): import("react").JSX.Element | null;
62
+ export declare function SignOutButton({ className, children, }: {
63
+ className?: string;
64
+ children?: ReactNode;
65
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,94 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { createContext, useContext, useMemo } from "react";
3
+ import { resolveGuestSession, sessionPlayer, } from "@jgengine/core/multiplayer/identity";
4
+ export function clerkIdentity(state, options) {
5
+ if (!state.isLoaded)
6
+ return { session: null, isLoading: true };
7
+ const user = state.user;
8
+ if (state.isSignedIn !== true || user === null || user === undefined) {
9
+ return { session: null, isLoading: false };
10
+ }
11
+ const displayName = user.fullName ?? user.username ?? undefined;
12
+ const session = { userId: user.id };
13
+ if (displayName !== undefined)
14
+ session.displayName = displayName;
15
+ if (user.imageUrl !== null && user.imageUrl !== undefined)
16
+ session.avatarUrl = user.imageUrl;
17
+ const email = user.primaryEmailAddress?.emailAddress;
18
+ if (email !== undefined)
19
+ session.email = email;
20
+ if (user.createdAt !== null && user.createdAt !== undefined) {
21
+ session.isNew =
22
+ user.lastSignInAt === null ||
23
+ user.lastSignInAt === undefined ||
24
+ user.lastSignInAt.getTime() <= user.createdAt.getTime();
25
+ }
26
+ const source = { session, isLoading: false };
27
+ if (options?.signOut !== undefined)
28
+ source.signOut = options.signOut;
29
+ return source;
30
+ }
31
+ export function betterAuthIdentity(state, options) {
32
+ if (state.isPending)
33
+ return { session: null, isLoading: true };
34
+ const user = state.data?.user;
35
+ if (user === undefined)
36
+ return { session: null, isLoading: false };
37
+ const session = { userId: user.id };
38
+ if (user.name !== null && user.name !== undefined)
39
+ session.displayName = user.name;
40
+ if (user.image !== null && user.image !== undefined)
41
+ session.avatarUrl = user.image;
42
+ if (user.email !== null && user.email !== undefined)
43
+ session.email = user.email;
44
+ const source = { session, isLoading: false };
45
+ if (options?.signOut !== undefined)
46
+ source.signOut = options.signOut;
47
+ return source;
48
+ }
49
+ export function guestIdentity(seed) {
50
+ return { session: resolveGuestSession(seed), isLoading: false };
51
+ }
52
+ const IdentityReactContext = createContext(null);
53
+ export function GameIdentityProvider({ source, children, }) {
54
+ return _jsx(IdentityReactContext.Provider, { value: source, children: children });
55
+ }
56
+ export function useSession() {
57
+ const source = useContext(IdentityReactContext);
58
+ if (source === null)
59
+ throw new Error("useSession must be used within <GameIdentityProvider>");
60
+ return source;
61
+ }
62
+ export function useAuthedPlayer(options) {
63
+ const { session, isLoading } = useSession();
64
+ const guestSeed = options?.guestSeed;
65
+ return useMemo(() => {
66
+ if (session !== null)
67
+ return sessionPlayer(session);
68
+ if (isLoading || guestSeed === undefined)
69
+ return null;
70
+ return sessionPlayer(resolveGuestSession(guestSeed));
71
+ }, [session, isLoading, guestSeed]);
72
+ }
73
+ export function RequireSession({ fallback, loading, children, }) {
74
+ const { session, isLoading } = useSession();
75
+ if (isLoading)
76
+ return _jsx(_Fragment, { children: loading ?? null });
77
+ if (session === null)
78
+ return _jsx(_Fragment, { children: fallback ?? null });
79
+ return _jsx(_Fragment, { children: children });
80
+ }
81
+ export function UserBadge({ className, avatarClassName, nameClassName, renderBadge, }) {
82
+ const { session } = useSession();
83
+ if (session === null)
84
+ return null;
85
+ if (renderBadge !== undefined)
86
+ return _jsx(_Fragment, { children: renderBadge(session) });
87
+ return (_jsxs("span", { className: className, "data-user": session.userId, children: [session.avatarUrl !== undefined && (_jsx("img", { className: avatarClassName, src: session.avatarUrl, alt: "", "data-avatar": true })), _jsx("span", { className: nameClassName, "data-display-name": true, children: session.displayName ?? session.userId })] }));
88
+ }
89
+ export function SignOutButton({ className, children, }) {
90
+ const { session, signOut } = useSession();
91
+ if (session === null || signOut === undefined)
92
+ return null;
93
+ return (_jsx("button", { type: "button", className: className, "data-sign-out": true, onClick: () => signOut(), children: children ?? "Sign out" }));
94
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  export * from "./provider.js";
2
+ export * from "./identity.js";
3
+ export * from "./chat.js";
4
+ export * from "./voice.js";
5
+ export * from "./social.js";
2
6
  export * from "./hooks.js";
3
7
  export * from "./components.js";
4
8
  export * from "./engineStore.js";
5
9
  export * from "./dragLayer.js";
10
+ export * from "./display.js";
6
11
  export * from "./map.js";
package/dist/index.js CHANGED
@@ -1,6 +1,11 @@
1
1
  export * from "./provider.js";
2
+ export * from "./identity.js";
3
+ export * from "./chat.js";
4
+ export * from "./voice.js";
5
+ export * from "./social.js";
2
6
  export * from "./hooks.js";
3
7
  export * from "./components.js";
4
8
  export * from "./engineStore.js";
5
9
  export * from "./dragLayer.js";
10
+ export * from "./display.js";
6
11
  export * from "./map.js";
@@ -0,0 +1,108 @@
1
+ import { type ReactNode } from "react";
2
+ import type { FriendEntry, FriendRequestEntry, PartyInviteEntry, PartyMemberEntry, WorldInvite, WorldInviteTarget } from "@jgengine/core/game/social";
3
+ import { type MatchFilter, type SessionListing } from "@jgengine/core/multiplayer/matchmaking";
4
+ export declare function PresenceDot({ userId, className }: {
5
+ userId: string;
6
+ className?: string;
7
+ }): import("react").JSX.Element;
8
+ export declare function FriendRow({ friend, className, dotClassName, children, }: {
9
+ friend: FriendEntry;
10
+ className?: string;
11
+ dotClassName?: string;
12
+ children?: ReactNode;
13
+ }): import("react").JSX.Element;
14
+ export declare function FriendsList({ className, rowClassName, dotClassName, emptyState, renderFriend, }: {
15
+ className?: string;
16
+ rowClassName?: string;
17
+ dotClassName?: string;
18
+ emptyState?: ReactNode;
19
+ renderFriend?: (friend: FriendEntry) => ReactNode;
20
+ }): import("react").JSX.Element;
21
+ export declare function AddFriendButton({ toUserId, className, children, onRequested, onRejected, }: {
22
+ toUserId: string;
23
+ className?: string;
24
+ children?: ReactNode;
25
+ onRequested?: (requestId: string) => void;
26
+ onRejected?: (reason: string) => void;
27
+ }): import("react").JSX.Element;
28
+ export declare function FriendRequestsList({ className, rowClassName, acceptClassName, declineClassName, emptyState, renderRequest, }: {
29
+ className?: string;
30
+ rowClassName?: string;
31
+ acceptClassName?: string;
32
+ declineClassName?: string;
33
+ emptyState?: ReactNode;
34
+ renderRequest?: (request: FriendRequestEntry) => ReactNode;
35
+ }): import("react").JSX.Element;
36
+ export declare function PartyMemberRow({ member, className, dotClassName, children, }: {
37
+ member: PartyMemberEntry;
38
+ className?: string;
39
+ dotClassName?: string;
40
+ children?: ReactNode;
41
+ }): import("react").JSX.Element;
42
+ export declare function PartyFrame({ className, rowClassName, dotClassName, emptyState, renderMember, }: {
43
+ className?: string;
44
+ rowClassName?: string;
45
+ dotClassName?: string;
46
+ emptyState?: ReactNode;
47
+ renderMember?: (member: PartyMemberEntry) => ReactNode;
48
+ }): import("react").JSX.Element;
49
+ export declare function PartyInviteToast({ className, acceptClassName, declineClassName, renderInvite, }: {
50
+ className?: string;
51
+ acceptClassName?: string;
52
+ declineClassName?: string;
53
+ renderInvite?: (invite: PartyInviteEntry) => ReactNode;
54
+ }): import("react").JSX.Element | null;
55
+ export declare function LeavePartyButton({ className, children, }: {
56
+ className?: string;
57
+ children?: ReactNode;
58
+ }): import("react").JSX.Element | null;
59
+ export declare function WorldInviteToast({ className, acceptClassName, declineClassName, onAccepted, renderInvite, }: {
60
+ className?: string;
61
+ acceptClassName?: string;
62
+ declineClassName?: string;
63
+ onAccepted: (target: WorldInviteTarget) => void;
64
+ renderInvite?: (invite: WorldInvite) => ReactNode;
65
+ }): import("react").JSX.Element | null;
66
+ export declare function InviteToWorldButton({ toUserId, target, className, children, onInvited, onRejected, }: {
67
+ toUserId: string;
68
+ target: WorldInviteTarget;
69
+ className?: string;
70
+ children?: ReactNode;
71
+ onInvited?: (inviteId: string) => void;
72
+ onRejected?: (reason: string) => void;
73
+ }): import("react").JSX.Element;
74
+ export declare function WorldBrowser({ listings, onJoin, className, rowClassName, joinClassName, emptyState, renderListing, }: {
75
+ listings: readonly SessionListing[];
76
+ onJoin: (listing: SessionListing) => void;
77
+ className?: string;
78
+ rowClassName?: string;
79
+ joinClassName?: string;
80
+ emptyState?: ReactNode;
81
+ renderListing?: (listing: SessionListing) => ReactNode;
82
+ }): import("react").JSX.Element;
83
+ export declare function JoinByCode({ onJoin, className, inputClassName, buttonClassName, placeholder, children, }: {
84
+ onJoin: (code: string) => void;
85
+ className?: string;
86
+ inputClassName?: string;
87
+ buttonClassName?: string;
88
+ placeholder?: string;
89
+ children?: ReactNode;
90
+ }): import("react").JSX.Element;
91
+ export declare function QuickMatchButton({ listings, onJoin, onNoMatch, filter, className, children, }: {
92
+ listings: readonly SessionListing[];
93
+ onJoin: (listing: SessionListing) => void;
94
+ onNoMatch?: () => void;
95
+ filter?: MatchFilter;
96
+ className?: string;
97
+ children?: ReactNode;
98
+ }): import("react").JSX.Element;
99
+ export declare function EmoteWheel({ emotes, radius, open, className, emoteClassName, onPlayed, onRejected, renderEmote, }: {
100
+ emotes: readonly string[];
101
+ radius?: number;
102
+ open?: boolean;
103
+ className?: string;
104
+ emoteClassName?: string;
105
+ onPlayed?: (emoteId: string) => void;
106
+ onRejected?: (reason: string) => void;
107
+ renderEmote?: (emoteId: string) => ReactNode;
108
+ }): import("react").JSX.Element | null;
package/dist/social.js ADDED
@@ -0,0 +1,119 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { normalizeJoinCode, quickMatch, } from "@jgengine/core/multiplayer/matchmaking";
4
+ import { useGameContext } from "./provider.js";
5
+ import { useFriendRequests, useFriends, useParty, usePartyInvites, usePresence, useWorldInvites, } from "./hooks.js";
6
+ export function PresenceDot({ userId, className }) {
7
+ const presence = usePresence(userId);
8
+ return (_jsx("span", { className: className, "data-presence-dot": true, "data-user": userId, "data-online": presence.online, "data-server": presence.serverId }));
9
+ }
10
+ export function FriendRow({ friend, className, dotClassName, children, }) {
11
+ return (_jsxs("div", { className: className, "data-friend": friend.userId, "data-online": friend.online, children: [_jsx(PresenceDot, { userId: friend.userId, className: dotClassName }), _jsx("span", { "data-friend-name": true, children: friend.userId }), children] }));
12
+ }
13
+ export function FriendsList({ className, rowClassName, dotClassName, emptyState, renderFriend, }) {
14
+ const friends = useFriends();
15
+ return (_jsx("div", { className: className, "data-friends-list": true, children: friends.length === 0
16
+ ? emptyState ?? null
17
+ : friends.map((friend) => renderFriend !== undefined ? (renderFriend(friend)) : (_jsx(FriendRow, { friend: friend, className: rowClassName, dotClassName: dotClassName }, friend.userId))) }));
18
+ }
19
+ export function AddFriendButton({ toUserId, className, children, onRequested, onRejected, }) {
20
+ const ctx = useGameContext();
21
+ const denied = ctx.game.social.friends.canRequest(ctx.player.userId, toUserId);
22
+ return (_jsx("button", { type: "button", className: className, "data-add-friend": toUserId, disabled: denied !== null, title: denied?.reason, onClick: () => {
23
+ const result = ctx.game.social.friends.request(ctx.player.userId, toUserId);
24
+ if ("reason" in result)
25
+ onRejected?.(result.reason);
26
+ else
27
+ onRequested?.(result.requestId);
28
+ }, children: children ?? "Add friend" }));
29
+ }
30
+ export function FriendRequestsList({ className, rowClassName, acceptClassName, declineClassName, emptyState, renderRequest, }) {
31
+ const ctx = useGameContext();
32
+ const requests = useFriendRequests();
33
+ return (_jsx("div", { className: className, "data-friend-requests": true, children: requests.length === 0
34
+ ? emptyState ?? null
35
+ : requests.map((request) => renderRequest !== undefined ? (renderRequest(request)) : (_jsxs("div", { className: rowClassName, "data-friend-request": request.requestId, children: [_jsx("span", { "data-request-from": true, children: request.fromUserId }), _jsx("button", { type: "button", className: acceptClassName, "data-accept": true, onClick: () => ctx.game.social.friends.accept(ctx.player.userId, request.requestId), children: "Accept" }), _jsx("button", { type: "button", className: declineClassName, "data-decline": true, onClick: () => ctx.game.social.friends.decline(ctx.player.userId, request.requestId), children: "Decline" })] }, request.requestId))) }));
36
+ }
37
+ export function PartyMemberRow({ member, className, dotClassName, children, }) {
38
+ return (_jsxs("div", { className: className, "data-party-member": member.userId, "data-role": member.role, children: [_jsx(PresenceDot, { userId: member.userId, className: dotClassName }), _jsx("span", { "data-member-name": true, children: member.userId }), children] }));
39
+ }
40
+ export function PartyFrame({ className, rowClassName, dotClassName, emptyState, renderMember, }) {
41
+ const members = useParty();
42
+ return (_jsx("div", { className: className, "data-party-frame": true, "data-party-size": members.length, children: members.length === 0
43
+ ? emptyState ?? null
44
+ : members.map((member) => renderMember !== undefined ? (renderMember(member)) : (_jsx(PartyMemberRow, { member: member, className: rowClassName, dotClassName: dotClassName }, member.userId))) }));
45
+ }
46
+ export function PartyInviteToast({ className, acceptClassName, declineClassName, renderInvite, }) {
47
+ const ctx = useGameContext();
48
+ const invites = usePartyInvites();
49
+ if (invites.length === 0)
50
+ return null;
51
+ return (_jsx("div", { className: className, "data-party-invites": true, children: invites.map((invite) => renderInvite !== undefined ? (renderInvite(invite)) : (_jsxs("div", { "data-party-invite": invite.inviteId, children: [_jsx("span", { "data-invite-from": true, children: invite.fromUserId }), _jsx("button", { type: "button", className: acceptClassName, "data-accept": true, onClick: () => ctx.game.social.party.accept(ctx.player.userId, invite.inviteId), children: "Join" }), _jsx("button", { type: "button", className: declineClassName, "data-decline": true, onClick: () => ctx.game.social.party.decline(ctx.player.userId, invite.inviteId), children: "Decline" })] }, invite.inviteId))) }));
52
+ }
53
+ export function LeavePartyButton({ className, children, }) {
54
+ const ctx = useGameContext();
55
+ const members = useParty();
56
+ if (members.length === 0)
57
+ return null;
58
+ return (_jsx("button", { type: "button", className: className, "data-leave-party": true, onClick: () => ctx.game.social.party.leave(ctx.player.userId), children: children ?? "Leave party" }));
59
+ }
60
+ export function WorldInviteToast({ className, acceptClassName, declineClassName, onAccepted, renderInvite, }) {
61
+ const ctx = useGameContext();
62
+ const invites = useWorldInvites();
63
+ if (invites.length === 0)
64
+ return null;
65
+ return (_jsx("div", { className: className, "data-world-invites": true, children: invites.map((invite) => renderInvite !== undefined ? (renderInvite(invite)) : (_jsxs("div", { "data-world-invite": invite.id, children: [_jsx("span", { "data-invite-from": true, children: invite.fromUserId }), _jsx("span", { "data-invite-server": true, children: invite.serverId }), _jsx("button", { type: "button", className: acceptClassName, "data-accept": true, onClick: () => {
66
+ const result = ctx.game.social.worldInvites.accept(ctx.player.userId, invite.id);
67
+ if ("target" in result)
68
+ onAccepted(result.target);
69
+ }, children: "Join" }), _jsx("button", { type: "button", className: declineClassName, "data-decline": true, onClick: () => ctx.game.social.worldInvites.decline(ctx.player.userId, invite.id), children: "Decline" })] }, invite.id))) }));
70
+ }
71
+ export function InviteToWorldButton({ toUserId, target, className, children, onInvited, onRejected, }) {
72
+ const ctx = useGameContext();
73
+ const denied = ctx.game.social.worldInvites.canInvite(ctx.player.userId, toUserId);
74
+ return (_jsx("button", { type: "button", className: className, "data-invite-to-world": toUserId, disabled: denied !== null, title: denied?.reason, onClick: () => {
75
+ const result = ctx.game.social.worldInvites.invite(ctx.player.userId, toUserId, target);
76
+ if ("reason" in result)
77
+ onRejected?.(result.reason);
78
+ else
79
+ onInvited?.(result.inviteId);
80
+ }, children: children ?? "Invite to world" }));
81
+ }
82
+ export function WorldBrowser({ listings, onJoin, className, rowClassName, joinClassName, emptyState, renderListing, }) {
83
+ return (_jsx("div", { className: className, "data-world-browser": true, children: listings.length === 0
84
+ ? emptyState ?? null
85
+ : listings.map((listing) => renderListing !== undefined ? (renderListing(listing)) : (_jsxs("div", { className: rowClassName, "data-world": listing.serverId, "data-mode": listing.mode, "data-full": listing.memberCount >= listing.slotsPerServer, children: [_jsx("span", { "data-world-label": true, children: listing.label ?? listing.serverId }), _jsxs("span", { "data-world-count": true, children: [listing.memberCount, "/", listing.slotsPerServer] }), _jsx("button", { type: "button", className: joinClassName, "data-join": true, disabled: listing.memberCount >= listing.slotsPerServer, onClick: () => onJoin(listing), children: "Join" })] }, listing.serverId))) }));
86
+ }
87
+ export function JoinByCode({ onJoin, className, inputClassName, buttonClassName, placeholder, children, }) {
88
+ const [code, setCode] = useState("");
89
+ function submit(event) {
90
+ event.preventDefault();
91
+ const normalized = normalizeJoinCode(code);
92
+ if (normalized.length === 0)
93
+ return;
94
+ onJoin(normalized);
95
+ setCode("");
96
+ }
97
+ return (_jsxs("form", { className: className, "data-join-by-code": true, onSubmit: submit, children: [_jsx("input", { className: inputClassName, type: "text", value: code, placeholder: placeholder ?? "Join code", onChange: (event) => setCode(event.target.value) }), _jsx("button", { type: "submit", className: buttonClassName, "data-join": true, children: children ?? "Join" })] }));
98
+ }
99
+ export function QuickMatchButton({ listings, onJoin, onNoMatch, filter, className, children, }) {
100
+ return (_jsx("button", { type: "button", className: className, "data-quick-match": true, onClick: () => {
101
+ const match = quickMatch(listings, filter);
102
+ if (match === null)
103
+ onNoMatch?.();
104
+ else
105
+ onJoin(match);
106
+ }, children: children ?? "Quick match" }));
107
+ }
108
+ export function EmoteWheel({ emotes, radius, open = true, className, emoteClassName, onPlayed, onRejected, renderEmote, }) {
109
+ const ctx = useGameContext();
110
+ if (!open)
111
+ return null;
112
+ return (_jsx("div", { className: className, role: "menu", "data-emote-wheel": true, "data-emote-count": emotes.length, children: emotes.map((emoteId, index) => (_jsx("button", { type: "button", role: "menuitem", className: emoteClassName, "data-emote": emoteId, "data-emote-index": index, onClick: () => {
113
+ const result = ctx.game.social.emotes.play(ctx.player.userId, emoteId, radius);
114
+ if ("reason" in result)
115
+ onRejected?.(result.reason);
116
+ else
117
+ onPlayed?.(emoteId);
118
+ }, children: renderEmote !== undefined ? renderEmote(emoteId) : emoteId }, emoteId))) }));
119
+ }
@@ -0,0 +1,58 @@
1
+ import { type ReactNode } from "react";
2
+ import { type PushToTalkMode, type PushToTalkStatus, type VoiceParticipant, type VoiceRoute, type VoiceTransport } from "@jgengine/core/multiplayer/voiceContract";
3
+ export interface UseVoiceOptions {
4
+ transport?: VoiceTransport;
5
+ channelId?: string;
6
+ mode?: PushToTalkMode;
7
+ resolveRoutes?: () => readonly VoiceRoute[];
8
+ getUserMedia?: (constraints: MediaStreamConstraints) => Promise<MediaStream>;
9
+ }
10
+ export interface VoiceState {
11
+ supported: boolean;
12
+ micStream: MediaStream | null;
13
+ micError: string | null;
14
+ requestMic(): Promise<boolean>;
15
+ transmitting: boolean;
16
+ status: PushToTalkStatus;
17
+ mode: PushToTalkMode;
18
+ setMode(mode: PushToTalkMode): void;
19
+ muted: boolean;
20
+ setMuted(muted: boolean): void;
21
+ keyDown(): void;
22
+ keyUp(): void;
23
+ participants: readonly VoiceParticipant[];
24
+ routes: readonly VoiceRoute[];
25
+ gainFor(userId: string): number;
26
+ }
27
+ /**
28
+ * Mic capture + push-to-talk + channel roster over the VoiceTransport
29
+ * signaling seam. Transmission gates the captured tracks' `enabled` flag; the
30
+ * media plane that actually moves audio bytes (WebRTC/SFU) stays behind the
31
+ * transport, host-supplied. Call once per voice channel and hand the returned
32
+ * state to the voice components.
33
+ */
34
+ export declare function useVoice(options?: UseVoiceOptions): VoiceState;
35
+ export declare function PushToTalkButton({ voice, className, children, }: {
36
+ voice: VoiceState;
37
+ className?: string;
38
+ children?: ReactNode;
39
+ }): import("react").JSX.Element;
40
+ export declare function MicToggle({ voice, className, mutedLabel, unmutedLabel, }: {
41
+ voice: VoiceState;
42
+ className?: string;
43
+ mutedLabel?: ReactNode;
44
+ unmutedLabel?: ReactNode;
45
+ }): import("react").JSX.Element;
46
+ export declare function SpeakingIndicator({ voice, userId, className, threshold, children, }: {
47
+ voice: VoiceState;
48
+ userId: string;
49
+ className?: string;
50
+ threshold?: number;
51
+ children?: ReactNode;
52
+ }): import("react").JSX.Element;
53
+ export declare function VoiceRoster({ voice, className, participantClassName, renderParticipant, }: {
54
+ voice: VoiceState;
55
+ className?: string;
56
+ participantClassName?: string;
57
+ renderParticipant?: (participant: VoiceParticipant, gain: number) => ReactNode;
58
+ }): import("react").JSX.Element;