@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,173 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Fragment, useEffect, useMemo, useState } from "react";
3
+ import * as THREE from "three";
4
+ import { getDocumentLiveSync, subscribeDocumentLiveSyncInstall, } from "@jgengine/core/editor/index";
5
+ import { placeAuthoredObjects, resolveAuthoredObjects, } from "@jgengine/core/world/authoredObjects";
6
+ import { buildRoadRibbon, roundPathCorners } from "@jgengine/core/world/roads";
7
+ import { isScatterPath, resolveScatter } from "@jgengine/core/world/scatterRegion";
8
+ import { getAssetGenerator } from "@jgengine/core/scene/assetGenerator";
9
+ import { useGameContext } from "@jgengine/react/provider";
10
+ import { createModelMapResolver } from "../render/resolveModel.js";
11
+ import { InstancedScatter } from "../scatter/InstancedScatter.js";
12
+ import { GeneratedAssetInstance } from "./GeneratedAssetRenderer.js";
13
+ import { registerBuiltinSceneKindRenderers } from "./builtinSceneKindRenderers.js";
14
+ import { getSceneKindRenderer } from "./sceneKindRenderers.js";
15
+ /**
16
+ * Resolves the document AuthoredScene renders: when a {@link getDocumentLiveSync} bus is
17
+ * installed (editor host live), subscribe to document patches; otherwise use the prop.
18
+ * Document is authoritative — runtime overrides never land here unless written back.
19
+ * @internal
20
+ */
21
+ export function useLiveEditorDocument(document, live = true) {
22
+ const [resolved, setResolved] = useState(document);
23
+ useEffect(() => {
24
+ if (!live) {
25
+ setResolved(document);
26
+ return;
27
+ }
28
+ let unsubDoc;
29
+ const attach = () => {
30
+ unsubDoc?.();
31
+ unsubDoc = undefined;
32
+ const sync = getDocumentLiveSync();
33
+ if (sync === null) {
34
+ setResolved(document);
35
+ return;
36
+ }
37
+ setResolved(sync.getDocument());
38
+ unsubDoc = sync.subscribeDocument((event) => setResolved(event.document));
39
+ };
40
+ attach();
41
+ const unsubInstall = subscribeDocumentLiveSyncInstall(attach);
42
+ return () => {
43
+ unsubInstall();
44
+ unsubDoc?.();
45
+ };
46
+ }, [document, live]);
47
+ return live ? resolved : document;
48
+ }
49
+ registerBuiltinSceneKindRenderers();
50
+ function markerToKindObject(marker) {
51
+ return { id: marker.id, kind: marker.kind, position: marker.position, ...(marker.rotationY === undefined ? {} : { rotationY: marker.rotationY }), ...(marker.meta === undefined ? {} : { meta: marker.meta }) };
52
+ }
53
+ function volumeToKindObject(volume) {
54
+ return {
55
+ id: volume.id,
56
+ kind: volume.kind,
57
+ center: volume.center,
58
+ ...(volume.halfExtents === undefined ? {} : { halfExtents: volume.halfExtents }),
59
+ ...(volume.radius === undefined ? {} : { radius: volume.radius }),
60
+ ...(volume.meta === undefined ? {} : { meta: volume.meta }),
61
+ };
62
+ }
63
+ function pathToKindObject(path) {
64
+ return { id: path.id, kind: path.kind, points: path.points.map((point) => ({ x: point.x, y: point.y, z: point.z })), ...(path.meta === undefined ? {} : { meta: path.meta }) };
65
+ }
66
+ /** Renders every marker placed as a parametric generator asset, re-resolved from its `meta` + seed. */
67
+ function AuthoredGenerators({ document, field }) {
68
+ const markers = useMemo(() => document.markers.filter((marker) => typeof marker.meta?.["assetId"] === "string" && getAssetGenerator(marker.meta["assetId"]) !== undefined), [document.markers]);
69
+ return (_jsx(_Fragment, { children: markers.map((marker) => (_jsx(GeneratedAssetInstance, { meta: marker.meta, position: [marker.position.x, field.sampleHeight(marker.position.x, marker.position.z), marker.position.z], rotationY: marker.rotationY ?? 0 }, marker.id))) }));
70
+ }
71
+ /**
72
+ * Mounts every registered studio renderer for the kinds present in a document — water and any
73
+ * game/example-registered kind — grouping objects by kind and handing each renderer its slice.
74
+ * Scatter stays on the batched {@link InstancedScatter} path (its cross-region avoid masks need the
75
+ * whole document), so it is skipped here.
76
+ */
77
+ function AuthoredStudios({ document, context }) {
78
+ const byKind = useMemo(() => {
79
+ const groups = new Map();
80
+ const add = (kind, object) => {
81
+ if (isScatterPath({ kind }) || getSceneKindRenderer(kind) === undefined)
82
+ return;
83
+ const bucket = groups.get(kind);
84
+ if (bucket === undefined)
85
+ groups.set(kind, [object]);
86
+ else
87
+ bucket.push(object);
88
+ };
89
+ for (const marker of document.markers)
90
+ add(marker.kind, markerToKindObject(marker));
91
+ for (const volume of document.volumes)
92
+ add(volume.kind, volumeToKindObject(volume));
93
+ for (const path of document.paths)
94
+ add(path.kind, pathToKindObject(path));
95
+ return groups;
96
+ }, [document]);
97
+ return (_jsx(_Fragment, { children: [...byKind.entries()].map(([kind, objects]) => {
98
+ const renderer = getSceneKindRenderer(kind);
99
+ return renderer === undefined ? null : _jsx(Fragment, { children: renderer({ objects, context }) }, kind);
100
+ }) }));
101
+ }
102
+ const DEFAULT_PATH_COLOR = "#7a6444";
103
+ /** One editor path drawn as a ground-draped ribbon — subdivided and sampled against the live field. */
104
+ function DrapedPath({ points, width, color, field, }) {
105
+ const geometry = useMemo(() => {
106
+ // Fillet sharp turns first so bends read as smooth arcs, not overlapping rectangles.
107
+ const rounded = roundPathCorners(points, Math.max(1.5, width * 0.9), 6);
108
+ const ribbon = buildRoadRibbon(rounded, width, (x, z) => field.sampleHeight(x, z), {
109
+ elevation: 0.18,
110
+ maxSegmentLength: 1,
111
+ });
112
+ const geo = new THREE.BufferGeometry();
113
+ geo.setAttribute("position", new THREE.BufferAttribute(ribbon.positions, 3));
114
+ geo.setIndex(new THREE.BufferAttribute(ribbon.indices, 1));
115
+ geo.computeVertexNormals();
116
+ return geo;
117
+ // eslint-disable-next-line react-hooks/exhaustive-deps
118
+ }, [points, width, field]);
119
+ useEffect(() => () => geometry.dispose(), [geometry]);
120
+ if (points.length < 2)
121
+ return null;
122
+ return (_jsx("mesh", { geometry: geometry, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: color, roughness: 1, metalness: 0, side: THREE.DoubleSide }) }));
123
+ }
124
+ function pathColor(meta, color) {
125
+ if (typeof meta?.["color"] === "string")
126
+ return meta["color"];
127
+ return color ?? DEFAULT_PATH_COLOR;
128
+ }
129
+ /**
130
+ * Renders a document's non-scatter paths (roads, routes, corridors) as ground-draped ribbons — the
131
+ * editor authors the polyline, the engine drapes it over the live terrain at runtime. Width comes
132
+ * from `path.width`, color from `path.meta.color`/`path.color`. A game never hand-rolls path meshes.
133
+ */
134
+ export function AuthoredPaths({ document, field, kinds }) {
135
+ const paths = document.paths.filter((path) => !isScatterPath(path) && getSceneKindRenderer(path.kind) === undefined && (kinds === undefined || kinds.includes(path.kind)));
136
+ return (_jsx(_Fragment, { children: paths.map((path) => (_jsx(DrapedPath, { points: path.points.map((point) => [point.x, point.z]), width: path.width ?? 4, color: pathColor(path.meta, path.color), field: field }, path.id))) }));
137
+ }
138
+ /**
139
+ * Places every editor marker with a catalog id into `ctx.scene.object`, grounded on `field`.
140
+ * WorldScene renders them through the game's `objectModels` seam — no bespoke place loop. Pure
141
+ * headless twin: `resolveAuthoredObjects` / `placeAuthoredObjects`.
142
+ */
143
+ export function AuthoredObjects({ document, field, verticalOffset = 0, onExisting = "keep", }) {
144
+ const ctx = useGameContext();
145
+ const objects = useMemo(() => resolveAuthoredObjects(document), [document]);
146
+ useEffect(() => {
147
+ if (objects.length === 0)
148
+ return;
149
+ placeAuthoredObjects(ctx.scene.object, objects, (x, z) => field.sampleHeight(x, z), {
150
+ verticalOffset,
151
+ onExisting,
152
+ });
153
+ }, [ctx.scene.object, field, objects, onExisting, verticalOffset]);
154
+ return null;
155
+ }
156
+ /**
157
+ * Renders an editor document's scene content — draped paths plus GPU-instanced foliage — from one
158
+ * mount, grounded on the live `field`. The runtime counterpart to authoring a scene in the editor:
159
+ * drag paths and foliage regions, save `editor.scene.json`, and the game plays them with no bespoke
160
+ * render code. When a live-sync bus is installed (editor host), document patches stream in and
161
+ * re-render automatically — document is authoritative; runtime overrides stay ephemeral unless
162
+ * written back. Terrain/collision come from the world's ground field (`environment({ sculpt })`);
163
+ * place markers with your own entity spawns. Pass `scatterModels`+`assets` to resolve palette items to
164
+ * real catalog GLBs; unmapped items keep the stylized proxy.
165
+ */
166
+ export function AuthoredScene({ document, field, pathKinds, scatterModels, assets, live = true, placeObjects, }) {
167
+ const liveDocument = useLiveEditorDocument(document, live);
168
+ const instances = useMemo(() => resolveScatter(liveDocument, field), [liveDocument, field]);
169
+ const resolveItem = useMemo(() => createModelMapResolver(scatterModels, assets, "scatterModels"), [scatterModels, assets]);
170
+ const shouldPlaceObjects = placeObjects === true || (typeof placeObjects === "object" && placeObjects !== null);
171
+ const objectVerticalOffset = typeof placeObjects === "object" && placeObjects !== null ? (placeObjects.verticalOffset ?? 0) : 0;
172
+ return (_jsxs(_Fragment, { children: [_jsx(AuthoredPaths, { document: liveDocument, field: field, ...(pathKinds === undefined ? {} : { kinds: pathKinds }) }), _jsx(InstancedScatter, { instances: instances, ...(resolveItem === undefined ? {} : { resolveItem }) }), _jsx(AuthoredStudios, { document: liveDocument, context: { document: liveDocument, field, ...(assets === undefined ? {} : { assets }) } }), _jsx(AuthoredGenerators, { document: liveDocument, field: field }), shouldPlaceObjects ? (_jsx(AuthoredObjects, { document: liveDocument, field: field, verticalOffset: objectVerticalOffset })) : null] }));
173
+ }
@@ -0,0 +1,14 @@
1
+ /** Props for {@link GeneratedAsset}: the placed instance's `meta` (assetId + params + seed) and transform. */
2
+ export interface GeneratedAssetProps {
3
+ /** The placed instance meta — `{ assetId, seed, ...params }`. Re-resolved every render, never baked. */
4
+ meta: Record<string, unknown> | undefined;
5
+ position?: readonly [number, number, number];
6
+ rotationY?: number;
7
+ }
8
+ /**
9
+ * Renders one placed generator-asset instance by re-resolving its `meta` through the registered
10
+ * generator and instancing the parts. Returns null when the meta names no generator. The runtime
11
+ * counterpart to placing a parametric asset — the geometry is data, recomputed from params + seed.
12
+ * @internal — `AuthoredScene` mounts this for generator markers automatically.
13
+ */
14
+ export declare function GeneratedAssetInstance({ meta, position, rotationY }: GeneratedAssetProps): import("react").JSX.Element | null;
@@ -0,0 +1,59 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef } from "react";
3
+ import * as THREE from "three";
4
+ import { resolveGeneratorAsset } from "@jgengine/core/scene/assetGenerator";
5
+ const UNIT_BOX = new THREE.BoxGeometry(1, 1, 1);
6
+ const NEUTRAL = "#b8b0a4";
7
+ /** All parts of one generated asset, batched into one instanced box draw per color. */
8
+ function GeneratedParts({ asset }) {
9
+ const byColor = useMemo(() => {
10
+ const groups = new Map();
11
+ for (const part of asset.parts) {
12
+ const color = part.color ?? NEUTRAL;
13
+ const bucket = groups.get(color);
14
+ if (bucket === undefined)
15
+ groups.set(color, [part]);
16
+ else
17
+ bucket.push(part);
18
+ }
19
+ return [...groups.entries()];
20
+ }, [asset]);
21
+ return (_jsx(_Fragment, { children: byColor.map(([color, parts]) => (_jsx(ColorBatch, { color: color, parts: parts }, color))) }));
22
+ }
23
+ function ColorBatch({ color, parts }) {
24
+ const meshRef = useRef(null);
25
+ useEffect(() => {
26
+ const mesh = meshRef.current;
27
+ if (mesh === null)
28
+ return;
29
+ const matrix = new THREE.Matrix4();
30
+ const position = new THREE.Vector3();
31
+ const quaternion = new THREE.Quaternion();
32
+ const euler = new THREE.Euler();
33
+ const scale = new THREE.Vector3();
34
+ parts.forEach((part, index) => {
35
+ position.set(part.position[0], part.position[1], part.position[2]);
36
+ euler.set(0, part.rotationY ?? 0, 0);
37
+ quaternion.setFromEuler(euler);
38
+ scale.set(Math.max(1e-3, part.size[0]), Math.max(1e-3, part.size[1]), Math.max(1e-3, part.size[2]));
39
+ matrix.compose(position, quaternion, scale);
40
+ mesh.setMatrixAt(index, matrix);
41
+ });
42
+ mesh.count = parts.length;
43
+ mesh.instanceMatrix.needsUpdate = true;
44
+ mesh.computeBoundingSphere();
45
+ }, [parts]);
46
+ return (_jsx("instancedMesh", { ref: meshRef, args: [UNIT_BOX, undefined, Math.max(1, parts.length)], castShadow: true, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: color, roughness: 0.62, metalness: 0.04 }) }));
47
+ }
48
+ /**
49
+ * Renders one placed generator-asset instance by re-resolving its `meta` through the registered
50
+ * generator and instancing the parts. Returns null when the meta names no generator. The runtime
51
+ * counterpart to placing a parametric asset — the geometry is data, recomputed from params + seed.
52
+ * @internal — `AuthoredScene` mounts this for generator markers automatically.
53
+ */
54
+ export function GeneratedAssetInstance({ meta, position = [0, 0, 0], rotationY = 0 }) {
55
+ const asset = useMemo(() => resolveGeneratorAsset(meta), [meta]);
56
+ if (asset === null)
57
+ return null;
58
+ return (_jsx("group", { position: [position[0], position[1], position[2]], rotation: [0, rotationY, 0], children: _jsx(GeneratedParts, { asset: asset }) }));
59
+ }
@@ -0,0 +1,2 @@
1
+ /** Registers the `grass_field` volume renderer. Called by `registerBuiltinSceneKindRenderers`. @internal */
2
+ export declare function registerGrassFieldRenderer(): void;
@@ -0,0 +1,25 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useMemo } from "react";
3
+ import { resolveGrassObject } from "@jgengine/core/world/grassKind";
4
+ import { GrassField } from "../terrain/GrassField.js";
5
+ import { registerSceneKindRenderer } from "./sceneKindRenderers.js";
6
+ function hashSeed(text) {
7
+ let h = 2166136261 >>> 0;
8
+ for (let i = 0; i < text.length; i += 1) {
9
+ h ^= text.charCodeAt(i);
10
+ h = Math.imul(h, 16777619);
11
+ }
12
+ return (h >>> 0) % 100000;
13
+ }
14
+ /** One authored grass volume → an instanced vertex-wind `GrassField` over its footprint. */
15
+ function OneGrass({ object, context }) {
16
+ const resolved = useMemo(() => resolveGrassObject(object), [object]);
17
+ if (resolved === null)
18
+ return null;
19
+ const { rules, size, center } = resolved;
20
+ return (_jsx(GrassField, { position: [center[0], center[1], center[2]], area: [size[0], size[1]], density: rules.density, bladeHeight: [rules.bladeHeight * 0.7, rules.bladeHeight], colorBase: rules.colorBase, colorTip: rules.colorTip, colorVariation: rules.colorVariation, roughness: rules.roughness, seed: hashSeed(rules.seed.length > 0 ? rules.seed : object.id), heightAt: (x, z) => context.field.sampleHeight(x, z), wind: { strength: rules.windStrength, speed: rules.windSpeed, gustScale: rules.windGust } }));
21
+ }
22
+ /** Registers the `grass_field` volume renderer. Called by `registerBuiltinSceneKindRenderers`. @internal */
23
+ export function registerGrassFieldRenderer() {
24
+ registerSceneKindRenderer("grass_field", ({ objects, context }) => (_jsx(_Fragment, { children: objects.map((object) => (_jsx(OneGrass, { object: object, context: context }, object.id))) })));
25
+ }
@@ -0,0 +1,2 @@
1
+ /** Registers the `soil` volume runtime renderer. Called by `registerBuiltinSceneKindRenderers`. @internal */
2
+ export declare function registerSoilPatchRenderer(): void;
@@ -0,0 +1,21 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useMemo } from "react";
3
+ import { resolveSoilObject } from "@jgengine/core/world/soilKind";
4
+ import { createSoilPatchMaterial } from "../terrain/soilPatchMaterial.js";
5
+ import { registerSceneKindRenderer } from "./sceneKindRenderers.js";
6
+ const SOIL_PATCH_THICKNESS = 0.08;
7
+ /** One authored soil volume → a ground-hugging patch mesh with a crack/moss shader material. */
8
+ function OneSoil({ object, context }) {
9
+ const resolved = useMemo(() => resolveSoilObject(object), [object]);
10
+ const material = useMemo(() => (resolved === null ? null : createSoilPatchMaterial(resolved.rules)), [resolved]);
11
+ useEffect(() => () => material?.dispose(), [material]);
12
+ if (resolved === null || material === null)
13
+ return null;
14
+ const groundY = context.field.sampleHeight(resolved.center[0], resolved.center[2]);
15
+ const y = groundY + SOIL_PATCH_THICKNESS / 2 + 0.02;
16
+ return (_jsx("mesh", { position: [resolved.center[0], y, resolved.center[2]], material: material, receiveShadow: true, children: _jsx("boxGeometry", { args: [resolved.size[0], SOIL_PATCH_THICKNESS, resolved.size[1]] }) }));
17
+ }
18
+ /** Registers the `soil` volume runtime renderer. Called by `registerBuiltinSceneKindRenderers`. @internal */
19
+ export function registerSoilPatchRenderer() {
20
+ registerSceneKindRenderer("soil", ({ objects, context }) => (_jsx(_Fragment, { children: objects.map((object) => (_jsx(OneSoil, { object: object, context: context }, object.id))) })));
21
+ }
@@ -0,0 +1,37 @@
1
+ import { type ReactNode } from "react";
2
+ /** Lighting mood for a {@link StudioStage} — a named 3-point rig + backdrop palette. */
3
+ export type StudioMood = "studio" | "daylight" | "dusk" | "night";
4
+ /** Props for {@link StudioStage}. */
5
+ export interface StudioStageProps {
6
+ mood?: StudioMood;
7
+ /** Override the scene background color (hex). Default: the mood's backdrop. */
8
+ backdrop?: string;
9
+ /** Turntable spin speed in radians/second (spins the children). 0 disables. Default 0. */
10
+ turntable?: number;
11
+ /** Draw the seamless floor + set the scene background. Off for open-world scenes. Default true. */
12
+ environment?: boolean;
13
+ /**
14
+ * Yaw the children every frame so `forward` points at the active camera — frame the declared front
15
+ * with zero `rotationY` tuning. Overrides `turntable` (the two are mutually exclusive: one spins the
16
+ * content, the other holds it facing whoever's looking). Default false.
17
+ */
18
+ faceCamera?: boolean;
19
+ /**
20
+ * The staged content's declared forward axis (`GeneratedAsset.forward`, or a scene kind's own
21
+ * convention) — the direction `faceCamera` turns to point at the camera. Default `[0, 0, 1]`, the
22
+ * engine-wide convention (`DEFAULT_FORWARD` in `@jgengine/core/scene/facing`).
23
+ */
24
+ forward?: readonly [number, number, number];
25
+ children: ReactNode;
26
+ }
27
+ /**
28
+ * A reusable cinematic "product-shot" stage — a 3-point lighting rig (key/fill/rim + ambient), a
29
+ * seamless backdrop, and an optional turntable — so any parametric studio renders framed and lit like
30
+ * a hero shot instead of a flat proxy under default light. Pair with `PlayableGame.postProcessing =
31
+ * STUDIO_STAGE_POST` for the full film grade. Set `environment: false` to keep an open-world sky/ground
32
+ * and use it purely as a lighting rig. Set `faceCamera` to auto-orient the declared `forward` toward
33
+ * the camera instead of hand-tuning `rotationY` on the child.
34
+ *
35
+ * @capability studio-stage cinematic lighting rig + backdrop + turntable for parametric studios
36
+ */
37
+ export declare function StudioStage({ mood, backdrop, turntable, environment, faceCamera, forward, children, }: StudioStageProps): import("react").JSX.Element;
@@ -0,0 +1,42 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRef } from "react";
3
+ import { useFrame, useThree } from "@react-three/fiber";
4
+ import * as THREE from "three";
5
+ import { DEFAULT_FORWARD, yawToFace } from "@jgengine/core/scene/facing";
6
+ const MOODS = {
7
+ studio: { key: 3.1, keyColor: "#fff4e6", fill: 0.9, fillColor: "#cdd8ff", rim: 2.6, rimColor: "#ffe9c7", ambient: 0.35, ambientColor: "#5b6472", backdrop: "#14161c", floor: "#1b1e26" },
8
+ daylight: { key: 3.4, keyColor: "#fff6ea", fill: 1.2, fillColor: "#d6e6ff", rim: 1.6, rimColor: "#ffffff", ambient: 0.6, ambientColor: "#8fa2bf", backdrop: "#cdd7e4", floor: "#aeb7c4" },
9
+ dusk: { key: 2.6, keyColor: "#ffcf9e", fill: 0.7, fillColor: "#7c8fd8", rim: 3.0, rimColor: "#ff9d6b", ambient: 0.4, ambientColor: "#4a4668", backdrop: "#221a26", floor: "#241d2a" },
10
+ night: { key: 1.6, keyColor: "#bcd0ff", fill: 0.4, fillColor: "#38507e", rim: 2.4, rimColor: "#9fd0ff", ambient: 0.22, ambientColor: "#1e2740", backdrop: "#0a0c12", floor: "#0e1119" },
11
+ };
12
+ /**
13
+ * A reusable cinematic "product-shot" stage — a 3-point lighting rig (key/fill/rim + ambient), a
14
+ * seamless backdrop, and an optional turntable — so any parametric studio renders framed and lit like
15
+ * a hero shot instead of a flat proxy under default light. Pair with `PlayableGame.postProcessing =
16
+ * STUDIO_STAGE_POST` for the full film grade. Set `environment: false` to keep an open-world sky/ground
17
+ * and use it purely as a lighting rig. Set `faceCamera` to auto-orient the declared `forward` toward
18
+ * the camera instead of hand-tuning `rotationY` on the child.
19
+ *
20
+ * @capability studio-stage cinematic lighting rig + backdrop + turntable for parametric studios
21
+ */
22
+ export function StudioStage({ mood = "studio", backdrop, turntable = 0, environment = true, faceCamera = false, forward = DEFAULT_FORWARD, children, }) {
23
+ const spin = useRef(null);
24
+ const camera = useThree((state) => state.camera);
25
+ const groupWorld = useRef(new THREE.Vector3()).current;
26
+ const cameraWorld = useRef(new THREE.Vector3()).current;
27
+ useFrame((_, delta) => {
28
+ const group = spin.current;
29
+ if (group === null)
30
+ return;
31
+ if (faceCamera) {
32
+ group.getWorldPosition(groupWorld);
33
+ camera.getWorldPosition(cameraWorld);
34
+ group.rotation.y = yawToFace(forward, [groupWorld.x, groupWorld.z], [cameraWorld.x, cameraWorld.z]);
35
+ return;
36
+ }
37
+ if (turntable !== 0)
38
+ group.rotation.y += turntable * delta;
39
+ });
40
+ const rig = MOODS[mood];
41
+ return (_jsxs(_Fragment, { children: [environment ? _jsx("color", { attach: "background", args: [backdrop ?? rig.backdrop] }) : null, _jsx("ambientLight", { intensity: rig.ambient, color: rig.ambientColor }), _jsx("directionalLight", { position: [7, 9, 5], intensity: rig.key, color: rig.keyColor, castShadow: true, "shadow-mapSize": [2048, 2048], "shadow-bias": -0.0004, children: _jsx("orthographicCamera", { attach: "shadow-camera", args: [-24, 24, 24, -24, 0.5, 80] }) }), _jsx("directionalLight", { position: [-8, 5, 3], intensity: rig.fill, color: rig.fillColor }), _jsx("directionalLight", { position: [0, 6, -10], intensity: rig.rim, color: rig.rimColor }), environment ? (_jsxs("mesh", { "rotation-x": -Math.PI / 2, "position-y": -0.01, receiveShadow: true, children: [_jsx("planeGeometry", { args: [400, 400] }), _jsx("meshStandardMaterial", { color: rig.floor, roughness: 0.95, metalness: 0 })] })) : null, _jsx("group", { ref: spin, children: children })] }));
42
+ }
@@ -0,0 +1,2 @@
1
+ /** Registers the `water` volume runtime renderer. Called by `registerBuiltinSceneKindRenderers`. @internal */
2
+ export declare function registerWaterRenderer(): void;
@@ -0,0 +1,37 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useMemo } from "react";
3
+ import { resolveWaterObject } from "@jgengine/core/world/waterKind";
4
+ import { Ocean } from "../water/Ocean.js";
5
+ import { registerSceneKindRenderer } from "./sceneKindRenderers.js";
6
+ /** Map the editor-authored water params onto the shell Ocean shader config. */
7
+ function toOceanConfig(rules, size) {
8
+ const maxSpan = Math.max(size[0], size[1]);
9
+ return {
10
+ size: size[0],
11
+ depth: size[1],
12
+ resolution: Math.min(160, Math.max(16, Math.round(maxSpan / 2))),
13
+ amplitude: rules.waveHeight,
14
+ speed: rules.waveSpeed,
15
+ timeScale: rules.waveSpeed,
16
+ waveScale: Math.min(300, Math.max(2, 6 / rules.waveScale)),
17
+ color: {
18
+ deep: rules.color,
19
+ shallow: rules.shallowColor,
20
+ opacity: rules.opacity,
21
+ fresnelStrength: rules.reflectivity,
22
+ },
23
+ foam: { coverage: Math.min(1, rules.foam / 4), intensity: Math.min(1.5, rules.foam / 3) },
24
+ };
25
+ }
26
+ /** One authored water volume → a bounded, animated Ocean plane at the box top. */
27
+ function OneWater({ object }) {
28
+ const resolved = useMemo(() => resolveWaterObject(object), [object]);
29
+ const config = useMemo(() => (resolved === null ? null : toOceanConfig(resolved.rules, resolved.size)), [resolved]);
30
+ if (resolved === null || config === null)
31
+ return null;
32
+ return _jsx(Ocean, { config: config, position: [resolved.center[0], resolved.center[1], resolved.center[2]] });
33
+ }
34
+ /** Registers the `water` volume runtime renderer. Called by `registerBuiltinSceneKindRenderers`. @internal */
35
+ export function registerWaterRenderer() {
36
+ registerSceneKindRenderer("water", ({ objects }) => (_jsx(_Fragment, { children: objects.map((object) => (_jsx(OneWater, { object: object }, object.id))) })));
37
+ }
@@ -0,0 +1,2 @@
1
+ /** Registers the engine's built-in environment scene-kind renderers. Called by `AuthoredScene`. @internal */
2
+ export declare function registerBuiltinSceneKindRenderers(): void;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Registers the engine's built-in *environment* scene-kind renderers (the shell half of #809) and
3
+ * ensures the matching core kinds are registered too. Idempotent; `AuthoredScene` calls it at module
4
+ * load so a document with a `water`/`grass_field`/`soil` volume renders without the game wiring
5
+ * anything up. Domain studios (poles, …) register their own renderer via the public
6
+ * `registerSceneKindRenderer` — no edit here.
7
+ */
8
+ import { registerBuiltinSceneKinds } from "@jgengine/core/scene/builtinSceneKinds";
9
+ import { registerGrassFieldRenderer } from "./GrassFieldRenderer.js";
10
+ import { registerWaterRenderer } from "./WaterRenderer.js";
11
+ import { registerSoilPatchRenderer } from "./SoilPatchRenderer.js";
12
+ let registered = false;
13
+ /** Registers the engine's built-in environment scene-kind renderers. Called by `AuthoredScene`. @internal */
14
+ export function registerBuiltinSceneKindRenderers() {
15
+ if (registered)
16
+ return;
17
+ registered = true;
18
+ registerBuiltinSceneKinds();
19
+ registerWaterRenderer();
20
+ registerGrassFieldRenderer();
21
+ registerSoilPatchRenderer();
22
+ }
@@ -0,0 +1,28 @@
1
+ import type { ReactElement } from "react";
2
+ import type { EditorDocument } from "@jgengine/core/editor/index";
3
+ import type { AssetCatalog } from "@jgengine/core/scene/assetCatalog";
4
+ import type { SceneKindObject } from "@jgengine/core/scene/sceneKinds";
5
+ import type { TerrainField } from "@jgengine/core/world/terrain";
6
+ /** Context a scene-kind renderer receives alongside the matching document objects. */
7
+ export interface SceneKindRenderContext {
8
+ document: EditorDocument;
9
+ /** Ground field to drape/ground kind geometry on. */
10
+ field: TerrainField;
11
+ /** Catalog for kinds that instance real GLBs (pole assets, etc.). */
12
+ assets?: AssetCatalog;
13
+ }
14
+ /** A registered renderer: given every document object of its kind, returns the render tree for them. */
15
+ export type SceneKindRenderer = (props: {
16
+ objects: readonly SceneKindObject[];
17
+ context: SceneKindRenderContext;
18
+ }) => ReactElement | null;
19
+ /**
20
+ * Register the runtime renderer for a scene kind — the shell half of the #809 seam. `AuthoredScene`
21
+ * groups a document's objects by kind and mounts the registered renderer for each, so a new studio
22
+ * renders from the document with no fork of `AuthoredScene`. Idempotent per kind (last wins).
23
+ */
24
+ export declare function registerSceneKindRenderer(kind: string, renderer: SceneKindRenderer): void;
25
+ /** The renderer registered for a kind, or undefined. @internal — `AuthoredScene` uses it internally. */
26
+ export declare function getSceneKindRenderer(kind: string): SceneKindRenderer | undefined;
27
+ /** Every kind that has a registered renderer. @internal */
28
+ export declare function renderedSceneKinds(): string[];
@@ -0,0 +1,17 @@
1
+ const renderers = new Map();
2
+ /**
3
+ * Register the runtime renderer for a scene kind — the shell half of the #809 seam. `AuthoredScene`
4
+ * groups a document's objects by kind and mounts the registered renderer for each, so a new studio
5
+ * renders from the document with no fork of `AuthoredScene`. Idempotent per kind (last wins).
6
+ */
7
+ export function registerSceneKindRenderer(kind, renderer) {
8
+ renderers.set(kind, renderer);
9
+ }
10
+ /** The renderer registered for a kind, or undefined. @internal — `AuthoredScene` uses it internally. */
11
+ export function getSceneKindRenderer(kind) {
12
+ return renderers.get(kind);
13
+ }
14
+ /** Every kind that has a registered renderer. @internal */
15
+ export function renderedSceneKinds() {
16
+ return [...renderers.keys()];
17
+ }
@@ -0,0 +1 @@
1
+ export * from "./scene/AuthoredScene.js";
package/dist/scene.js ADDED
@@ -0,0 +1 @@
1
+ export * from "./scene/AuthoredScene.js";
@@ -1,6 +1,11 @@
1
1
  import type { AudioBusDef } from "@jgengine/core/audio/audioFalloff";
