@jgengine/shell 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/CHANGELOG.md +34 -3
  2. package/LICENSE +203 -661
  3. package/README.md +1 -1
  4. package/dist/GamePhaseStamp.d.ts +1 -0
  5. package/dist/GamePhaseStamp.js +9 -0
  6. package/dist/GamePlayerShell.d.ts +4 -31
  7. package/dist/GamePlayerShell.js +162 -932
  8. package/dist/GameUiPreview.js +0 -2
  9. package/dist/audio/audioEngine.d.ts +8 -0
  10. package/dist/audio/audioEngine.js +63 -8
  11. package/dist/audio/audioWire.d.ts +22 -0
  12. package/dist/audio/audioWire.js +19 -0
  13. package/dist/audio/musicDirector.d.ts +31 -0
  14. package/dist/audio/musicDirector.js +125 -0
  15. package/dist/audio/musicVoices.d.ts +10 -0
  16. package/dist/audio/musicVoices.js +338 -0
  17. package/dist/audio/synthEngine.d.ts +9 -0
  18. package/dist/audio/synthEngine.js +57 -0
  19. package/dist/behaviour.d.ts +2 -1
  20. package/dist/behaviour.js +2 -1
  21. package/dist/behaviourAttach.d.ts +1 -0
  22. package/dist/behaviourAttach.js +1 -0
  23. package/dist/behaviourDriver.d.ts +1 -0
  24. package/dist/behaviourDriver.js +1 -0
  25. package/dist/boundActionDispatch.d.ts +13 -0
  26. package/dist/boundActionDispatch.js +37 -0
  27. package/dist/camera/GameCameraRig.d.ts +5 -2
  28. package/dist/camera/GameCameraRig.js +2 -2
  29. package/dist/camera/GameFirstPersonCamera.d.ts +12 -2
  30. package/dist/camera/GameFirstPersonCamera.js +29 -7
  31. package/dist/camera/GameInspectionCamera.d.ts +1 -1
  32. package/dist/camera/GameInspectionCamera.js +13 -2
  33. package/dist/camera/GameOrbitCamera.js +59 -10
  34. package/dist/camera/cameraBlendMath.d.ts +3 -0
  35. package/dist/camera/cameraBlendMath.js +3 -0
  36. package/dist/camera/fovPreference.d.ts +6 -1
  37. package/dist/camera/fovPreference.js +6 -1
  38. package/dist/camera/index.d.ts +4 -7
  39. package/dist/camera/index.js +4 -7
  40. package/dist/camera/inspectionCameraMath.d.ts +7 -2
  41. package/dist/camera/inspectionCameraMath.js +7 -2
  42. package/dist/camera/orbitCameraMath.d.ts +54 -6
  43. package/dist/camera/orbitCameraMath.js +61 -6
  44. package/dist/camera/rigMath.d.ts +94 -24
  45. package/dist/camera/rigMath.js +100 -24
  46. package/dist/camera/rigResolve.d.ts +4 -2
  47. package/dist/camera/rigResolve.js +4 -2
  48. package/dist/camera/shakeChannel.d.ts +6 -2
  49. package/dist/camera/shakeChannel.js +6 -2
  50. package/dist/camera/shakeChannelMath.d.ts +1 -0
  51. package/dist/camera/shakeChannelMath.js +1 -0
  52. package/dist/cartridge/runtime.d.ts +15 -0
  53. package/dist/cartridge/runtime.js +556 -0
  54. package/dist/cartridge/spec.d.ts +188 -0
  55. package/dist/cartridge/spec.js +19 -0
  56. package/dist/cartridge/validate.d.ts +2 -0
  57. package/dist/cartridge/validate.js +78 -0
  58. package/dist/cartridge.d.ts +1 -1
  59. package/dist/cartridge.js +4 -3
  60. package/dist/commandSink.d.ts +25 -0
  61. package/dist/commandSink.js +32 -0
  62. package/dist/defineGame.js +26 -5
  63. package/dist/devtools/ColPanel.d.ts +1 -0
  64. package/dist/devtools/ColPanel.js +14 -0
  65. package/dist/devtools/CollisionDebugWorld.js +1 -0
  66. package/dist/devtools/DevtoolsOverlay.d.ts +7 -3
  67. package/dist/devtools/DevtoolsOverlay.js +19 -450
  68. package/dist/devtools/KeysPanel.d.ts +4 -0
  69. package/dist/devtools/KeysPanel.js +31 -0
  70. package/dist/devtools/LogsPanel.d.ts +1 -0
  71. package/dist/devtools/LogsPanel.js +12 -0
  72. package/dist/devtools/NetPanel.d.ts +4 -0
  73. package/dist/devtools/NetPanel.js +10 -0
  74. package/dist/devtools/PerfPanel.d.ts +4 -0
  75. package/dist/devtools/PerfPanel.js +53 -0
  76. package/dist/devtools/TunePanel.d.ts +3 -0
  77. package/dist/devtools/TunePanel.js +309 -0
  78. package/dist/devtools/agentBridge.d.ts +41 -0
  79. package/dist/devtools/agentBridge.js +262 -0
  80. package/dist/devtools/collisionDebug.d.ts +6 -0
  81. package/dist/devtools/collisionDebug.js +6 -0
  82. package/dist/devtools/collisionDebugMath.d.ts +9 -2
  83. package/dist/devtools/collisionDebugMath.js +25 -5
  84. package/dist/devtools/devtoolsOverrides.d.ts +6 -0
  85. package/dist/devtools/devtoolsOverrides.js +48 -0
  86. package/dist/devtools/panelAtoms.d.ts +9 -0
  87. package/dist/devtools/panelAtoms.js +10 -0
  88. package/dist/devtools/perfDiagnose.d.ts +2 -0
  89. package/dist/devtools/perfDiagnose.js +43 -0
  90. package/dist/diagnostics/RuntimeDiagnostics.d.ts +29 -0
  91. package/dist/diagnostics/RuntimeDiagnostics.js +69 -0
  92. package/dist/drivers/FrameDriver.d.ts +37 -0
  93. package/dist/drivers/FrameDriver.js +232 -0
  94. package/dist/drivers/HudOnlyDriver.d.ts +16 -0
  95. package/dist/drivers/HudOnlyDriver.js +60 -0
  96. package/dist/environment/Daylight.d.ts +29 -7
  97. package/dist/environment/Daylight.js +153 -13
  98. package/dist/environment/EnvironmentScene.js +42 -7
  99. package/dist/environment/RoadRibbons.d.ts +7 -0
  100. package/dist/environment/RoadRibbons.js +57 -0
  101. package/dist/environment/VolumetricClouds.d.ts +17 -0
  102. package/dist/environment/VolumetricClouds.js +202 -0
  103. package/dist/environment/daylightCycle.d.ts +3 -1
  104. package/dist/environment/daylightCycle.js +3 -1
  105. package/dist/environment/groundPadMath.d.ts +3 -0
  106. package/dist/environment/groundPadMath.js +3 -0
  107. package/dist/environment/index.d.ts +1 -0
  108. package/dist/environment/index.js +1 -0
  109. package/dist/environment/skyLightingPolicy.d.ts +2 -0
  110. package/dist/environment/skyLightingPolicy.js +2 -0
  111. package/dist/hotbarActions.d.ts +26 -0
  112. package/dist/hotbarActions.js +27 -0
  113. package/dist/inputSink.d.ts +30 -0
  114. package/dist/inputSink.js +85 -0
  115. package/dist/materialOverride.d.ts +10 -0
  116. package/dist/materialOverride.js +15 -3
  117. package/dist/multiplayer.js +14 -4
  118. package/dist/postfx/PostProcessing.d.ts +10 -0
  119. package/dist/postfx/PostProcessing.js +120 -0
  120. package/dist/postfx/gradeShader.d.ts +4 -0
  121. package/dist/postfx/gradeShader.js +72 -0
  122. package/dist/registry.d.ts +3 -2
  123. package/dist/render/EnvironmentLighting.d.ts +10 -0
  124. package/dist/render/EnvironmentLighting.js +34 -0
  125. package/dist/render/SceneLighting.d.ts +7 -0
  126. package/dist/render/SceneLighting.js +18 -0
  127. package/dist/render/SceneModels.d.ts +14 -0
  128. package/dist/render/SceneModels.js +337 -0
  129. package/dist/render/defaultSurface.d.ts +29 -0
  130. package/dist/render/defaultSurface.js +101 -0
  131. package/dist/render/modelLoad.d.ts +24 -0
  132. package/dist/render/modelLoad.js +73 -0
  133. package/dist/render/modelRender.d.ts +11 -0
  134. package/dist/render/modelRender.js +15 -0
  135. package/dist/render/resolveModel.d.ts +39 -3
  136. package/dist/render/resolveModel.js +79 -2
  137. package/dist/render/useEntityRenderCues.d.ts +13 -0
  138. package/dist/render/useEntityRenderCues.js +62 -0
  139. package/dist/scatter/InstancedScatter.d.ts +24 -0
  140. package/dist/scatter/InstancedScatter.js +130 -0
  141. package/dist/scatter/scatterModels.d.ts +25 -0
  142. package/dist/scatter/scatterModels.js +61 -0
  143. package/dist/scatter/scatterProxies.d.ts +6 -0
  144. package/dist/scatter/scatterProxies.js +94 -0
  145. package/dist/scatter.d.ts +1 -0
  146. package/dist/scatter.js +1 -0
  147. package/dist/scene/AuthoredScene.d.ts +82 -0
  148. package/dist/scene/AuthoredScene.js +173 -0
  149. package/dist/scene/GeneratedAssetRenderer.d.ts +14 -0
  150. package/dist/scene/GeneratedAssetRenderer.js +59 -0
  151. package/dist/scene/GrassFieldRenderer.d.ts +2 -0
  152. package/dist/scene/GrassFieldRenderer.js +25 -0
  153. package/dist/scene/SoilPatchRenderer.d.ts +2 -0
  154. package/dist/scene/SoilPatchRenderer.js +21 -0
  155. package/dist/scene/StudioStage.d.ts +37 -0
  156. package/dist/scene/StudioStage.js +42 -0
  157. package/dist/scene/WaterRenderer.d.ts +2 -0
  158. package/dist/scene/WaterRenderer.js +37 -0
  159. package/dist/scene/builtinSceneKindRenderers.d.ts +2 -0
  160. package/dist/scene/builtinSceneKindRenderers.js +22 -0
  161. package/dist/scene/sceneKindRenderers.d.ts +28 -0
  162. package/dist/scene/sceneKindRenderers.js +17 -0
  163. package/dist/scene.d.ts +1 -0
  164. package/dist/scene.js +1 -0
  165. package/dist/settings/appliedSettings.d.ts +5 -0
  166. package/dist/settings/appliedSettings.js +9 -0
  167. package/dist/settings/settingsController.d.ts +2 -0
  168. package/dist/settings/settingsController.js +22 -1
  169. package/dist/shellConstants.d.ts +4 -0
  170. package/dist/shellConstants.js +4 -0
  171. package/dist/shellMovement.d.ts +8 -0
  172. package/dist/shellMovement.js +11 -0
  173. package/dist/shellPointer.d.ts +20 -0
  174. package/dist/shellPointer.js +28 -0
  175. package/dist/structures/PlacementGhost.d.ts +4 -0
  176. package/dist/structures/PlacementGhost.js +4 -0
  177. package/dist/structures/TransformGizmo.d.ts +43 -0
  178. package/dist/structures/TransformGizmo.js +55 -0
  179. package/dist/structures/index.d.ts +1 -0
  180. package/dist/structures/index.js +1 -0
  181. package/dist/terrain/CarvedTerrain.d.ts +4 -1
  182. package/dist/terrain/CarvedTerrain.js +4 -3
  183. package/dist/terrain/GrassField.js +1 -1
  184. package/dist/terrain/TerraformBrushCursor.js +3 -0
  185. package/dist/terrain/grassBudget.d.ts +11 -2
  186. package/dist/terrain/grassBudget.js +20 -4
  187. package/dist/terrain/grassGeometry.d.ts +3 -0
  188. package/dist/terrain/grassGeometry.js +3 -0
  189. package/dist/terrain/grassMaterial.d.ts +2 -0
  190. package/dist/terrain/grassMaterial.js +2 -0
  191. package/dist/terrain/random.d.ts +3 -0
  192. package/dist/terrain/random.js +3 -0
  193. package/dist/terrain/soilPatchMaterial.d.ts +10 -0
  194. package/dist/terrain/soilPatchMaterial.js +109 -0
  195. package/dist/terrain/terrainDetailMaterial.d.ts +31 -0
  196. package/dist/terrain/terrainDetailMaterial.js +157 -0
  197. package/dist/terrain/terrainMath.d.ts +8 -1
  198. package/dist/terrain/terrainMath.js +8 -1
  199. package/dist/touch/TouchControlsOverlay.d.ts +9 -5
  200. package/dist/touch/TouchControlsOverlay.js +211 -22
  201. package/dist/useShellMultiplayerSync.d.ts +9 -0
  202. package/dist/useShellMultiplayerSync.js +98 -0
  203. package/dist/visibility/CullingProvider.js +1 -1
  204. package/dist/water/OceanConfig.d.ts +2 -0
  205. package/dist/water/OceanConfig.js +2 -0
  206. package/dist/weather/fireSpreadPose.d.ts +1 -0
  207. package/dist/weather/fireSpreadPose.js +1 -0
  208. package/dist/weather/weatherGeometry.d.ts +1 -0
  209. package/dist/weather/weatherGeometry.js +1 -0
  210. package/dist/weather/weatherMath.d.ts +3 -0
  211. package/dist/weather/weatherMath.js +3 -0
  212. package/dist/weather/weatherUniforms.d.ts +3 -0
  213. package/dist/weather/weatherUniforms.js +3 -0
  214. package/dist/world/RetainedVfx.d.ts +10 -0
  215. package/dist/world/RetainedVfx.js +136 -0
  216. package/dist/world/WorldHud.d.ts +52 -3
  217. package/dist/world/WorldHud.js +94 -9
  218. package/dist/world/WorldScene.d.ts +21 -0
  219. package/dist/world/WorldScene.js +159 -0
  220. package/dist/world/WorldVfx.d.ts +9 -0
  221. package/dist/world/WorldVfx.js +184 -0
  222. package/dist/world/entityPose.d.ts +2 -0
  223. package/dist/world/entityPose.js +2 -0
  224. package/dist/world/floatTextStyle.d.ts +1 -0
  225. package/dist/world/floatTextStyle.js +1 -0
  226. package/dist/world/telegraphPulse.d.ts +1 -0
  227. package/dist/world/telegraphPulse.js +1 -0
  228. package/dist/world/worldBarSamples.d.ts +26 -1
  229. package/dist/world/worldBarSamples.js +45 -1
  230. package/dist/worldSky.d.ts +5 -0
  231. package/dist/worldSky.js +11 -0
  232. package/dist/worldSync.d.ts +11 -0
  233. package/dist/worldSync.js +20 -0
  234. package/package.json +6 -6
  235. package/skills/jgengine/SKILL.md +51 -0
  236. package/skills/jgengine/api.md +434 -0
  237. package/skills/jgengine/capabilities.md +29 -0
  238. package/skills/jgengine/reference-cartridge.md +72 -0
  239. package/skills/jgengine-ui/SKILL.md +36 -0
  240. package/skills/jgengine-ui/api.md +1852 -0
  241. package/skills/jgengine-ui/capabilities.md +142 -0
  242. package/skills/jgengine-ui/reference.md +500 -0
  243. package/skills/jgengine-ui/references/visual-scorecard.md +73 -0
  244. package/skills/jgengine-world/SKILL.md +38 -0
  245. package/skills/jgengine-world/api.md +2652 -0
  246. package/skills/jgengine-world/capabilities.md +370 -0
  247. package/skills/jgengine-world/reference.md +222 -0
  248. package/llms.txt +0 -2643
