@jgengine/shell 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/CHANGELOG.md +13 -2
  2. package/LICENSE +203 -661
  3. package/README.md +1 -1
  4. package/dist/GamePhaseStamp.d.ts +1 -0
  5. package/dist/GamePhaseStamp.js +9 -0
  6. package/dist/GamePlayerShell.d.ts +0 -12
  7. package/dist/GamePlayerShell.js +88 -1043
  8. package/dist/GameUiPreview.js +0 -2
  9. package/dist/audio/audioEngine.js +30 -8
  10. package/dist/audio/audioWire.d.ts +22 -0
  11. package/dist/audio/audioWire.js +19 -0
  12. package/dist/behaviour.d.ts +2 -1
  13. package/dist/behaviour.js +2 -1
  14. package/dist/behaviourAttach.d.ts +1 -0
  15. package/dist/behaviourAttach.js +1 -0
  16. package/dist/behaviourDriver.d.ts +1 -0
  17. package/dist/behaviourDriver.js +1 -0
  18. package/dist/boundActionDispatch.d.ts +13 -0
  19. package/dist/boundActionDispatch.js +37 -0
  20. package/dist/camera/GameCameraRig.d.ts +5 -2
  21. package/dist/camera/GameCameraRig.js +2 -2
  22. package/dist/camera/GameFirstPersonCamera.d.ts +9 -2
  23. package/dist/camera/GameFirstPersonCamera.js +8 -3
  24. package/dist/camera/GameInspectionCamera.d.ts +1 -1
  25. package/dist/camera/GameInspectionCamera.js +2 -2
  26. package/dist/camera/GameOrbitCamera.js +24 -9
  27. package/dist/camera/cameraBlendMath.d.ts +3 -0
  28. package/dist/camera/cameraBlendMath.js +3 -0
  29. package/dist/camera/fovPreference.d.ts +6 -1
  30. package/dist/camera/fovPreference.js +6 -1
  31. package/dist/camera/index.d.ts +4 -7
  32. package/dist/camera/index.js +4 -7
  33. package/dist/camera/inspectionCameraMath.d.ts +7 -2
  34. package/dist/camera/inspectionCameraMath.js +7 -2
  35. package/dist/camera/orbitCameraMath.d.ts +36 -6
  36. package/dist/camera/orbitCameraMath.js +56 -6
  37. package/dist/camera/rigMath.d.ts +94 -24
  38. package/dist/camera/rigMath.js +100 -24
  39. package/dist/camera/rigResolve.d.ts +4 -2
  40. package/dist/camera/rigResolve.js +4 -2
  41. package/dist/camera/shakeChannel.d.ts +6 -2
  42. package/dist/camera/shakeChannel.js +6 -2
  43. package/dist/camera/shakeChannelMath.d.ts +1 -0
  44. package/dist/camera/shakeChannelMath.js +1 -0
  45. package/dist/cartridge/runtime.d.ts +15 -0
  46. package/dist/cartridge/runtime.js +556 -0
  47. package/dist/cartridge/spec.d.ts +188 -0
  48. package/dist/cartridge/spec.js +19 -0
  49. package/dist/cartridge/validate.d.ts +2 -0
  50. package/dist/cartridge/validate.js +78 -0
  51. package/dist/cartridge.d.ts +1 -1
  52. package/dist/cartridge.js +3 -3
  53. package/dist/commandSink.d.ts +8 -3
  54. package/dist/commandSink.js +8 -3
  55. package/dist/defineGame.js +25 -6
  56. package/dist/devtools/ColPanel.d.ts +1 -0
  57. package/dist/devtools/ColPanel.js +14 -0
  58. package/dist/devtools/DevtoolsOverlay.d.ts +7 -3
  59. package/dist/devtools/DevtoolsOverlay.js +19 -450
  60. package/dist/devtools/KeysPanel.d.ts +4 -0
  61. package/dist/devtools/KeysPanel.js +31 -0
  62. package/dist/devtools/LogsPanel.d.ts +1 -0
  63. package/dist/devtools/LogsPanel.js +12 -0
  64. package/dist/devtools/NetPanel.d.ts +4 -0
  65. package/dist/devtools/NetPanel.js +10 -0
  66. package/dist/devtools/PerfPanel.d.ts +4 -0
  67. package/dist/devtools/PerfPanel.js +53 -0
  68. package/dist/devtools/TunePanel.d.ts +3 -0
  69. package/dist/devtools/TunePanel.js +309 -0
  70. package/dist/devtools/agentBridge.d.ts +41 -0
  71. package/dist/devtools/agentBridge.js +262 -0
  72. package/dist/devtools/collisionDebug.d.ts +6 -0
  73. package/dist/devtools/collisionDebug.js +6 -0
  74. package/dist/devtools/collisionDebugMath.d.ts +8 -2
  75. package/dist/devtools/collisionDebugMath.js +24 -5
  76. package/dist/devtools/devtoolsOverrides.d.ts +6 -0
  77. package/dist/devtools/devtoolsOverrides.js +48 -0
  78. package/dist/devtools/panelAtoms.d.ts +9 -0
  79. package/dist/devtools/panelAtoms.js +10 -0
  80. package/dist/devtools/perfDiagnose.d.ts +2 -0
  81. package/dist/devtools/perfDiagnose.js +43 -0
  82. package/dist/diagnostics/RuntimeDiagnostics.d.ts +29 -0
  83. package/dist/diagnostics/RuntimeDiagnostics.js +69 -0
  84. package/dist/drivers/FrameDriver.d.ts +37 -0
  85. package/dist/drivers/FrameDriver.js +232 -0
  86. package/dist/drivers/HudOnlyDriver.d.ts +16 -0
  87. package/dist/drivers/HudOnlyDriver.js +60 -0
  88. package/dist/environment/Daylight.d.ts +23 -7
  89. package/dist/environment/Daylight.js +106 -13
  90. package/dist/environment/EnvironmentScene.js +32 -6
  91. package/dist/environment/VolumetricClouds.d.ts +17 -0
  92. package/dist/environment/VolumetricClouds.js +202 -0
  93. package/dist/environment/daylightCycle.d.ts +3 -1
  94. package/dist/environment/daylightCycle.js +3 -1
  95. package/dist/environment/groundPadMath.d.ts +3 -0
  96. package/dist/environment/groundPadMath.js +3 -0
  97. package/dist/environment/index.d.ts +1 -0
  98. package/dist/environment/index.js +1 -0
  99. package/dist/environment/skyLightingPolicy.d.ts +2 -0
  100. package/dist/environment/skyLightingPolicy.js +2 -0
  101. package/dist/hotbarActions.d.ts +26 -0
  102. package/dist/hotbarActions.js +27 -0
  103. package/dist/inputSink.d.ts +16 -4
  104. package/dist/inputSink.js +57 -7
  105. package/dist/materialOverride.d.ts +10 -0
  106. package/dist/materialOverride.js +15 -3
  107. package/dist/multiplayer.js +8 -2
  108. package/dist/postfx/PostProcessing.js +47 -9
  109. package/dist/postfx/gradeShader.js +9 -1
  110. package/dist/registry.d.ts +3 -2
  111. package/dist/render/EnvironmentLighting.d.ts +10 -0
  112. package/dist/render/EnvironmentLighting.js +34 -0
  113. package/dist/render/SceneLighting.d.ts +7 -0
  114. package/dist/render/SceneLighting.js +18 -0
  115. package/dist/render/SceneModels.d.ts +14 -0
  116. package/dist/render/SceneModels.js +337 -0
  117. package/dist/render/defaultSurface.d.ts +29 -0
  118. package/dist/render/defaultSurface.js +101 -0
  119. package/dist/render/modelLoad.d.ts +24 -0
  120. package/dist/render/modelLoad.js +73 -0
  121. package/dist/render/modelRender.d.ts +11 -0
  122. package/dist/render/modelRender.js +15 -0
  123. package/dist/render/resolveModel.d.ts +39 -3
  124. package/dist/render/resolveModel.js +79 -2
  125. package/dist/render/useEntityRenderCues.d.ts +13 -0
  126. package/dist/render/useEntityRenderCues.js +62 -0
  127. package/dist/scatter/InstancedScatter.d.ts +24 -0
  128. package/dist/scatter/InstancedScatter.js +130 -0
  129. package/dist/scatter/scatterModels.d.ts +25 -0
  130. package/dist/scatter/scatterModels.js +61 -0
  131. package/dist/scatter/scatterProxies.d.ts +6 -0
  132. package/dist/scatter/scatterProxies.js +94 -0
  133. package/dist/scatter.d.ts +1 -0
  134. package/dist/scatter.js +1 -0
  135. package/dist/scene/AuthoredScene.d.ts +82 -0
  136. package/dist/scene/AuthoredScene.js +173 -0
  137. package/dist/scene/GeneratedAssetRenderer.d.ts +14 -0
  138. package/dist/scene/GeneratedAssetRenderer.js +59 -0
  139. package/dist/scene/GrassFieldRenderer.d.ts +2 -0
  140. package/dist/scene/GrassFieldRenderer.js +25 -0
  141. package/dist/scene/SoilPatchRenderer.d.ts +2 -0
  142. package/dist/scene/SoilPatchRenderer.js +21 -0
  143. package/dist/scene/StudioStage.d.ts +37 -0
  144. package/dist/scene/StudioStage.js +42 -0
  145. package/dist/scene/WaterRenderer.d.ts +2 -0
  146. package/dist/scene/WaterRenderer.js +37 -0
  147. package/dist/scene/builtinSceneKindRenderers.d.ts +2 -0
  148. package/dist/scene/builtinSceneKindRenderers.js +22 -0
  149. package/dist/scene/sceneKindRenderers.d.ts +28 -0
  150. package/dist/scene/sceneKindRenderers.js +17 -0
  151. package/dist/scene.d.ts +1 -0
  152. package/dist/scene.js +1 -0
  153. package/dist/settings/appliedSettings.d.ts +5 -0
  154. package/dist/settings/appliedSettings.js +9 -0
  155. package/dist/settings/settingsController.d.ts +2 -0
  156. package/dist/settings/settingsController.js +22 -1
  157. package/dist/shellConstants.d.ts +4 -0
  158. package/dist/shellConstants.js +4 -0
  159. package/dist/shellMovement.d.ts +8 -0
  160. package/dist/shellMovement.js +11 -0
  161. package/dist/shellPointer.d.ts +20 -0
  162. package/dist/shellPointer.js +28 -0
  163. package/dist/structures/PlacementGhost.d.ts +4 -0
  164. package/dist/structures/PlacementGhost.js +4 -0
  165. package/dist/structures/TransformGizmo.d.ts +43 -0
  166. package/dist/structures/TransformGizmo.js +55 -0
  167. package/dist/structures/index.d.ts +1 -0
  168. package/dist/structures/index.js +1 -0
  169. package/dist/terrain/GrassField.js +1 -1
  170. package/dist/terrain/TerraformBrushCursor.js +3 -0
  171. package/dist/terrain/grassBudget.d.ts +11 -2
  172. package/dist/terrain/grassBudget.js +20 -4
  173. package/dist/terrain/grassGeometry.d.ts +3 -0
  174. package/dist/terrain/grassGeometry.js +3 -0
  175. package/dist/terrain/grassMaterial.d.ts +2 -0
  176. package/dist/terrain/grassMaterial.js +2 -0
  177. package/dist/terrain/random.d.ts +3 -0
  178. package/dist/terrain/random.js +3 -0
  179. package/dist/terrain/soilPatchMaterial.d.ts +10 -0
  180. package/dist/terrain/soilPatchMaterial.js +109 -0
  181. package/dist/terrain/terrainDetailMaterial.d.ts +19 -2
  182. package/dist/terrain/terrainDetailMaterial.js +72 -5
  183. package/dist/terrain/terrainMath.d.ts +8 -1
  184. package/dist/terrain/terrainMath.js +8 -1
  185. package/dist/touch/TouchControlsOverlay.d.ts +9 -5
  186. package/dist/touch/TouchControlsOverlay.js +201 -22
  187. package/dist/useShellMultiplayerSync.d.ts +9 -0
  188. package/dist/useShellMultiplayerSync.js +98 -0
  189. package/dist/visibility/CullingProvider.js +1 -1
  190. package/dist/water/OceanConfig.d.ts +2 -0
  191. package/dist/water/OceanConfig.js +2 -0
  192. package/dist/weather/fireSpreadPose.d.ts +1 -0
  193. package/dist/weather/fireSpreadPose.js +1 -0
  194. package/dist/weather/weatherGeometry.d.ts +1 -0
  195. package/dist/weather/weatherGeometry.js +1 -0
  196. package/dist/weather/weatherMath.d.ts +3 -0
  197. package/dist/weather/weatherMath.js +3 -0
  198. package/dist/weather/weatherUniforms.d.ts +3 -0
  199. package/dist/weather/weatherUniforms.js +3 -0
  200. package/dist/world/RetainedVfx.d.ts +10 -0
  201. package/dist/world/RetainedVfx.js +136 -0
  202. package/dist/world/WorldHud.d.ts +49 -2
  203. package/dist/world/WorldHud.js +87 -2
  204. package/dist/world/WorldScene.d.ts +21 -0
  205. package/dist/world/WorldScene.js +159 -0
  206. package/dist/world/WorldVfx.d.ts +9 -0
  207. package/dist/world/WorldVfx.js +184 -0
  208. package/dist/world/entityPose.d.ts +2 -0
  209. package/dist/world/entityPose.js +2 -0
  210. package/dist/world/floatTextStyle.d.ts +1 -0
  211. package/dist/world/floatTextStyle.js +1 -0
  212. package/dist/world/telegraphPulse.d.ts +1 -0
  213. package/dist/world/telegraphPulse.js +1 -0
  214. package/dist/world/worldBarSamples.d.ts +25 -0
  215. package/dist/world/worldBarSamples.js +38 -0
  216. package/dist/worldSky.d.ts +5 -0
  217. package/dist/worldSky.js +11 -0
  218. package/dist/worldSync.d.ts +2 -1
  219. package/dist/worldSync.js +2 -1
  220. package/package.json +6 -6
  221. package/skills/jgengine/SKILL.md +51 -0
  222. package/skills/jgengine/api.md +434 -0
  223. package/skills/jgengine/capabilities.md +29 -0
  224. package/skills/jgengine/reference-cartridge.md +72 -0
  225. package/skills/jgengine-ui/SKILL.md +36 -0
  226. package/skills/jgengine-ui/api.md +1852 -0
  227. package/skills/jgengine-ui/capabilities.md +142 -0
  228. package/skills/jgengine-ui/reference.md +500 -0
  229. package/skills/jgengine-ui/references/visual-scorecard.md +73 -0
  230. package/skills/jgengine-world/SKILL.md +38 -0
  231. package/skills/jgengine-world/api.md +2652 -0
  232. package/skills/jgengine-world/capabilities.md +370 -0
  233. package/skills/jgengine-world/reference.md +222 -0
  234. package/llms.txt +0 -2761
