@jgengine/shell 0.9.0 → 0.11.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 (248) hide show
  1. package/CHANGELOG.md +34 -3
  2. package/LICENSE +203 -661
  3. package/README.md +1 -1
  4. package/dist/GamePhaseStamp.d.ts +1 -0
  5. package/dist/GamePhaseStamp.js +9 -0
  6. package/dist/GamePlayerShell.d.ts +4 -31
  7. package/dist/GamePlayerShell.js +162 -932
  8. package/dist/GameUiPreview.js +0 -2
  9. package/dist/audio/audioEngine.d.ts +8 -0
  10. package/dist/audio/audioEngine.js +63 -8
  11. package/dist/audio/audioWire.d.ts +22 -0
  12. package/dist/audio/audioWire.js +19 -0
  13. package/dist/audio/musicDirector.d.ts +31 -0
  14. package/dist/audio/musicDirector.js +125 -0
  15. package/dist/audio/musicVoices.d.ts +10 -0
  16. package/dist/audio/musicVoices.js +338 -0
  17. package/dist/audio/synthEngine.d.ts +9 -0
  18. package/dist/audio/synthEngine.js +57 -0
  19. package/dist/behaviour.d.ts +2 -1
  20. package/dist/behaviour.js +2 -1
  21. package/dist/behaviourAttach.d.ts +1 -0
  22. package/dist/behaviourAttach.js +1 -0
  23. package/dist/behaviourDriver.d.ts +1 -0
  24. package/dist/behaviourDriver.js +1 -0
  25. package/dist/boundActionDispatch.d.ts +13 -0
  26. package/dist/boundActionDispatch.js +37 -0
  27. package/dist/camera/GameCameraRig.d.ts +5 -2
  28. package/dist/camera/GameCameraRig.js +2 -2
  29. package/dist/camera/GameFirstPersonCamera.d.ts +12 -2
  30. package/dist/camera/GameFirstPersonCamera.js +29 -7
  31. package/dist/camera/GameInspectionCamera.d.ts +1 -1
  32. package/dist/camera/GameInspectionCamera.js +13 -2
  33. package/dist/camera/GameOrbitCamera.js +59 -10
  34. package/dist/camera/cameraBlendMath.d.ts +3 -0
  35. package/dist/camera/cameraBlendMath.js +3 -0
  36. package/dist/camera/fovPreference.d.ts +6 -1
  37. package/dist/camera/fovPreference.js +6 -1
  38. package/dist/camera/index.d.ts +4 -7
  39. package/dist/camera/index.js +4 -7
  40. package/dist/camera/inspectionCameraMath.d.ts +7 -2
  41. package/dist/camera/inspectionCameraMath.js +7 -2
  42. package/dist/camera/orbitCameraMath.d.ts +54 -6
  43. package/dist/camera/orbitCameraMath.js +61 -6
  44. package/dist/camera/rigMath.d.ts +94 -24
  45. package/dist/camera/rigMath.js +100 -24
  46. package/dist/camera/rigResolve.d.ts +4 -2
  47. package/dist/camera/rigResolve.js +4 -2
  48. package/dist/camera/shakeChannel.d.ts +6 -2
  49. package/dist/camera/shakeChannel.js +6 -2
  50. package/dist/camera/shakeChannelMath.d.ts +1 -0
  51. package/dist/camera/shakeChannelMath.js +1 -0
  52. package/dist/cartridge/runtime.d.ts +15 -0
  53. package/dist/cartridge/runtime.js +556 -0
  54. package/dist/cartridge/spec.d.ts +188 -0
  55. package/dist/cartridge/spec.js +19 -0
  56. package/dist/cartridge/validate.d.ts +2 -0
  57. package/dist/cartridge/validate.js +78 -0
  58. package/dist/cartridge.d.ts +1 -1
  59. package/dist/cartridge.js +4 -3
  60. package/dist/commandSink.d.ts +25 -0
  61. package/dist/commandSink.js +32 -0
  62. package/dist/defineGame.js +26 -5
  63. package/dist/devtools/ColPanel.d.ts +1 -0
  64. package/dist/devtools/ColPanel.js +14 -0
  65. package/dist/devtools/CollisionDebugWorld.js +1 -0
  66. package/dist/devtools/DevtoolsOverlay.d.ts +7 -3
  67. package/dist/devtools/DevtoolsOverlay.js +19 -450
  68. package/dist/devtools/KeysPanel.d.ts +4 -0
  69. package/dist/devtools/KeysPanel.js +31 -0
  70. package/dist/devtools/LogsPanel.d.ts +1 -0
  71. package/dist/devtools/LogsPanel.js +12 -0
  72. package/dist/devtools/NetPanel.d.ts +4 -0
  73. package/dist/devtools/NetPanel.js +10 -0
  74. package/dist/devtools/PerfPanel.d.ts +4 -0
  75. package/dist/devtools/PerfPanel.js +53 -0
  76. package/dist/devtools/TunePanel.d.ts +3 -0
  77. package/dist/devtools/TunePanel.js +309 -0
  78. package/dist/devtools/agentBridge.d.ts +41 -0
  79. package/dist/devtools/agentBridge.js +262 -0
  80. package/dist/devtools/collisionDebug.d.ts +6 -0
  81. package/dist/devtools/collisionDebug.js +6 -0
  82. package/dist/devtools/collisionDebugMath.d.ts +9 -2
  83. package/dist/devtools/collisionDebugMath.js +25 -5
  84. package/dist/devtools/devtoolsOverrides.d.ts +6 -0
  85. package/dist/devtools/devtoolsOverrides.js +48 -0
  86. package/dist/devtools/panelAtoms.d.ts +9 -0
  87. package/dist/devtools/panelAtoms.js +10 -0
  88. package/dist/devtools/perfDiagnose.d.ts +2 -0
  89. package/dist/devtools/perfDiagnose.js +43 -0
  90. package/dist/diagnostics/RuntimeDiagnostics.d.ts +29 -0
  91. package/dist/diagnostics/RuntimeDiagnostics.js +69 -0
  92. package/dist/drivers/FrameDriver.d.ts +37 -0
  93. package/dist/drivers/FrameDriver.js +232 -0
  94. package/dist/drivers/HudOnlyDriver.d.ts +16 -0
  95. package/dist/drivers/HudOnlyDriver.js +60 -0
  96. package/dist/environment/Daylight.d.ts +29 -7
  97. package/dist/environment/Daylight.js +153 -13
  98. package/dist/environment/EnvironmentScene.js +42 -7
  99. package/dist/environment/RoadRibbons.d.ts +7 -0
  100. package/dist/environment/RoadRibbons.js +57 -0
  101. package/dist/environment/VolumetricClouds.d.ts +17 -0
  102. package/dist/environment/VolumetricClouds.js +202 -0
  103. package/dist/environment/daylightCycle.d.ts +3 -1
  104. package/dist/environment/daylightCycle.js +3 -1
  105. package/dist/environment/groundPadMath.d.ts +3 -0
  106. package/dist/environment/groundPadMath.js +3 -0
  107. package/dist/environment/index.d.ts +1 -0
  108. package/dist/environment/index.js +1 -0
  109. package/dist/environment/skyLightingPolicy.d.ts +2 -0
  110. package/dist/environment/skyLightingPolicy.js +2 -0
  111. package/dist/hotbarActions.d.ts +26 -0
  112. package/dist/hotbarActions.js +27 -0
  113. package/dist/inputSink.d.ts +30 -0
  114. package/dist/inputSink.js +85 -0
  115. package/dist/materialOverride.d.ts +10 -0
  116. package/dist/materialOverride.js +15 -3
  117. package/dist/multiplayer.js +14 -4
  118. package/dist/postfx/PostProcessing.d.ts +10 -0
  119. package/dist/postfx/PostProcessing.js +120 -0
  120. package/dist/postfx/gradeShader.d.ts +4 -0
  121. package/dist/postfx/gradeShader.js +72 -0
  122. package/dist/registry.d.ts +3 -2
  123. package/dist/render/EnvironmentLighting.d.ts +10 -0
  124. package/dist/render/EnvironmentLighting.js +34 -0
  125. package/dist/render/SceneLighting.d.ts +7 -0
  126. package/dist/render/SceneLighting.js +18 -0
  127. package/dist/render/SceneModels.d.ts +14 -0
  128. package/dist/render/SceneModels.js +337 -0
  129. package/dist/render/defaultSurface.d.ts +29 -0
  130. package/dist/render/defaultSurface.js +101 -0
  131. package/dist/render/modelLoad.d.ts +24 -0
  132. package/dist/render/modelLoad.js +73 -0
  133. package/dist/render/modelRender.d.ts +11 -0
  134. package/dist/render/modelRender.js +15 -0
  135. package/dist/render/resolveModel.d.ts +39 -3
  136. package/dist/render/resolveModel.js +79 -2
  137. package/dist/render/useEntityRenderCues.d.ts +13 -0
  138. package/dist/render/useEntityRenderCues.js +62 -0
  139. package/dist/scatter/InstancedScatter.d.ts +24 -0
  140. package/dist/scatter/InstancedScatter.js +130 -0
  141. package/dist/scatter/scatterModels.d.ts +25 -0
  142. package/dist/scatter/scatterModels.js +61 -0
  143. package/dist/scatter/scatterProxies.d.ts +6 -0
  144. package/dist/scatter/scatterProxies.js +94 -0
  145. package/dist/scatter.d.ts +1 -0
  146. package/dist/scatter.js +1 -0
  147. package/dist/scene/AuthoredScene.d.ts +82 -0
  148. package/dist/scene/AuthoredScene.js +173 -0
  149. package/dist/scene/GeneratedAssetRenderer.d.ts +14 -0
  150. package/dist/scene/GeneratedAssetRenderer.js +59 -0
  151. package/dist/scene/GrassFieldRenderer.d.ts +2 -0
  152. package/dist/scene/GrassFieldRenderer.js +25 -0
  153. package/dist/scene/SoilPatchRenderer.d.ts +2 -0
  154. package/dist/scene/SoilPatchRenderer.js +21 -0
  155. package/dist/scene/StudioStage.d.ts +37 -0
  156. package/dist/scene/StudioStage.js +42 -0
  157. package/dist/scene/WaterRenderer.d.ts +2 -0
  158. package/dist/scene/WaterRenderer.js +37 -0
  159. package/dist/scene/builtinSceneKindRenderers.d.ts +2 -0
  160. package/dist/scene/builtinSceneKindRenderers.js +22 -0
  161. package/dist/scene/sceneKindRenderers.d.ts +28 -0
  162. package/dist/scene/sceneKindRenderers.js +17 -0
  163. package/dist/scene.d.ts +1 -0
  164. package/dist/scene.js +1 -0
  165. package/dist/settings/appliedSettings.d.ts +5 -0
  166. package/dist/settings/appliedSettings.js +9 -0
  167. package/dist/settings/settingsController.d.ts +2 -0
  168. package/dist/settings/settingsController.js +22 -1
  169. package/dist/shellConstants.d.ts +4 -0
  170. package/dist/shellConstants.js +4 -0
  171. package/dist/shellMovement.d.ts +8 -0
  172. package/dist/shellMovement.js +11 -0
  173. package/dist/shellPointer.d.ts +20 -0
  174. package/dist/shellPointer.js +28 -0
  175. package/dist/structures/PlacementGhost.d.ts +4 -0
  176. package/dist/structures/PlacementGhost.js +4 -0
  177. package/dist/structures/TransformGizmo.d.ts +43 -0
  178. package/dist/structures/TransformGizmo.js +55 -0
  179. package/dist/structures/index.d.ts +1 -0
  180. package/dist/structures/index.js +1 -0
  181. package/dist/terrain/CarvedTerrain.d.ts +4 -1
  182. package/dist/terrain/CarvedTerrain.js +4 -3
  183. package/dist/terrain/GrassField.js +1 -1
  184. package/dist/terrain/TerraformBrushCursor.js +3 -0
  185. package/dist/terrain/grassBudget.d.ts +11 -2
  186. package/dist/terrain/grassBudget.js +20 -4
  187. package/dist/terrain/grassGeometry.d.ts +3 -0
  188. package/dist/terrain/grassGeometry.js +3 -0
  189. package/dist/terrain/grassMaterial.d.ts +2 -0
  190. package/dist/terrain/grassMaterial.js +2 -0
  191. package/dist/terrain/random.d.ts +3 -0
  192. package/dist/terrain/random.js +3 -0
  193. package/dist/terrain/soilPatchMaterial.d.ts +10 -0
  194. package/dist/terrain/soilPatchMaterial.js +109 -0
  195. package/dist/terrain/terrainDetailMaterial.d.ts +31 -0
  196. package/dist/terrain/terrainDetailMaterial.js +157 -0
  197. package/dist/terrain/terrainMath.d.ts +8 -1
  198. package/dist/terrain/terrainMath.js +8 -1
  199. package/dist/touch/TouchControlsOverlay.d.ts +9 -5
  200. package/dist/touch/TouchControlsOverlay.js +211 -22
  201. package/dist/useShellMultiplayerSync.d.ts +9 -0
  202. package/dist/useShellMultiplayerSync.js +98 -0
  203. package/dist/visibility/CullingProvider.js +1 -1
  204. package/dist/water/OceanConfig.d.ts +2 -0
  205. package/dist/water/OceanConfig.js +2 -0
  206. package/dist/weather/fireSpreadPose.d.ts +1 -0
  207. package/dist/weather/fireSpreadPose.js +1 -0
  208. package/dist/weather/weatherGeometry.d.ts +1 -0
  209. package/dist/weather/weatherGeometry.js +1 -0
  210. package/dist/weather/weatherMath.d.ts +3 -0
  211. package/dist/weather/weatherMath.js +3 -0
  212. package/dist/weather/weatherUniforms.d.ts +3 -0
  213. package/dist/weather/weatherUniforms.js +3 -0
  214. package/dist/world/RetainedVfx.d.ts +10 -0
  215. package/dist/world/RetainedVfx.js +136 -0
  216. package/dist/world/WorldHud.d.ts +52 -3
  217. package/dist/world/WorldHud.js +94 -9
  218. package/dist/world/WorldScene.d.ts +21 -0
  219. package/dist/world/WorldScene.js +159 -0
  220. package/dist/world/WorldVfx.d.ts +9 -0
  221. package/dist/world/WorldVfx.js +184 -0
  222. package/dist/world/entityPose.d.ts +2 -0
  223. package/dist/world/entityPose.js +2 -0
  224. package/dist/world/floatTextStyle.d.ts +1 -0
  225. package/dist/world/floatTextStyle.js +1 -0
  226. package/dist/world/telegraphPulse.d.ts +1 -0
  227. package/dist/world/telegraphPulse.js +1 -0
  228. package/dist/world/worldBarSamples.d.ts +26 -1
  229. package/dist/world/worldBarSamples.js +45 -1
  230. package/dist/worldSky.d.ts +5 -0
  231. package/dist/worldSky.js +11 -0
  232. package/dist/worldSync.d.ts +11 -0
  233. package/dist/worldSync.js +20 -0
  234. package/package.json +6 -6
  235. package/skills/jgengine/SKILL.md +51 -0
  236. package/skills/jgengine/api.md +434 -0
  237. package/skills/jgengine/capabilities.md +29 -0
  238. package/skills/jgengine/reference-cartridge.md +72 -0
  239. package/skills/jgengine-ui/SKILL.md +36 -0
  240. package/skills/jgengine-ui/api.md +1852 -0
  241. package/skills/jgengine-ui/capabilities.md +142 -0
  242. package/skills/jgengine-ui/reference.md +500 -0
  243. package/skills/jgengine-ui/references/visual-scorecard.md +73 -0
  244. package/skills/jgengine-world/SKILL.md +38 -0
  245. package/skills/jgengine-world/api.md +2652 -0
  246. package/skills/jgengine-world/capabilities.md +370 -0
  247. package/skills/jgengine-world/reference.md +222 -0
  248. package/llms.txt +0 -2643
