@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,8 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useMemo, useRef } from "react";
2
+ import { useMemo, useRef, useState, } from "react";
3
3
  import { createGestureSurfaceTracker } from "@jgengine/core/input/gestureSurface";
4
4
  import { createTouchGestureTracker } from "@jgengine/core/input/touchGestures";
5
- import { touchCode } from "@jgengine/core/input/touchScheme";
5
+ import { touchCode, } from "@jgengine/core/input/touchScheme";
6
6
  import { GameIcon, iconForAction, isGameIconName } from "@jgengine/react/gameIcons";
7
7
  import { useRegisterLayoutRegion } from "@jgengine/react/gameViewport";
8
8
  const MOVEMENT_ZONE = { id: "control:movement", kind: "control", collisionPolicy: "forbid", collisionGroup: "touch-dock" };
@@ -14,6 +14,7 @@ const JOYSTICK_DEADZONE = 0.28;
14
14
  const JOYSTICK_AXIS_THRESHOLD = 0.42;
15
15
  const LOOK_TAP_TUNING = { tapMoveThresholdPx: 12, tapMaxMs: 280, longPressMs: 450 };
16
16
  const BUTTON_SIZE = 56;
17
+ const RAIL_BUTTON_SIZE = 64;
17
18
  const RING1_RADIUS = 112;
18
19
  const RING2_RADIUS = 178;
19
20
  const RING1_CAPACITY = 3;
@@ -21,6 +22,74 @@ const RING2_CAPACITY = 5;
21
22
  const RING1_ANGLES = [[], [40], [16, 64], [12, 44, 76]];
22
23
  const DOCK_BASE_PADDING = 20;
23
24
  const UTILITY_ROW_HEIGHT = 28;
