@jgengine/shell 0.10.0 → 0.12.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 (286) hide show
  1. package/CHANGELOG.md +39 -2
  2. package/LICENSE +203 -661
  3. package/README.md +1 -1
  4. package/dist/CombatPresentation.d.ts +5 -0
  5. package/dist/CombatPresentation.js +8 -0
  6. package/dist/GameHost.d.ts +25 -1
  7. package/dist/GameHost.js +60 -4
  8. package/dist/GamePhaseStamp.d.ts +1 -0
  9. package/dist/GamePhaseStamp.js +9 -0
  10. package/dist/GamePlayer.d.ts +5 -0
  11. package/dist/GamePlayer.js +5 -0
  12. package/dist/GamePlayerShell.d.ts +5 -12
  13. package/dist/GamePlayerShell.js +67 -1332
  14. package/dist/GameUiPreview.js +3 -2
  15. package/dist/Shell3dPresentation.d.ts +63 -0
  16. package/dist/Shell3dPresentation.js +302 -0
  17. package/dist/ShellChrome.d.ts +51 -0
  18. package/dist/ShellChrome.js +51 -0
  19. package/dist/ShellHudPresentation.d.ts +41 -0
  20. package/dist/ShellHudPresentation.js +22 -0
  21. package/dist/audio/audioEngine.d.ts +4 -0
  22. package/dist/audio/audioEngine.js +128 -21
  23. package/dist/audio/audioWire.d.ts +40 -0
  24. package/dist/audio/audioWire.js +62 -0
  25. package/dist/audio/loopParams.d.ts +20 -0
  26. package/dist/audio/loopParams.js +28 -0
  27. package/dist/behaviour.d.ts +2 -1
  28. package/dist/behaviour.js +2 -1
  29. package/dist/behaviourAttach.d.ts +1 -0
  30. package/dist/behaviourAttach.js +1 -0
  31. package/dist/behaviourDriver.d.ts +1 -0
  32. package/dist/behaviourDriver.js +1 -0
  33. package/dist/boundActionDispatch.d.ts +13 -0
  34. package/dist/boundActionDispatch.js +37 -0
  35. package/dist/camera/GameCameraRig.d.ts +5 -2
  36. package/dist/camera/GameCameraRig.js +8 -3
  37. package/dist/camera/GameFirstPersonCamera.d.ts +9 -2
  38. package/dist/camera/GameFirstPersonCamera.js +8 -3
  39. package/dist/camera/GameInspectionCamera.d.ts +1 -1
  40. package/dist/camera/GameInspectionCamera.js +2 -2
  41. package/dist/camera/GameOrbitCamera.js +24 -9
  42. package/dist/camera/cameraBlendMath.d.ts +3 -0
  43. package/dist/camera/cameraBlendMath.js +3 -0
  44. package/dist/camera/cameraRigs.d.ts +3 -1
  45. package/dist/camera/cameraRigs.js +21 -3
  46. package/dist/camera/fovPreference.d.ts +6 -1
  47. package/dist/camera/fovPreference.js +6 -1
  48. package/dist/camera/index.d.ts +4 -7
  49. package/dist/camera/index.js +4 -7
  50. package/dist/camera/inspectionCameraMath.d.ts +7 -2
  51. package/dist/camera/inspectionCameraMath.js +7 -2
  52. package/dist/camera/orbitCameraMath.d.ts +36 -6
  53. package/dist/camera/orbitCameraMath.js +56 -6
  54. package/dist/camera/rigMath.d.ts +114 -24
  55. package/dist/camera/rigMath.js +129 -24
  56. package/dist/camera/rigResolve.d.ts +4 -2
  57. package/dist/camera/rigResolve.js +4 -2
  58. package/dist/camera/shakeChannel.d.ts +6 -2
  59. package/dist/camera/shakeChannel.js +6 -2
  60. package/dist/camera/shakeChannelMath.d.ts +1 -0
  61. package/dist/camera/shakeChannelMath.js +1 -0
  62. package/dist/commandSink.d.ts +8 -3
  63. package/dist/commandSink.js +8 -3
  64. package/dist/defineGame.d.ts +15 -1
  65. package/dist/defineGame.js +50 -9
  66. package/dist/devtools/ColPanel.d.ts +1 -0
  67. package/dist/devtools/ColPanel.js +14 -0
  68. package/dist/devtools/DevtoolsOverlay.d.ts +11 -7
  69. package/dist/devtools/DevtoolsOverlay.js +47 -462
  70. package/dist/devtools/KeysPanel.d.ts +4 -0
  71. package/dist/devtools/KeysPanel.js +31 -0
  72. package/dist/devtools/LogsPanel.d.ts +1 -0
  73. package/dist/devtools/LogsPanel.js +12 -0
  74. package/dist/devtools/NetPanel.d.ts +4 -0
  75. package/dist/devtools/NetPanel.js +10 -0
  76. package/dist/devtools/PerfPanel.d.ts +4 -0
  77. package/dist/devtools/PerfPanel.js +53 -0
  78. package/dist/devtools/TunePanel.d.ts +3 -0
  79. package/dist/devtools/TunePanel.js +309 -0
  80. package/dist/devtools/agentBridge.d.ts +41 -0
  81. package/dist/devtools/agentBridge.js +266 -0
  82. package/dist/devtools/collisionDebug.d.ts +6 -0
  83. package/dist/devtools/collisionDebug.js +6 -0
  84. package/dist/devtools/collisionDebugMath.d.ts +8 -2
  85. package/dist/devtools/collisionDebugMath.js +24 -5
  86. package/dist/devtools/devtoolsOverrides.d.ts +6 -0
  87. package/dist/devtools/devtoolsOverrides.js +48 -0
  88. package/dist/devtools/panelAtoms.d.ts +9 -0
  89. package/dist/devtools/panelAtoms.js +10 -0
  90. package/dist/devtools/perfDiagnose.d.ts +2 -0
  91. package/dist/devtools/perfDiagnose.js +43 -0
  92. package/dist/diagnostics/RuntimeDiagnostics.d.ts +36 -0
  93. package/dist/diagnostics/RuntimeDiagnostics.js +80 -0
  94. package/dist/drivers/FrameDriver.d.ts +37 -0
  95. package/dist/drivers/FrameDriver.js +232 -0
  96. package/dist/drivers/HudOnlyDriver.d.ts +16 -0
  97. package/dist/drivers/HudOnlyDriver.js +60 -0
  98. package/dist/environment/Daylight.d.ts +23 -7
  99. package/dist/environment/Daylight.js +106 -13
  100. package/dist/environment/EnvironmentScene.js +44 -9
  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/environment/terrainGroundColor.d.ts +9 -0
  112. package/dist/environment/terrainGroundColor.js +22 -0
  113. package/dist/gameKit.d.ts +27 -0
  114. package/dist/gameKit.js +29 -0
  115. package/dist/hotbarActions.d.ts +26 -0
  116. package/dist/hotbarActions.js +27 -0
  117. package/dist/inputSink.d.ts +16 -4
  118. package/dist/inputSink.js +57 -7
  119. package/dist/map/MapMarkerBeacons.d.ts +3 -2
  120. package/dist/materialOverride.d.ts +10 -0
  121. package/dist/materialOverride.js +15 -3
  122. package/dist/multiplayer.js +8 -2
  123. package/dist/postfx/PostProcessing.d.ts +9 -1
  124. package/dist/postfx/PostProcessing.js +56 -11
  125. package/dist/postfx/gradeShader.js +9 -1
  126. package/dist/presentationResolve.d.ts +35 -0
  127. package/dist/presentationResolve.js +51 -0
  128. package/dist/registry.d.ts +4 -2
  129. package/dist/render/EnvironmentLighting.d.ts +10 -0
  130. package/dist/render/EnvironmentLighting.js +34 -0
  131. package/dist/render/SceneLighting.d.ts +7 -0
  132. package/dist/render/SceneLighting.js +18 -0
  133. package/dist/render/SceneModels.d.ts +14 -0
  134. package/dist/render/SceneModels.js +337 -0
  135. package/dist/render/assetBase.d.ts +17 -0
  136. package/dist/render/assetBase.js +35 -0
  137. package/dist/render/defaultSurface.d.ts +29 -0
  138. package/dist/render/defaultSurface.js +101 -0
  139. package/dist/render/modelLoad.d.ts +24 -0
  140. package/dist/render/modelLoad.js +76 -0
  141. package/dist/render/modelRender.d.ts +11 -0
  142. package/dist/render/modelRender.js +15 -0
  143. package/dist/render/resolveModel.d.ts +54 -3
  144. package/dist/render/resolveModel.js +116 -4
  145. package/dist/render/useEntityRenderCues.d.ts +13 -0
  146. package/dist/render/useEntityRenderCues.js +62 -0
  147. package/dist/scatter/InstancedScatter.d.ts +24 -0
  148. package/dist/scatter/InstancedScatter.js +135 -0
  149. package/dist/scatter/scatterModels.d.ts +25 -0
  150. package/dist/scatter/scatterModels.js +61 -0
  151. package/dist/scatter/scatterProxies.d.ts +6 -0
  152. package/dist/scatter/scatterProxies.js +118 -0
  153. package/dist/scatter.d.ts +1 -0
  154. package/dist/scatter.js +1 -0
  155. package/dist/scene/AuthoredScene.d.ts +84 -0
  156. package/dist/scene/AuthoredScene.js +178 -0
  157. package/dist/scene/CityRenderer.d.ts +2 -0
  158. package/dist/scene/CityRenderer.js +1031 -0
  159. package/dist/scene/GeneratedAssetRenderer.d.ts +14 -0
  160. package/dist/scene/GeneratedAssetRenderer.js +59 -0
  161. package/dist/scene/GrassFieldRenderer.d.ts +2 -0
  162. package/dist/scene/GrassFieldRenderer.js +73 -0
  163. package/dist/scene/PoleLineRenderer.d.ts +2 -0
  164. package/dist/scene/PoleLineRenderer.js +287 -0
  165. package/dist/scene/SoilPatchRenderer.d.ts +2 -0
  166. package/dist/scene/SoilPatchRenderer.js +84 -0
  167. package/dist/scene/StudioStage.d.ts +37 -0
  168. package/dist/scene/StudioStage.js +42 -0
  169. package/dist/scene/WaterRenderer.d.ts +2 -0
  170. package/dist/scene/WaterRenderer.js +61 -0
  171. package/dist/scene/builtinSceneKindRenderers.d.ts +2 -0
  172. package/dist/scene/builtinSceneKindRenderers.js +26 -0
  173. package/dist/scene/cityWindowMaterial.d.ts +16 -0
  174. package/dist/scene/cityWindowMaterial.js +81 -0
  175. package/dist/scene/sceneKindRenderers.d.ts +30 -0
  176. package/dist/scene/sceneKindRenderers.js +17 -0
  177. package/dist/scene.d.ts +1 -0
  178. package/dist/scene.js +1 -0
  179. package/dist/settings/appliedSettings.d.ts +7 -1
  180. package/dist/settings/appliedSettings.js +13 -2
  181. package/dist/settings/settingsController.d.ts +2 -0
  182. package/dist/settings/settingsController.js +22 -1
  183. package/dist/shellConstants.d.ts +4 -0
  184. package/dist/shellConstants.js +4 -0
  185. package/dist/shellMovement.d.ts +8 -0
  186. package/dist/shellMovement.js +11 -0
  187. package/dist/shellPointer.d.ts +20 -0
  188. package/dist/shellPointer.js +28 -0
  189. package/dist/structures/PlacementGhost.d.ts +4 -0
  190. package/dist/structures/PlacementGhost.js +4 -0
  191. package/dist/structures/TransformGizmo.d.ts +53 -0
  192. package/dist/structures/TransformGizmo.js +59 -0
  193. package/dist/structures/index.d.ts +1 -0
  194. package/dist/structures/index.js +1 -0
  195. package/dist/terrain/GrassField.d.ts +11 -3
  196. package/dist/terrain/GrassField.js +16 -6
  197. package/dist/terrain/TerraformBrushCursor.js +3 -0
  198. package/dist/terrain/grassBudget.d.ts +11 -2
  199. package/dist/terrain/grassBudget.js +20 -4
  200. package/dist/terrain/grassGeometry.d.ts +35 -0
  201. package/dist/terrain/grassGeometry.js +85 -20
  202. package/dist/terrain/grassMaterial.d.ts +20 -0
  203. package/dist/terrain/grassMaterial.js +68 -13
  204. package/dist/terrain/heightfieldGeometry.d.ts +34 -0
  205. package/dist/terrain/heightfieldGeometry.js +109 -0
  206. package/dist/terrain/index.d.ts +1 -0
  207. package/dist/terrain/index.js +1 -0
  208. package/dist/terrain/random.d.ts +3 -0
  209. package/dist/terrain/random.js +3 -0
  210. package/dist/terrain/soilPatchMaterial.d.ts +23 -0
  211. package/dist/terrain/soilPatchMaterial.js +147 -0
  212. package/dist/terrain/terrainDetailMaterial.d.ts +19 -2
  213. package/dist/terrain/terrainDetailMaterial.js +87 -7
  214. package/dist/terrain/terrainMath.d.ts +8 -1
  215. package/dist/terrain/terrainMath.js +8 -1
  216. package/dist/touch/TouchControlsOverlay.d.ts +9 -5
  217. package/dist/touch/TouchControlsOverlay.js +201 -22
  218. package/dist/useShellMultiplayerSync.d.ts +9 -0
  219. package/dist/useShellMultiplayerSync.js +98 -0
  220. package/dist/visibility/CullingProvider.js +1 -1
  221. package/dist/water/Ocean.d.ts +7 -1
  222. package/dist/water/Ocean.js +16 -3
  223. package/dist/water/OceanConfig.d.ts +11 -0
  224. package/dist/water/OceanConfig.js +8 -0
  225. package/dist/water/OceanMaterial.d.ts +12 -0
  226. package/dist/water/OceanMaterial.js +9 -0
  227. package/dist/water/OceanShader.d.ts +2 -2
  228. package/dist/water/OceanShader.js +78 -10
  229. package/dist/weather/fireSpreadPose.d.ts +1 -0
  230. package/dist/weather/fireSpreadPose.js +1 -0
  231. package/dist/weather/weatherGeometry.d.ts +1 -0
  232. package/dist/weather/weatherGeometry.js +1 -0
  233. package/dist/weather/weatherMath.d.ts +3 -0
  234. package/dist/weather/weatherMath.js +3 -0
  235. package/dist/weather/weatherUniforms.d.ts +3 -0
  236. package/dist/weather/weatherUniforms.js +3 -0
  237. package/dist/world/PlaceScene.d.ts +11 -0
  238. package/dist/world/PlaceScene.js +27 -0
  239. package/dist/world/RetainedVfx.d.ts +10 -0
  240. package/dist/world/RetainedVfx.js +136 -0
  241. package/dist/world/WorldHud.d.ts +49 -2
  242. package/dist/world/WorldHud.js +87 -2
  243. package/dist/world/WorldScene.d.ts +21 -0
  244. package/dist/world/WorldScene.js +186 -0
  245. package/dist/world/WorldVfx.d.ts +9 -0
  246. package/dist/world/WorldVfx.js +184 -0
  247. package/dist/world/entityPose.d.ts +2 -0
  248. package/dist/world/entityPose.js +2 -0
  249. package/dist/world/floatTextStyle.d.ts +1 -0
  250. package/dist/world/floatTextStyle.js +1 -0
  251. package/dist/world/telegraphPulse.d.ts +1 -0
  252. package/dist/world/telegraphPulse.js +1 -0
  253. package/dist/world/worldBarSamples.d.ts +25 -0
  254. package/dist/world/worldBarSamples.js +38 -0
  255. package/dist/worldSky.d.ts +11 -0
  256. package/dist/worldSky.js +21 -0
  257. package/dist/worldSync.d.ts +2 -1
  258. package/dist/worldSync.js +2 -1
  259. package/package.json +6 -6
  260. package/skills/game-design/SKILL.md +28 -0
  261. package/skills/game-design/agents/openai.yaml +4 -0
  262. package/skills/game-design/references/application-playbook.md +170 -0
  263. package/skills/game-design/references/field-guide.md +288 -0
  264. package/skills/jgengine/SKILL.md +54 -0
  265. package/skills/jgengine/api.md +527 -0
  266. package/skills/jgengine/capabilities.md +131 -0
  267. package/skills/jgengine/recipes/minimal-game.md +88 -0
  268. package/skills/jgengine-ui/SKILL.md +41 -0
  269. package/skills/jgengine-ui/api.md +2007 -0
  270. package/skills/jgengine-ui/capabilities.md +211 -0
  271. package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
  272. package/skills/jgengine-ui/reference.md +513 -0
  273. package/skills/jgengine-ui/references/visual-scorecard.md +73 -0
  274. package/skills/jgengine-world/SKILL.md +51 -0
  275. package/skills/jgengine-world/api.md +3056 -0
  276. package/skills/jgengine-world/capabilities.md +476 -0
  277. package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
  278. package/skills/jgengine-world/reference.md +261 -0
  279. package/skills/level-design/SKILL.md +54 -0
  280. package/skills/level-design/agents/openai.yaml +4 -0
  281. package/skills/level-design/references/application-playbook.md +192 -0
  282. package/skills/level-design/references/field-guide.md +337 -0
  283. package/skills/level-design/references/live-editor-loop.md +179 -0
  284. package/dist/cartridge.d.ts +0 -145
  285. package/dist/cartridge.js +0 -246
  286. package/llms.txt +0 -2761
