@jgengine/shell 0.8.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.
Files changed (127) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +11 -0
  3. package/dist/GamePlayerShell.d.ts +5 -2
  4. package/dist/GamePlayerShell.js +358 -98
  5. package/dist/GameUiPreview.js +2 -1
  6. package/dist/audio/AudioComponents.js +25 -8
  7. package/dist/audio/audioEngine.d.ts +1 -0
  8. package/dist/audio/audioEngine.js +4 -0
  9. package/dist/behaviour.d.ts +5 -24
  10. package/dist/behaviour.js +14 -50
  11. package/dist/behaviourAttach.d.ts +9 -0
  12. package/dist/behaviourAttach.js +32 -0
  13. package/dist/behaviourDriver.d.ts +7 -0
  14. package/dist/behaviourDriver.js +21 -0
  15. package/dist/camera/GameCameraRig.js +1 -1
  16. package/dist/camera/GameFirstPersonCamera.js +14 -0
  17. package/dist/camera/GameOrbitCamera.js +10 -1
  18. package/dist/camera/PlayerFov.d.ts +18 -0
  19. package/dist/camera/PlayerFov.js +48 -0
  20. package/dist/camera/cameraBlendMath.d.ts +13 -0
  21. package/dist/camera/cameraBlendMath.js +36 -0
  22. package/dist/camera/cameraRigs.d.ts +3 -0
  23. package/dist/camera/cameraRigs.js +36 -33
  24. package/dist/camera/fovPreference.d.ts +25 -0
  25. package/dist/camera/fovPreference.js +73 -0
  26. package/dist/camera/index.d.ts +3 -0
  27. package/dist/camera/index.js +3 -0
  28. package/dist/camera/rigMath.d.ts +10 -0
  29. package/dist/camera/rigMath.js +36 -1
  30. package/dist/camera/shakeChannel.d.ts +3 -17
  31. package/dist/camera/shakeChannel.js +2 -21
  32. package/dist/camera/shakeChannelMath.d.ts +8 -0
  33. package/dist/camera/shakeChannelMath.js +21 -0
  34. package/dist/cartridge.d.ts +145 -0
  35. package/dist/cartridge.js +245 -0
  36. package/dist/defineGame.js +2 -1
  37. package/dist/devtools/CollisionDebugWorld.d.ts +5 -0
  38. package/dist/devtools/CollisionDebugWorld.js +180 -0
  39. package/dist/devtools/DevtoolsOverlay.d.ts +65 -1
  40. package/dist/devtools/DevtoolsOverlay.js +383 -41
  41. package/dist/devtools/collisionDebug.d.ts +57 -0
  42. package/dist/devtools/collisionDebug.js +127 -0
  43. package/dist/devtools/collisionDebugMath.d.ts +102 -0
  44. package/dist/devtools/collisionDebugMath.js +128 -0
  45. package/dist/environment/Daylight.d.ts +13 -7
  46. package/dist/environment/Daylight.js +12 -11
  47. package/dist/environment/EnvironmentScene.js +82 -35
  48. package/dist/environment/groundPadMath.d.ts +2 -2
  49. package/dist/environment/groundPadMath.js +1 -1
  50. package/dist/environment/index.d.ts +2 -1
  51. package/dist/environment/index.js +1 -0
  52. package/dist/environment/skyLightingPolicy.d.ts +10 -0
  53. package/dist/environment/skyLightingPolicy.js +6 -0
  54. package/dist/input/mouseLook.d.ts +27 -0
  55. package/dist/input/mouseLook.js +35 -0
  56. package/dist/materialOverride.d.ts +4 -6
  57. package/dist/materialOverride.js +12 -16
  58. package/dist/pointer/PointerProbe.js +6 -2
  59. package/dist/pointer/pointerService.d.ts +3 -0
  60. package/dist/pointer/pointerService.js +6 -0
  61. package/dist/render/modelRender.d.ts +10 -1
  62. package/dist/render/modelRender.js +32 -5
  63. package/dist/render/resolveModel.d.ts +14 -0
  64. package/dist/render/resolveModel.js +24 -0
  65. package/dist/settings/QuickControls.d.ts +4 -0
  66. package/dist/settings/QuickControls.js +42 -0
  67. package/dist/settings/SettingsChrome.d.ts +1 -0
  68. package/dist/settings/SettingsChrome.js +10 -0
  69. package/dist/settings/SettingsMenu.d.ts +6 -0
  70. package/dist/settings/SettingsMenu.js +148 -0
  71. package/dist/settings/SettingsRuntime.d.ts +11 -0
  72. package/dist/settings/SettingsRuntime.js +19 -0
  73. package/dist/settings/appliedSettings.d.ts +14 -0
  74. package/dist/settings/appliedSettings.js +27 -0
  75. package/dist/settings/settingsController.d.ts +20 -0
  76. package/dist/settings/settingsController.js +165 -0
  77. package/dist/structures/GeneratedBuilding.d.ts +10 -0
  78. package/dist/structures/GeneratedBuilding.js +96 -8
  79. package/dist/structures/index.d.ts +1 -1
  80. package/dist/structures/index.js +1 -1
  81. package/dist/terrain/CarvedTerrain.d.ts +2 -1
  82. package/dist/terrain/CarvedTerrain.js +3 -3
  83. package/dist/terrain/GrassField.d.ts +3 -1
  84. package/dist/terrain/GrassField.js +4 -2
  85. package/dist/terrain/grassBudget.d.ts +3 -0
  86. package/dist/terrain/grassBudget.js +6 -0
  87. package/dist/terrain/grassGeometry.js +1 -1
  88. package/dist/terrain/terrainMath.d.ts +8 -0
  89. package/dist/terrain/terrainMath.js +9 -0
  90. package/dist/touch/OrientationHint.d.ts +3 -0
  91. package/dist/touch/OrientationHint.js +13 -0
  92. package/dist/touch/TouchControlsOverlay.d.ts +17 -1
  93. package/dist/touch/TouchControlsOverlay.js +115 -9
  94. package/dist/visibility/CullingProvider.d.ts +21 -0
  95. package/dist/visibility/CullingProvider.js +134 -0
  96. package/dist/vision/FrustumSensorHud.d.ts +1 -6
  97. package/dist/vision/FrustumSensorHud.js +25 -27
  98. package/dist/vision/frustumSampleEqual.d.ts +2 -0
  99. package/dist/vision/frustumSampleEqual.js +10 -0
  100. package/dist/water/Ocean.js +1 -1
  101. package/dist/water/OceanConfig.d.ts +13 -0
  102. package/dist/water/OceanConfig.js +25 -17
  103. package/dist/water/index.d.ts +1 -1
  104. package/dist/water/index.js +1 -1
  105. package/dist/weather/FireSpreadLayer.js +7 -2
  106. package/dist/weather/RainField.d.ts +3 -1
  107. package/dist/weather/RainField.js +4 -4
  108. package/dist/weather/SnowField.d.ts +3 -1
  109. package/dist/weather/SnowField.js +4 -4
  110. package/dist/weather/fireSpreadPose.d.ts +2 -0
  111. package/dist/weather/fireSpreadPose.js +4 -0
  112. package/dist/weather/weatherMath.d.ts +5 -1
  113. package/dist/weather/weatherMath.js +7 -2
  114. package/dist/world/DataObjects.d.ts +1 -1
  115. package/dist/world/DataObjects.js +3 -1
  116. package/dist/world/SpriteBatch.d.ts +44 -0
  117. package/dist/world/SpriteBatch.js +112 -0
  118. package/dist/world/WorldHud.d.ts +3 -0
  119. package/dist/world/WorldHud.js +89 -42
  120. package/dist/world/entityPose.d.ts +14 -0
  121. package/dist/world/entityPose.js +10 -0
  122. package/dist/world/telegraphPulse.d.ts +1 -0
  123. package/dist/world/telegraphPulse.js +4 -0
  124. package/dist/world/worldBarSamples.d.ts +30 -0
  125. package/dist/world/worldBarSamples.js +51 -0
  126. package/llms.txt +1404 -1143
  127. package/package.json +4 -4
