@jgengine/shell 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (286) hide show
  1. package/CHANGELOG.md +39 -2
  2. package/LICENSE +203 -661
  3. package/README.md +1 -1
  4. package/dist/CombatPresentation.d.ts +5 -0
  5. package/dist/CombatPresentation.js +8 -0
  6. package/dist/GameHost.d.ts +25 -1
  7. package/dist/GameHost.js +60 -4
  8. package/dist/GamePhaseStamp.d.ts +1 -0
  9. package/dist/GamePhaseStamp.js +9 -0
  10. package/dist/GamePlayer.d.ts +5 -0
  11. package/dist/GamePlayer.js +5 -0
  12. package/dist/GamePlayerShell.d.ts +5 -12
  13. package/dist/GamePlayerShell.js +67 -1332
  14. package/dist/GameUiPreview.js +3 -2
  15. package/dist/Shell3dPresentation.d.ts +63 -0
  16. package/dist/Shell3dPresentation.js +302 -0
  17. package/dist/ShellChrome.d.ts +51 -0
  18. package/dist/ShellChrome.js +51 -0
  19. package/dist/ShellHudPresentation.d.ts +41 -0
  20. package/dist/ShellHudPresentation.js +22 -0
  21. package/dist/audio/audioEngine.d.ts +4 -0
  22. package/dist/audio/audioEngine.js +128 -21
  23. package/dist/audio/audioWire.d.ts +40 -0
  24. package/dist/audio/audioWire.js +62 -0
  25. package/dist/audio/loopParams.d.ts +20 -0
  26. package/dist/audio/loopParams.js +28 -0
  27. package/dist/behaviour.d.ts +2 -1
  28. package/dist/behaviour.js +2 -1
  29. package/dist/behaviourAttach.d.ts +1 -0
  30. package/dist/behaviourAttach.js +1 -0
  31. package/dist/behaviourDriver.d.ts +1 -0
  32. package/dist/behaviourDriver.js +1 -0
  33. package/dist/boundActionDispatch.d.ts +13 -0
  34. package/dist/boundActionDispatch.js +37 -0
  35. package/dist/camera/GameCameraRig.d.ts +5 -2
  36. package/dist/camera/GameCameraRig.js +8 -3
  37. package/dist/camera/GameFirstPersonCamera.d.ts +9 -2
  38. package/dist/camera/GameFirstPersonCamera.js +8 -3
  39. package/dist/camera/GameInspectionCamera.d.ts +1 -1
  40. package/dist/camera/GameInspectionCamera.js +2 -2
  41. package/dist/camera/GameOrbitCamera.js +24 -9
  42. package/dist/camera/cameraBlendMath.d.ts +3 -0
  43. package/dist/camera/cameraBlendMath.js +3 -0
  44. package/dist/camera/cameraRigs.d.ts +3 -1
  45. package/dist/camera/cameraRigs.js +21 -3
  46. package/dist/camera/fovPreference.d.ts +6 -1
  47. package/dist/camera/fovPreference.js +6 -1
  48. package/dist/camera/index.d.ts +4 -7
  49. package/dist/camera/index.js +4 -7
  50. package/dist/camera/inspectionCameraMath.d.ts +7 -2
  51. package/dist/camera/inspectionCameraMath.js +7 -2
  52. package/dist/camera/orbitCameraMath.d.ts +36 -6
  53. package/dist/camera/orbitCameraMath.js +56 -6
  54. package/dist/camera/rigMath.d.ts +114 -24
  55. package/dist/camera/rigMath.js +129 -24
  56. package/dist/camera/rigResolve.d.ts +4 -2
  57. package/dist/camera/rigResolve.js +4 -2
  58. package/dist/camera/shakeChannel.d.ts +6 -2
  59. package/dist/camera/shakeChannel.js +6 -2
  60. package/dist/camera/shakeChannelMath.d.ts +1 -0
  61. package/dist/camera/shakeChannelMath.js +1 -0
  62. package/dist/commandSink.d.ts +8 -3
  63. package/dist/commandSink.js +8 -3
  64. package/dist/defineGame.d.ts +15 -1
  65. package/dist/defineGame.js +50 -9
  66. package/dist/devtools/ColPanel.d.ts +1 -0
  67. package/dist/devtools/ColPanel.js +14 -0
  68. package/dist/devtools/DevtoolsOverlay.d.ts +11 -7
  69. package/dist/devtools/DevtoolsOverlay.js +47 -462
  70. package/dist/devtools/KeysPanel.d.ts +4 -0
  71. package/dist/devtools/KeysPanel.js +31 -0
  72. package/dist/devtools/LogsPanel.d.ts +1 -0
  73. package/dist/devtools/LogsPanel.js +12 -0
  74. package/dist/devtools/NetPanel.d.ts +4 -0
  75. package/dist/devtools/NetPanel.js +10 -0
  76. package/dist/devtools/PerfPanel.d.ts +4 -0
  77. package/dist/devtools/PerfPanel.js +53 -0
  78. package/dist/devtools/TunePanel.d.ts +3 -0
  79. package/dist/devtools/TunePanel.js +309 -0
  80. package/dist/devtools/agentBridge.d.ts +41 -0
  81. package/dist/devtools/agentBridge.js +266 -0
  82. package/dist/devtools/collisionDebug.d.ts +6 -0
  83. package/dist/devtools/collisionDebug.js +6 -0
  84. package/dist/devtools/collisionDebugMath.d.ts +8 -2
  85. package/dist/devtools/collisionDebugMath.js +24 -5
  86. package/dist/devtools/devtoolsOverrides.d.ts +6 -0
  87. package/dist/devtools/devtoolsOverrides.js +48 -0
  88. package/dist/devtools/panelAtoms.d.ts +9 -0
  89. package/dist/devtools/panelAtoms.js +10 -0
  90. package/dist/devtools/perfDiagnose.d.ts +2 -0
  91. package/dist/devtools/perfDiagnose.js +43 -0
  92. package/dist/diagnostics/RuntimeDiagnostics.d.ts +36 -0
  93. package/dist/diagnostics/RuntimeDiagnostics.js +80 -0
  94. package/dist/drivers/FrameDriver.d.ts +37 -0
  95. package/dist/drivers/FrameDriver.js +232 -0
  96. package/dist/drivers/HudOnlyDriver.d.ts +16 -0
  97. package/dist/drivers/HudOnlyDriver.js +60 -0
  98. package/dist/environment/Daylight.d.ts +23 -7
  99. package/dist/environment/Daylight.js +106 -13
  100. package/dist/environment/EnvironmentScene.js +44 -9
  101. package/dist/environment/VolumetricClouds.d.ts +17 -0
  102. package/dist/environment/VolumetricClouds.js +202 -0
  103. package/dist/environment/daylightCycle.d.ts +3 -1
  104. package/dist/environment/daylightCycle.js +3 -1
  105. package/dist/environment/groundPadMath.d.ts +3 -0
  106. package/dist/environment/groundPadMath.js +3 -0
  107. package/dist/environment/index.d.ts +1 -0
  108. package/dist/environment/index.js +1 -0
  109. package/dist/environment/skyLightingPolicy.d.ts +2 -0
  110. package/dist/environment/skyLightingPolicy.js +2 -0
  111. package/dist/environment/terrainGroundColor.d.ts +9 -0
  112. package/dist/environment/terrainGroundColor.js +22 -0
  113. package/dist/gameKit.d.ts +27 -0
  114. package/dist/gameKit.js +29 -0
  115. package/dist/hotbarActions.d.ts +26 -0
  116. package/dist/hotbarActions.js +27 -0
  117. package/dist/inputSink.d.ts +16 -4
  118. package/dist/inputSink.js +57 -7
  119. package/dist/map/MapMarkerBeacons.d.ts +3 -2
  120. package/dist/materialOverride.d.ts +10 -0
  121. package/dist/materialOverride.js +15 -3
  122. package/dist/multiplayer.js +8 -2
  123. package/dist/postfx/PostProcessing.d.ts +9 -1
  124. package/dist/postfx/PostProcessing.js +56 -11
  125. package/dist/postfx/gradeShader.js +9 -1
  126. package/dist/presentationResolve.d.ts +35 -0
  127. package/dist/presentationResolve.js +51 -0
  128. package/dist/registry.d.ts +4 -2
  129. package/dist/render/EnvironmentLighting.d.ts +10 -0
  130. package/dist/render/EnvironmentLighting.js +34 -0
  131. package/dist/render/SceneLighting.d.ts +7 -0
  132. package/dist/render/SceneLighting.js +18 -0
  133. package/dist/render/SceneModels.d.ts +14 -0
  134. package/dist/render/SceneModels.js +337 -0
  135. package/dist/render/assetBase.d.ts +17 -0
  136. package/dist/render/assetBase.js +35 -0
  137. package/dist/render/defaultSurface.d.ts +29 -0
  138. package/dist/render/defaultSurface.js +101 -0
  139. package/dist/render/modelLoad.d.ts +24 -0
  140. package/dist/render/modelLoad.js +76 -0
  141. package/dist/render/modelRender.d.ts +11 -0
  142. package/dist/render/modelRender.js +15 -0
  143. package/dist/render/resolveModel.d.ts +54 -3
  144. package/dist/render/resolveModel.js +116 -4
  145. package/dist/render/useEntityRenderCues.d.ts +13 -0
  146. package/dist/render/useEntityRenderCues.js +62 -0
  147. package/dist/scatter/InstancedScatter.d.ts +24 -0
  148. package/dist/scatter/InstancedScatter.js +135 -0
  149. package/dist/scatter/scatterModels.d.ts +25 -0
  150. package/dist/scatter/scatterModels.js +61 -0
  151. package/dist/scatter/scatterProxies.d.ts +6 -0
  152. package/dist/scatter/scatterProxies.js +118 -0
  153. package/dist/scatter.d.ts +1 -0
  154. package/dist/scatter.js +1 -0
  155. package/dist/scene/AuthoredScene.d.ts +84 -0
  156. package/dist/scene/AuthoredScene.js +178 -0
  157. package/dist/scene/CityRenderer.d.ts +2 -0
  158. package/dist/scene/CityRenderer.js +1031 -0
  159. package/dist/scene/GeneratedAssetRenderer.d.ts +14 -0
  160. package/dist/scene/GeneratedAssetRenderer.js +59 -0
  161. package/dist/scene/GrassFieldRenderer.d.ts +2 -0
  162. package/dist/scene/GrassFieldRenderer.js +73 -0
  163. package/dist/scene/PoleLineRenderer.d.ts +2 -0
  164. package/dist/scene/PoleLineRenderer.js +287 -0
  165. package/dist/scene/SoilPatchRenderer.d.ts +2 -0
  166. package/dist/scene/SoilPatchRenderer.js +84 -0
  167. package/dist/scene/StudioStage.d.ts +37 -0
  168. package/dist/scene/StudioStage.js +42 -0
  169. package/dist/scene/WaterRenderer.d.ts +2 -0
  170. package/dist/scene/WaterRenderer.js +61 -0
  171. package/dist/scene/builtinSceneKindRenderers.d.ts +2 -0
  172. package/dist/scene/builtinSceneKindRenderers.js +26 -0
  173. package/dist/scene/cityWindowMaterial.d.ts +16 -0
  174. package/dist/scene/cityWindowMaterial.js +81 -0
  175. package/dist/scene/sceneKindRenderers.d.ts +30 -0
  176. package/dist/scene/sceneKindRenderers.js +17 -0
  177. package/dist/scene.d.ts +1 -0
  178. package/dist/scene.js +1 -0
  179. package/dist/settings/appliedSettings.d.ts +7 -1
  180. package/dist/settings/appliedSettings.js +13 -2
  181. package/dist/settings/settingsController.d.ts +2 -0
  182. package/dist/settings/settingsController.js +22 -1
  183. package/dist/shellConstants.d.ts +4 -0
  184. package/dist/shellConstants.js +4 -0
  185. package/dist/shellMovement.d.ts +8 -0
  186. package/dist/shellMovement.js +11 -0
  187. package/dist/shellPointer.d.ts +20 -0
  188. package/dist/shellPointer.js +28 -0
  189. package/dist/structures/PlacementGhost.d.ts +4 -0
  190. package/dist/structures/PlacementGhost.js +4 -0
  191. package/dist/structures/TransformGizmo.d.ts +53 -0
  192. package/dist/structures/TransformGizmo.js +59 -0
  193. package/dist/structures/index.d.ts +1 -0
  194. package/dist/structures/index.js +1 -0
  195. package/dist/terrain/GrassField.d.ts +11 -3
  196. package/dist/terrain/GrassField.js +16 -6
  197. package/dist/terrain/TerraformBrushCursor.js +3 -0
  198. package/dist/terrain/grassBudget.d.ts +11 -2
  199. package/dist/terrain/grassBudget.js +20 -4
  200. package/dist/terrain/grassGeometry.d.ts +35 -0
  201. package/dist/terrain/grassGeometry.js +85 -20
  202. package/dist/terrain/grassMaterial.d.ts +20 -0
  203. package/dist/terrain/grassMaterial.js +68 -13
  204. package/dist/terrain/heightfieldGeometry.d.ts +34 -0
  205. package/dist/terrain/heightfieldGeometry.js +109 -0
  206. package/dist/terrain/index.d.ts +1 -0
  207. package/dist/terrain/index.js +1 -0
  208. package/dist/terrain/random.d.ts +3 -0
  209. package/dist/terrain/random.js +3 -0
  210. package/dist/terrain/soilPatchMaterial.d.ts +23 -0
  211. package/dist/terrain/soilPatchMaterial.js +147 -0
  212. package/dist/terrain/terrainDetailMaterial.d.ts +19 -2
  213. package/dist/terrain/terrainDetailMaterial.js +87 -7
  214. package/dist/terrain/terrainMath.d.ts +8 -1
  215. package/dist/terrain/terrainMath.js +8 -1
  216. package/dist/touch/TouchControlsOverlay.d.ts +9 -5
  217. package/dist/touch/TouchControlsOverlay.js +201 -22
  218. package/dist/useShellMultiplayerSync.d.ts +9 -0
  219. package/dist/useShellMultiplayerSync.js +98 -0
  220. package/dist/visibility/CullingProvider.js +1 -1
  221. package/dist/water/Ocean.d.ts +7 -1
  222. package/dist/water/Ocean.js +16 -3
  223. package/dist/water/OceanConfig.d.ts +11 -0
  224. package/dist/water/OceanConfig.js +8 -0
  225. package/dist/water/OceanMaterial.d.ts +12 -0
  226. package/dist/water/OceanMaterial.js +9 -0
  227. package/dist/water/OceanShader.d.ts +2 -2
  228. package/dist/water/OceanShader.js +78 -10
  229. package/dist/weather/fireSpreadPose.d.ts +1 -0
  230. package/dist/weather/fireSpreadPose.js +1 -0
  231. package/dist/weather/weatherGeometry.d.ts +1 -0
  232. package/dist/weather/weatherGeometry.js +1 -0
  233. package/dist/weather/weatherMath.d.ts +3 -0
  234. package/dist/weather/weatherMath.js +3 -0
  235. package/dist/weather/weatherUniforms.d.ts +3 -0
  236. package/dist/weather/weatherUniforms.js +3 -0
  237. package/dist/world/PlaceScene.d.ts +11 -0
  238. package/dist/world/PlaceScene.js +27 -0
  239. package/dist/world/RetainedVfx.d.ts +10 -0
  240. package/dist/world/RetainedVfx.js +136 -0
  241. package/dist/world/WorldHud.d.ts +49 -2
  242. package/dist/world/WorldHud.js +87 -2
  243. package/dist/world/WorldScene.d.ts +21 -0
  244. package/dist/world/WorldScene.js +186 -0
  245. package/dist/world/WorldVfx.d.ts +9 -0
  246. package/dist/world/WorldVfx.js +184 -0
  247. package/dist/world/entityPose.d.ts +2 -0
  248. package/dist/world/entityPose.js +2 -0
  249. package/dist/world/floatTextStyle.d.ts +1 -0
  250. package/dist/world/floatTextStyle.js +1 -0
  251. package/dist/world/telegraphPulse.d.ts +1 -0
  252. package/dist/world/telegraphPulse.js +1 -0
  253. package/dist/world/worldBarSamples.d.ts +25 -0
  254. package/dist/world/worldBarSamples.js +38 -0
  255. package/dist/worldSky.d.ts +11 -0
  256. package/dist/worldSky.js +21 -0
  257. package/dist/worldSync.d.ts +2 -1
  258. package/dist/worldSync.js +2 -1
  259. package/package.json +6 -6
  260. package/skills/game-design/SKILL.md +28 -0
  261. package/skills/game-design/agents/openai.yaml +4 -0
  262. package/skills/game-design/references/application-playbook.md +170 -0
  263. package/skills/game-design/references/field-guide.md +288 -0
  264. package/skills/jgengine/SKILL.md +54 -0
  265. package/skills/jgengine/api.md +527 -0
  266. package/skills/jgengine/capabilities.md +131 -0
  267. package/skills/jgengine/recipes/minimal-game.md +88 -0
  268. package/skills/jgengine-ui/SKILL.md +41 -0
  269. package/skills/jgengine-ui/api.md +2007 -0
  270. package/skills/jgengine-ui/capabilities.md +211 -0
  271. package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
  272. package/skills/jgengine-ui/reference.md +513 -0
  273. package/skills/jgengine-ui/references/visual-scorecard.md +73 -0
  274. package/skills/jgengine-world/SKILL.md +51 -0
  275. package/skills/jgengine-world/api.md +3056 -0
  276. package/skills/jgengine-world/capabilities.md +476 -0
  277. package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
  278. package/skills/jgengine-world/reference.md +261 -0
  279. package/skills/level-design/SKILL.md +54 -0
  280. package/skills/level-design/agents/openai.yaml +4 -0
  281. package/skills/level-design/references/application-playbook.md +192 -0
  282. package/skills/level-design/references/field-guide.md +337 -0
  283. package/skills/level-design/references/live-editor-loop.md +179 -0
  284. package/dist/cartridge.d.ts +0 -145
  285. package/dist/cartridge.js +0 -246
  286. package/llms.txt +0 -2761
