@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,20 @@
1
+ import { type ContextMenu } from "@jgengine/core/interaction/contextMenu";
2
+ import type { Aim } from "@jgengine/core/scene/spatial";
3
+ import type { GameContext } from "@jgengine/core/runtime/gameContext";
4
+ import type { PointerService } from "./pointer/pointerService.js";
5
+ /** Minimal playable slice for context-menu verb lookup. @internal */
6
+ export type PointerPlayable = {
7
+ content: {
8
+ entityById?: (name: string) => {
9
+ verbs?: ContextMenu["verbs"];
10
+ } | null | undefined;
11
+ };
12
+ };
13
+ /** Aim from the possessed/local player eye toward the current pointer world hit. @internal */
14
+ export declare function pointerAimFor(ctx: GameContext, service: PointerService): Aim | undefined;
15
+ /** Context menu for an entity/object pointer hit, or null for empty ground. @internal */
16
+ export declare function pointerContextMenu(ctx: GameContext, playable: PointerPlayable, hit: {
17
+ point: readonly [number, number, number];
18
+ entity: string | null;
19
+ object: string | null;
20
+ }): ContextMenu | null;
@@ -0,0 +1,28 @@
1
+ import { buildContextMenu, } from "@jgengine/core/interaction/contextMenu";
2
+ import { aimToPoint } from "@jgengine/core/input/pointer";
3
+ import { eyeHeightFromColliders } from "@jgengine/core/combat/shotOrigin";
4
+ /** Aim from the possessed/local player eye toward the current pointer world hit. @internal */
5
+ export function pointerAimFor(ctx, service) {
6
+ const hit = service.worldHit();
7
+ if (hit === null)
8
+ return undefined;
9
+ const shooter = ctx.scene.entity.get(ctx.player.possession.active(ctx.player.userId)) ??
10
+ ctx.scene.entity.get(ctx.player.userId);
11
+ if (shooter === null)
12
+ return undefined;
13
+ const eye = eyeHeightFromColliders(ctx.scene.entity.collidersOf(shooter.id));
14
+ return aimToPoint([shooter.position[0], shooter.position[1] + eye, shooter.position[2]], hit.point);
15
+ }
16
+ /** Context menu for an entity/object pointer hit, or null for empty ground. @internal */
17
+ export function pointerContextMenu(ctx, playable, hit) {
18
+ if (hit.entity !== null) {
19
+ const entity = ctx.scene.entity.get(hit.entity);
20
+ const verbs = entity === null ? undefined : playable.content.entityById?.(entity.name)?.verbs;
21
+ return buildContextMenu({ kind: "entity", targetId: hit.entity, verbs, point: hit.point });
22
+ }
23
+ if (hit.object !== null) {
24
+ const verbs = ctx.scene.object.catalog(hit.object)?.verbs;
25
+ return buildContextMenu({ kind: "object", targetId: hit.object, verbs, point: hit.point });
26
+ }
27
+ return null;
28
+ }
@@ -5,4 +5,8 @@ export interface PlacementGhostProps {
5
5
  validColor?: string;
6
6
  invalidColor?: string;
7
7
  }
8
+ /**
9
+ * Cursor-following build ghost: valid/invalid tint from a placement controller preview.
10
+ * @capability placement-ghost render valid/invalid placement preview mesh
11
+ */
8
12
  export declare function PlacementGhost({ preview, height, validColor, invalidColor, }: PlacementGhostProps): import("react").JSX.Element | null;
@@ -1,6 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useMemo } from "react";
3
3
  import { quarterTurnsToRotationY } from "@jgengine/core/world/placementController";