@@ -1,53 +1,47 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Canvas, useFrame, useLoader } from "@react-three/fiber";
3
- import { Component, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
4
- import * as THREE from "three";
5
- import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
6
- import { actionRepeatMs, createActionStateTracker, hotbarSlotActionIndex, resolveActionCommand, shouldDispatchAction, toActionStateBindingMap, } from "@jgengine/core/input/actionBindings";
7
- import { deriveTouchScheme, withTouchCodes } from "@jgengine/core/input/touchScheme";
8
- import { buildContextMenu, contextVerbInput, } from "@jgengine/core/interaction/contextMenu";
9
- import { resolveActivePrompt } from "@jgengine/core/interaction/proximityPrompt";
10
- import { aimToPoint } from "@jgengine/core/input/pointer";
2
+ import { Canvas } from "@react-three/fiber";
3
+ import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
4
+ import { createActionStateTracker, toActionStateBindingMap, } from "@jgengine/core/input/actionBindings";
5
+ import { RESERVED_INPUT_ACTIONS } from "./boundActionDispatch.js";
6
+ import { resolveWorldSky } from "./worldSky.js";
7
+ import { pointerAimFor, pointerContextMenu } from "./shellPointer.js";
8
+ import { deriveTouchScheme, withTouchCodes, DEFAULT_TOUCH_STYLE } from "@jgengine/core/input/touchScheme";
9
+ import { contextVerbInput, } from "@jgengine/core/interaction/contextMenu";
11
10
  import { normalizePointerToAxis } from "@jgengine/core/input/pointerAxis";
12
11
  import { createSelectionSet, isMarquee, screenRect, selectWithinRect, } from "@jgengine/core/scene/selection";
13
- import { advancePlayerMotion, constrainStepToAxis, createEmptyMovementKeys, createPlayerMotionState, resolveMovementIntent, resolveObstacleStep, snapPositionToGrid, } from "@jgengine/core/movement/movementModel";
14
- import { steerYaw } from "@jgengine/core/movement/steering";
15
- import { advanceVoxelPlayer, createVoxelPlayerBody, } from "@jgengine/core/movement/voxelController";
16
12
  import { createGameContext } from "@jgengine/core/runtime/gameContext";
17
- import { applyHorizontalImpulses } from "@jgengine/core/runtime/motionIntents";
18
- import { groundFieldFor } from "@jgengine/core/world/terrain";
19
- import { objectVisualScale } from "@jgengine/core/scene/objectStore";
20
- import { DEFAULT_PICKUP_RADIUS, WORLD_ITEM_ENTITY_NAME } from "@jgengine/core/game/worldItem";
21
- import { useGameContext } from "@jgengine/react/provider";
13
+ import { DEFAULT_PICKUP_RADIUS } from "@jgengine/core/game/worldItem";
22
14
  import { useDisplayProfile } from "@jgengine/react/display";
23
15
  import { HudViewportProvider } from "@jgengine/react/hudViewport";
24
- import { useSceneEntities, useSceneObjects, usePlayer, useTarget } from "@jgengine/react/hooks";
16
+ import { GameViewportProvider } from "@jgengine/react/gameViewport";
17
+ import { RotateDeviceScreen } from "@jgengine/react/rotateDevice";
25
18
  import { GameProvider } from "@jgengine/react/provider";
26
19
  import { CAMERA_FRUSTUM_DEFAULTS } from "@jgengine/core/game/playableGame";
27
20
  import { createSettingsStore } from "@jgengine/core/settings/settingsModel";
28
21
  import { applyBindingOverrides, clearBindingOverride, loadBindingOverrides, saveBindingOverride, } from "@jgengine/core/input/bindingOverrides";
29
22
  import { playControlsActive } from "@jgengine/core/game/controlGate";
23
+ import { orientationGateActive, orientationHintActive, resolveOrientationRequirement, } from "@jgengine/core/ui/orientation";
30
24
  import { sky as resolveSkyDescriptor } from "@jgengine/core/world/features";
31
- import { devtools } from "@jgengine/core/devtools/devtools";
32
- import { VERSION } from "@jgengine/core/meta/changelog";
25
+ import { resolveGameLook } from "@jgengine/core/render/lookPreset";
33
26
  import { AudioListener, EntityAudioEmitters, ObjectAudioEmitters } from "./audio/AudioComponents.js";
34
27
  import { createAudioEngine } from "./audio/audioEngine.js";
28
+ import { attachAudioEventWire } from "./audio/audioWire.js";
29
+ import { PostProcessing } from "./postfx/PostProcessing.js";
30
+ import { EnvironmentLighting } from "./render/EnvironmentLighting.js";
35
31
  import { CollisionDebugWorld } from "./devtools/CollisionDebugWorld.js";
36
- import { collisionDebug } from "./devtools/collisionDebug.js";
37
32
  import { DevtoolsOverlay, DevtoolsRendererProbe, withDevtoolsLatency } from "./devtools/DevtoolsOverlay.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";
33
+ import { installAgentBridge } from "./devtools/agentBridge.js";
34
+ import { GameCameraRig, PlayerFovProvider, PlayerFovSlider, resolveRigKind, rtsPanKeysConflict, } from "./camera/index.js";
35
+ import { CullingProvider } from "./visibility/CullingProvider.js";
41
36
  import { SkyDaylight, TimeOfDayDaylight } from "./environment.js";
42
37
  import { resolveSkyLightOwnership, skyEmitsLights } from "./environment/skyLightingPolicy.js";
43
38
  import { EnvironmentScene } from "./environment/EnvironmentScene.js";
44
- import { applyMaterialOverride } from "./materialOverride.js";
45
39
  import { PointerProbe } from "./pointer/PointerProbe.js";
46
- import { applyPaintTextureToMaterials, cacheStandardMaterials, cloneModelScene, createPaintCanvas, disposeClonedMaterials, syncPaintCanvas, } from "./render/modelRender.js";
47
- import { writeEntityPose } from "./world/entityPose.js";
48
40
  import { MarqueeBox, ContextMenuView } from "./pointer/PointerOverlays.js";
49
- import { createPointerService, POINTER_ENTITY_KEY, POINTER_OBJECT_KEY, } from "./pointer/pointerService.js";
50
- import { CombatCameraShake, ProjectileTracers, Reticle, WorldEntityBars, WorldFloatText, WorldTelegraphs, } from "./world/WorldHud.js";
41
+ import { createPointerService } from "./pointer/pointerService.js";
42
+ import { CombatCameraShake, ProjectileTracers, Reticle, WorldEntityBars, WorldFloatText, WorldNameplates, WorldTelegraphs, } from "./world/WorldHud.js";
43
+ import { WorldSpellVfx } from "./world/WorldVfx.js";
44
+ import { RetainedVfx } from "./world/RetainedVfx.js";
51
45
  import { GridWorldScene } from "./world/GridWorldScene.js";
52
46
  import { WorldItems } from "./world/WorldItems.js";
53
47
  import { OrientationHint } from "./touch/OrientationHint.js";
@@ -56,771 +50,34 @@ import { BUILT_IN_SETTING_CATEGORIES } from "@jgengine/core/settings/settingsMod
56
50
  import { SettingsProvider } from "@jgengine/react/settings";
57
51
  import { SettingsRuntime } from "./settings/SettingsRuntime.js";
58
52
  import { SettingsChrome } from "./settings/SettingsChrome.js";
59
- import { AudioSettingsBridge, useGraphicsSettings } from "./settings/appliedSettings.js";
53
+ import { AudioSettingsBridge, useGraphicsSettings, useTouchStyle } from "./settings/appliedSettings.js";
54
+ import { ConfiguredLighting, BackdropFog } from "./render/SceneLighting.js";
55
+ import { WorldView, RemotePlayers } from "./world/WorldScene.js";
56
+ import { FrameDriver } from "./drivers/FrameDriver.js";
57
+ import { HudOnlyDriver } from "./drivers/HudOnlyDriver.js";
58
+ import { DiagnosticOverlay, GameUiErrorBoundary, logRuntimeError, } from "./diagnostics/RuntimeDiagnostics.js";
59
+ import { GamePhaseStamp } from "./GamePhaseStamp.js";
60
+ import { EMPTY_RESERVED } from "./shellConstants.js";
61
+ import { useShellMultiplayerSync } from "./useShellMultiplayerSync.js";
60
62
  const DEV_USER_ID = "dev-player";
61
- const TURN_SPEED = 2.4;
62
63
  const PRIMARY_CLICK_MOVE_THRESHOLD_PX = 6;
63
- const GROUND_SIZE = 160;
64
- const GROUND_SEGMENTS = 80;
65
64
  const DEFAULT_BACKGROUND_COLOR = "#14161b";
66
- const DEFAULT_BACKDROP_FOG_COLOR = "#1a1c22";
67
- function errorToDiagnostic(error, phase, componentStack) {
68
- const capturedAt = new Date().toISOString();
69
- if (error instanceof Error) {
70
- return { phase, message: error.message, stack: error.stack, componentStack, capturedAt };
71
- }
72
- return { phase, message: typeof error === "string" ? error : JSON.stringify(error), componentStack, capturedAt };
73
- }
74
- function logRuntimeError(error, phase, componentStack) {
75
- const diagnostic = errorToDiagnostic(error, phase, componentStack);
76
- console.error(`[jgengine:${phase}] ${diagnostic.message}`, error);
77
- return diagnostic;
78
- }
79
- const RESERVED_INPUT_ACTIONS = new Set([
80
- "moveForward",
81
- "moveBack",
82
- "moveLeft",
83
- "moveRight",
84
- "turnLeft",
85
- "turnRight",
86
- "sprint",
87
- "jump",
88
- "tabTarget",
89
- "clearTarget",
90
- "useAbility",
91
- "interact",
92
- ]);
93
- const SHELL_MOVEMENT_ACTIONS = ["moveForward", "moveBack", "moveLeft", "moveRight", "jump"];
94
- function shellDrivesPlayerPose(input) {
95
- const bound = input ?? {};
96
- return SHELL_MOVEMENT_ACTIONS.some((action) => action in bound);
97
- }
98
- function findHotbarSlotActions(input) {
99
- return Object.keys(input ?? {}).flatMap((action) => {
100
- const slot = hotbarSlotActionIndex(action);
101
- return slot === null ? [] : [{ action, slot }];
102
- });
103
- }
104
- function hotbarIdFor(playable) {
105
- const declarations = Object.entries(playable.game.inventories ?? {});
106
- const hud = declarations.find(([, declaration]) => declaration.hud === "hotbar");
107
- return (hud ?? declarations[0])?.[0] ?? null;
108
- }
109
- function executeHotbarSlot(ctx, fromId, hotbarId, slot, yaw, pitch, aimOverride) {
110
- const stack = ctx.player.inventory.state(hotbarId).slots[slot];
111
- if (stack === undefined || stack === null)
112
- return { ok: false, error: `Hotbar slot ${slot + 1} is empty` };
113
- const result = ctx.item.use.use({
114
- from: fromId,
115
- itemId: stack.itemId,
116
- inventoryId: hotbarId,
117
- aim: aimOverride ?? { yaw, pitch },
118
- });
119
- return result.error === undefined ? { ok: true } : { ok: false, error: result.error };
120
- }
121
- function pointerAimFor(ctx, service) {
122
- const hit = service.worldHit();
123
- if (hit === null)
124
- return undefined;
125
- const player = ctx.scene.entity.get(ctx.player.userId);
126
- const origin = player === null ? hit.point : player.position;
127
- return aimToPoint([origin[0], origin[1] + 1, origin[2]], hit.point);
128
- }
129
- function pointerContextMenu(ctx, playable, hit) {
130
- if (hit.entity !== null) {
131
- const entity = ctx.scene.entity.get(hit.entity);
132
- const verbs = entity === null ? undefined : playable.content.entityById?.(entity.name)?.verbs;
133
- return buildContextMenu({ kind: "entity", targetId: hit.entity, verbs, point: hit.point });
134
- }
135
- if (hit.object !== null) {
136
- const verbs = ctx.scene.object.catalog(hit.object)?.verbs;
137
- return buildContextMenu({ kind: "object", targetId: hit.object, verbs, point: hit.point });
138
- }
139
- return null;
140
- }
141
- /** Actions from `input` currently held down, for `ctx.input.publish` (#164.1); includes reserved movement/jump actions. */
142
- export function heldActionsFor(tracker, actions) {
143
- return actions.filter((action) => tracker.isDown(action));
144
- }
145
- /** Whether a bound action should fire this frame: on press, or on repeat interval while held (shared by `FrameDriver` and `HudOnlyDriver`). */
146
- export function shouldFireBoundAction(tracker, action, input, repeatFiredAt, now) {
147
- return shouldDispatchAction({
148
- pressed: tracker.wasPressed(action),
149
- down: tracker.isDown(action),
150
- repeatMs: actionRepeatMs(input?.[action]),
151
- lastFiredAt: repeatFiredAt.get(action) ?? null,
152
- now,
153
- });
154
- }
155
- /** Resolves and runs the command bound to `action` via the shell's action→command convention (shared by `FrameDriver` and `HudOnlyDriver`). */
156
- export function dispatchBoundAction(ctx, action, yaw, pitch, aim) {
157
- const command = resolveActionCommand(action, (name) => ctx.game.commands.has(name), RESERVED_INPUT_ACTIONS);
158
- if (command !== null)
159
- ctx.game.commands.run(command, { yaw, pitch, aim });
160
- }
161
- const OBSTACLE_GATHER_RADIUS = 3;
162
- /** Placed scene objects within `radius` of `center`, as `CollisionObstacle`s for `resolveObstacleStep` (#162.1). */
163
- export function nearbyObstacles(objects, center, radius = OBSTACLE_GATHER_RADIUS) {
164
- const radiusSq = radius * radius;
165
- const result = [];
166
- for (const object of objects) {
167
- const dx = object.position[0] - center[0];
168
- const dz = object.position[2] - center[2];
169
- if (dx * dx + dz * dz <= radiusSq)
170
- result.push({ position: object.position });
171
- }
172
- return result;
173
- }
174
- /** Applies a pending `MotionIntentBatch` to a vertical velocity: impulses add, then `verticalVelocity` replaces the result outright (#162.4). */
175
- export function applyMotionImpulses(currentVelocity, batch) {
176
- if (batch === null)
177
- return currentVelocity;
178
- let velocity = currentVelocity;
179
- for (const impulse of batch.impulses)
180
- velocity += impulse;
181
- return batch.verticalVelocity ?? velocity;
182
- }
183
- /** The world's declared sky, when its world feature is an environment with one (#196.1). */
184
- export function resolveWorldSky(world) {
185
- return world?.kind === "environment" ? world.sky : undefined;
186
- }
187
- /**
188
- * Maps the game's declared `physics` onto the movement controllers' tuning
189
- * overrides. `PhysicsConfig.gravity` is a signed world acceleration (negative
190
- * points down, matching every game's config and the Y-up convention), but the
191
- * controllers integrate `velocityY -= gravityAcceleration * dt` and so expect a
192
- * positive downward magnitude. Negating here is what keeps a down-pointing
193
- * gravity pulling the player *down*; passing the signed value straight through
194
- * flipped the sign and launched airborne players upward instead.
195
- */
196
- export function resolvePhysicsTuning(physics) {
197
- if (physics === undefined)
198
- return undefined;
199
- return {
200
- get gravityAcceleration() {
201
- return physics.gravity === undefined ? undefined : -physics.gravity;
202
- },
203
- get jumpVelocity() {
204
- return physics.jumpVelocity;
205
- },
206
- };
207
- }
208
- /** True when the world is an environment feature with terrain, so the voxel controller should sample its height. */
209
- export function hasEnvironmentTerrain(world) {
210
- return world?.kind === "environment" && (world.terrain !== undefined || (world.islands?.length ?? 0) > 0);
211
- }
212
- function colorFromId(id) {
213
- let hash = 0;
214
- for (let index = 0; index < id.length; index += 1)
215
- hash = (hash * 31 + id.charCodeAt(index)) >>> 0;
216
- return `hsl(${hash % 360}, 65%, 55%)`;
217
- }
218
- function ConfiguredLighting({ lighting }) {
219
- return (_jsxs(_Fragment, { children: [lighting.ambient !== undefined ? (_jsx("ambientLight", { color: lighting.ambient.color, intensity: lighting.ambient.intensity ?? 0.55 })) : null, lighting.hemisphere !== undefined ? (_jsx("hemisphereLight", { args: [
220
- lighting.hemisphere.skyColor ?? "#bfe3ff",
221
- lighting.hemisphere.groundColor ?? "#4c6b34",
222
- lighting.hemisphere.intensity ?? 0.55,
223
- ] })) : null, (lighting.directional ?? []).map((entry, index) => (_jsx("directionalLight", { position: [entry.position[0], entry.position[1], entry.position[2]], intensity: entry.intensity ?? 1.3, color: entry.color, castShadow: entry.castShadow ?? false }, index)))] }));
224
- }
225
- function BackdropFog({ fog }) {
226
- if (fog === undefined)
227
- return null;
228
- return fog.density !== undefined ? (_jsx("fogExp2", { attach: "fog", args: [fog.color ?? DEFAULT_BACKDROP_FOG_COLOR, fog.density] })) : (_jsx("fog", { attach: "fog", args: [fog.color ?? DEFAULT_BACKDROP_FOG_COLOR, fog.near ?? 10, fog.far ?? 200] }));
229
- }
230
- function EntitySprite({ sprite }) {
231
- const texture = useLoader(THREE.TextureLoader, sprite.url);
232
- useEffect(() => {
233
- texture.colorSpace = THREE.SRGBColorSpace;
234
- texture.anisotropy = 4;
235
- texture.needsUpdate = true;
236
- }, [texture]);
237
- return (_jsx("sprite", { "position-y": sprite.y, scale: [sprite.width, sprite.height, 1], children: _jsx("spriteMaterial", { map: texture, transparent: true, alphaTest: 0.08, depthWrite: false }) }));
238
- }
239
- function EntityModel({ model, instanceId }) {
240
- const gltf = useLoader(GLTFLoader, model.url);
241
- const ctx = useGameContext();
242
- const material = model.material;
243
- const scale = model.scale ?? 1;
244
- const baseY = model.y ?? 0;
245
- const dims = model.dims;
246
- const centered = (model.anchor ?? "center") === "center" && dims !== undefined;
247
- const position = centered
248
- ? [-scale * dims.center.x, baseY - scale * dims.minY, -scale * dims.center.z]
249
- : [0, baseY, 0];
250
- const scene = useMemo(() => {
251
- const cloned = cloneModelScene(gltf.scene);
252
- if (material !== undefined)
253
- applyMaterialOverride(cloned, material, { clone: false });
254
- return cloned;
255
- }, [gltf, material]);
256
- useEffect(() => () => {
257
- disposeClonedMaterials(scene);
258
- }, [scene]);
259
- const animation = model.animation;
260
- const mixerRef = useRef(null);
261
- const animationPausedRef = useRef(false);
262
- useEffect(() => {
263
- if (animation === undefined || gltf.animations.length === 0) {
264
- mixerRef.current = null;
265
- return;
266
- }
267
- const mixer = new THREE.AnimationMixer(scene);
268
- const clip = (animation.clip !== undefined ? THREE.AnimationClip.findByName(gltf.animations, animation.clip) : undefined) ??
269
- gltf.animations[0];
270
- const action = mixer.clipAction(clip);
271
- action.setLoop(animation.loop === false ? THREE.LoopOnce : THREE.LoopRepeat, Infinity);
272
- action.clampWhenFinished = animation.loop === false;
273
- action.timeScale = animation.timeScale ?? 1;
274
- action.enabled = true;
275
- action.paused = animation.paused === true;
276
- action.play();
277
- if (animation.time !== undefined)
278
- action.time = animation.time;
279
- mixer.update(0);
280
- mixerRef.current = mixer;
281
- animationPausedRef.current = animation.paused === true;
282
- return () => {
283
- mixer.stopAllAction();
284
- mixerRef.current = null;
285
- };
286
- }, [scene, gltf, animation?.clip, animation?.loop, animation?.timeScale, animation?.paused, animation?.time]);
287
- const paintCanvasRef = useRef(null);
288
- const paintDrawnCountRef = useRef(0);
289
- const paintVersionRef = useRef(-1);
290
- const materialCacheRef = useRef(null);
291
- useEffect(() => {
292
- paintCanvasRef.current = null;
293
- paintDrawnCountRef.current = 0;
294
- paintVersionRef.current = -1;
295
- materialCacheRef.current = null;
296
- }, [scene]);
297
- useFrame((_state, delta) => {
298
- if (mixerRef.current !== null && !animationPausedRef.current)
299
- mixerRef.current.update(delta);
300
- if (instanceId === undefined)
301
- return;
302
- const paint = ctx.scene.entity.paint;
303
- const version = paint.version(instanceId);
304
- if (version === paintVersionRef.current)
305
- return;
306
- paintVersionRef.current = version;
307
- const strokes = paint.strokes(instanceId);
308
- const cache = cacheStandardMaterials(scene, materialCacheRef.current);
309
- materialCacheRef.current = cache;
310
- if (paintCanvasRef.current === null) {
311
- if (strokes.length === 0)
312
- return;
313
- const seed = cache.materials[0];
314
- if (seed === undefined)
315
- return;
316
- const paintCanvas = createPaintCanvas(seed);
317
- paintCanvasRef.current = paintCanvas;
318
- applyPaintTextureToMaterials(cache.materials, paintCanvas);
319
- }
320
- paintDrawnCountRef.current = syncPaintCanvas(paintCanvasRef.current, cache.seedColor, strokes, paintDrawnCountRef.current);
321
- });
322
- return _jsx("primitive", { object: scene, position: position, scale: [scale, scale, scale] });
323
- }
324
- function EntityMarker({ entity, custom, model, sprite, isLocal, targeted, selected, onSelect, }) {
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) => {
343
- event.stopPropagation();
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] }));
350
- }
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;
356
- const [scaleX, scaleY, scaleZ] = objectVisualScale(object.visual);
357
- const color = object.visual?.color ?? style?.color ?? colorFromId(object.catalogId);
358
- const opacity = object.visual?.opacity ?? style?.opacity ?? 1;
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 })] })) }));
370
- }
371
- function GroundPlane() {
372
- const geometry = useMemo(() => {
373
- const next = new THREE.PlaneGeometry(GROUND_SIZE, GROUND_SIZE, GROUND_SEGMENTS, GROUND_SEGMENTS);
374
- const positions = next.attributes.position;
375
- for (let index = 0; index < positions.count; index += 1) {
376
- const x = positions.getX(index);
377
- const y = positions.getY(index);
378
- const height = Math.sin(x * 0.16) * 0.12 +
379
- Math.cos(y * 0.11) * 0.1 +
380
- Math.sin((x + y) * 0.05) * 0.16;
381
- positions.setZ(index, height);
382
- }
383
- next.computeVertexNormals();
384
- return next;
385
- }, []);
386
- return (_jsx("mesh", { "rotation-x": -Math.PI / 2, geometry: geometry, children: _jsx("meshStandardMaterial", { color: "#283729", roughness: 0.9, metalness: 0 }) }));
387
- }
388
- function RockField() {
389
- const rocks = useMemo(() => Array.from({ length: 18 }, (_, index) => {
390
- const angle = index * 2.399963229728653;
391
- const radius = 10 + ((index * 17) % 58);
392
- return {
393
- id: `rock-${index}`,
394
- x: Math.cos(angle) * radius,
395
- z: Math.sin(angle) * radius,
396
- scale: 0.45 + ((index * 13) % 11) / 10,
397
- rotation: angle,
398
- };
399
- }), []);
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))) }));
401
- }
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, }) {
408
- const ctx = useGameContext();
409
- const entities = useSceneEntities();
410
- const objects = useSceneObjects();
411
- const player = usePlayer();
412
- const targetId = useTarget(player.userId);
413
- const controlledId = ctx.player.possession.active(player.userId);
414
- const handleSelect = (entity) => {
415
- const relation = ctx.scene.entity.canReceive(entity.id, "damage") === null ? "hostile" : "friendly";
416
- ctx.scene.entity.setTarget(controlledId, relation === "hostile" || entity.role === "npc" ? entity.id : null);
417
- };
418
- return (_jsxs(_Fragment, { children: [entities
419
- .filter((entity) => entity.name !== WORLD_ITEM_ENTITY_NAME)
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);
428
- return (_jsx(ObjectMarker, { object: object, custom: renderObject?.(object), model: model, style: objectStyles?.[object.catalogId] }, object.instanceId));
429
- })] }));
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
- }
434
- function RemotePlayers({ rows }) {
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))) }));
436
- }
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);
440
- const motionRef = useRef(createPlayerMotionState());
441
- const voxelBodyRef = useRef(null);
442
- const solidCacheRef = useRef({ count: -1, set: new Set() });
443
- const hasReportedTickError = useRef(false);
444
- const repeatFiredAtRef = useRef(new Map());
445
- const slotActions = useMemo(() => findHotbarSlotActions(playable.game.input), [playable]);
446
- const hotbarId = useMemo(() => hotbarIdFor(playable), [playable]);
447
- const collision = playable.collision;
448
- const movement = playable.movement;
449
- const voxelDims = useMemo(() => ({
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
- },
459
- }), [collision]);
460
- const movementTuning = useMemo(() => resolvePhysicsTuning(playable.game.physics), [playable]);
461
- const autoPickupRadius = useMemo(() => {
462
- const cfg = playable.worldItem?.autoPickup;
463
- if (cfg === undefined || cfg === false)
464
- return null;
465
- const fallback = playable.worldItem?.pickupRadius ?? DEFAULT_PICKUP_RADIUS;
466
- return cfg === true ? fallback : cfg.radius ?? fallback;
467
- }, [playable]);
468
- const ground = useMemo(() => groundFieldFor(playable.game.world), [playable]);
469
- const drivesPose = useMemo(() => shellDrivesPlayerPose(playable.game.input), [playable]);
470
- const inputActions = useMemo(() => Object.keys(playable.game.input ?? {}), [playable]);
471
- const hasTerrain = useMemo(() => hasEnvironmentTerrain(playable.game.world), [playable]);
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
- }
483
- const simStart = performance.now();
484
- try {
485
- let endPhase = devtools.profile.begin("time+input");
486
- const dt = Math.min(rawDt, 0.05);
487
- const gameDt = ctx.time.advance(dt);
488
- ctx.input.publish(heldActionsFor(tracker, inputActions));
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();
494
- const playerId = ctx.player.possession.active(ctx.player.userId);
495
- const player = ctx.scene.entity.get(playerId);
496
- const forwardX = Math.sin(yawRef.current);
497
- const forwardZ = Math.cos(yawRef.current);
498
- if (player !== null && drivesPose) {
499
- endPhase = devtools.profile.begin("pose");
500
- const keys = createEmptyMovementKeys();
501
- keys.w = tracker.isDown("moveForward");
502
- keys.s = tracker.isDown("moveBack");
503
- keys.a = tracker.isDown("moveLeft");
504
- keys.d = tracker.isDown("moveRight");
505
- keys.shift = tracker.isDown("sprint") && (movement?.canSprint?.(ctx) ?? true);
506
- keys.space = tracker.isDown("jump");
507
- const intent = resolveMovementIntent(keys, true);
508
- const motionBatch = ctx.player.motion.takePending();
509
- if (collision?.voxel) {
510
- let body = voxelBodyRef.current;
511
- if (body === null) {
512
- body = createVoxelPlayerBody(player.position[0], player.position[1], player.position[2]);
513
- voxelBodyRef.current = body;
514
- }
515
- const objects = ctx.scene.object.list();
516
- const cache = solidCacheRef.current;
517
- if (cache.count !== objects.length) {
518
- cache.set = new Set(objects.map((o) => `${o.position[0]},${o.position[1]},${o.position[2]}`));
519
- cache.count = objects.length;
520
- }
521
- const solids = cache.set;
522
- const isSolid = (x, y, z) => solids.has(`${x},${y},${z}`);
523
- body.velocityY = applyMotionImpulses(body.velocityY, motionBatch);
524
- [body.velocityX, body.velocityZ] = applyHorizontalImpulses(body.velocityX, body.velocityZ, motionBatch);
525
- advanceVoxelPlayer(body, intent, forwardX, forwardZ, player.movement.walkSpeed ?? 2, rawDt, isSolid, voxelDims, movementTuning, hasTerrain ? (x, z) => ground.sampleHeight(x, z) : undefined);
526
- if (motionBatch !== null && motionBatch.y !== null)
527
- body.y = motionBatch.y;
528
- ctx.scene.entity.setPose(playerId, {
529
- position: [body.x, body.y, body.z],
530
- rotationY: intent.moving
531
- ? Math.atan2(body.velocityX, body.velocityZ)
532
- : player.rotationY,
533
- dt: rawDt,
534
- });
535
- }
536
- else {
537
- const motion = motionRef.current;
538
- motion.verticalVelocity = applyMotionImpulses(motion.verticalVelocity, motionBatch);
539
- [motion.horizontalVelocityX, motion.horizontalVelocityZ] = applyHorizontalImpulses(motion.horizontalVelocityX, motion.horizontalVelocityZ, motionBatch);
540
- const step = advancePlayerMotion(motion, intent, forwardX, forwardZ, player.movement.walkSpeed ?? 2, rawDt, movementTuning);
541
- let stepX = step.stepX;
542
- let stepZ = step.stepZ;
543
- if (movement?.mode === "axis") {
544
- const constrained = constrainStepToAxis(stepX, stepZ, movement.axis ?? "x");
545
- stepX = constrained.stepX;
546
- stepZ = constrained.stepZ;
547
- }
548
- if (movement?.collideObjects === true) {
549
- const obstacles = nearbyObstacles(ctx.scene.object.list(), player.position);
550
- const resolved = resolveObstacleStep(player.position, stepX, stepZ, obstacles);
551
- stepX = resolved.stepX;
552
- stepZ = resolved.stepZ;
553
- }
554
- let nextX = player.position[0] + stepX;
555
- let nextZ = player.position[2] + stepZ;
556
- if (movement?.mode === "grid") {
557
- const snapped = snapPositionToGrid(nextX, nextZ, movement.cellSize ?? 1);
558
- nextX = snapped[0];
559
- nextZ = snapped[1];
560
- }
561
- let nextY = ground.sampleHeight(nextX, nextZ) + motion.jumpOffset;
562
- if (motionBatch !== null && motionBatch.y !== null) {
563
- nextY = motionBatch.y;
564
- motion.jumpOffset = motionBatch.y - ground.sampleHeight(nextX, nextZ);
565
- }
566
- if (movement?.beforeCommit !== undefined) {
567
- const frame = {
568
- entityId: playerId,
569
- current: player.position,
570
- next: [nextX, nextY, nextZ],
571
- dt: rawDt,
572
- ctx,
573
- };
574
- const replacement = movement.beforeCommit(frame);
575
- if (replacement !== undefined) {
576
- nextX = replacement[0];
577
- nextY = replacement[1];
578
- nextZ = replacement[2];
579
- }
580
- }
581
- ctx.scene.entity.setPose(playerId, {
582
- position: [nextX, nextY, nextZ],
583
- rotationY: intent.moving
584
- ? Math.atan2(motion.horizontalVelocityX, motion.horizontalVelocityZ)
585
- : player.rotationY,
586
- dt: rawDt,
587
- });
588
- }
589
- endPhase();
590
- }
591
- if (autoPickupRadius !== null) {
592
- endPhase = devtools.profile.begin("pickup");
593
- const self = ctx.scene.entity.get(playerId);
594
- if (self !== null) {
595
- const nearest = ctx.scene.worldItem.nearestInRadius(self.position, autoPickupRadius);
596
- if (nearest !== null)
597
- ctx.scene.worldItem.pickup(nearest, ctx.player.userId);
598
- }
599
- endPhase();
600
- }
601
- devtools.profile.measure("onTick", () => {
602
- playable.loop.onTick(ctx, gameDt);
603
- });
604
- endPhase = devtools.profile.begin("actions");
605
- if (tracker.wasPressed("tabTarget")) {
606
- if (ctx.game.commands.has("target.cycle"))
607
- ctx.game.commands.run("target.cycle", {});
608
- else
609
- ctx.scene.entity.cycleTarget(playerId, { filter: "hostile" });
610
- }
611
- if (tracker.wasPressed("clearTarget")) {
612
- if (ctx.game.commands.has("target.clear"))
613
- ctx.game.commands.run("target.clear", {});
614
- else
615
- ctx.scene.entity.setTarget(playerId, null);
616
- }
617
- if (pingCommand !== undefined && tracker.wasPressed("ping")) {
618
- const hit = pointerService.worldHit();
619
- if (hit !== null && ctx.game.commands.has(pingCommand)) {
620
- ctx.game.commands.run(pingCommand, {
621
- point: hit.point,
622
- entity: hit.entity,
623
- object: hit.object,
624
- normal: hit.normal,
625
- });
626
- }
627
- }
628
- const aimOverride = pointerAim ? pointerAimFor(ctx, pointerService) : undefined;
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
- }
642
- const nowMs = performance.now();
643
- for (const action of Object.keys(playable.game.input ?? {})) {
644
- const pressed = tracker.wasPressed(action);
645
- if (action === "ping" && pingCommand !== undefined)
646
- continue;
647
- if (action === "interact") {
648
- if (!pressed)
649
- continue;
650
- const prompts = playable.prompts?.(ctx);
651
- const focus = prompts === undefined ? null : ctx.scene.entity.get(playerId);
652
- if (prompts !== undefined && focus !== null) {
653
- const active = resolveActivePrompt({ x: focus.position[0], z: focus.position[2] }, prompts);
654
- if (active !== null && active.prompt.invoke !== null) {
655
- ctx.game.commands.run(active.prompt.invoke.name, active.prompt.invoke.input);
656
- }
657
- }
658
- continue;
659
- }
660
- if (!shouldFireBoundAction(tracker, action, playable.game.input, repeatFiredAtRef.current, nowMs))
661
- continue;
662
- repeatFiredAtRef.current.set(action, nowMs);
663
- dispatchBoundAction(ctx, action, yawRef.current, pitchRef.current, commandAim);
664
- }
665
- if (hotbarId !== null) {
666
- for (const { action, slot } of slotActions) {
667
- if (!tracker.wasPressed(action))
668
- continue;
669
- const result = executeHotbarSlot(ctx, playerId, hotbarId, slot, yawRef.current, pitchRef.current, aimOverride);
670
- if (!result.ok)
671
- console.warn(`[jgengine:item-use] ${result.error}`);
672
- }
673
- const usePrimary = tracker.wasPressed("useAbility") || (primaryClickRef.current && hotbarId !== null);
674
- if (usePrimary) {
675
- primaryClickRef.current = false;
676
- const slots = ctx.player.inventory.state(hotbarId).slots;
677
- const preferred = playable.hotbarSelection?.() ?? -1;
678
- const slot = preferred >= 0 && slots[preferred] !== null && slots[preferred] !== undefined
679
- ? preferred
680
- : slots.findIndex((stack) => stack !== null);
681
- if (slot >= 0) {
682
- const result = executeHotbarSlot(ctx, playerId, hotbarId, slot, yawRef.current, pitchRef.current, aimOverride);
683
- if (!result.ok)
684
- console.warn(`[jgengine:item-use] ${result.error}`);
685
- }
686
- }
687
- }
688
- tracker.endFrame();
689
- endPhase();
690
- const serverId = serverIdRef.current;
691
- if (multiplayer !== null && serverId !== null) {
692
- endPhase = devtools.profile.begin("presence");
693
- const focus = ctx.scene.entity.get(playerId);
694
- if (focus !== null) {
695
- multiplayer.backend.presenceSync.syncPose(serverId, {
696
- x: focus.position[0],
697
- y: focus.position[1],
698
- z: focus.position[2],
699
- rotationY: focus.rotationY,
700
- rotationPitch: pitchRef.current,
701
- });
702
- }
703
- endPhase();
704
- }
705
- }
706
- catch (error) {
707
- if (!hasReportedTickError.current) {
708
- hasReportedTickError.current = true;
709
- onRuntimeError(error, "tick");
710
- }
711
- }
712
- devtools.frame.record({ frameMs: rawDt * 1000, simMs: performance.now() - simStart });
713
- }, GAME_SIM_FRAME_PRIORITY);
714
- return null;
715
- }
716
- function HudOnlyDriver({ ctx, playable, tracker, pointerAxisRef, onRuntimeError, }) {
717
- const hasReportedTickError = useRef(false);
718
- const repeatFiredAtRef = useRef(new Map());
719
- const lastFrameRef = useRef(null);
720
- const inputActions = useMemo(() => Object.keys(playable.game.input ?? {}), [playable]);
721
- useEffect(() => {
722
- let frameId;
723
- const tick = (now) => {
724
- frameId = requestAnimationFrame(tick);
725
- const last = lastFrameRef.current;
726
- lastFrameRef.current = now;
727
- if (last === null)
728
- return;
729
- const rawDt = (now - last) / 1000;
730
- const simStart = performance.now();
731
- try {
732
- let endPhase = devtools.profile.begin("time+input");
733
- const dt = Math.min(rawDt, 0.05);
734
- const gameDt = ctx.time.advance(dt);
735
- ctx.input.publish(heldActionsFor(tracker, inputActions));
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");
742
- const nowMs = performance.now();
743
- for (const action of inputActions) {
744
- if (!shouldFireBoundAction(tracker, action, playable.game.input, repeatFiredAtRef.current, nowMs))
745
- continue;
746
- repeatFiredAtRef.current.set(action, nowMs);
747
- dispatchBoundAction(ctx, action, 0, 0, { yaw: 0, pitch: 0 });
748
- }
749
- tracker.endFrame();
750
- endPhase();
751
- }
752
- catch (error) {
753
- if (!hasReportedTickError.current) {
754
- hasReportedTickError.current = true;
755
- onRuntimeError(error, "tick");
756
- }
757
- }
758
- devtools.frame.record({ frameMs: rawDt * 1000, simMs: performance.now() - simStart });
759
- };
760
- frameId = requestAnimationFrame(tick);
761
- return () => cancelAnimationFrame(frameId);
762
- }, [ctx, playable, tracker, pointerAxisRef, onRuntimeError, inputActions]);
763
- return null;
764
- }
765
- class GameUiErrorBoundary extends Component {
766
- state = { failed: false };
767
- static getDerivedStateFromError() {
768
- return { failed: true };
769
- }
770
- componentDidCatch(error, info) {
771
- this.props.onRuntimeError(error, "ui-render", info.componentStack ?? undefined);
772
- }
773
- render() {
774
- if (this.state.failed)
775
- return null;
776
- return this.props.children;
777
- }
778
- }
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);
805
- if (diagnostics.length === 0)
806
- return null;
807
- const latest = diagnostics[diagnostics.length - 1];
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" })] })] }));
818
- }
819
- const POSTER_SETTLE_SECONDS = 1.6;
65
+ export { applyMotionImpulses } from "@jgengine/core/runtime/motionIntents";
66
+ export { nearbyObstacles } from "@jgengine/core/movement/movementModel";
67
+ export { resolvePhysicsTuning } from "@jgengine/core/movement/playerMovement";
68
+ export { hasEnvironmentTerrain } from "@jgengine/core/world/terrain";
820
69
  export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null, poster = false, onContextReady, }) {
821
70
  const multiplayer = useMemo(() => (rawMultiplayer === null ? null : withDevtoolsLatency(rawMultiplayer)), [rawMultiplayer]);
822
71
  const devtoolsEnabled = playable.devtools !== false && !poster;
823
72
  const [devtoolsOpen, setDevtoolsOpen] = useState(false);
73
+ const devtoolsOpenRef = useRef(false);
74
+ devtoolsOpenRef.current = devtoolsOpen;
75
+ useEffect(() => installAgentBridge({
76
+ playable,
77
+ devtoolsEnabled,
78
+ isDevtoolsOpen: () => devtoolsOpenRef.current,
79
+ setDevtoolsOpen,
80
+ }), [playable, devtoolsEnabled]);
824
81
  const [posterFrozen, setPosterFrozen] = useState(false);
825
82
  const posterSettledRef = useRef(false);
826
83
  const [ctx, setCtx] = useState(null);
@@ -836,7 +93,6 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
836
93
  const pointerAxisRef = useRef(null);
837
94
  const marqueeStartRef = useRef(null);
838
95
  const f2HeldRef = useRef(false);
839
- const f2ChordedRef = useRef(false);
840
96
  const pointerService = useMemo(() => createPointerService(), []);
841
97
  const selection = useMemo(() => createSelectionSet(), [playable]);
842
98
  const [marquee, setMarquee] = useState(null);
@@ -864,28 +120,63 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
864
120
  pointerAxisRef.current = { ...state, active: false };
865
121
  };