@@ -1,6 +1,6 @@
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, useSyncExternalStore, } from "react";
3
+ import { Component, useCallback, 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 { actionRepeatMs, createActionStateTracker, hotbarSlotActionIndex, resolveActionCommand, shouldDispatchAction, toActionStateBindingMap, } from "@jgengine/core/input/actionBindings";
@@ -8,35 +8,55 @@ import { deriveTouchScheme, withTouchCodes } from "@jgengine/core/input/touchSch
8
8
  import { buildContextMenu, contextVerbInput, } from "@jgengine/core/interaction/contextMenu";
9
9
  import { resolveActivePrompt } from "@jgengine/core/interaction/proximityPrompt";
10
10
  import { aimToPoint } from "@jgengine/core/input/pointer";
11
+ import { normalizePointerToAxis } from "@jgengine/core/input/pointerAxis";
11
12
  import { createSelectionSet, isMarquee, screenRect, selectWithinRect, } from "@jgengine/core/scene/selection";
12
13
  import { advancePlayerMotion, constrainStepToAxis, createEmptyMovementKeys, createPlayerMotionState, resolveMovementIntent, resolveObstacleStep, snapPositionToGrid, } from "@jgengine/core/movement/movementModel";
14
+ import { steerYaw } from "@jgengine/core/movement/steering";
13
15
  import { advanceVoxelPlayer, createVoxelPlayerBody, } from "@jgengine/core/movement/voxelController";
14
16
  import { createGameContext } from "@jgengine/core/runtime/gameContext";
17
+ import { applyHorizontalImpulses } from "@jgengine/core/runtime/motionIntents";
15
18
  import { groundFieldFor } from "@jgengine/core/world/terrain";
16
19
  import { objectVisualScale } from "@jgengine/core/scene/objectStore";
17
20
  import { DEFAULT_PICKUP_RADIUS, WORLD_ITEM_ENTITY_NAME } from "@jgengine/core/game/worldItem";
18
21
  import { useGameContext } from "@jgengine/react/provider";
19
22
  import { useDisplayProfile } from "@jgengine/react/display";
23
+ import { HudViewportProvider } from "@jgengine/react/hudViewport";
20
24
  import { useSceneEntities, useSceneObjects, usePlayer, useTarget } from "@jgengine/react/hooks";
21
25
  import { GameProvider } from "@jgengine/react/provider";
22
26
  import { CAMERA_FRUSTUM_DEFAULTS } from "@jgengine/core/game/playableGame";
27
+ import { createSettingsStore } from "@jgengine/core/settings/settingsModel";
28
+ import { applyBindingOverrides, clearBindingOverride, loadBindingOverrides, saveBindingOverride, } from "@jgengine/core/input/bindingOverrides";
29
+ import { playControlsActive } from "@jgengine/core/game/controlGate";
23
30
  import { sky as resolveSkyDescriptor } from "@jgengine/core/world/features";
24
31
  import { devtools } from "@jgengine/core/devtools/devtools";
32
+ import { VERSION } from "@jgengine/core/meta/changelog";
25
33
  import { AudioListener, EntityAudioEmitters, ObjectAudioEmitters } from "./audio/AudioComponents.js";
26
34
  import { createAudioEngine } from "./audio/audioEngine.js";
35
+ import { CollisionDebugWorld } from "./devtools/CollisionDebugWorld.js";
36
+ import { collisionDebug } from "./devtools/collisionDebug.js";
27
37
  import { DevtoolsOverlay, DevtoolsRendererProbe, withDevtoolsLatency } from "./devtools/DevtoolsOverlay.js";
28
- import { GAME_SIM_FRAME_PRIORITY, GameCameraRig, resolveRigKind, rtsPanKeysConflict } from "./camera/index.js";
38
+ import { GAME_SIM_FRAME_PRIORITY, GameCameraRig, PlayerFovProvider, PlayerFovSlider, resolveRigKind, rtsPanKeysConflict, } from "./camera/index.js";
39
+ import { resolveModel, tryResolveCatalogModel } from "./render/resolveModel.js";
40
+ import { CullingProvider, useRenderVisibility } from "./visibility/CullingProvider.js";
29
41
  import { SkyDaylight, TimeOfDayDaylight } from "./environment.js";
42
+ import { resolveSkyLightOwnership, skyEmitsLights } from "./environment/skyLightingPolicy.js";
30
43
  import { EnvironmentScene } from "./environment/EnvironmentScene.js";
31
44
  import { applyMaterialOverride } from "./materialOverride.js";
32
45
  import { PointerProbe } from "./pointer/PointerProbe.js";
33
- import { applyPaintTexture, cloneModelScene, createPaintCanvas, standardMaterialsOf, syncPaintCanvas, } from "./render/modelRender.js";
46
+ import { applyPaintTextureToMaterials, cacheStandardMaterials, cloneModelScene, createPaintCanvas, disposeClonedMaterials, syncPaintCanvas, } from "./render/modelRender.js";
47
+ import { writeEntityPose } from "./world/entityPose.js";
34
48
  import { MarqueeBox, ContextMenuView } from "./pointer/PointerOverlays.js";
35
49
  import { createPointerService, POINTER_ENTITY_KEY, POINTER_OBJECT_KEY, } from "./pointer/pointerService.js";
36
50
  import { CombatCameraShake, ProjectileTracers, Reticle, WorldEntityBars, WorldFloatText, WorldTelegraphs, } from "./world/WorldHud.js";
37
51
  import { GridWorldScene } from "./world/GridWorldScene.js";
38
52
  import { WorldItems } from "./world/WorldItems.js";
39
- import { TouchControlsDock, TouchPlaySurface } from "./touch/TouchControlsOverlay.js";
53
+ import { OrientationHint } from "./touch/OrientationHint.js";
54
+ import { TouchControlsDock, TouchPlaySurface, touchDockClearance } from "./touch/TouchControlsOverlay.js";
55
+ import { BUILT_IN_SETTING_CATEGORIES } from "@jgengine/core/settings/settingsModel";
56
+ import { SettingsProvider } from "@jgengine/react/settings";
57
+ import { SettingsRuntime } from "./settings/SettingsRuntime.js";
58
+ import { SettingsChrome } from "./settings/SettingsChrome.js";
59
+ import { AudioSettingsBridge, useGraphicsSettings } from "./settings/appliedSettings.js";
40
60
  const DEV_USER_ID = "dev-player";
41
61
  const TURN_SPEED = 2.4;
42
62
  const PRIMARY_CLICK_MOVE_THRESHOLD_PX = 6;
@@ -44,14 +64,15 @@ const GROUND_SIZE = 160;
44
64
  const GROUND_SEGMENTS = 80;
45
65
  const DEFAULT_BACKGROUND_COLOR = "#14161b";
46
66
  const DEFAULT_BACKDROP_FOG_COLOR = "#1a1c22";
47
- function errorToDiagnostic(error, phase) {
67
+ function errorToDiagnostic(error, phase, componentStack) {
68
+ const capturedAt = new Date().toISOString();
48
69
  if (error instanceof Error) {
49
- return { phase, message: error.message, stack: error.stack };
70
+ return { phase, message: error.message, stack: error.stack, componentStack, capturedAt };
50
71
  }
51
- return { phase, message: typeof error === "string" ? error : JSON.stringify(error) };
72
+ return { phase, message: typeof error === "string" ? error : JSON.stringify(error), componentStack, capturedAt };
52
73
  }
53
- function logRuntimeError(error, phase) {
54
- const diagnostic = errorToDiagnostic(error, phase);
74
+ function logRuntimeError(error, phase, componentStack) {
75
+ const diagnostic = errorToDiagnostic(error, phase, componentStack);
55
76
  console.error(`[jgengine:${phase}] ${diagnostic.message}`, error);
56
77
  return diagnostic;
57
78
  }
@@ -173,18 +194,20 @@ export function resolveWorldSky(world) {
173
194
  * flipped the sign and launched airborne players upward instead.
174
195
  */
175
196
  export function resolvePhysicsTuning(physics) {
176
- if (physics?.gravity === undefined && physics?.jumpVelocity === undefined)
197
+ if (physics === undefined)
177
198
  return undefined;
178
- const tuning = {};
179
- if (physics.gravity !== undefined)
180
- tuning.gravityAcceleration = -physics.gravity;
181
- if (physics.jumpVelocity !== undefined)
182
- tuning.jumpVelocity = physics.jumpVelocity;
183
- return tuning;
199
+ return {
200
+ get gravityAcceleration() {
201
+ return physics.gravity === undefined ? undefined : -physics.gravity;
202
+ },
203
+ get jumpVelocity() {
204
+ return physics.jumpVelocity;
205
+ },
206
+ };
184
207
  }
185
208
  /** True when the world is an environment feature with terrain, so the voxel controller should sample its height. */
186
209
  export function hasEnvironmentTerrain(world) {
187
- return world?.kind === "environment" && world.terrain !== undefined;
210
+ return world?.kind === "environment" && (world.terrain !== undefined || (world.islands?.length ?? 0) > 0);
188
211
  }
189
212
  function colorFromId(id) {
190
213
  let hash = 0;
@@ -227,9 +250,12 @@ function EntityModel({ model, instanceId }) {
227
250
  const scene = useMemo(() => {
228
251
  const cloned = cloneModelScene(gltf.scene);
229
252
  if (material !== undefined)
230
- applyMaterialOverride(cloned, material);
253
+ applyMaterialOverride(cloned, material, { clone: false });
231
254
  return cloned;
232
255
  }, [gltf, material]);
256
+ useEffect(() => () => {
257
+ disposeClonedMaterials(scene);
258
+ }, [scene]);
233
259
  const animation = model.animation;
234
260
  const mixerRef = useRef(null);
235
261
  const animationPausedRef = useRef(false);
@@ -261,10 +287,12 @@ function EntityModel({ model, instanceId }) {
261
287
  const paintCanvasRef = useRef(null);
262
288
  const paintDrawnCountRef = useRef(0);
263
289
  const paintVersionRef = useRef(-1);
290
+ const materialCacheRef = useRef(null);
264
291
  useEffect(() => {
265
292
  paintCanvasRef.current = null;
266
293
  paintDrawnCountRef.current = 0;
267
294
  paintVersionRef.current = -1;
295
+ materialCacheRef.current = null;
268
296
  }, [scene]);
269
297
  useFrame((_state, delta) => {
270
298
  if (mixerRef.current !== null && !animationPausedRef.current)
@@ -277,45 +305,68 @@ function EntityModel({ model, instanceId }) {
277
305
  return;
278
306
  paintVersionRef.current = version;
279
307
  const strokes = paint.strokes(instanceId);
308
+ const cache = cacheStandardMaterials(scene, materialCacheRef.current);
309
+ materialCacheRef.current = cache;
280
310
  if (paintCanvasRef.current === null) {
281
311
  if (strokes.length === 0)
282
312
  return;
283
- const materials = standardMaterialsOf(scene);
284
- const seed = materials[0];
313
+ const seed = cache.materials[0];
285
314
  if (seed === undefined)
286
315
  return;
287
316
  const paintCanvas = createPaintCanvas(seed);
288
317
  paintCanvasRef.current = paintCanvas;
289
- applyPaintTexture(scene, paintCanvas);
318
+ applyPaintTextureToMaterials(cache.materials, paintCanvas);
290
319
  }
291
- const seedColor = standardMaterialsOf(scene)[0]?.color ?? new THREE.Color("#ffffff");
292
- paintDrawnCountRef.current = syncPaintCanvas(paintCanvasRef.current, seedColor, strokes, paintDrawnCountRef.current);
320
+ paintDrawnCountRef.current = syncPaintCanvas(paintCanvasRef.current, cache.seedColor, strokes, paintDrawnCountRef.current);
293
321
  });
294
322
  return _jsx("primitive", { object: scene, position: position, scale: [scale, scale, scale] });
295
323
  }
296
- function resolveModel(value, assets) {
297
- if (value === undefined)
298
- return undefined;
299
- if (typeof value !== "string")
300
- return value;
301
- const ref = assets.resolve(value);
302
- if (ref === null)
303
- return undefined;
304
- return ref.dims === undefined ? { url: ref.url } : { url: ref.url, dims: ref.dims };
305
- }
306
324
  function EntityMarker({ entity, custom, model, sprite, isLocal, targeted, selected, onSelect, }) {
307
- const color = isLocal ? "#4ade80" : entity.role === "npc" ? colorFromId(entity.name) : "#9ca3af";
308
- 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) => {
325
+ const groupRef = useRef(null);
326
+ const ctx = useGameContext();
327
+ const visibleRef = useRenderVisibility();
328
+ const entityId = entity.id;
329
+ const role = entity.role;
330
+ const name = entity.name;
331
+ const color = isLocal ? "#4ade80" : role === "npc" ? colorFromId(name) : "#9ca3af";
332
+ useFrame(() => {
333
+ const group = groupRef.current;
334
+ if (group === null)
335
+ return;
336
+ const live = ctx.scene.entity.get(entityId);
337
+ if (live === null)
338
+ return;
339
+ writeEntityPose(group, live);
340
+ group.visible = visibleRef.current(entityId);
341
+ });
342
+ return (_jsxs("group", { ref: groupRef, userData: { [POINTER_ENTITY_KEY]: entityId }, onPointerDown: (event) => {
309
343
  event.stopPropagation();
310
- if (!isLocal)
311
- onSelect(entity);
312
- }, 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, instanceId: entity.id })) : 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] }));
344
+ if (isLocal)
345
+ return;
346
+ const live = ctx.scene.entity.get(entityId);
347
+ if (live !== null)
348
+ onSelect(live);
349
+ }, 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, instanceId: entityId })) : sprite !== undefined ? (_jsx(EntitySprite, { sprite: sprite })) : 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] }));
313
350
  }
