@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,157 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
const NOISE_GLSL = /* glsl */ `
|
|
3
|
+
uint jgHashUint(uvec2 v){
|
|
4
|
+
uint x = v.x * 1664525u + v.y * 1013904223u;
|
|
5
|
+
x = (x ^ (x >> 16u)) * 2246822519u;
|
|
6
|
+
x = (x ^ (x >> 13u)) * 3266489917u;
|
|
7
|
+
x = x ^ (x >> 16u);
|
|
8
|
+
return x;
|
|
9
|
+
}
|
|
10
|
+
float jgHash(vec2 p){
|
|
11
|
+
ivec2 i = ivec2(floor(p));
|
|
12
|
+
return float(jgHashUint(uvec2(i))) * (1.0 / 4294967296.0);
|
|
13
|
+
}
|
|
14
|
+
float jgVNoise(vec2 p){
|
|
15
|
+
vec2 i = floor(p); vec2 f = fract(p);
|
|
16
|
+
vec2 u = f * f * (3.0 - 2.0 * f);
|
|
17
|
+
float a = jgHash(i);
|
|
18
|
+
float b = jgHash(i + vec2(1.0, 0.0));
|
|
19
|
+
float c = jgHash(i + vec2(0.0, 1.0));
|
|
20
|
+
float d = jgHash(i + vec2(1.0, 1.0));
|
|
21
|
+
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
|
|
22
|
+
}
|
|
23
|
+
float jgFbm(vec2 p){
|
|
24
|
+
float v = 0.0; float amp = 0.5;
|
|
25
|
+
for (int i = 0; i < 4; i++){ v += amp * jgVNoise(p); p *= 2.0; amp *= 0.5; }
|
|
26
|
+
return v;
|
|
27
|
+
}
|
|
28
|
+
`;
|
|
29
|
+
/**
|
|
30
|
+
* A `MeshStandardMaterial` whose fragment shader keeps the biome-tinted vertex
|
|
31
|
+
* colour as the base ground and blends procedural, noise-broken rock (by slope),
|
|
32
|
+
* sand (by waterline), and snow (by height) over it — textured-reading terrain
|
|
33
|
+
* with no image assets. Full PBR: lit, shadowed, and fogged like any standard
|
|
34
|
+
* material, so it composes with the post-processing chain.
|
|
35
|
+
*
|
|
36
|
+
* When `detail.material` is set, pass the matching loaded `textures` (color/normal/roughness/ao):
|
|
37
|
+
* they tile by world position at `detail.material.repeat` world units per tile and blend over the
|
|
38
|
+
* procedural result at `detail.material.strength` — the real-texture seam layers onto the existing
|
|
39
|
+
* look instead of replacing it.
|
|
40
|
+
* @internal
|
|
41
|
+
*/
|
|
42
|
+
export function createTerrainDetailMaterial(detail, textures) {
|
|
43
|
+
const hasTexture = textures !== undefined && detail.material !== undefined;
|
|
44
|
+
const repeat = detail.material?.repeat ?? 4;
|
|
45
|
+
const strength = detail.material?.strength ?? 1;
|
|
46
|
+
const uniforms = {
|
|
47
|
+
uRockColor: { value: new THREE.Color(detail.rockColor) },
|
|
48
|
+
uSandColor: { value: new THREE.Color(detail.sandColor) },
|
|
49
|
+
uSnowColor: { value: new THREE.Color(detail.snowColor) },
|
|
50
|
+
uRockSlopeStart: { value: detail.rockSlopeStart },
|
|
51
|
+
uSnowHeight: { value: detail.snowHeight },
|
|
52
|
+
uWaterLevel: { value: detail.waterLevel },
|
|
53
|
+
uDetailScale: { value: Math.max(0.5, detail.detailScale) },
|
|
54
|
+
uMacroScale: { value: Math.max(0.5, detail.macroScale) },
|
|
55
|
+
uStrength: { value: detail.strength },
|
|
56
|
+
...(hasTexture
|
|
57
|
+
? {
|
|
58
|
+
uMatColor: { value: textures.color },
|
|
59
|
+
uMatNormal: { value: textures.normal },
|
|
60
|
+
uMatRoughness: { value: textures.roughness },
|
|
61
|
+
uMatAo: { value: textures.ao },
|
|
62
|
+
uMatRepeat: { value: Math.max(0.01, repeat) },
|
|
63
|
+
uMatStrength: { value: strength },
|
|
64
|
+
}
|
|
65
|
+
: {}),
|
|
66
|
+
};
|
|
67
|
+
const material = new THREE.MeshStandardMaterial({
|
|
68
|
+
color: "#ffffff",
|
|
69
|
+
roughness: detail.roughness,
|
|
70
|
+
metalness: 0,
|
|
71
|
+
vertexColors: true,
|
|
72
|
+
});
|
|
73
|
+
material.onBeforeCompile = (shader) => {
|
|
74
|
+
Object.assign(shader.uniforms, uniforms);
|
|
75
|
+
shader.vertexShader = shader.vertexShader
|
|
76
|
+
.replace("#include <common>", `#include <common>
|
|
77
|
+
varying vec3 vJgWorldPos;
|
|
78
|
+
varying vec3 vJgWorldNormal;`)
|
|
79
|
+
.replace("#include <beginnormal_vertex>", `#include <beginnormal_vertex>
|
|
80
|
+
vJgWorldNormal = normalize(mat3(modelMatrix) * objectNormal);`)
|
|
81
|
+
.replace("#include <begin_vertex>", `#include <begin_vertex>
|
|
82
|
+
vJgWorldPos = (modelMatrix * vec4(transformed, 1.0)).xyz;`);
|
|
83
|
+
const materialUniformsGlsl = hasTexture
|
|
84
|
+
? `
|
|
85
|
+
uniform sampler2D uMatColor;
|
|
86
|
+
uniform sampler2D uMatNormal;
|
|
87
|
+
uniform sampler2D uMatRoughness;
|
|
88
|
+
uniform sampler2D uMatAo;
|
|
89
|
+
uniform float uMatRepeat;
|
|
90
|
+
uniform float uMatStrength;`
|
|
91
|
+
: "";
|
|
92
|
+
const materialColorGlsl = hasTexture
|
|
93
|
+
? `
|
|
94
|
+
vec2 jgMatUv = jgWp / uMatRepeat;
|
|
95
|
+
vec3 jgMatColor = texture2D(uMatColor, jgMatUv).rgb;
|
|
96
|
+
jgCol = mix(jgCol, jgMatColor, uMatStrength);`
|
|
97
|
+
: "";
|
|
98
|
+
const materialRoughnessGlsl = hasTexture
|
|
99
|
+
? `
|
|
100
|
+
float jgMatRough = texture2D(uMatRoughness, jgMatUv).g;
|
|
101
|
+
roughnessFactor = mix(roughnessFactor, jgMatRough, uMatStrength);`
|
|
102
|
+
: "";
|
|
103
|
+
const materialNormalGlsl = hasTexture
|
|
104
|
+
? `
|
|
105
|
+
vec2 jgMatNormalXy = texture2D(uMatNormal, jgMatUv).rg * 2.0 - 1.0;
|
|
106
|
+
normal = normalize(normal + vec3(jgMatNormalXy.x, 0.0, jgMatNormalXy.y) * uMatStrength * 0.6);`
|
|
107
|
+
: "";
|
|
108
|
+
const materialAoGlsl = hasTexture
|
|
109
|
+
? `
|
|
110
|
+
float jgMatAo = texture2D(uMatAo, jgMatUv).r;
|
|
111
|
+
reflectedLight.indirectDiffuse *= mix(1.0, jgMatAo, uMatStrength);`
|
|
112
|
+
: "";
|
|
113
|
+
shader.fragmentShader = shader.fragmentShader
|
|
114
|
+
.replace("#include <common>", `#include <common>
|
|
115
|
+
varying vec3 vJgWorldPos;
|
|
116
|
+
varying vec3 vJgWorldNormal;
|
|
117
|
+
uniform vec3 uRockColor;
|
|
118
|
+
uniform vec3 uSandColor;
|
|
119
|
+
uniform vec3 uSnowColor;
|
|
120
|
+
uniform float uRockSlopeStart;
|
|
121
|
+
uniform float uSnowHeight;
|
|
122
|
+
uniform float uWaterLevel;
|
|
123
|
+
uniform float uDetailScale;
|
|
124
|
+
uniform float uMacroScale;
|
|
125
|
+
uniform float uStrength;
|
|
126
|
+
${materialUniformsGlsl}
|
|
127
|
+
${NOISE_GLSL}`)
|
|
128
|
+
.replace("#include <color_fragment>", `#include <color_fragment>
|
|
129
|
+
vec2 jgWp = vJgWorldPos.xz;
|
|
130
|
+
float jgFine = jgFbm(jgWp / uDetailScale);
|
|
131
|
+
float jgMacro = jgFbm(jgWp / uMacroScale);
|
|
132
|
+
float jgSlope = 1.0 - clamp(vJgWorldNormal.y, 0.0, 1.0);
|
|
133
|
+
float jgH = vJgWorldPos.y;
|
|
134
|
+
vec3 jgBase = diffuseColor.rgb;
|
|
135
|
+
jgBase *= mix(1.0 - 0.22 * uStrength, 1.0 + 0.18 * uStrength, jgFine);
|
|
136
|
+
jgBase = mix(jgBase, jgBase * vec3(0.86, 0.9, 0.82), jgMacro * 0.25 * uStrength);
|
|
137
|
+
vec3 jgRock = uRockColor * mix(0.72, 1.15, jgFbm(jgWp / (uDetailScale * 1.7)));
|
|
138
|
+
float jgRockW = smoothstep(uRockSlopeStart, uRockSlopeStart + 0.18, jgSlope) * uStrength;
|
|
139
|
+
vec3 jgCol = mix(jgBase, jgRock, jgRockW);
|
|
140
|
+
vec3 jgSand = uSandColor * mix(0.9, 1.08, jgFine);
|
|
141
|
+
float jgSandW = smoothstep(uWaterLevel + 2.4, uWaterLevel - 0.4, jgH) * (1.0 - jgRockW) * uStrength;
|
|
142
|
+
jgCol = mix(jgCol, jgSand, jgSandW);
|
|
143
|
+
vec3 jgSnow = uSnowColor * mix(0.94, 1.04, jgFine);
|
|
144
|
+
float jgSnowW = smoothstep(uSnowHeight, uSnowHeight + 8.0, jgH) * (1.0 - jgRockW * 0.6) * uStrength;
|
|
145
|
+
jgCol = mix(jgCol, jgSnow, jgSnowW);
|
|
146
|
+
${materialColorGlsl}
|
|
147
|
+
diffuseColor.rgb = jgCol;`)
|
|
148
|
+
.replace("#include <roughnessmap_fragment>", `#include <roughnessmap_fragment>
|
|
149
|
+
${materialRoughnessGlsl}`)
|
|
150
|
+
.replace("#include <normal_fragment_maps>", `#include <normal_fragment_maps>
|
|
151
|
+
${materialNormalGlsl}`)
|
|
152
|
+
.replace("#include <aomap_fragment>", `#include <aomap_fragment>
|
|
153
|
+
${materialAoGlsl}`);
|
|
154
|
+
};
|
|
155
|
+
material.customProgramCacheKey = () => `jgengine-terrain-detail-${detail.rockSlopeStart}-${detail.snowHeight}-${hasTexture ? "tex" : "flat"}`;
|
|
156
|
+
return { material };
|
|
157
|
+
}
|
|
@@ -27,10 +27,15 @@ export interface TerrainVertexColorOptions {
|
|
|
27
27
|
waterline?: THREE.ColorRepresentation;
|
|
28
28
|
waterlineHeight?: number;
|
|
29
29
|
}
|
|
30
|
+
/** @internal */
|
|
30
31
|
export declare function normalizeHeightBlend(height: number, minHeight: number, maxHeight: number): number;
|
|
32
|
+
/** @internal */
|
|
31
33
|
export declare function resolveTerrainSize(size?: TerrainArea): ResolvedTerrainSize;
|
|
34
|
+
/** @internal */
|
|
32
35
|
export declare function resolveTerrainSegments(segments?: ProceduralTerrainConfig["segments"]): ResolvedTerrainSegments;
|
|
36
|
+
/** @internal */
|
|
33
37
|
export declare function toNoiseFieldConfig(config?: ProceduralTerrainConfig): NoiseFieldConfig;
|
|
38
|
+
/** @internal */
|
|
34
39
|
export declare function createProceduralTerrainSampler(config?: ProceduralTerrainConfig): TerrainHeightSampler;
|
|
35
40
|
/** Per-position palette override for multi-biome ground coloring — `createTerrainPaletteSampler` from `@jgengine/core/world/terrain` returns exactly this shape. */
|
|
36
41
|
export type TerrainPaletteSampler = (x: number, z: number) => {
|
|
@@ -51,6 +56,8 @@ export interface FieldGroundOptions {
|
|
|
51
56
|
* Mesh any `TerrainField` — including a `CarvableField` with craters/mounds written into it — into a
|
|
52
57
|
* vertex-coloured ground geometry. `sampleHeight` drives the vertices, so runtime carves show up as
|
|
53
58
|
* real depressions the moment the field is re-sampled (bump the caller's rebuild key after a carve).
|
|
54
|
-
|
|
59
|
+
* @internal
|
|
60
|
+
*/
|
|
55
61
|
export declare function createFieldGroundGeometry(field: TerrainField, options?: FieldGroundOptions): THREE.BufferGeometry;
|
|
62
|
+
/** @internal */
|
|
56
63
|
export declare function createProceduralGroundGeometry(config?: ProceduralTerrainConfig, colors?: TerrainVertexColorOptions): THREE.BufferGeometry;
|
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
import { noiseField } from "@jgengine/core/world/terrain";
|
|
2
2
|
import * as THREE from "three";
|
|
3
3
|
import {} from "./random.js";
|
|
4
|
+
/** @internal */
|
|
4
5
|
export function normalizeHeightBlend(height, minHeight, maxHeight) {
|
|
5
6
|
const range = maxHeight - minHeight;
|
|
6
7
|
if (range <= 1e-6)
|
|
7
8
|
return 0.5;
|
|
8
9
|
return THREE.MathUtils.clamp((height - minHeight) / range, 0, 1);
|
|
9
10
|
}
|
|
11
|
+
/** @internal */
|
|
10
12
|
export function resolveTerrainSize(size = 40) {
|
|
11
13
|
return typeof size === "number" ? { width: size, depth: size } : { width: size[0], depth: size[1] };
|
|
12
14
|
}
|
|
15
|
+
/** @internal */
|
|
13
16
|
export function resolveTerrainSegments(segments = 96) {
|
|
14
17
|
if (typeof segments !== "number")
|
|
15
18
|
return { x: Math.max(1, Math.floor(segments[0])), z: Math.max(1, Math.floor(segments[1])) };
|
|
16
19
|
const resolved = Math.max(1, Math.floor(segments));
|
|
17
20
|
return { x: resolved, z: resolved };
|
|
18
21
|
}
|
|
22
|
+
/** @internal */
|
|
19
23
|
export function toNoiseFieldConfig(config = {}) {
|
|
20
24
|
return {
|
|
21
25
|
seed: config.seed,
|
|
@@ -26,6 +30,7 @@ export function toNoiseFieldConfig(config = {}) {
|
|
|
26
30
|
baseHeight: config.baseOffset ?? 0,
|
|
27
31
|
};
|
|
28
32
|
}
|
|
33
|
+
/** @internal */
|
|
29
34
|
export function createProceduralTerrainSampler(config = {}) {
|
|
30
35
|
return noiseField(toNoiseFieldConfig(config)).sampleHeight;
|
|
31
36
|
}
|
|
@@ -33,7 +38,8 @@ export function createProceduralTerrainSampler(config = {}) {
|
|
|
33
38
|
* Mesh any `TerrainField` — including a `CarvableField` with craters/mounds written into it — into a
|
|
34
39
|
* vertex-coloured ground geometry. `sampleHeight` drives the vertices, so runtime carves show up as
|
|
35
40
|
* real depressions the moment the field is re-sampled (bump the caller's rebuild key after a carve).
|
|
36
|
-
|
|
41
|
+
* @internal
|
|
42
|
+
*/
|
|
37
43
|
export function createFieldGroundGeometry(field, options = {}) {
|
|
38
44
|
const size = resolveTerrainSize(options.size);
|
|
39
45
|
const segments = resolveTerrainSegments(options.segments);
|
|
@@ -118,6 +124,7 @@ function buildGroundGeometry(size, segments, sampler, opts) {
|
|
|
118
124
|
geometry.computeBoundingSphere();
|
|
119
125
|
return geometry;
|
|
120
126
|
}
|
|
127
|
+
/** @internal */
|
|
121
128
|
export function createProceduralGroundGeometry(config = {}, colors = {}) {
|
|
122
129
|
const size = resolveTerrainSize(config.size);
|
|
123
130
|
const segments = resolveTerrainSegments(config.segments);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type MutableRefObject } from "react";
|
|
2
|
-
import { type TouchScheme } from "@jgengine/core/input/touchScheme";
|
|
2
|
+
import { type TouchScheme, type TouchStyle } from "@jgengine/core/input/touchScheme";
|
|
3
3
|
export interface TouchCodeSink {
|
|
4
4
|
onCodeDown(code: string): void;
|
|
5
5
|
onCodeUp(code: string): void;
|
|
@@ -14,9 +14,11 @@ export declare function primaryButtonOffsets(count: number, scale?: number): {
|
|
|
14
14
|
bottom: number;
|
|
15
15
|
}[] | null;
|
|
16
16
|
/**
|
|
17
|
-
* Vertical space (px, excluding device safe areas)
|
|
18
|
-
* the bottom edge. The shell publishes it as
|
|
19
|
-
* `HudCanvas` regions never collide with touch
|
|
17
|
+
* Vertical space (px, excluding device safe areas) that *bottom-docked* clusters
|
|
18
|
+
* occupy above the bottom edge. The shell publishes it as
|
|
19
|
+
* `--jg-hud-dock-clearance` so `HudCanvas` regions never collide with touch
|
|
20
|
+
* controls. Side rails and top clusters reserve their own rectangles through the
|
|
21
|
+
* layout registry instead of this scalar.
|
|
20
22
|
*/
|
|
21
23
|
export declare function touchDockClearance(scheme: TouchScheme | null, scale?: number): number;
|
|
22
24
|
export declare function TouchPlaySurface({ scheme, sink, yawRef, pitchRef, maxPitch, onPrimaryTap, }: {
|
|
@@ -27,8 +29,10 @@ export declare function TouchPlaySurface({ scheme, sink, yawRef, pitchRef, maxPi
|
|
|
27
29
|
maxPitch: number;
|
|
28
30
|
onPrimaryTap: () => void;
|
|
29
31
|
}): import("react").JSX.Element;
|
|
30
|
-
export declare function TouchControlsDock({ scheme, sink, scale }: {
|
|
32
|
+
export declare function TouchControlsDock({ scheme, sink, style, scale, }: {
|
|
31
33
|
scheme: TouchScheme;
|
|
32
34
|
sink: TouchCodeSink;
|
|
35
|
+
/** Player-selected skin; falls back to the scheme's game default. */
|
|
36
|
+
style?: TouchStyle;
|
|
33
37
|
scale?: number;
|
|
34
38
|
}): import("react").JSX.Element;
|
|
@@ -1,15 +1,20 @@
|
|
|
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
|
+
import { useRegisterLayoutRegion } from "@jgengine/react/gameViewport";
|
|
8
|
+
const MOVEMENT_ZONE = { id: "control:movement", kind: "control", collisionPolicy: "forbid", collisionGroup: "touch-dock" };
|
|
9
|
+
const ACTIONS_ZONE = { id: "control:actions", kind: "control", collisionPolicy: "forbid", collisionGroup: "touch-dock" };
|
|
10
|
+
const UTILITY_ZONE = { id: "control:utility", kind: "control", collisionPolicy: "warn", collisionGroup: "touch-dock" };
|
|
7
11
|
const JOYSTICK_SIZE = 132;
|
|
8
12
|
const JOYSTICK_THUMB = 52;
|
|
9
13
|
const JOYSTICK_DEADZONE = 0.28;
|
|
10
14
|
const JOYSTICK_AXIS_THRESHOLD = 0.42;
|
|
11
15
|
const LOOK_TAP_TUNING = { tapMoveThresholdPx: 12, tapMaxMs: 280, longPressMs: 450 };
|
|
12
16
|
const BUTTON_SIZE = 56;
|
|
17
|
+
const RAIL_BUTTON_SIZE = 64;
|
|
13
18
|
const RING1_RADIUS = 112;
|
|
14
19
|
const RING2_RADIUS = 178;
|
|
15
20
|
const RING1_CAPACITY = 3;
|
|
@@ -17,6 +22,74 @@ const RING2_CAPACITY = 5;
|
|
|
17
22
|
const RING1_ANGLES = [[], [40], [16, 64], [12, 44, 76]];
|
|
18
23
|
const DOCK_BASE_PADDING = 20;
|
|
19
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
|
+
};
|
|
20
93
|
function polar(radius, deg, buttonSize) {
|
|
21
94
|
const rad = (deg * Math.PI) / 180;
|
|
22
95
|
return {
|
|
@@ -59,23 +132,38 @@ function primaryClusterExtent(count, scale = 1) {
|
|
|
59
132
|
return (RING2_RADIUS + BUTTON_SIZE / 2) * scale;
|
|
60
133
|
return Math.ceil(count / 4) * (BUTTON_SIZE + 12) * scale;
|
|
61
134
|
}
|
|
135
|
+
const BOTTOM_ANCHORS = new Set(["bottom-left", "bottom-center", "bottom-right"]);
|
|
136
|
+
const ARC_ANCHORS = new Set(["bottom-left", "bottom-right"]);
|
|
62
137
|
/**
|
|
63
|
-
* Vertical space (px, excluding device safe areas)
|
|
64
|
-
* the bottom edge. The shell publishes it as
|
|
65
|
-
* `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.
|
|
66
143
|
*/
|
|
67
144
|
export function touchDockClearance(scheme, scale = 1) {
|
|
68
145
|
if (scheme === null)
|
|
69
146
|
return 0;
|
|
147
|
+
const layout = scheme.layout;
|
|
70
148
|
let primary = 0;
|
|
71
149
|
let utility = 0;
|
|
72
150
|
for (const button of scheme.buttons) {
|
|
151
|
+
if (button.anchor !== null)
|
|
152
|
+
continue;
|
|
73
153
|
if (button.kind === "utility")
|
|
74
154
|
utility += 1;
|
|
75
155
|
else
|
|
76
156
|
primary += 1;
|
|
77
157
|
}
|
|
78
|
-
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);
|
|
79
167
|
if (tallest === 0)
|
|
80
168
|
return 0;
|
|
81
169
|
return tallest + DOCK_BASE_PADDING + 12;
|
|
@@ -154,7 +242,7 @@ function joystickDirections(joystick, nx, ny) {
|
|
|
154
242
|
active.add(joystick.right);
|
|
155
243
|
return active;
|
|
156
244
|
}
|
|
157
|
-
function VirtualJoystick({ joystick, sink, scale = 1 }) {
|
|
245
|
+
function VirtualJoystick({ joystick, sink, tokens, scale = 1, }) {
|
|
158
246
|
const baseRef = useRef(null);
|
|
159
247
|
const thumbRef = useRef(null);
|
|
160
248
|
const pointerIdRef = useRef(null);
|
|
@@ -196,7 +284,7 @@ function VirtualJoystick({ joystick, sink, scale = 1 }) {
|
|
|
196
284
|
if (thumbRef.current !== null)
|
|
197
285
|
thumbRef.current.style.transform = "translate(0px, 0px)";
|
|
198
286
|
};
|
|
199
|
-
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) => {
|
|
200
288
|
if (pointerIdRef.current !== null)
|
|
201
289
|
return;
|
|
202
290
|
pointerIdRef.current = event.pointerId;
|
|
@@ -212,7 +300,7 @@ function VirtualJoystick({ joystick, sink, scale = 1 }) {
|
|
|
212
300
|
return;
|
|
213
301
|
event.stopPropagation();
|
|
214
302
|
releaseAll();
|
|
215
|
-
}, 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 } }) }));
|
|
216
304
|
}
|
|
217
305
|
function touchButtonIcon(button) {
|
|
218
306
|
if (button.icon === false)
|
|
@@ -221,59 +309,160 @@ function touchButtonIcon(button) {
|
|
|
221
309
|
return button.icon;
|
|
222
310
|
return iconForAction(button.action);
|
|
223
311
|
}
|
|
224
|
-
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, }) {
|
|
225
336
|
const pointerIdRef = useRef(null);
|
|
337
|
+
const [pressed, setPressed] = useState(false);
|
|
226
338
|
const icon = touchButtonIcon(button);
|
|
227
|
-
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);
|
|
228
345
|
const releaseIfHeld = () => {
|
|
229
346
|
if (pointerIdRef.current === null)
|
|
230
347
|
return;
|
|
231
348
|
pointerIdRef.current = null;
|
|
349
|
+
setPressed(false);
|
|
232
350
|
sink.onCodeUp(touchCode(button.action));
|
|
233
351
|
};
|
|
234
|
-
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) => {
|
|
235
353
|
if (pointerIdRef.current !== null)
|
|
236
354
|
return;
|
|
237
355
|
pointerIdRef.current = event.pointerId;
|
|
238
356
|
event.currentTarget.setPointerCapture(event.pointerId);
|
|
357
|
+
setPressed(true);
|
|
239
358
|
sink.onCodeDown(touchCode(button.action));
|
|
240
359
|
}, onPointerUp: (event) => {
|
|
241
360
|
event.stopPropagation();
|
|
242
361
|
releaseIfHeld();
|
|
243
|
-
}, 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 }) })] }) }));
|
|
244
363
|
}
|
|
245
|
-
function TouchUtilityChip({ button, sink }) {
|
|
364
|
+
function TouchUtilityChip({ button, sink, tokens }) {
|
|
246
365
|
const pointerIdRef = useRef(null);
|
|
366
|
+
const [pressed, setPressed] = useState(false);
|
|
247
367
|
const releaseIfHeld = () => {
|
|
248
368
|
if (pointerIdRef.current === null)
|
|
249
369
|
return;
|
|
250
370
|
pointerIdRef.current = null;
|
|
371
|
+
setPressed(false);
|
|
251
372
|
sink.onCodeUp(touchCode(button.action));
|
|
252
373
|
};
|
|
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
|
|
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) => {
|
|
254
375
|
if (pointerIdRef.current !== null)
|
|
255
376
|
return;
|
|
256
377
|
pointerIdRef.current = event.pointerId;
|
|
257
378
|
event.currentTarget.setPointerCapture(event.pointerId);
|
|
379
|
+
setPressed(true);
|
|
258
380
|
sink.onCodeDown(touchCode(button.action));
|
|
259
381
|
}, onPointerUp: (event) => {
|
|
260
382
|
event.stopPropagation();
|
|
261
383
|
releaseIfHeld();
|
|
262
384
|
}, onPointerCancel: () => releaseIfHeld(), onContextMenu: (event) => event.preventDefault(), children: button.label }));
|
|
263
385
|
}
|
|
264
|
-
function
|
|
386
|
+
function ThumbArcCluster({ buttons, sink, tokens, scale = 1, }) {
|
|
265
387
|
const offsets = primaryButtonOffsets(buttons.length, scale);
|
|
266
388
|
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,
|
|
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))) }));
|
|
268
390
|
}
|
|
269
391
|
const extent = primaryClusterExtent(buttons.length, scale);
|
|
270
392
|
return (_jsx("div", { className: "pointer-events-none relative", style: { width: extent, height: extent }, children: buttons.map((button, index) => {
|
|
271
393
|
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,
|
|
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));
|
|
273
395
|
}) }));
|
|
274
396
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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]);
|
|
456
|
+
const joystickRef = useRef(null);
|
|
457
|
+
const actionsRef = useRef(null);
|
|
458
|
+
const utilityRef = useRef(null);
|
|
459
|
+
useRegisterLayoutRegion(MOVEMENT_ZONE, joystickRef, scheme.joystick !== null);
|
|
460
|
+
useRegisterLayoutRegion(ACTIONS_ZONE, actionsRef, primary.length > 0);
|
|
461
|
+
useRegisterLayoutRegion(UTILITY_ZONE, utilityRef, utility.length > 0);
|
|
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 });
|
|
279
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;
|