@jgengine/shell 0.10.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 (234) hide show
  1. package/CHANGELOG.md +13 -2
  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 +0 -12
  7. package/dist/GamePlayerShell.js +88 -1043
  8. package/dist/GameUiPreview.js +0 -2
  9. package/dist/audio/audioEngine.js +30 -8
  10. package/dist/audio/audioWire.d.ts +22 -0
  11. package/dist/audio/audioWire.js +19 -0
  12. package/dist/behaviour.d.ts +2 -1
  13. package/dist/behaviour.js +2 -1
  14. package/dist/behaviourAttach.d.ts +1 -0
  15. package/dist/behaviourAttach.js +1 -0
  16. package/dist/behaviourDriver.d.ts +1 -0
  17. package/dist/behaviourDriver.js +1 -0
  18. package/dist/boundActionDispatch.d.ts +13 -0
  19. package/dist/boundActionDispatch.js +37 -0
  20. package/dist/camera/GameCameraRig.d.ts +5 -2
  21. package/dist/camera/GameCameraRig.js +2 -2
  22. package/dist/camera/GameFirstPersonCamera.d.ts +9 -2
  23. package/dist/camera/GameFirstPersonCamera.js +8 -3
  24. package/dist/camera/GameInspectionCamera.d.ts +1 -1
  25. package/dist/camera/GameInspectionCamera.js +2 -2
  26. package/dist/camera/GameOrbitCamera.js +24 -9
  27. package/dist/camera/cameraBlendMath.d.ts +3 -0
  28. package/dist/camera/cameraBlendMath.js +3 -0
  29. package/dist/camera/fovPreference.d.ts +6 -1
  30. package/dist/camera/fovPreference.js +6 -1
  31. package/dist/camera/index.d.ts +4 -7
  32. package/dist/camera/index.js +4 -7
  33. package/dist/camera/inspectionCameraMath.d.ts +7 -2
  34. package/dist/camera/inspectionCameraMath.js +7 -2
  35. package/dist/camera/orbitCameraMath.d.ts +36 -6
  36. package/dist/camera/orbitCameraMath.js +56 -6
  37. package/dist/camera/rigMath.d.ts +94 -24
  38. package/dist/camera/rigMath.js +100 -24
  39. package/dist/camera/rigResolve.d.ts +4 -2
  40. package/dist/camera/rigResolve.js +4 -2
  41. package/dist/camera/shakeChannel.d.ts +6 -2
  42. package/dist/camera/shakeChannel.js +6 -2
  43. package/dist/camera/shakeChannelMath.d.ts +1 -0
  44. package/dist/camera/shakeChannelMath.js +1 -0
  45. package/dist/cartridge/runtime.d.ts +15 -0
  46. package/dist/cartridge/runtime.js +556 -0
  47. package/dist/cartridge/spec.d.ts +188 -0
  48. package/dist/cartridge/spec.js +19 -0
  49. package/dist/cartridge/validate.d.ts +2 -0
  50. package/dist/cartridge/validate.js +78 -0
  51. package/dist/cartridge.d.ts +1 -1
  52. package/dist/cartridge.js +3 -3
  53. package/dist/commandSink.d.ts +8 -3
  54. package/dist/commandSink.js +8 -3
  55. package/dist/defineGame.js +25 -6
  56. package/dist/devtools/ColPanel.d.ts +1 -0
  57. package/dist/devtools/ColPanel.js +14 -0
  58. package/dist/devtools/DevtoolsOverlay.d.ts +7 -3
  59. package/dist/devtools/DevtoolsOverlay.js +19 -450
  60. package/dist/devtools/KeysPanel.d.ts +4 -0
  61. package/dist/devtools/KeysPanel.js +31 -0
  62. package/dist/devtools/LogsPanel.d.ts +1 -0
  63. package/dist/devtools/LogsPanel.js +12 -0
  64. package/dist/devtools/NetPanel.d.ts +4 -0
  65. package/dist/devtools/NetPanel.js +10 -0
  66. package/dist/devtools/PerfPanel.d.ts +4 -0
  67. package/dist/devtools/PerfPanel.js +53 -0
  68. package/dist/devtools/TunePanel.d.ts +3 -0
  69. package/dist/devtools/TunePanel.js +309 -0
  70. package/dist/devtools/agentBridge.d.ts +41 -0
  71. package/dist/devtools/agentBridge.js +262 -0
  72. package/dist/devtools/collisionDebug.d.ts +6 -0
  73. package/dist/devtools/collisionDebug.js +6 -0
  74. package/dist/devtools/collisionDebugMath.d.ts +8 -2
  75. package/dist/devtools/collisionDebugMath.js +24 -5
  76. package/dist/devtools/devtoolsOverrides.d.ts +6 -0
  77. package/dist/devtools/devtoolsOverrides.js +48 -0
  78. package/dist/devtools/panelAtoms.d.ts +9 -0
  79. package/dist/devtools/panelAtoms.js +10 -0
  80. package/dist/devtools/perfDiagnose.d.ts +2 -0
  81. package/dist/devtools/perfDiagnose.js +43 -0
  82. package/dist/diagnostics/RuntimeDiagnostics.d.ts +29 -0
  83. package/dist/diagnostics/RuntimeDiagnostics.js +69 -0
  84. package/dist/drivers/FrameDriver.d.ts +37 -0
  85. package/dist/drivers/FrameDriver.js +232 -0
  86. package/dist/drivers/HudOnlyDriver.d.ts +16 -0
  87. package/dist/drivers/HudOnlyDriver.js +60 -0
  88. package/dist/environment/Daylight.d.ts +23 -7
  89. package/dist/environment/Daylight.js +106 -13
  90. package/dist/environment/EnvironmentScene.js +32 -6
  91. package/dist/environment/VolumetricClouds.d.ts +17 -0
  92. package/dist/environment/VolumetricClouds.js +202 -0
  93. package/dist/environment/daylightCycle.d.ts +3 -1
  94. package/dist/environment/daylightCycle.js +3 -1
  95. package/dist/environment/groundPadMath.d.ts +3 -0
  96. package/dist/environment/groundPadMath.js +3 -0
  97. package/dist/environment/index.d.ts +1 -0
  98. package/dist/environment/index.js +1 -0
  99. package/dist/environment/skyLightingPolicy.d.ts +2 -0
  100. package/dist/environment/skyLightingPolicy.js +2 -0
  101. package/dist/hotbarActions.d.ts +26 -0
  102. package/dist/hotbarActions.js +27 -0
  103. package/dist/inputSink.d.ts +16 -4
  104. package/dist/inputSink.js +57 -7
  105. package/dist/materialOverride.d.ts +10 -0
  106. package/dist/materialOverride.js +15 -3
  107. package/dist/multiplayer.js +8 -2
  108. package/dist/postfx/PostProcessing.js +47 -9
  109. package/dist/postfx/gradeShader.js +9 -1
  110. package/dist/registry.d.ts +3 -2
  111. package/dist/render/EnvironmentLighting.d.ts +10 -0
  112. package/dist/render/EnvironmentLighting.js +34 -0
  113. package/dist/render/SceneLighting.d.ts +7 -0
  114. package/dist/render/SceneLighting.js +18 -0
  115. package/dist/render/SceneModels.d.ts +14 -0
  116. package/dist/render/SceneModels.js +337 -0
  117. package/dist/render/defaultSurface.d.ts +29 -0
  118. package/dist/render/defaultSurface.js +101 -0
  119. package/dist/render/modelLoad.d.ts +24 -0
  120. package/dist/render/modelLoad.js +73 -0
  121. package/dist/render/modelRender.d.ts +11 -0
  122. package/dist/render/modelRender.js +15 -0
  123. package/dist/render/resolveModel.d.ts +39 -3
  124. package/dist/render/resolveModel.js +79 -2
  125. package/dist/render/useEntityRenderCues.d.ts +13 -0
  126. package/dist/render/useEntityRenderCues.js +62 -0
  127. package/dist/scatter/InstancedScatter.d.ts +24 -0
  128. package/dist/scatter/InstancedScatter.js +130 -0
  129. package/dist/scatter/scatterModels.d.ts +25 -0
  130. package/dist/scatter/scatterModels.js +61 -0
  131. package/dist/scatter/scatterProxies.d.ts +6 -0
  132. package/dist/scatter/scatterProxies.js +94 -0
  133. package/dist/scatter.d.ts +1 -0
  134. package/dist/scatter.js +1 -0
  135. package/dist/scene/AuthoredScene.d.ts +82 -0
  136. package/dist/scene/AuthoredScene.js +173 -0
  137. package/dist/scene/GeneratedAssetRenderer.d.ts +14 -0
  138. package/dist/scene/GeneratedAssetRenderer.js +59 -0
  139. package/dist/scene/GrassFieldRenderer.d.ts +2 -0
  140. package/dist/scene/GrassFieldRenderer.js +25 -0
  141. package/dist/scene/SoilPatchRenderer.d.ts +2 -0
  142. package/dist/scene/SoilPatchRenderer.js +21 -0
  143. package/dist/scene/StudioStage.d.ts +37 -0
  144. package/dist/scene/StudioStage.js +42 -0
  145. package/dist/scene/WaterRenderer.d.ts +2 -0
  146. package/dist/scene/WaterRenderer.js +37 -0
  147. package/dist/scene/builtinSceneKindRenderers.d.ts +2 -0
  148. package/dist/scene/builtinSceneKindRenderers.js +22 -0
  149. package/dist/scene/sceneKindRenderers.d.ts +28 -0
  150. package/dist/scene/sceneKindRenderers.js +17 -0
  151. package/dist/scene.d.ts +1 -0
  152. package/dist/scene.js +1 -0
  153. package/dist/settings/appliedSettings.d.ts +5 -0
  154. package/dist/settings/appliedSettings.js +9 -0
  155. package/dist/settings/settingsController.d.ts +2 -0
  156. package/dist/settings/settingsController.js +22 -1
  157. package/dist/shellConstants.d.ts +4 -0
  158. package/dist/shellConstants.js +4 -0
  159. package/dist/shellMovement.d.ts +8 -0
  160. package/dist/shellMovement.js +11 -0
  161. package/dist/shellPointer.d.ts +20 -0
  162. package/dist/shellPointer.js +28 -0
  163. package/dist/structures/PlacementGhost.d.ts +4 -0
  164. package/dist/structures/PlacementGhost.js +4 -0
  165. package/dist/structures/TransformGizmo.d.ts +43 -0
  166. package/dist/structures/TransformGizmo.js +55 -0
  167. package/dist/structures/index.d.ts +1 -0
  168. package/dist/structures/index.js +1 -0
  169. package/dist/terrain/GrassField.js +1 -1
  170. package/dist/terrain/TerraformBrushCursor.js +3 -0
  171. package/dist/terrain/grassBudget.d.ts +11 -2
  172. package/dist/terrain/grassBudget.js +20 -4
  173. package/dist/terrain/grassGeometry.d.ts +3 -0
  174. package/dist/terrain/grassGeometry.js +3 -0
  175. package/dist/terrain/grassMaterial.d.ts +2 -0
  176. package/dist/terrain/grassMaterial.js +2 -0
  177. package/dist/terrain/random.d.ts +3 -0
  178. package/dist/terrain/random.js +3 -0
  179. package/dist/terrain/soilPatchMaterial.d.ts +10 -0
  180. package/dist/terrain/soilPatchMaterial.js +109 -0
  181. package/dist/terrain/terrainDetailMaterial.d.ts +19 -2
  182. package/dist/terrain/terrainDetailMaterial.js +72 -5
  183. package/dist/terrain/terrainMath.d.ts +8 -1
  184. package/dist/terrain/terrainMath.js +8 -1
  185. package/dist/touch/TouchControlsOverlay.d.ts +9 -5
  186. package/dist/touch/TouchControlsOverlay.js +201 -22
  187. package/dist/useShellMultiplayerSync.d.ts +9 -0
  188. package/dist/useShellMultiplayerSync.js +98 -0
  189. package/dist/visibility/CullingProvider.js +1 -1
  190. package/dist/water/OceanConfig.d.ts +2 -0
  191. package/dist/water/OceanConfig.js +2 -0
  192. package/dist/weather/fireSpreadPose.d.ts +1 -0
  193. package/dist/weather/fireSpreadPose.js +1 -0
  194. package/dist/weather/weatherGeometry.d.ts +1 -0
  195. package/dist/weather/weatherGeometry.js +1 -0
  196. package/dist/weather/weatherMath.d.ts +3 -0
  197. package/dist/weather/weatherMath.js +3 -0
  198. package/dist/weather/weatherUniforms.d.ts +3 -0
  199. package/dist/weather/weatherUniforms.js +3 -0
  200. package/dist/world/RetainedVfx.d.ts +10 -0
  201. package/dist/world/RetainedVfx.js +136 -0
  202. package/dist/world/WorldHud.d.ts +49 -2
  203. package/dist/world/WorldHud.js +87 -2
  204. package/dist/world/WorldScene.d.ts +21 -0
  205. package/dist/world/WorldScene.js +159 -0
  206. package/dist/world/WorldVfx.d.ts +9 -0
  207. package/dist/world/WorldVfx.js +184 -0
  208. package/dist/world/entityPose.d.ts +2 -0
  209. package/dist/world/entityPose.js +2 -0
  210. package/dist/world/floatTextStyle.d.ts +1 -0
  211. package/dist/world/floatTextStyle.js +1 -0
  212. package/dist/world/telegraphPulse.d.ts +1 -0
  213. package/dist/world/telegraphPulse.js +1 -0
  214. package/dist/world/worldBarSamples.d.ts +25 -0
  215. package/dist/world/worldBarSamples.js +38 -0
  216. package/dist/worldSky.d.ts +5 -0
  217. package/dist/worldSky.js +11 -0
  218. package/dist/worldSync.d.ts +2 -1
  219. package/dist/worldSync.js +2 -1
  220. package/package.json +6 -6
  221. package/skills/jgengine/SKILL.md +51 -0
  222. package/skills/jgengine/api.md +434 -0
  223. package/skills/jgengine/capabilities.md +29 -0
  224. package/skills/jgengine/reference-cartridge.md +72 -0
  225. package/skills/jgengine-ui/SKILL.md +36 -0
  226. package/skills/jgengine-ui/api.md +1852 -0
  227. package/skills/jgengine-ui/capabilities.md +142 -0
  228. package/skills/jgengine-ui/reference.md +500 -0
  229. package/skills/jgengine-ui/references/visual-scorecard.md +73 -0
  230. package/skills/jgengine-world/SKILL.md +38 -0
  231. package/skills/jgengine-world/api.md +2652 -0
  232. package/skills/jgengine-world/capabilities.md +370 -0
  233. package/skills/jgengine-world/reference.md +222 -0
  234. package/llms.txt +0 -2761