25
+ /** width:height footprint of each silhouette; the longer side equals the button size. */
26
+ const SHAPE_ASPECT = {
27
+ circle: { w: 1, h: 1 },
28
+ square: { w: 1, h: 1 },
29
+ wheel: { w: 1, h: 1 },
30
+ pedal: { w: 0.78, h: 1 },
31
+ lever: { w: 0.64, h: 1 },
32
+ trigger: { w: 0.92, h: 1 },
33
+ tab: { w: 1.5, h: 0.72 },
34
+ };
35
+ const STYLE_TOKENS = {
36
+ glass: {
37
+ restFill: "rgba(255,255,255,0.10)",
38
+ restStroke: "rgba(255,255,255,0.28)",
39
+ strokeWidth: 2,
40
+ activeFill: "rgba(255,255,255,0.34)",
41
+ activeStroke: "rgba(255,255,255,0.7)",
42
+ text: "rgba(255,255,255,0.92)",
43
+ wrapper: "drop-shadow-[0_1px_3px_rgba(0,0,0,0.35)]",
44
+ activeWrapper: "drop-shadow-[0_1px_5px_rgba(0,0,0,0.4)]",
45
+ joyBase: { border: "1px solid rgba(255,255,255,0.2)", background: "rgba(255,255,255,0.1)", backdropFilter: "blur(4px)" },
46
+ joyThumb: { border: "1px solid rgba(255,255,255,0.3)", background: "rgba(255,255,255,0.25)" },
47
+ chipRest: { border: "1px solid rgba(255,255,255,0.2)", background: "rgba(0,0,0,0.4)", color: "rgba(255,255,255,0.8)", backdropFilter: "blur(4px)" },
48
+ chipActive: { border: "1px solid rgba(255,255,255,0.5)", background: "rgba(255,255,255,0.25)", color: "#fff" },
49
+ },
50
+ arcade: {
51
+ restFill: "rgba(9,12,20,0.82)",
52
+ restStroke: "rgba(120,224,255,0.75)",
53
+ strokeWidth: 3,
54
+ activeFill: "rgba(120,224,255,0.9)",
55
+ activeStroke: "rgba(240,255,255,0.95)",
56
+ text: "rgba(190,244,255,0.95)",
57
+ wrapper: "drop-shadow-[0_0_9px_rgba(80,200,255,0.5)]",
58
+ activeWrapper: "drop-shadow-[0_0_16px_rgba(120,224,255,0.85)]",
59
+ joyBase: { border: "3px solid rgba(120,224,255,0.7)", background: "rgba(9,12,20,0.8)", boxShadow: "0 0 16px rgba(80,200,255,0.4)" },
60
+ joyThumb: { border: "2px solid rgba(240,255,255,0.9)", background: "rgba(120,224,255,0.85)" },
61
+ chipRest: { border: "2px solid rgba(120,224,255,0.6)", background: "rgba(9,12,20,0.85)", color: "rgba(190,244,255,0.95)", fontWeight: 700 },
62
+ chipActive: { border: "2px solid rgba(240,255,255,0.95)", background: "rgba(120,224,255,0.9)", color: "#04121a", fontWeight: 700 },
63
+ },
64
+ mechanical: {
65
+ restFill: "rgba(63,68,78,0.9)",
66
+ restStroke: "rgba(210,214,222,0.4)",
67
+ strokeWidth: 2.5,
68
+ activeFill: "rgba(120,126,138,0.95)",
69
+ activeStroke: "rgba(240,242,246,0.7)",
70
+ text: "rgba(232,234,240,0.95)",
71
+ wrapper: "drop-shadow-[0_2px_4px_rgba(0,0,0,0.55)]",
72
+ activeWrapper: "drop-shadow-[0_1px_2px_rgba(0,0,0,0.7)]",
73
+ joyBase: { border: "2px solid rgba(210,214,222,0.35)", background: "linear-gradient(180deg,rgba(78,84,94,0.92),rgba(44,48,56,0.92))", boxShadow: "0 3px 8px rgba(0,0,0,0.5)" },
74
+ joyThumb: { border: "2px solid rgba(240,242,246,0.5)", background: "linear-gradient(180deg,rgba(150,156,166,0.95),rgba(96,102,112,0.95))" },
75
+ chipRest: { border: "1px solid rgba(210,214,222,0.35)", background: "linear-gradient(180deg,rgba(72,78,88,0.92),rgba(40,44,52,0.92))", color: "rgba(232,234,240,0.95)" },
76
+ chipActive: { border: "1px solid rgba(240,242,246,0.6)", background: "rgba(120,126,138,0.95)", color: "#fff" },
77
+ },
78
+ minimal: {
79
+ restFill: "rgba(0,0,0,0.001)",
80
+ restStroke: "rgba(255,255,255,0.45)",
81
+ strokeWidth: 1.75,
82
+ activeFill: "rgba(255,255,255,0.14)",
83
+ activeStroke: "rgba(255,255,255,0.85)",
84
+ text: "rgba(255,255,255,0.85)",
85
+ wrapper: "",
86
+ activeWrapper: "",
87
+ joyBase: { border: "1.5px solid rgba(255,255,255,0.4)", background: "transparent" },
88
+ joyThumb: { border: "1.5px solid rgba(255,255,255,0.6)", background: "rgba(255,255,255,0.1)" },
89
+ chipRest: { border: "1px solid rgba(255,255,255,0.35)", background: "transparent", color: "rgba(255,255,255,0.8)" },
90
+ chipActive: { border: "1px solid rgba(255,255,255,0.8)", background: "rgba(255,255,255,0.14)", color: "#fff" },
91
+ },
92
+ };
24
93
  function polar(radius, deg, buttonSize) {
25
94
  const rad = (deg * Math.PI) / 180;
26
95
  return {
@@ -63,23 +132,38 @@ function primaryClusterExtent(count, scale = 1) {
63
132
  return (RING2_RADIUS + BUTTON_SIZE / 2) * scale;
64
133
  return Math.ceil(count / 4) * (BUTTON_SIZE + 12) * scale;
65
134
  }
135
+ const BOTTOM_ANCHORS = new Set(["bottom-left", "bottom-center", "bottom-right"]);
136
+ const ARC_ANCHORS = new Set(["bottom-left", "bottom-right"]);
66
137
  /**
67
- * Vertical space (px, excluding device safe areas) the dock occupies above
68
- * the bottom edge. The shell publishes it as `--jg-hud-dock-clearance` so
69
- * `HudCanvas` regions never collide with touch controls.
138
+ * Vertical space (px, excluding device safe areas) that *bottom-docked* clusters
139
+ * occupy above the bottom edge. The shell publishes it as
140
+ * `--jg-hud-dock-clearance` so `HudCanvas` regions never collide with touch
141
+ * controls. Side rails and top clusters reserve their own rectangles through the
142
+ * layout registry instead of this scalar.
70
143
  */
71
144
  export function touchDockClearance(scheme, scale = 1) {
72
145
  if (scheme === null)
73
146
  return 0;
147
+ const layout = scheme.layout;
74
148
  let primary = 0;
75
149
  let utility = 0;
76
150
  for (const button of scheme.buttons) {
151
+ if (button.anchor !== null)
152
+ continue;
77
153
  if (button.kind === "utility")
78
154
  utility += 1;
79
155
  else
80
156
  primary += 1;
81
157
  }
82
- const tallest = Math.max(scheme.joystick !== null ? JOYSTICK_SIZE * scale : 0, primaryClusterExtent(primary, scale), utility > 0 ? UTILITY_ROW_HEIGHT : 0);
158
+ const heights = [];
159
+ if (scheme.joystick !== null && BOTTOM_ANCHORS.has(layout.movement))
160
+ heights.push(JOYSTICK_SIZE * scale);
161
+ if (primary > 0 && BOTTOM_ANCHORS.has(layout.actions)) {
162
+ heights.push(ARC_ANCHORS.has(layout.actions) ? primaryClusterExtent(primary, scale) : RAIL_BUTTON_SIZE * scale + 12);
163
+ }
164
+ if (utility > 0 && BOTTOM_ANCHORS.has(layout.utility))
165
+ heights.push(UTILITY_ROW_HEIGHT);
166
+ const tallest = heights.length === 0 ? 0 : Math.max(...heights);
83
167
  if (tallest === 0)
84
168
  return 0;
85
169
  return tallest + DOCK_BASE_PADDING + 12;
@@ -158,7 +242,7 @@ function joystickDirections(joystick, nx, ny) {
158
242
  active.add(joystick.right);
159
243
  return active;
160
244
  }
161
- function VirtualJoystick({ joystick, sink, scale = 1 }) {
245
+ function VirtualJoystick({ joystick, sink, tokens, scale = 1, }) {
162
246
  const baseRef = useRef(null);
163
247
  const thumbRef = useRef(null);
164
248
  const pointerIdRef = useRef(null);
@@ -200,7 +284,7 @@ function VirtualJoystick({ joystick, sink, scale = 1 }) {
200
284
  if (thumbRef.current !== null)
201
285
  thumbRef.current.style.transform = "translate(0px, 0px)";
202
286
  };
203
- return (_jsx("div", { ref: baseRef, className: "pointer-events-auto relative flex touch-none select-none items-center justify-center rounded-full border border-white/20 bg-white/10 backdrop-blur-sm", style: { width: size, height: size }, onPointerDown: (event) => {
287
+ return (_jsx("div", { ref: baseRef, className: "pointer-events-auto relative flex touch-none select-none items-center justify-center rounded-full", style: { width: size, height: size, ...tokens.joyBase }, onPointerDown: (event) => {
204
288
  if (pointerIdRef.current !== null)
205
289
  return;
206
290
  pointerIdRef.current = event.pointerId;
@@ -216,7 +300,7 @@ function VirtualJoystick({ joystick, sink, scale = 1 }) {
216
300
  return;
217
301
  event.stopPropagation();
218
302
  releaseAll();
219
- }, onPointerCancel: () => releaseAll(), children: _jsx("div", { ref: thumbRef, className: "rounded-full border border-white/30 bg-white/25", style: { width: thumbSize, height: thumbSize } }) }));
303
+ }, onPointerCancel: () => releaseAll(), children: _jsx("div", { ref: thumbRef, className: "rounded-full", style: { width: thumbSize, height: thumbSize, ...tokens.joyThumb } }) }));
220
304
  }
221
305
  function touchButtonIcon(button) {
222
306
  if (button.icon === false)
@@ -225,65 +309,160 @@ function touchButtonIcon(button) {
225
309
  return button.icon;
226
310
  return iconForAction(button.action);
227
311
  }
228
- function TouchActionButton({ button, sink, scale = 1 }) {
312
+ function ShapeSilhouette({ shape, width, height, fill, stroke, strokeWidth, }) {
313
+ const common = { fill, stroke, strokeWidth, strokeLinejoin: "round", strokeLinecap: "round" };
314
+ const body = (() => {
315
+ switch (shape) {
316
+ case "circle":
317
+ return _jsx("circle", { cx: 50, cy: 50, r: 46, ...common });
318
+ case "square":
319
+ return _jsx("rect", { x: 7, y: 7, width: 86, height: 86, rx: 20, ...common });
320
+ case "wheel":
321
+ return (_jsxs("g", { ...common, children: [_jsx("circle", { cx: 50, cy: 50, r: 44, fill: fill }), _jsx("circle", { cx: 50, cy: 50, r: 16, fill: "none" }), _jsx("path", { d: "M50 34 V18 M36 58 L22 72 M64 58 L78 72", fill: "none" })] }));
322
+ case "pedal":
323
+ return (_jsxs("g", { ...common, children: [_jsx("path", { d: "M30 6 h40 a10 10 0 0 1 10 10 v58 a16 16 0 0 1 -16 16 h-38 a16 16 0 0 1 -16 -16 v-58 a10 10 0 0 1 10 -10 Z" }), _jsx("path", { d: "M26 26 h48 M26 42 h48 M26 58 h48", fill: "none", strokeWidth: strokeWidth * 0.8 })] }));
324
+ case "lever":
325
+ return (_jsxs("g", { ...common, children: [_jsx("circle", { cx: 50, cy: 22, r: 18 }), _jsx("path", { d: "M42 36 h16 v52 a8 8 0 0 1 -16 0 Z" })] }));
326
+ case "trigger":
327
+ return _jsx("path", { d: "M18 20 h44 a24 24 0 0 1 24 24 v10 a30 30 0 0 1 -30 30 h-24 a14 14 0 0 1 -14 -14 v-40 a10 10 0 0 1 0 -10 Z", ...common });
328
+ case "tab":
329
+ return _jsx("path", { d: "M6 18 a12 12 0 0 1 12 -12 h124 a12 12 0 0 1 12 12 v20 a12 12 0 0 1 -12 12 h-124 a12 12 0 0 1 -12 -12 Z", ...common });
330
+ }
331
+ })();
332
+ const viewBox = shape === "tab" ? "0 0 160 56" : "0 0 100 100";
333
+ return (_jsx("svg", { width: width, height: height, viewBox: viewBox, className: "pointer-events-none absolute inset-0", children: body }));
334
+ }
335
+ function TouchActionButton({ button, sink, tokens, size, fit, }) {
229
336
  const pointerIdRef = useRef(null);
337
+ const [pressed, setPressed] = useState(false);
230
338
  const icon = touchButtonIcon(button);
231
- const size = BUTTON_SIZE * scale;
339
+ const aspect = SHAPE_ASPECT[button.shape];
340
+ const svgW = size * aspect.w;
341
+ const svgH = size * aspect.h;
342
+ const boxW = fit === "box" ? size : svgW;
343
+ const boxH = fit === "box" ? size : svgH;
344
+ const iconSize = Math.round(Math.min(svgW, svgH) * 0.46);
232
345
  const releaseIfHeld = () => {
233
346
  if (pointerIdRef.current === null)
234
347
  return;
235
348
  pointerIdRef.current = null;
349
+ setPressed(false);
236
350
  sink.onCodeUp(touchCode(button.action));
237
351
  };
238
- return (_jsx("button", { type: "button", "aria-label": button.label, style: { width: size, height: size }, className: "pointer-events-auto flex touch-none select-none items-center justify-center rounded-full border border-white/25 bg-white/10 text-center text-[10px] font-semibold uppercase leading-tight tracking-wide text-white/90 backdrop-blur-sm active:border-white/50 active:bg-white/30", onPointerDown: (event) => {
352
+ return (_jsx("button", { type: "button", "aria-label": button.label, "data-touch-shape": button.shape, style: { width: boxW, height: boxH, transform: pressed ? "scale(0.93)" : "scale(1)", transition: "transform 90ms" }, className: `pointer-events-auto relative flex touch-none select-none items-center justify-center ${pressed ? tokens.activeWrapper : tokens.wrapper}`, onPointerDown: (event) => {
239
353
  if (pointerIdRef.current !== null)
240
354
  return;
241
355
  pointerIdRef.current = event.pointerId;
242
356
  event.currentTarget.setPointerCapture(event.pointerId);
357
+ setPressed(true);
243
358
  sink.onCodeDown(touchCode(button.action));
244
359
  }, onPointerUp: (event) => {
245
360
  event.stopPropagation();
246
361
  releaseIfHeld();
247
- }, onPointerCancel: () => releaseIfHeld(), onContextMenu: (event) => event.preventDefault(), children: icon !== null ? _jsx(GameIcon, { name: icon, size: Math.round(26 * scale) }) : _jsx("span", { className: "px-1", children: button.label }) }));
362
+ }, onPointerCancel: () => releaseIfHeld(), onContextMenu: (event) => event.preventDefault(), children: _jsxs("div", { className: "relative", style: { width: svgW, height: svgH }, children: [button.image !== null ? (_jsx("img", { src: button.image, alt: "", draggable: false, className: "pointer-events-none absolute inset-0 h-full w-full select-none object-contain", style: { filter: pressed ? "brightness(1.25)" : undefined } })) : (_jsx(ShapeSilhouette, { shape: button.shape, width: svgW, height: svgH, fill: pressed ? tokens.activeFill : tokens.restFill, stroke: pressed ? tokens.activeStroke : tokens.restStroke, strokeWidth: tokens.strokeWidth })), _jsx("span", { className: "absolute inset-0 flex items-center justify-center text-center text-[10px] font-semibold uppercase leading-tight tracking-wide", style: { color: tokens.text }, children: icon !== null ? _jsx(GameIcon, { name: icon, size: iconSize }) : _jsx("span", { className: "px-1", children: button.label }) })] }) }));
248
363
  }
249
- function TouchUtilityChip({ button, sink }) {
364
+ function TouchUtilityChip({ button, sink, tokens }) {
250
365
  const pointerIdRef = useRef(null);
366
+ const [pressed, setPressed] = useState(false);
251
367
  const releaseIfHeld = () => {
252
368
  if (pointerIdRef.current === null)
253
369
  return;
254
370
  pointerIdRef.current = null;
371
+ setPressed(false);
255
372
  sink.onCodeUp(touchCode(button.action));
256
373
  };
257
- return (_jsx("button", { type: "button", "aria-label": button.label, className: "pointer-events-auto flex h-7 touch-none select-none items-center rounded-full border border-white/20 bg-black/40 px-3 text-[10px] font-semibold uppercase tracking-wide text-white/80 backdrop-blur-sm active:border-white/50 active:bg-white/25", onPointerDown: (event) => {
374
+ return (_jsx("button", { type: "button", "aria-label": button.label, style: pressed ? tokens.chipActive : tokens.chipRest, className: "pointer-events-auto flex h-7 touch-none select-none items-center rounded-full px-3 text-[10px] font-semibold uppercase tracking-wide", onPointerDown: (event) => {
258
375
  if (pointerIdRef.current !== null)
259
376
  return;
260
377
  pointerIdRef.current = event.pointerId;
261
378
  event.currentTarget.setPointerCapture(event.pointerId);
379
+ setPressed(true);
262
380
  sink.onCodeDown(touchCode(button.action));
263
381
  }, onPointerUp: (event) => {
264
382
  event.stopPropagation();
265
383
  releaseIfHeld();
266
384
  }, onPointerCancel: () => releaseIfHeld(), onContextMenu: (event) => event.preventDefault(), children: button.label }));
267
385
  }
268
- function PrimaryButtonCluster({ buttons, sink, scale = 1 }) {
386
+ function ThumbArcCluster({ buttons, sink, tokens, scale = 1, }) {
269
387
  const offsets = primaryButtonOffsets(buttons.length, scale);
270
388
  if (offsets === null) {
271
- return (_jsx("div", { className: "flex max-w-[60%] flex-wrap items-end justify-end gap-3", children: buttons.map((button) => (_jsx(TouchActionButton, { button: button, sink: sink, scale: scale }, button.action))) }));
389
+ return (_jsx("div", { className: "flex max-w-[60%] flex-wrap items-end justify-end gap-3", children: buttons.map((button) => (_jsx(TouchActionButton, { button: button, sink: sink, tokens: tokens, size: BUTTON_SIZE * scale, fit: "box" }, button.action))) }));
272
390
  }
273
391
  const extent = primaryClusterExtent(buttons.length, scale);
274
392
  return (_jsx("div", { className: "pointer-events-none relative", style: { width: extent, height: extent }, children: buttons.map((button, index) => {
275
393
  const offset = offsets[index];
276
- return (_jsx("div", { className: "absolute", style: { right: offset?.right ?? 0, bottom: offset?.bottom ?? 0 }, children: _jsx(TouchActionButton, { button: button, sink: sink, scale: scale }) }, button.action));
394
+ return (_jsx("div", { className: "absolute", style: { right: offset?.right ?? 0, bottom: offset?.bottom ?? 0 }, children: _jsx(TouchActionButton, { button: button, sink: sink, tokens: tokens, size: BUTTON_SIZE * scale, fit: "box" }) }, button.action));
277
395
  }) }));
278
396
  }
279
- export function TouchControlsDock({ scheme, sink, scale = 1 }) {
280
- const primary = scheme.buttons.filter((button) => button.kind !== "utility");
281
- const utility = scheme.buttons.filter((button) => button.kind === "utility");
397
+ /** Vertical rail (`left`/`right`) or a wrapping row for the remaining anchors. */
398
+ function StackCluster({ buttons, utility, sink, tokens, column, scale = 1, }) {
399
+ return (_jsxs("div", { className: `flex ${column ? "flex-col" : "flex-row flex-wrap"} items-center justify-center gap-3`, children: [buttons.map((button) => (_jsx(TouchActionButton, { button: button, sink: sink, tokens: tokens, size: RAIL_BUTTON_SIZE * scale, fit: "natural" }, button.action))), utility.map((button) => (_jsx(TouchUtilityChip, { button: button, sink: sink, tokens: tokens }, button.action)))] }));
400
+ }
401
+ const ANCHOR_POSITION = {
402
+ "bottom-left": { bottom: 0, left: 0 },
403
+ "bottom-center": { bottom: 0, left: "50%", transform: "translateX(-50%)" },
404
+ "bottom-right": { bottom: 0, right: 0 },
405
+ left: { top: "50%", left: 0, transform: "translateY(-50%)" },
406
+ right: { top: "50%", right: 0, transform: "translateY(-50%)" },
407
+ "top-left": { top: 0, left: 0 },
408
+ "top-center": { top: 0, left: "50%", transform: "translateX(-50%)" },
409
+ "top-right": { top: 0, right: 0 },
410
+ };
411
+ function anchorInset(anchor) {
412
+ const safeBottom = "env(safe-area-inset-bottom, 0px)";
413
+ const safeTop = "env(safe-area-inset-top, 0px)";
414
+ const safeLeft = "env(safe-area-inset-left, 0px)";
415
+ const safeRight = "env(safe-area-inset-right, 0px)";
416
+ const pad = `${DOCK_BASE_PADDING}px`;
417
+ const style = {};
418
+ if (anchor.startsWith("bottom"))
419
+ style.paddingBottom = `calc(${safeBottom} + ${pad})`;
420
+ if (anchor.startsWith("top"))
421
+ style.paddingTop = `calc(${safeTop} + ${pad})`;
422
+ if (anchor === "left" || anchor.endsWith("left"))
423
+ style.paddingLeft = `calc(${safeLeft} + ${pad})`;
424
+ if (anchor === "right" || anchor.endsWith("right"))
425
+ style.paddingRight = `calc(${safeRight} + ${pad})`;
426
+ if (anchor === "bottom-center" || anchor === "top-center") {
427
+ style.paddingLeft = `calc(${safeLeft} + ${pad})`;
428
+ style.paddingRight = `calc(${safeRight} + ${pad})`;
429
+ }
430
+ return style;
431
+ }
432
+ function AnchoredSlot({ anchor, children, slotRef, }) {
433
+ return (_jsx("div", { className: "pointer-events-none absolute z-40", style: { ...ANCHOR_POSITION[anchor], ...anchorInset(anchor) }, children: _jsx("div", { ref: slotRef, children: children }) }));
434
+ }
435
+ function bucketByAnchor(buttons) {
436
+ const map = new Map();
437
+ for (const button of buttons) {
438
+ if (button.anchor === null)
439
+ continue;
440
+ const bucket = map.get(button.anchor) ?? { primary: [], utility: [] };
441
+ if (button.kind === "utility")
442
+ bucket.utility.push(button);
443
+ else
444
+ bucket.primary.push(button);
445
+ map.set(button.anchor, bucket);
446
+ }
447
+ return map;
448
+ }
449
+ export function TouchControlsDock({ scheme, sink, style, scale = 1, }) {
450
+ const tokens = STYLE_TOKENS[style ?? scheme.style];
451
+ const layout = scheme.layout;
452
+ const docked = scheme.buttons.filter((button) => button.anchor === null);
453
+ const primary = docked.filter((button) => button.kind !== "utility");
454
+ const utility = docked.filter((button) => button.kind === "utility");
455
+ const extras = useMemo(() => bucketByAnchor(scheme.buttons), [scheme.buttons]);
282
456
  const joystickRef = useRef(null);
283
457
  const actionsRef = useRef(null);
284
458
  const utilityRef = useRef(null);
285
459
  useRegisterLayoutRegion(MOVEMENT_ZONE, joystickRef, scheme.joystick !== null);
286
460
  useRegisterLayoutRegion(ACTIONS_ZONE, actionsRef, primary.length > 0);
287
461
  useRegisterLayoutRegion(UTILITY_ZONE, utilityRef, utility.length > 0);
288
- return (_jsxs("div", { className: "pointer-events-none absolute inset-x-0 bottom-0 z-40", style: { paddingBottom: `calc(env(safe-area-inset-bottom, 0px) + ${DOCK_BASE_PADDING}px)` }, children: [_jsxs("div", { className: "flex items-end justify-between gap-4 px-5", children: [_jsx("div", { ref: joystickRef, children: scheme.joystick !== null ? _jsx(VirtualJoystick, { joystick: scheme.joystick, sink: sink, scale: scale }) : null }), _jsx("div", { ref: actionsRef, children: _jsx(PrimaryButtonCluster, { buttons: primary, sink: sink, scale: scale }) })] }), utility.length > 0 ? (_jsx("div", { className: "pointer-events-none absolute inset-x-0 flex justify-center", style: { bottom: "calc(env(safe-area-inset-bottom, 0px) + 8px)" }, children: _jsx("div", { ref: utilityRef, className: "flex gap-2", children: utility.map((button) => (_jsx(TouchUtilityChip, { button: button, sink: sink }, button.action))) }) })) : null] }));
462
+ const actionsColumn = layout.actions === "left" || layout.actions === "right";
463
+ const utilityColumn = layout.utility === "left" || layout.utility === "right";
464
+ return (_jsxs(ResolvedLayout, { children: [scheme.joystick !== null ? (_jsx(AnchoredSlot, { anchor: layout.movement, slotRef: joystickRef, children: _jsx(VirtualJoystick, { joystick: scheme.joystick, sink: sink, tokens: tokens, scale: scale }) })) : null, primary.length > 0 ? (_jsx(AnchoredSlot, { anchor: layout.actions, slotRef: actionsRef, children: ARC_ANCHORS.has(layout.actions) ? (_jsx(ThumbArcCluster, { buttons: primary, sink: sink, tokens: tokens, scale: scale })) : (_jsx(StackCluster, { buttons: primary, utility: [], sink: sink, tokens: tokens, column: actionsColumn, scale: scale })) })) : null, utility.length > 0 ? (_jsx(AnchoredSlot, { anchor: layout.utility, slotRef: utilityRef, children: _jsx(StackCluster, { buttons: [], utility: utility, sink: sink, tokens: tokens, column: utilityColumn, scale: scale }) })) : null, [...extras.entries()].map(([anchor, bucket]) => (_jsx(AnchoredSlot, { anchor: anchor, children: _jsx(StackCluster, { buttons: bucket.primary, utility: bucket.utility, sink: sink, tokens: tokens, column: anchor === "left" || anchor === "right", scale: scale }) }, anchor)))] }));
465
+ }
466
+ function ResolvedLayout({ children }) {
467
+ return _jsx("div", { className: "pointer-events-none absolute inset-0 z-40", children: children });
289
468
  }
@@ -0,0 +1,9 @@
1
+ import { type Dispatch, type SetStateAction } from "react";
2
+ import type { GameContext } from "@jgengine/core/runtime/gameContext";
3
+ import type { PresencePoseRow } from "@jgengine/core/runtime/transport";
4
+ import type { ShellMultiplayer } from "./multiplayer.js";
5
+ import type { PlayableGame } from "./registry.js";
6
+ /** Joins the multiplayer server for the live context and wires presence, feed relay, and chat sync until teardown. */
7
+ export declare function useShellMultiplayerSync(ctx: GameContext | null, multiplayer: ShellMultiplayer | null, playable: PlayableGame, serverIdRef: {
8
+ current: string | null;
9
+ }, setRemotePlayers: Dispatch<SetStateAction<PresencePoseRow[]>>): void;
@@ -0,0 +1,98 @@
1
+ import { useEffect } from "react";
2
+ import { isServerAuthoritative } from "@jgengine/core/runtime/adapter";
3
+ import { attachWorldSync } from "./worldSync.js";
4
+ /** Joins the multiplayer server for the live context and wires presence, feed relay, and chat sync until teardown. */
5
+ export function useShellMultiplayerSync(ctx, multiplayer, playable, serverIdRef, setRemotePlayers) {
6
+ useEffect(() => {
7
+ if (ctx === null || multiplayer === null)
8
+ return;
9
+ let disposed = false;
10
+ const cleanups = [];
11
+ void multiplayer.backend.transport
12
+ .joinServer({ gameId: multiplayer.gameId })
13
+ .then((joined) => {
14
+ if (disposed) {
15
+ void multiplayer.backend.transport.leaveServer({ serverId: joined.serverId });
16
+ return;
17
+ }
18
+ serverIdRef.current = joined.serverId;
19
+ if (isServerAuthoritative(playable.game.multiplayer) && multiplayer.backend.feeds !== undefined) {
20
+ cleanups.push(attachWorldSync(multiplayer.backend.feeds, joined.serverId, ctx));
21
+ }
22
+ cleanups.push(multiplayer.backend.presenceSync.subscribe(joined.serverId, (rows) => {
23
+ setRemotePlayers(rows.filter((row) => row.userId !== multiplayer.userId));
24
+ }));
25
+ const seen = new Set();
26
+ let injecting = false;
27
+ for (const action of multiplayer.feedActions) {
28
+ cleanups.push(ctx.game.feed.subscribe(action, (entry) => {
29
+ if (injecting)
30
+ return;
31
+ void multiplayer.backend
32
+ .pushFeedEntry({
33
+ serverId: joined.serverId,
34
+ action,
35
+ entry: { at: entry.at, data: entry.data, from: multiplayer.userId },
36
+ })
37
+ .catch(() => undefined);
38
+ }));
39
+ const remoteUnsub = multiplayer.backend.feeds?.subscribeFeed({ serverId: joined.serverId, action }, (view) => {
40
+ for (const raw of view.entries) {
41
+ const remote = raw;
42
+ if (typeof remote.from !== "string" || remote.from === multiplayer.userId)
43
+ continue;
44
+ const key = `${action}|${remote.from}|${remote.at ?? 0}`;
45
+ if (seen.has(key))
46
+ continue;
47
+ seen.add(key);
48
+ injecting = true;
49
+ ctx.game.feed.push(action, remote.data);
50
+ injecting = false;
51
+ }
52
+ });
53
+ if (remoteUnsub !== undefined)
54
+ cleanups.push(remoteUnsub);
55
+ }
56
+ const chatSync = multiplayer.backend.chatSyncFor?.(joined.serverId);
57
+ if (chatSync !== undefined && ctx.game.chat !== undefined) {
58
+ const chat = ctx.game.chat;
59
+ const globalChannelIds = new Set(chat
60
+ .channels()
61
+ .filter((channel) => channel.kind === "global")
62
+ .map((channel) => channel.id));
63
+ const seenRemoteChat = new Set();
64
+ cleanups.push(ctx.game.events.subscribe("chat.message", (event) => {
65
+ if (event.fromUserId !== multiplayer.userId)
66
+ return;
67
+ if (!globalChannelIds.has(event.channelId))
68
+ return;
69
+ void chatSync.send(event.channelId, event.body).catch(() => undefined);
70
+ }));
71
+ for (const channelId of globalChannelIds) {
72
+ cleanups.push(chatSync.subscribe(channelId, (messages) => {
73
+ for (const message of messages) {
74
+ if (message.fromUserId === multiplayer.userId)
75
+ continue;
76
+ if (seenRemoteChat.has(message.id))
77
+ continue;
78
+ seenRemoteChat.add(message.id);
79
+ chat.send(message.fromUserId, message.channelId, message.body);
80
+ }
81
+ }));
82
+ }
83
+ }
84
+ })
85
+ .catch(() => undefined);
86
+ return () => {
87
+ disposed = true;
88
+ for (const cleanup of cleanups)
89
+ cleanup();
90
+ const serverId = serverIdRef.current;
91
+ serverIdRef.current = null;
92
+ setRemotePlayers([]);
93
+ if (serverId !== null) {
94
+ void multiplayer.backend.transport.leaveServer({ serverId }).catch(() => undefined);
95
+ }
96
+ };
97
+ }, [ctx, multiplayer, playable]);
98
+ }
@@ -4,7 +4,7 @@ import { useThree, useFrame } from "@react-three/fiber";
4
4
  import * as THREE from "three";
5
5
  import { useGameContext } from "@jgengine/react/provider";
6
6
  import { createVisibilitySystem } from "@jgengine/core/visibility/visibilitySystem";
7
- import { CAMERA_POST_FRAME_PRIORITY } from "../camera/index.js";
7
+ import { CAMERA_POST_FRAME_PRIORITY } from "../camera/cameraRigs.js";
8
8
  const ALWAYS_VISIBLE = () => true;
9
9
  const alwaysVisibleRef = { current: ALWAYS_VISIBLE };
10
10
  const CullingContext = createContext(null);
@@ -96,7 +96,9 @@ export declare const OCEAN_QUALITY_PRESETS: Record<OceanQualityPreset, {
96
96
  resolution: number;
97
97
  }>;
98
98
  export declare const DEFAULT_OCEAN_CONFIG: ResolvedOceanConfig;
99
+ /** @internal */
99
100
  export declare function createOceanConfig(patch?: OceanConfig): ResolvedOceanConfig;
101
+ /** @internal */
100
102
  export declare function buildOceanWaveUniforms(config: ResolvedOceanConfig): {
101
103
  directions: THREE.Vector2[];
102
104
  params: THREE.Vector4[];
@@ -87,6 +87,7 @@ function resolveWave(wave, fallback, config, customWaves) {
87
87
  steepness: clamp(finiteOr(wave?.steepness, fallback.steepness) * config.steepness, 0, 1.2),
88
88
  };
89
89
  }
90
+ /** @internal */
90
91
  export function createOceanConfig(patch = {}) {
91
92
  const quality = patch.quality ?? DEFAULT_OCEAN_CONFIG.quality;
92
93
  const preset = OCEAN_QUALITY_PRESETS[quality];
@@ -126,6 +127,7 @@ export function createOceanConfig(patch = {}) {
126
127
  const waves = Array.from({ length: MAX_OCEAN_WAVES }, (_, index) => resolveWave(inputWaves[index], defaultWaves[index], withoutWaves, customWaves));
127
128
  return { ...withoutWaves, waves };
128
129
  }
130
+ /** @internal */
129
131
  export function buildOceanWaveUniforms(config) {
130
132
  const directions = [];
131
133
  const params = [];
@@ -1,2 +1,3 @@
1
1
  import type { Euler, Quaternion } from "three";
2
+ /** @internal */
2
3
  export declare function setBillboardQuaternion(quaternion: Quaternion, euler: Euler, yaw: number): void;
@@ -1,3 +1,4 @@
1
+ /** @internal */
1
2
  export function setBillboardQuaternion(quaternion, euler, yaw) {
2
3
  euler.set(0, yaw, 0);
3
4
  quaternion.setFromEuler(euler);
@@ -1,2 +1,3 @@
1
1
  import * as THREE from "three";
2
+ /** @internal */
2
3
  export declare function createWeatherQuadGeometry(maxCount: number, seed: number): THREE.InstancedBufferGeometry;
@@ -1,5 +1,6 @@
1
1
  import * as THREE from "three";
2
2
  import { createWeatherSeedAttributes } from "./weatherMath.js";
3
+ /** @internal */
3
4
  export function createWeatherQuadGeometry(maxCount, seed) {
4
5
  const count = Math.max(0, Math.floor(maxCount));
5
6
  const geometry = new THREE.InstancedBufferGeometry();
@@ -2,10 +2,13 @@ export declare const DEFAULT_RAIN_COUNT = 2000;
2
2
  export declare const DEFAULT_SNOW_COUNT = 1500;
3
3
  export declare const DEFAULT_RAIN_DENSITY = 0.45;
4
4
  export declare const DEFAULT_SNOW_DENSITY = 0.5;
5
+ /** @internal */
5
6
  export declare function clampWeatherRatio(value: number): number;
7
+ /** @internal */
6
8
  export declare function resolveWeatherInstanceCount(maxCount: number, density: number, budget?: number): number;
7
9
  export interface WeatherSeedAttributes {
8
10
  spawn: Float32Array;
9
11
  drift: Float32Array;
10
12
  }
13
+ /** @internal */
11
14
  export declare function createWeatherSeedAttributes(maxCount: number, seed: number): WeatherSeedAttributes;
@@ -2,6 +2,7 @@ export const DEFAULT_RAIN_COUNT = 2000;
2
2
  export const DEFAULT_SNOW_COUNT = 1500;
3
3
  export const DEFAULT_RAIN_DENSITY = 0.45;
4
4
  export const DEFAULT_SNOW_DENSITY = 0.5;
5
+ /** @internal */
5
6
  export function clampWeatherRatio(value) {
6
7
  if (!Number.isFinite(value))
7
8
  return 0;
@@ -11,11 +12,13 @@ export function clampWeatherRatio(value) {
11
12
  return 1;
12
13
  return value;
13
14
  }
15
+ /** @internal */
14
16
  export function resolveWeatherInstanceCount(maxCount, density, budget) {
15
17
  const safeMax = Math.max(0, Math.floor(maxCount));
16
18
  const capped = budget === undefined ? safeMax : Math.min(safeMax, Math.max(0, Math.floor(budget)));
17
19
  return Math.floor(capped * clampWeatherRatio(density));
18
20
  }
21
+ /** @internal */
19
22
  export function createWeatherSeedAttributes(maxCount, seed) {
20
23
  const count = Math.max(0, Math.floor(maxCount));
21
24
  const spawn = new Float32Array(count * 3);
@@ -11,8 +11,11 @@ export interface WeatherUniformOptions {
11
11
  lightning?: number;
12
12
  timeScale?: number;
13
13
  }
14
+ /** @internal */
14
15
  export declare function createWeatherUniformSet(options?: WeatherUniformOptions): WeatherUniformSet;
16
+ /** @internal */
15
17
  export declare function WeatherUniformProvider({ children, ...options }: WeatherUniformOptions & {
16
18
  children: ReactNode;
17
19
  }): import("react").JSX.Element;
20
+ /** @internal */
18
21
  export declare function useWeatherUniformSet(options?: WeatherUniformOptions): WeatherUniformSet;
@@ -3,6 +3,7 @@ import { createContext, useContext, useMemo } from "react";
3
3
  import { useFrame } from "@react-three/fiber";
4
4
  import * as THREE from "three";
5
5
  const WeatherUniformContext = createContext(null);
6
+ /** @internal */
6
7
  export function createWeatherUniformSet(options = {}) {
7
8
  const wind = options.wind ?? [0, 0, 0];
8
9
  return {
@@ -21,11 +22,13 @@ function useUniformTicker(uniforms, options, enabled = true) {
21
22
  uniforms.lightning.value = options.lightning ?? 0;
22
23
  });
23
24
  }
25
+ /** @internal */
24
26
  export function WeatherUniformProvider({ children, ...options }) {
25
27
  const uniforms = useMemo(() => createWeatherUniformSet(options), []);
26
28
  useUniformTicker(uniforms, options);
27
29
  return _jsx(WeatherUniformContext.Provider, { value: uniforms, children: children });
28
30
  }
31
+ /** @internal */
29
32
  export function useWeatherUniformSet(options = {}) {
30
33
  const shared = useContext(WeatherUniformContext);
31
34
  const local = useMemo(() => createWeatherUniformSet(options), []);
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Renders retained VFX instances (`combat.vfxInstance` ops from {@link GameContext}'s `scene.entity.vfxInstance`
3
+ * store) as long-lived beams whose endpoints follow live entity poses. Auto-mounted by the player shell alongside
4
+ * the one-shot {@link WorldSpellVfx} overlay. `upsert`/`update` mutate render resources imperatively (no per-frame
5
+ * React allocation); only create/stop change the mounted set, and `stop` fades out over its requested duration.
6
+ * Beam is the first shipped retained renderer; other kinds are ignored here for a future renderer to claim.
7
+ *
8
+ * @internal
9
+ */
10
+ export declare function RetainedVfx(): import("react").JSX.Element;