@@ -0,0 +1,109 @@
1
+ import * as THREE from "three";
2
+ const EXTENT_KEY = "jgHeightfieldExtent";
3
+ /**
4
+ * Re-samples a `PlaneGeometry(width, depth, segments, segments).rotateX(-π/2)` heightfield mesh
5
+ * from a `sampleHeight` field in place: vertex Y, optional vertex color, grid central-difference
6
+ * normals, and an analytically maintained bounding sphere. With a dirty `region`, the work is
7
+ * O(region vertices) — heights/colors update inside the covering vertex window, normals inside the
8
+ * window plus a one-vertex ring — never a whole-mesh `computeVertexNormals`/`computeBoundingSphere`
9
+ * pass, which is what keeps per-frame brush stamps inside the editor's frame budget. The bounding
10
+ * sphere derives from the fixed plane extents plus a running height range kept on
11
+ * `geometry.userData`; partial passes only expand it, a full pass resets it exactly.
12
+ * @capability heightfield-mesh-update in-place partial update of a displaced ground plane mesh
13
+ */
14
+ export function displaceHeightfieldGeometry(geometry, sampleHeight, options) {
15
+ const position = geometry.attributes.position;
16
+ const normals = geometry.attributes.normal;
17
+ const { bounds } = options;
18
+ const cx = (bounds.minX + bounds.maxX) / 2;
19
+ const cz = (bounds.minZ + bounds.maxZ) / 2;
20
+ const width = Math.max(1, bounds.maxX - bounds.minX);
21
+ const depth = Math.max(1, bounds.maxZ - bounds.minZ);
22
+ // PlaneGeometry(w, d, s, s) rotated -π/2 about X is a (s+1)² row-major grid: x fastest ascending,
23
+ // z ascending per row (verified by heightfieldGeometry.test.ts against the three.js constructor).
24
+ const side = Math.round(Math.sqrt(position.count));
25
+ const segments = Math.max(1, side - 1);
26
+ const stepX = width / segments;
27
+ const stepZ = depth / segments;
28
+ let colors = geometry.attributes.color;
29
+ const wantColors = options.color !== undefined;
30
+ const colorsMissing = wantColors && (colors === undefined || colors.count !== position.count);
31
+ const region = options.region ?? null;
32
+ const full = region === null || colorsMissing;
33
+ if (colorsMissing) {
34
+ colors = new THREE.BufferAttribute(new Float32Array(position.count * 3), 3);
35
+ geometry.setAttribute("color", colors);
36
+ }
37
+ const clampCell = (value) => (value < 0 ? 0 : value > segments ? segments : value);
38
+ const col0 = full ? 0 : clampCell(Math.floor(((region.minX - bounds.minX) / width) * segments));
39
+ const col1 = full ? segments : clampCell(Math.ceil(((region.maxX - bounds.minX) / width) * segments));
40
+ const row0 = full ? 0 : clampCell(Math.floor(((region.minZ - bounds.minZ) / depth) * segments));
41
+ const row1 = full ? segments : clampCell(Math.ceil(((region.maxZ - bounds.minZ) / depth) * segments));
42
+ const tone = new THREE.Color();
43
+ let windowMin = Infinity;
44
+ let windowMax = -Infinity;
45
+ for (let row = row0; row <= row1; row += 1) {
46
+ const base = row * side;
47
+ for (let col = col0; col <= col1; col += 1) {
48
+ const index = base + col;
49
+ const x = position.getX(index) + cx;
50
+ const z = position.getZ(index) + cz;
51
+ const height = sampleHeight(x, z);
52
+ position.setY(index, height);
53
+ if (height < windowMin)
54
+ windowMin = height;
55
+ if (height > windowMax)
56
+ windowMax = height;
57
+ if (options.color !== undefined) {
58
+ options.color(x, z, height, tone);
59
+ colors.setXYZ(index, tone.r, tone.g, tone.b);
60
+ }
61
+ }
62
+ }
63
+ // Grid central-difference normals over the window plus a one-vertex ring — a height change moves
64
+ // its neighbors' normals too. Matches smooth shading without a whole-mesh triangle pass.
65
+ const nc0 = Math.max(0, col0 - 1);
66
+ const nc1 = Math.min(segments, col1 + 1);
67
+ const nr0 = Math.max(0, row0 - 1);
68
+ const nr1 = Math.min(segments, row1 + 1);
69
+ for (let row = nr0; row <= nr1; row += 1) {
70
+ const rowD = Math.max(0, row - 1);
71
+ const rowU = Math.min(segments, row + 1);
72
+ for (let col = nc0; col <= nc1; col += 1) {
73
+ const colL = Math.max(0, col - 1);
74
+ const colR = Math.min(segments, col + 1);
75
+ const hL = position.getY(row * side + colL);
76
+ const hR = position.getY(row * side + colR);
77
+ const hD = position.getY(rowD * side + col);
78
+ const hU = position.getY(rowU * side + col);
79
+ const nx = -(hR - hL) / Math.max(1e-6, (colR - colL) * stepX);
80
+ const nz = -(hU - hD) / Math.max(1e-6, (rowU - rowD) * stepZ);
81
+ const inv = 1 / Math.sqrt(nx * nx + 1 + nz * nz);
82
+ normals.setXYZ(row * side + col, nx * inv, inv, nz * inv);
83
+ }
84
+ }
85
+ const extent = geometry.userData[EXTENT_KEY] ?? {
86
+ minY: Infinity,
87
+ maxY: -Infinity,
88
+ };
89
+ if (full) {
90
+ extent.minY = windowMin;
91
+ extent.maxY = windowMax;
92
+ }
93
+ else {
94
+ extent.minY = Math.min(extent.minY, windowMin);
95
+ extent.maxY = Math.max(extent.maxY, windowMax);
96
+ }
97
+ geometry.userData[EXTENT_KEY] = extent;
98
+ const hasExtent = extent.minY <= extent.maxY;
99
+ const midY = hasExtent ? (extent.minY + extent.maxY) / 2 : 0;
100
+ const halfY = hasExtent ? (extent.maxY - extent.minY) / 2 : 0;
101
+ if (geometry.boundingSphere === null)
102
+ geometry.boundingSphere = new THREE.Sphere();
103
+ geometry.boundingSphere.center.set(0, midY, 0);
104
+ geometry.boundingSphere.radius = Math.sqrt((width / 2) ** 2 + (depth / 2) ** 2 + halfY ** 2);
105
+ position.needsUpdate = true;
106
+ if (colors !== undefined && wantColors)
107
+ colors.needsUpdate = true;
108
+ normals.needsUpdate = true;
109
+ }
@@ -2,6 +2,7 @@ export { CarvedTerrain, type CarvedTerrainProps } from "./CarvedTerrain.js";
2
2
  export { GrassField, type GrassFieldProps } from "./GrassField.js";
