@jgengine/shell 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -3
- package/LICENSE +203 -661
- package/README.md +1 -1
- package/dist/GamePhaseStamp.d.ts +1 -0
- package/dist/GamePhaseStamp.js +9 -0
- package/dist/GamePlayerShell.d.ts +4 -31
- package/dist/GamePlayerShell.js +162 -932
- package/dist/GameUiPreview.js +0 -2
- package/dist/audio/audioEngine.d.ts +8 -0
- package/dist/audio/audioEngine.js +63 -8
- package/dist/audio/audioWire.d.ts +22 -0
- package/dist/audio/audioWire.js +19 -0
- package/dist/audio/musicDirector.d.ts +31 -0
- package/dist/audio/musicDirector.js +125 -0
- package/dist/audio/musicVoices.d.ts +10 -0
- package/dist/audio/musicVoices.js +338 -0
- package/dist/audio/synthEngine.d.ts +9 -0
- package/dist/audio/synthEngine.js +57 -0
- package/dist/behaviour.d.ts +2 -1
- package/dist/behaviour.js +2 -1
- package/dist/behaviourAttach.d.ts +1 -0
- package/dist/behaviourAttach.js +1 -0
- package/dist/behaviourDriver.d.ts +1 -0
- package/dist/behaviourDriver.js +1 -0
- package/dist/boundActionDispatch.d.ts +13 -0
- package/dist/boundActionDispatch.js +37 -0
- package/dist/camera/GameCameraRig.d.ts +5 -2
- package/dist/camera/GameCameraRig.js +2 -2
- package/dist/camera/GameFirstPersonCamera.d.ts +12 -2
- package/dist/camera/GameFirstPersonCamera.js +29 -7
- package/dist/camera/GameInspectionCamera.d.ts +1 -1
- package/dist/camera/GameInspectionCamera.js +13 -2
- package/dist/camera/GameOrbitCamera.js +59 -10
- package/dist/camera/cameraBlendMath.d.ts +3 -0
- package/dist/camera/cameraBlendMath.js +3 -0
- package/dist/camera/fovPreference.d.ts +6 -1
- package/dist/camera/fovPreference.js +6 -1
- package/dist/camera/index.d.ts +4 -7
- package/dist/camera/index.js +4 -7
- package/dist/camera/inspectionCameraMath.d.ts +7 -2
- package/dist/camera/inspectionCameraMath.js +7 -2
- package/dist/camera/orbitCameraMath.d.ts +54 -6
- package/dist/camera/orbitCameraMath.js +61 -6
- package/dist/camera/rigMath.d.ts +94 -24
- package/dist/camera/rigMath.js +100 -24
- package/dist/camera/rigResolve.d.ts +4 -2
- package/dist/camera/rigResolve.js +4 -2
- package/dist/camera/shakeChannel.d.ts +6 -2
- package/dist/camera/shakeChannel.js +6 -2
- package/dist/camera/shakeChannelMath.d.ts +1 -0
- package/dist/camera/shakeChannelMath.js +1 -0
- package/dist/cartridge/runtime.d.ts +15 -0
- package/dist/cartridge/runtime.js +556 -0
- package/dist/cartridge/spec.d.ts +188 -0
- package/dist/cartridge/spec.js +19 -0
- package/dist/cartridge/validate.d.ts +2 -0
- package/dist/cartridge/validate.js +78 -0
- package/dist/cartridge.d.ts +1 -1
- package/dist/cartridge.js +4 -3
- package/dist/commandSink.d.ts +25 -0
- package/dist/commandSink.js +32 -0
- package/dist/defineGame.js +26 -5
- package/dist/devtools/ColPanel.d.ts +1 -0
- package/dist/devtools/ColPanel.js +14 -0
- package/dist/devtools/CollisionDebugWorld.js +1 -0
- package/dist/devtools/DevtoolsOverlay.d.ts +7 -3
- package/dist/devtools/DevtoolsOverlay.js +19 -450
- package/dist/devtools/KeysPanel.d.ts +4 -0
- package/dist/devtools/KeysPanel.js +31 -0
- package/dist/devtools/LogsPanel.d.ts +1 -0
- package/dist/devtools/LogsPanel.js +12 -0
- package/dist/devtools/NetPanel.d.ts +4 -0
- package/dist/devtools/NetPanel.js +10 -0
- package/dist/devtools/PerfPanel.d.ts +4 -0
- package/dist/devtools/PerfPanel.js +53 -0
- package/dist/devtools/TunePanel.d.ts +3 -0
- package/dist/devtools/TunePanel.js +309 -0
- package/dist/devtools/agentBridge.d.ts +41 -0
- package/dist/devtools/agentBridge.js +262 -0
- package/dist/devtools/collisionDebug.d.ts +6 -0
- package/dist/devtools/collisionDebug.js +6 -0
- package/dist/devtools/collisionDebugMath.d.ts +9 -2
- package/dist/devtools/collisionDebugMath.js +25 -5
- package/dist/devtools/devtoolsOverrides.d.ts +6 -0
- package/dist/devtools/devtoolsOverrides.js +48 -0
- package/dist/devtools/panelAtoms.d.ts +9 -0
- package/dist/devtools/panelAtoms.js +10 -0
- package/dist/devtools/perfDiagnose.d.ts +2 -0
- package/dist/devtools/perfDiagnose.js +43 -0
- package/dist/diagnostics/RuntimeDiagnostics.d.ts +29 -0
- package/dist/diagnostics/RuntimeDiagnostics.js +69 -0
- package/dist/drivers/FrameDriver.d.ts +37 -0
- package/dist/drivers/FrameDriver.js +232 -0
- package/dist/drivers/HudOnlyDriver.d.ts +16 -0
- package/dist/drivers/HudOnlyDriver.js +60 -0
- package/dist/environment/Daylight.d.ts +29 -7
- package/dist/environment/Daylight.js +153 -13
- package/dist/environment/EnvironmentScene.js +42 -7
- package/dist/environment/RoadRibbons.d.ts +7 -0
- package/dist/environment/RoadRibbons.js +57 -0
- package/dist/environment/VolumetricClouds.d.ts +17 -0
- package/dist/environment/VolumetricClouds.js +202 -0
- package/dist/environment/daylightCycle.d.ts +3 -1
- package/dist/environment/daylightCycle.js +3 -1
- package/dist/environment/groundPadMath.d.ts +3 -0
- package/dist/environment/groundPadMath.js +3 -0
- package/dist/environment/index.d.ts +1 -0
- package/dist/environment/index.js +1 -0
- package/dist/environment/skyLightingPolicy.d.ts +2 -0
- package/dist/environment/skyLightingPolicy.js +2 -0
- package/dist/hotbarActions.d.ts +26 -0
- package/dist/hotbarActions.js +27 -0
- package/dist/inputSink.d.ts +30 -0
- package/dist/inputSink.js +85 -0
- package/dist/materialOverride.d.ts +10 -0
- package/dist/materialOverride.js +15 -3
- package/dist/multiplayer.js +14 -4
- package/dist/postfx/PostProcessing.d.ts +10 -0
- package/dist/postfx/PostProcessing.js +120 -0
- package/dist/postfx/gradeShader.d.ts +4 -0
- package/dist/postfx/gradeShader.js +72 -0
- package/dist/registry.d.ts +3 -2
- package/dist/render/EnvironmentLighting.d.ts +10 -0
- package/dist/render/EnvironmentLighting.js +34 -0
- package/dist/render/SceneLighting.d.ts +7 -0
- package/dist/render/SceneLighting.js +18 -0
- package/dist/render/SceneModels.d.ts +14 -0
- package/dist/render/SceneModels.js +337 -0
- package/dist/render/defaultSurface.d.ts +29 -0
- package/dist/render/defaultSurface.js +101 -0
- package/dist/render/modelLoad.d.ts +24 -0
- package/dist/render/modelLoad.js +73 -0
- package/dist/render/modelRender.d.ts +11 -0
- package/dist/render/modelRender.js +15 -0
- package/dist/render/resolveModel.d.ts +39 -3
- package/dist/render/resolveModel.js +79 -2
- package/dist/render/useEntityRenderCues.d.ts +13 -0
- package/dist/render/useEntityRenderCues.js +62 -0
- package/dist/scatter/InstancedScatter.d.ts +24 -0
- package/dist/scatter/InstancedScatter.js +130 -0
- package/dist/scatter/scatterModels.d.ts +25 -0
- package/dist/scatter/scatterModels.js +61 -0
- package/dist/scatter/scatterProxies.d.ts +6 -0
- package/dist/scatter/scatterProxies.js +94 -0
- package/dist/scatter.d.ts +1 -0
- package/dist/scatter.js +1 -0
- package/dist/scene/AuthoredScene.d.ts +82 -0
- package/dist/scene/AuthoredScene.js +173 -0
- package/dist/scene/GeneratedAssetRenderer.d.ts +14 -0
- package/dist/scene/GeneratedAssetRenderer.js +59 -0
- package/dist/scene/GrassFieldRenderer.d.ts +2 -0
- package/dist/scene/GrassFieldRenderer.js +25 -0
- package/dist/scene/SoilPatchRenderer.d.ts +2 -0
- package/dist/scene/SoilPatchRenderer.js +21 -0
- package/dist/scene/StudioStage.d.ts +37 -0
- package/dist/scene/StudioStage.js +42 -0
- package/dist/scene/WaterRenderer.d.ts +2 -0
- package/dist/scene/WaterRenderer.js +37 -0
- package/dist/scene/builtinSceneKindRenderers.d.ts +2 -0
- package/dist/scene/builtinSceneKindRenderers.js +22 -0
- package/dist/scene/sceneKindRenderers.d.ts +28 -0
- package/dist/scene/sceneKindRenderers.js +17 -0
- package/dist/scene.d.ts +1 -0
- package/dist/scene.js +1 -0
- package/dist/settings/appliedSettings.d.ts +5 -0
- package/dist/settings/appliedSettings.js +9 -0
- package/dist/settings/settingsController.d.ts +2 -0
- package/dist/settings/settingsController.js +22 -1
- package/dist/shellConstants.d.ts +4 -0
- package/dist/shellConstants.js +4 -0
- package/dist/shellMovement.d.ts +8 -0
- package/dist/shellMovement.js +11 -0
- package/dist/shellPointer.d.ts +20 -0
- package/dist/shellPointer.js +28 -0
- package/dist/structures/PlacementGhost.d.ts +4 -0
- package/dist/structures/PlacementGhost.js +4 -0
- package/dist/structures/TransformGizmo.d.ts +43 -0
- package/dist/structures/TransformGizmo.js +55 -0
- package/dist/structures/index.d.ts +1 -0
- package/dist/structures/index.js +1 -0
- package/dist/terrain/CarvedTerrain.d.ts +4 -1
- package/dist/terrain/CarvedTerrain.js +4 -3
- package/dist/terrain/GrassField.js +1 -1
- package/dist/terrain/TerraformBrushCursor.js +3 -0
- package/dist/terrain/grassBudget.d.ts +11 -2
- package/dist/terrain/grassBudget.js +20 -4
- package/dist/terrain/grassGeometry.d.ts +3 -0
- package/dist/terrain/grassGeometry.js +3 -0
- package/dist/terrain/grassMaterial.d.ts +2 -0
- package/dist/terrain/grassMaterial.js +2 -0
- package/dist/terrain/random.d.ts +3 -0
- package/dist/terrain/random.js +3 -0
- package/dist/terrain/soilPatchMaterial.d.ts +10 -0
- package/dist/terrain/soilPatchMaterial.js +109 -0
- package/dist/terrain/terrainDetailMaterial.d.ts +31 -0
- package/dist/terrain/terrainDetailMaterial.js +157 -0
- package/dist/terrain/terrainMath.d.ts +8 -1
- package/dist/terrain/terrainMath.js +8 -1
- package/dist/touch/TouchControlsOverlay.d.ts +9 -5
- package/dist/touch/TouchControlsOverlay.js +211 -22
- package/dist/useShellMultiplayerSync.d.ts +9 -0
- package/dist/useShellMultiplayerSync.js +98 -0
- package/dist/visibility/CullingProvider.js +1 -1
- package/dist/water/OceanConfig.d.ts +2 -0
- package/dist/water/OceanConfig.js +2 -0
- package/dist/weather/fireSpreadPose.d.ts +1 -0
- package/dist/weather/fireSpreadPose.js +1 -0
- package/dist/weather/weatherGeometry.d.ts +1 -0
- package/dist/weather/weatherGeometry.js +1 -0
- package/dist/weather/weatherMath.d.ts +3 -0
- package/dist/weather/weatherMath.js +3 -0
- package/dist/weather/weatherUniforms.d.ts +3 -0
- package/dist/weather/weatherUniforms.js +3 -0
- package/dist/world/RetainedVfx.d.ts +10 -0
- package/dist/world/RetainedVfx.js +136 -0
- package/dist/world/WorldHud.d.ts +52 -3
- package/dist/world/WorldHud.js +94 -9
- package/dist/world/WorldScene.d.ts +21 -0
- package/dist/world/WorldScene.js +159 -0
- package/dist/world/WorldVfx.d.ts +9 -0
- package/dist/world/WorldVfx.js +184 -0
- package/dist/world/entityPose.d.ts +2 -0
- package/dist/world/entityPose.js +2 -0
- package/dist/world/floatTextStyle.d.ts +1 -0
- package/dist/world/floatTextStyle.js +1 -0
- package/dist/world/telegraphPulse.d.ts +1 -0
- package/dist/world/telegraphPulse.js +1 -0
- package/dist/world/worldBarSamples.d.ts +26 -1
- package/dist/world/worldBarSamples.js +45 -1
- package/dist/worldSky.d.ts +5 -0
- package/dist/worldSky.js +11 -0
- package/dist/worldSync.d.ts +11 -0
- package/dist/worldSync.js +20 -0
- package/package.json +6 -6
- package/skills/jgengine/SKILL.md +51 -0
- package/skills/jgengine/api.md +434 -0
- package/skills/jgengine/capabilities.md +29 -0
- package/skills/jgengine/reference-cartridge.md +72 -0
- package/skills/jgengine-ui/SKILL.md +36 -0
- package/skills/jgengine-ui/api.md +1852 -0
- package/skills/jgengine-ui/capabilities.md +142 -0
- package/skills/jgengine-ui/reference.md +500 -0
- package/skills/jgengine-ui/references/visual-scorecard.md +73 -0
- package/skills/jgengine-world/SKILL.md +38 -0
- package/skills/jgengine-world/api.md +2652 -0
- package/skills/jgengine-world/capabilities.md +370 -0
- package/skills/jgengine-world/reference.md +222 -0
- package/llms.txt +0 -2643
|
@@ -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,16 +1,28 @@
|
|
|
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;
|
|
7
8
|
radius?: number;
|
|
8
9
|
offset?: number;
|
|
9
10
|
exponent?: number;
|
|
11
|
+
/** Direction toward the sun; a bright HDR sun disc + warm glow is drawn there (blooms through the post chain). Omit to skip the sun. */
|
|
12
|
+
sunDirection?: readonly [number, number, number];
|
|
13
|
+
/** Sun disc/glow colour. Default warm white. */
|
|
14
|
+
sunColor?: string;
|
|
15
|
+
/** Sun disc/glow intensity multiplier. Default 1. */
|
|
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;
|
|
10
21
|
/** Exposes the created shader material so a time-of-day driver can mutate its uniforms per frame without recreating it. */
|
|
11
22
|
materialRef?: MutableRefObject<THREE.ShaderMaterial | null>;
|
|
12
23
|
}
|
|
13
|
-
|
|
24
|
+
/** @internal */
|
|
25
|
+
export declare function SkyDome({ topColor, horizonColor, radius, offset, exponent, sunDirection, sunColor, sunIntensity, hazeStrength, sunGlowStrength, materialRef, }?: SkyDomeProps): import("react").JSX.Element;
|
|
14
26
|
export interface DaylightProps {
|
|
15
27
|
sky?: SkyDomeProps | false;
|
|
16
28
|
fog?: {
|
|
@@ -30,14 +42,21 @@ export interface DaylightProps {
|
|
|
30
42
|
};
|
|
31
43
|
/** When false, only the sky dome and fog mount — use with authored `PlayableGame.lighting`. Default true. */
|
|
32
44
|
lights?: boolean;
|
|
45
|
+
/** Raymarched volumetric cloud layer over the sky dome. Omit for no clouds — off by default. */
|
|
46
|
+
clouds?: VolumetricCloudsConfig;
|
|
33
47
|
}
|
|
34
|
-
|
|
48
|
+
/** @internal */
|
|
49
|
+
export declare function Daylight({ sky, fog, sun, ambient, lights, clouds }?: DaylightProps): import("react").JSX.Element;
|
|
35
50
|
export interface SkyDaylightProps {
|
|
36
51
|
sky: SkyEnvironmentDescriptor;
|
|
37
52
|
lights?: boolean;
|
|
53
|
+
/** Terrain `biomeBands`; when any carries `fog`/`sky`, fog and dome cross-fade per camera z. */
|
|
54
|
+
bands?: readonly BiomeBand[];
|
|
38
55
|
}
|
|
39
|
-
/** 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.
|
|
40
|
-
|
|
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;
|
|
41
60
|
export interface TimeOfDayDaylightProps {
|
|
42
61
|
sky: SkyEnvironmentDescriptor;
|
|
43
62
|
/** The world's `SimClock` (or a stub exposing `calendar().dayFraction`). Absent means static rendering. */
|
|
@@ -47,10 +66,13 @@ export interface TimeOfDayDaylightProps {
|
|
|
47
66
|
};
|
|
48
67
|
};
|
|
49
68
|
lights?: boolean;
|
|
69
|
+
/** Terrain `biomeBands`; when any carries `fog`/`sky`, fog and dome cross-fade per camera z. */
|
|
70
|
+
bands?: readonly BiomeBand[];
|
|
50
71
|
}
|
|
51
72
|
/**
|
|
52
73
|
* Drives sky/fog (and optional default lights) from the world clock when `sky.timeOfDay` and `clock`
|
|
53
74
|
* are both present. Authored `PlayableGame.lighting` is never rewritten — pass `lights={false}` so
|
|
54
75
|
* only dome colors and fog track the day fraction.
|
|
55
|
-
|
|
56
|
-
|
|
76
|
+
* @internal
|
|
77
|
+
*/
|
|
78
|
+
export declare function TimeOfDayDaylight({ sky, clock, lights, bands }: TimeOfDayDaylightProps): import("react").JSX.Element;
|
|
@@ -2,14 +2,42 @@ 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";
|
|
9
|
+
function normalizeVec3(v) {
|
|
10
|
+
const out = new THREE.Vector3(v[0], v[1], v[2]);
|
|
11
|
+
return out.lengthSq() === 0 ? new THREE.Vector3(0, 1, 0) : out.normalize();
|
|
12
|
+
}
|
|
6
13
|
const SKY_TOP = "#3fa4f2";
|
|
7
14
|
const SKY_HORIZON = "#e3f4ff";
|
|
8
15
|
const FOG_COLOR = "#e9f6ff";
|
|
9
16
|
const SUN_COLOR = "#fff1c9";
|
|
10
17
|
const HEMI_SKY = "#bfe3ff";
|
|
11
18
|
const HEMI_GROUND = "#4c6b34";
|
|
12
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Sun directional light whose high-resolution shadow camera follows the view each
|
|
21
|
+
* frame, so grounded shadows stay crisp under the player anywhere in a large world
|
|
22
|
+
* instead of only near the origin.
|
|
23
|
+
*/
|
|
24
|
+
function ShadowCastingSun({ position, intensity, color, }) {
|
|
25
|
+
const ref = useRef(null);
|
|
26
|
+
useFrame((state) => {
|
|
27
|
+
const light = ref.current;
|
|
28
|
+
if (light === null)
|
|
29
|
+
return;
|
|
30
|
+
const cx = state.camera.position.x;
|
|
31
|
+
const cz = state.camera.position.z;
|
|
32
|
+
light.position.set(cx + position[0], position[1], cz + position[2]);
|
|
33
|
+
light.target.position.set(cx, 0, cz);
|
|
34
|
+
light.target.updateMatrixWorld();
|
|
35
|
+
});
|
|
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 }));
|
|
37
|
+
}
|
|
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);
|
|
13
41
|
const material = useMemo(() => {
|
|
14
42
|
return new THREE.ShaderMaterial({
|
|
15
43
|
uniforms: {
|
|
@@ -17,6 +45,11 @@ export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius
|
|
|
17
45
|
bottomColor: { value: new THREE.Color(horizonColor) },
|
|
18
46
|
offset: { value: offset },
|
|
19
47
|
exponent: { value: exponent },
|
|
48
|
+
uSunColor: { value: new THREE.Color(sunColor) },
|
|
49
|
+
uSunDirection: { value: sunDirection === undefined ? new THREE.Vector3(0, 1, 0) : normalizeVec3(sunDirection) },
|
|
50
|
+
uSunIntensity: { value: sunDirection === undefined ? 0 : sunIntensity },
|
|
51
|
+
uHazeStrength: { value: hazeStrength },
|
|
52
|
+
uSunGlow: { value: sunGlowStrength },
|
|
20
53
|
},
|
|
21
54
|
vertexShader: `
|
|
22
55
|
varying vec3 vWorldPosition;
|
|
@@ -29,19 +62,42 @@ export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius
|
|
|
29
62
|
fragmentShader: `
|
|
30
63
|
uniform vec3 topColor;
|
|
31
64
|
uniform vec3 bottomColor;
|
|
65
|
+
uniform vec3 uSunColor;
|
|
66
|
+
uniform vec3 uSunDirection;
|
|
67
|
+
uniform float uSunIntensity;
|
|
68
|
+
uniform float uHazeStrength;
|
|
69
|
+
uniform float uSunGlow;
|
|
32
70
|
uniform float offset;
|
|
33
71
|
uniform float exponent;
|
|
34
72
|
varying vec3 vWorldPosition;
|
|
73
|
+
float sHash(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
|
|
74
|
+
float sNoise(vec2 p){
|
|
75
|
+
vec2 i = floor(p); vec2 f = fract(p); vec2 u = f * f * (3.0 - 2.0 * f);
|
|
76
|
+
return mix(mix(sHash(i), sHash(i + vec2(1.0, 0.0)), u.x),
|
|
77
|
+
mix(sHash(i + vec2(0.0, 1.0)), sHash(i + vec2(1.0, 1.0)), u.x), u.y);
|
|
78
|
+
}
|
|
79
|
+
float sFbm(vec2 p){ float v = 0.0; float a = 0.5; for (int i = 0; i < 5; i++){ v += a * sNoise(p); p *= 2.0; a *= 0.5; } return v; }
|
|
35
80
|
void main() {
|
|
36
|
-
|
|
37
|
-
|
|
81
|
+
vec3 dir = normalize(vWorldPosition + vec3(0.0, offset, 0.0));
|
|
82
|
+
float h = max(dir.y, 0.0);
|
|
83
|
+
vec3 col = mix(bottomColor, topColor, pow(h, exponent));
|
|
84
|
+
col = mix(col, bottomColor * 1.12, pow(1.0 - h, 6.0) * uHazeStrength);
|
|
85
|
+
vec3 vd = normalize(vWorldPosition);
|
|
86
|
+
vec2 cuv = vd.xz / (max(vd.y, 0.06) + 0.15) * 1.2;
|
|
87
|
+
float clouds = smoothstep(0.52, 0.82, sFbm(cuv));
|
|
88
|
+
float band = smoothstep(0.08, 0.35, dir.y) * smoothstep(1.0, 0.35, dir.y);
|
|
89
|
+
col = mix(col, mix(topColor, vec3(1.0), 0.6), clouds * band * 0.35);
|
|
90
|
+
float sd = max(dot(vd, normalize(uSunDirection)), 0.0);
|
|
91
|
+
float glow = pow(sd, 8.0) * 0.35 + pow(sd, 128.0) * 2.6;
|
|
92
|
+
col += uSunColor * glow * uSunIntensity * uSunGlow;
|
|
93
|
+
gl_FragColor = vec4(col, 1.0);
|
|
38
94
|
}
|
|
39
95
|
`,
|
|
40
96
|
side: THREE.BackSide,
|
|
41
97
|
depthWrite: false,
|
|
42
98
|
fog: false,
|
|
43
99
|
});
|
|
44
|
-
}, [topColor, horizonColor, offset, exponent]);
|
|
100
|
+
}, [topColor, horizonColor, offset, exponent, sunColor, sunDirection, sunIntensity, hazeStrength, sunGlowStrength]);
|
|
45
101
|
useEffect(() => {
|
|
46
102
|
if (materialRef !== undefined)
|
|
47
103
|
materialRef.current = material;
|
|
@@ -51,27 +107,111 @@ export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius
|
|
|
51
107
|
materialRef.current = null;
|
|
52
108
|
};
|
|
53
109
|
}, [material, materialRef]);
|
|
54
|
-
|
|
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] }) }));
|
|
55
118
|
}
|
|
56
|
-
|
|
119
|
+
/** @internal */
|
|
120
|
+
export function Daylight({ sky, fog, sun, ambient, lights = true, clouds } = {}) {
|
|
57
121
|
const sunPosition = sun?.position ?? [120, 160, 70];
|
|
58
|
-
return (_jsxs(_Fragment, { children: [sky === false ? null : _jsx(SkyDome, { ...(sky ?? {}) }), 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(
|
|
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);
|
|
59
127
|
}
|
|
60
|
-
/** 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.
|
|
61
|
-
|
|
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
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function StaticSkyDaylight({ sky, lights }) {
|
|
62
145
|
const state = useMemo(() => daylightStateAt(SKY_PRESET_DAY_FRACTION[sky.preset], sky), [sky]);
|
|
63
|
-
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 }));
|
|
64
147
|
}
|
|
65
148
|
/**
|
|
66
149
|
* Drives sky/fog (and optional default lights) from the world clock when `sky.timeOfDay` and `clock`
|
|
67
150
|
* are both present. Authored `PlayableGame.lighting` is never rewritten — pass `lights={false}` so
|
|
68
151
|
* only dome colors and fog track the day fraction.
|
|
69
|
-
|
|
70
|
-
|
|
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 });
|
|
71
157
|
if (!sky.timeOfDay || clock === undefined)
|
|
72
158
|
return _jsx(SkyDaylight, { sky: sky, lights: lights });
|
|
73
159
|
return _jsx(DrivenDaylight, { sky: sky, clock: clock, lights: lights });
|
|
74
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
|
+
}
|
|
75
215
|
function DrivenDaylight({ sky, clock, lights, }) {
|
|
76
216
|
const initial = useMemo(() => daylightStateAt(clock.calendar().dayFraction, sky), [clock, sky]);
|
|
77
217
|
const sunRef = useRef(null);
|
|
@@ -97,5 +237,5 @@ function DrivenDaylight({ sky, clock, lights, }) {
|
|
|
97
237
|
skyMaterial.uniforms.bottomColor.value.set(state.skyBottom);
|
|
98
238
|
}
|
|
99
239
|
});
|
|
100
|
-
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] }));
|
|
101
241
|
}
|