@jgengine/react 0.6.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/CHANGELOG.md +214 -2
- package/dist/chat.d.ts +52 -0
- package/dist/chat.js +68 -0
- package/dist/components.d.ts +52 -2
- package/dist/components.js +58 -2
- package/dist/display.d.ts +13 -0
- package/dist/display.js +44 -0
- package/dist/dragLayer.d.ts +67 -0
- package/dist/dragLayer.js +143 -0
- package/dist/gameIcons.d.ts +12 -0
- package/dist/gameIcons.js +282 -0
- package/dist/hooks.d.ts +70 -1
- package/dist/hooks.js +146 -1
- package/dist/identity.d.ts +65 -0
- package/dist/identity.js +94 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/map.d.ts +68 -0
- package/dist/map.js +210 -0
- package/dist/social.d.ts +108 -0
- package/dist/social.js +119 -0
- package/dist/voice.d.ts +58 -0
- package/dist/voice.js +130 -0
- package/llms.txt +1726 -0
- package/package.json +4 -3
package/dist/hooks.d.ts
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
|
+
import { AxisChannel, type AxisChannelConfig } from "@jgengine/core/input/axisInput";
|
|
1
2
|
import type { GameContext } from "@jgengine/core/runtime/gameContext";
|
|
3
|
+
import type { AbilityKit, AbilitySlotSnapshot } from "@jgengine/core/combat/abilityKit";
|
|
4
|
+
import type { EventMeter } from "@jgengine/core/stats/eventMeter";
|
|
2
5
|
import type { GameEvents } from "@jgengine/core/game/events";
|
|
3
6
|
import type { FeedEntry } from "@jgengine/core/game/feed";
|
|
4
7
|
import type { QuestInstance } from "@jgengine/core/game/quest";
|
|
5
|
-
import type {
|
|
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";
|
|
6
11
|
import type { LeaderboardScope } from "@jgengine/core/game/leaderboard";
|
|
7
12
|
import type { InventorySlot } from "@jgengine/core/inventory/inventoryModel";
|
|
8
13
|
import type { StatValue } from "@jgengine/core/scene/entityStats";
|
|
9
14
|
import type { SceneEntity } from "@jgengine/core/scene/entityStore";
|
|
10
15
|
import type { SceneObject } from "@jgengine/core/scene/objectStore";
|
|
16
|
+
import type { RosterEntry } from "@jgengine/core/scene/roster";
|
|
17
|
+
import type { WorldItemRecord } from "@jgengine/core/game/worldItem";
|
|
11
18
|
import type { ClockSnapshot, SimClock } from "@jgengine/core/time/simClock";
|
|
12
19
|
import { type PositionedPrompt } from "@jgengine/core/interaction/proximityPrompt";
|
|
13
20
|
export declare function useGameStore<T>(selector: (ctx: GameContext) => T): T;
|
|
@@ -21,6 +28,9 @@ export declare function usePlayer(): {
|
|
|
21
28
|
};
|
|
22
29
|
export declare function useSceneEntities(): readonly SceneEntity[];
|
|
23
30
|
export declare function useSceneObjects(): readonly SceneObject[];
|
|
31
|
+
export declare function useWorldItems(): readonly WorldItemRecord[];
|
|
32
|
+
/** Nearest ground item within `radius` of the local player — drives a pickup prompt/highlight. */
|
|
33
|
+
export declare function useNearestWorldItem(radius: number): WorldItemRecord | null;
|
|
24
34
|
export declare function useEntityStat(instanceId: string, statId: string): StatValue | null;
|
|
25
35
|
export declare function useTarget(fromInstanceId: string): string | null;
|
|
26
36
|
export declare function useInventory(inventoryId: string): readonly InventorySlot[];
|
|
@@ -33,6 +43,31 @@ export declare function useQuestJournal(): QuestInstance[];
|
|
|
33
43
|
export declare function useFriends(): FriendEntry[];
|
|
34
44
|
export declare function useParty(): PartyMemberEntry[];
|
|
35
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;
|
|
70
|
+
export declare function useRoster(userId?: string): readonly RosterEntry[];
|
|
36
71
|
export declare function useLeaderboard(stat: string, options: {
|
|
37
72
|
scope: LeaderboardScope;
|
|
38
73
|
limit?: number;
|
|
@@ -46,3 +81,37 @@ export declare function useGameClock(): ClockSnapshot & {
|
|
|
46
81
|
controls: SimClock;
|
|
47
82
|
};
|
|
48
83
|
export declare function useActivePrompt<T extends PositionedPrompt>(prompts?: readonly T[]): T | null;
|
|
84
|
+
export interface AbilitySlotBindingOptions {
|
|
85
|
+
intervalMs?: number;
|
|
86
|
+
}
|
|
87
|
+
export declare function useAbilitySlots(kit: AbilityKit, resourceAvailable?: number, options?: AbilitySlotBindingOptions): AbilitySlotSnapshot[];
|
|
88
|
+
export declare function useAbilitySlot(kit: AbilityKit, slotId: string, resourceAvailable?: number, options?: AbilitySlotBindingOptions): AbilitySlotSnapshot | null;
|
|
89
|
+
export interface EventMeterView {
|
|
90
|
+
value: number;
|
|
91
|
+
fraction: number;
|
|
92
|
+
tier: string | null;
|
|
93
|
+
ready: boolean;
|
|
94
|
+
}
|
|
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 { useMemo, 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) {
|
|
@@ -20,6 +22,19 @@ export function useSceneEntities() {
|
|
|
20
22
|
export function useSceneObjects() {
|
|
21
23
|
return useGameStore((ctx) => ctx.scene.object.list());
|
|
22
24
|
}
|
|
25
|
+
export function useWorldItems() {
|
|
26
|
+
return useGameStore((ctx) => ctx.scene.worldItem.list());
|
|
27
|
+
}
|
|
28
|
+
/** Nearest ground item within `radius` of the local player — drives a pickup prompt/highlight. */
|
|
29
|
+
export function useNearestWorldItem(radius) {
|
|
30
|
+
return useGameStore((ctx) => {
|
|
31
|
+
const player = localPlayerEntity(ctx);
|
|
32
|
+
if (player === null)
|
|
33
|
+
return null;
|
|
34
|
+
const instanceId = ctx.scene.worldItem.nearestInRadius(player.position, radius);
|
|
35
|
+
return instanceId === null ? null : ctx.scene.worldItem.get(instanceId);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
23
38
|
export function useEntityStat(instanceId, statId) {
|
|
24
39
|
return useGameStore((ctx) => ctx.scene.entity.stats.get(instanceId, statId));
|
|
25
40
|
}
|
|
@@ -47,6 +62,66 @@ export function useParty() {
|
|
|
47
62
|
export function usePresence(userId) {
|
|
48
63
|
return useGameStore((ctx) => ctx.game.social.presence.get(userId));
|
|
49
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
|
+
}
|
|
122
|
+
export function useRoster(userId) {
|
|
123
|
+
return useGameStore((ctx) => ctx.game.roster.list(userId ?? ctx.player.userId));
|
|
124
|
+
}
|
|
50
125
|
export function useLeaderboard(stat, options) {
|
|
51
126
|
return useGameStore((ctx) => ctx.game.leaderboard.getTop(stat, options));
|
|
52
127
|
}
|
|
@@ -79,3 +154,73 @@ export function useActivePrompt(prompts) {
|
|
|
79
154
|
return resolveActivePrompt({ x: player.position[0], z: player.position[2] }, prompts);
|
|
80
155
|
});
|
|
81
156
|
}
|
|
157
|
+
function useEngineHeartbeat(intervalMs) {
|
|
158
|
+
const ctx = useGameContext();
|
|
159
|
+
useSyncExternalStore(ctx.subscribe, ctx.version, ctx.version);
|
|
160
|
+
const [, setTick] = useState(0);
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
if (intervalMs <= 0 || typeof window === "undefined")
|
|
163
|
+
return undefined;
|
|
164
|
+
const id = window.setInterval(() => setTick((current) => current + 1), intervalMs);
|
|
165
|
+
return () => window.clearInterval(id);
|
|
166
|
+
}, [intervalMs]);
|
|
167
|
+
}
|
|
168
|
+
export function useAbilitySlots(kit, resourceAvailable, options) {
|
|
169
|
+
useEngineHeartbeat(options?.intervalMs ?? 80);
|
|
170
|
+
return kit.snapshot(resourceAvailable);
|
|
171
|
+
}
|
|
172
|
+
export function useAbilitySlot(kit, slotId, resourceAvailable, options) {
|
|
173
|
+
useEngineHeartbeat(options?.intervalMs ?? 80);
|
|
174
|
+
return kit.state(slotId, resourceAvailable);
|
|
175
|
+
}
|
|
176
|
+
export function useEventMeter(meter, options) {
|
|
177
|
+
useEngineHeartbeat(options?.intervalMs ?? 80);
|
|
178
|
+
return { value: meter.value(), fraction: meter.fraction(), tier: meter.tier(), ready: meter.ready() };
|
|
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;
|
package/dist/identity.js
ADDED
|
@@ -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,4 +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";
|
|
9
|
+
export * from "./dragLayer.js";
|
|
10
|
+
export * from "./display.js";
|
|
11
|
+
export * from "./map.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +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";
|
|
9
|
+
export * from "./dragLayer.js";
|
|
10
|
+
export * from "./display.js";
|
|
11
|
+
export * from "./map.js";
|
package/dist/map.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { FogField } from "@jgengine/core/world/fog";
|
|
3
|
+
import { type MapMarker, type MarkerKindStyle, type MarkerSet } from "@jgengine/core/world/markers";
|
|
4
|
+
import { type WorldXZ } from "@jgengine/core/world/minimap";
|
|
5
|
+
export declare function useMarkers(markers: MarkerSet): readonly MapMarker[];
|
|
6
|
+
export declare function useFog(fog: FogField): ReturnType<FogField["cells"]>;
|
|
7
|
+
export interface MinimapProps {
|
|
8
|
+
markers: MarkerSet;
|
|
9
|
+
center: WorldXZ;
|
|
10
|
+
worldRadius: number;
|
|
11
|
+
fog?: FogField;
|
|
12
|
+
size?: number;
|
|
13
|
+
heading?: number;
|
|
14
|
+
rotate?: boolean;
|
|
15
|
+
kindStyles?: Record<string, MarkerKindStyle>;
|
|
16
|
+
background?: string;
|
|
17
|
+
/** World bounds the `background` image spans, so it pans correctly under a player-centered map. */
|
|
18
|
+
mapBounds?: MapBounds;
|
|
19
|
+
className?: string;
|
|
20
|
+
title?: string;
|
|
21
|
+
children?: ReactNode;
|
|
22
|
+
}
|
|
23
|
+
export interface MapBounds {
|
|
24
|
+
minX: number;
|
|
25
|
+
minZ: number;
|
|
26
|
+
maxX: number;
|
|
27
|
+
maxZ: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Framed circular minimap: optional baked terrain background, reveal-on-event
|
|
31
|
+
* fog overlay, categorized marker icons, and a facing arrow. Reads a core
|
|
32
|
+
* `MarkerSet` / `FogField`; supply your own `kindStyles` palette to reskin.
|
|
33
|
+
*/
|
|
34
|
+
export declare function Minimap({ markers, center, worldRadius, fog, size, heading, rotate, kindStyles, background, mapBounds, className, title, children, }: MinimapProps): ReactNode;
|
|
35
|
+
export interface CompassProps {
|
|
36
|
+
heading: number;
|
|
37
|
+
center?: WorldXZ;
|
|
38
|
+
markers?: MarkerSet;
|
|
39
|
+
width?: number;
|
|
40
|
+
fov?: number;
|
|
41
|
+
kindStyles?: Record<string, MarkerKindStyle>;
|
|
42
|
+
className?: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Horizontal compass strip centered on the player's facing bearing, with the
|
|
46
|
+
* eight cardinals and optional marker pips (bearing to each `MarkerSet` entry).
|
|
47
|
+
*/
|
|
48
|
+
export declare function Compass({ heading, center, markers, width, fov, kindStyles, className, }: CompassProps): ReactNode;
|
|
49
|
+
export interface WorldMapProps {
|
|
50
|
+
markers: MarkerSet;
|
|
51
|
+
bounds: MapBounds;
|
|
52
|
+
player?: WorldXZ;
|
|
53
|
+
heading?: number;
|
|
54
|
+
fog?: FogField;
|
|
55
|
+
background?: string;
|
|
56
|
+
width?: number;
|
|
57
|
+
height?: number;
|
|
58
|
+
kindStyles?: Record<string, MarkerKindStyle>;
|
|
59
|
+
className?: string;
|
|
60
|
+
title?: string;
|
|
61
|
+
onClose?: () => void;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Full-bounds top-down world map (the "press M" overlay): baked terrain
|
|
65
|
+
* background, reveal-on-event fog, all markers with labels, and the player.
|
|
66
|
+
* Rectangular linear projection over the supplied world `bounds`.
|
|
67
|
+
*/
|
|
68
|
+
export declare function WorldMap({ markers, bounds, player, heading, fog, background, width, height, kindStyles, className, title, onClose, }: WorldMapProps): ReactNode;
|