3
3
  export { ProceduralGround, type ProceduralGroundProps } from "./ProceduralGround.js";
4
4
  export { EditableGround, type EditableGroundProps } from "./EditableGround.js";
5
+ export { displaceHeightfieldGeometry, type HeightfieldColorFn, type HeightfieldDisplaceOptions, type HeightfieldRect, } from "./heightfieldGeometry.js";
5
6
  export { TerraformBrushCursor, type TerraformBrushCursorProps } from "./TerraformBrushCursor.js";
6
7
  export { createGrassBladeGeometry, resolveGrassBladeGeometryOptions, resolveGrassRange, type GrassBladeGeometryOptions, type GrassRange, type ResolvedGrassBladeGeometryOptions, } from "./grassGeometry.js";
7
8
  export { createGrassMaterial, DEFAULT_GRASS_WIND, resolveGrassWind, type GrassMaterialHandle, type GrassMaterialOptions, type GrassShaderUniforms, type GrassWindOptions, } from "./grassMaterial.js";
@@ -2,6 +2,7 @@ export { CarvedTerrain } from "./CarvedTerrain.js";
2
2
  export { GrassField } from "./GrassField.js";
3
3
  export { ProceduralGround } from "./ProceduralGround.js";
4
4
  export { EditableGround } from "./EditableGround.js";