@@ -1,61 +1,47 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Canvas, useFrame, useLoader } from "@react-three/fiber";
3
- import { Component, 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";
2
+ import { Canvas } from "@react-three/fiber";
3
+ import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
4
+ import { createActionStateTracker, toActionStateBindingMap, } from "@jgengine/core/input/actionBindings";
5
+ import { RESERVED_INPUT_ACTIONS } from "./boundActionDispatch.js";
6
+ import { resolveWorldSky } from "./worldSky.js";
7
+ import { pointerAimFor, pointerContextMenu } from "./shellPointer.js";
8
+ import { deriveTouchScheme, withTouchCodes, DEFAULT_TOUCH_STYLE } from "@jgengine/core/input/touchScheme";
9
+ import { contextVerbInput, } from "@jgengine/core/interaction/contextMenu";
13
10
  import { normalizePointerToAxis } from "@jgengine/core/input/pointerAxis";
14
11
  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
12
  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";
13
+ import { DEFAULT_PICKUP_RADIUS } from "@jgengine/core/game/worldItem";
25
14
  import { useDisplayProfile } from "@jgengine/react/display";
26
15
  import { HudViewportProvider } from "@jgengine/react/hudViewport";
27
16
  import { GameViewportProvider } from "@jgengine/react/gameViewport";
28
17
  import { RotateDeviceScreen } from "@jgengine/react/rotateDevice";
29
- import { useSceneEntities, useSceneObjects, usePlayer, useTarget } from "@jgengine/react/hooks";
30
18
  import { GameProvider } from "@jgengine/react/provider";
31
19
  import { CAMERA_FRUSTUM_DEFAULTS } from "@jgengine/core/game/playableGame";
32
20
  import { createSettingsStore } from "@jgengine/core/settings/settingsModel";
33
21
  import { applyBindingOverrides, clearBindingOverride, loadBindingOverrides, saveBindingOverride, } from "@jgengine/core/input/bindingOverrides";
34
22
  import { playControlsActive } from "@jgengine/core/game/controlGate";
35
23
  import { orientationGateActive, orientationHintActive, resolveOrientationRequirement, } from "@jgengine/core/ui/orientation";
36
- import { resolveOneShotClip } from "@jgengine/core/game/modelAnimation";
37
24
  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";
25
+ import { resolveGameLook } from "@jgengine/core/render/lookPreset";
40
26
  import { AudioListener, EntityAudioEmitters, ObjectAudioEmitters } from "./audio/AudioComponents.js";
41
27
  import { createAudioEngine } from "./audio/audioEngine.js";
28
+ import { attachAudioEventWire } from "./audio/audioWire.js";
42
29
  import { PostProcessing } from "./postfx/PostProcessing.js";
30
+ import { EnvironmentLighting } from "./render/EnvironmentLighting.js";
43
31
  import { CollisionDebugWorld } from "./devtools/CollisionDebugWorld.js";
44
- import { collisionDebug } from "./devtools/collisionDebug.js";
45
32
  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";
33
+ import { installAgentBridge } from "./devtools/agentBridge.js";
34
+ import { GameCameraRig, PlayerFovProvider, PlayerFovSlider, resolveRigKind, rtsPanKeysConflict, } from "./camera/index.js";
35
+ import { CullingProvider } from "./visibility/CullingProvider.js";
49
36
  import { SkyDaylight, TimeOfDayDaylight } from "./environment.js";
50
37
  import { resolveSkyLightOwnership, skyEmitsLights } from "./environment/skyLightingPolicy.js";
51
38
  import { EnvironmentScene } from "./environment/EnvironmentScene.js";
52
- import { applyMaterialOverride } from "./materialOverride.js";
53
39
  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
40
  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";
41
+ import { createPointerService } from "./pointer/pointerService.js";
42
+ import { CombatCameraShake, ProjectileTracers, Reticle, WorldEntityBars, WorldFloatText, WorldNameplates, WorldTelegraphs, } from "./world/WorldHud.js";
43
+ import { WorldSpellVfx } from "./world/WorldVfx.js";
44
+ import { RetainedVfx } from "./world/RetainedVfx.js";
59
45
  import { GridWorldScene } from "./world/GridWorldScene.js";
60
46
  import { WorldItems } from "./world/WorldItems.js";
61
47
  import { OrientationHint } from "./touch/OrientationHint.js";
@@ -64,894 +50,34 @@ import { BUILT_IN_SETTING_CATEGORIES } from "@jgengine/core/settings/settingsMod
64
50
  import { SettingsProvider } from "@jgengine/react/settings";
65
51
  import { SettingsRuntime } from "./settings/SettingsRuntime.js";
66
52
  import { SettingsChrome } from "./settings/SettingsChrome.js";
67
- import { AudioSettingsBridge, useGraphicsSettings } from "./settings/appliedSettings.js";
53
+ import { AudioSettingsBridge, useGraphicsSettings, useTouchStyle } from "./settings/appliedSettings.js";
54
+ import { ConfiguredLighting, BackdropFog } from "./render/SceneLighting.js";
55
+ import { WorldView, RemotePlayers } from "./world/WorldScene.js";
56
+ import { FrameDriver } from "./drivers/FrameDriver.js";
57
+ import { HudOnlyDriver } from "./drivers/HudOnlyDriver.js";
58
+ import { DiagnosticOverlay, GameUiErrorBoundary, logRuntimeError, } from "./diagnostics/RuntimeDiagnostics.js";
59
+ import { GamePhaseStamp } from "./GamePhaseStamp.js";
60
+ import { EMPTY_RESERVED } from "./shellConstants.js";
61
+ import { useShellMultiplayerSync } from "./useShellMultiplayerSync.js";
68
62
  const DEV_USER_ID = "dev-player";
69
- const TURN_SPEED = 2.4;
70
63
  const PRIMARY_CLICK_MOVE_THRESHOLD_PX = 6;
71
- const GROUND_SIZE = 160;
72
- const GROUND_SEGMENTS = 80;
73
64
  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
65
  export { applyMotionImpulses } from "@jgengine/core/runtime/motionIntents";
177
66
  export { nearbyObstacles } from "@jgengine/core/movement/movementModel";
178
67
  export { resolvePhysicsTuning } from "@jgengine/core/movement/playerMovement";
179
68
  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;
951
69
  export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null, poster = false, onContextReady, }) {
952
70
  const multiplayer = useMemo(() => (rawMultiplayer === null ? null : withDevtoolsLatency(rawMultiplayer)), [rawMultiplayer]);
953
71
  const devtoolsEnabled = playable.devtools !== false && !poster;
954
72
  const [devtoolsOpen, setDevtoolsOpen] = useState(false);
73
+ const devtoolsOpenRef = useRef(false);
74
+ devtoolsOpenRef.current = devtoolsOpen;
75
+ useEffect(() => installAgentBridge({
76
+ playable,
77
+ devtoolsEnabled,
78
+ isDevtoolsOpen: () => devtoolsOpenRef.current,
79
+ setDevtoolsOpen,
80
+ }), [playable, devtoolsEnabled]);
955
81
  const [posterFrozen, setPosterFrozen] = useState(false);
956
82
  const posterSettledRef = useRef(false);
957
83
  const [ctx, setCtx] = useState(null);
@@ -967,7 +93,6 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
967
93
  const pointerAxisRef = useRef(null);
968
94
  const marqueeStartRef = useRef(null);
969
95
  const f2HeldRef = useRef(false);
970
- const f2ChordedRef = useRef(false);
971
96
  const pointerService = useMemo(() => createPointerService(), []);
972
97
  const selection = useMemo(() => createSelectionSet(), [playable]);
973
98
  const [marquee, setMarquee] = useState(null);
@@ -1001,6 +126,7 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
1001
126
  firstPerson: resolveRigKind(playable.camera) === "first",
1002
127
  config: playable.touch,
1003
128
  }), [playable]);
