@jgengine/shell 0.14.0 → 0.15.0-next.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 (114) hide show
  1. package/CHANGELOG.md +483 -1
  2. package/dist/GamePlayerShell.js +25 -5
  3. package/dist/Shell3dPresentation.d.ts +6 -6
  4. package/dist/Shell3dPresentation.js +4 -4
  5. package/dist/camera/ControllerCameraShake.d.ts +31 -0
  6. package/dist/camera/ControllerCameraShake.js +34 -0
  7. package/dist/camera/GameCameraRig.js +10 -5
  8. package/dist/camera/GameOrbitCamera.js +1 -10
  9. package/dist/camera/cameraRigs.js +12 -4
  10. package/dist/camera/index.d.ts +1 -0
  11. package/dist/camera/index.js +1 -0
  12. package/dist/camera/rigMath.d.ts +2 -0
  13. package/dist/camera/rigMath.js +7 -0
  14. package/dist/devtools/CollisionDebugWorld.js +2 -2
  15. package/dist/devtools/DevtoolsOverlay.js +2 -0
  16. package/dist/drivers/FrameDriver.d.ts +4 -1
  17. package/dist/drivers/FrameDriver.js +5 -3
  18. package/dist/environment/DayNightSky.d.ts +33 -0
  19. package/dist/environment/DayNightSky.js +51 -0
  20. package/dist/environment/Daylight.d.ts +3 -1
  21. package/dist/environment/Daylight.js +7 -3
  22. package/dist/environment/EnvironmentScene.js +6 -6
  23. package/dist/environment/RoadJunctions.d.ts +8 -4
  24. package/dist/environment/RoadJunctions.js +16 -15
  25. package/dist/environment/RoadRibbons.d.ts +42 -6
  26. package/dist/environment/RoadRibbons.js +150 -27
  27. package/dist/environment/VolumetricClouds.js +3 -3
  28. package/dist/environment/index.d.ts +1 -0
  29. package/dist/environment/index.js +1 -0
  30. package/dist/gameKit.d.ts +7 -4
  31. package/dist/gameKit.js +8 -5
  32. package/dist/inputSink.js +14 -0
  33. package/dist/materialOverride.js +30 -0
  34. package/dist/postfx/PostProcessing.d.ts +3 -2
  35. package/dist/postfx/PostProcessing.js +11 -3
  36. package/dist/postfx/ScreenEffectsOverlay.d.ts +32 -0
  37. package/dist/postfx/ScreenEffectsOverlay.js +70 -0
  38. package/dist/presentationResolve.d.ts +3 -2
  39. package/dist/presentationResolve.js +11 -6
  40. package/dist/render/CascadedShadows.d.ts +13 -0
  41. package/dist/render/CascadedShadows.js +96 -0
  42. package/dist/render/EnvironmentLighting.d.ts +23 -7
  43. package/dist/render/EnvironmentLighting.js +69 -10
  44. package/dist/render/PartMotion.js +4 -4
  45. package/dist/render/SceneLighting.js +7 -0
  46. package/dist/render/SceneModels.d.ts +37 -1
  47. package/dist/render/SceneModels.js +40 -11
  48. package/dist/render/modelLoad.d.ts +41 -5
  49. package/dist/render/modelLoad.js +85 -18
  50. package/dist/render/sceneCapture.d.ts +27 -0
  51. package/dist/render/sceneCapture.js +43 -0
  52. package/dist/render/useDisposable.d.ts +16 -0
  53. package/dist/render/useDisposable.js +24 -0
  54. package/dist/render/useModelAnimation.js +47 -4
  55. package/dist/scene/AuthoredScene.js +14 -24
  56. package/dist/scene/CityRenderer.js +103 -36
  57. package/dist/scene/GrassFieldRenderer.js +4 -1
  58. package/dist/scene/PoleLineRenderer.js +7 -16
  59. package/dist/settings/QuickControls.js +5 -3
  60. package/dist/settings/SettingsMenu.js +6 -3
  61. package/dist/settings/appliedSettings.d.ts +3 -1
  62. package/dist/settings/appliedSettings.js +17 -5
  63. package/dist/settings/settingsController.js +11 -5
  64. package/dist/shellMovement.d.ts +15 -1
  65. package/dist/shellMovement.js +15 -1
  66. package/dist/structures/GeneratedBuilding.js +4 -5
  67. package/dist/structures/TransformGizmo.js +6 -5
  68. package/dist/terrain/CarvedTerrain.js +3 -7
  69. package/dist/terrain/GrassField.js +2 -2
  70. package/dist/terrain/ProceduralGround.js +3 -5
  71. package/dist/terrain/grassMaterial.d.ts +8 -2
  72. package/dist/terrain/grassMaterial.js +24 -5
  73. package/dist/terrain/index.d.ts +1 -1
  74. package/dist/terrain/terrainDetailMaterial.js +11 -2
  75. package/dist/terrain/terrainMath.d.ts +38 -1
  76. package/dist/terrain/terrainMath.js +86 -64
  77. package/dist/touch/TouchControlsOverlay.d.ts +6 -2
  78. package/dist/touch/TouchControlsOverlay.js +136 -17
  79. package/dist/vfx/ParticleField.d.ts +32 -0
  80. package/dist/vfx/ParticleField.js +91 -0
  81. package/dist/vfx/WorldFloatingText.d.ts +32 -0
  82. package/dist/vfx/WorldFloatingText.js +32 -0
  83. package/dist/water/Ocean.js +3 -6
  84. package/dist/weather/LightningStrike.js +3 -9
  85. package/dist/weather/RainField.js +3 -7
  86. package/dist/weather/SnowField.js +3 -7
  87. package/dist/world/DataObjects.js +3 -6
  88. package/dist/world/GridWorldScene.js +3 -6
  89. package/dist/world/InstancedBodies.js +3 -6
  90. package/dist/world/InstancedJoints.js +2 -2
  91. package/dist/world/SpriteBatch.js +3 -6
  92. package/dist/world/WorldEntityFrames.d.ts +47 -0
  93. package/dist/world/WorldEntityFrames.js +75 -0
  94. package/dist/world/WorldHud.js +7 -5
  95. package/dist/world/WorldPings.d.ts +26 -0
  96. package/dist/world/WorldPings.js +68 -0
  97. package/dist/world/WorldScene.js +9 -4
  98. package/dist/world/pingPulse.d.ts +21 -0
  99. package/dist/world/pingPulse.js +17 -0
  100. package/package.json +5 -5
  101. package/skills/ce-handoff/SKILL.md +149 -0
  102. package/skills/game-design/SKILL.md +2 -0
  103. package/skills/jgengine/SKILL.md +3 -1
  104. package/skills/jgengine/api.md +17 -7
  105. package/skills/jgengine/capabilities.md +12 -0
  106. package/skills/jgengine/recipes/minimal-game.md +7 -4
  107. package/skills/jgengine-ui/SKILL.md +5 -3
  108. package/skills/jgengine-ui/api.md +708 -19
  109. package/skills/jgengine-ui/capabilities.md +362 -1
  110. package/skills/jgengine-ui/recipes/overhead-entity-frames.md +136 -0
  111. package/skills/jgengine-ui/reference.md +7 -4
  112. package/skills/jgengine-world/api.md +378 -11
  113. package/skills/jgengine-world/capabilities.md +123 -2
  114. package/skills/jgengine-world/reference.md +15 -1
@@ -3,6 +3,7 @@ import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore
3
3
  import { createActionStateTracker, toActionStateBindingMap, } from "@jgengine/core/input/actionBindings";
4
4
  import { RESERVED_INPUT_ACTIONS } from "./boundActionDispatch.js";