2
2
  import { type SettingsStore } from "@jgengine/core/settings/settingsModel";
3
+ import { type TouchStyle } from "@jgengine/core/input/touchScheme";
3
4
  import type { AudioEngine } from "../audio/audioEngine.js";
5
+ /** Sentinel Controls value meaning "defer to the game's suggested touch skin". */
6
+ export declare const TOUCH_STYLE_AUTO = "auto";
7
+ /** Shell-internal: binds the SettingsStore touch-style choice to the touch renderer. @internal */
8
+ export declare function useTouchStyle(store: SettingsStore, fallback: TouchStyle): TouchStyle;
4
9
  export declare function useSettingsRevision(store: SettingsStore): number;
5
10
  export declare function useGraphicsSettings(store: SettingsStore, shadowsDefault: boolean): {
6
11
  shadows: boolean;
@@ -1,5 +1,14 @@
1
1
  import { useEffect, useReducer } from "react";
2
2
  import { busVolumeSettingId, DEFAULT_GRAPHICS_QUALITY, DEFAULT_MASTER_VOLUME, DEFAULT_UI_SCALE, GRAPHICS_QUALITY_DPR, SETTING_IDS, UI_SCALE_MAX, UI_SCALE_MIN, } from "@jgengine/core/settings/settingsModel";
3
+ import { TOUCH_STYLES } from "@jgengine/core/input/touchScheme";
4
+ /** Sentinel Controls value meaning "defer to the game's suggested touch skin". */
5
+ export const TOUCH_STYLE_AUTO = "auto";
6
+ /** Shell-internal: binds the SettingsStore touch-style choice to the touch renderer. @internal */
7
+ export function useTouchStyle(store, fallback) {
8
+ useSettingsRevision(store);
9
+ const raw = store.get(SETTING_IDS.touchStyle, TOUCH_STYLE_AUTO);
10
+ return TOUCH_STYLES.includes(raw) ? raw : fallback;
11
+ }
3
12
  export function useSettingsRevision(store) {
4
13
  const [rev, bump] = useReducer((n) => n + 1, 0);
5
14
  useEffect(() => store.subscribe(() => bump()), [store]);
@@ -12,6 +12,8 @@ export interface SettingsControllerInput {
12
12
  hide: readonly SettingCategory[];
13
13
  fovEnabled: boolean;
14
14
  hideBindings: readonly string[];
15
+ /** Show the touch-controls skin selector in Controls (touch device with a dock). */
16
+ touchStyle: boolean;
15
17
  overrides: BindingOverrides;
16
18
  rebind: (action: string, code: string) => void;
17
19
  resetBinding: (action: string) => void;
@@ -1,6 +1,8 @@
1
1
  import { useEffect, useReducer } from "react";
2
2
  import { actionLabel, bindingLabel } from "@jgengine/core/input/actionBindings";
3
3
  import { BUILT_IN_SETTING_CATEGORIES, busVolumeSettingId, DEFAULT_GRAPHICS_QUALITY, DEFAULT_GRAPHICS_SHADOWS, DEFAULT_MASTER_VOLUME, DEFAULT_UI_SCALE, GRAPHICS_QUALITY_OPTIONS, SETTING_IDS, UI_SCALE_MAX, UI_SCALE_MIN, } from "@jgengine/core/settings/settingsModel";
4
+ import { TOUCH_STYLE_OPTIONS } from "@jgengine/core/input/touchScheme";
5
+ import { TOUCH_STYLE_AUTO } from "./appliedSettings.js";
4
6
  import { useSettingsStore, } from "@jgengine/react/settings";
5
7
  import { usePlayerFov } from "../camera/PlayerFov.js";
6
8
  const DEFAULT_CATEGORY_LABELS = {
@@ -10,6 +12,10 @@ const DEFAULT_CATEGORY_LABELS = {
10
12
  controls: "Controls",
11
13
  };
12
14
  const percent = (value) => `${Math.round(value * 100)}%`;
15
+ const TOUCH_STYLE_SELECT_OPTIONS = [
16
+ { value: TOUCH_STYLE_AUTO, label: "Auto (game default)" },
17
+ ...TOUCH_STYLE_OPTIONS.map((option) => ({ value: option.value, label: option.label })),
18
+ ];
13
19
  export function useSettingsCategories(config) {
14
20
  const store = useSettingsStore();
15
21
  const fov = usePlayerFov();
@@ -119,11 +125,26 @@ export function useSettingsCategories(config) {
119
125
  reset: () => config.resetBinding(action),
120
126
  };
121
127
  });
128
+ const controlsRows = [
129
+ ...(config.touchStyle
130
+ ? [
131
+ {
132
+ id: SETTING_IDS.touchStyle,
133
+ label: "Touch controls",
134
+ kind: "select",
135
+ value: store.get(SETTING_IDS.touchStyle, TOUCH_STYLE_AUTO),
136
+ options: TOUCH_STYLE_SELECT_OPTIONS,
137
+ set: (value) => store.set(SETTING_IDS.touchStyle, value),
138
+ },
139
+ ]
140
+ : []),
141
+ ...extrasFor("controls"),
142
+ ];
122
143
  const builtInRows = {
123
144
  sound: soundRows,
124
145
  graphics: graphicsRows,
125
146
  gameplay: gameplayRows,
126
- controls: extrasFor("controls"),
147
+ controls: controlsRows,
127
148
  };
128
149
  const builtInKeybinds = { controls: keybinds };
129
150
  const declaredLabels = new Map(config.categories.map((c) => [c.id, c.label]));
@@ -0,0 +1,4 @@
1
+ /** No action names are reserved when no camera rig is active (hud/none presentation): games may bind `turnLeft`/`interact`/etc. as their own. */
2
+ export declare const EMPTY_RESERVED: ReadonlySet<string>;
3
+ /** Empty action list — published while the orientation gate is up to suppress all held input without touching the tracker. */
4
+ export declare const NO_ACTIONS: string[];
@@ -0,0 +1,4 @@
1
+ /** No action names are reserved when no camera rig is active (hud/none presentation): games may bind `turnLeft`/`interact`/etc. as their own. */
2
+ export const EMPTY_RESERVED = new Set();
3
+ /** Empty action list — published while the orientation gate is up to suppress all held input without touching the tracker. */
4
+ export const NO_ACTIONS = [];
@@ -0,0 +1,8 @@
1
+ /** Movement action names the shell drives via the walk controller. @internal */
2
+ export declare const SHELL_MOVEMENT_ACTIONS: readonly ["moveForward", "moveBack", "moveLeft", "moveRight", "jump"];
3
+ /**
4
+ * True when the game's input map binds any walk-controller action — the shell owns pose
5
+ * for those games. Games that omit all five drive pose themselves.
6
+ * @internal
7
+ */
8
+ export declare function shellDrivesPlayerPose(input: Record<string, unknown> | undefined): boolean;
@@ -0,0 +1,11 @@
1
+ /** Movement action names the shell drives via the walk controller. @internal */
2
+ export const SHELL_MOVEMENT_ACTIONS = ["moveForward", "moveBack", "moveLeft", "moveRight", "jump"];
3
+ /**
4
+ * True when the game's input map binds any walk-controller action — the shell owns pose
5
+ * for those games. Games that omit all five drive pose themselves.
6
+ * @internal
7
+ */
8
+ export function shellDrivesPlayerPose(input) {
9
+ const bound = input ?? {};
10
+ return SHELL_MOVEMENT_ACTIONS.some((action) => action in bound);
11
+ }