@jgengine/shell 0.6.0 → 0.7.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.
Files changed (77) hide show
  1. package/CHANGELOG.md +167 -2
  2. package/dist/GamePlayerShell.js +210 -39
  3. package/dist/audio/AudioComponents.d.ts +12 -0
  4. package/dist/audio/AudioComponents.js +53 -0
  5. package/dist/audio/audioEngine.d.ts +23 -0
  6. package/dist/audio/audioEngine.js +110 -0
  7. package/dist/camera/GameCameraRig.d.ts +11 -0
  8. package/dist/camera/GameCameraRig.js +42 -0
  9. package/dist/camera/GameOrbitCamera.d.ts +3 -1
  10. package/dist/camera/GameOrbitCamera.js +4 -2
  11. package/dist/camera/cameraRigs.d.ts +22 -0
  12. package/dist/camera/cameraRigs.js +485 -0
  13. package/dist/camera/index.d.ts +5 -0
  14. package/dist/camera/index.js +5 -0
  15. package/dist/camera/rigMath.d.ts +130 -0
  16. package/dist/camera/rigMath.js +338 -0
  17. package/dist/camera/shakeChannel.d.ts +29 -0
  18. package/dist/camera/shakeChannel.js +38 -0
  19. package/dist/demo/builderDemo.d.ts +2 -0
  20. package/dist/demo/builderDemo.js +189 -0
  21. package/dist/demo/mapDemo.d.ts +2 -0
  22. package/dist/demo/mapDemo.js +206 -0
  23. package/dist/demo/pointerDemo.d.ts +2 -0
  24. package/dist/demo/pointerDemo.js +151 -0
  25. package/dist/demo/sensorShowcase.d.ts +2 -0
  26. package/dist/demo/sensorShowcase.js +131 -0
  27. package/dist/demo/survivalDemo.d.ts +2 -0
  28. package/dist/demo/survivalDemo.js +291 -0
  29. package/dist/map/MapMarkerBeacons.d.ts +12 -0
  30. package/dist/map/MapMarkerBeacons.js +16 -0
  31. package/dist/map/index.d.ts +2 -0
  32. package/dist/map/index.js +2 -0
  33. package/dist/map/terrainMap.d.ts +23 -0
  34. package/dist/map/terrainMap.js +79 -0
  35. package/dist/pointer/PointerOverlays.d.ts +12 -0
  36. package/dist/pointer/PointerOverlays.js +17 -0
  37. package/dist/pointer/PointerProbe.d.ts +4 -0
  38. package/dist/pointer/PointerProbe.js +27 -0
  39. package/dist/pointer/pointerService.d.ts +24 -0
  40. package/dist/pointer/pointerService.js +68 -0
  41. package/dist/replay/useSessionRecorder.d.ts +14 -0
  42. package/dist/replay/useSessionRecorder.js +22 -0
  43. package/dist/structures/PlacementGhost.d.ts +8 -0
  44. package/dist/structures/PlacementGhost.js +11 -0
  45. package/dist/structures/index.d.ts +2 -0
  46. package/dist/structures/index.js +2 -0
  47. package/dist/structures.d.ts +1 -0
  48. package/dist/structures.js +1 -0
  49. package/dist/terrain/CarvedTerrain.d.ts +22 -0
  50. package/dist/terrain/CarvedTerrain.js +19 -0
  51. package/dist/terrain/EditableGround.d.ts +11 -0
  52. package/dist/terrain/EditableGround.js +38 -0
  53. package/dist/terrain/TerraformBrushCursor.d.ts +8 -0
  54. package/dist/terrain/TerraformBrushCursor.js +13 -0
  55. package/dist/terrain/index.d.ts +4 -1
  56. package/dist/terrain/index.js +4 -1
  57. package/dist/terrain/terrainMath.d.ts +14 -1
  58. package/dist/terrain/terrainMath.js +80 -0
  59. package/dist/vision/FrustumSensorHud.d.ts +21 -0
  60. package/dist/vision/FrustumSensorHud.js +67 -0
  61. package/dist/vision/HiddenStateProbeHud.d.ts +11 -0
  62. package/dist/vision/HiddenStateProbeHud.js +11 -0
  63. package/dist/vision/RevealVision.d.ts +28 -0
  64. package/dist/vision/RevealVision.js +53 -0
  65. package/dist/weather/FireSpreadLayer.d.ts +14 -0
  66. package/dist/weather/FireSpreadLayer.js +68 -0
  67. package/dist/weather/index.d.ts +1 -0
  68. package/dist/weather/index.js +1 -0
  69. package/dist/world/InstancedJoints.d.ts +14 -0
  70. package/dist/world/InstancedJoints.js +32 -0
  71. package/dist/world/WorldHud.d.ts +2 -0
  72. package/dist/world/WorldHud.js +70 -12
  73. package/dist/world/WorldItems.d.ts +5 -0
  74. package/dist/world/WorldItems.js +31 -0
  75. package/dist/world/floatTextStyle.d.ts +14 -0
  76. package/dist/world/floatTextStyle.js +39 -0
  77. package/package.json +8 -4