314
351
  function ObjectMarker({ object, custom, model, style, }) {
352
+ const groupRef = useRef(null);
353
+ const ctx = useGameContext();
354
+ const visibleRef = useRenderVisibility();
355
+ const instanceId = object.instanceId;
315
356
  const [scaleX, scaleY, scaleZ] = objectVisualScale(object.visual);
316
357
  const color = object.visual?.color ?? style?.color ?? colorFromId(object.catalogId);
317
358
  const opacity = object.visual?.opacity ?? style?.opacity ?? 1;
318
- return (_jsx("group", { position: [object.position[0], object.position[1], object.position[2]], "rotation-y": object.rotationY, userData: { [POINTER_OBJECT_KEY]: object.instanceId }, children: custom !== undefined && custom !== null ? (custom) : model !== undefined ? (_jsx(EntityModel, { model: model, instanceId: object.instanceId })) : style?.hidden === true ? null : (_jsxs("mesh", { "position-y": 0.5 * scaleY, scale: [scaleX, scaleY, scaleZ], children: [_jsx("boxGeometry", { args: [1, 1, 1] }), _jsx("meshStandardMaterial", { color: color, transparent: opacity < 1, opacity: opacity })] })) }));
359
+ useFrame(() => {
360
+ const group = groupRef.current;
361
+ if (group === null)
362
+ return;
363
+ const live = ctx.scene.object.get(instanceId);
364
+ if (live === null)
365
+ return;
366
+ writeEntityPose(group, live);
367
+ group.visible = visibleRef.current(instanceId);
368
+ });
369
+ return (_jsx("group", { ref: groupRef, userData: { [POINTER_OBJECT_KEY]: instanceId }, children: custom !== undefined && custom !== null ? (custom) : model !== undefined ? (_jsx(EntityModel, { model: model, instanceId: instanceId })) : style?.hidden === true ? null : (_jsxs("mesh", { "position-y": 0.5 * scaleY, scale: [scaleX, scaleY, scaleZ], children: [_jsx("boxGeometry", { args: [1, 1, 1] }), _jsx("meshStandardMaterial", { color: color, transparent: opacity < 1, opacity: opacity })] })) }));
319
370
  }
320
371
  function GroundPlane() {
321
372
  const geometry = useMemo(() => {
@@ -348,7 +399,12 @@ function RockField() {
348
399
  }), []);
349
400
  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))) }));
350
401
  }