866
122
  const touchScheme = useMemo(() => deriveTouchScheme(playable.game.input, {
867
- reserved: RESERVED_INPUT_ACTIONS,
123
+ reserved: resolveRigKind(playable.camera) === "none" || playable.presentation === "hud"
124
+ ? EMPTY_RESERVED
125
+ : RESERVED_INPUT_ACTIONS,
868
126
  firstPerson: resolveRigKind(playable.camera) === "first",
869
127
  config: playable.touch,
870
128
  }), [playable]);
129
+ const touchStyle = useTouchStyle(settingsStore, touchScheme?.style ?? DEFAULT_TOUCH_STYLE);
871
130
  const { coarsePointer, portrait, compact } = useDisplayProfile();
872
131
  const touchSink = useMemo(() => ({ onCodeDown: (code) => tracker.handleDown(code), onCodeUp: (code) => tracker.handleUp(code) }), [tracker]);
873
- const audioEngine = useMemo(() => createAudioEngine({ sounds: playable.audio?.sounds, buses: playable.audio?.buses }), [playable]);
132
+ const gateRef = useRef(false);
133
+ const orientationPlatform = coarsePointer ? "mobile" : "desktop";
134
+ const orientationRequirement = useMemo(() => resolveOrientationRequirement(playable.orientation, orientationPlatform), [playable, orientationPlatform]);
135
+ const liveOrientation = portrait ? "portrait" : "landscape";
136
+ const orientationGate = !poster && coarsePointer && orientationGateActive(orientationRequirement, liveOrientation);
137
+ const orientationHint = !poster && coarsePointer && orientationHintActive(orientationRequirement, liveOrientation);
138
+ gateRef.current = orientationGate;
139
+ const orientationGateEl = orientationGate ? (_jsx(RotateDeviceScreen, { requiredOrientation: orientationRequirement.required ?? "landscape", title: orientationRequirement.required === "portrait" ? "Turn your phone upright" : "Turn your phone sideways", description: `${playable.game.name} is built for ${orientationRequirement.required ?? "landscape"} play.` })) : orientationHint && orientationRequirement.preferred !== null ? (_jsx(OrientationHint, { wanted: orientationRequirement.preferred })) : null;
140
+ const audioEngine = useMemo(() => createAudioEngine({
141
+ sounds: playable.audio?.sounds,
142
+ buses: playable.audio?.buses,
143
+ music: playable.audio?.music,
144
+ musicBus: playable.audio?.musicBus,
145
+ }), [playable]);
874
146
  useEffect(() => () => audioEngine.dispose(), [audioEngine]);