129
+ const touchStyle = useTouchStyle(settingsStore, touchScheme?.style ?? DEFAULT_TOUCH_STYLE);
1004
130
  const { coarsePointer, portrait, compact } = useDisplayProfile();
1005
131
  const touchSink = useMemo(() => ({ onCodeDown: (code) => tracker.handleDown(code), onCodeUp: (code) => tracker.handleUp(code) }), [tracker]);
1006
132
  const gateRef = useRef(false);
@@ -1021,18 +147,7 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
1021
147
  useEffect(() => {
1022
148
  if (ctx === null)
1023
149
  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
- };
150
+ return attachAudioEventWire(ctx.game.events, audioEngine);
1036
151
  }, [ctx, audioEngine]);
1037
152
  useEffect(() => {
1038
153
  if (typeof document === "undefined")
@@ -1062,7 +177,6 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
1062
177
  setDiagnostics((current) => [...current.slice(-4), { ...diagnostic, id: Date.now() + current.length }]);
1063
178
  };
1064
179
  useEffect(() => {
1065
- playable.game.scene.clear();
1066
180
  setDiagnostics([]);
1067
181
  try {
1068
182
  const context = createGameContext({
@@ -1080,102 +194,10 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
1080
194
  setCtx(null);
1081
195
  }
1082
196
  return () => {
1083
- playable.game.scene.clear();
1084
197
  setCtx(null);
1085
198
  };
1086
199
  }, [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]);
200
+ useShellMultiplayerSync(ctx, multiplayer, playable, serverIdRef, setRemotePlayers);
1179
201
  useEffect(() => {
1180
202
  wrapperRef.current?.focus();
1181
203
  }, [ctx]);
@@ -1192,31 +214,33 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
1192
214
  if (rigKind === "none" || playable.presentation === "hud") {
1193
215
  const GameUI = playable.GameUI;
1194
216
  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) {
217
+ if (event.code === "F2") {
1196
218
  event.preventDefault();
1197
219
  f2HeldRef.current = true;
1198
- f2ChordedRef.current = false;
1199
220
  return;
1200
221
  }
1201
222
  if (f2HeldRef.current) {
1202
- f2ChordedRef.current = true;
223
+ if (event.code === "KeyD" && devtoolsEnabled) {
224
+ event.preventDefault();
225
+ setDevtoolsOpen((current) => !current);
226
+ }
1203
227
  return;
1204
228
  }
1205
229
  if (event.code === "Tab" || event.code === "Space")
1206
230
  event.preventDefault();
1207
- tracker.handleDown(event.code);
231
+ if (playControlsActive(ctx))
232
+ tracker.handleDown(event.code);
1208
233
  }, onKeyUp: (event) => {
1209
- if (event.code === "F2" && devtoolsEnabled) {
234
+ if (event.code === "F2") {
1210
235
  f2HeldRef.current = false;
1211
- if (!f2ChordedRef.current)
1212
- setDevtoolsOpen((current) => !current);
1213
236
  return;
1214
237
  }
1215
- tracker.handleUp(event.code);
238
+ if (playControlsActive(ctx))
239
+ tracker.handleUp(event.code);
1216
240
  }, onBlur: () => {
1217
241
  f2HeldRef.current = false;
1218
242
  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 })] }) }));
