@jgengine/shell 0.6.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 +167 -2
- package/dist/GamePlayerShell.js +210 -39
- 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/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/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/replay/useSessionRecorder.d.ts +14 -0
- package/dist/replay/useSessionRecorder.js +22 -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/TerraformBrushCursor.d.ts +8 -0
- package/dist/terrain/TerraformBrushCursor.js +13 -0
- package/dist/terrain/index.d.ts +4 -1
- package/dist/terrain/index.js +4 -1
- package/dist/terrain/terrainMath.d.ts +14 -1
- package/dist/terrain/terrainMath.js +80 -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/weather/FireSpreadLayer.d.ts +14 -0
- package/dist/weather/FireSpreadLayer.js +68 -0
- package/dist/weather/index.d.ts +1 -0
- package/dist/weather/index.js +1 -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 +8 -4
|
@@ -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
|
+
}
|
|
@@ -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,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,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;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
const MODE_COLOR = {
|
|
3
|
+
raise: "#38bdf8",
|
|
4
|
+
lower: "#fb923c",
|
|
5
|
+
flatten: "#a3e635",
|
|
6
|
+
paint: "#e879f9",
|
|
7
|
+
};
|
|
8
|
+
export function TerraformBrushCursor({ center, y = 0.05, radius, mode }) {
|
|
9
|
+
if (center === null)
|
|
10
|
+
return null;
|
|
11
|
+
const color = MODE_COLOR[mode];
|
|
12
|
+
return (_jsxs("group", { position: [center[0], y, center[1]], children: [_jsxs("mesh", { "rotation-x": -Math.PI / 2, children: [_jsx("ringGeometry", { args: [radius - 0.15, radius, 48] }), _jsx("meshBasicMaterial", { color: color, transparent: true, opacity: 0.9 })] }), _jsxs("mesh", { "rotation-x": -Math.PI / 2, "position-y": -0.02, children: [_jsx("circleGeometry", { args: [radius, 48] }), _jsx("meshBasicMaterial", { color: color, transparent: true, opacity: 0.14, depthWrite: false })] })] }));
|
|
13
|
+
}
|
package/dist/terrain/index.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
export { CarvedTerrain, type CarvedTerrainProps } from "./CarvedTerrain.js";
|
|
1
2
|
export { GrassField, type GrassFieldProps } from "./GrassField.js";
|
|
2
3
|
export { ProceduralGround, type ProceduralGroundProps } from "./ProceduralGround.js";
|
|
4
|
+
export { EditableGround, type EditableGroundProps } from "./EditableGround.js";
|
|
5
|
+
export { TerraformBrushCursor, type TerraformBrushCursorProps } from "./TerraformBrushCursor.js";
|
|
3
6
|
export { createGrassBladeGeometry, resolveGrassBladeGeometryOptions, resolveGrassRange, type GrassBladeGeometryOptions, type GrassRange, type ResolvedGrassBladeGeometryOptions, } from "./grassGeometry.js";
|
|
4
7
|
export { createGrassMaterial, DEFAULT_GRASS_WIND, resolveGrassWind, type GrassMaterialHandle, type GrassMaterialOptions, type GrassShaderUniforms, type GrassWindOptions, } from "./grassMaterial.js";
|
|
5
8
|
export { createSeededRandom, hashNoise2, seedToUint32, type TerrainSeed } from "./random.js";
|
|
6
|
-
export { createProceduralGroundGeometry, createProceduralTerrainSampler, resolveTerrainSegments, resolveTerrainSize, toNoiseFieldConfig, type ProceduralTerrainConfig, type ResolvedTerrainSegments, type ResolvedTerrainSize, type TerrainArea, type TerrainHeightSampler, type TerrainVertexColorOptions, } from "./terrainMath.js";
|
|
9
|
+
export { createFieldGroundGeometry, createProceduralGroundGeometry, createProceduralTerrainSampler, resolveTerrainSegments, resolveTerrainSize, toNoiseFieldConfig, type FieldGroundOptions, type ProceduralTerrainConfig, type ResolvedTerrainSegments, type ResolvedTerrainSize, type TerrainArea, type TerrainHeightSampler, type TerrainVertexColorOptions, } from "./terrainMath.js";
|
|
7
10
|
export { arenaField, flatField, fractalNoise, noiseField, resolveGroundStep, resolveTerrainField, valueNoise, withNormal, type FractalNoiseConfig, type NoiseFieldConfig, type TerrainField, type TerrainNormal, } from "@jgengine/core/world/terrain";
|
package/dist/terrain/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
export { CarvedTerrain } from "./CarvedTerrain.js";
|
|
1
2
|
export { GrassField } from "./GrassField.js";
|
|
2
3
|
export { ProceduralGround } from "./ProceduralGround.js";
|
|
4
|
+
export { EditableGround } from "./EditableGround.js";
|
|
5
|
+
export { TerraformBrushCursor } from "./TerraformBrushCursor.js";
|
|
3
6
|
export { createGrassBladeGeometry, resolveGrassBladeGeometryOptions, resolveGrassRange, } from "./grassGeometry.js";
|
|
4
7
|
export { createGrassMaterial, DEFAULT_GRASS_WIND, resolveGrassWind, } from "./grassMaterial.js";
|
|
5
8
|
export { createSeededRandom, hashNoise2, seedToUint32 } from "./random.js";
|
|
6
|
-
export { createProceduralGroundGeometry, createProceduralTerrainSampler, resolveTerrainSegments, resolveTerrainSize, toNoiseFieldConfig, } from "./terrainMath.js";
|
|
9
|
+
export { createFieldGroundGeometry, createProceduralGroundGeometry, createProceduralTerrainSampler, resolveTerrainSegments, resolveTerrainSize, toNoiseFieldConfig, } from "./terrainMath.js";
|
|
7
10
|
export { arenaField, flatField, fractalNoise, noiseField, resolveGroundStep, resolveTerrainField, valueNoise, withNormal, } from "@jgengine/core/world/terrain";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type NoiseFieldConfig } from "@jgengine/core/world/terrain";
|
|
1
|
+
import { type NoiseFieldConfig, type TerrainField } from "@jgengine/core/world/terrain";
|
|
2
2
|
import * as THREE from "three";
|
|
3
3
|
import { type TerrainSeed } from "./random.js";
|
|
4
4
|
export type TerrainArea = number | readonly [width: number, depth: number];
|
|
@@ -31,4 +31,17 @@ export declare function resolveTerrainSize(size?: TerrainArea): ResolvedTerrainS
|
|
|
31
31
|
export declare function resolveTerrainSegments(segments?: ProceduralTerrainConfig["segments"]): ResolvedTerrainSegments;
|
|
32
32
|
export declare function toNoiseFieldConfig(config?: ProceduralTerrainConfig): NoiseFieldConfig;
|
|
33
33
|
export declare function createProceduralTerrainSampler(config?: ProceduralTerrainConfig): TerrainHeightSampler;
|
|
34
|
+
export interface FieldGroundOptions {
|
|
35
|
+
size?: TerrainArea;
|
|
36
|
+
segments?: number | readonly [x: number, z: number];
|
|
37
|
+
center?: readonly [x: number, z: number];
|
|
38
|
+
colors?: TerrainVertexColorOptions;
|
|
39
|
+
heightRange?: readonly [min: number, max: number];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Mesh any `TerrainField` — including a `CarvableField` with craters/mounds written into it — into a
|
|
43
|
+
* vertex-coloured ground geometry. `sampleHeight` drives the vertices, so runtime carves show up as
|
|
44
|
+
* real depressions the moment the field is re-sampled (bump the caller's rebuild key after a carve).
|
|
45
|
+
*/
|
|
46
|
+
export declare function createFieldGroundGeometry(field: TerrainField, options?: FieldGroundOptions): THREE.BufferGeometry;
|
|
34
47
|
export declare function createProceduralGroundGeometry(config?: ProceduralTerrainConfig, colors?: TerrainVertexColorOptions): THREE.BufferGeometry;
|
|
@@ -23,6 +23,86 @@ export function toNoiseFieldConfig(config = {}) {
|
|
|
23
23
|
export function createProceduralTerrainSampler(config = {}) {
|
|
24
24
|
return noiseField(toNoiseFieldConfig(config)).sampleHeight;
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Mesh any `TerrainField` — including a `CarvableField` with craters/mounds written into it — into a
|
|
28
|
+
* vertex-coloured ground geometry. `sampleHeight` drives the vertices, so runtime carves show up as
|
|
29
|
+
* real depressions the moment the field is re-sampled (bump the caller's rebuild key after a carve).
|
|
30
|
+
*/
|
|
31
|
+
export function createFieldGroundGeometry(field, options = {}) {
|
|
32
|
+
const size = resolveTerrainSize(options.size);
|
|
33
|
+
const segments = resolveTerrainSegments(options.segments);
|
|
34
|
+
const [cx, cz] = options.center ?? [0, 0];
|
|
35
|
+
return buildGroundGeometry(size, segments, (x, z) => field.sampleHeight(x, z), {
|
|
36
|
+
colors: options.colors ?? {},
|
|
37
|
+
center: [cx, cz],
|
|
38
|
+
heightRange: options.heightRange,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function buildGroundGeometry(size, segments, sampler, opts) {
|
|
42
|
+
const colors = opts.colors;
|
|
43
|
+
const vertexCountX = segments.x + 1;
|
|
44
|
+
const vertexCountZ = segments.z + 1;
|
|
45
|
+
const positions = new Float32Array(vertexCountX * vertexCountZ * 3);
|
|
46
|
+
const uvs = new Float32Array(vertexCountX * vertexCountZ * 2);
|
|
47
|
+
const colorValues = new Float32Array(vertexCountX * vertexCountZ * 3);
|
|
48
|
+
const indices = new Uint32Array(segments.x * segments.z * 6);
|
|
49
|
+
const low = new THREE.Color(colors.low ?? "#30402c");
|
|
50
|
+
const high = new THREE.Color(colors.high ?? "#7f8b50");
|
|
51
|
+
const waterline = colors.waterline === undefined ? null : new THREE.Color(colors.waterline);
|
|
52
|
+
const minHeight = opts.heightRange?.[0] ?? -3;
|
|
53
|
+
const maxHeight = opts.heightRange?.[1] ?? 3;
|
|
54
|
+
let index = 0;
|
|
55
|
+
let uvIndex = 0;
|
|
56
|
+
let colorIndex = 0;
|
|
57
|
+
for (let zIndex = 0; zIndex < vertexCountZ; zIndex += 1) {
|
|
58
|
+
const v = zIndex / segments.z;
|
|
59
|
+
const z = opts.center[1] + (v - 0.5) * size.depth;
|
|
60
|
+
for (let xIndex = 0; xIndex < vertexCountX; xIndex += 1) {
|
|
61
|
+
const u = xIndex / segments.x;
|
|
62
|
+
const x = opts.center[0] + (u - 0.5) * size.width;
|
|
63
|
+
const y = sampler(x, z);
|
|
64
|
+
positions[index] = x;
|
|
65
|
+
positions[index + 1] = y;
|
|
66
|
+
positions[index + 2] = z;
|
|
67
|
+
uvs[uvIndex] = u;
|
|
68
|
+
uvs[uvIndex + 1] = v;
|
|
69
|
+
const blend = THREE.MathUtils.clamp((y - minHeight) / (maxHeight - minHeight), 0, 1);
|
|
70
|
+
const color = low.clone().lerp(high, blend);
|
|
71
|
+
if (waterline !== null && y <= (colors.waterlineHeight ?? 0))
|
|
72
|
+
color.lerp(waterline, 0.65);
|
|
73
|
+
colorValues[colorIndex] = color.r;
|
|
74
|
+
colorValues[colorIndex + 1] = color.g;
|
|
75
|
+
colorValues[colorIndex + 2] = color.b;
|
|
76
|
+
index += 3;
|
|
77
|
+
uvIndex += 2;
|
|
78
|
+
colorIndex += 3;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
let triangleIndex = 0;
|
|
82
|
+
for (let zIndex = 0; zIndex < segments.z; zIndex += 1) {
|
|
83
|
+
for (let xIndex = 0; xIndex < segments.x; xIndex += 1) {
|
|
84
|
+
const a = zIndex * vertexCountX + xIndex;
|
|
85
|
+
const b = a + 1;
|
|
86
|
+
const c = a + vertexCountX;
|
|
87
|
+
const d = c + 1;
|
|
88
|
+
indices[triangleIndex] = a;
|
|
89
|
+
indices[triangleIndex + 1] = c;
|
|
90
|
+
indices[triangleIndex + 2] = b;
|
|
91
|
+
indices[triangleIndex + 3] = b;
|
|
92
|
+
indices[triangleIndex + 4] = c;
|
|
93
|
+
indices[triangleIndex + 5] = d;
|
|
94
|
+
triangleIndex += 6;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const geometry = new THREE.BufferGeometry();
|
|
98
|
+
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
|
|
99
|
+
geometry.setAttribute("uv", new THREE.BufferAttribute(uvs, 2));
|
|
100
|
+
geometry.setAttribute("color", new THREE.BufferAttribute(colorValues, 3));
|
|
101
|
+
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
|
|
102
|
+
geometry.computeVertexNormals();
|
|
103
|
+
geometry.computeBoundingSphere();
|
|
104
|
+
return geometry;
|
|
105
|
+
}
|
|
26
106
|
export function createProceduralGroundGeometry(config = {}, colors = {}) {
|
|
27
107
|
const size = resolveTerrainSize(config.size);
|
|
28
108
|
const segments = resolveTerrainSegments(config.segments);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type FramingConfig, type FrustumSample } from "@jgengine/core/sensor/frustumSensor";
|
|
2
|
+
export interface FrustumSensorProbeOptions extends FramingConfig {
|
|
3
|
+
subjectIds: readonly string[];
|
|
4
|
+
subjectRadius?: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* View-frustum sensor (#117) driven by the live render camera: which held-camera
|
|
8
|
+
* subjects are in frame, how well framed, and their dwell time on-screen
|
|
9
|
+
* (Content Warning-style "is this monster filmed well" scoring). Returns the
|
|
10
|
+
* best-framed in-view subject each frame for a photo-mode HUD readout.
|
|
11
|
+
*/
|
|
12
|
+
export declare function useFrustumSensor(options: FrustumSensorProbeOptions): FrustumSample | null;
|
|
13
|
+
/**
|
|
14
|
+
* Renders inside the Canvas (needs the live camera via `useFrame`/`useThree`)
|
|
15
|
+
* but portals a real HTML readout via drei's `Html fullscreen` — a photo-mode
|
|
16
|
+
* "is this subject framed" HUD.
|
|
17
|
+
*/
|
|
18
|
+
export declare function FrustumSensorReadout(props: FrustumSensorProbeOptions & {
|
|
19
|
+
wrapperClassName?: string;
|
|
20
|
+
className?: string;
|
|
21
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Html } from "@react-three/drei";
|
|
3
|
+
import { useFrame, useThree } from "@react-three/fiber";
|
|
4
|
+
import { useMemo, useRef, useState } from "react";
|
|
5
|
+
import { PerspectiveCamera, Vector3 } from "three";
|
|
6
|
+
import { createFrustumSensor, } from "@jgengine/core/sensor/frustumSensor";
|
|
7
|
+
import { useSceneEntities } from "@jgengine/react/hooks";
|
|
8
|
+
function isPerspective(camera) {
|
|
9
|
+
return camera.isPerspectiveCamera === true;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* View-frustum sensor (#117) driven by the live render camera: which held-camera
|
|
13
|
+
* subjects are in frame, how well framed, and their dwell time on-screen
|
|
14
|
+
* (Content Warning-style "is this monster filmed well" scoring). Returns the
|
|
15
|
+
* best-framed in-view subject each frame for a photo-mode HUD readout.
|
|
16
|
+
*/
|
|
17
|
+
export function useFrustumSensor(options) {
|
|
18
|
+
const camera = useThree((state) => state.camera);
|
|
19
|
+
const size = useThree((state) => state.size);
|
|
20
|
+
const entities = useSceneEntities();
|
|
21
|
+
const sensor = useMemo(() => createFrustumSensor(options), []);
|
|
22
|
+
const [best, setBest] = useState(null);
|
|
23
|
+
const forward = useRef(new Vector3());
|
|
24
|
+
useFrame((_, dt) => {
|
|
25
|
+
const targets = [];
|
|
26
|
+
for (const id of options.subjectIds) {
|
|
27
|
+
const entity = entities.find((candidate) => candidate.id === id);
|
|
28
|
+
if (entity === undefined)
|
|
29
|
+
continue;
|
|
30
|
+
targets.push({ id, position: entity.position, radius: options.subjectRadius });
|
|
31
|
+
}
|
|
32
|
+
if (targets.length === 0) {
|
|
33
|
+
setBest(null);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
camera.getWorldDirection(forward.current);
|
|
37
|
+
const samples = sensor.tick({
|
|
38
|
+
position: [camera.position.x, camera.position.y, camera.position.z],
|
|
39
|
+
lookAt: [
|
|
40
|
+
camera.position.x + forward.current.x,
|
|
41
|
+
camera.position.y + forward.current.y,
|
|
42
|
+
camera.position.z + forward.current.z,
|
|
43
|
+
],
|
|
44
|
+
fovDeg: isPerspective(camera) ? camera.fov : 55,
|
|
45
|
+
aspect: size.height === 0 ? 16 / 9 : size.width / size.height,
|
|
46
|
+
}, targets, dt);
|
|
47
|
+
let leader = null;
|
|
48
|
+
for (const sample of samples) {
|
|
49
|
+
if (!sample.inView)
|
|
50
|
+
continue;
|
|
51
|
+
if (leader === null || sample.framing > leader.framing)
|
|
52
|
+
leader = sample;
|
|
53
|
+
}
|
|
54
|
+
setBest(leader);
|
|
55
|
+
});
|
|
56
|
+
return best;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Renders inside the Canvas (needs the live camera via `useFrame`/`useThree`)
|
|
60
|
+
* but portals a real HTML readout via drei's `Html fullscreen` — a photo-mode
|
|
61
|
+
* "is this subject framed" HUD.
|
|
62
|
+
*/
|
|
63
|
+
export function FrustumSensorReadout(props) {
|
|
64
|
+
const sample = useFrustumSensor(props);
|
|
65
|
+
return (_jsx(Html, { fullscreen: true, children: _jsx("div", { className: props.wrapperClassName ?? "pointer-events-none absolute bottom-24 right-4", children: sample === null ? (_jsx("div", { className: props.className ?? "rounded border border-white/15 bg-black/60 px-3 py-1.5 text-xs text-white/50", children: "No subject framed" })) : (_jsxs("div", { className: props.className ??
|
|
66
|
+
"rounded border border-amber-300/40 bg-black/70 px-3 py-1.5 text-xs text-amber-200 shadow-lg", children: [_jsx("span", { className: "font-semibold uppercase tracking-wide", children: sample.id }), _jsxs("span", { className: "ml-2 tabular-nums", children: ["framing ", Math.round(sample.framing * 100), "%"] }), _jsxs("span", { className: "ml-2 tabular-nums", children: ["dwell ", sample.dwellSeconds.toFixed(1), "s"] })] })) }) }));
|
|
67
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { probeHiddenState, type HiddenStateSource, type SensorProbeOptions } from "@jgengine/core/sensor/hiddenStateProbe";
|
|
2
|
+
import type { EntityPosition } from "@jgengine/core/scene/entityStore";
|
|
3
|
+
/** Reads a hidden zone/entity state variable in range (EMF / thermometer / geiger style sensor verb, #116). */
|
|
4
|
+
export declare function useHiddenStateProbe(origin: EntityPosition, sources: readonly HiddenStateSource[], options: SensorProbeOptions): import("@jgengine/core/sensor/hiddenStateProbe").SensorReading | null;
|
|
5
|
+
export interface SensorReadoutMeterProps {
|
|
6
|
+
label: string;
|
|
7
|
+
reading: ReturnType<typeof probeHiddenState>;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
/** A handheld-sensor readout: needle strength bar + the raw reading, or a "no signal" idle state. */
|
|
11
|
+
export declare function SensorReadoutMeter({ label, reading, className }: SensorReadoutMeterProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { probeHiddenState } from "@jgengine/core/sensor/hiddenStateProbe";
|
|
3
|
+
/** Reads a hidden zone/entity state variable in range (EMF / thermometer / geiger style sensor verb, #116). */
|
|
4
|
+
export function useHiddenStateProbe(origin, sources, options) {
|
|
5
|
+
return probeHiddenState(origin, sources, options);
|
|
6
|
+
}
|
|
7
|
+
/** A handheld-sensor readout: needle strength bar + the raw reading, or a "no signal" idle state. */
|
|
8
|
+
export function SensorReadoutMeter({ label, reading, className }) {
|
|
9
|
+
const percent = reading === null ? 0 : Math.round(reading.strength * 100);
|
|
10
|
+
return (_jsxs("div", { className: className ?? "w-44 rounded border border-emerald-400/30 bg-black/70 p-2 text-xs text-emerald-200 shadow-lg", children: [_jsxs("div", { className: "mb-1 flex items-center justify-between uppercase tracking-wide", children: [_jsx("span", { children: label }), _jsx("span", { className: "tabular-nums", children: reading === null ? "—" : `${percent}%` })] }), _jsx("div", { className: "h-1.5 overflow-hidden rounded bg-white/10", children: _jsx("div", { className: "h-full bg-emerald-400 transition-[width]", style: { width: `${percent}%` } }) }), _jsx("div", { className: "mt-1 text-[10px] text-emerald-300/70", children: reading === null ? "no signal" : String(reading.value) })] }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type RevealHit } from "@jgengine/core/sensor/revealQuery";
|
|
2
|
+
import type { SceneEntity } from "@jgengine/core/scene/entityStore";
|
|
3
|
+
export interface RevealVisionOptions {
|
|
4
|
+
originEntityId?: string;
|
|
5
|
+
radius: number;
|
|
6
|
+
tags: readonly string[];
|
|
7
|
+
resolveTags: (entity: SceneEntity) => readonly string[];
|
|
8
|
+
}
|
|
9
|
+
/** Occlusion-ignoring tagged-entity radius query (#115), bound to the live scene. */
|
|
10
|
+
export declare function useRevealHits(options: RevealVisionOptions): readonly RevealHit[];
|
|
11
|
+
export interface RevealHighlightsProps extends RevealVisionOptions {
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
color?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Screen-space reveal effect (#115) — highlights tagged entities through
|
|
17
|
+
* occluders (Dark Sight / detective-vision / wallhack style). Renders with
|
|
18
|
+
* `depthTest: false` so the highlight draws over any wall standing between
|
|
19
|
+
* the origin and the revealed entity, rather than the usual depth-sorted scene.
|
|
20
|
+
*/
|
|
21
|
+
export declare function RevealHighlights(props: RevealHighlightsProps): import("react").JSX.Element | null;
|
|
22
|
+
export interface RevealScreenTintProps {
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
color?: string;
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
/** Full-screen desaturating tint that reads as "vision mode is on" (Dark Sight / thermal / detective vision). */
|
|
28
|
+
export declare function RevealScreenTint({ enabled, color, className }: RevealScreenTintProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { DoubleSide } from "three";
|
|
4
|
+
import { createRevealQuery } from "@jgengine/core/sensor/revealQuery";
|
|
5
|
+
import { usePlayer, useSceneEntities } from "@jgengine/react/hooks";
|
|
6
|
+
/** Occlusion-ignoring tagged-entity radius query (#115), bound to the live scene. */
|
|
7
|
+
export function useRevealHits(options) {
|
|
8
|
+
const entities = useSceneEntities();
|
|
9
|
+
const player = usePlayer();
|
|
10
|
+
const originId = options.originEntityId ?? player.userId;
|
|
11
|
+
const tagsKey = options.tags.join("|");
|
|
12
|
+
return useMemo(() => {
|
|
13
|
+
const byId = new Map(entities.map((entity) => [entity.id, entity]));
|
|
14
|
+
const query = createRevealQuery({
|
|
15
|
+
resolvePosition: (id) => byId.get(id)?.position,
|
|
16
|
+
resolveTags: (id) => {
|
|
17
|
+
const entity = byId.get(id);
|
|
18
|
+
return entity === undefined ? [] : options.resolveTags(entity);
|
|
19
|
+
},
|
|
20
|
+
candidates: () => entities.map((entity) => entity.id),
|
|
21
|
+
});
|
|
22
|
+
return query.inRadius(originId, options.radius, options.tags);
|
|
23
|
+
}, [entities, originId, options.radius, tagsKey, options.tags, options.resolveTags]);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Screen-space reveal effect (#115) — highlights tagged entities through
|
|
27
|
+
* occluders (Dark Sight / detective-vision / wallhack style). Renders with
|
|
28
|
+
* `depthTest: false` so the highlight draws over any wall standing between
|
|
29
|
+
* the origin and the revealed entity, rather than the usual depth-sorted scene.
|
|
30
|
+
*/
|
|
31
|
+
export function RevealHighlights(props) {
|
|
32
|
+
const { enabled, color = "#7dd3fc" } = props;
|
|
33
|
+
const hits = useRevealHits(props);
|
|
34
|
+
const entities = useSceneEntities();
|
|
35
|
+
if (!enabled)
|
|
36
|
+
return null;
|
|
37
|
+
return (_jsx(_Fragment, { children: hits.map((hit) => {
|
|
38
|
+
const entity = entities.find((candidate) => candidate.id === hit.instanceId);
|
|
39
|
+
if (entity === undefined)
|
|
40
|
+
return null;
|
|
41
|
+
const pulse = Math.max(0.25, 1 - hit.distance / Math.max(props.radius, 1));
|
|
42
|
+
return (_jsxs("group", { position: [entity.position[0], entity.position[1], entity.position[2]], children: [_jsxs("mesh", { "position-y": 0.95, renderOrder: 999, children: [_jsx("capsuleGeometry", { args: [0.42, 1.2, 6, 14] }), _jsx("meshBasicMaterial", { color: color, transparent: true, opacity: 0.55 + pulse * 0.35, depthTest: false })] }), _jsxs("mesh", { "position-y": 2.6, renderOrder: 999, children: [_jsx("coneGeometry", { args: [0.14, 0.3, 12] }), _jsx("meshBasicMaterial", { color: color, transparent: true, opacity: 0.85, depthTest: false })] }), _jsxs("mesh", { "rotation-x": -Math.PI / 2, "position-y": 0.04, renderOrder: 999, children: [_jsx("ringGeometry", { args: [0.55, 0.85, 32] }), _jsx("meshBasicMaterial", { color: color, transparent: true, opacity: 0.9, depthTest: false, side: DoubleSide })] })] }, hit.instanceId));
|
|
43
|
+
}) }));
|
|
44
|
+
}
|
|
45
|
+
/** Full-screen desaturating tint that reads as "vision mode is on" (Dark Sight / thermal / detective vision). */
|
|
46
|
+
export function RevealScreenTint({ enabled, color = "rgba(56, 189, 248, 0.16)", className }) {
|
|
47
|
+
if (!enabled)
|
|
48
|
+
return null;
|
|
49
|
+
return (_jsx("div", { className: className ?? "pointer-events-none absolute inset-0 z-10 mix-blend-screen", style: {
|
|
50
|
+
background: `radial-gradient(circle at 50% 50%, transparent 25%, ${color} 100%)`,
|
|
51
|
+
filter: "saturate(0.55) contrast(1.15) brightness(0.92)",
|
|
52
|
+
} }));
|
|
53
|
+
}
|