@jgengine/shell 0.18.1 → 0.21.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 (261) hide show
  1. package/CHANGELOG.md +504 -7
  2. package/dist/GameHost.d.ts +12 -1
  3. package/dist/GameHost.js +14 -1
  4. package/dist/GamePlayerShell.js +132 -17
  5. package/dist/Shell3dPresentation.d.ts +5 -11
  6. package/dist/Shell3dPresentation.js +73 -11
  7. package/dist/ShellChrome.d.ts +2 -1
  8. package/dist/ShellChrome.js +22 -6
  9. package/dist/ShellHudPresentation.d.ts +20 -2
  10. package/dist/ShellHudPresentation.js +41 -6
  11. package/dist/audio/AudioComponents.js +6 -2
  12. package/dist/audio/audioEngine.d.ts +24 -0
  13. package/dist/audio/audioEngine.js +231 -83
  14. package/dist/audio/audioWire.d.ts +3 -0
  15. package/dist/audio/audioWire.js +9 -3
  16. package/dist/audio/loopParams.d.ts +39 -0
  17. package/dist/audio/loopParams.js +43 -0
  18. package/dist/audio/synthEngine.d.ts +7 -1
  19. package/dist/audio/synthEngine.js +103 -38
  20. package/dist/camera/GameCameraRig.js +3 -2
  21. package/dist/camera/GameFirstPersonCamera.d.ts +6 -4
  22. package/dist/camera/GameFirstPersonCamera.js +68 -27
  23. package/dist/camera/Viewports.d.ts +34 -0
  24. package/dist/camera/Viewports.js +123 -0
  25. package/dist/camera/cameraRigs.js +76 -62
  26. package/dist/camera/chaseHeading.d.ts +9 -0
  27. package/dist/camera/chaseHeading.js +10 -0
  28. package/dist/camera/customMuzzle.d.ts +14 -0
  29. package/dist/camera/customMuzzle.js +68 -0
  30. package/dist/camera/index.d.ts +1 -0
  31. package/dist/camera/index.js +1 -0
  32. package/dist/camera/orbitCameraMath.js +2 -2
  33. package/dist/camera/rigMath.d.ts +82 -4
  34. package/dist/camera/rigMath.js +133 -15
  35. package/dist/camera/rigResolve.d.ts +3 -0
  36. package/dist/camera/rigResolve.js +11 -0
  37. package/dist/combat/ProjectileModels.d.ts +31 -0
  38. package/dist/combat/ProjectileModels.js +70 -0
  39. package/dist/commandSink.d.ts +1 -2
  40. package/dist/commandSink.js +3 -2
  41. package/dist/defineGame.d.ts +19 -3
  42. package/dist/defineGame.js +39 -7
  43. package/dist/devtools/DevtoolsOverlay.d.ts +2 -69
  44. package/dist/devtools/DevtoolsOverlay.js +15 -167
  45. package/dist/devtools/DevtoolsRuntime.d.ts +7 -0
  46. package/dist/devtools/DevtoolsRuntime.js +61 -0
  47. package/dist/devtools/agentBridge.d.ts +6 -0
  48. package/dist/devtools/agentBridge.js +16 -2
  49. package/dist/devtools/devtoolsReports.d.ts +68 -0
  50. package/dist/devtools/devtoolsReports.js +100 -0
  51. package/dist/devtools/latencyInstrumentation.d.ts +3 -0
  52. package/dist/devtools/latencyInstrumentation.js +11 -0
  53. package/dist/devtools/playtestProbe.d.ts +4 -0
  54. package/dist/devtools/playtestProbe.js +22 -0
  55. package/dist/diagnostics/FpsCounter.d.ts +17 -0
  56. package/dist/diagnostics/FpsCounter.js +32 -0
  57. package/dist/drivers/FrameDriver.js +35 -20
  58. package/dist/drivers/FrameRateLimiter.d.ts +9 -0
  59. package/dist/drivers/FrameRateLimiter.js +39 -0
  60. package/dist/drivers/HudOnlyDriver.d.ts +6 -1
  61. package/dist/drivers/HudOnlyDriver.js +32 -14
  62. package/dist/environment/DayNightSky.js +5 -5
  63. package/dist/environment/Daylight.d.ts +35 -1
  64. package/dist/environment/Daylight.js +98 -28
  65. package/dist/environment/EnvironmentScene.js +11 -10
  66. package/dist/environment/VolumetricClouds.js +20 -2
  67. package/dist/environment/daylightCycle.d.ts +20 -0
  68. package/dist/environment/daylightCycle.js +59 -7
  69. package/dist/environment/sunShadowMath.d.ts +45 -0
  70. package/dist/environment/sunShadowMath.js +50 -0
  71. package/dist/input/gamepadPoll.d.ts +47 -0
  72. package/dist/input/gamepadPoll.js +142 -0
  73. package/dist/input/gamepadSource.d.ts +11 -1
  74. package/dist/input/gamepadSource.js +117 -71
  75. package/dist/input/mouseLook.js +2 -1
  76. package/dist/input/padHaptics.d.ts +17 -0
  77. package/dist/input/padHaptics.js +32 -0
  78. package/dist/input/pointerLock.d.ts +11 -0
  79. package/dist/input/pointerLock.js +19 -0
  80. package/dist/inputSink.d.ts +6 -6
  81. package/dist/inputSink.js +60 -21
  82. package/dist/lazyShell3dPresentation.d.ts +9 -0
  83. package/dist/lazyShell3dPresentation.js +24 -0
  84. package/dist/materialOverride.d.ts +42 -0
  85. package/dist/materialOverride.js +95 -36
  86. package/dist/multiplayer.d.ts +33 -3
  87. package/dist/multiplayer.js +105 -22
  88. package/dist/pointer/pointerService.d.ts +2 -0
  89. package/dist/pointer/pointerService.js +12 -2
  90. package/dist/postfx/PostProcessing.d.ts +11 -1
  91. package/dist/postfx/PostProcessing.js +14 -42
  92. package/dist/postfx/gradeShader.d.ts +1 -1
  93. package/dist/postfx/gradeShader.js +30 -2
  94. package/dist/postfx/shadowPrepass.d.ts +3 -0
  95. package/dist/postfx/shadowPrepass.js +18 -0
  96. package/dist/postfx/stylizePass.d.ts +23 -0
  97. package/dist/postfx/stylizePass.js +216 -0
  98. package/dist/presentationRecovery.d.ts +75 -0
  99. package/dist/presentationRecovery.js +204 -0
  100. package/dist/presentationRecovery3d.d.ts +4 -0
  101. package/dist/presentationRecovery3d.js +32 -0
  102. package/dist/presentationResolve.d.ts +8 -1
  103. package/dist/presentationResolve.js +5 -2
  104. package/dist/render/CascadedShadows.d.ts +7 -2
  105. package/dist/render/CascadedShadows.js +98 -20
  106. package/dist/render/EnvironmentLighting.js +74 -35
  107. package/dist/render/HairCards.d.ts +7 -0
  108. package/dist/render/HairCards.js +23 -0
  109. package/dist/render/SceneModels.js +52 -78
  110. package/dist/render/SkinnedInstances.d.ts +91 -0
  111. package/dist/render/SkinnedInstances.js +249 -0
  112. package/dist/render/TextureFiltering.d.ts +16 -0
  113. package/dist/render/TextureFiltering.js +64 -0
  114. package/dist/render/authoredSurfaceMaterial.d.ts +43 -0
  115. package/dist/render/authoredSurfaceMaterial.js +108 -0
  116. package/dist/render/displayColor.d.ts +33 -0
  117. package/dist/render/displayColor.js +138 -0
  118. package/dist/render/materialAppearanceSignals.d.ts +23 -0
  119. package/dist/render/materialAppearanceSignals.js +58 -0
  120. package/dist/render/materialAsset.d.ts +98 -0
  121. package/dist/render/materialAsset.js +454 -0
  122. package/dist/render/measureBounds.js +16 -3
  123. package/dist/render/measureCollisionMesh.js +8 -1
  124. package/dist/render/modelAssets.d.ts +8 -0
  125. package/dist/render/modelAssets.js +43 -0
  126. package/dist/render/modelBindPose.d.ts +5 -0
  127. package/dist/render/modelBindPose.js +35 -0
  128. package/dist/render/modelLoad.d.ts +32 -0
  129. package/dist/render/modelLoad.js +70 -1
  130. package/dist/render/modelNodeVisibility.d.ts +6 -0
  131. package/dist/render/modelNodeVisibility.js +18 -0
  132. package/dist/render/modelRender.d.ts +18 -1
  133. package/dist/render/modelRender.js +81 -3
  134. package/dist/render/projectedOverdraw.d.ts +35 -0
  135. package/dist/render/projectedOverdraw.js +204 -0
  136. package/dist/render/resolveModel.d.ts +12 -2
  137. package/dist/render/resolveModel.js +25 -4
  138. package/dist/render/rigNode.d.ts +26 -0
  139. package/dist/render/rigNode.js +32 -0
  140. package/dist/render/spriteRender.d.ts +4 -0
  141. package/dist/render/spriteRender.js +5 -0
  142. package/dist/render/staticShapeInstances.d.ts +34 -0
  143. package/dist/render/staticShapeInstances.js +78 -0
  144. package/dist/render/useFootIk.d.ts +53 -4
  145. package/dist/render/useFootIk.js +262 -70
  146. package/dist/render/useModelAnimation.d.ts +50 -0
  147. package/dist/render/useModelAnimation.js +245 -99
  148. package/dist/render/useModelInstance.d.ts +25 -0
  149. package/dist/render/useModelInstance.js +43 -0
  150. package/dist/scatter/scatterModels.d.ts +3 -2
  151. package/dist/scatter/scatterModels.js +15 -7
  152. package/dist/scene/AuthoredScene.d.ts +23 -6
  153. package/dist/scene/AuthoredScene.js +46 -9
  154. package/dist/scene/CityRenderer.js +6 -6
  155. package/dist/scene/GeneratedAssetRenderer.js +11 -42
  156. package/dist/settings/QuickControls.js +1 -1
  157. package/dist/settings/SettingsMenu.js +11 -18
  158. package/dist/settings/SettingsRuntime.d.ts +5 -0
  159. package/dist/settings/SettingsRuntime.js +29 -2
  160. package/dist/settings/appliedSettings.d.ts +9 -3
  161. package/dist/settings/appliedSettings.js +6 -5
  162. package/dist/settings/settingsController.d.ts +3 -0
  163. package/dist/settings/settingsController.js +47 -3
  164. package/dist/shellControlSuspension.d.ts +18 -0
  165. package/dist/shellControlSuspension.js +28 -0
  166. package/dist/shellInputPublication.d.ts +28 -0
  167. package/dist/shellInputPublication.js +49 -0
  168. package/dist/shellPointerInput.d.ts +18 -0
  169. package/dist/shellPointerInput.js +49 -0
  170. package/dist/structures/BuildingKitBatch.d.ts +3 -1
  171. package/dist/structures/BuildingKitBatch.js +35 -56
  172. package/dist/structures/BuildingRenderBudget.d.ts +23 -0
  173. package/dist/structures/BuildingRenderBudget.js +54 -0
  174. package/dist/structures/GeneratedBuilding.d.ts +3 -15
  175. package/dist/structures/GeneratedBuilding.js +33 -22
  176. package/dist/structures/buildingKitMaterials.d.ts +20 -0
  177. package/dist/structures/buildingKitMaterials.js +70 -0
  178. package/dist/structures/buildingSpatialBatch.d.ts +11 -0
  179. package/dist/structures/buildingSpatialBatch.js +45 -0
  180. package/dist/structures/buildingSurface.d.ts +21 -0
  181. package/dist/structures/buildingSurface.js +134 -0
  182. package/dist/terrain/GrassField.d.ts +2 -1
  183. package/dist/terrain/GrassField.js +59 -5
  184. package/dist/terrain/grassGeometry.d.ts +10 -0
  185. package/dist/terrain/grassGeometry.js +55 -0
  186. package/dist/terrain/grassMaterial.js +2 -2
  187. package/dist/terrain/terrainDetailMaterial.d.ts +3 -3
  188. package/dist/terrain/terrainDetailMaterial.js +7 -7
  189. package/dist/touch/TouchControlsOverlay.js +4 -4
  190. package/dist/useShellMultiplayerSync.d.ts +1 -1
  191. package/dist/useShellMultiplayerSync.js +9 -3
  192. package/dist/vfx/ParticleField.d.ts +23 -20
  193. package/dist/vfx/ParticleField.js +168 -50
  194. package/dist/visibility/CullingProvider.js +2 -62
  195. package/dist/visibility/cullingDriver.d.ts +9 -0
  196. package/dist/visibility/cullingDriver.js +68 -0
  197. package/dist/weather/AuthoredWeatherLayer.d.ts +16 -0
  198. package/dist/weather/AuthoredWeatherLayer.js +31 -0
  199. package/dist/weather/DustField.d.ts +2 -1
  200. package/dist/weather/DustField.js +17 -3
  201. package/dist/weather/FireSpreadLayer.d.ts +4 -2
  202. package/dist/weather/FireSpreadLayer.js +5 -11
  203. package/dist/weather/LightningStrike.d.ts +4 -1
  204. package/dist/weather/LightningStrike.js +13 -17
  205. package/dist/weather/RainField.d.ts +2 -1
  206. package/dist/weather/RainField.js +17 -3
  207. package/dist/weather/RainImpactField.d.ts +16 -0
  208. package/dist/weather/RainImpactField.js +89 -0
  209. package/dist/weather/SnowField.d.ts +2 -1
  210. package/dist/weather/SnowField.js +17 -3
  211. package/dist/weather/WeatherLayer.d.ts +11 -2
  212. package/dist/weather/WeatherLayer.js +30 -6
  213. package/dist/weather/index.d.ts +2 -0
  214. package/dist/weather/index.js +2 -0
  215. package/dist/weather/weatherMath.d.ts +2 -0
  216. package/dist/weather/weatherMath.js +8 -0
  217. package/dist/weather/weatherShelter.d.ts +18 -0
  218. package/dist/weather/weatherShelter.js +75 -0
  219. package/dist/weather/weatherUniforms.d.ts +40 -1
  220. package/dist/weather/weatherUniforms.js +29 -9
  221. package/dist/world/AuthoredFlocks.d.ts +22 -0
  222. package/dist/world/AuthoredFlocks.js +87 -0
  223. package/dist/world/StaticObjectModels.d.ts +8 -0
  224. package/dist/world/StaticObjectModels.js +75 -0
  225. package/dist/world/WorldHud.d.ts +14 -6
  226. package/dist/world/WorldHud.js +25 -12
  227. package/dist/world/WorldParticles.d.ts +10 -23
  228. package/dist/world/WorldParticles.js +210 -62
  229. package/dist/world/WorldScene.d.ts +3 -1
  230. package/dist/world/WorldScene.js +41 -26
  231. package/dist/world/staticObjectBatches.d.ts +35 -0
  232. package/dist/world/staticObjectBatches.js +163 -0
  233. package/dist/world/worldBarSamples.d.ts +37 -33
  234. package/dist/world/worldBarSamples.js +117 -75
  235. package/package.json +4 -4
  236. package/skills/ce-handoff/SKILL.md +2 -2
  237. package/skills/game-design/SKILL.md +2 -2
  238. package/skills/jgengine/SKILL.md +28 -6
  239. package/skills/jgengine/api.md +139 -99
  240. package/skills/jgengine/capabilities.md +28 -0
  241. package/skills/jgengine/recipes/minimal-game.md +10 -2
  242. package/skills/jgengine-ui/SKILL.md +16 -17
  243. package/skills/jgengine-ui/api.md +603 -365
  244. package/skills/jgengine-ui/capabilities.md +131 -1
  245. package/skills/jgengine-ui/reference-primitives.md +179 -0
  246. package/skills/jgengine-ui/reference.md +4 -81
  247. package/skills/jgengine-world/SKILL.md +25 -4
  248. package/skills/jgengine-world/api.md +1045 -655
  249. package/skills/jgengine-world/capabilities.md +246 -9
  250. package/skills/jgengine-world/recipes/character-animation.md +62 -0
  251. package/skills/jgengine-world/recipes/controls.md +48 -0
  252. package/skills/jgengine-world/recipes/flight-feel.md +76 -0
  253. package/skills/jgengine-world/recipes/npc-senses.md +55 -0
  254. package/skills/jgengine-world/recipes/vehicle-feel.md +87 -0
  255. package/skills/jgengine-world/reference.md +180 -4
  256. package/skills/jgengine-world/visibility.md +139 -0
  257. package/skills/level-design/SKILL.md +1 -1
  258. package/dist/render/testFixtures/proceduralSpriteAtlas.d.ts +0 -15
  259. package/dist/render/testFixtures/proceduralSpriteAtlas.js +0 -30
  260. package/dist/render/testFixtures/proceduralTileLayer.d.ts +0 -13
  261. package/dist/render/testFixtures/proceduralTileLayer.js +0 -30