5
5
  import { deriveTouchScheme, withTouchCodes, DEFAULT_TOUCH_STYLE } from "@jgengine/core/input/touchScheme";
6
+ import { activeTouchControlsMode } from "@jgengine/core/input/touchControlsMode";
6
7
  import { normalizePointerToAxis } from "@jgengine/core/input/pointerAxis";
7
8
  import { createGameContext } from "@jgengine/core/runtime/gameContext";
8
9
  import { useDisplayProfile } from "@jgengine/react/display";
@@ -11,6 +12,7 @@ import { createSettingsStore } from "@jgengine/core/settings/settingsModel";
11
12
  import { applyBindingOverrides, clearBindingOverride, loadBindingOverrides, saveBindingOverride, } from "@jgengine/core/input/bindingOverrides";
12
13
  import { orientationGateActive, orientationHintActive, resolveOrientationRequirement, } from "@jgengine/core/ui/orientation";
13
14
  import { armFallbackSeams } from "@jgengine/core/devtools/fallbackSeams";
15
+ import { armTextureErrors } from "@jgengine/core/devtools/textureErrors";
14
16
  import { readUrlFlag, subscribeUrlChange, writeUrlParam } from "@jgengine/core/devtools/urlFlags";
15
17
  import { createAudioEngine } from "./audio/audioEngine.js";
16
18
  import { attachAudioEventWire } from "./audio/audioWire.js";
@@ -19,7 +21,7 @@ import { withDevtoolsLatency } from "./devtools/DevtoolsOverlay.js";
19
21
  import { resolveRigKind } from "./camera/index.js";
20
22
  import { contextModels } from "./render/resolveModel.js";
21
23
  import { OrientationHint } from "./touch/OrientationHint.js";
22
- import { useTouchStyle, useGraphicsSettings } from "./settings/appliedSettings.js";
24
+ import { useTouchStyle, useTouchJoystickVariant, useGraphicsSettings } from "./settings/appliedSettings.js";
23
25
  import { logRuntimeError, } from "./diagnostics/RuntimeDiagnostics.js";
24
26
  import { EMPTY_RESERVED } from "./shellConstants.js";
25
27
  import { useShellMultiplayerSync } from "./useShellMultiplayerSync.js";
@@ -41,6 +43,7 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
41
43
  const multiplayer = useMemo(() => (rawMultiplayer === null ? null : withDevtoolsLatency(rawMultiplayer)), [rawMultiplayer]);
42
44
  const devtoolsEnabled = playable.devtools !== false && !poster;
43
45
  armFallbackSeams(devtoolsEnabled);
46
+ armTextureErrors(devtoolsEnabled);
44
47
  // `?debug` mirrors the devtools overlay into the URL: open with a link, strip the param to close.
45
48
  const [devtoolsOpen, setDevtoolsOpen] = useState(() => devtoolsEnabled && readUrlFlag(DEBUG_PARAM));
46
49
  const devtoolsOpenRef = useRef(false);
@@ -76,9 +79,12 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
76
79
  const f2HeldRef = useRef(false);
77
80
  const settingsStore = useMemo(() => createSettingsStore(), []);
78
81
  const [bindingOverrides, setBindingOverrides] = useState(() => loadBindingOverrides(playable.game.name));
79
- useEffect(() => {
82
+ // Render-time reset (no stale-keybinds frame): reload persisted overrides once per playable change.
83
+ const [overridesPlayable, setOverridesPlayable] = useState(playable);
84
+ if (overridesPlayable !== playable) {
85
+ setOverridesPlayable(playable);
80
86
  setBindingOverrides(loadBindingOverrides(playable.game.name));
81
- }, [playable]);
87
+ }
82
88
  const effectiveInput = useMemo(() => applyBindingOverrides(playable.game.input ?? {}, bindingOverrides), [playable, bindingOverrides]);
83
89
  const rebindAction = useCallback((action, code) => setBindingOverrides(saveBindingOverride(playable.game.name, action, [code])), [playable]);
84
90
  const resetActionBinding = useCallback((action) => setBindingOverrides(clearBindingOverride(playable.game.name, action)), [playable]);
@@ -95,16 +101,28 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
95
101
  if (state !== null && state.active)
96
102
  pointerAxisRef.current = { ...state, active: false };
97
103
  };
104
+ // Reads fresh every render: the ctx-version useSyncExternalStore below re-renders on store
105
+ // writes, so a gameplay setTouchControlsMode("car") swaps the visible control set that frame.
106
+ const touchMode = ctx === null ? null : activeTouchControlsMode(ctx);
98
107
  const touchScheme = useMemo(() => deriveTouchScheme(playable.game.input, {
99
108
  reserved: resolveRigKind(playable.camera) === "none" || playable.presentation === "hud"
100
109
  ? EMPTY_RESERVED
101
110
  : RESERVED_INPUT_ACTIONS,
102
111
  firstPerson: resolveRigKind(playable.camera) === "first",
103
112
  config: playable.touch,
104
- }), [playable]);
113
+ mode: touchMode,
114
+ }), [playable, touchMode]);
105
115
  const touchStyle = useTouchStyle(settingsStore, touchScheme?.style ?? DEFAULT_TOUCH_STYLE);
116
+ const touchJoystickVariant = useTouchJoystickVariant(settingsStore);
106
117
  const { coarsePointer, portrait, compact } = useDisplayProfile();
107
- const touchSink = useMemo(() => ({ onCodeDown: (code) => tracker.handleDown(code), onCodeUp: (code) => tracker.handleUp(code) }), [tracker]);
118
+ const analogRef = useRef(null);
119
+ const touchSink = useMemo(() => ({
120
+ onCodeDown: (code) => tracker.handleDown(code),
121
+ onCodeUp: (code) => tracker.handleUp(code),
122
+ onAnalog: (values) => {
123
+ analogRef.current = values;
124
+ },
125
+ }), [tracker]);
108
126
  const gateRef = useRef(false);
109
127
  const orientationPlatform = coarsePointer ? "mobile" : "desktop";
110
128
  const orientationRequirement = useMemo(() => resolveOrientationRequirement(playable.orientation, orientationPlatform), [playable, orientationPlatform]);
@@ -199,6 +217,8 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
199
217
  pitchRef,
200
218
  touchScheme,
201
219
  touchSink,
220
+ touchJoystickVariant,
221
+ analogRef,
202
222
  orientationGate,
203
223
  orientationGateEl,
204
224
  coarsePointer,
