@jgengine/shell 0.6.0 → 0.8.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.
Files changed (118) hide show
  1. package/CHANGELOG.md +214 -2
  2. package/README.md +26 -1
  3. package/dist/GameHost.d.ts +12 -0
  4. package/dist/GameHost.js +33 -0
  5. package/dist/GamePlayer.d.ts +11 -0
  6. package/dist/GamePlayer.js +24 -0
  7. package/dist/GamePlayerShell.d.ts +33 -1
  8. package/dist/GamePlayerShell.js +641 -59
  9. package/dist/audio/AudioComponents.d.ts +12 -0
  10. package/dist/audio/AudioComponents.js +53 -0
  11. package/dist/audio/audioEngine.d.ts +23 -0
  12. package/dist/audio/audioEngine.js +110 -0
  13. package/dist/behaviour.d.ts +31 -0
  14. package/dist/behaviour.js +63 -0
  15. package/dist/camera/GameCameraRig.d.ts +16 -0
  16. package/dist/camera/GameCameraRig.js +56 -0
  17. package/dist/camera/GameFirstPersonCamera.js +2 -0
  18. package/dist/camera/GameInspectionCamera.d.ts +6 -0
  19. package/dist/camera/GameInspectionCamera.js +24 -0
  20. package/dist/camera/GameOrbitCamera.d.ts +3 -1
  21. package/dist/camera/GameOrbitCamera.js +4 -2
  22. package/dist/camera/cameraRigs.d.ts +26 -0
  23. package/dist/camera/cameraRigs.js +522 -0
  24. package/dist/camera/index.d.ts +7 -0
  25. package/dist/camera/index.js +7 -0
  26. package/dist/camera/inspectionCameraMath.d.ts +25 -0
  27. package/dist/camera/inspectionCameraMath.js +44 -0
  28. package/dist/camera/rigMath.d.ts +169 -0
  29. package/dist/camera/rigMath.js +393 -0
  30. package/dist/camera/rigResolve.d.ts +15 -0
  31. package/dist/camera/rigResolve.js +52 -0
  32. package/dist/camera/shakeChannel.d.ts +29 -0
  33. package/dist/camera/shakeChannel.js +38 -0
  34. package/dist/defineGame.d.ts +16 -0
  35. package/dist/defineGame.js +51 -0
  36. package/dist/devtools/DevtoolsOverlay.d.ts +14 -0
  37. package/dist/devtools/DevtoolsOverlay.js +334 -0
  38. package/dist/environment/Daylight.d.ts +50 -0
  39. package/dist/environment/Daylight.js +100 -0
  40. package/dist/environment/EnvironmentScene.js +18 -26
  41. package/dist/environment/GroundPad.d.ts +7 -0
  42. package/dist/environment/GroundPad.js +12 -0
  43. package/dist/environment/daylightCycle.d.ts +26 -0
  44. package/dist/environment/daylightCycle.js +116 -0
  45. package/dist/environment/groundPadMath.d.ts +13 -0
  46. package/dist/environment/groundPadMath.js +10 -0
  47. package/dist/environment/index.d.ts +2 -0
  48. package/dist/environment/index.js +2 -0
  49. package/dist/map/MapMarkerBeacons.d.ts +12 -0
  50. package/dist/map/MapMarkerBeacons.js +16 -0
  51. package/dist/map/index.d.ts +2 -0
  52. package/dist/map/index.js +2 -0
  53. package/dist/map/terrainMap.d.ts +23 -0
  54. package/dist/map/terrainMap.js +79 -0
  55. package/dist/materialOverride.d.ts +8 -0
  56. package/dist/materialOverride.js +32 -0
  57. package/dist/multiplayer.d.ts +16 -9
  58. package/dist/multiplayer.js +62 -12
  59. package/dist/pointer/PointerOverlays.d.ts +12 -0
  60. package/dist/pointer/PointerOverlays.js +17 -0
  61. package/dist/pointer/PointerProbe.d.ts +4 -0
  62. package/dist/pointer/PointerProbe.js +40 -0
  63. package/dist/pointer/pointerService.d.ts +26 -0
  64. package/dist/pointer/pointerService.js +89 -0
  65. package/dist/registry.d.ts +4 -1
  66. package/dist/registry.js +3 -1
  67. package/dist/render/modelRender.d.ts +14 -0
  68. package/dist/render/modelRender.js +79 -0
  69. package/dist/replay/useSessionRecorder.d.ts +14 -0
  70. package/dist/replay/useSessionRecorder.js +22 -0
  71. package/dist/structures/PlacementGhost.d.ts +8 -0
  72. package/dist/structures/PlacementGhost.js +11 -0
  73. package/dist/structures/index.d.ts +2 -0
  74. package/dist/structures/index.js +2 -0
  75. package/dist/structures.d.ts +1 -0
  76. package/dist/structures.js +1 -0
  77. package/dist/terrain/CarvedTerrain.d.ts +22 -0
  78. package/dist/terrain/CarvedTerrain.js +19 -0
  79. package/dist/terrain/EditableGround.d.ts +11 -0
  80. package/dist/terrain/EditableGround.js +38 -0
  81. package/dist/terrain/TerraformBrushCursor.d.ts +8 -0
  82. package/dist/terrain/TerraformBrushCursor.js +13 -0
  83. package/dist/terrain/index.d.ts +4 -1
  84. package/dist/terrain/index.js +4 -1
  85. package/dist/terrain/terrainMath.d.ts +15 -1
  86. package/dist/terrain/terrainMath.js +87 -1
  87. package/dist/touch/TouchControlsOverlay.d.ts +18 -0
  88. package/dist/touch/TouchControlsOverlay.js +173 -0
  89. package/dist/vision/FrustumSensorHud.d.ts +21 -0
  90. package/dist/vision/FrustumSensorHud.js +67 -0
  91. package/dist/vision/HiddenStateProbeHud.d.ts +11 -0
  92. package/dist/vision/HiddenStateProbeHud.js +11 -0
  93. package/dist/vision/RevealVision.d.ts +28 -0
  94. package/dist/vision/RevealVision.js +53 -0
  95. package/dist/water/OceanShader.d.ts +1 -1
  96. package/dist/water/OceanShader.js +3 -3
  97. package/dist/weather/FireSpreadLayer.d.ts +14 -0
  98. package/dist/weather/FireSpreadLayer.js +68 -0
  99. package/dist/weather/index.d.ts +1 -0
  100. package/dist/weather/index.js +1 -0
  101. package/dist/world/DataObjects.d.ts +44 -0
  102. package/dist/world/DataObjects.js +75 -0
  103. package/dist/world/GridWorldScene.d.ts +10 -0
  104. package/dist/world/GridWorldScene.js +54 -0
  105. package/dist/world/InstancedJoints.d.ts +14 -0
  106. package/dist/world/InstancedJoints.js +32 -0
  107. package/dist/world/WorldHud.d.ts +7 -1
  108. package/dist/world/WorldHud.js +76 -14
  109. package/dist/world/WorldItems.d.ts +5 -0
  110. package/dist/world/WorldItems.js +50 -0
  111. package/dist/world/floatTextStyle.d.ts +14 -0
  112. package/dist/world/floatTextStyle.js +39 -0
  113. package/llms.txt +2382 -0
  114. package/package.json +10 -5
  115. package/dist/demo/demoGame.d.ts +0 -2
  116. package/dist/demo/demoGame.js +0 -208
  117. package/dist/demo/environmentShowcase.d.ts +0 -2
  118. package/dist/demo/environmentShowcase.js +0 -15
