@jgengine/shell 0.5.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 (130) hide show
  1. package/CHANGELOG.md +191 -2
  2. package/README.md +27 -0
  3. package/dist/GamePlayerShell.js +224 -43
  4. package/dist/audio/AudioComponents.d.ts +12 -0
  5. package/dist/audio/AudioComponents.js +53 -0
  6. package/dist/audio/audioEngine.d.ts +23 -0
  7. package/dist/audio/audioEngine.js +110 -0
  8. package/dist/camera/GameCameraRig.d.ts +11 -0
  9. package/dist/camera/GameCameraRig.js +42 -0
  10. package/dist/camera/GameOrbitCamera.d.ts +3 -1
  11. package/dist/camera/GameOrbitCamera.js +4 -2
  12. package/dist/camera/cameraRigs.d.ts +22 -0
  13. package/dist/camera/cameraRigs.js +485 -0
  14. package/dist/camera/index.d.ts +5 -0
  15. package/dist/camera/index.js +5 -0
  16. package/dist/camera/rigMath.d.ts +130 -0
  17. package/dist/camera/rigMath.js +338 -0
  18. package/dist/camera/shakeChannel.d.ts +29 -0
  19. package/dist/camera/shakeChannel.js +38 -0
  20. package/dist/demo/builderDemo.d.ts +2 -0
  21. package/dist/demo/builderDemo.js +189 -0
  22. package/dist/demo/demoGame.js +10 -2
  23. package/dist/demo/environmentShowcase.d.ts +2 -0
  24. package/dist/demo/environmentShowcase.js +15 -0
  25. package/dist/demo/mapDemo.d.ts +2 -0
  26. package/dist/demo/mapDemo.js +206 -0
  27. package/dist/demo/pointerDemo.d.ts +2 -0
  28. package/dist/demo/pointerDemo.js +151 -0
  29. package/dist/demo/sensorShowcase.d.ts +2 -0
  30. package/dist/demo/sensorShowcase.js +131 -0
  31. package/dist/demo/survivalDemo.d.ts +2 -0
  32. package/dist/demo/survivalDemo.js +291 -0
  33. package/dist/environment/EnvironmentScene.d.ts +5 -0
  34. package/dist/environment/EnvironmentScene.js +75 -0
  35. package/dist/environment/index.d.ts +1 -0
  36. package/dist/environment/index.js +1 -0
  37. package/dist/environment.d.ts +1 -0
  38. package/dist/environment.js +1 -0
  39. package/dist/map/MapMarkerBeacons.d.ts +12 -0
  40. package/dist/map/MapMarkerBeacons.js +16 -0
  41. package/dist/map/index.d.ts +2 -0
  42. package/dist/map/index.js +2 -0
  43. package/dist/map/terrainMap.d.ts +23 -0
  44. package/dist/map/terrainMap.js +79 -0
  45. package/dist/pointer/PointerOverlays.d.ts +12 -0
  46. package/dist/pointer/PointerOverlays.js +17 -0
  47. package/dist/pointer/PointerProbe.d.ts +4 -0
  48. package/dist/pointer/PointerProbe.js +27 -0
  49. package/dist/pointer/pointerService.d.ts +24 -0
  50. package/dist/pointer/pointerService.js +68 -0
  51. package/dist/registry.d.ts +4 -2
  52. package/dist/replay/useSessionRecorder.d.ts +14 -0
  53. package/dist/replay/useSessionRecorder.js +22 -0
  54. package/dist/structures/GeneratedBuilding.d.ts +44 -0
  55. package/dist/structures/GeneratedBuilding.js +86 -0
  56. package/dist/structures/PlacementGhost.d.ts +8 -0
  57. package/dist/structures/PlacementGhost.js +11 -0
  58. package/dist/structures/index.d.ts +2 -0
  59. package/dist/structures/index.js +2 -0
  60. package/dist/structures.d.ts +1 -0
  61. package/dist/structures.js +1 -0
  62. package/dist/terrain/CarvedTerrain.d.ts +22 -0
  63. package/dist/terrain/CarvedTerrain.js +19 -0
  64. package/dist/terrain/EditableGround.d.ts +11 -0
  65. package/dist/terrain/EditableGround.js +38 -0
  66. package/dist/terrain/GrassField.d.ts +21 -0
  67. package/dist/terrain/GrassField.js +31 -0
  68. package/dist/terrain/ProceduralGround.d.ts +9 -0
  69. package/dist/terrain/ProceduralGround.js +17 -0
  70. package/dist/terrain/TerraformBrushCursor.d.ts +8 -0
  71. package/dist/terrain/TerraformBrushCursor.js +13 -0
  72. package/dist/terrain/grassGeometry.d.ts +27 -0
  73. package/dist/terrain/grassGeometry.js +77 -0
  74. package/dist/terrain/grassMaterial.d.ts +33 -0
  75. package/dist/terrain/grassMaterial.js +108 -0
  76. package/dist/terrain/index.d.ts +10 -0
  77. package/dist/terrain/index.js +10 -0
  78. package/dist/terrain/random.d.ts +4 -0
  79. package/dist/terrain/random.js +27 -0
  80. package/dist/terrain/terrainMath.d.ts +47 -0
  81. package/dist/terrain/terrainMath.js +172 -0
  82. package/dist/terrain.d.ts +1 -0
  83. package/dist/terrain.js +1 -0
  84. package/dist/vision/FrustumSensorHud.d.ts +21 -0
  85. package/dist/vision/FrustumSensorHud.js +67 -0
  86. package/dist/vision/HiddenStateProbeHud.d.ts +11 -0
  87. package/dist/vision/HiddenStateProbeHud.js +11 -0
  88. package/dist/vision/RevealVision.d.ts +28 -0
  89. package/dist/vision/RevealVision.js +53 -0
  90. package/dist/water/Ocean.d.ts +6 -0
  91. package/dist/water/Ocean.js +30 -0
  92. package/dist/water/OceanConfig.d.ts +90 -0
  93. package/dist/water/OceanConfig.js +131 -0
  94. package/dist/water/OceanMaterial.d.ts +54 -0
  95. package/dist/water/OceanMaterial.js +51 -0
  96. package/dist/water/OceanShader.d.ts +2 -0
  97. package/dist/water/OceanShader.js +78 -0
  98. package/dist/water/index.d.ts +3 -0
  99. package/dist/water/index.js +3 -0
  100. package/dist/water.d.ts +1 -0
  101. package/dist/water.js +1 -0
  102. package/dist/weather/FireSpreadLayer.d.ts +14 -0
  103. package/dist/weather/FireSpreadLayer.js +68 -0
  104. package/dist/weather/LightningStrike.d.ts +17 -0
  105. package/dist/weather/LightningStrike.js +130 -0
  106. package/dist/weather/RainField.d.ts +20 -0
  107. package/dist/weather/RainField.js +107 -0
  108. package/dist/weather/SnowField.d.ts +19 -0
  109. package/dist/weather/SnowField.js +108 -0
  110. package/dist/weather/WeatherLayer.d.ts +17 -0
  111. package/dist/weather/WeatherLayer.js +16 -0
  112. package/dist/weather/index.d.ts +6 -0
  113. package/dist/weather/index.js +6 -0
  114. package/dist/weather/weatherGeometry.d.ts +2 -0
  115. package/dist/weather/weatherGeometry.js +14 -0
  116. package/dist/weather/weatherMath.d.ts +7 -0
  117. package/dist/weather/weatherMath.js +30 -0
  118. package/dist/weather/weatherUniforms.d.ts +18 -0
  119. package/dist/weather/weatherUniforms.js +34 -0
  120. package/dist/world/InstancedBodies.d.ts +17 -0
  121. package/dist/world/InstancedBodies.js +84 -0
  122. package/dist/world/InstancedJoints.d.ts +14 -0
  123. package/dist/world/InstancedJoints.js +32 -0
  124. package/dist/world/WorldHud.d.ts +2 -0
  125. package/dist/world/WorldHud.js +70 -12
  126. package/dist/world/WorldItems.d.ts +5 -0
  127. package/dist/world/WorldItems.js +31 -0
  128. package/dist/world/floatTextStyle.d.ts +14 -0
  129. package/dist/world/floatTextStyle.js +39 -0
  130. package/package.json +12 -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)