4
+ /**
5
+ * Cursor-following build ghost: valid/invalid tint from a placement controller preview.
6
+ * @capability placement-ghost render valid/invalid placement preview mesh
7
+ */
4
8
  export function PlacementGhost({ preview, height = 1, validColor = "#34d399", invalidColor = "#f87171", }) {
5
9
  const footprint = preview?.footprint;
6
10
  const size = useMemo(() => [footprint?.w ?? 1, height, footprint?.d ?? 1], [footprint?.w, footprint?.d, height]);
@@ -0,0 +1,43 @@
1
+ /** Active TransformControls mode for the runtime selection gizmo. */
2
+ export type TransformGizmoMode = "translate" | "rotate" | "scale";
3
+ /** Snap policy: grid quantize, free drag, or ground-height sample on release. */
4
+ export type TransformGizmoSnap = "grid" | "free" | "ground";
5
+ /** Pose reported by {@link TransformGizmo} when the user releases a drag. */
6
+ export interface TransformGizmoPose {
7
+ position: {
8
+ x: number;
9
+ y: number;
10
+ z: number;
11
+ };
12
+ rotationY: number;
13
+ scale: {
14
+ x: number;
15
+ y: number;
16
+ z: number;
17
+ };
18
+ }
19
+ /** Props for the shared runtime/editor selection gizmo. */
20
+ export interface TransformGizmoProps {
21
+ position: {
22
+ x: number;
23
+ y: number;
24
+ z: number;
25
+ };
26
+ rotationY?: number;
27
+ mode?: TransformGizmoMode;
28
+ snapMode?: TransformGizmoSnap;
29
+ gridSize?: number;
30
+ /** Vertical offset so the gizmo sits above ground markers. */
31
+ lift?: number;
32
+ size?: number;
33
+ groundSnap?: (x: number, z: number) => number;
34
+ /** Disable orbit/pan while dragging (shell/editor cameras). */
35
+ onDraggingChange?: (dragging: boolean) => void;
36
+ onRelease: (pose: TransformGizmoPose) => void;
37
+ }
38
+ /**
39
+ * Runtime selection/move gizmo shared by games and the editor.
40
+ * Wraps TransformControls; callers own selection and commit side-effects.
41
+ * @capability transform-gizmo in-game select/move/rotate/scale gizmo
42
+ */
43
+ export declare const TransformGizmo: import("react").MemoExoticComponent<({ position, rotationY, mode, snapMode, gridSize, lift, size, groundSnap, onDraggingChange, onRelease, }: TransformGizmoProps) => import("react").JSX.Element>;
@@ -0,0 +1,55 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { TransformControls } from "@react-three/drei";
3
+ import { useThree } from "@react-three/fiber";
4
+ import { memo, useEffect, useRef, useState } from "react";
5
+ /**
6
+ * Runtime selection/move gizmo shared by games and the editor.
7
+ * Wraps TransformControls; callers own selection and commit side-effects.
8
+ * @capability transform-gizmo in-game select/move/rotate/scale gizmo
9
+ */
10
+ export const TransformGizmo = memo(function TransformGizmo({ position, rotationY = 0, mode = "translate", snapMode = "free", gridSize = 1, lift = 0, size = 0.85, groundSnap, onDraggingChange, onRelease, }) {
11
+ const groupRef = useRef(null);
12
+ const [object, setObject] = useState(null);
13
+ const draggingRef = useRef(false);
14
+ const controls = useThree((state) => state.controls);
15
+ const anchorKey = `${position.x}:${position.y}:${position.z}:${rotationY}:${mode}`;
16
+ useEffect(() => {
17
+ setObject(groupRef.current);
18
+ }, [anchorKey]);
19
+ useEffect(() => {
20
+ const group = groupRef.current;
21
+ if (group === null || draggingRef.current)
22
+ return;
23
+ group.position.set(position.x, position.y + lift, position.z);
24
+ group.rotation.set(0, rotationY, 0);
25
+ group.scale.set(1, 1, 1);
26
+ }, [position.x, position.y, position.z, rotationY, lift, anchorKey, mode]);
27
+ const snapProps = snapMode === "grid"
28
+ ? { translationSnap: gridSize, rotationSnap: Math.PI / 12 }
29
+ : { rotationSnap: Math.PI / 12 };
30
+ const handleRelease = () => {
31
+ draggingRef.current = false;
32
+ if (controls !== null && "enabled" in controls)
33
+ controls.enabled = true;
34
+ onDraggingChange?.(false);
35
+ const current = groupRef.current;
36
+ if (current === null)
37
+ return;
38
+ let y = current.position.y - lift;
39
+ if (snapMode === "ground" && groundSnap !== undefined) {
40
+ y = groundSnap(current.position.x, current.position.z);
41
+ current.position.y = y + lift;
42
+ }
43
+ onRelease({
44
+ position: { x: current.position.x, y, z: current.position.z },
45
+ rotationY: current.rotation.y,
46
+ scale: { x: current.scale.x, y: current.scale.y, z: current.scale.z },
47
+ });
48
+ };
49
+ return (_jsxs(_Fragment, { children: [_jsx("group", { ref: groupRef }), object !== null ? (_jsx(TransformControls, { object: object, mode: mode, size: size, ...snapProps, onMouseDown: () => {
50
+ draggingRef.current = true;
51
+ if (controls !== null && "enabled" in controls)
52
+ controls.enabled = false;
53
+ onDraggingChange?.(true);
54
+ }, onMouseUp: handleRelease })) : null] }));
55
+ });
@@ -1,2 +1,3 @@
1
1
  export { BuildingBlock, GeneratedBuilding, InstancedBuildings, type BuildingBlockProps, type GeneratedBuildingProps, type InstancedBuildingPlacement, type InstancedBuildingsProps, } from "./GeneratedBuilding.js";
2
2
  export { PlacementGhost, type PlacementGhostProps } from "./PlacementGhost.js";
3
+ export { TransformGizmo, type TransformGizmoMode, type TransformGizmoPose, type TransformGizmoProps, type TransformGizmoSnap, } from "./TransformGizmo.js";
@@ -1,2 +1,3 @@
1
1
  export { BuildingBlock, GeneratedBuilding, InstancedBuildings, } from "./GeneratedBuilding.js";
2
2
  export { PlacementGhost } from "./PlacementGhost.js";
3
+ export { TransformGizmo, } from "./TransformGizmo.js";
@@ -1,4 +1,5 @@
1
1
  import { type ThreeElements } from "@react-three/fiber";
2
+ import * as THREE from "three";
2
3
  import type { TerrainField } from "@jgengine/core/world/terrain";
3
4
  import { type FieldGroundOptions } from "./terrainMath.js";
4
5
  export interface CarvedTerrainProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material"> {
@@ -12,6 +13,8 @@ export interface CarvedTerrainProps extends Omit<ThreeElements["mesh"], "args" |
12
13
  paletteAt?: FieldGroundOptions["paletteAt"];
13
14
  roughness?: number;
14
15
  metalness?: number;
16
+ /** Override the default vertex-colour material (e.g. a procedural detail material). The caller owns its lifecycle; it is not disposed here. */
17
+ surfaceMaterial?: THREE.Material;
15
18
  /** Bump after a runtime carve/deposit to re-mesh the deformed surface. */
16
19
  epoch?: number;
17
20
  }