@@ -0,0 +1,18 @@
1
+ import { type MutableRefObject } from "react";
2
+ import { type TouchScheme } from "@jgengine/core/input/touchScheme";
3
+ export interface TouchCodeSink {
4
+ onCodeDown(code: string): void;
5
+ onCodeUp(code: string): void;
6
+ }
7
+ export declare function TouchPlaySurface({ scheme, sink, yawRef, pitchRef, maxPitch, onPrimaryTap, }: {
8
+ scheme: TouchScheme;
9
+ sink: TouchCodeSink;
10
+ yawRef: MutableRefObject<number>;
11
+ pitchRef: MutableRefObject<number>;
12
+ maxPitch: number;
13
+ onPrimaryTap: () => void;
14
+ }): import("react").JSX.Element;
15
+ export declare function TouchControlsDock({ scheme, sink }: {
16
+ scheme: TouchScheme;
17
+ sink: TouchCodeSink;
18
+ }): import("react").JSX.Element;
@@ -0,0 +1,173 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo, useRef } from "react";
3
+ import { createGestureSurfaceTracker } from "@jgengine/core/input/gestureSurface";
4
+ import { createTouchGestureTracker } from "@jgengine/core/input/touchGestures";
5
+ import { touchCode } from "@jgengine/core/input/touchScheme";
6
+ import { GameIcon, iconForAction, isGameIconName } from "@jgengine/react/gameIcons";
7
+ const JOYSTICK_SIZE = 132;
8
+ const JOYSTICK_THUMB = 52;
9
+ const JOYSTICK_DEADZONE = 0.28;
10
+ const JOYSTICK_AXIS_THRESHOLD = 0.42;
11
+ const LOOK_TAP_TUNING = { tapMoveThresholdPx: 12, tapMaxMs: 280, longPressMs: 450 };
12
+ function pressOnce(sink, action) {
13
+ const code = touchCode(action);
14
+ sink.onCodeDown(code);
15
+ sink.onCodeUp(code);
16
+ }
17
+ export function TouchPlaySurface({ scheme, sink, yawRef, pitchRef, maxPitch, onPrimaryTap, }) {
18
+ const pointerIdRef = useRef(null);
19
+ const gestureTracker = useMemo(() => (scheme.gestures === null ? null : createGestureSurfaceTracker(scheme.gestures)), [scheme]);
20
+ const lookTracker = useMemo(() => (scheme.look ? createTouchGestureTracker(LOOK_TAP_TUNING) : null), [scheme]);
21
+ const release = (event) => {
22
+ if (event.pointerId !== pointerIdRef.current)
23
+ return;
24
+ pointerIdRef.current = null;
25
+ };
26
+ return (_jsx("div", { className: "absolute inset-0 touch-none", onPointerDown: (event) => {
27
+ if (event.pointerType !== "touch" || pointerIdRef.current !== null)
28
+ return;
29
+ pointerIdRef.current = event.pointerId;
30
+ event.currentTarget.setPointerCapture(event.pointerId);
31
+ if (lookTracker !== null)
32
+ lookTracker.begin(event.clientX, event.clientY, performance.now());
33
+ else
34
+ gestureTracker?.begin(event.clientX, event.clientY, performance.now());
35
+ }, onPointerMove: (event) => {
36
+ if (event.pointerId !== pointerIdRef.current)
37
+ return;
38
+ event.stopPropagation();
39
+ if (lookTracker !== null) {
40
+ const delta = lookTracker.move(event.clientX, event.clientY);
41
+ if (delta !== null) {
42
+ yawRef.current -= delta.dx * scheme.lookSensitivity;
43
+ pitchRef.current = Math.max(-maxPitch, Math.min(maxPitch, pitchRef.current - delta.dy * scheme.lookSensitivity));
44
+ }
45
+ return;
46
+ }
47
+ for (const action of gestureTracker?.move(event.clientX, event.clientY) ?? [])
48
+ pressOnce(sink, action);
49
+ }, onPointerUp: (event) => {
50
+ if (event.pointerId !== pointerIdRef.current)
51
+ return;
52
+ event.stopPropagation();
53
+ release(event);
54
+ if (lookTracker !== null) {
55
+ if (lookTracker.end(performance.now()) === "tap") {
56
+ if (scheme.gestures?.tap !== undefined)
57
+ pressOnce(sink, scheme.gestures.tap);
58
+ else
59
+ onPrimaryTap();
60
+ }
61
+ return;
62
+ }
63
+ for (const action of gestureTracker?.end(event.clientX, event.clientY, performance.now()) ?? []) {
64
+ pressOnce(sink, action);
65
+ }
66
+ }, onPointerCancel: (event) => {
67
+ release(event);
68
+ lookTracker?.cancel();
69
+ gestureTracker?.cancel();
70
+ } }));
71
+ }
72
+ function joystickDirections(joystick, nx, ny) {
73
+ const active = new Set();
74
+ if (Math.hypot(nx, ny) < JOYSTICK_DEADZONE)
75
+ return active;
76
+ if (joystick.up !== null && ny < -JOYSTICK_AXIS_THRESHOLD)
77
+ active.add(joystick.up);
78
+ if (joystick.down !== null && ny > JOYSTICK_AXIS_THRESHOLD)
79
+ active.add(joystick.down);
80
+ if (joystick.left !== null && nx < -JOYSTICK_AXIS_THRESHOLD)
81
+ active.add(joystick.left);
82
+ if (joystick.right !== null && nx > JOYSTICK_AXIS_THRESHOLD)
83
+ active.add(joystick.right);
84
+ return active;
85
+ }
86
+ function VirtualJoystick({ joystick, sink }) {
87
+ const baseRef = useRef(null);
88
+ const thumbRef = useRef(null);
89
+ const pointerIdRef = useRef(null);
90
+ const activeRef = useRef(new Set());
91
+ const applyVector = (clientX, clientY) => {
92
+ const base = baseRef.current;
93
+ const thumb = thumbRef.current;
94
+ if (base === null || thumb === null)
95
+ return;
96
+ const rect = base.getBoundingClientRect();
97
+ const radius = rect.width / 2;
98
+ let nx = (clientX - rect.left - radius) / radius;
99
+ let ny = (clientY - rect.top - radius) / radius;
100
+ const length = Math.hypot(nx, ny);
101
+ if (length > 1) {
102
+ nx /= length;
103
+ ny /= length;
104
+ }
105
+ const travel = radius - JOYSTICK_THUMB / 2;
106
+ thumb.style.transform = `translate(${nx * travel}px, ${ny * travel}px)`;
107
+ const next = joystickDirections(joystick, nx, ny);
108
+ for (const action of activeRef.current) {
109
+ if (!next.has(action))
110
+ sink.onCodeUp(touchCode(action));
111
+ }
112
+ for (const action of next) {
113
+ if (!activeRef.current.has(action))
114
+ sink.onCodeDown(touchCode(action));
115
+ }
116
+ activeRef.current = next;
117
+ };
118
+ const releaseAll = () => {
119
+ pointerIdRef.current = null;
120
+ for (const action of activeRef.current)
121
+ sink.onCodeUp(touchCode(action));
122
+ activeRef.current = new Set();
123
+ if (thumbRef.current !== null)
124
+ thumbRef.current.style.transform = "translate(0px, 0px)";
125
+ };
126
+ return (_jsx("div", { ref: baseRef, className: "pointer-events-auto relative flex touch-none select-none items-center justify-center rounded-full border border-white/20 bg-white/10 backdrop-blur-sm", style: { width: JOYSTICK_SIZE, height: JOYSTICK_SIZE }, onPointerDown: (event) => {
127
+ if (pointerIdRef.current !== null)
128
+ return;
129
+ pointerIdRef.current = event.pointerId;
130
+ event.currentTarget.setPointerCapture(event.pointerId);
131
+ applyVector(event.clientX, event.clientY);
132
+ }, onPointerMove: (event) => {
133
+ if (event.pointerId !== pointerIdRef.current)
134
+ return;
135
+ event.stopPropagation();
136
+ applyVector(event.clientX, event.clientY);
137
+ }, onPointerUp: (event) => {
138
+ if (event.pointerId !== pointerIdRef.current)
139
+ return;
140
+ event.stopPropagation();
141
+ releaseAll();
142
+ }, onPointerCancel: () => releaseAll(), children: _jsx("div", { ref: thumbRef, className: "rounded-full border border-white/30 bg-white/25", style: { width: JOYSTICK_THUMB, height: JOYSTICK_THUMB } }) }));
143
+ }
144
+ function touchButtonIcon(button) {
145
+ if (button.icon === false)
146
+ return null;
147
+ if (button.icon !== null && isGameIconName(button.icon))
148
+ return button.icon;
149
+ return iconForAction(button.action);
150
+ }
151
+ function TouchActionButton({ button, sink }) {
152
+ const pointerIdRef = useRef(null);
153
+ const icon = touchButtonIcon(button);
154
+ const releaseIfHeld = () => {
155
+ if (pointerIdRef.current === null)
156
+ return;
157
+ pointerIdRef.current = null;
158
+ sink.onCodeUp(touchCode(button.action));
159
+ };
160
+ return (_jsx("button", { type: "button", "aria-label": button.label, className: "pointer-events-auto flex h-14 w-14 touch-none select-none items-center justify-center rounded-full border border-white/25 bg-white/10 text-center text-[10px] font-semibold uppercase leading-tight tracking-wide text-white/90 backdrop-blur-sm active:border-white/50 active:bg-white/30", onPointerDown: (event) => {
161
+ if (pointerIdRef.current !== null)
162
+ return;
163
+ pointerIdRef.current = event.pointerId;
164
+ event.currentTarget.setPointerCapture(event.pointerId);
165
+ sink.onCodeDown(touchCode(button.action));
166
+ }, onPointerUp: (event) => {
167
+ event.stopPropagation();
168
+ releaseIfHeld();
169
+ }, onPointerCancel: () => releaseIfHeld(), onContextMenu: (event) => event.preventDefault(), children: icon !== null ? _jsx(GameIcon, { name: icon, size: 26 }) : _jsx("span", { className: "px-1", children: button.label }) }));
170
+ }
171
+ export function TouchControlsDock({ scheme, sink }) {
172
+ return (_jsxs("div", { className: "pointer-events-none absolute inset-x-0 bottom-0 z-40 flex items-end justify-between gap-4 px-5", style: { paddingBottom: "calc(env(safe-area-inset-bottom, 0px) + 20px)" }, children: [_jsx("div", { children: scheme.joystick !== null ? _jsx(VirtualJoystick, { joystick: scheme.joystick, sink: sink }) : null }), _jsx("div", { className: "flex max-w-[60%] flex-wrap items-end justify-end gap-3", children: scheme.buttons.map((button) => (_jsx(TouchActionButton, { button: button, sink: sink }, button.action))) })] }));
173
+ }
@@ -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
+ }
@@ -1,2 +1,2 @@
1
- export declare const oceanVertexShader = "\nuniform float uTime;\nuniform vec2 uWaveDirections[6];\nuniform vec4 uWaveParams[6];\nuniform float uChoppiness;\nuniform float uFoamThreshold;\nuniform float uFoamSoftness;\nuniform float uFoamCoverage;\n\nvarying vec3 vWorldPosition;\nvarying vec3 vNormal;\nvarying float vCrest;\nvarying float vWaveHeight;\n\nvoid main() {\n vec3 displaced = position;\n vec3 tangent = vec3(1.0, 0.0, 0.0);\n vec3 bitangent = vec3(0.0, 0.0, 1.0);\n float crest = 0.0;\n\n for (int i = 0; i < 6; i++) {\n vec2 direction = normalize(uWaveDirections[i]);\n vec4 wave = uWaveParams[i];\n float k = wave.x;\n float amplitude = wave.y;\n float steepness = wave.z;\n float omega = wave.w;\n float phase = k * dot(direction, position.xz) - omega * uTime;\n float sine = sin(phase);\n float cosine = cos(phase);\n float horizontal = steepness * amplitude * uChoppiness;\n displaced.x += horizontal * direction.x * cosine;\n displaced.z += horizontal * direction.y * cosine;\n displaced.y += amplitude * sine;\n float common = horizontal * k * sine;\n tangent += vec3(-common * direction.x * direction.x, amplitude * k * direction.x * cosine, -common * direction.x * direction.y);\n bitangent += vec3(-common * direction.x * direction.y, amplitude * k * direction.y * cosine, -common * direction.y * direction.y);\n crest = max(crest, smoothstep(uFoamThreshold, uFoamThreshold + uFoamSoftness, sine * steepness + uFoamCoverage * 0.35));\n }\n\n vNormal = normalize(cross(bitangent, tangent));\n vCrest = crest;\n vWaveHeight = displaced.y;\n vec4 worldPosition = modelMatrix * vec4(displaced, 1.0);\n vWorldPosition = worldPosition.xyz;\n gl_Position = projectionMatrix * viewMatrix * worldPosition;\n}\n";
1
+ export declare const oceanVertexShader = "\nuniform float uTime;\nuniform vec2 uWaveDirections[6];\nuniform vec4 uWaveParams[6];\nuniform float uChoppiness;\nuniform float uFoamThreshold;\nuniform float uFoamSoftness;\nuniform float uFoamCoverage;\n\nvarying vec3 vWorldPosition;\nvarying vec3 vNormal;\nvarying float vCrest;\nvarying float vWaveHeight;\n\nvoid main() {\n vec3 displaced = position;\n vec3 tangent = vec3(1.0, 0.0, 0.0);\n vec3 bitangent = vec3(0.0, 0.0, 1.0);\n float crest = 0.0;\n\n for (int i = 0; i < 6; i++) {\n vec2 direction = normalize(uWaveDirections[i]);\n vec4 wave = uWaveParams[i];\n float k = wave.x;\n float amplitude = wave.y;\n float steepness = wave.z;\n float omega = wave.w;\n float phase = k * dot(direction, position.xz) - omega * uTime;\n float sine = sin(phase);\n float cosine = cos(phase);\n float horizontal = steepness * amplitude * uChoppiness;\n displaced.x += horizontal * direction.x * cosine;\n displaced.z += horizontal * direction.y * cosine;\n displaced.y += amplitude * sine;\n float slopeTerm = horizontal * k * sine;\n tangent += vec3(-slopeTerm * direction.x * direction.x, amplitude * k * direction.x * cosine, -slopeTerm * direction.x * direction.y);\n bitangent += vec3(-slopeTerm * direction.x * direction.y, amplitude * k * direction.y * cosine, -slopeTerm * direction.y * direction.y);\n crest = max(crest, smoothstep(uFoamThreshold, uFoamThreshold + uFoamSoftness, sine * steepness + uFoamCoverage * 0.35));\n }\n\n vNormal = normalize(cross(bitangent, tangent));\n vCrest = crest;\n vWaveHeight = displaced.y;\n vec4 worldPosition = modelMatrix * vec4(displaced, 1.0);\n vWorldPosition = worldPosition.xyz;\n gl_Position = projectionMatrix * viewMatrix * worldPosition;\n}\n";
2
2
  export declare const oceanFragmentShader = "\nuniform vec3 uShallowColor;\nuniform vec3 uDeepColor;\nuniform vec3 uCrestColor;\nuniform vec3 uFoamColor;\nuniform float uOpacity;\nuniform float uFresnelStrength;\nuniform float uHorizonBlend;\nuniform float uFoamIntensity;\n\nvarying vec3 vWorldPosition;\nvarying vec3 vNormal;\nvarying float vCrest;\nvarying float vWaveHeight;\n\nvoid main() {\n vec3 normal = normalize(vNormal);\n vec3 viewDirection = normalize(cameraPosition - vWorldPosition);\n float fresnel = pow(1.0 - max(dot(normal, viewDirection), 0.0), 4.5);\n float depthMix = smoothstep(-1.8, 1.6, vWaveHeight);\n vec3 waterColor = mix(uDeepColor, uShallowColor, depthMix);\n waterColor = mix(waterColor, uCrestColor, smoothstep(0.15, 1.2, vWaveHeight) * 0.28);\n waterColor = mix(waterColor, vec3(0.86, 0.95, 1.0), fresnel * uFresnelStrength);\n float horizon = smoothstep(0.0, 1.0, 1.0 - abs(normal.y));\n waterColor = mix(waterColor, uShallowColor, horizon * uHorizonBlend);\n float foam = clamp(vCrest * uFoamIntensity, 0.0, 1.0);\n vec3 color = mix(waterColor, uFoamColor, foam);\n gl_FragColor = vec4(color, uOpacity);\n}\n";