243
+ }, onPointerDown: () => audioEngine.resume(), onPointerMove: trackPointerAxis, onPointerLeave: deactivatePointerAxis, onPointerCancel: deactivatePointerAxis, children: _jsxs(GameViewportProvider, { platforms: playable.platforms, children: [_jsx(HudOnlyDriver, { ctx: ctx, playable: playable, tracker: tracker, pointerAxisRef: pointerAxisRef, gateRef: gateRef, onRuntimeError: reportRuntimeError }), !orientationGate && coarsePointer && touchScheme !== null && touchScheme.gestures !== null && playControlsActive(ctx) ? (_jsx(TouchPlaySurface, { scheme: touchScheme, sink: touchSink, yawRef: yawRef, pitchRef: pitchRef, maxPitch: 0, onPrimaryTap: () => undefined })) : null, _jsx(GameUiErrorBoundary, { onRuntimeError: reportRuntimeError, children: _jsxs(GameProvider, { context: ctx, children: [_jsx(GamePhaseStamp, {}), _jsx(HudViewportProvider, { platforms: playable.platforms, config: playable.hudFit, userScale: graphics.uiScale, children: orientationGate ? null : _jsx(GameUI, {}) })] }) }), orientationGateEl, devtoolsEnabled ? (_jsx(DevtoolsOverlay, { open: devtoolsOpen, ctx: ctx, playable: playable, multiplayer: multiplayer })) : null, _jsx(DiagnosticOverlay, { diagnostics: diagnostics, gameName: playable.game.name })] }) }));
1220
244
  }