@@ -20,4 +23,4 @@ export interface CarvedTerrainProps extends Omit<ThreeElements["mesh"], "args" |
20
23
  * Because the geometry samples `field.sampleHeight`, a `CarvableField.carve(...)` shows as a real bowl
21
24
  * once `epoch` changes. Pair with `InstancedBodies` to see debris resting in the crater it blasted.
22
25
  */
23
- export declare function CarvedTerrain({ field, size, segments, center, colors, heightRange, paletteAt, roughness, metalness, receiveShadow, epoch, ...meshProps }: CarvedTerrainProps): import("react").JSX.Element;
26
+ export declare function CarvedTerrain({ field, size, segments, center, colors, heightRange, paletteAt, roughness, metalness, surfaceMaterial, receiveShadow, epoch, ...meshProps }: CarvedTerrainProps): import("react").JSX.Element;
@@ -8,12 +8,13 @@ import { createFieldGroundGeometry } from "./terrainMath.js";
8
8
  * Because the geometry samples `field.sampleHeight`, a `CarvableField.carve(...)` shows as a real bowl
9
9
  * once `epoch` changes. Pair with `InstancedBodies` to see debris resting in the crater it blasted.
10
10
  */
11
- export function CarvedTerrain({ field, size, segments, center, colors, heightRange, paletteAt, roughness = 0.95, metalness = 0, receiveShadow = true, epoch = 0, ...meshProps }) {
11
+ export function CarvedTerrain({ field, size, segments, center, colors, heightRange, paletteAt, roughness = 0.95, metalness = 0, surfaceMaterial, receiveShadow = true, epoch = 0, ...meshProps }) {
12
12
  const geometry = useMemo(() => createFieldGroundGeometry(field, { size, segments, center, colors, heightRange, paletteAt }),
13
13
  // eslint-disable-next-line react-hooks/exhaustive-deps
14
14
  [field, size, segments, center, colors, heightRange, paletteAt, epoch]);
15
- const material = useMemo(() => new THREE.MeshStandardMaterial({ color: "#ffffff", roughness, metalness, vertexColors: true }), [metalness, roughness]);
15
+ const defaultMaterial = useMemo(() => new THREE.MeshStandardMaterial({ color: "#ffffff", roughness, metalness, vertexColors: true }), [metalness, roughness]);
16
+ const material = surfaceMaterial ?? defaultMaterial;
16
17
  useEffect(() => () => geometry.dispose(), [geometry]);
17
- useEffect(() => () => material.dispose(), [material]);
18
+ useEffect(() => () => defaultMaterial.dispose(), [defaultMaterial]);
18
19
  return _jsx("mesh", { ...meshProps, geometry: geometry, material: material, receiveShadow: receiveShadow });
19
20
  }
@@ -23,7 +23,7 @@ export function GrassField({ count = DEFAULT_GRASS_COUNT, density = DEFAULT_GRAS
23
23
  wind,
24
24
  roughness,
25
25
  }), [colorBase, colorTip, colorVariation, roughness, wind]);