@@ -32,9 +32,9 @@ void main() {
32
32
  displaced.x += horizontal * direction.x * cosine;
33
33
  displaced.z += horizontal * direction.y * cosine;
34
34
  displaced.y += amplitude * sine;
35
- float common = horizontal * k * sine;
36
- tangent += vec3(-common * direction.x * direction.x, amplitude * k * direction.x * cosine, -common * direction.x * direction.y);
37
- bitangent += vec3(-common * direction.x * direction.y, amplitude * k * direction.y * cosine, -common * direction.y * direction.y);
35
+ float slopeTerm = horizontal * k * sine;
36
+ tangent += vec3(-slopeTerm * direction.x * direction.x, amplitude * k * direction.x * cosine, -slopeTerm * direction.x * direction.y);
37
+ bitangent += vec3(-slopeTerm * direction.x * direction.y, amplitude * k * direction.y * cosine, -slopeTerm * direction.y * direction.y);
38
38
  crest = max(crest, smoothstep(uFoamThreshold, uFoamThreshold + uFoamSoftness, sine * steepness + uFoamCoverage * 0.35));
39
39
  }
40
40
 
@@ -0,0 +1,14 @@
1
+ import * as THREE from "three";
2
+ import type { FireGrid } from "@jgengine/core/world/weather";
3
+ export interface FireSpreadLayerProps {
4
+ grid: FireGrid;
5
+ cellSize: number;
6
+ origin?: readonly [number, number];
7
+ /** Sample ground height so flames sit on terrain; defaults to y=0. */
8
+ heightAt?: (x: number, z: number) => number;
9
+ /** Flame quad height. Default 1.6. */
10
+ flameHeight?: number;
11
+ burningColor?: THREE.ColorRepresentation;
12
+ emberColor?: THREE.ColorRepresentation;
13
+ }
14
+ export declare function FireSpreadLayer({ grid, cellSize, origin, heightAt, flameHeight, burningColor, emberColor, }: FireSpreadLayerProps): import("react").JSX.Element;
@@ -0,0 +1,68 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef } from "react";
3
+ import { useFrame } from "@react-three/fiber";
4
+ import * as THREE from "three";
5
+ const HIDDEN = new THREE.Matrix4().makeScale(0, 0, 0);
6
+ export function FireSpreadLayer({ grid, cellSize, origin = [0, 0], heightAt, flameHeight = 1.6, burningColor = "#ff6a1a", emberColor = "#4a1206", }) {
7
+ const flames = useRef(null);
8
+ const scorch = useRef(null);
9
+ const capacity = grid.cols * grid.rows;
10
+ const flameGeometry = useMemo(() => new THREE.PlaneGeometry(cellSize * 0.9, flameHeight), [cellSize, flameHeight]);
11
+ const scorchGeometry = useMemo(() => {
12
+ const geometry = new THREE.PlaneGeometry(cellSize, cellSize);
13
+ geometry.rotateX(-Math.PI / 2);
14
+ return geometry;
15
+ }, [cellSize]);
16
+ const flameMaterial = useMemo(() => new THREE.MeshBasicMaterial({ color: burningColor, transparent: true, opacity: 0.85, depthWrite: false, side: THREE.DoubleSide }), [burningColor]);
17
+ const scorchMaterial = useMemo(() => new THREE.MeshBasicMaterial({ color: emberColor, transparent: true, opacity: 0.7, depthWrite: false }), [emberColor]);
18
+ useEffect(() => () => {
19
+ flameGeometry.dispose();
20
+ scorchGeometry.dispose();
21
+ flameMaterial.dispose();
22
+ scorchMaterial.dispose();
23
+ }, [flameGeometry, scorchGeometry, flameMaterial, scorchMaterial]);
24
+ const matrix = useMemo(() => new THREE.Matrix4(), []);
25
+ const position = useMemo(() => new THREE.Vector3(), []);
26
+ const quaternion = useMemo(() => new THREE.Quaternion(), []);
27
+ const scale = useMemo(() => new THREE.Vector3(1, 1, 1), []);
28
+ useFrame((state) => {
29
+ const flameMesh = flames.current;
30
+ const scorchMesh = scorch.current;
31
+ if (flameMesh === null || scorchMesh === null)
32
+ return;
33
+ const cells = grid.snapshot();
34
+ const flicker = 0.85 + 0.15 * Math.sin(state.clock.elapsedTime * 12);
35
+ let flameIndex = 0;
36
+ let scorchIndex = 0;
37
+ for (let row = 0; row < grid.rows; row += 1) {
38
+ for (let col = 0; col < grid.cols; col += 1) {
39
+ const cell = cells[row * grid.cols + col];
40
+ const x = origin[0] + col * cellSize;
41
+ const z = origin[1] + row * cellSize;
42
+ const groundY = heightAt?.(x, z) ?? 0;
43
+ if (cell.state === "burning") {
44
+ position.set(x, groundY + flameHeight * 0.5, z);
45
+ quaternion.setFromEuler(new THREE.Euler(0, state.camera.rotation.y, 0));
46
+ scale.set(1, flicker, 1);
47
+ matrix.compose(position, quaternion, scale);
48
+ flameMesh.setMatrixAt(flameIndex, matrix);
49
+ flameIndex += 1;
50
+ }
51
+ else if (cell.state === "burnt") {
52
+ position.set(x, groundY + 0.05, z);
53
+ scorchMesh.setMatrixAt(scorchIndex, matrix.compose(position, new THREE.Quaternion(), scale.set(1, 1, 1)));
54
+ scorchIndex += 1;
55
+ }
56
+ }
57
+ }
58
+ for (let i = flameIndex; i < capacity; i += 1)
59
+ flameMesh.setMatrixAt(i, HIDDEN);
60
+ for (let i = scorchIndex; i < capacity; i += 1)
61
+ scorchMesh.setMatrixAt(i, HIDDEN);
62
+ flameMesh.count = capacity;
63
+ scorchMesh.count = capacity;
64
+ flameMesh.instanceMatrix.needsUpdate = true;
65
+ scorchMesh.instanceMatrix.needsUpdate = true;
66
+ });
67
+ return (_jsxs("group", { children: [_jsx("instancedMesh", { ref: scorch, args: [scorchGeometry, scorchMaterial, capacity], frustumCulled: false, renderOrder: 5 }), _jsx("instancedMesh", { ref: flames, args: [flameGeometry, flameMaterial, capacity], frustumCulled: false, renderOrder: 11 })] }));
68
+ }
@@ -1,3 +1,4 @@
1
+ export { FireSpreadLayer, type FireSpreadLayerProps } from "./FireSpreadLayer.js";
1
2
  export { LightningStrike, type LightningStrikeProps } from "./LightningStrike.js";