@@ -1,957 +1,52 @@
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, Suspense, 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 { MeshoptDecoder } from "three/examples/jsm/libs/meshopt_decoder.module.js";
7
- import { actionRepeatMs, createActionStateTracker, hotbarSlotActionIndex, resolveActionCommand, shouldDispatchAction, toActionStateBindingMap, } from "@jgengine/core/input/actionBindings";
8
- import { deriveTouchScheme, withTouchCodes } from "@jgengine/core/input/touchScheme";
9
- import { buildContextMenu, contextVerbInput, } from "@jgengine/core/interaction/contextMenu";
10
- import { resolveActivePrompt } from "@jgengine/core/interaction/proximityPrompt";
11
- import { aimToPoint } from "@jgengine/core/input/pointer";
12
- import { eyeHeightFromColliders } from "@jgengine/core/combat/shotOrigin";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
3
+ import { createActionStateTracker, toActionStateBindingMap, } from "@jgengine/core/input/actionBindings";
4
+ import { RESERVED_INPUT_ACTIONS } from "./boundActionDispatch.js";
5
+ import { deriveTouchScheme, withTouchCodes, DEFAULT_TOUCH_STYLE } from "@jgengine/core/input/touchScheme";
13
6
  import { normalizePointerToAxis } from "@jgengine/core/input/pointerAxis";
14
- import { createSelectionSet, isMarquee, screenRect, selectWithinRect, } from "@jgengine/core/scene/selection";
15
- import { steerYaw } from "@jgengine/core/movement/steering";
16
- import { stepPlayerMovement, resolvePlayerMovementTuning } from "@jgengine/core/movement/playerMovement";
17
7
  import { createGameContext } from "@jgengine/core/runtime/gameContext";
18
- import { isServerAuthoritative } from "@jgengine/core/runtime/adapter";
19
- import { attachWorldSync } from "./worldSync.js";
20
- import { localCommandSink, resolveCommandSink } from "./commandSink.js";
21
- import { inputFramesEqual, resolveInputSink } from "./inputSink.js";
22
- import { objectVisualScale } from "@jgengine/core/scene/objectStore";
23
- import { DEFAULT_PICKUP_RADIUS, WORLD_ITEM_ENTITY_NAME } from "@jgengine/core/game/worldItem";
24
- import { useGameContext } from "@jgengine/react/provider";
25
8
  import { useDisplayProfile } from "@jgengine/react/display";
26
- import { HudViewportProvider } from "@jgengine/react/hudViewport";
27
- import { GameViewportProvider } from "@jgengine/react/gameViewport";
28
9
  import { RotateDeviceScreen } from "@jgengine/react/rotateDevice";
29
- import { useSceneEntities, useSceneObjects, usePlayer, useTarget } from "@jgengine/react/hooks";
30
- import { GameProvider } from "@jgengine/react/provider";
31
- import { CAMERA_FRUSTUM_DEFAULTS } from "@jgengine/core/game/playableGame";
32
10
  import { createSettingsStore } from "@jgengine/core/settings/settingsModel";
33
11
  import { applyBindingOverrides, clearBindingOverride, loadBindingOverrides, saveBindingOverride, } from "@jgengine/core/input/bindingOverrides";
34
- import { playControlsActive } from "@jgengine/core/game/controlGate";
35
12
  import { orientationGateActive, orientationHintActive, resolveOrientationRequirement, } from "@jgengine/core/ui/orientation";
36
- import { resolveOneShotClip } from "@jgengine/core/game/modelAnimation";
37
- import { sky as resolveSkyDescriptor } from "@jgengine/core/world/features";
38
- import { devtools } from "@jgengine/core/devtools/devtools";
39
- import { VERSION } from "@jgengine/core/meta/changelog";
40
- import { AudioListener, EntityAudioEmitters, ObjectAudioEmitters } from "./audio/AudioComponents.js";
13
+ import { armFallbackSeams } from "@jgengine/core/devtools/fallbackSeams";
41
14
  import { createAudioEngine } from "./audio/audioEngine.js";
42
- import { PostProcessing } from "./postfx/PostProcessing.js";
43
- import { CollisionDebugWorld } from "./devtools/CollisionDebugWorld.js";
44
- import { collisionDebug } from "./devtools/collisionDebug.js";
45
- import { DevtoolsOverlay, DevtoolsRendererProbe, withDevtoolsLatency } from "./devtools/DevtoolsOverlay.js";
46
- import { GAME_SIM_FRAME_PRIORITY, GameCameraRig, PlayerFovProvider, PlayerFovSlider, resolveRigKind, rtsPanKeysConflict, } from "./camera/index.js";
47
- import { resolveModel, tryResolveCatalogModel } from "./render/resolveModel.js";
48
- import { CullingProvider, useRenderVisibility } from "./visibility/CullingProvider.js";
49
- import { SkyDaylight, TimeOfDayDaylight } from "./environment.js";
50
- import { resolveSkyLightOwnership, skyEmitsLights } from "./environment/skyLightingPolicy.js";
51
- import { EnvironmentScene } from "./environment/EnvironmentScene.js";
52
- import { applyMaterialOverride } from "./materialOverride.js";
53
- import { PointerProbe } from "./pointer/PointerProbe.js";
54
- import { applyPaintTextureToMaterials, cacheStandardMaterials, cloneModelScene, createPaintCanvas, disposeClonedMaterials, syncPaintCanvas, } from "./render/modelRender.js";
55
- import { writeEntityPose } from "./world/entityPose.js";
56
- import { MarqueeBox, ContextMenuView } from "./pointer/PointerOverlays.js";
57
- import { createPointerService, POINTER_ENTITY_KEY, POINTER_OBJECT_KEY, } from "./pointer/pointerService.js";
58
- import { CombatCameraShake, ProjectileTracers, Reticle, WorldEntityBars, WorldFloatText, WorldTelegraphs, } from "./world/WorldHud.js";
59
- import { GridWorldScene } from "./world/GridWorldScene.js";
60
- import { WorldItems } from "./world/WorldItems.js";
15
+ import { attachAudioEventWire } from "./audio/audioWire.js";
16
+ import { installAgentBridge } from "./devtools/agentBridge.js";
17
+ import { withDevtoolsLatency } from "./devtools/DevtoolsOverlay.js";
18
+ import { resolveRigKind } from "./camera/index.js";
19
+ import { contextModels } from "./render/resolveModel.js";
61
20
  import { OrientationHint } from "./touch/OrientationHint.js";
