@jgengine/shell 0.10.0 → 0.12.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 +39 -2
- package/LICENSE +203 -661
- package/README.md +1 -1
- package/dist/CombatPresentation.d.ts +5 -0
- package/dist/CombatPresentation.js +8 -0
- package/dist/GameHost.d.ts +25 -1
- package/dist/GameHost.js +60 -4
- package/dist/GamePhaseStamp.d.ts +1 -0
- package/dist/GamePhaseStamp.js +9 -0
- package/dist/GamePlayer.d.ts +5 -0
- package/dist/GamePlayer.js +5 -0
- package/dist/GamePlayerShell.d.ts +5 -12
- package/dist/GamePlayerShell.js +67 -1332
- package/dist/GameUiPreview.js +3 -2
- package/dist/Shell3dPresentation.d.ts +63 -0
- package/dist/Shell3dPresentation.js +302 -0
- package/dist/ShellChrome.d.ts +51 -0
- package/dist/ShellChrome.js +51 -0
- package/dist/ShellHudPresentation.d.ts +41 -0
- package/dist/ShellHudPresentation.js +22 -0
- package/dist/audio/audioEngine.d.ts +4 -0
- package/dist/audio/audioEngine.js +128 -21
- package/dist/audio/audioWire.d.ts +40 -0
- package/dist/audio/audioWire.js +62 -0
- package/dist/audio/loopParams.d.ts +20 -0
- package/dist/audio/loopParams.js +28 -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 +8 -3
- package/dist/camera/GameFirstPersonCamera.d.ts +9 -2
- package/dist/camera/GameFirstPersonCamera.js +8 -3
- package/dist/camera/GameInspectionCamera.d.ts +1 -1
- package/dist/camera/GameInspectionCamera.js +2 -2
- package/dist/camera/GameOrbitCamera.js +24 -9
- package/dist/camera/cameraBlendMath.d.ts +3 -0
- package/dist/camera/cameraBlendMath.js +3 -0
- package/dist/camera/cameraRigs.d.ts +3 -1
- package/dist/camera/cameraRigs.js +21 -3
- 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 +36 -6
- package/dist/camera/orbitCameraMath.js +56 -6
- package/dist/camera/rigMath.d.ts +114 -24
- package/dist/camera/rigMath.js +129 -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/commandSink.d.ts +8 -3
- package/dist/commandSink.js +8 -3
- package/dist/defineGame.d.ts +15 -1
- package/dist/defineGame.js +50 -9
- package/dist/devtools/ColPanel.d.ts +1 -0
- package/dist/devtools/ColPanel.js +14 -0
- package/dist/devtools/DevtoolsOverlay.d.ts +11 -7
- package/dist/devtools/DevtoolsOverlay.js +47 -462
- 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 +266 -0
- package/dist/devtools/collisionDebug.d.ts +6 -0
- package/dist/devtools/collisionDebug.js +6 -0
- package/dist/devtools/collisionDebugMath.d.ts +8 -2
- package/dist/devtools/collisionDebugMath.js +24 -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 +36 -0
- package/dist/diagnostics/RuntimeDiagnostics.js +80 -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 +23 -7
- package/dist/environment/Daylight.js +106 -13
- package/dist/environment/EnvironmentScene.js +44 -9
- 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/environment/terrainGroundColor.d.ts +9 -0
- package/dist/environment/terrainGroundColor.js +22 -0
- package/dist/gameKit.d.ts +27 -0
- package/dist/gameKit.js +29 -0
- package/dist/hotbarActions.d.ts +26 -0
- package/dist/hotbarActions.js +27 -0
- package/dist/inputSink.d.ts +16 -4
- package/dist/inputSink.js +57 -7
- package/dist/map/MapMarkerBeacons.d.ts +3 -2
- package/dist/materialOverride.d.ts +10 -0
- package/dist/materialOverride.js +15 -3
- package/dist/multiplayer.js +8 -2
- package/dist/postfx/PostProcessing.d.ts +9 -1
- package/dist/postfx/PostProcessing.js +56 -11
- package/dist/postfx/gradeShader.js +9 -1
- package/dist/presentationResolve.d.ts +35 -0
- package/dist/presentationResolve.js +51 -0
- package/dist/registry.d.ts +4 -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/assetBase.d.ts +17 -0
- package/dist/render/assetBase.js +35 -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 +76 -0
- package/dist/render/modelRender.d.ts +11 -0
- package/dist/render/modelRender.js +15 -0
- package/dist/render/resolveModel.d.ts +54 -3
- package/dist/render/resolveModel.js +116 -4
- 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 +135 -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 +118 -0
- package/dist/scatter.d.ts +1 -0
- package/dist/scatter.js +1 -0
- package/dist/scene/AuthoredScene.d.ts +84 -0
- package/dist/scene/AuthoredScene.js +178 -0
- package/dist/scene/CityRenderer.d.ts +2 -0
- package/dist/scene/CityRenderer.js +1031 -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 +73 -0
- package/dist/scene/PoleLineRenderer.d.ts +2 -0
- package/dist/scene/PoleLineRenderer.js +287 -0
- package/dist/scene/SoilPatchRenderer.d.ts +2 -0
- package/dist/scene/SoilPatchRenderer.js +84 -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 +61 -0
- package/dist/scene/builtinSceneKindRenderers.d.ts +2 -0
- package/dist/scene/builtinSceneKindRenderers.js +26 -0
- package/dist/scene/cityWindowMaterial.d.ts +16 -0
- package/dist/scene/cityWindowMaterial.js +81 -0
- package/dist/scene/sceneKindRenderers.d.ts +30 -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 +7 -1
- package/dist/settings/appliedSettings.js +13 -2
- 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 +53 -0
- package/dist/structures/TransformGizmo.js +59 -0
- package/dist/structures/index.d.ts +1 -0
- package/dist/structures/index.js +1 -0
- package/dist/terrain/GrassField.d.ts +11 -3
- package/dist/terrain/GrassField.js +16 -6
- 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 +35 -0
- package/dist/terrain/grassGeometry.js +85 -20
- package/dist/terrain/grassMaterial.d.ts +20 -0
- package/dist/terrain/grassMaterial.js +68 -13
- package/dist/terrain/heightfieldGeometry.d.ts +34 -0
- package/dist/terrain/heightfieldGeometry.js +109 -0
- package/dist/terrain/index.d.ts +1 -0
- package/dist/terrain/index.js +1 -0
- package/dist/terrain/random.d.ts +3 -0
- package/dist/terrain/random.js +3 -0
- package/dist/terrain/soilPatchMaterial.d.ts +23 -0
- package/dist/terrain/soilPatchMaterial.js +147 -0
- package/dist/terrain/terrainDetailMaterial.d.ts +19 -2
- package/dist/terrain/terrainDetailMaterial.js +87 -7
- 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 +201 -22
- package/dist/useShellMultiplayerSync.d.ts +9 -0
- package/dist/useShellMultiplayerSync.js +98 -0
- package/dist/visibility/CullingProvider.js +1 -1
- package/dist/water/Ocean.d.ts +7 -1
- package/dist/water/Ocean.js +16 -3
- package/dist/water/OceanConfig.d.ts +11 -0
- package/dist/water/OceanConfig.js +8 -0
- package/dist/water/OceanMaterial.d.ts +12 -0
- package/dist/water/OceanMaterial.js +9 -0
- package/dist/water/OceanShader.d.ts +2 -2
- package/dist/water/OceanShader.js +78 -10
- 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/PlaceScene.d.ts +11 -0
- package/dist/world/PlaceScene.js +27 -0
- package/dist/world/RetainedVfx.d.ts +10 -0
- package/dist/world/RetainedVfx.js +136 -0
- package/dist/world/WorldHud.d.ts +49 -2
- package/dist/world/WorldHud.js +87 -2
- package/dist/world/WorldScene.d.ts +21 -0
- package/dist/world/WorldScene.js +186 -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 +25 -0
- package/dist/world/worldBarSamples.js +38 -0
- package/dist/worldSky.d.ts +11 -0
- package/dist/worldSky.js +21 -0
- package/dist/worldSync.d.ts +2 -1
- package/dist/worldSync.js +2 -1
- package/package.json +6 -6
- package/skills/game-design/SKILL.md +28 -0
- package/skills/game-design/agents/openai.yaml +4 -0
- package/skills/game-design/references/application-playbook.md +170 -0
- package/skills/game-design/references/field-guide.md +288 -0
- package/skills/jgengine/SKILL.md +54 -0
- package/skills/jgengine/api.md +527 -0
- package/skills/jgengine/capabilities.md +131 -0
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-ui/SKILL.md +41 -0
- package/skills/jgengine-ui/api.md +2007 -0
- package/skills/jgengine-ui/capabilities.md +211 -0
- package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
- package/skills/jgengine-ui/reference.md +513 -0
- package/skills/jgengine-ui/references/visual-scorecard.md +73 -0
- package/skills/jgengine-world/SKILL.md +51 -0
- package/skills/jgengine-world/api.md +3056 -0
- package/skills/jgengine-world/capabilities.md +476 -0
- package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
- package/skills/jgengine-world/reference.md +261 -0
- package/skills/level-design/SKILL.md +54 -0
- package/skills/level-design/agents/openai.yaml +4 -0
- package/skills/level-design/references/application-playbook.md +192 -0
- package/skills/level-design/references/field-guide.md +337 -0
- package/skills/level-design/references/live-editor-loop.md +179 -0
- package/dist/cartridge.d.ts +0 -145
- package/dist/cartridge.js +0 -246
- package/llms.txt +0 -2761
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useMemo, useRef } from "react";
|
|
2
|
+
import { useMemo, useRef, useState, } from "react";
|
|
3
3
|
import { createGestureSurfaceTracker } from "@jgengine/core/input/gestureSurface";
|
|
4
4
|
import { createTouchGestureTracker } from "@jgengine/core/input/touchGestures";
|
|
5
|
-
import { touchCode } from "@jgengine/core/input/touchScheme";
|
|
5
|
+
import { touchCode, } from "@jgengine/core/input/touchScheme";
|
|
6
6
|
import { GameIcon, iconForAction, isGameIconName } from "@jgengine/react/gameIcons";
|
|
7
7
|
import { useRegisterLayoutRegion } from "@jgengine/react/gameViewport";
|
|
8
8
|
const MOVEMENT_ZONE = { id: "control:movement", kind: "control", collisionPolicy: "forbid", collisionGroup: "touch-dock" };
|
|
@@ -14,6 +14,7 @@ const JOYSTICK_DEADZONE = 0.28;
|
|
|
14
14
|
const JOYSTICK_AXIS_THRESHOLD = 0.42;
|
|
15
15
|
const LOOK_TAP_TUNING = { tapMoveThresholdPx: 12, tapMaxMs: 280, longPressMs: 450 };
|
|
16
16
|
const BUTTON_SIZE = 56;
|
|
17
|
+
const RAIL_BUTTON_SIZE = 64;
|
|
17
18
|
const RING1_RADIUS = 112;
|
|
18
19
|
const RING2_RADIUS = 178;
|
|
19
20
|
const RING1_CAPACITY = 3;
|
|
@@ -21,6 +22,74 @@ const RING2_CAPACITY = 5;
|
|
|
21
22
|
const RING1_ANGLES = [[], [40], [16, 64], [12, 44, 76]];
|
|
22
23
|
const DOCK_BASE_PADDING = 20;
|
|
23
24
|
const UTILITY_ROW_HEIGHT = 28;
|
|
25
|
+
/** width:height footprint of each silhouette; the longer side equals the button size. */
|
|
26
|
+
const SHAPE_ASPECT = {
|
|
27
|
+
circle: { w: 1, h: 1 },
|
|
28
|
+
square: { w: 1, h: 1 },
|
|
29
|
+
wheel: { w: 1, h: 1 },
|
|
30
|
+
pedal: { w: 0.78, h: 1 },
|
|
31
|
+
lever: { w: 0.64, h: 1 },
|
|
32
|
+
trigger: { w: 0.92, h: 1 },
|
|
33
|
+
tab: { w: 1.5, h: 0.72 },
|
|
34
|
+
};
|
|
35
|
+
const STYLE_TOKENS = {
|
|
36
|
+
glass: {
|
|
37
|
+
restFill: "rgba(255,255,255,0.10)",
|
|
38
|
+
restStroke: "rgba(255,255,255,0.28)",
|
|
39
|
+
strokeWidth: 2,
|
|
40
|
+
activeFill: "rgba(255,255,255,0.34)",
|
|
41
|
+
activeStroke: "rgba(255,255,255,0.7)",
|
|
42
|
+
text: "rgba(255,255,255,0.92)",
|
|
43
|
+
wrapper: "drop-shadow-[0_1px_3px_rgba(0,0,0,0.35)]",
|
|
44
|
+
activeWrapper: "drop-shadow-[0_1px_5px_rgba(0,0,0,0.4)]",
|
|
45
|
+
joyBase: { border: "1px solid rgba(255,255,255,0.2)", background: "rgba(255,255,255,0.1)", backdropFilter: "blur(4px)" },
|
|
46
|
+
joyThumb: { border: "1px solid rgba(255,255,255,0.3)", background: "rgba(255,255,255,0.25)" },
|
|
47
|
+
chipRest: { border: "1px solid rgba(255,255,255,0.2)", background: "rgba(0,0,0,0.4)", color: "rgba(255,255,255,0.8)", backdropFilter: "blur(4px)" },
|
|
48
|
+
chipActive: { border: "1px solid rgba(255,255,255,0.5)", background: "rgba(255,255,255,0.25)", color: "#fff" },
|
|
49
|
+
},
|
|
50
|
+
arcade: {
|
|
51
|
+
restFill: "rgba(9,12,20,0.82)",
|
|
52
|
+
restStroke: "rgba(120,224,255,0.75)",
|
|
53
|
+
strokeWidth: 3,
|
|
54
|
+
activeFill: "rgba(120,224,255,0.9)",
|
|
55
|
+
activeStroke: "rgba(240,255,255,0.95)",
|
|
56
|
+
text: "rgba(190,244,255,0.95)",
|
|
57
|
+
wrapper: "drop-shadow-[0_0_9px_rgba(80,200,255,0.5)]",
|
|
58
|
+
activeWrapper: "drop-shadow-[0_0_16px_rgba(120,224,255,0.85)]",
|
|
59
|
+
joyBase: { border: "3px solid rgba(120,224,255,0.7)", background: "rgba(9,12,20,0.8)", boxShadow: "0 0 16px rgba(80,200,255,0.4)" },
|
|
60
|
+
joyThumb: { border: "2px solid rgba(240,255,255,0.9)", background: "rgba(120,224,255,0.85)" },
|
|
61
|
+
chipRest: { border: "2px solid rgba(120,224,255,0.6)", background: "rgba(9,12,20,0.85)", color: "rgba(190,244,255,0.95)", fontWeight: 700 },
|
|
62
|
+
chipActive: { border: "2px solid rgba(240,255,255,0.95)", background: "rgba(120,224,255,0.9)", color: "#04121a", fontWeight: 700 },
|
|
63
|
+
},
|
|
64
|
+
mechanical: {
|
|
65
|
+
restFill: "rgba(63,68,78,0.9)",
|
|
66
|
+
restStroke: "rgba(210,214,222,0.4)",
|
|
67
|
+
strokeWidth: 2.5,
|
|
68
|
+
activeFill: "rgba(120,126,138,0.95)",
|
|
69
|
+
activeStroke: "rgba(240,242,246,0.7)",
|
|
70
|
+
text: "rgba(232,234,240,0.95)",
|
|
71
|
+
wrapper: "drop-shadow-[0_2px_4px_rgba(0,0,0,0.55)]",
|
|
72
|
+
activeWrapper: "drop-shadow-[0_1px_2px_rgba(0,0,0,0.7)]",
|
|
73
|
+
joyBase: { border: "2px solid rgba(210,214,222,0.35)", background: "linear-gradient(180deg,rgba(78,84,94,0.92),rgba(44,48,56,0.92))", boxShadow: "0 3px 8px rgba(0,0,0,0.5)" },
|
|
74
|
+
joyThumb: { border: "2px solid rgba(240,242,246,0.5)", background: "linear-gradient(180deg,rgba(150,156,166,0.95),rgba(96,102,112,0.95))" },
|
|
75
|
+
chipRest: { border: "1px solid rgba(210,214,222,0.35)", background: "linear-gradient(180deg,rgba(72,78,88,0.92),rgba(40,44,52,0.92))", color: "rgba(232,234,240,0.95)" },
|
|
76
|
+
chipActive: { border: "1px solid rgba(240,242,246,0.6)", background: "rgba(120,126,138,0.95)", color: "#fff" },
|
|
77
|
+
},
|
|
78
|
+
minimal: {
|
|
79
|
+
restFill: "rgba(0,0,0,0.001)",
|
|
80
|
+
restStroke: "rgba(255,255,255,0.45)",
|
|
81
|
+
strokeWidth: 1.75,
|
|
82
|
+
activeFill: "rgba(255,255,255,0.14)",
|
|
83
|
+
activeStroke: "rgba(255,255,255,0.85)",
|
|
84
|
+
text: "rgba(255,255,255,0.85)",
|
|
85
|
+
wrapper: "",
|
|
86
|
+
activeWrapper: "",
|
|
87
|
+
joyBase: { border: "1.5px solid rgba(255,255,255,0.4)", background: "transparent" },
|
|
88
|
+
joyThumb: { border: "1.5px solid rgba(255,255,255,0.6)", background: "rgba(255,255,255,0.1)" },
|
|
89
|
+
chipRest: { border: "1px solid rgba(255,255,255,0.35)", background: "transparent", color: "rgba(255,255,255,0.8)" },
|
|
90
|
+
chipActive: { border: "1px solid rgba(255,255,255,0.8)", background: "rgba(255,255,255,0.14)", color: "#fff" },
|
|
91
|
+
},
|
|
92
|
+
};
|
|
24
93
|
function polar(radius, deg, buttonSize) {
|
|
25
94
|
const rad = (deg * Math.PI) / 180;
|
|
26
95
|
return {
|
|
@@ -63,23 +132,38 @@ function primaryClusterExtent(count, scale = 1) {
|
|
|
63
132
|
return (RING2_RADIUS + BUTTON_SIZE / 2) * scale;
|
|
64
133
|
return Math.ceil(count / 4) * (BUTTON_SIZE + 12) * scale;
|
|
65
134
|
}
|
|
135
|
+
const BOTTOM_ANCHORS = new Set(["bottom-left", "bottom-center", "bottom-right"]);
|
|
136
|
+
const ARC_ANCHORS = new Set(["bottom-left", "bottom-right"]);
|
|
66
137
|
/**
|
|
67
|
-
* Vertical space (px, excluding device safe areas)
|
|
68
|
-
* the bottom edge. The shell publishes it as
|
|
69
|
-
* `HudCanvas` regions never collide with touch
|
|
138
|
+
* Vertical space (px, excluding device safe areas) that *bottom-docked* clusters
|
|
139
|
+
* occupy above the bottom edge. The shell publishes it as
|
|
140
|
+
* `--jg-hud-dock-clearance` so `HudCanvas` regions never collide with touch
|
|
141
|
+
* controls. Side rails and top clusters reserve their own rectangles through the
|
|
142
|
+
* layout registry instead of this scalar.
|
|
70
143
|
*/
|
|
71
144
|
export function touchDockClearance(scheme, scale = 1) {
|
|
72
145
|
if (scheme === null)
|
|
73
146
|
return 0;
|
|
147
|
+
const layout = scheme.layout;
|
|
74
148
|
let primary = 0;
|
|
75
149
|
let utility = 0;
|
|
76
150
|
for (const button of scheme.buttons) {
|
|
151
|
+
if (button.anchor !== null)
|
|
152
|
+
continue;
|
|
77
153
|
if (button.kind === "utility")
|
|
78
154
|
utility += 1;
|
|
79
155
|
else
|
|
80
156
|
primary += 1;
|
|
81
157
|
}
|
|
82
|
-
const
|
|
158
|
+
const heights = [];
|
|
159
|
+
if (scheme.joystick !== null && BOTTOM_ANCHORS.has(layout.movement))
|
|
160
|
+
heights.push(JOYSTICK_SIZE * scale);
|
|
161
|
+
if (primary > 0 && BOTTOM_ANCHORS.has(layout.actions)) {
|
|
162
|
+
heights.push(ARC_ANCHORS.has(layout.actions) ? primaryClusterExtent(primary, scale) : RAIL_BUTTON_SIZE * scale + 12);
|
|
163
|
+
}
|
|
164
|
+
if (utility > 0 && BOTTOM_ANCHORS.has(layout.utility))
|
|
165
|
+
heights.push(UTILITY_ROW_HEIGHT);
|
|
166
|
+
const tallest = heights.length === 0 ? 0 : Math.max(...heights);
|
|
83
167
|
if (tallest === 0)
|
|
84
168
|
return 0;
|
|
85
169
|
return tallest + DOCK_BASE_PADDING + 12;
|
|
@@ -158,7 +242,7 @@ function joystickDirections(joystick, nx, ny) {
|
|
|
158
242
|
active.add(joystick.right);
|
|
159
243
|
return active;
|
|
160
244
|
}
|
|
161
|
-
function VirtualJoystick({ joystick, sink, scale = 1 }) {
|
|
245
|
+
function VirtualJoystick({ joystick, sink, tokens, scale = 1, }) {
|
|
162
246
|
const baseRef = useRef(null);
|
|
163
247
|
const thumbRef = useRef(null);
|
|
164
248
|
const pointerIdRef = useRef(null);
|
|
@@ -200,7 +284,7 @@ function VirtualJoystick({ joystick, sink, scale = 1 }) {
|
|
|
200
284
|
if (thumbRef.current !== null)
|
|
201
285
|
thumbRef.current.style.transform = "translate(0px, 0px)";
|
|
202
286
|
};
|
|
203
|
-
return (_jsx("div", { ref: baseRef, className: "pointer-events-auto relative flex touch-none select-none items-center justify-center rounded-full
|
|
287
|
+
return (_jsx("div", { ref: baseRef, className: "pointer-events-auto relative flex touch-none select-none items-center justify-center rounded-full", style: { width: size, height: size, ...tokens.joyBase }, onPointerDown: (event) => {
|
|
204
288
|
if (pointerIdRef.current !== null)
|
|
205
289
|
return;
|
|
206
290
|
pointerIdRef.current = event.pointerId;
|
|
@@ -216,7 +300,7 @@ function VirtualJoystick({ joystick, sink, scale = 1 }) {
|
|
|
216
300
|
return;
|
|
217
301
|
event.stopPropagation();
|
|
218
302
|
releaseAll();
|
|
219
|
-
}, onPointerCancel: () => releaseAll(), children: _jsx("div", { ref: thumbRef, className: "rounded-full
|
|
303
|
+
}, onPointerCancel: () => releaseAll(), children: _jsx("div", { ref: thumbRef, className: "rounded-full", style: { width: thumbSize, height: thumbSize, ...tokens.joyThumb } }) }));
|
|
220
304
|
}
|
|
221
305
|
function touchButtonIcon(button) {
|
|
222
306
|
if (button.icon === false)
|
|
@@ -225,65 +309,160 @@ function touchButtonIcon(button) {
|
|
|
225
309
|
return button.icon;
|
|
226
310
|
return iconForAction(button.action);
|
|
227
311
|
}
|
|
228
|
-
function
|
|
312
|
+
function ShapeSilhouette({ shape, width, height, fill, stroke, strokeWidth, }) {
|
|
313
|
+
const common = { fill, stroke, strokeWidth, strokeLinejoin: "round", strokeLinecap: "round" };
|
|
314
|
+
const body = (() => {
|
|
315
|
+
switch (shape) {
|
|
316
|
+
case "circle":
|
|
317
|
+
return _jsx("circle", { cx: 50, cy: 50, r: 46, ...common });
|
|
318
|
+
case "square":
|
|
319
|
+
return _jsx("rect", { x: 7, y: 7, width: 86, height: 86, rx: 20, ...common });
|
|
320
|
+
case "wheel":
|
|
321
|
+
return (_jsxs("g", { ...common, children: [_jsx("circle", { cx: 50, cy: 50, r: 44, fill: fill }), _jsx("circle", { cx: 50, cy: 50, r: 16, fill: "none" }), _jsx("path", { d: "M50 34 V18 M36 58 L22 72 M64 58 L78 72", fill: "none" })] }));
|
|
322
|
+
case "pedal":
|
|
323
|
+
return (_jsxs("g", { ...common, children: [_jsx("path", { d: "M30 6 h40 a10 10 0 0 1 10 10 v58 a16 16 0 0 1 -16 16 h-38 a16 16 0 0 1 -16 -16 v-58 a10 10 0 0 1 10 -10 Z" }), _jsx("path", { d: "M26 26 h48 M26 42 h48 M26 58 h48", fill: "none", strokeWidth: strokeWidth * 0.8 })] }));
|
|
324
|
+
case "lever":
|
|
325
|
+
return (_jsxs("g", { ...common, children: [_jsx("circle", { cx: 50, cy: 22, r: 18 }), _jsx("path", { d: "M42 36 h16 v52 a8 8 0 0 1 -16 0 Z" })] }));
|
|
326
|
+
case "trigger":
|
|
327
|
+
return _jsx("path", { d: "M18 20 h44 a24 24 0 0 1 24 24 v10 a30 30 0 0 1 -30 30 h-24 a14 14 0 0 1 -14 -14 v-40 a10 10 0 0 1 0 -10 Z", ...common });
|
|
328
|
+
case "tab":
|
|
329
|
+
return _jsx("path", { d: "M6 18 a12 12 0 0 1 12 -12 h124 a12 12 0 0 1 12 12 v20 a12 12 0 0 1 -12 12 h-124 a12 12 0 0 1 -12 -12 Z", ...common });
|
|
330
|
+
}
|
|
331
|
+
})();
|
|
332
|
+
const viewBox = shape === "tab" ? "0 0 160 56" : "0 0 100 100";
|
|
333
|
+
return (_jsx("svg", { width: width, height: height, viewBox: viewBox, className: "pointer-events-none absolute inset-0", children: body }));
|
|
334
|
+
}
|
|
335
|
+
function TouchActionButton({ button, sink, tokens, size, fit, }) {
|
|
229
336
|
const pointerIdRef = useRef(null);
|
|
337
|
+
const [pressed, setPressed] = useState(false);
|
|
230
338
|
const icon = touchButtonIcon(button);
|
|
231
|
-
const
|
|
339
|
+
const aspect = SHAPE_ASPECT[button.shape];
|
|
340
|
+
const svgW = size * aspect.w;
|
|
341
|
+
const svgH = size * aspect.h;
|
|
342
|
+
const boxW = fit === "box" ? size : svgW;
|
|
343
|
+
const boxH = fit === "box" ? size : svgH;
|
|
344
|
+
const iconSize = Math.round(Math.min(svgW, svgH) * 0.46);
|
|
232
345
|
const releaseIfHeld = () => {
|
|
233
346
|
if (pointerIdRef.current === null)
|
|
234
347
|
return;
|
|
235
348
|
pointerIdRef.current = null;
|
|
349
|
+
setPressed(false);
|
|
236
350
|
sink.onCodeUp(touchCode(button.action));
|
|
237
351
|
};
|
|
238
|
-
return (_jsx("button", { type: "button", "aria-label": button.label, style: { width:
|
|
352
|
+
return (_jsx("button", { type: "button", "aria-label": button.label, "data-touch-shape": button.shape, style: { width: boxW, height: boxH, transform: pressed ? "scale(0.93)" : "scale(1)", transition: "transform 90ms" }, className: `pointer-events-auto relative flex touch-none select-none items-center justify-center ${pressed ? tokens.activeWrapper : tokens.wrapper}`, onPointerDown: (event) => {
|
|
239
353
|
if (pointerIdRef.current !== null)
|
|
240
354
|
return;
|
|
241
355
|
pointerIdRef.current = event.pointerId;
|
|
242
356
|
event.currentTarget.setPointerCapture(event.pointerId);
|
|
357
|
+
setPressed(true);
|
|
243
358
|
sink.onCodeDown(touchCode(button.action));
|
|
244
359
|
}, onPointerUp: (event) => {
|
|
245
360
|
event.stopPropagation();
|
|
246
361
|
releaseIfHeld();
|
|
247
|
-
}, onPointerCancel: () => releaseIfHeld(), onContextMenu: (event) => event.preventDefault(), children:
|
|
362
|
+
}, onPointerCancel: () => releaseIfHeld(), onContextMenu: (event) => event.preventDefault(), children: _jsxs("div", { className: "relative", style: { width: svgW, height: svgH }, children: [button.image !== null ? (_jsx("img", { src: button.image, alt: "", draggable: false, className: "pointer-events-none absolute inset-0 h-full w-full select-none object-contain", style: { filter: pressed ? "brightness(1.25)" : undefined } })) : (_jsx(ShapeSilhouette, { shape: button.shape, width: svgW, height: svgH, fill: pressed ? tokens.activeFill : tokens.restFill, stroke: pressed ? tokens.activeStroke : tokens.restStroke, strokeWidth: tokens.strokeWidth })), _jsx("span", { className: "absolute inset-0 flex items-center justify-center text-center text-[10px] font-semibold uppercase leading-tight tracking-wide", style: { color: tokens.text }, children: icon !== null ? _jsx(GameIcon, { name: icon, size: iconSize }) : _jsx("span", { className: "px-1", children: button.label }) })] }) }));
|
|
248
363
|
}
|
|
249
|
-
function TouchUtilityChip({ button, sink }) {
|
|
364
|
+
function TouchUtilityChip({ button, sink, tokens }) {
|
|
250
365
|
const pointerIdRef = useRef(null);
|
|
366
|
+
const [pressed, setPressed] = useState(false);
|
|
251
367
|
const releaseIfHeld = () => {
|
|
252
368
|
if (pointerIdRef.current === null)
|
|
253
369
|
return;
|
|
254
370
|
pointerIdRef.current = null;
|
|
371
|
+
setPressed(false);
|
|
255
372
|
sink.onCodeUp(touchCode(button.action));
|
|
256
373
|
};
|
|
257
|
-
return (_jsx("button", { type: "button", "aria-label": button.label, className: "pointer-events-auto flex h-7 touch-none select-none items-center rounded-full
|
|
374
|
+
return (_jsx("button", { type: "button", "aria-label": button.label, style: pressed ? tokens.chipActive : tokens.chipRest, className: "pointer-events-auto flex h-7 touch-none select-none items-center rounded-full px-3 text-[10px] font-semibold uppercase tracking-wide", onPointerDown: (event) => {
|
|
258
375
|
if (pointerIdRef.current !== null)
|
|
259
376
|
return;
|
|
260
377
|
pointerIdRef.current = event.pointerId;
|
|
261
378
|
event.currentTarget.setPointerCapture(event.pointerId);
|
|
379
|
+
setPressed(true);
|
|
262
380
|
sink.onCodeDown(touchCode(button.action));
|
|
263
381
|
}, onPointerUp: (event) => {
|
|
264
382
|
event.stopPropagation();
|
|
265
383
|
releaseIfHeld();
|
|
266
384
|
}, onPointerCancel: () => releaseIfHeld(), onContextMenu: (event) => event.preventDefault(), children: button.label }));
|
|
267
385
|
}
|
|
268
|
-
function
|
|
386
|
+
function ThumbArcCluster({ buttons, sink, tokens, scale = 1, }) {
|
|
269
387
|
const offsets = primaryButtonOffsets(buttons.length, scale);
|
|
270
388
|
if (offsets === null) {
|
|
271
|
-
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,
|
|
389
|
+
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, tokens: tokens, size: BUTTON_SIZE * scale, fit: "box" }, button.action))) }));
|
|
272
390
|
}
|
|
273
391
|
const extent = primaryClusterExtent(buttons.length, scale);
|
|
274
392
|
return (_jsx("div", { className: "pointer-events-none relative", style: { width: extent, height: extent }, children: buttons.map((button, index) => {
|
|
275
393
|
const offset = offsets[index];
|
|
276
|
-
return (_jsx("div", { className: "absolute", style: { right: offset?.right ?? 0, bottom: offset?.bottom ?? 0 }, children: _jsx(TouchActionButton, { button: button, sink: sink,
|
|
394
|
+
return (_jsx("div", { className: "absolute", style: { right: offset?.right ?? 0, bottom: offset?.bottom ?? 0 }, children: _jsx(TouchActionButton, { button: button, sink: sink, tokens: tokens, size: BUTTON_SIZE * scale, fit: "box" }) }, button.action));
|
|
277
395
|
}) }));
|
|
278
396
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
397
|
+
/** Vertical rail (`left`/`right`) or a wrapping row for the remaining anchors. */
|
|
398
|
+
function StackCluster({ buttons, utility, sink, tokens, column, scale = 1, }) {
|
|
399
|
+
return (_jsxs("div", { className: `flex ${column ? "flex-col" : "flex-row flex-wrap"} items-center justify-center gap-3`, children: [buttons.map((button) => (_jsx(TouchActionButton, { button: button, sink: sink, tokens: tokens, size: RAIL_BUTTON_SIZE * scale, fit: "natural" }, button.action))), utility.map((button) => (_jsx(TouchUtilityChip, { button: button, sink: sink, tokens: tokens }, button.action)))] }));
|
|
400
|
+
}
|
|
401
|
+
const ANCHOR_POSITION = {
|
|
402
|
+
"bottom-left": { bottom: 0, left: 0 },
|
|
403
|
+
"bottom-center": { bottom: 0, left: "50%", transform: "translateX(-50%)" },
|
|
404
|
+
"bottom-right": { bottom: 0, right: 0 },
|
|
405
|
+
left: { top: "50%", left: 0, transform: "translateY(-50%)" },
|
|
406
|
+
right: { top: "50%", right: 0, transform: "translateY(-50%)" },
|
|
407
|
+
"top-left": { top: 0, left: 0 },
|
|
408
|
+
"top-center": { top: 0, left: "50%", transform: "translateX(-50%)" },
|
|
409
|
+
"top-right": { top: 0, right: 0 },
|
|
410
|
+
};
|
|
411
|
+
function anchorInset(anchor) {
|
|
412
|
+
const safeBottom = "env(safe-area-inset-bottom, 0px)";
|
|
413
|
+
const safeTop = "env(safe-area-inset-top, 0px)";
|
|
414
|
+
const safeLeft = "env(safe-area-inset-left, 0px)";
|
|
415
|
+
const safeRight = "env(safe-area-inset-right, 0px)";
|
|
416
|
+
const pad = `${DOCK_BASE_PADDING}px`;
|
|
417
|
+
const style = {};
|
|
418
|
+
if (anchor.startsWith("bottom"))
|
|
419
|
+
style.paddingBottom = `calc(${safeBottom} + ${pad})`;
|
|
420
|
+
if (anchor.startsWith("top"))
|
|
421
|
+
style.paddingTop = `calc(${safeTop} + ${pad})`;
|
|
422
|
+
if (anchor === "left" || anchor.endsWith("left"))
|
|
423
|
+
style.paddingLeft = `calc(${safeLeft} + ${pad})`;
|
|
424
|
+
if (anchor === "right" || anchor.endsWith("right"))
|
|
425
|
+
style.paddingRight = `calc(${safeRight} + ${pad})`;
|
|
426
|
+
if (anchor === "bottom-center" || anchor === "top-center") {
|
|
427
|
+
style.paddingLeft = `calc(${safeLeft} + ${pad})`;
|
|
428
|
+
style.paddingRight = `calc(${safeRight} + ${pad})`;
|
|
429
|
+
}
|
|
430
|
+
return style;
|
|
431
|
+
}
|
|
432
|
+
function AnchoredSlot({ anchor, children, slotRef, }) {
|
|
433
|
+
return (_jsx("div", { className: "pointer-events-none absolute z-40", style: { ...ANCHOR_POSITION[anchor], ...anchorInset(anchor) }, children: _jsx("div", { ref: slotRef, children: children }) }));
|
|
434
|
+
}
|
|
435
|
+
function bucketByAnchor(buttons) {
|
|
436
|
+
const map = new Map();
|
|
437
|
+
for (const button of buttons) {
|
|
438
|
+
if (button.anchor === null)
|
|
439
|
+
continue;
|
|
440
|
+
const bucket = map.get(button.anchor) ?? { primary: [], utility: [] };
|
|
441
|
+
if (button.kind === "utility")
|
|
442
|
+
bucket.utility.push(button);
|
|
443
|
+
else
|
|
444
|
+
bucket.primary.push(button);
|
|
445
|
+
map.set(button.anchor, bucket);
|
|
446
|
+
}
|
|
447
|
+
return map;
|
|
448
|
+
}
|
|
449
|
+
export function TouchControlsDock({ scheme, sink, style, scale = 1, }) {
|
|
450
|
+
const tokens = STYLE_TOKENS[style ?? scheme.style];
|
|
451
|
+
const layout = scheme.layout;
|
|
452
|
+
const docked = scheme.buttons.filter((button) => button.anchor === null);
|
|
453
|
+
const primary = docked.filter((button) => button.kind !== "utility");
|
|
454
|
+
const utility = docked.filter((button) => button.kind === "utility");
|
|
455
|
+
const extras = useMemo(() => bucketByAnchor(scheme.buttons), [scheme.buttons]);
|
|
282
456
|
const joystickRef = useRef(null);
|
|
283
457
|
const actionsRef = useRef(null);
|
|
284
458
|
const utilityRef = useRef(null);
|
|
285
459
|
useRegisterLayoutRegion(MOVEMENT_ZONE, joystickRef, scheme.joystick !== null);
|
|
286
460
|
useRegisterLayoutRegion(ACTIONS_ZONE, actionsRef, primary.length > 0);
|
|
287
461
|
useRegisterLayoutRegion(UTILITY_ZONE, utilityRef, utility.length > 0);
|
|
288
|
-
|
|
462
|
+
const actionsColumn = layout.actions === "left" || layout.actions === "right";
|
|
463
|
+
const utilityColumn = layout.utility === "left" || layout.utility === "right";
|
|
464
|
+
return (_jsxs(ResolvedLayout, { children: [scheme.joystick !== null ? (_jsx(AnchoredSlot, { anchor: layout.movement, slotRef: joystickRef, children: _jsx(VirtualJoystick, { joystick: scheme.joystick, sink: sink, tokens: tokens, scale: scale }) })) : null, primary.length > 0 ? (_jsx(AnchoredSlot, { anchor: layout.actions, slotRef: actionsRef, children: ARC_ANCHORS.has(layout.actions) ? (_jsx(ThumbArcCluster, { buttons: primary, sink: sink, tokens: tokens, scale: scale })) : (_jsx(StackCluster, { buttons: primary, utility: [], sink: sink, tokens: tokens, column: actionsColumn, scale: scale })) })) : null, utility.length > 0 ? (_jsx(AnchoredSlot, { anchor: layout.utility, slotRef: utilityRef, children: _jsx(StackCluster, { buttons: [], utility: utility, sink: sink, tokens: tokens, column: utilityColumn, scale: scale }) })) : null, [...extras.entries()].map(([anchor, bucket]) => (_jsx(AnchoredSlot, { anchor: anchor, children: _jsx(StackCluster, { buttons: bucket.primary, utility: bucket.utility, sink: sink, tokens: tokens, column: anchor === "left" || anchor === "right", scale: scale }) }, anchor)))] }));
|
|
465
|
+
}
|
|
466
|
+
function ResolvedLayout({ children }) {
|
|
467
|
+
return _jsx("div", { className: "pointer-events-none absolute inset-0 z-40", children: children });
|
|
289
468
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type Dispatch, type SetStateAction } from "react";
|
|
2
|
+
import type { GameContext } from "@jgengine/core/runtime/gameContext";
|
|
3
|
+
import type { PresencePoseRow } from "@jgengine/core/runtime/transport";
|
|
4
|
+
import type { ShellMultiplayer } from "./multiplayer.js";
|
|
5
|
+
import type { PlayableGame } from "./registry.js";
|
|
6
|
+
/** Joins the multiplayer server for the live context and wires presence, feed relay, and chat sync until teardown. */
|
|
7
|
+
export declare function useShellMultiplayerSync(ctx: GameContext | null, multiplayer: ShellMultiplayer | null, playable: PlayableGame, serverIdRef: {
|
|
8
|
+
current: string | null;
|
|
9
|
+
}, setRemotePlayers: Dispatch<SetStateAction<PresencePoseRow[]>>): void;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { isServerAuthoritative } from "@jgengine/core/runtime/adapter";
|
|
3
|
+
import { attachWorldSync } from "./worldSync.js";
|
|
4
|
+
/** Joins the multiplayer server for the live context and wires presence, feed relay, and chat sync until teardown. */
|
|
5
|
+
export function useShellMultiplayerSync(ctx, multiplayer, playable, serverIdRef, setRemotePlayers) {
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
if (ctx === null || multiplayer === null)
|
|
8
|
+
return;
|
|
9
|
+
let disposed = false;
|
|
10
|
+
const cleanups = [];
|
|
11
|
+
void multiplayer.backend.transport
|
|
12
|
+
.joinServer({ gameId: multiplayer.gameId })
|
|
13
|
+
.then((joined) => {
|
|
14
|
+
if (disposed) {
|
|
15
|
+
void multiplayer.backend.transport.leaveServer({ serverId: joined.serverId });
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
serverIdRef.current = joined.serverId;
|
|
19
|
+
if (isServerAuthoritative(playable.game.multiplayer) && multiplayer.backend.feeds !== undefined) {
|
|
20
|
+
cleanups.push(attachWorldSync(multiplayer.backend.feeds, joined.serverId, ctx));
|
|
21
|
+
}
|
|
22
|
+
cleanups.push(multiplayer.backend.presenceSync.subscribe(joined.serverId, (rows) => {
|
|
23
|
+
setRemotePlayers(rows.filter((row) => row.userId !== multiplayer.userId));
|
|
24
|
+
}));
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
let injecting = false;
|
|
27
|
+
for (const action of multiplayer.feedActions) {
|
|
28
|
+
cleanups.push(ctx.game.feed.subscribe(action, (entry) => {
|
|
29
|
+
if (injecting)
|
|
30
|
+
return;
|
|
31
|
+
void multiplayer.backend
|
|
32
|
+
.pushFeedEntry({
|
|
33
|
+
serverId: joined.serverId,
|
|
34
|
+
action,
|
|
35
|
+
entry: { at: entry.at, data: entry.data, from: multiplayer.userId },
|
|
36
|
+
})
|
|
37
|
+
.catch(() => undefined);
|
|
38
|
+
}));
|
|
39
|
+
const remoteUnsub = multiplayer.backend.feeds?.subscribeFeed({ serverId: joined.serverId, action }, (view) => {
|
|
40
|
+
for (const raw of view.entries) {
|
|
41
|
+
const remote = raw;
|
|
42
|
+
if (typeof remote.from !== "string" || remote.from === multiplayer.userId)
|
|
43
|
+
continue;
|
|
44
|
+
const key = `${action}|${remote.from}|${remote.at ?? 0}`;
|
|
45
|
+
if (seen.has(key))
|
|
46
|
+
continue;
|
|
47
|
+
seen.add(key);
|
|
48
|
+
injecting = true;
|
|
49
|
+
ctx.game.feed.push(action, remote.data);
|
|
50
|
+
injecting = false;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
if (remoteUnsub !== undefined)
|
|
54
|
+
cleanups.push(remoteUnsub);
|
|
55
|
+
}
|
|
56
|
+
const chatSync = multiplayer.backend.chatSyncFor?.(joined.serverId);
|
|
57
|
+
if (chatSync !== undefined && ctx.game.chat !== undefined) {
|
|
58
|
+
const chat = ctx.game.chat;
|
|
59
|
+
const globalChannelIds = new Set(chat
|
|
60
|
+
.channels()
|
|
61
|
+
.filter((channel) => channel.kind === "global")
|
|
62
|
+
.map((channel) => channel.id));
|
|
63
|
+
const seenRemoteChat = new Set();
|
|
64
|
+
cleanups.push(ctx.game.events.subscribe("chat.message", (event) => {
|
|
65
|
+
if (event.fromUserId !== multiplayer.userId)
|
|
66
|
+
return;
|
|
67
|
+
if (!globalChannelIds.has(event.channelId))
|
|
68
|
+
return;
|
|
69
|
+
void chatSync.send(event.channelId, event.body).catch(() => undefined);
|
|
70
|
+
}));
|
|
71
|
+
for (const channelId of globalChannelIds) {
|
|
72
|
+
cleanups.push(chatSync.subscribe(channelId, (messages) => {
|
|
73
|
+
for (const message of messages) {
|
|
74
|
+
if (message.fromUserId === multiplayer.userId)
|
|
75
|
+
continue;
|
|
76
|
+
if (seenRemoteChat.has(message.id))
|
|
77
|
+
continue;
|
|
78
|
+
seenRemoteChat.add(message.id);
|
|
79
|
+
chat.send(message.fromUserId, message.channelId, message.body);
|
|
80
|
+
}
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
.catch(() => undefined);
|
|
86
|
+
return () => {
|
|
87
|
+
disposed = true;
|
|
88
|
+
for (const cleanup of cleanups)
|
|
89
|
+
cleanup();
|
|
90
|
+
const serverId = serverIdRef.current;
|
|
91
|
+
serverIdRef.current = null;
|
|
92
|
+
setRemotePlayers([]);
|
|
93
|
+
if (serverId !== null) {
|
|
94
|
+
void multiplayer.backend.transport.leaveServer({ serverId }).catch(() => undefined);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}, [ctx, multiplayer, playable]);
|
|
98
|
+
}
|
|
@@ -4,7 +4,7 @@ import { useThree, useFrame } from "@react-three/fiber";
|
|
|
4
4
|
import * as THREE from "three";
|
|
5
5
|
import { useGameContext } from "@jgengine/react/provider";
|
|
6
6
|
import { createVisibilitySystem } from "@jgengine/core/visibility/visibilitySystem";
|
|
7
|
-
import { CAMERA_POST_FRAME_PRIORITY } from "../camera/
|
|
7
|
+
import { CAMERA_POST_FRAME_PRIORITY } from "../camera/cameraRigs.js";
|
|
8
8
|
const ALWAYS_VISIBLE = () => true;
|
|
9
9
|
const alwaysVisibleRef = { current: ALWAYS_VISIBLE };
|
|
10
10
|
const CullingContext = createContext(null);
|
package/dist/water/Ocean.d.ts
CHANGED
|
@@ -2,5 +2,11 @@ import { type ThreeElements } from "@react-three/fiber";
|
|
|
2
2
|
import { type OceanConfig } from "./OceanConfig.js";
|
|
3
3
|
export interface OceanProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material"> {
|
|
4
4
|
config?: OceanConfig;
|
|
5
|
+
/**
|
|
6
|
+
* Water-column depth (m) under a point of the sheet, in the sheet's local XZ. When provided,
|
|
7
|
+
* the shader shades shallow→deep by real depth, foams and turns transparent at the shore, and
|
|
8
|
+
* damps waves as the bed shoals. Omit for an open ocean (everywhere-deep behavior).
|
|
9
|
+
*/
|
|
10
|
+
depthAt?: (x: number, z: number) => number;
|
|
5
11
|
}
|
|
6
|
-
export declare function Ocean({ config, ...meshProps }: OceanProps): import("react").JSX.Element;
|
|
12
|
+
export declare function Ocean({ config, depthAt, ...meshProps }: OceanProps): import("react").JSX.Element;
|
package/dist/water/Ocean.js
CHANGED
|
@@ -4,16 +4,29 @@ import { useEffect, useMemo, useRef } from "react";
|
|
|
4
4
|
import * as THREE from "three";
|
|
5
5
|
import { createOceanConfig } from "./OceanConfig.js";
|
|
6
6
|
import { createOceanMaterial, syncOceanMaterial } from "./OceanMaterial.js";
|
|
7
|
-
|
|
7
|
+
/** aDepth when no sampler is given: everywhere-deep, matching the pre-depth shader look. */
|
|
8
|
+
const OCEAN_DEFAULT_DEPTH = 1000;
|
|
9
|
+
function createOceanGeometry(config, depthAt) {
|
|
8
10
|
const geometry = new THREE.PlaneGeometry(config.size, config.depth, config.resolution, config.resolution);
|
|
9
11
|
geometry.rotateX(-Math.PI / 2);
|
|
12
|
+
const positions = geometry.getAttribute("position");
|
|
13
|
+
const depths = new Float32Array(positions.count);
|
|
14
|
+
if (depthAt === undefined) {
|
|
15
|
+
depths.fill(OCEAN_DEFAULT_DEPTH);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
for (let i = 0; i < positions.count; i += 1) {
|
|
19
|
+
depths[i] = Math.max(0, depthAt(positions.getX(i), positions.getZ(i)));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
geometry.setAttribute("aDepth", new THREE.BufferAttribute(depths, 1));
|
|
10
23
|
geometry.computeBoundingSphere();
|
|
11
24
|
return geometry;
|
|
12
25
|
}
|
|
13
|
-
export function Ocean({ config, ...meshProps }) {
|
|
26
|
+
export function Ocean({ config, depthAt, ...meshProps }) {
|
|
14
27
|
const resolved = useMemo(() => createOceanConfig(config), [config]);
|
|
15
28
|
const elapsedRef = useRef(0);
|
|
16
|
-
const geometry = useMemo(() => createOceanGeometry(resolved), [resolved]);
|
|
29
|
+
const geometry = useMemo(() => createOceanGeometry(resolved, depthAt), [resolved, depthAt]);
|
|
17
30
|
const material = useMemo(() => createOceanMaterial(resolved), [resolved]);
|
|
18
31
|
useEffect(() => {
|
|
19
32
|
return () => geometry.dispose();
|
|
@@ -21,12 +21,18 @@ export interface OceanColorConfig {
|
|
|
21
21
|
opacity?: number;
|
|
22
22
|
fresnelStrength?: number;
|
|
23
23
|
horizonBlend?: number;
|
|
24
|
+
/** Water depth (m) at which the body color reaches the full `deep` tone. Default 2.6. */
|
|
25
|
+
depthRange?: number;
|
|
26
|
+
/** Sun-glint sparkle intensity, 0 disables. Default 0.35. */
|
|
27
|
+
sparkle?: number;
|
|
24
28
|
}
|
|
25
29
|
export interface OceanFoamConfig {
|
|
26
30
|
crestThreshold?: number;
|
|
27
31
|
softness?: number;
|
|
28
32
|
intensity?: number;
|
|
29
33
|
coverage?: number;
|
|
34
|
+
/** Shoreline foam lick width (m of water depth it covers). Default 0.5. */
|
|
35
|
+
shoreWidth?: number;
|
|
30
36
|
}
|
|
31
37
|
export interface OceanConfig {
|
|
32
38
|
quality?: OceanQualityPreset;
|
|
@@ -59,12 +65,15 @@ export interface ResolvedOceanColorConfig {
|
|
|
59
65
|
opacity: number;
|
|
60
66
|
fresnelStrength: number;
|
|
61
67
|
horizonBlend: number;
|
|
68
|
+
depthRange: number;
|
|
69
|
+
sparkle: number;
|
|
62
70
|
}
|
|
63
71
|
export interface ResolvedOceanFoamConfig {
|
|
64
72
|
crestThreshold: number;
|
|
65
73
|
softness: number;
|
|
66
74
|
intensity: number;
|
|
67
75
|
coverage: number;
|
|
76
|
+
shoreWidth: number;
|
|
68
77
|
}
|
|
69
78
|
export interface ResolvedOceanWaveConfig {
|
|
70
79
|
amplitude: number;
|
|
@@ -96,7 +105,9 @@ export declare const OCEAN_QUALITY_PRESETS: Record<OceanQualityPreset, {
|
|
|
96
105
|
resolution: number;
|
|
97
106
|
}>;
|
|
98
107
|
export declare const DEFAULT_OCEAN_CONFIG: ResolvedOceanConfig;
|
|
108
|
+
/** @internal */
|
|
99
109
|
export declare function createOceanConfig(patch?: OceanConfig): ResolvedOceanConfig;
|
|
110
|
+
/** @internal */
|
|
100
111
|
export declare function buildOceanWaveUniforms(config: ResolvedOceanConfig): {
|
|
101
112
|
directions: THREE.Vector2[];
|
|
102
113
|
params: THREE.Vector4[];
|
|
@@ -28,12 +28,15 @@ export const DEFAULT_OCEAN_CONFIG = {
|
|
|
28
28
|
opacity: 0.88,
|
|
29
29
|
fresnelStrength: 0.7,
|
|
30
30
|
horizonBlend: 0.38,
|
|
31
|
+
depthRange: 2.6,
|
|
32
|
+
sparkle: 0.35,
|
|
31
33
|
},
|
|
32
34
|
foam: {
|
|
33
35
|
crestThreshold: 0.64,
|
|
34
36
|
softness: 0.22,
|
|
35
37
|
intensity: 0.8,
|
|
36
38
|
coverage: 0.45,
|
|
39
|
+
shoreWidth: 0.5,
|
|
37
40
|
},
|
|
38
41
|
waves: [],
|
|
39
42
|
};
|
|
@@ -87,6 +90,7 @@ function resolveWave(wave, fallback, config, customWaves) {
|
|
|
87
90
|
steepness: clamp(finiteOr(wave?.steepness, fallback.steepness) * config.steepness, 0, 1.2),
|
|
88
91
|
};
|
|
89
92
|
}
|
|
93
|
+
/** @internal */
|
|
90
94
|
export function createOceanConfig(patch = {}) {
|
|
91
95
|
const quality = patch.quality ?? DEFAULT_OCEAN_CONFIG.quality;
|
|
92
96
|
const preset = OCEAN_QUALITY_PRESETS[quality];
|
|
@@ -110,6 +114,8 @@ export function createOceanConfig(patch = {}) {
|
|
|
110
114
|
opacity: clamp(finiteOr(patch.color?.opacity, DEFAULT_OCEAN_CONFIG.color.opacity), 0, 1),
|
|
111
115
|
fresnelStrength: Math.max(0, finiteOr(patch.color?.fresnelStrength, DEFAULT_OCEAN_CONFIG.color.fresnelStrength)),
|
|
112
116
|
horizonBlend: clamp(finiteOr(patch.color?.horizonBlend, DEFAULT_OCEAN_CONFIG.color.horizonBlend), 0, 1),
|
|
117
|
+
depthRange: Math.max(0.05, finiteOr(patch.color?.depthRange, DEFAULT_OCEAN_CONFIG.color.depthRange)),
|
|
118
|
+
sparkle: Math.max(0, finiteOr(patch.color?.sparkle, DEFAULT_OCEAN_CONFIG.color.sparkle)),
|
|
113
119
|
},
|
|
114
120
|
foam: {
|
|
115
121
|
...DEFAULT_OCEAN_CONFIG.foam,
|
|
@@ -118,6 +124,7 @@ export function createOceanConfig(patch = {}) {
|
|
|
118
124
|
softness: clamp(finiteOr(patch.foam?.softness, DEFAULT_OCEAN_CONFIG.foam.softness), 0.001, 1),
|
|
119
125
|
intensity: Math.max(0, finiteOr(patch.foam?.intensity, DEFAULT_OCEAN_CONFIG.foam.intensity)),
|
|
120
126
|
coverage: clamp(finiteOr(patch.foam?.coverage, DEFAULT_OCEAN_CONFIG.foam.coverage), 0, 1),
|
|
127
|
+
shoreWidth: Math.max(0, finiteOr(patch.foam?.shoreWidth, DEFAULT_OCEAN_CONFIG.foam.shoreWidth)),
|
|
121
128
|
},
|
|
122
129
|
};
|
|
123
130
|
const defaultWaves = createDefaultWaves(withoutWaves);
|
|
@@ -126,6 +133,7 @@ export function createOceanConfig(patch = {}) {
|
|
|
126
133
|
const waves = Array.from({ length: MAX_OCEAN_WAVES }, (_, index) => resolveWave(inputWaves[index], defaultWaves[index], withoutWaves, customWaves));
|
|
127
134
|
return { ...withoutWaves, waves };
|
|
128
135
|
}
|
|
136
|
+
/** @internal */
|
|
129
137
|
export function buildOceanWaveUniforms(config) {
|
|
130
138
|
const directions = [];
|
|
131
139
|
const params = [];
|
|
@@ -46,6 +46,18 @@ export interface OceanMaterialUniforms {
|
|
|
46
46
|
uFoamCoverage: {
|
|
47
47
|
value: number;
|
|
48
48
|
};
|
|
49
|
+
uDepthRange: {
|
|
50
|
+
value: number;
|
|
51
|
+
};
|
|
52
|
+
uShoreFoamWidth: {
|
|
53
|
+
value: number;
|
|
54
|
+
};
|
|
55
|
+
uSparkle: {
|
|
56
|
+
value: number;
|
|
57
|
+
};
|
|
58
|
+
uSunDirection: {
|
|
59
|
+
value: THREE.Vector3;
|
|
60
|
+
};
|
|
49
61
|
}
|
|
50
62
|
export type OceanShaderMaterial = THREE.ShaderMaterial & {
|
|
51
63
|
uniforms: OceanMaterialUniforms;
|