2
3
  export { RainField, type RainFieldProps } from "./RainField.js";
3
4
  export { SnowField, type SnowFieldProps } from "./SnowField.js";
@@ -1,3 +1,4 @@
1
+ export { FireSpreadLayer } from "./FireSpreadLayer.js";
1
2
  export { LightningStrike } from "./LightningStrike.js";
2
3
  export { RainField } from "./RainField.js";
3
4
  export { SnowField } from "./SnowField.js";
@@ -0,0 +1,44 @@
1
+ import { type ReactNode } from "react";
2
+ import * as THREE from "three";
3
+ /**
4
+ * Renders one placed 3D object per data item — a 3D bar chart, a heatmap, a city
5
+ * of buildings, a crowd. By default each item is an extruded box (all boxes share
6
+ * one `InstancedMesh`, so hundreds cost one draw call), sized and colored from the
7
+ * item; `renderItem` swaps the box for arbitrary content (a sprite, a GLB, a full
8
+ * entity). Genre-agnostic: the caller owns the data `T`, this owns the placement.
9
+ */
10
+ export interface DataObjectsProps<T> {
11
+ data: readonly T[];
12
+ /** World `[x, z]` for an item (placed on the ground plane). */
13
+ position: (item: T, index: number) => readonly [number, number];
14
+ /** Box height (Y-scale) for an item. */
15
+ height: (item: T, index: number) => number;
16
+ /** Box color for an item (any CSS/hex/THREE color). */
17
+ color: (item: T, index: number) => THREE.ColorRepresentation;
18
+ /** Footprint on X and Z. Default 0.28. */
19
+ cellSize?: number;
20
+ /** Index of the highlighted item, painted with `hoverColor`. */
21
+ hovered?: number | null;
22
+ hoverColor?: THREE.ColorRepresentation;
23
+ /** Called with the item index under the pointer, or null on exit. */
24
+ onHover?: (index: number | null) => void;
25
+ /** Staggered grow-in; restarts whenever `data` identity changes. */
26
+ grow?: {
27
+ duration?: number;
28
+ delay?: (item: T, index: number) => number;
29
+ };
30
+ castShadow?: boolean;
31
+ receiveShadow?: boolean;
32
+ /**
33
+ * Render arbitrary content per item instead of an instanced box — a sprite, a
34
+ * GLB, a full entity. The layout positions each on the ground at its `[x, z]`
35
+ * and hands you the resolved `height` to size against (e.g. one walking figure
36
+ * per unit of value). When set, the instanced-box path is bypassed.
37
+ */
38
+ renderItem?: (item: T, index: number, info: {
39
+ x: number;
40
+ z: number;
41
+ height: number;
42
+ }) => ReactNode;
43
+ }
44
+ export declare function DataObjects<T>({ data, position, height, color, cellSize, hovered, hoverColor, onHover, grow, castShadow, receiveShadow, renderItem, }: DataObjectsProps<T>): import("react").JSX.Element;
@@ -0,0 +1,75 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useFrame } from "@react-three/fiber";
3
+ import { useEffect, useMemo, useRef } from "react";
4
+ import * as THREE from "three";
5
+ function clamp01(v) {
6
+ return v < 0 ? 0 : v > 1 ? 1 : v;
7
+ }
8
+ export function DataObjects({ data, position, height, color, cellSize = 0.28, hovered = null, hoverColor = "#ffffff", onHover, grow, castShadow = true, receiveShadow = true, renderItem, }) {
9
+ const meshRef = useRef(null);
10
+ const dummy = useMemo(() => new THREE.Object3D(), []);
11
+ const geometry = useMemo(() => new THREE.BoxGeometry(1, 1, 1), []);
12
+ const material = useMemo(() => new THREE.MeshStandardMaterial({ roughness: 0.62, metalness: 0.04 }), []);
13
+ const hover = useMemo(() => new THREE.Color(hoverColor), [hoverColor]);
14
+ const startRef = useRef(null);
15
+ const capacity = data.length;
16
+ const layout = useMemo(() => {
17
+ const positions = new Float32Array(capacity * 2);
18
+ const targets = new Float32Array(capacity);
19
+ const delays = new Float32Array(capacity);
20
+ const colors = [];
21
+ data.forEach((item, i) => {
22
+ const [x, z] = position(item, i);
23
+ positions[i * 2] = x;
24
+ positions[i * 2 + 1] = z;
25
+ targets[i] = height(item, i);
26
+ delays[i] = grow?.delay?.(item, i) ?? 0;
27
+ colors.push(new THREE.Color(color(item, i)));
28
+ });
29
+ return { positions, targets, delays, colors };
30
+ // eslint-disable-next-line react-hooks/exhaustive-deps
31
+ }, [data]);
32
+ useEffect(() => {
33
+ startRef.current = null;
34
+ }, [data]);
35
+ useEffect(() => () => {
36
+ geometry.dispose();
37
+ material.dispose();
38
+ }, [geometry, material]);
39
+ const duration = grow?.duration ?? 0;
40
+ useFrame((state) => {
41
+ const mesh = meshRef.current;
42
+ if (mesh === null)
43
+ return;
44
+ if (startRef.current === null)
45
+ startRef.current = state.clock.elapsedTime;
46
+ const elapsed = state.clock.elapsedTime - startRef.current;
47
+ const { positions, targets, delays, colors } = layout;
48
+ for (let i = 0; i < capacity; i += 1) {
49
+ const grown = duration <= 0 ? 1 : clamp01((elapsed - delays[i]) / duration);
50
+ const eased = 1 - Math.pow(1 - grown, 3);
51
+ const h = Math.max(targets[i] * eased, 0.001);
52
+ dummy.position.set(positions[i * 2], h / 2, positions[i * 2 + 1]);
53
+ dummy.scale.set(cellSize, h, cellSize);
54
+ dummy.updateMatrix();
55
+ mesh.setMatrixAt(i, dummy.matrix);
56
+ mesh.setColorAt(i, i === hovered ? hover : colors[i]);
57
+ }
58
+ mesh.instanceMatrix.needsUpdate = true;
59
+ if (mesh.instanceColor !== null)
60
+ mesh.instanceColor.needsUpdate = true;
61
+ });
62
+ function handleMove(event) {
63
+ if (onHover === undefined)
64
+ return;
65
+ event.stopPropagation();
66
+ onHover(event.instanceId ?? null);
67
+ }
68
+ if (renderItem !== undefined) {
69
+ return (_jsx("group", { children: data.map((item, i) => {
70
+ const [x, z] = position(item, i);
71
+ return (_jsx("group", { position: [x, 0, z], children: renderItem(item, i, { x, z, height: height(item, i) }) }, i));
72
+ }) }));
73
+ }
74
+ return (_jsx("instancedMesh", { ref: meshRef, args: [geometry, material, Math.max(capacity, 1)], castShadow: castShadow, receiveShadow: receiveShadow, frustumCulled: false, onPointerMove: onHover === undefined ? undefined : handleMove, onPointerOut: onHover === undefined ? undefined : () => onHover(null) }, capacity));
75
+ }
@@ -0,0 +1,10 @@
1
+ import type { WorldFeature } from "@jgengine/core/world/features";
2
+ export interface GridWorldSceneProps {
3
+ feature: WorldFeature;
4
+ }
5
+ /**
6
+ * Data-driven renderer for the `biomes()`/`voxel()`/`plots()`/`tilemap()` world-feature kinds
7
+ * (#207.1): one `InstancedMesh` of extruded, colored boxes built from each feature's declared
8
+ * `cells`, following the same direct-buffer pattern as `InstancedBodies`.
9
+ */
10
+ export declare function GridWorldScene({ feature }: GridWorldSceneProps): import("react").JSX.Element | null;