@jgengine/shell 0.5.0 → 0.7.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 +191 -2
- package/README.md +27 -0
- package/dist/GamePlayerShell.js +224 -43
- package/dist/audio/AudioComponents.d.ts +12 -0
- package/dist/audio/AudioComponents.js +53 -0
- package/dist/audio/audioEngine.d.ts +23 -0
- package/dist/audio/audioEngine.js +110 -0
- package/dist/camera/GameCameraRig.d.ts +11 -0
- package/dist/camera/GameCameraRig.js +42 -0
- package/dist/camera/GameOrbitCamera.d.ts +3 -1
- package/dist/camera/GameOrbitCamera.js +4 -2
- package/dist/camera/cameraRigs.d.ts +22 -0
- package/dist/camera/cameraRigs.js +485 -0
- package/dist/camera/index.d.ts +5 -0
- package/dist/camera/index.js +5 -0
- package/dist/camera/rigMath.d.ts +130 -0
- package/dist/camera/rigMath.js +338 -0
- package/dist/camera/shakeChannel.d.ts +29 -0
- package/dist/camera/shakeChannel.js +38 -0
- package/dist/demo/builderDemo.d.ts +2 -0
- package/dist/demo/builderDemo.js +189 -0
- package/dist/demo/demoGame.js +10 -2
- package/dist/demo/environmentShowcase.d.ts +2 -0
- package/dist/demo/environmentShowcase.js +15 -0
- package/dist/demo/mapDemo.d.ts +2 -0
- package/dist/demo/mapDemo.js +206 -0
- package/dist/demo/pointerDemo.d.ts +2 -0
- package/dist/demo/pointerDemo.js +151 -0
- package/dist/demo/sensorShowcase.d.ts +2 -0
- package/dist/demo/sensorShowcase.js +131 -0
- package/dist/demo/survivalDemo.d.ts +2 -0
- package/dist/demo/survivalDemo.js +291 -0
- package/dist/environment/EnvironmentScene.d.ts +5 -0
- package/dist/environment/EnvironmentScene.js +75 -0
- package/dist/environment/index.d.ts +1 -0
- package/dist/environment/index.js +1 -0
- package/dist/environment.d.ts +1 -0
- package/dist/environment.js +1 -0
- package/dist/map/MapMarkerBeacons.d.ts +12 -0
- package/dist/map/MapMarkerBeacons.js +16 -0
- package/dist/map/index.d.ts +2 -0
- package/dist/map/index.js +2 -0
- package/dist/map/terrainMap.d.ts +23 -0
- package/dist/map/terrainMap.js +79 -0
- package/dist/pointer/PointerOverlays.d.ts +12 -0
- package/dist/pointer/PointerOverlays.js +17 -0
- package/dist/pointer/PointerProbe.d.ts +4 -0
- package/dist/pointer/PointerProbe.js +27 -0
- package/dist/pointer/pointerService.d.ts +24 -0
- package/dist/pointer/pointerService.js +68 -0
- package/dist/registry.d.ts +4 -2
- package/dist/replay/useSessionRecorder.d.ts +14 -0
- package/dist/replay/useSessionRecorder.js +22 -0
- package/dist/structures/GeneratedBuilding.d.ts +44 -0
- package/dist/structures/GeneratedBuilding.js +86 -0
- package/dist/structures/PlacementGhost.d.ts +8 -0
- package/dist/structures/PlacementGhost.js +11 -0
- package/dist/structures/index.d.ts +2 -0
- package/dist/structures/index.js +2 -0
- package/dist/structures.d.ts +1 -0
- package/dist/structures.js +1 -0
- package/dist/terrain/CarvedTerrain.d.ts +22 -0
- package/dist/terrain/CarvedTerrain.js +19 -0
- package/dist/terrain/EditableGround.d.ts +11 -0
- package/dist/terrain/EditableGround.js +38 -0
- package/dist/terrain/GrassField.d.ts +21 -0
- package/dist/terrain/GrassField.js +31 -0
- package/dist/terrain/ProceduralGround.d.ts +9 -0
- package/dist/terrain/ProceduralGround.js +17 -0
- package/dist/terrain/TerraformBrushCursor.d.ts +8 -0
- package/dist/terrain/TerraformBrushCursor.js +13 -0
- package/dist/terrain/grassGeometry.d.ts +27 -0
- package/dist/terrain/grassGeometry.js +77 -0
- package/dist/terrain/grassMaterial.d.ts +33 -0
- package/dist/terrain/grassMaterial.js +108 -0
- package/dist/terrain/index.d.ts +10 -0
- package/dist/terrain/index.js +10 -0
- package/dist/terrain/random.d.ts +4 -0
- package/dist/terrain/random.js +27 -0
- package/dist/terrain/terrainMath.d.ts +47 -0
- package/dist/terrain/terrainMath.js +172 -0
- package/dist/terrain.d.ts +1 -0
- package/dist/terrain.js +1 -0
- package/dist/vision/FrustumSensorHud.d.ts +21 -0
- package/dist/vision/FrustumSensorHud.js +67 -0
- package/dist/vision/HiddenStateProbeHud.d.ts +11 -0
- package/dist/vision/HiddenStateProbeHud.js +11 -0
- package/dist/vision/RevealVision.d.ts +28 -0
- package/dist/vision/RevealVision.js +53 -0
- package/dist/water/Ocean.d.ts +6 -0
- package/dist/water/Ocean.js +30 -0
- package/dist/water/OceanConfig.d.ts +90 -0
- package/dist/water/OceanConfig.js +131 -0
- package/dist/water/OceanMaterial.d.ts +54 -0
- package/dist/water/OceanMaterial.js +51 -0
- package/dist/water/OceanShader.d.ts +2 -0
- package/dist/water/OceanShader.js +78 -0
- package/dist/water/index.d.ts +3 -0
- package/dist/water/index.js +3 -0
- package/dist/water.d.ts +1 -0
- package/dist/water.js +1 -0
- package/dist/weather/FireSpreadLayer.d.ts +14 -0
- package/dist/weather/FireSpreadLayer.js +68 -0
- package/dist/weather/LightningStrike.d.ts +17 -0
- package/dist/weather/LightningStrike.js +130 -0
- package/dist/weather/RainField.d.ts +20 -0
- package/dist/weather/RainField.js +107 -0
- package/dist/weather/SnowField.d.ts +19 -0
- package/dist/weather/SnowField.js +108 -0
- package/dist/weather/WeatherLayer.d.ts +17 -0
- package/dist/weather/WeatherLayer.js +16 -0
- package/dist/weather/index.d.ts +6 -0
- package/dist/weather/index.js +6 -0
- package/dist/weather/weatherGeometry.d.ts +2 -0
- package/dist/weather/weatherGeometry.js +14 -0
- package/dist/weather/weatherMath.d.ts +7 -0
- package/dist/weather/weatherMath.js +30 -0
- package/dist/weather/weatherUniforms.d.ts +18 -0
- package/dist/weather/weatherUniforms.js +34 -0
- package/dist/world/InstancedBodies.d.ts +17 -0
- package/dist/world/InstancedBodies.js +84 -0
- package/dist/world/InstancedJoints.d.ts +14 -0
- package/dist/world/InstancedJoints.js +32 -0
- package/dist/world/WorldHud.d.ts +2 -0
- package/dist/world/WorldHud.js +70 -12
- package/dist/world/WorldItems.d.ts +5 -0
- package/dist/world/WorldItems.js +31 -0
- package/dist/world/floatTextStyle.d.ts +14 -0
- package/dist/world/floatTextStyle.js +39 -0
- package/package.json +12 -4
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { isRegionField } from "@jgengine/core/world/regions";
|
|
2
|
+
function mix(a, b, t) {
|
|
3
|
+
return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
|
|
4
|
+
}
|
|
5
|
+
const DEFAULT_LAND_LOW = [46, 74, 45];
|
|
6
|
+
const DEFAULT_LAND_HIGH = [180, 172, 128];
|
|
7
|
+
const DEFAULT_WATER = [28, 74, 108];
|
|
8
|
+
/**
|
|
9
|
+
* Bake a top-down image of a `TerrainField` (or `RegionField`) over `bounds`
|
|
10
|
+
* for the react `Minimap` / `WorldMap` background. Runs in the browser via a
|
|
11
|
+
* 2D canvas; renderer-side, so it lives in the shell.
|
|
12
|
+
*/
|
|
13
|
+
export function bakeTerrainMap(field, bounds, options = {}) {
|
|
14
|
+
const res = options.resolution ?? 160;
|
|
15
|
+
if (typeof document === "undefined")
|
|
16
|
+
return null;
|
|
17
|
+
const canvas = document.createElement("canvas");
|
|
18
|
+
canvas.width = res;
|
|
19
|
+
canvas.height = res;
|
|
20
|
+
const context = canvas.getContext("2d");
|
|
21
|
+
if (context === null)
|
|
22
|
+
return null;
|
|
23
|
+
const region = isRegionField(field) ? field : null;
|
|
24
|
+
const waterLevel = field.waterLevel ?? Number.NEGATIVE_INFINITY;
|
|
25
|
+
const worldW = bounds.maxX - bounds.minX;
|
|
26
|
+
const worldD = bounds.maxZ - bounds.minZ;
|
|
27
|
+
const landLow = options.landLow ?? DEFAULT_LAND_LOW;
|
|
28
|
+
const landHigh = options.landHigh ?? DEFAULT_LAND_HIGH;
|
|
29
|
+
const water = options.water ?? DEFAULT_WATER;
|
|
30
|
+
const heights = new Float32Array(res * res);
|
|
31
|
+
let minHeight = Number.POSITIVE_INFINITY;
|
|
32
|
+
let maxHeight = Number.NEGATIVE_INFINITY;
|
|
33
|
+
for (let py = 0; py < res; py += 1) {
|
|
34
|
+
for (let px = 0; px < res; px += 1) {
|
|
35
|
+
const worldX = bounds.minX + ((px + 0.5) / res) * worldW;
|
|
36
|
+
const worldZ = bounds.minZ + ((py + 0.5) / res) * worldD;
|
|
37
|
+
const height = field.sampleHeight(worldX, worldZ);
|
|
38
|
+
heights[py * res + px] = height;
|
|
39
|
+
if (height < minHeight)
|
|
40
|
+
minHeight = height;
|
|
41
|
+
if (height > maxHeight)
|
|
42
|
+
maxHeight = height;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const span = maxHeight - minHeight || 1;
|
|
46
|
+
const image = context.createImageData(res, res);
|
|
47
|
+
for (let py = 0; py < res; py += 1) {
|
|
48
|
+
for (let px = 0; px < res; px += 1) {
|
|
49
|
+
const index = py * res + px;
|
|
50
|
+
const height = heights[index];
|
|
51
|
+
const normalized = (height - minHeight) / span;
|
|
52
|
+
let color;
|
|
53
|
+
if (height < waterLevel) {
|
|
54
|
+
color = [...water];
|
|
55
|
+
}
|
|
56
|
+
else if (region !== null) {
|
|
57
|
+
const worldX = bounds.minX + ((px + 0.5) / res) * worldW;
|
|
58
|
+
const worldZ = bounds.minZ + ((py + 0.5) / res) * worldD;
|
|
59
|
+
const sample = region.sampleRegion(worldX, worldZ);
|
|
60
|
+
const shade = 0.55 + normalized * 0.5;
|
|
61
|
+
color = [
|
|
62
|
+
Math.min(255, sample.tint[0] * 255 * shade),
|
|
63
|
+
Math.min(255, sample.tint[1] * 255 * shade),
|
|
64
|
+
Math.min(255, sample.tint[2] * 255 * shade),
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
color = mix(landLow, landHigh, normalized);
|
|
69
|
+
}
|
|
70
|
+
const offset = index * 4;
|
|
71
|
+
image.data[offset] = color[0];
|
|
72
|
+
image.data[offset + 1] = color[1];
|
|
73
|
+
image.data[offset + 2] = color[2];
|
|
74
|
+
image.data[offset + 3] = 255;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
context.putImageData(image, 0, 0);
|
|
78
|
+
return { url: canvas.toDataURL("image/png"), bounds };
|
|
79
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ContextMenu, ContextVerb } from "@jgengine/core/interaction/contextMenu";
|
|
2
|
+
import type { ScreenRect } from "@jgengine/core/scene/selection";
|
|
3
|
+
export declare function MarqueeBox({ rect }: {
|
|
4
|
+
rect: ScreenRect;
|
|
5
|
+
}): import("react").JSX.Element;
|
|
6
|
+
export declare function ContextMenuView({ menu, x, y, onPick, onClose, }: {
|
|
7
|
+
menu: ContextMenu;
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
onPick: (verb: ContextVerb) => void;
|
|
11
|
+
onClose: () => void;
|
|
12
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export function MarqueeBox({ rect }) {
|
|
3
|
+
return (_jsx("div", { className: "pointer-events-none absolute z-40 rounded-sm border border-emerald-300/80 bg-emerald-300/15", style: {
|
|
4
|
+
left: rect.minX,
|
|
5
|
+
top: rect.minY,
|
|
6
|
+
width: rect.maxX - rect.minX,
|
|
7
|
+
height: rect.maxY - rect.minY,
|
|
8
|
+
} }));
|
|
9
|
+
}
|
|
10
|
+
export function ContextMenuView({ menu, x, y, onPick, onClose, }) {
|
|
11
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40", onPointerDown: onClose, onContextMenu: (event) => event.preventDefault() }), _jsxs("div", { className: "pointer-events-auto absolute z-50 min-w-40 overflow-hidden rounded-md border border-white/15 bg-neutral-900/95 py-1 shadow-2xl backdrop-blur-sm", style: { left: x, top: y }, onContextMenu: (event) => event.preventDefault(), children: [_jsx("div", { className: "border-b border-white/10 px-3 py-1.5 text-[10px] font-semibold uppercase tracking-wider text-white/45", children: menu.kind === "entity" ? "Unit" : "Object" }), menu.verbs.map((verb, index) => (_jsx("button", { type: "button", disabled: verb.disabled === true, onPointerDown: (event) => {
|
|
12
|
+
event.stopPropagation();
|
|
13
|
+
if (verb.disabled === true)
|
|
14
|
+
return;
|
|
15
|
+
onPick(verb);
|
|
16
|
+
}, className: "flex w-full items-center px-3 py-1.5 text-left text-[13px] text-white/85 hover:bg-emerald-400/20 hover:text-white disabled:cursor-not-allowed disabled:text-white/30 disabled:hover:bg-transparent", children: verb.label }, `${verb.command}-${index}`)))] })] }));
|
|
17
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useThree } from "@react-three/fiber";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
export function PointerProbe({ service }) {
|
|
4
|
+
const camera = useThree((state) => state.camera);
|
|
5
|
+
const scene = useThree((state) => state.scene);
|
|
6
|
+
const gl = useThree((state) => state.gl);
|
|
7
|
+
const size = useThree((state) => state.size);
|
|
8
|
+
service.bind({ camera, scene, width: size.width, height: size.height });
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
const el = gl.domElement;
|
|
11
|
+
const onMove = (event) => {
|
|
12
|
+
const rect = el.getBoundingClientRect();
|
|
13
|
+
const x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
|
14
|
+
const y = -(((event.clientY - rect.top) / rect.height) * 2 - 1);
|
|
15
|
+
service.setCursor(x, y, true);
|
|
16
|
+
};
|
|
17
|
+
const onLeave = () => service.setCursor(0, 0, false);
|
|
18
|
+
el.addEventListener("pointermove", onMove);
|
|
19
|
+
el.addEventListener("pointerleave", onLeave);
|
|
20
|
+
return () => {
|
|
21
|
+
el.removeEventListener("pointermove", onMove);
|
|
22
|
+
el.removeEventListener("pointerleave", onLeave);
|
|
23
|
+
service.bind(null);
|
|
24
|
+
};
|
|
25
|
+
}, [gl, service]);
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import type { PointerHit, PointerVec3 } from "@jgengine/core/input/pointer";
|
|
3
|
+
export declare const POINTER_ENTITY_KEY = "jgEntityId";
|
|
4
|
+
export declare const POINTER_OBJECT_KEY = "jgObjectId";
|
|
5
|
+
interface PointerDeps {
|
|
6
|
+
camera: THREE.Camera;
|
|
7
|
+
scene: THREE.Scene;
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
}
|
|
11
|
+
export interface PointerService {
|
|
12
|
+
/** Cast the current cursor into the world; null when the cursor is off-canvas. */
|
|
13
|
+
worldHit(): PointerHit | null;
|
|
14
|
+
/** Project a world point to CSS pixels for the marquee / HUD; null before the probe binds. */
|
|
15
|
+
screenOf(world: PointerVec3): {
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
} | null;
|
|
19
|
+
hasCursor(): boolean;
|
|
20
|
+
bind(deps: PointerDeps | null): void;
|
|
21
|
+
setCursor(ndcX: number, ndcY: number, present: boolean): void;
|
|
22
|
+
}
|
|
23
|
+
export declare function createPointerService(): PointerService;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
export const POINTER_ENTITY_KEY = "jgEntityId";
|
|
3
|
+
export const POINTER_OBJECT_KEY = "jgObjectId";
|
|
4
|
+
function tagOf(object, key) {
|
|
5
|
+
let cursor = object;
|
|
6
|
+
while (cursor !== null) {
|
|
7
|
+
const value = cursor.userData[key];
|
|
8
|
+
if (typeof value === "string")
|
|
9
|
+
return value;
|
|
10
|
+
cursor = cursor.parent;
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
export function createPointerService() {
|
|
15
|
+
const raycaster = new THREE.Raycaster();
|
|
16
|
+
const ndc = new THREE.Vector2();
|
|
17
|
+
const scratch = new THREE.Vector3();
|
|
18
|
+
const normalMatrix = new THREE.Matrix3();
|
|
19
|
+
const groundPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
|
|
20
|
+
let deps = null;
|
|
21
|
+
let cursorPresent = false;
|
|
22
|
+
return {
|
|
23
|
+
hasCursor: () => cursorPresent,
|
|
24
|
+
bind: (next) => {
|
|
25
|
+
deps = next;
|
|
26
|
+
},
|
|
27
|
+
setCursor: (ndcX, ndcY, present) => {
|
|
28
|
+
ndc.set(ndcX, ndcY);
|
|
29
|
+
cursorPresent = present;
|
|
30
|
+
},
|
|
31
|
+
worldHit() {
|
|
32
|
+
if (deps === null || !cursorPresent)
|
|
33
|
+
return null;
|
|
34
|
+
raycaster.setFromCamera(ndc, deps.camera);
|
|
35
|
+
const intersects = raycaster.intersectObjects(deps.scene.children, true);
|
|
36
|
+
for (const hit of intersects) {
|
|
37
|
+
if (!hit.object.isMesh)
|
|
38
|
+
continue;
|
|
39
|
+
const point = [hit.point.x, hit.point.y, hit.point.z];
|
|
40
|
+
let normal = [0, 1, 0];
|
|
41
|
+
if (hit.face !== null && hit.face !== undefined) {
|
|
42
|
+
normalMatrix.getNormalMatrix(hit.object.matrixWorld);
|
|
43
|
+
scratch.copy(hit.face.normal).applyMatrix3(normalMatrix).normalize();
|
|
44
|
+
normal = [scratch.x, scratch.y, scratch.z];
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
point,
|
|
48
|
+
normal,
|
|
49
|
+
entity: tagOf(hit.object, POINTER_ENTITY_KEY),
|
|
50
|
+
object: tagOf(hit.object, POINTER_OBJECT_KEY),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const grounded = raycaster.ray.intersectPlane(groundPlane, scratch);
|
|
54
|
+
if (grounded === null)
|
|
55
|
+
return null;
|
|
56
|
+
return { point: [grounded.x, grounded.y, grounded.z], normal: [0, 1, 0], entity: null, object: null };
|
|
57
|
+
},
|
|
58
|
+
screenOf(world) {
|
|
59
|
+
if (deps === null)
|
|
60
|
+
return null;
|
|
61
|
+
scratch.set(world[0], world[1], world[2]).project(deps.camera);
|
|
62
|
+
return {
|
|
63
|
+
x: (scratch.x * 0.5 + 0.5) * deps.width,
|
|
64
|
+
y: (-scratch.y * 0.5 + 0.5) * deps.height,
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
package/dist/registry.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type { ComponentType } from "react";
|
|
1
|
+
import type { ComponentType, ReactNode } from "react";
|
|
2
2
|
import type { PlayableGame as EnginePlayableGame } from "@jgengine/core/game/playableGame";
|
|
3
|
-
|
|
3
|
+
import type { SceneEntity } from "@jgengine/core/scene/entityStore";
|
|
4
|
+
export type RenderEntity = (entity: SceneEntity) => ReactNode;
|
|
5
|
+
export type PlayableGame = EnginePlayableGame<ComponentType, ComponentType, RenderEntity>;
|
|
4
6
|
export type GameRegistry = Record<string, () => Promise<PlayableGame>>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type RecordingBuffer, type RecordingBufferOptions } from "@jgengine/core/sensor/recordingBuffer";
|
|
2
|
+
import type { EntityPosition } from "@jgengine/core/scene/entityStore";
|
|
3
|
+
export interface RecordedPose {
|
|
4
|
+
position: EntityPosition;
|
|
5
|
+
rotationY: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Session-recording buffer (#120) for replay / photo mode / kill-cam: records
|
|
9
|
+
* an entity's pose on game-time every frame into a `RecordingBuffer`, which a
|
|
10
|
+
* game can then `seek()` to scrub, drive an observer cam ghost, or export a
|
|
11
|
+
* kill-cam clip. Recording rides on `ctx.time.now()`, so pause/fast-forward
|
|
12
|
+
* scrub the recording exactly like the live sim.
|
|
13
|
+
*/
|
|
14
|
+
export declare function useSessionRecorder(entityId: string, options?: RecordingBufferOptions): RecordingBuffer<RecordedPose>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useFrame } from "@react-three/fiber";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { createRecordingBuffer } from "@jgengine/core/sensor/recordingBuffer";
|
|
4
|
+
import { useGameContext } from "@jgengine/react/provider";
|
|
5
|
+
/**
|
|
6
|
+
* Session-recording buffer (#120) for replay / photo mode / kill-cam: records
|
|
7
|
+
* an entity's pose on game-time every frame into a `RecordingBuffer`, which a
|
|
8
|
+
* game can then `seek()` to scrub, drive an observer cam ghost, or export a
|
|
9
|
+
* kill-cam clip. Recording rides on `ctx.time.now()`, so pause/fast-forward
|
|
10
|
+
* scrub the recording exactly like the live sim.
|
|
11
|
+
*/
|
|
12
|
+
export function useSessionRecorder(entityId, options) {
|
|
13
|
+
const ctx = useGameContext();
|
|
14
|
+
const buffer = useMemo(() => createRecordingBuffer(options), []);
|
|
15
|
+
useFrame(() => {
|
|
16
|
+
const entity = ctx.scene.entity.get(entityId);
|
|
17
|
+
if (entity === null)
|
|
18
|
+
return;
|
|
19
|
+
buffer.append(ctx.time.now(), { position: entity.position, rotationY: entity.rotationY });
|
|
20
|
+
});
|
|
21
|
+
return buffer;
|
|
22
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export type BuildingFacade = "front" | "back" | "left" | "right" | "roof";
|
|
3
|
+
export type BuildingPartKind = "wall" | "window" | "awning" | "airConditioner" | "clothesline" | "storefront" | "shutter" | "storeSign" | "roof" | "roofProp" | "guardrail" | "corner";
|
|
4
|
+
export interface BuildingPartPlacement {
|
|
5
|
+
id: string;
|
|
6
|
+
kind: BuildingPartKind;
|
|
7
|
+
facade: BuildingFacade;
|
|
8
|
+
position: readonly [number, number, number];
|
|
9
|
+
rotationY: number;
|
|
10
|
+
scale: readonly [number, number, number];
|
|
11
|
+
}
|
|
12
|
+
export interface GeneratedBuildingData {
|
|
13
|
+
id: string;
|
|
14
|
+
parts: readonly BuildingPartPlacement[];
|
|
15
|
+
}
|
|
16
|
+
export interface BuildingMaterialPalette {
|
|
17
|
+
wall?: string;
|
|
18
|
+
window?: string;
|
|
19
|
+
awning?: string;
|
|
20
|
+
airConditioner?: string;
|
|
21
|
+
clothesline?: string;
|
|
22
|
+
storefront?: string;
|
|
23
|
+
shutter?: string;
|
|
24
|
+
storeSign?: string;
|
|
25
|
+
roof?: string;
|
|
26
|
+
roofProp?: string;
|
|
27
|
+
guardrail?: string;
|
|
28
|
+
corner?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface BuildingKitRenderer {
|
|
31
|
+
renderPart?: (part: BuildingPartPlacement) => ReactNode | undefined;
|
|
32
|
+
}
|
|
33
|
+
export interface BuildingBlockProps {
|
|
34
|
+
part: BuildingPartPlacement;
|
|
35
|
+
palette?: BuildingMaterialPalette;
|
|
36
|
+
}
|
|
37
|
+
export interface GeneratedBuildingProps {
|
|
38
|
+
building: GeneratedBuildingData;
|
|
39
|
+
palette?: BuildingMaterialPalette;
|
|
40
|
+
kit?: BuildingKitRenderer;
|
|
41
|
+
visibleKinds?: readonly BuildingPartKind[];
|
|
42
|
+
}
|
|
43
|
+
export declare function BuildingBlock({ part, palette }: BuildingBlockProps): import("react").JSX.Element;
|
|
44
|
+
export declare function GeneratedBuilding({ building, palette, kit, visibleKinds }: GeneratedBuildingProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
const DEFAULT_PALETTE = {
|
|
4
|
+
wall: "#83766a",
|
|
5
|
+
window: "#8ecae6",
|
|
6
|
+
awning: "#c2410c",
|
|
7
|
+
airConditioner: "#d4d4d8",
|
|
8
|
+
clothesline: "#facc15",
|
|
9
|
+
storefront: "#3f3f46",
|
|
10
|
+
shutter: "#52525b",
|
|
11
|
+
storeSign: "#f97316",
|
|
12
|
+
roof: "#57534e",
|
|
13
|
+
roofProp: "#14b8a6",
|
|
14
|
+
guardrail: "#a8a29e",
|
|
15
|
+
corner: "#6b6258",
|
|
16
|
+
};
|
|
17
|
+
function colorFor(kind, palette) {
|
|
18
|
+
return palette?.[kind] ?? DEFAULT_PALETTE[kind];
|
|
19
|
+
}
|
|
20
|
+
function normalFor(facade) {
|
|
21
|
+
if (facade === "front")
|
|
22
|
+
return [0, 1];
|
|
23
|
+
if (facade === "back")
|
|
24
|
+
return [0, -1];
|
|
25
|
+
if (facade === "left")
|
|
26
|
+
return [-1, 0];
|
|
27
|
+
if (facade === "right")
|
|
28
|
+
return [1, 0];
|
|
29
|
+
return [0, 0];
|
|
30
|
+
}
|
|
31
|
+
function outwardOffset(part) {
|
|
32
|
+
if (part.kind === "wall" || part.kind === "roof" || part.kind === "corner")
|
|
33
|
+
return 0;
|
|
34
|
+
if (part.kind === "window" || part.kind === "shutter" || part.kind === "storefront")
|
|
35
|
+
return 0.025;
|
|
36
|
+
if (part.kind === "awning" || part.kind === "storeSign")
|
|
37
|
+
return 0.09;
|
|
38
|
+
return 0.12;
|
|
39
|
+
}
|
|
40
|
+
function materialFor(part, palette) {
|
|
41
|
+
const color = colorFor(part.kind, palette);
|
|
42
|
+
if (part.kind === "window" || part.kind === "storefront") {
|
|
43
|
+
return _jsx("meshPhysicalMaterial", { color: color, roughness: 0.12, metalness: 0, transparent: true, opacity: 0.56 });
|
|
44
|
+
}
|
|
45
|
+
if (part.kind === "storeSign") {
|
|
46
|
+
return _jsx("meshStandardMaterial", { color: color, emissive: color, emissiveIntensity: 0.6, roughness: 0.5 });
|
|
47
|
+
}
|
|
48
|
+
if (part.kind === "roofProp") {
|
|
49
|
+
return _jsx("meshStandardMaterial", { color: color, roughness: 0.65, metalness: 0.1 });
|
|
50
|
+
}
|
|
51
|
+
return _jsx("meshStandardMaterial", { color: color, roughness: 0.88, metalness: 0 });
|
|
52
|
+
}
|
|
53
|
+
function BlockMesh({ part, palette }) {
|
|
54
|
+
const [nx, nz] = normalFor(part.facade);
|
|
55
|
+
const offset = outwardOffset(part);
|
|
56
|
+
const position = [
|
|
57
|
+
part.position[0] + nx * offset,
|
|
58
|
+
part.position[1],
|
|
59
|
+
part.position[2] + nz * offset,
|
|
60
|
+
];
|
|
61
|
+
return (_jsxs("mesh", { position: position, rotation: [0, part.rotationY, 0], castShadow: true, receiveShadow: true, children: [_jsx("boxGeometry", { args: [part.scale[0], part.scale[1], part.scale[2]] }), materialFor(part, palette)] }));
|
|
62
|
+
}
|
|
63
|
+
export function BuildingBlock({ part, palette }) {
|
|
64
|
+
if (part.kind === "clothesline") {
|
|
65
|
+
const [nx, nz] = normalFor(part.facade);
|
|
66
|
+
const offset = outwardOffset(part);
|
|
67
|
+
const position = [
|
|
68
|
+
part.position[0] + nx * offset,
|
|
69
|
+
part.position[1],
|
|
70
|
+
part.position[2] + nz * offset,
|
|
71
|
+
];
|
|
72
|
+
return (_jsxs("group", { position: position, rotation: [0, part.rotationY, 0], children: [_jsxs("mesh", { position: [0, 0, -0.09], castShadow: true, children: [_jsx("boxGeometry", { args: [part.scale[0], Math.max(part.scale[1], 0.025), 0.025] }), materialFor(part, palette)] }), _jsxs("mesh", { position: [0, 0, 0.09], castShadow: true, children: [_jsx("boxGeometry", { args: [part.scale[0], Math.max(part.scale[1], 0.025), 0.025] }), materialFor(part, palette)] })] }));
|
|
73
|
+
}
|
|
74
|
+
return _jsx(BlockMesh, { part: part, palette: palette });
|
|
75
|
+
}
|
|
76
|
+
export function GeneratedBuilding({ building, palette, kit, visibleKinds }) {
|
|
77
|
+
const visible = useMemo(() => (visibleKinds === undefined ? null : new Set(visibleKinds)), [visibleKinds]);
|
|
78
|
+
return (_jsx("group", { name: building.id, children: building.parts.map((part) => {
|
|
79
|
+
if (visible !== null && !visible.has(part.kind))
|
|
80
|
+
return null;
|
|
81
|
+
const rendered = kit?.renderPart?.(part);
|
|
82
|
+
if (rendered !== undefined)
|
|
83
|
+
return _jsx("group", { children: rendered }, part.id);
|
|
84
|
+
return _jsx(BuildingBlock, { part: part, palette: palette }, part.id);
|
|
85
|
+
}) }));
|
|
86
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PlacementPreview } from "@jgengine/core/world/placementController";
|
|
2
|
+
export interface PlacementGhostProps {
|
|
3
|
+
preview: PlacementPreview | null;
|
|
4
|
+
height?: number;
|
|
5
|
+
validColor?: string;
|
|
6
|
+
invalidColor?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function PlacementGhost({ preview, height, validColor, invalidColor, }: PlacementGhostProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { quarterTurnsToRotationY } from "@jgengine/core/world/placementController";
|
|
4
|
+
export function PlacementGhost({ preview, height = 1, validColor = "#34d399", invalidColor = "#f87171", }) {
|
|
5
|
+
const footprint = preview?.footprint;
|
|
6
|
+
const size = useMemo(() => [footprint?.w ?? 1, height, footprint?.d ?? 1], [footprint?.w, footprint?.d, height]);
|
|
7
|
+
if (preview === null)
|
|
8
|
+
return null;
|
|
9
|
+
const color = preview.valid ? validColor : invalidColor;
|
|
10
|
+
return (_jsxs("group", { position: [preview.center[0], preview.y, preview.center[1]], "rotation-y": quarterTurnsToRotationY(preview.quarterTurns), children: [_jsxs("mesh", { "position-y": height / 2, children: [_jsx("boxGeometry", { args: size }), _jsx("meshStandardMaterial", { color: color, transparent: true, opacity: 0.35, depthWrite: false, emissive: color, emissiveIntensity: 0.4 })] }), _jsxs("mesh", { "rotation-x": -Math.PI / 2, "position-y": 0.02, children: [_jsx("ringGeometry", { args: [Math.max(size[0], size[2]) * 0.5, Math.max(size[0], size[2]) * 0.5 + 0.12, 40] }), _jsx("meshBasicMaterial", { color: color, transparent: true, opacity: 0.85 })] })] }));
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./structures/index.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./structures/index.js";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ThreeElements } from "@react-three/fiber";
|
|
2
|
+
import type { TerrainField } from "@jgengine/core/world/terrain";
|
|
3
|
+
import { type FieldGroundOptions } from "./terrainMath.js";
|
|
4
|
+
export interface CarvedTerrainProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material"> {
|
|
5
|
+
/** The terrain field to mesh — a `CarvableField` renders its craters and mounds. */
|
|
6
|
+
field: TerrainField;
|
|
7
|
+
size?: FieldGroundOptions["size"];
|
|
8
|
+
segments?: FieldGroundOptions["segments"];
|
|
9
|
+
center?: FieldGroundOptions["center"];
|
|
10
|
+
colors?: FieldGroundOptions["colors"];
|
|
11
|
+
heightRange?: FieldGroundOptions["heightRange"];
|
|
12
|
+
roughness?: number;
|
|
13
|
+
metalness?: number;
|
|
14
|
+
/** Bump after a runtime carve/deposit to re-mesh the deformed surface. */
|
|
15
|
+
epoch?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Renders a `TerrainField` as a deformed ground mesh — the crater/mound view for destructible terrain.
|
|
19
|
+
* Because the geometry samples `field.sampleHeight`, a `CarvableField.carve(...)` shows as a real bowl
|
|
20
|
+
* once `epoch` changes. Pair with `InstancedBodies` to see debris resting in the crater it blasted.
|
|
21
|
+
*/
|
|
22
|
+
export declare function CarvedTerrain({ field, size, segments, center, colors, heightRange, roughness, metalness, receiveShadow, epoch, ...meshProps }: CarvedTerrainProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import {} from "@react-three/fiber";
|
|
3
|
+
import { useEffect, useMemo } from "react";
|
|
4
|
+
import * as THREE from "three";
|
|
5
|
+
import { createFieldGroundGeometry } from "./terrainMath.js";
|
|
6
|
+
/**
|
|
7
|
+
* Renders a `TerrainField` as a deformed ground mesh — the crater/mound view for destructible terrain.
|
|
8
|
+
* Because the geometry samples `field.sampleHeight`, a `CarvableField.carve(...)` shows as a real bowl
|
|
9
|
+
* once `epoch` changes. Pair with `InstancedBodies` to see debris resting in the crater it blasted.
|
|
10
|
+
*/
|
|
11
|
+
export function CarvedTerrain({ field, size, segments, center, colors, heightRange, roughness = 0.95, metalness = 0, receiveShadow = true, epoch = 0, ...meshProps }) {
|
|
12
|
+
const geometry = useMemo(() => createFieldGroundGeometry(field, { size, segments, center, colors, heightRange }),
|
|
13
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
14
|
+
[field, size, segments, center, colors, heightRange, epoch]);
|
|
15
|
+
const material = useMemo(() => new THREE.MeshStandardMaterial({ color: "#ffffff", roughness, metalness, vertexColors: true }), [metalness, roughness]);
|
|
16
|
+
useEffect(() => () => geometry.dispose(), [geometry]);
|
|
17
|
+
useEffect(() => () => material.dispose(), [material]);
|
|
18
|
+
return _jsx("mesh", { ...meshProps, geometry: geometry, material: material, receiveShadow: receiveShadow });
|
|
19
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Aabb } from "@jgengine/core/world/geometry";
|
|
2
|
+
import type { EditableTerrain } from "@jgengine/core/world/terraform";
|
|
3
|
+
export interface EditableGroundProps {
|
|
4
|
+
terrain: Pick<EditableTerrain, "sampleHeight" | "surfaceAt">;
|
|
5
|
+
bounds: Aabb;
|
|
6
|
+
segments?: number;
|
|
7
|
+
version?: number;
|
|
8
|
+
baseColor?: string;
|
|
9
|
+
surfaceColors?: Record<string, string>;
|
|
10
|
+
}
|
|
11
|
+
export declare function EditableGround({ terrain, bounds, segments, version, baseColor, surfaceColors, }: EditableGroundProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import * as THREE from "three";
|
|
4
|
+
const DEFAULT_SURFACE_COLORS = {
|
|
5
|
+
path: "#b8a06a",
|
|
6
|
+
gravel: "#8d8d84",
|
|
7
|
+
stone: "#7c7f86",
|
|
8
|
+
grass: "#4b7f3f",
|
|
9
|
+
};
|
|
10
|
+
export function EditableGround({ terrain, bounds, segments = 96, version = 0, baseColor = "#3f6b3a", surfaceColors = DEFAULT_SURFACE_COLORS, }) {
|
|
11
|
+
const geometry = useMemo(() => {
|
|
12
|
+
void version;
|
|
13
|
+
const width = bounds.maxX - bounds.minX;
|
|
14
|
+
const depth = bounds.maxZ - bounds.minZ;
|
|
15
|
+
const geo = new THREE.PlaneGeometry(width, depth, segments, segments);
|
|
16
|
+
geo.rotateX(-Math.PI / 2);
|
|
17
|
+
const position = geo.attributes.position;
|
|
18
|
+
const base = new THREE.Color(baseColor);
|
|
19
|
+
const colors = new Float32Array(position.count * 3);
|
|
20
|
+
const cx = (bounds.minX + bounds.maxX) / 2;
|
|
21
|
+
const cz = (bounds.minZ + bounds.maxZ) / 2;
|
|
22
|
+
for (let index = 0; index < position.count; index += 1) {
|
|
23
|
+
const x = position.getX(index) + cx;
|
|
24
|
+
const z = position.getZ(index) + cz;
|
|
25
|
+
position.setY(index, terrain.sampleHeight(x, z));
|
|
26
|
+
const surface = terrain.surfaceAt(x, z);
|
|
27
|
+
const color = surface !== null && surfaceColors[surface] !== undefined ? new THREE.Color(surfaceColors[surface]) : base;
|
|
28
|
+
colors[index * 3] = color.r;
|
|
29
|
+
colors[index * 3 + 1] = color.g;
|
|
30
|
+
colors[index * 3 + 2] = color.b;
|
|
31
|
+
}
|
|
32
|
+
geo.setAttribute("color", new THREE.BufferAttribute(colors, 3));
|
|
33
|
+
geo.computeVertexNormals();
|
|
34
|
+
return geo;
|
|
35
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
36
|
+
}, [terrain, bounds, segments, version, baseColor, surfaceColors]);
|
|
37
|
+
return (_jsx("mesh", { geometry: geometry, receiveShadow: true, children: _jsx("meshStandardMaterial", { vertexColors: true, roughness: 0.95, metalness: 0 }) }));
|
|
38
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ThreeElements } from "@react-three/fiber";
|
|
2
|
+
import { type GrassBladeGeometryOptions, type GrassRange } from "./grassGeometry.js";
|
|
3
|
+
import { type GrassMaterialOptions, type GrassWindOptions } from "./grassMaterial.js";
|
|
4
|
+
import type { TerrainArea, TerrainHeightSampler } from "./terrainMath.js";
|
|
5
|
+
export interface GrassFieldProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material"> {
|
|
6
|
+
count?: number;
|
|
7
|
+
density?: number;
|
|
8
|
+
area?: TerrainArea;
|
|
9
|
+
seed?: GrassBladeGeometryOptions["seed"];
|
|
10
|
+
segments?: number;
|
|
11
|
+
bladeHeight?: GrassRange;
|
|
12
|
+
bladeWidth?: GrassRange;
|
|
13
|
+
bladeBend?: GrassRange;
|
|
14
|
+
heightAt?: TerrainHeightSampler;
|
|
15
|
+
colorBase?: GrassMaterialOptions["colorBase"];
|
|
16
|
+
colorTip?: GrassMaterialOptions["colorTip"];
|
|
17
|
+
colorVariation?: number;
|
|
18
|
+
wind?: GrassWindOptions | false;
|
|
19
|
+
roughness?: number;
|
|
20
|
+
}
|
|
21
|
+
export declare function GrassField({ count, density, area, seed, segments, bladeHeight, bladeWidth, bladeBend, heightAt, colorBase, colorTip, colorVariation, wind, roughness, castShadow, receiveShadow, frustumCulled, ...meshProps }: GrassFieldProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useFrame } from "@react-three/fiber";
|
|
3
|
+
import { useEffect, useMemo } from "react";
|
|
4
|
+
import { createGrassBladeGeometry } from "./grassGeometry.js";
|
|
5
|
+
import { createGrassMaterial } from "./grassMaterial.js";
|
|
6
|
+
export function GrassField({ count = 6000, density = 1, area = 40, seed = 1, segments = 4, bladeHeight, bladeWidth, bladeBend, heightAt, colorBase, colorTip, colorVariation, wind, roughness, castShadow = false, receiveShadow = true, frustumCulled = false, ...meshProps }) {
|
|
7
|
+
const geometry = useMemo(() => createGrassBladeGeometry({
|
|
8
|
+
count,
|
|
9
|
+
area,
|
|
10
|
+
seed,
|
|
11
|
+
segments,
|
|
12
|
+
height: bladeHeight,
|
|
13
|
+
width: bladeWidth,
|
|
14
|
+
bend: bladeBend,
|
|
15
|
+
heightAt,
|
|
16
|
+
}), [area, bladeBend, bladeHeight, bladeWidth, count, heightAt, seed, segments]);
|
|
17
|
+
const handle = useMemo(() => createGrassMaterial({
|
|
18
|
+
colorBase,
|
|
19
|
+
colorTip,
|
|
20
|
+
colorVariation,
|
|
21
|
+
wind,
|
|
22
|
+
roughness,
|
|
23
|
+
}), [colorBase, colorTip, colorVariation, roughness, wind]);
|
|
24
|
+
geometry.instanceCount = Math.floor(Math.max(0, Math.min(1, density)) * count);
|
|
25
|
+
useFrame((state) => {
|
|
26
|
+
handle.uniforms.uTime.value = state.clock.elapsedTime;
|
|
27
|
+
});
|
|
28
|
+
useEffect(() => () => geometry.dispose(), [geometry]);
|
|
29
|
+
useEffect(() => () => handle.material.dispose(), [handle]);
|
|
30
|
+
return (_jsx("mesh", { ...meshProps, geometry: geometry, material: handle.material, castShadow: castShadow, receiveShadow: receiveShadow, frustumCulled: frustumCulled }));
|
|
31
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ThreeElements } from "@react-three/fiber";
|
|
2
|
+
import { type ProceduralTerrainConfig, type TerrainVertexColorOptions } from "./terrainMath.js";
|
|
3
|
+
export interface ProceduralGroundProps extends Omit<ThreeElements["mesh"], "args" | "children" | "geometry" | "material"> {
|
|
4
|
+
terrain?: ProceduralTerrainConfig;
|
|
5
|
+
colors?: TerrainVertexColorOptions;
|
|
6
|
+
roughness?: number;
|
|
7
|
+
metalness?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function ProceduralGround({ terrain, colors, roughness, metalness, receiveShadow, ...meshProps }: ProceduralGroundProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import {} from "@react-three/fiber";
|
|
3
|
+
import { useEffect, useMemo } from "react";
|
|
4
|
+
import * as THREE from "three";
|
|
5
|
+
import { createProceduralGroundGeometry } from "./terrainMath.js";
|
|
6
|
+
export function ProceduralGround({ terrain, colors, roughness = 0.94, metalness = 0, receiveShadow = true, ...meshProps }) {
|
|
7
|
+
const geometry = useMemo(() => createProceduralGroundGeometry(terrain, colors), [colors, terrain]);
|
|
8
|
+
const material = useMemo(() => new THREE.MeshStandardMaterial({
|
|
9
|
+
color: "#ffffff",
|
|
10
|
+
roughness,
|
|
11
|
+
metalness,
|
|
12
|
+
vertexColors: true,
|
|
13
|
+
}), [metalness, roughness]);
|
|
14
|
+
useEffect(() => () => geometry.dispose(), [geometry]);
|
|
15
|
+
useEffect(() => () => material.dispose(), [material]);
|
|
16
|
+
return _jsx("mesh", { ...meshProps, geometry: geometry, material: material, receiveShadow: receiveShadow });
|
|
17
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { TerraformMode } from "@jgengine/core/world/terraform";
|
|
2
|
+
export interface TerraformBrushCursorProps {
|
|
3
|
+
center: readonly [number, number] | null;
|
|
4
|
+
y?: number;
|
|
5
|
+
radius: number;
|
|
6
|
+
mode: TerraformMode;
|
|
7
|
+
}
|
|
8
|
+
export declare function TerraformBrushCursor({ center, y, radius, mode }: TerraformBrushCursorProps): import("react").JSX.Element | null;
|