@jgengine/shell 0.21.0 β 0.22.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 +18 -0
- package/dist/defineGame.js +1 -1
- package/dist/environment/EnvironmentScene.js +10 -7
- package/dist/environment/terrainGroundColor.d.ts +5 -4
- package/dist/environment/terrainGroundColor.js +19 -8
- package/dist/scene/GrassFieldRenderer.js +20 -2
- package/dist/terrain/CarvedTerrain.d.ts +2 -1
- package/dist/terrain/CarvedTerrain.js +2 -2
- package/dist/terrain/GrassField.d.ts +2 -1
- package/dist/terrain/GrassField.js +3 -2
- package/dist/terrain/grassGeometry.d.ts +3 -0
- package/dist/terrain/grassGeometry.js +2 -1
- package/dist/terrain/index.d.ts +1 -0
- package/dist/terrain/index.js +1 -0
- package/dist/terrain/terrainMath.d.ts +2 -0
- package/dist/terrain/terrainMath.js +2 -0
- package/dist/terrain/terrainPaintColor.d.ts +11 -0
- package/dist/terrain/terrainPaintColor.js +47 -0
- package/package.json +4 -4
- package/skills/game-design/SKILL.md +2 -0
- package/skills/jgengine/SKILL.md +1 -0
- package/skills/jgengine/api.md +2 -2
- package/skills/jgengine-ui/api.md +18 -6
- package/skills/jgengine-ui/capabilities.md +4 -0
- package/skills/jgengine-world/api.md +1 -1
- package/skills/jgengine-world/reference.md +15 -0
package/CHANGELOG.md
CHANGED
|
@@ -26,6 +26,24 @@ At publish, rename this heading to the new version and mirror the entries into
|
|
|
26
26
|
`packages/core/src/meta/changelog.ts` (the typed `CHANGELOG` export).
|
|
27
27
|
-->
|
|
28
28
|
|
|
29
|
+
## 0.22.0
|
|
30
|
+
|
|
31
|
+
### Migrate
|
|
32
|
+
|
|
33
|
+
- **Bump lockstep SDK packages to `^0.22.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.19.0`; `@jgengine/github` is `0.9.0`.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- Editor-authored grass exposes blade width and bend ranges, tuft radius and normal lift, using the same bounded grass renderer as environment fields. Existing scene documents keep their previous shape and lighting defaults.
|
|
38
|
+
- The full `jgengine skills` install includes `game-improvement`, with an ordered implementation workflow and checks for meaningful decisions, authored terrain and grass, and proven play quality.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- Grass volumes sample terrain at their authored world position and subtract their placement height once, keeping translated patches rooted on slopes instead of buried underground.
|
|
43
|
+
|
|
44
|
+
- Authored grass respects spawn, objective and road/path clearances with feathered regrowth outside the usable footing. Diagonal paths keep their own corridor clear without clearing the entire surrounding rectangle.
|
|
45
|
+
- Runtime environment ground now renders editor-authored terrain paint surfaces, layer tints, opacity and weighted blends through the same sampler as the editor. Legacy unpainted terrain keeps descriptor shading. Layer texture tiling, roughness and triplanar controls remain serialized authoring metadata; this fix does not add layered PBR texture rendering.
|
|
46
|
+
|
|
29
47
|
## 0.21.0
|
|
30
48
|
|
|
31
49
|
### Migrate
|
package/dist/defineGame.js
CHANGED
|
@@ -42,7 +42,7 @@ function isEnvironmentSource(value) {
|
|
|
42
42
|
function authoredSceneOverlay(document, diagnostics, placement, scatterModels, flockModels, pathKinds, assets, world, Vfx) {
|
|
43
43
|
const terrain = world !== undefined && world.kind === "environment" ? world.terrain : undefined;
|
|
44
44
|
return function AuthoredSceneOverlay(props) {
|
|
45
|
-
const groundColorAt = useMemo(() => terrainGroundColorSampler(terrain, props.ctx.world.ground), [props.ctx.world.ground]);
|
|
45
|
+
const groundColorAt = useMemo(() => terrainGroundColorSampler(terrain, props.ctx.world.ground, document.terrain), [props.ctx.world.ground]);
|
|
46
46
|
return (_jsxs(_Fragment, { children: [_jsx(AuthoredScene, { document: document, diagnostics: diagnostics, field: props.ctx.world.ground, placeObjects: placement, assets: assets, ...(scatterModels === undefined ? {} : { scatterModels }), ...(flockModels === undefined ? {} : { flockModels }), ...(pathKinds === undefined ? {} : { pathKinds }), ...(groundColorAt === undefined ? {} : { groundColorAt }) }), Vfx === undefined ? null : _jsx(Vfx, { ...props })] }));
|
|
47
47
|
};
|
|
48
48
|
}
|
|
@@ -10,6 +10,8 @@ import { resolveStructureBuildings } from "@jgengine/core/world/environmentSumma
|
|
|
10
10
|
import { findRoadJunctions } from "@jgengine/core/world/streets";
|
|
11
11
|
import { createTerrainPaletteSampler, resolveEnvironmentField, resolveTerrainDetail, resolveTerrainPalette, } from "@jgengine/core/world/terrain";
|
|
12
12
|
import { useDisposable } from "../render/useDisposable.js";
|
|
13
|
+
import { editableTerrainFromSnapshot } from "@jgengine/core/world/terraform";
|
|
14
|
+
import { createTerrainPaintColorSampler } from "../terrain/terrainPaintColor.js";
|
|
13
15
|
import { GroundPad } from "./GroundPad.js";
|
|
14
16
|
import { RoadJunctions } from "./RoadJunctions.js";
|
|
15
17
|
import { buildAuthoredRoadNetwork, RoadRibbons } from "./RoadRibbons.js";
|
|
@@ -38,9 +40,10 @@ function TexturedTerrainGround({ detail, layout, }) {
|
|
|
38
40
|
}
|
|
39
41
|
}, [textures]);
|
|
40
42
|
const material = useDisposable(() => createTerrainDetailMaterial(detail, textures).material, [detail, textures]);
|
|
41
|
-
return (_jsxs(_Fragment, { children: [_jsx(PresentationRecoveredDraw, {}), _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 })] }));
|
|
43
|
+
return (_jsxs(_Fragment, { children: [_jsx(PresentationRecoveredDraw, {}), _jsx(CarvedTerrain, { field: layout.field, size: layout.size, segments: layout.segments, colors: layout.colors, heightRange: layout.heightRange, paletteAt: layout.paletteAt, paintColorAt: layout.paintColorAt, center: layout.center, roughness: 0.94, surfaceMaterial: material })] }));
|
|
42
44
|
}
|
|
43
|
-
function TerrainGround({ terrain, field, center, }) {
|
|
45
|
+
function TerrainGround({ terrain, field, center, sculpt, }) {
|
|
46
|
+
const paintColorAt = useMemo(() => sculpt === undefined ? undefined : createTerrainPaintColorSampler(editableTerrainFromSnapshot(sculpt)), [sculpt]);
|
|
44
47
|
const palette = useMemo(() => resolveTerrainPalette(terrain), [terrain]);
|
|
45
48
|
const paletteAt = useMemo(() => {
|
|
46
49
|
const hasRegions = terrain.materialRegions !== undefined && terrain.materialRegions.length > 0;
|
|
@@ -69,9 +72,9 @@ function TerrainGround({ terrain, field, center, }) {
|
|
|
69
72
|
: createTerrainDetailMaterial(resolvedDetail).material, [resolvedDetail]);
|
|
70
73
|
useEffect(() => () => proceduralMaterial?.dispose(), [proceduralMaterial]);
|
|
71
74
|
if (resolvedDetail?.material !== undefined) {
|
|
72
|
-
return (_jsx(PresentationAssetBoundary, { children: _jsx(TexturedTerrainGround, { detail: resolvedDetail, layout: { field, size, segments: terrain.segments, colors, heightRange, paletteAt, center } }) }));
|
|
75
|
+
return (_jsx(PresentationAssetBoundary, { children: _jsx(TexturedTerrainGround, { detail: resolvedDetail, layout: { field, size, segments: terrain.segments, colors, heightRange, paletteAt, paintColorAt, center } }) }));
|
|
73
76
|
}
|
|
74
|
-
return (_jsx(CarvedTerrain, { field: field, size: size, segments: terrain.segments, colors: colors, heightRange: heightRange, paletteAt: paletteAt, center: center, roughness: 0.94, surfaceMaterial: proceduralMaterial }));
|
|
77
|
+
return (_jsx(CarvedTerrain, { field: field, size: size, segments: terrain.segments, colors: colors, heightRange: heightRange, paletteAt: paletteAt, paintColorAt: paintColorAt, center: center, roughness: 0.94, surfaceMaterial: proceduralMaterial }));
|
|
75
78
|
}
|
|
76
79
|
function areaCenter(area) {
|
|
77
80
|
return area.position ?? [0, 0];
|
|
@@ -88,8 +91,8 @@ function Vegetation({ grass, field, groundColor, }) {
|
|
|
88
91
|
return (_jsx("group", { position: [cx, 0, cz], children: _jsx(GrassField, { area: area, count: Math.max(1500, Math.min(250000, Math.ceil(grass.density * grass.area.w * grass.area.d))), density: grass.density, seed: grass.seed, bladeHeight: grass.bladeHeight, bladeWidth: grass.bladeWidth, bladeBend: grass.bladeBend, tuftRadius: grass.tuftRadius, colorVariation: grass.colorVariation, normalLift: grass.normalLift, heightAt: heightAt, colorBase: colorBase, colorTip: colorTip, colorGround: groundColor, wind: wind, edgeFeather: Math.min(8, Math.max(1.5, Math.min(grass.area.w, grass.area.d) * 0.12)), frustumCulled: true }) }));
|
|
89
92
|
}
|
|
90
93
|
/** Terrain color under a grass patch center β so blade roots blend into the ground they stand on. */
|
|
91
|
-
function vegetationGroundColor(terrain, field, grass) {
|
|
92
|
-
const sampler = terrainGroundColorSampler(terrain, field);
|
|
94
|
+
function vegetationGroundColor(terrain, field, grass, sculpt) {
|
|
95
|
+
const sampler = terrainGroundColorSampler(terrain, field, sculpt);
|
|
93
96
|
if (sampler === undefined)
|
|
94
97
|
return undefined;
|
|
95
98
|
const [cx, cz] = areaCenter(grass.area);
|
|
@@ -160,5 +163,5 @@ export function EnvironmentScene({ feature }) {
|
|
|
160
163
|
const roads = feature.roads ?? [];
|
|
161
164
|
const junctions = useMemo(() => findRoadJunctions(roads), [roads]);
|
|
162
165
|
const roadNetwork = useMemo(() => buildAuthoredRoadNetwork(roads, junctions, (x, z) => field.sampleHeight(x, z)), [roads, junctions, field]);
|
|
163
|
-
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] }));
|
|
166
|
+
return (_jsxs(_Fragment, { children: [feature.terrain !== undefined ? _jsx(TerrainGround, { terrain: feature.terrain, field: field, sculpt: feature.sculpt }) : null, islands.map((entry, index) => (_jsx(TerrainGround, { terrain: entry, field: field, center: entry.origin, sculpt: feature.sculpt }, `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, feature.sculpt) }, `grass-${index}`))), feature.weather !== undefined && feature.weather.length > 0 ? (_jsx(Weather, { weather: feature.weather })) : null] }));
|
|
164
167
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { TerrainEnvironmentDescriptor } from "@jgengine/core/world/features";
|
|
2
2
|
import { type TerrainField } from "@jgengine/core/world/terrain";
|
|
3
|
+
import { type TerraformSnapshot } from "@jgengine/core/world/terraform";
|
|
3
4
|
/**
|
|
4
|
-
* Terrain
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Terrain base color at a world XZ β authored paint first, otherwise the regional palette lerped
|
|
6
|
+
* by ground height. Grass roots and soil-patch borders share the painted color with ground vertices.
|
|
7
|
+
* Descriptor surface mottling and fragment detail are presentation overlays, not sampled here.
|
|
7
8
|
* @internal
|
|
8
9
|
*/
|
|
9
|
-
export declare function terrainGroundColorSampler(terrain: Omit<TerrainEnvironmentDescriptor, "kind"> | undefined, field: TerrainField): ((x: number, z: number) => string) | undefined;
|
|
10
|
+
export declare function terrainGroundColorSampler(terrain: Omit<TerrainEnvironmentDescriptor, "kind"> | undefined, field: TerrainField, sculpt?: TerraformSnapshot): ((x: number, z: number) => string) | undefined;
|
|
@@ -1,21 +1,32 @@
|
|
|
1
1
|
import * as THREE from "three";
|
|
2
|
-
import { resolveTerrainPalette } from "@jgengine/core/world/terrain";
|
|
2
|
+
import { createTerrainPaletteSampler, resolveTerrainPalette } from "@jgengine/core/world/terrain";
|
|
3
|
+
import { editableTerrainFromSnapshot } from "@jgengine/core/world/terraform";
|
|
4
|
+
import { createTerrainPaintColorSampler } from "../terrain/terrainPaintColor.js";
|
|
3
5
|
/**
|
|
4
|
-
* Terrain
|
|
5
|
-
*
|
|
6
|
-
*
|
|
6
|
+
* Terrain base color at a world XZ β authored paint first, otherwise the regional palette lerped
|
|
7
|
+
* by ground height. Grass roots and soil-patch borders share the painted color with ground vertices.
|
|
8
|
+
* Descriptor surface mottling and fragment detail are presentation overlays, not sampled here.
|
|
7
9
|
* @internal
|
|
8
10
|
*/
|
|
9
|
-
export function terrainGroundColorSampler(terrain, field) {
|
|
10
|
-
if (terrain === undefined)
|
|
11
|
+
export function terrainGroundColorSampler(terrain, field, sculpt) {
|
|
12
|
+
if (terrain === undefined && sculpt === undefined)
|
|
11
13
|
return undefined;
|
|
12
14
|
const palette = resolveTerrainPalette(terrain);
|
|
15
|
+
const paletteAt = terrain === undefined ? undefined : createTerrainPaletteSampler(terrain);
|
|
16
|
+
const paintColorAt = sculpt === undefined ? undefined : createTerrainPaintColorSampler(editableTerrainFromSnapshot(sculpt));
|
|
13
17
|
const low = new THREE.Color(palette.low);
|
|
14
18
|
const high = new THREE.Color(palette.high);
|
|
15
|
-
const base = terrain
|
|
16
|
-
const swing = terrain
|
|
19
|
+
const base = terrain?.baseHeight ?? 0;
|
|
20
|
+
const swing = (terrain?.height ?? 0) * 1.2;
|
|
17
21
|
const scratch = new THREE.Color();
|
|
18
22
|
return (x, z) => {
|
|
23
|
+
if (paintColorAt?.(x, z, scratch))
|
|
24
|
+
return `#${scratch.getHexString()}`;
|
|
25
|
+
const regional = paletteAt?.(x, z);
|
|
26
|
+
if (regional !== undefined) {
|
|
27
|
+
low.set(regional.low);
|
|
28
|
+
high.set(regional.high);
|
|
29
|
+
}
|
|
19
30
|
const t = swing <= 0 ? 0.5 : THREE.MathUtils.clamp((field.sampleHeight(x, z) - (base - swing)) / (swing * 2), 0, 1);
|
|
20
31
|
return `#${scratch.copy(low).lerp(high, t).getHexString()}`;
|
|
21
32
|
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useMemo } from "react";
|
|
3
3
|
import { resolveGrassObject } from "@jgengine/core/world/grassKind";
|
|
4
|
+
import { clearanceMasksFrom, DEFAULT_CLEARANCE_KINDS } from "@jgengine/core/world/scatterRegion";
|
|
5
|
+
import { masksInfluence } from "@jgengine/core/world/geometry";
|
|
4
6
|
import { GrassField } from "../terrain/GrassField.js";
|
|
5
7
|
import { registerSceneKindRenderer } from "./sceneKindRenderers.js";
|
|
6
8
|
/** Volume kinds turf yields to β blades never grow through cracked soil or out of open water. */
|
|
@@ -47,9 +49,25 @@ function sampleHeight(field, x, z) {
|
|
|
47
49
|
*/
|
|
48
50
|
function OneGrass({ object, context }) {
|
|
49
51
|
const resolved = useMemo(() => resolveGrassObject(object), [object]);
|
|
50
|
-
const heightAt = useCallback((x, z) =>
|
|
52
|
+
const heightAt = useCallback((x, z) => resolved === null ? 0 :
|
|
53
|
+
sampleHeight(context.field, x + resolved.center[0], z + resolved.center[2]) - resolved.center[1], [context.field, resolved]);
|
|
51
54
|
const area = useMemo(() => (resolved === null ? null : [resolved.size[0], resolved.size[1]]), [resolved]);
|
|
52
55
|
const bladeHeight = useMemo(() => (resolved === null ? null : [resolved.rules.bladeHeight * 0.7, resolved.rules.bladeHeight]), [resolved]);
|
|
56
|
+
const bladeWidth = useMemo(() => resolved === null ? undefined : [resolved.rules.bladeWidthMin, resolved.rules.bladeWidthMax], [resolved]);
|
|
57
|
+
const bladeBend = useMemo(() => resolved === null ? undefined : [resolved.rules.bladeBendMin, resolved.rules.bladeBendMax], [resolved]);
|
|
58
|
+
const { markers, paths } = context.document;
|
|
59
|
+
const masks = useMemo(() => {
|
|
60
|
+
const authored = clearanceMasksFrom(context.document, {
|
|
61
|
+
kinds: [...DEFAULT_CLEARANCE_KINDS, "road", "walkway", "route", "path"],
|
|
62
|
+
});
|
|
63
|
+
// Keep the entire authored footing clear; feather regrowth outside its edge.
|
|
64
|
+
return {
|
|
65
|
+
discs: authored.discs.map(zone => ({ ...zone, radius: zone.radius + (zone.feather ?? 0) })),
|
|
66
|
+
corridors: authored.corridors.map(zone => ({ ...zone, halfWidth: zone.halfWidth + (zone.feather ?? 0) })),
|
|
67
|
+
};
|
|
68
|
+
}, [markers, paths, context.document.volumes]);
|
|
69
|
+
const coverageAt = useCallback((x, z) => resolved === null ? 0 :
|
|
70
|
+
1 - masksInfluence(x + resolved.center[0], z + resolved.center[2], masks), [resolved, masks]);
|
|
53
71
|
const wind = useMemo(() => resolved === null
|
|
54
72
|
? null
|
|
55
73
|
: { strength: resolved.rules.windStrength, speed: resolved.rules.windSpeed, gustScale: resolved.rules.windGust }, [resolved]);
|
|
@@ -68,7 +86,7 @@ function OneGrass({ object, context }) {
|
|
|
68
86
|
// footprint, and the fade only thins how many *draw* at range in the vertex shader β so a deeper
|
|
69
87
|
// meadow reads expansive without raising the instance ceiling.
|
|
70
88
|
const instanceCount = Math.max(1500, Math.min(250000, Math.ceil(rules.density * area[0] * area[1])));
|
|
71
|
-
return (_jsx(GrassField, { position: [center[0], center[1], center[2]], area: area, count: instanceCount, density: rules.density, bladeHeight: bladeHeight, colorBase: rules.colorBase, colorTip: rules.colorTip, colorGround: context.groundColorAt?.(center[0], center[2]), colorVariation: rules.colorVariation, roughness: rules.roughness, seed: hashSeed(rules.seed.length > 0 ? rules.seed : object.id), heightAt: heightAt, wind: wind, edgeFeather: 2.2, exclude: exclude }));
|
|
89
|
+
return (_jsx(GrassField, { position: [center[0], center[1], center[2]], area: area, count: instanceCount, density: rules.density, bladeHeight: bladeHeight, bladeWidth: bladeWidth, bladeBend: bladeBend, tuftRadius: rules.tuftRadius, normalLift: rules.normalLift, colorBase: rules.colorBase, colorTip: rules.colorTip, colorGround: context.groundColorAt?.(center[0], center[2]), colorVariation: rules.colorVariation, roughness: rules.roughness, seed: hashSeed(rules.seed.length > 0 ? rules.seed : object.id), heightAt: heightAt, coverageAt: coverageAt, wind: wind, edgeFeather: 2.2, exclude: exclude }));
|
|
72
90
|
}
|
|
73
91
|
/** Registers the `grass_field` volume renderer. Called by `registerBuiltinSceneKindRenderers`. @internal */
|
|
74
92
|
export function registerGrassFieldRenderer() {
|
|
@@ -11,6 +11,7 @@ export interface CarvedTerrainProps extends Omit<ThreeElements["mesh"], "args" |
|
|
|
11
11
|
colors?: FieldGroundOptions["colors"];
|
|
12
12
|
heightRange?: FieldGroundOptions["heightRange"];
|
|
13
13
|
paletteAt?: FieldGroundOptions["paletteAt"];
|
|
14
|
+
paintColorAt?: FieldGroundOptions["paintColorAt"];
|
|
14
15
|
roughness?: number;
|
|
15
16
|
metalness?: number;
|
|
16
17
|
/** Override the default vertex-colour material (e.g. a procedural detail material). The caller owns its lifecycle; it is not disposed here. */
|
|
@@ -23,4 +24,4 @@ export interface CarvedTerrainProps extends Omit<ThreeElements["mesh"], "args" |
|
|
|
23
24
|
* Because the geometry samples `field.sampleHeight`, a `CarvableField.carve(...)` shows as a real bowl
|
|
24
25
|
* once `epoch` changes. Pair with `InstancedBodies` to see debris resting in the crater it blasted.
|
|
25
26
|
*/
|
|
26
|
-
export declare function CarvedTerrain({ field, size, segments, center, colors, heightRange, paletteAt, roughness, metalness, surfaceMaterial, receiveShadow, epoch, ...meshProps }: CarvedTerrainProps): import("react").JSX.Element;
|
|
27
|
+
export declare function CarvedTerrain({ field, size, segments, center, colors, heightRange, paletteAt, paintColorAt, roughness, metalness, surfaceMaterial, receiveShadow, epoch, ...meshProps }: CarvedTerrainProps): import("react").JSX.Element;
|
|
@@ -8,8 +8,8 @@ import { createFieldGroundGeometry } from "./terrainMath.js";
|
|
|
8
8
|
* Because the geometry samples `field.sampleHeight`, a `CarvableField.carve(...)` shows as a real bowl
|
|
9
9
|
* once `epoch` changes. Pair with `InstancedBodies` to see debris resting in the crater it blasted.
|
|
10
10
|
*/
|
|
11
|
-
export function CarvedTerrain({ field, size, segments, center, colors, heightRange, paletteAt, roughness = 0.95, metalness = 0, surfaceMaterial, receiveShadow = true, epoch = 0, ...meshProps }) {
|
|
12
|
-
const geometry = useDisposable(() => createFieldGroundGeometry(field, { size, segments, center, colors, heightRange, paletteAt }), [field, size, segments, center, colors, heightRange, paletteAt, epoch]);
|
|
11
|
+
export function CarvedTerrain({ field, size, segments, center, colors, heightRange, paletteAt, paintColorAt, roughness = 0.95, metalness = 0, surfaceMaterial, receiveShadow = true, epoch = 0, ...meshProps }) {
|
|
12
|
+
const geometry = useDisposable(() => createFieldGroundGeometry(field, { size, segments, center, colors, heightRange, paletteAt, paintColorAt }), [field, size, segments, center, colors, heightRange, paletteAt, paintColorAt, epoch]);
|
|
13
13
|
const defaultMaterial = useDisposable(() => new THREE.MeshStandardMaterial({ color: "#ffffff", roughness, metalness, vertexColors: true }), [metalness, roughness]);
|
|
14
14
|
const material = surfaceMaterial ?? defaultMaterial;
|
|
15
15
|
return _jsx("mesh", { ...meshProps, geometry: geometry, material: material, receiveShadow: receiveShadow });
|
|
@@ -19,6 +19,7 @@ export interface GrassFieldProps extends Omit<ThreeElements["mesh"], "args" | "c
|
|
|
19
19
|
edgeFeather?: number;
|
|
20
20
|
exclude?: readonly GrassExclusion[];
|
|
21
21
|
heightAt?: TerrainHeightSampler;
|
|
22
|
+
coverageAt?: TerrainHeightSampler;
|
|
22
23
|
colorBase?: GrassMaterialOptions["colorBase"];
|
|
23
24
|
colorTip?: GrassMaterialOptions["colorTip"];
|
|
24
25
|
colorGround?: GrassMaterialOptions["colorGround"];
|
|
@@ -29,4 +30,4 @@ export interface GrassFieldProps extends Omit<ThreeElements["mesh"], "args" | "c
|
|
|
29
30
|
roughness?: number;
|
|
30
31
|
}
|
|
31
32
|
/** Seeded grass tufts with bounded per-camera chunk submission; density and budgets stay in blades. */
|
|
32
|
-
export declare function GrassField({ count, density, budget, area, seed, segments, bladeHeight, bladeWidth, bladeBend, tuftBlades, tuftRadius, edgeFeather, exclude, heightAt, colorBase, colorTip, colorGround, colorVariation, wind, distanceFade, normalLift, roughness, castShadow, receiveShadow, frustumCulled, onBeforeRender, onBeforeShadow, onAfterRender, onAfterShadow, ...meshProps }: GrassFieldProps): import("react").JSX.Element;
|
|
33
|
+
export declare function GrassField({ count, density, budget, area, seed, segments, bladeHeight, bladeWidth, bladeBend, tuftBlades, tuftRadius, edgeFeather, exclude, heightAt, coverageAt, colorBase, colorTip, colorGround, colorVariation, wind, distanceFade, normalLift, roughness, castShadow, receiveShadow, frustumCulled, onBeforeRender, onBeforeShadow, onAfterRender, onAfterShadow, ...meshProps }: GrassFieldProps): import("react").JSX.Element;
|
|
@@ -9,7 +9,7 @@ import { DEFAULT_GRASS_COUNT, DEFAULT_GRASS_DENSITY, resolveGrassInstanceBudget,
|
|
|
9
9
|
export { DEFAULT_GRASS_COUNT, DEFAULT_GRASS_DENSITY, resolveGrassInstanceBudget } from "./grassBudget.js";
|
|
10
10
|
export { GRASS_TUFT_BLADES } from "./grassGeometry.js";
|
|
11
11
|
/** Seeded grass tufts with bounded per-camera chunk submission; density and budgets stay in blades. */
|
|
12
|
-
export function GrassField({ count = DEFAULT_GRASS_COUNT, density = DEFAULT_GRASS_DENSITY, budget, area = 40, seed = 1, segments = 4, bladeHeight, bladeWidth, bladeBend, tuftBlades = GRASS_TUFT_BLADES, tuftRadius, edgeFeather, exclude, heightAt, colorBase, colorTip, colorGround, colorVariation, wind, distanceFade, normalLift, roughness, castShadow = false, receiveShadow = true, frustumCulled = true, onBeforeRender, onBeforeShadow, onAfterRender, onAfterShadow, ...meshProps }) {
|
|
12
|
+
export function GrassField({ count = DEFAULT_GRASS_COUNT, density = DEFAULT_GRASS_DENSITY, budget, area = 40, seed = 1, segments = 4, bladeHeight, bladeWidth, bladeBend, tuftBlades = GRASS_TUFT_BLADES, tuftRadius, edgeFeather, exclude, heightAt, coverageAt, colorBase, colorTip, colorGround, colorVariation, wind, distanceFade, normalLift, roughness, castShadow = false, receiveShadow = true, frustumCulled = true, onBeforeRender, onBeforeShadow, onAfterRender, onAfterShadow, ...meshProps }) {
|
|
13
13
|
const chunks = useMemo(() => createGrassGeometryChunks({
|
|
14
14
|
count,
|
|
15
15
|
area,
|
|
@@ -23,7 +23,8 @@ export function GrassField({ count = DEFAULT_GRASS_COUNT, density = DEFAULT_GRAS
|
|
|
23
23
|
...(edgeFeather === undefined ? {} : { edgeFeather }),
|
|
24
24
|
...(exclude === undefined ? {} : { exclude }),
|
|
25
25
|
heightAt,
|
|
26
|
-
|
|
26
|
+
coverageAt,
|
|
27
|
+
}), [area, bladeBend, bladeHeight, bladeWidth, count, coverageAt, edgeFeather, exclude, heightAt, seed, segments, tuftBlades, tuftRadius]);
|
|
27
28
|
useDisposable(() => chunks.map((chunk) => chunk.geometry), [chunks]);
|
|
28
29
|
const handle = useMemo(() => createGrassMaterial({
|
|
29
30
|
colorBase,
|
|
@@ -31,6 +31,8 @@ export interface GrassBladeGeometryOptions {
|
|
|
31
31
|
/** Patch-local rectangles kept clear of blades (soil patches, water) with a soft regrow ramp. */
|
|
32
32
|
exclude?: readonly GrassExclusion[];
|
|
33
33
|
heightAt?: TerrainHeightSampler;
|
|
34
|
+
/** Patch-local coverage, 0..1, composed with explicit rectangular exclusions. */
|
|
35
|
+
coverageAt?: TerrainHeightSampler;
|
|
34
36
|
}
|
|
35
37
|
export interface ResolvedGrassBladeGeometryOptions {
|
|
36
38
|
count: number;
|
|
@@ -45,6 +47,7 @@ export interface ResolvedGrassBladeGeometryOptions {
|
|
|
45
47
|
edgeFeather: number;
|
|
46
48
|
exclude: readonly GrassExclusion[];
|
|
47
49
|
heightAt: TerrainHeightSampler;
|
|
50
|
+
coverageAt: TerrainHeightSampler;
|
|
48
51
|
}
|
|
49
52
|
/** @internal */
|
|
50
53
|
export declare function resolveGrassRange(value: GrassRange | undefined, fallback: readonly [number, number]): readonly [number, number];
|
|
@@ -24,6 +24,7 @@ export function resolveGrassBladeGeometryOptions(options = {}) {
|
|
|
24
24
|
edgeFeather: Math.max(0, options.edgeFeather ?? 1.5),
|
|
25
25
|
exclude: options.exclude ?? [],
|
|
26
26
|
heightAt: options.heightAt ?? (() => 0),
|
|
27
|
+
coverageAt: options.coverageAt ?? (() => 1),
|
|
27
28
|
};
|
|
28
29
|
}
|
|
29
30
|
/** Tuft instances needed to carry `bladeCount` blades at `tuftBlades` blades per tuft. @internal */
|
|
@@ -123,7 +124,7 @@ export function createGrassBladeGeometry(options = {}) {
|
|
|
123
124
|
const d = Math.max(Math.abs(x - zone.center[0]) - zone.half[0], Math.abs(z - zone.center[1]) - zone.half[1]);
|
|
124
125
|
heightScale *= Math.min(1, Math.max(0, d / Math.max(0.001, zone.feather ?? 1.5)));
|
|
125
126
|
}
|
|
126
|
-
heights[index] = heightScale;
|
|
127
|
+
heights[index] = heightScale * Math.max(0, Math.min(1, resolved.coverageAt(x, z)));
|
|
127
128
|
widths[index] = randomRange(random, resolved.width);
|
|
128
129
|
bend[index] = randomRange(random, resolved.bend);
|
|
129
130
|
phase[index] = random() * Math.PI * 2;
|
package/dist/terrain/index.d.ts
CHANGED
|
@@ -9,3 +9,4 @@ export { createGrassMaterial, DEFAULT_GRASS_WIND, resolveGrassWind, type GrassMa
|
|
|
9
9
|
export { createSeededRandom, hashNoise2, seedToUint32, type TerrainSeed } from "./random.js";
|
|
10
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";
|
|
12
|
+
export { createTerrainPaintColorSampler, type TerrainPaintColorSampler, type TerrainPaintSource } from "./terrainPaintColor.js";
|
package/dist/terrain/index.js
CHANGED
|
@@ -9,3 +9,4 @@ export { createGrassMaterial, DEFAULT_GRASS_WIND, resolveGrassWind, } from "./gr
|
|
|
9
9
|
export { createSeededRandom, hashNoise2, seedToUint32 } from "./random.js";
|
|
10
10
|
export { createFieldGroundGeometry, createProceduralGroundGeometry, createProceduralTerrainSampler, normalizeHeightBlend, resolveTerrainSegments, resolveTerrainSize, toNoiseFieldConfig, } from "./terrainMath.js";
|
|
11
11
|
export { arenaField, flatField, fractalNoise, noiseField, resolveGroundStep, resolveTerrainField, valueNoise, withNormal, } from "@jgengine/core/world/terrain";
|
|
12
|
+
export { createTerrainPaintColorSampler } from "./terrainPaintColor.js";
|
|
@@ -82,6 +82,8 @@ export interface FieldGroundOptions {
|
|
|
82
82
|
paletteAt?: TerrainPaletteSampler;
|
|
83
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
84
|
surface?: TerrainSurfaceColorOptions;
|
|
85
|
+
/** Authored paint overrides descriptor shading at painted vertices. */
|
|
86
|
+
paintColorAt?: (x: number, z: number, out: THREE.Color) => boolean;
|
|
85
87
|
}
|
|
86
88
|
/**
|
|
87
89
|
* Mesh any `TerrainField` β including a `CarvableField` with craters/mounds written into it β into a
|
|
@@ -106,6 +106,7 @@ export function createFieldGroundGeometry(field, options = {}) {
|
|
|
106
106
|
heightRange: options.heightRange,
|
|
107
107
|
paletteAt: options.paletteAt,
|
|
108
108
|
surface: options.surface,
|
|
109
|
+
paintColorAt: options.paintColorAt,
|
|
109
110
|
});
|
|
110
111
|
}
|
|
111
112
|
function buildGroundGeometry(size, segments, sampler, opts) {
|
|
@@ -162,6 +163,7 @@ function buildGroundGeometry(size, segments, sampler, opts) {
|
|
|
162
163
|
slope01 = grad / (grad + 1);
|
|
163
164
|
}
|
|
164
165
|
applyTerrainSurfaceShading(color, x, z, slope01, surface);
|
|
166
|
+
opts.paintColorAt?.(x, z, color);
|
|
165
167
|
colorValues[colorIndex] = color.r;
|
|
166
168
|
colorValues[colorIndex + 1] = color.g;
|
|
167
169
|
colorValues[colorIndex + 2] = color.b;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import type { EditableTerrain } from "@jgengine/core/world/terraform";
|
|
3
|
+
/** Live authored paint channels needed to sample a terrain's material stack without its heightfield. */
|
|
4
|
+
export type TerrainPaintSource = Pick<EditableTerrain, "layers" | "weightsAt" | "surfaceAt">;
|
|
5
|
+
/** Writes authored paint into an existing linear color; returns false for unpainted ground. */
|
|
6
|
+
export type TerrainPaintColorSampler = (x: number, z: number, out: THREE.Color) => boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Shares layer tint, opacity and linear weighted-color blending between editor and runtime ground.
|
|
9
|
+
* @capability terrain-paint-color sample authored terrain surfaces and blended layers for shared ground rendering
|
|
10
|
+
*/
|
|
11
|
+
export declare function createTerrainPaintColorSampler(terrain: TerrainPaintSource): TerrainPaintColorSampler;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { TERRAIN_MATERIAL_COLORS } from "@jgengine/core/editor/terrainMaterials";
|
|
3
|
+
/**
|
|
4
|
+
* Shares layer tint, opacity and linear weighted-color blending between editor and runtime ground.
|
|
5
|
+
* @capability terrain-paint-color sample authored terrain surfaces and blended layers for shared ground rendering
|
|
6
|
+
*/
|
|
7
|
+
export function createTerrainPaintColorSampler(terrain) {
|
|
8
|
+
const layers = terrain.layers;
|
|
9
|
+
const colors = layers.map(layer => new THREE.Color(layer.tint ?? TERRAIN_MATERIAL_COLORS[layer.surface] ?? "#7c7f86"));
|
|
10
|
+
const defaults = new Map(Object.entries(TERRAIN_MATERIAL_COLORS).map(([id, color]) => [id, new THREE.Color(color)]));
|
|
11
|
+
return (x, z, out) => {
|
|
12
|
+
const weights = terrain.weightsAt(x, z);
|
|
13
|
+
if (layers.length > 0 && weights.length === layers.length) {
|
|
14
|
+
let total = 0, r = 0, g = 0, b = 0;
|
|
15
|
+
for (let i = 0; i < layers.length; i++) {
|
|
16
|
+
const weight = weights[i] * (layers[i].opacity ?? 1);
|
|
17
|
+
if (weight <= 0)
|
|
18
|
+
continue;
|
|
19
|
+
const color = colors[i];
|
|
20
|
+
r += color.r * weight;
|
|
21
|
+
g += color.g * weight;
|
|
22
|
+
b += color.b * weight;
|
|
23
|
+
total += weight;
|
|
24
|
+
}
|
|
25
|
+
if (total > 0) {
|
|
26
|
+
out.setRGB(r / total, g / total, b / total);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
const surface = terrain.surfaceAt(x, z);
|
|
32
|
+
if (surface === null)
|
|
33
|
+
return false;
|
|
34
|
+
const index = layers.findIndex(layer => layer.surface === surface);
|
|
35
|
+
if (index >= 0) {
|
|
36
|
+
if ((layers[index].opacity ?? 1) <= 0)
|
|
37
|
+
return false;
|
|
38
|
+
out.copy(colors[index]);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
const color = defaults.get(surface);
|
|
42
|
+
if (color === undefined)
|
|
43
|
+
return false;
|
|
44
|
+
out.copy(color);
|
|
45
|
+
return true;
|
|
46
|
+
};
|
|
47
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgengine/shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Game player shell for JGengine: React Three Fiber canvas, orbit camera, input tracking, HUD mounting, GameUiPreview, and a demo game. Consumers supply a GameRegistry.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"test": "bun test src"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@jgengine/core": "^0.
|
|
39
|
-
"@jgengine/react": "^0.
|
|
40
|
-
"@jgengine/ws": "^0.
|
|
38
|
+
"@jgengine/core": "^0.22.0",
|
|
39
|
+
"@jgengine/react": "^0.22.0",
|
|
40
|
+
"@jgengine/ws": "^0.22.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"@react-three/drei": "^10.0.0",
|
|
@@ -11,6 +11,8 @@ Own the intended player experience and the rules that produce it: fantasy, pilla
|
|
|
11
11
|
|
|
12
12
|
Use `level-design` for spatial realization; route implementation to `jgengine-gameplay`, `jgengine-world`, `jgengine-combat`, `jgengine-ui`, `jgengine-multiplayer` once intent is coherent.
|
|
13
13
|
|
|
14
|
+
For a substantial existing-game quality pass spanning concept, depth, world and presentation, use `game-improvement` to sequence these owners and keep a per-game implementation plan and acceptance evidence.
|
|
15
|
+
|
|
14
16
|
For a greenfield build, read [references/application-playbook.md](references/application-playbook.md) and produce its greenfield contract (promise, pillars, anti-pillars, core decision, and falsifier) before writing code. Audits may read only the playbook's audit sections. Read [references/field-guide.md](references/field-guide.md) when the task needs deeper loop, economy, progression, genre, accessibility, or research guidance.
|
|
15
17
|
|
|
16
18
|
## Choose the mode
|
package/skills/jgengine/SKILL.md
CHANGED
|
@@ -53,6 +53,7 @@ Load only rows the target needs. Do not preload every domain.
|
|
|
53
53
|
| Need | Skill | Owner |
|
|
54
54
|
| --- | --- | --- |
|
|
55
55
|
| player promise, pillars, verbs, loops, economy, progression, balance | `game-design` | experience and rule intent |
|
|
56
|
+
| substantial existing-game improvement across depth, world and presentation | `game-improvement` | ordered improvement plan and per-game completion |
|
|
56
57
|
| metrics, topology, encounters, navigation, spatial pacing | `level-design` | playable space intent |
|
|
57
58
|
| scene placement, terrain painting, paths, asset authoring | `jgengine-editor` | editor document and authoring tools |
|
|
58
59
|
| runtime world, movement, input, interaction, AI, navigation, audio | `jgengine-world` | spatial simulation and world runtime |
|
package/skills/jgengine/api.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
- `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> β Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
|
|
8
8
|
- `ChangelogEntry` (interface): interface ChangelogEntry β One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
|
|
9
|
-
- `VERSION` (const): const VERSION: "0.
|
|
9
|
+
- `VERSION` (const): const VERSION: "0.22.0" β Installed `@jgengine/core` semver β compare against {@link CHANGELOG} keys when migrating.
|
|
10
10
|
|
|
11
11
|
## @jgengine/core/authoring
|
|
12
12
|
|
|
@@ -232,7 +232,7 @@
|
|
|
232
232
|
|
|
233
233
|
- `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> β Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
|
|
234
234
|
- `ChangelogEntry` (interface): interface ChangelogEntry β One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
|
|
235
|
-
- `VERSION` (const): const VERSION: "0.
|
|
235
|
+
- `VERSION` (const): const VERSION: "0.22.0" β Installed `@jgengine/core` semver β compare against {@link CHANGELOG} keys when migrating.
|
|
236
236
|
|
|
237
237
|
## @jgengine/core/runtime/adapter
|
|
238
238
|
|
|
@@ -2702,7 +2702,7 @@
|
|
|
2702
2702
|
|
|
2703
2703
|
## @jgengine/shell/terrain
|
|
2704
2704
|
|
|
2705
|
-
- `CarvedTerrain` (function): function CarvedTerrain({ field, size, segments, center, colors, heightRange, paletteAt, roughness = 0.95, metalness = 0, surfaceMaterial, receiveShadow = true, epoch = 0, ...meshProps }: CarvedTerrainProps): React.JSX.Element β Renders a `TerrainField` as a deformed ground mesh β the crater/mound view for destructible terrain. Because the geometry samples `field.sampleHeight`, a `CarvableField.carve(...)` shows as a real bowl once `epoch` changes. Pair with `InstancedBodies` to see debris resting in the crater it blasted.
|
|
2705
|
+
- `CarvedTerrain` (function): function CarvedTerrain({ field, size, segments, center, colors, heightRange, paletteAt, paintColorAt, roughness = 0.95, metalness = 0, surfaceMaterial, receiveShadow = true, epoch = 0, ...meshProps }: CarvedTerrainProps): React.JSX.Element β Renders a `TerrainField` as a deformed ground mesh β the crater/mound view for destructible terrain. Because the geometry samples `field.sampleHeight`, a `CarvableField.carve(...)` shows as a real bowl once `epoch` changes. Pair with `InstancedBodies` to see debris resting in the crater it blasted.
|
|
2706
2706
|
- `CarvedTerrainProps` (interface): interface CarvedTerrainProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material"> { field: TerrainField; size?: FieldGroundOptions["size"]; segments?: FieldGroundOptions["segments"]; center?: FieldGroundOptions["center"]; colors?: FieldGroundOptions["colors"]; heightRange?: FieldGroundOptions["heightRange"]; paletteAt?: β¦ β β undocumented Β· used by `CarvedTerrain`: Renders a `TerrainField` as a deformed ground mesh β the crater/mound view for destructible terrain.
|
|
2707
2707
|
- `DEFAULT_GRASS_WIND` (const): const DEFAULT_GRASS_WIND: Required<GrassWindOptions> β β undocumented
|
|
2708
2708
|
- `EditableGround` (function): function EditableGround({ terrain, bounds, segments = 96, version = 0, baseColor = "#3f6b3a", surfaceColors = DEFAULT_SURFACE_COLORS, }: EditableGroundProps): React.JSX.Element β β undocumented
|
|
@@ -2710,7 +2710,7 @@
|
|
|
2710
2710
|
- `FieldGroundOptions` (interface): interface FieldGroundOptions { size?: TerrainArea; segments?: number | readonly [x: number, z: number]; center?: readonly [x: number, z: number]; colors?: TerrainVertexColorOptions; heightRange?: readonly [min: number, max: number]; paletteAt?: TerrainPaletteSampler; sβ¦ β β undocumented
|
|
2711
2711
|
- `FractalNoiseConfig` (interface): interface FractalNoiseConfig β Octave settings for {@link fractalNoise}: frequency, layering, and optional ridged shaping.
|
|
2712
2712
|
- `GrassBladeGeometryOptions` (interface): interface GrassBladeGeometryOptions { count?: number; area?: TerrainArea; seed?: TerrainSeed; segments?: number; height?: GrassRange; width?: GrassRange; bend?: GrassRange; tuftBlades?: number; tuftRadius?: number; edgeFeather?: number; exclude?: readonly GrassExclusion[]; heβ¦ β β undocumented
|
|
2713
|
-
- `GrassField` (function): function GrassField({ count = DEFAULT_GRASS_COUNT, density = DEFAULT_GRASS_DENSITY, budget, area = 40, seed = 1, segments = 4, bladeHeight, bladeWidth, bladeBend, tuftBlades = GRASS_TUFT_BLADES, tuftRadius, edgeFeather, exclude, heightAt, colorBase, colorTip, colorGround, colorVariation
|
|
2713
|
+
- `GrassField` (function): function GrassField({ count = DEFAULT_GRASS_COUNT, density = DEFAULT_GRASS_DENSITY, budget, area = 40, seed = 1, segments = 4, bladeHeight, bladeWidth, bladeBend, tuftBlades = GRASS_TUFT_BLADES, tuftRadius, edgeFeather, exclude, heightAt, coverageAt, colorBase, colorTip, colorGround, colorVariation,β¦ β Seeded grass tufts with bounded per-camera chunk submission; density and budgets stay in blades.
|
|
2714
2714
|
- `GrassFieldProps` (interface): interface GrassFieldProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material"> { count?: number; density?: number; budget?: number; area?: TerrainArea; seed?: GrassBladeGeometryOptions["seed"]; segments?: number; bladeHeight?: GrassRange; bladeWidth?: GrassRange; bladeBend?: GrassRange; tuftBlades?: number; tuftRadiusβ¦ β β undocumented
|
|
2715
2715
|
- `GrassMaterialHandle` (interface): interface GrassMaterialHandle { material: THREE.MeshStandardMaterial; uniforms: GrassShaderUniforms } β β undocumented
|
|
2716
2716
|
- `GrassMaterialOptions` (interface): interface GrassMaterialOptions { colorBase?: THREE.ColorRepresentation; colorTip?: THREE.ColorRepresentation; colorGround?: THREE.ColorRepresentation; colorVariation?: number; wind?: GrassWindOptions | false; distanceFade?: GrassDistanceFadeOptions | false; normalLift?: β¦ β β undocumented
|
|
@@ -2733,14 +2733,17 @@
|
|
|
2733
2733
|
- `TerrainField` (interface): interface TerrainField β A sampleable ground surface: height and normal at any x/z, with optional bounds and water level.
|
|
2734
2734
|
- `TerrainHeightSampler` (type): type TerrainHeightSampler = (x: number, z: number) => number β β undocumented
|
|
2735
2735
|
- `TerrainNormal` (type): type TerrainNormal = readonly [number, number, number] β A surface normal vector at a terrain sample point.
|
|
2736
|
+
- `TerrainPaintColorSampler` (type): type TerrainPaintColorSampler = (x: number, z: number, out: THREE.Color) => boolean β Writes authored paint into an existing linear color; returns false for unpainted ground.
|
|
2737
|
+
- `TerrainPaintSource` (type): type TerrainPaintSource = Pick<EditableTerrain, "layers" | "weightsAt" | "surfaceAt"> β Live authored paint channels needed to sample a terrain's material stack without its heightfield.
|
|
2736
2738
|
- `TerrainSeed` (type): type TerrainSeed = number | string β β undocumented
|
|
2737
2739
|
- `TerrainSurfaceColorOptions` (interface): interface TerrainSurfaceColorOptions β Slope/noise surface shading laid over the height lerp so untextured ground reads like terrain instead of a smooth two-tone gradient. All knobs are optional with sane defaults; every field is a pure function of vertex position (+ `seed`), so the coloring is stable across rebuilds and tests.
|
|
2738
2740
|
- `TerrainVertexColorOptions` (interface): interface TerrainVertexColorOptions { low?: THREE.ColorRepresentation; high?: THREE.ColorRepresentation; waterline?: THREE.ColorRepresentation; waterlineHeight?: number } β β undocumented
|
|
2741
|
+
- `createTerrainPaintColorSampler` (function): function createTerrainPaintColorSampler(terrain: TerrainPaintSource): TerrainPaintColorSampler β Shares layer tint, opacity and linear weighted-color blending between editor and runtime ground.
|
|
2739
2742
|
- `displaceHeightfieldGeometry` (function): function displaceHeightfieldGeometry(geometry: THREE.BufferGeometry, sampleHeight: (x: number, z: number) => number, options: HeightfieldDisplaceOptions): void β Re-samples a `PlaneGeometry(width, depth, segments, segments).rotateX(-Ο/2)` heightfield mesh from a `sampleHeight` field in place: vertex Y, optional vertex color, grid central-difference normals, and an analytically maintained bounding sphere. With a dirty `region`, the work is O(region vertices) β heights/colors update inside the covering vertex window, normals inside the window plus a one-vertex ring β never a whole-mesh `computeVertexNormals`/`computeBoundingSphere` pass, which is what keeps per-frame brush stamps inside the editor's frame budget. The bounding sphere derives from the fixed plane extents plus a running height range kept on `geometry.userData`; partial passes only expand it, a full pass resets it exactly.
|
|
2740
2743
|
|
|
2741
2744
|
## @jgengine/shell/terrain
|
|
2742
2745
|
|
|
2743
|
-
- `CarvedTerrain` (function): function CarvedTerrain({ field, size, segments, center, colors, heightRange, paletteAt, roughness = 0.95, metalness = 0, surfaceMaterial, receiveShadow = true, epoch = 0, ...meshProps }: CarvedTerrainProps): React.JSX.Element β Renders a `TerrainField` as a deformed ground mesh β the crater/mound view for destructible terrain. Because the geometry samples `field.sampleHeight`, a `CarvableField.carve(...)` shows as a real bowl once `epoch` changes. Pair with `InstancedBodies` to see debris resting in the crater it blasted.
|
|
2746
|
+
- `CarvedTerrain` (function): function CarvedTerrain({ field, size, segments, center, colors, heightRange, paletteAt, paintColorAt, roughness = 0.95, metalness = 0, surfaceMaterial, receiveShadow = true, epoch = 0, ...meshProps }: CarvedTerrainProps): React.JSX.Element β Renders a `TerrainField` as a deformed ground mesh β the crater/mound view for destructible terrain. Because the geometry samples `field.sampleHeight`, a `CarvableField.carve(...)` shows as a real bowl once `epoch` changes. Pair with `InstancedBodies` to see debris resting in the crater it blasted.
|
|
2744
2747
|
- `CarvedTerrainProps` (interface): interface CarvedTerrainProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material"> { field: TerrainField; size?: FieldGroundOptions["size"]; segments?: FieldGroundOptions["segments"]; center?: FieldGroundOptions["center"]; colors?: FieldGroundOptions["colors"]; heightRange?: FieldGroundOptions["heightRange"]; paletteAt?: β¦ β β undocumented Β· used by `CarvedTerrain`: Renders a `TerrainField` as a deformed ground mesh β the crater/mound view for destructible terrain.
|
|
2745
2748
|
- `DEFAULT_GRASS_WIND` (const): const DEFAULT_GRASS_WIND: Required<GrassWindOptions> β β undocumented
|
|
2746
2749
|
- `EditableGround` (function): function EditableGround({ terrain, bounds, segments = 96, version = 0, baseColor = "#3f6b3a", surfaceColors = DEFAULT_SURFACE_COLORS, }: EditableGroundProps): React.JSX.Element β β undocumented
|
|
@@ -2748,7 +2751,7 @@
|
|
|
2748
2751
|
- `FieldGroundOptions` (interface): interface FieldGroundOptions { size?: TerrainArea; segments?: number | readonly [x: number, z: number]; center?: readonly [x: number, z: number]; colors?: TerrainVertexColorOptions; heightRange?: readonly [min: number, max: number]; paletteAt?: TerrainPaletteSampler; sβ¦ β β undocumented
|
|
2749
2752
|
- `FractalNoiseConfig` (interface): interface FractalNoiseConfig β Octave settings for {@link fractalNoise}: frequency, layering, and optional ridged shaping.
|
|
2750
2753
|
- `GrassBladeGeometryOptions` (interface): interface GrassBladeGeometryOptions { count?: number; area?: TerrainArea; seed?: TerrainSeed; segments?: number; height?: GrassRange; width?: GrassRange; bend?: GrassRange; tuftBlades?: number; tuftRadius?: number; edgeFeather?: number; exclude?: readonly GrassExclusion[]; heβ¦ β β undocumented
|
|
2751
|
-
- `GrassField` (function): function GrassField({ count = DEFAULT_GRASS_COUNT, density = DEFAULT_GRASS_DENSITY, budget, area = 40, seed = 1, segments = 4, bladeHeight, bladeWidth, bladeBend, tuftBlades = GRASS_TUFT_BLADES, tuftRadius, edgeFeather, exclude, heightAt, colorBase, colorTip, colorGround, colorVariation
|
|
2754
|
+
- `GrassField` (function): function GrassField({ count = DEFAULT_GRASS_COUNT, density = DEFAULT_GRASS_DENSITY, budget, area = 40, seed = 1, segments = 4, bladeHeight, bladeWidth, bladeBend, tuftBlades = GRASS_TUFT_BLADES, tuftRadius, edgeFeather, exclude, heightAt, coverageAt, colorBase, colorTip, colorGround, colorVariation,β¦ β Seeded grass tufts with bounded per-camera chunk submission; density and budgets stay in blades.
|
|
2752
2755
|
- `GrassFieldProps` (interface): interface GrassFieldProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material"> { count?: number; density?: number; budget?: number; area?: TerrainArea; seed?: GrassBladeGeometryOptions["seed"]; segments?: number; bladeHeight?: GrassRange; bladeWidth?: GrassRange; bladeBend?: GrassRange; tuftBlades?: number; tuftRadiusβ¦ β β undocumented
|
|
2753
2756
|
- `GrassMaterialHandle` (interface): interface GrassMaterialHandle { material: THREE.MeshStandardMaterial; uniforms: GrassShaderUniforms } β β undocumented
|
|
2754
2757
|
- `GrassMaterialOptions` (interface): interface GrassMaterialOptions { colorBase?: THREE.ColorRepresentation; colorTip?: THREE.ColorRepresentation; colorGround?: THREE.ColorRepresentation; colorVariation?: number; wind?: GrassWindOptions | false; distanceFade?: GrassDistanceFadeOptions | false; normalLift?: β¦ β β undocumented
|
|
@@ -2771,14 +2774,17 @@
|
|
|
2771
2774
|
- `TerrainField` (interface): interface TerrainField β A sampleable ground surface: height and normal at any x/z, with optional bounds and water level.
|
|
2772
2775
|
- `TerrainHeightSampler` (type): type TerrainHeightSampler = (x: number, z: number) => number β β undocumented
|
|
2773
2776
|
- `TerrainNormal` (type): type TerrainNormal = readonly [number, number, number] β A surface normal vector at a terrain sample point.
|
|
2777
|
+
- `TerrainPaintColorSampler` (type): type TerrainPaintColorSampler = (x: number, z: number, out: THREE.Color) => boolean β Writes authored paint into an existing linear color; returns false for unpainted ground.
|
|
2778
|
+
- `TerrainPaintSource` (type): type TerrainPaintSource = Pick<EditableTerrain, "layers" | "weightsAt" | "surfaceAt"> β Live authored paint channels needed to sample a terrain's material stack without its heightfield.
|
|
2774
2779
|
- `TerrainSeed` (type): type TerrainSeed = number | string β β undocumented
|
|
2775
2780
|
- `TerrainSurfaceColorOptions` (interface): interface TerrainSurfaceColorOptions β Slope/noise surface shading laid over the height lerp so untextured ground reads like terrain instead of a smooth two-tone gradient. All knobs are optional with sane defaults; every field is a pure function of vertex position (+ `seed`), so the coloring is stable across rebuilds and tests.
|
|
2776
2781
|
- `TerrainVertexColorOptions` (interface): interface TerrainVertexColorOptions { low?: THREE.ColorRepresentation; high?: THREE.ColorRepresentation; waterline?: THREE.ColorRepresentation; waterlineHeight?: number } β β undocumented
|
|
2782
|
+
- `createTerrainPaintColorSampler` (function): function createTerrainPaintColorSampler(terrain: TerrainPaintSource): TerrainPaintColorSampler β Shares layer tint, opacity and linear weighted-color blending between editor and runtime ground.
|
|
2777
2783
|
- `displaceHeightfieldGeometry` (function): function displaceHeightfieldGeometry(geometry: THREE.BufferGeometry, sampleHeight: (x: number, z: number) => number, options: HeightfieldDisplaceOptions): void β Re-samples a `PlaneGeometry(width, depth, segments, segments).rotateX(-Ο/2)` heightfield mesh from a `sampleHeight` field in place: vertex Y, optional vertex color, grid central-difference normals, and an analytically maintained bounding sphere. With a dirty `region`, the work is O(region vertices) β heights/colors update inside the covering vertex window, normals inside the window plus a one-vertex ring β never a whole-mesh `computeVertexNormals`/`computeBoundingSphere` pass, which is what keeps per-frame brush stamps inside the editor's frame budget. The bounding sphere derives from the fixed plane extents plus a running height range kept on `geometry.userData`; partial passes only expand it, a full pass resets it exactly.
|
|
2778
2784
|
|
|
2779
2785
|
## @jgengine/shell/terrain/CarvedTerrain
|
|
2780
2786
|
|
|
2781
|
-
- `CarvedTerrain` (function): function CarvedTerrain({ field, size, segments, center, colors, heightRange, paletteAt, roughness = 0.95, metalness = 0, surfaceMaterial, receiveShadow = true, epoch = 0, ...meshProps }: CarvedTerrainProps): React.JSX.Element β Renders a `TerrainField` as a deformed ground mesh β the crater/mound view for destructible terrain. Because the geometry samples `field.sampleHeight`, a `CarvableField.carve(...)` shows as a real bowl once `epoch` changes. Pair with `InstancedBodies` to see debris resting in the crater it blasted.
|
|
2787
|
+
- `CarvedTerrain` (function): function CarvedTerrain({ field, size, segments, center, colors, heightRange, paletteAt, paintColorAt, roughness = 0.95, metalness = 0, surfaceMaterial, receiveShadow = true, epoch = 0, ...meshProps }: CarvedTerrainProps): React.JSX.Element β Renders a `TerrainField` as a deformed ground mesh β the crater/mound view for destructible terrain. Because the geometry samples `field.sampleHeight`, a `CarvableField.carve(...)` shows as a real bowl once `epoch` changes. Pair with `InstancedBodies` to see debris resting in the crater it blasted.
|
|
2782
2788
|
- `CarvedTerrainProps` (interface): interface CarvedTerrainProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material"> { field: TerrainField; size?: FieldGroundOptions["size"]; segments?: FieldGroundOptions["segments"]; center?: FieldGroundOptions["center"]; colors?: FieldGroundOptions["colors"]; heightRange?: FieldGroundOptions["heightRange"]; paletteAt?: β¦ β β undocumented Β· used by `CarvedTerrain`: Renders a `TerrainField` as a deformed ground mesh β the crater/mound view for destructible terrain.
|
|
2783
2789
|
|
|
2784
2790
|
## @jgengine/shell/terrain/EditableGround
|
|
@@ -2791,7 +2797,7 @@
|
|
|
2791
2797
|
- `DEFAULT_GRASS_COUNT` (const): const DEFAULT_GRASS_COUNT: 1500 β β undocumented
|
|
2792
2798
|
- `DEFAULT_GRASS_DENSITY` (const): const DEFAULT_GRASS_DENSITY: 4 β Blades per square meter β matches `@jgengine/core/world/vegetation`'s `VEGETATION_DEFAULTS.density`, so an editor-placed grass volume and a bare `<GrassField>` agree on what "4" means.
|
|
2793
2799
|
- `GRASS_TUFT_BLADES` (const): const GRASS_TUFT_BLADES: 5 β Blades baked into each tuft instance β one instance reads as a clump, not a lone blade.
|
|
2794
|
-
- `GrassField` (function): function GrassField({ count = DEFAULT_GRASS_COUNT, density = DEFAULT_GRASS_DENSITY, budget, area = 40, seed = 1, segments = 4, bladeHeight, bladeWidth, bladeBend, tuftBlades = GRASS_TUFT_BLADES, tuftRadius, edgeFeather, exclude, heightAt, colorBase, colorTip, colorGround, colorVariation
|
|
2800
|
+
- `GrassField` (function): function GrassField({ count = DEFAULT_GRASS_COUNT, density = DEFAULT_GRASS_DENSITY, budget, area = 40, seed = 1, segments = 4, bladeHeight, bladeWidth, bladeBend, tuftBlades = GRASS_TUFT_BLADES, tuftRadius, edgeFeather, exclude, heightAt, coverageAt, colorBase, colorTip, colorGround, colorVariation,β¦ β Seeded grass tufts with bounded per-camera chunk submission; density and budgets stay in blades.
|
|
2795
2801
|
- `GrassFieldProps` (interface): interface GrassFieldProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material"> { count?: number; density?: number; budget?: number; area?: TerrainArea; seed?: GrassBladeGeometryOptions["seed"]; segments?: number; bladeHeight?: GrassRange; bladeWidth?: GrassRange; bladeBend?: GrassRange; tuftBlades?: number; tuftRadiusβ¦ β β undocumented
|
|
2796
2802
|
|
|
2797
2803
|
## @jgengine/shell/terrain/ProceduralGround
|
|
@@ -2859,6 +2865,12 @@
|
|
|
2859
2865
|
- `TerrainSurfaceColorOptions` (interface): interface TerrainSurfaceColorOptions β Slope/noise surface shading laid over the height lerp so untextured ground reads like terrain instead of a smooth two-tone gradient. All knobs are optional with sane defaults; every field is a pure function of vertex position (+ `seed`), so the coloring is stable across rebuilds and tests.
|
|
2860
2866
|
- `TerrainVertexColorOptions` (interface): interface TerrainVertexColorOptions { low?: THREE.ColorRepresentation; high?: THREE.ColorRepresentation; waterline?: THREE.ColorRepresentation; waterlineHeight?: number } β β undocumented
|
|
2861
2867
|
|
|
2868
|
+
## @jgengine/shell/terrain/terrainPaintColor
|
|
2869
|
+
|
|
2870
|
+
- `TerrainPaintColorSampler` (type): type TerrainPaintColorSampler = (x: number, z: number, out: THREE.Color) => boolean β Writes authored paint into an existing linear color; returns false for unpainted ground.
|
|
2871
|
+
- `TerrainPaintSource` (type): type TerrainPaintSource = Pick<EditableTerrain, "layers" | "weightsAt" | "surfaceAt"> β Live authored paint channels needed to sample a terrain's material stack without its heightfield.
|
|
2872
|
+
- `createTerrainPaintColorSampler` (function): function createTerrainPaintColorSampler(terrain: TerrainPaintSource): TerrainPaintColorSampler β Shares layer tint, opacity and linear weighted-color blending between editor and runtime ground.
|
|
2873
|
+
|
|
2862
2874
|
## @jgengine/shell/touch/OrientationHint
|
|
2863
2875
|
|
|
2864
2876
|
- `OrientationHint` (function): function OrientationHint({ wanted }: { wanted: "landscape" | "portrait" }): React.JSX.Element | null β β undocumented
|
|
@@ -670,6 +670,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* β *the p
|
|
|
670
670
|
|
|
671
671
|
- `TalentTree` (function) Β· `import { TalentTree } from "@jgengine/react"`
|
|
672
672
|
|
|
673
|
+
## terrain-paint-color β sample authored terrain surfaces and blended layers for shared ground rendering
|
|
674
|
+
|
|
675
|
+
- `createTerrainPaintColorSampler` (function) Β· `import { createTerrainPaintColorSampler } from "@jgengine/shell/terrain"`
|
|
676
|
+
|
|
673
677
|
## territory-overlay β show footprint ownership, claim cost, and affordability
|
|
674
678
|
|
|
675
679
|
- `TerritoryOverlay` (function) Β· `import { TerritoryOverlay } from "@jgengine/shell/TerritoryOverlay"`
|
|
@@ -3403,7 +3403,7 @@
|
|
|
3403
3403
|
- `TerrainFlattenMask` (interface): interface TerrainFlattenMask { center: EnvironmentVec2; radius: number; height?: number; falloff?: number } β β undocumented Β· used by `padFlattenMasks`: Derives implicit `TerrainFlattenMask`s carving each pad's footprint into the terrain beneath it.
|
|
3404
3404
|
- `TerrainIslandConfig` (interface): interface TerrainIslandConfig extends TerrainEnvironmentConfig β A bounded terrain patch floating at its own altitude β sky islands, arena platforms, split landmasses with void between.
|
|
3405
3405
|
- `TerrainIslandDescriptor` (type): type TerrainIslandDescriptor = Omit<TerrainEnvironmentDescriptor, "kind"> & { kind: "island"; origin: EnvironmentVec2; } β β undocumented
|
|
3406
|
-
- `TerrainMaterial` (type): type TerrainMaterial = "grass" | "sand" | "snow" | "rock" | "ash" | "highland" | "slate" β β undocumented Β· used by `TERRAIN_MATERIALS` (@jgengine/editor): The default terrain paint palette (surface id β color) shared by the panel and the mesh.
|
|
3406
|
+
- `TerrainMaterial` (type): type TerrainMaterial = "grass" | "sand" | "snow" | "rock" | "ash" | "highland" | "slate" β β undocumented Β· used by `TERRAIN_MATERIALS` (@jgengine/core/editor): The default terrain paint palette (surface id β color) shared by the panel and the mesh.
|
|
3407
3407
|
- `TerrainMaterialMaps` (interface): interface TerrainMaterialMaps β PBR map URLs for a real ground texture β the same shape `buildMaterialCatalog({ basePath }).resolve(id)!.maps` from `@jgengine/assets` returns. Kept dependency-free here so `core` never imports the assets package; any URLs (pulled maps, a CDN, a data URI) satisfy it. Omitted roughness keeps the terrain scalar; omitted AO skips occlusion sampling.
|
|
3408
3408
|
- `TerrainMaterialRegion` (type): type TerrainMaterialRegion = TerrainCircleRegion | TerrainPolylineRegion | TerrainRectRegion β A palette zone painted over the base terrain palette. Circle (the default when no `shape` is given), `polyline` ribbons for roads/rivers, and rotatable `rect` districts all paint fully inside their core and blend back across `falloff`; later regions in the list win overlaps.
|
|
3409
3409
|
- `TerrainPolylineRegion` (interface): interface TerrainPolylineRegion extends TerrainRegionStyle β A ribbon palette zone following a centerline β roads and rivers, instead of chaining overlapping circles.
|
|
@@ -28,6 +28,21 @@ export const overworld = world({
|
|
|
28
28
|
|
|
29
29
|
## Legacy world features
|
|
30
30
|
|
|
31
|
+
Authored terrain paint carried by `environmentContentFromDocument(document).sculpt` renders on
|
|
32
|
+
the normal environment ground. Single surface IDs use `TERRAIN_MATERIAL_COLORS` from
|
|
33
|
+
`@jgengine/core/editor/terrainMaterials`; a layer's `tint` replaces that default color. Layer
|
|
34
|
+
weights are multiplied by `opacity`, normalized, and blended in linear color space, matching
|
|
35
|
+
the editor. Unpainted cells retain the descriptor palette. Grass roots and soil patch borders
|
|
36
|
+
sample the same authored colors. Paint changes color, never ground height or collision.
|
|
37
|
+
Descriptor-wide terrain detail still overlays the painted ground. Per-layer `roughness`,
|
|
38
|
+
`tiling` and `triplanar` remain serialized authoring metadata; they do not select or configure
|
|
39
|
+
separate runtime materials.
|
|
40
|
+
|
|
41
|
+
Custom ground consumers can use `createTerrainPaintColorSampler` from `@jgengine/shell/terrain`
|
|
42
|
+
with a `TerrainPaintSource` (an editable terrain's layer/surface/weight channels). The returned
|
|
43
|
+
`TerrainPaintColorSampler` writes a linear `THREE.Color` and returns whether the point is painted;
|
|
44
|
+
false leaves the caller's fallback color unchanged. The normal ground wires this automatically.
|
|
45
|
+
|
|
31
46
|
Descriptors from `@jgengine/core/world/features` β legacy code-declared world shapes kept for
|
|
32
47
|
existing games. Do not start new worlds here; `environment()` is now the consumption target for
|
|
33
48
|
editor/preset-written scene data (`environmentContentFromDocument`), not a world definition:
|