@@ -0,0 +1,157 @@
1
+ import * as THREE from "three";
2
+ const NOISE_GLSL = /* glsl */ `
3
+ uint jgHashUint(uvec2 v){
4
+ uint x = v.x * 1664525u + v.y * 1013904223u;
5
+ x = (x ^ (x >> 16u)) * 2246822519u;
6
+ x = (x ^ (x >> 13u)) * 3266489917u;
7
+ x = x ^ (x >> 16u);
8
+ return x;
9
+ }
10
+ float jgHash(vec2 p){
11
+ ivec2 i = ivec2(floor(p));
12
+ return float(jgHashUint(uvec2(i))) * (1.0 / 4294967296.0);
13
+ }
14
+ float jgVNoise(vec2 p){
15
+ vec2 i = floor(p); vec2 f = fract(p);
16
+ vec2 u = f * f * (3.0 - 2.0 * f);
17
+ float a = jgHash(i);
18
+ float b = jgHash(i + vec2(1.0, 0.0));
19
+ float c = jgHash(i + vec2(0.0, 1.0));
20
+ float d = jgHash(i + vec2(1.0, 1.0));
21
+ return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
22
+ }
23
+ float jgFbm(vec2 p){
24
+ float v = 0.0; float amp = 0.5;
25
+ for (int i = 0; i < 4; i++){ v += amp * jgVNoise(p); p *= 2.0; amp *= 0.5; }
26
+ return v;
27
+ }
28
+ `;
29
+ /**
30
+ * A `MeshStandardMaterial` whose fragment shader keeps the biome-tinted vertex
31
+ * colour as the base ground and blends procedural, noise-broken rock (by slope),
32
+ * sand (by waterline), and snow (by height) over it — textured-reading terrain
33
+ * with no image assets. Full PBR: lit, shadowed, and fogged like any standard
34
+ * material, so it composes with the post-processing chain.
35
+ *
36
+ * When `detail.material` is set, pass the matching loaded `textures` (color/normal/roughness/ao):
37
+ * they tile by world position at `detail.material.repeat` world units per tile and blend over the
38
+ * procedural result at `detail.material.strength` — the real-texture seam layers onto the existing
39
+ * look instead of replacing it.
40
+ * @internal
41
+ */
42
+ export function createTerrainDetailMaterial(detail, textures) {
43
+ const hasTexture = textures !== undefined && detail.material !== undefined;
44
+ const repeat = detail.material?.repeat ?? 4;
45
+ const strength = detail.material?.strength ?? 1;
46
+ const uniforms = {
47
+ uRockColor: { value: new THREE.Color(detail.rockColor) },
48
+ uSandColor: { value: new THREE.Color(detail.sandColor) },
49
+ uSnowColor: { value: new THREE.Color(detail.snowColor) },
50
+ uRockSlopeStart: { value: detail.rockSlopeStart },
51
+ uSnowHeight: { value: detail.snowHeight },
52
+ uWaterLevel: { value: detail.waterLevel },
53
+ uDetailScale: { value: Math.max(0.5, detail.detailScale) },
54
+ uMacroScale: { value: Math.max(0.5, detail.macroScale) },
55
+ uStrength: { value: detail.strength },
56
+ ...(hasTexture
57
+ ? {
58
+ uMatColor: { value: textures.color },
59
+ uMatNormal: { value: textures.normal },
60
+ uMatRoughness: { value: textures.roughness },
61
+ uMatAo: { value: textures.ao },
62
+ uMatRepeat: { value: Math.max(0.01, repeat) },
63
+ uMatStrength: { value: strength },
64
+ }
65
+ : {}),
66
+ };
67
+ const material = new THREE.MeshStandardMaterial({
68
+ color: "#ffffff",
69
+ roughness: detail.roughness,
70
+ metalness: 0,
71
+ vertexColors: true,
72
+ });
73
+ material.onBeforeCompile = (shader) => {
74
+ Object.assign(shader.uniforms, uniforms);
75
+ shader.vertexShader = shader.vertexShader
76
+ .replace("#include <common>", `#include <common>
77
+ varying vec3 vJgWorldPos;
78
+ varying vec3 vJgWorldNormal;`)
79
+ .replace("#include <beginnormal_vertex>", `#include <beginnormal_vertex>
80
+ vJgWorldNormal = normalize(mat3(modelMatrix) * objectNormal);`)
81
+ .replace("#include <begin_vertex>", `#include <begin_vertex>
82
+ vJgWorldPos = (modelMatrix * vec4(transformed, 1.0)).xyz;`);
83
+ const materialUniformsGlsl = hasTexture
84
+ ? `
85
+ uniform sampler2D uMatColor;
86
+ uniform sampler2D uMatNormal;
87
+ uniform sampler2D uMatRoughness;
88
+ uniform sampler2D uMatAo;
89
+ uniform float uMatRepeat;
90
+ uniform float uMatStrength;`
91
+ : "";
92
+ const materialColorGlsl = hasTexture
93
+ ? `
94
+ vec2 jgMatUv = jgWp / uMatRepeat;
95
+ vec3 jgMatColor = texture2D(uMatColor, jgMatUv).rgb;
96
+ jgCol = mix(jgCol, jgMatColor, uMatStrength);`
97
+ : "";
98
+ const materialRoughnessGlsl = hasTexture
99
+ ? `
100
+ float jgMatRough = texture2D(uMatRoughness, jgMatUv).g;
101
+ roughnessFactor = mix(roughnessFactor, jgMatRough, uMatStrength);`
102
+ : "";
103
+ const materialNormalGlsl = hasTexture
104
+ ? `
105
+ vec2 jgMatNormalXy = texture2D(uMatNormal, jgMatUv).rg * 2.0 - 1.0;
106
+ normal = normalize(normal + vec3(jgMatNormalXy.x, 0.0, jgMatNormalXy.y) * uMatStrength * 0.6);`
107
+ : "";
108
+ const materialAoGlsl = hasTexture
109
+ ? `
110
+ float jgMatAo = texture2D(uMatAo, jgMatUv).r;
111
+ reflectedLight.indirectDiffuse *= mix(1.0, jgMatAo, uMatStrength);`
112
+ : "";
113
+ shader.fragmentShader = shader.fragmentShader
114
+ .replace("#include <common>", `#include <common>
115
+ varying vec3 vJgWorldPos;
116
+ varying vec3 vJgWorldNormal;
117
+ uniform vec3 uRockColor;
118
+ uniform vec3 uSandColor;
119
+ uniform vec3 uSnowColor;
120
+ uniform float uRockSlopeStart;
121
+ uniform float uSnowHeight;
122
+ uniform float uWaterLevel;
123
+ uniform float uDetailScale;
124
+ uniform float uMacroScale;
125
+ uniform float uStrength;
126
+ ${materialUniformsGlsl}
127
+ ${NOISE_GLSL}`)
128
+ .replace("#include <color_fragment>", `#include <color_fragment>
129
+ vec2 jgWp = vJgWorldPos.xz;
130
+ float jgFine = jgFbm(jgWp / uDetailScale);
131
+ float jgMacro = jgFbm(jgWp / uMacroScale);
132
+ float jgSlope = 1.0 - clamp(vJgWorldNormal.y, 0.0, 1.0);
133
+ float jgH = vJgWorldPos.y;
134
+ vec3 jgBase = diffuseColor.rgb;
135
+ jgBase *= mix(1.0 - 0.22 * uStrength, 1.0 + 0.18 * uStrength, jgFine);
136
+ jgBase = mix(jgBase, jgBase * vec3(0.86, 0.9, 0.82), jgMacro * 0.25 * uStrength);
137
+ vec3 jgRock = uRockColor * mix(0.72, 1.15, jgFbm(jgWp / (uDetailScale * 1.7)));
138
+ float jgRockW = smoothstep(uRockSlopeStart, uRockSlopeStart + 0.18, jgSlope) * uStrength;
139
+ vec3 jgCol = mix(jgBase, jgRock, jgRockW);
140
+ vec3 jgSand = uSandColor * mix(0.9, 1.08, jgFine);
141
+ float jgSandW = smoothstep(uWaterLevel + 2.4, uWaterLevel - 0.4, jgH) * (1.0 - jgRockW) * uStrength;
142
+ jgCol = mix(jgCol, jgSand, jgSandW);
143
+ vec3 jgSnow = uSnowColor * mix(0.94, 1.04, jgFine);
144
+ float jgSnowW = smoothstep(uSnowHeight, uSnowHeight + 8.0, jgH) * (1.0 - jgRockW * 0.6) * uStrength;
145
+ jgCol = mix(jgCol, jgSnow, jgSnowW);
146
+ ${materialColorGlsl}
147
+ diffuseColor.rgb = jgCol;`)
148
+ .replace("#include <roughnessmap_fragment>", `#include <roughnessmap_fragment>
149
+ ${materialRoughnessGlsl}`)
150
+ .replace("#include <normal_fragment_maps>", `#include <normal_fragment_maps>
151
+ ${materialNormalGlsl}`)
152
+ .replace("#include <aomap_fragment>", `#include <aomap_fragment>
153
+ ${materialAoGlsl}`);
154
+ };
155
+ material.customProgramCacheKey = () => `jgengine-terrain-detail-${detail.rockSlopeStart}-${detail.snowHeight}-${hasTexture ? "tex" : "flat"}`;
156
+ return { material };
157
+ }
@@ -27,10 +27,15 @@ export interface TerrainVertexColorOptions {
27
27
  waterline?: THREE.ColorRepresentation;
28
28
  waterlineHeight?: number;
29
29
  }