62
- import { TouchControlsDock, TouchPlaySurface, touchDockClearance } from "./touch/TouchControlsOverlay.js";
63
- import { BUILT_IN_SETTING_CATEGORIES } from "@jgengine/core/settings/settingsModel";
64
- import { SettingsProvider } from "@jgengine/react/settings";
65
- import { SettingsRuntime } from "./settings/SettingsRuntime.js";
66
- import { SettingsChrome } from "./settings/SettingsChrome.js";
67
- import { AudioSettingsBridge, useGraphicsSettings } from "./settings/appliedSettings.js";
21
+ import { useTouchStyle, useGraphicsSettings } from "./settings/appliedSettings.js";
22
+ import { logRuntimeError, } from "./diagnostics/RuntimeDiagnostics.js";
23
+ import { EMPTY_RESERVED } from "./shellConstants.js";
24
+ import { useShellMultiplayerSync } from "./useShellMultiplayerSync.js";
25
+ import { ShellHudPresentation } from "./ShellHudPresentation.js";
26
+ import { Shell3dPresentation } from "./Shell3dPresentation.js";
68
27
  const DEV_USER_ID = "dev-player";
69
- const TURN_SPEED = 2.4;
70
- const PRIMARY_CLICK_MOVE_THRESHOLD_PX = 6;
71
- const GROUND_SIZE = 160;
72
- const GROUND_SEGMENTS = 80;
73
- const DEFAULT_BACKGROUND_COLOR = "#14161b";
74
- const DEFAULT_BACKDROP_FOG_COLOR = "#1a1c22";
75
- function errorToDiagnostic(error, phase, componentStack) {
76
- const capturedAt = new Date().toISOString();
77
- if (error instanceof Error) {
78
- return { phase, message: error.message, stack: error.stack, componentStack, capturedAt };
79
- }
80
- return { phase, message: typeof error === "string" ? error : JSON.stringify(error), componentStack, capturedAt };
81
- }
82
- function logRuntimeError(error, phase, componentStack) {
83
- const diagnostic = errorToDiagnostic(error, phase, componentStack);
84
- console.error(`[jgengine:${phase}] ${diagnostic.message}`, error);
85
- return diagnostic;
86
- }
87
- const RESERVED_INPUT_ACTIONS = new Set([
88
- "moveForward",
89
- "moveBack",
90
- "moveLeft",
91
- "moveRight",
92
- "turnLeft",
93
- "turnRight",
94
- "sprint",
95
- "jump",
96
- "tabTarget",
97
- "clearTarget",
98
- "useAbility",
99
- "interact",
100
- ]);
101
- /** No action names are reserved when no camera rig is active (hud/none presentation): games may bind `turnLeft`/`interact`/etc. as their own. */
102
- const EMPTY_RESERVED = new Set();
103
- /** Empty action list — published while the orientation gate is up to suppress all held input without touching the tracker. */
104
- const NO_ACTIONS = [];
105
- const SHELL_MOVEMENT_ACTIONS = ["moveForward", "moveBack", "moveLeft", "moveRight", "jump"];
106
- function shellDrivesPlayerPose(input) {
107
- const bound = input ?? {};
108
- return SHELL_MOVEMENT_ACTIONS.some((action) => action in bound);
109
- }
110
- function findHotbarSlotActions(input) {
111
- return Object.keys(input ?? {}).flatMap((action) => {
112
- const slot = hotbarSlotActionIndex(action);
113
- return slot === null ? [] : [{ action, slot }];
114
- });
115
- }
116
- function hotbarIdFor(playable) {
117
- const declarations = Object.entries(playable.game.inventories ?? {});
118
- const hud = declarations.find(([, declaration]) => declaration.hud === "hotbar");
119
- return (hud ?? declarations[0])?.[0] ?? null;
120
- }
121
- function executeHotbarSlot(ctx, fromId, hotbarId, slot, yaw, pitch, aimOverride) {
122
- const stack = ctx.player.inventory.state(hotbarId).slots[slot];
123
- if (stack === undefined || stack === null)
124
- return { ok: false, error: `Hotbar slot ${slot + 1} is empty` };
125
- const result = ctx.item.use.use({
126
- from: fromId,
127
- itemId: stack.itemId,
128
- inventoryId: hotbarId,
129
- aim: aimOverride ?? { yaw, pitch },
130
- });
131
- return result.error === undefined ? { ok: true } : { ok: false, error: result.error };
132
- }
133
- function pointerAimFor(ctx, service) {
134
- const hit = service.worldHit();
135
- if (hit === null)
136
- return undefined;
137
- const shooter = ctx.scene.entity.get(ctx.player.possession.active(ctx.player.userId)) ??
138
- ctx.scene.entity.get(ctx.player.userId);
139
- if (shooter === null)
140
- return undefined;
141
- const eye = eyeHeightFromColliders(ctx.scene.entity.collidersOf(shooter.id));
142
- return aimToPoint([shooter.position[0], shooter.position[1] + eye, shooter.position[2]], hit.point);
143
- }
144
- function pointerContextMenu(ctx, playable, hit) {
145
- if (hit.entity !== null) {
146
- const entity = ctx.scene.entity.get(hit.entity);
147
- const verbs = entity === null ? undefined : playable.content.entityById?.(entity.name)?.verbs;
148
- return buildContextMenu({ kind: "entity", targetId: hit.entity, verbs, point: hit.point });
149
- }
150
- if (hit.object !== null) {
151
- const verbs = ctx.scene.object.catalog(hit.object)?.verbs;
152
- return buildContextMenu({ kind: "object", targetId: hit.object, verbs, point: hit.point });
153
- }
154
- return null;
155
- }
156
- /** Actions from `input` currently held down, for `ctx.input.publish` (#164.1); includes reserved movement/jump actions. */
157
- export function heldActionsFor(tracker, actions) {
158
- return actions.filter((action) => tracker.isDown(action));
159
- }
160
- /** Whether a bound action should fire this frame: on press, or on repeat interval while held (shared by `FrameDriver` and `HudOnlyDriver`). */
161
- export function shouldFireBoundAction(tracker, action, input, repeatFiredAt, now) {
162
- return shouldDispatchAction({
163
- pressed: tracker.wasPressed(action),
164
- down: tracker.isDown(action),
165
- repeatMs: actionRepeatMs(input?.[action]),
166
- lastFiredAt: repeatFiredAt.get(action) ?? null,
167
- now,
168
- });
169
- }
170
- /** Resolves and runs the command bound to `action` via the shell's action→command convention (shared by `FrameDriver` and `HudOnlyDriver`). */
171
- export function dispatchBoundAction(ctx, action, yaw, pitch, aim, reserved = RESERVED_INPUT_ACTIONS, sink = localCommandSink(ctx)) {
172
- const command = resolveActionCommand(action, (name) => ctx.game.commands.has(name), reserved);
173
- if (command !== null)
174
- sink.run(command, { yaw, pitch, aim });
175
- }
176
28
  export { applyMotionImpulses } from "@jgengine/core/runtime/motionIntents";
177
29
  export { nearbyObstacles } from "@jgengine/core/movement/movementModel";
178
30
  export { resolvePhysicsTuning } from "@jgengine/core/movement/playerMovement";
179
31
  export { hasEnvironmentTerrain } from "@jgengine/core/world/terrain";