@@ -0,0 +1,37 @@
1
+ import type { GameContext } from "@jgengine/core/runtime/gameContext";
2
+ import type { PointerAxisState } from "@jgengine/core/input/pointerAxis";
3
+ import type { ActionStateTracker } from "@jgengine/core/input/actionBindings";
4
+ import type { PointerService } from "../pointer/pointerService.js";
5
+ import type { ShellMultiplayer } from "../multiplayer.js";
6
+ import type { PlayableGame } from "../registry.js";
7
+ export declare const POSTER_SETTLE_SECONDS = 1.6;
8
+ export declare function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef, pointerAxisRef, gateRef, onRuntimeError, multiplayer, serverIdRef, pointerService, pointerAim, pingCommand, poster, onPosterSettled, }: {
9
+ ctx: GameContext;
10
+ playable: PlayableGame;
11
+ tracker: ActionStateTracker<string>;
12
+ yawRef: {
13
+ current: number;
14
+ };
15
+ pitchRef: {
16
+ current: number;
17
+ };
18
+ primaryClickRef: {
19
+ current: boolean;
20
+ };
21
+ pointerAxisRef: {
22
+ current: PointerAxisState | null;
23
+ };
24
+ gateRef: {
25
+ current: boolean;
26
+ };
27
+ onRuntimeError: (error: unknown, phase: string) => void;
28
+ multiplayer: ShellMultiplayer | null;
29
+ serverIdRef: {
30
+ current: string | null;
31
+ };
32
+ pointerService: PointerService;
33
+ pointerAim: boolean;
34
+ pingCommand: string | undefined;
35
+ poster: boolean;
36
+ onPosterSettled: () => void;
37
+ }): null;
@@ -0,0 +1,232 @@
1
+ import { useFrame } from "@react-three/fiber";
2
+ import { useMemo, useRef } from "react";
3
+ import { RESERVED_INPUT_ACTIONS, dispatchBoundAction, heldActionsFor, shouldFireBoundAction } from "../boundActionDispatch.js";
4
+ import { executeHotbarSlot, findHotbarSlotActions, hotbarIdFor } from "../hotbarActions.js";
5
+ import { pointerAimFor } from "../shellPointer.js";
6
+ import { shellDrivesPlayerPose } from "../shellMovement.js";
7
+ import { steerYaw } from "@jgengine/core/movement/steering";
8
+ import { stepPlayerMovement, resolvePlayerMovementTuning } from "@jgengine/core/movement/playerMovement";
9
+ import { isServerAuthoritative } from "@jgengine/core/runtime/adapter";
10
+ import { resolveCommandSink } from "../commandSink.js";
11
+ import { inputFramesEqual, resolveInputSink } from "../inputSink.js";
12
+ import { advanceBehaviors } from "@jgengine/core/scene/behaviorRuntime";
13
+ import { resolveActivePrompt } from "@jgengine/core/interaction/proximityPrompt";
14
+ import { DEFAULT_PICKUP_RADIUS } from "@jgengine/core/game/worldItem";
15
+ import { playControlsActive } from "@jgengine/core/game/controlGate";
16
+ import { devtools } from "@jgengine/core/devtools/devtools";
17
+ import { collisionDebug } from "../devtools/collisionDebug.js";
18
+ import { GAME_SIM_FRAME_PRIORITY } from "../camera/index.js";
19
+ import { NO_ACTIONS } from "../shellConstants.js";
20
+ const TURN_SPEED = 2.4;
21
+ export const POSTER_SETTLE_SECONDS = 1.6;
22
+ export function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef, pointerAxisRef, gateRef, onRuntimeError, multiplayer, serverIdRef, pointerService, pointerAim, pingCommand, poster, onPosterSettled, }) {
23
+ const posterElapsedRef = useRef(0);
24
+ const posterDoneRef = useRef(false);
25
+ const hasReportedTickError = useRef(false);
26
+ const repeatFiredAtRef = useRef(new Map());
27
+ const slotActions = useMemo(() => findHotbarSlotActions(playable.game.input), [playable]);
28
+ const hotbarId = useMemo(() => hotbarIdFor(playable), [playable]);
29
+ const movementTuning = useMemo(() => resolvePlayerMovementTuning({
30
+ collision: playable.collision,
31
+ movement: playable.movement,
32
+ physics: playable.game.physics,
33
+ world: playable.game.world,
34
+ }), [playable]);
35
+ const autoPickupRadius = useMemo(() => {
36
+ const cfg = playable.worldItem?.autoPickup;
37
+ if (cfg === undefined || cfg === false)
38
+ return null;
39
+ const fallback = playable.worldItem?.pickupRadius ?? DEFAULT_PICKUP_RADIUS;
40
+ return cfg === true ? fallback : cfg.radius ?? fallback;
41
+ }, [playable]);
42
+ const drivesPose = useMemo(() => shellDrivesPlayerPose(playable.game.input), [playable]);
43
+ const serverAuthoritative = useMemo(() => isServerAuthoritative(playable.game.multiplayer) && multiplayer !== null, [playable, multiplayer]);
44
+ const commandSink = useMemo(() => ({
45
+ run: (name, input) => resolveCommandSink(ctx, {
46
+ serverAuthoritative,
47
+ backend: multiplayer?.backend ?? null,
48
+ serverId: serverIdRef.current,
49
+ }).run(name, input),
50
+ }), [ctx, multiplayer, serverAuthoritative, serverIdRef]);
51
+ const inputSink = useMemo(() => ({
52
+ send: (frame) => resolveInputSink({
53
+ serverAuthoritative,
54
+ backend: multiplayer?.backend ?? null,
55
+ serverId: serverIdRef.current,
56
+ }).send(frame),
57
+ }), [multiplayer, serverAuthoritative, serverIdRef]);
58
+ const lastSentInputRef = useRef(null);
59
+ const inputActions = useMemo(() => Object.keys(playable.game.input ?? {}), [playable]);
60
+ useFrame((_state, rawDt) => {
61
+ if (poster) {
62
+ if (posterDoneRef.current)
63
+ return;
64
+ posterElapsedRef.current += Math.min(rawDt, 0.05);
65
+ if (posterElapsedRef.current >= POSTER_SETTLE_SECONDS) {
66
+ posterDoneRef.current = true;
67
+ onPosterSettled();
68
+ return;
69
+ }
70
+ }
71
+ const sendInput = () => {
72
+ if (!serverAuthoritative)
73
+ return;
74
+ const frame = { held: ctx.input.held(), pointer: ctx.input.pointer() };
75
+ const last = lastSentInputRef.current;
76
+ if (last !== null && inputFramesEqual(last, frame))
77
+ return;
78
+ lastSentInputRef.current = frame;
79
+ inputSink.send(frame);
80
+ };
81
+ if (gateRef.current || !playControlsActive(ctx)) {
82
+ ctx.input.publish(heldActionsFor(tracker, NO_ACTIONS));
83
+ sendInput();
84
+ return;
85
+ }
86
+ const simStart = performance.now();
87
+ try {
88
+ let endPhase = devtools.profile.begin("time+input");
89
+ const dt = Math.min(rawDt, 0.05);
90
+ const gameDt = ctx.time.advance(dt);
91
+ ctx.input.publish(heldActionsFor(tracker, inputActions));
92
+ ctx.input.publishPointer(pointerAxisRef.current);
93
+ sendInput();
94
+ const turnInput = (tracker.isDown("turnRight") ? 1 : 0) - (tracker.isDown("turnLeft") ? 1 : 0);
95
+ if (turnInput !== 0)
96
+ yawRef.current = steerYaw(yawRef.current, turnInput, TURN_SPEED, dt);
97
+ endPhase();
98
+ const playerId = ctx.player.possession.active(ctx.player.userId);
99
+ const player = ctx.scene.entity.get(playerId);
100
+ if (player !== null && drivesPose && !serverAuthoritative) {
101
+ endPhase = devtools.profile.begin("pose");
102
+ stepPlayerMovement(ctx, ctx.player.userId, { held: ctx.input.held(), pointer: ctx.input.pointer() }, rawDt, movementTuning, yawRef.current);
103
+ endPhase();
104
+ }
105
+ if (autoPickupRadius !== null && !serverAuthoritative) {
106
+ endPhase = devtools.profile.begin("pickup");
107
+ const self = ctx.scene.entity.get(playerId);
108
+ if (self !== null) {
109
+ const nearest = ctx.scene.worldItem.nearestInRadius(self.position, autoPickupRadius);
110
+ if (nearest !== null)
111
+ ctx.scene.worldItem.pickup(nearest, ctx.player.userId);
112
+ }
113
+ endPhase();
114
+ }
115
+ if (!serverAuthoritative) {
116
+ devtools.profile.measure("onTick", () => {
117
+ playable.loop.onTick(ctx, gameDt);
118
+ });
119
+ advanceBehaviors(ctx, gameDt);
120
+ }
121
+ endPhase = devtools.profile.begin("actions");
122
+ if (tracker.wasPressed("tabTarget")) {
123
+ if (ctx.game.commands.has("target.cycle"))
124
+ ctx.game.commands.run("target.cycle", {});
125
+ else
126
+ ctx.scene.entity.cycleTarget(playerId, { filter: "hostile" });
127
+ }
128
+ if (tracker.wasPressed("clearTarget")) {
129
+ if (ctx.game.commands.has("target.clear"))
130
+ ctx.game.commands.run("target.clear", {});
131
+ else
132
+ ctx.scene.entity.setTarget(playerId, null);
133
+ }
134
+ if (pingCommand !== undefined && tracker.wasPressed("ping")) {
135
+ const hit = pointerService.worldHit();
136
+ if (hit !== null && ctx.game.commands.has(pingCommand)) {
137
+ commandSink.run(pingCommand, {
138
+ point: hit.point,
139
+ entity: hit.entity,
140
+ object: hit.object,
141
+ normal: hit.normal,
142
+ });
143
+ }
144
+ }
145
+ const aimOverride = pointerAim ? pointerAimFor(ctx, pointerService) : undefined;
146
+ const commandAim = aimOverride ?? { yaw: yawRef.current, pitch: pitchRef.current };
147
+ if (collisionDebug.getState().layers.aimLaser) {
148
+ const aimFrom = ctx.player.possession.active(playerId) ?? playerId;
149
+ collisionDebug.setAimProbe({
150
+ from: aimFrom,
151
+ aim: commandAim,
152
+ originPolicy: { kind: "converge" },
153
+ maxDistance: 100,
154
+ });
155
+ }
156
+ else if (collisionDebug.getAimProbe() !== null) {
157
+ collisionDebug.setAimProbe(null);
158
+ }
159
+ const nowMs = performance.now();
160
+ for (const action of Object.keys(playable.game.input ?? {})) {
161
+ const pressed = tracker.wasPressed(action);
162
+ if (action === "ping" && pingCommand !== undefined)
163
+ continue;
164
+ if (action === "interact") {
165
+ if (!pressed)
166
+ continue;
167
+ const prompts = playable.prompts?.(ctx);
168
+ const focus = prompts === undefined ? null : ctx.scene.entity.get(playerId);
169
+ if (prompts !== undefined && focus !== null) {
170
+ const active = resolveActivePrompt({ x: focus.position[0], z: focus.position[2] }, prompts);
171
+ if (active !== null && active.prompt.invoke !== null) {
172
+ commandSink.run(active.prompt.invoke.name, active.prompt.invoke.input);
173
+ }
174
+ }
175
+ continue;
176
+ }
177
+ if (!shouldFireBoundAction(tracker, action, playable.game.input, repeatFiredAtRef.current, nowMs))
178
+ continue;
179
+ repeatFiredAtRef.current.set(action, nowMs);
180
+ dispatchBoundAction(ctx, action, yawRef.current, pitchRef.current, commandAim, RESERVED_INPUT_ACTIONS, commandSink);
181
+ }
182
+ if (hotbarId !== null) {
183
+ for (const { action, slot } of slotActions) {
184
+ if (!tracker.wasPressed(action))
185
+ continue;
186
+ const result = executeHotbarSlot(ctx, playerId, hotbarId, slot, yawRef.current, pitchRef.current, aimOverride);
187
+ if (!result.ok)
188
+ console.warn(`[jgengine:item-use] ${result.error}`);
189
+ }
190
+ const usePrimary = tracker.wasPressed("useAbility") || (primaryClickRef.current && hotbarId !== null);
191
+ if (usePrimary) {
192
+ primaryClickRef.current = false;
193
+ const slots = ctx.player.inventory.state(hotbarId).slots;
194
+ const preferred = playable.hotbarSelection?.() ?? -1;
195
+ const slot = preferred >= 0 && slots[preferred] !== null && slots[preferred] !== undefined
196
+ ? preferred
197
+ : slots.findIndex((stack) => stack !== null);
198
+ if (slot >= 0) {
199
+ const result = executeHotbarSlot(ctx, playerId, hotbarId, slot, yawRef.current, pitchRef.current, aimOverride);
200
+ if (!result.ok)
201
+ console.warn(`[jgengine:item-use] ${result.error}`);
202
+ }
203
+ }
204
+ }
205
+ tracker.endFrame();
206
+ endPhase();
207
+ const serverId = serverIdRef.current;
208
+ if (multiplayer !== null && serverId !== null) {
209
+ endPhase = devtools.profile.begin("presence");
210
+ const focus = ctx.scene.entity.get(playerId);
211
+ if (focus !== null) {
212
+ multiplayer.backend.presenceSync.syncPose(serverId, {
213
+ x: focus.position[0],
214
+ y: focus.position[1],
215
+ z: focus.position[2],
216
+ rotationY: focus.rotationY,
217
+ rotationPitch: pitchRef.current,
218
+ });
219
+ }
220
+ endPhase();
221
+ }
222
+ }
223
+ catch (error) {
224
+ if (!hasReportedTickError.current) {
225
+ hasReportedTickError.current = true;
226
+ onRuntimeError(error, "tick");
227
+ }
228
+ }
229
+ devtools.frame.record({ frameMs: rawDt * 1000, simMs: performance.now() - simStart });
230
+ }, GAME_SIM_FRAME_PRIORITY);
231
+ return null;
232
+ }
@@ -0,0 +1,16 @@
1
+ import type { GameContext } from "@jgengine/core/runtime/gameContext";
2
+ import type { PointerAxisState } from "@jgengine/core/input/pointerAxis";
3
+ import type { ActionStateTracker } from "@jgengine/core/input/actionBindings";
4
+ import type { PlayableGame } from "../registry.js";
5
+ export declare function HudOnlyDriver({ ctx, playable, tracker, pointerAxisRef, gateRef, onRuntimeError, }: {
6
+ ctx: GameContext;
7
+ playable: PlayableGame;
8
+ tracker: ActionStateTracker<string>;
9
+ pointerAxisRef: {
10
+ current: PointerAxisState | null;
11
+ };
12
+ gateRef: {
13
+ current: boolean;
14
+ };
15
+ onRuntimeError: (error: unknown, phase: string) => void;
16
+ }): null;
@@ -0,0 +1,60 @@
1
+ import { useEffect, useMemo, useRef } from "react";
2
+ import { dispatchBoundAction, heldActionsFor, shouldFireBoundAction } from "../boundActionDispatch.js";
3
+ import { advanceBehaviors } from "@jgengine/core/scene/behaviorRuntime";
4
+ import { playControlsActive } from "@jgengine/core/game/controlGate";
5
+ import { devtools } from "@jgengine/core/devtools/devtools";
6
+ import { EMPTY_RESERVED, NO_ACTIONS } from "../shellConstants.js";
7
+ export function HudOnlyDriver({ ctx, playable, tracker, pointerAxisRef, gateRef, onRuntimeError, }) {
8
+ const hasReportedTickError = useRef(false);
9
+ const repeatFiredAtRef = useRef(new Map());
10
+ const lastFrameRef = useRef(null);
11
+ const inputActions = useMemo(() => Object.keys(playable.game.input ?? {}), [playable]);
12
+ useEffect(() => {
13
+ let frameId;
14
+ const tick = (now) => {
15
+ frameId = requestAnimationFrame(tick);
16
+ const last = lastFrameRef.current;
17
+ lastFrameRef.current = now;
18
+ if (last === null)
19
+ return;
20
+ if (gateRef.current || !playControlsActive(ctx)) {
21
+ ctx.input.publish(heldActionsFor(tracker, NO_ACTIONS));
22
+ return;
23
+ }
24
+ const rawDt = (now - last) / 1000;
25
+ const simStart = performance.now();
26
+ try {
27
+ let endPhase = devtools.profile.begin("time+input");
28
+ const dt = Math.min(rawDt, 0.05);
29
+ const gameDt = ctx.time.advance(dt);
30
+ ctx.input.publish(heldActionsFor(tracker, inputActions));
31
+ ctx.input.publishPointer(pointerAxisRef.current);
32
+ endPhase();
33
+ devtools.profile.measure("onTick", () => {
34
+ playable.loop.onTick(ctx, gameDt);
35
+ });
36
+ advanceBehaviors(ctx, gameDt);
37
+ endPhase = devtools.profile.begin("actions");
38
+ const nowMs = performance.now();
39
+ for (const action of inputActions) {
40
+ if (!shouldFireBoundAction(tracker, action, playable.game.input, repeatFiredAtRef.current, nowMs))
41
+ continue;
42
+ repeatFiredAtRef.current.set(action, nowMs);
43
+ dispatchBoundAction(ctx, action, 0, 0, { yaw: 0, pitch: 0 }, EMPTY_RESERVED);
44
+ }
45
+ tracker.endFrame();
46
+ endPhase();
47
+ }
48
+ catch (error) {
49
+ if (!hasReportedTickError.current) {
50
+ hasReportedTickError.current = true;
51
+ onRuntimeError(error, "tick");
52
+ }
53
+ }
54
+ devtools.frame.record({ frameMs: rawDt * 1000, simMs: performance.now() - simStart });
55
+ };
56
+ frameId = requestAnimationFrame(tick);
57
+ return () => cancelAnimationFrame(frameId);
58
+ }, [ctx, playable, tracker, pointerAxisRef, gateRef, onRuntimeError, inputActions]);
59
+ return null;
60
+ }
@@ -1,6 +1,7 @@
1
1
  import { type MutableRefObject } from "react";