30
+ /** @internal */
30
31
  export declare function normalizeHeightBlend(height: number, minHeight: number, maxHeight: number): number;
32
+ /** @internal */
31
33
  export declare function resolveTerrainSize(size?: TerrainArea): ResolvedTerrainSize;
34
+ /** @internal */
32
35
  export declare function resolveTerrainSegments(segments?: ProceduralTerrainConfig["segments"]): ResolvedTerrainSegments;
36
+ /** @internal */
33
37
  export declare function toNoiseFieldConfig(config?: ProceduralTerrainConfig): NoiseFieldConfig;
38
+ /** @internal */
34
39
  export declare function createProceduralTerrainSampler(config?: ProceduralTerrainConfig): TerrainHeightSampler;
35
40
  /** Per-position palette override for multi-biome ground coloring — `createTerrainPaletteSampler` from `@jgengine/core/world/terrain` returns exactly this shape. */
36
41
  export type TerrainPaletteSampler = (x: number, z: number) => {
@@ -51,6 +56,8 @@ export interface FieldGroundOptions {
51
56
  * Mesh any `TerrainField` — including a `CarvableField` with craters/mounds written into it — into a
52
57
  * vertex-coloured ground geometry. `sampleHeight` drives the vertices, so runtime carves show up as
53
58
  * real depressions the moment the field is re-sampled (bump the caller's rebuild key after a carve).
54
- */
59
+ * @internal
60
+ */
55
61
  export declare function createFieldGroundGeometry(field: TerrainField, options?: FieldGroundOptions): THREE.BufferGeometry;
62
+ /** @internal */
56
63
  export declare function createProceduralGroundGeometry(config?: ProceduralTerrainConfig, colors?: TerrainVertexColorOptions): THREE.BufferGeometry;
@@ -1,21 +1,25 @@
1
1
  import { noiseField } from "@jgengine/core/world/terrain";
2
2
  import * as THREE from "three";
3
3
  import {} from "./random.js";
4
+ /** @internal */
4
5
  export function normalizeHeightBlend(height, minHeight, maxHeight) {
5
6
  const range = maxHeight - minHeight;
6
7
  if (range <= 1e-6)
7
8
  return 0.5;
8
9
  return THREE.MathUtils.clamp((height - minHeight) / range, 0, 1);
9
10
  }
11
+ /** @internal */
10
12
  export function resolveTerrainSize(size = 40) {
11
13
  return typeof size === "number" ? { width: size, depth: size } : { width: size[0], depth: size[1] };
12
14
  }
15
+ /** @internal */
13
16
  export function resolveTerrainSegments(segments = 96) {
14
17
  if (typeof segments !== "number")
15
18
  return { x: Math.max(1, Math.floor(segments[0])), z: Math.max(1, Math.floor(segments[1])) };
16
19
  const resolved = Math.max(1, Math.floor(segments));
17
20
  return { x: resolved, z: resolved };
18
21
  }
22
+ /** @internal */
19
23
  export function toNoiseFieldConfig(config = {}) {
20
24
  return {
21
25
  seed: config.seed,
@@ -26,6 +30,7 @@ export function toNoiseFieldConfig(config = {}) {
26
30
  baseHeight: config.baseOffset ?? 0,
27
31
  };
28
32
  }
33
+ /** @internal */
29
34
  export function createProceduralTerrainSampler(config = {}) {
30
35
  return noiseField(toNoiseFieldConfig(config)).sampleHeight;
31
36
  }
@@ -33,7 +38,8 @@ export function createProceduralTerrainSampler(config = {}) {
33
38
  * Mesh any `TerrainField` — including a `CarvableField` with craters/mounds written into it — into a
34
39
  * vertex-coloured ground geometry. `sampleHeight` drives the vertices, so runtime carves show up as
35
40
  * real depressions the moment the field is re-sampled (bump the caller's rebuild key after a carve).
36
- */
41
+ * @internal
42
+ */
37
43
  export function createFieldGroundGeometry(field, options = {}) {
38
44
  const size = resolveTerrainSize(options.size);
39
45
  const segments = resolveTerrainSegments(options.segments);
@@ -118,6 +124,7 @@ function buildGroundGeometry(size, segments, sampler, opts) {
118
124
  geometry.computeBoundingSphere();
119
125
  return geometry;
120
126
  }
127
+ /** @internal */
121
128
  export function createProceduralGroundGeometry(config = {}, colors = {}) {
122
129
  const size = resolveTerrainSize(config.size);
123
130
  const segments = resolveTerrainSegments(config.segments);
@@ -1,5 +1,5 @@
1
1
  import { type MutableRefObject } from "react";
2
- import { type TouchScheme } from "@jgengine/core/input/touchScheme";
2
+ import { type TouchScheme, type TouchStyle } from "@jgengine/core/input/touchScheme";
3
3
  export interface TouchCodeSink {
4
4
  onCodeDown(code: string): void;
5
5
  onCodeUp(code: string): void;
@@ -14,9 +14,11 @@ export declare function primaryButtonOffsets(count: number, scale?: number): {
14
14
  bottom: number;
15
15
  }[] | null;
16
16
  /**
17
- * Vertical space (px, excluding device safe areas) the dock occupies above
18
- * the bottom edge. The shell publishes it as `--jg-hud-dock-clearance` so
19
- * `HudCanvas` regions never collide with touch controls.
17
+ * Vertical space (px, excluding device safe areas) that *bottom-docked* clusters
18
+ * occupy above the bottom edge. The shell publishes it as
19
+ * `--jg-hud-dock-clearance` so `HudCanvas` regions never collide with touch
20
+ * controls. Side rails and top clusters reserve their own rectangles through the
21
+ * layout registry instead of this scalar.
20
22
  */
21
23
  export declare function touchDockClearance(scheme: TouchScheme | null, scale?: number): number;
22
24
  export declare function TouchPlaySurface({ scheme, sink, yawRef, pitchRef, maxPitch, onPrimaryTap, }: {
@@ -27,8 +29,10 @@ export declare function TouchPlaySurface({ scheme, sink, yawRef, pitchRef, maxPi
27
29
  maxPitch: number;
28
30
  onPrimaryTap: () => void;
29
31
  }): import("react").JSX.Element;
30
- export declare function TouchControlsDock({ scheme, sink, scale }: {
32
+ export declare function TouchControlsDock({ scheme, sink, style, scale, }: {
31
33
  scheme: TouchScheme;
32
34
  sink: TouchCodeSink;
35
+ /** Player-selected skin; falls back to the scheme's game default. */
36
+ style?: TouchStyle;
33
37
  scale?: number;
34
38
  }): import("react").JSX.Element;
@@ -1,15 +1,20 @@
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
+ import { useRegisterLayoutRegion } from "@jgengine/react/gameViewport";
8
+ const MOVEMENT_ZONE = { id: "control:movement", kind: "control", collisionPolicy: "forbid", collisionGroup: "touch-dock" };
9
+ const ACTIONS_ZONE = { id: "control:actions", kind: "control", collisionPolicy: "forbid", collisionGroup: "touch-dock" };
10
+ const UTILITY_ZONE = { id: "control:utility", kind: "control", collisionPolicy: "warn", collisionGroup: "touch-dock" };
7
11
  const JOYSTICK_SIZE = 132;
8
12
  const JOYSTICK_THUMB = 52;
9
13
  const JOYSTICK_DEADZONE = 0.28;
10
14
  const JOYSTICK_AXIS_THRESHOLD = 0.42;
11
15
  const LOOK_TAP_TUNING = { tapMoveThresholdPx: 12, tapMaxMs: 280, longPressMs: 450 };
12
16
  const BUTTON_SIZE = 56;
17
+ const RAIL_BUTTON_SIZE = 64;
13
18
  const RING1_RADIUS = 112;
14
19
  const RING2_RADIUS = 178;
15
20
  const RING1_CAPACITY = 3;
@@ -17,6 +22,74 @@ const RING2_CAPACITY = 5;
17
22
  const RING1_ANGLES = [[], [40], [16, 64], [12, 44, 76]];
18
23
  const DOCK_BASE_PADDING = 20;
19
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
+ };
20
93
  function polar(radius, deg, buttonSize) {
21
94
  const rad = (deg * Math.PI) / 180;
22
95
  return {
@@ -59,23 +132,38 @@ function primaryClusterExtent(count, scale = 1) {
59
132
  return (RING2_RADIUS + BUTTON_SIZE / 2) * scale;
60
133
  return Math.ceil(count / 4) * (BUTTON_SIZE + 12) * scale;
61
134
  }
135
+ const BOTTOM_ANCHORS = new Set(["bottom-left", "bottom-center", "bottom-right"]);
136
+ const ARC_ANCHORS = new Set(["bottom-left", "bottom-right"]);
62
137
  /**
63
- * Vertical space (px, excluding device safe areas) the dock occupies above
64
- * the bottom edge. The shell publishes it as `--jg-hud-dock-clearance` so
65
- * `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.
66
143
  */
67
144
  export function touchDockClearance(scheme, scale = 1) {
68
145
  if (scheme === null)
69
146
  return 0;
147
+ const layout = scheme.layout;
70
148
  let primary = 0;
71
149
  let utility = 0;
72
150
  for (const button of scheme.buttons) {
151
+ if (button.anchor !== null)
152
+ continue;
73
153
  if (button.kind === "utility")
74
154
  utility += 1;
75
155
  else
76
156
  primary += 1;
77
157
  }
78
- 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);
79
167
  if (tallest === 0)
80
168
  return 0;
81
169
  return tallest + DOCK_BASE_PADDING + 12;
@@ -154,7 +242,7 @@ function joystickDirections(joystick, nx, ny) {
154
242
  active.add(joystick.right);
155
243
  return active;
156
244
  }
157
- function VirtualJoystick({ joystick, sink, scale = 1 }) {
245
+ function VirtualJoystick({ joystick, sink, tokens, scale = 1, }) {
158
246
  const baseRef = useRef(null);
159
247
  const thumbRef = useRef(null);
160
248
  const pointerIdRef = useRef(null);
@@ -196,7 +284,7 @@ function VirtualJoystick({ joystick, sink, scale = 1 }) {
196
284
  if (thumbRef.current !== null)
197
285
  thumbRef.current.style.transform = "translate(0px, 0px)";
198
286
  };
199
- 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) => {
200
288
  if (pointerIdRef.current !== null)
201
289
  return;
202
290
  pointerIdRef.current = event.pointerId;
@@ -212,7 +300,7 @@ function VirtualJoystick({ joystick, sink, scale = 1 }) {
212
300
  return;
213
301
  event.stopPropagation();
214
302
  releaseAll();
215
- }, 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 } }) }));
216
304
  }
217
305
  function touchButtonIcon(button) {
218
306
  if (button.icon === false)
@@ -221,59 +309,160 @@ function touchButtonIcon(button) {
221
309
  return button.icon;
222
310
  return iconForAction(button.action);
223
311
  }
224
- 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, }) {
225
336
  const pointerIdRef = useRef(null);
337
+ const [pressed, setPressed] = useState(false);
226
338
  const icon = touchButtonIcon(button);
227
- 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);
228
345
  const releaseIfHeld = () => {
229
346
  if (pointerIdRef.current === null)
230
347
  return;
231
348
  pointerIdRef.current = null;
349
+ setPressed(false);
232
350
  sink.onCodeUp(touchCode(button.action));
233
351
  };
234
- 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) => {
235
353
  if (pointerIdRef.current !== null)
236
354
  return;
237
355
  pointerIdRef.current = event.pointerId;
238
356
  event.currentTarget.setPointerCapture(event.pointerId);
357
+ setPressed(true);
239
358
  sink.onCodeDown(touchCode(button.action));
240
359
  }, onPointerUp: (event) => {
241
360
  event.stopPropagation();
242
361
  releaseIfHeld();
243
- }, 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 }) })] }) }));
244
363
  }
