@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
@@ -1,22 +1,48 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useMemo, useRef } from "react";
2
+ import { useEffect, useMemo, useRef } from "react";
3
3
  import { useFrame } from "@react-three/fiber";
4
+ import { useTexture } from "@react-three/drei";
5
+ import * as THREE from "three";
4
6
  import { useGameContext } from "@jgengine/react/provider";
5
7
  import { resolveBuildingPalette } from "@jgengine/core/world/buildings";
6
8
  import { resolveStructureBuildings } from "@jgengine/core/world/environmentSummary";
7
- import { createTerrainPaletteSampler, resolveEnvironmentField, resolveTerrainPalette, } from "@jgengine/core/world/terrain";
9
+ import { createTerrainPaletteSampler, resolveEnvironmentField, resolveTerrainDetail, resolveTerrainPalette, } from "@jgengine/core/world/terrain";
8
10
  import { GroundPad } from "./GroundPad.js";
11
+ import { RoadRibbons } from "./RoadRibbons.js";
9
12
  import { InstancedBuildings } from "../structures/GeneratedBuilding.js";
10
13
  import { GrassField } from "../terrain/GrassField.js";
11
14
  import { CarvedTerrain } from "../terrain/CarvedTerrain.js";
15
+ import { createTerrainDetailMaterial } from "../terrain/terrainDetailMaterial.js";
12
16
  import { Ocean } from "../water/Ocean.js";
13
17
  import { RainField } from "../weather/RainField.js";
14
18
  import { SnowField } from "../weather/SnowField.js";
15
19
  import { WeatherUniformProvider } from "../weather/weatherUniforms.js";