@@ -85,23 +114,31 @@ function EntityModel({ model }) {
85
114
  const gltf = useLoader(GLTFLoader, model.url);
86
115
  const scene = useMemo(() => gltf.scene.clone(true), [gltf]);
87
116
  const scale = model.scale ?? 1;
88
- return _jsx("primitive", { object: scene, "position-y": model.y ?? 0, scale: [scale, scale, scale] });
117
+ const baseY = model.y ?? 0;
118
+ const dims = model.dims;
119
+ const centered = (model.anchor ?? "center") === "center" && dims !== undefined;
120
+ const position = centered
121
+ ? [-scale * dims.center.x, baseY - scale * dims.minY, -scale * dims.center.z]
122
+ : [0, baseY, 0];
123
+ return _jsx("primitive", { object: scene, position: position, scale: [scale, scale, scale] });
89
124
  }
90
125
  function resolveModel(value, assets) {
91
126
  if (value === undefined)
92
127
  return undefined;
93
128
  if (typeof value !== "string")
94
129
  return value;
95
- const url = assets.resolve(value)?.url;
96
- return url === undefined ? undefined : { url };
130
+ const ref = assets.resolve(value);
131
+ if (ref === null)
132
+ return undefined;
133
+ return ref.dims === undefined ? { url: ref.url } : { url: ref.url, dims: ref.dims };
97
134
  }