245
- function TouchUtilityChip({ button, sink }) {
364
+ function TouchUtilityChip({ button, sink, tokens }) {
246
365
  const pointerIdRef = useRef(null);
366
+ const [pressed, setPressed] = useState(false);
247
367
  const releaseIfHeld = () => {
248
368
  if (pointerIdRef.current === null)
249
369
  return;
250
370
  pointerIdRef.current = null;
371
+ setPressed(false);
251
372
  sink.onCodeUp(touchCode(button.action));
252
373
  };
253
- 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) => {
254
375
  if (pointerIdRef.current !== null)
255
376
  return;
256
377
  pointerIdRef.current = event.pointerId;
257
378
  event.currentTarget.setPointerCapture(event.pointerId);
379
+ setPressed(true);
258
380
  sink.onCodeDown(touchCode(button.action));
259
381
  }, onPointerUp: (event) => {
260
382
  event.stopPropagation();
261
383
  releaseIfHeld();
262
384
  }, onPointerCancel: () => releaseIfHeld(), onContextMenu: (event) => event.preventDefault(), children: button.label }));
263
385
  }
264
- function PrimaryButtonCluster({ buttons, sink, scale = 1 }) {
386
+ function ThumbArcCluster({ buttons, sink, tokens, scale = 1, }) {
265
387
  const offsets = primaryButtonOffsets(buttons.length, scale);
266
388
  if (offsets === null) {
267
- 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))) }));
268
390
  }