20
+ function TexturedTerrainGround({ detail, layout, }) {
21
+ const maps = detail.material.maps;
22
+ const textures = useTexture({
23
+ color: maps.color,
24
+ normal: maps.normal,
25
+ roughness: maps.roughness,
26
+ ao: maps.ao,
27
+ });
28
+ useEffect(() => {
29
+ textures.color.colorSpace = THREE.SRGBColorSpace;
30
+ for (const texture of Object.values(textures)) {
31
+ texture.wrapS = THREE.RepeatWrapping;
32
+ texture.wrapT = THREE.RepeatWrapping;
33
+ texture.needsUpdate = true;
34
+ }
35
+ }, [textures]);
36
+ const material = useMemo(() => createTerrainDetailMaterial(detail, textures).material, [detail, textures]);
37
+ useEffect(() => () => material.dispose(), [material]);
38
+ return (_jsx(CarvedTerrain, { field: layout.field, size: layout.size, segments: layout.segments, colors: layout.colors, heightRange: layout.heightRange, paletteAt: layout.paletteAt, center: layout.center, roughness: 0.94, surfaceMaterial: material }));
39
+ }
16
40
  function TerrainGround({ terrain, field, center, }) {
17
41
  const palette = useMemo(() => resolveTerrainPalette(terrain), [terrain]);
18
42
  const paletteAt = useMemo(() => {
19
- if (terrain.materialRegions === undefined || terrain.materialRegions.length === 0)
43
+ const hasRegions = terrain.materialRegions !== undefined && terrain.materialRegions.length > 0;
44
+ const hasBands = terrain.biomeBands !== undefined && terrain.biomeBands.length > 0;
45
+ if (!hasRegions && !hasBands)
20
46
  return undefined;
21
47
  const sampler = createTerrainPaletteSampler(terrain);
22
48
  if (center === undefined)
@@ -34,7 +60,15 @@ function TerrainGround({ terrain, field, center, }) {
34
60
  const swing = terrain.height * 1.2;
35
61
  return [base - swing, base + swing];
36
62
  }, [terrain.baseHeight, terrain.height]);
37
- return (_jsx(CarvedTerrain, { field: field, size: size, segments: terrain.segments, colors: colors, heightRange: heightRange, paletteAt: paletteAt, center: center, roughness: 0.94 }));
63
+ const resolvedDetail = useMemo(() => (terrain.detail === undefined ? undefined : resolveTerrainDetail(terrain.detail, terrain.waterLevel)), [terrain.detail, terrain.waterLevel]);
64
+ const proceduralMaterial = useMemo(() => resolvedDetail === undefined || resolvedDetail.material !== undefined
65
+ ? undefined
66
+ : createTerrainDetailMaterial(resolvedDetail).material, [resolvedDetail]);
67
+ useEffect(() => () => proceduralMaterial?.dispose(), [proceduralMaterial]);
68
+ if (resolvedDetail?.material !== undefined) {
69
+ return (_jsx(TexturedTerrainGround, { detail: resolvedDetail, layout: { field, size, segments: terrain.segments, colors, heightRange, paletteAt, center } }));
70
+ }
71
+ return (_jsx(CarvedTerrain, { field: field, size: size, segments: terrain.segments, colors: colors, heightRange: heightRange, paletteAt: paletteAt, center: center, roughness: 0.94, surfaceMaterial: proceduralMaterial }));
38
72
  }
39
73
  function areaCenter(area) {
40
74
  return area.position ?? [0, 0];
@@ -56,9 +90,9 @@ function weatherVolume(area) {
56
90
  function Precipitation({ weather }) {
57
91
  const [cx, cz] = areaCenter(weather.area);
58
92
  if (weather.kind === "rain") {
59
- return (_jsx("group", { position: [cx, 0, cz], children: _jsx(RainField, { density: weather.density, speed: weather.speed, length: weather.dropLength, color: weather.color, wind: [weather.wind[0], 0, weather.wind[1]], volume: weatherVolume(weather.area) }) }));
93
+ return (_jsx("group", { position: [cx, 0, cz], children: _jsx(RainField, { density: weather.density, speed: weather.speed, length: weather.dropLength, width: weather.width, opacity: weather.opacity, color: weather.color, wind: [weather.wind[0], 0, weather.wind[1]], volume: weatherVolume(weather.area) }) }));
60
94
  }
61
- return (_jsx("group", { position: [cx, 0, cz], children: _jsx(SnowField, { density: weather.density, speed: weather.speed, size: weather.flakeSize, sway: weather.drift, color: weather.color, wind: [weather.wind[0], 0, weather.wind[1]], volume: weatherVolume(weather.area) }) }));
95
+ return (_jsx("group", { position: [cx, 0, cz], children: _jsx(SnowField, { density: weather.density, speed: weather.speed, size: weather.flakeSize, sway: weather.drift, opacity: weather.opacity, color: weather.color, wind: [weather.wind[0], 0, weather.wind[1]], volume: weatherVolume(weather.area) }) }));
62
96
  }
63
97
  function Weather({ weather }) {
64
98
  return (_jsx(WeatherUniformProvider, { children: weather.map((entry, index) => (_jsx(Precipitation, { weather: entry, index: index }, `${entry.kind}-${index}`))) }));
@@ -110,5 +144,6 @@ export function EnvironmentScene({ feature }) {
110
144
  const structures = feature.structures ?? [];
111
145
  const pads = feature.pads ?? [];
112
146
  const islands = feature.islands ?? [];
113
- return (_jsxs(_Fragment, { children: [feature.terrain !== undefined ? _jsx(TerrainGround, { terrain: feature.terrain, field: field }) : null, islands.map((entry, index) => (_jsx(TerrainGround, { terrain: entry, field: field, center: entry.origin }, `island-${index}`))), water.map((ocean, index) => (_jsx(Water, { ocean: ocean }, `ocean-${index}`))), structures.map((entry, index) => (_jsx(Structures, { structures: entry, field: field }, `structures-${index}`))), pads.map((entry, index) => (_jsx(GroundPad, { pad: entry, field: field }, `pad-${index}`))), vegetation.map((grass, index) => (_jsx(Vegetation, { grass: grass, field: field }, `grass-${index}`))), feature.weather !== undefined && feature.weather.length > 0 ? (_jsx(Weather, { weather: feature.weather })) : null] }));
147
+ const roads = feature.roads ?? [];
148
+ return (_jsxs(_Fragment, { children: [feature.terrain !== undefined ? _jsx(TerrainGround, { terrain: feature.terrain, field: field }) : null, islands.map((entry, index) => (_jsx(TerrainGround, { terrain: entry, field: field, center: entry.origin }, `island-${index}`))), water.map((ocean, index) => (_jsx(Water, { ocean: ocean }, `ocean-${index}`))), roads.map((entry, index) => (_jsx(RoadRibbons, { road: entry, field: field }, `road-${index}`))), structures.map((entry, index) => (_jsx(Structures, { structures: entry, field: field }, `structures-${index}`))), pads.map((entry, index) => (_jsx(GroundPad, { pad: entry, field: field }, `pad-${index}`))), vegetation.map((grass, index) => (_jsx(Vegetation, { grass: grass, field: field }, `grass-${index}`))), feature.weather !== undefined && feature.weather.length > 0 ? (_jsx(Weather, { weather: feature.weather })) : null] }));
114
149
  }
@@ -0,0 +1,7 @@
1
+ import type { RoadEnvironmentDescriptor } from "@jgengine/core/world/features";
2
+ import type { TerrainField } from "@jgengine/core/world/terrain";
3
+ /** Renders one `road()` descriptor: an asphalt ribbon plus optional dashed centerline, draped on the terrain. */
4
+ export declare function RoadRibbons({ road, field }: {
5
+ road: RoadEnvironmentDescriptor;
6
+ field: TerrainField;
7
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,57 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo } from "react";
3
+ import { BufferAttribute, BufferGeometry, DoubleSide } from "three";
4
+ import { buildRoadRibbon, dashSegments } from "@jgengine/core/world/roads";
5
+ import { offsetPath, sidewalkWidthOf } from "@jgengine/core/world/streets";
6
+ function toGeometry(positions, indices) {
7
+ if (positions.length === 0)
8
+ return null;
9
+ const geometry = new BufferGeometry();
10
+ geometry.setAttribute("position", new BufferAttribute(positions, 3));
11
+ geometry.setIndex(new BufferAttribute(indices, 1));
12
+ geometry.computeVertexNormals();
13
+ return geometry;
14
+ }
15
+ function ribbonGeometry(path, width, field, elevation) {
16
+ const ribbon = buildRoadRibbon(path, width, (x, z) => field.sampleHeight(x, z), { elevation });
17
+ return toGeometry(ribbon.positions, ribbon.indices);
18
+ }
19
+ function mergedDashGeometry(road, field) {
20
+ const width = Math.max(0.25, road.width * 0.035);
21
+ const ribbons = dashSegments(road.path, 3.2, 4).map((dash) => buildRoadRibbon(dash, width, (x, z) => field.sampleHeight(x, z), { elevation: road.elevation + 0.04 }));
22
+ const vertexCount = ribbons.reduce((sum, r) => sum + r.positions.length, 0);
23
+ const indexCount = ribbons.reduce((sum, r) => sum + r.indices.length, 0);
24
+ if (vertexCount === 0)
25
+ return null;
26
+ const positions = new Float32Array(vertexCount);
27
+ const indices = new Uint32Array(indexCount);
28
+ let vertexOffset = 0;
29
+ let indexOffset = 0;
30
+ for (const ribbon of ribbons) {
31
+ positions.set(ribbon.positions, vertexOffset);
32
+ const base = vertexOffset / 3;
33
+ for (let i = 0; i < ribbon.indices.length; i += 1) {
34
+ indices[indexOffset + i] = ribbon.indices[i] + base;
35
+ }
36
+ vertexOffset += ribbon.positions.length;
37
+ indexOffset += ribbon.indices.length;
38
+ }
39
+ return toGeometry(positions, indices);
40
+ }
41
+ /** Renders one `road()` descriptor: an asphalt ribbon plus optional dashed centerline, draped on the terrain. */
42
+ export function RoadRibbons({ road, field }) {
43
+ const asphalt = useMemo(() => ribbonGeometry(road.path, road.width, field, road.elevation), [road, field]);
44
+ const dashes = useMemo(() => (road.markings ? mergedDashGeometry(road, field) : null), [road, field]);
45
+ const sidewalks = useMemo(() => {
46
+ const width = sidewalkWidthOf(road);
47
+ if (width <= 0)
48
+ return [];
49
+ const offset = road.width / 2 + width / 2;
50
+ return [offsetPath(road.path, offset), offsetPath(road.path, -offset)]
51
+ .map((path) => ribbonGeometry(path, width, field, road.elevation + 0.06))
52
+ .filter((geometry) => geometry !== null);
53
+ }, [road, field]);
54
+ if (asphalt === null)
55
+ return null;
56
+ return (_jsxs("group", { children: [_jsx("mesh", { geometry: asphalt, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: road.color, roughness: 0.95, metalness: 0, side: DoubleSide }) }), dashes !== null ? (_jsx("mesh", { geometry: dashes, children: _jsx("meshStandardMaterial", { color: road.markingColor, roughness: 0.8, metalness: 0, side: DoubleSide }) })) : null, sidewalks.map((geometry, index) => (_jsx("mesh", { geometry: geometry, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: road.sidewalk === false ? "#a7adb8" : road.sidewalk.color, roughness: 0.9, metalness: 0, side: DoubleSide }) }, `walk-${index}`)))] }));
57
+ }
@@ -0,0 +1,17 @@
1
+ import type { VolumetricCloudsRules } from "@jgengine/core/world/volumetricClouds";
2
+ /** Props for {@link VolumetricClouds} — fully-resolved cloud rules plus the shared sun direction. */
3
+ export interface VolumetricCloudsProps {
4
+ rules: VolumetricCloudsRules;
5
+ /** Direction toward the sun (same vector `SkyDome`/`Daylight` use for the sun disc). */
6
+ sunDirection: readonly [number, number, number];
7
+ }
8
+ /**
9
+ * A raymarched volumetric cloud slab: a large horizontal box straddling `[height, height+thickness]`,
10
+ * following the camera in XZ (like `SkyDome`), whose fragment shader raymarches 3D FBM noise inside
11
+ * the slab and accumulates density-weighted color with a sun-facing forward-scatter term. Mounted
12
+ * from `Daylight`/`SkyDaylight`/`TimeOfDayDaylight` whenever a sky descriptor carries
13
+ * `volumetricClouds`; off by default (the component itself is simply not rendered). A game never
14
+ * mounts this directly — author clouds via `sky({ volumetricClouds })`.
15
+ * @internal
16
+ */
17
+ export declare function VolumetricClouds({ rules, sunDirection }: VolumetricCloudsProps): import("react").JSX.Element;
@@ -0,0 +1,202 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useFrame } from "@react-three/fiber";
3
+ import { useEffect, useMemo, useRef } from "react";
4
+ import * as THREE from "three";
5
+ /**
6
+ * Camera-centered proxy dome radius. Must sit inside the default camera far plane
7
+ * (`CAMERA_FRUSTUM_DEFAULTS.far` = 300) — a bigger sphere is clipped whole and renders nothing. The
8
+ * raymarch works in world space against the absolute cloud slab, so this size only has to cover the
9
+ * screen (camera is always at the dome center), never bound the clouds.
10
+ */
11
+ const INSIDE_FRUSTUM_DOME_RADIUS = 240;
12
+ function hashSeed(text) {
13
+ let h = 2166136261 >>> 0;
14
+ for (let i = 0; i < text.length; i += 1) {
15
+ h ^= text.charCodeAt(i);
16
+ h = Math.imul(h, 16777619);
17
+ }
18
+ return (h >>> 0) % 100000;
19
+ }
20
+ function normalizeVec3(v) {
21
+ const out = new THREE.Vector3(v[0], v[1], v[2]);
22
+ return out.lengthSq() === 0 ? new THREE.Vector3(0, 1, 0) : out.normalize();
23
+ }
24
+ const CLOUD_VERTEX_SHADER = /* glsl */ `
25
+ varying vec3 vJgCloudWorldPos;
26
+ void main() {
27
+ vJgCloudWorldPos = (modelMatrix * vec4(position, 1.0)).xyz;
28
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
29
+ }
30
+ `;
31
+ const CLOUD_FRAGMENT_SHADER = /* glsl */ `
32
+ uniform vec3 uCameraPos;
33
+ uniform vec3 uColor;
34
+ uniform vec3 uSunColor;
35
+ uniform vec3 uSunDirection;
36
+ uniform float uSunScatter;
37
+ uniform float uCoverage;
38
+ uniform float uDensity;
39
+ uniform float uBaseHeight;
40
+ uniform float uThickness;
41
+ uniform float uScale;
42
+ uniform float uSpeed;
43
+ uniform float uTime;
44
+ uniform float uSeedOffset;
45
+ varying vec3 vJgCloudWorldPos;
46
+
47
+ float jgCloudHash13(vec3 p){
48
+ p = fract(p * 0.1031 + uSeedOffset * 0.0007);
49
+ p += dot(p, p.yzx + 33.33);
50
+ return fract((p.x + p.y) * p.z);
51
+ }
52
+
53
+ float jgCloudNoise(vec3 p){
54
+ vec3 i = floor(p);
55
+ vec3 f = fract(p);
56
+ f = f * f * (3.0 - 2.0 * f);
57
+ float n000 = jgCloudHash13(i + vec3(0.0, 0.0, 0.0));
58
+ float n100 = jgCloudHash13(i + vec3(1.0, 0.0, 0.0));
59
+ float n010 = jgCloudHash13(i + vec3(0.0, 1.0, 0.0));
60
+ float n110 = jgCloudHash13(i + vec3(1.0, 1.0, 0.0));
61
+ float n001 = jgCloudHash13(i + vec3(0.0, 0.0, 1.0));
62
+ float n101 = jgCloudHash13(i + vec3(1.0, 0.0, 1.0));
63
+ float n011 = jgCloudHash13(i + vec3(0.0, 1.0, 1.0));
64
+ float n111 = jgCloudHash13(i + vec3(1.0, 1.0, 1.0));
65
+ return mix(
66
+ mix(mix(n000, n100, f.x), mix(n010, n110, f.x), f.y),
67
+ mix(mix(n001, n101, f.x), mix(n011, n111, f.x), f.y),
68
+ f.z
69
+ );
70
+ }
71
+
72
+ float jgCloudFbm(vec3 p){
73
+ float v = 0.0;
74
+ float a = 0.5;
75
+ for (int i = 0; i < 5; i++) {
76
+ v += a * jgCloudNoise(p);
77
+ p = p * 2.03 + vec3(11.3, 7.1, 5.7);
78
+ a *= 0.5;
79
+ }
80
+ return v;
81
+ }
82
+
83
+ // Density of the cloud slab at a world point: a 3D-FBM puff field carved by a coverage threshold and
84
+ // shaped by a cumulus vertical profile (rounded base, eroded anvil top). 0 outside the slab.
85
+ float jgCloudDensity(vec3 pos){
86
+ float heightFrac = (pos.y - uBaseHeight) / uThickness;
87
+ if (heightFrac < 0.0 || heightFrac > 1.0) return 0.0;
88
+ float profile = smoothstep(0.0, 0.2, heightFrac) * smoothstep(1.0, 0.5, heightFrac);
89
+ vec3 wind = vec3(uTime * uSpeed, 0.0, uTime * uSpeed * 0.6);
90
+ vec3 sp = (pos + wind) / uScale;
91
+ float shape = jgCloudFbm(sp);
92
+ float threshold = mix(0.62, 0.2, uCoverage);
93
+ float d = smoothstep(threshold, threshold + 0.3, shape * profile + profile * 0.12);
94
+ return d * profile * uDensity;
95
+ }
96
+
97
+ // March a few steps toward the sun and turn the accumulated density into transmittance (Beer's law) —
98
+ // the self-shadowing that gives the clouds volume instead of a flat wash.
99
+ float jgCloudLight(vec3 pos){
100
+ vec3 ldir = normalize(uSunDirection);
101
+ float stepL = uThickness * 0.2;
102
+ float dens = 0.0;
103
+ vec3 p = pos;
104
+ for (int i = 0; i < 4; i++) {
105
+ p += ldir * stepL;
106
+ dens += jgCloudDensity(p);
107
+ }
108
+ return exp(-dens * stepL * 1.6);
109
+ }
110
+
111
+ void main() {
112
+ vec3 rayOrigin = uCameraPos;
113
+ vec3 rayDir = normalize(vJgCloudWorldPos - uCameraPos);
114
+ if (rayDir.y < 0.015) discard;
115
+
116
+ float tBottom = (uBaseHeight - rayOrigin.y) / rayDir.y;
117
+ float tTop = (uBaseHeight + uThickness - rayOrigin.y) / rayDir.y;
118
+ float tNear = max(min(tBottom, tTop), 0.0);
119
+ float tFar = max(tBottom, tTop);
120
+ if (tFar <= tNear) discard;
121
+ tFar = min(tFar, tNear + uThickness * 6.0);
122
+
123
+ const int STEPS = 32;
124
+ float stepLen = (tFar - tNear) / float(STEPS);
125
+ float t = tNear + stepLen * 0.5;
126
+ float transmittance = 1.0;
127
+ vec3 color = vec3(0.0);
128
+ float sunDot = max(dot(rayDir, normalize(uSunDirection)), 0.0);
129
+ float silver = pow(sunDot, 5.0) * uSunScatter;
130
+
131
+ for (int i = 0; i < STEPS; i++) {
132
+ vec3 pos = rayOrigin + rayDir * t;
133
+ float dens = jgCloudDensity(pos);
134
+ if (dens > 0.002) {
135
+ float light = jgCloudLight(pos);
136
+ float powder = 1.0 - exp(-dens * 3.0);
137
+ vec3 lit = mix(uColor, uSunColor, light) * (0.55 + 0.45 * powder) + uSunColor * silver * 0.5;
138
+ float dt = dens * stepLen * 1.3;
139
+ float a = 1.0 - exp(-dt);
140
+ color += transmittance * a * lit;
141
+ transmittance *= 1.0 - a;
142
+ if (transmittance < 0.02) break;
143
+ }
144
+ t += stepLen;
145
+ }
146
+
147
+ float alpha = (1.0 - transmittance) * smoothstep(0.015, 0.22, rayDir.y);
148
+ if (alpha < 0.01) discard;
149
+ gl_FragColor = vec4(color, alpha);
150
+ }
151
+ `;
152
+ /**
153
+ * A raymarched volumetric cloud slab: a large horizontal box straddling `[height, height+thickness]`,
154
+ * following the camera in XZ (like `SkyDome`), whose fragment shader raymarches 3D FBM noise inside
155
+ * the slab and accumulates density-weighted color with a sun-facing forward-scatter term. Mounted
156
+ * from `Daylight`/`SkyDaylight`/`TimeOfDayDaylight` whenever a sky descriptor carries
157
+ * `volumetricClouds`; off by default (the component itself is simply not rendered). A game never
158
+ * mounts this directly — author clouds via `sky({ volumetricClouds })`.
159
+ * @internal
160
+ */
161
+ export function VolumetricClouds({ rules, sunDirection }) {
162
+ const meshRef = useRef(null);
163
+ const timeRef = useRef(0);
164
+ const material = useMemo(() => {
165
+ return new THREE.ShaderMaterial({
166
+ uniforms: {
167
+ uCameraPos: { value: new THREE.Vector3() },
168
+ uColor: { value: new THREE.Color(rules.color) },
169
+ uSunColor: { value: new THREE.Color(rules.sunColor) },
170
+ uSunDirection: { value: normalizeVec3(sunDirection) },
171
+ uSunScatter: { value: rules.sunScatter },
172
+ uCoverage: { value: rules.coverage },
173
+ uDensity: { value: rules.density },
174
+ uBaseHeight: { value: rules.height },
175
+ uThickness: { value: rules.thickness },
176
+ uScale: { value: rules.scale },
177
+ uSpeed: { value: rules.speed },
178
+ uTime: { value: 0 },
179
+ uSeedOffset: { value: hashSeed(rules.seed) },
180
+ },
181
+ vertexShader: CLOUD_VERTEX_SHADER,
182
+ fragmentShader: CLOUD_FRAGMENT_SHADER,
183
+ transparent: true,
184
+ depthWrite: false,
185
+ side: THREE.BackSide,
186
+ fog: false,
187
+ });
188
+ }, [rules, sunDirection]);
189
+ useEffect(() => () => material.dispose(), [material]);
190
+ useFrame((state, delta) => {
191
+ timeRef.current += delta;
192
+ const mesh = meshRef.current;
193
+ if (mesh !== null) {
194
+ mesh.position.x = state.camera.position.x;
195
+ mesh.position.z = state.camera.position.z;
196
+ }
197
+ const uniforms = material.uniforms;
198
+ uniforms.uCameraPos.value.copy(state.camera.position);
199
+ uniforms.uTime.value = timeRef.current;
200
+ });
201
+ return (_jsx("mesh", { ref: meshRef, material: material, renderOrder: -0.5, frustumCulled: false, children: _jsx("sphereGeometry", { args: [INSIDE_FRUSTUM_DOME_RADIUS, 32, 16] }) }));
202
+ }
@@ -17,10 +17,12 @@ export declare const DEFAULT_DAY_AMBIENT_INTENSITY = 0.6;
17
17
  export declare const DEFAULT_DAY_SKY_TOP = "#3fa4f2";