2
2
  import * as THREE from "three";
3
- import type { SkyEnvironmentDescriptor } from "@jgengine/core/world/features";
3
+ import type { BiomeBand, SkyEnvironmentDescriptor } from "@jgengine/core/world/features";
4
+ import { type VolumetricCloudsConfig } from "@jgengine/core/world/volumetricClouds";
4
5
  export interface SkyDomeProps {
5
6
  topColor?: string;
6
7
  horizonColor?: string;
@@ -13,10 +14,15 @@ export interface SkyDomeProps {
13
14
  sunColor?: string;
14
15
  /** Sun disc/glow intensity multiplier. Default 1. */
15
16
  sunIntensity?: number;
17
+ /** Horizon haze-band strength: 0 removes the dusty band, ~1 makes it heavy. Default 0.5. */
18
+ hazeStrength?: number;
19
+ /** Sun-glow brightness multiplier around the sun disc. Default 1. */
20
+ sunGlowStrength?: number;
16
21
  /** Exposes the created shader material so a time-of-day driver can mutate its uniforms per frame without recreating it. */
17
22
  materialRef?: MutableRefObject<THREE.ShaderMaterial | null>;
18
23
  }
19
- export declare function SkyDome({ topColor, horizonColor, radius, offset, exponent, sunDirection, sunColor, sunIntensity, materialRef, }?: SkyDomeProps): import("react").JSX.Element;
24
+ /** @internal */
25
+ export declare function SkyDome({ topColor, horizonColor, radius, offset, exponent, sunDirection, sunColor, sunIntensity, hazeStrength, sunGlowStrength, materialRef, }?: SkyDomeProps): import("react").JSX.Element;
20
26
  export interface DaylightProps {
21
27
  sky?: SkyDomeProps | false;
22
28
  fog?: {
@@ -36,14 +42,21 @@ export interface DaylightProps {
36
42
  };
37
43
  /** When false, only the sky dome and fog mount — use with authored `PlayableGame.lighting`. Default true. */
38
44
  lights?: boolean;
45
+ /** Raymarched volumetric cloud layer over the sky dome. Omit for no clouds — off by default. */
46
+ clouds?: VolumetricCloudsConfig;
39
47
  }
40
- export declare function Daylight({ sky, fog, sun, ambient, lights }?: DaylightProps): import("react").JSX.Element;
48
+ /** @internal */
49
+ export declare function Daylight({ sky, fog, sun, ambient, lights, clouds }?: DaylightProps): import("react").JSX.Element;
41
50
  export interface SkyDaylightProps {
42
51
  sky: SkyEnvironmentDescriptor;
43
52
  lights?: boolean;
53
+ /** Terrain `biomeBands`; when any carries `fog`/`sky`, fog and dome cross-fade per camera z. */
54
+ bands?: readonly BiomeBand[];
44
55
  }
45
- /** Renders a fixed sky/sun/fog look sampled from `sky`'s preset (or, when `timeOfDay` is on but no clock drives it, its noon look). No per-frame updates. */
46
- export declare function SkyDaylight({ sky, lights }: SkyDaylightProps): import("react").JSX.Element;
56
+ /** Renders a fixed sky/sun/fog look sampled from `sky`'s preset (or, when `timeOfDay` is on but no clock drives it, its noon look). No per-frame updates.
57
+ * @internal
58
+ */
59
+ export declare function SkyDaylight({ sky, lights, bands }: SkyDaylightProps): import("react").JSX.Element;
47
60
  export interface TimeOfDayDaylightProps {
48
61
  sky: SkyEnvironmentDescriptor;
49
62
  /** The world's `SimClock` (or a stub exposing `calendar().dayFraction`). Absent means static rendering. */
@@ -53,10 +66,13 @@ export interface TimeOfDayDaylightProps {
53
66
  };
54
67
  };
55
68
  lights?: boolean;
69
+ /** Terrain `biomeBands`; when any carries `fog`/`sky`, fog and dome cross-fade per camera z. */
70
+ bands?: readonly BiomeBand[];
56
71
  }
57
72
  /**
58
73
  * Drives sky/fog (and optional default lights) from the world clock when `sky.timeOfDay` and `clock`
59
74
  * are both present. Authored `PlayableGame.lighting` is never rewritten — pass `lights={false}` so
60
75
  * only dome colors and fog track the day fraction.
61
- */
62
- export declare function TimeOfDayDaylight({ sky, clock, lights }: TimeOfDayDaylightProps): import("react").JSX.Element;
76
+ * @internal
77
+ */
78
+ export declare function TimeOfDayDaylight({ sky, clock, lights, bands }: TimeOfDayDaylightProps): import("react").JSX.Element;
@@ -2,7 +2,10 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
2
2
  import { useFrame } from "@react-three/fiber";
3
3
  import { useEffect, useMemo, useRef } from "react";
4
4
  import * as THREE from "three";
5
+ import { createBiomeFogSampler, createBiomeSkySampler } from "@jgengine/core/world/terrain";
6
+ import { resolveVolumetricClouds } from "@jgengine/core/world/volumetricClouds";
5
7
  import { daylightStateAt, SKY_PRESET_DAY_FRACTION } from "./daylightCycle.js";
8
+ import { VolumetricClouds } from "./VolumetricClouds.js";
6
9
  function normalizeVec3(v) {
7
10
  const out = new THREE.Vector3(v[0], v[1], v[2]);
8
11
  return out.lengthSq() === 0 ? new THREE.Vector3(0, 1, 0) : out.normalize();
@@ -32,7 +35,9 @@ function ShadowCastingSun({ position, intensity, color, }) {
32
35
  });
33
36
  return (_jsx("directionalLight", { ref: ref, position: [position[0], position[1], position[2]], intensity: intensity, color: color, castShadow: true, "shadow-mapSize-width": 2048, "shadow-mapSize-height": 2048, "shadow-camera-left": -90, "shadow-camera-right": 90, "shadow-camera-top": 90, "shadow-camera-bottom": -90, "shadow-camera-near": 10, "shadow-camera-far": 520, "shadow-bias": -0.0004, "shadow-normalBias": 0.02 }));
34
37
  }