180
- /** The world's declared sky, when its world feature is an environment with one (#196.1). */
181
- export function resolveWorldSky(world) {
182
- return world?.kind === "environment" ? world.sky : undefined;
183
- }
184
- function colorFromId(id) {
185
- let hash = 0;
186
- for (let index = 0; index < id.length; index += 1)
187
- hash = (hash * 31 + id.charCodeAt(index)) >>> 0;
188
- return `hsl(${hash % 360}, 65%, 55%)`;
189
- }
190
- function DirectionalShadowLight({ entry }) {
191
- const size = entry.shadowCameraSize ?? 40;
192
- return (_jsx("directionalLight", { position: [entry.position[0], entry.position[1], entry.position[2]], intensity: entry.intensity ?? 1.3, color: entry.color, castShadow: entry.castShadow ?? false, "shadow-mapSize-width": entry.shadowMapSize ?? 1024, "shadow-mapSize-height": entry.shadowMapSize ?? 1024, "shadow-camera-left": -size, "shadow-camera-right": size, "shadow-camera-top": size, "shadow-camera-bottom": -size, "shadow-camera-near": 0.5, "shadow-camera-far": Math.max(200, size * 6), "shadow-bias": entry.shadowBias ?? -0.0004, "shadow-normalBias": entry.shadowNormalBias ?? 0.02 }));
193
- }
194
- function ConfiguredLighting({ lighting }) {
195
- 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: [
196
- lighting.hemisphere.skyColor ?? "#bfe3ff",
197
- lighting.hemisphere.groundColor ?? "#4c6b34",
198
- lighting.hemisphere.intensity ?? 0.55,
199
- ] })) : null, (lighting.directional ?? []).map((entry, index) => (_jsx(DirectionalShadowLight, { entry: entry }, index)))] }));
200
- }
201
- function BackdropFog({ fog }) {
202
- if (fog === undefined)
203
- return null;
204
- 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] }));
205
- }
206
- function EntitySprite({ sprite }) {
207
- const texture = useLoader(THREE.TextureLoader, sprite.url);
208
- useEffect(() => {
209
- texture.colorSpace = THREE.SRGBColorSpace;
210
- texture.anisotropy = 4;
211
- texture.needsUpdate = true;
212
- }, [texture]);
213
- 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 }) }));
214
- }
215
- class ModelFallbackBoundary extends Component {
216
- state = { failed: false };
217
- static getDerivedStateFromError() {
218
- return { failed: true };
219
- }
220
- render() {
221
- return this.state.failed ? this.props.fallback : this.props.children;
222
- }
223
- }
224
- function IsolatedEntityModel({ model, instanceId, fallback, }) {
225
- return (_jsx(ModelFallbackBoundary, { fallback: fallback ?? null, children: _jsx(Suspense, { fallback: null, children: _jsx(EntityModel, { model: model, instanceId: instanceId }) }) }));
226
- }
227
- function BoneAttachment({ rig, model, slot, position, rotation, scale, }) {
228
- const gltf = useLoader(GLTFLoader, model.url, (loader) => {
229
- loader.setMeshoptDecoder(MeshoptDecoder);
230
- });
231
- const weaponScene = useMemo(() => cloneModelScene(gltf.scene), [gltf]);
232
- const px = position?.[0] ?? 0;
233
- const py = position?.[1] ?? 0;
234
- const pz = position?.[2] ?? 0;
235
- const rx = rotation?.[0] ?? 0;
236
- const ry = rotation?.[1] ?? 0;
237
- const rz = rotation?.[2] ?? 0;
238
- const s = scale ?? 1;
239
- useEffect(() => {
240
- const bone = rig.getObjectByName(slot);
241
- if (bone === undefined) {
242
- if (typeof console !== "undefined") {
243
- console.warn(`[jgengine] entityModels attachment: bone/slot "${slot}" not found on the rig`);
244
- }
245
- return;
246
- }
247
- weaponScene.position.set(px, py, pz);
248
- weaponScene.rotation.set(rx, ry, rz);
249
- weaponScene.scale.setScalar(s);
250
- bone.add(weaponScene);
251
- return () => {
252
- bone.remove(weaponScene);
253
- };
254
- }, [rig, weaponScene, slot, px, py, pz, rx, ry, rz, s]);
255
- useEffect(() => () => disposeClonedMaterials(weaponScene), [weaponScene]);
256
- return null;
257
- }
258
- /** Resolves an entity model plus any bone attachments' models through the asset catalog, so `EntityModel` receives fully-resolved `ModelConfig`s. */
259
- function resolveEntityModel(value, assets, key) {
260
- const model = resolveModel(value, assets, { seam: "entityModels", key });
261
- if (model?.attachments === undefined)
262
- return model;
263
- return {
264
- ...model,
265
- attachments: model.attachments.map((attachment) => ({
266
- ...attachment,
267
- model: resolveModel(attachment.model, assets) ?? attachment.model,
268
- })),
269
- };
270
- }
271
- function EntityModel({ model, instanceId }) {
272
- const gltf = useLoader(GLTFLoader, model.url, (loader) => {
273
- loader.setMeshoptDecoder(MeshoptDecoder);
274
- });
275
- const ctx = useGameContext();
276
- const material = model.material;
277
- const baseY = model.y ?? 0;
278
- const dims = model.dims;
279
- const scene = useMemo(() => {
280
- const cloned = cloneModelScene(gltf.scene);
281
- if (material !== undefined)
282
- applyMaterialOverride(cloned, material, { clone: false });
283
- return cloned;
284
- }, [gltf, material]);
285
- const measured = useMemo(() => {
286
- if (model.targetHeight === undefined)
287
- return null;
288
- const box = new THREE.Box3().setFromObject(scene);
289
- const height = box.max.y - box.min.y;
290
- if (!Number.isFinite(height) || height <= 0)
291
- return null;
292
- return {
293
- normalize: model.targetHeight / height,
294
- minY: box.min.y,
295
- centerX: (box.min.x + box.max.x) / 2,
296
- centerZ: (box.min.z + box.max.z) / 2,
297
- };
298
- }, [scene, model.targetHeight]);
299
- const scale = (model.scale ?? 1) * (measured?.normalize ?? 1);
300
- const centered = (model.anchor ?? "center") === "center" && dims !== undefined;
301
- const position = measured !== null
302
- ? [-scale * measured.centerX, baseY - scale * measured.minY, -scale * measured.centerZ]
303
- : centered
304
- ? [-scale * dims.center.x, baseY - scale * dims.minY, -scale * dims.center.z]
305
- : [0, baseY, 0];
306
- useEffect(() => () => {
307
- disposeClonedMaterials(scene);
308
- }, [scene]);
309
- const animation = model.animation;
310
- const mixerRef = useRef(null);
311
- const animationPausedRef = useRef(false);
312
- const stateActionsRef = useRef(null);
313
- const states = animation?.states;
314
- const oneShots = animation?.oneShots;
315
- const oneShotPlayRef = useRef(null);
316
- const activeOneShotRef = useRef(null);
317
- useEffect(() => {
318
- if (animation === undefined || gltf.animations.length === 0) {
319
- mixerRef.current = null;
320
- stateActionsRef.current = null;
321
- return;
322
- }
323
- const mixer = new THREE.AnimationMixer(scene);
324
- if (states !== undefined) {
325
- const clipFor = (name) => THREE.AnimationClip.findByName(gltf.animations, name) ?? gltf.animations[0];
326
- const actions = {
327
- idle: mixer.clipAction(clipFor(states.idle)),
328
- walk: mixer.clipAction(clipFor(states.walk)),
329
- ...(states.run === undefined ? {} : { run: mixer.clipAction(clipFor(states.run)) }),
330
- };
331
- for (const action of Object.values(actions)) {
332
- action.setLoop(THREE.LoopRepeat, Infinity);
333
- action.timeScale = animation.timeScale ?? 1;
334
- action.enabled = true;
335
- }
336
- actions.idle.play();
337
- mixer.update(0);
338
- mixerRef.current = mixer;
339
- stateActionsRef.current = { actions, active: "idle", lastPos: null, smoothedSpeed: 0 };
340
- animationPausedRef.current = false;
341
- let onOneShotFinished = null;
342
- if (oneShots !== undefined) {
343
- const clipNames = new Set();
344
- for (const spec of Object.values(oneShots)) {
345
- if (typeof spec === "string")
346
- clipNames.add(spec);
347
- else
348
- for (const name of spec)
349
- clipNames.add(name);
350
- }
351
- const oneShotActions = new Map();
352
- for (const name of clipNames) {
353
- const found = THREE.AnimationClip.findByName(gltf.animations, name);
354
- if (found === null)
355
- continue;
356
- const oneShotAction = mixer.clipAction(found);
357
- oneShotAction.setLoop(THREE.LoopOnce, 1);
358
- oneShotAction.enabled = true;
359
- oneShotActions.set(name, oneShotAction);
360
- }
361
- onOneShotFinished = ({ action }) => {
362
- const active = activeOneShotRef.current;
363
- if (active === null || action !== active.action || active.isDeath)
364
- return;
365
- const machine = stateActionsRef.current;
366
- const back = machine?.actions[machine.active];
367
- action.fadeOut(0.15);
368
- if (back !== undefined)
369
- back.reset().fadeIn(0.15).play();
370
- activeOneShotRef.current = null;
371
- };
372
- mixer.addEventListener("finished", onOneShotFinished);
373
- oneShotPlayRef.current = (event) => {
374
- const active = activeOneShotRef.current;
375
- if (active !== null && active.isDeath)
376
- return;
377
- const clipName = resolveOneShotClip(oneShots, event, Math.random());
378
- if (clipName === null)
379
- return;
380
- const oneShotAction = oneShotActions.get(clipName);
381
- if (oneShotAction === undefined)
382
- return;
383
- const machine = stateActionsRef.current;
384
- machine?.actions[machine.active]?.fadeOut(0.1);
385
- if (active !== null && active.action !== oneShotAction)
386
- active.action.stop();
387
- oneShotAction.clampWhenFinished = event === "death";
388
- oneShotAction.reset().fadeIn(0.1).play();
389
- activeOneShotRef.current = { action: oneShotAction, isDeath: event === "death" };
390
- };
391
- }
392
- return () => {
393
- if (onOneShotFinished !== null)
394
- mixer.removeEventListener("finished", onOneShotFinished);
395
- oneShotPlayRef.current = null;
396
- activeOneShotRef.current = null;
397
- mixer.stopAllAction();
398
- mixerRef.current = null;
399
- stateActionsRef.current = null;
400
- };
401
- }
402
- const clip = (animation.clip !== undefined ? THREE.AnimationClip.findByName(gltf.animations, animation.clip) : undefined) ??
403
- gltf.animations[0];
404
- const action = mixer.clipAction(clip);
405
- action.setLoop(animation.loop === false ? THREE.LoopOnce : THREE.LoopRepeat, Infinity);
406
- action.clampWhenFinished = animation.loop === false;
407
- action.timeScale = animation.timeScale ?? 1;
408
- action.enabled = true;
409
- action.paused = animation.paused === true;
410
- action.play();
411
- if (animation.time !== undefined)
412
- action.time = animation.time;
413
- mixer.update(0);
414
- mixerRef.current = mixer;
415
- animationPausedRef.current = animation.paused === true;
416
- return () => {
417
- mixer.stopAllAction();
418
- mixerRef.current = null;
419
- };
420
- }, [
421
- scene,
422
- gltf,
423
- animation?.clip,
424
- animation?.loop,
425
- animation?.timeScale,
426
- animation?.paused,
427
- animation?.time,
428
- states,
429
- oneShots,
430
- ]);
431
- useEffect(() => {
432
- if (instanceId === undefined || oneShots === undefined)
433
- return;
434
- const fire = (event) => oneShotPlayRef.current?.(event);
435
- const offAnimation = ctx.game.events.on("entity.animation", (event) => {
436
- if (event.instanceId === instanceId)
437
- fire(event.event);
438
- });
439
- const offHit = ctx.game.events.on("combat.hitReaction", (event) => {
440
- if (event.instanceId === instanceId)
441
- fire("hit");
442
- });
443
- const offDied = ctx.game.events.on("entity.died", (event) => {
444
- if (event.instanceId === instanceId)
445
- fire("death");
446
- });
447
- return () => {
448
- offAnimation();
449
- offHit();
450
- offDied();
451
- };
452
- }, [ctx, instanceId, oneShots]);
453
- const paintCanvasRef = useRef(null);
454
- const paintDrawnCountRef = useRef(0);
455
- const paintVersionRef = useRef(-1);
456
- const materialCacheRef = useRef(null);
457
- useEffect(() => {
458
- paintCanvasRef.current = null;
459
- paintDrawnCountRef.current = 0;
460
- paintVersionRef.current = -1;
461
- materialCacheRef.current = null;
462
- }, [scene]);
463
- useFrame((_state, delta) => {
464
- const stateMachine = stateActionsRef.current;
465
- if (stateMachine !== null && states !== undefined && instanceId !== undefined && delta > 0) {
466
- const entity = ctx.scene.entity.get(instanceId);
467
- if (entity !== null) {
468
- const [x, , z] = entity.position;
469
- if (stateMachine.lastPos !== null) {
470
- const instantSpeed = Math.hypot(x - stateMachine.lastPos[0], z - stateMachine.lastPos[2]) / delta;
471
- stateMachine.smoothedSpeed +=
472
- (instantSpeed - stateMachine.smoothedSpeed) * Math.min(1, delta * 12);
473
- }
474
- stateMachine.lastPos = [x, entity.position[1], z];
475
- const walkSpeed = states.walkSpeed ?? 0.5;
476
- const runSpeed = states.runSpeed ?? 6;
477
- const next = stateMachine.smoothedSpeed < walkSpeed
478
- ? "idle"
479
- : stateMachine.actions.run !== undefined && stateMachine.smoothedSpeed >= runSpeed
480
- ? "run"
481
- : "walk";
482
- if (next !== stateMachine.active) {
483
- if (activeOneShotRef.current === null) {
484
- const fade = states.fadeSec ?? 0.2;
485
- const from = stateMachine.actions[stateMachine.active];
486
- const to = stateMachine.actions[next];
487
- if (from !== undefined && to !== undefined) {
488
- to.reset().fadeIn(fade).play();
489
- from.fadeOut(fade);
490
- }
491
- }
492
- stateMachine.active = next;
493
- }
494
- }
495
- }
496
- if (mixerRef.current !== null && !animationPausedRef.current)
497
- mixerRef.current.update(delta);
498
- if (instanceId === undefined)
499
- return;
500
- const paint = ctx.scene.entity.paint;
501
- const version = paint.version(instanceId);
502
- if (version === paintVersionRef.current)
503
- return;
504
- paintVersionRef.current = version;
505
- const strokes = paint.strokes(instanceId);
506
- const cache = cacheStandardMaterials(scene, materialCacheRef.current);
507
- materialCacheRef.current = cache;
508
- if (paintCanvasRef.current === null) {
509
- if (strokes.length === 0)
510
- return;
511
- const seed = cache.materials[0];
512
- if (seed === undefined)
513
- return;
514
- const paintCanvas = createPaintCanvas(seed);
515
- paintCanvasRef.current = paintCanvas;
516
- applyPaintTextureToMaterials(cache.materials, paintCanvas);
517
- }
518
- paintDrawnCountRef.current = syncPaintCanvas(paintCanvasRef.current, cache.seedColor, strokes, paintDrawnCountRef.current);
519
- });
520
- return (_jsxs(_Fragment, { children: [_jsx("primitive", { object: scene, position: position, scale: [scale, scale, scale] }), (model.attachments ?? []).map((attachment, index) => typeof attachment.model === "string" ? null : (_jsx(BoneAttachment, { rig: scene, model: attachment.model, slot: attachment.slot, position: attachment.position, rotation: attachment.rotation, scale: attachment.scale }, `${attachment.slot}-${index}`)))] }));
521
- }
522
- function EntityMarker({ entity, custom, model, sprite, isLocal, targeted, selected, onSelect, }) {
523
- const groupRef = useRef(null);
524
- const ctx = useGameContext();
525
- const visibleRef = useRenderVisibility();
526
- const entityId = entity.id;
527
- const role = entity.role;
528
- const name = entity.name;
529
- const color = isLocal ? "#4ade80" : role === "npc" ? colorFromId(name) : "#9ca3af";
530
- useFrame(() => {
531
- const group = groupRef.current;
532
- if (group === null)
533
- return;
534
- const live = ctx.scene.entity.get(entityId);
535
- if (live === null)
536
- return;
537
- writeEntityPose(group, live);
538
- group.visible = visibleRef.current(entityId);
539
- });
540
- return (_jsxs("group", { ref: groupRef, userData: { [POINTER_ENTITY_KEY]: entityId }, onPointerDown: (event) => {
541
- event.stopPropagation();
542
- if (isLocal)
543
- return;
544
- const live = ctx.scene.entity.get(entityId);
545
- if (live !== null)
546
- onSelect(live);
547
- }, 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(IsolatedEntityModel, { model: model, instanceId: entityId, fallback: sprite !== undefined ? _jsx(EntitySprite, { sprite: sprite }) : undefined })) : 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] }));
548
- }
549
- function ObjectMarker({ object, custom, model, style, }) {
550
- const groupRef = useRef(null);
551
- const ctx = useGameContext();
552
- const visibleRef = useRenderVisibility();
553
- const instanceId = object.instanceId;
554
- const [scaleX, scaleY, scaleZ] = objectVisualScale(object.visual);
555
- const color = object.visual?.color ?? style?.color ?? colorFromId(object.catalogId);
556
- const opacity = object.visual?.opacity ?? style?.opacity ?? 1;
557
- useFrame(() => {
558
- const group = groupRef.current;
559
- if (group === null)
560
- return;
561
- const live = ctx.scene.object.get(instanceId);
562
- if (live === null)
563
- return;
564
- writeEntityPose(group, live);
565
- group.visible = visibleRef.current(instanceId);
566
- });
567
- return (_jsx("group", { ref: groupRef, userData: { [POINTER_OBJECT_KEY]: instanceId }, children: custom !== undefined && custom !== null ? (custom) : model !== undefined ? (_jsx(IsolatedEntityModel, { 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 })] })) }));
568
- }
569
- function GroundPlane() {
570
- const geometry = useMemo(() => {
571
- const next = new THREE.PlaneGeometry(GROUND_SIZE, GROUND_SIZE, GROUND_SEGMENTS, GROUND_SEGMENTS);
572
- const positions = next.attributes.position;
573
- for (let index = 0; index < positions.count; index += 1) {
574
- const x = positions.getX(index);
575
- const y = positions.getY(index);
576
- const height = Math.sin(x * 0.16) * 0.12 +
577
- Math.cos(y * 0.11) * 0.1 +
578
- Math.sin((x + y) * 0.05) * 0.16;
579
- positions.setZ(index, height);
580
- }
581
- next.computeVertexNormals();
582
- return next;
583
- }, []);
584
- return (_jsx("mesh", { "rotation-x": -Math.PI / 2, geometry: geometry, children: _jsx("meshStandardMaterial", { color: "#283729", roughness: 0.9, metalness: 0 }) }));
585
- }
586
- function RockField() {
587
- const rocks = useMemo(() => Array.from({ length: 18 }, (_, index) => {
588
- const angle = index * 2.399963229728653;
589
- const radius = 10 + ((index * 17) % 58);
590
- return {
591
- id: `rock-${index}`,
592
- x: Math.cos(angle) * radius,
593
- z: Math.sin(angle) * radius,
594
- scale: 0.45 + ((index * 13) % 11) / 10,
595
- rotation: angle,
596
- };
597
- }), []);
598
- 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))) }));
599
- }
600
- function WorldEnvironment({ environment: Environment }) {
601
- if (Environment !== undefined)
602
- return _jsx(Environment, {});
603
- return (_jsxs(_Fragment, { children: [_jsx(GroundPlane, {}), _jsx("gridHelper", { args: [160, 80, "#3a3f4a", "#2b2f38"], "position-y": 0.01 }), _jsx(RockField, {})] }));
604
- }
605
- function WorldActors({ entitySprites, entityModels, objectModels, objectStyles, assets, renderEntity, renderObject, selectedIds, hideLocalActor, }) {
606
- const ctx = useGameContext();
607
- const entities = useSceneEntities();
608
- const objects = useSceneObjects();
609
- const player = usePlayer();
610
- const targetId = useTarget(player.userId);
611
- const controlledId = ctx.player.possession.active(player.userId);
612
- const handleSelect = (entity) => {
613
- const relation = ctx.scene.entity.canReceive(entity.id, "damage") === null ? "hostile" : "friendly";
614
- ctx.scene.entity.setTarget(controlledId, relation === "hostile" || entity.role === "npc" ? entity.id : null);
615
- };
616
- return (_jsxs(_Fragment, { children: [entities
617
- .filter((entity) => entity.name !== WORLD_ITEM_ENTITY_NAME)
618
- .filter((entity) => !(hideLocalActor && entity.id === controlledId))
619
- .map((entity) => (_jsx(EntityMarker, { entity: entity, custom: renderEntity?.(entity), model: resolveEntityModel(entityModels?.[entity.name], assets, entity.name), sprite: entitySprites?.[entity.name], isLocal: entity.id === controlledId, targeted: entity.id === targetId, selected: selectedIds.has(entity.id), onSelect: handleSelect }, entity.id))), objects.map((object) => {
620
- const model = resolveModel(objectModels?.[object.catalogId], assets, {
621
- seam: "objectModels",
622
- key: object.catalogId,
623
- }) ?? tryResolveCatalogModel(object.catalogId, assets);
624
- return (_jsx(ObjectMarker, { object: object, custom: renderObject?.(object), model: model, style: objectStyles?.[object.catalogId] }, object.instanceId));
625
- })] }));
626
- }
627
- function WorldView({ entitySprites, entityModels, objectModels, objectStyles, environment, assets, renderEntity, renderObject, selectedIds, hideLocalActor, }) {
628
- 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, hideLocalActor: hideLocalActor })] }));
629
- }
630
- function RemotePlayers({ rows }) {
631
- 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))) }));
632
- }
633
- function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef, pointerAxisRef, gateRef, onRuntimeError, multiplayer, serverIdRef, pointerService, pointerAim, pingCommand, poster, onPosterSettled, }) {
634
- const posterElapsedRef = useRef(0);
635
- const posterDoneRef = useRef(false);
636
- const hasReportedTickError = useRef(false);
637
- const repeatFiredAtRef = useRef(new Map());
638
- const slotActions = useMemo(() => findHotbarSlotActions(playable.game.input), [playable]);
639
- const hotbarId = useMemo(() => hotbarIdFor(playable), [playable]);
640
- const movementTuning = useMemo(() => resolvePlayerMovementTuning({
641
- collision: playable.collision,
642
- movement: playable.movement,
643
- physics: playable.game.physics,
644
- world: playable.game.world,
645
- }), [playable]);
646
- const autoPickupRadius = useMemo(() => {
647
- const cfg = playable.worldItem?.autoPickup;
648
- if (cfg === undefined || cfg === false)
649
- return null;
650
- const fallback = playable.worldItem?.pickupRadius ?? DEFAULT_PICKUP_RADIUS;
651
- return cfg === true ? fallback : cfg.radius ?? fallback;
652
- }, [playable]);
653
- const drivesPose = useMemo(() => shellDrivesPlayerPose(playable.game.input), [playable]);
654
- const serverAuthoritative = useMemo(() => isServerAuthoritative(playable.game.multiplayer) && multiplayer !== null, [playable, multiplayer]);
655
- const commandSink = useMemo(() => ({
656
- run: (name, input) => resolveCommandSink(ctx, {
657
- serverAuthoritative,
658
- backend: multiplayer?.backend ?? null,
659
- serverId: serverIdRef.current,
660
- }).run(name, input),
661
- }), [ctx, multiplayer, serverAuthoritative, serverIdRef]);
662
- const inputSink = useMemo(() => ({
663
- send: (frame) => resolveInputSink({
664
- serverAuthoritative,
665
- backend: multiplayer?.backend ?? null,
666
- serverId: serverIdRef.current,
667
- }).send(frame),
668
- }), [multiplayer, serverAuthoritative, serverIdRef]);
669
- const lastSentInputRef = useRef(null);
670
- const inputActions = useMemo(() => Object.keys(playable.game.input ?? {}), [playable]);
671
- useFrame((_state, rawDt) => {
672
- if (poster) {
673
- if (posterDoneRef.current)
674
- return;
675
- posterElapsedRef.current += Math.min(rawDt, 0.05);
676
- if (posterElapsedRef.current >= POSTER_SETTLE_SECONDS) {
677
- posterDoneRef.current = true;
678
- onPosterSettled();
679
- return;
680
- }
681
- }
682
- const sendInput = () => {
683
- if (!serverAuthoritative)
684
- return;
685
- const frame = { held: ctx.input.held(), pointer: ctx.input.pointer() };
686
- const last = lastSentInputRef.current;
687
- if (last !== null && inputFramesEqual(last, frame))
688
- return;
689
- lastSentInputRef.current = frame;
690
- inputSink.send(frame);
691
- };
692
- if (gateRef.current) {
693
- ctx.input.publish(heldActionsFor(tracker, NO_ACTIONS));
694
- sendInput();
695
- return;
696
- }
697
- const simStart = performance.now();
698
- try {
699
- let endPhase = devtools.profile.begin("time+input");
700
- const dt = Math.min(rawDt, 0.05);
701
- const gameDt = ctx.time.advance(dt);
702
- ctx.input.publish(heldActionsFor(tracker, inputActions));
703
- ctx.input.publishPointer(pointerAxisRef.current);
704
- sendInput();
705
- const turnInput = (tracker.isDown("turnRight") ? 1 : 0) - (tracker.isDown("turnLeft") ? 1 : 0);
706
- if (turnInput !== 0)
707
- yawRef.current = steerYaw(yawRef.current, turnInput, TURN_SPEED, dt);
708
- endPhase();
709
- const playerId = ctx.player.possession.active(ctx.player.userId);
710
- const player = ctx.scene.entity.get(playerId);
711
- if (player !== null && drivesPose && !serverAuthoritative) {
712
- endPhase = devtools.profile.begin("pose");
713
- stepPlayerMovement(ctx, ctx.player.userId, { held: ctx.input.held(), pointer: ctx.input.pointer() }, rawDt, movementTuning, yawRef.current);
714
- endPhase();
715
- }
716
- if (autoPickupRadius !== null && !serverAuthoritative) {
717
- endPhase = devtools.profile.begin("pickup");
718
- const self = ctx.scene.entity.get(playerId);
719
- if (self !== null) {
720
- const nearest = ctx.scene.worldItem.nearestInRadius(self.position, autoPickupRadius);
721
- if (nearest !== null)
722
- ctx.scene.worldItem.pickup(nearest, ctx.player.userId);
723
- }
724
- endPhase();
725
- }
726
- if (!serverAuthoritative) {
727
- devtools.profile.measure("onTick", () => {
728
- playable.loop.onTick(ctx, gameDt);
729
- });
730
- }
731
- endPhase = devtools.profile.begin("actions");
732
- if (tracker.wasPressed("tabTarget")) {
733
- if (ctx.game.commands.has("target.cycle"))
734
- ctx.game.commands.run("target.cycle", {});
735
- else
736
- ctx.scene.entity.cycleTarget(playerId, { filter: "hostile" });
737
- }
738
- if (tracker.wasPressed("clearTarget")) {
739
- if (ctx.game.commands.has("target.clear"))
740
- ctx.game.commands.run("target.clear", {});
741
- else
742
- ctx.scene.entity.setTarget(playerId, null);
743
- }
744
- if (pingCommand !== undefined && tracker.wasPressed("ping")) {
745
- const hit = pointerService.worldHit();
746
- if (hit !== null && ctx.game.commands.has(pingCommand)) {
747
- commandSink.run(pingCommand, {
748
- point: hit.point,
749
- entity: hit.entity,
750
- object: hit.object,
751
- normal: hit.normal,
752
- });
753
- }
754
- }
755
- const aimOverride = pointerAim ? pointerAimFor(ctx, pointerService) : undefined;
756
- const commandAim = aimOverride ?? { yaw: yawRef.current, pitch: pitchRef.current };
757
- if (collisionDebug.getState().layers.aimLaser) {
758
- const aimFrom = ctx.player.possession.active(playerId) ?? playerId;
759
- collisionDebug.setAimProbe({
760
- from: aimFrom,
761
- aim: commandAim,
762
- originPolicy: { kind: "eye" },
763
- maxDistance: 100,
764
- });
765
- }
766
- else if (collisionDebug.getAimProbe() !== null) {
767
- collisionDebug.setAimProbe(null);
768
- }
769
- const nowMs = performance.now();
770
- for (const action of Object.keys(playable.game.input ?? {})) {
771
- const pressed = tracker.wasPressed(action);
772
- if (action === "ping" && pingCommand !== undefined)
773
- continue;
774
- if (action === "interact") {
775
- if (!pressed)
776
- continue;
777
- const prompts = playable.prompts?.(ctx);
778
- const focus = prompts === undefined ? null : ctx.scene.entity.get(playerId);
779
- if (prompts !== undefined && focus !== null) {
780
- const active = resolveActivePrompt({ x: focus.position[0], z: focus.position[2] }, prompts);
781
- if (active !== null && active.prompt.invoke !== null) {
782
- commandSink.run(active.prompt.invoke.name, active.prompt.invoke.input);
783
- }
784
- }
785
- continue;
786
- }
787
- if (!shouldFireBoundAction(tracker, action, playable.game.input, repeatFiredAtRef.current, nowMs))
788
- continue;
789
- repeatFiredAtRef.current.set(action, nowMs);
790
- dispatchBoundAction(ctx, action, yawRef.current, pitchRef.current, commandAim, RESERVED_INPUT_ACTIONS, commandSink);
791
- }
792
- if (hotbarId !== null) {
793
- for (const { action, slot } of slotActions) {
794
- if (!tracker.wasPressed(action))
795
- continue;
796
- const result = executeHotbarSlot(ctx, playerId, hotbarId, slot, yawRef.current, pitchRef.current, aimOverride);
797
- if (!result.ok)
798
- console.warn(`[jgengine:item-use] ${result.error}`);
799
- }
800
- const usePrimary = tracker.wasPressed("useAbility") || (primaryClickRef.current && hotbarId !== null);
801
- if (usePrimary) {
802
- primaryClickRef.current = false;
803
- const slots = ctx.player.inventory.state(hotbarId).slots;
804
- const preferred = playable.hotbarSelection?.() ?? -1;
805
- const slot = preferred >= 0 && slots[preferred] !== null && slots[preferred] !== undefined
806
- ? preferred
807
- : slots.findIndex((stack) => stack !== null);
808
- if (slot >= 0) {
809
- const result = executeHotbarSlot(ctx, playerId, hotbarId, slot, yawRef.current, pitchRef.current, aimOverride);
810
- if (!result.ok)
811
- console.warn(`[jgengine:item-use] ${result.error}`);
812
- }
813
- }
814
- }
815
- tracker.endFrame();
816
- endPhase();
817
- const serverId = serverIdRef.current;
818
- if (multiplayer !== null && serverId !== null) {
819
- endPhase = devtools.profile.begin("presence");
820
- const focus = ctx.scene.entity.get(playerId);
821
- if (focus !== null) {
822
- multiplayer.backend.presenceSync.syncPose(serverId, {
823
- x: focus.position[0],
824
- y: focus.position[1],
825
- z: focus.position[2],
826
- rotationY: focus.rotationY,
827
- rotationPitch: pitchRef.current,
828
- });
829
- }
830
- endPhase();
831
- }
832
- }
833
- catch (error) {
834
- if (!hasReportedTickError.current) {
835
- hasReportedTickError.current = true;
836
- onRuntimeError(error, "tick");
837
- }
838
- }
839
- devtools.frame.record({ frameMs: rawDt * 1000, simMs: performance.now() - simStart });
840
- }, GAME_SIM_FRAME_PRIORITY);
841
- return null;
842
- }
843
- function HudOnlyDriver({ ctx, playable, tracker, pointerAxisRef, gateRef, onRuntimeError, }) {
844
- const hasReportedTickError = useRef(false);
845
- const repeatFiredAtRef = useRef(new Map());
846
- const lastFrameRef = useRef(null);
847
- const inputActions = useMemo(() => Object.keys(playable.game.input ?? {}), [playable]);
848
- useEffect(() => {
849
- let frameId;
850
- const tick = (now) => {
851
- frameId = requestAnimationFrame(tick);
852
- const last = lastFrameRef.current;
853
- lastFrameRef.current = now;
854
- if (last === null)
855
- return;
856
- if (gateRef.current) {
857
- ctx.input.publish(heldActionsFor(tracker, NO_ACTIONS));
858
- return;
859
- }
860
- const rawDt = (now - last) / 1000;
861
- const simStart = performance.now();
862
- try {
863
- let endPhase = devtools.profile.begin("time+input");
864
- const dt = Math.min(rawDt, 0.05);
865
- const gameDt = ctx.time.advance(dt);
866
- ctx.input.publish(heldActionsFor(tracker, inputActions));
867
- ctx.input.publishPointer(pointerAxisRef.current);
868
- endPhase();
869
- devtools.profile.measure("onTick", () => {
870
- playable.loop.onTick(ctx, gameDt);
871
- });
872
- endPhase = devtools.profile.begin("actions");
873
- const nowMs = performance.now();
874
- for (const action of inputActions) {
875
- if (!shouldFireBoundAction(tracker, action, playable.game.input, repeatFiredAtRef.current, nowMs))
876
- continue;
877
- repeatFiredAtRef.current.set(action, nowMs);
878
- dispatchBoundAction(ctx, action, 0, 0, { yaw: 0, pitch: 0 }, EMPTY_RESERVED);
879
- }
880
- tracker.endFrame();
881
- endPhase();
882
- }
883
- catch (error) {
884
- if (!hasReportedTickError.current) {
885
- hasReportedTickError.current = true;
886
- onRuntimeError(error, "tick");
887
- }
888
- }
889
- devtools.frame.record({ frameMs: rawDt * 1000, simMs: performance.now() - simStart });
890
- };
891
- frameId = requestAnimationFrame(tick);
892
- return () => cancelAnimationFrame(frameId);
893
- }, [ctx, playable, tracker, pointerAxisRef, gateRef, onRuntimeError, inputActions]);
894
- return null;
895
- }
896
- class GameUiErrorBoundary extends Component {
897
- state = { failed: false };
898
- static getDerivedStateFromError() {
899
- return { failed: true };
900
- }
901
- componentDidCatch(error, info) {
902
- this.props.onRuntimeError(error, "ui-render", info.componentStack ?? undefined);
903
- }
904
- render() {
905
- if (this.state.failed)
906
- return null;
907
- return this.props.children;
908
- }
909
- }
910
- function expandedReactError(message) {
911
- return /Minified React error #185\b/.test(message)
912
- ? "Maximum update depth exceeded. A component is repeatedly updating state during render or after every update."
913
- : null;
914
- }
915
- function diagnosticReport(diagnostic, gameName) {
916
- const explanation = expandedReactError(diagnostic.message);
917
- return [
918
- "JGengine runtime error",
919
- `Game: ${gameName}`,
920
- `Engine: ${VERSION}`,
921
- `Phase: ${diagnostic.phase}`,
922
- `Time: ${diagnostic.capturedAt}`,
923
- `Page: ${window.location.origin}${window.location.pathname}`,
924
- `Browser: ${navigator.userAgent}`,
925
- explanation === null ? null : `Explanation: ${explanation}`,
926
- "",
927
- `Message: ${diagnostic.message}`,
928
- diagnostic.stack === undefined ? null : `JavaScript stack:\n${diagnostic.stack}`,
929
- diagnostic.componentStack === undefined ? null : `React component stack:\n${diagnostic.componentStack}`,
930
- ]
931
- .filter((line) => line !== null)
932
- .join("\n");
933
- }
934
- function DiagnosticOverlay({ diagnostics, gameName }) {
935
- const [copied, setCopied] = useState(false);
936
- if (diagnostics.length === 0)
937
- return null;
938
- const latest = diagnostics[diagnostics.length - 1];
939
- const explanation = expandedReactError(latest.message);
940
- const report = diagnosticReport(latest, gameName);
941
- const issueBody = report.length > 8000 ? `${report.slice(0, 8000)}\n\n[Report truncated; use Copy error for the full report.]` : report;
942
- const issueUrl = `https://github.com/Noisemaker111/jgengine/issues/new?title=${encodeURIComponent(`[BUG] ${latest.phase}: ${latest.message.slice(0, 100)}`)}&body=${encodeURIComponent(issueBody)}`;
943
- 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: () => {
944
- void navigator.clipboard.writeText(report).then(() => {
945
- setCopied(true);
946
- window.setTimeout(() => setCopied(false), 2000);
947
- });
948
- }, 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" })] })] }));
949
- }
950
- const POSTER_SETTLE_SECONDS = 1.6;
32
+ /**
33
+ * The shell body `GameHost` renders after resolving multiplayer. Hosts that resolve their own
34
+ * sessions (the dev runner) render it directly; games mount through `GameHost`.
35
+ * @internal
36
+ */
951
37
  export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null, poster = false, onContextReady, }) {
952
38
  const multiplayer = useMemo(() => (rawMultiplayer === null ? null : withDevtoolsLatency(rawMultiplayer)), [rawMultiplayer]);
953
39
  const devtoolsEnabled = playable.devtools !== false && !poster;
40
+ armFallbackSeams(devtoolsEnabled);
954
41
  const [devtoolsOpen, setDevtoolsOpen] = useState(false);
42
+ const devtoolsOpenRef = useRef(false);
43
+ devtoolsOpenRef.current = devtoolsOpen;
44
+ useEffect(() => installAgentBridge({
45
+ playable,
46
+ devtoolsEnabled,
47
+ isDevtoolsOpen: () => devtoolsOpenRef.current,
48
+ setDevtoolsOpen,
49
+ }), [playable, devtoolsEnabled]);
955
50
  const [posterFrozen, setPosterFrozen] = useState(false);
956
51
  const posterSettledRef = useRef(false);
957
52
  const [ctx, setCtx] = useState(null);
@@ -963,16 +58,8 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
963
58
  const serverIdRef = useRef(null);
964
59
  const cameraDraggingRef = useRef(false);
965
60
  const primaryClickRef = useRef(false);
966
- const pointerDownRef = useRef(null);
967
61
  const pointerAxisRef = useRef(null);
968
- const marqueeStartRef = useRef(null);
969
62
  const f2HeldRef = useRef(false);
970
- const f2ChordedRef = useRef(false);
971
- const pointerService = useMemo(() => createPointerService(), []);
972
- const selection = useMemo(() => createSelectionSet(), [playable]);
973
- const [marquee, setMarquee] = useState(null);
974
- const [selectedIds, setSelectedIds] = useState(() => new Set());
975
- const [contextMenu, setContextMenu] = useState(null);
976
63
  const settingsStore = useMemo(() => createSettingsStore(), []);
977
64
  const [bindingOverrides, setBindingOverrides] = useState(() => loadBindingOverrides(playable.game.name));
978
65
  useEffect(() => {
@@ -1001,6 +88,7 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
1001
88
  firstPerson: resolveRigKind(playable.camera) === "first",
1002
89
  config: playable.touch,
1003
90
  }), [playable]);