@@ -1,9 +1,15 @@
1
1
  import { type ComponentType } from "react";
2
2
  import type { EditorCatalogDefinition } from "@jgengine/core/editor/types";
3
+ import type { CreatorConfig } from "@jgengine/core/editor/creatorStorage";
3
4
  import { type ResolveShellMultiplayerArgs, type ShellMultiplayer } from "./multiplayer.js";
4
5
  import type { PlayableGame } from "./registry.js";
5
6
  /** Structural shape of the module `GameHost`'s `editor` loader resolves — `import("@jgengine/editor")` satisfies it. */
6
7
  export interface EditorSummonModule {
8
+ CreatorApp?: ComponentType<{
9
+ gameId: string;
10
+ config: CreatorConfig<PlayableGame>;
11
+ onExit: () => void;
12
+ }>;
7
13
  EditorApp: ComponentType<{
8
14
  gameId: string;
9
15
  playable: PlayableGame;
@@ -28,6 +34,11 @@ export interface GameHostProps {
28
34
  * stays a lazy chunk the game bundles only when this prop is set.
29
35
  */
30
36
  editor?: () => Promise<EditorSummonModule>;
37
+ /** Player creator: injected durable storage, bounded catalog and a fresh runtime factory. */
38
+ creator?: CreatorConfig<PlayableGame>;
39
+ /** Controlled by the game's production menu. */
40
+ creatorOpen?: boolean;
41
+ onCreatorOpenChange?: (open: boolean) => void;
31
42
  }
32
43
  /**
33
44
  * The one documented mount: resolves multiplayer for the playable and renders the shell. With the
@@ -35,4 +46,4 @@ export interface GameHostProps {
35
46
  *
36
47
  * @capability mount-game mount a defined game in the browser — `<GameHost playable={game} editor={() => import("@jgengine/editor")} />`
37
48
  */
38
- export declare function GameHost({ playable, gameId, wsUrl, multiplayer, resolveMultiplayer, editor }: GameHostProps): import("react").JSX.Element;
49
+ export declare function GameHost({ playable, gameId, wsUrl, multiplayer, resolveMultiplayer, editor, creator, creatorOpen, onCreatorOpenChange }: GameHostProps): import("react").JSX.Element;
package/dist/GameHost.js CHANGED
@@ -27,7 +27,7 @@ function initialEditorMode(enabled) {
27
27
  *
28
28
  * @capability mount-game mount a defined game in the browser — `<GameHost playable={game} editor={() => import("@jgengine/editor")} />`
29
29
  */
30
- export function GameHost({ playable, gameId, wsUrl, multiplayer, resolveMultiplayer, editor }) {
30
+ export function GameHost({ playable, gameId, wsUrl, multiplayer, resolveMultiplayer, editor, creator, creatorOpen = false, onCreatorOpenChange }) {
31
31
  const resolvedGameId = gameId ?? playable.game.name;
32
32
  const [editorOpen, setEditorOpen] = useState(() => initialEditorMode(editor !== undefined));
33
33
  useEffect(() => {
@@ -88,6 +88,16 @@ export function GameHost({ playable, gameId, wsUrl, multiplayer, resolveMultipla
88
88
  return null;
89
89
  return lazy(async () => ({ default: (await editor()).EditorApp }));
90
90
  }, [editor]);
91
+ const CreatorLazy = useMemo(() => {
92
+ if (editor === undefined)
93
+ return null;
94
+ return lazy(async () => {
95
+ const module = await editor();
96
+ if (module.CreatorApp === undefined)
97
+ throw new Error("Editor module does not expose CreatorApp");
98
+ return { default: module.CreatorApp };
99
+ });
100
+ }, [editor]);
91
101
  const resolved = useMemo(() => {
92
102
  if (multiplayer !== undefined)
93
103
  return multiplayer;
@@ -105,6 +115,9 @@ export function GameHost({ playable, gameId, wsUrl, multiplayer, resolveMultipla
105
115
  }
106
116
  return session;
107
117
  }, [playable, resolvedGameId, wsUrl, multiplayer, resolveMultiplayer]);
118
+ if (creatorOpen && creator !== undefined && CreatorLazy !== null) {
119
+ return _jsx(Suspense, { fallback: null, children: _jsx(CreatorLazy, { gameId: resolvedGameId, config: creator, onExit: () => onCreatorOpenChange?.(false) }) });
120
+ }
108
121
  if (editorOpen && EditorLazy !== null) {
109
122
  return (_jsx(Suspense, { fallback: null, children: _jsx(EditorLazy, { gameId: resolvedGameId, playable: playable, catalogs: playable.editorCatalogs, onExitEditor: () => setEditorOpen(false) }) }));
110
123
  }
@@ -1,25 +1,28 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
2
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
3
3
  import { createActionStateTracker, toActionStateBindingMap, } from "@jgengine/core/input/actionBindings";
4
4
  import { RESERVED_INPUT_ACTIONS } from "./boundActionDispatch.js";
5
5
  import { deriveTouchScheme, withTouchCodes, DEFAULT_TOUCH_STYLE } from "@jgengine/core/input/touchScheme";
6
6
  import { activeTouchControlsMode } from "@jgengine/core/input/touchControlsMode";
7
7
  import { normalizePointerToAxis } from "@jgengine/core/input/pointerAxis";
8
8
  import { createGameContext } from "@jgengine/core/runtime/gameContext";
9
- import { activeActionCodes } from "@jgengine/core/game/controlGate";
9
+ import { localPlayers } from "@jgengine/core/runtime/localPlayers";
10
+ import { actionContextStack, activeActionCodes } from "@jgengine/core/game/controlGate";
10
11
  import { useDisplayProfile } from "@jgengine/react/display";
11
12
  import { RotateDeviceScreen } from "@jgengine/react/rotateDevice";
12
13
  import { createSettingsStore } from "@jgengine/core/settings/settingsModel";
13
14
  import { applyBindingOverrides, clearBindingOverride, loadBindingOverrides, saveBindingOverride, } from "@jgengine/core/input/bindingOverrides";
14
15
  import { orientationGateActive, orientationHintActive, resolveOrientationRequirement, } from "@jgengine/core/ui/orientation";
16
+ import { devtools } from "@jgengine/core/devtools/devtools";
15
17
  import { armFallbackSeams } from "@jgengine/core/devtools/fallbackSeams";
16
18
  import { armTextureErrors } from "@jgengine/core/devtools/textureErrors";
17
19
  import { readUrlFlag, subscribeUrlChange, writeUrlParam } from "@jgengine/core/devtools/urlFlags";
18
20
  import { createAudioEngine } from "./audio/audioEngine.js";
19
21
  import { attachAudioEventWire } from "./audio/audioWire.js";
20
22
  import { installAgentBridge } from "./devtools/agentBridge.js";
21
- import { withDevtoolsLatency } from "./devtools/DevtoolsOverlay.js";
22
- import { resolveRigKind } from "./camera/index.js";
23
+ import { attachPlaytestProbe } from "./devtools/playtestProbe.js";
24
+ import { withDevtoolsLatency } from "./devtools/latencyInstrumentation.js";
25
+ import { resolveRigKind } from "./camera/rigResolve.js";
23
26
  import { contextModels } from "./render/resolveModel.js";
24
27
  import { OrientationHint } from "./touch/OrientationHint.js";
25
28
  import { useTouchStyle, useTouchJoystickVariant, useGraphicsSettings } from "./settings/appliedSettings.js";
@@ -28,10 +31,18 @@ import { EMPTY_RESERVED } from "./shellConstants.js";
28
31
  import { JoinGate } from "./JoinGate.js";
29
32
  import { useShellMultiplayerSync } from "./useShellMultiplayerSync.js";
30
33
  import { ShellHudPresentation } from "./ShellHudPresentation.js";
31
- import { Shell3dPresentation } from "./Shell3dPresentation.js";
34
+ import { isServerAuthoritative } from "@jgengine/core/runtime/adapter";
35
+ import { playControlsActive } from "@jgengine/core/game/controlGate";
36
+ import { resolveInputSink } from "./inputSink.js";
37
+ import { observableShellTracker, attachShellInputPublication } from "./shellInputPublication.js";
38
+ import { attachShellControlSuspension } from "./shellControlSuspension.js";
39
+ import { LazyShell3dPresentation } from "./lazyShell3dPresentation.js";
40
+ import { createPresentationDiagnosticOwnership, createPlaySurfaceFocusOwnership, PresentationRecovery } from "./presentationRecovery.js";
32
41
  const DEV_USER_ID = "dev-player";
33
42
  /** The query param that mirrors the devtools overlay into the URL — present = open. */
34
43
  const DEBUG_PARAM = "debug";
44
+ const NO_SUBSCRIBE = () => () => undefined;
45
+ const ZERO_VERSION = () => 0;
35
46
  export { applyMotionImpulses } from "@jgengine/core/runtime/motionIntents";
36
47
  export { nearbyObstacles } from "@jgengine/core/movement/movementModel";
37
48
  export { resolvePhysicsTuning } from "@jgengine/core/movement/playerMovement";
@@ -70,8 +81,11 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
70
81
  const posterSettledRef = useRef(false);
71
82
  const [ctx, setCtx] = useState(null);
72
83
  const [diagnostics, setDiagnostics] = useState([]);
84
+ const diagnosticSequence = useRef(0);
85
+ const presentationDiagnostics = useRef(createPresentationDiagnosticOwnership());
73
86
  const [remotePlayers, setRemotePlayers] = useState([]);
74
87
  const wrapperRef = useRef(null);
88
+ const recoveredFocus = useRef(createPlaySurfaceFocusOwnership());
75
89
  const yawRef = useRef(0);
76
90
  const pitchRef = useRef(0);
77
91
  const serverIdRef = useRef(null);
@@ -90,7 +104,19 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
90
104
  const effectiveInput = useMemo(() => applyBindingOverrides(playable.game.input ?? {}, bindingOverrides), [playable, bindingOverrides]);
91
105
  const rebindAction = useCallback((action, code) => setBindingOverrides(saveBindingOverride(playable.game.name, action, [code])), [playable]);
92
106
  const resetActionBinding = useCallback((action) => setBindingOverrides(clearBindingOverride(playable.game.name, action)), [playable]);
93
- const tracker = useMemo(() => createActionStateTracker(toActionStateBindingMap(withTouchCodes(ctx === null ? effectiveInput : activeActionCodes(ctx, effectiveInput)))), [ctx, effectiveInput]);
107
+ const contextStack = ctx === null ? null : actionContextStack(ctx);
108
+ const contextVersion = useSyncExternalStore(contextStack?.subscribe ?? NO_SUBSCRIBE, contextStack?.version ?? ZERO_VERSION, contextStack?.version ?? ZERO_VERSION);
109
+ const activeInput = useMemo(() => (ctx === null ? effectiveInput : activeActionCodes(ctx, effectiveInput)),
110
+ // contextVersion re-derives the layered map when a context is pushed or popped mid-game.
111
+ [ctx, effectiveInput, contextVersion]);
112
+ const activeBindingMap = useMemo(() => toActionStateBindingMap(withTouchCodes(activeInput)), [activeInput]);
113
+ // One tracker per context, rebound in place so keys held across a context swap stay held.
114
+ const tracker = useMemo(() => observableShellTracker(createActionStateTracker(activeBindingMap)), [ctx]);
115
+ const boundMapRef = useRef(activeBindingMap);
116
+ if (boundMapRef.current !== activeBindingMap) {
117
+ boundMapRef.current = activeBindingMap;
118
+ tracker.rebind(activeBindingMap);
119
+ }
94
120
  const graphics = useGraphicsSettings(settingsStore, playable.shadows ?? true, playable.graphics);
95
121
  const trackPointerAxis = (event) => {
96
122
  const rect = wrapperRef.current?.getBoundingClientRect();
@@ -100,8 +126,10 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
100
126
  };
101
127
  const deactivatePointerAxis = () => {
102
128
  const state = pointerAxisRef.current;
103
- if (state !== null && state.active)
129
+ if (state !== null && state.active) {
104
130
  pointerAxisRef.current = { ...state, active: false };
131
+ tracker.notify("discrete");
132
+ }
105
133
  };
106
134
  // Reads fresh every render: the ctx-version useSyncExternalStore below re-renders on store
107
135
  // writes, so a gameplay setTouchControlsMode("car") swaps the visible control set that frame.
@@ -123,9 +151,31 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
123
151
  onCodeUp: (code) => tracker.handleUp(code),
124
152
  onAnalog: (values) => {
125
153
  analogRef.current = values;
154
+ tracker.notify("analog");
126
155
  },
127
156
  }), [tracker]);
157
+ useLayoutEffect(() => {
158
+ if (ctx === null)
159
+ return;
160
+ return attachShellControlSuspension({
161
+ ctx, tracker, pointerAxisRef, analogRef, primaryClickRef, cameraDraggingRef, f2HeldRef,
162
+ });
163
+ }, [ctx, tracker]);
128
164
  const gateRef = useRef(false);
165
+ useLayoutEffect(() => {
166
+ if (ctx === null || poster)
167
+ return;
168
+ return attachShellInputPublication({
169
+ ctx, tracker,
170
+ active: () => !gateRef.current && playControlsActive(ctx),
171
+ analog: () => analogRef.current,
172
+ pointer: () => pointerAxisRef.current,
173
+ sink: () => resolveInputSink({
174
+ serverAuthoritative: isServerAuthoritative(playable.game.multiplayer) && multiplayer !== null,
175
+ backend: multiplayer?.backend ?? null, serverId: serverIdRef.current,
176
+ }),
177
+ });
178
+ }, [ctx, tracker, multiplayer, playable, poster]);
129
179
  const orientationPlatform = coarsePointer ? "mobile" : "desktop";
130
180
  const orientationRequirement = useMemo(() => resolveOrientationRequirement(playable.orientation, orientationPlatform), [playable, orientationPlatform]);
131
181
  const liveOrientation = portrait ? "portrait" : "landscape";
@@ -135,6 +185,8 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
135
185
  const orientationGateEl = orientationGate ? (_jsx(RotateDeviceScreen, { requiredOrientation: orientationRequirement.required ?? "landscape", title: orientationRequirement.required === "portrait" ? "Turn your phone upright" : "Turn your phone sideways", description: `${playable.game.name} is built for ${orientationRequirement.required ?? "landscape"} play.` })) : orientationHint && orientationRequirement.preferred !== null ? (_jsx(OrientationHint, { wanted: orientationRequirement.preferred })) : null;
136
186
  const audioEngine = useMemo(() => createAudioEngine({
137
187
  sounds: playable.audio?.sounds,
188
+ maxVoices: playable.audio?.maxVoices,
189
+ voiceOverflow: playable.audio?.voiceOverflow,
138
190
  buses: playable.audio?.buses,
139
191
  music: playable.audio?.music,
140
192
  musicBus: playable.audio?.musicBus,
@@ -170,10 +222,59 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
170
222
  const userId = multiplayer?.userId ?? DEV_USER_ID;
171
223
  const reportRuntimeError = (error, phase, componentStack) => {
172
224
  const diagnostic = logRuntimeError(error, phase, componentStack);
173
- setDiagnostics((current) => [...current.slice(-4), { ...diagnostic, id: Date.now() + current.length }]);
225
+ const id = ++diagnosticSequence.current;
226
+ presentationDiagnostics.current.register(error, id, phase);
227
+ setDiagnostics((current) => [...current.slice(-4), { ...diagnostic, id }]);
228
+ };
229
+ const recoveryTrace = useRef([]);
230
+ const traceRecovery = (event) => {
231
+ if (!devtoolsEnabled)
232
+ return;
233
+ const wrapper = wrapperRef.current;
234
+ const active = document.activeElement;
235
+ const rect = wrapper?.getBoundingClientRect();
236
+ recoveryTrace.current.push({ ...event, at: performance.now(), connected: wrapper?.isConnected ?? false,
237
+ rects: wrapper?.getClientRects().length ?? 0, width: rect?.width ?? null, height: rect?.height ?? null,
238
+ display: wrapper === null ? null : getComputedStyle(wrapper).display,
239
+ active: active?.tagName ?? null, ownsFocus: active === wrapper, focusInside: wrapper?.contains(active) ?? false });
240
+ };
241
+ useEffect(() => {
242
+ recoveryTrace.current = [];
243
+ if (!devtoolsEnabled)
244
+ return;
245
+ return devtools.probes.register("presentationRecovery", () => recoveryTrace.current);
246
+ }, [ctx, devtoolsEnabled]);
247
+ const revealRecoveredPresentation = () => {
248
+ if (ctx === null)
249
+ return;
250
+ traceRecovery({ phase: "shell-reveal-layout" });
251
+ recoveredFocus.current.reveal(ctx, wrapperRef.current, document.activeElement, document.body);
252
+ traceRecovery({ phase: "shell-focus-result" });
253
+ };
254
+ const retireRecoveredPresentation = (error) => {
255
+ traceRecovery({ phase: "shell-recovered-callback" });
256
+ const owned = presentationDiagnostics.current.recovered(error);
257
+ setDiagnostics((current) => current.filter((diagnostic) => !owned.has(diagnostic.id)));
258
+ if (ctx !== null)
259
+ recoveredFocus.current.request(ctx);
260
+ revealRecoveredPresentation();
174
261
  };
175
262
  useEffect(() => {
176
263
  setDiagnostics([]);
264
+ let detachProbe = () => { };
265
+ let ownedContext = null;
266
+ const disposeOwnedContext = () => {
267
+ const context = ownedContext;
268
+ ownedContext = null;
269
+ if (context === null)
270
+ return;
271
+ try {
272
+ playable.loop.onDispose(context);
273
+ }
274
+ catch (error) {
275
+ reportRuntimeError(error, "dispose");
276
+ }
277
+ };
177
278
  try {
178
279
  const models = contextModels(playable);
179
280
  const context = createGameContext({
@@ -182,17 +283,31 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
182
283
  player: { userId, isNew: true },
183
284
  ...(models === undefined ? {} : { models }),
184
285
  });
286
+ ownedContext = context;
287
+ if (playable.localPlayers !== undefined)
288
+ localPlayers(context).retune(playable.localPlayers);
185
289
  playable.loop.onInit(context);
186
290
  playable.loop.onNewPlayer(context);
187
291
  onContextReady?.(context);
292
+ const probe = playable.capture?.probe;
293
+ if (probe !== undefined && typeof window !== "undefined") {
294
+ detachProbe = attachPlaytestProbe(window, () => probe(context));
295
+ }
188
296
  setCtx(context);
189
297
  }
190
298
  catch (error) {
299
+ disposeOwnedContext();
191
300
  reportRuntimeError(error, "init");
192
301
  setCtx(null);
193
302
  }
194
303
  return () => {
195
- setCtx(null);
304
+ try {
305
+ detachProbe();
306
+ }
307
+ finally {
308
+ disposeOwnedContext();
309
+ setCtx(null);
310
+ }
196
311
  };
197
312
  }, [playable, userId]);
198
313
  const authoritativeFrameRef = useRef(null);
@@ -224,7 +339,7 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
224
339
  touchSink,
225
340
  touchJoystickVariant,
226
341
  analogRef,
227
- inputBindings: effectiveInput,
342
+ inputBindings: activeInput,
228
343
  orientationGate,
229
344
  orientationGateEl,
230
345
  coarsePointer,
@@ -237,12 +352,12 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
237
352
  deactivatePointerAxis,
238
353
  };
239
354
  if (rigKind === "none" || playable.presentation === "hud") {
240
- return (_jsx(ShellHudPresentation, { ...shared, uiScale: graphics.uiScale, onPointerResumeAudio: () => audioEngine.resume() }));
355
+ return (_jsx(PresentationRecovery, { ctx: ctx, onRuntimeError: reportRuntimeError, onRetryCommitted: () => wrapperRef.current?.focus({ preventScroll: true }), onRecoveredDraw: retireRecoveredPresentation, onRecoveryEvent: traceRecovery, children: _jsx(ShellHudPresentation, { ...shared, serverIdRef: serverIdRef, uiScale: graphics.uiScale, onPointerResumeAudio: () => audioEngine.resume(), settingsStore: settingsStore, bindingOverrides: bindingOverrides, rebindAction: rebindAction, resetActionBinding: resetActionBinding, audioEngine: audioEngine, poster: poster }) }));
241
356
  }
242
- 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: () => {
243
- if (posterSettledRef.current)
244
- return;
245
- posterSettledRef.current = true;
246
- setPosterFrozen(true);
247
- }, authoritativeFrameRef: authoritativeFrameRef }));
357
+ return (_jsx(PresentationRecovery, { ctx: ctx, onRuntimeError: reportRuntimeError, onRetryCommitted: () => wrapperRef.current?.focus({ preventScroll: true }), onRecoveredDraw: retireRecoveredPresentation, onRecoveryEvent: traceRecovery, children: _jsx(LazyShell3dPresentation, { ...shared, onPresentationReveal: revealRecoveredPresentation, 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: () => {
358
+ if (posterSettledRef.current)
359
+ return;
360
+ posterSettledRef.current = true;
361
+ setPosterFrozen(true);
362
+ }, authoritativeFrameRef: authoritativeFrameRef }) }));
248
363
  }
@@ -1,10 +1,9 @@
1
1
  import { type Dispatch, type MutableRefObject, type RefObject, type SetStateAction } from "react";
2
2
  import type { PointerAxisState } from "@jgengine/core/input/pointerAxis";
3
- import type { ActionStateTracker } from "@jgengine/core/input/actionBindings";
3
+ import type { observableShellTracker } from "./shellInputPublication.js";
4
4
  import type { BindingOverrides } from "@jgengine/core/input/bindingOverrides";
5
5
  import type { TouchJoystickVariant, TouchScheme, TouchStyle } from "@jgengine/core/input/touchScheme";
6
- import type { GraphicsProfile } from "@jgengine/core/settings/graphicsProfile";
7
- import { type GraphicsQuality, type SettingsStore } from "@jgengine/core/settings/settingsModel";
6
+ import { type SettingsStore } from "@jgengine/core/settings/settingsModel";
8
7
  import type { PresencePoseRow } from "@jgengine/core/runtime/transport";
9
8
  import type { GameContext } from "@jgengine/core/runtime/gameContext";
10
9
  import type { AudioEngine } from "./audio/audioEngine.js";
@@ -12,13 +11,14 @@ import type { ShellMultiplayer } from "./multiplayer.js";
12
11
  import type { PlayableGame } from "./registry.js";
13
12
  import type { AuthoritativeFrameHandler } from "./worldSync.js";
14
13
  import { type TouchCodeSink } from "./touch/TouchControlsOverlay.js";
14
+ import { type AppliedGraphicsSettings } from "./settings/appliedSettings.js";
15
15
  import type { RuntimeDiagnostic } from "./diagnostics/RuntimeDiagnostics.js";
16
16
  /** 3D play surface: canvas, world overlays, and shared chrome. @internal */
17
17
  export declare function Shell3dPresentation({ playable, ctx, multiplayer, tracker, pointerAxisRef, gateRef, wrapperRef, f2HeldRef, yawRef, pitchRef, primaryClickRef, cameraDraggingRef, serverIdRef, remotePlayers, touchScheme, touchSink, touchStyle, touchJoystickVariant, analogRef, inputBindings, orientationGate, orientationGateEl, coarsePointer, compact, graphics, settingsStore, bindingOverrides, rebindAction, resetActionBinding, diagnostics, devtoolsEnabled, devtoolsOpen, setDevtoolsOpen, reportRuntimeError, trackPointerAxis, deactivatePointerAxis, audioEngine, poster, posterFrozen, onPosterSettled, authoritativeFrameRef, }: {
18
18
  playable: PlayableGame;
19
19
  ctx: GameContext;
20
20
  multiplayer: ShellMultiplayer | null;
21
- tracker: ActionStateTracker<string>;
21
+ tracker: ReturnType<typeof observableShellTracker>;
22
22
  pointerAxisRef: MutableRefObject<PointerAxisState | null>;
23
23
  gateRef: MutableRefObject<boolean>;
24
24
  wrapperRef: RefObject<HTMLDivElement | null>;
@@ -39,13 +39,7 @@ export declare function Shell3dPresentation({ playable, ctx, multiplayer, tracke
39
39
  orientationGateEl: React.ReactNode;
40
40
  coarsePointer: boolean;
41
41
  compact: boolean;
42
- graphics: {
43
- shadows: boolean;
44
- dpr: number;
45
- uiScale: number;
46
- quality: GraphicsQuality;
47
- profile: GraphicsProfile;
48
- };
42
+ graphics: AppliedGraphicsSettings;
49
43
  settingsStore: SettingsStore;
50
44
  bindingOverrides: BindingOverrides;
51
45
  rebindAction: (action: string, code: string) => void;
@@ -1,6 +1,6 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Canvas } from "@react-three/fiber";
3
- import { useMemo, useRef, useState, } from "react";
3
+ import { useMemo, useEffect, useRef, useState, useSyncExternalStore, } from "react";
4
4
  import { useFrame, useThree } from "@react-three/fiber";
5
5
  import { contextVerbInput, } from "@jgengine/core/interaction/contextMenu";
6
6
  import { createSelectionSet, isMarquee, screenRect, selectWithinRect, } from "@jgengine/core/scene/selection";
@@ -15,7 +15,9 @@ import { GameViewportProvider } from "@jgengine/react/gameViewport";
15
15
  import { GameProvider } from "@jgengine/react/provider";
16
16
  import { SettingsProvider } from "@jgengine/react/settings";
17
17
  import { resolveWorldSky } from "./worldSky.js";
18
+ import { resolveRuntimeCameraConfig } from "./camera/rigResolve.js";
18
19
  import { pointerAimFor, pointerContextMenu } from "./shellPointer.js";
20
+ import { shellPointerInput } from "./shellPointerInput.js";
19
21
  import { AudioListener, EntityAudioEmitters, ObjectAudioEmitters } from "./audio/AudioComponents.js";
20
22
  import { PostProcessing } from "./postfx/PostProcessing.js";
21
23
  import { EnvironmentLighting } from "./render/EnvironmentLighting.js";
@@ -23,7 +25,9 @@ import { CollisionDebugWorld } from "./devtools/CollisionDebugWorld.js";
23
25
  import { DevtoolsRendererProbe } from "./devtools/DevtoolsOverlay.js";
24
26
  import { CAMERA_TRANSPARENT_USERDATA, GameCameraRig, PlayerFovProvider, PlayerFovSlider, resolveRigKind, rtsPanKeysConflict, } from "./camera/index.js";
25
27
  import { CullingProvider } from "./visibility/CullingProvider.js";
28
+ import { BuildingRenderBudget } from "./structures/BuildingRenderBudget.js";
26
29
  import { SkyDaylight, TimeOfDayDaylight } from "./environment.js";
30
+ import { bearingFromDirection } from "./environment/daylightCycle.js";
27
31
  import { resolveSkyLightOwnership, skyEmitsLights } from "./environment/skyLightingPolicy.js";
28
32
  import { EnvironmentScene } from "./environment/EnvironmentScene.js";
29
33
  import { PointerProbe } from "./pointer/PointerProbe.js";
@@ -37,10 +41,15 @@ import { TouchControlsDock, TouchPlaySurface, touchDockClearance } from "./touch
37
41
  import { SettingsRuntime } from "./settings/SettingsRuntime.js";
38
42
  import { SettingsChrome } from "./settings/SettingsChrome.js";
39
43
  import { AudioSettingsBridge } from "./settings/appliedSettings.js";
44
+ import { TextureFiltering } from "./render/TextureFiltering.js";
45
+ import { FrameRateLimiter } from "./drivers/FrameRateLimiter.js";
46
+ import { FpsCounter, FpsProbe } from "./diagnostics/FpsCounter.js";
40
47
  import { ConfiguredLighting, BackdropFog } from "./render/SceneLighting.js";
41
48
  import { WorldView, RemotePlayers } from "./world/WorldScene.js";
42
49
  import { FrameDriver } from "./drivers/FrameDriver.js";
43
50
  import { GamepadSource } from "./input/gamepadSource.js";
51
+ import { SplitScreenRenderer, useSeatViewports } from "./camera/Viewports.js";
52
+ import { localPlayers } from "@jgengine/core/runtime/localPlayers";
44
53
  import { createShellKeyHandlers, ShellDebugOverlays, ShellGameUiChrome } from "./ShellChrome.js";
45
54
  import { CombatPresentation } from "./CombatPresentation.js";
46
55
  import { WorldParticles } from "./world/WorldParticles.js";
@@ -70,16 +79,19 @@ function PixelPerfectCamera({ pixelsPerUnit, integerScale }) {
70
79
  export function Shell3dPresentation({ playable, ctx, multiplayer, tracker, pointerAxisRef, gateRef, wrapperRef, f2HeldRef, yawRef, pitchRef, primaryClickRef, cameraDraggingRef, serverIdRef, remotePlayers, touchScheme, touchSink, touchStyle, touchJoystickVariant, analogRef, inputBindings, orientationGate, orientationGateEl, coarsePointer, compact, graphics, settingsStore, bindingOverrides, rebindAction, resetActionBinding, diagnostics, devtoolsEnabled, devtoolsOpen, setDevtoolsOpen, reportRuntimeError, trackPointerAxis, deactivatePointerAxis, audioEngine, poster, posterFrozen, onPosterSettled, authoritativeFrameRef, }) {
71
80
  const GameUI = playable.GameUI;
72
81
  const ktx2SupportDetected = useRef(false);
82
+ const frameTally = useRef({ frames: 0 }).current;
73
83
  const WorldOverlay = playable.WorldOverlay;
74
84
  const pointerService = useMemo(() => createPointerService(), []);
75
85
  const selection = useMemo(() => createSelectionSet(), [playable]);
76
86
  const controlledEntityId = ctx.player.possession.active(ctx.player.userId);
77
- const cameraConfig = playable.camera?.followEntityId !== undefined
87
+ const cameraOverride = useSyncExternalStore(ctx.camera.subscribe, ctx.camera.rig, ctx.camera.rig);
88
+ const configuredCamera = playable.camera?.followEntityId !== undefined
78
89
  ? playable.camera
79
90
  : { ...playable.camera, followEntityId: controlledEntityId };
91
+ const cameraConfig = resolveRuntimeCameraConfig(configuredCamera, cameraOverride);
80
92
  const rigKind = resolveRigKind(cameraConfig);
81
93
  const firstPerson = rigKind === "first";
82
- const showReticle = (firstPerson && playable.camera?.firstPerson?.reticle !== false) || rigKind === "shoulder";
94
+ const showReticle = (firstPerson && cameraConfig?.firstPerson?.reticle !== false) || rigKind === "shoulder";
83
95
  const rtsPanKeysEnabled = !rtsPanKeysConflict(playable.game.input);
84
96
  const bars = resolveWorldOverlayBars(playable.worldHealthBars);
85
97
  const nameplates = resolveWorldOverlayBars(playable.nameplates);
@@ -109,9 +121,13 @@ export function Shell3dPresentation({ playable, ctx, multiplayer, tracker, point
109
121
  const cinematicLook = (playable.look ?? "cinematic") !== "flat";
110
122
  const backdrop = resolvedLook.backdrop;
111
123
  const backdropSky = backdrop?.sky !== undefined ? resolveSkyDescriptor(backdrop.sky) : undefined;
112
- const effectiveSky = backdropSky ?? worldSky;
113
- const backgroundColor = backdrop?.background ?? (effectiveSky === undefined ? DEFAULT_BACKGROUND_COLOR : undefined);
124
+ const authoredSky = backdropSky ?? worldSky;
114
125
  const lighting = resolvedLook.lighting;
126
+ const sunFromLighting = lighting?.directional?.[0]?.position;
127
+ const effectiveSky = useMemo(() => authoredSky === undefined || authoredSky.sun !== undefined || authoredSky.timeOfDay || sunFromLighting === undefined
128
+ ? authoredSky
129
+ : { ...authoredSky, sun: bearingFromDirection(sunFromLighting) }, [authoredSky, sunFromLighting]);
130
+ const backgroundColor = backdrop?.background ?? (effectiveSky === undefined ? DEFAULT_BACKGROUND_COLOR : undefined);
115
131
  const orthographic = playable.camera?.projection === "orthographic";
116
132
  const pointerDownRef = useRef(null);
117
133
  const marqueeStartRef = useRef(null);
@@ -141,7 +157,36 @@ export function Shell3dPresentation({ playable, ctx, multiplayer, tracker, point
141
157
  };
142
158
  /** DOM UI sits in the same wrapper as the canvas — only canvas-targeted clicks are world input. */
143
159
  const isWorldPointerTarget = (event) => event.target instanceof HTMLCanvasElement;
160
+ const pointerInput = useMemo(() => shellPointerInput(tracker, () => playControlsActive(ctx)), [tracker, ctx]);
161
+ useEffect(() => {
162
+ const clearGesture = () => { pointerDownRef.current = null; marqueeStartRef.current = null; setMarquee(null); };
163
+ const up = (event) => {
164
+ pointerInput.up(event);
165
+ if (!(event.target instanceof Node) || !wrapperRef.current?.contains(event.target))
166
+ clearGesture();
167
+ };
168
+ const cancel = (event) => { pointerInput.cancel(event.pointerId); clearGesture(); };
169
+ const retire = () => { pointerInput.cancel(); clearGesture(); };
170
+ const blur = () => { tracker.reset(); retire(); };
171
+ const move = (event) => { pointerInput.move(event, false); };
172
+ const detachReset = pointerInput.attachReset(tracker, clearGesture);
173
+ window.addEventListener("pointerup", up);
174
+ window.addEventListener("pointermove", move);
175
+ window.addEventListener("pointercancel", cancel);
176
+ window.addEventListener("blur", blur);
177
+ return () => {
178
+ window.removeEventListener("pointerup", up);
179
+ window.removeEventListener("pointermove", move);
180
+ window.removeEventListener("pointercancel", cancel);
181
+ window.removeEventListener("blur", blur);
182
+ detachReset();
183
+ pointerInput.cancel();
184
+ };
185
+ }, [pointerInput, tracker]);
144
186
  const handlePointerDown = (event) => {
187
+ pointerInput.down(event, isWorldPointerTarget(event));
188
+ if (!playControlsActive(ctx))
189
+ return;
145
190
  if (isWorldPointerTarget(event))
146
191
  wrapperRef.current?.focus();
147
192
  trackPointerAxis(event);
@@ -156,6 +201,7 @@ export function Shell3dPresentation({ playable, ctx, multiplayer, tracker, point
156
201
  }
157
202
  };
158
203
  const handlePointerMove = (event) => {
204
+ pointerInput.move(event, isWorldPointerTarget(event));
159
205
  trackPointerAxis(event);
160
206
  if (pointer?.select !== true)
161
207
  return;
@@ -166,6 +212,13 @@ export function Shell3dPresentation({ playable, ctx, multiplayer, tracker, point
166
212
  setMarquee(screenRect(start.x, start.y, point.x, point.y));
167
213
  };
168
214
  const handlePointerUp = (event) => {
215
+ const boundPointer = pointerInput.up(event);
216
+ if (!playControlsActive(ctx)) {
217
+ pointerDownRef.current = null;
218
+ marqueeStartRef.current = null;
219
+ setMarquee(null);
220
+ return;
221
+ }
169
222
  if (event.button !== 0 || pointerDownRef.current === null)
170
223
  return;
171
224
  const start = pointerDownRef.current;
@@ -216,7 +269,7 @@ export function Shell3dPresentation({ playable, ctx, multiplayer, tracker, point
216
269
  return;
217
270
  }
218
271
  const moved = (end.x - start.x) ** 2 + (end.y - start.y) ** 2;
219
- if (moved <= PRIMARY_CLICK_MOVE_THRESHOLD_PX * PRIMARY_CLICK_MOVE_THRESHOLD_PX)
272
+ if (!boundPointer && moved <= PRIMARY_CLICK_MOVE_THRESHOLD_PX * PRIMARY_CLICK_MOVE_THRESHOLD_PX)
220
273
  primaryClickRef.current = true;
221
274
  };
222
275
  const handleContextMenu = (event) => {
@@ -258,6 +311,8 @@ export function Shell3dPresentation({ playable, ctx, multiplayer, tracker, point
258
311
  }
259
312
  };
260
313
  const controlsActive = playControlsActive(ctx);
314
+ const seatViewports = useSeatViewports(ctx, playable.viewports);
315
+ const splitScreen = playable.viewports !== undefined && seatViewports.length > 1;
261
316
  const settingsDisabled = playable.settings === false;
262
317
  const settingsConfig = playable.settings === false || playable.settings === undefined ? {} : playable.settings;
263
318
  const settingsSurface = settingsDisabled ? false : settingsConfig.surface ?? false;
@@ -289,7 +344,7 @@ export function Shell3dPresentation({ playable, ctx, multiplayer, tracker, point
289
344
  controlsActive: () => playControlsActive(ctx),
290
345
  exitPointerLockOnDevtools: true,
291
346
  });
292
- 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 &&
347
+ 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, graphics: playable.graphics, hideBindings: settingsConfig.hideBindings ?? [], touchStyle: coarsePointer &&
293
348
  touchScheme !== null &&
294
349
  (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: {
295
350
  "--jg-hud-dock-clearance": `${dockMounted ? touchDockClearance(touchScheme, touchScale) : 0}px`,
@@ -308,7 +363,7 @@ export function Shell3dPresentation({ playable, ctx, multiplayer, tracker, point
308
363
  return;
309
364
  ktx2SupportDetected.current = true;
310
365
  detectKtx2Support(gl);
311
- }, frameloop: poster && posterFrozen ? "demand" : "always", orthographic: orthographic, camera: orthographic
366
+ }, frameloop: poster && posterFrozen ? "demand" : graphics.frameRateLimit > 0 && !poster ? "never" : "always", orthographic: orthographic, camera: orthographic
312
367
  ? {
313
368
  zoom: playable.camera?.frustum?.zoom ?? CAMERA_FRUSTUM_DEFAULTS.zoom,
314
369
  near: playable.camera?.frustum?.near ?? CAMERA_FRUSTUM_DEFAULTS.near,
@@ -318,9 +373,16 @@ export function Shell3dPresentation({ playable, ctx, multiplayer, tracker, point
318
373
  fov: playable.camera?.frustum?.fov ?? CAMERA_FRUSTUM_DEFAULTS.fov,
319
374
  near: playable.camera?.frustum?.near ?? CAMERA_FRUSTUM_DEFAULTS.near,
320
375
  far: playable.camera?.frustum?.far ?? CAMERA_FRUSTUM_DEFAULTS.far,
321
- }, shadows: graphics.shadows, dpr: [Math.min(1, graphics.dpr), graphics.dpr], gl: { preserveDrawingBuffer: true }, style: { touchAction: "none" }, children: [orthographic && playable.camera?.pixelPerfect !== undefined ? (_jsx(PixelPerfectCamera, { ...playable.camera.pixelPerfect })) : null, backgroundColor !== undefined ? _jsx("color", { attach: "background", args: [backgroundColor] }) : null, cinematicLook ? (_jsx(EnvironmentLighting, { source: playable.environmentSource, skyColor: lighting?.hemisphere?.skyColor ?? "#87b5e0", groundColor: lighting?.hemisphere?.groundColor ?? "#3d4a38", sunDirection: lighting?.directional?.[0]?.position, sunColor: lighting?.directional?.[0]?.color ?? "#fff2d6" })) : null, lighting !== undefined ? (_jsx(ConfiguredLighting, { lighting: lighting, profile: graphics.profile })) : 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, drawDistance: graphics.profile.drawDistance, 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("group", { userData: CAMERA_TRANSPARENT_USERDATA, children: _jsx(WorldOverlay, { ctx: ctx }) })) : null, bars !== null ? (_jsx(WorldEntityBars, { statId: bars.statId, roles: bars.roles, resolveRole: resolveEntityRole, ...(bars.maxDistance === undefined ? {} : { maxDistance: bars.maxDistance }), ...(bars.occlude === undefined ? {} : { occlude: bars.occlude }) })) : null, nameplates !== null ? (_jsx(WorldNameplates, { statId: nameplates.statId, roles: nameplates.roles, resolveRole: resolveEntityRole, ...(nameplates.maxDistance === undefined ? {} : { maxDistance: nameplates.maxDistance }), ...(nameplates.showHealth === undefined ? {} : { showHealth: nameplates.showHealth }) })) : null, _jsx(WorldItems, { config: playable.worldItem }), _jsx(WorldParticles, { quality: graphics.quality, particleCap: graphics.profile.particleCap }), _jsx(CombatPresentation, { effects: effects }), devtoolsEnabled ? _jsx(CollisionDebugWorld, {}) : null, _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) => {
322
- cameraDraggingRef.current = dragging;
323
- } }), _jsx(PointerProbe, { service: pointerService })] }), _jsx(RemotePlayers, { rows: remotePlayers }), _jsx(FrameDriver, { ctx: ctx, playable: playable, tracker: tracker, yawRef: yawRef, pitchRef: pitchRef, primaryClickRef: primaryClickRef, pointerAxisRef: pointerAxisRef, analogRef: analogRef, gateRef: gateRef, onRuntimeError: reportRuntimeError, multiplayer: multiplayer, serverIdRef: serverIdRef, pointerService: pointerService, pointerAim: pointer?.aim === true, pingCommand: pointer?.pingCommand, poster: poster, onPosterSettled: onPosterSettled, authoritativeFrameRef: authoritativeFrameRef }), _jsx(GamepadSource, { tracker: tracker, bindings: inputBindings ?? {}, analogRef: analogRef, input: ctx.input }), _jsx(DevtoolsRendererProbe, {}), resolvedLook.postProcessing !== undefined && resolvedLook.postProcessing.enabled !== false ? (_jsx(PostProcessing, { config: resolvedLook.postProcessing, quality: graphics.quality, stages: graphics.profile.postStages })) : null] }), !poster &&
376
+ }, shadows: graphics.shadows, dpr: [Math.min(1, graphics.dpr), graphics.dpr], gl: { preserveDrawingBuffer: true, powerPreference: "high-performance" }, style: { touchAction: "none" }, children: [_jsx(TextureFiltering, { anisotropy: graphics.profile.anisotropy }), graphics.frameRateLimit > 0 && !poster ? _jsx(FrameRateLimiter, { fps: graphics.frameRateLimit }) : null, graphics.showFps ? _jsx(FpsProbe, { tally: frameTally }) : null, orthographic && playable.camera?.pixelPerfect !== undefined ? (_jsx(PixelPerfectCamera, { ...playable.camera.pixelPerfect })) : null, backgroundColor !== undefined ? _jsx("color", { attach: "background", args: [backgroundColor] }) : null, cinematicLook ? (_jsx(EnvironmentLighting, { source: playable.environmentSource, skyColor: lighting?.hemisphere?.skyColor ?? "#87b5e0", groundColor: lighting?.hemisphere?.groundColor ?? "#3d4a38", sunDirection: lighting?.directional?.[0]?.position, sunColor: lighting?.directional?.[0]?.color ?? "#fff2d6" })) : null, lighting !== undefined ? (_jsx(ConfiguredLighting, { lighting: lighting, profile: graphics.profile })) : 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 }), _jsx(GameProvider, { context: ctx, children: _jsxs(BuildingRenderBudget, { drawDistance: graphics.profile.drawDistance, visibility: playable.visibility, cullingDisabled: splitScreen, children: [_jsxs(CullingProvider, { config: playable.visibility, drawDistance: graphics.profile.drawDistance, 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, editorLayers: playable.editorLayers }), bars !== null ? (_jsx(WorldEntityBars, { statId: bars.statId, roles: bars.roles, resolveRole: resolveEntityRole, ...(bars.maxSamples === undefined ? {} : { maxSamples: bars.maxSamples }), ...(bars.tickMs === undefined ? {} : { tickMs: bars.tickMs }), ...(bars.maxDistance === undefined ? {} : { maxDistance: bars.maxDistance }), ...(bars.occlude === undefined ? {} : { occlude: bars.occlude }) })) : null, nameplates !== null ? (_jsx(WorldNameplates, { statId: nameplates.statId, roles: nameplates.roles, resolveRole: resolveEntityRole, ...(nameplates.maxSamples === undefined ? {} : { maxSamples: nameplates.maxSamples }), ...(nameplates.tickMs === undefined ? {} : { tickMs: nameplates.tickMs }), ...(nameplates.maxDistance === undefined ? {} : { maxDistance: nameplates.maxDistance }), occlude: nameplates.occlude, ...(nameplates.resolveName === undefined ? {} : { resolveName: nameplates.resolveName }), ...(nameplates.showHealth === undefined ? {} : { showHealth: nameplates.showHealth }) })) : null, WorldOverlay !== undefined ? (_jsx("group", { userData: CAMERA_TRANSPARENT_USERDATA, children: _jsx(WorldOverlay, { ctx: ctx }) })) : null] }), _jsx(WorldItems, { config: playable.worldItem }), _jsx(WorldParticles, { quality: graphics.quality, particleCap: graphics.profile.particleCap, diagnostics: devtoolsEnabled }), _jsx(CombatPresentation, { effects: effects }), devtoolsEnabled ? _jsx(CollisionDebugWorld, {}) : null, _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) => {
377
+ cameraDraggingRef.current = dragging;
378
+ } }), _jsx(PointerProbe, { service: pointerService })] }) }), _jsx(RemotePlayers, { rows: remotePlayers }), _jsx(FrameDriver, { ctx: ctx, playable: playable, tracker: tracker, yawRef: yawRef, pitchRef: pitchRef, primaryClickRef: primaryClickRef, pointerAxisRef: pointerAxisRef, analogRef: analogRef, gateRef: gateRef, onRuntimeError: reportRuntimeError, multiplayer: multiplayer, serverIdRef: serverIdRef, pointerService: pointerService, pointerAim: pointer?.aim === true, pingCommand: pointer?.pingCommand, poster: poster, onPosterSettled: onPosterSettled, authoritativeFrameRef: authoritativeFrameRef }), _jsx(GamepadSource, { tracker: tracker, bindings: inputBindings ?? {}, analogRef: analogRef, input: ctx.input, feel: playable.gamepad, seats: localPlayers(ctx), onSeatJoin: (slot) => {
379
+ try {
380
+ playable.loop.onNewPlayer(ctx, { userId: slot.userId, isNew: true });
381
+ }
382
+ catch (error) {
383
+ reportRuntimeError(error, "tick");
384
+ }
385
+ }, seatsActive: () => !gateRef.current && playControlsActive(ctx) }), _jsx(DevtoolsRendererProbe, {}), splitScreen ? (_jsx(SplitScreenRenderer, { ctx: ctx, viewports: seatViewports, config: playable.viewports, mainCamera: playable.camera })) : null, !splitScreen && resolvedLook.postProcessing !== undefined && resolvedLook.postProcessing.enabled !== false ? (_jsx(PostProcessing, { config: resolvedLook.postProcessing, quality: graphics.quality, stages: graphics.profile.postStages })) : null] }), graphics.showFps && !poster ? _jsx(FpsCounter, { tally: frameTally }) : null, !poster &&
324
386
  !orientationGate &&
325
387
  coarsePointer &&
326
388
  controlsActive &&
@@ -1,4 +1,4 @@
1
- import type { ComponentType } from "react";
1
+ import { type ComponentType } from "react";
2
2
  import type { GameContext } from "@jgengine/core/runtime/gameContext";
3
3
  import { type RuntimeDiagnostic } from "./diagnostics/RuntimeDiagnostics.js";
4
4
  import type { ShellMultiplayer } from "./multiplayer.js";
@@ -27,6 +27,7 @@ export type ShellKeyHandlers = {
27
27
  onKeyDown: (event: {
28
28
  code: string;
29
29
  target?: EventTarget | null;
30
+ defaultPrevented?: boolean;
30
31
  preventDefault: () => void;
31
32
  }) => void;
32
33
  onKeyUp: (event: {