35
- export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius = 260, offset = 24, exponent = 0.65, sunDirection, sunColor = "#fff4d6", sunIntensity = 1, materialRef, } = {}) {
38
+ /** @internal */
39
+ export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius = 260, offset = 24, exponent = 0.65, sunDirection, sunColor = "#fff4d6", sunIntensity = 1, hazeStrength = 0.5, sunGlowStrength = 1, materialRef, } = {}) {
40
+ const meshRef = useRef(null);
36
41
  const material = useMemo(() => {
37
42
  return new THREE.ShaderMaterial({
38
43
  uniforms: {
@@ -43,6 +48,8 @@ export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius
43
48
  uSunColor: { value: new THREE.Color(sunColor) },
44
49
  uSunDirection: { value: sunDirection === undefined ? new THREE.Vector3(0, 1, 0) : normalizeVec3(sunDirection) },
45
50
  uSunIntensity: { value: sunDirection === undefined ? 0 : sunIntensity },
51
+ uHazeStrength: { value: hazeStrength },
52
+ uSunGlow: { value: sunGlowStrength },
46
53
  },
47
54
  vertexShader: `
48
55
  varying vec3 vWorldPosition;
@@ -58,6 +65,8 @@ export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius
58
65
  uniform vec3 uSunColor;
59
66
  uniform vec3 uSunDirection;
60
67
  uniform float uSunIntensity;
68
+ uniform float uHazeStrength;
69
+ uniform float uSunGlow;
61
70
  uniform float offset;
62
71
  uniform float exponent;
63
72
  varying vec3 vWorldPosition;
@@ -72,7 +81,7 @@ export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius
72
81
  vec3 dir = normalize(vWorldPosition + vec3(0.0, offset, 0.0));
73
82
  float h = max(dir.y, 0.0);
74
83
  vec3 col = mix(bottomColor, topColor, pow(h, exponent));
75
- col = mix(col, bottomColor * 1.12, pow(1.0 - h, 6.0) * 0.5);
84
+ col = mix(col, bottomColor * 1.12, pow(1.0 - h, 6.0) * uHazeStrength);
76
85
  vec3 vd = normalize(vWorldPosition);
77
86
  vec2 cuv = vd.xz / (max(vd.y, 0.06) + 0.15) * 1.2;
78
87
  float clouds = smoothstep(0.52, 0.82, sFbm(cuv));
@@ -80,7 +89,7 @@ export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius
80
89
  col = mix(col, mix(topColor, vec3(1.0), 0.6), clouds * band * 0.35);
81
90
  float sd = max(dot(vd, normalize(uSunDirection)), 0.0);
82
91
  float glow = pow(sd, 8.0) * 0.35 + pow(sd, 128.0) * 2.6;
83
- col += uSunColor * glow * uSunIntensity;
92
+ col += uSunColor * glow * uSunIntensity * uSunGlow;
84
93
  gl_FragColor = vec4(col, 1.0);
85
94
  }
86
95
  `,
@@ -88,7 +97,7 @@ export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius
88
97
  depthWrite: false,
89
98
  fog: false,
90
99
  });
91
- }, [topColor, horizonColor, offset, exponent, sunColor, sunDirection, sunIntensity]);
100
+ }, [topColor, horizonColor, offset, exponent, sunColor, sunDirection, sunIntensity, hazeStrength, sunGlowStrength]);
92
101
  useEffect(() => {
93
102
  if (materialRef !== undefined)
94
103
  materialRef.current = material;
@@ -98,27 +107,111 @@ export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius
98
107
  materialRef.current = null;
99
108
  };
100
109
  }, [material, materialRef]);