351
- function WorldView({ entitySprites, entityModels, objectModels, objectStyles, environment: Environment, assets, renderEntity, renderObject, selectedIds, }) {
402
+ function WorldEnvironment({ environment: Environment }) {
403
+ if (Environment !== undefined)
404
+ return _jsx(Environment, {});
405
+ return (_jsxs(_Fragment, { children: [_jsx(GroundPlane, {}), _jsx("gridHelper", { args: [160, 80, "#3a3f4a", "#2b2f38"], "position-y": 0.01 }), _jsx(RockField, {})] }));
406
+ }
407
+ function WorldActors({ entitySprites, entityModels, objectModels, objectStyles, assets, renderEntity, renderObject, selectedIds, }) {
352
408
  const ctx = useGameContext();
353
409
  const entities = useSceneEntities();
354
410
  const objects = useSceneObjects();
@@ -359,18 +415,28 @@ function WorldView({ entitySprites, entityModels, objectModels, objectStyles, en
359
415
  const relation = ctx.scene.entity.canReceive(entity.id, "damage") === null ? "hostile" : "friendly";
360
416
  ctx.scene.entity.setTarget(controlledId, relation === "hostile" || entity.role === "npc" ? entity.id : null);
361
417
  };
362
- 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
418
+ return (_jsxs(_Fragment, { children: [entities
363
419
  .filter((entity) => entity.name !== WORLD_ITEM_ENTITY_NAME)
364
- .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) => {
365
- const model = resolveModel(objectModels?.[object.catalogId], assets) ??
366
- resolveModel(object.catalogId, assets);
420
+ .map((entity) => (_jsx(EntityMarker, { entity: entity, custom: renderEntity?.(entity), model: resolveModel(entityModels?.[entity.name], assets, {
421
+ seam: "entityModels",
422
+ key: entity.name,
423
+ }), sprite: entitySprites?.[entity.name], isLocal: entity.id === controlledId, targeted: entity.id === targetId, selected: selectedIds.has(entity.id), onSelect: handleSelect }, entity.id))), objects.map((object) => {
424
+ const model = resolveModel(objectModels?.[object.catalogId], assets, {
425
+ seam: "objectModels",
426
+ key: object.catalogId,
427
+ }) ?? tryResolveCatalogModel(object.catalogId, assets);
367
428
  return (_jsx(ObjectMarker, { object: object, custom: renderObject?.(object), model: model, style: objectStyles?.[object.catalogId] }, object.instanceId));
368
429
  })] }));
369
430
  }
431
+ function WorldView({ entitySprites, entityModels, objectModels, objectStyles, environment, assets, renderEntity, renderObject, selectedIds, }) {
432
+ return (_jsxs(_Fragment, { children: [_jsx(WorldEnvironment, { environment: environment }), _jsx(WorldActors, { entitySprites: entitySprites, entityModels: entityModels, objectModels: objectModels, objectStyles: objectStyles, assets: assets, renderEntity: renderEntity, renderObject: renderObject, selectedIds: selectedIds })] }));
433
+ }
370
434
  function RemotePlayers({ rows }) {
371
435
  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))) }));
372
436
  }
373
- function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef, onRuntimeError, multiplayer, serverIdRef, pointerService, pointerAim, pingCommand, }) {
437
+ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef, pointerAxisRef, onRuntimeError, multiplayer, serverIdRef, pointerService, pointerAim, pingCommand, poster, onPosterSettled, }) {
438
+ const posterElapsedRef = useRef(0);
439
+ const posterDoneRef = useRef(false);
374
440
  const motionRef = useRef(createPlayerMotionState());
375
441
  const voxelBodyRef = useRef(null);
376
442
  const solidCacheRef = useRef({ count: -1, set: new Set() });
@@ -381,9 +447,15 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
381
447
  const collision = playable.collision;
382
448
  const movement = playable.movement;
383
449
  const voxelDims = useMemo(() => ({
384
- halfWidth: collision?.halfWidth ?? 0.3,
385
- height: collision?.height ?? 1.8,
386
- stepHeight: collision?.stepHeight ?? 0.6,
450
+ get halfWidth() {
451
+ return collision?.halfWidth ?? 0.3;
452
+ },
453
+ get height() {
454
+ return collision?.height ?? 1.8;
455
+ },
456
+ get stepHeight() {
457
+ return collision?.stepHeight ?? 0.6;
458
+ },
387
459
  }), [collision]);
388
460
  const movementTuning = useMemo(() => resolvePhysicsTuning(playable.game.physics), [playable]);
389
461
  const autoPickupRadius = useMemo(() => {
@@ -398,26 +470,39 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
398
470
  const inputActions = useMemo(() => Object.keys(playable.game.input ?? {}), [playable]);
399
471
  const hasTerrain = useMemo(() => hasEnvironmentTerrain(playable.game.world), [playable]);
400
472
  useFrame((_state, rawDt) => {
473
+ if (poster) {
474
+ if (posterDoneRef.current)
475
+ return;
476
+ posterElapsedRef.current += Math.min(rawDt, 0.05);
477
+ if (posterElapsedRef.current >= POSTER_SETTLE_SECONDS) {
478
+ posterDoneRef.current = true;
479
+ onPosterSettled();
480
+ return;
481
+ }
482
+ }
401
483
  const simStart = performance.now();
402
484
  try {
485
+ let endPhase = devtools.profile.begin("time+input");
403
486
  const dt = Math.min(rawDt, 0.05);
404
487
  const gameDt = ctx.time.advance(dt);
405
488
  ctx.input.publish(heldActionsFor(tracker, inputActions));
406
- if (tracker.isDown("turnLeft"))
407
- yawRef.current += TURN_SPEED * dt;
408
- if (tracker.isDown("turnRight"))
409
- yawRef.current -= TURN_SPEED * dt;
489
+ ctx.input.publishPointer(pointerAxisRef.current);
490
+ const turnInput = (tracker.isDown("turnRight") ? 1 : 0) - (tracker.isDown("turnLeft") ? 1 : 0);
491
+ if (turnInput !== 0)
492
+ yawRef.current = steerYaw(yawRef.current, turnInput, TURN_SPEED, dt);
493
+ endPhase();
410
494
  const playerId = ctx.player.possession.active(ctx.player.userId);
411
495
  const player = ctx.scene.entity.get(playerId);
412
496
  const forwardX = Math.sin(yawRef.current);
413
497
  const forwardZ = Math.cos(yawRef.current);
414
498
  if (player !== null && drivesPose) {
499
+ endPhase = devtools.profile.begin("pose");
415
500
  const keys = createEmptyMovementKeys();
416
501
  keys.w = tracker.isDown("moveForward");
417
502
  keys.s = tracker.isDown("moveBack");
418
503
  keys.a = tracker.isDown("moveLeft");
419
504
  keys.d = tracker.isDown("moveRight");
420
- keys.shift = tracker.isDown("sprint");
505
+ keys.shift = tracker.isDown("sprint") && (movement?.canSprint?.(ctx) ?? true);
421
506
  keys.space = tracker.isDown("jump");
422
507
  const intent = resolveMovementIntent(keys, true);
423
508
  const motionBatch = ctx.player.motion.takePending();
@@ -436,6 +521,7 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
436
521
  const solids = cache.set;
437
522
  const isSolid = (x, y, z) => solids.has(`${x},${y},${z}`);
438
523
  body.velocityY = applyMotionImpulses(body.velocityY, motionBatch);
524
+ [body.velocityX, body.velocityZ] = applyHorizontalImpulses(body.velocityX, body.velocityZ, motionBatch);
439
525
  advanceVoxelPlayer(body, intent, forwardX, forwardZ, player.movement.walkSpeed ?? 2, rawDt, isSolid, voxelDims, movementTuning, hasTerrain ? (x, z) => ground.sampleHeight(x, z) : undefined);
440
526
  if (motionBatch !== null && motionBatch.y !== null)
441
527
  body.y = motionBatch.y;
@@ -450,6 +536,7 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
450
536
  else {
451
537
  const motion = motionRef.current;
452
538
  motion.verticalVelocity = applyMotionImpulses(motion.verticalVelocity, motionBatch);
539
+ [motion.horizontalVelocityX, motion.horizontalVelocityZ] = applyHorizontalImpulses(motion.horizontalVelocityX, motion.horizontalVelocityZ, motionBatch);
453
540
  const step = advancePlayerMotion(motion, intent, forwardX, forwardZ, player.movement.walkSpeed ?? 2, rawDt, movementTuning);
454
541
  let stepX = step.stepX;
455
542
  let stepZ = step.stepZ;
@@ -482,6 +569,7 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
482
569
  current: player.position,
483
570
  next: [nextX, nextY, nextZ],
484
571
  dt: rawDt,
572
+ ctx,
485
573
  };
486
574
  const replacement = movement.beforeCommit(frame);
487
575
  if (replacement !== undefined) {
@@ -498,16 +586,22 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
498
586
  dt: rawDt,
499
587
  });
500
588
  }
589
+ endPhase();
501
590
  }
502
591
  if (autoPickupRadius !== null) {
592
+ endPhase = devtools.profile.begin("pickup");
503
593
  const self = ctx.scene.entity.get(playerId);
504
594
  if (self !== null) {
505
595
  const nearest = ctx.scene.worldItem.nearestInRadius(self.position, autoPickupRadius);
506
596
  if (nearest !== null)
507
597
  ctx.scene.worldItem.pickup(nearest, ctx.player.userId);
508
598
  }
599
+ endPhase();
509
600
  }
510
- playable.loop.onTick(ctx, gameDt);
601
+ devtools.profile.measure("onTick", () => {
602
+ playable.loop.onTick(ctx, gameDt);
603
+ });
604
+ endPhase = devtools.profile.begin("actions");
511
605
  if (tracker.wasPressed("tabTarget")) {
512
606
  if (ctx.game.commands.has("target.cycle"))
513
607
  ctx.game.commands.run("target.cycle", {});
@@ -533,6 +627,18 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
533
627
  }
534
628
  const aimOverride = pointerAim ? pointerAimFor(ctx, pointerService) : undefined;
535
629
  const commandAim = aimOverride ?? { yaw: yawRef.current, pitch: pitchRef.current };
630
+ if (collisionDebug.getState().layers.aimLaser) {
631
+ const aimFrom = ctx.player.possession.active(playerId) ?? playerId;
632
+ collisionDebug.setAimProbe({
633
+ from: aimFrom,
634
+ aim: commandAim,
635
+ originPolicy: { kind: "muzzle" },
636
+ maxDistance: 100,
637
+ });
638
+ }
639
+ else if (collisionDebug.getAimProbe() !== null) {
640
+ collisionDebug.setAimProbe(null);
641
+ }
536
642
  const nowMs = performance.now();
537
643
  for (const action of Object.keys(playable.game.input ?? {})) {
538
644
  const pressed = tracker.wasPressed(action);
@@ -580,8 +686,10 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
580
686
  }
581
687
  }
582
688
  tracker.endFrame();
689
+ endPhase();
583
690
  const serverId = serverIdRef.current;
584
691
  if (multiplayer !== null && serverId !== null) {
692
+ endPhase = devtools.profile.begin("presence");
585
693
  const focus = ctx.scene.entity.get(playerId);
586
694
  if (focus !== null) {
587
695
  multiplayer.backend.presenceSync.syncPose(serverId, {
@@ -592,6 +700,7 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
592
700
  rotationPitch: pitchRef.current,
593
701
  });
594
702
  }
703
+ endPhase();
595
704
  }
596
705
  }
597
706
  catch (error) {
@@ -604,7 +713,7 @@ function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef
604
713
  }, GAME_SIM_FRAME_PRIORITY);
605
714
  return null;
606
715
  }