875
147
  useEffect(() => {
876
148
  if (ctx === null)
877
149
  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
- });
150
+ return attachAudioEventWire(ctx.game.events, audioEngine);
881
151
  }, [ctx, audioEngine]);
152
+ useEffect(() => {
153
+ if (typeof document === "undefined")
154
+ return;
155
+ document.documentElement.dataset.jgPresentation = playable.presentation ?? "3d";
156
+ return () => {
157
+ delete document.documentElement.dataset.jgPresentation;
158
+ };
159
+ }, [playable]);
160
+ useEffect(() => {
161
+ if (ctx === null || typeof document === "undefined")
162
+ return;
163
+ const hud = resolveRigKind(playable.camera) === "none" || playable.presentation === "hud";
164
+ if (!hud)
165
+ return;
166
+ const frame = requestAnimationFrame(() => {
167
+ document.documentElement.dataset.jgFrameReady = "1";
168
+ });
169
+ return () => {
170
+ cancelAnimationFrame(frame);
171
+ delete document.documentElement.dataset.jgFrameReady;
172
+ };
173
+ }, [ctx, playable]);
882
174
  const userId = multiplayer?.userId ?? DEV_USER_ID;
883
175
  const reportRuntimeError = (error, phase, componentStack) => {
884
176
  const diagnostic = logRuntimeError(error, phase, componentStack);
885
177
  setDiagnostics((current) => [...current.slice(-4), { ...diagnostic, id: Date.now() + current.length }]);
886
178
  };