26
- geometry.instanceCount = resolveGrassInstanceBudget(count, density, budget);
26
+ geometry.instanceCount = resolveGrassInstanceBudget(count, density, area, budget);
27
27
  useFrame((state) => {
28
28
  handle.uniforms.uTime.value = state.clock.elapsedTime;
29
29
  });
@@ -2,7 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  const MODE_COLOR = {
3
3
  raise: "#38bdf8",
4
4
  lower: "#fb923c",
5
+ smooth: "#c084fc",
5
6
  flatten: "#a3e635",
7
+ noise: "#facc15",
8
+ ramp: "#2dd4bf",
6
9
  paint: "#e879f9",
7
10
  };
8
11
  export function TerraformBrushCursor({ center, y = 0.05, radius, mode }) {
@@ -1,3 +1,12 @@
1
+ import { type TerrainArea } from "./terrainMath.js";
1
2
  export declare const DEFAULT_GRASS_COUNT = 1500;
2
- export declare const DEFAULT_GRASS_DENSITY = 1;
3
- export declare function resolveGrassInstanceBudget(count: number, density: number, budget?: number): number;
3
+ /** Blades per square meter — matches `@jgengine/core/world/vegetation`'s `VEGETATION_DEFAULTS.density`, so an editor-placed grass volume and a bare `<GrassField>` agree on what "4" means. */
4
+ export declare const DEFAULT_GRASS_DENSITY = 4;
5
+ /**
6
+ * Blade instance count for a patch: `density` (blades/m²) times the patch's
7
+ * `area`, capped at `budget` (defaulting to `count`, the buffer's allocated
8
+ * capacity) so a big field never exceeds the perf ceiling — it just renders
9
+ * sparser than requested and logs a devtools warning when that happens.
10
+ * @internal
11
+ */
12
+ export declare function resolveGrassInstanceBudget(count: number, density: number, area: TerrainArea, budget?: number): number;
@@ -1,6 +1,22 @@
1
+ import { devtools } from "@jgengine/core/devtools/devtools";
2
+ import { resolveTerrainSize } from "./terrainMath.js";
1
3
  export const DEFAULT_GRASS_COUNT = 1500;
2
- export const DEFAULT_GRASS_DENSITY = 1;
3
- export function resolveGrassInstanceBudget(count, density, budget) {
4
- const capped = budget === undefined ? count : Math.min(count, Math.max(0, Math.floor(budget)));
5
- return Math.floor(Math.max(0, Math.min(1, density)) * capped);
4
+ /** Blades per square meter — matches `@jgengine/core/world/vegetation`'s `VEGETATION_DEFAULTS.density`, so an editor-placed grass volume and a bare `<GrassField>` agree on what "4" means. */
5
+ export const DEFAULT_GRASS_DENSITY = 4;
6
+ /**
7
+ * Blade instance count for a patch: `density` (blades/m²) times the patch's
8
+ * `area`, capped at `budget` (defaulting to `count`, the buffer's allocated
9
+ * capacity) so a big field never exceeds the perf ceiling — it just renders
10
+ * sparser than requested and logs a devtools warning when that happens.
11
+ * @internal
12
+ */
13
+ export function resolveGrassInstanceBudget(count, density, area, budget) {
14
+ const size = resolveTerrainSize(area);
15
+ const areaSqMeters = Math.max(0, size.width) * Math.max(0, size.depth);
16
+ const requested = Math.max(0, density) * areaSqMeters;
17
+ const cap = budget === undefined ? count : Math.min(count, Math.max(0, Math.floor(budget)));
18
+ if (requested > cap) {
19
+ devtools.logs.push("warn", `[jgengine:grass] density ${density} blades/m² over ${areaSqMeters.toFixed(0)}m² wants ${Math.floor(requested)} blades — clamped to budget ${cap}`);
20
+ }
21
+ return Math.floor(Math.min(Math.max(0, cap), requested));
6
22
  }
@@ -22,6 +22,9 @@ export interface ResolvedGrassBladeGeometryOptions {
22
22
  bend: readonly [min: number, max: number];
23
23
  heightAt: TerrainHeightSampler;
24
24
  }
25
+ /** @internal */
25
26
  export declare function resolveGrassRange(value: GrassRange | undefined, fallback: readonly [number, number]): readonly [number, number];
27
+ /** @internal */
26
28
  export declare function resolveGrassBladeGeometryOptions(options?: GrassBladeGeometryOptions): ResolvedGrassBladeGeometryOptions;
29
+ /** @internal */
27
30
  export declare function createGrassBladeGeometry(options?: GrassBladeGeometryOptions): THREE.InstancedBufferGeometry;
@@ -1,11 +1,13 @@
1
1
  import * as THREE from "three";
2
2
  import { createSeededRandom } from "./random.js";
3
3
  import { resolveTerrainSize } from "./terrainMath.js";
4
+ /** @internal */
4
5
  export function resolveGrassRange(value, fallback) {
5
6
  if (value === undefined)
6
7
  return fallback;
7
8
  return typeof value === "number" ? [value, value] : value;
8
9
  }
10
+ /** @internal */
9
11
  export function resolveGrassBladeGeometryOptions(options = {}) {
10
12
  return {
11
13
  count: Math.max(0, Math.floor(options.count ?? 1500)),
@@ -21,6 +23,7 @@ export function resolveGrassBladeGeometryOptions(options = {}) {
21
23
  function randomRange(random, range) {
22
24
  return THREE.MathUtils.lerp(range[0], range[1], random());
23
25
  }
26
+ /** @internal */
24
27
  export function createGrassBladeGeometry(options = {}) {
25
28
  const resolved = resolveGrassBladeGeometryOptions(options);
26
29
  const size = resolveTerrainSize(resolved.area);
@@ -29,5 +29,7 @@ export interface GrassMaterialHandle {
29
29
  uniforms: GrassShaderUniforms;
30
30
  }
31
31
  export declare const DEFAULT_GRASS_WIND: Required<GrassWindOptions>;
32
+ /** @internal */
32
33
  export declare function resolveGrassWind(wind: GrassWindOptions | false | undefined): Required<GrassWindOptions>;
34
+ /** @internal */
33
35
  export declare function createGrassMaterial(options?: GrassMaterialOptions): GrassMaterialHandle;
@@ -10,6 +10,7 @@ function normalizeWindDirection(direction) {
10
10
  const vector = new THREE.Vector2(direction[0], direction[1]);
11
11
  return vector.lengthSq() === 0 ? new THREE.Vector2(1, 0) : vector.normalize();
12
12
  }
13
+ /** @internal */
13
14
  export function resolveGrassWind(wind) {
14
15
  if (wind === false)
15
16
  return { ...DEFAULT_GRASS_WIND, strength: 0, flutter: 0 };
@@ -21,6 +22,7 @@ export function resolveGrassWind(wind) {
21
22
  flutter: wind?.flutter ?? DEFAULT_GRASS_WIND.flutter,
22
23
  };
23
24
  }
25
+ /** @internal */
24
26
  export function createGrassMaterial(options = {}) {
25
27
  const wind = resolveGrassWind(options.wind);
26
28
  const uniforms = {
@@ -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,10 @@
1
+ import * as THREE from "three";
2
+ import type { SoilRules } from "@jgengine/core/world/soilKind";
3
+ /**
4
+ * A `MeshStandardMaterial` for one authored `soil` volume: a base dirt tint with a Worley crack-line
5
+ * network and an FBM moss-patch overlay injected via `onBeforeCompile`, seeded from `rules.seed`. Full
6
+ * PBR — lit, shadowed, fogged — like the terrain detail material it borrows its noise machinery from.
7
+ * A game never calls this directly — author a soil patch via the editor's `soil` studio kind.
8
+ * @internal
9
+ */
10
+ export declare function createSoilPatchMaterial(rules: SoilRules): THREE.MeshStandardMaterial;
@@ -0,0 +1,109 @@
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
+ `;
57
+ function hashSeed(text) {
58
+ let h = 2166136261 >>> 0;
59
+ for (let i = 0; i < text.length; i += 1) {
60
+ h ^= text.charCodeAt(i);
61
+ h = Math.imul(h, 16777619);
62
+ }
63
+ return (h >>> 0) % 100000;
64
+ }
65
+ /**
66
+ * A `MeshStandardMaterial` for one authored `soil` volume: a base dirt tint with a Worley crack-line
67
+ * network and an FBM moss-patch overlay injected via `onBeforeCompile`, seeded from `rules.seed`. Full
68
+ * PBR — lit, shadowed, fogged — like the terrain detail material it borrows its noise machinery from.
69
+ * A game never calls this directly — author a soil patch via the editor's `soil` studio kind.
70
+ * @internal
71
+ */
72
+ export function createSoilPatchMaterial(rules) {
73
+ const seedOffset = hashSeed(rules.seed);
74
+ const material = new THREE.MeshStandardMaterial({
75
+ color: rules.baseColor,
76
+ roughness: 0.92,
77
+ metalness: 0,
78
+ });
79
+ material.onBeforeCompile = (shader) => {
80
+ shader.uniforms.uSoilCrackColor = { value: new THREE.Color(rules.crackColor) };
81
+ shader.uniforms.uSoilCrackScale = { value: Math.max(0.1, rules.crackScale) };
82
+ shader.uniforms.uSoilCrackIntensity = { value: rules.crackIntensity };
83
+ shader.uniforms.uSoilMossColor = { value: new THREE.Color(rules.mossColor) };
84
+ shader.uniforms.uSoilMossCoverage = { value: rules.mossCoverage };
85
+ shader.uniforms.uSoilSeed = { value: seedOffset };
86
+ shader.vertexShader = shader.vertexShader.replace("#include <common>", `#include <common>
87
+ varying vec3 vJgSoilWorldPos;`).replace("#include <begin_vertex>", `#include <begin_vertex>
88
+ vJgSoilWorldPos = (modelMatrix * vec4(transformed, 1.0)).xyz;`);
89
+ shader.fragmentShader = shader.fragmentShader.replace("#include <common>", `#include <common>
90
+ varying vec3 vJgSoilWorldPos;
91
+ uniform vec3 uSoilCrackColor;
92
+ uniform float uSoilCrackScale;
93
+ uniform float uSoilCrackIntensity;
94
+ uniform vec3 uSoilMossColor;
95
+ uniform float uSoilMossCoverage;
96
+ uniform float uSoilSeed;
97
+ ${SOIL_NOISE_GLSL}`).replace("#include <color_fragment>", `#include <color_fragment>
98
+ vec2 jgSoilP = vJgSoilWorldPos.xz + vec2(uSoilSeed * 0.37, uSoilSeed * 0.71);
99
+ vec2 jgSoilF1F2 = jgSoilWorleyF1F2(jgSoilP / uSoilCrackScale);
100
+ float jgSoilCrack = 1.0 - smoothstep(0.0, 0.1, jgSoilF1F2.y - jgSoilF1F2.x);
101
+ vec3 jgSoilCol = mix(diffuseColor.rgb, uSoilCrackColor, jgSoilCrack * uSoilCrackIntensity);
102
+ float jgSoilMossN = jgSoilFbm(jgSoilP / 2.4);
103
+ float jgSoilMossMask = smoothstep(1.0 - uSoilMossCoverage, 1.0 - uSoilMossCoverage + 0.18, jgSoilMossN);
104
+ jgSoilCol = mix(jgSoilCol, uSoilMossColor, jgSoilMossMask);
105
+ diffuseColor.rgb = jgSoilCol;`);
106
+ };
107
+ material.customProgramCacheKey = () => `jgengine-soil-${rules.crackScale}-${rules.crackIntensity}-${rules.mossCoverage}-${seedOffset}`;
108
+ return material;
109
+ }
@@ -0,0 +1,31 @@
1
+ import * as THREE from "three";
2
+ import type { ResolvedTerrainDetail } from "@jgengine/core/world/terrain";
3
+ /** The built procedural detail terrain material, ready to mount on the ground mesh. */
4
+ export interface TerrainDetailMaterialHandle {
5
+ material: THREE.MeshStandardMaterial;
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
+ }
18
+ /**
19
+ * A `MeshStandardMaterial` whose fragment shader keeps the biome-tinted vertex
20
+ * colour as the base ground and blends procedural, noise-broken rock (by slope),
21
+ * sand (by waterline), and snow (by height) over it — textured-reading terrain
22
+ * with no image assets. Full PBR: lit, shadowed, and fogged like any standard
23
+ * material, so it composes with the post-processing chain.
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;