607
- function HudOnlyDriver({ ctx, playable, tracker, onRuntimeError, }) {
716
+ function HudOnlyDriver({ ctx, playable, tracker, pointerAxisRef, onRuntimeError, }) {
608
717
  const hasReportedTickError = useRef(false);
609
718
  const repeatFiredAtRef = useRef(new Map());
610
719
  const lastFrameRef = useRef(null);
@@ -620,10 +729,16 @@ function HudOnlyDriver({ ctx, playable, tracker, onRuntimeError, }) {
620
729
  const rawDt = (now - last) / 1000;
621
730
  const simStart = performance.now();
622
731
  try {
732
+ let endPhase = devtools.profile.begin("time+input");
623
733
  const dt = Math.min(rawDt, 0.05);
624
734
  const gameDt = ctx.time.advance(dt);
625
735
  ctx.input.publish(heldActionsFor(tracker, inputActions));
626
- playable.loop.onTick(ctx, gameDt);
736
+ ctx.input.publishPointer(pointerAxisRef.current);
737
+ endPhase();
738
+ devtools.profile.measure("onTick", () => {
739
+ playable.loop.onTick(ctx, gameDt);
740
+ });
741
+ endPhase = devtools.profile.begin("actions");
627
742
  const nowMs = performance.now();
628
743
  for (const action of inputActions) {
629
744
  if (!shouldFireBoundAction(tracker, action, playable.game.input, repeatFiredAtRef.current, nowMs))
@@ -632,6 +747,7 @@ function HudOnlyDriver({ ctx, playable, tracker, onRuntimeError, }) {
632
747
  dispatchBoundAction(ctx, action, 0, 0, { yaw: 0, pitch: 0 });
633
748
  }
634
749
  tracker.endFrame();
750
+ endPhase();
635
751
  }
636
752
  catch (error) {
637
753
  if (!hasReportedTickError.current) {
@@ -643,7 +759,7 @@ function HudOnlyDriver({ ctx, playable, tracker, onRuntimeError, }) {
643
759
  };
644
760
  frameId = requestAnimationFrame(tick);
645
761
  return () => cancelAnimationFrame(frameId);
646
- }, [ctx, playable, tracker, onRuntimeError, inputActions]);
762
+ }, [ctx, playable, tracker, pointerAxisRef, onRuntimeError, inputActions]);
647
763
  return null;
648
764
  }
649
765
  class GameUiErrorBoundary extends Component {
@@ -651,8 +767,8 @@ class GameUiErrorBoundary extends Component {
651
767
  static getDerivedStateFromError() {
652
768
  return { failed: true };
653
769
  }
654
- componentDidCatch(error) {
655
- this.props.onRuntimeError(error, "ui-render");
770
+ componentDidCatch(error, info) {
771
+ this.props.onRuntimeError(error, "ui-render", info.componentStack ?? undefined);
656
772
  }
657
773
  render() {
658
774
  if (this.state.failed)
@@ -660,16 +776,53 @@ class GameUiErrorBoundary extends Component {
660
776
  return this.props.children;
661
777
  }
662
778
  }
663
- function DiagnosticOverlay({ diagnostics }) {
779
+ function expandedReactError(message) {
780
+ return /Minified React error #185\b/.test(message)
781
+ ? "Maximum update depth exceeded. A component is repeatedly updating state during render or after every update."
782
+ : null;
783
+ }
784
+ function diagnosticReport(diagnostic, gameName) {
785
+ const explanation = expandedReactError(diagnostic.message);
786
+ return [
787
+ "JGengine runtime error",
788
+ `Game: ${gameName}`,
789
+ `Engine: ${VERSION}`,
790
+ `Phase: ${diagnostic.phase}`,
791
+ `Time: ${diagnostic.capturedAt}`,
792
+ `Page: ${window.location.origin}${window.location.pathname}`,
793
+ `Browser: ${navigator.userAgent}`,
794
+ explanation === null ? null : `Explanation: ${explanation}`,
795
+ "",
796
+ `Message: ${diagnostic.message}`,
797
+ diagnostic.stack === undefined ? null : `JavaScript stack:\n${diagnostic.stack}`,
798
+ diagnostic.componentStack === undefined ? null : `React component stack:\n${diagnostic.componentStack}`,
799
+ ]
800
+ .filter((line) => line !== null)
801
+ .join("\n");
802
+ }
803
+ function DiagnosticOverlay({ diagnostics, gameName }) {
804
+ const [copied, setCopied] = useState(false);
664
805
  if (diagnostics.length === 0)
665
806
  return null;
666
807
  const latest = diagnostics[diagnostics.length - 1];
667
- return (_jsxs("div", { className: "pointer-events-auto absolute right-4 top-4 z-50 max-w-lg rounded border border-red-400/60 bg-red-950/95 p-3 text-xs text-red-50 shadow-2xl", children: [_jsx("div", { className: "mb-1 font-semibold uppercase tracking-wide text-red-200", children: "JG engine error" }), _jsxs("div", { className: "font-mono text-[11px] text-red-100", children: ["[", latest.phase, "] ", latest.message] }), latest.stack !== undefined ? (_jsx("pre", { className: "mt-2 max-h-36 overflow-auto whitespace-pre-wrap text-[10px] text-red-200/80", children: latest.stack })) : null] }));
808
+ const explanation = expandedReactError(latest.message);
809
+ const report = diagnosticReport(latest, gameName);
810
+ const issueBody = report.length > 8000 ? `${report.slice(0, 8000)}\n\n[Report truncated; use Copy error for the full report.]` : report;
811
+ const issueUrl = `https://github.com/Noisemaker111/jgengine/issues/new?title=${encodeURIComponent(`[BUG] ${latest.phase}: ${latest.message.slice(0, 100)}`)}&body=${encodeURIComponent(issueBody)}`;
812
+ return (_jsxs("div", { className: "pointer-events-auto absolute right-4 top-4 z-50 max-w-lg rounded border border-red-400/60 bg-red-950/95 p-3 text-xs text-red-50 shadow-2xl", children: [_jsx("div", { className: "mb-1 font-semibold uppercase tracking-wide text-red-200", children: "JG engine error" }), _jsxs("div", { className: "font-mono text-[11px] text-red-100", children: ["[", latest.phase, "] ", latest.message] }), explanation !== null ? _jsx("div", { className: "mt-2 text-red-100", children: explanation }) : null, latest.stack !== undefined ? (_jsx("pre", { className: "mt-2 max-h-36 overflow-auto whitespace-pre-wrap text-[10px] text-red-200/80", children: latest.stack })) : null, latest.componentStack !== undefined ? (_jsx("pre", { className: "mt-2 max-h-36 overflow-auto whitespace-pre-wrap text-[10px] text-red-200/80", children: latest.componentStack })) : null, _jsxs("div", { className: "mt-3 flex gap-2", children: [_jsx("button", { className: "rounded border border-red-300/50 bg-red-900 px-2 py-1 font-semibold hover:bg-red-800", type: "button", onClick: () => {
813
+ void navigator.clipboard.writeText(report).then(() => {
814
+ setCopied(true);
815
+ window.setTimeout(() => setCopied(false), 2000);
816
+ });
817
+ }, children: copied ? "Copied" : "Copy error" }), _jsx("a", { className: "rounded border border-red-300/50 bg-red-900 px-2 py-1 font-semibold hover:bg-red-800", href: issueUrl, rel: "noreferrer", target: "_blank", children: "File issue" })] })] }));
668
818
  }
669
- export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null, }) {
819
+ const POSTER_SETTLE_SECONDS = 1.6;
820
+ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null, poster = false, onContextReady, }) {
670
821
  const multiplayer = useMemo(() => (rawMultiplayer === null ? null : withDevtoolsLatency(rawMultiplayer)), [rawMultiplayer]);
671
- const devtoolsEnabled = playable.devtools !== false;
822
+ const devtoolsEnabled = playable.devtools !== false && !poster;
672
823
  const [devtoolsOpen, setDevtoolsOpen] = useState(false);
824
+ const [posterFrozen, setPosterFrozen] = useState(false);
825
+ const posterSettledRef = useRef(false);
673
826
  const [ctx, setCtx] = useState(null);
674
827
  const [diagnostics, setDiagnostics] = useState([]);
675
828
  const [remotePlayers, setRemotePlayers] = useState([]);
@@ -680,25 +833,55 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
680
833
  const cameraDraggingRef = useRef(false);
681
834
  const primaryClickRef = useRef(false);
682
835
  const pointerDownRef = useRef(null);
836
+ const pointerAxisRef = useRef(null);
683
837
  const marqueeStartRef = useRef(null);
838
+ const f2HeldRef = useRef(false);
839
+ const f2ChordedRef = useRef(false);
684
840
  const pointerService = useMemo(() => createPointerService(), []);
685
841
  const selection = useMemo(() => createSelectionSet(), [playable]);
686
842
  const [marquee, setMarquee] = useState(null);
687
843
  const [selectedIds, setSelectedIds] = useState(() => new Set());
688
844
  const [contextMenu, setContextMenu] = useState(null);
689
- const tracker = useMemo(() => createActionStateTracker(toActionStateBindingMap(withTouchCodes(playable.game.input))), [playable]);
845
+ const settingsStore = useMemo(() => createSettingsStore(), []);
846
+ const [bindingOverrides, setBindingOverrides] = useState(() => loadBindingOverrides(playable.game.name));
847
+ useEffect(() => {
848
+ setBindingOverrides(loadBindingOverrides(playable.game.name));
849
+ }, [playable]);
850
+ const effectiveInput = useMemo(() => applyBindingOverrides(playable.game.input ?? {}, bindingOverrides), [playable, bindingOverrides]);
851
+ const rebindAction = useCallback((action, code) => setBindingOverrides(saveBindingOverride(playable.game.name, action, [code])), [playable]);
852
+ const resetActionBinding = useCallback((action) => setBindingOverrides(clearBindingOverride(playable.game.name, action)), [playable]);
853
+ const tracker = useMemo(() => createActionStateTracker(toActionStateBindingMap(withTouchCodes(effectiveInput))), [effectiveInput]);
854
+ const graphics = useGraphicsSettings(settingsStore, playable.shadows ?? true);
855
+ const trackPointerAxis = (event) => {
856
+ const rect = wrapperRef.current?.getBoundingClientRect();
857
+ if (rect === undefined)
858
+ return;
859
+ pointerAxisRef.current = normalizePointerToAxis(event.clientX, event.clientY, rect);
860
+ };
861
+ const deactivatePointerAxis = () => {
862
+ const state = pointerAxisRef.current;
863
+ if (state !== null && state.active)
864
+ pointerAxisRef.current = { ...state, active: false };
865
+ };
690
866
  const touchScheme = useMemo(() => deriveTouchScheme(playable.game.input, {
691
867
  reserved: RESERVED_INPUT_ACTIONS,
692
868
  firstPerson: resolveRigKind(playable.camera) === "first",
693
869
  config: playable.touch,
694
870
  }), [playable]);
695
- const { coarsePointer } = useDisplayProfile();
871
+ const { coarsePointer, portrait, compact } = useDisplayProfile();
696
872
  const touchSink = useMemo(() => ({ onCodeDown: (code) => tracker.handleDown(code), onCodeUp: (code) => tracker.handleUp(code) }), [tracker]);
697
873
  const audioEngine = useMemo(() => createAudioEngine({ sounds: playable.audio?.sounds, buses: playable.audio?.buses }), [playable]);
698
874
  useEffect(() => () => audioEngine.dispose(), [audioEngine]);
875
+ useEffect(() => {
876
+ if (ctx === null)
877
+ return;
878
+ return ctx.game.events.on("audio.play", ({ sound, at }) => {
879
+ audioEngine.playOneShot(sound, at === undefined ? undefined : { x: at[0], y: at[1], z: at[2] });
880
+ });
881
+ }, [ctx, audioEngine]);
699
882
  const userId = multiplayer?.userId ?? DEV_USER_ID;
700
- const reportRuntimeError = (error, phase) => {
701
- const diagnostic = logRuntimeError(error, phase);
883
+ const reportRuntimeError = (error, phase, componentStack) => {
884
+ const diagnostic = logRuntimeError(error, phase, componentStack);
702
885
  setDiagnostics((current) => [...current.slice(-4), { ...diagnostic, id: Date.now() + current.length }]);
703
886
  };
704
887
  useEffect(() => {
@@ -712,6 +895,7 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
712
895
  });
713
896
  playable.loop.onInit(context);
714
897
  playable.loop.onNewPlayer(context);
898
+ onContextReady?.(context);
715
899
  setCtx(context);
716
900
  }
717
901
  catch (error) {
@@ -829,13 +1013,29 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
829
1013
  return (_jsxs("div", { ref: wrapperRef, tabIndex: 0, className: "relative h-full w-full bg-neutral-950 outline-none", onKeyDown: (event) => {
830
1014
  if (event.code === "F2" && devtoolsEnabled) {
831
1015
  event.preventDefault();
832
- setDevtoolsOpen((current) => !current);
1016
+ f2HeldRef.current = true;
1017
+ f2ChordedRef.current = false;
1018
+ return;
1019
+ }
1020
+ if (f2HeldRef.current) {
1021
+ f2ChordedRef.current = true;
833
1022
  return;
834
1023
  }
835
1024
  if (event.code === "Tab" || event.code === "Space")
836
1025
  event.preventDefault();
837
1026
  tracker.handleDown(event.code);
838
- }, onKeyUp: (event) => tracker.handleUp(event.code), onBlur: () => tracker.reset(), children: [_jsx(HudOnlyDriver, { ctx: ctx, playable: playable, tracker: tracker, onRuntimeError: reportRuntimeError }), _jsx(GameUiErrorBoundary, { onRuntimeError: reportRuntimeError, children: _jsx(GameProvider, { context: ctx, children: _jsx(GameUI, {}) }) }), devtoolsEnabled ? (_jsx(DevtoolsOverlay, { open: devtoolsOpen, ctx: ctx, playable: playable, multiplayer: multiplayer })) : null, _jsx(DiagnosticOverlay, { diagnostics: diagnostics })] }));
1027
+ }, onKeyUp: (event) => {
1028
+ if (event.code === "F2" && devtoolsEnabled) {
1029
+ f2HeldRef.current = false;
1030
+ if (!f2ChordedRef.current)
1031
+ setDevtoolsOpen((current) => !current);
1032
+ return;
1033
+ }
1034
+ tracker.handleUp(event.code);
1035
+ }, onBlur: () => {
1036
+ f2HeldRef.current = false;
1037
+ tracker.reset();
1038
+ }, onPointerMove: trackPointerAxis, onPointerLeave: deactivatePointerAxis, onPointerCancel: deactivatePointerAxis, children: [_jsx(HudOnlyDriver, { ctx: ctx, playable: playable, tracker: tracker, pointerAxisRef: pointerAxisRef, onRuntimeError: reportRuntimeError }), _jsx(GameUiErrorBoundary, { onRuntimeError: reportRuntimeError, children: _jsx(GameProvider, { context: ctx, children: _jsx(HudViewportProvider, { platforms: playable.platforms, config: playable.hudFit, userScale: graphics.uiScale, children: _jsx(GameUI, {}) }) }) }), devtoolsEnabled ? (_jsx(DevtoolsOverlay, { open: devtoolsOpen, ctx: ctx, playable: playable, multiplayer: multiplayer })) : null, _jsx(DiagnosticOverlay, { diagnostics: diagnostics, gameName: playable.game.name })] }));
839
1039
  }