5
+ export { displaceHeightfieldGeometry, } from "./heightfieldGeometry.js";
5
6
  export { TerraformBrushCursor } from "./TerraformBrushCursor.js";
6
7
  export { createGrassBladeGeometry, resolveGrassBladeGeometryOptions, resolveGrassRange, } from "./grassGeometry.js";
7
8
  export { createGrassMaterial, DEFAULT_GRASS_WIND, resolveGrassWind, } from "./grassMaterial.js";
@@ -1,4 +1,7 @@
1
1
  export type TerrainSeed = number | string;
2
+ /** @internal */
2
3
  export declare function seedToUint32(seed?: TerrainSeed): number;
4
+ /** @internal */
3
5
  export declare function createSeededRandom(seed?: TerrainSeed): () => number;
6
+ /** @internal */
4
7
  export declare function hashNoise2(x: number, z: number, seed?: TerrainSeed): number;
@@ -1,3 +1,4 @@
1
+ /** @internal */
1
2
  export function seedToUint32(seed = 1) {
2
3
  if (typeof seed === "number")
3
4
  return seed >>> 0;
@@ -8,6 +9,7 @@ export function seedToUint32(seed = 1) {
8
9
  }
9
10
  return hash >>> 0;
10
11
  }
12
+ /** @internal */
11
13
  export function createSeededRandom(seed = 1) {
12
14
  let state = seedToUint32(seed);
13
15
  return () => {
@@ -18,6 +20,7 @@ export function createSeededRandom(seed = 1) {
18
20
  return ((value ^ (value >>> 14)) >>> 0) / 4294967296;
19
21
  };
20
22
  }
23
+ /** @internal */
21
24
  export function hashNoise2(x, z, seed = 1) {
22
25
  let hash = seedToUint32(seed);
23
26
  hash ^= Math.imul(x | 0, 374761393);
@@ -0,0 +1,23 @@
1
+ import * as THREE from "three";
2
+ import type { SoilRules } from "@jgengine/core/world/soilKind";
3
+ /** Optional noise-eroded edge-fade config for {@link createSoilPatchMaterial} — the patch dissolves into the ground at its border. */
4
+ export interface SoilPatchEdge {
5
+ /** Patch center on XZ, world units — with `halfSize`, drives the noise-eroded edge fade. */
6
+ center: readonly [number, number];
7
+ /** Patch half extents on XZ, world units. */
8
+ halfSize: readonly [number, number];
9
+ /** Fade band width in meters from the patch border inward. Default 1.6. */
10
+ fade?: number;
11
+ /** Terrain color under the patch: the border blends its *color* toward the ground it meets, not just its alpha. Defaults to the soil base color (color blend off). */
12
+ groundColor?: string;
13
+ }
14
+ /**
15
+ * A `MeshStandardMaterial` for one authored `soil` volume: a base dirt tint with a Worley crack-line
16
+ * network and an FBM moss-patch overlay injected via `onBeforeCompile`, seeded from `rules.seed`. Full
17
+ * PBR — lit, shadowed, fogged — like the terrain detail material it borrows its noise machinery from.
18
+ * Pass `edge` to add a noise-eroded alpha fade at the patch border (the draped scene-kind renderer
19
+ * does) so the patch blends into the surrounding ground. A game never calls this directly — author a
20
+ * soil patch via the editor's `soil` studio kind.
21
+ * @internal
22
+ */
23
+ export declare function createSoilPatchMaterial(rules: SoilRules, edge?: SoilPatchEdge): THREE.MeshStandardMaterial;
@@ -0,0 +1,147 @@
1
+ import * as THREE from "three";
2
+ /**
3
+ * Worley (F1/F2 cellular) crack mask + FBM moss mask — the same noise-driven-shader technique as
4
+ * `terrainDetailMaterial.ts`'s rock/sand/snow blend, injected here via `onBeforeCompile` into a
5
+ * dedicated soil-patch material instead of the whole-ground detail material. `F2 - F1` (the gap
6
+ * between nearest and second-nearest Worley feature distances) goes to zero right at a cell boundary,
7
+ * which reads as a network of cracked-earth veins once thresholded — the standard "cracked ground"
8
+ * shader trick.
9
+ */
10
+ const SOIL_NOISE_GLSL = /* glsl */ `
11
+ uint jgSoilHashUint(uvec2 v){
12
+ uint x = v.x * 1664525u + v.y * 1013904223u;
13
+ x = (x ^ (x >> 16u)) * 2246822519u;
14
+ x = (x ^ (x >> 13u)) * 3266489917u;
15
+ x = x ^ (x >> 16u);
16
+ return x;
17
+ }
18
+ float jgSoilHash1(vec2 p){
19
+ return float(jgSoilHashUint(uvec2(floor(p)))) * (1.0 / 4294967296.0);
20
+ }
21
+ vec2 jgSoilHash2(vec2 p){
22
+ uvec2 base = uvec2(floor(p));
23
+ float a = float(jgSoilHashUint(base)) * (1.0 / 4294967296.0);
24
+ float b = float(jgSoilHashUint(base + uvec2(19u, 47u))) * (1.0 / 4294967296.0);
25
+ return vec2(a, b);
26
+ }
27
+ float jgSoilVNoise(vec2 p){
28
+ vec2 i = floor(p); vec2 f = fract(p);
29
+ vec2 u = f * f * (3.0 - 2.0 * f);
30
+ float a = jgSoilHash1(i);
31
+ float b = jgSoilHash1(i + vec2(1.0, 0.0));
32
+ float c = jgSoilHash1(i + vec2(0.0, 1.0));
33
+ float d = jgSoilHash1(i + vec2(1.0, 1.0));
34
+ return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
35
+ }
36
+ float jgSoilFbm(vec2 p){
37
+ float v = 0.0; float amp = 0.5;
38
+ for (int i = 0; i < 4; i++){ v += amp * jgSoilVNoise(p); p *= 2.0; amp *= 0.5; }
39
+ return v;
40
+ }
41
+ vec2 jgSoilWorleyF1F2(vec2 p){
42
+ vec2 ip = floor(p);
43
+ vec2 fp = fract(p);
44
+ float f1 = 8.0; float f2 = 8.0;
45
+ for (int y = -1; y <= 1; y++){
46
+ for (int x = -1; x <= 1; x++){
47
+ vec2 neighbor = vec2(float(x), float(y));
48
+ vec2 point = jgSoilHash2(ip + neighbor);
49
+ float dist = length(neighbor + point - fp);
50
+ if (dist < f1) { f2 = f1; f1 = dist; }
51
+ else if (dist < f2) { f2 = dist; }
52
+ }
53
+ }
54
+ return vec2(f1, f2);
55
+ }
56
+ float jgSoilCrackMask(vec2 p, float scale){
57
+ vec2 f = jgSoilWorleyF1F2(p / scale);
58
+ return 1.0 - smoothstep(0.0, 0.065, f.y - f.x);
59
+ }
60
+ `;
61
+ function hashSeed(text) {
62
+ let h = 2166136261 >>> 0;
63
+ for (let i = 0; i < text.length; i += 1) {
64
+ h ^= text.charCodeAt(i);
65
+ h = Math.imul(h, 16777619);
66
+ }
67
+ return (h >>> 0) % 100000;
68
+ }
69
+ /**
70
+ * A `MeshStandardMaterial` for one authored `soil` volume: a base dirt tint with a Worley crack-line
71
+ * network and an FBM moss-patch overlay injected via `onBeforeCompile`, seeded from `rules.seed`. Full
72
+ * PBR — lit, shadowed, fogged — like the terrain detail material it borrows its noise machinery from.
73
+ * Pass `edge` to add a noise-eroded alpha fade at the patch border (the draped scene-kind renderer
74
+ * does) so the patch blends into the surrounding ground. A game never calls this directly — author a
75
+ * soil patch via the editor's `soil` studio kind.
76
+ * @internal
77
+ */
78
+ export function createSoilPatchMaterial(rules, edge) {
79
+ const seedOffset = hashSeed(rules.seed);
80
+ const material = new THREE.MeshStandardMaterial({
81
+ color: rules.baseColor,
82
+ roughness: 0.92,
83
+ metalness: 0,
84
+ ...(edge === undefined ? {} : { transparent: true, polygonOffset: true, polygonOffsetFactor: -2, polygonOffsetUnits: -2 }),
85
+ });
86
+ material.onBeforeCompile = (shader) => {
87
+ shader.uniforms.uSoilCrackColor = { value: new THREE.Color(rules.crackColor) };
88
+ shader.uniforms.uSoilCrackScale = { value: Math.max(0.1, rules.crackScale) };
89
+ shader.uniforms.uSoilCrackIntensity = { value: rules.crackIntensity };
90
+ shader.uniforms.uSoilMossColor = { value: new THREE.Color(rules.mossColor) };
91
+ shader.uniforms.uSoilMossCoverage = { value: rules.mossCoverage };
92
+ shader.uniforms.uSoilSeed = { value: seedOffset };
93
+ if (edge !== undefined) {
94
+ shader.uniforms.uSoilCenter = { value: new THREE.Vector2(edge.center[0], edge.center[1]) };
95
+ shader.uniforms.uSoilHalfSize = { value: new THREE.Vector2(edge.halfSize[0], edge.halfSize[1]) };
96
+ shader.uniforms.uSoilEdgeFade = { value: edge.fade ?? 1.6 };
97
+ shader.uniforms.uSoilGroundColor = { value: new THREE.Color(edge.groundColor ?? rules.baseColor) };
98
+ }
99
+ shader.vertexShader = shader.vertexShader.replace("#include <common>", `#include <common>
100
+ varying vec3 vJgSoilWorldPos;`).replace("#include <begin_vertex>", `#include <begin_vertex>
101
+ vJgSoilWorldPos = (modelMatrix * vec4(transformed, 1.0)).xyz;`);
102
+ shader.fragmentShader = shader.fragmentShader.replace("#include <common>", `#include <common>
103
+ varying vec3 vJgSoilWorldPos;
104
+ uniform vec3 uSoilCrackColor;
105
+ uniform float uSoilCrackScale;
106
+ uniform float uSoilCrackIntensity;
107
+ uniform vec3 uSoilMossColor;
108
+ uniform float uSoilMossCoverage;
109
+ uniform float uSoilSeed;
110
+ ${edge === undefined ? "" : "uniform vec2 uSoilCenter;\nuniform vec2 uSoilHalfSize;\nuniform float uSoilEdgeFade;\nuniform vec3 uSoilGroundColor;\n"}${SOIL_NOISE_GLSL}`).replace("#include <color_fragment>", `#include <color_fragment>
111
+ vec2 jgSoilP = vJgSoilWorldPos.xz + vec2(uSoilSeed * 0.37, uSoilSeed * 0.71);
112
+ float jgSoilCrack = jgSoilCrackMask(jgSoilP, uSoilCrackScale);
113
+ vec3 jgSoilCol = mix(diffuseColor.rgb, uSoilCrackColor, jgSoilCrack * uSoilCrackIntensity);
114
+ // Crack floors go darker still — paired with the relief normals they read as recessed, not painted.
115
+ jgSoilCol *= 1.0 - jgSoilCrack * uSoilCrackIntensity * 0.4;
116
+ // Sun-baked plate mottling so the dirt between cracks isn't one flat swatch.
117
+ jgSoilCol *= mix(0.92, 1.08, jgSoilFbm(jgSoilP / 1.1));
118
+ float jgSoilMossN = jgSoilFbm(jgSoilP / 2.4);
119
+ float jgSoilMossMask = smoothstep(1.0 - uSoilMossCoverage, 1.0 - uSoilMossCoverage + 0.18, jgSoilMossN);
120
+ // Fine clump break-up: moss creeps in speckled colonies (and pools into the cracks), not airbrushed blobs.
121
+ jgSoilMossMask *= smoothstep(0.3, 0.75, jgSoilFbm(jgSoilP / 0.55));
122
+ jgSoilMossMask = min(1.0, jgSoilMossMask + jgSoilMossMask * jgSoilCrack * 0.8);
123
+ jgSoilCol = mix(jgSoilCol, uSoilMossColor * mix(0.85, 1.1, jgSoilFbm(jgSoilP / 0.9)), jgSoilMossMask);${edge === undefined
124
+ ? "\ndiffuseColor.rgb = jgSoilCol;"
125
+ : `
126
+ // Border: color walks toward the terrain tint across the fade band (alpha alone left a decal
127
+ // seam — the dirt must *become* the ground it meets), then a noise-eaten alpha finishes it.
128
+ vec2 jgSoilEdgeD = uSoilHalfSize - abs(vJgSoilWorldPos.xz - uSoilCenter);
129
+ float jgSoilEdgeDist = min(jgSoilEdgeD.x, jgSoilEdgeD.y);
130
+ float jgSoilEdgeNoise = jgSoilFbm(jgSoilP * 0.9) * uSoilEdgeFade;
131
+ float jgSoilInside = clamp((jgSoilEdgeDist - jgSoilEdgeNoise) / max(uSoilEdgeFade, 0.001) + 0.55, 0.0, 1.0);
132
+ jgSoilCol = mix(uSoilGroundColor, jgSoilCol, smoothstep(0.0, 0.85, jgSoilInside));
133
+ diffuseColor.rgb = jgSoilCol;
134
+ diffuseColor.a *= clamp(jgSoilInside * 1.5, 0.0, 1.0);`}`).replace("#include <normal_fragment_maps>", `#include <normal_fragment_maps>
135
+ // Crack relief: tilt normals along the Worley gradient so crack walls catch and lose the light
136
+ // (finite differences over the same field that colors the cracks — they self-shadow in sync).
137
+ vec2 jgSoilNp = vJgSoilWorldPos.xz + vec2(uSoilSeed * 0.37, uSoilSeed * 0.71);
138
+ float jgSoilNe = 0.05;
139
+ vec2 jgSoilGrad = vec2(
140
+ jgSoilCrackMask(jgSoilNp + vec2(jgSoilNe, 0.0), uSoilCrackScale) - jgSoilCrackMask(jgSoilNp - vec2(jgSoilNe, 0.0), uSoilCrackScale),
141
+ jgSoilCrackMask(jgSoilNp + vec2(0.0, jgSoilNe), uSoilCrackScale) - jgSoilCrackMask(jgSoilNp - vec2(0.0, jgSoilNe), uSoilCrackScale)
142
+ ) / (2.0 * jgSoilNe);
143
+ normal = normalize(normal + vec3(jgSoilGrad.x, 0.0, jgSoilGrad.y) * uSoilCrackIntensity * 0.2);`);
144
+ };
145
+ material.customProgramCacheKey = () => `jgengine-soil-${rules.crackScale}-${rules.crackIntensity}-${rules.mossCoverage}-${seedOffset}-${edge === undefined ? "solid" : "faded"}`;
146
+ return material;
147
+ }
@@ -4,11 +4,28 @@ import type { ResolvedTerrainDetail } from "@jgengine/core/world/terrain";
4
4
  export interface TerrainDetailMaterialHandle {
5
5
  material: THREE.MeshStandardMaterial;
6
6
  }
7
+ /**
8
+ * Loaded PBR textures matching a resolved `ResolvedTerrainDetailMaterial.maps`' roles. The caller
9
+ * (a React component, via `useTexture`/`useLoader`) owns loading and disposal; `core` never touches
10
+ * `THREE.Texture` and this shell function never fetches a URL itself.
11
+ */
12
+ export interface TerrainDetailMaterialTextures {
13
+ color: THREE.Texture;
14
+ normal: THREE.Texture;
15
+ roughness: THREE.Texture;
16
+ ao: THREE.Texture;
17
+ }
7
18
  /**
8
19
  * A `MeshStandardMaterial` whose fragment shader keeps the biome-tinted vertex
9
20
  * colour as the base ground and blends procedural, noise-broken rock (by slope),
10
21
  * sand (by waterline), and snow (by height) over it — textured-reading terrain
11
22
  * with no image assets. Full PBR: lit, shadowed, and fogged like any standard
12
23
  * material, so it composes with the post-processing chain.
13
- */
14
- export declare function createTerrainDetailMaterial(detail: ResolvedTerrainDetail): TerrainDetailMaterialHandle;
24
+ *
25
+ * When `detail.material` is set, pass the matching loaded `textures` (color/normal/roughness/ao):
26
+ * they tile by world position at `detail.material.repeat` world units per tile and blend over the
27
+ * procedural result at `detail.material.strength` — the real-texture seam layers onto the existing
28
+ * look instead of replacing it.
29
+ * @internal
30
+ */
31
+ export declare function createTerrainDetailMaterial(detail: ResolvedTerrainDetail, textures?: TerrainDetailMaterialTextures): TerrainDetailMaterialHandle;
@@ -1,6 +1,16 @@
1
1
  import * as THREE from "three";
2
2
  const NOISE_GLSL = /* glsl */ `
3
- float jgHash(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
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
+ }
4
14
  float jgVNoise(vec2 p){
5
15
  vec2 i = floor(p); vec2 f = fract(p);
6
16
  vec2 u = f * f * (3.0 - 2.0 * f);
@@ -22,8 +32,17 @@ float jgFbm(vec2 p){
22
32
  * sand (by waterline), and snow (by height) over it — textured-reading terrain
23
33
  * with no image assets. Full PBR: lit, shadowed, and fogged like any standard
24
34
  * material, so it composes with the post-processing chain.
25
- */
26
- export function createTerrainDetailMaterial(detail) {
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;
27
46
  const uniforms = {
28
47
  uRockColor: { value: new THREE.Color(detail.rockColor) },
29
48
  uSandColor: { value: new THREE.Color(detail.sandColor) },
@@ -34,6 +53,16 @@ export function createTerrainDetailMaterial(detail) {
34
53
  uDetailScale: { value: Math.max(0.5, detail.detailScale) },
35
54
  uMacroScale: { value: Math.max(0.5, detail.macroScale) },
36
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
+ : {}),
37
66
  };
38
67
  const material = new THREE.MeshStandardMaterial({
39
68
  color: "#ffffff",
@@ -51,6 +80,36 @@ varying vec3 vJgWorldNormal;`)
51
80
  vJgWorldNormal = normalize(mat3(modelMatrix) * objectNormal);`)
52
81
  .replace("#include <begin_vertex>", `#include <begin_vertex>
53
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
+ : "";
54
113
  shader.fragmentShader = shader.fragmentShader
55
114
  .replace("#include <common>", `#include <common>
56
115
  varying vec3 vJgWorldPos;
@@ -64,16 +123,30 @@ uniform float uWaterLevel;
64
123
  uniform float uDetailScale;
65
124
  uniform float uMacroScale;
66
125
  uniform float uStrength;
126
+ ${materialUniformsGlsl}
67
127
  ${NOISE_GLSL}`)
68
128
  .replace("#include <color_fragment>", `#include <color_fragment>
69
129
  vec2 jgWp = vJgWorldPos.xz;
70
130
  float jgFine = jgFbm(jgWp / uDetailScale);
71
131
  float jgMacro = jgFbm(jgWp / uMacroScale);
132
+ float jgPatch = jgFbm(jgWp / (uMacroScale * 0.35) + vec2(37.2, 11.7));
72
133
  float jgSlope = 1.0 - clamp(vJgWorldNormal.y, 0.0, 1.0);
73
134
  float jgH = vJgWorldPos.y;
135
+ // Blade-scale flecks fade with view distance so close ground reads as turf without
136
+ // shimmering into noise on the horizon.
137
+ float jgViewDist = length(vJgWorldPos - cameraPosition);
138
+ float jgNear = 1.0 - smoothstep(14.0, 70.0, jgViewDist);
139
+ float jgFleck = jgVNoise(jgWp / max(0.05, uDetailScale * 0.024));
140
+ float jgFleckMid = jgVNoise(jgWp / max(0.12, uDetailScale * 0.07) + vec2(51.3, 7.9));
74
141
  vec3 jgBase = diffuseColor.rgb;
75
- jgBase *= mix(1.0 - 0.22 * uStrength, 1.0 + 0.18 * uStrength, jgFine);
76
- jgBase = mix(jgBase, jgBase * vec3(0.86, 0.9, 0.82), jgMacro * 0.25 * uStrength);
142
+ jgBase *= mix(1.0 - 0.2 * uStrength, 1.0 + 0.16 * uStrength, jgFine);
143
+ // Meadow patchwork: hue-shifted sun-dried sweeps and cooler lush pockets, not just darker spots.
144
+ jgBase = mix(jgBase, jgBase * vec3(1.14, 1.05, 0.76), smoothstep(0.5, 0.8, jgMacro) * 0.55 * uStrength);
145
+ jgBase = mix(jgBase, jgBase * vec3(0.78, 1.0, 0.86), smoothstep(0.52, 0.85, jgPatch) * 0.5 * uStrength);
146
+ // Rolling large-scale light/dark sweeps keep far hills from flattening into one tone.
147
+ jgBase *= 1.0 + (jgFbm(jgWp / (uMacroScale * 2.3) + vec2(9.1, 73.4)) - 0.5) * 0.22 * uStrength;
148
+ jgBase *= 1.0 + (jgFleck - 0.5) * 0.48 * uStrength * jgNear;
149
+ jgBase *= 1.0 + (jgFleckMid - 0.5) * 0.34 * uStrength;
77
150
  vec3 jgRock = uRockColor * mix(0.72, 1.15, jgFbm(jgWp / (uDetailScale * 1.7)));
78
151
  float jgRockW = smoothstep(uRockSlopeStart, uRockSlopeStart + 0.18, jgSlope) * uStrength;
79
152
  vec3 jgCol = mix(jgBase, jgRock, jgRockW);
@@ -83,8 +156,15 @@ jgCol = mix(jgCol, jgSand, jgSandW);
83
156
  vec3 jgSnow = uSnowColor * mix(0.94, 1.04, jgFine);
84
157
  float jgSnowW = smoothstep(uSnowHeight, uSnowHeight + 8.0, jgH) * (1.0 - jgRockW * 0.6) * uStrength;
85
158
  jgCol = mix(jgCol, jgSnow, jgSnowW);
86
- diffuseColor.rgb = jgCol;`);
159
+ ${materialColorGlsl}
160
+ diffuseColor.rgb = jgCol;`)
161
+ .replace("#include <roughnessmap_fragment>", `#include <roughnessmap_fragment>
162
+ ${materialRoughnessGlsl}`)
163
+ .replace("#include <normal_fragment_maps>", `#include <normal_fragment_maps>
164
+ ${materialNormalGlsl}`)
165
+ .replace("#include <aomap_fragment>", `#include <aomap_fragment>
166
+ ${materialAoGlsl}`);
87
167
  };
88
- material.customProgramCacheKey = () => `jgengine-terrain-detail-${detail.rockSlopeStart}-${detail.snowHeight}`;
168
+ material.customProgramCacheKey = () => `jgengine-terrain-detail-${detail.rockSlopeStart}-${detail.snowHeight}-${hasTexture ? "tex" : "flat"}`;
89
169
  return { material };
90
170
  }
@@ -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;