@jgengine/shell 0.14.0 → 0.15.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 +500 -1
- package/dist/GamePlayerShell.js +25 -5
- package/dist/Shell3dPresentation.d.ts +6 -6
- package/dist/Shell3dPresentation.js +4 -4
- package/dist/audio/synthEngine.js +12 -2
- package/dist/camera/ControllerCameraShake.d.ts +31 -0
- package/dist/camera/ControllerCameraShake.js +34 -0
- package/dist/camera/GameCameraRig.js +10 -5
- package/dist/camera/GameOrbitCamera.js +1 -10
- package/dist/camera/cameraRigs.js +12 -4
- package/dist/camera/index.d.ts +1 -0
- package/dist/camera/index.js +1 -0
- package/dist/camera/rigMath.d.ts +2 -0
- package/dist/camera/rigMath.js +7 -0
- package/dist/devtools/CollisionDebugWorld.js +2 -2
- package/dist/devtools/DevtoolsOverlay.js +2 -0
- package/dist/drivers/FrameDriver.d.ts +4 -1
- package/dist/drivers/FrameDriver.js +5 -3
- package/dist/environment/DayNightSky.d.ts +33 -0
- package/dist/environment/DayNightSky.js +51 -0
- package/dist/environment/Daylight.d.ts +3 -1
- package/dist/environment/Daylight.js +7 -3
- package/dist/environment/EnvironmentScene.js +6 -6
- package/dist/environment/RoadJunctions.d.ts +8 -4
- package/dist/environment/RoadJunctions.js +16 -15
- package/dist/environment/RoadRibbons.d.ts +42 -6
- package/dist/environment/RoadRibbons.js +150 -27
- package/dist/environment/VolumetricClouds.js +3 -3
- package/dist/environment/index.d.ts +1 -0
- package/dist/environment/index.js +1 -0
- package/dist/gameKit.d.ts +7 -4
- package/dist/gameKit.js +8 -5
- package/dist/inputSink.js +14 -0
- package/dist/materialOverride.js +30 -0
- package/dist/postfx/PostProcessing.d.ts +3 -2
- package/dist/postfx/PostProcessing.js +11 -3
- package/dist/postfx/ScreenEffectsOverlay.d.ts +32 -0
- package/dist/postfx/ScreenEffectsOverlay.js +70 -0
- package/dist/presentationResolve.d.ts +3 -2
- package/dist/presentationResolve.js +11 -6
- package/dist/render/CascadedShadows.d.ts +13 -0
- package/dist/render/CascadedShadows.js +96 -0
- package/dist/render/EnvironmentLighting.d.ts +23 -7
- package/dist/render/EnvironmentLighting.js +69 -10
- package/dist/render/PartMotion.js +4 -4
- package/dist/render/SceneLighting.js +7 -0
- package/dist/render/SceneModels.d.ts +37 -1
- package/dist/render/SceneModels.js +40 -11
- package/dist/render/modelLoad.d.ts +41 -5
- package/dist/render/modelLoad.js +85 -18
- package/dist/render/sceneCapture.d.ts +27 -0
- package/dist/render/sceneCapture.js +43 -0
- package/dist/render/useDisposable.d.ts +16 -0
- package/dist/render/useDisposable.js +24 -0
- package/dist/render/useModelAnimation.js +47 -4
- package/dist/scene/AuthoredScene.js +14 -24
- package/dist/scene/CityRenderer.js +103 -36
- package/dist/scene/GrassFieldRenderer.js +4 -1
- package/dist/scene/PoleLineRenderer.js +7 -16
- package/dist/settings/QuickControls.js +5 -3
- package/dist/settings/SettingsMenu.js +6 -3
- package/dist/settings/appliedSettings.d.ts +3 -1
- package/dist/settings/appliedSettings.js +17 -5
- package/dist/settings/settingsController.js +11 -5
- package/dist/shellMovement.d.ts +15 -1
- package/dist/shellMovement.js +15 -1
- package/dist/structures/GeneratedBuilding.js +4 -5
- package/dist/structures/TransformGizmo.js +6 -5
- package/dist/terrain/CarvedTerrain.js +3 -7
- package/dist/terrain/GrassField.js +2 -2
- package/dist/terrain/ProceduralGround.js +3 -5
- package/dist/terrain/grassMaterial.d.ts +8 -2
- package/dist/terrain/grassMaterial.js +24 -5
- package/dist/terrain/index.d.ts +1 -1
- package/dist/terrain/terrainDetailMaterial.js +11 -2
- package/dist/terrain/terrainMath.d.ts +38 -1
- package/dist/terrain/terrainMath.js +86 -64
- package/dist/touch/TouchControlsOverlay.d.ts +6 -2
- package/dist/touch/TouchControlsOverlay.js +136 -17
- package/dist/vfx/ParticleField.d.ts +32 -0
- package/dist/vfx/ParticleField.js +91 -0
- package/dist/vfx/WorldFloatingText.d.ts +32 -0
- package/dist/vfx/WorldFloatingText.js +32 -0
- package/dist/water/Ocean.js +3 -6
- package/dist/weather/LightningStrike.js +3 -9
- package/dist/weather/RainField.js +3 -7
- package/dist/weather/SnowField.js +3 -7
- package/dist/world/DataObjects.js +3 -6
- package/dist/world/GridWorldScene.js +3 -6
- package/dist/world/InstancedBodies.js +3 -6
- package/dist/world/InstancedJoints.js +2 -2
- package/dist/world/SpriteBatch.js +3 -6
- package/dist/world/WorldEntityFrames.d.ts +47 -0
- package/dist/world/WorldEntityFrames.js +75 -0
- package/dist/world/WorldHud.js +7 -5
- package/dist/world/WorldPings.d.ts +26 -0
- package/dist/world/WorldPings.js +68 -0
- package/dist/world/WorldScene.js +9 -4
- package/dist/world/pingPulse.d.ts +21 -0
- package/dist/world/pingPulse.js +17 -0
- package/package.json +5 -5
- package/skills/ce-handoff/SKILL.md +149 -0
- package/skills/game-design/SKILL.md +2 -0
- package/skills/jgengine/SKILL.md +3 -1
- package/skills/jgengine/api.md +17 -7
- package/skills/jgengine/capabilities.md +12 -0
- package/skills/jgengine/recipes/minimal-game.md +7 -4
- package/skills/jgengine-ui/SKILL.md +5 -3
- package/skills/jgengine-ui/api.md +708 -19
- package/skills/jgengine-ui/capabilities.md +362 -1
- package/skills/jgengine-ui/recipes/overhead-entity-frames.md +136 -0
- package/skills/jgengine-ui/reference.md +7 -4
- package/skills/jgengine-world/api.md +397 -11
- package/skills/jgengine-world/capabilities.md +123 -2
- package/skills/jgengine-world/reference.md +15 -1
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import {} from "@react-three/fiber";
|
|
3
|
-
import { useEffect, useMemo } from "react";
|
|
4
3
|
import * as THREE from "three";
|
|
4
|
+
import { useDisposable } from "../render/useDisposable.js";
|
|
5
5
|
import { createProceduralGroundGeometry } from "./terrainMath.js";
|
|
6
6
|
export function ProceduralGround({ terrain, colors, roughness = 0.94, metalness = 0, receiveShadow = true, ...meshProps }) {
|
|
7
|
-
const geometry =
|
|
8
|
-
const material =
|
|
7
|
+
const geometry = useDisposable(() => createProceduralGroundGeometry(terrain, colors), [colors, terrain]);
|
|
8
|
+
const material = useDisposable(() => new THREE.MeshStandardMaterial({
|
|
9
9
|
color: "#ffffff",
|
|
10
10
|
roughness,
|
|
11
11
|
metalness,
|
|
12
12
|
vertexColors: true,
|
|
13
13
|
}), [metalness, roughness]);
|
|
14
|
-
useEffect(() => () => geometry.dispose(), [geometry]);
|
|
15
|
-
useEffect(() => () => material.dispose(), [material]);
|
|
16
14
|
return _jsx("mesh", { ...meshProps, geometry: geometry, material: material, receiveShadow: receiveShadow });
|
|
17
15
|
}
|
|
@@ -5,6 +5,12 @@ export interface GrassWindOptions {
|
|
|
5
5
|
speed?: number;
|
|
6
6
|
gustScale?: number;
|
|
7
7
|
flutter?: number;
|
|
8
|
+
/**
|
|
9
|
+
* Layered rolling gust field: three phase-offset directional waves (one carrying a cross-wind
|
|
10
|
+
* component) instead of a single sine, so visible wave fronts sweep the meadow. Default true.
|
|
11
|
+
* `false` restores the single-sine gust. Compile-time variation — folded into the program cache key.
|
|
12
|
+
*/
|
|
13
|
+
layered?: boolean;
|
|
8
14
|
}
|
|
9
15
|
/** Camera-distance fade band: tufts thin out between `start` and `end` meters, so the instance budget spends where the camera lives. */
|
|
10
16
|
export interface GrassDistanceFadeOptions {
|
|
@@ -18,7 +24,7 @@ export interface GrassMaterialOptions {
|
|
|
18
24
|
colorGround?: THREE.ColorRepresentation;
|
|
19
25
|
colorVariation?: number;
|
|
20
26
|
wind?: GrassWindOptions | false;
|
|
21
|
-
/** Distance thinning; `false` disables. Default fades between
|
|
27
|
+
/** Distance thinning; `false` disables. Default fades between 55 m and 150 m so meadows read expansive. */
|
|
22
28
|
distanceFade?: GrassDistanceFadeOptions | false;
|
|
23
29
|
/** 0..1 blend of blade normals toward straight-up — lit like a soft carpet instead of dark individual planes. Default 0.6. */
|
|
24
30
|
normalLift?: number;
|
|
@@ -43,7 +49,7 @@ export interface GrassMaterialHandle {
|
|
|
43
49
|
uniforms: GrassShaderUniforms;
|
|
44
50
|
}
|
|
45
51
|
export declare const DEFAULT_GRASS_WIND: Required<GrassWindOptions>;
|
|
46
|
-
/** Default camera-distance fade band: tufts start thinning at
|
|
52
|
+
/** Default camera-distance fade band: tufts start thinning at 55 m and are gone by 150 m so the meadow reads deep. */
|
|
47
53
|
export declare const DEFAULT_GRASS_DISTANCE_FADE: Required<GrassDistanceFadeOptions>;
|
|
48
54
|
/** @internal */
|
|
49
55
|
export declare function resolveGrassWind(wind: GrassWindOptions | false | undefined): Required<GrassWindOptions>;
|
|
@@ -5,11 +5,12 @@ export const DEFAULT_GRASS_WIND = {
|
|
|
5
5
|
speed: 1.6,
|
|
6
6
|
gustScale: 0.16,
|
|
7
7
|
flutter: 0.08,
|
|
8
|
+
layered: true,
|
|
8
9
|
};
|
|
9
|
-
/** Default camera-distance fade band: tufts start thinning at
|
|
10
|
+
/** Default camera-distance fade band: tufts start thinning at 55 m and are gone by 150 m so the meadow reads deep. */
|
|
10
11
|
export const DEFAULT_GRASS_DISTANCE_FADE = {
|
|
11
|
-
start:
|
|
12
|
-
end:
|
|
12
|
+
start: 55,
|
|
13
|
+
end: 150,
|
|
13
14
|
};
|
|
14
15
|
function normalizeWindDirection(direction) {
|
|
15
16
|
const vector = new THREE.Vector2(direction[0], direction[1]);
|
|
@@ -25,6 +26,7 @@ export function resolveGrassWind(wind) {
|
|
|
25
26
|
speed: wind?.speed ?? DEFAULT_GRASS_WIND.speed,
|
|
26
27
|
gustScale: wind?.gustScale ?? DEFAULT_GRASS_WIND.gustScale,
|
|
27
28
|
flutter: wind?.flutter ?? DEFAULT_GRASS_WIND.flutter,
|
|
29
|
+
layered: wind?.layered ?? DEFAULT_GRASS_WIND.layered,
|
|
28
30
|
};
|
|
29
31
|
}
|
|
30
32
|
/** @internal */
|
|
@@ -122,7 +124,15 @@ void jgComputeGrassBlade() {
|
|
|
122
124
|
float st = sin(instanceYaw);
|
|
123
125
|
vec3 tuftOffset = vec3(bladeOffset.x * ct + bladeOffset.y * st, 0.0, -bladeOffset.x * st + bladeOffset.y * ct);
|
|
124
126
|
float gustPhase = dot(instanceOffset.xz, uWindDirection) * uWindGustScale + uTime * uWindSpeed + instancePhase;
|
|
125
|
-
|
|
127
|
+
${wind.layered
|
|
128
|
+
? ` // Layered rolling gust field: three phase-offset directional waves, one carrying a cross-wind
|
|
129
|
+
// term, so coherent wave fronts sweep across the meadow instead of every tuft waving in place.
|
|
130
|
+
vec2 windPerp = vec2(-uWindDirection.y, uWindDirection.x);
|
|
131
|
+
float crossPhase = dot(instanceOffset.xz, windPerp) * uWindGustScale * 0.6;
|
|
132
|
+
float gust = sin(gustPhase) * 0.55
|
|
133
|
+
+ sin(gustPhase * 0.53 + crossPhase + 2.1) * 0.32
|
|
134
|
+
+ sin(gustPhase * 1.7 + crossPhase * 0.4 + 4.3) * 0.13;`
|
|
135
|
+
: ` float gust = sin(gustPhase) * 0.7 + sin(gustPhase * 0.43 + 2.4) * 0.3;`}
|
|
126
136
|
float flutter = sin(uTime * 7.5 + instancePhase * 2.7 + bladeVary.w * 6.2832) * uWindFlutter;
|
|
127
137
|
vec2 windOffset = uWindDirection * (gust * uWindStrength + flutter) * t * t * (0.4 + 0.6 * bladeVary.y);
|
|
128
138
|
jgGrassPosition = instanceOffset + tuftOffset + yawedPosition + vec3(windOffset.x, 0.0, windOffset.y);
|
|
@@ -156,8 +166,17 @@ grassColor *= mix(1.0 - uColorVariation, 1.0 + uColorVariation, vGrassColorMix);
|
|
|
156
166
|
grassColor = mix(uColorGround, grassColor, smoothstep(0.0, 0.55, vGrassBladeT));
|
|
157
167
|
grassColor = mix(grassColor, uColorGround * 1.04, vGrassFade * 0.6);
|
|
158
168
|
diffuseColor.rgb = grassColor;
|
|
169
|
+
`)
|
|
170
|
+
.replace("#include <lights_fragment_end>", `
|
|
171
|
+
#include <lights_fragment_end>
|
|
172
|
+
// Warm tip sheen / backscatter: blade tips lift toward a warm glow, scaled by how strongly the
|
|
173
|
+
// fragment is directly lit — a cheap stand-in for sun shining through the canopy, no extra lights.
|
|
174
|
+
float jgTipMask = smoothstep(0.55, 1.0, vGrassBladeT);
|
|
175
|
+
float jgLit = clamp(dot(reflectedLight.directDiffuse, vec3(0.3333)) * 1.6, 0.0, 1.0);
|
|
176
|
+
reflectedLight.directDiffuse += vec3(0.34, 0.28, 0.12) * jgTipMask * jgLit * (1.0 - vGrassFade * 0.7);
|
|
159
177
|
`);
|
|
160
178
|
};
|
|
161
|
-
|
|
179
|
+
// Only the layered-gust branch changes the compiled shader source, so it is the sole compile-time key.
|
|
180
|
+
material.customProgramCacheKey = () => `jgengine-grass-${wind.layered ? "layered" : "single"}`;
|
|
162
181
|
return { material, uniforms };
|
|
163
182
|
}
|
package/dist/terrain/index.d.ts
CHANGED
|
@@ -7,5 +7,5 @@ export { TerraformBrushCursor, type TerraformBrushCursorProps } from "./Terrafor
|
|
|
7
7
|
export { createGrassBladeGeometry, resolveGrassBladeGeometryOptions, resolveGrassRange, type GrassBladeGeometryOptions, type GrassRange, type ResolvedGrassBladeGeometryOptions, } from "./grassGeometry.js";
|
|
8
8
|
export { createGrassMaterial, DEFAULT_GRASS_WIND, resolveGrassWind, type GrassMaterialHandle, type GrassMaterialOptions, type GrassShaderUniforms, type GrassWindOptions, } from "./grassMaterial.js";
|
|
9
9
|
export { createSeededRandom, hashNoise2, seedToUint32, type TerrainSeed } from "./random.js";
|
|
10
|
-
export { createFieldGroundGeometry, createProceduralGroundGeometry, createProceduralTerrainSampler, normalizeHeightBlend, resolveTerrainSegments, resolveTerrainSize, toNoiseFieldConfig, type FieldGroundOptions, type ProceduralTerrainConfig, type ResolvedTerrainSegments, type ResolvedTerrainSize, type TerrainArea, type TerrainHeightSampler, type TerrainVertexColorOptions, } from "./terrainMath.js";
|
|
10
|
+
export { createFieldGroundGeometry, createProceduralGroundGeometry, createProceduralTerrainSampler, normalizeHeightBlend, resolveTerrainSegments, resolveTerrainSize, toNoiseFieldConfig, type FieldGroundOptions, type ProceduralTerrainConfig, type ResolvedTerrainSegments, type ResolvedTerrainSize, type TerrainArea, type TerrainHeightSampler, type TerrainSurfaceColorOptions, type TerrainVertexColorOptions, } from "./terrainMath.js";
|
|
11
11
|
export { arenaField, flatField, fractalNoise, noiseField, resolveGroundStep, resolveTerrainField, valueNoise, withNormal, type FractalNoiseConfig, type NoiseFieldConfig, type TerrainField, type TerrainNormal, } from "@jgengine/core/world/terrain";
|
|
@@ -43,6 +43,7 @@ export function createTerrainDetailMaterial(detail, textures) {
|
|
|
43
43
|
const hasTexture = textures !== undefined && detail.material !== undefined;
|
|
44
44
|
const repeat = detail.material?.repeat ?? 4;
|
|
45
45
|
const strength = detail.material?.strength ?? 1;
|
|
46
|
+
const tint = detail.material?.tint ?? 1;
|
|
46
47
|
const uniforms = {
|
|
47
48
|
uRockColor: { value: new THREE.Color(detail.rockColor) },
|
|
48
49
|
uSandColor: { value: new THREE.Color(detail.sandColor) },
|
|
@@ -61,6 +62,7 @@ export function createTerrainDetailMaterial(detail, textures) {
|
|
|
61
62
|
uMatAo: { value: textures.ao },
|
|
62
63
|
uMatRepeat: { value: Math.max(0.01, repeat) },
|
|
63
64
|
uMatStrength: { value: strength },
|
|
65
|
+
uMatTint: { value: tint },
|
|
64
66
|
}
|
|
65
67
|
: {}),
|
|
66
68
|
};
|
|
@@ -87,13 +89,20 @@ uniform sampler2D uMatNormal;
|
|
|
87
89
|
uniform sampler2D uMatRoughness;
|
|
88
90
|
uniform sampler2D uMatAo;
|
|
89
91
|
uniform float uMatRepeat;
|
|
90
|
-
uniform float uMatStrength
|
|
92
|
+
uniform float uMatStrength;
|
|
93
|
+
uniform float uMatTint;`
|
|
91
94
|
: "";
|
|
92
95
|
const materialColorGlsl = hasTexture
|
|
93
96
|
? `
|
|
94
97
|
vec2 jgMatUv = jgWp / uMatRepeat;
|
|
95
98
|
vec3 jgMatColor = texture2D(uMatColor, jgMatUv).rgb;
|
|
96
|
-
|
|
99
|
+
// Two separable jobs. GRAIN: the map's luminance relative to its own mid-grey shades the authored
|
|
100
|
+
// colour, which is what makes ground read as a surface. TINT: how much of the map's own hue
|
|
101
|
+
// replaces that colour — at 1 the biome palette, macro sweeps, and slope rock underneath are simply
|
|
102
|
+
// painted over, and a whole region collapses to the texture's single hue.
|
|
103
|
+
float jgMatLum = dot(jgMatColor, vec3(0.2126, 0.7152, 0.0722));
|
|
104
|
+
jgCol *= mix(1.0, clamp(jgMatLum / 0.35, 0.55, 1.7), uMatStrength * (1.0 - uMatTint));
|
|
105
|
+
jgCol = mix(jgCol, jgMatColor, uMatStrength * uMatTint);`
|
|
97
106
|
: "";
|
|
98
107
|
const materialRoughnessGlsl = hasTexture
|
|
99
108
|
? `
|
|
@@ -27,6 +27,35 @@ export interface TerrainVertexColorOptions {
|
|
|
27
27
|
waterline?: THREE.ColorRepresentation;
|
|
28
28
|
waterlineHeight?: number;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Slope/noise surface shading laid over the height lerp so untextured ground reads like terrain
|
|
32
|
+
* instead of a smooth two-tone gradient. All knobs are optional with sane defaults; every field is
|
|
33
|
+
* a pure function of vertex position (+ `seed`), so the coloring is stable across rebuilds and tests.
|
|
34
|
+
*/
|
|
35
|
+
export interface TerrainSurfaceColorOptions {
|
|
36
|
+
/** 0..1 darkening + desaturation on steep faces (slope sampled from neighbor heights). Default 0.5; 0 disables. */
|
|
37
|
+
slopeStrength?: number;
|
|
38
|
+
/** 0..1 amplitude of deterministic hash mottling (brightness jitter per patch). Default 0.14; 0 disables. */
|
|
39
|
+
mottleStrength?: number;
|
|
40
|
+
/** World-space frequency of the mottling lattice (cells per meter). Default 0.12. */
|
|
41
|
+
mottleScale?: number;
|
|
42
|
+
/** Seed for the mottling hash. Same seed → identical mottling. Default 1. */
|
|
43
|
+
seed?: TerrainSeed;
|
|
44
|
+
}
|
|
45
|
+
interface ResolvedTerrainSurfaceColorOptions {
|
|
46
|
+
slopeStrength: number;
|
|
47
|
+
mottleStrength: number;
|
|
48
|
+
mottleScale: number;
|
|
49
|
+
seed: TerrainSeed;
|
|
50
|
+
}
|
|
51
|
+
/** @internal */
|
|
52
|
+
export declare const DEFAULT_TERRAIN_SURFACE_COLOR: ResolvedTerrainSurfaceColorOptions;
|
|
53
|
+
/**
|
|
54
|
+
* Layer the slope darkening and hash mottling onto an already height-lerped vertex color, in place.
|
|
55
|
+
* `slope01` is the 0..1 steepness at the vertex; `x,z` seed the deterministic mottling.
|
|
56
|
+
* @internal
|
|
57
|
+
*/
|
|
58
|
+
export declare function applyTerrainSurfaceShading(color: THREE.Color, x: number, z: number, slope01: number, surface: ResolvedTerrainSurfaceColorOptions): void;
|
|
30
59
|
/** @internal */
|
|
31
60
|
export declare function normalizeHeightBlend(height: number, minHeight: number, maxHeight: number): number;
|
|
32
61
|
/** @internal */
|
|
@@ -51,6 +80,8 @@ export interface FieldGroundOptions {
|
|
|
51
80
|
heightRange?: readonly [min: number, max: number];
|
|
52
81
|
/** Sampled per vertex when set; `colors` still supplies `waterlineHeight` and the fallback. */
|
|
53
82
|
paletteAt?: TerrainPaletteSampler;
|
|
83
|
+
/** Slope/noise surface shading over the height lerp. Omit for the default terrain look; pass `{ slopeStrength: 0, mottleStrength: 0 }` for a flat gradient. */
|
|
84
|
+
surface?: TerrainSurfaceColorOptions;
|
|
54
85
|
}
|
|
55
86
|
/**
|
|
56
87
|
* Mesh any `TerrainField` — including a `CarvableField` with craters/mounds written into it — into a
|
|
@@ -59,5 +90,11 @@ export interface FieldGroundOptions {
|
|
|
59
90
|
* @internal
|
|
60
91
|
*/
|
|
61
92
|
export declare function createFieldGroundGeometry(field: TerrainField, options?: FieldGroundOptions): THREE.BufferGeometry;
|
|
62
|
-
/**
|
|
93
|
+
/**
|
|
94
|
+
* Meshes the built-in procedural noise terrain into a vertex-coloured ground. A thin wrapper over the
|
|
95
|
+
* shared {@link buildGroundGeometry}: same height lerp, plus slope-aware shading and hash mottling
|
|
96
|
+
* seeded from `config.seed` so the plain ground reads like terrain rather than a two-tone gradient.
|
|
97
|
+
* @internal
|
|
98
|
+
*/
|
|
63
99
|
export declare function createProceduralGroundGeometry(config?: ProceduralTerrainConfig, colors?: TerrainVertexColorOptions): THREE.BufferGeometry;
|
|
100
|
+
export {};
|
|
@@ -1,6 +1,62 @@
|
|
|
1
1
|
import { noiseField } from "@jgengine/core/world/terrain";
|
|
2
2
|
import * as THREE from "three";
|
|
3
|
-
import {} from "./random.js";
|
|
3
|
+
import { hashNoise2 } from "./random.js";
|
|
4
|
+
/** @internal */
|
|
5
|
+
export const DEFAULT_TERRAIN_SURFACE_COLOR = {
|
|
6
|
+
slopeStrength: 0.5,
|
|
7
|
+
mottleStrength: 0.14,
|
|
8
|
+
mottleScale: 0.12,
|
|
9
|
+
seed: 1,
|
|
10
|
+
};
|
|
11
|
+
function resolveSurfaceColor(surface) {
|
|
12
|
+
return {
|
|
13
|
+
slopeStrength: THREE.MathUtils.clamp(surface?.slopeStrength ?? DEFAULT_TERRAIN_SURFACE_COLOR.slopeStrength, 0, 1),
|
|
14
|
+
mottleStrength: THREE.MathUtils.clamp(surface?.mottleStrength ?? DEFAULT_TERRAIN_SURFACE_COLOR.mottleStrength, 0, 1),
|
|
15
|
+
mottleScale: Math.max(0, surface?.mottleScale ?? DEFAULT_TERRAIN_SURFACE_COLOR.mottleScale),
|
|
16
|
+
seed: surface?.seed ?? DEFAULT_TERRAIN_SURFACE_COLOR.seed,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Smoothed value noise on the integer hash lattice — continuous, seamless, and a pure function of `x,z`. */
|
|
20
|
+
function groundValueNoise(x, z, scale, seed) {
|
|
21
|
+
const sx = x * scale;
|
|
22
|
+
const sz = z * scale;
|
|
23
|
+
const xi = Math.floor(sx);
|
|
24
|
+
const zi = Math.floor(sz);
|
|
25
|
+
const xf = sx - xi;
|
|
26
|
+
const zf = sz - zi;
|
|
27
|
+
const u = xf * xf * (3 - 2 * xf);
|
|
28
|
+
const w = zf * zf * (3 - 2 * zf);
|
|
29
|
+
const n00 = hashNoise2(xi, zi, seed);
|
|
30
|
+
const n10 = hashNoise2(xi + 1, zi, seed);
|
|
31
|
+
const n01 = hashNoise2(xi, zi + 1, seed);
|
|
32
|
+
const n11 = hashNoise2(xi + 1, zi + 1, seed);
|
|
33
|
+
const nx0 = n00 + (n10 - n00) * u;
|
|
34
|
+
const nx1 = n01 + (n11 - n01) * u;
|
|
35
|
+
return nx0 + (nx1 - nx0) * w;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Layer the slope darkening and hash mottling onto an already height-lerped vertex color, in place.
|
|
39
|
+
* `slope01` is the 0..1 steepness at the vertex; `x,z` seed the deterministic mottling.
|
|
40
|
+
* @internal
|
|
41
|
+
*/
|
|
42
|
+
export function applyTerrainSurfaceShading(color, x, z, slope01, surface) {
|
|
43
|
+
if (surface.mottleStrength > 0 && surface.mottleScale > 0) {
|
|
44
|
+
// Two octaves: broad patchiness plus a finer break-up. Centered on 0.5 so it only jitters brightness.
|
|
45
|
+
const broad = groundValueNoise(x, z, surface.mottleScale, surface.seed);
|
|
46
|
+
const fine = groundValueNoise(x + 41.7, z - 19.3, surface.mottleScale * 3.1, surface.seed);
|
|
47
|
+
const mottle = broad * 0.65 + fine * 0.35;
|
|
48
|
+
color.multiplyScalar(1 + (mottle - 0.5) * 2 * surface.mottleStrength);
|
|
49
|
+
}
|
|
50
|
+
if (surface.slopeStrength > 0 && slope01 > 0) {
|
|
51
|
+
// Steep faces read as exposed earth: pull toward luminance (desaturate) then darken.
|
|
52
|
+
const slopeK = THREE.MathUtils.clamp(slope01 * surface.slopeStrength, 0, 1);
|
|
53
|
+
const lum = color.r * 0.299 + color.g * 0.587 + color.b * 0.114;
|
|
54
|
+
color.r = THREE.MathUtils.lerp(color.r, lum, slopeK * 0.5);
|
|
55
|
+
color.g = THREE.MathUtils.lerp(color.g, lum, slopeK * 0.5);
|
|
56
|
+
color.b = THREE.MathUtils.lerp(color.b, lum, slopeK * 0.5);
|
|
57
|
+
color.multiplyScalar(1 - 0.4 * slopeK);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
4
60
|
/** @internal */
|
|
5
61
|
export function normalizeHeightBlend(height, minHeight, maxHeight) {
|
|
6
62
|
const range = maxHeight - minHeight;
|
|
@@ -49,11 +105,17 @@ export function createFieldGroundGeometry(field, options = {}) {
|
|
|
49
105
|
center: [cx, cz],
|
|
50
106
|
heightRange: options.heightRange,
|
|
51
107
|
paletteAt: options.paletteAt,
|
|
108
|
+
surface: options.surface,
|
|
52
109
|
});
|
|
53
110
|
}
|
|
54
111
|
function buildGroundGeometry(size, segments, sampler, opts) {
|
|
55
112
|
const colors = opts.colors;
|
|
56
113
|
const paletteAt = opts.paletteAt;
|
|
114
|
+
const surface = resolveSurfaceColor(opts.surface);
|
|
115
|
+
const shadeSlope = surface.slopeStrength > 0;
|
|
116
|
+
// Neighbor spacing for the finite-difference slope estimate — one grid cell per axis.
|
|
117
|
+
const slopeStepX = shadeSlope ? Math.max(1e-3, size.width / Math.max(1, segments.x)) : 0;
|
|
118
|
+
const slopeStepZ = shadeSlope ? Math.max(1e-3, size.depth / Math.max(1, segments.z)) : 0;
|
|
57
119
|
const vertexCountX = segments.x + 1;
|
|
58
120
|
const vertexCountZ = segments.z + 1;
|
|
59
121
|
const positions = new Float32Array(vertexCountX * vertexCountZ * 3);
|
|
@@ -91,6 +153,15 @@ function buildGroundGeometry(size, segments, sampler, opts) {
|
|
|
91
153
|
const color = low.clone().lerp(high, blend);
|
|
92
154
|
if (waterline !== null && y <= (colors.waterlineHeight ?? 0))
|
|
93
155
|
color.lerp(waterline, 0.65);
|
|
156
|
+
let slope01 = 0;
|
|
157
|
+
if (shadeSlope) {
|
|
158
|
+
// Bounded per-vertex finite difference (4 extra samples) — build-time only, no per-frame scan.
|
|
159
|
+
const dhx = (sampler(x + slopeStepX, z) - sampler(x - slopeStepX, z)) / (2 * slopeStepX);
|
|
160
|
+
const dhz = (sampler(x, z + slopeStepZ) - sampler(x, z - slopeStepZ)) / (2 * slopeStepZ);
|
|
161
|
+
const grad = Math.hypot(dhx, dhz);
|
|
162
|
+
slope01 = grad / (grad + 1);
|
|
163
|
+
}
|
|
164
|
+
applyTerrainSurfaceShading(color, x, z, slope01, surface);
|
|
94
165
|
colorValues[colorIndex] = color.r;
|
|
95
166
|
colorValues[colorIndex + 1] = color.g;
|
|
96
167
|
colorValues[colorIndex + 2] = color.b;
|
|
@@ -124,71 +195,22 @@ function buildGroundGeometry(size, segments, sampler, opts) {
|
|
|
124
195
|
geometry.computeBoundingSphere();
|
|
125
196
|
return geometry;
|
|
126
197
|
}
|
|
127
|
-
/**
|
|
198
|
+
/**
|
|
199
|
+
* Meshes the built-in procedural noise terrain into a vertex-coloured ground. A thin wrapper over the
|
|
200
|
+
* shared {@link buildGroundGeometry}: same height lerp, plus slope-aware shading and hash mottling
|
|
201
|
+
* seeded from `config.seed` so the plain ground reads like terrain rather than a two-tone gradient.
|
|
202
|
+
* @internal
|
|
203
|
+
*/
|
|
128
204
|
export function createProceduralGroundGeometry(config = {}, colors = {}) {
|
|
129
205
|
const size = resolveTerrainSize(config.size);
|
|
130
206
|
const segments = resolveTerrainSegments(config.segments);
|
|
131
207
|
const sampler = createProceduralTerrainSampler(config);
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const waterline = colors.waterline === undefined ? null : new THREE.Color(colors.waterline);
|
|
141
|
-
const minHeight = (config.baseOffset ?? 0) - (config.height ?? 1.4) * 1.2;
|
|
142
|
-
const maxHeight = (config.baseOffset ?? 0) + (config.height ?? 1.4) * 1.2;
|
|
143
|
-
let index = 0;
|
|
144
|
-
let uvIndex = 0;
|
|
145
|
-
let colorIndex = 0;
|
|
146
|
-
for (let zIndex = 0; zIndex < vertexCountZ; zIndex += 1) {
|
|
147
|
-
const v = zIndex / segments.z;
|
|
148
|
-
const z = (v - 0.5) * size.depth;
|
|
149
|
-
for (let xIndex = 0; xIndex < vertexCountX; xIndex += 1) {
|
|
150
|
-
const u = xIndex / segments.x;
|
|
151
|
-
const x = (u - 0.5) * size.width;
|
|
152
|
-
const y = sampler(x, z);
|
|
153
|
-
positions[index] = x;
|
|
154
|
-
positions[index + 1] = y;
|
|
155
|
-
positions[index + 2] = z;
|
|
156
|
-
uvs[uvIndex] = u;
|
|
157
|
-
uvs[uvIndex + 1] = v;
|
|
158
|
-
const blend = normalizeHeightBlend(y, minHeight, maxHeight);
|
|
159
|
-
const color = low.clone().lerp(high, blend);
|
|
160
|
-
if (waterline !== null && y <= (colors.waterlineHeight ?? 0))
|
|
161
|
-
color.lerp(waterline, 0.65);
|
|
162
|
-
colorValues[colorIndex] = color.r;
|
|
163
|
-
colorValues[colorIndex + 1] = color.g;
|
|
164
|
-
colorValues[colorIndex + 2] = color.b;
|
|
165
|
-
index += 3;
|
|
166
|
-
uvIndex += 2;
|
|
167
|
-
colorIndex += 3;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
let triangleIndex = 0;
|
|
171
|
-
for (let zIndex = 0; zIndex < segments.z; zIndex += 1) {
|
|
172
|
-
for (let xIndex = 0; xIndex < segments.x; xIndex += 1) {
|
|
173
|
-
const a = zIndex * vertexCountX + xIndex;
|
|
174
|
-
const b = a + 1;
|
|
175
|
-
const c = a + vertexCountX;
|
|
176
|
-
const d = c + 1;
|
|
177
|
-
indices[triangleIndex] = a;
|
|
178
|
-
indices[triangleIndex + 1] = c;
|
|
179
|
-
indices[triangleIndex + 2] = b;
|
|
180
|
-
indices[triangleIndex + 3] = b;
|
|
181
|
-
indices[triangleIndex + 4] = c;
|
|
182
|
-
indices[triangleIndex + 5] = d;
|
|
183
|
-
triangleIndex += 6;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
const geometry = new THREE.BufferGeometry();
|
|
187
|
-
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
|
|
188
|
-
geometry.setAttribute("uv", new THREE.BufferAttribute(uvs, 2));
|
|
189
|
-
geometry.setAttribute("color", new THREE.BufferAttribute(colorValues, 3));
|
|
190
|
-
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
|
|
191
|
-
geometry.computeVertexNormals();
|
|
192
|
-
geometry.computeBoundingSphere();
|
|
193
|
-
return geometry;
|
|
208
|
+
const base = config.baseOffset ?? 0;
|
|
209
|
+
const span = (config.height ?? 1.4) * 1.2;
|
|
210
|
+
return buildGroundGeometry(size, segments, sampler, {
|
|
211
|
+
colors,
|
|
212
|
+
center: [0, 0],
|
|
213
|
+
heightRange: [base - span, base + span],
|
|
214
|
+
surface: { seed: config.seed ?? DEFAULT_TERRAIN_SURFACE_COLOR.seed },
|
|
215
|
+
});
|
|
194
216
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { type MutableRefObject } from "react";
|
|
2
|
-
import { type TouchScheme, type TouchStyle } from "@jgengine/core/input/touchScheme";
|
|
2
|
+
import { type TouchJoystickVariant, 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;
|
|
6
|
+
/** Analog per-action magnitudes (0..1) from the joystick's continuous vector; `null` when the stick is released. Optional so bespoke docks that only press buttons need not care. */
|
|
7
|
+
onAnalog?(values: Readonly<Record<string, number>> | null): void;
|
|
6
8
|
}
|
|
7
9
|
/**
|
|
8
10
|
* Thumb-arc placement for primary buttons around the bottom-right corner:
|
|
@@ -29,10 +31,12 @@ export declare function TouchPlaySurface({ scheme, sink, yawRef, pitchRef, maxPi
|
|
|
29
31
|
maxPitch: number;
|
|
30
32
|
onPrimaryTap: () => void;
|
|
31
33
|
}): import("react").JSX.Element;
|
|
32
|
-
export declare function TouchControlsDock({ scheme, sink, style, scale, }: {
|
|
34
|
+
export declare function TouchControlsDock({ scheme, sink, style, scale, joystickVariant, }: {
|
|
33
35
|
scheme: TouchScheme;
|
|
34
36
|
sink: TouchCodeSink;
|
|
35
37
|
/** Player-selected skin; falls back to the scheme's game default. */
|
|
36
38
|
style?: TouchStyle;
|
|
37
39
|
scale?: number;
|
|
40
|
+
/** Player-selected joystick behavior (Settings → Controls). Default `floating`. */
|
|
41
|
+
joystickVariant?: TouchJoystickVariant;
|
|
38
42
|
}): import("react").JSX.Element;
|
|
@@ -10,7 +10,9 @@ const ACTIONS_ZONE = { id: "control:actions", kind: "control", collisionPolicy:
|
|
|
10
10
|
const UTILITY_ZONE = { id: "control:utility", kind: "control", collisionPolicy: "warn", collisionGroup: "touch-dock" };
|
|
11
11
|
const JOYSTICK_SIZE = 132;
|
|
12
12
|
const JOYSTICK_THUMB = 52;
|
|
13
|
-
|
|
13
|
+
/** Radial deflection below which the stick reads as centered; travel past it rescales to 0..1 so analog output starts from zero instead of jumping. */
|
|
14
|
+
const JOYSTICK_DEADZONE = 0.16;
|
|
15
|
+
/** Digital fallback codes fire past this per-axis deflection — kept for edge-detection consumers (`justPressed`); analog movement itself flows through `onAnalog`. */
|
|
14
16
|
const JOYSTICK_AXIS_THRESHOLD = 0.42;
|
|
15
17
|
const LOOK_TAP_TUNING = { tapMoveThresholdPx: 12, tapMaxMs: 280, longPressMs: 450 };
|
|
16
18
|
const BUTTON_SIZE = 56;
|
|
@@ -242,11 +244,60 @@ function joystickDirections(joystick, nx, ny) {
|
|
|
242
244
|
active.add(joystick.right);
|
|
243
245
|
return active;
|
|
244
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* Per-action analog magnitudes for a stick deflection: radial deadzone, then travel rescaled to
|
|
249
|
+
* 0..1 and split across the joystick's four mapped actions. This is what actually moves the
|
|
250
|
+
* player — the digital codes above only exist for edge-detection consumers.
|
|
251
|
+
*/
|
|
252
|
+
function joystickAnalogValues(joystick, nx, ny) {
|
|
253
|
+
const values = {};
|
|
254
|
+
const assign = (action, magnitude) => {
|
|
255
|
+
if (action !== null)
|
|
256
|
+
values[action] = magnitude;
|
|
257
|
+
};
|
|
258
|
+
const length = Math.hypot(nx, ny);
|
|
259
|
+
let dx = 0;
|
|
260
|
+
let dy = 0;
|
|
261
|
+
if (length >= JOYSTICK_DEADZONE) {
|
|
262
|
+
const travel = Math.min(1, (length - JOYSTICK_DEADZONE) / (1 - JOYSTICK_DEADZONE));
|
|
263
|
+
dx = (nx / length) * travel;
|
|
264
|
+
dy = (ny / length) * travel;
|
|
265
|
+
}
|
|
266
|
+
assign(joystick.up, Math.max(0, -dy));
|
|
267
|
+
assign(joystick.down, Math.max(0, dy));
|
|
268
|
+
assign(joystick.left, Math.max(0, -dx));
|
|
269
|
+
assign(joystick.right, Math.max(0, dx));
|
|
270
|
+
return values;
|
|
271
|
+
}
|
|
272
|
+
/** Shared drag state machine for both joystick variants: converts pointer travel around an origin into digital edge codes + analog magnitudes, and releases both on lift. */
|
|
273
|
+
function useJoystickDrag(joystick, sink) {
|
|
274
|
+
const activeRef = useRef(new Set());
|
|
275
|
+
const applyNormalized = (nx, ny) => {
|
|
276
|
+
const next = joystickDirections(joystick, nx, ny);
|
|
277
|
+
for (const action of activeRef.current) {
|
|
278
|
+
if (!next.has(action))
|
|
279
|
+
sink.onCodeUp(touchCode(action));
|
|
280
|
+
}
|
|
281
|
+
for (const action of next) {
|
|
282
|
+
if (!activeRef.current.has(action))
|
|
283
|
+
sink.onCodeDown(touchCode(action));
|
|
284
|
+
}
|
|
285
|
+
activeRef.current = next;
|
|
286
|
+
sink.onAnalog?.(joystickAnalogValues(joystick, nx, ny));
|
|
287
|
+
};
|
|
288
|
+
const release = () => {
|
|
289
|
+
for (const action of activeRef.current)
|
|
290
|
+
sink.onCodeUp(touchCode(action));
|
|
291
|
+
activeRef.current = new Set();
|
|
292
|
+
sink.onAnalog?.(null);
|
|
293
|
+
};
|
|
294
|
+
return { applyNormalized, release };
|
|
295
|
+
}
|
|
245
296
|
function VirtualJoystick({ joystick, sink, tokens, scale = 1, }) {
|
|
246
297
|
const baseRef = useRef(null);
|
|
247
298
|
const thumbRef = useRef(null);
|
|
248
299
|
const pointerIdRef = useRef(null);
|
|
249
|
-
const
|
|
300
|
+
const drag = useJoystickDrag(joystick, sink);
|
|
250
301
|
const size = JOYSTICK_SIZE * scale;
|
|
251
302
|
const thumbSize = JOYSTICK_THUMB * scale;
|
|
252
303
|
const applyVector = (clientX, clientY) => {
|
|
@@ -265,22 +316,11 @@ function VirtualJoystick({ joystick, sink, tokens, scale = 1, }) {
|
|
|
265
316
|
}
|
|
266
317
|
const travel = radius - thumbSize / 2;
|
|
267
318
|
thumb.style.transform = `translate(${nx * travel}px, ${ny * travel}px)`;
|
|
268
|
-
|
|
269
|
-
for (const action of activeRef.current) {
|
|
270
|
-
if (!next.has(action))
|
|
271
|
-
sink.onCodeUp(touchCode(action));
|
|
272
|
-
}
|
|
273
|
-
for (const action of next) {
|
|
274
|
-
if (!activeRef.current.has(action))
|
|
275
|
-
sink.onCodeDown(touchCode(action));
|
|
276
|
-
}
|
|
277
|
-
activeRef.current = next;
|
|
319
|
+
drag.applyNormalized(nx, ny);
|
|
278
320
|
};
|
|
279
321
|
const releaseAll = () => {
|
|
280
322
|
pointerIdRef.current = null;
|
|
281
|
-
|
|
282
|
-
sink.onCodeUp(touchCode(action));
|
|
283
|
-
activeRef.current = new Set();
|
|
323
|
+
drag.release();
|
|
284
324
|
if (thumbRef.current !== null)
|
|
285
325
|
thumbRef.current.style.transform = "translate(0px, 0px)";
|
|
286
326
|
};
|
|
@@ -302,6 +342,85 @@ function VirtualJoystick({ joystick, sink, tokens, scale = 1, }) {
|
|
|
302
342
|
releaseAll();
|
|
303
343
|
}, onPointerCancel: () => releaseAll(), children: _jsx("div", { ref: thumbRef, className: "rounded-full", style: { width: thumbSize, height: thumbSize, ...tokens.joyThumb } }) }));
|
|
304
344
|
}
|
|
345
|
+
/** Capture-zone footprint of the floating joystick — generous around the dock corner, but bounded so it never blankets HUD elements further up the screen (the zone eats pointer events). */
|
|
346
|
+
const FLOATING_ZONE_WIDTH = "min(42vw, 300px)";
|
|
347
|
+
const FLOATING_ZONE_HEIGHT = "min(40vh, 260px)";
|
|
348
|
+
/**
|
|
349
|
+
* The floating joystick variant: its dock corner is an invisible capture zone with a faint
|
|
350
|
+
* resting ring for discoverability; the stick spawns centered under wherever the thumb lands and
|
|
351
|
+
* follows the same deadzone/analog math as the fixed stick. The modern mobile-shooter default —
|
|
352
|
+
* the hand never has to find a fixed target mid-fight.
|
|
353
|
+
*/
|
|
354
|
+
function FloatingJoystick({ joystick, sink, tokens, scale = 1, }) {
|
|
355
|
+
const pointerIdRef = useRef(null);
|
|
356
|
+
const originRef = useRef(null);
|
|
357
|
+
const baseRef = useRef(null);
|
|
358
|
+
const thumbRef = useRef(null);
|
|
359
|
+
const [engaged, setEngaged] = useState(false);
|
|
360
|
+
const drag = useJoystickDrag(joystick, sink);
|
|
361
|
+
const size = JOYSTICK_SIZE * scale;
|
|
362
|
+
const thumbSize = JOYSTICK_THUMB * scale;
|
|
363
|
+
const radius = size / 2;
|
|
364
|
+
const applyVector = (clientX, clientY) => {
|
|
365
|
+
const origin = originRef.current;
|
|
366
|
+
const thumb = thumbRef.current;
|
|
367
|
+
if (origin === null || thumb === null)
|
|
368
|
+
return;
|
|
369
|
+
let nx = (clientX - origin.x) / radius;
|
|
370
|
+
let ny = (clientY - origin.y) / radius;
|
|
371
|
+
const length = Math.hypot(nx, ny);
|
|
372
|
+
if (length > 1) {
|
|
373
|
+
nx /= length;
|
|
374
|
+
ny /= length;
|
|
375
|
+
}
|
|
376
|
+
const travel = radius - thumbSize / 2;
|
|
377
|
+
thumb.style.transform = `translate(${nx * travel}px, ${ny * travel}px)`;
|
|
378
|
+
drag.applyNormalized(nx, ny);
|
|
379
|
+
};
|
|
380
|
+
const releaseAll = () => {
|
|
381
|
+
pointerIdRef.current = null;
|
|
382
|
+
originRef.current = null;
|
|
383
|
+
setEngaged(false);
|
|
384
|
+
drag.release();
|
|
385
|
+
if (thumbRef.current !== null)
|
|
386
|
+
thumbRef.current.style.transform = "translate(0px, 0px)";
|
|
387
|
+
};
|
|
388
|
+
return (_jsxs("div", { className: "pointer-events-auto relative touch-none select-none", style: { width: FLOATING_ZONE_WIDTH, height: FLOATING_ZONE_HEIGHT }, onPointerDown: (event) => {
|
|
389
|
+
if (pointerIdRef.current !== null)
|
|
390
|
+
return;
|
|
391
|
+
pointerIdRef.current = event.pointerId;
|
|
392
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
393
|
+
const zone = event.currentTarget.getBoundingClientRect();
|
|
394
|
+
// Clamp the spawn point so the whole base stays inside the zone.
|
|
395
|
+
const x = Math.min(Math.max(event.clientX, zone.left + radius), zone.right - radius);
|
|
396
|
+
const y = Math.min(Math.max(event.clientY, zone.top + radius), zone.bottom - radius);
|
|
397
|
+
originRef.current = { x, y };
|
|
398
|
+
setEngaged(true);
|
|
399
|
+
const base = baseRef.current;
|
|
400
|
+
if (base !== null) {
|
|
401
|
+
base.style.left = `${x - zone.left - radius}px`;
|
|
402
|
+
base.style.top = `${y - zone.top - radius}px`;
|
|
403
|
+
}
|
|
404
|
+
applyVector(event.clientX, event.clientY);
|
|
405
|
+
}, onPointerMove: (event) => {
|
|
406
|
+
if (event.pointerId !== pointerIdRef.current)
|
|
407
|
+
return;
|
|
408
|
+
event.stopPropagation();
|
|
409
|
+
applyVector(event.clientX, event.clientY);
|
|
410
|
+
}, onPointerUp: (event) => {
|
|
411
|
+
if (event.pointerId !== pointerIdRef.current)
|
|
412
|
+
return;
|
|
413
|
+
event.stopPropagation();
|
|
414
|
+
releaseAll();
|
|
415
|
+
}, onPointerCancel: () => releaseAll(), children: [_jsx("div", { className: "pointer-events-none absolute rounded-full transition-opacity", style: {
|
|
416
|
+
width: size,
|
|
417
|
+
height: size,
|
|
418
|
+
left: 0,
|
|
419
|
+
bottom: 0,
|
|
420
|
+
opacity: engaged ? 0 : 0.4,
|
|
421
|
+
...tokens.joyBase,
|
|
422
|
+
} }), _jsx("div", { ref: baseRef, className: "pointer-events-none absolute flex items-center justify-center rounded-full", style: { width: size, height: size, visibility: engaged ? "visible" : "hidden", ...tokens.joyBase }, children: _jsx("div", { ref: thumbRef, className: "rounded-full", style: { width: thumbSize, height: thumbSize, ...tokens.joyThumb } }) })] }));
|
|
423
|
+
}
|
|
305
424
|
function touchButtonIcon(button) {
|
|
306
425
|
if (button.icon === false)
|
|
307
426
|
return null;
|
|
@@ -446,7 +565,7 @@ function bucketByAnchor(buttons) {
|
|
|
446
565
|
}
|
|
447
566
|
return map;
|
|
448
567
|
}
|
|
449
|
-
export function TouchControlsDock({ scheme, sink, style, scale = 1, }) {
|
|
568
|
+
export function TouchControlsDock({ scheme, sink, style, scale = 1, joystickVariant = "floating", }) {
|
|
450
569
|
const tokens = STYLE_TOKENS[style ?? scheme.style];
|
|
451
570
|
const layout = scheme.layout;
|
|
452
571
|
const docked = scheme.buttons.filter((button) => button.anchor === null);
|
|
@@ -461,7 +580,7 @@ export function TouchControlsDock({ scheme, sink, style, scale = 1, }) {
|
|
|
461
580
|
useRegisterLayoutRegion(UTILITY_ZONE, utilityRef, utility.length > 0);
|
|
462
581
|
const actionsColumn = layout.actions === "left" || layout.actions === "right";
|
|
463
582
|
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)))] }));
|
|
583
|
+
return (_jsxs(ResolvedLayout, { children: [scheme.joystick !== null ? (_jsx(AnchoredSlot, { anchor: layout.movement, slotRef: joystickRef, children: joystickVariant === "floating" ? (_jsx(FloatingJoystick, { joystick: scheme.joystick, sink: sink, tokens: tokens, scale: scale })) : (_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
584
|
}
|
|
466
585
|
function ResolvedLayout({ children }) {
|
|
467
586
|
return _jsx("div", { className: "pointer-events-none absolute inset-0 z-40", children: children });
|