887
179
  useEffect(() => {
888
- playable.game.scene.clear();
889
180
  setDiagnostics([]);
890
181
  try {
891
182
  const context = createGameContext({
@@ -903,98 +194,10 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
903
194
  setCtx(null);
904
195
  }
905
196
  return () => {
906
- playable.game.scene.clear();
907
197
  setCtx(null);
908
198
  };
909
199
  }, [playable, userId]);
910
- useEffect(() => {
911
- if (ctx === null || multiplayer === null)
912
- return;
913
- let disposed = false;
914
- const cleanups = [];
915
- void multiplayer.backend.transport
916
- .joinServer({ gameId: multiplayer.gameId })
917
- .then((joined) => {
918
- if (disposed) {
919
- void multiplayer.backend.transport.leaveServer({ serverId: joined.serverId });
920
- return;
921
- }
922
- serverIdRef.current = joined.serverId;
923
- cleanups.push(multiplayer.backend.presenceSync.subscribe(joined.serverId, (rows) => {
924
- setRemotePlayers(rows.filter((row) => row.userId !== multiplayer.userId));
925
- }));
926
- const seen = new Set();
927
- let injecting = false;
928
- for (const action of multiplayer.feedActions) {
929
- cleanups.push(ctx.game.feed.subscribe(action, (entry) => {
930
- if (injecting)
931
- return;
932
- void multiplayer.backend
933
- .pushFeedEntry({
934
- serverId: joined.serverId,
935
- action,
936
- entry: { at: entry.at, data: entry.data, from: multiplayer.userId },
937
- })
938
- .catch(() => undefined);
939
- }));
940
- const remoteUnsub = multiplayer.backend.feeds?.subscribeFeed({ serverId: joined.serverId, action }, (view) => {
941
- for (const raw of view.entries) {
942
- const remote = raw;
943
- if (typeof remote.from !== "string" || remote.from === multiplayer.userId)
944
- continue;
945
- const key = `${action}|${remote.from}|${remote.at ?? 0}`;
946
- if (seen.has(key))
947
- continue;
948
- seen.add(key);
949
- injecting = true;
950
- ctx.game.feed.push(action, remote.data);
951
- injecting = false;
952
- }
953
- });
954
- if (remoteUnsub !== undefined)
955
- cleanups.push(remoteUnsub);
956
- }
957
- const chatSync = multiplayer.backend.chatSyncFor?.(joined.serverId);
958
- if (chatSync !== undefined) {
959
- const globalChannelIds = new Set(ctx.game.chat
960
- .channels()
961
- .filter((channel) => channel.kind === "global")
962
- .map((channel) => channel.id));
963
- const seenRemoteChat = new Set();
964
- cleanups.push(ctx.game.events.subscribe("chat.message", (event) => {
965
- if (event.fromUserId !== multiplayer.userId)
966
- return;
967
- if (!globalChannelIds.has(event.channelId))
968
- return;
969
- void chatSync.send(event.channelId, event.body).catch(() => undefined);
970
- }));
971
- for (const channelId of globalChannelIds) {
972
- cleanups.push(chatSync.subscribe(channelId, (messages) => {
973
- for (const message of messages) {
974
- if (message.fromUserId === multiplayer.userId)
975
- continue;
976
- if (seenRemoteChat.has(message.id))
977
- continue;
978
- seenRemoteChat.add(message.id);
979
- ctx.game.chat.send(message.fromUserId, message.channelId, message.body);
980
- }
981
- }));
982
- }
983
- }
984
- })
985
- .catch(() => undefined);
986
- return () => {
987
- disposed = true;
988
- for (const cleanup of cleanups)
989
- cleanup();
990
- const serverId = serverIdRef.current;
991
- serverIdRef.current = null;
992
- setRemotePlayers([]);
993
- if (serverId !== null) {
994
- void multiplayer.backend.transport.leaveServer({ serverId }).catch(() => undefined);
995
- }
996
- };
997
- }, [ctx, multiplayer]);
200
+ useShellMultiplayerSync(ctx, multiplayer, playable, serverIdRef, setRemotePlayers);
998
201
  useEffect(() => {
999
202
  wrapperRef.current?.focus();
1000
203
  }, [ctx]);
@@ -1010,32 +213,34 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
1010
213
  const rigKind = resolveRigKind(cameraConfig);
1011
214
  if (rigKind === "none" || playable.presentation === "hud") {
1012
215
  const GameUI = playable.GameUI;
1013
- return (_jsxs("div", { ref: wrapperRef, tabIndex: 0, className: "relative h-full w-full bg-neutral-950 outline-none", onKeyDown: (event) => {
1014
- if (event.code === "F2" && devtoolsEnabled) {
216
+ return (_jsx("div", { ref: wrapperRef, tabIndex: 0, className: "relative h-full w-full bg-neutral-950 outline-none", onKeyDown: (event) => {
217
+ if (event.code === "F2") {
1015
218
  event.preventDefault();
1016
219
  f2HeldRef.current = true;
1017
- f2ChordedRef.current = false;
1018
220
  return;
1019
221
  }
1020
222
  if (f2HeldRef.current) {
1021
- f2ChordedRef.current = true;
223
+ if (event.code === "KeyD" && devtoolsEnabled) {
224
+ event.preventDefault();
225
+ setDevtoolsOpen((current) => !current);
226
+ }
1022
227
  return;
1023
228
  }
1024
229
  if (event.code === "Tab" || event.code === "Space")
1025
230
  event.preventDefault();
1026
- tracker.handleDown(event.code);
231
+ if (playControlsActive(ctx))
232
+ tracker.handleDown(event.code);
1027
233
  }, onKeyUp: (event) => {
1028
- if (event.code === "F2" && devtoolsEnabled) {
234
+ if (event.code === "F2") {
1029
235
  f2HeldRef.current = false;
1030
- if (!f2ChordedRef.current)
1031
- setDevtoolsOpen((current) => !current);
1032
236
  return;
1033
237
  }
1034
- tracker.handleUp(event.code);
238
+ if (playControlsActive(ctx))
239
+ tracker.handleUp(event.code);
1035
240
  }, onBlur: () => {
1036
241
  f2HeldRef.current = false;
1037
242
  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 })] }));
243
+ }, onPointerDown: () => audioEngine.resume(), onPointerMove: trackPointerAxis, onPointerLeave: deactivatePointerAxis, onPointerCancel: deactivatePointerAxis, children: _jsxs(GameViewportProvider, { platforms: playable.platforms, children: [_jsx(HudOnlyDriver, { ctx: ctx, playable: playable, tracker: tracker, pointerAxisRef: pointerAxisRef, gateRef: gateRef, onRuntimeError: reportRuntimeError }), !orientationGate && coarsePointer && touchScheme !== null && touchScheme.gestures !== null && playControlsActive(ctx) ? (_jsx(TouchPlaySurface, { scheme: touchScheme, sink: touchSink, yawRef: yawRef, pitchRef: pitchRef, maxPitch: 0, onPrimaryTap: () => undefined })) : null, _jsx(GameUiErrorBoundary, { onRuntimeError: reportRuntimeError, children: _jsxs(GameProvider, { context: ctx, children: [_jsx(GamePhaseStamp, {}), _jsx(HudViewportProvider, { platforms: playable.platforms, config: playable.hudFit, userScale: graphics.uiScale, children: orientationGate ? null : _jsx(GameUI, {}) })] }) }), orientationGateEl, devtoolsEnabled ? (_jsx(DevtoolsOverlay, { open: devtoolsOpen, ctx: ctx, playable: playable, multiplayer: multiplayer })) : null, _jsx(DiagnosticOverlay, { diagnostics: diagnostics, gameName: playable.game.name })] }) }));
1039
244
  }
1040
245
  const firstPerson = rigKind === "first";
1041
246
  const showReticle = (firstPerson && playable.camera?.firstPerson?.reticle !== false) || rigKind === "shoulder";
@@ -1047,23 +252,43 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
1047
252
  ? "health"
1048
253
  : worldBars.statId ?? "health";
1049
254
  const barsRoles = worldBars === undefined || worldBars === true || worldBars === false ? undefined : worldBars.roles;
255
+ const barsMaxDistance = worldBars === undefined || worldBars === true || worldBars === false
256
+ ? undefined
257
+ : worldBars.maxDistance;
258
+ const nameplates = playable.nameplates;
259
+ const nameplatesStatId = nameplates === undefined || nameplates === false
260
+ ? null
261
+ : nameplates === true
262
+ ? "health"
263
+ : nameplates.statId ?? "health";
264
+ const nameplatesRoles = nameplates === undefined || nameplates === true || nameplates === false ? undefined : nameplates.roles;
265
+ const nameplatesMaxDistance = nameplates === undefined || nameplates === true || nameplates === false ? undefined : nameplates.maxDistance;
1050
266
  const resolveEntityRole = (entity) => playable.content.entityById?.(entity.name)?.role;
1051
267
  const pointer = playable.pointer;
1052
268
  const pointerUsesLeft = pointer !== undefined && (pointer.select === true || pointer.moveCommand !== undefined);
1053
269
  const selectFilter = pointer?.selectFilter;
1054
270
  const worldSky = resolveWorldSky(playable.game.world);
1055
271
  const world = playable.game.world;
272
+ const biomeBands = world?.kind === "environment" ? world.terrain?.biomeBands : undefined;
1056
273
  const AutoEnvironment = playable.environment ??
1057
274
  (world?.kind === "environment"
1058
275
  ? () => _jsx(EnvironmentScene, { feature: world })
1059
276
  : world?.kind === "biomes" || world?.kind === "voxel" || world?.kind === "plots" || world?.kind === "tilemap"
1060
277
  ? () => _jsx(GridWorldScene, { feature: world })
1061
278
  : undefined);
1062
- const backdrop = playable.backdrop;
279
+ const resolvedLook = resolveGameLook({
280
+ look: playable.look,
281
+ lighting: playable.lighting,
282
+ backdrop: playable.backdrop,
283
+ postProcessing: playable.postProcessing,
284
+ hasWorldSky: worldSky !== undefined,
285
+ });
286
+ const cinematicLook = (playable.look ?? "cinematic") !== "flat";
287
+ const backdrop = resolvedLook.backdrop;
1063
288
  const backdropSky = backdrop?.sky !== undefined ? resolveSkyDescriptor(backdrop.sky) : undefined;
1064
289
  const effectiveSky = backdropSky ?? worldSky;
1065
290
  const backgroundColor = backdrop?.background ?? (effectiveSky === undefined ? DEFAULT_BACKGROUND_COLOR : undefined);
1066
- const lighting = playable.lighting;
291
+ const lighting = resolvedLook.lighting;
1067
292
  const orthographic = playable.camera?.projection === "orthographic";
1068
293
  const localXY = (event) => {
1069
294
  const rect = wrapperRef.current?.getBoundingClientRect();
@@ -1086,13 +311,16 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
1086
311
  }
1087
312
  commitSelection(selectWithinRect(candidates, rect));
1088
313
  };
314
+ /** DOM UI (menus, HUD buttons) sits in the same wrapper as the canvas — only canvas-targeted clicks are world input. */
315
+ const isWorldPointerTarget = (event) => event.target instanceof HTMLCanvasElement;
1089
316
  const handlePointerDown = (event) => {
1090
- wrapperRef.current?.focus();
317
+ if (isWorldPointerTarget(event))
318
+ wrapperRef.current?.focus();
1091
319
  trackPointerAxis(event);
1092
320
  audioEngine.resume();
1093
321
  if (contextMenu !== null)
1094
322
  setContextMenu(null);
1095
- if (event.button === 0) {
323
+ if (event.button === 0 && isWorldPointerTarget(event)) {
1096
324
  const point = localXY(event);
1097
325
  pointerDownRef.current = point;
1098
326
  if (pointer?.select === true)
@@ -1166,6 +394,8 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
1166
394
  const handleContextMenu = (event) => {
1167
395
  if (pointer === undefined)
1168
396
  return;
397
+ if (!isWorldPointerTarget(event))
398
+ return;
1169
399
  event.preventDefault();
1170
400
  const hit = pointerService.worldHit();
1171
401
  if (hit === null)
@@ -1218,40 +448,40 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
1218
448
  }));
1219
449
  const touchScale = compact ? 0.88 : 1;
1220
450
  const dockMounted = !poster &&
451
+ !orientationGate &&
1221
452
  coarsePointer &&
1222
453
  controlsActive &&
1223
454
  touchScheme !== null &&
1224
455
  (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: {
456
+ 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 ?? [], touchStyle: coarsePointer &&
457
+ touchScheme !== null &&
458
+ (touchScheme.joystick !== null || touchScheme.buttons.length > 0), overrides: bindingOverrides, rebind: rebindAction, resetBinding: resetActionBinding, children: _jsx("div", { ref: wrapperRef, tabIndex: 0, ...(poster && posterFrozen ? { "data-poster-ready": "" } : {}), className: "relative h-full w-full bg-neutral-950 outline-none", style: {
1230
459
  "--jg-hud-dock-clearance": `${dockMounted ? touchDockClearance(touchScheme, touchScale) : 0}px`,
1231
460
  }, onKeyDown: (event) => {
1232
- if (event.code === "F2" && devtoolsEnabled) {
461
+ if (event.code === "F2") {
1233
462
  event.preventDefault();
1234
463
  f2HeldRef.current = true;
1235
- f2ChordedRef.current = false;
1236
464
  return;
1237
465
  }
1238
466
  if (f2HeldRef.current) {
1239
- f2ChordedRef.current = true;
467
+ if (event.code === "KeyD" && devtoolsEnabled) {
468
+ event.preventDefault();
469
+ document.exitPointerLock?.();
470
+ setDevtoolsOpen((current) => !current);
471
+ }
1240
472
  return;
1241
473
  }
1242
474
  if (event.code === "Tab" || event.code === "Space")
1243
475
  event.preventDefault();
1244
- tracker.handleDown(event.code);
476
+ if (playControlsActive(ctx))
477
+ tracker.handleDown(event.code);
1245
478
  }, onKeyUp: (event) => {
1246
- if (event.code === "F2" && devtoolsEnabled) {
479
+ if (event.code === "F2") {
1247
480
  f2HeldRef.current = false;
1248
- if (!f2ChordedRef.current) {
1249
- document.exitPointerLock?.();
1250
- setDevtoolsOpen((current) => !current);
1251
- }
1252
481
  return;
1253
482
  }
1254
- tracker.handleUp(event.code);
483
+ if (playControlsActive(ctx))
484
+ tracker.handleUp(event.code);
1255
485
  }, onBlur: () => {
1256
486
  f2HeldRef.current = false;
1257
487
  tracker.reset();
@@ -1265,24 +495,24 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
1265
495
  else if (event.deltaY > 0 && ctx.game.commands.has("ui.hotbarScrollPrev")) {
1266
496
  ctx.game.commands.run("ui.hotbarScrollPrev", {});
1267
497
  }
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, {})] }) })] }) }));
498
+ }, children: _jsxs(GameViewportProvider, { platforms: playable.platforms, children: [_jsxs(Canvas, { frameloop: poster && posterFrozen ? "demand" : "always", orthographic: orthographic, camera: orthographic
499
+ ? {
500
+ zoom: playable.camera?.frustum?.zoom ?? CAMERA_FRUSTUM_DEFAULTS.zoom,
501
+ near: playable.camera?.frustum?.near ?? CAMERA_FRUSTUM_DEFAULTS.near,
502
+ far: playable.camera?.frustum?.far ?? CAMERA_FRUSTUM_DEFAULTS.far,
503
+ }
504
+ : {
505
+ fov: playable.camera?.frustum?.fov ?? CAMERA_FRUSTUM_DEFAULTS.fov,
506
+ near: playable.camera?.frustum?.near ?? CAMERA_FRUSTUM_DEFAULTS.near,
507
+ far: playable.camera?.frustum?.far ?? CAMERA_FRUSTUM_DEFAULTS.far,
508
+ }, shadows: graphics.shadows, dpr: graphics.dpr, gl: { preserveDrawingBuffer: true }, style: { touchAction: "none" }, children: [backgroundColor !== undefined ? _jsx("color", { attach: "background", args: [backgroundColor] }) : null, cinematicLook ? _jsx(EnvironmentLighting, {}) : 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, bands: effectiveSky === worldSky ? biomeBands : undefined, lights: skyEmitsLights(resolveSkyLightOwnership(lighting !== undefined)) })) : (_jsx(SkyDaylight, { sky: effectiveSky, bands: effectiveSky === worldSky ? biomeBands : undefined, 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, hideLocalActor: firstPerson }) }), WorldOverlay !== undefined ? _jsx(WorldOverlay, { ctx: ctx }) : null, barsStatId !== null ? (_jsx(WorldEntityBars, { statId: barsStatId, roles: barsRoles, resolveRole: resolveEntityRole, ...(barsMaxDistance === undefined ? {} : { maxDistance: barsMaxDistance }) })) : null, nameplatesStatId !== null ? (_jsx(WorldNameplates, { statId: nameplatesStatId, roles: nameplatesRoles, resolveRole: resolveEntityRole, ...(nameplatesMaxDistance === undefined ? {} : { maxDistance: nameplatesMaxDistance }) })) : null, _jsx(WorldItems, { config: playable.worldItem }), _jsx(WorldTelegraphs, {}), _jsx(WorldSpellVfx, {}), _jsx(RetainedVfx, {}), _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, viewmodel: playable.viewmodel, pointerControls: pointerUsesLeft, panKeysEnabled: rtsPanKeysEnabled, director: ctx.camera, onDragChange: (dragging) => {
509
+ cameraDraggingRef.current = dragging;
510
+ } }), _jsx(PointerProbe, { service: pointerService })] }), _jsx(RemotePlayers, { rows: remotePlayers }), _jsx(FrameDriver, { ctx: ctx, playable: playable, tracker: tracker, yawRef: yawRef, pitchRef: pitchRef, primaryClickRef: primaryClickRef, pointerAxisRef: pointerAxisRef, gateRef: gateRef, onRuntimeError: reportRuntimeError, multiplayer: multiplayer, serverIdRef: serverIdRef, pointerService: pointerService, pointerAim: pointer?.aim === true, pingCommand: pointer?.pingCommand, poster: poster, onPosterSettled: () => {
511
+ if (posterSettledRef.current)
512
+ return;
513
+ posterSettledRef.current = true;
514
+ setPosterFrozen(true);
515
+ } }), _jsx(DevtoolsRendererProbe, {}), resolvedLook.postProcessing !== undefined && resolvedLook.postProcessing.enabled !== false ? (_jsx(PostProcessing, { config: resolvedLook.postProcessing })) : null] }), !poster && !orientationGate && 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: () => {
516
+ primaryClickRef.current = true;
517
+ } })) : null, _jsx(GameUiErrorBoundary, { onRuntimeError: reportRuntimeError, children: _jsxs(GameProvider, { context: ctx, children: [_jsx(GamePhaseStamp, {}), _jsx(HudViewportProvider, { platforms: playable.platforms, config: playable.hudFit, userScale: graphics.uiScale, children: orientationGate ? null : _jsx(GameUI, {}) })] }) }), !poster && !orientationGate && showReticle ? _jsx(Reticle, {}) : null, dockMounted && touchScheme !== null ? (_jsx(TouchControlsDock, { scheme: touchScheme, sink: touchSink, style: touchStyle, scale: touchScale })) : null, orientationGateEl, 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, {})] }) }) })] }) }));
1288
518
  }