@jgengine/shell 0.26.0 → 0.27.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 +44 -0
- package/dist/GamePlayerShell.d.ts +8 -0
- package/dist/GamePlayerShell.js +15 -1
- package/dist/environment/EnvironmentScene.js +51 -8
- package/dist/render/decalGeometry.d.ts +26 -0
- package/dist/render/decalGeometry.js +54 -0
- package/dist/render/decalTexture.d.ts +13 -0
- package/dist/render/decalTexture.js +114 -0
- package/dist/render/modelWind.d.ts +28 -0
- package/dist/render/modelWind.js +103 -0
- package/dist/render/useModelInstance.d.ts +1 -1
- package/dist/render/useModelInstance.js +3 -1
- package/dist/scatter/scatterModels.js +2 -0
- package/dist/scene/DecalRenderer.d.ts +2 -0
- package/dist/scene/DecalRenderer.js +74 -0
- package/dist/scene/GrassFieldRenderer.js +3 -2
- package/dist/scene/builtinSceneKindRenderers.js +2 -0
- package/dist/terrain/GrassCover.d.ts +21 -0
- package/dist/terrain/GrassCover.js +84 -0
- package/dist/terrain/grassCoverGeometry.d.ts +40 -0
- package/dist/terrain/grassCoverGeometry.js +112 -0
- package/dist/terrain/terrainDetailMaterial.d.ts +12 -0
- package/dist/terrain/terrainDetailMaterial.js +54 -1
- package/dist/terrain/terrainSurfaces.d.ts +6 -0
- package/dist/terrain/terrainSurfaces.js +10 -0
- package/dist/world/WorldScene.js +2 -1
- package/package.json +4 -4
- package/skills/jgengine/api.md +2 -2
- package/skills/jgengine-ui/api.md +32 -1
- package/skills/jgengine-ui/capabilities.md +4 -0
- package/skills/jgengine-world/SKILL.md +4 -0
- package/skills/jgengine-world/api.md +115 -5
- package/skills/jgengine-world/capabilities.md +20 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +5 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import * as THREE from "three";
|
|
4
|
+
import { resolveDecalObject } from "@jgengine/core/world/decalKind";
|
|
5
|
+
import { createDecalDrapeGeometry } from "../render/decalGeometry.js";
|
|
6
|
+
import { createDecalTexture } from "../render/decalTexture.js";
|
|
7
|
+
import { useDisposable } from "../render/useDisposable.js";
|
|
8
|
+
import { registerSceneKindRenderer } from "./sceneKindRenderers.js";
|
|
9
|
+
/** Tilts at or above this stand the decal up on a wall instead of draping it on the ground. */
|
|
10
|
+
const WALL_TILT = 45;
|
|
11
|
+
function dataTexture(style, seed) {
|
|
12
|
+
const baked = createDecalTexture(style, seed);
|
|
13
|
+
const texture = new THREE.DataTexture(baked.data, baked.size, baked.size, THREE.RGBAFormat, THREE.UnsignedByteType);
|
|
14
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
15
|
+
texture.magFilter = THREE.LinearFilter;
|
|
16
|
+
texture.minFilter = THREE.LinearMipmapLinearFilter;
|
|
17
|
+
texture.generateMipmaps = true;
|
|
18
|
+
texture.wrapS = THREE.ClampToEdgeWrapping;
|
|
19
|
+
texture.wrapT = THREE.ClampToEdgeWrapping;
|
|
20
|
+
texture.needsUpdate = true;
|
|
21
|
+
return texture;
|
|
22
|
+
}
|
|
23
|
+
function OneDecal({ object, context }) {
|
|
24
|
+
const resolved = useMemo(() => resolveDecalObject(object), [object]);
|
|
25
|
+
const rules = resolved?.rules;
|
|
26
|
+
const style = rules?.style ?? "dirt";
|
|
27
|
+
const seed = rules?.seed ?? "";
|
|
28
|
+
const texture = useDisposable(() => dataTexture(style, seed), [style, seed]);
|
|
29
|
+
const wall = (rules?.tilt ?? 0) >= WALL_TILT;
|
|
30
|
+
const geometry = useDisposable(() => {
|
|
31
|
+
if (resolved === null || rules === undefined)
|
|
32
|
+
return new THREE.BufferGeometry();
|
|
33
|
+
if (wall)
|
|
34
|
+
return new THREE.PlaneGeometry(rules.size, rules.size);
|
|
35
|
+
const draped = createDecalDrapeGeometry({
|
|
36
|
+
size: rules.size,
|
|
37
|
+
rotationY: resolved.rotationY,
|
|
38
|
+
heightAt: context.field.sampleHeight,
|
|
39
|
+
lift: rules.lift,
|
|
40
|
+
});
|
|
41
|
+
const next = new THREE.BufferGeometry();
|
|
42
|
+
next.setAttribute("position", new THREE.BufferAttribute(draped.positions, 3));
|
|
43
|
+
next.setAttribute("normal", new THREE.BufferAttribute(draped.normals, 3));
|
|
44
|
+
next.setAttribute("uv", new THREE.BufferAttribute(draped.uvs, 2));
|
|
45
|
+
next.setIndex(new THREE.BufferAttribute(draped.indices, 1));
|
|
46
|
+
next.computeBoundingSphere();
|
|
47
|
+
return next;
|
|
48
|
+
}, [resolved, rules, wall, context.field]);
|
|
49
|
+
const material = useDisposable(() => new THREE.MeshStandardMaterial({
|
|
50
|
+
map: texture,
|
|
51
|
+
transparent: true,
|
|
52
|
+
depthWrite: false,
|
|
53
|
+
opacity: rules?.opacity ?? 1,
|
|
54
|
+
roughness: 0.92,
|
|
55
|
+
metalness: 0,
|
|
56
|
+
polygonOffset: true,
|
|
57
|
+
polygonOffsetFactor: -4,
|
|
58
|
+
polygonOffsetUnits: -4,
|
|
59
|
+
}), [texture, rules?.opacity]);
|
|
60
|
+
if (resolved === null || rules === undefined)
|
|
61
|
+
return null;
|
|
62
|
+
const [px, py, pz] = resolved.position;
|
|
63
|
+
if (!wall) {
|
|
64
|
+
return _jsx("mesh", { geometry: geometry, material: material, position: [px, py, pz], receiveShadow: true, frustumCulled: true });
|
|
65
|
+
}
|
|
66
|
+
// A standing decal is pushed out along its own normal so it clears the wall it describes.
|
|
67
|
+
const normalX = Math.sin(resolved.rotationY);
|
|
68
|
+
const normalZ = Math.cos(resolved.rotationY);
|
|
69
|
+
return (_jsx("mesh", { geometry: geometry, material: material, position: [px + normalX * rules.offset, py + rules.lift, pz + normalZ * rules.offset], rotation: [THREE.MathUtils.degToRad(rules.tilt - 90), resolved.rotationY, 0], receiveShadow: true, frustumCulled: true }));
|
|
70
|
+
}
|
|
71
|
+
/** Registers the `decal` marker renderer. Called by `registerBuiltinSceneKindRenderers`. @internal */
|
|
72
|
+
export function registerDecalRenderer() {
|
|
73
|
+
registerSceneKindRenderer("decal", ({ objects, context }) => (_jsx(_Fragment, { children: objects.map((object) => (_jsx(OneDecal, { object: object, context: context }, object.id))) })));
|
|
74
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useMemo } from "react";
|
|
3
3
|
import { resolveGrassObject } from "@jgengine/core/world/grassKind";
|
|
4
4
|
import { clearanceMasksFrom, DEFAULT_CLEARANCE_KINDS } from "@jgengine/core/world/scatterRegion";
|
|
5
5
|
import { masksInfluence } from "@jgengine/core/world/geometry";
|
|
6
6
|
import { GrassField } from "../terrain/GrassField.js";
|
|
7
|
+
import { GrassCover } from "../terrain/GrassCover.js";
|
|
7
8
|
import { registerSceneKindRenderer } from "./sceneKindRenderers.js";
|
|
8
9
|
/** Volume kinds turf yields to — blades never grow through cracked soil or out of open water. */
|
|
9
10
|
const GRASS_YIELDS_TO = new Set(["soil", "water"]);
|
|
@@ -86,7 +87,7 @@ function OneGrass({ object, context }) {
|
|
|
86
87
|
// footprint, and the fade only thins how many *draw* at range in the vertex shader — so a deeper
|
|
87
88
|
// meadow reads expansive without raising the instance ceiling.
|
|
88
89
|
const instanceCount = Math.max(1500, Math.min(250000, Math.ceil(rules.density * area[0] * area[1])));
|
|
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 }));
|
|
90
|
+
return (_jsxs(_Fragment, { children: [_jsx(GrassCover, { position: [center[0], center[1], center[2]], area: area, seed: hashSeed(rules.seed.length > 0 ? rules.seed : object.id), colorBase: rules.colorBase, colorTip: rules.colorTip, strength: rules.groundCover, edgeFeather: 2.2, heightAt: heightAt, coverageAt: coverageAt }), _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 })] }));
|
|
90
91
|
}
|
|
91
92
|
/** Registers the `grass_field` volume renderer. Called by `registerBuiltinSceneKindRenderers`. @internal */
|
|
92
93
|
export function registerGrassFieldRenderer() {
|
|
@@ -11,6 +11,7 @@ import { registerWaterRenderer } from "./WaterRenderer.js";
|
|
|
11
11
|
import { registerSoilPatchRenderer } from "./SoilPatchRenderer.js";
|
|
12
12
|
import { registerCityRenderer } from "./CityRenderer.js";
|
|
13
13
|
import { registerPoleLineRenderer } from "./PoleLineRenderer.js";
|
|
14
|
+
import { registerDecalRenderer } from "./DecalRenderer.js";
|
|
14
15
|
let registered = false;
|
|
15
16
|
/** Registers the engine's built-in environment scene-kind renderers. Called by `AuthoredScene`. @internal */
|
|
16
17
|
export function registerBuiltinSceneKindRenderers() {
|
|
@@ -23,4 +24,5 @@ export function registerBuiltinSceneKindRenderers() {
|
|
|
23
24
|
registerSoilPatchRenderer();
|
|
24
25
|
registerCityRenderer();
|
|
25
26
|
registerPoleLineRenderer();
|
|
27
|
+
registerDecalRenderer();
|
|
26
28
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import type { TerrainHeightSampler } from "./terrainMath.js";
|
|
3
|
+
/** Props for {@link GrassCover}: one authored patch's footprint, colours and drape samplers. */
|
|
4
|
+
export interface GrassCoverProps {
|
|
5
|
+
area: readonly [number, number];
|
|
6
|
+
seed: number;
|
|
7
|
+
colorBase: THREE.ColorRepresentation;
|
|
8
|
+
colorTip: THREE.ColorRepresentation;
|
|
9
|
+
strength: number;
|
|
10
|
+
edgeFeather: number;
|
|
11
|
+
heightAt: TerrainHeightSampler;
|
|
12
|
+
coverageAt: TerrainHeightSampler;
|
|
13
|
+
segments?: number;
|
|
14
|
+
position?: readonly [number, number, number];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The turf surface under one authored `grass_field` patch: a ground-draped, edge-feathered mat that
|
|
18
|
+
* turns a bare pavement footprint into grass the blade field stands in. Bounded to one draw and a
|
|
19
|
+
* {@link GRASS_COVER_MAX_SEGMENTS}² grid, so a large park still costs a single mesh.
|
|
20
|
+
*/
|
|
21
|
+
export declare function GrassCover({ area, seed, colorBase, colorTip, strength, edgeFeather, heightAt, coverageAt, segments, position, }: GrassCoverProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as THREE from "three";
|
|
3
|
+
import { useDisposable } from "../render/useDisposable.js";
|
|
4
|
+
import { createGrassCoverGeometry, GRASS_COVER_MAX_SEGMENTS } from "./grassCoverGeometry.js";
|
|
5
|
+
let grainTexture = null;
|
|
6
|
+
/**
|
|
7
|
+
* Tileable thatch grain for the cover mat (luminance around 0.9), generated once from a hash — no
|
|
8
|
+
* asset pull, deterministic, and cheap enough that a 96² mat still reads as grass up close.
|
|
9
|
+
*/
|
|
10
|
+
function coverGrain() {
|
|
11
|
+
if (grainTexture !== null)
|
|
12
|
+
return grainTexture;
|
|
13
|
+
const size = 64;
|
|
14
|
+
const data = new Uint8Array(size * size * 4);
|
|
15
|
+
const hash = (x, y) => {
|
|
16
|
+
let h = Math.imul(x + 1, 374761393) ^ Math.imul(y + 1, 668265263) ^ 0x9e3779b9;
|
|
17
|
+
h = Math.imul(h ^ (h >>> 13), 1274126177);
|
|
18
|
+
return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
|
|
19
|
+
};
|
|
20
|
+
for (let y = 0; y < size; y += 1) {
|
|
21
|
+
for (let x = 0; x < size; x += 1) {
|
|
22
|
+
const fine = (hash(x, y) + hash((x + 32) % size, (y + 17) % size)) * 0.5;
|
|
23
|
+
const clump = (hash((x >> 2) * 3, (y >> 2) * 5) + hash(((x >> 2) + 7) % 16, ((y >> 2) + 3) % 16)) * 0.5;
|
|
24
|
+
const value = Math.max(180, Math.min(255, Math.round(232 + (fine - 0.5) * 46 + (clump - 0.5) * 30)));
|
|
25
|
+
const i = (y * size + x) * 4;
|
|
26
|
+
data[i] = value;
|
|
27
|
+
data[i + 1] = value;
|
|
28
|
+
data[i + 2] = value;
|
|
29
|
+
data[i + 3] = 255;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
grainTexture = new THREE.DataTexture(data, size, size, THREE.RGBAFormat, THREE.UnsignedByteType);
|
|
33
|
+
grainTexture.wrapS = THREE.RepeatWrapping;
|
|
34
|
+
grainTexture.wrapT = THREE.RepeatWrapping;
|
|
35
|
+
grainTexture.magFilter = THREE.LinearFilter;
|
|
36
|
+
grainTexture.minFilter = THREE.LinearMipmapLinearFilter;
|
|
37
|
+
grainTexture.generateMipmaps = true;
|
|
38
|
+
grainTexture.needsUpdate = true;
|
|
39
|
+
return grainTexture;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The turf surface under one authored `grass_field` patch: a ground-draped, edge-feathered mat that
|
|
43
|
+
* turns a bare pavement footprint into grass the blade field stands in. Bounded to one draw and a
|
|
44
|
+
* {@link GRASS_COVER_MAX_SEGMENTS}² grid, so a large park still costs a single mesh.
|
|
45
|
+
*/
|
|
46
|
+
export function GrassCover({ area, seed, colorBase, colorTip, strength, edgeFeather, heightAt, coverageAt, segments, position, }) {
|
|
47
|
+
const geometry = useDisposable(() => {
|
|
48
|
+
const baked = createGrassCoverGeometry({
|
|
49
|
+
area,
|
|
50
|
+
seed,
|
|
51
|
+
colorBase,
|
|
52
|
+
colorTip,
|
|
53
|
+
strength,
|
|
54
|
+
edgeFeather,
|
|
55
|
+
heightAt,
|
|
56
|
+
coverageAt,
|
|
57
|
+
...(segments === undefined ? {} : { segments }),
|
|
58
|
+
});
|
|
59
|
+
const next = new THREE.BufferGeometry();
|
|
60
|
+
if (baked.positions.length === 0)
|
|
61
|
+
return next;
|
|
62
|
+
next.setAttribute("position", new THREE.BufferAttribute(baked.positions, 3));
|
|
63
|
+
next.setAttribute("normal", new THREE.BufferAttribute(baked.normals, 3));
|
|
64
|
+
next.setAttribute("color", new THREE.BufferAttribute(baked.colors, 4));
|
|
65
|
+
next.setAttribute("uv", new THREE.BufferAttribute(baked.uvs, 2));
|
|
66
|
+
next.setIndex(new THREE.BufferAttribute(baked.indices, 1));
|
|
67
|
+
next.computeBoundingSphere();
|
|
68
|
+
return next;
|
|
69
|
+
}, [area, seed, colorBase, colorTip, strength, edgeFeather, heightAt, coverageAt, segments]);
|
|
70
|
+
const material = useDisposable(() => new THREE.MeshStandardMaterial({
|
|
71
|
+
map: coverGrain(),
|
|
72
|
+
vertexColors: true,
|
|
73
|
+
transparent: true,
|
|
74
|
+
depthWrite: false,
|
|
75
|
+
roughness: 0.96,
|
|
76
|
+
metalness: 0,
|
|
77
|
+
polygonOffset: true,
|
|
78
|
+
polygonOffsetFactor: -1,
|
|
79
|
+
polygonOffsetUnits: -1,
|
|
80
|
+
}), []);
|
|
81
|
+
if (strength <= 0)
|
|
82
|
+
return null;
|
|
83
|
+
return (_jsx("mesh", { geometry: geometry, material: material, receiveShadow: true, frustumCulled: true, ...(position === undefined ? {} : { position: [position[0], position[1], position[2]] }) }));
|
|
84
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { type TerrainArea, type TerrainHeightSampler } from "./terrainMath.js";
|
|
3
|
+
/** World metres one cover-texture tile spans; the mat's own grain is sampled in world space. */
|
|
4
|
+
export declare const GRASS_COVER_TILE_METRES = 3;
|
|
5
|
+
/** Largest grid a cover mat bakes to, per axis — a 66 m park stays one draw at ~1 m cells. */
|
|
6
|
+
export declare const GRASS_COVER_MAX_SEGMENTS = 96;
|
|
7
|
+
/** Inputs for {@link createGrassCoverGeometry}. */
|
|
8
|
+
export interface GrassCoverGeometryOptions {
|
|
9
|
+
area: TerrainArea;
|
|
10
|
+
seed: number;
|
|
11
|
+
colorBase: THREE.ColorRepresentation;
|
|
12
|
+
colorTip: THREE.ColorRepresentation;
|
|
13
|
+
/** Ground-cover mat strength 0..1 (the authored `groundCover`); 0 produces no geometry. */
|
|
14
|
+
strength: number;
|
|
15
|
+
/** Meters of soft fringe at the patch border, matching the blade field's own feather. */
|
|
16
|
+
edgeFeather: number;
|
|
17
|
+
heightAt: TerrainHeightSampler;
|
|
18
|
+
coverageAt: TerrainHeightSampler;
|
|
19
|
+
/** Lift above the ground in meters so the mat never z-fights the terrain. Default 0.035. */
|
|
20
|
+
lift?: number;
|
|
21
|
+
/** Grid segments per axis; defaults to one per 2 m, clamped to {@link GRASS_COVER_MAX_SEGMENTS}. */
|
|
22
|
+
segments?: number;
|
|
23
|
+
}
|
|
24
|
+
/** Bake result for {@link createGrassCoverGeometry}; empty arrays when the mat is switched off. */
|
|
25
|
+
export interface GrassCoverGeometry {
|
|
26
|
+
positions: Float32Array;
|
|
27
|
+
normals: Float32Array;
|
|
28
|
+
colors: Float32Array;
|
|
29
|
+
uvs: Float32Array;
|
|
30
|
+
indices: Uint32Array;
|
|
31
|
+
size: readonly [number, number];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Drapes an authored `grass_field` volume's footprint with a feathered turf mat: vertices follow the
|
|
35
|
+
* runtime ground, the patch edge fades out over `edgeFeather`, and authored clearance masks punch
|
|
36
|
+
* the cover back out where a path or footing crosses it. This is the surface under the blades, so a
|
|
37
|
+
* park reads as grass instead of bare pavement with stubble on it.
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
40
|
+
export declare function createGrassCoverGeometry(options: GrassCoverGeometryOptions): GrassCoverGeometry;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { resolveTerrainSize } from "./terrainMath.js";
|
|
3
|
+
/** World metres one cover-texture tile spans; the mat's own grain is sampled in world space. */
|
|
4
|
+
export const GRASS_COVER_TILE_METRES = 3;
|
|
5
|
+
/** Largest grid a cover mat bakes to, per axis — a 66 m park stays one draw at ~1 m cells. */
|
|
6
|
+
export const GRASS_COVER_MAX_SEGMENTS = 96;
|
|
7
|
+
function hash2(x, z, seed) {
|
|
8
|
+
let h = Math.imul(x | 0, 374761393) ^ Math.imul(z | 0, 668265263) ^ Math.imul(seed | 0, 2246822519);
|
|
9
|
+
h = Math.imul(h ^ (h >>> 13), 1274126177);
|
|
10
|
+
return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
|
|
11
|
+
}
|
|
12
|
+
function smooth(t) {
|
|
13
|
+
return t * t * (3 - 2 * t);
|
|
14
|
+
}
|
|
15
|
+
/** Bilinear value noise in world space; deterministic per seed, no allocation on the hot path. */
|
|
16
|
+
function valueNoise(x, z, seed) {
|
|
17
|
+
const xi = Math.floor(x);
|
|
18
|
+
const zi = Math.floor(z);
|
|
19
|
+
const tx = smooth(x - xi);
|
|
20
|
+
const tz = smooth(z - zi);
|
|
21
|
+
const a = hash2(xi, zi, seed);
|
|
22
|
+
const b = hash2(xi + 1, zi, seed);
|
|
23
|
+
const c = hash2(xi, zi + 1, seed);
|
|
24
|
+
const d = hash2(xi + 1, zi + 1, seed);
|
|
25
|
+
return (a + (b - a) * tx) * (1 - tz) + (c + (d - c) * tx) * tz;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Drapes an authored `grass_field` volume's footprint with a feathered turf mat: vertices follow the
|
|
29
|
+
* runtime ground, the patch edge fades out over `edgeFeather`, and authored clearance masks punch
|
|
30
|
+
* the cover back out where a path or footing crosses it. This is the surface under the blades, so a
|
|
31
|
+
* park reads as grass instead of bare pavement with stubble on it.
|
|
32
|
+
* @internal
|
|
33
|
+
*/
|
|
34
|
+
export function createGrassCoverGeometry(options) {
|
|
35
|
+
const size = resolveTerrainSize(options.area);
|
|
36
|
+
const segments = Math.max(1, Math.min(GRASS_COVER_MAX_SEGMENTS, Math.floor(options.segments ?? Math.min(GRASS_COVER_MAX_SEGMENTS, Math.max(2, Math.round(Math.max(size.width, size.depth) / 2))))));
|
|
37
|
+
if (options.strength <= 0 || size.width <= 0 || size.depth <= 0) {
|
|
38
|
+
return { positions: new Float32Array(), normals: new Float32Array(), colors: new Float32Array(), uvs: new Float32Array(), indices: new Uint32Array(), size: [size.width, size.depth] };
|
|
39
|
+
}
|
|
40
|
+
const lift = options.lift ?? 0.035;
|
|
41
|
+
const halfW = size.width / 2;
|
|
42
|
+
const halfD = size.depth / 2;
|
|
43
|
+
const base = new THREE.Color(options.colorBase);
|
|
44
|
+
const tip = new THREE.Color(options.colorTip);
|
|
45
|
+
const root = base.clone().multiplyScalar(0.55);
|
|
46
|
+
const sheen = base.clone().multiplyScalar(0.98);
|
|
47
|
+
const cols = segments + 1;
|
|
48
|
+
const count = cols * cols;
|
|
49
|
+
const positions = new Float32Array(count * 3);
|
|
50
|
+
const normals = new Float32Array(count * 3);
|
|
51
|
+
const colors = new Float32Array(count * 4);
|
|
52
|
+
const uvs = new Float32Array(count * 2);
|
|
53
|
+
const eps = Math.max(0.25, Math.min(size.width, size.depth) / segments / 2);
|
|
54
|
+
for (let iz = 0; iz < cols; iz += 1) {
|
|
55
|
+
const z = -halfD + (iz / segments) * size.depth;
|
|
56
|
+
for (let ix = 0; ix < cols; ix += 1) {
|
|
57
|
+
const x = -halfW + (ix / segments) * size.width;
|
|
58
|
+
const index = iz * cols + ix;
|
|
59
|
+
positions[index * 3] = x;
|
|
60
|
+
positions[index * 3 + 1] = options.heightAt(x, z) + lift;
|
|
61
|
+
positions[index * 3 + 2] = z;
|
|
62
|
+
const hL = options.heightAt(x - eps, z);
|
|
63
|
+
const hR = options.heightAt(x + eps, z);
|
|
64
|
+
const hD = options.heightAt(x, z - eps);
|
|
65
|
+
const hU = options.heightAt(x, z + eps);
|
|
66
|
+
const nx = hL - hR;
|
|
67
|
+
const nz = hD - hU;
|
|
68
|
+
const ny = 2 * eps;
|
|
69
|
+
const length = Math.hypot(nx, ny, nz) || 1;
|
|
70
|
+
normals[index * 3] = nx / length;
|
|
71
|
+
normals[index * 3 + 1] = ny / length;
|
|
72
|
+
normals[index * 3 + 2] = nz / length;
|
|
73
|
+
const edge = Math.min(halfW - Math.abs(x), halfD - Math.abs(z));
|
|
74
|
+
const feather = Math.max(0.001, options.edgeFeather);
|
|
75
|
+
const edgeFade = smooth(Math.min(1, Math.max(0, edge / feather)));
|
|
76
|
+
const coverage = Math.max(0, Math.min(1, options.coverageAt(x, z)));
|
|
77
|
+
const grain = valueNoise(x / 1.7, z / 1.7, options.seed);
|
|
78
|
+
const patch = valueNoise(x / 11 + 31.7, z / 11 - 9.2, options.seed + 17);
|
|
79
|
+
const alpha = options.strength * edgeFade * coverage * (0.82 + 0.18 * grain);
|
|
80
|
+
// Macro patches lift the mat toward the blade tip hue and back to a rooted shade, so a park
|
|
81
|
+
// reads as turf with light and dry pockets instead of one flat green sheet.
|
|
82
|
+
const light = smooth(Math.min(1, Math.max(0, (patch - 0.25) / 0.6)));
|
|
83
|
+
const r = root.r + (sheen.r - root.r) * light;
|
|
84
|
+
const g = root.g + (sheen.g - root.g) * light;
|
|
85
|
+
const b = root.b + (sheen.b - root.b) * light;
|
|
86
|
+
const tipMix = grain * 0.3;
|
|
87
|
+
colors[index * 4] = r + (tip.r - r) * tipMix;
|
|
88
|
+
colors[index * 4 + 1] = g + (tip.g - g) * tipMix;
|
|
89
|
+
colors[index * 4 + 2] = b + (tip.b - b) * tipMix;
|
|
90
|
+
colors[index * 4 + 3] = alpha;
|
|
91
|
+
uvs[index * 2] = x / GRASS_COVER_TILE_METRES;
|
|
92
|
+
uvs[index * 2 + 1] = z / GRASS_COVER_TILE_METRES;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const indices = new Uint32Array(segments * segments * 6);
|
|
96
|
+
let cursor = 0;
|
|
97
|
+
for (let iz = 0; iz < segments; iz += 1) {
|
|
98
|
+
for (let ix = 0; ix < segments; ix += 1) {
|
|
99
|
+
const a = iz * cols + ix;
|
|
100
|
+
const b = a + 1;
|
|
101
|
+
const c = a + cols;
|
|
102
|
+
const d = c + 1;
|
|
103
|
+
indices[cursor++] = a;
|
|
104
|
+
indices[cursor++] = c;
|
|
105
|
+
indices[cursor++] = b;
|
|
106
|
+
indices[cursor++] = b;
|
|
107
|
+
indices[cursor++] = c;
|
|
108
|
+
indices[cursor++] = d;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return { positions, normals, colors, uvs, indices, size: [size.width, size.depth] };
|
|
112
|
+
}
|
|
@@ -34,9 +34,21 @@ export interface TerrainDetailMaterialOptions {
|
|
|
34
34
|
classMask?: number;
|
|
35
35
|
/** Initial editor relief-contour strength. Default 0. */
|
|
36
36
|
contour?: number;
|
|
37
|
+
/**
|
|
38
|
+
* Per-painted-slot scanned color maps (slot 1 first), each with its own world tile metres. Layers
|
|
39
|
+
* without a map (or beyond {@link MAX_TERRAIN_LAYER_MAPS}) keep the procedural surface response.
|
|
40
|
+
*/
|
|
41
|
+
layerMaps?: readonly (TerrainLayerMap | undefined)[];
|
|
37
42
|
}
|
|
38
43
|
/** Compile every surface response, for an editor viewport whose paint can change to any surface. */
|
|
39
44
|
export declare const ALL_TERRAIN_SURFACE_CLASSES = 511;
|
|
45
|
+
/** One painted layer's scanned color map plus the world metres one tile covers. */
|
|
46
|
+
export interface TerrainLayerMap {
|
|
47
|
+
color: THREE.Texture;
|
|
48
|
+
tileMetres: number;
|
|
49
|
+
}
|
|
50
|
+
/** At most this many painted layers sample their own scanned map; extra layers keep procedural shading. */
|
|
51
|
+
export declare const MAX_TERRAIN_LAYER_MAPS = 4;
|
|
40
52
|
/**
|
|
41
53
|
* A `MeshStandardMaterial` that renders terrain as ground rather than flat colour. Each painted
|
|
42
54
|
* layer draws its surface type's procedural response (grass clumps and blades, dirt clods and
|
|
@@ -1,10 +1,38 @@
|
|
|
1
1
|
import * as THREE from "three";
|
|
2
2
|
import { NOISE_GLSL, PAINT_PARS_GLSL, SURFACE_BUMP_NORMAL_GLSL, SURFACE_FUNCTIONS_GLSL } from "./terrainSurfaceShader.js";
|
|
3
|
+
let placeholder;
|
|
4
|
+
function placeholderTexture() {
|
|
5
|
+
if (placeholder !== undefined)
|
|
6
|
+
return placeholder;
|
|
7
|
+
placeholder = new THREE.DataTexture(new Uint8Array([255, 255, 255, 255]), 1, 1, THREE.RGBAFormat);
|
|
8
|
+
placeholder.needsUpdate = true;
|
|
9
|
+
return placeholder;
|
|
10
|
+
}
|
|
3
11
|
const ROCK_CLASS_BIT = 1 << 4;
|
|
4
12
|
/** Compile every surface response, for an editor viewport whose paint can change to any surface. */
|
|
5
13
|
export const ALL_TERRAIN_SURFACE_CLASSES = 0x1ff;
|
|
14
|
+
/** At most this many painted layers sample their own scanned map; extra layers keep procedural shading. */
|
|
15
|
+
export const MAX_TERRAIN_LAYER_MAPS = 4;
|
|
6
16
|
// Slot 0 is the unpainted base, 1–8 the paint layers, 9 slope rock.
|
|
7
17
|
const SLOT_GLSL = /* glsl */ `
|
|
18
|
+
#ifdef JG_TERRAIN_LAYER_MAPS
|
|
19
|
+
uniform sampler2D uLayerMap1;
|
|
20
|
+
uniform sampler2D uLayerMap2;
|
|
21
|
+
uniform sampler2D uLayerMap3;
|
|
22
|
+
uniform sampler2D uLayerMap4;
|
|
23
|
+
uniform float uLayerTile1;
|
|
24
|
+
uniform float uLayerTile2;
|
|
25
|
+
uniform float uLayerTile3;
|
|
26
|
+
uniform float uLayerTile4;
|
|
27
|
+
vec3 jgLayerMap(int slot, vec3 wp, out bool has) {
|
|
28
|
+
has = false;
|
|
29
|
+
if (slot == 1) { if (uLayerTile1 > 0.0) { has = true; return texture2D(uLayerMap1, wp.xz / uLayerTile1).rgb; } }
|
|
30
|
+
else if (slot == 2) { if (uLayerTile2 > 0.0) { has = true; return texture2D(uLayerMap2, wp.xz / uLayerTile2).rgb; } }
|
|
31
|
+
else if (slot == 3) { if (uLayerTile3 > 0.0) { has = true; return texture2D(uLayerMap3, wp.xz / uLayerTile3).rgb; } }
|
|
32
|
+
else if (slot == 4) { if (uLayerTile4 > 0.0) { has = true; return texture2D(uLayerMap4, wp.xz / uLayerTile4).rgb; } }
|
|
33
|
+
return vec3(0.0);
|
|
34
|
+
}
|
|
35
|
+
#endif
|
|
8
36
|
JgSurf jgSlot(int slot, vec3 baseTone, vec3 wp){
|
|
9
37
|
vec3 jgFlatC = baseTone;
|
|
10
38
|
JgSurf s;
|
|
@@ -22,6 +50,15 @@ JgSurf jgSlot(int slot, vec3 baseTone, vec3 wp){
|
|
|
22
50
|
else {
|
|
23
51
|
s = jgSurface(0, baseTone, roughness, 1.0, wp);
|
|
24
52
|
}
|
|
53
|
+
#ifdef JG_TERRAIN_LAYER_MAPS
|
|
54
|
+
if (slot > 0 && slot <= 4) {
|
|
55
|
+
bool jgHasMap;
|
|
56
|
+
vec3 jgMapped = jgLayerMap(slot, wp, jgHasMap);
|
|
57
|
+
// The layer owns its own scan: grain, colour and all, tiled at its authored metres, and the
|
|
58
|
+
// mask weights decide how much of it shows against the neighbour layer.
|
|
59
|
+
if (jgHasMap) s.albedo = mix(s.albedo, jgMapped, 0.92);
|
|
60
|
+
}
|
|
61
|
+
#endif
|
|
25
62
|
s.albedo = mix(jgFlatC, s.albedo, uSurface);
|
|
26
63
|
s.height *= uSurface;
|
|
27
64
|
s.bump *= uSurface;
|
|
@@ -48,6 +85,20 @@ export function createTerrainDetailMaterial(detail, textures, options = {}) {
|
|
|
48
85
|
const strength = detail.material?.strength ?? 1;
|
|
49
86
|
const tint = detail.material?.tint ?? 1;
|
|
50
87
|
const paint = options.paint;
|
|
88
|
+
const layerMaps = (options.layerMaps ?? []).slice(0, MAX_TERRAIN_LAYER_MAPS);
|
|
89
|
+
const hasLayerMaps = layerMaps.some((entry) => entry !== undefined);
|
|
90
|
+
const layerMapUniforms = hasLayerMaps
|
|
91
|
+
? {
|
|
92
|
+
uLayerMap1: { value: layerMaps[0]?.color ?? placeholderTexture() },
|
|
93
|
+
uLayerMap2: { value: layerMaps[1]?.color ?? placeholderTexture() },
|
|
94
|
+
uLayerMap3: { value: layerMaps[2]?.color ?? placeholderTexture() },
|
|
95
|
+
uLayerMap4: { value: layerMaps[3]?.color ?? placeholderTexture() },
|
|
96
|
+
uLayerTile1: { value: layerMaps[0]?.tileMetres ?? 0 },
|
|
97
|
+
uLayerTile2: { value: layerMaps[1]?.tileMetres ?? 0 },
|
|
98
|
+
uLayerTile3: { value: layerMaps[2]?.tileMetres ?? 0 },
|
|
99
|
+
uLayerTile4: { value: layerMaps[3]?.tileMetres ?? 0 },
|
|
100
|
+
}
|
|
101
|
+
: {};
|
|
51
102
|
const low = options.quality === "low";
|
|
52
103
|
const contour = { value: options.contour ?? 0 };
|
|
53
104
|
// Rock is always compiled for slope rock; each extra class costs shader time even where unused on
|
|
@@ -68,6 +119,7 @@ export function createTerrainDetailMaterial(detail, textures, options = {}) {
|
|
|
68
119
|
uSurface: { value: detail.surfaceStrength },
|
|
69
120
|
uContour: contour,
|
|
70
121
|
...(paint === undefined ? {} : paint.uniforms),
|
|
122
|
+
...layerMapUniforms,
|
|
71
123
|
...(hasTexture
|
|
72
124
|
? {
|
|
73
125
|
uMatColor: { value: textures.color },
|
|
@@ -88,6 +140,7 @@ export function createTerrainDetailMaterial(detail, textures, options = {}) {
|
|
|
88
140
|
});
|
|
89
141
|
material.defines = {
|
|
90
142
|
...(paint === undefined ? {} : { JG_TERRAIN_PAINT: "" }),
|
|
143
|
+
...(hasLayerMaps ? { JG_TERRAIN_LAYER_MAPS: "" } : {}),
|
|
91
144
|
...(low ? { JG_SURFACE_LOW: "" } : {}),
|
|
92
145
|
JG_SURFACE_CLASSES: String(classMask),
|
|
93
146
|
};
|
|
@@ -264,6 +317,6 @@ ${materialNormalGlsl}`)
|
|
|
264
317
|
.replace("#include <aomap_fragment>", `#include <aomap_fragment>
|
|
265
318
|
${materialAoGlsl}`);
|
|
266
319
|
};
|
|
267
|
-
material.customProgramCacheKey = () => `jgengine-terrain-detail-${hasTexture ? "tex" : "flat"}-${hasTexture && textures.roughness !== undefined ? "rough" : "scalar"}-${hasTexture && textures.ao !== undefined ? "ao" : "unoccluded"}-${paint === undefined ? "bare" : "paint"}-${low ? "low" : "full"}-${classMask}`;
|
|
320
|
+
material.customProgramCacheKey = () => `jgengine-terrain-detail-${hasTexture ? "tex" : "flat"}-${hasTexture && textures.roughness !== undefined ? "rough" : "scalar"}-${hasTexture && textures.ao !== undefined ? "ao" : "unoccluded"}-${paint === undefined ? "bare" : "paint"}-${hasLayerMaps ? "layers" : "procedural"}-${low ? "low" : "full"}-${classMask}`;
|
|
268
321
|
return { material, contour };
|
|
269
322
|
}
|
|
@@ -21,6 +21,10 @@ export interface TerrainPaintSlot {
|
|
|
21
21
|
roughness: number;
|
|
22
22
|
/** Pattern frequency multiplier from the layer's authored `tiling`. */
|
|
23
23
|
scale: number;
|
|
24
|
+
/** World metres per texture repeat for this layer's scanned color map; 0 when the layer has no map. */
|
|
25
|
+
tileMetres: number;
|
|
26
|
+
/** Resolved color-map URL the slot's scanned texture comes from, if any. */
|
|
27
|
+
materialUrl?: string;
|
|
24
28
|
}
|
|
25
29
|
/** Per-cell paint coverage packed for the shader: two RGBA8 planes holding up to eight layer weights. */
|
|
26
30
|
export interface TerrainPaintWeights {
|
|
@@ -50,6 +54,8 @@ export interface TerrainPaintTextures {
|
|
|
50
54
|
readonly classMask: number;
|
|
51
55
|
/** Re-upload coverage and layer slots; reallocates only when the cell grid size changes. */
|
|
52
56
|
update(weights: TerrainPaintWeights): void;
|
|
57
|
+
/** The slot table the current coverage was packed from (layer materials, tile metres, classes). */
|
|
58
|
+
readonly slots: readonly TerrainPaintSlot[];
|
|
53
59
|
dispose(): void;
|
|
54
60
|
}
|
|
55
61
|
/**
|
|
@@ -53,6 +53,8 @@ function slotFor(surface, layer) {
|
|
|
53
53
|
classIndex: CLASS_INDEX[surfaceClass],
|
|
54
54
|
roughness: layer?.roughness ?? CLASS_ROUGHNESS[surfaceClass],
|
|
55
55
|
scale: tiling === undefined || tiling <= 0 ? 1 : Math.min(2, Math.max(0.5, CLASS_TILING[surfaceClass] / tiling)),
|
|
56
|
+
tileMetres: layer?.materialUrl === undefined ? 0 : tiling !== undefined && tiling > 0 ? tiling : 6,
|
|
57
|
+
...(layer?.materialUrl === undefined ? {} : { materialUrl: layer.materialUrl }),
|
|
56
58
|
};
|
|
57
59
|
}
|
|
58
60
|
/**
|
|
@@ -145,6 +147,7 @@ export function createTerrainPaintTextures(weights) {
|
|
|
145
147
|
const classes = new Float32Array(MAX_TERRAIN_PAINT_LAYERS);
|
|
146
148
|
const roughness = new Float32Array(MAX_TERRAIN_PAINT_LAYERS);
|
|
147
149
|
const scales = new Float32Array(MAX_TERRAIN_PAINT_LAYERS);
|
|
150
|
+
const tiles = new Float32Array(MAX_TERRAIN_PAINT_LAYERS);
|
|
148
151
|
const uniforms = {
|
|
149
152
|
uPaintA: { value: coverageTexture(weights.planes[0], weights.cols, weights.rows) },
|
|
150
153
|
uPaintB: { value: coverageTexture(weights.planes[1], weights.cols, weights.rows) },
|
|
@@ -156,9 +159,12 @@ export function createTerrainPaintTextures(weights) {
|
|
|
156
159
|
uLayerClass: { value: classes },
|
|
157
160
|
uLayerRough: { value: roughness },
|
|
158
161
|
uLayerScale: { value: scales },
|
|
162
|
+
uLayerTile: { value: tiles },
|
|
159
163
|
};
|
|
160
164
|
let classMask = 0;
|
|
165
|
+
let slots = weights.slots;
|
|
161
166
|
const update = (next) => {
|
|
167
|
+
slots = next.slots;
|
|
162
168
|
classMask = next.slots.reduce((mask, slot) => mask | (1 << slot.classIndex), 0);
|
|
163
169
|
for (const [key, plane] of [["uPaintA", next.planes[0]], ["uPaintB", next.planes[1]]]) {
|
|
164
170
|
const current = uniforms[key].value;
|
|
@@ -182,6 +188,7 @@ export function createTerrainPaintTextures(weights) {
|
|
|
182
188
|
classes[i] = slot?.classIndex ?? 0;
|
|
183
189
|
roughness[i] = slot?.roughness ?? 0.9;
|
|
184
190
|
scales[i] = slot?.scale ?? 1;
|
|
191
|
+
tiles[i] = slot?.tileMetres ?? 0;
|
|
185
192
|
}
|
|
186
193
|
};
|
|
187
194
|
update(weights);
|
|
@@ -190,6 +197,9 @@ export function createTerrainPaintTextures(weights) {
|
|
|
190
197
|
get classMask() {
|
|
191
198
|
return classMask;
|
|
192
199
|
},
|
|
200
|
+
get slots() {
|
|
201
|
+
return slots;
|
|
202
|
+
},
|
|
193
203
|
update,
|
|
194
204
|
dispose: () => {
|
|
195
205
|
uniforms.uPaintA.value.dispose();
|
package/dist/world/WorldScene.js
CHANGED
|
@@ -17,6 +17,7 @@ import { useSceneEntityIds, useSceneObjectIds, useGameStore, usePlayer, useTarge
|
|
|
17
17
|
import { colorFromId } from "../worldSky.js";
|
|
18
18
|
import { DefaultSurface, detailMaps } from "../render/defaultSurface.js";
|
|
19
19
|
import { useDisposable } from "../render/useDisposable.js";
|
|
20
|
+
import { WindClock } from "../render/modelWind.js";
|
|
20
21
|
import { MeasuredBoundsGroup } from "../render/measureBounds.js";
|
|
21
22
|
import { EntitySprite, IsolatedEntityModel } from "../render/SceneModels.js";
|
|
22
23
|
import { resolveEntityModel, resolveObjectModel } from "../render/resolveModel.js";
|
|
@@ -202,7 +203,7 @@ function WorldActors({ entitySprites, entityModels, objectModels, objectStyles,
|
|
|
202
203
|
}), grouped.singles.map((candidate) => renderSingle(candidate.object)), grouped.batches.map((batch) => _jsx(StaticObjectModels, { batch: batch, single: renderSingle }, batch.key))] }));
|
|
203
204
|
}
|
|
204
205
|
export function WorldView({ entitySprites, entityModels, objectModels, objectStyles, environment, assets, renderEntity, renderObject, selectedIds, hideLocalActor, editorLayers, }) {
|
|
205
|
-
return (_jsxs(_Fragment, { children: [_jsx(WorldEnvironment, { environment: environment }), _jsx(WorldActors, { entitySprites: entitySprites, entityModels: entityModels, objectModels: objectModels, objectStyles: objectStyles, assets: assets, renderEntity: renderEntity, renderObject: renderObject, selectedIds: selectedIds, hideLocalActor: hideLocalActor, editorLayers: editorLayers })] }));
|
|
206
|
+
return (_jsxs(_Fragment, { children: [_jsx(WindClock, {}), _jsx(WorldEnvironment, { environment: environment }), _jsx(WorldActors, { entitySprites: entitySprites, entityModels: entityModels, objectModels: objectModels, objectStyles: objectStyles, assets: assets, renderEntity: renderEntity, renderObject: renderObject, selectedIds: selectedIds, hideLocalActor: hideLocalActor, editorLayers: editorLayers })] }));
|
|
206
207
|
}
|
|
207
208
|
export function RemotePlayers({ rows, presenceInterpolationMs = 100 }) {
|
|
208
209
|
const groups = useRef(new Map());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgengine/shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.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.27.0",
|
|
39
|
+
"@jgengine/react": "^0.27.0",
|
|
40
|
+
"@jgengine/ws": "^0.27.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"@react-three/drei": "^10.0.0",
|
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.27.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
10
10
|
|
|
11
11
|
## @jgengine/core/authoring
|
|
12
12
|
|
|
@@ -285,7 +285,7 @@
|
|
|
285
285
|
|
|
286
286
|
- `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
|
|
287
287
|
- `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
|
|
288
|
-
- `VERSION` (const): const VERSION: "0.
|
|
288
|
+
- `VERSION` (const): const VERSION: "0.27.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
289
289
|
|
|
290
290
|
## @jgengine/core/runtime/adapter
|
|
291
291
|
|