@@ -2,16 +2,17 @@ import { type Dispatch, type MutableRefObject, type RefObject, type SetStateActi
2
2
  import type { PointerAxisState } from "@jgengine/core/input/pointerAxis";
3
3
  import type { ActionStateTracker } from "@jgengine/core/input/actionBindings";
4
4
  import type { BindingOverrides } from "@jgengine/core/input/bindingOverrides";
5
- import type { TouchScheme, TouchStyle } from "@jgengine/core/input/touchScheme";
5
+ import type { TouchJoystickVariant, TouchScheme, TouchStyle } from "@jgengine/core/input/touchScheme";
6
6
  import { type GraphicsQuality, type SettingsStore } from "@jgengine/core/settings/settingsModel";
7
7
  import type { PresencePoseRow } from "@jgengine/core/runtime/transport";
8
8
  import type { GameContext } from "@jgengine/core/runtime/gameContext";
9
9
  import type { AudioEngine } from "./audio/audioEngine.js";
10
10
  import type { ShellMultiplayer } from "./multiplayer.js";
11
11
  import type { PlayableGame } from "./registry.js";
12
+ import { type TouchCodeSink } from "./touch/TouchControlsOverlay.js";
12
13
  import type { RuntimeDiagnostic } from "./diagnostics/RuntimeDiagnostics.js";
13
14
  /** 3D play surface: canvas, world overlays, and shared chrome. @internal */
14
- export declare function Shell3dPresentation({ playable, ctx, multiplayer, tracker, pointerAxisRef, gateRef, wrapperRef, f2HeldRef, yawRef, pitchRef, primaryClickRef, cameraDraggingRef, serverIdRef, remotePlayers, touchScheme, touchSink, touchStyle, orientationGate, orientationGateEl, coarsePointer, compact, graphics, settingsStore, bindingOverrides, rebindAction, resetActionBinding, diagnostics, devtoolsEnabled, devtoolsOpen, setDevtoolsOpen, reportRuntimeError, trackPointerAxis, deactivatePointerAxis, audioEngine, poster, posterFrozen, onPosterSettled, }: {
15
+ export declare function Shell3dPresentation({ playable, ctx, multiplayer, tracker, pointerAxisRef, gateRef, wrapperRef, f2HeldRef, yawRef, pitchRef, primaryClickRef, cameraDraggingRef, serverIdRef, remotePlayers, touchScheme, touchSink, touchStyle, touchJoystickVariant, analogRef, orientationGate, orientationGateEl, coarsePointer, compact, graphics, settingsStore, bindingOverrides, rebindAction, resetActionBinding, diagnostics, devtoolsEnabled, devtoolsOpen, setDevtoolsOpen, reportRuntimeError, trackPointerAxis, deactivatePointerAxis, audioEngine, poster, posterFrozen, onPosterSettled, }: {
15
16
  playable: PlayableGame;
16
17
  ctx: GameContext;
17
18
  multiplayer: ShellMultiplayer | null;
@@ -27,11 +28,10 @@ export declare function Shell3dPresentation({ playable, ctx, multiplayer, tracke
27
28
  serverIdRef: MutableRefObject<string | null>;
28
29
  remotePlayers: PresencePoseRow[];
29
30
  touchScheme: TouchScheme | null;
30
- touchSink: {
31
- onCodeDown: (code: string) => void;
32
- onCodeUp: (code: string) => void;
33
- };
31
+ touchSink: TouchCodeSink;
34
32
  touchStyle: TouchStyle;
33
+ touchJoystickVariant: TouchJoystickVariant;
34
+ analogRef: MutableRefObject<Readonly<Record<string, number>> | null>;
35
35
  orientationGate: boolean;
36
36
  orientationGateEl: React.ReactNode;
37
37
  coarsePointer: boolean;
@@ -44,7 +44,7 @@ import { resolvePresentationEffects, resolveWorldOverlayBars, } from "./presenta
44
44
  const PRIMARY_CLICK_MOVE_THRESHOLD_PX = 6;
45
45
  const DEFAULT_BACKGROUND_COLOR = "#14161b";
46
46
  /** 3D play surface: canvas, world overlays, and shared chrome. @internal */
47
- export function Shell3dPresentation({ playable, ctx, multiplayer, tracker, pointerAxisRef, gateRef, wrapperRef, f2HeldRef, yawRef, pitchRef, primaryClickRef, cameraDraggingRef, serverIdRef, remotePlayers, touchScheme, touchSink, touchStyle, orientationGate, orientationGateEl, coarsePointer, compact, graphics, settingsStore, bindingOverrides, rebindAction, resetActionBinding, diagnostics, devtoolsEnabled, devtoolsOpen, setDevtoolsOpen, reportRuntimeError, trackPointerAxis, deactivatePointerAxis, audioEngine, poster, posterFrozen, onPosterSettled, }) {
47
+ export function Shell3dPresentation({ playable, ctx, multiplayer, tracker, pointerAxisRef, gateRef, wrapperRef, f2HeldRef, yawRef, pitchRef, primaryClickRef, cameraDraggingRef, serverIdRef, remotePlayers, touchScheme, touchSink, touchStyle, touchJoystickVariant, analogRef, orientationGate, orientationGateEl, coarsePointer, compact, graphics, settingsStore, bindingOverrides, rebindAction, resetActionBinding, diagnostics, devtoolsEnabled, devtoolsOpen, setDevtoolsOpen, reportRuntimeError, trackPointerAxis, deactivatePointerAxis, audioEngine, poster, posterFrozen, onPosterSettled, }) {
48
48
  const GameUI = playable.GameUI;
49
49
  const WorldOverlay = playable.WorldOverlay;
50
50
  const pointerService = useMemo(() => createPointerService(), []);
@@ -289,14 +289,14 @@ export function Shell3dPresentation({ playable, ctx, multiplayer, tracker, point
289
289
  fov: playable.camera?.frustum?.fov ?? CAMERA_FRUSTUM_DEFAULTS.fov,
290
290
  near: playable.camera?.frustum?.near ?? CAMERA_FRUSTUM_DEFAULTS.near,
291
291
  far: playable.camera?.frustum?.far ?? CAMERA_FRUSTUM_DEFAULTS.far,
292
- }, shadows: graphics.shadows, dpr: [Math.min(1, graphics.dpr), graphics.dpr], gl: { preserveDrawingBuffer: true }, style: { touchAction: "none" }, children: [backgroundColor !== undefined ? _jsx("color", { attach: "background", args: [backgroundColor] }) : null, cinematicLook ? _jsx(EnvironmentLighting, {}) : null, lighting !== undefined ? (_jsx(ConfiguredLighting, { lighting: lighting })) : effectiveSky === undefined ? (_jsxs(_Fragment, { children: [_jsx("ambientLight", { intensity: 0.55 }), _jsx("directionalLight", { position: [10, 16, 6], intensity: 1.3 })] })) : null, effectiveSky !== undefined ? (effectiveSky.timeOfDay ? (_jsx(TimeOfDayDaylight, { sky: effectiveSky, clock: ctx.time, bands: effectiveSky === worldSky ? biomeBands : undefined, lights: skyEmitsLights(resolveSkyLightOwnership(lighting !== undefined)) })) : (_jsx(SkyDaylight, { sky: effectiveSky, bands: effectiveSky === worldSky ? biomeBands : undefined, lights: skyEmitsLights(resolveSkyLightOwnership(lighting !== undefined)) }))) : null, _jsx(BackdropFog, { fog: backdrop?.fog }), _jsxs(GameProvider, { context: ctx, children: [_jsx(CullingProvider, { config: playable.visibility, children: _jsx(WorldView, { entitySprites: playable.entitySprites, entityModels: playable.entityModels, objectModels: playable.objectModels, objectStyles: playable.objectStyles, environment: AutoEnvironment, assets: playable.game.assets, renderEntity: playable.renderEntity, renderObject: playable.renderObject, selectedIds: selectedIds, hideLocalActor: firstPerson }) }), WorldOverlay !== undefined ? (_jsx("group", { userData: CAMERA_TRANSPARENT_USERDATA, children: _jsx(WorldOverlay, { ctx: ctx }) })) : null, bars !== null ? (_jsx(WorldEntityBars, { statId: bars.statId, roles: bars.roles, resolveRole: resolveEntityRole, ...(bars.maxDistance === undefined ? {} : { maxDistance: bars.maxDistance }) })) : null, nameplates !== null ? (_jsx(WorldNameplates, { statId: nameplates.statId, roles: nameplates.roles, resolveRole: resolveEntityRole, ...(nameplates.maxDistance === undefined ? {} : { maxDistance: nameplates.maxDistance }), ...(nameplates.showHealth === undefined ? {} : { showHealth: nameplates.showHealth }) })) : null, _jsx(WorldItems, { config: playable.worldItem }), _jsx(CombatPresentation, { effects: effects }), devtoolsEnabled ? _jsx(CollisionDebugWorld, {}) : null, _jsx(AudioListener, { engine: audioEngine }), _jsx(EntityAudioEmitters, { engine: audioEngine, entitySounds: playable.entitySounds }), _jsx(ObjectAudioEmitters, { engine: audioEngine, objectSounds: playable.objectSounds }), _jsx(GameCameraRig, { yawRef: yawRef, pitchRef: pitchRef, config: cameraConfig, viewmodel: playable.viewmodel, pointerControls: pointerUsesLeft, panKeysEnabled: rtsPanKeysEnabled, director: ctx.camera, onDragChange: (dragging) => {
292
+ }, shadows: graphics.shadows, dpr: [Math.min(1, graphics.dpr), graphics.dpr], gl: { preserveDrawingBuffer: true }, style: { touchAction: "none" }, children: [backgroundColor !== undefined ? _jsx("color", { attach: "background", args: [backgroundColor] }) : null, cinematicLook ? (_jsx(EnvironmentLighting, { skyColor: lighting?.hemisphere?.skyColor ?? "#87b5e0", groundColor: lighting?.hemisphere?.groundColor ?? "#3d4a38", sunDirection: lighting?.directional?.[0]?.position, sunColor: lighting?.directional?.[0]?.color ?? "#fff2d6" })) : null, lighting !== undefined ? (_jsx(ConfiguredLighting, { lighting: lighting })) : effectiveSky === undefined ? (_jsxs(_Fragment, { children: [_jsx("ambientLight", { intensity: 0.55 }), _jsx("directionalLight", { position: [10, 16, 6], intensity: 1.3 })] })) : null, effectiveSky !== undefined ? (effectiveSky.timeOfDay ? (_jsx(TimeOfDayDaylight, { sky: effectiveSky, clock: ctx.time, bands: effectiveSky === worldSky ? biomeBands : undefined, lights: skyEmitsLights(resolveSkyLightOwnership(lighting !== undefined)) })) : (_jsx(SkyDaylight, { sky: effectiveSky, bands: effectiveSky === worldSky ? biomeBands : undefined, lights: skyEmitsLights(resolveSkyLightOwnership(lighting !== undefined)) }))) : null, _jsx(BackdropFog, { fog: backdrop?.fog }), _jsxs(GameProvider, { context: ctx, children: [_jsx(CullingProvider, { config: playable.visibility, children: _jsx(WorldView, { entitySprites: playable.entitySprites, entityModels: playable.entityModels, objectModels: playable.objectModels, objectStyles: playable.objectStyles, environment: AutoEnvironment, assets: playable.game.assets, renderEntity: playable.renderEntity, renderObject: playable.renderObject, selectedIds: selectedIds, hideLocalActor: firstPerson }) }), WorldOverlay !== undefined ? (_jsx("group", { userData: CAMERA_TRANSPARENT_USERDATA, children: _jsx(WorldOverlay, { ctx: ctx }) })) : null, bars !== null ? (_jsx(WorldEntityBars, { statId: bars.statId, roles: bars.roles, resolveRole: resolveEntityRole, ...(bars.maxDistance === undefined ? {} : { maxDistance: bars.maxDistance }) })) : null, nameplates !== null ? (_jsx(WorldNameplates, { statId: nameplates.statId, roles: nameplates.roles, resolveRole: resolveEntityRole, ...(nameplates.maxDistance === undefined ? {} : { maxDistance: nameplates.maxDistance }), ...(nameplates.showHealth === undefined ? {} : { showHealth: nameplates.showHealth }) })) : null, _jsx(WorldItems, { config: playable.worldItem }), _jsx(CombatPresentation, { effects: effects }), devtoolsEnabled ? _jsx(CollisionDebugWorld, {}) : null, _jsx(AudioListener, { engine: audioEngine }), _jsx(EntityAudioEmitters, { engine: audioEngine, entitySounds: playable.entitySounds }), _jsx(ObjectAudioEmitters, { engine: audioEngine, objectSounds: playable.objectSounds }), _jsx(GameCameraRig, { yawRef: yawRef, pitchRef: pitchRef, config: cameraConfig, viewmodel: playable.viewmodel, pointerControls: pointerUsesLeft, panKeysEnabled: rtsPanKeysEnabled, director: ctx.camera, onDragChange: (dragging) => {
293
293
  cameraDraggingRef.current = dragging;
294
- } }), _jsx(PointerProbe, { service: pointerService })] }), _jsx(RemotePlayers, { rows: remotePlayers }), _jsx(FrameDriver, { ctx: ctx, playable: playable, tracker: tracker, yawRef: yawRef, pitchRef: pitchRef, primaryClickRef: primaryClickRef, pointerAxisRef: pointerAxisRef, gateRef: gateRef, onRuntimeError: reportRuntimeError, multiplayer: multiplayer, serverIdRef: serverIdRef, pointerService: pointerService, pointerAim: pointer?.aim === true, pingCommand: pointer?.pingCommand, poster: poster, onPosterSettled: onPosterSettled }), _jsx(DevtoolsRendererProbe, {}), resolvedLook.postProcessing !== undefined && resolvedLook.postProcessing.enabled !== false ? (_jsx(PostProcessing, { config: resolvedLook.postProcessing, quality: graphics.quality })) : null] }), !poster &&
294
+ } }), _jsx(PointerProbe, { service: pointerService })] }), _jsx(RemotePlayers, { rows: remotePlayers }), _jsx(FrameDriver, { ctx: ctx, playable: playable, tracker: tracker, yawRef: yawRef, pitchRef: pitchRef, primaryClickRef: primaryClickRef, pointerAxisRef: pointerAxisRef, analogRef: analogRef, gateRef: gateRef, onRuntimeError: reportRuntimeError, multiplayer: multiplayer, serverIdRef: serverIdRef, pointerService: pointerService, pointerAim: pointer?.aim === true, pingCommand: pointer?.pingCommand, poster: poster, onPosterSettled: onPosterSettled }), _jsx(DevtoolsRendererProbe, {}), resolvedLook.postProcessing !== undefined && resolvedLook.postProcessing.enabled !== false ? (_jsx(PostProcessing, { config: resolvedLook.postProcessing, quality: graphics.quality })) : null] }), !poster &&
295
295
  !orientationGate &&
296
296
  coarsePointer &&
297
297
  controlsActive &&
298
298
  touchScheme !== null &&
299
299
  (touchScheme.gestures !== null || touchScheme.look) ? (_jsx(TouchPlaySurface, { scheme: touchScheme, sink: touchSink, yawRef: yawRef, pitchRef: pitchRef, maxPitch: playable.camera?.firstPerson?.maxPitch ?? 1.45, onPrimaryTap: () => {
300
300
  primaryClickRef.current = true;
301
- } })) : null, _jsx(ShellGameUiChrome, { ctx: ctx, playable: playable, GameUI: GameUI, uiScale: graphics.uiScale, orientationGate: orientationGate, onRuntimeError: reportRuntimeError }), !poster && !orientationGate && showReticle ? _jsx(Reticle, {}) : null, dockMounted && touchScheme !== null ? (_jsx(TouchControlsDock, { scheme: touchScheme, sink: touchSink, style: touchStyle, scale: touchScale })) : null, orientationGateEl, marquee !== null ? _jsx(MarqueeBox, { rect: marquee }) : null, contextMenu !== null ? (_jsx(ContextMenuView, { menu: contextMenu.menu, x: contextMenu.x, y: contextMenu.y, onPick: handleVerbPick, onClose: () => setContextMenu(null) })) : null, _jsx(ShellDebugOverlays, { ctx: ctx, playable: playable, multiplayer: multiplayer, diagnostics: diagnostics, devtoolsEnabled: devtoolsEnabled, devtoolsOpen: devtoolsOpen, hideDiagnostics: poster }), poster || orthographic || settingsHostsFov ? null : _jsx(PlayerFovSlider, {}), poster ? null : _jsx(SettingsChrome, {})] }) }) })] }) }));
301
+ } })) : null, _jsx(ShellGameUiChrome, { ctx: ctx, playable: playable, GameUI: GameUI, uiScale: graphics.uiScale, orientationGate: orientationGate, onRuntimeError: reportRuntimeError }), !poster && !orientationGate && showReticle ? _jsx(Reticle, {}) : null, dockMounted && touchScheme !== null ? (_jsx(TouchControlsDock, { scheme: touchScheme, sink: touchSink, style: touchStyle, scale: touchScale, joystickVariant: touchJoystickVariant })) : null, orientationGateEl, marquee !== null ? _jsx(MarqueeBox, { rect: marquee }) : null, contextMenu !== null ? (_jsx(ContextMenuView, { menu: contextMenu.menu, x: contextMenu.x, y: contextMenu.y, onPick: handleVerbPick, onClose: () => setContextMenu(null) })) : null, _jsx(ShellDebugOverlays, { ctx: ctx, playable: playable, multiplayer: multiplayer, diagnostics: diagnostics, devtoolsEnabled: devtoolsEnabled, devtoolsOpen: devtoolsOpen, hideDiagnostics: poster }), poster || orthographic || settingsHostsFov ? null : _jsx(PlayerFovSlider, {}), poster ? null : _jsx(SettingsChrome, {})] }) }) })] }) }));
302
302
  }