840
1040
  const firstPerson = rigKind === "first";
841
1041
  const showReticle = (firstPerson && playable.camera?.firstPerson?.reticle !== false) || rigKind === "shoulder";
@@ -864,6 +1064,7 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
864
1064
  const effectiveSky = backdropSky ?? worldSky;
865
1065
  const backgroundColor = backdrop?.background ?? (effectiveSky === undefined ? DEFAULT_BACKGROUND_COLOR : undefined);
866
1066
  const lighting = playable.lighting;
1067
+ const orthographic = playable.camera?.projection === "orthographic";
867
1068
  const localXY = (event) => {
868
1069
  const rect = wrapperRef.current?.getBoundingClientRect();
869
1070
  return rect === undefined
@@ -887,6 +1088,7 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
887
1088
  };
888
1089
  const handlePointerDown = (event) => {
889
1090
  wrapperRef.current?.focus();
1091
+ trackPointerAxis(event);
890
1092
  audioEngine.resume();
891
1093
  if (contextMenu !== null)
892
1094
  setContextMenu(null);
@@ -898,6 +1100,7 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
898
1100
  }
899
1101
  };
900
1102
  const handlePointerMove = (event) => {
1103
+ trackPointerAxis(event);
901
1104
  if (pointer?.select !== true)
902
1105
  return;
903
1106
  const start = marqueeStartRef.current;
@@ -996,33 +1199,90 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
996
1199
  ctx.game.commands.run(verb.command, contextVerbInput(state.menu, verb));
997
1200
  }