98
- function EntityMarker({ entity, model, sprite, isLocal, targeted, onSelect, }) {
135
+ function EntityMarker({ entity, custom, model, sprite, isLocal, targeted, selected, onSelect, }) {
99
136
  const color = isLocal ? "#4ade80" : entity.role === "npc" ? colorFromId(entity.name) : "#9ca3af";
100
- 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) => {
101
138
  event.stopPropagation();
102
139
  if (!isLocal)
103
140
  onSelect(entity);
104
- }, children: [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] }));
105
142
  }
106
143
  function GroundPlane() {
107
144
  const geometry = useMemo(() => {
@@ -134,26 +171,29 @@ function RockField() {
134
171
  }), []);
135
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))) }));
136
173
  }
137
- function WorldView({ entitySprites, entityModels, objectModels, assets, }) {
174
+ function WorldView({ entitySprites, entityModels, objectModels, environment: Environment, assets, renderEntity, selectedIds, }) {
138
175
  const ctx = useGameContext();
139
176
  const entities = useSceneEntities();
140
177
  const objects = useSceneObjects();
141
178
  const player = usePlayer();
142
179
  const targetId = useTarget(player.userId);
180
+ const controlledId = ctx.player.possession.active(player.userId);
143
181
  const handleSelect = (entity) => {
144
182
  const relation = ctx.scene.entity.canReceive(entity.id, "damage") === null ? "hostile" : "friendly";
145
- 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);
146
184
  };
147
- return (_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, 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) => {
148
188
  const model = resolveModel(objectModels?.[object.catalogId], assets) ??
149
189
  resolveModel(object.catalogId, assets);
150
- 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));
151
191
  })] }));
152
192
  }
153
193
  function RemotePlayers({ rows }) {
154
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))) }));
155
195
  }