1221
245
  const firstPerson = rigKind === "first";
1222
246
  const showReticle = (firstPerson && playable.camera?.firstPerson?.reticle !== false) || rigKind === "shoulder";
@@ -1231,23 +255,40 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
1231
255
  const barsMaxDistance = worldBars === undefined || worldBars === true || worldBars === false
1232
256
  ? undefined
1233
257
  : worldBars.maxDistance;
258
+ const nameplates = playable.nameplates;
259
+ const nameplatesStatId = nameplates === undefined || nameplates === false
260
+ ? null
261
+ : nameplates === true
262
+ ? "health"
263
+ : nameplates.statId ?? "health";
264
+ const nameplatesRoles = nameplates === undefined || nameplates === true || nameplates === false ? undefined : nameplates.roles;
265
+ const nameplatesMaxDistance = nameplates === undefined || nameplates === true || nameplates === false ? undefined : nameplates.maxDistance;
1234
266
  const resolveEntityRole = (entity) => playable.content.entityById?.(entity.name)?.role;
1235
267
  const pointer = playable.pointer;
1236
268
  const pointerUsesLeft = pointer !== undefined && (pointer.select === true || pointer.moveCommand !== undefined);
1237
269
  const selectFilter = pointer?.selectFilter;
1238
270
  const worldSky = resolveWorldSky(playable.game.world);
