@jgengine/shell 0.7.0 → 0.9.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 +63 -0
- package/README.md +37 -1
- package/dist/GameHost.d.ts +12 -0
- package/dist/GameHost.js +33 -0
- package/dist/GamePlayer.d.ts +11 -0
- package/dist/GamePlayer.js +24 -0
- package/dist/GamePlayerShell.d.ts +36 -1
- package/dist/GamePlayerShell.js +751 -80
- package/dist/GameUiPreview.js +2 -1
- package/dist/audio/AudioComponents.js +25 -8
- package/dist/audio/audioEngine.d.ts +1 -0
- package/dist/audio/audioEngine.js +4 -0
- package/dist/behaviour.d.ts +12 -0
- package/dist/behaviour.js +27 -0
- package/dist/behaviourAttach.d.ts +9 -0
- package/dist/behaviourAttach.js +32 -0
- package/dist/behaviourDriver.d.ts +7 -0
- package/dist/behaviourDriver.js +21 -0
- package/dist/camera/GameCameraRig.d.ts +8 -3
- package/dist/camera/GameCameraRig.js +28 -14
- package/dist/camera/GameFirstPersonCamera.js +16 -0
- package/dist/camera/GameInspectionCamera.d.ts +6 -0
- package/dist/camera/GameInspectionCamera.js +24 -0
- package/dist/camera/GameOrbitCamera.js +10 -1
- package/dist/camera/PlayerFov.d.ts +18 -0
- package/dist/camera/PlayerFov.js +48 -0
- package/dist/camera/cameraBlendMath.d.ts +13 -0
- package/dist/camera/cameraBlendMath.js +36 -0
- package/dist/camera/cameraRigs.d.ts +8 -1
- package/dist/camera/cameraRigs.js +78 -38
- package/dist/camera/fovPreference.d.ts +25 -0
- package/dist/camera/fovPreference.js +73 -0
- package/dist/camera/index.d.ts +7 -2
- package/dist/camera/index.js +7 -2
- package/dist/camera/inspectionCameraMath.d.ts +25 -0
- package/dist/camera/inspectionCameraMath.js +44 -0
- package/dist/camera/rigMath.d.ts +50 -1
- package/dist/camera/rigMath.js +91 -1
- package/dist/camera/rigResolve.d.ts +15 -0
- package/dist/camera/rigResolve.js +52 -0
- package/dist/camera/shakeChannel.d.ts +3 -17
- package/dist/camera/shakeChannel.js +2 -21
- package/dist/camera/shakeChannelMath.d.ts +8 -0
- package/dist/camera/shakeChannelMath.js +21 -0
- package/dist/cartridge.d.ts +145 -0
- package/dist/cartridge.js +245 -0
- package/dist/defineGame.d.ts +16 -0
- package/dist/defineGame.js +52 -0
- package/dist/devtools/CollisionDebugWorld.d.ts +5 -0
- package/dist/devtools/CollisionDebugWorld.js +180 -0
- package/dist/devtools/DevtoolsOverlay.d.ts +78 -0
- package/dist/devtools/DevtoolsOverlay.js +676 -0
- package/dist/devtools/collisionDebug.d.ts +57 -0
- package/dist/devtools/collisionDebug.js +127 -0
- package/dist/devtools/collisionDebugMath.d.ts +102 -0
- package/dist/devtools/collisionDebugMath.js +128 -0
- package/dist/environment/Daylight.d.ts +56 -0
- package/dist/environment/Daylight.js +101 -0
- package/dist/environment/EnvironmentScene.js +87 -48
- package/dist/environment/GroundPad.d.ts +7 -0
- package/dist/environment/GroundPad.js +12 -0
- package/dist/environment/daylightCycle.d.ts +26 -0
- package/dist/environment/daylightCycle.js +116 -0
- package/dist/environment/groundPadMath.d.ts +13 -0
- package/dist/environment/groundPadMath.js +10 -0
- package/dist/environment/index.d.ts +3 -0
- package/dist/environment/index.js +3 -0
- package/dist/environment/skyLightingPolicy.d.ts +10 -0
- package/dist/environment/skyLightingPolicy.js +6 -0
- package/dist/input/mouseLook.d.ts +27 -0
- package/dist/input/mouseLook.js +35 -0
- package/dist/materialOverride.d.ts +6 -0
- package/dist/materialOverride.js +28 -0
- package/dist/multiplayer.d.ts +16 -9
- package/dist/multiplayer.js +62 -12
- package/dist/pointer/PointerProbe.js +20 -3
- package/dist/pointer/pointerService.d.ts +5 -0
- package/dist/pointer/pointerService.js +51 -24
- package/dist/registry.d.ts +4 -1
- package/dist/registry.js +3 -1
- package/dist/render/modelRender.d.ts +23 -0
- package/dist/render/modelRender.js +106 -0
- package/dist/render/resolveModel.d.ts +14 -0
- package/dist/render/resolveModel.js +24 -0
- package/dist/settings/QuickControls.d.ts +4 -0
- package/dist/settings/QuickControls.js +42 -0
- package/dist/settings/SettingsChrome.d.ts +1 -0
- package/dist/settings/SettingsChrome.js +10 -0
- package/dist/settings/SettingsMenu.d.ts +6 -0
- package/dist/settings/SettingsMenu.js +148 -0
- package/dist/settings/SettingsRuntime.d.ts +11 -0
- package/dist/settings/SettingsRuntime.js +19 -0
- package/dist/settings/appliedSettings.d.ts +14 -0
- package/dist/settings/appliedSettings.js +27 -0
- package/dist/settings/settingsController.d.ts +20 -0
- package/dist/settings/settingsController.js +165 -0
- package/dist/structures/GeneratedBuilding.d.ts +10 -0
- package/dist/structures/GeneratedBuilding.js +96 -8
- package/dist/structures/index.d.ts +1 -1
- package/dist/structures/index.js +1 -1
- package/dist/terrain/CarvedTerrain.d.ts +2 -1
- package/dist/terrain/CarvedTerrain.js +3 -3
- package/dist/terrain/GrassField.d.ts +3 -1
- package/dist/terrain/GrassField.js +4 -2
- package/dist/terrain/grassBudget.d.ts +3 -0
- package/dist/terrain/grassBudget.js +6 -0
- package/dist/terrain/grassGeometry.js +1 -1
- package/dist/terrain/index.d.ts +1 -1
- package/dist/terrain/index.js +1 -1
- package/dist/terrain/terrainMath.d.ts +9 -0
- package/dist/terrain/terrainMath.js +17 -2
- package/dist/touch/OrientationHint.d.ts +3 -0
- package/dist/touch/OrientationHint.js +13 -0
- package/dist/touch/TouchControlsOverlay.d.ts +34 -0
- package/dist/touch/TouchControlsOverlay.js +279 -0
- package/dist/visibility/CullingProvider.d.ts +21 -0
- package/dist/visibility/CullingProvider.js +134 -0
- package/dist/vision/FrustumSensorHud.d.ts +1 -6
- package/dist/vision/FrustumSensorHud.js +25 -27
- package/dist/vision/frustumSampleEqual.d.ts +2 -0
- package/dist/vision/frustumSampleEqual.js +10 -0
- package/dist/water/Ocean.js +1 -1
- package/dist/water/OceanConfig.d.ts +13 -0
- package/dist/water/OceanConfig.js +25 -17
- package/dist/water/OceanShader.d.ts +1 -1
- package/dist/water/OceanShader.js +3 -3
- package/dist/water/index.d.ts +1 -1
- package/dist/water/index.js +1 -1
- package/dist/weather/FireSpreadLayer.js +7 -2
- package/dist/weather/RainField.d.ts +3 -1
- package/dist/weather/RainField.js +4 -4
- package/dist/weather/SnowField.d.ts +3 -1
- package/dist/weather/SnowField.js +4 -4
- package/dist/weather/fireSpreadPose.d.ts +2 -0
- package/dist/weather/fireSpreadPose.js +4 -0
- package/dist/weather/weatherMath.d.ts +5 -1
- package/dist/weather/weatherMath.js +7 -2
- package/dist/world/DataObjects.d.ts +44 -0
- package/dist/world/DataObjects.js +77 -0
- package/dist/world/GridWorldScene.d.ts +10 -0
- package/dist/world/GridWorldScene.js +54 -0
- package/dist/world/SpriteBatch.d.ts +44 -0
- package/dist/world/SpriteBatch.js +112 -0
- package/dist/world/WorldHud.d.ts +8 -1
- package/dist/world/WorldHud.js +90 -39
- package/dist/world/WorldItems.js +21 -2
- package/dist/world/entityPose.d.ts +14 -0
- package/dist/world/entityPose.js +10 -0
- package/dist/world/telegraphPulse.d.ts +1 -0
- package/dist/world/telegraphPulse.js +4 -0
- package/dist/world/worldBarSamples.d.ts +30 -0
- package/dist/world/worldBarSamples.js +51 -0
- package/llms.txt +2643 -0
- package/package.json +6 -5
- package/dist/demo/builderDemo.d.ts +0 -2
- package/dist/demo/builderDemo.js +0 -189
- package/dist/demo/demoGame.d.ts +0 -2
- package/dist/demo/demoGame.js +0 -208
- package/dist/demo/environmentShowcase.d.ts +0 -2
- package/dist/demo/environmentShowcase.js +0 -15
- package/dist/demo/mapDemo.d.ts +0 -2
- package/dist/demo/mapDemo.js +0 -206
- package/dist/demo/pointerDemo.d.ts +0 -2
- package/dist/demo/pointerDemo.js +0 -151
- package/dist/demo/sensorShowcase.d.ts +0 -2
- package/dist/demo/sensorShowcase.js +0 -131
- package/dist/demo/survivalDemo.d.ts +0 -2
- package/dist/demo/survivalDemo.js +0 -291
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useRef } from "react";
|
|
3
|
+
import { createGestureSurfaceTracker } from "@jgengine/core/input/gestureSurface";
|
|
4
|
+
import { createTouchGestureTracker } from "@jgengine/core/input/touchGestures";
|
|
5
|
+
import { touchCode } from "@jgengine/core/input/touchScheme";
|
|
6
|
+
import { GameIcon, iconForAction, isGameIconName } from "@jgengine/react/gameIcons";
|
|
7
|
+
const JOYSTICK_SIZE = 132;
|
|
8
|
+
const JOYSTICK_THUMB = 52;
|
|
9
|
+
const JOYSTICK_DEADZONE = 0.28;
|
|
10
|
+
const JOYSTICK_AXIS_THRESHOLD = 0.42;
|
|
11
|
+
const LOOK_TAP_TUNING = { tapMoveThresholdPx: 12, tapMaxMs: 280, longPressMs: 450 };
|
|
12
|
+
const BUTTON_SIZE = 56;
|
|
13
|
+
const RING1_RADIUS = 112;
|
|
14
|
+
const RING2_RADIUS = 178;
|
|
15
|
+
const RING1_CAPACITY = 3;
|
|
16
|
+
const RING2_CAPACITY = 5;
|
|
17
|
+
const RING1_ANGLES = [[], [40], [16, 64], [12, 44, 76]];
|
|
18
|
+
const DOCK_BASE_PADDING = 20;
|
|
19
|
+
const UTILITY_ROW_HEIGHT = 28;
|
|
20
|
+
function polar(radius, deg, buttonSize) {
|
|
21
|
+
const rad = (deg * Math.PI) / 180;
|
|
22
|
+
return {
|
|
23
|
+
right: radius * Math.cos(rad) - buttonSize / 2,
|
|
24
|
+
bottom: radius * Math.sin(rad) - buttonSize / 2,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function ringAngles(count, from, to) {
|
|
28
|
+
if (count === 1)
|
|
29
|
+
return [(from + to) / 2];
|
|
30
|
+
const step = (to - from) / (count - 1);
|
|
31
|
+
return Array.from({ length: count }, (_, index) => from + index * step);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Thumb-arc placement for primary buttons around the bottom-right corner:
|
|
35
|
+
* up to three on an inner ring, the rest on an outer ring. Null means too
|
|
36
|
+
* many buttons for an arc — the dock falls back to a wrapping grid.
|
|
37
|
+
*/
|
|
38
|
+
export function primaryButtonOffsets(count, scale = 1) {
|
|
39
|
+
if (count === 0)
|
|
40
|
+
return [];
|
|
41
|
+
if (count > RING1_CAPACITY + RING2_CAPACITY)
|
|
42
|
+
return null;
|
|
43
|
+
const button = BUTTON_SIZE * scale;
|
|
44
|
+
const ring1 = RING1_ANGLES[Math.min(count, RING1_CAPACITY)] ?? [];
|
|
45
|
+
const offsets = ring1.map((angle) => polar(RING1_RADIUS * scale, angle, button));
|
|
46
|
+
const rest = count - ring1.length;
|
|
47
|
+
if (rest > 0) {
|
|
48
|
+
for (const angle of ringAngles(rest, 10, 80))
|
|
49
|
+
offsets.push(polar(RING2_RADIUS * scale, angle, button));
|
|
50
|
+
}
|
|
51
|
+
return offsets;
|
|
52
|
+
}
|
|
53
|
+
function primaryClusterExtent(count, scale = 1) {
|
|
54
|
+
if (count === 0)
|
|
55
|
+
return 0;
|
|
56
|
+
if (count <= RING1_CAPACITY)
|
|
57
|
+
return (RING1_RADIUS + BUTTON_SIZE / 2) * scale;
|
|
58
|
+
if (count <= RING1_CAPACITY + RING2_CAPACITY)
|
|
59
|
+
return (RING2_RADIUS + BUTTON_SIZE / 2) * scale;
|
|
60
|
+
return Math.ceil(count / 4) * (BUTTON_SIZE + 12) * scale;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Vertical space (px, excluding device safe areas) the dock occupies above
|
|
64
|
+
* the bottom edge. The shell publishes it as `--jg-hud-dock-clearance` so
|
|
65
|
+
* `HudCanvas` regions never collide with touch controls.
|
|
66
|
+
*/
|
|
67
|
+
export function touchDockClearance(scheme, scale = 1) {
|
|
68
|
+
if (scheme === null)
|
|
69
|
+
return 0;
|
|
70
|
+
let primary = 0;
|
|
71
|
+
let utility = 0;
|
|
72
|
+
for (const button of scheme.buttons) {
|
|
73
|
+
if (button.kind === "utility")
|
|
74
|
+
utility += 1;
|
|
75
|
+
else
|
|
76
|
+
primary += 1;
|
|
77
|
+
}
|
|
78
|
+
const tallest = Math.max(scheme.joystick !== null ? JOYSTICK_SIZE * scale : 0, primaryClusterExtent(primary, scale), utility > 0 ? UTILITY_ROW_HEIGHT : 0);
|
|
79
|
+
if (tallest === 0)
|
|
80
|
+
return 0;
|
|
81
|
+
return tallest + DOCK_BASE_PADDING + 12;
|
|
82
|
+
}
|
|
83
|
+
function pressOnce(sink, action) {
|
|
84
|
+
const code = touchCode(action);
|
|
85
|
+
sink.onCodeDown(code);
|
|
86
|
+
sink.onCodeUp(code);
|
|
87
|
+
}
|
|
88
|
+
export function TouchPlaySurface({ scheme, sink, yawRef, pitchRef, maxPitch, onPrimaryTap, }) {
|
|
89
|
+
const pointerIdRef = useRef(null);
|
|
90
|
+
const gestureTracker = useMemo(() => (scheme.gestures === null ? null : createGestureSurfaceTracker(scheme.gestures)), [scheme]);
|
|
91
|
+
const lookTracker = useMemo(() => (scheme.look ? createTouchGestureTracker(LOOK_TAP_TUNING) : null), [scheme]);
|
|
92
|
+
const release = (event) => {
|
|
93
|
+
if (event.pointerId !== pointerIdRef.current)
|
|
94
|
+
return;
|
|
95
|
+
pointerIdRef.current = null;
|
|
96
|
+
};
|
|
97
|
+
return (_jsx("div", { className: "absolute inset-0 touch-none", onPointerDown: (event) => {
|
|
98
|
+
if (event.pointerType !== "touch" || pointerIdRef.current !== null)
|
|
99
|
+
return;
|
|
100
|
+
pointerIdRef.current = event.pointerId;
|
|
101
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
102
|
+
if (lookTracker !== null)
|
|
103
|
+
lookTracker.begin(event.clientX, event.clientY, performance.now());
|
|
104
|
+
else
|
|
105
|
+
gestureTracker?.begin(event.clientX, event.clientY, performance.now());
|
|
106
|
+
}, onPointerMove: (event) => {
|
|
107
|
+
if (event.pointerId !== pointerIdRef.current)
|
|
108
|
+
return;
|
|
109
|
+
event.stopPropagation();
|
|
110
|
+
if (lookTracker !== null) {
|
|
111
|
+
const delta = lookTracker.move(event.clientX, event.clientY);
|
|
112
|
+
if (delta !== null) {
|
|
113
|
+
yawRef.current -= delta.dx * scheme.lookSensitivity;
|
|
114
|
+
pitchRef.current = Math.max(-maxPitch, Math.min(maxPitch, pitchRef.current - delta.dy * scheme.lookSensitivity));
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
for (const action of gestureTracker?.move(event.clientX, event.clientY) ?? [])
|
|
119
|
+
pressOnce(sink, action);
|
|
120
|
+
}, onPointerUp: (event) => {
|
|
121
|
+
if (event.pointerId !== pointerIdRef.current)
|
|
122
|
+
return;
|
|
123
|
+
event.stopPropagation();
|
|
124
|
+
release(event);
|
|
125
|
+
if (lookTracker !== null) {
|
|
126
|
+
if (lookTracker.end(performance.now()) === "tap") {
|
|
127
|
+
if (scheme.gestures?.tap !== undefined)
|
|
128
|
+
pressOnce(sink, scheme.gestures.tap);
|
|
129
|
+
else
|
|
130
|
+
onPrimaryTap();
|
|
131
|
+
}
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
for (const action of gestureTracker?.end(event.clientX, event.clientY, performance.now()) ?? []) {
|
|
135
|
+
pressOnce(sink, action);
|
|
136
|
+
}
|
|
137
|
+
}, onPointerCancel: (event) => {
|
|
138
|
+
release(event);
|
|
139
|
+
lookTracker?.cancel();
|
|
140
|
+
gestureTracker?.cancel();
|
|
141
|
+
} }));
|
|
142
|
+
}
|
|
143
|
+
function joystickDirections(joystick, nx, ny) {
|
|
144
|
+
const active = new Set();
|
|
145
|
+
if (Math.hypot(nx, ny) < JOYSTICK_DEADZONE)
|
|
146
|
+
return active;
|
|
147
|
+
if (joystick.up !== null && ny < -JOYSTICK_AXIS_THRESHOLD)
|
|
148
|
+
active.add(joystick.up);
|
|
149
|
+
if (joystick.down !== null && ny > JOYSTICK_AXIS_THRESHOLD)
|
|
150
|
+
active.add(joystick.down);
|
|
151
|
+
if (joystick.left !== null && nx < -JOYSTICK_AXIS_THRESHOLD)
|
|
152
|
+
active.add(joystick.left);
|
|
153
|
+
if (joystick.right !== null && nx > JOYSTICK_AXIS_THRESHOLD)
|
|
154
|
+
active.add(joystick.right);
|
|
155
|
+
return active;
|
|
156
|
+
}
|
|
157
|
+
function VirtualJoystick({ joystick, sink, scale = 1 }) {
|
|
158
|
+
const baseRef = useRef(null);
|
|
159
|
+
const thumbRef = useRef(null);
|
|
160
|
+
const pointerIdRef = useRef(null);
|
|
161
|
+
const activeRef = useRef(new Set());
|
|
162
|
+
const size = JOYSTICK_SIZE * scale;
|
|
163
|
+
const thumbSize = JOYSTICK_THUMB * scale;
|
|
164
|
+
const applyVector = (clientX, clientY) => {
|
|
165
|
+
const base = baseRef.current;
|
|
166
|
+
const thumb = thumbRef.current;
|
|
167
|
+
if (base === null || thumb === null)
|
|
168
|
+
return;
|
|
169
|
+
const rect = base.getBoundingClientRect();
|
|
170
|
+
const radius = rect.width / 2;
|
|
171
|
+
let nx = (clientX - rect.left - radius) / radius;
|
|
172
|
+
let ny = (clientY - rect.top - radius) / radius;
|
|
173
|
+
const length = Math.hypot(nx, ny);
|
|
174
|
+
if (length > 1) {
|
|
175
|
+
nx /= length;
|
|
176
|
+
ny /= length;
|
|
177
|
+
}
|
|
178
|
+
const travel = radius - thumbSize / 2;
|
|
179
|
+
thumb.style.transform = `translate(${nx * travel}px, ${ny * travel}px)`;
|
|
180
|
+
const next = joystickDirections(joystick, nx, ny);
|
|
181
|
+
for (const action of activeRef.current) {
|
|
182
|
+
if (!next.has(action))
|
|
183
|
+
sink.onCodeUp(touchCode(action));
|
|
184
|
+
}
|
|
185
|
+
for (const action of next) {
|
|
186
|
+
if (!activeRef.current.has(action))
|
|
187
|
+
sink.onCodeDown(touchCode(action));
|
|
188
|
+
}
|
|
189
|
+
activeRef.current = next;
|
|
190
|
+
};
|
|
191
|
+
const releaseAll = () => {
|
|
192
|
+
pointerIdRef.current = null;
|
|
193
|
+
for (const action of activeRef.current)
|
|
194
|
+
sink.onCodeUp(touchCode(action));
|
|
195
|
+
activeRef.current = new Set();
|
|
196
|
+
if (thumbRef.current !== null)
|
|
197
|
+
thumbRef.current.style.transform = "translate(0px, 0px)";
|
|
198
|
+
};
|
|
199
|
+
return (_jsx("div", { ref: baseRef, className: "pointer-events-auto relative flex touch-none select-none items-center justify-center rounded-full border border-white/20 bg-white/10 backdrop-blur-sm", style: { width: size, height: size }, onPointerDown: (event) => {
|
|
200
|
+
if (pointerIdRef.current !== null)
|
|
201
|
+
return;
|
|
202
|
+
pointerIdRef.current = event.pointerId;
|
|
203
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
204
|
+
applyVector(event.clientX, event.clientY);
|
|
205
|
+
}, onPointerMove: (event) => {
|
|
206
|
+
if (event.pointerId !== pointerIdRef.current)
|
|
207
|
+
return;
|
|
208
|
+
event.stopPropagation();
|
|
209
|
+
applyVector(event.clientX, event.clientY);
|
|
210
|
+
}, onPointerUp: (event) => {
|
|
211
|
+
if (event.pointerId !== pointerIdRef.current)
|
|
212
|
+
return;
|
|
213
|
+
event.stopPropagation();
|
|
214
|
+
releaseAll();
|
|
215
|
+
}, onPointerCancel: () => releaseAll(), children: _jsx("div", { ref: thumbRef, className: "rounded-full border border-white/30 bg-white/25", style: { width: thumbSize, height: thumbSize } }) }));
|
|
216
|
+
}
|
|
217
|
+
function touchButtonIcon(button) {
|
|
218
|
+
if (button.icon === false)
|
|
219
|
+
return null;
|
|
220
|
+
if (button.icon !== null && isGameIconName(button.icon))
|
|
221
|
+
return button.icon;
|
|
222
|
+
return iconForAction(button.action);
|
|
223
|
+
}
|
|
224
|
+
function TouchActionButton({ button, sink, scale = 1 }) {
|
|
225
|
+
const pointerIdRef = useRef(null);
|
|
226
|
+
const icon = touchButtonIcon(button);
|
|
227
|
+
const size = BUTTON_SIZE * scale;
|
|
228
|
+
const releaseIfHeld = () => {
|
|
229
|
+
if (pointerIdRef.current === null)
|
|
230
|
+
return;
|
|
231
|
+
pointerIdRef.current = null;
|
|
232
|
+
sink.onCodeUp(touchCode(button.action));
|
|
233
|
+
};
|
|
234
|
+
return (_jsx("button", { type: "button", "aria-label": button.label, style: { width: size, height: size }, className: "pointer-events-auto flex touch-none select-none items-center justify-center rounded-full border border-white/25 bg-white/10 text-center text-[10px] font-semibold uppercase leading-tight tracking-wide text-white/90 backdrop-blur-sm active:border-white/50 active:bg-white/30", onPointerDown: (event) => {
|
|
235
|
+
if (pointerIdRef.current !== null)
|
|
236
|
+
return;
|
|
237
|
+
pointerIdRef.current = event.pointerId;
|
|
238
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
239
|
+
sink.onCodeDown(touchCode(button.action));
|
|
240
|
+
}, onPointerUp: (event) => {
|
|
241
|
+
event.stopPropagation();
|
|
242
|
+
releaseIfHeld();
|
|
243
|
+
}, onPointerCancel: () => releaseIfHeld(), onContextMenu: (event) => event.preventDefault(), children: icon !== null ? _jsx(GameIcon, { name: icon, size: Math.round(26 * scale) }) : _jsx("span", { className: "px-1", children: button.label }) }));
|
|
244
|
+
}
|
|
245
|
+
function TouchUtilityChip({ button, sink }) {
|
|
246
|
+
const pointerIdRef = useRef(null);
|
|
247
|
+
const releaseIfHeld = () => {
|
|
248
|
+
if (pointerIdRef.current === null)
|
|
249
|
+
return;
|
|
250
|
+
pointerIdRef.current = null;
|
|
251
|
+
sink.onCodeUp(touchCode(button.action));
|
|
252
|
+
};
|
|
253
|
+
return (_jsx("button", { type: "button", "aria-label": button.label, className: "pointer-events-auto flex h-7 touch-none select-none items-center rounded-full border border-white/20 bg-black/40 px-3 text-[10px] font-semibold uppercase tracking-wide text-white/80 backdrop-blur-sm active:border-white/50 active:bg-white/25", onPointerDown: (event) => {
|
|
254
|
+
if (pointerIdRef.current !== null)
|
|
255
|
+
return;
|
|
256
|
+
pointerIdRef.current = event.pointerId;
|
|
257
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
258
|
+
sink.onCodeDown(touchCode(button.action));
|
|
259
|
+
}, onPointerUp: (event) => {
|
|
260
|
+
event.stopPropagation();
|
|
261
|
+
releaseIfHeld();
|
|
262
|
+
}, onPointerCancel: () => releaseIfHeld(), onContextMenu: (event) => event.preventDefault(), children: button.label }));
|
|
263
|
+
}
|
|
264
|
+
function PrimaryButtonCluster({ buttons, sink, scale = 1 }) {
|
|
265
|
+
const offsets = primaryButtonOffsets(buttons.length, scale);
|
|
266
|
+
if (offsets === null) {
|
|
267
|
+
return (_jsx("div", { className: "flex max-w-[60%] flex-wrap items-end justify-end gap-3", children: buttons.map((button) => (_jsx(TouchActionButton, { button: button, sink: sink, scale: scale }, button.action))) }));
|
|
268
|
+
}
|
|
269
|
+
const extent = primaryClusterExtent(buttons.length, scale);
|
|
270
|
+
return (_jsx("div", { className: "pointer-events-none relative", style: { width: extent, height: extent }, children: buttons.map((button, index) => {
|
|
271
|
+
const offset = offsets[index];
|
|
272
|
+
return (_jsx("div", { className: "absolute", style: { right: offset?.right ?? 0, bottom: offset?.bottom ?? 0 }, children: _jsx(TouchActionButton, { button: button, sink: sink, scale: scale }) }, button.action));
|
|
273
|
+
}) }));
|
|
274
|
+
}
|
|
275
|
+
export function TouchControlsDock({ scheme, sink, scale = 1 }) {
|
|
276
|
+
const primary = scheme.buttons.filter((button) => button.kind !== "utility");
|
|
277
|
+
const utility = scheme.buttons.filter((button) => button.kind === "utility");
|
|
278
|
+
return (_jsxs("div", { className: "pointer-events-none absolute inset-x-0 bottom-0 z-40", style: { paddingBottom: `calc(env(safe-area-inset-bottom, 0px) + ${DOCK_BASE_PADDING}px)` }, children: [_jsxs("div", { className: "flex items-end justify-between gap-4 px-5", children: [_jsx("div", { children: scheme.joystick !== null ? _jsx(VirtualJoystick, { joystick: scheme.joystick, sink: sink, scale: scale }) : null }), _jsx(PrimaryButtonCluster, { buttons: primary, sink: sink, scale: scale })] }), utility.length > 0 ? (_jsx("div", { className: "pointer-events-none absolute inset-x-0 flex justify-center gap-2", style: { bottom: "calc(env(safe-area-inset-bottom, 0px) + 8px)" }, children: utility.map((button) => (_jsx(TouchUtilityChip, { button: button, sink: sink }, button.action))) })) : null] }));
|
|
279
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ReactNode, type MutableRefObject } from "react";
|
|
2
|
+
import type { VisibilityConfig } from "@jgengine/core/visibility/config";
|
|
3
|
+
type VisiblePredicate = (id: string) => boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Read the current render-visibility predicate. Backward compatible: with no CullingProvider
|
|
6
|
+
* mounted (or culling disabled) it returns an always-visible ref, so a marker that consults it
|
|
7
|
+
* behaves exactly as before this feature existed.
|
|
8
|
+
*/
|
|
9
|
+
export declare function useRenderVisibility(): MutableRefObject<VisiblePredicate>;
|
|
10
|
+
/**
|
|
11
|
+
* Drives automatic frustum + distance culling for every entity and placed object. It reads the
|
|
12
|
+
* live render camera each frame, updates the engine VisibilitySystem, and exposes a predicate the
|
|
13
|
+
* entity/object markers consult to toggle `group.visible` — objects fully outside the view (plus a
|
|
14
|
+
* conservative preload margin) are never submitted to the renderer, without unmounting them or
|
|
15
|
+
* touching gameplay. UI, sky, terrain, and environment live outside this subtree and are unaffected.
|
|
16
|
+
*/
|
|
17
|
+
export declare function CullingProvider({ config, children }: {
|
|
18
|
+
config: VisibilityConfig | undefined;
|
|
19
|
+
children: ReactNode;
|
|
20
|
+
}): ReactNode;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useMemo, useRef } from "react";
|
|
3
|
+
import { useThree, useFrame } from "@react-three/fiber";
|
|
4
|
+
import * as THREE from "three";
|
|
5
|
+
import { useGameContext } from "@jgengine/react/provider";
|
|
6
|
+
import { createVisibilitySystem } from "@jgengine/core/visibility/visibilitySystem";
|
|
7
|
+
import { CAMERA_POST_FRAME_PRIORITY } from "../camera/index.js";
|
|
8
|
+
const ALWAYS_VISIBLE = () => true;
|
|
9
|
+
const alwaysVisibleRef = { current: ALWAYS_VISIBLE };
|
|
10
|
+
const CullingContext = createContext(null);
|
|
11
|
+
/**
|
|
12
|
+
* Read the current render-visibility predicate. Backward compatible: with no CullingProvider
|
|
13
|
+
* mounted (or culling disabled) it returns an always-visible ref, so a marker that consults it
|
|
14
|
+
* behaves exactly as before this feature existed.
|
|
15
|
+
*/
|
|
16
|
+
export function useRenderVisibility() {
|
|
17
|
+
return useContext(CullingContext) ?? alwaysVisibleRef;
|
|
18
|
+
}
|
|
19
|
+
const ENTITY_BOUNDS = { kind: "sphere", radius: 2, offset: [0, 1, 0] };
|
|
20
|
+
function objectRadius(scale) {
|
|
21
|
+
if (scale === undefined)
|
|
22
|
+
return 1;
|
|
23
|
+
const s = typeof scale === "number" ? scale : Math.max(scale[0], scale[1], scale[2]);
|
|
24
|
+
return Math.max(0.75, s);
|
|
25
|
+
}
|
|
26
|
+
function buildSystem(ctx, config) {
|
|
27
|
+
const versions = new Map();
|
|
28
|
+
let currentView = null;
|
|
29
|
+
const scratch = { id: "", position: [0, 0, 0], version: 0 };
|
|
30
|
+
const objectSpec = { kind: "sphere", radius: 1, offset: [0, 0.5, 0] };
|
|
31
|
+
function bump(id, position, rot) {
|
|
32
|
+
const prev = versions.get(id);
|
|
33
|
+
if (prev !== undefined && prev.x === position[0] && prev.y === position[1] && prev.z === position[2] && prev.rot === rot) {
|
|
34
|
+
return prev.version;
|
|
35
|
+
}
|
|
36
|
+
const version = (prev?.version ?? 0) + 1;
|
|
37
|
+
versions.set(id, { x: position[0], y: position[1], z: position[2], rot, version });
|
|
38
|
+
return version;
|
|
39
|
+
}
|
|
40
|
+
function* renderables() {
|
|
41
|
+
const entityOverrides = config?.entities;
|
|
42
|
+
for (const entity of ctx.scene.entity.list()) {
|
|
43
|
+
scratch.id = entity.id;
|
|
44
|
+
scratch.position = entity.position;
|
|
45
|
+
scratch.version = bump(entity.id, entity.position, entity.rotationY);
|
|
46
|
+
scratch.bounds = ENTITY_BOUNDS;
|
|
47
|
+
scratch.overrides = entityOverrides?.[entity.name];
|
|
48
|
+
scratch.layer = "entity";
|
|
49
|
+
yield scratch;
|
|
50
|
+
}
|
|
51
|
+
const objectOverrides = config?.objects;
|
|
52
|
+
for (const object of ctx.scene.object.list()) {
|
|
53
|
+
scratch.id = object.instanceId;
|
|
54
|
+
scratch.position = object.position;
|
|
55
|
+
scratch.version = bump(object.instanceId, object.position, object.rotationY);
|
|
56
|
+
objectSpec.radius = objectRadius(object.visual?.scale);
|
|
57
|
+
scratch.bounds = objectSpec;
|
|
58
|
+
scratch.overrides = objectOverrides?.[object.catalogId];
|
|
59
|
+
scratch.layer = "object";
|
|
60
|
+
yield scratch;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const cameras = () => (currentView === null ? EMPTY_CAMERAS : [{ id: "main", view: currentView }]);
|
|
64
|
+
const options = {
|
|
65
|
+
renderables,
|
|
66
|
+
cameras,
|
|
67
|
+
...(config?.culling !== undefined ? { settings: config.culling } : {}),
|
|
68
|
+
...(config?.scene !== undefined ? { sceneOverrides: config.scene } : {}),
|
|
69
|
+
};
|
|
70
|
+
const system = createVisibilitySystem(options);
|
|
71
|
+
return {
|
|
72
|
+
system,
|
|
73
|
+
setView(view) {
|
|
74
|
+
currentView = view;
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const EMPTY_CAMERAS = [];
|
|
79
|
+
const tmpDir = new THREE.Vector3();
|
|
80
|
+
function viewFromCamera(camera) {
|
|
81
|
+
camera.getWorldDirection(tmpDir);
|
|
82
|
+
const p = camera.position;
|
|
83
|
+
const position = [p.x, p.y, p.z];
|
|
84
|
+
const target = [p.x + tmpDir.x, p.y + tmpDir.y, p.z + tmpDir.z];
|
|
85
|
+
const up = [camera.up.x, camera.up.y, camera.up.z];
|
|
86
|
+
if (camera.isPerspectiveCamera) {
|
|
87
|
+
const c = camera;
|
|
88
|
+
return { kind: "perspective", position, target, up, fovDeg: c.fov, aspect: c.aspect, near: c.near, far: c.far };
|
|
89
|
+
}
|
|
90
|
+
if (camera.isOrthographicCamera) {
|
|
91
|
+
const c = camera;
|
|
92
|
+
const zoom = c.zoom || 1;
|
|
93
|
+
return {
|
|
94
|
+
kind: "orthographic",
|
|
95
|
+
position,
|
|
96
|
+
target,
|
|
97
|
+
up,
|
|
98
|
+
halfWidth: Math.abs((c.right - c.left) / 2 / zoom),
|
|
99
|
+
halfHeight: Math.abs((c.top - c.bottom) / 2 / zoom),
|
|
100
|
+
near: c.near,
|
|
101
|
+
far: c.far,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Drives automatic frustum + distance culling for every entity and placed object. It reads the
|
|
108
|
+
* live render camera each frame, updates the engine VisibilitySystem, and exposes a predicate the
|
|
109
|
+
* entity/object markers consult to toggle `group.visible` — objects fully outside the view (plus a
|
|
110
|
+
* conservative preload margin) are never submitted to the renderer, without unmounting them or
|
|
111
|
+
* touching gameplay. UI, sky, terrain, and environment live outside this subtree and are unaffected.
|
|
112
|
+
*/
|
|
113
|
+
export function CullingProvider({ config, children }) {
|
|
114
|
+
const ctx = useGameContext();
|
|
115
|
+
const enabled = config?.enabled !== false;
|
|
116
|
+
const predicateRef = useRef(ALWAYS_VISIBLE);
|
|
117
|
+
const camera = useThree((state) => state.camera);
|
|
118
|
+
const driver = useMemo(() => (enabled ? buildSystem(ctx, config) : null), [ctx, config, enabled]);
|
|
119
|
+
useFrame(() => {
|
|
120
|
+
if (driver === null) {
|
|
121
|
+
predicateRef.current = ALWAYS_VISIBLE;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const view = viewFromCamera(camera);
|
|
125
|
+
if (view === null) {
|
|
126
|
+
predicateRef.current = ALWAYS_VISIBLE;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
driver.setView(view);
|
|
130
|
+
const visible = driver.system.update().visible;
|
|
131
|
+
predicateRef.current = (id) => visible.has(id);
|
|
132
|
+
}, CAMERA_POST_FRAME_PRIORITY);
|
|
133
|
+
return _jsx(CullingContext.Provider, { value: predicateRef, children: children });
|
|
134
|
+
}
|
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import { type FramingConfig, type FrustumSample } from "@jgengine/core/sensor/frustumSensor";
|
|
2
|
+
export { frustumSampleDisplayEqual } from "./frustumSampleEqual.js";
|
|
2
3
|
export interface FrustumSensorProbeOptions extends FramingConfig {
|
|
3
4
|
subjectIds: readonly string[];
|
|
4
5
|
subjectRadius?: number;
|
|
5
6
|
}
|
|
6
|
-
/**
|
|
7
|
-
* View-frustum sensor (#117) driven by the live render camera: which held-camera
|
|
8
|
-
* subjects are in frame, how well framed, and their dwell time on-screen
|
|
9
|
-
* (Content Warning-style "is this monster filmed well" scoring). Returns the
|
|
10
|
-
* best-framed in-view subject each frame for a photo-mode HUD readout.
|
|
11
|
-
*/
|
|
12
7
|
export declare function useFrustumSensor(options: FrustumSensorProbeOptions): FrustumSample | null;
|
|
13
8
|
/**
|
|
14
9
|
* Renders inside the Canvas (needs the live camera via `useFrame`/`useThree`)
|
|
@@ -5,21 +5,18 @@ import { useMemo, useRef, useState } from "react";
|
|
|
5
5
|
import { PerspectiveCamera, Vector3 } from "three";
|
|
6
6
|
import { createFrustumSensor, } from "@jgengine/core/sensor/frustumSensor";
|
|
7
7
|
import { useSceneEntities } from "@jgengine/react/hooks";
|
|
8
|
+
import { frustumSampleDisplayEqual } from "./frustumSampleEqual.js";
|
|
9
|
+
export { frustumSampleDisplayEqual } from "./frustumSampleEqual.js";
|
|
8
10
|
function isPerspective(camera) {
|
|
9
11
|
return camera.isPerspectiveCamera === true;
|
|
10
12
|
}
|
|
11
|
-
/**
|
|
12
|
-
* View-frustum sensor (#117) driven by the live render camera: which held-camera
|
|
13
|
-
* subjects are in frame, how well framed, and their dwell time on-screen
|
|
14
|
-
* (Content Warning-style "is this monster filmed well" scoring). Returns the
|
|
15
|
-
* best-framed in-view subject each frame for a photo-mode HUD readout.
|
|
16
|
-
*/
|
|
17
13
|
export function useFrustumSensor(options) {
|
|
18
14
|
const camera = useThree((state) => state.camera);
|
|
19
15
|
const size = useThree((state) => state.size);
|
|
20
16
|
const entities = useSceneEntities();
|
|
21
17
|
const sensor = useMemo(() => createFrustumSensor(options), []);
|
|
22
18
|
const [best, setBest] = useState(null);
|
|
19
|
+
const bestRef = useRef(null);
|
|
23
20
|
const forward = useRef(new Vector3());
|
|
24
21
|
useFrame((_, dt) => {
|
|
25
22
|
const targets = [];
|
|
@@ -29,28 +26,29 @@ export function useFrustumSensor(options) {
|
|
|
29
26
|
continue;
|
|
30
27
|
targets.push({ id, position: entity.position, radius: options.subjectRadius });
|
|
31
28
|
}
|
|
32
|
-
if (targets.length === 0) {
|
|
33
|
-
setBest(null);
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
camera.getWorldDirection(forward.current);
|
|
37
|
-
const samples = sensor.tick({
|
|
38
|
-
position: [camera.position.x, camera.position.y, camera.position.z],
|
|
39
|
-
lookAt: [
|
|
40
|
-
camera.position.x + forward.current.x,
|
|
41
|
-
camera.position.y + forward.current.y,
|
|
42
|
-
camera.position.z + forward.current.z,
|
|
43
|
-
],
|
|
44
|
-
fovDeg: isPerspective(camera) ? camera.fov : 55,
|
|
45
|
-
aspect: size.height === 0 ? 16 / 9 : size.width / size.height,
|
|
46
|
-
}, targets, dt);
|
|
47
29
|
let leader = null;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
30
|
+
if (targets.length > 0) {
|
|
31
|
+
camera.getWorldDirection(forward.current);
|
|
32
|
+
const samples = sensor.tick({
|
|
33
|
+
position: [camera.position.x, camera.position.y, camera.position.z],
|
|
34
|
+
lookAt: [
|
|
35
|
+
camera.position.x + forward.current.x,
|
|
36
|
+
camera.position.y + forward.current.y,
|
|
37
|
+
camera.position.z + forward.current.z,
|
|
38
|
+
],
|
|
39
|
+
fovDeg: isPerspective(camera) ? camera.fov : 55,
|
|
40
|
+
aspect: size.height === 0 ? 16 / 9 : size.width / size.height,
|
|
41
|
+
}, targets, dt);
|
|
42
|
+
for (const sample of samples) {
|
|
43
|
+
if (!sample.inView)
|
|
44
|
+
continue;
|
|
45
|
+
if (leader === null || sample.framing > leader.framing)
|
|
46
|
+
leader = sample;
|
|
47
|
+
}
|
|
53
48
|
}
|
|
49
|
+
if (frustumSampleDisplayEqual(bestRef.current, leader))
|
|
50
|
+
return;
|
|
51
|
+
bestRef.current = leader;
|
|
54
52
|
setBest(leader);
|
|
55
53
|
});
|
|
56
54
|
return best;
|
|
@@ -62,6 +60,6 @@ export function useFrustumSensor(options) {
|
|
|
62
60
|
*/
|
|
63
61
|
export function FrustumSensorReadout(props) {
|
|
64
62
|
const sample = useFrustumSensor(props);
|
|
65
|
-
return (_jsx(Html, { fullscreen: true, children: _jsx("div", { className: props.wrapperClassName ?? "pointer-events-none absolute bottom-24 right-4", children: sample === null ? (_jsx("div", { className: props.className ?? "rounded border border-white/15 bg-black/60 px-3 py-1.5 text-xs text-white/50", children: "No subject framed" })) : (_jsxs("div", { className: props.className ??
|
|
63
|
+
return (_jsx(Html, { fullscreen: true, calculatePosition: (_el, _camera, size) => [size.width / 2, size.height / 2], children: _jsx("div", { className: props.wrapperClassName ?? "pointer-events-none absolute bottom-24 right-4", children: sample === null ? (_jsx("div", { className: props.className ?? "rounded border border-white/15 bg-black/60 px-3 py-1.5 text-xs text-white/50", children: "No subject framed" })) : (_jsxs("div", { className: props.className ??
|
|
66
64
|
"rounded border border-amber-300/40 bg-black/70 px-3 py-1.5 text-xs text-amber-200 shadow-lg", children: [_jsx("span", { className: "font-semibold uppercase tracking-wide", children: sample.id }), _jsxs("span", { className: "ml-2 tabular-nums", children: ["framing ", Math.round(sample.framing * 100), "%"] }), _jsxs("span", { className: "ml-2 tabular-nums", children: ["dwell ", sample.dwellSeconds.toFixed(1), "s"] })] })) }) }));
|
|
67
65
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function frustumSampleDisplayEqual(a, b) {
|
|
2
|
+
if (a === b)
|
|
3
|
+
return true;
|
|
4
|
+
if (a === null || b === null)
|
|
5
|
+
return false;
|
|
6
|
+
return (a.id === b.id &&
|
|
7
|
+
a.inView === b.inView &&
|
|
8
|
+
Math.round(a.framing * 100) === Math.round(b.framing * 100) &&
|
|
9
|
+
Math.round(a.dwellSeconds * 10) === Math.round(b.dwellSeconds * 10));
|
|
10
|
+
}
|
package/dist/water/Ocean.js
CHANGED
|
@@ -5,7 +5,7 @@ import * as THREE from "three";
|
|
|
5
5
|
import { createOceanConfig } from "./OceanConfig.js";
|
|
6
6
|
import { createOceanMaterial, syncOceanMaterial } from "./OceanMaterial.js";
|
|
7
7
|
function createOceanGeometry(config) {
|
|
8
|
-
const geometry = new THREE.PlaneGeometry(config.size, config.
|
|
8
|
+
const geometry = new THREE.PlaneGeometry(config.size, config.depth, config.resolution, config.resolution);
|
|
9
9
|
geometry.rotateX(-Math.PI / 2);
|
|
10
10
|
geometry.computeBoundingSphere();
|
|
11
11
|
return geometry;
|
|
@@ -30,7 +30,10 @@ export interface OceanFoamConfig {
|
|
|
30
30
|
}
|
|
31
31
|
export interface OceanConfig {
|
|
32
32
|
quality?: OceanQualityPreset;
|
|
33
|
+
/** Ocean plane width in world units (X). When `depth` is omitted, also used for depth (square). */
|
|
33
34
|
size?: number;
|
|
35
|
+
/** Ocean plane depth in world units (Z). Defaults to `size` for a square sheet. */
|
|
36
|
+
depth?: number;
|
|
34
37
|
resolution?: number;
|
|
35
38
|
amplitude?: number;
|
|
36
39
|
speed?: number;
|
|
@@ -38,6 +41,12 @@ export interface OceanConfig {
|
|
|
38
41
|
choppiness?: number;
|
|
39
42
|
steepness?: number;
|
|
40
43
|
timeScale?: number;
|
|
44
|
+
/**
|
|
45
|
+
* Primary Gerstner wavelength in world units — same unit as `@jgengine/core/world/water` `waveScale`
|
|
46
|
+
* (default 18). Scales the default wave set so the longest wave matches this value; explicit `waves`
|
|
47
|
+
* wavelengths are multiplied by `waveScale / 18`.
|
|
48
|
+
*/
|
|
49
|
+
waveScale?: number;
|
|
41
50
|
color?: OceanColorConfig;
|
|
42
51
|
foam?: OceanFoamConfig;
|
|
43
52
|
waves?: readonly OceanWaveConfig[];
|
|
@@ -67,6 +76,7 @@ export interface ResolvedOceanWaveConfig {
|
|
|
67
76
|
export interface ResolvedOceanConfig {
|
|
68
77
|
quality: OceanQualityPreset;
|
|
69
78
|
size: number;
|
|
79
|
+
depth: number;
|
|
70
80
|
resolution: number;
|
|
71
81
|
amplitude: number;
|
|
72
82
|
speed: number;
|
|
@@ -74,10 +84,13 @@ export interface ResolvedOceanConfig {
|
|
|
74
84
|
choppiness: number;
|
|
75
85
|
steepness: number;
|
|
76
86
|
timeScale: number;
|
|
87
|
+
waveScale: number;
|
|
77
88
|
color: ResolvedOceanColorConfig;
|
|
78
89
|
foam: ResolvedOceanFoamConfig;
|
|
79
90
|
waves: readonly ResolvedOceanWaveConfig[];
|
|
80
91
|
}
|
|
92
|
+
/** Shared with `@jgengine/core/world/water` — primary wavelength in world units. */
|
|
93
|
+
export declare const DEFAULT_OCEAN_WAVE_SCALE = 18;
|
|
81
94
|
export declare const OCEAN_QUALITY_PRESETS: Record<OceanQualityPreset, {
|
|
82
95
|
size: number;
|
|
83
96
|
resolution: number;
|