@@ -0,0 +1,31 @@
1
+ import type { ReactNode } from "react";
2
+ import type { CameraShakeController } from "@jgengine/core/vfx/cameraShake";
3
+ /** Props for {@link ControllerCameraShake}. */
4
+ export interface ControllerCameraShakeProps {
5
+ /**
6
+ * The core camera-shake controller to drive and apply. Feed it impacts with
7
+ * `controller.add(amount, kind?)` from anywhere; this component ticks its decay
8
+ * and applies the resulting offset to the active camera every frame.
9
+ */
10
+ controller: CameraShakeController;
11
+ /**
12
+ * `useFrame` priority. Must run AFTER the camera rig has posed the camera this
13
+ * frame so the shake lands on top of the freshly-set base pose (the rig
14
+ * overwrites the base each frame, so no manual restore is needed). Default
15
+ * {@link GAME_SIM_FRAME_PRIORITY} (`0`) which runs after the orbit/rig cameras
16
+ * (priority `-1`). Keep it `<= 0` so R3F's automatic render is preserved.
17
+ */
18
+ priority?: number;
19
+ }
20
+ /**
21
+ * The shell-side consumer of a core {@link CameraShakeController}: each frame it
22
+ * calls `controller.update(delta)` to bleed trauma, reads the pooled
23
+ * `controller.offset()`, and applies it additively to the active camera — a
24
+ * positional kick plus a pitch/yaw/roll rotation — so the view VISIBLY shakes on
25
+ * impacts. It runs after the camera rig (which re-poses the camera to its base
26
+ * every frame), so the shake composes with any rig without a manual save/restore
27
+ * and without fighting the built-in `shakeChannel`. Renders nothing.
28
+ *
29
+ * @capability controller-camera-shake R3F consumer that applies a core camera-shake controller's pooled per-frame offset to the active camera (translation + pitch/yaw/roll) so the view visibly shakes
30
+ */
31
+ export declare function ControllerCameraShake({ controller, priority, }: ControllerCameraShakeProps): ReactNode;
@@ -0,0 +1,34 @@
1
+ import { useFrame, useThree } from "@react-three/fiber";
2
+ import * as THREE from "three";
3
+ import { GAME_SIM_FRAME_PRIORITY } from "./orbitCameraMath.js";
4
+ // Reused scratch objects — never allocate inside the frame loop.
5
+ const shakeEuler = new THREE.Euler();
6
+ const shakeQuat = new THREE.Quaternion();
7
+ /**
8
+ * The shell-side consumer of a core {@link CameraShakeController}: each frame it
9
+ * calls `controller.update(delta)` to bleed trauma, reads the pooled
10
+ * `controller.offset()`, and applies it additively to the active camera — a
11
+ * positional kick plus a pitch/yaw/roll rotation — so the view VISIBLY shakes on
12
+ * impacts. It runs after the camera rig (which re-poses the camera to its base
13
+ * every frame), so the shake composes with any rig without a manual save/restore
14
+ * and without fighting the built-in `shakeChannel`. Renders nothing.
15
+ *
16
+ * @capability controller-camera-shake R3F consumer that applies a core camera-shake controller's pooled per-frame offset to the active camera (translation + pitch/yaw/roll) so the view visibly shakes
17
+ */
18
+ export function ControllerCameraShake({ controller, priority = GAME_SIM_FRAME_PRIORITY, }) {
19
+ const camera = useThree((state) => state.camera);
20
+ useFrame((_, delta) => {
21
+ controller.update(delta);
22
+ const offset = controller.offset();
23
+ // Additive on top of the rig's freshly-set base pose (rig runs at priority -1).
24
+ camera.position.x += offset.x;
25
+ camera.position.y += offset.y;
26
+ camera.position.z += offset.z;
27
+ if (offset.pitch !== 0 || offset.yaw !== 0 || offset.roll !== 0) {
28
+ shakeEuler.set(offset.pitch, offset.yaw, offset.roll, "XYZ");
29
+ shakeQuat.setFromEuler(shakeEuler);
30
+ camera.quaternion.multiply(shakeQuat);
31
+ }
32
+ }, priority);
33
+ return null;
34
+ }
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useEffect, useMemo, useReducer } from "react";
2
+ import { useCallback, useMemo, useRef, useSyncExternalStore } from "react";
3
3
  import { ChaseRig, CinematicRig, LockOnRig, ObserverRig, RtsRig, ShoulderRig, SideScrollRig, TopDownRig, } from "./cameraRigs.js";