1239
271
  const world = playable.game.world;
272
+ const biomeBands = world?.kind === "environment" ? world.terrain?.biomeBands : undefined;
1240
273
  const AutoEnvironment = playable.environment ??
1241
274
  (world?.kind === "environment"
1242
275
  ? () => _jsx(EnvironmentScene, { feature: world })
1243
276
  : world?.kind === "biomes" || world?.kind === "voxel" || world?.kind === "plots" || world?.kind === "tilemap"
1244
277
  ? () => _jsx(GridWorldScene, { feature: world })
1245
278
  : undefined);
1246
- const backdrop = playable.backdrop;
279
+ const resolvedLook = resolveGameLook({
280
+ look: playable.look,
281
+ lighting: playable.lighting,
282
+ backdrop: playable.backdrop,
283
+ postProcessing: playable.postProcessing,
284
+ hasWorldSky: worldSky !== undefined,
285
+ });
286
+ const cinematicLook = (playable.look ?? "cinematic") !== "flat";
287
+ const backdrop = resolvedLook.backdrop;
1247
288
  const backdropSky = backdrop?.sky !== undefined ? resolveSkyDescriptor(backdrop.sky) : undefined;
1248
289
  const effectiveSky = backdropSky ?? worldSky;
1249
290
  const backgroundColor = backdrop?.background ?? (effectiveSky === undefined ? DEFAULT_BACKGROUND_COLOR : undefined);
1250
- const lighting = playable.lighting;
291
+ const lighting = resolvedLook.lighting;
1251
292
  const orthographic = playable.camera?.projection === "orthographic";
