@jgengine/shell 0.14.0 → 0.15.0-next.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 +483 -1
- package/dist/GamePlayerShell.js +25 -5
- package/dist/Shell3dPresentation.d.ts +6 -6
- package/dist/Shell3dPresentation.js +4 -4
- package/dist/camera/ControllerCameraShake.d.ts +31 -0
- package/dist/camera/ControllerCameraShake.js +34 -0
- package/dist/camera/GameCameraRig.js +10 -5
- package/dist/camera/GameOrbitCamera.js +1 -10
- package/dist/camera/cameraRigs.js +12 -4
- package/dist/camera/index.d.ts +1 -0
- package/dist/camera/index.js +1 -0
- package/dist/camera/rigMath.d.ts +2 -0
- package/dist/camera/rigMath.js +7 -0
- package/dist/devtools/CollisionDebugWorld.js +2 -2
- package/dist/devtools/DevtoolsOverlay.js +2 -0
- package/dist/drivers/FrameDriver.d.ts +4 -1
- package/dist/drivers/FrameDriver.js +5 -3
- package/dist/environment/DayNightSky.d.ts +33 -0
- package/dist/environment/DayNightSky.js +51 -0
- package/dist/environment/Daylight.d.ts +3 -1
- package/dist/environment/Daylight.js +7 -3
- package/dist/environment/EnvironmentScene.js +6 -6
- package/dist/environment/RoadJunctions.d.ts +8 -4
- package/dist/environment/RoadJunctions.js +16 -15
- package/dist/environment/RoadRibbons.d.ts +42 -6
- package/dist/environment/RoadRibbons.js +150 -27
- package/dist/environment/VolumetricClouds.js +3 -3
- package/dist/environment/index.d.ts +1 -0
- package/dist/environment/index.js +1 -0
- package/dist/gameKit.d.ts +7 -4
- package/dist/gameKit.js +8 -5
- package/dist/inputSink.js +14 -0
- package/dist/materialOverride.js +30 -0
- package/dist/postfx/PostProcessing.d.ts +3 -2
- package/dist/postfx/PostProcessing.js +11 -3
- package/dist/postfx/ScreenEffectsOverlay.d.ts +32 -0
- package/dist/postfx/ScreenEffectsOverlay.js +70 -0
- package/dist/presentationResolve.d.ts +3 -2
- package/dist/presentationResolve.js +11 -6
- package/dist/render/CascadedShadows.d.ts +13 -0
- package/dist/render/CascadedShadows.js +96 -0
- package/dist/render/EnvironmentLighting.d.ts +23 -7
- package/dist/render/EnvironmentLighting.js +69 -10
- package/dist/render/PartMotion.js +4 -4
- package/dist/render/SceneLighting.js +7 -0
- package/dist/render/SceneModels.d.ts +37 -1
- package/dist/render/SceneModels.js +40 -11
- package/dist/render/modelLoad.d.ts +41 -5
- package/dist/render/modelLoad.js +85 -18
- package/dist/render/sceneCapture.d.ts +27 -0
- package/dist/render/sceneCapture.js +43 -0
- package/dist/render/useDisposable.d.ts +16 -0
- package/dist/render/useDisposable.js +24 -0
- package/dist/render/useModelAnimation.js +47 -4
- package/dist/scene/AuthoredScene.js +14 -24
- package/dist/scene/CityRenderer.js +103 -36
- package/dist/scene/GrassFieldRenderer.js +4 -1
- package/dist/scene/PoleLineRenderer.js +7 -16
- package/dist/settings/QuickControls.js +5 -3
- package/dist/settings/SettingsMenu.js +6 -3
- package/dist/settings/appliedSettings.d.ts +3 -1
- package/dist/settings/appliedSettings.js +17 -5
- package/dist/settings/settingsController.js +11 -5
- package/dist/shellMovement.d.ts +15 -1
- package/dist/shellMovement.js +15 -1
- package/dist/structures/GeneratedBuilding.js +4 -5
- package/dist/structures/TransformGizmo.js +6 -5
- package/dist/terrain/CarvedTerrain.js +3 -7
- package/dist/terrain/GrassField.js +2 -2
- package/dist/terrain/ProceduralGround.js +3 -5
- package/dist/terrain/grassMaterial.d.ts +8 -2
- package/dist/terrain/grassMaterial.js +24 -5
- package/dist/terrain/index.d.ts +1 -1
- package/dist/terrain/terrainDetailMaterial.js +11 -2
- package/dist/terrain/terrainMath.d.ts +38 -1
- package/dist/terrain/terrainMath.js +86 -64
- package/dist/touch/TouchControlsOverlay.d.ts +6 -2
- package/dist/touch/TouchControlsOverlay.js +136 -17
- package/dist/vfx/ParticleField.d.ts +32 -0
- package/dist/vfx/ParticleField.js +91 -0
- package/dist/vfx/WorldFloatingText.d.ts +32 -0
- package/dist/vfx/WorldFloatingText.js +32 -0
- package/dist/water/Ocean.js +3 -6
- package/dist/weather/LightningStrike.js +3 -9
- package/dist/weather/RainField.js +3 -7
- package/dist/weather/SnowField.js +3 -7
- package/dist/world/DataObjects.js +3 -6
- package/dist/world/GridWorldScene.js +3 -6
- package/dist/world/InstancedBodies.js +3 -6
- package/dist/world/InstancedJoints.js +2 -2
- package/dist/world/SpriteBatch.js +3 -6
- package/dist/world/WorldEntityFrames.d.ts +47 -0
- package/dist/world/WorldEntityFrames.js +75 -0
- package/dist/world/WorldHud.js +7 -5
- package/dist/world/WorldPings.d.ts +26 -0
- package/dist/world/WorldPings.js +68 -0
- package/dist/world/WorldScene.js +9 -4
- package/dist/world/pingPulse.d.ts +21 -0
- package/dist/world/pingPulse.js +17 -0
- package/package.json +5 -5
- package/skills/ce-handoff/SKILL.md +149 -0
- package/skills/game-design/SKILL.md +2 -0
- package/skills/jgengine/SKILL.md +3 -1
- package/skills/jgengine/api.md +17 -7
- package/skills/jgengine/capabilities.md +12 -0
- package/skills/jgengine/recipes/minimal-game.md +7 -4
- package/skills/jgengine-ui/SKILL.md +5 -3
- package/skills/jgengine-ui/api.md +708 -19
- package/skills/jgengine-ui/capabilities.md +362 -1
- package/skills/jgengine-ui/recipes/overhead-entity-frames.md +136 -0
- package/skills/jgengine-ui/reference.md +7 -4
- package/skills/jgengine-world/api.md +378 -11
- package/skills/jgengine-world/capabilities.md +123 -2
- package/skills/jgengine-world/reference.md +15 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type ReactElement } from "react";
|
|
2
|
+
import type { ParticleSystem } from "@jgengine/core/vfx/particles";
|
|
3
|
+
/** How particle fragments composite: `additive` for fire/sparks/magic glow, `normal` for smoke/dust. */
|
|
4
|
+
export type ParticleBlending = "additive" | "normal";
|
|
5
|
+
/** Props for {@link ParticleField}. */
|
|
6
|
+
export interface ParticleFieldProps {
|
|
7
|
+
/** The core simulation to render (from `createParticleSystem`). */
|
|
8
|
+
system: ParticleSystem;
|
|
9
|
+
/**
|
|
10
|
+
* Advance the simulation each frame with the render delta. Leave `true` for a
|
|
11
|
+
* fire-and-forget effect; set `false` when your game loop already ticks the
|
|
12
|
+
* system (so it advances exactly once per fixed step).
|
|
13
|
+
*/
|
|
14
|
+
advance?: boolean;
|
|
15
|
+
/** Compositing mode. Default `additive` (glowy). */
|
|
16
|
+
blending?: ParticleBlending;
|
|
17
|
+
/** Pixel-size multiplier applied to every particle's world radius. Default 300. */
|
|
18
|
+
scale?: number;
|
|
19
|
+
/** Write depth (occludes particles behind geometry). Default `false` so a glow layer never self-occludes. */
|
|
20
|
+
depthWrite?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Renders a core `ParticleSystem` as a GPU point cloud: one draw call, per-particle
|
|
24
|
+
* size / color / alpha via a soft-round shader, and a `drawRange` clamped to the live
|
|
25
|
+
* count so dead particles cost nothing. The simulation stays engine-side and
|
|
26
|
+
* genre-agnostic; this is purely how it reaches the screen. By default it advances
|
|
27
|
+
* the sim with the frame delta — pass `advance={false}` to drive it from your own
|
|
28
|
+
* fixed loop instead.
|
|
29
|
+
*
|
|
30
|
+
* @capability particle-field render a core particle system as a soft-point GPU cloud with per-particle size, color, and alpha
|
|
31
|
+
*/
|
|
32
|
+
export declare function ParticleField({ system, advance, blending, scale, depthWrite, }: ParticleFieldProps): ReactElement;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useFrame, useThree } from "@react-three/fiber";
|
|
3
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
4
|
+
import * as THREE from "three";
|
|
5
|
+
const VERTEX_SHADER = /* glsl */ `
|
|
6
|
+
attribute float aSize;
|
|
7
|
+
attribute float aAlpha;
|
|
8
|
+
attribute vec3 aColor;
|
|
9
|
+
varying float vAlpha;
|
|
10
|
+
varying vec3 vColor;
|
|
11
|
+
uniform float uScale;
|
|
12
|
+
void main() {
|
|
13
|
+
vAlpha = aAlpha;
|
|
14
|
+
vColor = aColor;
|
|
15
|
+
vec4 mv = modelViewMatrix * vec4(position, 1.0);
|
|
16
|
+
gl_PointSize = aSize * uScale / max(0.0001, -mv.z);
|
|
17
|
+
gl_Position = projectionMatrix * mv;
|
|
18
|
+
}
|
|
19
|
+
`;
|
|
20
|
+
const FRAGMENT_SHADER = /* glsl */ `
|
|
21
|
+
varying float vAlpha;
|
|
22
|
+
varying vec3 vColor;
|
|
23
|
+
void main() {
|
|
24
|
+
// Soft round sprite: radial falloff from the point center, discard the corners.
|
|
25
|
+
vec2 d = gl_PointCoord - vec2(0.5);
|
|
26
|
+
float r = length(d);
|
|
27
|
+
if (r > 0.5) discard;
|
|
28
|
+
float soft = smoothstep(0.5, 0.0, r);
|
|
29
|
+
gl_FragColor = vec4(vColor, vAlpha * soft);
|
|
30
|
+
}
|
|
31
|
+
`;
|
|
32
|
+
/**
|
|
33
|
+
* Renders a core `ParticleSystem` as a GPU point cloud: one draw call, per-particle
|
|
34
|
+
* size / color / alpha via a soft-round shader, and a `drawRange` clamped to the live
|
|
35
|
+
* count so dead particles cost nothing. The simulation stays engine-side and
|
|
36
|
+
* genre-agnostic; this is purely how it reaches the screen. By default it advances
|
|
37
|
+
* the sim with the frame delta — pass `advance={false}` to drive it from your own
|
|
38
|
+
* fixed loop instead.
|
|
39
|
+
*
|
|
40
|
+
* @capability particle-field render a core particle system as a soft-point GPU cloud with per-particle size, color, and alpha
|
|
41
|
+
*/
|
|
42
|
+
export function ParticleField({ system, advance = true, blending = "additive", scale = 300, depthWrite = false, }) {
|
|
43
|
+
const pointsRef = useRef(null);
|
|
44
|
+
const dpr = useThree((state) => state.viewport.dpr);
|
|
45
|
+
const { geometry, material } = useMemo(() => {
|
|
46
|
+
const initial = system.buffers();
|
|
47
|
+
const max = initial.positions.length / 3;
|
|
48
|
+
const geo = new THREE.BufferGeometry();
|
|
49
|
+
geo.setAttribute("position", new THREE.BufferAttribute(new Float32Array(max * 3), 3));
|
|
50
|
+
geo.setAttribute("aSize", new THREE.BufferAttribute(new Float32Array(max), 1));
|
|
51
|
+
geo.setAttribute("aColor", new THREE.BufferAttribute(new Float32Array(max * 3), 3));
|
|
52
|
+
geo.setAttribute("aAlpha", new THREE.BufferAttribute(new Float32Array(max), 1));
|
|
53
|
+
geo.setDrawRange(0, 0);
|
|
54
|
+
const mat = new THREE.ShaderMaterial({
|
|
55
|
+
vertexShader: VERTEX_SHADER,
|
|
56
|
+
fragmentShader: FRAGMENT_SHADER,
|
|
57
|
+
uniforms: { uScale: { value: scale } },
|
|
58
|
+
transparent: true,
|
|
59
|
+
depthWrite,
|
|
60
|
+
blending: blending === "additive" ? THREE.AdditiveBlending : THREE.NormalBlending,
|
|
61
|
+
});
|
|
62
|
+
return { geometry: geo, material: mat };
|
|
63
|
+
}, [system, scale, depthWrite, blending]);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
return () => {
|
|
66
|
+
geometry.dispose();
|
|
67
|
+
material.dispose();
|
|
68
|
+
};
|
|
69
|
+
}, [geometry, material]);
|
|
70
|
+
useFrame((_, delta) => {
|
|
71
|
+
if (advance)
|
|
72
|
+
system.update(Math.min(delta, 0.1));
|
|
73
|
+
const buffers = system.buffers();
|
|
74
|
+
const count = buffers.count;
|
|
75
|
+
const pos = geometry.getAttribute("position");
|
|
76
|
+
const size = geometry.getAttribute("aSize");
|
|
77
|
+
const color = geometry.getAttribute("aColor");
|
|
78
|
+
const alpha = geometry.getAttribute("aAlpha");
|
|
79
|
+
pos.array.set(buffers.positions.subarray(0, count * 3));
|
|
80
|
+
size.array.set(buffers.sizes.subarray(0, count));
|
|
81
|
+
color.array.set(buffers.colors.subarray(0, count * 3));
|
|
82
|
+
alpha.array.set(buffers.alphas.subarray(0, count));
|
|
83
|
+
pos.needsUpdate = true;
|
|
84
|
+
size.needsUpdate = true;
|
|
85
|
+
color.needsUpdate = true;
|
|
86
|
+
alpha.needsUpdate = true;
|
|
87
|
+
geometry.setDrawRange(0, count);
|
|
88
|
+
material.uniforms.uScale.value = scale * dpr;
|
|
89
|
+
});
|
|
90
|
+
return _jsx("points", { ref: pointsRef, geometry: geometry, material: material, frustumCulled: false });
|
|
91
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type CSSProperties } from "react";
|
|
2
|
+
import type { FloatingTextField, FloatingTextView } from "@jgengine/core/ui/floatingText";
|
|
3
|
+
import { type FloatingTextLayoutOptions } from "@jgengine/react/floatingText";
|
|
4
|
+
/** Props for {@link WorldFloatingText}. */
|
|
5
|
+
export interface WorldFloatingTextProps extends FloatingTextLayoutOptions {
|
|
6
|
+
/** The core field to render (`createFloatingTextField`). */
|
|
7
|
+
field: FloatingTextField;
|
|
8
|
+
/**
|
|
9
|
+
* Advance the field each frame with the render delta. Leave `true` for a
|
|
10
|
+
* fire-and-forget overlay; set `false` when your game loop already ticks the
|
|
11
|
+
* field so it advances exactly once per fixed step.
|
|
12
|
+
*/
|
|
13
|
+
advance?: boolean;
|
|
14
|
+
/** Per-`kind` art direction merged over the defaults (color, font, shadow). */
|
|
15
|
+
styleFor?: (view: FloatingTextView) => CSSProperties;
|
|
16
|
+
/** Base font size in px at `size === 1`. Default 20. */
|
|
17
|
+
fontSizePx?: number;
|
|
18
|
+
/** Class on the overlay layer. */
|
|
19
|
+
className?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* R3F one-line drop-in for world-anchored floating text: mounts inside the scene,
|
|
23
|
+
* (optionally) advances a core {@link FloatingTextField} each frame, samples the
|
|
24
|
+
* live camera to project every live entry to the screen, and renders
|
|
25
|
+
* `@jgengine/react`'s {@link FloatingText} through a fullscreen `<Html>` overlay.
|
|
26
|
+
* This is the turnkey way an R3F game gets damage numbers / heal pops / XP gains
|
|
27
|
+
* from its own field without wiring projection by hand — the camera binding,
|
|
28
|
+
* viewport culling, and depth stacking come for free. Skin per `kind` with `styleFor`.
|
|
29
|
+
*
|
|
30
|
+
* @capability floating-text R3F drop-in that advances a floating-text field and renders it camera-projected over the scene
|
|
31
|
+
*/
|
|
32
|
+
export declare function WorldFloatingText({ field, advance, styleFor, fontSizePx, className, ...layout }: WorldFloatingTextProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Html } from "@react-three/drei";
|
|
3
|
+
import { useFrame, useThree } from "@react-three/fiber";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { FloatingText } from "@jgengine/react/floatingText";
|
|
6
|
+
import { useWorldProjection } from "../world/WorldEntityFrames.js";
|
|
7
|
+
function pinToViewport(_el, _camera, size) {
|
|
8
|
+
return [size.width / 2, size.height / 2];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* R3F one-line drop-in for world-anchored floating text: mounts inside the scene,
|
|
12
|
+
* (optionally) advances a core {@link FloatingTextField} each frame, samples the
|
|
13
|
+
* live camera to project every live entry to the screen, and renders
|
|
14
|
+
* `@jgengine/react`'s {@link FloatingText} through a fullscreen `<Html>` overlay.
|
|
15
|
+
* This is the turnkey way an R3F game gets damage numbers / heal pops / XP gains
|
|
16
|
+
* from its own field without wiring projection by hand — the camera binding,
|
|
17
|
+
* viewport culling, and depth stacking come for free. Skin per `kind` with `styleFor`.
|
|
18
|
+
*
|
|
19
|
+
* @capability floating-text R3F drop-in that advances a floating-text field and renders it camera-projected over the scene
|
|
20
|
+
*/
|
|
21
|
+
export function WorldFloatingText({ field, advance = true, styleFor, fontSizePx, className, ...layout }) {
|
|
22
|
+
const size = useThree((state) => state.size);
|
|
23
|
+
const project = useWorldProjection();
|
|
24
|
+
const [, setTick] = useState(0);
|
|
25
|
+
useFrame((_, delta) => {
|
|
26
|
+
if (advance)
|
|
27
|
+
field.update(Math.min(delta, 0.1));
|
|
28
|
+
// Re-render every frame so projected positions follow the camera and the sim.
|
|
29
|
+
setTick((n) => n + 1);
|
|
30
|
+
});
|
|
31
|
+
return (_jsx(Html, { fullscreen: true, calculatePosition: pinToViewport, zIndexRange: [20, 0], style: { pointerEvents: "none" }, children: _jsx(FloatingText, { entries: field.active(), project: project, viewport: { width: size.width, height: size.height }, styleFor: styleFor, fontSizePx: fontSizePx, className: className, ...layout }) }));
|
|
32
|
+
}
|
package/dist/water/Ocean.js
CHANGED
|
@@ -2,6 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { useFrame } from "@react-three/fiber";
|
|
3
3
|
import { useEffect, useMemo, useRef } from "react";
|
|
4
4
|
import * as THREE from "three";
|
|
5
|
+
import { useDisposable } from "../render/useDisposable.js";
|
|
5
6
|
import { createOceanConfig } from "./OceanConfig.js";
|
|
6
7
|
import { createOceanMaterial, syncOceanMaterial } from "./OceanMaterial.js";
|
|
7
8
|
/** aDepth when no sampler is given: everywhere-deep, matching the pre-depth shader look. */
|
|
@@ -26,14 +27,10 @@ function createOceanGeometry(config, depthAt) {
|
|
|
26
27
|
export function Ocean({ config, depthAt, ...meshProps }) {
|
|
27
28
|
const resolved = useMemo(() => createOceanConfig(config), [config]);
|
|
28
29
|
const elapsedRef = useRef(0);
|
|
29
|
-
const geometry =
|
|
30
|
-
const material =
|
|
31
|
-
useEffect(() => {
|
|
32
|
-
return () => geometry.dispose();
|
|
33
|
-
}, [geometry]);
|
|
30
|
+
const geometry = useDisposable(() => createOceanGeometry(resolved, depthAt), [resolved, depthAt]);
|
|
31
|
+
const material = useDisposable(() => createOceanMaterial(resolved), [resolved]);
|
|
34
32
|
useEffect(() => {
|
|
35
33
|
elapsedRef.current = 0;
|
|
36
|
-
return () => material.dispose();
|
|
37
34
|
}, [material]);
|
|
38
35
|
useFrame((_, delta) => {
|
|
39
36
|
elapsedRef.current += delta * resolved.timeScale;
|
|
@@ -2,6 +2,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import { useEffect, useMemo, useRef } from "react";
|
|
3
3
|
import { useFrame } from "@react-three/fiber";
|
|
4
4
|
import * as THREE from "three";
|
|
5
|
+
import { useDisposable } from "../render/useDisposable.js";
|
|
5
6
|
const DEFAULT_COLOR = "#bdd4ff";
|
|
6
7
|
function createRandom(seed) {
|
|
7
8
|
let state = seed >>> 0;
|
|
@@ -80,8 +81,8 @@ function writeLightningGeometry(geometry, segments) {
|
|
|
80
81
|
export function LightningStrike({ origin, target, strikeKey = 0, seed = 451, visible = true, duration = 0.18, color = DEFAULT_COLOR, glow = 2.4, branches = 5, jaggedness = 0.08, impactLight = 26, renderOrder = 20, }) {
|
|
81
82
|
const lifeRef = useRef(0);
|
|
82
83
|
const lightRef = useRef(null);
|
|
83
|
-
const geometry =
|
|
84
|
-
const material =
|
|
84
|
+
const geometry = useDisposable(() => new THREE.BufferGeometry(), []);
|
|
85
|
+
const material = useDisposable(() => new THREE.LineBasicMaterial({
|
|
85
86
|
color,
|
|
86
87
|
transparent: true,
|
|
87
88
|
opacity: 0,
|
|
@@ -94,9 +95,6 @@ export function LightningStrike({ origin, target, strikeKey = 0, seed = 451, vis
|
|
|
94
95
|
next.renderOrder = renderOrder;
|
|
95
96
|
return next;
|
|
96
97
|
}, [geometry, material, renderOrder]);
|
|
97
|
-
useEffect(() => {
|
|
98
|
-
material.color.set(color);
|
|
99
|
-
}, [color, material]);
|
|
100
98
|
useEffect(() => {
|
|
101
99
|
if (!visible) {
|
|
102
100
|
lifeRef.current = 0;
|
|
@@ -122,9 +120,5 @@ export function LightningStrike({ origin, target, strikeKey = 0, seed = 451, vis
|
|
|
122
120
|
if (lightRef.current !== null)
|
|
123
121
|
lightRef.current.intensity = amount * impactLight * flicker;
|
|
124
122
|
});
|
|
125
|
-
useEffect(() => () => {
|
|
126
|
-
geometry.dispose();
|
|
127
|
-
material.dispose();
|
|
128
|
-
}, [geometry, material]);
|
|
129
123
|
return (_jsxs(_Fragment, { children: [_jsx("primitive", { object: line }), _jsx("pointLight", { ref: lightRef, position: [target[0], target[1], target[2]], color: color, intensity: 0, distance: 56, decay: 2 })] }));
|
|
130
124
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useMemo } from "react";
|
|
3
2
|
import { useFrame, useThree } from "@react-three/fiber";
|
|
4
3
|
import * as THREE from "three";
|
|
4
|
+
import { useDisposable } from "../render/useDisposable.js";
|
|
5
5
|
import { createWeatherQuadGeometry } from "./weatherGeometry.js";
|
|
6
6
|
import { DEFAULT_RAIN_COUNT, DEFAULT_RAIN_DENSITY, resolveWeatherInstanceCount } from "./weatherMath.js";
|
|
7
7
|
import { useWeatherUniformSet } from "./weatherUniforms.js";
|
|
@@ -11,8 +11,8 @@ const DEFAULT_RAIN_COLOR = "#b8c4d8";
|
|
|
11
11
|
export function RainField({ count = DEFAULT_RAIN_COUNT, density = DEFAULT_RAIN_DENSITY, budget, volume = DEFAULT_VOLUME, wind, origin = DEFAULT_ORIGIN, followCamera = true, speed = 22, length = 1.35, width = 0.018, opacity = 0.48, color = DEFAULT_RAIN_COLOR, lightning, timeScale, seed = 11939, renderOrder = 10, frustumCulled, }) {
|
|
12
12
|
const { camera } = useThree();
|
|
13
13
|
const shared = useWeatherUniformSet({ wind, lightning, timeScale });
|
|
14
|
-
const geometry =
|
|
15
|
-
const material =
|
|
14
|
+
const geometry = useDisposable(() => createWeatherQuadGeometry(count, seed), [count, seed]);
|
|
15
|
+
const material = useDisposable(() => {
|
|
16
16
|
const uniforms = {
|
|
17
17
|
uTime: shared.time,
|
|
18
18
|
uWind: shared.wind,
|
|
@@ -99,9 +99,5 @@ export function RainField({ count = DEFAULT_RAIN_COUNT, density = DEFAULT_RAIN_D
|
|
|
99
99
|
uniforms.uColor.value.set(color);
|
|
100
100
|
geometry.instanceCount = resolveWeatherInstanceCount(count, density, budget);
|
|
101
101
|
});
|
|
102
|
-
useEffect(() => () => {
|
|
103
|
-
geometry.dispose();
|
|
104
|
-
material.dispose();
|
|
105
|
-
}, [geometry, material]);
|
|
106
102
|
return (_jsx("mesh", { geometry: geometry, material: material, frustumCulled: frustumCulled ?? !followCamera, renderOrder: renderOrder }));
|
|
107
103
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useMemo } from "react";
|
|
3
2
|
import { useFrame, useThree } from "@react-three/fiber";
|
|
4
3
|
import * as THREE from "three";
|
|
4
|
+
import { useDisposable } from "../render/useDisposable.js";
|
|
5
5
|
import { createWeatherQuadGeometry } from "./weatherGeometry.js";
|
|
6
6
|
import { DEFAULT_SNOW_COUNT, DEFAULT_SNOW_DENSITY, resolveWeatherInstanceCount } from "./weatherMath.js";
|
|
7
7
|
import { useWeatherUniformSet } from "./weatherUniforms.js";
|
|
@@ -11,8 +11,8 @@ const DEFAULT_SNOW_COLOR = "#ffffff";
|
|
|
11
11
|
export function SnowField({ count = DEFAULT_SNOW_COUNT, density = DEFAULT_SNOW_DENSITY, budget, volume = DEFAULT_VOLUME, wind, origin = DEFAULT_ORIGIN, followCamera = true, speed = 3.2, size = 0.11, sway = 0.62, opacity = 0.86, color = DEFAULT_SNOW_COLOR, timeScale, seed = 72931, renderOrder = 11, frustumCulled, }) {
|
|
12
12
|
const { camera } = useThree();
|
|
13
13
|
const shared = useWeatherUniformSet({ wind, timeScale });
|
|
14
|
-
const geometry =
|
|
15
|
-
const material =
|
|
14
|
+
const geometry = useDisposable(() => createWeatherQuadGeometry(count, seed), [count, seed]);
|
|
15
|
+
const material = useDisposable(() => {
|
|
16
16
|
const uniforms = {
|
|
17
17
|
uTime: shared.time,
|
|
18
18
|
uWind: shared.wind,
|
|
@@ -100,9 +100,5 @@ export function SnowField({ count = DEFAULT_SNOW_COUNT, density = DEFAULT_SNOW_D
|
|
|
100
100
|
uniforms.uColor.value.set(color);
|
|
101
101
|
geometry.instanceCount = resolveWeatherInstanceCount(count, density, budget);
|
|
102
102
|
});
|
|
103
|
-
useEffect(() => () => {
|
|
104
|
-
geometry.dispose();
|
|
105
|
-
material.dispose();
|
|
106
|
-
}, [geometry, material]);
|
|
107
103
|
return (_jsx("mesh", { geometry: geometry, material: material, frustumCulled: frustumCulled ?? !followCamera, renderOrder: renderOrder }));
|
|
108
104
|
}
|
|
@@ -2,14 +2,15 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { useFrame } from "@react-three/fiber";
|
|
3
3
|
import { useEffect, useMemo, useRef } from "react";
|
|
4
4
|
import * as THREE from "three";
|
|
5
|
+
import { useDisposable } from "../render/useDisposable.js";
|
|
5
6
|
function clamp01(v) {
|
|
6
7
|
return v < 0 ? 0 : v > 1 ? 1 : v;
|
|
7
8
|
}
|
|
8
9
|
export function DataObjects({ data, position, height, color, cellSize = 0.28, hovered = null, hoverColor = "#ffffff", onHover, grow, castShadow = true, receiveShadow = true, renderItem, }) {
|
|
9
10
|
const meshRef = useRef(null);
|
|
10
11
|
const dummy = useMemo(() => new THREE.Object3D(), []);
|
|
11
|
-
const geometry =
|
|
12
|
-
const material =
|
|
12
|
+
const geometry = useDisposable(() => new THREE.BoxGeometry(1, 1, 1), []);
|
|
13
|
+
const material = useDisposable(() => new THREE.MeshStandardMaterial({ roughness: 0.62, metalness: 0.04 }), []);
|
|
13
14
|
const hover = useMemo(() => new THREE.Color(hoverColor), [hoverColor]);
|
|
14
15
|
const startRef = useRef(null);
|
|
15
16
|
const capacity = data.length;
|
|
@@ -32,10 +33,6 @@ export function DataObjects({ data, position, height, color, cellSize = 0.28, ho
|
|
|
32
33
|
useEffect(() => {
|
|
33
34
|
startRef.current = null;
|
|
34
35
|
}, [data]);
|
|
35
|
-
useEffect(() => () => {
|
|
36
|
-
geometry.dispose();
|
|
37
|
-
material.dispose();
|
|
38
|
-
}, [geometry, material]);
|
|
39
36
|
const duration = grow?.duration ?? 0;
|
|
40
37
|
useFrame((state) => {
|
|
41
38
|
const mesh = meshRef.current;
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { useEffect, useMemo, useRef } from "react";
|
|
3
3
|
import * as THREE from "three";
|
|
4
4
|
import { resolveGridInstances } from "@jgengine/core/world/gridInstances";
|
|
5
|
+
import { useDisposable } from "../render/useDisposable.js";
|
|
5
6
|
function isGridWorldFeature(feature) {
|
|
6
7
|
return (feature.kind === "biomes" || feature.kind === "voxel" || feature.kind === "plots" || feature.kind === "tilemap");
|
|
7
8
|
}
|
|
@@ -13,8 +14,8 @@ function isGridWorldFeature(feature) {
|
|
|
13
14
|
export function GridWorldScene({ feature }) {
|
|
14
15
|
const instances = useMemo(() => (isGridWorldFeature(feature) ? resolveGridInstances(feature) : []), [feature]);
|
|
15
16
|
const meshRef = useRef(null);
|
|
16
|
-
const geometry =
|
|
17
|
-
const material =
|
|
17
|
+
const geometry = useDisposable(() => new THREE.BoxGeometry(1, 1, 1), []);
|
|
18
|
+
const material = useDisposable(() => new THREE.MeshStandardMaterial({ roughness: 0.9, metalness: 0 }), []);
|
|
18
19
|
const instanceColor = useMemo(() => new THREE.InstancedBufferAttribute(new Float32Array(Math.max(instances.length, 1) * 3), 3), [instances.length]);
|
|
19
20
|
useEffect(() => {
|
|
20
21
|
const mesh = meshRef.current;
|
|
@@ -44,10 +45,6 @@ export function GridWorldScene({ feature }) {
|
|
|
44
45
|
mesh.instanceMatrix.needsUpdate = true;
|
|
45
46
|
instanceColor.needsUpdate = true;
|
|
46
47
|
}, [instances, instanceColor]);
|
|
47
|
-
useEffect(() => () => {
|
|
48
|
-
geometry.dispose();
|
|
49
|
-
material.dispose();
|
|
50
|
-
}, [geometry, material]);
|
|
51
48
|
if (instances.length === 0)
|
|
52
49
|
return null;
|
|
53
50
|
return (_jsx("instancedMesh", { ref: meshRef, args: [geometry, material, instances.length], castShadow: true, receiveShadow: true }));
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { useFrame } from "@react-three/fiber";
|
|
3
3
|
import { useEffect, useMemo, useRef } from "react";
|
|
4
4
|
import * as THREE from "three";
|
|
5
|
+
import { useDisposable } from "../render/useDisposable.js";
|
|
5
6
|
const SLEEPING_COLOR = new THREE.Color(0.32, 0.4, 0.62);
|
|
6
7
|
const AWAKE_COLOR = new THREE.Color(0.45, 0.78, 0.5);
|
|
7
8
|
const CONTACT_COLOR = new THREE.Color(0.96, 0.55, 0.18);
|
|
@@ -13,8 +14,8 @@ const DEFAULT_BASE = new THREE.Color(0.62, 0.64, 0.68);
|
|
|
13
14
|
*/
|
|
14
15
|
export function InstancedBodies({ world, debugTint = false, baseColors, epoch = 0 }) {
|
|
15
16
|
const meshRef = useRef(null);
|
|
16
|
-
const geometry =
|
|
17
|
-
const material =
|
|
17
|
+
const geometry = useDisposable(() => new THREE.BoxGeometry(1, 1, 1), []);
|
|
18
|
+
const material = useDisposable(() => new THREE.MeshStandardMaterial({ roughness: 0.85, metalness: 0, vertexColors: false }), []);
|
|
18
19
|
const capacity = world.capacity;
|
|
19
20
|
const instanceColor = useMemo(() => new THREE.InstancedBufferAttribute(new Float32Array(capacity * 3), 3), [capacity]);
|
|
20
21
|
useEffect(() => {
|
|
@@ -76,9 +77,5 @@ export function InstancedBodies({ world, debugTint = false, baseColors, epoch =
|
|
|
76
77
|
}
|
|
77
78
|
lastTintRef.current = debugTint;
|
|
78
79
|
});
|
|
79
|
-
useEffect(() => () => {
|
|
80
|
-
geometry.dispose();
|
|
81
|
-
material.dispose();
|
|
82
|
-
}, [geometry, material]);
|
|
83
80
|
return (_jsx("instancedMesh", { ref: meshRef, args: [geometry, material, capacity], frustumCulled: false, castShadow: false, receiveShadow: false }));
|
|
84
81
|
}
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useFrame } from "@react-three/fiber";
|
|
3
3
|
import { useEffect, useMemo, useRef } from "react";
|
|
4
4
|
import * as THREE from "three";
|
|
5
|
+
import { useDisposable } from "../render/useDisposable.js";
|
|
5
6
|
/**
|
|
6
7
|
* Debug overlay drawing a PhysicsWorld's joints (suspension, ragdoll links, carry tethers) as one
|
|
7
8
|
* LineSegments batch. Endpoints are streamed each frame from `world.readJointSegments`; pair with
|
|
@@ -11,7 +12,7 @@ export function InstancedJoints({ world, color = "#f5c542" }) {
|
|
|
11
12
|
const geometryRef = useRef(null);
|
|
12
13
|
const positions = useMemo(() => new Float32Array(world.jointCapacity * 6), [world.jointCapacity]);
|
|
13
14
|
const attribute = useMemo(() => new THREE.BufferAttribute(positions, 3), [positions]);
|
|
14
|
-
const material =
|
|
15
|
+
const material = useDisposable(() => new THREE.LineBasicMaterial({ color }), [color]);
|
|
15
16
|
useEffect(() => {
|
|
16
17
|
const geometry = geometryRef.current;
|
|
17
18
|
if (geometry === null)
|
|
@@ -27,6 +28,5 @@ export function InstancedJoints({ world, color = "#f5c542" }) {
|
|
|
27
28
|
attribute.needsUpdate = true;
|
|
28
29
|
geometry.setDrawRange(0, count * 2);
|
|
29
30
|
});
|
|
30
|
-
useEffect(() => () => material.dispose(), [material]);
|
|
31
31
|
return (_jsxs("lineSegments", { frustumCulled: false, children: [_jsx("bufferGeometry", { ref: geometryRef }), _jsx("primitive", { object: material, attach: "material" })] }));
|
|
32
32
|
}
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { useFrame, useLoader, useThree } from "@react-three/fiber";
|
|
3
3
|
import { useEffect, useMemo, useRef } from "react";
|
|
4
4
|
import * as THREE from "three";
|
|
5
|
+
import { useDisposable } from "../render/useDisposable.js";
|
|
5
6
|
const DEFAULT_CAPACITY = 1024;
|
|
6
7
|
const DEFAULT_ALPHA_TEST = 0.08;
|
|
7
8
|
const XZ_BASE_QUATERNION = new THREE.Quaternion().setFromEuler(new THREE.Euler(-Math.PI / 2, 0, 0));
|
|
@@ -29,9 +30,9 @@ export function SpriteBatch({ url, columns = 1, rows = 1, capacity = DEFAULT_CAP
|
|
|
29
30
|
}
|
|
30
31
|
texture.needsUpdate = true;
|
|
31
32
|
}, [texture, pixelated]);
|
|
32
|
-
const geometry =
|
|
33
|
+
const geometry = useDisposable(() => new THREE.PlaneGeometry(1, 1), []);
|
|
33
34
|
const spriteUvOffset = useMemo(() => new THREE.InstancedBufferAttribute(new Float32Array(capacity * 2), 2), [capacity]);
|
|
34
|
-
const material =
|
|
35
|
+
const material = useDisposable(() => {
|
|
35
36
|
const mat = new THREE.MeshBasicMaterial({
|
|
36
37
|
map: texture,
|
|
37
38
|
transparent: true,
|
|
@@ -104,9 +105,5 @@ uniform vec2 spriteUvScale;`)
|
|
|
104
105
|
mesh.instanceMatrix.needsUpdate = true;
|
|
105
106
|
spriteUvOffset.needsUpdate = true;
|
|
106
107
|
});
|
|
107
|
-
useEffect(() => () => {
|
|
108
|
-
geometry.dispose();
|
|
109
|
-
material.dispose();
|
|
110
|
-
}, [geometry, material]);
|
|
111
108
|
return (_jsx("instancedMesh", { ref: meshRef, args: [geometry, material, capacity], frustumCulled: false, castShadow: false, receiveShadow: false }));
|
|
112
109
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import * as THREE from "three";
|
|
3
|
+
import { type EntityFrameEntry, type EntityFrameLayoutOptions, type EntityFramePlacement, type EntityScreenProjection, type ProjectEntity } from "@jgengine/react/entityFrames";
|
|
4
|
+
/**
|
|
5
|
+
* Returns a {@link ProjectEntity} bound to the live R3F camera and canvas size —
|
|
6
|
+
* the world→screen half of the data-first entity-frame seam. Call the returned
|
|
7
|
+
* function with a world position to get a CSS-pixel screen point (or `null`/`behind`
|
|
8
|
+
* when the point is outside the frustum). Reads the camera fresh on every call, so
|
|
9
|
+
* sample it inside a frame loop. Games that already own their entities use this to
|
|
10
|
+
* feed `@jgengine/react`'s `EntityFrames` without mirroring state into a store.
|
|
11
|
+
*
|
|
12
|
+
* @capability entity-frames R3F camera→screen projector for world-anchored frames
|
|
13
|
+
*/
|
|
14
|
+
export declare function useWorldProjection(): ProjectEntity;
|
|
15
|
+
/**
|
|
16
|
+
* Pure world→screen projection: NDC via `camera.project`, mapped to CSS pixels.
|
|
17
|
+
* `behind` is set when the point falls outside the near/far clip range (NDC z
|
|
18
|
+
* beyond [-1, 1]), which covers positions behind the camera. Exposed for tests
|
|
19
|
+
* and for callers wiring their own frame loop around a bare THREE camera.
|
|
20
|
+
*
|
|
21
|
+
* @capability entity-frames pure world→screen projection for a bare THREE camera
|
|
22
|
+
*/
|
|
23
|
+
export declare function projectWorldToScreen(scratch: THREE.Vector3, camera: THREE.Camera, worldPosition: readonly [number, number, number], width: number, height: number): EntityScreenProjection;
|
|
24
|
+
/** Props for {@link WorldEntityFrames}. */
|
|
25
|
+
export interface WorldEntityFramesProps<E extends EntityFrameEntry> extends EntityFrameLayoutOptions {
|
|
26
|
+
/** Caller-owned entities to anchor; `worldPosition` is the overhead point. */
|
|
27
|
+
entries: readonly E[];
|
|
28
|
+
/** Renders one entity's frame — compose `HealthBar value/max` + a name, themed via `barTokens`. */
|
|
29
|
+
renderFrame: (entry: E, placement: EntityFramePlacement<E>) => ReactNode;
|
|
30
|
+
/** Min ms between projection refreshes — trades smoothness for fewer re-renders at scale. Default 0 (every frame). */
|
|
31
|
+
tickMs?: number;
|
|
32
|
+
/** Class on the overlay layer. */
|
|
33
|
+
className?: string;
|
|
34
|
+
/** Per-frame wrapper transform. Default `translate(-50%, -100%)`. */
|
|
35
|
+
anchorTransform?: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* R3F convenience that mounts inside the scene, samples the live camera each
|
|
39
|
+
* frame (throttled by `tickMs`), and renders `@jgengine/react`'s {@link EntityFrames}
|
|
40
|
+
* through a fullscreen `<Html>` overlay. This is the one-line way an R3F game
|
|
41
|
+
* gets overhead enemy nameplates/health bars from its own entity array without a
|
|
42
|
+
* store: pass `entries` + a `renderFrame` that composes the shipped bars. The
|
|
43
|
+
* projection, viewport culling, and stacking come for free.
|
|
44
|
+
*
|
|
45
|
+
* @capability entity-frames R3F overhead entity frames from a caller-owned entity array
|
|
46
|
+
*/
|
|
47
|
+
export declare function WorldEntityFrames<E extends EntityFrameEntry>({ entries, renderFrame, tickMs, className, anchorTransform, ...layout }: WorldEntityFramesProps<E>): import("react").JSX.Element;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Html } from "@react-three/drei";
|
|
3
|
+
import { useFrame, useThree } from "@react-three/fiber";
|
|
4
|
+
import { useCallback, useRef, useState } from "react";
|
|
5
|
+
import * as THREE from "three";
|
|
6
|
+
import { EntityFrames, } from "@jgengine/react/entityFrames";
|
|
7
|
+
/**
|
|
8
|
+
* Returns a {@link ProjectEntity} bound to the live R3F camera and canvas size —
|
|
9
|
+
* the world→screen half of the data-first entity-frame seam. Call the returned
|
|
10
|
+
* function with a world position to get a CSS-pixel screen point (or `null`/`behind`
|
|
11
|
+
* when the point is outside the frustum). Reads the camera fresh on every call, so
|
|
12
|
+
* sample it inside a frame loop. Games that already own their entities use this to
|
|
13
|
+
* feed `@jgengine/react`'s `EntityFrames` without mirroring state into a store.
|
|
14
|
+
*
|
|
15
|
+
* @capability entity-frames R3F camera→screen projector for world-anchored frames
|
|
16
|
+
*/
|
|
17
|
+
export function useWorldProjection() {
|
|
18
|
+
const camera = useThree((state) => state.camera);
|
|
19
|
+
const size = useThree((state) => state.size);
|
|
20
|
+
const scratch = useRef(new THREE.Vector3());
|
|
21
|
+
return useCallback((worldPosition) => {
|
|
22
|
+
camera.updateMatrixWorld();
|
|
23
|
+
return projectWorldToScreen(scratch.current, camera, worldPosition, size.width, size.height);
|
|
24
|
+
}, [camera, size.width, size.height]);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Pure world→screen projection: NDC via `camera.project`, mapped to CSS pixels.
|
|
28
|
+
* `behind` is set when the point falls outside the near/far clip range (NDC z
|
|
29
|
+
* beyond [-1, 1]), which covers positions behind the camera. Exposed for tests
|
|
30
|
+
* and for callers wiring their own frame loop around a bare THREE camera.
|
|
31
|
+
*
|
|
32
|
+
* @capability entity-frames pure world→screen projection for a bare THREE camera
|
|
33
|
+
*/
|
|
34
|
+
export function projectWorldToScreen(scratch, camera, worldPosition, width, height) {
|
|
35
|
+
scratch.set(worldPosition[0], worldPosition[1], worldPosition[2]);
|
|
36
|
+
scratch.project(camera);
|
|
37
|
+
return {
|
|
38
|
+
x: (scratch.x * 0.5 + 0.5) * width,
|
|
39
|
+
y: (-scratch.y * 0.5 + 0.5) * height,
|
|
40
|
+
depth: scratch.z,
|
|
41
|
+
behind: scratch.z < -1 || scratch.z > 1,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function pinToViewport(_el, _camera, size) {
|
|
45
|
+
return [size.width / 2, size.height / 2];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* R3F convenience that mounts inside the scene, samples the live camera each
|
|
49
|
+
* frame (throttled by `tickMs`), and renders `@jgengine/react`'s {@link EntityFrames}
|
|
50
|
+
* through a fullscreen `<Html>` overlay. This is the one-line way an R3F game
|
|
51
|
+
* gets overhead enemy nameplates/health bars from its own entity array without a
|
|
52
|
+
* store: pass `entries` + a `renderFrame` that composes the shipped bars. The
|
|
53
|
+
* projection, viewport culling, and stacking come for free.
|
|
54
|
+
*
|
|
55
|
+
* @capability entity-frames R3F overhead entity frames from a caller-owned entity array
|
|
56
|
+
*/
|
|
57
|
+
export function WorldEntityFrames({ entries, renderFrame, tickMs = 0, className, anchorTransform, ...layout }) {
|
|
58
|
+
const size = useThree((state) => state.size);
|
|
59
|
+
const project = useWorldProjection();
|
|
60
|
+
// Re-render on a frame cadence so projected screen positions follow the camera.
|
|
61
|
+
const [, setTick] = useState(0);
|
|
62
|
+
useThreeFrameTick(setTick, tickMs);
|
|
63
|
+
return (_jsx(Html, { fullscreen: true, calculatePosition: pinToViewport, zIndexRange: [19, 0], style: { pointerEvents: "none" }, children: _jsx(EntityFrames, { entries: entries, project: project, renderFrame: renderFrame, viewport: { width: size.width, height: size.height }, className: className, anchorTransform: anchorTransform, ...layout }) }));
|
|
64
|
+
}
|
|
65
|
+
/** Bumps a state setter on a throttled frame cadence to drive re-projection. */
|
|
66
|
+
function useThreeFrameTick(setTick, tickMs) {
|
|
67
|
+
const last = useRef(0);
|
|
68
|
+
useFrame((state) => {
|
|
69
|
+
const elapsedMs = state.clock.elapsedTime * 1000;
|
|
70
|
+
if (elapsedMs - last.current < tickMs)
|
|
71
|
+
return;
|
|
72
|
+
last.current = elapsedMs;
|
|
73
|
+
setTick((n) => n + 1);
|
|
74
|
+
});
|
|
75
|
+
}
|
package/dist/world/WorldHud.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Html, Line } from "@react-three/drei";
|
|
|
3
3
|
import { useFrame, useThree } from "@react-three/fiber";
|
|
4
4
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
5
5
|
import * as THREE from "three";
|
|
6
|
+
import { useGameStore } from "@jgengine/react/hooks";
|
|
6
7
|
import { useGameContext } from "@jgengine/react/provider";
|
|
7
8
|
import { useCameraShake } from "../camera/shakeChannel.js";
|
|
8
9
|
import { CALIBRATED_TRAUMA_SHAKE_DECAY_PER_SECOND } from "../camera/rigMath.js";
|
|
@@ -108,11 +109,7 @@ export function WorldNameplates({ statId = "health", height = 2.3, roles, resolv
|
|
|
108
109
|
*/
|
|
109
110
|
export function WorldObjectHighlights({ color = "#facc15", radius, y = 0.05 }) {
|
|
110
111
|
const ctx = useGameContext();
|
|
111
|
-
const
|
|
112
|
-
useEffect(() => {
|
|
113
|
-
setIds(ctx.scene.object.selection.list());
|
|
114
|
-
return ctx.subscribe(() => setIds(ctx.scene.object.selection.list()));
|
|
115
|
-
}, [ctx]);
|
|
112
|
+
const ids = useGameStore((c) => c.scene.object.selection.list());
|
|
116
113
|
return (_jsx(_Fragment, { children: ids.map((id) => {
|
|
117
114
|
const object = ctx.scene.object.get(id);
|
|
118
115
|
if (object === null)
|
|
@@ -241,6 +238,11 @@ export function ProjectileTracers({ lifeMs = 130 }) {
|
|
|
241
238
|
const timers = useRef(new Set());
|
|
242
239
|
useEffect(() => {
|
|
243
240
|
const unsub = ctx.game.events.on("projectile.settled", (event) => {
|
|
241
|
+
// fireProjectile is a generic seam (bullets, bolts, grenades, launchers). A straight
|
|
242
|
+
// muzzle→impact line only reads as a real shot for direct-fire projectiles; drawing one
|
|
243
|
+
// for an arced/lobbed shot fakes a beam through a path it never flew, so skip those.
|
|
244
|
+
if (event.ballistic)
|
|
245
|
+
return;
|
|
244
246
|
const id = nextId.current++;
|
|
245
247
|
const start = new THREE.Vector3(event.origin[0], event.origin[1], event.origin[2]);
|
|
246
248
|
if (event.from === ctx.player.userId)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type MarkerCollection, type MarkerKindStyle, type MarkerView } from "@jgengine/core/world/markers";
|
|
3
|
+
/** Props for {@link WorldPings}. */
|
|
4
|
+
export interface WorldPingsProps {
|
|
5
|
+
/** Static views, an external marker source, or a native `MarkerSet` (e.g. the one a `createPingSystem` writes to). */
|
|
6
|
+
markers: MarkerCollection;
|
|
7
|
+
kindStyles?: Record<string, MarkerKindStyle>;
|
|
8
|
+
/** Only render markers whose `meta.ping === true`. Default true; false renders every marker as a ping. */
|
|
9
|
+
pingsOnly?: boolean;
|
|
10
|
+
/** Height above the ground the arrowhead floats. Default 2.4. */
|
|
11
|
+
height?: number;
|
|
12
|
+
/** Draw a billboarded callout chip (glyph + `meta.callout`/`label`). Default true. */
|
|
13
|
+
showCallout?: boolean;
|
|
14
|
+
/** Full override for a ping's callout markup. */
|
|
15
|
+
renderCallout?: (marker: MarkerView, style: MarkerKindStyle) => ReactNode;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* World-space pings — the in-scene side of a ping/marker: a bobbing downward
|
|
19
|
+
* arrowhead pointing at the spot, a ground ring, and a billboarded callout,
|
|
20
|
+
* colored by marker kind and fading in/out over the marker's lifetime. Reads a
|
|
21
|
+
* `MarkerSet` (typically the one a `createPingSystem` writes to), a marker
|
|
22
|
+
* source, or a static array; mount through `PlayableGame.WorldOverlay`.
|
|
23
|
+
*
|
|
24
|
+
* @capability world-pings in-scene ping markers — a bobbing downward arrowhead, ground ring, and billboarded callout per marker, colored by kind and fading over the marker's lifetime
|
|
25
|
+
*/
|
|
26
|
+
export declare function WorldPings({ markers, kindStyles, pingsOnly, height, showCallout, renderCallout, }: WorldPingsProps): ReactNode;
|