4
4
  import { GameFirstPersonCamera } from "./GameFirstPersonCamera.js";
5
5
  import { GameInspectionCamera } from "./GameInspectionCamera.js";
@@ -10,12 +10,17 @@ import { CameraShakeContext, createCameraShakeChannel } from "./shakeChannel.js"
10
10
  export { resolveRigKind } from "./rigResolve.js";
11
11
  export function GameCameraRig({ yawRef, pitchRef, config, onDragChange, pointerControls, panKeysEnabled, director, viewmodel, }) {
12
12
  const channel = useMemo(() => createCameraShakeChannel(config?.shake?.decayPerSecond), [config?.shake?.decayPerSecond]);
13
- const [, notifyDirectorChange] = useReducer((count) => count + 1, 0);
14
- useEffect(() => {
13
+ const directorVersionRef = useRef(0);
14
+ const subscribeDirector = useCallback((onStoreChange) => {
15
15
  if (director === undefined)
16
- return undefined;
17
- return director.subscribe(notifyDirectorChange);
16
+ return () => undefined;
17
+ return director.subscribe(() => {
18
+ directorVersionRef.current += 1;
19
+ onStoreChange();
20
+ });
18
21
  }, [director]);
22
+ const directorVersion = useCallback(() => directorVersionRef.current, []);
23
+ useSyncExternalStore(subscribeDirector, directorVersion, directorVersion);
19
24
  const directed = resolveDirectedCamera(director === undefined
20
25
  ? undefined
21
26
  : { followEntityId: director.followedEntityId(), cinematic: director.cinematic() }, { followEntityId: config?.followEntityId, cinematic: config?.cinematic });
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { OrbitControls } from "@react-three/drei";
3
3
  import { useFrame, useThree } from "@react-three/fiber";
4
- import { useEffect, useRef } from "react";
4
+ import { useRef } from "react";
5
5
  import { MOUSE, PerspectiveCamera, Raycaster, Vector3 } from "three";
6
6
  import { isCameraOccluderTransparent } from "./cameraCollision.js";
7
7
  import { useGameContext } from "@jgengine/react/provider";
@@ -28,15 +28,6 @@ export function GameOrbitCamera({ yawRef, pitchRef, config: configPatch, followE
28
28
  distance: 0,
29
29
  });
30
30
  const followId = followEntityId ?? userId;
31
- useEffect(() => {
32
- const entity = ctx.scene.entity.get(followId);
33
- if (entity === null || runtimeRef.current !== null)
34
- return;
35
- const seeded = seedOrbitFollowState({ entityPosition: entity.position, config });
36
- runtimeRef.current = seeded;
37
- camera.position.set(seeded.camera.x, seeded.camera.y, seeded.camera.z);
38
- camera.lookAt(seeded.target.x, seeded.target.y, seeded.target.z);
39
- }, [camera, config, ctx, followId]);
40
31
  useFrame((_, delta) => {
41
32
  const controls = controlsRef.current;
42
33
  const entity = ctx.scene.entity.get(followId);
@@ -452,15 +452,23 @@ export function ChaseRig(props) {
452
452
  commit(pose, dt);
453
453
  return;
454
454
  }
455
- let anchorYaw = yaw;
455
+ // Anchor yaw always eases toward its target (#1370): with drift-lag configured the target is
456
+ // the velocity-blended yaw at velocityYawResponse, otherwise the raw body facing at
457
+ // yawResponse — so a strafe-flipped facing arcs the boom around the character instead of
458
+ // teleporting it to the far side. yawResponse: Infinity restores the legacy rigid follow.
459
+ let targetYaw = yaw;
460
+ let response = resolved.yawResponse;
456
461
  if (resolved.velocityYawBlend > 0) {
457
462
  const velocity = dt > 0
458
463
  ? { x: (follow.x - last.x) / dt, y: 0, z: (follow.z - last.z) / dt }
459
464
  : { x: 0, y: 0, z: 0 };
460
- const targetYaw = velocityYawTarget(yaw, velocity, resolved);
461
- anchorYaw = smoothYaw(anchorYawRef.current ?? targetYaw, targetYaw, resolved.velocityYawResponse, dt);
462
- anchorYawRef.current = anchorYaw;
465
+ targetYaw = velocityYawTarget(yaw, velocity, resolved);
466
+ response = resolved.velocityYawResponse;
463
467
  }
468
+ const anchorYaw = Number.isFinite(response)
469
+ ? smoothYaw(anchorYawRef.current ?? targetYaw, targetYaw, response, dt)
470
+ : targetYaw;
471
+ anchorYawRef.current = anchorYaw;
464
472
  // Report camera yaw back to the shell like every other player-facing rig, so
465
473
  // on-foot movement and aim stay camera-relative instead of frozen at yaw 0.
466
474
  props.yawRef.current = anchorYaw;
@@ -5,6 +5,7 @@ export { GameInspectionCamera, type GameInspectionCameraProps } from "./GameInsp
5
5
  export { GameCameraRig, resolveRigKind, type GameCameraRigProps } from "./GameCameraRig.js";
6
6
  export { PlayerFovProvider, PlayerFovSlider, usePlayerFov, type PlayerFovState, } from "./PlayerFov.js";
7
7
  export { CameraShakeContext, cameraShake, createCameraShakeChannel, defaultCameraShakeChannel, useCameraShake, type CameraShakeChannel, } from "./shakeChannel.js";
8
+ export { ControllerCameraShake, type ControllerCameraShakeProps } from "./ControllerCameraShake.js";
8
9
  export { rtsPanKeysConflict } from "./rigMath.js";
9
10
  export { GAME_SIM_FRAME_PRIORITY } from "./orbitCameraMath.js";
10
11
  export { CAMERA_TRANSPARENT_USERDATA, isCameraOccluderTransparent, type CameraOccluder, } from "./cameraCollision.js";
@@ -5,6 +5,7 @@ export { GameInspectionCamera } from "./GameInspectionCamera.js";
5
5
  export { GameCameraRig, resolveRigKind } from "./GameCameraRig.js";
6
6
  export { PlayerFovProvider, PlayerFovSlider, usePlayerFov, } from "./PlayerFov.js";
7
7
  export { CameraShakeContext, cameraShake, createCameraShakeChannel, defaultCameraShakeChannel, useCameraShake, } from "./shakeChannel.js";
8
+ export { ControllerCameraShake } from "./ControllerCameraShake.js";
8
9
  export { rtsPanKeysConflict } from "./rigMath.js";
9
10
  export { GAME_SIM_FRAME_PRIORITY } from "./orbitCameraMath.js";
10
11
  export { CAMERA_TRANSPARENT_USERDATA, isCameraOccluderTransparent, } from "./cameraCollision.js";
@@ -134,6 +134,8 @@ export interface ResolvedChase {
134
134
  velocityYawMinSpeed: number;
135
135
  /** Exponential smoothing rate toward the blended anchor yaw (1-exp(-response*dt)). */
136
136
  velocityYawResponse: number;
137
+ /** Exponential smoothing rate the anchor yaw follows the body's facing with (#1370). `Infinity` restores the legacy rigid follow. */
138
+ yawResponse: number;
137
139
  }
138
140
  /** @internal */
139
141
  export declare function resolveChase(config: ChaseCameraConfig | undefined): ResolvedChase;
@@ -245,6 +245,12 @@ export function lockOnPose(player, target, config, fov) {
245
245
  };
246
246
  return { pose: { position, lookAt: focus, fov }, yaw };
247
247
  }
248
+ /**
249
+ * Default chase-yaw follow response: fast enough that the camera settles behind the runner in
250
+ * well under a second, slow enough that a strafe-flipped body facing arcs the boom around the
251
+ * character instead of teleporting it to the other side.
252
+ */
253
+ const DEFAULT_CHASE_YAW_RESPONSE = 5;
248
254
  /** @internal */
249
255
  export function resolveChase(config) {
250
256
  return {
@@ -261,6 +267,7 @@ export function resolveChase(config) {
261
267
  velocityYawBlend: config?.velocityYaw === undefined ? 0 : config.velocityYaw.blend ?? 0.65,
262
268
  velocityYawMinSpeed: config?.velocityYaw?.minSpeed ?? 4,
263
269
  velocityYawResponse: config?.velocityYaw?.response ?? 6,
270
+ yawResponse: config?.yawResponse ?? DEFAULT_CHASE_YAW_RESPONSE,
264
271
  };
265
272
  }
266
273
  /** Velocity-lead the follow point (#286.9): aim `leadTime` seconds along the target's frame velocity, clamped to `leadMax`.
@@ -4,6 +4,7 @@ import { useFrame } from "@react-three/fiber";
4
4
  import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
5
5
  import * as THREE from "three";
6
6
  import { useGameContext } from "@jgengine/react/provider";
7
+ import { useDisposable } from "../render/useDisposable.js";
7
8
  import { POSTFX_OVERLAY_USERDATA } from "../postfx/postfxOverlay.js";
8
9
  import { aimProbeNeeded, anyCollisionLayerOn, colliderScanNeeded, collisionDebug, projectileListenNeeded, } from "./collisionDebug.js";
9
10
  import { AIM_DAMAGE_COLOR, AIM_LASER_COLOR, AIM_MISS_COLOR, AIM_SOLID_COLOR, BODY_WIRE_COLOR, collectDebugShapes, computeAimLaser, HITBOX_WIRE_COLOR, muzzleMarkerFromOrigin, PROJECTILE_PATH_COLOR, } from "./collisionDebugMath.js";
@@ -17,13 +18,12 @@ function WireAabb({ center, halfExtents, rotationY, color, label, }) {
17
18
  return (_jsxs("group", { position: [center[0], center[1], center[2]], rotation: [0, rotationY, 0], children: [_jsxs("mesh", { children: [_jsx("boxGeometry", { args: [halfExtents[0] * 2, halfExtents[1] * 2, halfExtents[2] * 2] }), _jsx("meshBasicMaterial", { color: color, wireframe: true, transparent: true, opacity: 0.85, depthTest: false })] }), _jsx(Html, { position: [0, halfExtents[1] + 0.15, 0], center: true, distanceFactor: 14, zIndexRange: [25, 0], style: { pointerEvents: "none" }, children: _jsx("span", { className: "rounded bg-black/70 px-1 text-[9px] font-medium text-white/90", children: label }) })] }));
18
19
  }
19
20
  function WireTriangleMesh({ entry, shape, color, }) {
20
- const geometry = useMemo(() => {
21
+ const geometry = useDisposable(() => {
21
22
  const built = new THREE.BufferGeometry();
22
23
  built.setAttribute("position", new THREE.BufferAttribute(shape.mesh.positions, 3));
23
24
  built.setIndex(new THREE.BufferAttribute(shape.mesh.indices, 1));
24
25
  return built;
25
26
  }, [shape.mesh]);
26
- useEffect(() => () => geometry.dispose(), [geometry]);
27
27
  return (_jsxs("group", { children: [_jsx("group", { position: [entry.position[0], entry.position[1], entry.position[2]], rotation: [0, entry.rotationY, 0], children: _jsx("group", { position: [shape.meshTranslate[0], shape.meshTranslate[1], shape.meshTranslate[2]], scale: shape.meshScale, children: _jsx("mesh", { geometry: geometry, children: _jsx("meshBasicMaterial", { color: color, wireframe: true, transparent: true, opacity: 0.85, depthTest: false }) }) }) }), _jsx(Html, { position: [shape.center[0], shape.center[1] + shape.halfExtents[1] + 0.15, shape.center[2]], center: true, distanceFactor: 14, zIndexRange: [25, 0], style: { pointerEvents: "none" }, children: _jsx("span", { className: "rounded bg-black/70 px-1 text-[9px] font-medium text-white/90", children: entry.label }) })] }));
28
28
  }
29
29
  function ColliderShapeMesh({ entry }) {
@@ -3,6 +3,7 @@ import { useFrame, useThree } from "@react-three/fiber";
3
3
  import { useEffect, useRef, useState, useSyncExternalStore } from "react";
4
4
  import { devtools, instrumentLatency, } from "@jgengine/core/devtools/devtools";
5
5
  import { fallbackSeamsSnapshot } from "@jgengine/core/devtools/fallbackSeams";
6
+ import { textureErrorsSnapshot } from "@jgengine/core/devtools/textureErrors";
6
7
  import { MOVEMENT_TUNING } from "@jgengine/core/movement/movementModel";
7
8
  import { collisionDebug } from "./collisionDebug.js";
8
9
  import { readStoredOverrides } from "./devtoolsOverrides.js";
@@ -210,6 +211,7 @@ export function DevtoolsOverlay({ open, ctx, playable, multiplayer, }) {
210
211
  devtools.probes.register("entities", () => ctx.scene.entity.list().length),
211
212
  devtools.probes.register("objects", () => ctx.scene.object.list().length),
212
213
  devtools.probes.register("fallbacks", () => fallbackSeamsSnapshot()),
214
+ devtools.probes.register("textureErrors", () => textureErrorsSnapshot()),
213
215
  ];
214
216
  return () => {
215
217
  for (const dispose of disposers)
@@ -5,7 +5,7 @@ import type { PointerService } from "../pointer/pointerService.js";
5
5
  import type { ShellMultiplayer } from "../multiplayer.js";
6
6
  import type { PlayableGame } from "../registry.js";
7
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, }: {
8
+ export declare function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef, pointerAxisRef, analogRef, gateRef, onRuntimeError, multiplayer, serverIdRef, pointerService, pointerAim, pingCommand, poster, onPosterSettled, }: {
9
9
  ctx: GameContext;
10
10
  playable: PlayableGame;
11
11
  tracker: ActionStateTracker<string>;
@@ -21,6 +21,9 @@ export declare function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef,
21
21
  pointerAxisRef: {
22
22
  current: PointerAxisState | null;
23
23
  };
24
+ analogRef: {
25
+ current: Readonly<Record<string, number>> | null;
26
+ };
24
27
  gateRef: {
25
28
  current: boolean;
26
29
  };
@@ -19,7 +19,7 @@ import { GAME_SIM_FRAME_PRIORITY } from "../camera/index.js";
19
19
  import { NO_ACTIONS } from "../shellConstants.js";
20
20
  const TURN_SPEED = 2.4;
21
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, }) {
22
+ export function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryClickRef, pointerAxisRef, analogRef, gateRef, onRuntimeError, multiplayer, serverIdRef, pointerService, pointerAim, pingCommand, poster, onPosterSettled, }) {
23
23
  const posterElapsedRef = useRef(0);
24
24
  const posterDoneRef = useRef(false);
25
25
  const hasReportedTickError = useRef(false);
@@ -71,7 +71,7 @@ export function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryC
71
71
  const sendInput = () => {
72
72
  if (!serverAuthoritative)
73
73
  return;
74
- const frame = { held: ctx.input.held(), pointer: ctx.input.pointer() };
74
+ const frame = { held: ctx.input.held(), pointer: ctx.input.pointer(), analog: ctx.input.analog() };
75
75
  const last = lastSentInputRef.current;
76
76
  if (last !== null && inputFramesEqual(last, frame))
77
77
  return;
@@ -80,6 +80,7 @@ export function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryC
80
80
  };
81
81
  if (gateRef.current || !playControlsActive(ctx)) {
82
82
  ctx.input.publish(heldActionsFor(tracker, NO_ACTIONS));
83
+ ctx.input.publishAnalog(null);
83
84
  sendInput();
84
85
  return;
85
86
  }
@@ -90,6 +91,7 @@ export function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryC
90
91
  const gameDt = ctx.time.advance(dt);
91
92
  ctx.input.publish(heldActionsFor(tracker, inputActions));
92
93
  ctx.input.publishPointer(pointerAxisRef.current);
94
+ ctx.input.publishAnalog(analogRef.current);
93
95
  sendInput();
94
96
  const turnInput = (tracker.isDown("turnRight") ? 1 : 0) - (tracker.isDown("turnLeft") ? 1 : 0);
95
97
  if (turnInput !== 0)
@@ -99,7 +101,7 @@ export function FrameDriver({ ctx, playable, tracker, yawRef, pitchRef, primaryC
99
101
  const player = ctx.scene.entity.get(playerId);
100
102
  if (player !== null && drivesPose && !serverAuthoritative) {
101
103
  endPhase = devtools.profile.begin("pose");
102
- stepPlayerMovement(ctx, ctx.player.userId, { held: ctx.input.held(), pointer: ctx.input.pointer() }, rawDt, movementTuning, yawRef.current);
104
+ stepPlayerMovement(ctx, ctx.player.userId, { held: ctx.input.held(), pointer: ctx.input.pointer(), analog: ctx.input.analog() }, rawDt, movementTuning, yawRef.current);
103
105
  endPhase();
104
106
  }
105
107
  if (autoPickupRadius !== null && !serverAuthoritative) {
@@ -0,0 +1,33 @@
1
+ import type { DayNightCycle } from "@jgengine/core/time/dayNightCycle";
2
+ /** Props for {@link DayNightSky}: the cycle to bind plus optional dome shape and light-strength knobs. */
3
+ export interface DayNightSkyProps {
4
+ /** The serializable day-night brain. Its `sample()` drives the sky tint/light each frame. */
5
+ cycle: DayNightCycle;
6
+ /** Sky dome radius. Default `260`. */
7
+ radius?: number;
8
+ /** Horizon haze-band strength passed straight to the dome. Default `0.5`. */
9
+ hazeStrength?: number;
10
+ /** Sun-glow brightness passed straight to the dome. Default `1`. */
11
+ sunGlowStrength?: number;
12
+ /**
13
+ * Mount a hemisphere + shadow-casting directional light whose color and intensity
14
+ * track the cycle. Turn off (`false`) when the game authors its own `lighting`. Default `true`.
15
+ */
16
+ lights?: boolean;
17
+ /** Peak directional-light intensity at full `sample().intensity` (1). Default `1.1`. */
18
+ keyLightIntensity?: number;
19
+ /** Peak hemisphere (ambient) intensity at full `sample().intensity` (1). Default `0.6`. */
20
+ ambientIntensity?: number;
21
+ }
22
+ /**
23
+ * Binds a {@link DayNightCycle} to the engine's existing `SkyDome` shader and a pair of
24
+ * lights: each frame it reads `cycle.sample()` and writes the blended tint into the dome's
25
+ * top/horizon/sun uniforms while the sun's arc comes from the shared `daylightStateAt`
26
+ * geometry. This is the turnkey presentation seam — a game mounts this one component in its
27
+ * scene and gets a moving, color-graded day-night sky from the serializable model, with no
28
+ * hand-rolled per-frame lerp and no new renderer. The cycle's `phase`/color values are
29
+ * free-form and never interpreted here.
30
+ *
31
+ * @capability day-night-sky drop-in R3F presenter that drives the engine's existing sky dome and lights from a serializable day-night cycle for a moving, color-graded day-night look
32
+ */
33
+ export declare function DayNightSky({ cycle, radius, hazeStrength, sunGlowStrength, lights, keyLightIntensity, ambientIntensity, }: DayNightSkyProps): import("react").JSX.Element;
@@ -0,0 +1,51 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useFrame } from "@react-three/fiber";
3
+ import { useMemo, useRef } from "react";
4
+ import * as THREE from "three";
5
+ import { SkyDome } from "./Daylight.js";
6
+ import { daylightStateAt } from "./daylightCycle.js";
7
+ const HEMI_GROUND = "#4c6b34";
8
+ /**
9
+ * Binds a {@link DayNightCycle} to the engine's existing `SkyDome` shader and a pair of
10
+ * lights: each frame it reads `cycle.sample()` and writes the blended tint into the dome's
11
+ * top/horizon/sun uniforms while the sun's arc comes from the shared `daylightStateAt`
12
+ * geometry. This is the turnkey presentation seam — a game mounts this one component in its
13
+ * scene and gets a moving, color-graded day-night sky from the serializable model, with no
14
+ * hand-rolled per-frame lerp and no new renderer. The cycle's `phase`/color values are
15
+ * free-form and never interpreted here.
16
+ *
17
+ * @capability day-night-sky drop-in R3F presenter that drives the engine's existing sky dome and lights from a serializable day-night cycle for a moving, color-graded day-night look
18
+ */
19
+ export function DayNightSky({ cycle, radius, hazeStrength, sunGlowStrength, lights = true, keyLightIntensity = 1.1, ambientIntensity = 0.6, }) {
20
+ const skyMaterialRef = useRef(null);
21
+ const sunRef = useRef(null);
22
+ const hemiRef = useRef(null);
23
+ const initial = useMemo(() => cycle.sample(), [cycle]);
24
+ const initialSun = useMemo(() => daylightStateAt(initial.dayFraction).sunPosition, [initial.dayFraction]);
25
+ useFrame(() => {
26
+ const sample = cycle.sample();
27
+ const geometry = daylightStateAt(sample.dayFraction);
28
+ const material = skyMaterialRef.current;
29
+ if (material !== null) {
30
+ material.uniforms.topColor.value.set(sample.lightColor);
31
+ material.uniforms.bottomColor.value.set(sample.color);
32
+ material.uniforms.uSunColor.value.set(sample.lightColor);
33
+ material.uniforms.uSunDirection.value
34
+ .set(geometry.sunPosition[0], geometry.sunPosition[1], geometry.sunPosition[2])
35
+ .normalize();
36
+ material.uniforms.uSunIntensity.value = sample.intensity;
37
+ }
38
+ const sun = sunRef.current;
39
+ if (sun !== null) {
40
+ sun.position.set(geometry.sunPosition[0], geometry.sunPosition[1], geometry.sunPosition[2]);
41
+ sun.intensity = sample.intensity * keyLightIntensity;
42
+ sun.color.set(sample.lightColor);
43
+ }
44
+ const hemi = hemiRef.current;
45
+ if (hemi !== null) {
46
+ hemi.intensity = sample.intensity * ambientIntensity;
47
+ hemi.color.set(sample.lightColor);
48
+ }
49
+ });
50
+ return (_jsxs(_Fragment, { children: [_jsx(SkyDome, { topColor: initial.lightColor, horizonColor: initial.color, sunDirection: initialSun, sunColor: initial.lightColor, sunIntensity: initial.intensity, materialRef: skyMaterialRef, ...(radius === undefined ? {} : { radius }), ...(hazeStrength === undefined ? {} : { hazeStrength }), ...(sunGlowStrength === undefined ? {} : { sunGlowStrength }) }), lights ? (_jsxs(_Fragment, { children: [_jsx("hemisphereLight", { ref: hemiRef, args: [initial.lightColor, HEMI_GROUND, initial.intensity * ambientIntensity] }), _jsx("directionalLight", { ref: sunRef, position: initialSun, intensity: initial.intensity * keyLightIntensity, color: initial.lightColor, 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 })] })) : null] }));
51
+ }
@@ -16,13 +16,15 @@ export interface SkyDomeProps {
16
16
  sunIntensity?: number;
17
17
  /** Horizon haze-band strength: 0 removes the dusty band, ~1 makes it heavy. Default 0.5. */
18
18
  hazeStrength?: number;
19
+ /** Procedural cloud-band opacity: 0 is a clear sky, 1 is heavy overcast. Default 0.35. */
20
+ cloudiness?: number;
19
21
  /** Sun-glow brightness multiplier around the sun disc. Default 1. */
20
22
  sunGlowStrength?: number;
21
23
  /** Exposes the created shader material so a time-of-day driver can mutate its uniforms per frame without recreating it. */
22
24
  materialRef?: MutableRefObject<THREE.ShaderMaterial | null>;
23
25
  }
24
26
  /** @internal */
25
- export declare function SkyDome({ topColor, horizonColor, radius, offset, exponent, sunDirection, sunColor, sunIntensity, hazeStrength, sunGlowStrength, materialRef, }?: SkyDomeProps): import("react").JSX.Element;
27
+ export declare function SkyDome({ topColor, horizonColor, radius, offset, exponent, sunDirection, sunColor, sunIntensity, hazeStrength, sunGlowStrength, cloudiness, materialRef, }?: SkyDomeProps): import("react").JSX.Element;
26
28
  export interface DaylightProps {
27
29
  sky?: SkyDomeProps | false;
28
30
  fog?: {