269
391
  const extent = primaryClusterExtent(buttons.length, scale);
270
392
  return (_jsx("div", { className: "pointer-events-none relative", style: { width: extent, height: extent }, children: buttons.map((button, index) => {
271
393
  const offset = offsets[index];
272
- 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));
273
395
  }) }));
274
396
  }
275
- export function TouchControlsDock({ scheme, sink, scale = 1 }) {
276
- const primary = scheme.buttons.filter((button) => button.kind !== "utility");
277
- const utility = scheme.buttons.filter((button) => button.kind === "utility");
278
- 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", { children: scheme.joystick !== null ? _jsx(VirtualJoystick, { joystick: scheme.joystick, sink: sink, scale: scale }) : null }), _jsx(PrimaryButtonCluster, { buttons: primary, sink: sink, scale: scale })] }), utility.length > 0 ? (_jsx("div", { className: "pointer-events-none absolute inset-x-0 flex justify-center gap-2", style: { bottom: "calc(env(safe-area-inset-bottom, 0px) + 8px)" }, children: utility.map((button) => (_jsx(TouchUtilityChip, { button: button, sink: sink }, button.action))) })) : null] }));
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]);
456
+ const joystickRef = useRef(null);
457
+ const actionsRef = useRef(null);
458
+ const utilityRef = useRef(null);
459
+ useRegisterLayoutRegion(MOVEMENT_ZONE, joystickRef, scheme.joystick !== null);
460
+ useRegisterLayoutRegion(ACTIONS_ZONE, actionsRef, primary.length > 0);
461
+ useRegisterLayoutRegion(UTILITY_ZONE, utilityRef, utility.length > 0);
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 });
279
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;