91
+ const touchStyle = useTouchStyle(settingsStore, touchScheme?.style ?? DEFAULT_TOUCH_STYLE);
1004
92
  const { coarsePointer, portrait, compact } = useDisplayProfile();
1005
93
  const touchSink = useMemo(() => ({ onCodeDown: (code) => tracker.handleDown(code), onCodeUp: (code) => tracker.handleUp(code) }), [tracker]);
1006
94
  const gateRef = useRef(false);
@@ -1021,18 +109,7 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
1021
109
  useEffect(() => {
1022
110
  if (ctx === null)
1023
111
  return;
1024
- const offPlay = ctx.game.events.on("audio.play", ({ sound, at }) => {
1025
- audioEngine.playOneShot(sound, at === undefined ? undefined : { x: at[0], y: at[1], z: at[2] });
1026
- });
1027
- const offMusic = ctx.game.events.on("audio.music", ({ theme, transpose }) => {
1028
- audioEngine.playMusic(theme, transpose === undefined ? undefined : { transpose });
1029
- });
1030
- const offResume = ctx.game.events.on("audio.resume", () => audioEngine.resume());
1031
- return () => {
1032
- offPlay();
1033
- offMusic();
1034
- offResume();
1035
- };
112
+ return attachAudioEventWire(ctx.game.events, audioEngine);
1036
113
  }, [ctx, audioEngine]);
