@jgengine/react 0.7.0 → 0.9.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 +63 -0
- package/dist/chat.d.ts +52 -0
- package/dist/chat.js +68 -0
- package/dist/chatBubbles.d.ts +15 -0
- package/dist/chatBubbles.js +45 -0
- package/dist/components.d.ts +1 -0
- package/dist/components.js +35 -22
- package/dist/display.d.ts +14 -0
- package/dist/display.js +45 -0
- package/dist/dragLayer.d.ts +6 -1
- package/dist/dragLayer.js +64 -31
- package/dist/engineStore.d.ts +1 -1
- package/dist/engineStore.js +10 -2
- package/dist/fogOverlay.d.ts +21 -0
- package/dist/fogOverlay.js +34 -0
- package/dist/gameIcons.d.ts +12 -0
- package/dist/gameIcons.js +282 -0
- package/dist/hooks.d.ts +59 -2
- package/dist/hooks.js +170 -12
- package/dist/hudLayout.d.ts +61 -0
- package/dist/hudLayout.js +488 -0
- package/dist/hudViewport.d.ts +21 -0
- package/dist/hudViewport.js +17 -0
- package/dist/identity.d.ts +65 -0
- package/dist/identity.js +94 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/liveBind.d.ts +4 -10
- package/dist/liveBind.js +28 -16
- package/dist/map.d.ts +27 -7
- package/dist/map.js +152 -43
- package/dist/preview.d.ts +6 -0
- package/dist/preview.js +1 -0
- package/dist/selectSnapshot.d.ts +7 -0
- package/dist/selectSnapshot.js +16 -0
- package/dist/settings.d.ts +75 -0
- package/dist/settings.js +61 -0
- package/dist/skillCheckPaint.d.ts +4 -0
- package/dist/skillCheckPaint.js +28 -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 +1633 -0
- package/package.json +11 -6
package/dist/hooks.js
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
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";
|
|
5
|
+
import { gamePhase, setGamePhase } from "@jgengine/core/game/gamePhase";
|
|
3
6
|
import { useGameContext } from "./provider.js";
|
|
4
|
-
|
|
7
|
+
import { createSelectCache, readSelectSnapshot } from "./selectSnapshot.js";
|
|
8
|
+
export function useGameStore(selector, isEqual = Object.is) {
|
|
5
9
|
const ctx = useGameContext();
|
|
6
|
-
|
|
7
|
-
|
|
10
|
+
const selectorRef = useRef(selector);
|
|
11
|
+
selectorRef.current = selector;
|
|
12
|
+
const isEqualRef = useRef(isEqual);
|
|
13
|
+
isEqualRef.current = isEqual;
|
|
14
|
+
const cacheRef = useRef(createSelectCache());
|
|
15
|
+
const getSnapshot = useCallback(() => readSelectSnapshot(cacheRef.current, ctx.version(), () => selectorRef.current(ctx), (previous, next) => isEqualRef.current(previous, next)), [ctx]);
|
|
16
|
+
return useSyncExternalStore(ctx.subscribe, getSnapshot, getSnapshot);
|
|
8
17
|
}
|
|
9
18
|
export function useGame() {
|
|
10
19
|
const ctx = useGameContext();
|
|
@@ -14,6 +23,13 @@ export function usePlayer() {
|
|
|
14
23
|
const ctx = useGameContext();
|
|
15
24
|
return useMemo(() => ({ userId: ctx.player.userId, isNew: ctx.player.isNew }), [ctx]);
|
|
16
25
|
}
|
|
26
|
+
/** Live run phase + a setter that also gates the shell's touch controls. `menu`/`paused`/`ended` hide the touch dock; `playing` shows it. */
|
|
27
|
+
export function useGamePhase() {
|
|
28
|
+
const ctx = useGameContext();
|
|
29
|
+
const phase = useGameStore((c) => gamePhase(c));
|
|
30
|
+
const setPhase = useCallback((next) => setGamePhase(ctx, next), [ctx]);
|
|
31
|
+
return { phase, setPhase };
|
|
32
|
+
}
|
|
17
33
|
export function useSceneEntities() {
|
|
18
34
|
return useGameStore((ctx) => ctx.scene.entity.list());
|
|
19
35
|
}
|
|
@@ -60,6 +76,63 @@ export function useParty() {
|
|
|
60
76
|
export function usePresence(userId) {
|
|
61
77
|
return useGameStore((ctx) => ctx.game.social.presence.get(userId));
|
|
62
78
|
}
|
|
79
|
+
export function useWorldInvites() {
|
|
80
|
+
return useGameStore((ctx) => ctx.game.social.worldInvites.listFor(ctx.player.userId));
|
|
81
|
+
}
|
|
82
|
+
export function useChat(channelId, options) {
|
|
83
|
+
const limit = options?.limit ?? 50;
|
|
84
|
+
return useGameStore((ctx) => ctx.game.chat.history(channelId, { limit, viewerUserId: ctx.player.userId }));
|
|
85
|
+
}
|
|
86
|
+
export function useFriendRequests() {
|
|
87
|
+
return useGameStore((ctx) => ctx.game.social.friends.requestsFor(ctx.player.userId));
|
|
88
|
+
}
|
|
89
|
+
export function usePartyInvites() {
|
|
90
|
+
return useGameStore((ctx) => ctx.game.social.party.invitesFor(ctx.player.userId));
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Polls a host-supplied session fetcher (e.g. createWsBackend().browse) and
|
|
94
|
+
* filters through matchmaking's browseSessions. fetchSessions must be
|
|
95
|
+
* identity-stable (wrap in useCallback at the call site) or every render
|
|
96
|
+
* refetches.
|
|
97
|
+
*/
|
|
98
|
+
export function useWorldBrowser(options) {
|
|
99
|
+
const { fetchSessions, refreshMs } = options;
|
|
100
|
+
const [raw, setRaw] = useState([]);
|
|
101
|
+
const [loading, setLoading] = useState(true);
|
|
102
|
+
const [error, setError] = useState(null);
|
|
103
|
+
const [refreshTick, setRefreshTick] = useState(0);
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
let cancelled = false;
|
|
106
|
+
setLoading(true);
|
|
107
|
+
fetchSessions()
|
|
108
|
+
.then((listings) => {
|
|
109
|
+
if (cancelled)
|
|
110
|
+
return;
|
|
111
|
+
setRaw(listings);
|
|
112
|
+
setError(null);
|
|
113
|
+
setLoading(false);
|
|
114
|
+
})
|
|
115
|
+
.catch((cause) => {
|
|
116
|
+
if (cancelled)
|
|
117
|
+
return;
|
|
118
|
+
setError(cause instanceof Error ? cause.message : "failed to browse sessions");
|
|
119
|
+
setLoading(false);
|
|
120
|
+
});
|
|
121
|
+
return () => {
|
|
122
|
+
cancelled = true;
|
|
123
|
+
};
|
|
124
|
+
}, [fetchSessions, refreshTick]);
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (refreshMs === undefined || refreshMs <= 0)
|
|
127
|
+
return undefined;
|
|
128
|
+
const id = setInterval(() => setRefreshTick((tick) => tick + 1), refreshMs);
|
|
129
|
+
return () => clearInterval(id);
|
|
130
|
+
}, [refreshMs]);
|
|
131
|
+
const filter = options.filter;
|
|
132
|
+
const limit = options.limit;
|
|
133
|
+
const listings = useMemo(() => browseSessions(raw, filter, limit === undefined ? {} : { limit }), [raw, filter, limit]);
|
|
134
|
+
return useMemo(() => ({ listings, loading, error, refresh: () => setRefreshTick((tick) => tick + 1) }), [listings, loading, error]);
|
|
135
|
+
}
|
|
63
136
|
export function useRoster(userId) {
|
|
64
137
|
return useGameStore((ctx) => ctx.game.roster.list(userId ?? ctx.player.userId));
|
|
65
138
|
}
|
|
@@ -95,26 +168,111 @@ export function useActivePrompt(prompts) {
|
|
|
95
168
|
return resolveActivePrompt({ x: player.position[0], z: player.position[2] }, prompts);
|
|
96
169
|
});
|
|
97
170
|
}
|
|
98
|
-
function
|
|
99
|
-
const
|
|
100
|
-
|
|
171
|
+
export function abilityKitNeedsHeartbeat(kit, resourceAvailable) {
|
|
172
|
+
for (const slot of kit.snapshot(resourceAvailable)) {
|
|
173
|
+
if (slot.cooldownRemainingMs > 0 || slot.justCast || slot.groupRemainingMs > 0)
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
export function eventMeterNeedsHeartbeat(meter, previous) {
|
|
179
|
+
const next = {
|
|
180
|
+
value: meter.value(),
|
|
181
|
+
fraction: meter.fraction(),
|
|
182
|
+
tier: meter.tier(),
|
|
183
|
+
ready: meter.ready(),
|
|
184
|
+
};
|
|
185
|
+
if (previous === null)
|
|
186
|
+
return true;
|
|
187
|
+
return (previous.value !== next.value ||
|
|
188
|
+
previous.fraction !== next.fraction ||
|
|
189
|
+
previous.tier !== next.tier ||
|
|
190
|
+
previous.ready !== next.ready);
|
|
191
|
+
}
|
|
192
|
+
function useEngineHeartbeat(intervalMs, shouldTick) {
|
|
101
193
|
const [, setTick] = useState(0);
|
|
194
|
+
const shouldTickRef = useRef(shouldTick);
|
|
195
|
+
shouldTickRef.current = shouldTick;
|
|
102
196
|
useEffect(() => {
|
|
103
197
|
if (intervalMs <= 0 || typeof window === "undefined")
|
|
104
198
|
return undefined;
|
|
105
|
-
const id = window.setInterval(() =>
|
|
199
|
+
const id = window.setInterval(() => {
|
|
200
|
+
if (shouldTickRef.current())
|
|
201
|
+
setTick((current) => current + 1);
|
|
202
|
+
}, intervalMs);
|
|
106
203
|
return () => window.clearInterval(id);
|
|
107
204
|
}, [intervalMs]);
|
|
108
205
|
}
|
|
109
206
|
export function useAbilitySlots(kit, resourceAvailable, options) {
|
|
110
|
-
useEngineHeartbeat(options?.intervalMs ?? 80);
|
|
207
|
+
useEngineHeartbeat(options?.intervalMs ?? 80, () => abilityKitNeedsHeartbeat(kit, resourceAvailable));
|
|
111
208
|
return kit.snapshot(resourceAvailable);
|
|
112
209
|
}
|
|
113
210
|
export function useAbilitySlot(kit, slotId, resourceAvailable, options) {
|
|
114
|
-
useEngineHeartbeat(options?.intervalMs ?? 80);
|
|
211
|
+
useEngineHeartbeat(options?.intervalMs ?? 80, () => abilityKitNeedsHeartbeat(kit, resourceAvailable));
|
|
115
212
|
return kit.state(slotId, resourceAvailable);
|
|
116
213
|
}
|
|
117
214
|
export function useEventMeter(meter, options) {
|
|
118
|
-
|
|
119
|
-
|
|
215
|
+
const previous = useRef(null);
|
|
216
|
+
useEngineHeartbeat(options?.intervalMs ?? 80, () => {
|
|
217
|
+
const changed = eventMeterNeedsHeartbeat(meter, previous.current);
|
|
218
|
+
if (changed) {
|
|
219
|
+
previous.current = {
|
|
220
|
+
value: meter.value(),
|
|
221
|
+
fraction: meter.fraction(),
|
|
222
|
+
tier: meter.tier(),
|
|
223
|
+
ready: meter.ready(),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
return changed;
|
|
227
|
+
});
|
|
228
|
+
const view = { value: meter.value(), fraction: meter.fraction(), tier: meter.tier(), ready: meter.ready() };
|
|
229
|
+
previous.current = view;
|
|
230
|
+
return view;
|
|
231
|
+
}
|
|
232
|
+
export function createHeldKeyTracker(target) {
|
|
233
|
+
const held = new Set();
|
|
234
|
+
const onKeyDown = (event) => held.add(event.code);
|
|
235
|
+
const onKeyUp = (event) => held.delete(event.code);
|
|
236
|
+
const onBlur = () => held.clear();
|
|
237
|
+
target.addEventListener("keydown", onKeyDown);
|
|
238
|
+
target.addEventListener("keyup", onKeyUp);
|
|
239
|
+
target.addEventListener("blur", onBlur);
|
|
240
|
+
return {
|
|
241
|
+
isDown: (code) => held.has(code),
|
|
242
|
+
dispose: () => {
|
|
243
|
+
target.removeEventListener("keydown", onKeyDown);
|
|
244
|
+
target.removeEventListener("keyup", onKeyUp);
|
|
245
|
+
target.removeEventListener("blur", onBlur);
|
|
246
|
+
held.clear();
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Held-key predicate backed by window keydown/keyup/blur listeners (blur clears held state so a
|
|
252
|
+
* released-off-window key doesn't stick). SSR-safe: listeners attach in an effect, never at module
|
|
253
|
+
* scope. The returned predicate is stable across renders.
|
|
254
|
+
*/
|
|
255
|
+
export function useHeldKeys() {
|
|
256
|
+
const trackerRef = useRef(null);
|
|
257
|
+
useEffect(() => {
|
|
258
|
+
if (typeof window === "undefined")
|
|
259
|
+
return undefined;
|
|
260
|
+
const tracker = createHeldKeyTracker(window);
|
|
261
|
+
trackerRef.current = tracker;
|
|
262
|
+
return () => {
|
|
263
|
+
tracker.dispose();
|
|
264
|
+
trackerRef.current = null;
|
|
265
|
+
};
|
|
266
|
+
}, []);
|
|
267
|
+
return useCallback((code) => trackerRef.current?.isDown(code) ?? false, []);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Wires useHeldKeys into a fresh AxisChannel, ready for a per-frame `channel.sample(dt, isDown)`.
|
|
271
|
+
* The channel is recreated when `config` identity changes, so pass a stable config (useMemo/module
|
|
272
|
+
* constant at the call site) unless a rebind is intended.
|
|
273
|
+
*/
|
|
274
|
+
export function useAxisChannel(config) {
|
|
275
|
+
const isDown = useHeldKeys();
|
|
276
|
+
const channel = useMemo(() => new AxisChannel(config), [config]);
|
|
277
|
+
return { channel, isDown };
|
|
120
278
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode } from "react";
|
|
2
|
+
import { type HudAnchor, type HudLayoutStore } from "@jgengine/core/ui/hudLayout";
|
|
3
|
+
/**
|
|
4
|
+
* How a panel behaves on compact (phone-scale) displays. `keep` stays visible
|
|
5
|
+
* at the global compact scale, `chip` collapses to a small tap-to-expand
|
|
6
|
+
* pill, `hide` unmounts entirely.
|
|
7
|
+
*/
|
|
8
|
+
export type HudCompactMode = "keep" | "chip" | "hide";
|
|
9
|
+
export interface HudEditChord {
|
|
10
|
+
hold: string;
|
|
11
|
+
press: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function useHudLayout(options?: {
|
|
14
|
+
storageKey?: string;
|
|
15
|
+
snap?: number;
|
|
16
|
+
locked?: boolean;
|
|
17
|
+
}): HudLayoutStore;
|
|
18
|
+
/**
|
|
19
|
+
* Full-viewport HUD surface. Panels declared with `HudPanel` flow into nine
|
|
20
|
+
* anchor regions and stack automatically with a gap — no per-panel pixel
|
|
21
|
+
* offsets, no manual clearance for sibling panels, the touch-control dock
|
|
22
|
+
* (`--jg-hud-dock-clearance`), or device safe areas. On compact displays the
|
|
23
|
+
* whole surface scales down and each panel applies its `compact` behavior.
|
|
24
|
+
*/
|
|
25
|
+
export declare function HudCanvas({ layout, editChord, compactScale, className, style, children, }: {
|
|
26
|
+
layout: HudLayoutStore;
|
|
27
|
+
editChord?: HudEditChord | false;
|
|
28
|
+
/** Zoom applied to the whole HUD on compact displays. Default 0.85. */
|
|
29
|
+
compactScale?: number;
|
|
30
|
+
className?: string;
|
|
31
|
+
style?: CSSProperties;
|
|
32
|
+
children?: ReactNode;
|
|
33
|
+
}): import("react").JSX.Element;
|
|
34
|
+
/**
|
|
35
|
+
* A HUD block that lives in one of the nine anchor regions. Panels sharing a
|
|
36
|
+
* region stack outward from the screen edge in ascending `order`. On fine
|
|
37
|
+
* pointers panels stay draggable through the edit chord; a dragged panel
|
|
38
|
+
* leaves the flow and keeps its custom placement. On compact displays custom
|
|
39
|
+
* placements are ignored and the `compact` behavior applies.
|
|
40
|
+
*/
|
|
41
|
+
export declare function HudPanel({ id, anchor, order, compact: compactMode, chip, interactive, inset, locked, className, style, children, }: {
|
|
42
|
+
id: string;
|
|
43
|
+
anchor?: HudAnchor;
|
|
44
|
+
/** Stack position within the region, ascending outward from the screen edge. Default 0. */
|
|
45
|
+
order?: number;
|
|
46
|
+
/** Behavior on compact displays. Default `"keep"`. */
|
|
47
|
+
compact?: HudCompactMode;
|
|
48
|
+
/** Chip label when `compact="chip"`. Defaults to the panel id. */
|
|
49
|
+
chip?: string;
|
|
50
|
+
/** `false` lets pointer events pass through to the game (read-only panels). Default true. */
|
|
51
|
+
interactive?: boolean;
|
|
52
|
+
/** Legacy pixel inset from the anchor; only used as the reset placement for dragged panels. */
|
|
53
|
+
inset?: {
|
|
54
|
+
x: number;
|
|
55
|
+
y: number;
|
|
56
|
+
};
|
|
57
|
+
locked?: boolean;
|
|
58
|
+
className?: string;
|
|
59
|
+
style?: CSSProperties;
|
|
60
|
+
children?: ReactNode;
|
|
61
|
+
}): import("react").JSX.Element | null;
|