18
18
  export declare const DEFAULT_DAY_SKY_BOTTOM = "#e3f4ff";
19
19
  export declare const SKY_PRESET_DAY_FRACTION: Record<"day" | "dusk" | "night", number>;
20
+ /** @internal */
20
21
  export declare function lerpHexColor(a: string, b: string, t: number): string;
21
22
  /**
22
23
  * Samples the daylight cycle at a point in the day (0 = midnight, 0.25 = dawn, 0.5 = noon, 0.75 = dusk),
23
24
  * lerping sun position/intensity, ambient intensity, and sky colors through a dawn/day/dusk/night
24
25
  * keyframe table. `config` overrides the noon (peak-day) colors and intensities only.
25
- */
26
+ * @internal
27
+ */
26
28
  export declare function daylightStateAt(dayFraction: number, config?: DaylightCycleConfig): DaylightState;
@@ -38,6 +38,7 @@ function clampByte(value) {
38
38
  function rgbToHex(rgb) {
39
39
  return `#${rgb.map((channel) => clampByte(channel).toString(16).padStart(2, "0")).join("")}`;
40
40
  }
41
+ /** @internal */
41
42
  export function lerpHexColor(a, b, t) {
42
43
  const [ar, ag, ab] = hexToRgb(a);
43
44
  const [br, bg, bb] = hexToRgb(b);
@@ -95,7 +96,8 @@ function segmentAt(wrapped, keyframes) {
95
96
  * Samples the daylight cycle at a point in the day (0 = midnight, 0.25 = dawn, 0.5 = noon, 0.75 = dusk),
96
97
  * lerping sun position/intensity, ambient intensity, and sky colors through a dawn/day/dusk/night
97
98
  * keyframe table. `config` overrides the noon (peak-day) colors and intensities only.
98
- */
99
+ * @internal
100
+ */
99
101
  export function daylightStateAt(dayFraction, config = {}) {
100
102
  const wrapped = wrapFraction(dayFraction);
101
103
  const keyframes = buildKeyframes(config);
@@ -8,6 +8,9 @@ export type PadShape = {
8
8
  width: number;
9
9
  depth: number;
10
10
  };
11
+ /** @internal */
11
12
  export declare function resolvePadShape(size: PadSize): PadShape;
13
+ /** @internal */
12
14
  export declare function resolvePadSurfaceY(groundHeight: number, pad: Pick<PadEnvironmentDescriptor, "height" | "elevation">): number;
15
+ /** @internal */
13
16
  export declare function resolvePadMeshY(groundHeight: number, pad: Pick<PadEnvironmentDescriptor, "height" | "elevation">): number;
@@ -1,10 +1,13 @@
1
1
  export const PAD_THICKNESS = 0.1;
2
+ /** @internal */
2
3
  export function resolvePadShape(size) {
3
4
  return "radius" in size ? { circular: true, radius: size.radius } : { circular: false, width: size[0], depth: size[1] };
4
5
  }
6
+ /** @internal */
5
7
  export function resolvePadSurfaceY(groundHeight, pad) {
6
8
  return pad.elevation ?? groundHeight + pad.height;
7
9
  }
10
+ /** @internal */
8
11
  export function resolvePadMeshY(groundHeight, pad) {
9
12
  return resolvePadSurfaceY(groundHeight, pad) - PAD_THICKNESS / 2;
10
13
  }
@@ -1,4 +1,5 @@
1
1
  export { Daylight, SkyDaylight, SkyDome, TimeOfDayDaylight, type DaylightProps, type SkyDaylightProps, type SkyDomeProps, type TimeOfDayDaylightProps, } from "./Daylight.js";
2
2
  export { EnvironmentScene, type EnvironmentSceneProps } from "./EnvironmentScene.js";
3
+ export { VolumetricClouds, type VolumetricCloudsProps } from "./VolumetricClouds.js";
3
4
  export { daylightStateAt, lerpHexColor, SKY_PRESET_DAY_FRACTION, type DaylightCycleConfig, type DaylightState, } from "./daylightCycle.js";
4
5
  export { resolveSkyLightOwnership, skyEmitsLights, type SkyLightOwnership, } from "./skyLightingPolicy.js";
@@ -1,4 +1,5 @@
1
1
  export { Daylight, SkyDaylight, SkyDome, TimeOfDayDaylight, } from "./Daylight.js";
2
2
  export { EnvironmentScene } from "./EnvironmentScene.js";
3
+ export { VolumetricClouds } from "./VolumetricClouds.js";
3
4
  export { daylightStateAt, lerpHexColor, SKY_PRESET_DAY_FRACTION, } from "./daylightCycle.js";
4
5
  export { resolveSkyLightOwnership, skyEmitsLights, } from "./skyLightingPolicy.js";
@@ -6,5 +6,7 @@
6
6
  * sky-owned lights when the game did not author lighting).
7
7
  */
8
8
  export type SkyLightOwnership = "authored" | "sky-default";
9
+ /** @internal */
9
10
  export declare function resolveSkyLightOwnership(hasAuthoredLighting: boolean): SkyLightOwnership;
11
+ /** @internal */
10
12
  export declare function skyEmitsLights(ownership: SkyLightOwnership): boolean;
@@ -1,6 +1,8 @@
1
+ /** @internal */
1
2
  export function resolveSkyLightOwnership(hasAuthoredLighting) {
2
3
  return hasAuthoredLighting ? "authored" : "sky-default";
3
4
  }
5
+ /** @internal */
4
6
  export function skyEmitsLights(ownership) {
5
7
  return ownership === "sky-default";
6
8
  }
@@ -0,0 +1,26 @@
1
+ import { type ActionCodesMap } from "@jgengine/core/input/actionBindings";
2
+ import type { Aim } from "@jgengine/core/scene/spatial";
3
+ import type { GameContext } from "@jgengine/core/runtime/gameContext";
4
+ type Inventories = Record<string, {
5
+ hud?: string;
6
+ }>;
7
+ /** Minimal playable slice — avoids coupling to shell vs core PlayableGame generics. @internal */
8
+ export type HotbarPlayable = {
9
+ game: {
10
+ inventories?: Inventories;
11
+ input?: ActionCodesMap;
12
+ };
13
+ };
14
+ /** @internal Map bound action names to hotbar slot indices. */
15
+ export declare function findHotbarSlotActions(input: ActionCodesMap | undefined): {
16
+ action: string;
17
+ slot: number;
18
+ }[];
19
+ /** @internal Resolve the inventory id used as the hotbar HUD binding. */
20
+ export declare function hotbarIdFor(playable: HotbarPlayable): string | null;
21
+ /** @internal Activate an item from a hotbar slot via `ctx.item.use`. */
22
+ export declare function executeHotbarSlot(ctx: GameContext, fromId: string, hotbarId: string, slot: number, yaw: number, pitch: number, aimOverride?: Aim): {
23
+ ok: boolean;
24
+ error?: string;
25
+ };
26
+ export {};
@@ -0,0 +1,27 @@
1
+ import { hotbarSlotActionIndex } from "@jgengine/core/input/actionBindings";
2
+ /** @internal Map bound action names to hotbar slot indices. */
3
+ export function findHotbarSlotActions(input) {
4
+ return Object.keys(input ?? {}).flatMap((action) => {
5
+ const slot = hotbarSlotActionIndex(action);
6
+ return slot === null ? [] : [{ action, slot }];
7
+ });
8
+ }
9
+ /** @internal Resolve the inventory id used as the hotbar HUD binding. */
10
+ export function hotbarIdFor(playable) {
11
+ const declarations = Object.entries(playable.game.inventories ?? {});
12
+ const hud = declarations.find(([, declaration]) => declaration.hud === "hotbar");
13
+ return (hud ?? declarations[0])?.[0] ?? null;
14
+ }
15
+ /** @internal Activate an item from a hotbar slot via `ctx.item.use`. */
16
+ export function executeHotbarSlot(ctx, fromId, hotbarId, slot, yaw, pitch, aimOverride) {
17
+ const stack = ctx.player.inventory.state(hotbarId).slots[slot];
18
+ if (stack === undefined || stack === null)
19
+ return { ok: false, error: `Hotbar slot ${slot + 1} is empty` };
20
+ const result = ctx.item.use.use({
21
+ from: fromId,
22
+ itemId: stack.itemId,
23
+ inventoryId: hotbarId,
24
+ aim: aimOverride ?? { yaw, pitch },
25
+ });
26
+ return result.error === undefined ? { ok: true } : { ok: false, error: result.error };
27
+ }
@@ -0,0 +1,30 @@
1
+ import { type InputFrame } from "@jgengine/core/runtime/hostedGameRunner";
2
+ import type { LiveGameBackend } from "@jgengine/core/runtime/transport";
3
+ /** Where the local player's per-frame input goes: discarded in single-player, sent to the authoritative host under `authority: "server"`. */
4
+ export interface InputSink {
5
+ send(frame: InputFrame): void;
6
+ }
7
+ /** Discards input — the single-player / client-authoritative default, where the client integrates movement itself.
8
+ * @internal
9
+ */
10
+ export declare function noopInputSink(): InputSink;
11
+ /**
12
+ * Sends each frame's input to the authoritative host over the transport, reusing the `runCommand` path via
13
+ * {@link INPUT_COMMAND}. Sends are sequenced one-in-flight-at-a-time per `serverId` and stamped with a monotonic
14
+ * seq: a frame sent while one is already in flight replaces the pending frame rather than racing it, so the
15
+ * latest intent always wins and a stale frame can never resolve after (and overwrite) a newer one.
16
+ * @internal
17
+ */
18
+ export declare function remoteInputSink(backend: Pick<LiveGameBackend, "transport">, serverId: string): InputSink;
19
+ /** The sink a server-authoritative shell sends its per-frame input through: remote when `authority: "server"` and a server is joined, a no-op otherwise.
20
+ * @internal
21
+ */
22
+ export declare function resolveInputSink(opts: {
23
+ serverAuthoritative: boolean;
24
+ backend: Pick<LiveGameBackend, "transport"> | null;
25
+ serverId: string | null;
26
+ }): InputSink;
27
+ /** Whether two input frames carry identical intent — the shell skips resending unchanged frames so a still player floods the host with nothing.
28
+ * @internal
29
+ */
30
+ export declare function inputFramesEqual(a: InputFrame, b: InputFrame): boolean;
@@ -0,0 +1,85 @@
1
+ import { INPUT_COMMAND } from "@jgengine/core/runtime/hostedGameRunner";
2
+ /** Discards input — the single-player / client-authoritative default, where the client integrates movement itself.
3
+ * @internal
4
+ */
5
+ export function noopInputSink() {
6
+ return { send() { } };
7
+ }
8
+ const remoteInputSources = new Map();
9
+ function remoteInputSourceFor(serverId) {
10
+ let source = remoteInputSources.get(serverId);
11
+ if (source === undefined) {
12
+ source = { pending: null, inFlight: false };
13
+ remoteInputSources.set(serverId, source);
14
+ }
15
+ return source;
16
+ }
17
+ function monotonicInputSeq() {
18
+ return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
19
+ }
20
+ function pumpRemoteInput(backend, serverId, source) {
21
+ if (source.inFlight)
22
+ return;
23
+ const frame = source.pending;
24
+ if (frame === null) {
25
+ remoteInputSources.delete(serverId);
26
+ return;
27
+ }
28
+ source.pending = null;
29
+ source.inFlight = true;
30
+ const seq = monotonicInputSeq();
31
+ void backend.transport
32
+ .runCommand({ serverId, command: INPUT_COMMAND, input: { ...frame, seq } })
33
+ .then((result) => {
34
+ if (!result.ok) {
35
+ console.warn(`[jgengine:input] frame seq=${seq} to server "${serverId}" rejected: ${result.reason}`);
36
+ }
37
+ })
38
+ .catch((error) => {
39
+ console.warn(`[jgengine:input] frame seq=${seq} to server "${serverId}" failed to send`, error);
40
+ })
41
+ .finally(() => {
42
+ source.inFlight = false;
43
+ pumpRemoteInput(backend, serverId, source);
44
+ });
45
+ }
46
+ /**
47
+ * Sends each frame's input to the authoritative host over the transport, reusing the `runCommand` path via
48
+ * {@link INPUT_COMMAND}. Sends are sequenced one-in-flight-at-a-time per `serverId` and stamped with a monotonic
49
+ * seq: a frame sent while one is already in flight replaces the pending frame rather than racing it, so the
50
+ * latest intent always wins and a stale frame can never resolve after (and overwrite) a newer one.
51
+ * @internal
52
+ */
53
+ export function remoteInputSink(backend, serverId) {
54
+ return {
55
+ send(frame) {
56
+ const source = remoteInputSourceFor(serverId);
57
+ source.pending = frame;
58
+ pumpRemoteInput(backend, serverId, source);
59
+ },
60
+ };
61
+ }
62
+ /** The sink a server-authoritative shell sends its per-frame input through: remote when `authority: "server"` and a server is joined, a no-op otherwise.
63
+ * @internal
64
+ */
65
+ export function resolveInputSink(opts) {
66
+ if (opts.serverAuthoritative && opts.backend !== null && opts.serverId !== null) {
67
+ return remoteInputSink(opts.backend, opts.serverId);
68
+ }
69
+ return noopInputSink();
70
+ }
71
+ /** Whether two input frames carry identical intent — the shell skips resending unchanged frames so a still player floods the host with nothing.
72
+ * @internal
73
+ */
74
+ export function inputFramesEqual(a, b) {
75
+ if (a.held.length !== b.held.length)
76
+ return false;
77
+ for (let i = 0; i < a.held.length; i++)
78
+ if (a.held[i] !== b.held[i])
79
+ return false;
80
+ const pa = a.pointer;
81
+ const pb = b.pointer;
82
+ if (pa === null || pb === null)
83
+ return pa === pb;
84
+ return pa.x === pb.x && pa.y === pb.y && pa.active === pb.active;
85
+ }