156
- 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, }) {
157
197
  const motionRef = useRef(createPlayerMotionState());
158
198
  const hasReportedTickError = useRef(false);
159
199
  const slotActions = useMemo(() => findHotbarSlotActions(playable.game.input), [playable]);
@@ -161,11 +201,12 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
161
201
  useFrame((_state, rawDt) => {
162
202
  try {
163
203
  const dt = Math.min(rawDt, 0.05);
204
+ const gameDt = ctx.time.advance(dt);
164
205
  if (tracker.isDown("turnLeft"))
165
206
  yawRef.current += TURN_SPEED * dt;
166
207
  if (tracker.isDown("turnRight"))
167
208
  yawRef.current -= TURN_SPEED * dt;
168
- const playerId = ctx.player.userId;
209
+ const playerId = ctx.player.possession.active(ctx.player.userId);
169
210
  const player = ctx.scene.entity.get(playerId);
170
211
  const forwardX = Math.sin(yawRef.current);
171
212
  const forwardZ = Math.cos(yawRef.current);
@@ -185,9 +226,10 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
185
226
  rotationY: intent.moving
186
227
  ? Math.atan2(motion.horizontalVelocityX, motion.horizontalVelocityZ)
187
228
  : player.rotationY,
229
+ dt: rawDt,
188
230
  });
189
231
  }
190
- playable.loop.onTick(ctx, dt);
232
+ playable.loop.onTick(ctx, gameDt);
191
233
  if (tracker.wasPressed("tabTarget")) {
192
234
  if (ctx.game.commands.has("target.cycle"))
193
235
  ctx.game.commands.run("target.cycle", {});
@@ -200,9 +242,22 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
200
242
  else
201
243
  ctx.scene.entity.setTarget(playerId, null);
202
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
+ }
203
256
  for (const action of Object.keys(playable.game.input ?? {})) {
204
257
  if (!tracker.wasPressed(action))
205
258
  continue;
259
+ if (action === "ping" && pingCommand !== undefined)
260
+ continue;
206
261
  if (action === "interact") {
207
262
  const prompts = playable.prompts?.(ctx);
208
263
  const focus = prompts === undefined ? null : ctx.scene.entity.get(playerId);
@@ -219,10 +274,11 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
219
274
  ctx.game.commands.run(command, {});
220
275
  }
221
276
  if (hotbarId !== null) {
277
+ const aimOverride = pointerAim ? pointerAimFor(ctx, pointerService) : undefined;
222
278
  for (const { action, slot } of slotActions) {
223
279
  if (!tracker.wasPressed(action))
224
280
  continue;
225
- const result = executeHotbarSlot(ctx, hotbarId, slot, yawRef.current, pitchRef.current);
281
+ const result = executeHotbarSlot(ctx, playerId, hotbarId, slot, yawRef.current, pitchRef.current, aimOverride);
226
282
  if (!result.ok)
227
283
  console.warn(`[jgengine:item-use] ${result.error}`);
228
284
  }
@@ -235,7 +291,7 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
235
291
  ? preferred
236
292
  : slots.findIndex((stack) => stack !== null);
237
293
  if (slot >= 0) {
238
- const result = executeHotbarSlot(ctx, hotbarId, slot, yawRef.current, pitchRef.current);
294
+ const result = executeHotbarSlot(ctx, playerId, hotbarId, slot, yawRef.current, pitchRef.current, aimOverride);
239
295
  if (!result.ok)
240
296
  console.warn(`[jgengine:item-use] ${result.error}`);
241
297
  }
@@ -296,7 +352,15 @@ export function GamePlayerShell({ playable, multiplayer = null, }) {
296
352
  const cameraDraggingRef = useRef(false);
297
353
  const primaryClickRef = useRef(false);
298
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);
299
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]);
300
364
  const userId = multiplayer?.userId ?? DEV_USER_ID;
301
365
  const reportRuntimeError = (error, phase) => {
302
366
  const diagnostic = logRuntimeError(error, phase);
@@ -388,39 +452,156 @@ export function GamePlayerShell({ playable, multiplayer = null, }) {
388
452
  useEffect(() => {
389
453
  wrapperRef.current?.focus();
390
454
  }, [ctx]);
455
+ useSyncExternalStore(ctx?.subscribe ?? (() => () => undefined), ctx?.version ?? (() => 0), ctx?.version ?? (() => 0));
391
456
  if (ctx === null)
392
457
  return _jsx("div", { className: "h-full w-full bg-neutral-950" });
393
458
  const GameUI = playable.GameUI;
394
459
  const WorldOverlay = playable.WorldOverlay;
395
- const firstPerson = playable.camera?.perspective === "first";
396
- 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";
397
467
  const worldBars = playable.worldHealthBars;
398
468
  const barsStatId = worldBars === undefined || worldBars === false
399
469
  ? null
400
470
  : worldBars === true
401
471
  ? "health"
402
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
+ };
403
600
  return (_jsxs("div", { ref: wrapperRef, tabIndex: 0, className: "relative h-full w-full bg-neutral-950 outline-none", onKeyDown: (event) => {
404
601
  if (event.code === "Tab" || event.code === "Space")
405
602
  event.preventDefault();
406
603
  tracker.handleDown(event.code);
407
- }, onKeyUp: (event) => tracker.handleUp(event.code), onBlur: () => tracker.reset(), onPointerDown: (event) => {
408
- wrapperRef.current?.focus();
409
- if (event.button === 0) {
410
- pointerDownRef.current = { x: event.clientX, y: event.clientY };
411
- }
412
- }, onPointerUp: (event) => {
413
- if (event.button !== 0 || pointerDownRef.current === null)
414
- return;
415
- const start = pointerDownRef.current;
416
- pointerDownRef.current = null;
417
- const dx = event.clientX - start.x;
418
- const dy = event.clientY - start.y;
419
- const moved = dx * dx + dy * dy;
420
- if (!cameraDraggingRef.current && moved <= PRIMARY_CLICK_MOVE_THRESHOLD_PX * PRIMARY_CLICK_MOVE_THRESHOLD_PX) {
421
- primaryClickRef.current = true;
422
- }
423
- }, onWheel: (event) => {
604
+ }, onKeyUp: (event) => tracker.handleUp(event.code), onBlur: () => tracker.reset(), onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, onContextMenu: handleContextMenu, onWheel: (event) => {
424
605
  if (ctx === null || !event.shiftKey)
425
606
  return;
426
607
  event.preventDefault();
@@ -430,7 +611,7 @@ export function GamePlayerShell({ playable, multiplayer = null, }) {
430
611
  else if (event.deltaY > 0 && ctx.game.commands.has("ui.hotbarScrollPrev")) {
431
612
  ctx.game.commands.run("ui.hotbarScrollPrev", {});
432
613
  }
433
- }, 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, assets: playable.game.assets }), 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) => {
434
615
  cameraDraggingRef.current = dragging;
435
- } }))] }), _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 })] }));
436
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;