@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
|
@@ -36,7 +36,7 @@ function ShadowCastingSun({ position, intensity, color, }) {
|
|
|
36
36
|
return (_jsx("directionalLight", { ref: ref, position: [position[0], position[1], position[2]], intensity: intensity, color: color, castShadow: true, "shadow-mapSize-width": 2048, "shadow-mapSize-height": 2048, "shadow-camera-left": -90, "shadow-camera-right": 90, "shadow-camera-top": 90, "shadow-camera-bottom": -90, "shadow-camera-near": 10, "shadow-camera-far": 520, "shadow-bias": -0.0004, "shadow-normalBias": 0.02 }));
|
|
37
37
|
}
|
|
38
38
|
/** @internal */
|
|
39
|
-
export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius = 260, offset = 24, exponent = 0.65, sunDirection, sunColor = "#fff4d6", sunIntensity = 1, hazeStrength = 0.5, sunGlowStrength = 1, materialRef, } = {}) {
|
|
39
|
+
export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius = 260, offset = 24, exponent = 0.65, sunDirection, sunColor = "#fff4d6", sunIntensity = 1, hazeStrength = 0.5, sunGlowStrength = 1, cloudiness = 0.35, materialRef, } = {}) {
|
|
40
40
|
const meshRef = useRef(null);
|
|
41
41
|
const material = useMemo(() => {
|
|
42
42
|
return new THREE.ShaderMaterial({
|
|
@@ -50,6 +50,7 @@ export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius
|
|
|
50
50
|
uSunIntensity: { value: sunDirection === undefined ? 0 : sunIntensity },
|
|
51
51
|
uHazeStrength: { value: hazeStrength },
|
|
52
52
|
uSunGlow: { value: sunGlowStrength },
|
|
53
|
+
uCloudiness: { value: cloudiness },
|
|
53
54
|
},
|
|
54
55
|
vertexShader: `
|
|
55
56
|
varying vec3 vWorldPosition;
|
|
@@ -67,6 +68,7 @@ export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius
|
|
|
67
68
|
uniform float uSunIntensity;
|
|
68
69
|
uniform float uHazeStrength;
|
|
69
70
|
uniform float uSunGlow;
|
|
71
|
+
uniform float uCloudiness;
|
|
70
72
|
uniform float offset;
|
|
71
73
|
uniform float exponent;
|
|
72
74
|
varying vec3 vWorldPosition;
|
|
@@ -86,7 +88,7 @@ export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius
|
|
|
86
88
|
vec2 cuv = vd.xz / (max(vd.y, 0.06) + 0.15) * 1.2;
|
|
87
89
|
float clouds = smoothstep(0.52, 0.82, sFbm(cuv));
|
|
88
90
|
float band = smoothstep(0.08, 0.35, dir.y) * smoothstep(1.0, 0.35, dir.y);
|
|
89
|
-
col = mix(col, mix(topColor, vec3(1.0), 0.6), clouds * band *
|
|
91
|
+
col = mix(col, mix(topColor, vec3(1.0), 0.6), clouds * band * uCloudiness);
|
|
90
92
|
float sd = max(dot(vd, normalize(uSunDirection)), 0.0);
|
|
91
93
|
float glow = pow(sd, 8.0) * 0.35 + pow(sd, 128.0) * 2.6;
|
|
92
94
|
col += uSunColor * glow * uSunIntensity * uSunGlow;
|
|
@@ -97,7 +99,7 @@ export function SkyDome({ topColor = SKY_TOP, horizonColor = SKY_HORIZON, radius
|
|
|
97
99
|
depthWrite: false,
|
|
98
100
|
fog: false,
|
|
99
101
|
});
|
|
100
|
-
}, [topColor, horizonColor, offset, exponent, sunColor, sunDirection, sunIntensity, hazeStrength, sunGlowStrength]);
|
|
102
|
+
}, [topColor, horizonColor, offset, exponent, sunColor, sunDirection, sunIntensity, hazeStrength, sunGlowStrength, cloudiness]);
|
|
101
103
|
useEffect(() => {
|
|
102
104
|
if (materialRef !== undefined)
|
|
103
105
|
materialRef.current = material;
|
|
@@ -139,6 +141,8 @@ function skyDomeShape(sky) {
|
|
|
139
141
|
...(sky.radius === undefined ? {} : { radius: sky.radius }),
|
|
140
142
|
...(sky.hazeStrength === undefined ? {} : { hazeStrength: sky.hazeStrength }),
|
|
141
143
|
...(sky.sunGlowStrength === undefined ? {} : { sunGlowStrength: sky.sunGlowStrength }),
|
|
144
|
+
...(sky.gradientExponent === undefined ? {} : { exponent: sky.gradientExponent }),
|
|
145
|
+
...(sky.cloudiness === undefined ? {} : { cloudiness: sky.cloudiness }),
|
|
142
146
|
};
|
|
143
147
|
}
|
|
144
148
|
function StaticSkyDaylight({ sky, lights }) {
|
|
@@ -6,11 +6,12 @@ import * as THREE from "three";
|
|
|
6
6
|
import { useGameContext } from "@jgengine/react/provider";
|
|
7
7
|
import { resolveBuildingPalette } from "@jgengine/core/world/buildings";
|
|
8
8
|
import { resolveStructureBuildings } from "@jgengine/core/world/environmentSummary";
|
|
9
|
-
import { findRoadJunctions
|
|
9
|
+
import { findRoadJunctions } from "@jgengine/core/world/streets";
|
|
10
10
|
import { createTerrainPaletteSampler, resolveEnvironmentField, resolveTerrainDetail, resolveTerrainPalette, } from "@jgengine/core/world/terrain";
|
|
11
|
+
import { useDisposable } from "../render/useDisposable.js";
|
|
11
12
|
import { GroundPad } from "./GroundPad.js";
|
|
12
13
|
import { RoadJunctions } from "./RoadJunctions.js";
|
|
13
|
-
import { RoadRibbons } from "./RoadRibbons.js";
|
|
14
|
+
import { buildAuthoredRoadNetwork, RoadRibbons } from "./RoadRibbons.js";
|
|
14
15
|
import { terrainGroundColorSampler } from "./terrainGroundColor.js";
|
|
15
16
|
import { InstancedBuildings } from "../structures/GeneratedBuilding.js";
|
|
16
17
|
import { GrassField } from "../terrain/GrassField.js";
|
|
@@ -36,8 +37,7 @@ function TexturedTerrainGround({ detail, layout, }) {
|
|
|
36
37
|
texture.needsUpdate = true;
|
|
37
38
|
}
|
|
38
39
|
}, [textures]);
|
|
39
|
-
const material =
|
|
40
|
-
useEffect(() => () => material.dispose(), [material]);
|
|
40
|
+
const material = useDisposable(() => createTerrainDetailMaterial(detail, textures).material, [detail, textures]);
|
|
41
41
|
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 }));
|
|
42
42
|
}
|
|
43
43
|
function TerrainGround({ terrain, field, center, }) {
|
|
@@ -158,6 +158,6 @@ export function EnvironmentScene({ feature }) {
|
|
|
158
158
|
const islands = feature.islands ?? [];
|
|
159
159
|
const roads = feature.roads ?? [];
|
|
160
160
|
const junctions = useMemo(() => findRoadJunctions(roads), [roads]);
|
|
161
|
-
const
|
|
162
|
-
return (_jsxs(_Fragment, { children: [feature.terrain !== undefined ? _jsx(TerrainGround, { terrain: feature.terrain, field: field }) : null, islands.map((entry, index) => (_jsx(TerrainGround, { terrain: entry, field: field, center: entry.origin }, `island-${index}`))), water.map((ocean, index) => (_jsx(Water, { ocean: ocean }, `ocean-${index}`))), roads.map((entry, index) => (_jsx(RoadRibbons, { road: entry, field: field,
|
|
161
|
+
const roadNetwork = useMemo(() => buildAuthoredRoadNetwork(roads, junctions, (x, z) => field.sampleHeight(x, z)), [roads, junctions, field]);
|
|
162
|
+
return (_jsxs(_Fragment, { children: [feature.terrain !== undefined ? _jsx(TerrainGround, { terrain: feature.terrain, field: field }) : null, islands.map((entry, index) => (_jsx(TerrainGround, { terrain: entry, field: field, center: entry.origin }, `island-${index}`))), water.map((ocean, index) => (_jsx(Water, { ocean: ocean }, `ocean-${index}`))), roads.map((entry, index) => (_jsx(RoadRibbons, { road: entry, field: field, paths: roadNetwork.roadPaths[index] ?? [] }, `road-${index}`))), _jsx(RoadJunctions, { surfaces: roadNetwork.junctionSurfaces, junctions: junctions, field: field }), structures.map((entry, index) => (_jsx(Structures, { structures: entry, field: field }, `structures-${index}`))), pads.map((entry, index) => (_jsx(GroundPad, { pad: entry, field: field }, `pad-${index}`))), vegetation.map((grass, index) => (_jsx(Vegetation, { grass: grass, field: field, groundColor: vegetationGroundColor(feature.terrain, field, grass) }, `grass-${index}`))), feature.weather !== undefined && feature.weather.length > 0 ? (_jsx(Weather, { weather: feature.weather })) : null] }));
|
|
163
163
|
}
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { type RoadJunction } from "@jgengine/core/world/streets";
|
|
2
2
|
import type { TerrainField } from "@jgengine/core/world/terrain";
|
|
3
|
+
import type { JunctionSurface } from "./RoadRibbons.js";
|
|
3
4
|
/**
|
|
4
|
-
* Renders welded road intersections: one asphalt
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Renders welded road intersections: one seam-shared asphalt surface per junction (built by
|
|
6
|
+
* {@link buildAuthoredRoadNetwork}, its boundary vertices welded onto the trimmed ribbon ends so
|
|
7
|
+
* there is no overlap and no floating disc), plus white stop lines and crosswalk bars at each
|
|
8
|
+
* approach. Markings ride the {@link GROUND_DECAL_LAYERS} marking layer above the surface, with
|
|
9
|
+
* `polygonOffset`/`renderOrder` so they win the depth test on sloped ground.
|
|
7
10
|
* @internal
|
|
8
11
|
*/
|
|
9
|
-
export declare function RoadJunctions({ junctions, field, }: {
|
|
12
|
+
export declare function RoadJunctions({ surfaces, junctions, field, }: {
|
|
13
|
+
surfaces: readonly JunctionSurface[];
|
|
10
14
|
junctions: readonly RoadJunction[];
|
|
11
15
|
field: TerrainField;
|
|
12
16
|
}): import("react").JSX.Element | null;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo } from "react";
|
|
3
3
|
import { BufferAttribute, BufferGeometry, DoubleSide } from "three";
|
|
4
|
-
import {
|
|
4
|
+
import { buildRoadRibbon, GROUND_DECAL_LAYERS } from "@jgengine/core/world/roads";
|
|
5
5
|
import { junctionMarkings } from "@jgengine/core/world/streets";
|
|
6
|
+
/** Lane paint sits the table's road→marking gap above the welded junction surface (also polygonOffset). */
|
|
7
|
+
const MARKING_LIFT = GROUND_DECAL_LAYERS.marking - GROUND_DECAL_LAYERS.road;
|
|
6
8
|
function toGeometry(positions, indices) {
|
|
7
9
|
if (positions.length === 0)
|
|
8
10
|
return null;
|
|
@@ -34,18 +36,17 @@ function mergedBars(bars, width, field, elevation) {
|
|
|
34
36
|
return toGeometry(positions, indices);
|
|
35
37
|
}
|
|
36
38
|
/**
|
|
37
|
-
* Renders welded road intersections: one asphalt
|
|
38
|
-
*
|
|
39
|
-
*
|
|
39
|
+
* Renders welded road intersections: one seam-shared asphalt surface per junction (built by
|
|
40
|
+
* {@link buildAuthoredRoadNetwork}, its boundary vertices welded onto the trimmed ribbon ends so
|
|
41
|
+
* there is no overlap and no floating disc), plus white stop lines and crosswalk bars at each
|
|
42
|
+
* approach. Markings ride the {@link GROUND_DECAL_LAYERS} marking layer above the surface, with
|
|
43
|
+
* `polygonOffset`/`renderOrder` so they win the depth test on sloped ground.
|
|
40
44
|
* @internal
|
|
41
45
|
*/
|
|
42
|
-
export function RoadJunctions({ junctions, field, }) {
|
|
43
|
-
const patches = useMemo(() =>
|
|
44
|
-
.map((
|
|
45
|
-
|
|
46
|
-
return { color: junction.color, geometry: toGeometry(patch.positions, patch.indices) };
|
|
47
|
-
})
|
|
48
|
-
.filter((entry) => entry.geometry !== null), [junctions, field]);
|
|
46
|
+
export function RoadJunctions({ surfaces, junctions, field, }) {
|
|
47
|
+
const patches = useMemo(() => surfaces
|
|
48
|
+
.map((surface) => ({ color: surface.color, geometry: toGeometry(surface.ribbon.positions, surface.ribbon.indices) }))
|
|
49
|
+
.filter((entry) => entry.geometry !== null), [surfaces]);
|
|
49
50
|
const markings = useMemo(() => {
|
|
50
51
|
const stopBars = [];
|
|
51
52
|
const crossBars = [];
|
|
@@ -59,11 +60,11 @@ export function RoadJunctions({ junctions, field, }) {
|
|
|
59
60
|
crossBars.push(bar);
|
|
60
61
|
}
|
|
61
62
|
return {
|
|
62
|
-
stops: mergedBars(stopBars, 0.5, field, elevation +
|
|
63
|
-
crosswalks: mergedBars(crossBars, 0.55, field, elevation +
|
|
63
|
+
stops: mergedBars(stopBars, 0.5, field, elevation + MARKING_LIFT),
|
|
64
|
+
crosswalks: mergedBars(crossBars, 0.55, field, elevation + MARKING_LIFT),
|
|
64
65
|
};
|
|
65
66
|
}, [junctions, field]);
|
|
66
|
-
if (patches.length === 0)
|
|
67
|
+
if (patches.length === 0 && markings.stops === null && markings.crosswalks === null)
|
|
67
68
|
return null;
|
|
68
|
-
return (_jsxs("group", { children: [patches.map((patch, index) => (_jsx("mesh", { geometry: patch.geometry, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: patch.color, roughness: 0.95, metalness: 0, side: DoubleSide }) }, `junction-${index}`))), markings.stops !== null ? (_jsx("mesh", { geometry: markings.stops, children: _jsx("meshStandardMaterial", { color: "#e6e9ee", roughness: 0.85, metalness: 0, side: DoubleSide }) })) : null, markings.crosswalks !== null ? (_jsx("mesh", { geometry: markings.crosswalks, children: _jsx("meshStandardMaterial", { color: "#e6e9ee", roughness: 0.85, metalness: 0, side: DoubleSide }) })) : null] }));
|
|
69
|
+
return (_jsxs("group", { children: [patches.map((patch, index) => (_jsx("mesh", { geometry: patch.geometry, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: patch.color, roughness: 0.95, metalness: 0, side: DoubleSide }) }, `junction-${index}`))), markings.stops !== null ? (_jsx("mesh", { geometry: markings.stops, renderOrder: 1, children: _jsx("meshStandardMaterial", { color: "#e6e9ee", roughness: 0.85, metalness: 0, side: DoubleSide, polygonOffset: true, polygonOffsetFactor: -1, polygonOffsetUnits: -1 }) })) : null, markings.crosswalks !== null ? (_jsx("mesh", { geometry: markings.crosswalks, renderOrder: 1, children: _jsx("meshStandardMaterial", { color: "#e6e9ee", roughness: 0.85, metalness: 0, side: DoubleSide, polygonOffset: true, polygonOffsetFactor: -1, polygonOffsetUnits: -1 }) })) : null] }));
|
|
69
70
|
}
|
|
@@ -1,13 +1,49 @@
|
|
|
1
1
|
import type { RoadEnvironmentDescriptor } from "@jgengine/core/world/features";
|
|
2
|
-
import { type
|
|
2
|
+
import { type RoadJunctionInput, type RoadPoint, type RoadRibbon } from "@jgengine/core/world/roads";
|
|
3
|
+
import { type RoadJunction } from "@jgengine/core/world/streets";
|
|
3
4
|
import type { TerrainField } from "@jgengine/core/world/terrain";
|
|
5
|
+
/** Convert a detected {@link RoadJunction} into the structural {@link RoadJunctionInput} the trimmer wants. @internal */
|
|
6
|
+
export declare function roadJunctionInput(junction: RoadJunction): RoadJunctionInput;
|
|
4
7
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
+
* Split every path segment at the junction centers that fall on it, inserting the junction point as
|
|
9
|
+
* a real vertex so {@link trimPathAtJunctions} (which matches junctions to path vertices) can cut the
|
|
10
|
+
* ribbon back there. Authored road crossings from `findRoadJunctions` land mid-segment, not on the
|
|
11
|
+
* hand-authored vertices, so without this the trimmer would never see them.
|
|
12
|
+
* @internal
|
|
8
13
|
*/
|
|
9
|
-
export declare function
|
|
14
|
+
export declare function insertJunctionNodes(path: readonly RoadPoint[], junctions: readonly RoadJunctionInput[], tolerance?: number): RoadPoint[];
|
|
15
|
+
/** One welded asphalt junction surface plus the color it should render in. */
|
|
16
|
+
export interface JunctionSurface {
|
|
17
|
+
color: string;
|
|
18
|
+
ribbon: RoadRibbon;
|
|
19
|
+
}
|
|
20
|
+
/** Trimmed ribbons for every road plus the welded surfaces that fill the crossings between them. */
|
|
21
|
+
export interface AuthoredRoadNetwork {
|
|
22
|
+
/** Trimmed centerline sub-paths per input road (parallel to the `roads` array; empty for degenerate roads). */
|
|
23
|
+
roadPaths: RoadPoint[][][];
|
|
24
|
+
/** One welded surface per junction that at least one trimmed ribbon actually ends at. */
|
|
25
|
+
junctionSurfaces: JunctionSurface[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Trim every authored road back to its junction boundaries and weld one surface into each crossing —
|
|
29
|
+
* the drop-in replacement for the old "full ribbons + floating disc" model. Each road's path gets its
|
|
30
|
+
* junction crossings inserted as vertices, is trimmed by {@link trimPathAtJunctions} (a through-road
|
|
31
|
+
* splits in two), and the terminal cross-section corners feed {@link buildJunctionSurface}. Ribbon
|
|
32
|
+
* corners drape at the road's own authored `elevation`, so each welded corner meets the ribbon end at
|
|
33
|
+
* the exact same height (seam-shared, per {@link GROUND_DECAL_LAYERS}); the surface interior drapes at
|
|
34
|
+
* the junction's representative elevation. Pure geometry, so the shell just meshes the result.
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
export declare function buildAuthoredRoadNetwork(roads: readonly RoadEnvironmentDescriptor[], junctions: readonly RoadJunction[], sampleHeight: (x: number, z: number) => number): AuthoredRoadNetwork;
|
|
38
|
+
/**
|
|
39
|
+
* Renders one `road()` descriptor from its trimmed centerline sub-paths (cut back at every junction
|
|
40
|
+
* by {@link buildAuthoredRoadNetwork}): an asphalt ribbon, a curb/edge strip along each border, an
|
|
41
|
+
* optional dashed centerline, and optional sidewalks. Because the sub-paths already stop at the
|
|
42
|
+
* junction boundary, every layer naturally ends there — no ribbon ploughs through the crossing and
|
|
43
|
+
* the dashed line no longer needs exclusion circles to skip intersections.
|
|
44
|
+
*/
|
|
45
|
+
export declare function RoadRibbons({ road, field, paths, }: {
|
|
10
46
|
road: RoadEnvironmentDescriptor;
|
|
11
47
|
field: TerrainField;
|
|
12
|
-
|
|
48
|
+
paths: readonly (readonly RoadPoint[])[];
|
|
13
49
|
}): import("react").JSX.Element | null;
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo } from "react";
|
|
3
3
|
import { BufferAttribute, BufferGeometry, Color, DoubleSide } from "three";
|
|
4
|
-
import { buildRoadRibbon, dashSegments } from "@jgengine/core/world/roads";
|
|
5
|
-
import {
|
|
4
|
+
import { buildJunctionSurface, buildRoadRibbon, dashSegments, GROUND_DECAL_LAYERS, trimPathAtJunctions, } from "@jgengine/core/world/roads";
|
|
5
|
+
import { offsetPath, sidewalkWidthOf } from "@jgengine/core/world/streets";
|
|
6
|
+
/**
|
|
7
|
+
* Decal sub-layers as small lifts above each road's authored base `elevation`, spaced by the
|
|
8
|
+
* single-owner {@link GROUND_DECAL_LAYERS} table so the edge strip and lane paint never z-fight the
|
|
9
|
+
* asphalt they sit on. The asphalt ribbon and the welded junction surface stay on the authored base
|
|
10
|
+
* (they are seam-welded — never stacked); markings lift by the table's road→marking gap and ALSO get
|
|
11
|
+
* `polygonOffset`/`renderOrder` on the material. Curbs and sidewalks are their own surfaces beside
|
|
12
|
+
* the road edge (not table decal layers), so they carry documented offsets rather than a table entry.
|
|
13
|
+
*/
|
|
14
|
+
const MARKING_LIFT = GROUND_DECAL_LAYERS.marking - GROUND_DECAL_LAYERS.road;
|
|
15
|
+
const CURB_LIFT = 0.02;
|
|
16
|
+
const SIDEWALK_LIFT = 0.06;
|
|
6
17
|
/** Darken an asphalt color toward black for a curb/edge strip (deterministic, no alloc churn). */
|
|
7
18
|
function darken(hex, amount) {
|
|
8
19
|
const c = new Color(hex);
|
|
@@ -18,13 +29,8 @@ function toGeometry(positions, indices) {
|
|
|
18
29
|
geometry.computeVertexNormals();
|
|
19
30
|
return geometry;
|
|
20
31
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return toGeometry(ribbon.positions, ribbon.indices);
|
|
24
|
-
}
|
|
25
|
-
function mergedDashGeometry(road, field, exclusions) {
|
|
26
|
-
const width = Math.max(0.25, road.width * 0.035);
|
|
27
|
-
const ribbons = dashSegments(road.path, 3.2, 4, exclusions).map((dash) => buildRoadRibbon(dash, width, (x, z) => field.sampleHeight(x, z), { elevation: road.elevation + 0.04 }));
|
|
32
|
+
/** Merge many ribbons into one buffer geometry (draws collapse to a single mesh). */
|
|
33
|
+
function mergeRibbons(ribbons) {
|
|
28
34
|
const vertexCount = ribbons.reduce((sum, r) => sum + r.positions.length, 0);
|
|
29
35
|
const indexCount = ribbons.reduce((sum, r) => sum + r.indices.length, 0);
|
|
30
36
|
if (vertexCount === 0)
|
|
@@ -36,40 +42,157 @@ function mergedDashGeometry(road, field, exclusions) {
|
|
|
36
42
|
for (const ribbon of ribbons) {
|
|
37
43
|
positions.set(ribbon.positions, vertexOffset);
|
|
38
44
|
const base = vertexOffset / 3;
|
|
39
|
-
for (let i = 0; i < ribbon.indices.length; i += 1)
|
|
45
|
+
for (let i = 0; i < ribbon.indices.length; i += 1)
|
|
40
46
|
indices[indexOffset + i] = ribbon.indices[i] + base;
|
|
41
|
-
}
|
|
42
47
|
vertexOffset += ribbon.positions.length;
|
|
43
48
|
indexOffset += ribbon.indices.length;
|
|
44
49
|
}
|
|
45
50
|
return toGeometry(positions, indices);
|
|
46
51
|
}
|
|
52
|
+
/** Convert a detected {@link RoadJunction} into the structural {@link RoadJunctionInput} the trimmer wants. @internal */
|
|
53
|
+
export function roadJunctionInput(junction) {
|
|
54
|
+
return {
|
|
55
|
+
x: junction.center[0],
|
|
56
|
+
z: junction.center[1],
|
|
57
|
+
// Arm direction `[dx, dz]` → angle `atan2(dx, dz)`, matching the generator convention the core uses.
|
|
58
|
+
arms: junction.approaches.map((approach) => ({
|
|
59
|
+
angle: Math.atan2(approach.direction[0], approach.direction[1]),
|
|
60
|
+
width: approach.width,
|
|
61
|
+
})),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Split every path segment at the junction centers that fall on it, inserting the junction point as
|
|
66
|
+
* a real vertex so {@link trimPathAtJunctions} (which matches junctions to path vertices) can cut the
|
|
67
|
+
* ribbon back there. Authored road crossings from `findRoadJunctions` land mid-segment, not on the
|
|
68
|
+
* hand-authored vertices, so without this the trimmer would never see them.
|
|
69
|
+
* @internal
|
|
70
|
+
*/
|
|
71
|
+
export function insertJunctionNodes(path, junctions, tolerance = 1e-3) {
|
|
72
|
+
if (path.length < 2)
|
|
73
|
+
return path.map((p) => [p[0], p[1]]);
|
|
74
|
+
const out = [[path[0][0], path[0][1]]];
|
|
75
|
+
for (let i = 0; i < path.length - 1; i += 1) {
|
|
76
|
+
const a = path[i];
|
|
77
|
+
const b = path[i + 1];
|
|
78
|
+
const abx = b[0] - a[0];
|
|
79
|
+
const abz = b[1] - a[1];
|
|
80
|
+
const lengthSq = abx * abx + abz * abz;
|
|
81
|
+
if (lengthSq > 1e-12) {
|
|
82
|
+
const hits = [];
|
|
83
|
+
for (const j of junctions) {
|
|
84
|
+
const t = ((j.x - a[0]) * abx + (j.z - a[1]) * abz) / lengthSq;
|
|
85
|
+
if (t <= 1e-6 || t >= 1 - 1e-6)
|
|
86
|
+
continue; // coincident with an existing vertex — already covered
|
|
87
|
+
const px = a[0] + abx * t;
|
|
88
|
+
const pz = a[1] + abz * t;
|
|
89
|
+
if (Math.hypot(j.x - px, j.z - pz) <= tolerance)
|
|
90
|
+
hits.push({ t, x: j.x, z: j.z });
|
|
91
|
+
}
|
|
92
|
+
hits.sort((u, v) => u.t - v.t);
|
|
93
|
+
for (const hit of hits) {
|
|
94
|
+
const prev = out[out.length - 1];
|
|
95
|
+
if (Math.hypot(prev[0] - hit.x, prev[1] - hit.z) > 1e-6)
|
|
96
|
+
out.push([hit.x, hit.z]);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
out.push([b[0], b[1]]);
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
47
103
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
104
|
+
* Trim every authored road back to its junction boundaries and weld one surface into each crossing —
|
|
105
|
+
* the drop-in replacement for the old "full ribbons + floating disc" model. Each road's path gets its
|
|
106
|
+
* junction crossings inserted as vertices, is trimmed by {@link trimPathAtJunctions} (a through-road
|
|
107
|
+
* splits in two), and the terminal cross-section corners feed {@link buildJunctionSurface}. Ribbon
|
|
108
|
+
* corners drape at the road's own authored `elevation`, so each welded corner meets the ribbon end at
|
|
109
|
+
* the exact same height (seam-shared, per {@link GROUND_DECAL_LAYERS}); the surface interior drapes at
|
|
110
|
+
* the junction's representative elevation. Pure geometry, so the shell just meshes the result.
|
|
111
|
+
* @internal
|
|
51
112
|
*/
|
|
52
|
-
export function
|
|
53
|
-
const
|
|
54
|
-
const
|
|
113
|
+
export function buildAuthoredRoadNetwork(roads, junctions, sampleHeight) {
|
|
114
|
+
const inputs = junctions.map(roadJunctionInput);
|
|
115
|
+
const roadPaths = [];
|
|
116
|
+
const approachesByJunction = new Map();
|
|
117
|
+
for (const road of roads) {
|
|
118
|
+
if (road.path.length < 2 || road.width <= 0) {
|
|
119
|
+
roadPaths.push([]);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const noded = insertJunctionNodes(road.path, inputs);
|
|
123
|
+
const subs = trimPathAtJunctions(noded, road.width, inputs);
|
|
124
|
+
roadPaths.push(subs.map((sub) => sub.path));
|
|
125
|
+
for (const sub of subs) {
|
|
126
|
+
for (const cut of sub.cuts) {
|
|
127
|
+
const approach = {
|
|
128
|
+
center: cut.center,
|
|
129
|
+
left: [cut.left[0], sampleHeight(cut.left[0], cut.left[1]) + road.elevation, cut.left[1]],
|
|
130
|
+
right: [cut.right[0], sampleHeight(cut.right[0], cut.right[1]) + road.elevation, cut.right[1]],
|
|
131
|
+
};
|
|
132
|
+
const list = approachesByJunction.get(cut.junctionIndex);
|
|
133
|
+
if (list)
|
|
134
|
+
list.push(approach);
|
|
135
|
+
else
|
|
136
|
+
approachesByJunction.set(cut.junctionIndex, [approach]);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const junctionSurfaces = [];
|
|
141
|
+
for (const ji of [...approachesByJunction.keys()].sort((p, q) => p - q)) {
|
|
142
|
+
const junction = junctions[ji];
|
|
143
|
+
const ribbon = buildJunctionSurface({ x: junction.center[0], z: junction.center[1] }, approachesByJunction.get(ji), sampleHeight, { elevation: junction.elevation });
|
|
144
|
+
junctionSurfaces.push({ color: junction.color, ribbon });
|
|
145
|
+
}
|
|
146
|
+
return { roadPaths, junctionSurfaces };
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Renders one `road()` descriptor from its trimmed centerline sub-paths (cut back at every junction
|
|
150
|
+
* by {@link buildAuthoredRoadNetwork}): an asphalt ribbon, a curb/edge strip along each border, an
|
|
151
|
+
* optional dashed centerline, and optional sidewalks. Because the sub-paths already stop at the
|
|
152
|
+
* junction boundary, every layer naturally ends there — no ribbon ploughs through the crossing and
|
|
153
|
+
* the dashed line no longer needs exclusion circles to skip intersections.
|
|
154
|
+
*/
|
|
155
|
+
export function RoadRibbons({ road, field, paths, }) {
|
|
156
|
+
const asphalt = useMemo(() => mergeRibbons(paths.map((path) => buildRoadRibbon(path, road.width, (x, z) => field.sampleHeight(x, z), { elevation: road.elevation }))), [paths, road.width, road.elevation, field]);
|
|
157
|
+
const dashes = useMemo(() => {
|
|
158
|
+
if (!road.markings)
|
|
159
|
+
return null;
|
|
160
|
+
const width = Math.max(0.25, road.width * 0.035);
|
|
161
|
+
const ribbons = [];
|
|
162
|
+
for (const path of paths) {
|
|
163
|
+
for (const dash of dashSegments(path, 3.2, 4)) {
|
|
164
|
+
ribbons.push(buildRoadRibbon(dash, width, (x, z) => field.sampleHeight(x, z), { elevation: road.elevation + MARKING_LIFT }));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return mergeRibbons(ribbons);
|
|
168
|
+
}, [paths, road.markings, road.width, road.elevation, field]);
|
|
55
169
|
const curbColor = useMemo(() => darken(road.color, 0.45), [road.color]);
|
|
56
170
|
const curbs = useMemo(() => {
|
|
57
171
|
// A thin strip straddling each asphalt edge, lifted just above the ribbon.
|
|
58
172
|
const strip = Math.max(0.4, road.width * 0.09);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
173
|
+
const edge = road.width / 2 - strip / 2;
|
|
174
|
+
const ribbons = [];
|
|
175
|
+
for (const path of paths) {
|
|
176
|
+
for (const off of [edge, -edge]) {
|
|
177
|
+
ribbons.push(buildRoadRibbon(offsetPath(path, off), strip, (x, z) => field.sampleHeight(x, z), { elevation: road.elevation + CURB_LIFT }));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return mergeRibbons(ribbons);
|
|
181
|
+
}, [paths, road.width, road.elevation, field]);
|
|
63
182
|
const sidewalks = useMemo(() => {
|
|
64
183
|
const width = sidewalkWidthOf(road);
|
|
65
184
|
if (width <= 0)
|
|
66
|
-
return
|
|
185
|
+
return null;
|
|
67
186
|
const offset = road.width / 2 + width / 2;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
187
|
+
const ribbons = [];
|
|
188
|
+
for (const path of paths) {
|
|
189
|
+
for (const off of [offset, -offset]) {
|
|
190
|
+
ribbons.push(buildRoadRibbon(offsetPath(path, off), width, (x, z) => field.sampleHeight(x, z), { elevation: road.elevation + SIDEWALK_LIFT }));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return mergeRibbons(ribbons);
|
|
194
|
+
}, [paths, road, field]);
|
|
72
195
|
if (asphalt === null)
|
|
73
196
|
return null;
|
|
74
|
-
return (_jsxs("group", { children: [_jsx("mesh", { geometry: asphalt, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: road.color, roughness: 0.95, metalness: 0, side: DoubleSide }) }), curbs
|
|
197
|
+
return (_jsxs("group", { children: [_jsx("mesh", { geometry: asphalt, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: road.color, roughness: 0.95, metalness: 0, side: DoubleSide }) }), curbs !== null ? (_jsx("mesh", { geometry: curbs, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: curbColor, roughness: 0.9, metalness: 0, side: DoubleSide }) })) : null, dashes !== null ? (_jsx("mesh", { geometry: dashes, renderOrder: 1, children: _jsx("meshStandardMaterial", { color: road.markingColor, roughness: 0.8, metalness: 0, side: DoubleSide, polygonOffset: true, polygonOffsetFactor: -1, polygonOffsetUnits: -1 }) })) : null, sidewalks !== null ? (_jsx("mesh", { geometry: sidewalks, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: road.sidewalk === false ? "#a7adb8" : road.sidewalk.color, roughness: 0.9, metalness: 0, side: DoubleSide }) })) : null] }));
|
|
75
198
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useFrame } from "@react-three/fiber";
|
|
3
|
-
import {
|
|
3
|
+
import { useRef } from "react";
|
|
4
4
|
import * as THREE from "three";
|
|
5
|
+
import { useDisposable } from "../render/useDisposable.js";
|
|
5
6
|
/**
|
|
6
7
|
* Camera-centered proxy dome radius. Must sit inside the default camera far plane
|
|
7
8
|
* (`CAMERA_FRUSTUM_DEFAULTS.far` = 300) — a bigger sphere is clipped whole and renders nothing. The
|
|
@@ -161,7 +162,7 @@ void main() {
|
|
|
161
162
|
export function VolumetricClouds({ rules, sunDirection }) {
|
|
162
163
|
const meshRef = useRef(null);
|
|
163
164
|
const timeRef = useRef(0);
|
|
164
|
-
const material =
|
|
165
|
+
const material = useDisposable(() => {
|
|
165
166
|
return new THREE.ShaderMaterial({
|
|
166
167
|
uniforms: {
|
|
167
168
|
uCameraPos: { value: new THREE.Vector3() },
|
|
@@ -186,7 +187,6 @@ export function VolumetricClouds({ rules, sunDirection }) {
|
|
|
186
187
|
fog: false,
|
|
187
188
|
});
|
|
188
189
|
}, [rules, sunDirection]);
|
|
189
|
-
useEffect(() => () => material.dispose(), [material]);
|
|
190
190
|
useFrame((state, delta) => {
|
|
191
191
|
timeRef.current += delta;
|
|
192
192
|
const mesh = meshRef.current;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { Daylight, SkyDaylight, SkyDome, TimeOfDayDaylight, type DaylightProps, type SkyDaylightProps, type SkyDomeProps, type TimeOfDayDaylightProps, } from "./Daylight.js";
|
|
2
|
+
export { DayNightSky, type DayNightSkyProps } from "./DayNightSky.js";
|
|
2
3
|
export { EnvironmentScene, type EnvironmentSceneProps } from "./EnvironmentScene.js";
|
|
3
4
|
export { VolumetricClouds, type VolumetricCloudsProps } from "./VolumetricClouds.js";
|
|
4
5
|
export { daylightStateAt, lerpHexColor, SKY_PRESET_DAY_FRACTION, type DaylightCycleConfig, type DaylightState, } from "./daylightCycle.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { Daylight, SkyDaylight, SkyDome, TimeOfDayDaylight, } from "./Daylight.js";
|
|
2
|
+
export { DayNightSky } from "./DayNightSky.js";
|
|
2
3
|
export { EnvironmentScene } from "./EnvironmentScene.js";
|
|
3
4
|
export { VolumetricClouds } from "./VolumetricClouds.js";
|
|
4
5
|
export { daylightStateAt, lerpHexColor, SKY_PRESET_DAY_FRACTION, } from "./daylightCycle.js";
|
package/dist/gameKit.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* (`defineGame`), mounting (`GameHost`), serializable state (`defineStore`), behavior
|
|
4
|
-
* (`defineSystem`),
|
|
5
|
-
*
|
|
2
|
+
* **Start here for game code.** The sole happy-path import surface for a whole simple game:
|
|
3
|
+
* authoring (`defineGame`), mounting (`GameHost`), serializable state (`defineStore`), behavior
|
|
4
|
+
* (`defineSystem`), authored-scene helpers, HUD building blocks, and deterministic randomness.
|
|
5
|
+
* Reach for deep package paths only when a game outgrows the kit. Do not start from
|
|
6
|
+
* `@jgengine/core/authoring` — that module is a core-side helper barrel, not the game entrypoint.
|
|
6
7
|
*
|
|
7
8
|
* @capability game-kit one-stop happy-path import surface for authoring, mounting, state, systems, authored scenes, and HUD
|
|
8
9
|
*/
|
|
@@ -20,7 +21,9 @@ export { authoredSpawnPosition } from "@jgengine/core/world/authoredSpawn";
|
|
|
20
21
|
export { collectAuthoredTriggers } from "@jgengine/core/scene/authoredTriggers";
|
|
21
22
|
export { environmentContentFromDocument } from "@jgengine/core/editor/environment";
|
|
22
23
|
export { environment } from "@jgengine/core/world/features";
|
|
24
|
+
export { grounded } from "@jgengine/core/world/grounded";
|
|
23
25
|
export type { GameCameraConfig } from "@jgengine/core/game/playableGame";
|
|
26
|
+
export { DEFAULT_WALK_CODES } from "./shellMovement.js";
|
|
24
27
|
export { HudCanvas, HudPanel, useHudLayout } from "@jgengine/react/hudLayout";
|
|
25
28
|
export { StatBar, Hotbar, Clock, Coins } from "@jgengine/react/hud";
|
|
26
29
|
export { seededRng } from "@jgengine/core/random/rng";
|
package/dist/gameKit.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* (`defineGame`), mounting (`GameHost`), serializable state (`defineStore`), behavior
|
|
4
|
-
* (`defineSystem`),
|
|
5
|
-
*
|
|
2
|
+
* **Start here for game code.** The sole happy-path import surface for a whole simple game:
|
|
3
|
+
* authoring (`defineGame`), mounting (`GameHost`), serializable state (`defineStore`), behavior
|
|
4
|
+
* (`defineSystem`), authored-scene helpers, HUD building blocks, and deterministic randomness.
|
|
5
|
+
* Reach for deep package paths only when a game outgrows the kit. Do not start from
|
|
6
|
+
* `@jgengine/core/authoring` — that module is a core-side helper barrel, not the game entrypoint.
|
|
6
7
|
*
|
|
7
8
|
* @capability game-kit one-stop happy-path import surface for authoring, mounting, state, systems, authored scenes, and HUD
|
|
8
9
|
*/
|
|
@@ -20,7 +21,9 @@ export { collectAuthoredTriggers } from "@jgengine/core/scene/authoredTriggers";
|
|
|
20
21
|
export { environmentContentFromDocument } from "@jgengine/core/editor/environment";
|
|
21
22
|
// World + camera.
|
|
22
23
|
export { environment } from "@jgengine/core/world/features";
|
|
23
|
-
|
|
24
|
+
export { grounded } from "@jgengine/core/world/grounded";
|
|
25
|
+
export { DEFAULT_WALK_CODES } from "./shellMovement.js";
|
|
26
|
+
// HUD layout + drop-in building blocks — compose these, reskin with HudTheme; games own layout/terminology/art direction.
|
|
24
27
|
export { HudCanvas, HudPanel, useHudLayout } from "@jgengine/react/hudLayout";
|
|
25
28
|
export { StatBar, Hotbar, Clock, Coins } from "@jgengine/react/hud";
|
|
26
29
|
// Deterministic randomness (inject seeds; never Math.random in game logic).
|
package/dist/inputSink.js
CHANGED
|
@@ -77,6 +77,20 @@ export function inputFramesEqual(a, b) {
|
|
|
77
77
|
for (let i = 0; i < a.held.length; i++)
|
|
78
78
|
if (a.held[i] !== b.held[i])
|
|
79
79
|
return false;
|
|
80
|
+
const aa = a.analog ?? null;
|
|
81
|
+
const ba = b.analog ?? null;
|
|
82
|
+
if (aa === null || ba === null) {
|
|
83
|
+
if (aa !== ba)
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
const keys = Object.keys(aa);
|
|
88
|
+
if (keys.length !== Object.keys(ba).length)
|
|
89
|
+
return false;
|
|
90
|
+
for (const key of keys)
|
|
91
|
+
if (aa[key] !== ba[key])
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
80
94
|
const pa = a.pointer;
|
|
81
95
|
const pb = b.pointer;
|
|
82
96
|
if (pa === null || pb === null)
|
package/dist/materialOverride.js
CHANGED
|
@@ -36,5 +36,35 @@ function overrideOne(material, override, clone, textures) {
|
|
|
36
36
|
target.aoMap = textures.ao;
|
|
37
37
|
if (textures !== undefined)
|
|
38
38
|
target.needsUpdate = true;
|
|
39
|
+
if (override.rim !== undefined)
|
|
40
|
+
applyRimLight(target, override.rim);
|
|
39
41
|
return target;
|
|
40
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Adds a fresnel rim term to a standard material's outgoing light. Patched through
|
|
45
|
+
* `onBeforeCompile` rather than as a second pass so the rim shadows, fogs, and tonemaps with the
|
|
46
|
+
* body it belongs to instead of floating in front of it.
|
|
47
|
+
*/
|
|
48
|
+
function applyRimLight(target, rim) {
|
|
49
|
+
const color = new THREE.Color(rim.color ?? "#ffffff");
|
|
50
|
+
const strength = rim.strength ?? 0.6;
|
|
51
|
+
const power = rim.power ?? 2.5;
|
|
52
|
+
if (strength <= 0)
|
|
53
|
+
return;
|
|
54
|
+
target.onBeforeCompile = (shader) => {
|
|
55
|
+
shader.uniforms.uJgRimColor = { value: color };
|
|
56
|
+
shader.uniforms.uJgRimStrength = { value: strength };
|
|
57
|
+
shader.uniforms.uJgRimPower = { value: power };
|
|
58
|
+
shader.fragmentShader = shader.fragmentShader
|
|
59
|
+
.replace("#include <common>", `#include <common>
|
|
60
|
+
uniform vec3 uJgRimColor;
|
|
61
|
+
uniform float uJgRimStrength;
|
|
62
|
+
uniform float uJgRimPower;`)
|
|
63
|
+
.replace("#include <opaque_fragment>", `float jgRim = pow(1.0 - clamp(dot(normalize(normal), normalize(vViewPosition)), 0.0, 1.0), uJgRimPower);
|
|
64
|
+
outgoingLight += uJgRimColor * jgRim * uJgRimStrength;
|
|
65
|
+
#include <opaque_fragment>`);
|
|
66
|
+
};
|
|
67
|
+
// Two materials differing only in rim tuning must not share a compiled program.
|
|
68
|
+
target.customProgramCacheKey = () => `jgengine-rim-${rim.color ?? "#ffffff"}-${strength}-${power}`;
|
|
69
|
+
target.needsUpdate = true;
|
|
70
|
+
}
|
|
@@ -3,14 +3,15 @@ import type { GraphicsQuality } from "@jgengine/core/settings/settingsModel";
|
|
|
3
3
|
/**
|
|
4
4
|
* Mounts an `EffectComposer` inside the shell Canvas and takes over rendering
|
|
5
5
|
* (priority-1 `useFrame`, which disables R3F auto-render) to run the configured
|
|
6
|
-
* post chain: RenderPass → GTAO → UnrealBloom → OutputPass → Grade. Rendered only
|
|
6
|
+
* post chain: RenderPass → GTAO → UnrealBloom → SMAA → OutputPass → Grade. Rendered only
|
|
7
7
|
* when `PlayableGame.postProcessing` is set, so games without it draw unchanged.
|
|
8
8
|
*
|
|
9
9
|
* `quality` (the player's graphics-quality setting) gates the passes whose cost
|
|
10
10
|
* scales with scene geometry or resolution beyond the dpr cap: GTAO re-renders
|
|
11
11
|
* the whole scene for depth/normals and runs a multi-sample full-screen pass,
|
|
12
12
|
* and Bokeh DOF renders scene depth again — both run on "high" only. Bloom,
|
|
13
|
-
* tone mapping, and grade stay on every tier
|
|
13
|
+
* SMAA, tone mapping, and grade stay on every tier (SMAA is the cheap edge fix
|
|
14
|
+
* for alpha-tested foliage that MSAA samples alone leave crawling).
|
|
14
15
|
*/
|
|
15
16
|
export declare function PostProcessing({ config, quality }: {
|
|
16
17
|
config: PostProcessingConfig;
|