101
- return (_jsx("mesh", { material: material, renderOrder: -1, children: _jsx("sphereGeometry", { args: [radius, 32, 16] }) }));
110
+ useFrame((state) => {
111
+ const mesh = meshRef.current;
112
+ if (mesh === null)
113
+ return;
114
+ mesh.position.x = state.camera.position.x;
115
+ mesh.position.z = state.camera.position.z;
116
+ });
117
+ return (_jsx("mesh", { ref: meshRef, material: material, renderOrder: -1, frustumCulled: false, children: _jsx("sphereGeometry", { args: [radius, 32, 16] }) }));
102
118
  }
103
- export function Daylight({ sky, fog, sun, ambient, lights = true } = {}) {
119
+ /** @internal */
120
+ export function Daylight({ sky, fog, sun, ambient, lights = true, clouds } = {}) {
104
121
  const sunPosition = sun?.position ?? [120, 160, 70];
105
- return (_jsxs(_Fragment, { children: [sky === false ? null : (_jsx(SkyDome, { ...(sky ?? {}), sunDirection: sunPosition, sunColor: sun?.color ?? SUN_COLOR, sunIntensity: sun?.intensity ?? 1 })), fog === false ? null : (_jsx("fog", { attach: "fog", args: [fog?.color ?? FOG_COLOR, fog?.near ?? 70, fog?.far ?? 260] })), lights ? (_jsxs(_Fragment, { children: [_jsx("hemisphereLight", { args: [ambient?.skyColor ?? HEMI_SKY, ambient?.groundColor ?? HEMI_GROUND, ambient?.intensity ?? 0.55] }), _jsx(ShadowCastingSun, { position: sunPosition, intensity: sun?.intensity ?? 0.85, color: sun?.color ?? SUN_COLOR })] })) : null] }));
122
+ return (_jsxs(_Fragment, { children: [sky === false ? null : (_jsx(SkyDome, { ...(sky ?? {}), sunDirection: sunPosition, sunColor: sun?.color ?? SUN_COLOR, sunIntensity: sun?.intensity ?? 1 })), clouds === undefined ? null : (_jsx(VolumetricClouds, { rules: resolveVolumetricClouds(clouds), sunDirection: sunPosition })), fog === false ? null : (_jsx("fog", { attach: "fog", args: [fog?.color ?? FOG_COLOR, fog?.near ?? 70, fog?.far ?? 260] })), lights ? (_jsxs(_Fragment, { children: [_jsx("hemisphereLight", { args: [ambient?.skyColor ?? HEMI_SKY, ambient?.groundColor ?? HEMI_GROUND, ambient?.intensity ?? 0.55] }), _jsx(ShadowCastingSun, { position: sunPosition, intensity: sun?.intensity ?? 0.85, color: sun?.color ?? SUN_COLOR })] })) : null] }));
123
+ }
124
+ /** True when at least one band overrides fog or sky — the trigger for the per-frame biome driver. */
125
+ function bandsDriveSkyOrFog(bands) {
126
+ return bands !== undefined && bands.some((band) => band.fog !== undefined || band.sky !== undefined);
127
+ }
128
+ /** Renders a fixed sky/sun/fog look sampled from `sky`'s preset (or, when `timeOfDay` is on but no clock drives it, its noon look). No per-frame updates.
129
+ * @internal
130
+ */
131
+ export function SkyDaylight({ sky, lights = true, bands }) {
132
+ if (bandsDriveSkyOrFog(bands))
133
+ return _jsx(BiomeDaylight, { sky: sky, bands: bands, lights: lights });
134
+ return _jsx(StaticSkyDaylight, { sky: sky, lights: lights });
135
+ }
136
+ /** Dome-shape overrides (radius, haze, sun-glow) carried by the sky descriptor, spread onto `SkyDome`; unset fields keep the dome defaults. */
137
+ function skyDomeShape(sky) {
138
+ return {
139
+ ...(sky.radius === undefined ? {} : { radius: sky.radius }),
140
+ ...(sky.hazeStrength === undefined ? {} : { hazeStrength: sky.hazeStrength }),
141
+ ...(sky.sunGlowStrength === undefined ? {} : { sunGlowStrength: sky.sunGlowStrength }),
142
+ };
106
143
  }
107
- /** Renders a fixed sky/sun/fog look sampled from `sky`'s preset (or, when `timeOfDay` is on but no clock drives it, its noon look). No per-frame updates. */
108
- export function SkyDaylight({ sky, lights = true }) {
144
+ function StaticSkyDaylight({ sky, lights }) {
109
145
  const state = useMemo(() => daylightStateAt(SKY_PRESET_DAY_FRACTION[sky.preset], sky), [sky]);
110
- return (_jsx(Daylight, { sky: { topColor: state.skyTop, horizonColor: state.skyBottom }, fog: { color: sky.fog?.color ?? state.background, near: sky.fog?.near, far: sky.fog?.far }, sun: { position: state.sunPosition, intensity: state.sunIntensity }, ambient: { intensity: state.ambientIntensity }, lights: lights }));
146
+ return (_jsx(Daylight, { sky: { topColor: state.skyTop, horizonColor: state.skyBottom, ...skyDomeShape(sky) }, fog: { color: sky.fog?.color ?? state.background, near: sky.fog?.near, far: sky.fog?.far }, sun: { position: state.sunPosition, intensity: state.sunIntensity }, ambient: { intensity: state.ambientIntensity }, lights: lights, clouds: sky.volumetricClouds }));
111
147
  }
112
148
  /**
113
149
  * Drives sky/fog (and optional default lights) from the world clock when `sky.timeOfDay` and `clock`
114
150
  * are both present. Authored `PlayableGame.lighting` is never rewritten — pass `lights={false}` so
115
151
  * only dome colors and fog track the day fraction.
116
- */
117
- export function TimeOfDayDaylight({ sky, clock, lights = true }) {
152
+ * @internal
153
+ */
154
+ export function TimeOfDayDaylight({ sky, clock, lights = true, bands }) {
155
+ if (bandsDriveSkyOrFog(bands))
156
+ return _jsx(BiomeDaylight, { sky: sky, bands: bands, clock: clock, lights: lights });
118
157
  if (!sky.timeOfDay || clock === undefined)
119
158
  return _jsx(SkyDaylight, { sky: sky, lights: lights });
120
159
  return _jsx(DrivenDaylight, { sky: sky, clock: clock, lights: lights });
121
160
  }
161
+ /**
162
+ * Per-frame biome fog/sky driver: cross-fades fog color/range, dome colors, and sun/ambient intensity
163
+ * along the camera's z through the terrain's `biomeBands`. Base look is the sky preset (or, when
164
+ * `sky.timeOfDay` and a `clock` are present, the live day fraction); bands ride on top.
165
+ */
166
+ function BiomeDaylight({ sky, bands, clock, lights, }) {
167
+ const timeOfDay = sky.timeOfDay && clock !== undefined;
168
+ const baseFraction = timeOfDay ? clock.calendar().dayFraction : SKY_PRESET_DAY_FRACTION[sky.preset];
169
+ const initial = useMemo(() => daylightStateAt(baseFraction, sky), [sky, baseFraction]);
170
+ const sunRef = useRef(null);
171
+ const hemiRef = useRef(null);
172
+ const fogRef = useRef(null);
173
+ const skyMaterialRef = useRef(null);
174
+ const fogFallback = useMemo(() => ({
175
+ color: sky.fog?.color ?? initial.background,
176
+ near: sky.fog?.near ?? 70,
177
+ far: sky.fog?.far ?? 260,
178
+ density: 0,
179
+ }), [sky.fog?.color, sky.fog?.near, sky.fog?.far, initial.background]);
180
+ const skyFallback = useMemo(() => ({
181
+ horizonColor: initial.skyBottom,
182
+ zenithColor: initial.skyTop,
183
+ sunIntensity: initial.sunIntensity,
184
+ ambientIntensity: initial.ambientIntensity,
185
+ }), [initial.skyBottom, initial.skyTop, initial.sunIntensity, initial.ambientIntensity]);
186
+ const fogSampler = useMemo(() => createBiomeFogSampler(bands, fogFallback), [bands, fogFallback]);
187
+ const skySampler = useMemo(() => createBiomeSkySampler(bands, skyFallback), [bands, skyFallback]);
188
+ useFrame((state) => {
189
+ const base = timeOfDay ? daylightStateAt(clock.calendar().dayFraction, sky) : initial;
190
+ const z = state.camera.position.z;
191
+ const fog = fogSampler(z);
192
+ const skyValue = skySampler(z);
193
+ const fogNode = fogRef.current;
194
+ if (fogNode !== null) {
195
+ fogNode.color.set(fog.color);
196
+ fogNode.near = fog.near;
197
+ fogNode.far = fog.far;
198
+ }
199
+ const skyMaterial = skyMaterialRef.current;
200
+ if (skyMaterial !== null) {
201
+ skyMaterial.uniforms.topColor.value.set(skyValue.zenithColor);
202
+ skyMaterial.uniforms.bottomColor.value.set(skyValue.horizonColor);
203
+ }
204
+ const sun = sunRef.current;
205
+ if (sun !== null) {
206
+ sun.position.set(base.sunPosition[0], base.sunPosition[1], base.sunPosition[2]);
207
+ sun.intensity = skyValue.sunIntensity;
208
+ }
209
+ const hemi = hemiRef.current;
210
+ if (hemi !== null)
211
+ hemi.intensity = skyValue.ambientIntensity;
212
+ });
213
+ return (_jsxs(_Fragment, { children: [_jsx(SkyDome, { topColor: initial.skyTop, horizonColor: initial.skyBottom, materialRef: skyMaterialRef, ...skyDomeShape(sky) }), sky.volumetricClouds === undefined ? null : (_jsx(VolumetricClouds, { rules: resolveVolumetricClouds(sky.volumetricClouds), sunDirection: initial.sunPosition })), _jsx("fog", { attach: "fog", ref: fogRef, args: [fogFallback.color, fogFallback.near, fogFallback.far] }), lights ? (_jsxs(_Fragment, { children: [_jsx("hemisphereLight", { ref: hemiRef, args: [HEMI_SKY, HEMI_GROUND, initial.ambientIntensity] }), _jsx("directionalLight", { ref: sunRef, position: initial.sunPosition, intensity: initial.sunIntensity, color: SUN_COLOR, castShadow: true })] })) : null] }));
214
+ }
122
215
  function DrivenDaylight({ sky, clock, lights, }) {
123
216
  const initial = useMemo(() => daylightStateAt(clock.calendar().dayFraction, sky), [clock, sky]);
124
217
  const sunRef = useRef(null);
@@ -144,5 +237,5 @@ function DrivenDaylight({ sky, clock, lights, }) {
144
237
  skyMaterial.uniforms.bottomColor.value.set(state.skyBottom);
145
238
  }
146
239
  });
147
- return (_jsxs(_Fragment, { children: [_jsx(SkyDome, { topColor: initial.skyTop, horizonColor: initial.skyBottom, materialRef: skyMaterialRef }), _jsx("fog", { attach: "fog", ref: fogRef, args: [sky.fog?.color ?? initial.background, sky.fog?.near ?? 70, sky.fog?.far ?? 260] }), lights ? (_jsxs(_Fragment, { children: [_jsx("hemisphereLight", { ref: hemiRef, args: [HEMI_SKY, HEMI_GROUND, initial.ambientIntensity] }), _jsx("directionalLight", { ref: sunRef, position: initial.sunPosition, intensity: initial.sunIntensity, color: SUN_COLOR, castShadow: true })] })) : null] }));
240
+ return (_jsxs(_Fragment, { children: [_jsx(SkyDome, { topColor: initial.skyTop, horizonColor: initial.skyBottom, materialRef: skyMaterialRef, ...skyDomeShape(sky) }), sky.volumetricClouds === undefined ? null : (_jsx(VolumetricClouds, { rules: resolveVolumetricClouds(sky.volumetricClouds), sunDirection: initial.sunPosition })), _jsx("fog", { attach: "fog", ref: fogRef, args: [sky.fog?.color ?? initial.background, sky.fog?.near ?? 70, sky.fog?.far ?? 260] }), lights ? (_jsxs(_Fragment, { children: [_jsx("hemisphereLight", { ref: hemiRef, args: [HEMI_SKY, HEMI_GROUND, initial.ambientIntensity] }), _jsx("directionalLight", { ref: sunRef, position: initial.sunPosition, intensity: initial.sunIntensity, color: SUN_COLOR, castShadow: true })] })) : null] }));
148
241
  }