1037
114
  useEffect(() => {
1038
115
  if (typeof document === "undefined")
@@ -1062,13 +139,14 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
1062
139
  setDiagnostics((current) => [...current.slice(-4), { ...diagnostic, id: Date.now() + current.length }]);
1063
140
  };
1064
141
  useEffect(() => {
1065
- playable.game.scene.clear();
1066
142
  setDiagnostics([]);
1067
143
  try {
144
+ const models = contextModels(playable);
1068
145
  const context = createGameContext({
1069
146
  definition: playable.game,
1070
147
  content: playable.content,
1071
148
  player: { userId, isNew: true },
149
+ ...(models === undefined ? {} : { models }),
1072
150
  });
1073
151
  playable.loop.onInit(context);
1074
152
  playable.loop.onNewPlayer(context);
@@ -1080,394 +158,51 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
1080
158
  setCtx(null);
1081
159
  }
1082
160
  return () => {
1083
- playable.game.scene.clear();
1084
161
  setCtx(null);
1085
162
  };
1086
163
  }, [playable, userId]);
1087
- useEffect(() => {
1088
- if (ctx === null || multiplayer === null)
1089
- return;
1090
- let disposed = false;
1091
- const cleanups = [];
1092
- void multiplayer.backend.transport
1093
- .joinServer({ gameId: multiplayer.gameId })
1094
- .then((joined) => {
1095
- if (disposed) {
1096
- void multiplayer.backend.transport.leaveServer({ serverId: joined.serverId });
1097
- return;
1098
- }
1099
- serverIdRef.current = joined.serverId;
1100
- if (isServerAuthoritative(playable.game.multiplayer) && multiplayer.backend.feeds !== undefined) {
1101
- cleanups.push(attachWorldSync(multiplayer.backend.feeds, joined.serverId, ctx));
1102
- }
1103
- cleanups.push(multiplayer.backend.presenceSync.subscribe(joined.serverId, (rows) => {
1104
- setRemotePlayers(rows.filter((row) => row.userId !== multiplayer.userId));
1105
- }));
1106
- const seen = new Set();
1107
- let injecting = false;
1108
- for (const action of multiplayer.feedActions) {
1109
- cleanups.push(ctx.game.feed.subscribe(action, (entry) => {
1110
- if (injecting)
1111
- return;
1112
- void multiplayer.backend
1113
- .pushFeedEntry({
1114
- serverId: joined.serverId,
1115
- action,
1116
- entry: { at: entry.at, data: entry.data, from: multiplayer.userId },
1117
- })
1118
- .catch(() => undefined);
1119
- }));
1120
- const remoteUnsub = multiplayer.backend.feeds?.subscribeFeed({ serverId: joined.serverId, action }, (view) => {
1121
- for (const raw of view.entries) {
1122
- const remote = raw;
1123
- if (typeof remote.from !== "string" || remote.from === multiplayer.userId)
1124
- continue;
1125
- const key = `${action}|${remote.from}|${remote.at ?? 0}`;
1126
- if (seen.has(key))
1127
- continue;
1128
- seen.add(key);
1129
- injecting = true;
1130
- ctx.game.feed.push(action, remote.data);
1131
- injecting = false;
1132
- }
1133
- });
1134
- if (remoteUnsub !== undefined)
1135
- cleanups.push(remoteUnsub);
1136
- }
1137
- const chatSync = multiplayer.backend.chatSyncFor?.(joined.serverId);
1138
- if (chatSync !== undefined && ctx.game.chat !== undefined) {
1139
- const chat = ctx.game.chat;
1140
- const globalChannelIds = new Set(chat
1141
- .channels()
1142
- .filter((channel) => channel.kind === "global")
1143
- .map((channel) => channel.id));
1144
- const seenRemoteChat = new Set();
1145
- cleanups.push(ctx.game.events.subscribe("chat.message", (event) => {
1146
- if (event.fromUserId !== multiplayer.userId)
1147
- return;
1148
- if (!globalChannelIds.has(event.channelId))
1149
- return;
1150
- void chatSync.send(event.channelId, event.body).catch(() => undefined);
1151
- }));
1152
- for (const channelId of globalChannelIds) {
1153
- cleanups.push(chatSync.subscribe(channelId, (messages) => {
1154
- for (const message of messages) {
1155
- if (message.fromUserId === multiplayer.userId)
1156
- continue;
1157
- if (seenRemoteChat.has(message.id))
1158
- continue;
1159
- seenRemoteChat.add(message.id);
1160
- chat.send(message.fromUserId, message.channelId, message.body);
1161
- }
1162
- }));
1163
- }
1164
- }
1165
- })
1166
- .catch(() => undefined);
1167
- return () => {
1168
- disposed = true;
1169
- for (const cleanup of cleanups)
1170
- cleanup();
1171
- const serverId = serverIdRef.current;
1172
- serverIdRef.current = null;
1173
- setRemotePlayers([]);
1174
- if (serverId !== null) {
1175
- void multiplayer.backend.transport.leaveServer({ serverId }).catch(() => undefined);
1176
- }
1177
- };
1178
- }, [ctx, multiplayer, playable]);
164
+ useShellMultiplayerSync(ctx, multiplayer, playable, serverIdRef, setRemotePlayers);
1179
165
  useEffect(() => {
1180
166
  wrapperRef.current?.focus();
1181
167
  }, [ctx]);
1182
168
  useSyncExternalStore(ctx?.subscribe ?? (() => () => undefined), ctx?.version ?? (() => 0), ctx?.version ?? (() => 0));
1183
169
  if (ctx === null)
1184
170
  return _jsx("div", { className: "h-full w-full bg-neutral-950" });
1185
- const GameUI = playable.GameUI;
1186
- const WorldOverlay = playable.WorldOverlay;
1187
- const controlledEntityId = ctx.player.possession.active(userId);
1188
171
  const cameraConfig = playable.camera?.followEntityId !== undefined
1189
172
  ? playable.camera
1190
- : { ...playable.camera, followEntityId: controlledEntityId };
173
+ : { ...playable.camera, followEntityId: ctx.player.possession.active(userId) };
1191
174
  const rigKind = resolveRigKind(cameraConfig);
175
+ const shared = {
176
+ playable,
177
+ ctx,
178
+ multiplayer,
179
+ tracker,
180
+ pointerAxisRef,
181
+ gateRef,
182
+ wrapperRef,
183
+ f2HeldRef,
184
+ yawRef,
185
+ pitchRef,
186
+ touchScheme,
187
+ touchSink,
188
+ orientationGate,
189
+ orientationGateEl,
190
+ coarsePointer,
191
+ diagnostics,
192
+ devtoolsEnabled,
193
+ devtoolsOpen,
194
+ setDevtoolsOpen,
195
+ reportRuntimeError,
196
+ trackPointerAxis,
197
+ deactivatePointerAxis,
198
+ };
1192
199
  if (rigKind === "none" || playable.presentation === "hud") {
1193
- const GameUI = playable.GameUI;
1194
- return (_jsx("div", { ref: wrapperRef, tabIndex: 0, className: "relative h-full w-full bg-neutral-950 outline-none", onKeyDown: (event) => {
1195
- if (event.code === "F2" && devtoolsEnabled) {
1196
- event.preventDefault();
1197
- f2HeldRef.current = true;
1198
- f2ChordedRef.current = false;
1199
- return;
1200
- }
1201
- if (f2HeldRef.current) {
1202
- f2ChordedRef.current = true;
1203
- return;
1204
- }
1205
- if (event.code === "Tab" || event.code === "Space")
1206
- event.preventDefault();
1207
- tracker.handleDown(event.code);
1208
- }, onKeyUp: (event) => {
1209
- if (event.code === "F2" && devtoolsEnabled) {
1210
- f2HeldRef.current = false;
1211
- if (!f2ChordedRef.current)
1212
- setDevtoolsOpen((current) => !current);
1213
- return;
1214
- }
1215
- tracker.handleUp(event.code);
1216
- }, onBlur: () => {
1217
- f2HeldRef.current = false;
1218
- tracker.reset();
1219
- }, 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: _jsx(GameProvider, { context: ctx, children: _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 })] }) }));
200
+ return (_jsx(ShellHudPresentation, { ...shared, uiScale: graphics.uiScale, onPointerResumeAudio: () => audioEngine.resume() }));
1220
201
  }