1252
293
  const localXY = (event) => {
1253
294
  const rect = wrapperRef.current?.getBoundingClientRect();
@@ -1273,7 +314,8 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
1273
314
  /** DOM UI (menus, HUD buttons) sits in the same wrapper as the canvas — only canvas-targeted clicks are world input. */
1274
315
  const isWorldPointerTarget = (event) => event.target instanceof HTMLCanvasElement;
1275
316
  const handlePointerDown = (event) => {
1276
- wrapperRef.current?.focus();
317
+ if (isWorldPointerTarget(event))
318
+ wrapperRef.current?.focus();
1277
319
  trackPointerAxis(event);
1278
320
  audioEngine.resume();
1279
321
  if (contextMenu !== null)
@@ -1411,32 +453,35 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
1411
453
  controlsActive &&
1412
454
  touchScheme !== null &&
1413
455
  (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: {
456
+ return (_jsx(SettingsProvider, { store: settingsStore, children: _jsxs(PlayerFovProvider, { config: playable.camera, orthographic: orthographic, children: [_jsx(AudioSettingsBridge, { store: settingsStore, engine: audioEngine, buses: playable.audio?.buses }), _jsx(SettingsRuntime, { variant: settingsVariant, surface: settingsSurface, actions: settingsActions, input: playable.game.input ?? {}, buses: playable.audio?.buses, extra: settingsConfig.extra ?? [], categories: settingsConfig.categories ?? [], hide: hideCategories, fovEnabled: fovControlEnabled, hideBindings: settingsConfig.hideBindings ?? [], touchStyle: coarsePointer &&
457
+ touchScheme !== null &&
458
+ (touchScheme.joystick !== null || touchScheme.buttons.length > 0), overrides: bindingOverrides, rebind: rebindAction, resetBinding: resetActionBinding, children: _jsx("div", { ref: wrapperRef, tabIndex: 0, ...(poster && posterFrozen ? { "data-poster-ready": "" } : {}), className: "relative h-full w-full bg-neutral-950 outline-none", style: {
1415
459
  "--jg-hud-dock-clearance": `${dockMounted ? touchDockClearance(touchScheme, touchScale) : 0}px`,
1416
460
  }, onKeyDown: (event) => {
1417
- if (event.code === "F2" && devtoolsEnabled) {
461
+ if (event.code === "F2") {
1418
462
  event.preventDefault();
1419
463
  f2HeldRef.current = true;
1420
- f2ChordedRef.current = false;
1421
464
  return;
1422
465
  }
1423
466
  if (f2HeldRef.current) {
1424
- f2ChordedRef.current = true;
467
+ if (event.code === "KeyD" && devtoolsEnabled) {
468
+ event.preventDefault();
469
+ document.exitPointerLock?.();
470
+ setDevtoolsOpen((current) => !current);
471
+ }
1425
472
  return;
1426
473
  }
1427
474
  if (event.code === "Tab" || event.code === "Space")
1428
475
  event.preventDefault();
1429
- tracker.handleDown(event.code);
476
+ if (playControlsActive(ctx))
477
+ tracker.handleDown(event.code);
1430
478
  }, onKeyUp: (event) => {
1431
- if (event.code === "F2" && devtoolsEnabled) {
479
+ if (event.code === "F2") {
1432
480
  f2HeldRef.current = false;
1433
- if (!f2ChordedRef.current) {
1434
- document.exitPointerLock?.();
1435
- setDevtoolsOpen((current) => !current);
1436
- }
1437
481
  return;
1438
482
  }
1439
- tracker.handleUp(event.code);
483
+ if (playControlsActive(ctx))
484
+ tracker.handleUp(event.code);
1440
485
  }, onBlur: () => {
1441
486
  f2HeldRef.current = false;
1442
487
  tracker.reset();
@@ -1460,14 +505,14 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
1460
505
  fov: playable.camera?.frustum?.fov ?? CAMERA_FRUSTUM_DEFAULTS.fov,
1461
506
  near: playable.camera?.frustum?.near ?? CAMERA_FRUSTUM_DEFAULTS.near,
1462
507
  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) => {
508
+ }, shadows: graphics.shadows, dpr: graphics.dpr, gl: { preserveDrawingBuffer: true }, style: { touchAction: "none" }, children: [backgroundColor !== undefined ? _jsx("color", { attach: "background", args: [backgroundColor] }) : null, cinematicLook ? _jsx(EnvironmentLighting, {}) : null, lighting !== undefined ? (_jsx(ConfiguredLighting, { lighting: lighting })) : effectiveSky === undefined ? (_jsxs(_Fragment, { children: [_jsx("ambientLight", { intensity: 0.55 }), _jsx("directionalLight", { position: [10, 16, 6], intensity: 1.3 })] })) : null, effectiveSky !== undefined ? (effectiveSky.timeOfDay ? (_jsx(TimeOfDayDaylight, { sky: effectiveSky, clock: ctx.time, bands: effectiveSky === worldSky ? biomeBands : undefined, lights: skyEmitsLights(resolveSkyLightOwnership(lighting !== undefined)) })) : (_jsx(SkyDaylight, { sky: effectiveSky, bands: effectiveSky === worldSky ? biomeBands : undefined, lights: skyEmitsLights(resolveSkyLightOwnership(lighting !== undefined)) }))) : null, _jsx(BackdropFog, { fog: backdrop?.fog }), _jsxs(GameProvider, { context: ctx, children: [_jsx(CullingProvider, { config: playable.visibility, children: _jsx(WorldView, { entitySprites: playable.entitySprites, entityModels: playable.entityModels, objectModels: playable.objectModels, objectStyles: playable.objectStyles, environment: AutoEnvironment, assets: playable.game.assets, renderEntity: playable.renderEntity, renderObject: playable.renderObject, selectedIds: selectedIds, hideLocalActor: firstPerson }) }), WorldOverlay !== undefined ? _jsx(WorldOverlay, { ctx: ctx }) : null, barsStatId !== null ? (_jsx(WorldEntityBars, { statId: barsStatId, roles: barsRoles, resolveRole: resolveEntityRole, ...(barsMaxDistance === undefined ? {} : { maxDistance: barsMaxDistance }) })) : null, nameplatesStatId !== null ? (_jsx(WorldNameplates, { statId: nameplatesStatId, roles: nameplatesRoles, resolveRole: resolveEntityRole, ...(nameplatesMaxDistance === undefined ? {} : { maxDistance: nameplatesMaxDistance }) })) : null, _jsx(WorldItems, { config: playable.worldItem }), _jsx(WorldTelegraphs, {}), _jsx(WorldSpellVfx, {}), _jsx(RetainedVfx, {}), _jsx(WorldFloatText, {}), _jsx(ProjectileTracers, {}), devtoolsEnabled ? _jsx(CollisionDebugWorld, {}) : null, _jsx(CombatCameraShake, {}), _jsx(AudioListener, { engine: audioEngine }), _jsx(EntityAudioEmitters, { engine: audioEngine, entitySounds: playable.entitySounds }), _jsx(ObjectAudioEmitters, { engine: audioEngine, objectSounds: playable.objectSounds }), _jsx(GameCameraRig, { yawRef: yawRef, pitchRef: pitchRef, config: cameraConfig, viewmodel: playable.viewmodel, pointerControls: pointerUsesLeft, panKeysEnabled: rtsPanKeysEnabled, director: ctx.camera, onDragChange: (dragging) => {
1464
509
  cameraDraggingRef.current = dragging;
1465
510
  } }), _jsx(PointerProbe, { service: pointerService })] }), _jsx(RemotePlayers, { rows: remotePlayers }), _jsx(FrameDriver, { ctx: ctx, playable: playable, tracker: tracker, yawRef: yawRef, pitchRef: pitchRef, primaryClickRef: primaryClickRef, pointerAxisRef: pointerAxisRef, gateRef: gateRef, onRuntimeError: reportRuntimeError, multiplayer: multiplayer, serverIdRef: serverIdRef, pointerService: pointerService, pointerAim: pointer?.aim === true, pingCommand: pointer?.pingCommand, poster: poster, onPosterSettled: () => {
1466
511
  if (posterSettledRef.current)
1467
512
  return;
1468
513
  posterSettledRef.current = true;
1469
514
  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: () => {
515
+ } }), _jsx(DevtoolsRendererProbe, {}), resolvedLook.postProcessing !== undefined && resolvedLook.postProcessing.enabled !== false ? (_jsx(PostProcessing, { config: resolvedLook.postProcessing })) : null] }), !poster && !orientationGate && coarsePointer && controlsActive && touchScheme !== null && (touchScheme.gestures !== null || touchScheme.look) ? (_jsx(TouchPlaySurface, { scheme: touchScheme, sink: touchSink, yawRef: yawRef, pitchRef: pitchRef, maxPitch: playable.camera?.firstPerson?.maxPitch ?? 1.45, onPrimaryTap: () => {
1471
516
  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, {})] }) }) })] }) }));
517
+ } })) : null, _jsx(GameUiErrorBoundary, { onRuntimeError: reportRuntimeError, children: _jsxs(GameProvider, { context: ctx, children: [_jsx(GamePhaseStamp, {}), _jsx(HudViewportProvider, { platforms: playable.platforms, config: playable.hudFit, userScale: graphics.uiScale, children: orientationGate ? null : _jsx(GameUI, {}) })] }) }), !poster && !orientationGate && showReticle ? _jsx(Reticle, {}) : null, dockMounted && touchScheme !== null ? (_jsx(TouchControlsDock, { scheme: touchScheme, sink: touchSink, style: touchStyle, scale: touchScale })) : null, orientationGateEl, marquee !== null ? _jsx(MarqueeBox, { rect: marquee }) : null, contextMenu !== null ? (_jsx(ContextMenuView, { menu: contextMenu.menu, x: contextMenu.x, y: contextMenu.y, onPick: handleVerbPick, onClose: () => setContextMenu(null) })) : null, devtoolsEnabled ? (_jsx(DevtoolsOverlay, { open: devtoolsOpen, ctx: ctx, playable: playable, multiplayer: multiplayer })) : null, poster ? null : _jsx(DiagnosticOverlay, { diagnostics: diagnostics, gameName: playable.game.name }), poster || orthographic || settingsHostsFov ? null : _jsx(PlayerFovSlider, {}), poster ? null : _jsx(SettingsChrome, {})] }) }) })] }) }));
1473
518
  }