@@ -1,18 +1,27 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Canvas, useFrame, useLoader } from "@react-three/fiber";
3
- import { Component, useEffect, useMemo, useRef, useState } from "react";
3
+ import { Component, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
4
4
  import * as THREE from "three";
5
5
  import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
6
6
  import { createActionStateTracker, hotbarSlotActionIndex, resolveActionCommand, toActionStateBindingMap, } from "@jgengine/core/input/actionBindings";
7
+ import { buildContextMenu, contextVerbInput, } from "@jgengine/core/interaction/contextMenu";
7
8
  import { resolveActivePrompt } from "@jgengine/core/interaction/proximityPrompt";
9
+ import { aimToPoint } from "@jgengine/core/input/pointer";
10
+ import { createSelectionSet, isMarquee, screenRect, selectWithinRect, } from "@jgengine/core/scene/selection";
8
11
  import { advancePlayerMotion, createEmptyMovementKeys, createPlayerMotionState, resolveMovementIntent, } from "@jgengine/core/movement/movementModel";
9
12
  import { createGameContext } from "@jgengine/core/runtime/gameContext";
13
+ import { DEFAULT_PICKUP_RADIUS, WORLD_ITEM_ENTITY_NAME } from "@jgengine/core/game/worldItem";
10
14
  import { useGameContext } from "@jgengine/react/provider";
11
15
  import { useSceneEntities, useSceneObjects, usePlayer, useTarget } from "@jgengine/react/hooks";
12
16
  import { GameProvider } from "@jgengine/react/provider";
13
- import { GAME_SIM_FRAME_PRIORITY, GameOrbitCamera } from "./camera/index.js";
14
- import { GameFirstPersonCamera } from "./camera/GameFirstPersonCamera.js";
15
- import { ProjectileTracers, Reticle, WorldEntityBars, WorldFloatText } from "./world/WorldHud.js";
17
+ import { AudioListener, EntityAudioEmitters, ObjectAudioEmitters } from "./audio/AudioComponents.js";
18
+ import { createAudioEngine } from "./audio/audioEngine.js";
19
+ import { GAME_SIM_FRAME_PRIORITY, GameCameraRig, resolveRigKind } from "./camera/index.js";
20
+ import { PointerProbe } from "./pointer/PointerProbe.js";
21
+ import { MarqueeBox, ContextMenuView } from "./pointer/PointerOverlays.js";
22
+ import { createPointerService, POINTER_ENTITY_KEY, POINTER_OBJECT_KEY, } from "./pointer/pointerService.js";
23
+ import { CombatCameraShake, ProjectileTracers, Reticle, WorldEntityBars, WorldFloatText, WorldTelegraphs, } from "./world/WorldHud.js";
24
+ import { WorldItems } from "./world/WorldItems.js";
16
25
  const DEV_USER_ID = "dev-player";
17
26
  const TURN_SPEED = 2.4;
18
27
  const PRIMARY_CLICK_MOVE_THRESHOLD_PX = 6;
@@ -54,18 +63,38 @@ function hotbarIdFor(playable) {
54
63
  const hud = declarations.find(([, declaration]) => declaration.hud === "hotbar");
55
64
  return (hud ?? declarations[0])?.[0] ?? null;
56
65
  }
57
- function executeHotbarSlot(ctx, hotbarId, slot, yaw, pitch) {
66
+ function executeHotbarSlot(ctx, fromId, hotbarId, slot, yaw, pitch, aimOverride) {
58
67
  const stack = ctx.player.inventory.state(hotbarId).slots[slot];
59
68
  if (stack === undefined || stack === null)
60
69
  return { ok: false, error: `Hotbar slot ${slot + 1} is empty` };
61
70
  const result = ctx.item.use.use({
62
- from: ctx.player.userId,
71
+ from: fromId,
63
72
  itemId: stack.itemId,
64
73
  inventoryId: hotbarId,
65
- aim: { yaw, pitch },
74
+ aim: aimOverride ?? { yaw, pitch },
66
75
  });
67
76
  return result.error === undefined ? { ok: true } : { ok: false, error: result.error };
68
77
  }
78
+ function pointerAimFor(ctx, service) {
79
+ const hit = service.worldHit();
80
+ if (hit === null)
81
+ return undefined;
82
+ const player = ctx.scene.entity.get(ctx.player.userId);
83
+ const origin = player === null ? hit.point : player.position;
84
+ return aimToPoint([origin[0], origin[1] + 1, origin[2]], hit.point);
85
+ }
86
+ function pointerContextMenu(ctx, playable, hit) {
87
+ if (hit.entity !== null) {
88
+ const entity = ctx.scene.entity.get(hit.entity);
89
+ const verbs = entity === null ? undefined : playable.content.entityById?.(entity.name)?.verbs;
90
+ return buildContextMenu({ kind: "entity", targetId: hit.entity, verbs, point: hit.point });
91
+ }
92
+ if (hit.object !== null) {
93
+ const verbs = ctx.scene.object.catalog(hit.object)?.verbs;
94
+ return buildContextMenu({ kind: "object", targetId: hit.object, verbs, point: hit.point });
95
+ }
96
+ return null;
97
+ }
69
98
  function colorFromId(id) {
70
99
  let hash = 0;
71
100
  for (let index = 0; index < id.length; index += 1)
@@ -103,13 +132,13 @@ function resolveModel(value, assets) {
103
132
  return undefined;
104
133
  return ref.dims === undefined ? { url: ref.url } : { url: ref.url, dims: ref.dims };
105
134
  }
106
- function EntityMarker({ entity, custom, model, sprite, isLocal, targeted, onSelect, }) {
135
+ function EntityMarker({ entity, custom, model, sprite, isLocal, targeted, selected, onSelect, }) {
107
136
  const color = isLocal ? "#4ade80" : entity.role === "npc" ? colorFromId(entity.name) : "#9ca3af";
108
- return (_jsxs("group", { position: [entity.position[0], entity.position[1], entity.position[2]], "rotation-y": entity.rotationY, onPointerDown: (event) => {
137
+ return (_jsxs("group", { position: [entity.position[0], entity.position[1], entity.position[2]], "rotation-y": entity.rotationY, userData: { [POINTER_ENTITY_KEY]: entity.id }, onPointerDown: (event) => {
109
138
  event.stopPropagation();
110
139
  if (!isLocal)
111
140
  onSelect(entity);
112
- }, children: [custom !== undefined && custom !== null ? (custom) : model !== undefined ? (_jsx(EntityModel, { model: model })) : sprite !== undefined ? (_jsx(EntitySprite, { sprite: sprite })) : entity.role === "prop" ? (_jsxs("mesh", { "position-y": 0.5, children: [_jsx("sphereGeometry", { args: [0.45, 16, 16] }), _jsx("meshStandardMaterial", { color: color })] })) : (_jsxs(_Fragment, { children: [_jsxs("mesh", { "position-y": 0.95, children: [_jsx("capsuleGeometry", { args: [0.35, 1.1, 6, 14] }), _jsx("meshStandardMaterial", { color: color })] }), _jsxs("mesh", { position: [0, 1.35, 0.32], children: [_jsx("boxGeometry", { args: [0.16, 0.16, 0.16] }), _jsx("meshStandardMaterial", { color: "#f8fafc" })] })] })), targeted ? (_jsxs("mesh", { "rotation-x": -Math.PI / 2, "position-y": 0.03, children: [_jsx("ringGeometry", { args: [0.6, 0.75, 28] }), _jsx("meshBasicMaterial", { color: "#f87171" })] })) : null] }));
141
+ }, children: [selected ? (_jsxs("mesh", { "rotation-x": -Math.PI / 2, "position-y": 0.02, children: [_jsx("ringGeometry", { args: [0.8, 0.95, 32] }), _jsx("meshBasicMaterial", { color: "#34d399", transparent: true, opacity: 0.9 })] })) : null, custom !== undefined && custom !== null ? (custom) : model !== undefined ? (_jsx(EntityModel, { model: model })) : sprite !== undefined ? (_jsx(EntitySprite, { sprite: sprite })) : entity.role === "prop" ? (_jsxs("mesh", { "position-y": 0.5, children: [_jsx("sphereGeometry", { args: [0.45, 16, 16] }), _jsx("meshStandardMaterial", { color: color })] })) : (_jsxs(_Fragment, { children: [_jsxs("mesh", { "position-y": 0.95, children: [_jsx("capsuleGeometry", { args: [0.35, 1.1, 6, 14] }), _jsx("meshStandardMaterial", { color: color })] }), _jsxs("mesh", { position: [0, 1.35, 0.32], children: [_jsx("boxGeometry", { args: [0.16, 0.16, 0.16] }), _jsx("meshStandardMaterial", { color: "#f8fafc" })] })] })), targeted ? (_jsxs("mesh", { "rotation-x": -Math.PI / 2, "position-y": 0.03, children: [_jsx("ringGeometry", { args: [0.6, 0.75, 28] }), _jsx("meshBasicMaterial", { color: "#f87171" })] })) : null] }));
113
142
  }
114
143
  function GroundPlane() {
115
144
  const geometry = useMemo(() => {
@@ -142,26 +171,29 @@ function RockField() {
142
171
  }), []);
143
172
  return (_jsx(_Fragment, { children: rocks.map((rock) => (_jsxs("mesh", { position: [rock.x, 0.25 * rock.scale, rock.z], rotation: [0.1, rock.rotation, -0.08], scale: [rock.scale * 1.4, rock.scale * 0.7, rock.scale], children: [_jsx("dodecahedronGeometry", { args: [0.8, 0] }), _jsx("meshStandardMaterial", { color: "#6b6f63", roughness: 1 })] }, rock.id))) }));
144
173
  }
145
- function WorldView({ entitySprites, entityModels, objectModels, environment: Environment, assets, renderEntity, }) {
174
+ function WorldView({ entitySprites, entityModels, objectModels, environment: Environment, assets, renderEntity, selectedIds, }) {
146
175
  const ctx = useGameContext();
147
176
  const entities = useSceneEntities();
148
177
  const objects = useSceneObjects();
149
178
  const player = usePlayer();
150
179
  const targetId = useTarget(player.userId);
180
+ const controlledId = ctx.player.possession.active(player.userId);
151
181
  const handleSelect = (entity) => {
152
182
  const relation = ctx.scene.entity.canReceive(entity.id, "damage") === null ? "hostile" : "friendly";
153
- ctx.scene.entity.setTarget(player.userId, relation === "hostile" || entity.role === "npc" ? entity.id : null);
183
+ ctx.scene.entity.setTarget(controlledId, relation === "hostile" || entity.role === "npc" ? entity.id : null);
154
184
  };
155
- return (_jsxs(_Fragment, { children: [Environment !== undefined ? (_jsx(Environment, {})) : (_jsxs(_Fragment, { children: [_jsx(GroundPlane, {}), _jsx("gridHelper", { args: [160, 80, "#3a3f4a", "#2b2f38"], "position-y": 0.01 }), _jsx(RockField, {})] })), entities.map((entity) => (_jsx(EntityMarker, { entity: entity, custom: renderEntity?.(entity), model: resolveModel(entityModels?.[entity.name], assets), sprite: entitySprites?.[entity.name], isLocal: entity.id === player.userId, targeted: entity.id === targetId, onSelect: handleSelect }, entity.id))), objects.map((object) => {
185
+ return (_jsxs(_Fragment, { children: [Environment !== undefined ? (_jsx(Environment, {})) : (_jsxs(_Fragment, { children: [_jsx(GroundPlane, {}), _jsx("gridHelper", { args: [160, 80, "#3a3f4a", "#2b2f38"], "position-y": 0.01 }), _jsx(RockField, {})] })), entities
186
+ .filter((entity) => entity.name !== WORLD_ITEM_ENTITY_NAME)
187
+ .map((entity) => (_jsx(EntityMarker, { entity: entity, custom: renderEntity?.(entity), model: resolveModel(entityModels?.[entity.name], assets), sprite: entitySprites?.[entity.name], isLocal: entity.id === controlledId, targeted: entity.id === targetId, selected: selectedIds.has(entity.id), onSelect: handleSelect }, entity.id))), objects.map((object) => {
156
188
  const model = resolveModel(objectModels?.[object.catalogId], assets) ??
157
189
  resolveModel(object.catalogId, assets);
158
- return (_jsx("group", { position: [object.position[0], object.position[1], object.position[2]], "rotation-y": object.rotationY, children: model !== undefined ? (_jsx(EntityModel, { model: model })) : (_jsxs("mesh", { "position-y": 0.5, children: [_jsx("boxGeometry", { args: [1, 1, 1] }), _jsx("meshStandardMaterial", { color: colorFromId(object.catalogId) })] })) }, object.instanceId));
190
+ return (_jsx("group", { position: [object.position[0], object.position[1], object.position[2]], "rotation-y": object.rotationY, userData: { [POINTER_OBJECT_KEY]: object.instanceId }, children: model !== undefined ? (_jsx(EntityModel, { model: model })) : (_jsxs("mesh", { "position-y": 0.5, children: [_jsx("boxGeometry", { args: [1, 1, 1] }), _jsx("meshStandardMaterial", { color: colorFromId(object.catalogId) })] })) }, object.instanceId));
159
191
  })] }));
160
192
  }
161
193
  function RemotePlayers({ rows }) {
162
194
  return (_jsx(_Fragment, { children: rows.map((row) => (_jsxs("group", { position: [row.position.x, row.position.y, row.position.z], "rotation-y": row.rotationY, children: [_jsxs("mesh", { "position-y": 0.95, children: [_jsx("capsuleGeometry", { args: [0.35, 1.1, 6, 14] }), _jsx("meshStandardMaterial", { color: colorFromId(row.userId) })] }), _jsxs("mesh", { position: [0, 1.35, 0.32], children: [_jsx("boxGeometry", { args: [0.16, 0.16, 0.16] }), _jsx("meshStandardMaterial", { color: "#f8fafc" })] })] }, row.userId))) }));
163
195
  }
164
- function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef, onRuntimeError, multiplayer, serverIdRef, }) {
196
+ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef, onRuntimeError, multiplayer, serverIdRef, pointerService, pointerAim, pingCommand, }) {
165
197
  const motionRef = useRef(createPlayerMotionState());
166
198
  const hasReportedTickError = useRef(false);
167
199
  const slotActions = useMemo(() => findHotbarSlotActions(playable.game.input), [playable]);
@@ -174,7 +206,7 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
174
206
  yawRef.current += TURN_SPEED * dt;
175
207
  if (tracker.isDown("turnRight"))
176
208
  yawRef.current -= TURN_SPEED * dt;
177
- const playerId = ctx.player.userId;
209
+ const playerId = ctx.player.possession.active(ctx.player.userId);
178
210
  const player = ctx.scene.entity.get(playerId);
179
211
  const forwardX = Math.sin(yawRef.current);
180
212
  const forwardZ = Math.cos(yawRef.current);
@@ -210,9 +242,22 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
210
242
  else
211
243
  ctx.scene.entity.setTarget(playerId, null);
212
244
  }
245
+ if (pingCommand !== undefined && tracker.wasPressed("ping")) {
246
+ const hit = pointerService.worldHit();
247
+ if (hit !== null && ctx.game.commands.has(pingCommand)) {
248
+ ctx.game.commands.run(pingCommand, {
249
+ point: hit.point,
250
+ entity: hit.entity,
251
+ object: hit.object,
252
+ normal: hit.normal,
253
+ });
254
+ }
255
+ }
213
256
  for (const action of Object.keys(playable.game.input ?? {})) {
214
257
  if (!tracker.wasPressed(action))
215
258
  continue;
259
+ if (action === "ping" && pingCommand !== undefined)
260
+ continue;
216
261
  if (action === "interact") {
217
262
  const prompts = playable.prompts?.(ctx);
218
263
  const focus = prompts === undefined ? null : ctx.scene.entity.get(playerId);
@@ -229,10 +274,11 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
229
274
  ctx.game.commands.run(command, {});
230
275
  }
231
276
  if (hotbarId !== null) {
277
+ const aimOverride = pointerAim ? pointerAimFor(ctx, pointerService) : undefined;
232
278
  for (const { action, slot } of slotActions) {
233
279
  if (!tracker.wasPressed(action))
234
280
  continue;
235
- const result = executeHotbarSlot(ctx, hotbarId, slot, yawRef.current, pitchRef.current);
281
+ const result = executeHotbarSlot(ctx, playerId, hotbarId, slot, yawRef.current, pitchRef.current, aimOverride);
236
282
  if (!result.ok)
237
283
  console.warn(`[jgengine:item-use] ${result.error}`);
238
284
  }
@@ -245,7 +291,7 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
245
291
  ? preferred
246
292
  : slots.findIndex((stack) => stack !== null);
247
293
  if (slot >= 0) {
248
- const result = executeHotbarSlot(ctx, hotbarId, slot, yawRef.current, pitchRef.current);
294
+ const result = executeHotbarSlot(ctx, playerId, hotbarId, slot, yawRef.current, pitchRef.current, aimOverride);
249
295
  if (!result.ok)
250
296
  console.warn(`[jgengine:item-use] ${result.error}`);
251
297
  }
@@ -306,7 +352,15 @@ export function GamePlayerShell({ playable, multiplayer = null, }) {
306
352
  const cameraDraggingRef = useRef(false);
307
353
  const primaryClickRef = useRef(false);
308
354
  const pointerDownRef = useRef(null);
355
+ const marqueeStartRef = useRef(null);
356
+ const pointerService = useMemo(() => createPointerService(), []);
357
+ const selection = useMemo(() => createSelectionSet(), [playable]);
358
+ const [marquee, setMarquee] = useState(null);
359
+ const [selectedIds, setSelectedIds] = useState(() => new Set());
360
+ const [contextMenu, setContextMenu] = useState(null);
309
361
  const tracker = useMemo(() => createActionStateTracker(toActionStateBindingMap(playable.game.input ?? {})), [playable]);
362
+ const audioEngine = useMemo(() => createAudioEngine({ sounds: playable.audio?.sounds, buses: playable.audio?.buses }), [playable]);
363
+ useEffect(() => () => audioEngine.dispose(), [audioEngine]);
310
364
  const userId = multiplayer?.userId ?? DEV_USER_ID;
311
365
  const reportRuntimeError = (error, phase) => {
312
366
  const diagnostic = logRuntimeError(error, phase);
@@ -398,39 +452,156 @@ export function GamePlayerShell({ playable, multiplayer = null, }) {
398
452
  useEffect(() => {
399
453
  wrapperRef.current?.focus();
400
454
  }, [ctx]);
455
+ useSyncExternalStore(ctx?.subscribe ?? (() => () => undefined), ctx?.version ?? (() => 0), ctx?.version ?? (() => 0));
401
456
  if (ctx === null)
402
457
  return _jsx("div", { className: "h-full w-full bg-neutral-950" });
403
458
  const GameUI = playable.GameUI;
404
459
  const WorldOverlay = playable.WorldOverlay;
405
- const firstPerson = playable.camera?.perspective === "first";
406
- const showReticle = firstPerson && playable.camera?.firstPerson?.reticle !== false;
460
+ const controlledEntityId = ctx.player.possession.active(userId);
461
+ const cameraConfig = playable.camera?.followEntityId !== undefined
462
+ ? playable.camera
463
+ : { ...playable.camera, followEntityId: controlledEntityId };
464
+ const rigKind = resolveRigKind(cameraConfig);
465
+ const firstPerson = rigKind === "first";
466
+ const showReticle = (firstPerson && playable.camera?.firstPerson?.reticle !== false) || rigKind === "shoulder";
407
467
  const worldBars = playable.worldHealthBars;
408
468
  const barsStatId = worldBars === undefined || worldBars === false
409
469
  ? null
410
470
  : worldBars === true
411
471
  ? "health"
412
472
  : worldBars.statId ?? "health";
473
+ const pointer = playable.pointer;
474
+ const pointerUsesLeft = pointer !== undefined && (pointer.select === true || pointer.moveCommand !== undefined);
475
+ const selectFilter = pointer?.selectFilter;
476
+ const localXY = (event) => {
477
+ const rect = wrapperRef.current?.getBoundingClientRect();
478
+ return rect === undefined
479
+ ? { x: event.clientX, y: event.clientY }
480
+ : { x: event.clientX - rect.left, y: event.clientY - rect.top };
481
+ };
482
+ const commitSelection = (ids) => {
483
+ selection.replace(ids);
484
+ setSelectedIds(new Set(ids));
485
+ };
486
+ const finishMarquee = (rect) => {
487
+ const candidates = [];
488
+ for (const entity of ctx.scene.entity.list()) {
489
+ if (selectFilter !== undefined && !selectFilter(entity.id))
490
+ continue;
491
+ const screen = pointerService.screenOf(entity.position);
492
+ if (screen !== null)
493
+ candidates.push({ id: entity.id, x: screen.x, y: screen.y });
494
+ }
495
+ commitSelection(selectWithinRect(candidates, rect));
496
+ };
497
+ const handlePointerDown = (event) => {
498
+ wrapperRef.current?.focus();
499
+ audioEngine.resume();
500
+ if (contextMenu !== null)
501
+ setContextMenu(null);
502
+ if (event.button === 0) {
503
+ const point = localXY(event);
504
+ pointerDownRef.current = point;
505
+ if (pointer?.select === true)
506
+ marqueeStartRef.current = point;
507
+ }
508
+ };
509
+ const handlePointerMove = (event) => {
510
+ if (pointer?.select !== true)
511
+ return;
512
+ const start = marqueeStartRef.current;
513
+ if (start === null || (event.buttons & 1) === 0)
514
+ return;
515
+ const point = localXY(event);
516
+ setMarquee(screenRect(start.x, start.y, point.x, point.y));
517
+ };
518
+ const handlePointerUp = (event) => {
519
+ if (event.button !== 0 || pointerDownRef.current === null)
520
+ return;
521
+ const start = pointerDownRef.current;
522
+ pointerDownRef.current = null;
523
+ marqueeStartRef.current = null;
524
+ const end = localXY(event);
525
+ const rect = screenRect(start.x, start.y, end.x, end.y);
526
+ const wasMarquee = marquee !== null && isMarquee(rect, PRIMARY_CLICK_MOVE_THRESHOLD_PX);
527
+ setMarquee(null);
528
+ if (pointer?.select === true && wasMarquee) {
529
+ finishMarquee(rect);
530
+ return;
531
+ }
532
+ if (cameraDraggingRef.current)
533
+ return;
534
+ if (pointer?.grabWorldItems === true) {
535
+ const hit = pointerService.worldHit();
536
+ const itemInstanceId = hit?.entity ?? null;
537
+ const record = itemInstanceId === null ? null : ctx.scene.worldItem.get(itemInstanceId);
538
+ if (record !== null && itemInstanceId !== null) {
539
+ const itemEntity = ctx.scene.entity.get(itemInstanceId);
540
+ const localPlayer = ctx.scene.entity.get(ctx.player.userId);
541
+ const pickupRadius = playable.worldItem?.pickupRadius ?? DEFAULT_PICKUP_RADIUS;
542
+ const withinRadius = itemEntity !== null &&
543
+ localPlayer !== null &&
544
+ Math.hypot(localPlayer.position[0] - itemEntity.position[0], localPlayer.position[1] - itemEntity.position[1], localPlayer.position[2] - itemEntity.position[2]) <= pickupRadius;
545
+ if (withinRadius) {
546
+ ctx.scene.worldItem.pickup(itemInstanceId, ctx.player.userId);
547
+ return;
548
+ }
549
+ }
550
+ }
551
+ if (pointer?.select === true) {
552
+ const hit = pointerService.worldHit();
553
+ if (hit !== null && hit.entity !== null && (selectFilter === undefined || selectFilter(hit.entity))) {
554
+ commitSelection([hit.entity]);
555
+ }
556
+ else {
557
+ commitSelection([]);
558
+ }
559
+ return;
560
+ }
561
+ if (pointer?.moveCommand !== undefined) {
562
+ const hit = pointerService.worldHit();
563
+ if (hit !== null && ctx.game.commands.has(pointer.moveCommand)) {
564
+ ctx.game.commands.run(pointer.moveCommand, { point: hit.point, entity: hit.entity, object: hit.object });
565
+ }
566
+ return;
567
+ }
568
+ const moved = (end.x - start.x) ** 2 + (end.y - start.y) ** 2;
569
+ if (moved <= PRIMARY_CLICK_MOVE_THRESHOLD_PX * PRIMARY_CLICK_MOVE_THRESHOLD_PX)
570
+ primaryClickRef.current = true;
571
+ };
572
+ const handleContextMenu = (event) => {
573
+ if (pointer === undefined)
574
+ return;
575
+ event.preventDefault();
576
+ const hit = pointerService.worldHit();
577
+ if (hit === null)
578
+ return;
579
+ if (pointer.contextMenu === true && (hit.entity !== null || hit.object !== null)) {
580
+ const menu = pointerContextMenu(ctx, playable, hit);
581
+ if (menu !== null) {
582
+ const point = localXY(event);
583
+ setContextMenu({ menu, x: point.x, y: point.y });
584
+ return;
585
+ }
586
+ }
587
+ if (pointer.select === true && pointer.orderCommand !== undefined && selection.size() > 0) {
588
+ if (ctx.game.commands.has(pointer.orderCommand)) {
589
+ ctx.game.commands.run(pointer.orderCommand, { selection: selection.list(), point: hit.point });
590
+ }
591
+ }
592
+ };
593
+ const handleVerbPick = (verb) => {
594
+ const state = contextMenu;
595
+ setContextMenu(null);
596
+ if (state !== null && ctx.game.commands.has(verb.command)) {
597
+ ctx.game.commands.run(verb.command, contextVerbInput(state.menu, verb));
598
+ }
599
+ };
413
600
  return (_jsxs("div", { ref: wrapperRef, tabIndex: 0, className: "relative h-full w-full bg-neutral-950 outline-none", onKeyDown: (event) => {
414
601
  if (event.code === "Tab" || event.code === "Space")
415
602
  event.preventDefault();
416
603
  tracker.handleDown(event.code);
417
- }, onKeyUp: (event) => tracker.handleUp(event.code), onBlur: () => tracker.reset(), onPointerDown: (event) => {
418
- wrapperRef.current?.focus();
419
- if (event.button === 0) {
420
- pointerDownRef.current = { x: event.clientX, y: event.clientY };
421
- }
422
- }, onPointerUp: (event) => {
423
- if (event.button !== 0 || pointerDownRef.current === null)
424
- return;
425
- const start = pointerDownRef.current;
426
- pointerDownRef.current = null;
427
- const dx = event.clientX - start.x;
428
- const dy = event.clientY - start.y;
429
- const moved = dx * dx + dy * dy;
430
- if (!cameraDraggingRef.current && moved <= PRIMARY_CLICK_MOVE_THRESHOLD_PX * PRIMARY_CLICK_MOVE_THRESHOLD_PX) {
431
- primaryClickRef.current = true;
432
- }
433
- }, onWheel: (event) => {
604
+ }, onKeyUp: (event) => tracker.handleUp(event.code), onBlur: () => tracker.reset(), onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, onContextMenu: handleContextMenu, onWheel: (event) => {
434
605
  if (ctx === null || !event.shiftKey)
435
606
  return;
436
607
  event.preventDefault();
@@ -440,7 +611,7 @@ export function GamePlayerShell({ playable, multiplayer = null, }) {
440
611
  else if (event.deltaY > 0 && ctx.game.commands.has("ui.hotbarScrollPrev")) {
441
612
  ctx.game.commands.run("ui.hotbarScrollPrev", {});
442
613
  }
443
- }, children: [_jsxs(Canvas, { camera: { fov: 55, near: 0.1, far: 300 }, children: [_jsx("color", { attach: "background", args: ["#14161b"] }), _jsx("ambientLight", { intensity: 0.55 }), _jsx("directionalLight", { position: [10, 16, 6], intensity: 1.3 }), _jsxs(GameProvider, { context: ctx, children: [_jsx(WorldView, { entitySprites: playable.entitySprites, entityModels: playable.entityModels, objectModels: playable.objectModels, environment: playable.environment, assets: playable.game.assets, renderEntity: playable.renderEntity }), WorldOverlay !== undefined ? _jsx(WorldOverlay, {}) : null, barsStatId !== null ? _jsx(WorldEntityBars, { statId: barsStatId }) : null, _jsx(WorldFloatText, {}), _jsx(ProjectileTracers, {}), firstPerson ? (_jsx(GameFirstPersonCamera, { yawRef: yawRef, pitchRef: pitchRef, config: playable.camera?.firstPerson, followEntityId: playable.camera?.followEntityId })) : (_jsx(GameOrbitCamera, { yawRef: yawRef, pitchRef: pitchRef, config: playable.camera, followEntityId: playable.camera?.followEntityId, onCameraFollow: playable.camera?.onCameraFollow, onDragChange: (dragging) => {
614
+ }, children: [_jsxs(Canvas, { camera: { fov: 55, near: 0.1, far: 300 }, children: [_jsx("color", { attach: "background", args: ["#14161b"] }), _jsx("ambientLight", { intensity: 0.55 }), _jsx("directionalLight", { position: [10, 16, 6], intensity: 1.3 }), _jsxs(GameProvider, { context: ctx, children: [_jsx(WorldView, { entitySprites: playable.entitySprites, entityModels: playable.entityModels, objectModels: playable.objectModels, environment: playable.environment, assets: playable.game.assets, renderEntity: playable.renderEntity, selectedIds: selectedIds }), WorldOverlay !== undefined ? _jsx(WorldOverlay, {}) : null, barsStatId !== null ? _jsx(WorldEntityBars, { statId: barsStatId }) : null, _jsx(WorldItems, { config: playable.worldItem }), _jsx(WorldTelegraphs, {}), _jsx(WorldFloatText, {}), _jsx(ProjectileTracers, {}), _jsx(CombatCameraShake, {}), _jsx(AudioListener, { engine: audioEngine }), _jsx(EntityAudioEmitters, { engine: audioEngine, entitySounds: playable.entitySounds }), _jsx(ObjectAudioEmitters, { engine: audioEngine, objectSounds: playable.objectSounds }), _jsx(GameCameraRig, { yawRef: yawRef, pitchRef: pitchRef, config: cameraConfig, pointerControls: pointerUsesLeft, onDragChange: (dragging) => {
444
615
  cameraDraggingRef.current = dragging;
445
- } }))] }), _jsx(RemotePlayers, { rows: remotePlayers }), _jsx(FrameDriver, { ctx: ctx, playable: playable, tracker: tracker, yawRef: yawRef, pitchRef: pitchRef, primaryClickRef: primaryClickRef, onRuntimeError: reportRuntimeError, multiplayer: multiplayer, serverIdRef: serverIdRef })] }), _jsx(GameUiErrorBoundary, { onRuntimeError: reportRuntimeError, children: _jsx(GameProvider, { context: ctx, children: _jsx(GameUI, {}) }) }), showReticle ? _jsx(Reticle, {}) : null, _jsx(DiagnosticOverlay, { diagnostics: diagnostics })] }));
616
+ } }), _jsx(PointerProbe, { service: pointerService })] }), _jsx(RemotePlayers, { rows: remotePlayers }), _jsx(FrameDriver, { ctx: ctx, playable: playable, tracker: tracker, yawRef: yawRef, pitchRef: pitchRef, primaryClickRef: primaryClickRef, onRuntimeError: reportRuntimeError, multiplayer: multiplayer, serverIdRef: serverIdRef, pointerService: pointerService, pointerAim: pointer?.aim === true, pingCommand: pointer?.pingCommand })] }), _jsx(GameUiErrorBoundary, { onRuntimeError: reportRuntimeError, children: _jsx(GameProvider, { context: ctx, children: _jsx(GameUI, {}) }) }), showReticle ? _jsx(Reticle, {}) : null, marquee !== null ? _jsx(MarqueeBox, { rect: marquee }) : null, contextMenu !== null ? (_jsx(ContextMenuView, { menu: contextMenu.menu, x: contextMenu.x, y: contextMenu.y, onPick: handleVerbPick, onClose: () => setContextMenu(null) })) : null, _jsx(DiagnosticOverlay, { diagnostics: diagnostics })] }));
446
617
  }
@@ -0,0 +1,12 @@
1
+ import type { AudioEngine } from "./audioEngine.js";
2
+ export declare function AudioListener({ engine }: {
3
+ engine: AudioEngine;
4
+ }): null;
5
+ export declare function EntityAudioEmitters({ engine, entitySounds, }: {
6
+ engine: AudioEngine;
7
+ entitySounds: Record<string, string> | undefined;
8
+ }): null;
9
+ export declare function ObjectAudioEmitters({ engine, objectSounds, }: {
10
+ engine: AudioEngine;
11
+ objectSounds: Record<string, string> | undefined;
12
+ }): null;
@@ -0,0 +1,53 @@
1
+ import { useFrame, useThree } from "@react-three/fiber";
2
+ import { useEffect, useRef } from "react";
3
+ import { useSceneEntities, useSceneObjects } from "@jgengine/react/hooks";
4
+ export function AudioListener({ engine }) {
5
+ const camera = useThree((state) => state.camera);
6
+ useFrame(() => {
7
+ engine.setListenerPose({ x: camera.position.x, y: camera.position.y, z: camera.position.z });
8
+ });
9
+ return null;
10
+ }
11
+ function useTrackedEmitters(engine, soundByKey, entries) {
12
+ const handles = useRef(new Map());
13
+ useEffect(() => () => {
14
+ for (const handle of handles.current.values())
15
+ handle.stop();
16
+ handles.current.clear();
17
+ }, [engine]);
18
+ useFrame(() => {
19
+ if (soundByKey === undefined)
20
+ return;
21
+ const live = new Set();
22
+ for (const entry of entries) {
23
+ const soundId = soundByKey[entry.key];
24
+ if (soundId === undefined)
25
+ continue;
26
+ live.add(entry.id);
27
+ let handle = handles.current.get(entry.id);
28
+ if (handle === undefined) {
29
+ handle = engine.playLoop(soundId, { x: entry.position[0], y: entry.position[1], z: entry.position[2] }) ?? undefined;
30
+ if (handle !== undefined)
31
+ handles.current.set(entry.id, handle);
32
+ continue;
33
+ }
34
+ handle.setPosition({ x: entry.position[0], y: entry.position[1], z: entry.position[2] });
35
+ }
36
+ for (const [id, handle] of handles.current) {
37
+ if (!live.has(id)) {
38
+ handle.stop();
39
+ handles.current.delete(id);
40
+ }
41
+ }
42
+ });
43
+ }
44
+ export function EntityAudioEmitters({ engine, entitySounds, }) {
45
+ const entities = useSceneEntities();
46
+ useTrackedEmitters(engine, entitySounds, entities.map((entity) => ({ id: entity.id, key: entity.name, position: entity.position })));
47
+ return null;
48
+ }
49
+ export function ObjectAudioEmitters({ engine, objectSounds, }) {
50
+ const objects = useSceneObjects();
51
+ useTrackedEmitters(engine, objectSounds, objects.map((object) => ({ id: object.instanceId, key: object.catalogId, position: object.position })));
52
+ return null;
53
+ }
@@ -0,0 +1,23 @@
1
+ import { type AudioBusDef, type SoundDef } from "@jgengine/core/audio/audioFalloff";
2
+ export interface Vec3 {
3
+ x: number;
4
+ y: number;
5
+ z: number;
6
+ }
7
+ export interface AudioSceneConfig {
8
+ sounds?: Record<string, SoundDef>;
9
+ buses?: Record<string, AudioBusDef>;
10
+ }
11
+ export interface AudioEmitterHandle {
12
+ setPosition(position: Vec3): void;
13
+ stop(): void;
14
+ }
15
+ export interface AudioEngine {
16
+ setListenerPose(position: Vec3): void;
17
+ playOneShot(soundId: string, position?: Vec3): void;
18
+ playLoop(soundId: string, position?: Vec3): AudioEmitterHandle | null;
19
+ setBusGain(busId: string, gain: number): void;
20
+ resume(): void;
21
+ dispose(): void;
22
+ }
23
+ export declare function createAudioEngine(config?: AudioSceneConfig): AudioEngine;
@@ -0,0 +1,110 @@
1
+ import { distance3, resolveEmitterGain } from "@jgengine/core/audio/audioFalloff";
2
+ function createNoopEngine() {
3
+ return {
4
+ setListenerPose: () => undefined,
5
+ playOneShot: () => undefined,
6
+ playLoop: () => null,
7
+ setBusGain: () => undefined,
8
+ resume: () => undefined,
9
+ dispose: () => undefined,
10
+ };
11
+ }
12
+ function resolveAudioContextCtor() {
13
+ if (typeof window === "undefined")
14
+ return undefined;
15
+ return (window.AudioContext ??
16
+ window.webkitAudioContext);
17
+ }
18
+ export function createAudioEngine(config = {}) {
19
+ const sounds = config.sounds ?? {};
20
+ const busDefs = config.buses ?? {};
21
+ const AudioContextCtor = resolveAudioContextCtor();
22
+ if (AudioContextCtor === undefined)
23
+ return createNoopEngine();
24
+ let context;
25
+ let masterGain;
26
+ try {
27
+ context = new AudioContextCtor();
28
+ masterGain = context.createGain();
29
+ masterGain.connect(context.destination);
30
+ }
31
+ catch {
32
+ return createNoopEngine();
33
+ }
34
+ const busGains = new Map();
35
+ function busGainNode(busId) {
36
+ let node = busGains.get(busId);
37
+ if (node === undefined) {
38
+ node = context.createGain();
39
+ node.gain.value = busDefs[busId]?.gain ?? 1;
40
+ node.connect(masterGain);
41
+ busGains.set(busId, node);
42
+ }
43
+ return node;
44
+ }
45
+ const bufferCache = new Map();
46
+ function loadBuffer(url) {
47
+ let pending = bufferCache.get(url);
48
+ if (pending === undefined) {
49
+ pending = fetch(url)
50
+ .then((response) => response.arrayBuffer())
51
+ .then((data) => context.decodeAudioData(data))
52
+ .catch(() => null);
53
+ bufferCache.set(url, pending);
54
+ }
55
+ return pending;
56
+ }
57
+ let listenerPosition = { x: 0, y: 0, z: 0 };
58
+ function playInternal(soundId, position, loop) {
59
+ const sound = sounds[soundId];
60
+ if (sound === undefined)
61
+ return null;
62
+ const bus = busGainNode(sound.bus);
63
+ let currentPosition = position ?? listenerPosition;
64
+ let gainNode = null;
65
+ let stopped = false;
66
+ void loadBuffer(sound.url).then((buffer) => {
67
+ if (stopped || buffer === null)
68
+ return;
69
+ const sourceNode = context.createBufferSource();
70
+ sourceNode.buffer = buffer;
71
+ sourceNode.loop = loop || (sound.loop ?? false);
72
+ gainNode = context.createGain();
73
+ gainNode.gain.value = resolveEmitterGain(distance3(currentPosition, listenerPosition), sound, 1);
74
+ sourceNode.connect(gainNode);
75
+ gainNode.connect(bus);
76
+ sourceNode.start();
77
+ });
78
+ return {
79
+ setPosition(next) {
80
+ currentPosition = next;
81
+ if (gainNode !== null) {
82
+ gainNode.gain.value = resolveEmitterGain(distance3(currentPosition, listenerPosition), sound, 1);
83
+ }
84
+ },
85
+ stop() {
86
+ stopped = true;
87
+ },
88
+ };
89
+ }
90
+ return {
91
+ setListenerPose(position) {
92
+ listenerPosition = position;
93
+ },
94
+ playOneShot(soundId, position) {
95
+ playInternal(soundId, position, false);
96
+ },
97
+ playLoop(soundId, position) {
98
+ return playInternal(soundId, position, true);
99
+ },
100
+ setBusGain(busId, gain) {
101
+ busGainNode(busId).gain.value = gain;
102
+ },
103
+ resume() {
104
+ void context.resume().catch(() => undefined);
105
+ },
106
+ dispose() {
107
+ void context.close().catch(() => undefined);
108
+ },
109
+ };
110
+ }
@@ -0,0 +1,11 @@
1
+ import { type MutableRefObject } from "react";
2
+ import type { CameraRigKind, GameCameraConfig } from "@jgengine/core/game/playableGame";
3
+ export interface GameCameraRigProps {
4
+ yawRef: MutableRefObject<number>;
5
+ pitchRef: MutableRefObject<number>;
6
+ config?: GameCameraConfig;
7
+ onDragChange?: (dragging: boolean) => void;
8
+ pointerControls?: boolean;
9
+ }
10
+ export declare function resolveRigKind(config: GameCameraConfig | undefined): CameraRigKind;
11
+ export declare function GameCameraRig({ yawRef, pitchRef, config, onDragChange, pointerControls }: GameCameraRigProps): import("react").JSX.Element;
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useMemo } from "react";
3
+ import { ChaseRig, CinematicRig, LockOnRig, ObserverRig, RtsRig, ShoulderRig, TopDownRig, } from "./cameraRigs.js";
4
+ import { GameFirstPersonCamera } from "./GameFirstPersonCamera.js";
5
+ import { GameOrbitCamera } from "./GameOrbitCamera.js";
6
+ import { CameraShakeContext, createCameraShakeChannel } from "./shakeChannel.js";
7
+ export function resolveRigKind(config) {
8
+ if (config?.rig !== undefined)
9
+ return config.rig;
10
+ if (config?.perspective === "first")
11
+ return "first";
12
+ return "orbit";
13
+ }
14
+ export function GameCameraRig({ yawRef, pitchRef, config, onDragChange, pointerControls }) {
15
+ const channel = useMemo(() => createCameraShakeChannel(config?.shake?.decayPerSecond), [config?.shake?.decayPerSecond]);
16
+ const followEntityId = config?.followEntityId;
17
+ const rig = (() => {
18
+ if (config?.cinematic !== undefined) {
19
+ return _jsx(CinematicRig, { yawRef: yawRef, pitchRef: pitchRef, config: config, followEntityId: followEntityId });
20
+ }
21
+ const kind = resolveRigKind(config);
22
+ switch (kind) {
23
+ case "first":
24
+ return (_jsx(GameFirstPersonCamera, { yawRef: yawRef, pitchRef: pitchRef, config: config?.firstPerson, followEntityId: followEntityId ?? undefined }));
25
+ case "topDown":
26
+ return _jsx(TopDownRig, { yawRef: yawRef, pitchRef: pitchRef, config: config, followEntityId: followEntityId });
27
+ case "rts":
28
+ return _jsx(RtsRig, { yawRef: yawRef, pitchRef: pitchRef, config: config, followEntityId: followEntityId });
29
+ case "shoulder":
30
+ return _jsx(ShoulderRig, { yawRef: yawRef, pitchRef: pitchRef, config: config, followEntityId: followEntityId });
31
+ case "lockOn":
32
+ return _jsx(LockOnRig, { yawRef: yawRef, pitchRef: pitchRef, config: config, followEntityId: followEntityId });
33
+ case "chase":
34
+ return _jsx(ChaseRig, { yawRef: yawRef, pitchRef: pitchRef, config: config, followEntityId: followEntityId });
35
+ case "observer":
36
+ return _jsx(ObserverRig, { yawRef: yawRef, pitchRef: pitchRef, config: config, followEntityId: followEntityId });
37
+ default:
38
+ return (_jsx(GameOrbitCamera, { yawRef: yawRef, pitchRef: pitchRef, config: config, followEntityId: followEntityId ?? undefined, onCameraFollow: config?.onCameraFollow, onDragChange: onDragChange, pointerControls: pointerControls }));
39
+ }
40
+ })();
41
+ return _jsx(CameraShakeContext.Provider, { value: channel, children: rig });
42
+ }