998
1201
  };
999
- return (_jsxs("div", { ref: wrapperRef, tabIndex: 0, className: "relative h-full w-full bg-neutral-950 outline-none", onKeyDown: (event) => {
1000
- if (event.code === "F2" && devtoolsEnabled) {
1001
- event.preventDefault();
1002
- document.exitPointerLock?.();
1003
- setDevtoolsOpen((current) => !current);
1004
- return;
1005
- }
1006
- if (event.code === "Tab" || event.code === "Space")
1007
- event.preventDefault();
1008
- tracker.handleDown(event.code);
1009
- }, onKeyUp: (event) => tracker.handleUp(event.code), onBlur: () => tracker.reset(), onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, onContextMenu: handleContextMenu, onWheel: (event) => {
1010
- if (ctx === null || !event.shiftKey)
1011
- return;
1012
- event.preventDefault();
1013
- if (event.deltaY < 0 && ctx.game.commands.has("ui.hotbarScrollNext")) {
1014
- ctx.game.commands.run("ui.hotbarScrollNext", {});
1015
- }
1016
- else if (event.deltaY > 0 && ctx.game.commands.has("ui.hotbarScrollPrev")) {
1017
- ctx.game.commands.run("ui.hotbarScrollPrev", {});
1018
- }
1019
- }, children: [_jsxs(Canvas, { camera: {
1020
- fov: playable.camera?.frustum?.fov ?? CAMERA_FRUSTUM_DEFAULTS.fov,
1021
- near: playable.camera?.frustum?.near ?? CAMERA_FRUSTUM_DEFAULTS.near,
1022
- far: playable.camera?.frustum?.far ?? CAMERA_FRUSTUM_DEFAULTS.far,
1023
- }, shadows: playable.shadows ?? true, gl: { preserveDrawingBuffer: true }, style: { touchAction: "none" }, children: [backgroundColor !== undefined ? _jsx("color", { attach: "background", args: [backgroundColor] }) : null, effectiveSky === undefined ? (lighting !== undefined ? (_jsx(ConfiguredLighting, { lighting: lighting })) : (_jsxs(_Fragment, { children: [_jsx("ambientLight", { intensity: 0.55 }), _jsx("directionalLight", { position: [10, 16, 6], intensity: 1.3 })] }))) : effectiveSky.timeOfDay ? (_jsx(TimeOfDayDaylight, { sky: effectiveSky, clock: ctx.time })) : backdropSky !== undefined ? (_jsx(SkyDaylight, { sky: backdropSky })) : null, _jsx(BackdropFog, { fog: backdrop?.fog }), _jsxs(GameProvider, { context: ctx, children: [_jsx(WorldView, { entitySprites: playable.entitySprites, entityModels: playable.entityModels, objectModels: playable.objectModels, objectStyles: playable.objectStyles, environment: AutoEnvironment, assets: playable.game.assets, renderEntity: playable.renderEntity, renderObject: playable.renderObject, selectedIds: selectedIds }), WorldOverlay !== undefined ? _jsx(WorldOverlay, {}) : null, barsStatId !== null ? (_jsx(WorldEntityBars, { statId: barsStatId, roles: barsRoles, resolveRole: resolveEntityRole })) : 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, panKeysEnabled: rtsPanKeysEnabled, director: ctx.camera, onDragChange: (dragging) => {
1024
- cameraDraggingRef.current = dragging;
1025
- } }), _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(DevtoolsRendererProbe, {})] }), coarsePointer && touchScheme !== null && (touchScheme.gestures !== null || touchScheme.look) ? (_jsx(TouchPlaySurface, { scheme: touchScheme, sink: touchSink, yawRef: yawRef, pitchRef: pitchRef, maxPitch: playable.camera?.firstPerson?.maxPitch ?? 1.45, onPrimaryTap: () => {
1026
- primaryClickRef.current = true;
1027
- } })) : null, _jsx(GameUiErrorBoundary, { onRuntimeError: reportRuntimeError, children: _jsx(GameProvider, { context: ctx, children: _jsx(GameUI, {}) }) }), showReticle ? _jsx(Reticle, {}) : null, coarsePointer && touchScheme !== null && (touchScheme.joystick !== null || touchScheme.buttons.length > 0) ? (_jsx(TouchControlsDock, { scheme: touchScheme, sink: touchSink })) : 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, devtoolsEnabled ? (_jsx(DevtoolsOverlay, { open: devtoolsOpen, ctx: ctx, playable: playable, multiplayer: multiplayer })) : null, _jsx(DiagnosticOverlay, { diagnostics: diagnostics })] }));
1202
+ const controlsActive = playControlsActive(ctx);
1203
+ const settingsDisabled = playable.settings === false;
1204
+ const settingsConfig = playable.settings === false || playable.settings === undefined ? {} : playable.settings;
1205
+ const settingsSurface = settingsDisabled ? false : settingsConfig.surface ?? false;
1206
+ const settingsVariant = settingsConfig.variant ?? "panel";
1207
+ const hideCategories = settingsDisabled ? BUILT_IN_SETTING_CATEGORIES : settingsConfig.hide ?? [];
1208
+ const fovControlEnabled = !orthographic && playable.camera?.playerFov?.control !== false;
1209
+ const settingsHostsFov = !settingsDisabled && !hideCategories.includes("gameplay") && fovControlEnabled;
1210
+ const settingsActions = settingsDisabled
1211
+ ? []
1212
+ : (settingsConfig.actions ?? []).map((action) => ({
1213
+ id: action.id,
1214
+ label: action.label,
1215
+ kind: action.kind ?? "default",
1216
+ description: action.description,
1217
+ run: () => action.run(ctx),
1218
+ }));
1219
+ const touchScale = compact ? 0.88 : 1;
1220
+ const dockMounted = !poster &&
1221
+ coarsePointer &&
1222
+ controlsActive &&
1223
+ touchScheme !== null &&
1224
+ (touchScheme.joystick !== null || touchScheme.buttons.length > 0);
1225
+ const orientationMismatch = !poster &&
1226
+ coarsePointer &&
1227
+ playable.orientation !== undefined &&
1228
+ (playable.orientation === "landscape") === portrait;
1229
+ return (_jsx(SettingsProvider, { store: settingsStore, children: _jsxs(PlayerFovProvider, { config: playable.camera, orthographic: orthographic, children: [_jsx(AudioSettingsBridge, { store: settingsStore, engine: audioEngine, buses: playable.audio?.buses }), _jsx(SettingsRuntime, { variant: settingsVariant, surface: settingsSurface, actions: settingsActions, input: playable.game.input ?? {}, buses: playable.audio?.buses, extra: settingsConfig.extra ?? [], categories: settingsConfig.categories ?? [], hide: hideCategories, fovEnabled: fovControlEnabled, hideBindings: settingsConfig.hideBindings ?? [], overrides: bindingOverrides, rebind: rebindAction, resetBinding: resetActionBinding, children: _jsxs("div", { ref: wrapperRef, tabIndex: 0, ...(poster && posterFrozen ? { "data-poster-ready": "" } : {}), className: "relative h-full w-full bg-neutral-950 outline-none", style: {
1230
+ "--jg-hud-dock-clearance": `${dockMounted ? touchDockClearance(touchScheme, touchScale) : 0}px`,
1231
+ }, onKeyDown: (event) => {
1232
+ if (event.code === "F2" && devtoolsEnabled) {
1233
+ event.preventDefault();
1234
+ f2HeldRef.current = true;
1235
+ f2ChordedRef.current = false;
1236
+ return;
1237
+ }
1238
+ if (f2HeldRef.current) {
1239
+ f2ChordedRef.current = true;
1240
+ return;
1241
+ }
1242
+ if (event.code === "Tab" || event.code === "Space")
1243
+ event.preventDefault();
1244
+ tracker.handleDown(event.code);
1245
+ }, onKeyUp: (event) => {
1246
+ if (event.code === "F2" && devtoolsEnabled) {
1247
+ f2HeldRef.current = false;
1248
+ if (!f2ChordedRef.current) {
1249
+ document.exitPointerLock?.();
1250
+ setDevtoolsOpen((current) => !current);
1251
+ }
1252
+ return;
1253
+ }
1254
+ tracker.handleUp(event.code);
1255
+ }, onBlur: () => {
1256
+ f2HeldRef.current = false;
1257
+ tracker.reset();
1258
+ }, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, onPointerLeave: deactivatePointerAxis, onPointerCancel: deactivatePointerAxis, onContextMenu: handleContextMenu, onWheel: (event) => {
1259
+ if (ctx === null || !event.shiftKey)
1260
+ return;
1261
+ event.preventDefault();
1262
+ if (event.deltaY < 0 && ctx.game.commands.has("ui.hotbarScrollNext")) {
1263
+ ctx.game.commands.run("ui.hotbarScrollNext", {});
1264
+ }
1265
+ else if (event.deltaY > 0 && ctx.game.commands.has("ui.hotbarScrollPrev")) {
1266
+ ctx.game.commands.run("ui.hotbarScrollPrev", {});
1267
+ }
1268
+ }, children: [_jsxs(Canvas, { frameloop: poster && posterFrozen ? "demand" : "always", orthographic: orthographic, camera: orthographic
1269
+ ? {
1270
+ zoom: playable.camera?.frustum?.zoom ?? CAMERA_FRUSTUM_DEFAULTS.zoom,
1271
+ near: playable.camera?.frustum?.near ?? CAMERA_FRUSTUM_DEFAULTS.near,
1272
+ far: playable.camera?.frustum?.far ?? CAMERA_FRUSTUM_DEFAULTS.far,
1273
+ }
1274
+ : {
1275
+ fov: playable.camera?.frustum?.fov ?? CAMERA_FRUSTUM_DEFAULTS.fov,
1276
+ near: playable.camera?.frustum?.near ?? CAMERA_FRUSTUM_DEFAULTS.near,
1277
+ far: playable.camera?.frustum?.far ?? CAMERA_FRUSTUM_DEFAULTS.far,
1278
+ }, shadows: graphics.shadows, dpr: graphics.dpr, gl: { preserveDrawingBuffer: true }, style: { touchAction: "none" }, children: [backgroundColor !== undefined ? _jsx("color", { attach: "background", args: [backgroundColor] }) : null, lighting !== undefined ? (_jsx(ConfiguredLighting, { lighting: lighting })) : effectiveSky === undefined ? (_jsxs(_Fragment, { children: [_jsx("ambientLight", { intensity: 0.55 }), _jsx("directionalLight", { position: [10, 16, 6], intensity: 1.3 })] })) : null, effectiveSky !== undefined ? (effectiveSky.timeOfDay ? (_jsx(TimeOfDayDaylight, { sky: effectiveSky, clock: ctx.time, lights: skyEmitsLights(resolveSkyLightOwnership(lighting !== undefined)) })) : (_jsx(SkyDaylight, { sky: effectiveSky, lights: skyEmitsLights(resolveSkyLightOwnership(lighting !== undefined)) }))) : null, _jsx(BackdropFog, { fog: backdrop?.fog }), _jsxs(GameProvider, { context: ctx, children: [_jsx(CullingProvider, { config: playable.visibility, children: _jsx(WorldView, { entitySprites: playable.entitySprites, entityModels: playable.entityModels, objectModels: playable.objectModels, objectStyles: playable.objectStyles, environment: AutoEnvironment, assets: playable.game.assets, renderEntity: playable.renderEntity, renderObject: playable.renderObject, selectedIds: selectedIds }) }), WorldOverlay !== undefined ? _jsx(WorldOverlay, {}) : null, barsStatId !== null ? (_jsx(WorldEntityBars, { statId: barsStatId, roles: barsRoles, resolveRole: resolveEntityRole })) : null, _jsx(WorldItems, { config: playable.worldItem }), _jsx(WorldTelegraphs, {}), _jsx(WorldFloatText, {}), _jsx(ProjectileTracers, {}), devtoolsEnabled ? _jsx(CollisionDebugWorld, {}) : null, _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, panKeysEnabled: rtsPanKeysEnabled, director: ctx.camera, onDragChange: (dragging) => {
1279
+ cameraDraggingRef.current = dragging;
1280
+ } }), _jsx(PointerProbe, { service: pointerService })] }), _jsx(RemotePlayers, { rows: remotePlayers }), _jsx(FrameDriver, { ctx: ctx, playable: playable, tracker: tracker, yawRef: yawRef, pitchRef: pitchRef, primaryClickRef: primaryClickRef, pointerAxisRef: pointerAxisRef, onRuntimeError: reportRuntimeError, multiplayer: multiplayer, serverIdRef: serverIdRef, pointerService: pointerService, pointerAim: pointer?.aim === true, pingCommand: pointer?.pingCommand, poster: poster, onPosterSettled: () => {
1281
+ if (posterSettledRef.current)
1282
+ return;
1283
+ posterSettledRef.current = true;
1284
+ setPosterFrozen(true);
1285
+ } }), _jsx(DevtoolsRendererProbe, {})] }), !poster && coarsePointer && controlsActive && touchScheme !== null && (touchScheme.gestures !== null || touchScheme.look) ? (_jsx(TouchPlaySurface, { scheme: touchScheme, sink: touchSink, yawRef: yawRef, pitchRef: pitchRef, maxPitch: playable.camera?.firstPerson?.maxPitch ?? 1.45, onPrimaryTap: () => {
1286
+ primaryClickRef.current = true;
1287
+ } })) : null, _jsx(GameUiErrorBoundary, { onRuntimeError: reportRuntimeError, children: _jsx(GameProvider, { context: ctx, children: _jsx(HudViewportProvider, { platforms: playable.platforms, config: playable.hudFit, userScale: graphics.uiScale, children: _jsx(GameUI, {}) }) }) }), !poster && showReticle ? _jsx(Reticle, {}) : null, dockMounted && touchScheme !== null ? (_jsx(TouchControlsDock, { scheme: touchScheme, sink: touchSink, scale: touchScale })) : null, orientationMismatch && playable.orientation !== undefined ? (_jsx(OrientationHint, { wanted: playable.orientation })) : 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, devtoolsEnabled ? (_jsx(DevtoolsOverlay, { open: devtoolsOpen, ctx: ctx, playable: playable, multiplayer: multiplayer })) : null, poster ? null : _jsx(DiagnosticOverlay, { diagnostics: diagnostics, gameName: playable.game.name }), poster || orthographic || settingsHostsFov ? null : _jsx(PlayerFovSlider, {}), poster ? null : _jsx(SettingsChrome, {})] }) })] }) }));
1028
1288
  }