1221
- const firstPerson = rigKind === "first";
1222
- const showReticle = (firstPerson && playable.camera?.firstPerson?.reticle !== false) || rigKind === "shoulder";
1223
- const rtsPanKeysEnabled = !rtsPanKeysConflict(playable.game.input);
1224
- const worldBars = playable.worldHealthBars;
1225
- const barsStatId = worldBars === undefined || worldBars === false
1226
- ? null
1227
- : worldBars === true
1228
- ? "health"
1229
- : worldBars.statId ?? "health";
1230
- const barsRoles = worldBars === undefined || worldBars === true || worldBars === false ? undefined : worldBars.roles;
1231
- const barsMaxDistance = worldBars === undefined || worldBars === true || worldBars === false
1232
- ? undefined
1233
- : worldBars.maxDistance;
1234
- const resolveEntityRole = (entity) => playable.content.entityById?.(entity.name)?.role;
1235
- const pointer = playable.pointer;
1236
- const pointerUsesLeft = pointer !== undefined && (pointer.select === true || pointer.moveCommand !== undefined);
1237
- const selectFilter = pointer?.selectFilter;
1238
- const worldSky = resolveWorldSky(playable.game.world);
1239
- const world = playable.game.world;
1240
- const AutoEnvironment = playable.environment ??
1241
- (world?.kind === "environment"
1242
- ? () => _jsx(EnvironmentScene, { feature: world })
1243
- : world?.kind === "biomes" || world?.kind === "voxel" || world?.kind === "plots" || world?.kind === "tilemap"
1244
- ? () => _jsx(GridWorldScene, { feature: world })
1245
- : undefined);
1246
- const backdrop = playable.backdrop;
1247
- const backdropSky = backdrop?.sky !== undefined ? resolveSkyDescriptor(backdrop.sky) : undefined;
1248
- const effectiveSky = backdropSky ?? worldSky;
1249
- const backgroundColor = backdrop?.background ?? (effectiveSky === undefined ? DEFAULT_BACKGROUND_COLOR : undefined);
1250
- const lighting = playable.lighting;
1251
- const orthographic = playable.camera?.projection === "orthographic";
1252
- const localXY = (event) => {
1253
- const rect = wrapperRef.current?.getBoundingClientRect();
1254
- return rect === undefined
1255
- ? { x: event.clientX, y: event.clientY }
1256
- : { x: event.clientX - rect.left, y: event.clientY - rect.top };
1257
- };
1258
- const commitSelection = (ids) => {
1259
- selection.replace(ids);
1260
- setSelectedIds(new Set(ids));
1261
- };
1262
- const finishMarquee = (rect) => {
1263
- const candidates = [];
1264
- for (const entity of ctx.scene.entity.list()) {
1265
- if (selectFilter !== undefined && !selectFilter(entity.id))
1266
- continue;
1267
- const screen = pointerService.screenOf(entity.position);
1268
- if (screen !== null)
1269
- candidates.push({ id: entity.id, x: screen.x, y: screen.y });
1270
- }
1271
- commitSelection(selectWithinRect(candidates, rect));
1272
- };
1273
- /** DOM UI (menus, HUD buttons) sits in the same wrapper as the canvas — only canvas-targeted clicks are world input. */
1274
- const isWorldPointerTarget = (event) => event.target instanceof HTMLCanvasElement;
1275
- const handlePointerDown = (event) => {
1276
- wrapperRef.current?.focus();
1277
- trackPointerAxis(event);
1278
- audioEngine.resume();
1279
- if (contextMenu !== null)
1280
- setContextMenu(null);
1281
- if (event.button === 0 && isWorldPointerTarget(event)) {
1282
- const point = localXY(event);
1283
- pointerDownRef.current = point;
1284
- if (pointer?.select === true)
1285
- marqueeStartRef.current = point;
1286
- }
1287
- };
1288
- const handlePointerMove = (event) => {
1289
- trackPointerAxis(event);
1290
- if (pointer?.select !== true)
1291
- return;
1292
- const start = marqueeStartRef.current;
1293
- if (start === null || (event.buttons & 1) === 0)
1294
- return;
1295
- const point = localXY(event);
1296
- setMarquee(screenRect(start.x, start.y, point.x, point.y));
1297
- };
1298
- const handlePointerUp = (event) => {
1299
- if (event.button !== 0 || pointerDownRef.current === null)
1300
- return;
1301
- const start = pointerDownRef.current;
1302
- pointerDownRef.current = null;
1303
- marqueeStartRef.current = null;
1304
- const end = localXY(event);
1305
- const rect = screenRect(start.x, start.y, end.x, end.y);
1306
- const wasMarquee = marquee !== null && isMarquee(rect, PRIMARY_CLICK_MOVE_THRESHOLD_PX);
1307
- setMarquee(null);
1308
- if (pointer?.select === true && wasMarquee) {
1309
- finishMarquee(rect);
1310
- return;
1311
- }
1312
- if (cameraDraggingRef.current)
1313
- return;
1314
- if (pointer?.grabWorldItems === true) {
1315
- const hit = pointerService.worldHit();
1316
- const itemInstanceId = hit?.entity ?? null;
1317
- const record = itemInstanceId === null ? null : ctx.scene.worldItem.get(itemInstanceId);
1318
- if (record !== null && itemInstanceId !== null) {
1319
- const itemEntity = ctx.scene.entity.get(itemInstanceId);
1320
- const localPlayer = ctx.scene.entity.get(ctx.player.userId);
1321
- const pickupRadius = playable.worldItem?.pickupRadius ?? DEFAULT_PICKUP_RADIUS;
1322
- const withinRadius = itemEntity !== null &&
1323
- localPlayer !== null &&
1324
- Math.hypot(localPlayer.position[0] - itemEntity.position[0], localPlayer.position[1] - itemEntity.position[1], localPlayer.position[2] - itemEntity.position[2]) <= pickupRadius;
1325
- if (withinRadius) {
1326
- ctx.scene.worldItem.pickup(itemInstanceId, ctx.player.userId);
1327
- return;
1328
- }
1329
- }
1330
- }
1331
- if (pointer?.select === true) {
1332
- const hit = pointerService.worldHit();
1333
- if (hit !== null && hit.entity !== null && (selectFilter === undefined || selectFilter(hit.entity))) {
1334
- commitSelection([hit.entity]);
1335
- }
1336
- else {
1337
- commitSelection([]);
1338
- }
1339
- return;
1340
- }
1341
- if (pointer?.moveCommand !== undefined) {
1342
- const hit = pointerService.worldHit();
1343
- if (hit !== null && ctx.game.commands.has(pointer.moveCommand)) {
1344
- ctx.game.commands.run(pointer.moveCommand, { point: hit.point, entity: hit.entity, object: hit.object });
1345
- }
1346
- return;
1347
- }
1348
- const moved = (end.x - start.x) ** 2 + (end.y - start.y) ** 2;
1349
- if (moved <= PRIMARY_CLICK_MOVE_THRESHOLD_PX * PRIMARY_CLICK_MOVE_THRESHOLD_PX)
1350
- primaryClickRef.current = true;
1351
- };
1352
- const handleContextMenu = (event) => {
1353
- if (pointer === undefined)
1354
- return;
1355
- if (!isWorldPointerTarget(event))
1356
- return;
1357
- event.preventDefault();
1358
- const hit = pointerService.worldHit();
1359
- if (hit === null)
1360
- return;
1361
- if (pointer.contextMenu === true && (hit.entity !== null || hit.object !== null)) {
1362
- const menu = pointerContextMenu(ctx, playable, hit);
1363
- if (menu !== null) {
1364
- const point = localXY(event);
1365
- setContextMenu({ menu, x: point.x, y: point.y });
202
+ return (_jsx(Shell3dPresentation, { ...shared, primaryClickRef: primaryClickRef, cameraDraggingRef: cameraDraggingRef, serverIdRef: serverIdRef, remotePlayers: remotePlayers, touchStyle: touchStyle, compact: compact, graphics: graphics, settingsStore: settingsStore, bindingOverrides: bindingOverrides, rebindAction: rebindAction, resetActionBinding: resetActionBinding, audioEngine: audioEngine, poster: poster, posterFrozen: posterFrozen, onPosterSettled: () => {
203
+ if (posterSettledRef.current)
1366
204
  return;
1367
- }
1368
- }
1369
- if (pointer.select === true && pointer.orderCommand !== undefined && selection.size() > 0) {
1370
- if (ctx.game.commands.has(pointer.orderCommand)) {
1371
- ctx.game.commands.run(pointer.orderCommand, { selection: selection.list(), point: hit.point });
1372
- }
1373
- return;
1374
- }
1375
- if (pointer.secondaryCommand !== undefined && ctx.game.commands.has(pointer.secondaryCommand)) {
1376
- const aim = (pointer.aim === true ? pointerAimFor(ctx, pointerService) : undefined) ?? {
1377
- yaw: yawRef.current,
1378
- pitch: pitchRef.current,
1379
- };
1380
- ctx.game.commands.run(pointer.secondaryCommand, { point: hit.point, entity: hit.entity, object: hit.object, aim });
1381
- }
1382
- };
1383
- const handleVerbPick = (verb) => {
1384
- const state = contextMenu;
1385
- setContextMenu(null);
1386
- if (state !== null && ctx.game.commands.has(verb.command)) {
1387
- ctx.game.commands.run(verb.command, contextVerbInput(state.menu, verb));
1388
- }
1389
- };
1390
- const controlsActive = playControlsActive(ctx);
1391
- const settingsDisabled = playable.settings === false;
1392
- const settingsConfig = playable.settings === false || playable.settings === undefined ? {} : playable.settings;
1393
- const settingsSurface = settingsDisabled ? false : settingsConfig.surface ?? false;
1394
- const settingsVariant = settingsConfig.variant ?? "panel";
1395
- const hideCategories = settingsDisabled ? BUILT_IN_SETTING_CATEGORIES : settingsConfig.hide ?? [];
1396
- const fovControlEnabled = !orthographic && playable.camera?.playerFov?.control !== false;
1397
- const settingsHostsFov = !settingsDisabled && !hideCategories.includes("gameplay") && fovControlEnabled;
1398
- const settingsActions = settingsDisabled
1399
- ? []
1400
- : (settingsConfig.actions ?? []).map((action) => ({
1401
- id: action.id,
1402
- label: action.label,
1403
- kind: action.kind ?? "default",
1404
- description: action.description,
1405
- run: () => action.run(ctx),
1406
- }));
1407
- const touchScale = compact ? 0.88 : 1;
1408
- const dockMounted = !poster &&
1409
- !orientationGate &&
1410
- coarsePointer &&
1411
- controlsActive &&
1412
- touchScheme !== null &&
1413
- (touchScheme.joystick !== null || touchScheme.buttons.length > 0);
1414
- 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: _jsx("div", { ref: wrapperRef, tabIndex: 0, ...(poster && posterFrozen ? { "data-poster-ready": "" } : {}), className: "relative h-full w-full bg-neutral-950 outline-none", style: {
1415
- "--jg-hud-dock-clearance": `${dockMounted ? touchDockClearance(touchScheme, touchScale) : 0}px`,
1416
- }, onKeyDown: (event) => {
1417
- if (event.code === "F2" && devtoolsEnabled) {
1418
- event.preventDefault();
1419
- f2HeldRef.current = true;
1420
- f2ChordedRef.current = false;
1421
- return;
1422
- }
1423
- if (f2HeldRef.current) {
1424
- f2ChordedRef.current = true;
1425
- return;
1426
- }
1427
- if (event.code === "Tab" || event.code === "Space")
1428
- event.preventDefault();
1429
- tracker.handleDown(event.code);
1430
- }, onKeyUp: (event) => {
1431
- if (event.code === "F2" && devtoolsEnabled) {
1432
- f2HeldRef.current = false;
1433
- if (!f2ChordedRef.current) {
1434
- document.exitPointerLock?.();
1435
- setDevtoolsOpen((current) => !current);
1436
- }
1437
- return;
1438
- }
1439
- tracker.handleUp(event.code);
1440
- }, onBlur: () => {
1441
- f2HeldRef.current = false;
1442
- tracker.reset();
1443
- }, onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, onPointerLeave: deactivatePointerAxis, onPointerCancel: deactivatePointerAxis, onContextMenu: handleContextMenu, onWheel: (event) => {
1444
- if (ctx === null || !event.shiftKey)
1445
- return;
1446
- event.preventDefault();
1447
- if (event.deltaY < 0 && ctx.game.commands.has("ui.hotbarScrollNext")) {
1448
- ctx.game.commands.run("ui.hotbarScrollNext", {});
1449
- }
1450
- else if (event.deltaY > 0 && ctx.game.commands.has("ui.hotbarScrollPrev")) {
1451
- ctx.game.commands.run("ui.hotbarScrollPrev", {});
1452
- }
1453
- }, children: _jsxs(GameViewportProvider, { platforms: playable.platforms, children: [_jsxs(Canvas, { frameloop: poster && posterFrozen ? "demand" : "always", orthographic: orthographic, camera: orthographic
1454
- ? {
1455
- zoom: playable.camera?.frustum?.zoom ?? CAMERA_FRUSTUM_DEFAULTS.zoom,
1456
- near: playable.camera?.frustum?.near ?? CAMERA_FRUSTUM_DEFAULTS.near,
1457
- far: playable.camera?.frustum?.far ?? CAMERA_FRUSTUM_DEFAULTS.far,
1458
- }
1459
- : {
1460
- fov: playable.camera?.frustum?.fov ?? CAMERA_FRUSTUM_DEFAULTS.fov,
1461
- near: playable.camera?.frustum?.near ?? CAMERA_FRUSTUM_DEFAULTS.near,
1462
- far: playable.camera?.frustum?.far ?? CAMERA_FRUSTUM_DEFAULTS.far,
1463
- }, 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, hideLocalActor: firstPerson }) }), WorldOverlay !== undefined ? _jsx(WorldOverlay, {}) : null, barsStatId !== null ? (_jsx(WorldEntityBars, { statId: barsStatId, roles: barsRoles, resolveRole: resolveEntityRole, ...(barsMaxDistance === undefined ? {} : { maxDistance: barsMaxDistance }) })) : 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) => {
1464
- cameraDraggingRef.current = dragging;
1465
- } }), _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: () => {
1466
- if (posterSettledRef.current)
1467
- return;
1468
- posterSettledRef.current = true;
1469
- setPosterFrozen(true);
1470
- } }), _jsx(DevtoolsRendererProbe, {}), playable.postProcessing !== undefined && playable.postProcessing.enabled !== false ? (_jsx(PostProcessing, { config: playable.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: () => {
1471
- primaryClickRef.current = true;
1472
- } })) : null, _jsx(GameUiErrorBoundary, { onRuntimeError: reportRuntimeError, children: _jsx(GameProvider, { context: ctx, children: _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, 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, {})] }) }) })] }) }));
205
+ posterSettledRef.current = true;
206
+ setPosterFrozen(true);
207
+ } }));
1473
208
  }