@jgengine/shell 0.10.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 +13 -2
- 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 +0 -12
- package/dist/GamePlayerShell.js +88 -1043
- package/dist/GameUiPreview.js +0 -2
- package/dist/audio/audioEngine.js +30 -8
- package/dist/audio/audioWire.d.ts +22 -0
- package/dist/audio/audioWire.js +19 -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 +9 -2
- package/dist/camera/GameFirstPersonCamera.js +8 -3
- package/dist/camera/GameInspectionCamera.d.ts +1 -1
- package/dist/camera/GameInspectionCamera.js +2 -2
- package/dist/camera/GameOrbitCamera.js +24 -9
- package/dist/camera/cameraBlendMath.d.ts +3 -0
- package/dist/camera/cameraBlendMath.js +3 -0
- package/dist/camera/fovPreference.d.ts +6 -1
- package/dist/camera/fovPreference.js +6 -1
- package/dist/camera/index.d.ts +4 -7
- package/dist/camera/index.js +4 -7
- package/dist/camera/inspectionCameraMath.d.ts +7 -2
- package/dist/camera/inspectionCameraMath.js +7 -2
- package/dist/camera/orbitCameraMath.d.ts +36 -6
- package/dist/camera/orbitCameraMath.js +56 -6
- package/dist/camera/rigMath.d.ts +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 +3 -3
- package/dist/commandSink.d.ts +8 -3
- package/dist/commandSink.js +8 -3
- package/dist/defineGame.js +25 -6
- package/dist/devtools/ColPanel.d.ts +1 -0
- package/dist/devtools/ColPanel.js +14 -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 +8 -2
- package/dist/devtools/collisionDebugMath.js +24 -5
- package/dist/devtools/devtoolsOverrides.d.ts +6 -0
- package/dist/devtools/devtoolsOverrides.js +48 -0
- package/dist/devtools/panelAtoms.d.ts +9 -0
- package/dist/devtools/panelAtoms.js +10 -0
- package/dist/devtools/perfDiagnose.d.ts +2 -0
- package/dist/devtools/perfDiagnose.js +43 -0
- package/dist/diagnostics/RuntimeDiagnostics.d.ts +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 +23 -7
- package/dist/environment/Daylight.js +106 -13
- package/dist/environment/EnvironmentScene.js +32 -6
- 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 +16 -4
- package/dist/inputSink.js +57 -7
- package/dist/materialOverride.d.ts +10 -0
- package/dist/materialOverride.js +15 -3
- package/dist/multiplayer.js +8 -2
- package/dist/postfx/PostProcessing.js +47 -9
- package/dist/postfx/gradeShader.js +9 -1
- 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/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 +19 -2
- package/dist/terrain/terrainDetailMaterial.js +72 -5
- package/dist/terrain/terrainMath.d.ts +8 -1
- package/dist/terrain/terrainMath.js +8 -1
- package/dist/touch/TouchControlsOverlay.d.ts +9 -5
- package/dist/touch/TouchControlsOverlay.js +201 -22
- package/dist/useShellMultiplayerSync.d.ts +9 -0
- package/dist/useShellMultiplayerSync.js +98 -0
- package/dist/visibility/CullingProvider.js +1 -1
- package/dist/water/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 +49 -2
- package/dist/world/WorldHud.js +87 -2
- 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 +25 -0
- package/dist/world/worldBarSamples.js +38 -0
- package/dist/worldSky.d.ts +5 -0
- package/dist/worldSky.js +11 -0
- package/dist/worldSync.d.ts +2 -1
- package/dist/worldSync.js +2 -1
- 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 -2761
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useMemo, useRef } from "react";
|
|
3
3
|
import { useFrame } from "@react-three/fiber";
|
|
4
|
+
import { useTexture } from "@react-three/drei";
|
|
5
|
+
import * as THREE from "three";
|
|
4
6
|
import { useGameContext } from "@jgengine/react/provider";
|
|
5
7
|
import { resolveBuildingPalette } from "@jgengine/core/world/buildings";
|
|
6
8
|
import { resolveStructureBuildings } from "@jgengine/core/world/environmentSummary";
|
|
@@ -15,6 +17,26 @@ import { Ocean } from "../water/Ocean.js";
|
|
|
15
17
|
import { RainField } from "../weather/RainField.js";
|
|
16
18
|
import { SnowField } from "../weather/SnowField.js";
|
|
17
19
|
import { WeatherUniformProvider } from "../weather/weatherUniforms.js";
|
|
20
|
+
function TexturedTerrainGround({ detail, layout, }) {
|
|
21
|
+
const maps = detail.material.maps;
|
|
22
|
+
const textures = useTexture({
|
|
23
|
+
color: maps.color,
|
|
24
|
+
normal: maps.normal,
|
|
25
|
+
roughness: maps.roughness,
|
|
26
|
+
ao: maps.ao,
|
|
27
|
+
});
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
textures.color.colorSpace = THREE.SRGBColorSpace;
|
|
30
|
+
for (const texture of Object.values(textures)) {
|
|
31
|
+
texture.wrapS = THREE.RepeatWrapping;
|
|
32
|
+
texture.wrapT = THREE.RepeatWrapping;
|
|
33
|
+
texture.needsUpdate = true;
|
|
34
|
+
}
|
|
35
|
+
}, [textures]);
|
|
36
|
+
const material = useMemo(() => createTerrainDetailMaterial(detail, textures).material, [detail, textures]);
|
|
37
|
+
useEffect(() => () => material.dispose(), [material]);
|
|
38
|
+
return (_jsx(CarvedTerrain, { field: layout.field, size: layout.size, segments: layout.segments, colors: layout.colors, heightRange: layout.heightRange, paletteAt: layout.paletteAt, center: layout.center, roughness: 0.94, surfaceMaterial: material }));
|
|
39
|
+
}
|
|
18
40
|
function TerrainGround({ terrain, field, center, }) {
|
|
19
41
|
const palette = useMemo(() => resolveTerrainPalette(terrain), [terrain]);
|
|
20
42
|
const paletteAt = useMemo(() => {
|
|
@@ -38,11 +60,15 @@ function TerrainGround({ terrain, field, center, }) {
|
|
|
38
60
|
const swing = terrain.height * 1.2;
|
|
39
61
|
return [base - swing, base + swing];
|
|
40
62
|
}, [terrain.baseHeight, terrain.height]);
|
|
41
|
-
const
|
|
63
|
+
const resolvedDetail = useMemo(() => (terrain.detail === undefined ? undefined : resolveTerrainDetail(terrain.detail, terrain.waterLevel)), [terrain.detail, terrain.waterLevel]);
|
|
64
|
+
const proceduralMaterial = useMemo(() => resolvedDetail === undefined || resolvedDetail.material !== undefined
|
|
42
65
|
? undefined
|
|
43
|
-
: createTerrainDetailMaterial(
|
|
44
|
-
useEffect(() => () =>
|
|
45
|
-
|
|
66
|
+
: createTerrainDetailMaterial(resolvedDetail).material, [resolvedDetail]);
|
|
67
|
+
useEffect(() => () => proceduralMaterial?.dispose(), [proceduralMaterial]);
|
|
68
|
+
if (resolvedDetail?.material !== undefined) {
|
|
69
|
+
return (_jsx(TexturedTerrainGround, { detail: resolvedDetail, layout: { field, size, segments: terrain.segments, colors, heightRange, paletteAt, center } }));
|
|
70
|
+
}
|
|
71
|
+
return (_jsx(CarvedTerrain, { field: field, size: size, segments: terrain.segments, colors: colors, heightRange: heightRange, paletteAt: paletteAt, center: center, roughness: 0.94, surfaceMaterial: proceduralMaterial }));
|
|
46
72
|
}
|
|
47
73
|
function areaCenter(area) {
|
|
48
74
|
return area.position ?? [0, 0];
|
|
@@ -64,9 +90,9 @@ function weatherVolume(area) {
|
|
|
64
90
|
function Precipitation({ weather }) {
|
|
65
91
|
const [cx, cz] = areaCenter(weather.area);
|
|
66
92
|
if (weather.kind === "rain") {
|
|
67
|
-
return (_jsx("group", { position: [cx, 0, cz], children: _jsx(RainField, { density: weather.density, speed: weather.speed, length: weather.dropLength, color: weather.color, wind: [weather.wind[0], 0, weather.wind[1]], volume: weatherVolume(weather.area) }) }));
|
|
93
|
+
return (_jsx("group", { position: [cx, 0, cz], children: _jsx(RainField, { density: weather.density, speed: weather.speed, length: weather.dropLength, width: weather.width, opacity: weather.opacity, color: weather.color, wind: [weather.wind[0], 0, weather.wind[1]], volume: weatherVolume(weather.area) }) }));
|
|
68
94
|
}
|
|
69
|
-
return (_jsx("group", { position: [cx, 0, cz], children: _jsx(SnowField, { density: weather.density, speed: weather.speed, size: weather.flakeSize, sway: weather.drift, color: weather.color, wind: [weather.wind[0], 0, weather.wind[1]], volume: weatherVolume(weather.area) }) }));
|
|
95
|
+
return (_jsx("group", { position: [cx, 0, cz], children: _jsx(SnowField, { density: weather.density, speed: weather.speed, size: weather.flakeSize, sway: weather.drift, opacity: weather.opacity, color: weather.color, wind: [weather.wind[0], 0, weather.wind[1]], volume: weatherVolume(weather.area) }) }));
|
|
70
96
|
}
|
|
71
97
|
function Weather({ weather }) {
|
|
72
98
|
return (_jsx(WeatherUniformProvider, { children: weather.map((entry, index) => (_jsx(Precipitation, { weather: entry, index: index }, `${entry.kind}-${index}`))) }));
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { VolumetricCloudsRules } from "@jgengine/core/world/volumetricClouds";
|
|
2
|
+
/** Props for {@link VolumetricClouds} — fully-resolved cloud rules plus the shared sun direction. */
|
|
3
|
+
export interface VolumetricCloudsProps {
|
|
4
|
+
rules: VolumetricCloudsRules;
|
|
5
|
+
/** Direction toward the sun (same vector `SkyDome`/`Daylight` use for the sun disc). */
|
|
6
|
+
sunDirection: readonly [number, number, number];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* A raymarched volumetric cloud slab: a large horizontal box straddling `[height, height+thickness]`,
|
|
10
|
+
* following the camera in XZ (like `SkyDome`), whose fragment shader raymarches 3D FBM noise inside
|
|
11
|
+
* the slab and accumulates density-weighted color with a sun-facing forward-scatter term. Mounted
|
|
12
|
+
* from `Daylight`/`SkyDaylight`/`TimeOfDayDaylight` whenever a sky descriptor carries
|
|
13
|
+
* `volumetricClouds`; off by default (the component itself is simply not rendered). A game never
|
|
14
|
+
* mounts this directly — author clouds via `sky({ volumetricClouds })`.
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export declare function VolumetricClouds({ rules, sunDirection }: VolumetricCloudsProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useFrame } from "@react-three/fiber";
|
|
3
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
4
|
+
import * as THREE from "three";
|
|
5
|
+
/**
|
|
6
|
+
* Camera-centered proxy dome radius. Must sit inside the default camera far plane
|
|
7
|
+
* (`CAMERA_FRUSTUM_DEFAULTS.far` = 300) — a bigger sphere is clipped whole and renders nothing. The
|
|
8
|
+
* raymarch works in world space against the absolute cloud slab, so this size only has to cover the
|
|
9
|
+
* screen (camera is always at the dome center), never bound the clouds.
|
|
10
|
+
*/
|
|
11
|
+
const INSIDE_FRUSTUM_DOME_RADIUS = 240;
|
|
12
|
+
function hashSeed(text) {
|
|
13
|
+
let h = 2166136261 >>> 0;
|
|
14
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
15
|
+
h ^= text.charCodeAt(i);
|
|
16
|
+
h = Math.imul(h, 16777619);
|
|
17
|
+
}
|
|
18
|
+
return (h >>> 0) % 100000;
|
|
19
|
+
}
|
|
20
|
+
function normalizeVec3(v) {
|
|
21
|
+
const out = new THREE.Vector3(v[0], v[1], v[2]);
|
|
22
|
+
return out.lengthSq() === 0 ? new THREE.Vector3(0, 1, 0) : out.normalize();
|
|
23
|
+
}
|
|
24
|
+
const CLOUD_VERTEX_SHADER = /* glsl */ `
|
|
25
|
+
varying vec3 vJgCloudWorldPos;
|
|
26
|
+
void main() {
|
|
27
|
+
vJgCloudWorldPos = (modelMatrix * vec4(position, 1.0)).xyz;
|
|
28
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
29
|
+
}
|
|
30
|
+
`;
|
|
31
|
+
const CLOUD_FRAGMENT_SHADER = /* glsl */ `
|
|
32
|
+
uniform vec3 uCameraPos;
|
|
33
|
+
uniform vec3 uColor;
|
|
34
|
+
uniform vec3 uSunColor;
|
|
35
|
+
uniform vec3 uSunDirection;
|
|
36
|
+
uniform float uSunScatter;
|
|
37
|
+
uniform float uCoverage;
|
|
38
|
+
uniform float uDensity;
|
|
39
|
+
uniform float uBaseHeight;
|
|
40
|
+
uniform float uThickness;
|
|
41
|
+
uniform float uScale;
|
|
42
|
+
uniform float uSpeed;
|
|
43
|
+
uniform float uTime;
|
|
44
|
+
uniform float uSeedOffset;
|
|
45
|
+
varying vec3 vJgCloudWorldPos;
|
|
46
|
+
|
|
47
|
+
float jgCloudHash13(vec3 p){
|
|
48
|
+
p = fract(p * 0.1031 + uSeedOffset * 0.0007);
|
|
49
|
+
p += dot(p, p.yzx + 33.33);
|
|
50
|
+
return fract((p.x + p.y) * p.z);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
float jgCloudNoise(vec3 p){
|
|
54
|
+
vec3 i = floor(p);
|
|
55
|
+
vec3 f = fract(p);
|
|
56
|
+
f = f * f * (3.0 - 2.0 * f);
|
|
57
|
+
float n000 = jgCloudHash13(i + vec3(0.0, 0.0, 0.0));
|
|
58
|
+
float n100 = jgCloudHash13(i + vec3(1.0, 0.0, 0.0));
|
|
59
|
+
float n010 = jgCloudHash13(i + vec3(0.0, 1.0, 0.0));
|
|
60
|
+
float n110 = jgCloudHash13(i + vec3(1.0, 1.0, 0.0));
|
|
61
|
+
float n001 = jgCloudHash13(i + vec3(0.0, 0.0, 1.0));
|
|
62
|
+
float n101 = jgCloudHash13(i + vec3(1.0, 0.0, 1.0));
|
|
63
|
+
float n011 = jgCloudHash13(i + vec3(0.0, 1.0, 1.0));
|
|
64
|
+
float n111 = jgCloudHash13(i + vec3(1.0, 1.0, 1.0));
|
|
65
|
+
return mix(
|
|
66
|
+
mix(mix(n000, n100, f.x), mix(n010, n110, f.x), f.y),
|
|
67
|
+
mix(mix(n001, n101, f.x), mix(n011, n111, f.x), f.y),
|
|
68
|
+
f.z
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
float jgCloudFbm(vec3 p){
|
|
73
|
+
float v = 0.0;
|
|
74
|
+
float a = 0.5;
|
|
75
|
+
for (int i = 0; i < 5; i++) {
|
|
76
|
+
v += a * jgCloudNoise(p);
|
|
77
|
+
p = p * 2.03 + vec3(11.3, 7.1, 5.7);
|
|
78
|
+
a *= 0.5;
|
|
79
|
+
}
|
|
80
|
+
return v;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Density of the cloud slab at a world point: a 3D-FBM puff field carved by a coverage threshold and
|
|
84
|
+
// shaped by a cumulus vertical profile (rounded base, eroded anvil top). 0 outside the slab.
|
|
85
|
+
float jgCloudDensity(vec3 pos){
|
|
86
|
+
float heightFrac = (pos.y - uBaseHeight) / uThickness;
|
|
87
|
+
if (heightFrac < 0.0 || heightFrac > 1.0) return 0.0;
|
|
88
|
+
float profile = smoothstep(0.0, 0.2, heightFrac) * smoothstep(1.0, 0.5, heightFrac);
|
|
89
|
+
vec3 wind = vec3(uTime * uSpeed, 0.0, uTime * uSpeed * 0.6);
|
|
90
|
+
vec3 sp = (pos + wind) / uScale;
|
|
91
|
+
float shape = jgCloudFbm(sp);
|
|
92
|
+
float threshold = mix(0.62, 0.2, uCoverage);
|
|
93
|
+
float d = smoothstep(threshold, threshold + 0.3, shape * profile + profile * 0.12);
|
|
94
|
+
return d * profile * uDensity;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// March a few steps toward the sun and turn the accumulated density into transmittance (Beer's law) —
|
|
98
|
+
// the self-shadowing that gives the clouds volume instead of a flat wash.
|
|
99
|
+
float jgCloudLight(vec3 pos){
|
|
100
|
+
vec3 ldir = normalize(uSunDirection);
|
|
101
|
+
float stepL = uThickness * 0.2;
|
|
102
|
+
float dens = 0.0;
|
|
103
|
+
vec3 p = pos;
|
|
104
|
+
for (int i = 0; i < 4; i++) {
|
|
105
|
+
p += ldir * stepL;
|
|
106
|
+
dens += jgCloudDensity(p);
|
|
107
|
+
}
|
|
108
|
+
return exp(-dens * stepL * 1.6);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
void main() {
|
|
112
|
+
vec3 rayOrigin = uCameraPos;
|
|
113
|
+
vec3 rayDir = normalize(vJgCloudWorldPos - uCameraPos);
|
|
114
|
+
if (rayDir.y < 0.015) discard;
|
|
115
|
+
|
|
116
|
+
float tBottom = (uBaseHeight - rayOrigin.y) / rayDir.y;
|
|
117
|
+
float tTop = (uBaseHeight + uThickness - rayOrigin.y) / rayDir.y;
|
|
118
|
+
float tNear = max(min(tBottom, tTop), 0.0);
|
|
119
|
+
float tFar = max(tBottom, tTop);
|
|
120
|
+
if (tFar <= tNear) discard;
|
|
121
|
+
tFar = min(tFar, tNear + uThickness * 6.0);
|
|
122
|
+
|
|
123
|
+
const int STEPS = 32;
|
|
124
|
+
float stepLen = (tFar - tNear) / float(STEPS);
|
|
125
|
+
float t = tNear + stepLen * 0.5;
|
|
126
|
+
float transmittance = 1.0;
|
|
127
|
+
vec3 color = vec3(0.0);
|
|
128
|
+
float sunDot = max(dot(rayDir, normalize(uSunDirection)), 0.0);
|
|
129
|
+
float silver = pow(sunDot, 5.0) * uSunScatter;
|
|
130
|
+
|
|
131
|
+
for (int i = 0; i < STEPS; i++) {
|
|
132
|
+
vec3 pos = rayOrigin + rayDir * t;
|
|
133
|
+
float dens = jgCloudDensity(pos);
|
|
134
|
+
if (dens > 0.002) {
|
|
135
|
+
float light = jgCloudLight(pos);
|
|
136
|
+
float powder = 1.0 - exp(-dens * 3.0);
|
|
137
|
+
vec3 lit = mix(uColor, uSunColor, light) * (0.55 + 0.45 * powder) + uSunColor * silver * 0.5;
|
|
138
|
+
float dt = dens * stepLen * 1.3;
|
|
139
|
+
float a = 1.0 - exp(-dt);
|
|
140
|
+
color += transmittance * a * lit;
|
|
141
|
+
transmittance *= 1.0 - a;
|
|
142
|
+
if (transmittance < 0.02) break;
|
|
143
|
+
}
|
|
144
|
+
t += stepLen;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
float alpha = (1.0 - transmittance) * smoothstep(0.015, 0.22, rayDir.y);
|
|
148
|
+
if (alpha < 0.01) discard;
|
|
149
|
+
gl_FragColor = vec4(color, alpha);
|
|
150
|
+
}
|
|
151
|
+
`;
|
|
152
|
+
/**
|
|
153
|
+
* A raymarched volumetric cloud slab: a large horizontal box straddling `[height, height+thickness]`,
|
|
154
|
+
* following the camera in XZ (like `SkyDome`), whose fragment shader raymarches 3D FBM noise inside
|
|
155
|
+
* the slab and accumulates density-weighted color with a sun-facing forward-scatter term. Mounted
|
|
156
|
+
* from `Daylight`/`SkyDaylight`/`TimeOfDayDaylight` whenever a sky descriptor carries
|
|
157
|
+
* `volumetricClouds`; off by default (the component itself is simply not rendered). A game never
|
|
158
|
+
* mounts this directly — author clouds via `sky({ volumetricClouds })`.
|
|
159
|
+
* @internal
|
|
160
|
+
*/
|
|
161
|
+
export function VolumetricClouds({ rules, sunDirection }) {
|
|
162
|
+
const meshRef = useRef(null);
|
|
163
|
+
const timeRef = useRef(0);
|
|
164
|
+
const material = useMemo(() => {
|
|
165
|
+
return new THREE.ShaderMaterial({
|
|
166
|
+
uniforms: {
|
|
167
|
+
uCameraPos: { value: new THREE.Vector3() },
|
|
168
|
+
uColor: { value: new THREE.Color(rules.color) },
|
|
169
|
+
uSunColor: { value: new THREE.Color(rules.sunColor) },
|
|
170
|
+
uSunDirection: { value: normalizeVec3(sunDirection) },
|
|
171
|
+
uSunScatter: { value: rules.sunScatter },
|
|
172
|
+
uCoverage: { value: rules.coverage },
|
|
173
|
+
uDensity: { value: rules.density },
|
|
174
|
+
uBaseHeight: { value: rules.height },
|
|
175
|
+
uThickness: { value: rules.thickness },
|
|
176
|
+
uScale: { value: rules.scale },
|
|
177
|
+
uSpeed: { value: rules.speed },
|
|
178
|
+
uTime: { value: 0 },
|
|
179
|
+
uSeedOffset: { value: hashSeed(rules.seed) },
|
|
180
|
+
},
|
|
181
|
+
vertexShader: CLOUD_VERTEX_SHADER,
|
|
182
|
+
fragmentShader: CLOUD_FRAGMENT_SHADER,
|
|
183
|
+
transparent: true,
|
|
184
|
+
depthWrite: false,
|
|
185
|
+
side: THREE.BackSide,
|
|
186
|
+
fog: false,
|
|
187
|
+
});
|
|
188
|
+
}, [rules, sunDirection]);
|
|
189
|
+
useEffect(() => () => material.dispose(), [material]);
|
|
190
|
+
useFrame((state, delta) => {
|
|
191
|
+
timeRef.current += delta;
|
|
192
|
+
const mesh = meshRef.current;
|
|
193
|
+
if (mesh !== null) {
|
|
194
|
+
mesh.position.x = state.camera.position.x;
|
|
195
|
+
mesh.position.z = state.camera.position.z;
|
|
196
|
+
}
|
|
197
|
+
const uniforms = material.uniforms;
|
|
198
|
+
uniforms.uCameraPos.value.copy(state.camera.position);
|
|
199
|
+
uniforms.uTime.value = timeRef.current;
|
|
200
|
+
});
|
|
201
|
+
return (_jsx("mesh", { ref: meshRef, material: material, renderOrder: -0.5, frustumCulled: false, children: _jsx("sphereGeometry", { args: [INSIDE_FRUSTUM_DOME_RADIUS, 32, 16] }) }));
|
|
202
|
+
}
|
|
@@ -17,10 +17,12 @@ export declare const DEFAULT_DAY_AMBIENT_INTENSITY = 0.6;
|
|
|
17
17
|
export declare const DEFAULT_DAY_SKY_TOP = "#3fa4f2";
|
|
18
18
|
export declare const DEFAULT_DAY_SKY_BOTTOM = "#e3f4ff";
|
|
19
19
|
export declare const SKY_PRESET_DAY_FRACTION: Record<"day" | "dusk" | "night", number>;
|
|
20
|
+
/** @internal */
|
|
20
21
|
export declare function lerpHexColor(a: string, b: string, t: number): string;
|
|
21
22
|
/**
|
|
22
23
|
* Samples the daylight cycle at a point in the day (0 = midnight, 0.25 = dawn, 0.5 = noon, 0.75 = dusk),
|
|
23
24
|
* lerping sun position/intensity, ambient intensity, and sky colors through a dawn/day/dusk/night
|
|
24
25
|
* keyframe table. `config` overrides the noon (peak-day) colors and intensities only.
|
|
25
|
-
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
26
28
|
export declare function daylightStateAt(dayFraction: number, config?: DaylightCycleConfig): DaylightState;
|
|
@@ -38,6 +38,7 @@ function clampByte(value) {
|
|
|
38
38
|
function rgbToHex(rgb) {
|
|
39
39
|
return `#${rgb.map((channel) => clampByte(channel).toString(16).padStart(2, "0")).join("")}`;
|
|
40
40
|
}
|
|
41
|
+
/** @internal */
|
|
41
42
|
export function lerpHexColor(a, b, t) {
|
|
42
43
|
const [ar, ag, ab] = hexToRgb(a);
|
|
43
44
|
const [br, bg, bb] = hexToRgb(b);
|
|
@@ -95,7 +96,8 @@ function segmentAt(wrapped, keyframes) {
|
|
|
95
96
|
* Samples the daylight cycle at a point in the day (0 = midnight, 0.25 = dawn, 0.5 = noon, 0.75 = dusk),
|
|
96
97
|
* lerping sun position/intensity, ambient intensity, and sky colors through a dawn/day/dusk/night
|
|
97
98
|
* keyframe table. `config` overrides the noon (peak-day) colors and intensities only.
|
|
98
|
-
|
|
99
|
+
* @internal
|
|
100
|
+
*/
|
|
99
101
|
export function daylightStateAt(dayFraction, config = {}) {
|
|
100
102
|
const wrapped = wrapFraction(dayFraction);
|
|
101
103
|
const keyframes = buildKeyframes(config);
|
|
@@ -8,6 +8,9 @@ export type PadShape = {
|
|
|
8
8
|
width: number;
|
|
9
9
|
depth: number;
|
|
10
10
|
};
|
|
11
|
+
/** @internal */
|
|
11
12
|
export declare function resolvePadShape(size: PadSize): PadShape;
|
|
13
|
+
/** @internal */
|
|
12
14
|
export declare function resolvePadSurfaceY(groundHeight: number, pad: Pick<PadEnvironmentDescriptor, "height" | "elevation">): number;
|
|
15
|
+
/** @internal */
|
|
13
16
|
export declare function resolvePadMeshY(groundHeight: number, pad: Pick<PadEnvironmentDescriptor, "height" | "elevation">): number;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
export const PAD_THICKNESS = 0.1;
|
|
2
|
+
/** @internal */
|
|
2
3
|
export function resolvePadShape(size) {
|
|
3
4
|
return "radius" in size ? { circular: true, radius: size.radius } : { circular: false, width: size[0], depth: size[1] };
|
|
4
5
|
}
|
|
6
|
+
/** @internal */
|
|
5
7
|
export function resolvePadSurfaceY(groundHeight, pad) {
|
|
6
8
|
return pad.elevation ?? groundHeight + pad.height;
|
|
7
9
|
}
|
|
10
|
+
/** @internal */
|
|
8
11
|
export function resolvePadMeshY(groundHeight, pad) {
|
|
9
12
|
return resolvePadSurfaceY(groundHeight, pad) - PAD_THICKNESS / 2;
|
|
10
13
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { Daylight, SkyDaylight, SkyDome, TimeOfDayDaylight, type DaylightProps, type SkyDaylightProps, type SkyDomeProps, type TimeOfDayDaylightProps, } from "./Daylight.js";
|
|
2
2
|
export { EnvironmentScene, type EnvironmentSceneProps } from "./EnvironmentScene.js";
|
|
3
|
+
export { VolumetricClouds, type VolumetricCloudsProps } from "./VolumetricClouds.js";
|
|
3
4
|
export { daylightStateAt, lerpHexColor, SKY_PRESET_DAY_FRACTION, type DaylightCycleConfig, type DaylightState, } from "./daylightCycle.js";
|
|
4
5
|
export { resolveSkyLightOwnership, skyEmitsLights, type SkyLightOwnership, } from "./skyLightingPolicy.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { Daylight, SkyDaylight, SkyDome, TimeOfDayDaylight, } from "./Daylight.js";
|
|
2
2
|
export { EnvironmentScene } from "./EnvironmentScene.js";
|
|
3
|
+
export { VolumetricClouds } from "./VolumetricClouds.js";
|
|
3
4
|
export { daylightStateAt, lerpHexColor, SKY_PRESET_DAY_FRACTION, } from "./daylightCycle.js";
|
|
4
5
|
export { resolveSkyLightOwnership, skyEmitsLights, } from "./skyLightingPolicy.js";
|
|
@@ -6,5 +6,7 @@
|
|
|
6
6
|
* sky-owned lights when the game did not author lighting).
|
|
7
7
|
*/
|
|
8
8
|
export type SkyLightOwnership = "authored" | "sky-default";
|
|
9
|
+
/** @internal */
|
|
9
10
|
export declare function resolveSkyLightOwnership(hasAuthoredLighting: boolean): SkyLightOwnership;
|
|
11
|
+
/** @internal */
|
|
10
12
|
export declare function skyEmitsLights(ownership: SkyLightOwnership): boolean;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type ActionCodesMap } from "@jgengine/core/input/actionBindings";
|
|
2
|
+
import type { Aim } from "@jgengine/core/scene/spatial";
|
|
3
|
+
import type { GameContext } from "@jgengine/core/runtime/gameContext";
|
|
4
|
+
type Inventories = Record<string, {
|
|
5
|
+
hud?: string;
|
|
6
|
+
}>;
|
|
7
|
+
/** Minimal playable slice — avoids coupling to shell vs core PlayableGame generics. @internal */
|
|
8
|
+
export type HotbarPlayable = {
|
|
9
|
+
game: {
|
|
10
|
+
inventories?: Inventories;
|
|
11
|
+
input?: ActionCodesMap;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
/** @internal Map bound action names to hotbar slot indices. */
|
|
15
|
+
export declare function findHotbarSlotActions(input: ActionCodesMap | undefined): {
|
|
16
|
+
action: string;
|
|
17
|
+
slot: number;
|
|
18
|
+
}[];
|
|
19
|
+
/** @internal Resolve the inventory id used as the hotbar HUD binding. */
|
|
20
|
+
export declare function hotbarIdFor(playable: HotbarPlayable): string | null;
|
|
21
|
+
/** @internal Activate an item from a hotbar slot via `ctx.item.use`. */
|
|
22
|
+
export declare function executeHotbarSlot(ctx: GameContext, fromId: string, hotbarId: string, slot: number, yaw: number, pitch: number, aimOverride?: Aim): {
|
|
23
|
+
ok: boolean;
|
|
24
|
+
error?: string;
|
|
25
|
+
};
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { hotbarSlotActionIndex } from "@jgengine/core/input/actionBindings";
|
|
2
|
+
/** @internal Map bound action names to hotbar slot indices. */
|
|
3
|
+
export function findHotbarSlotActions(input) {
|
|
4
|
+
return Object.keys(input ?? {}).flatMap((action) => {
|
|
5
|
+
const slot = hotbarSlotActionIndex(action);
|
|
6
|
+
return slot === null ? [] : [{ action, slot }];
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
/** @internal Resolve the inventory id used as the hotbar HUD binding. */
|
|
10
|
+
export function hotbarIdFor(playable) {
|
|
11
|
+
const declarations = Object.entries(playable.game.inventories ?? {});
|
|
12
|
+
const hud = declarations.find(([, declaration]) => declaration.hud === "hotbar");
|
|
13
|
+
return (hud ?? declarations[0])?.[0] ?? null;
|
|
14
|
+
}
|
|
15
|
+
/** @internal Activate an item from a hotbar slot via `ctx.item.use`. */
|
|
16
|
+
export function executeHotbarSlot(ctx, fromId, hotbarId, slot, yaw, pitch, aimOverride) {
|
|
17
|
+
const stack = ctx.player.inventory.state(hotbarId).slots[slot];
|
|
18
|
+
if (stack === undefined || stack === null)
|
|
19
|
+
return { ok: false, error: `Hotbar slot ${slot + 1} is empty` };
|
|
20
|
+
const result = ctx.item.use.use({
|
|
21
|
+
from: fromId,
|
|
22
|
+
itemId: stack.itemId,
|
|
23
|
+
inventoryId: hotbarId,
|
|
24
|
+
aim: aimOverride ?? { yaw, pitch },
|
|
25
|
+
});
|
|
26
|
+
return result.error === undefined ? { ok: true } : { ok: false, error: result.error };
|
|
27
|
+
}
|
package/dist/inputSink.d.ts
CHANGED
|
@@ -4,15 +4,27 @@ import type { LiveGameBackend } from "@jgengine/core/runtime/transport";
|
|
|
4
4
|
export interface InputSink {
|
|
5
5
|
send(frame: InputFrame): void;
|
|
6
6
|
}
|
|
7
|
-
/** Discards input — the single-player / client-authoritative default, where the client integrates movement itself.
|
|
7
|
+
/** Discards input — the single-player / client-authoritative default, where the client integrates movement itself.
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
8
10
|
export declare function noopInputSink(): InputSink;
|
|
9
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Sends each frame's input to the authoritative host over the transport, reusing the `runCommand` path via
|
|
13
|
+
* {@link INPUT_COMMAND}. Sends are sequenced one-in-flight-at-a-time per `serverId` and stamped with a monotonic
|
|
14
|
+
* seq: a frame sent while one is already in flight replaces the pending frame rather than racing it, so the
|
|
15
|
+
* latest intent always wins and a stale frame can never resolve after (and overwrite) a newer one.
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
10
18
|
export declare function remoteInputSink(backend: Pick<LiveGameBackend, "transport">, serverId: string): InputSink;
|
|
11
|
-
/** The sink a server-authoritative shell sends its per-frame input through: remote when `authority: "server"` and a server is joined, a no-op otherwise.
|
|
19
|
+
/** The sink a server-authoritative shell sends its per-frame input through: remote when `authority: "server"` and a server is joined, a no-op otherwise.
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
12
22
|
export declare function resolveInputSink(opts: {
|
|
13
23
|
serverAuthoritative: boolean;
|
|
14
24
|
backend: Pick<LiveGameBackend, "transport"> | null;
|
|
15
25
|
serverId: string | null;
|
|
16
26
|
}): InputSink;
|
|
17
|
-
/** Whether two input frames carry identical intent — the shell skips resending unchanged frames so a still player floods the host with nothing.
|
|
27
|
+
/** Whether two input frames carry identical intent — the shell skips resending unchanged frames so a still player floods the host with nothing.
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
18
30
|
export declare function inputFramesEqual(a: InputFrame, b: InputFrame): boolean;
|
package/dist/inputSink.js
CHANGED
|
@@ -1,26 +1,76 @@
|
|
|
1
1
|
import { INPUT_COMMAND } from "@jgengine/core/runtime/hostedGameRunner";
|
|
2
|
-
/** Discards input — the single-player / client-authoritative default, where the client integrates movement itself.
|
|
2
|
+
/** Discards input — the single-player / client-authoritative default, where the client integrates movement itself.
|
|
3
|
+
* @internal
|
|
4
|
+
*/
|
|
3
5
|
export function noopInputSink() {
|
|
4
6
|
return { send() { } };
|
|
5
7
|
}
|
|
6
|
-
|
|
8
|
+
const remoteInputSources = new Map();
|
|
9
|
+
function remoteInputSourceFor(serverId) {
|
|
10
|
+
let source = remoteInputSources.get(serverId);
|
|
11
|
+
if (source === undefined) {
|
|
12
|
+
source = { pending: null, inFlight: false };
|
|
13
|
+
remoteInputSources.set(serverId, source);
|
|
14
|
+
}
|
|
15
|
+
return source;
|
|
16
|
+
}
|
|
17
|
+
function monotonicInputSeq() {
|
|
18
|
+
return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
19
|
+
}
|
|
20
|
+
function pumpRemoteInput(backend, serverId, source) {
|
|
21
|
+
if (source.inFlight)
|
|
22
|
+
return;
|
|
23
|
+
const frame = source.pending;
|
|
24
|
+
if (frame === null) {
|
|
25
|
+
remoteInputSources.delete(serverId);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
source.pending = null;
|
|
29
|
+
source.inFlight = true;
|
|
30
|
+
const seq = monotonicInputSeq();
|
|
31
|
+
void backend.transport
|
|
32
|
+
.runCommand({ serverId, command: INPUT_COMMAND, input: { ...frame, seq } })
|
|
33
|
+
.then((result) => {
|
|
34
|
+
if (!result.ok) {
|
|
35
|
+
console.warn(`[jgengine:input] frame seq=${seq} to server "${serverId}" rejected: ${result.reason}`);
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
.catch((error) => {
|
|
39
|
+
console.warn(`[jgengine:input] frame seq=${seq} to server "${serverId}" failed to send`, error);
|
|
40
|
+
})
|
|
41
|
+
.finally(() => {
|
|
42
|
+
source.inFlight = false;
|
|
43
|
+
pumpRemoteInput(backend, serverId, source);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Sends each frame's input to the authoritative host over the transport, reusing the `runCommand` path via
|
|
48
|
+
* {@link INPUT_COMMAND}. Sends are sequenced one-in-flight-at-a-time per `serverId` and stamped with a monotonic
|
|
49
|
+
* seq: a frame sent while one is already in flight replaces the pending frame rather than racing it, so the
|
|
50
|
+
* latest intent always wins and a stale frame can never resolve after (and overwrite) a newer one.
|
|
51
|
+
* @internal
|
|
52
|
+
*/
|
|
7
53
|
export function remoteInputSink(backend, serverId) {
|
|
8
54
|
return {
|
|
9
55
|
send(frame) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
56
|
+
const source = remoteInputSourceFor(serverId);
|
|
57
|
+
source.pending = frame;
|
|
58
|
+
pumpRemoteInput(backend, serverId, source);
|
|
13
59
|
},
|
|
14
60
|
};
|
|
15
61
|
}
|
|
16
|
-
/** The sink a server-authoritative shell sends its per-frame input through: remote when `authority: "server"` and a server is joined, a no-op otherwise.
|
|
62
|
+
/** The sink a server-authoritative shell sends its per-frame input through: remote when `authority: "server"` and a server is joined, a no-op otherwise.
|
|
63
|
+
* @internal
|
|
64
|
+
*/
|
|
17
65
|
export function resolveInputSink(opts) {
|
|
18
66
|
if (opts.serverAuthoritative && opts.backend !== null && opts.serverId !== null) {
|
|
19
67
|
return remoteInputSink(opts.backend, opts.serverId);
|
|
20
68
|
}
|
|
21
69
|
return noopInputSink();
|
|
22
70
|
}
|
|
23
|
-
/** Whether two input frames carry identical intent — the shell skips resending unchanged frames so a still player floods the host with nothing.
|
|
71
|
+
/** Whether two input frames carry identical intent — the shell skips resending unchanged frames so a still player floods the host with nothing.
|
|
72
|
+
* @internal
|
|
73
|
+
*/
|
|
24
74
|
export function inputFramesEqual(a, b) {
|
|
25
75
|
if (a.held.length !== b.held.length)
|
|
26
76
|
return false;
|
|
@@ -2,5 +2,15 @@ import * as THREE from "three";
|
|
|
2
2
|
import type { ModelMaterialOverride } from "@jgengine/core/game/playableGame";
|
|
3
3
|
export interface MaterialOverrideOptions {
|
|
4
4
|
clone?: boolean;
|
|
5
|
+
/** Loaded PBR textures matching `ModelMaterialOverride.maps`' roles, applied onto each `MeshStandardMaterial` alongside the tint override. The caller (a React component, via `useTexture`) owns loading; this function never fetches a URL. */
|
|
6
|
+
textures?: MaterialOverrideTextures;
|
|
5
7
|
}
|
|
8
|
+
/** Loaded PBR textures for `applyMaterialOverride`'s `textures` option — matches `ModelMaterialMaps`' roles. */
|
|
9
|
+
export interface MaterialOverrideTextures {
|
|
10
|
+
color?: THREE.Texture;
|
|
11
|
+
normal?: THREE.Texture;
|
|
12
|
+
roughness?: THREE.Texture;
|
|
13
|
+
ao?: THREE.Texture;
|
|
14
|
+
}
|
|
15
|
+
/** @internal */
|
|
6
16
|
export declare function applyMaterialOverride(root: THREE.Object3D, override: ModelMaterialOverride, options?: MaterialOverrideOptions): void;
|
package/dist/materialOverride.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import * as THREE from "three";
|
|
2
|
+
/** @internal */
|
|
2
3
|
export function applyMaterialOverride(root, override, options) {
|
|
3
4
|
const clone = options?.clone !== false;
|
|
5
|
+
const textures = options?.textures;
|
|
4
6
|
root.traverse((node) => {
|
|
5
7
|
const mesh = node;
|
|
6
8
|
if (!mesh.isMesh)
|
|
7
9
|
return;
|
|
8
10
|
mesh.material = Array.isArray(mesh.material)
|
|
9
|
-
? mesh.material.map((material) => overrideOne(material, override, clone))
|
|
10
|
-
: overrideOne(mesh.material, override, clone);
|
|
11
|
+
? mesh.material.map((material) => overrideOne(material, override, clone, textures))
|
|
12
|
+
: overrideOne(mesh.material, override, clone, textures);
|
|
11
13
|
});
|
|
12
14
|
}
|
|
13
|
-
function overrideOne(material, override, clone) {
|
|
15
|
+
function overrideOne(material, override, clone, textures) {
|
|
14
16
|
if (!(material instanceof THREE.MeshStandardMaterial))
|
|
15
17
|
return material;
|
|
16
18
|
const target = clone ? material.clone() : material;
|
|
@@ -24,5 +26,15 @@ function overrideOne(material, override, clone) {
|
|
|
24
26
|
target.emissive.set(override.emissive);
|
|
25
27
|
if (override.emissiveIntensity !== undefined)
|
|
26
28
|
target.emissiveIntensity = override.emissiveIntensity;
|
|
29
|
+
if (textures?.color !== undefined)
|
|
30
|
+
target.map = textures.color;
|
|
31
|
+
if (textures?.normal !== undefined)
|
|
32
|
+
target.normalMap = textures.normal;
|
|
33
|
+
if (textures?.roughness !== undefined)
|
|
34
|
+
target.roughnessMap = textures.roughness;
|
|
35
|
+
if (textures?.ao !== undefined)
|
|
36
|
+
target.aoMap = textures.ao;
|
|
37
|
+
if (textures !== undefined)
|
|
38
|
+
target.needsUpdate = true;
|
|
27
39
|
return target;
|
|
28
40
|
}
|
package/dist/multiplayer.js
CHANGED
|
@@ -29,16 +29,22 @@ export function resolveShellMultiplayer(args) {
|
|
|
29
29
|
if (args.force === true)
|
|
30
30
|
return build(args.url ?? DEFAULT_WS_URL);
|
|
31
31
|
const adapter = adapterOf(args.game.multiplayer);
|
|
32
|
-
if (adapter === null)
|
|
32
|
+
if (adapter === null || adapter.kind === "offline")
|
|
33
33
|
return null;
|
|
34
34
|
if (adapter.kind === "ws") {
|
|
35
35
|
const url = args.url ?? adapter.url;
|
|
36
|
-
if (url === undefined && isServerAuthoritative(args.game.multiplayer))
|
|
36
|
+
if (url === undefined && isServerAuthoritative(args.game.multiplayer)) {
|
|
37
|
+
console.warn(`[jgengine:multiplayer] ${args.gameId}: ws({ authority: "server" }) has no url; falling back to single-player. ` +
|
|
38
|
+
`Pass a url (or set adapter.url) so the shell can reach the host.`);
|
|
37
39
|
return null;
|
|
40
|
+
}
|
|
38
41
|
return build(url ?? DEFAULT_WS_URL);
|
|
39
42
|
}
|
|
40
43
|
if (adapter.kind === "lan")
|
|
41
44
|
return build(args.url ?? lanUrl(adapter));
|
|
45
|
+
console.warn(`[jgengine:multiplayer] ${args.gameId}: multiplayer adapter "${adapter.kind}" needs its own session bootstrap ` +
|
|
46
|
+
`(e.g. resolvePeerShellMultiplayer for p2p()); resolveShellMultiplayer cannot build a ws backend for it, ` +
|
|
47
|
+
`so the game shipped single-player. Wire up the ${adapter.kind} transport or switch to ws()/lan().`);
|
|
42
48
|
return null;
|
|
43
49
|
}
|
|
44
50
|
export async function resolvePeerShellMultiplayer(args) {
|