@jgengine/shell 0.19.0 → 0.22.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 +364 -0
- package/dist/GameHost.d.ts +12 -1
- package/dist/GameHost.js +14 -1
- package/dist/GamePlayerShell.js +113 -15
- package/dist/Shell3dPresentation.d.ts +2 -2
- package/dist/Shell3dPresentation.js +49 -7
- package/dist/ShellChrome.d.ts +2 -1
- package/dist/ShellChrome.js +22 -6
- package/dist/ShellHudPresentation.d.ts +11 -1
- package/dist/ShellHudPresentation.js +20 -6
- package/dist/audio/audioEngine.d.ts +16 -0
- package/dist/audio/audioEngine.js +178 -89
- package/dist/audio/audioWire.js +2 -2
- package/dist/audio/synthEngine.d.ts +7 -1
- package/dist/audio/synthEngine.js +103 -38
- package/dist/camera/GameCameraRig.js +2 -1
- package/dist/camera/GameFirstPersonCamera.d.ts +2 -2
- package/dist/camera/GameFirstPersonCamera.js +17 -13
- package/dist/camera/cameraRigs.js +8 -2
- package/dist/camera/chaseHeading.d.ts +9 -0
- package/dist/camera/chaseHeading.js +10 -0
- package/dist/camera/customMuzzle.d.ts +14 -0
- package/dist/camera/customMuzzle.js +68 -0
- package/dist/camera/index.d.ts +1 -0
- package/dist/camera/index.js +1 -0
- package/dist/camera/rigResolve.d.ts +3 -0
- package/dist/camera/rigResolve.js +11 -0
- package/dist/combat/ProjectileModels.d.ts +31 -0
- package/dist/combat/ProjectileModels.js +70 -0
- package/dist/commandSink.d.ts +1 -2
- package/dist/commandSink.js +3 -2
- package/dist/defineGame.d.ts +12 -5
- package/dist/defineGame.js +25 -10
- package/dist/devtools/DevtoolsOverlay.d.ts +2 -69
- package/dist/devtools/DevtoolsOverlay.js +14 -168
- package/dist/devtools/DevtoolsRuntime.d.ts +7 -0
- package/dist/devtools/DevtoolsRuntime.js +61 -0
- package/dist/devtools/agentBridge.js +1 -1
- package/dist/devtools/devtoolsReports.d.ts +68 -0
- package/dist/devtools/devtoolsReports.js +100 -0
- package/dist/devtools/latencyInstrumentation.d.ts +3 -0
- package/dist/devtools/latencyInstrumentation.js +11 -0
- package/dist/devtools/playtestProbe.d.ts +4 -0
- package/dist/devtools/playtestProbe.js +22 -0
- package/dist/drivers/FrameDriver.js +25 -21
- package/dist/drivers/HudOnlyDriver.d.ts +6 -1
- package/dist/drivers/HudOnlyDriver.js +32 -14
- package/dist/environment/EnvironmentScene.js +19 -15
- package/dist/environment/terrainGroundColor.d.ts +5 -4
- package/dist/environment/terrainGroundColor.js +19 -8
- package/dist/inputSink.d.ts +6 -6
- package/dist/inputSink.js +60 -21
- package/dist/lazyShell3dPresentation.d.ts +9 -0
- package/dist/lazyShell3dPresentation.js +24 -0
- package/dist/materialOverride.d.ts +42 -0
- package/dist/materialOverride.js +95 -36
- package/dist/multiplayer.d.ts +33 -3
- package/dist/multiplayer.js +105 -22
- package/dist/pointer/pointerService.d.ts +2 -0
- package/dist/pointer/pointerService.js +12 -2
- package/dist/postfx/PostProcessing.d.ts +10 -0
- package/dist/postfx/PostProcessing.js +9 -41
- package/dist/postfx/gradeShader.d.ts +1 -1
- package/dist/postfx/gradeShader.js +30 -2
- package/dist/postfx/shadowPrepass.d.ts +3 -0
- package/dist/postfx/shadowPrepass.js +18 -0
- package/dist/presentationRecovery.d.ts +75 -0
- package/dist/presentationRecovery.js +204 -0
- package/dist/presentationRecovery3d.d.ts +4 -0
- package/dist/presentationRecovery3d.js +32 -0
- package/dist/presentationResolve.d.ts +8 -1
- package/dist/presentationResolve.js +5 -2
- package/dist/render/CascadedShadows.d.ts +7 -2
- package/dist/render/CascadedShadows.js +98 -20
- package/dist/render/EnvironmentLighting.js +74 -35
- package/dist/render/HairCards.d.ts +7 -0
- package/dist/render/HairCards.js +23 -0
- package/dist/render/SceneModels.js +51 -77
- package/dist/render/authoredSurfaceMaterial.d.ts +43 -0
- package/dist/render/authoredSurfaceMaterial.js +108 -0
- package/dist/render/materialAppearanceSignals.d.ts +23 -0
- package/dist/render/materialAppearanceSignals.js +58 -0
- package/dist/render/materialAsset.d.ts +98 -0
- package/dist/render/materialAsset.js +454 -0
- package/dist/render/measureBounds.js +16 -3
- package/dist/render/measureCollisionMesh.js +8 -1
- package/dist/render/modelAssets.d.ts +8 -0
- package/dist/render/modelAssets.js +43 -0
- package/dist/render/modelBindPose.d.ts +5 -0
- package/dist/render/modelBindPose.js +35 -0
- package/dist/render/modelLoad.d.ts +3 -0
- package/dist/render/modelLoad.js +9 -0
- package/dist/render/modelNodeVisibility.d.ts +6 -0
- package/dist/render/modelNodeVisibility.js +18 -0
- package/dist/render/modelRender.d.ts +18 -1
- package/dist/render/modelRender.js +81 -3
- package/dist/render/projectedOverdraw.d.ts +35 -0
- package/dist/render/projectedOverdraw.js +204 -0
- package/dist/render/resolveModel.d.ts +12 -2
- package/dist/render/resolveModel.js +21 -4
- package/dist/render/rigNode.d.ts +26 -0
- package/dist/render/rigNode.js +32 -0
- package/dist/render/spriteRender.d.ts +4 -0
- package/dist/render/spriteRender.js +5 -0
- package/dist/render/staticShapeInstances.d.ts +34 -0
- package/dist/render/staticShapeInstances.js +78 -0
- package/dist/render/useFootIk.js +76 -11
- package/dist/render/useModelAnimation.d.ts +28 -2
- package/dist/render/useModelAnimation.js +194 -69
- package/dist/render/useModelInstance.d.ts +25 -0
- package/dist/render/useModelInstance.js +43 -0
- package/dist/scatter/scatterModels.d.ts +3 -2
- package/dist/scatter/scatterModels.js +15 -7
- package/dist/scene/AuthoredScene.d.ts +14 -6
- package/dist/scene/AuthoredScene.js +30 -9
- package/dist/scene/GeneratedAssetRenderer.js +11 -42
- package/dist/scene/GrassFieldRenderer.js +20 -2
- package/dist/settings/SettingsMenu.js +11 -18
- package/dist/settings/SettingsRuntime.d.ts +5 -0
- package/dist/settings/SettingsRuntime.js +29 -2
- package/dist/shellControlSuspension.d.ts +18 -0
- package/dist/shellControlSuspension.js +28 -0
- package/dist/shellInputPublication.d.ts +28 -0
- package/dist/shellInputPublication.js +49 -0
- package/dist/shellPointerInput.d.ts +18 -0
- package/dist/shellPointerInput.js +49 -0
- package/dist/structures/BuildingKitBatch.d.ts +3 -1
- package/dist/structures/BuildingKitBatch.js +34 -59
- package/dist/structures/BuildingRenderBudget.d.ts +23 -0
- package/dist/structures/BuildingRenderBudget.js +54 -0
- package/dist/structures/GeneratedBuilding.js +13 -8
- package/dist/structures/buildingKitMaterials.d.ts +20 -0
- package/dist/structures/buildingKitMaterials.js +70 -0
- package/dist/structures/buildingSpatialBatch.d.ts +11 -0
- package/dist/structures/buildingSpatialBatch.js +45 -0
- package/dist/terrain/CarvedTerrain.d.ts +2 -1
- package/dist/terrain/CarvedTerrain.js +2 -2
- package/dist/terrain/GrassField.d.ts +3 -1
- package/dist/terrain/GrassField.js +61 -6
- package/dist/terrain/grassGeometry.d.ts +13 -0
- package/dist/terrain/grassGeometry.js +57 -1
- package/dist/terrain/grassMaterial.js +2 -2
- package/dist/terrain/index.d.ts +1 -0
- package/dist/terrain/index.js +1 -0
- package/dist/terrain/terrainDetailMaterial.d.ts +3 -3
- package/dist/terrain/terrainDetailMaterial.js +7 -7
- package/dist/terrain/terrainMath.d.ts +2 -0
- package/dist/terrain/terrainMath.js +2 -0
- package/dist/terrain/terrainPaintColor.d.ts +11 -0
- package/dist/terrain/terrainPaintColor.js +47 -0
- package/dist/touch/TouchControlsOverlay.js +4 -4
- package/dist/useShellMultiplayerSync.js +4 -0
- package/dist/vfx/ParticleField.d.ts +23 -20
- package/dist/vfx/ParticleField.js +168 -50
- package/dist/visibility/CullingProvider.js +2 -62
- package/dist/visibility/cullingDriver.d.ts +9 -0
- package/dist/visibility/cullingDriver.js +68 -0
- package/dist/weather/AuthoredWeatherLayer.d.ts +16 -0
- package/dist/weather/AuthoredWeatherLayer.js +31 -0
- package/dist/weather/DustField.d.ts +2 -1
- package/dist/weather/DustField.js +17 -3
- package/dist/weather/FireSpreadLayer.d.ts +4 -2
- package/dist/weather/FireSpreadLayer.js +5 -11
- package/dist/weather/LightningStrike.d.ts +4 -1
- package/dist/weather/LightningStrike.js +13 -17
- package/dist/weather/RainField.d.ts +2 -1
- package/dist/weather/RainField.js +17 -3
- package/dist/weather/RainImpactField.d.ts +16 -0
- package/dist/weather/RainImpactField.js +89 -0
- package/dist/weather/SnowField.d.ts +2 -1
- package/dist/weather/SnowField.js +17 -3
- package/dist/weather/WeatherLayer.d.ts +11 -2
- package/dist/weather/WeatherLayer.js +30 -6
- package/dist/weather/index.d.ts +2 -0
- package/dist/weather/index.js +2 -0
- package/dist/weather/weatherMath.d.ts +2 -0
- package/dist/weather/weatherMath.js +8 -0
- package/dist/weather/weatherShelter.d.ts +18 -0
- package/dist/weather/weatherShelter.js +75 -0
- package/dist/weather/weatherUniforms.d.ts +40 -1
- package/dist/weather/weatherUniforms.js +29 -9
- package/dist/world/AuthoredFlocks.d.ts +22 -0
- package/dist/world/AuthoredFlocks.js +87 -0
- package/dist/world/StaticObjectModels.d.ts +8 -0
- package/dist/world/StaticObjectModels.js +75 -0
- package/dist/world/WorldHud.d.ts +14 -6
- package/dist/world/WorldHud.js +25 -12
- package/dist/world/WorldParticles.d.ts +10 -23
- package/dist/world/WorldParticles.js +209 -61
- package/dist/world/WorldScene.d.ts +3 -1
- package/dist/world/WorldScene.js +41 -26
- package/dist/world/staticObjectBatches.d.ts +35 -0
- package/dist/world/staticObjectBatches.js +163 -0
- package/dist/world/worldBarSamples.d.ts +37 -33
- package/dist/world/worldBarSamples.js +117 -75
- package/package.json +4 -4
- package/skills/ce-handoff/SKILL.md +1 -1
- package/skills/game-design/SKILL.md +2 -0
- package/skills/jgengine/SKILL.md +23 -2
- package/skills/jgengine/api.md +28 -17
- package/skills/jgengine/recipes/minimal-game.md +7 -2
- package/skills/jgengine-ui/SKILL.md +3 -1
- package/skills/jgengine-ui/api.md +219 -70
- package/skills/jgengine-ui/capabilities.md +119 -1
- package/skills/jgengine-ui/reference-primitives.md +93 -3
- package/skills/jgengine-ui/reference.md +2 -0
- package/skills/jgengine-world/SKILL.md +12 -3
- package/skills/jgengine-world/api.md +201 -58
- package/skills/jgengine-world/capabilities.md +116 -3
- package/skills/jgengine-world/recipes/character-animation.md +31 -8
- package/skills/jgengine-world/recipes/controls.md +4 -0
- package/skills/jgengine-world/recipes/npc-senses.md +12 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +3 -1
- package/skills/jgengine-world/reference.md +167 -5
- package/skills/jgengine-world/visibility.md +28 -1
package/dist/world/WorldHud.d.ts
CHANGED
|
@@ -5,15 +5,19 @@ import { type NameplateSample } from "./worldBarSamples.js";
|
|
|
5
5
|
export type { NameplateSample, WorldBarSample } from "./worldBarSamples.js";
|
|
6
6
|
export { collectNameplateSamples, collectWorldBarSamples, paintWorldBarSamples } from "./worldBarSamples.js";
|
|
7
7
|
export { telegraphPulseOpacity } from "./telegraphPulse.js";
|
|
8
|
-
export declare function WorldEntityBars({ statId, height, roles, resolveRole, maxDistance, occlude, }: {
|
|
8
|
+
export declare function WorldEntityBars({ statId, height, roles, resolveRole, maxDistance, occlude, tickMs, maxSamples, }: {
|
|
9
9
|
statId: string;
|
|
10
10
|
height?: number;
|
|
11
11
|
roles?: readonly CatalogEntityRole[];
|
|
12
12
|
resolveRole?: (entity: SceneEntity) => CatalogEntityRole | undefined;
|
|
13
|
-
/** Hide bars
|
|
13
|
+
/** Hide bars beyond this camera distance in world units. Default 60. */
|
|
14
14
|
maxDistance?: number;
|
|
15
|
-
/** Hide
|
|
15
|
+
/** Hide bars behind blocking geometry from the render camera. Default true; false explicitly reveals through walls. */
|
|
16
16
|
occlude?: boolean;
|
|
17
|
+
/** Minimum visibility/health refresh interval in ms; anchors reproject every frame. Default 80. */
|
|
18
|
+
tickMs?: number;
|
|
19
|
+
/** Maximum overlays and occlusion rays per refresh. Default 64, capped at 256. */
|
|
20
|
+
maxSamples?: number;
|
|
17
21
|
}): import("react").JSX.Element;
|
|
18
22
|
/** Props for `WorldNameplates` — entity filter, refresh rate, and headless className/render hooks. */
|
|
19
23
|
export interface WorldNameplatesProps {
|
|
@@ -21,12 +25,16 @@ export interface WorldNameplatesProps {
|
|
|
21
25
|
height?: number;
|
|
22
26
|
roles?: readonly CatalogEntityRole[];
|
|
23
27
|
resolveRole?: (entity: SceneEntity) => CatalogEntityRole | undefined;
|
|
24
|
-
/** Hide nameplates
|
|
28
|
+
/** Hide nameplates beyond this camera distance in world units. Default 40. */
|
|
25
29
|
maxDistance?: number;
|
|
26
|
-
/** Hide
|
|
30
|
+
/** Hide plates behind blocking geometry from the render camera. Default true; false explicitly reveals through walls. */
|
|
27
31
|
occlude?: boolean;
|
|
28
32
|
/** Minimum ms between position/health refreshes — trades smoothness for fewer re-renders at scale. Default 120. */
|
|
29
33
|
tickMs?: number;
|
|
34
|
+
/** Maximum overlays and occlusion rays per refresh. Default 64, capped at 256. */
|
|
35
|
+
maxSamples?: number;
|
|
36
|
+
/** Authored display name. Return null to omit a plate; default suppresses machine identifiers. */
|
|
37
|
+
resolveName?: (entity: SceneEntity) => string | null;
|
|
30
38
|
/** Draw the built-in HP bar under the name. Set `false` for a name-only plate when the game already draws its own health bar (its own HUD, or `worldHealthBars`). Default true. */
|
|
31
39
|
showHealth?: boolean;
|
|
32
40
|
className?: string;
|
|
@@ -45,7 +53,7 @@ export interface WorldNameplatesProps {
|
|
|
45
53
|
*
|
|
46
54
|
* @capability nameplates billboarded name + HP bar over nearby entities
|
|
47
55
|
*/
|
|
48
|
-
export declare function WorldNameplates({ statId, height, roles, resolveRole, maxDistance, occlude, tickMs, showHealth, className, nameplateClassName, nameClassName, barClassName, fillClassName, renderNameplate, }: WorldNameplatesProps): import("react").JSX.Element;
|
|
56
|
+
export declare function WorldNameplates({ statId, height, roles, resolveRole, maxDistance, occlude, tickMs, maxSamples, resolveName, showHealth, className, nameplateClassName, nameClassName, barClassName, fillClassName, renderNameplate, }: WorldNameplatesProps): import("react").JSX.Element;
|
|
49
57
|
/** Props for {@link WorldObjectHighlights}. */
|
|
50
58
|
export interface WorldObjectHighlightsProps {
|
|
51
59
|
/** Ring tint. Default a construction-select amber. */
|
package/dist/world/WorldHud.js
CHANGED
|
@@ -9,14 +9,16 @@ import { useCameraShake } from "../camera/shakeChannel.js";
|
|
|
9
9
|
import { CALIBRATED_TRAUMA_SHAKE_DECAY_PER_SECOND } from "../camera/rigMath.js";
|
|
10
10
|
import { readFirstPersonMuzzle } from "../camera/GameFirstPersonCamera.js";
|
|
11
11
|
import { resolveFloatTextStyle } from "./floatTextStyle.js";
|
|
12
|
-
import { collectNameplateSamples, collectWorldBarSamples, paintWorldBarSamples, } from "./worldBarSamples.js";
|
|
12
|
+
import { collectNameplateSamples, collectWorldBarSamples, paintWorldBarSamples, refreshWorldBarSamples, } from "./worldBarSamples.js";
|
|
13
13
|
import { telegraphPulseOpacity } from "./telegraphPulse.js";
|
|
14
|
+
import { useRenderVisibility } from "../visibility/CullingProvider.js";
|
|
15
|
+
import { CAMERA_POST_FRAME_PRIORITY } from "../camera/cameraRigs.js";
|
|
14
16
|
export { collectNameplateSamples, collectWorldBarSamples, paintWorldBarSamples } from "./worldBarSamples.js";
|
|
15
17
|
export { telegraphPulseOpacity } from "./telegraphPulse.js";
|
|
16
18
|
function pinOverlayToViewport(_el, _camera, size) {
|
|
17
19
|
return [size.width / 2, size.height / 2];
|
|
18
20
|
}
|
|
19
|
-
export function WorldEntityBars({ statId, height = 2.2, roles, resolveRole, maxDistance = 60, occlude =
|
|
21
|
+
export function WorldEntityBars({ statId, height = 2.2, roles, resolveRole, maxDistance = 60, occlude = true, tickMs = 80, maxSamples = 64, }) {
|
|
20
22
|
const ctx = useGameContext();
|
|
21
23
|
const camera = useThree((state) => state.camera);
|
|
22
24
|
const size = useThree((state) => state.size);
|
|
@@ -24,7 +26,10 @@ export function WorldEntityBars({ statId, height = 2.2, roles, resolveRole, maxD
|
|
|
24
26
|
const canvasRef = useRef(null);
|
|
25
27
|
const samplesRef = useRef([]);
|
|
26
28
|
const projectRef = useRef(new THREE.Vector3());
|
|
27
|
-
|
|
29
|
+
const lastTickRef = useRef(-Infinity);
|
|
30
|
+
const visibility = useRenderVisibility();
|
|
31
|
+
useFrame((state) => {
|
|
32
|
+
const nowMs = state.clock.elapsedTime * 1000;
|
|
28
33
|
const canvas = canvasRef.current;
|
|
29
34
|
if (canvas === null)
|
|
30
35
|
return;
|
|
@@ -38,9 +43,15 @@ export function WorldEntityBars({ statId, height = 2.2, roles, resolveRole, maxD
|
|
|
38
43
|
canvas.height = pixelH;
|
|
39
44
|
}
|
|
40
45
|
camera.updateMatrixWorld();
|
|
41
|
-
|
|
46
|
+
if (nowMs - lastTickRef.current >= Math.max(0, tickMs)) {
|
|
47
|
+
lastTickRef.current = nowMs;
|
|
48
|
+
collectWorldBarSamples(ctx, statId, height, roles, resolveRole, camera, { width: cssW, height: cssH }, samplesRef.current, projectRef.current, maxDistance, occlude, { maxSamples, isVisible: visibility.current });
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
refreshWorldBarSamples(ctx, height, camera, { width: cssW, height: cssH }, samplesRef.current, projectRef.current, maxDistance, { isVisible: visibility.current });
|
|
52
|
+
}
|
|
42
53
|
paintWorldBarSamples(canvas, samplesRef.current, dpr);
|
|
43
|
-
});
|
|
54
|
+
}, CAMERA_POST_FRAME_PRIORITY + 0.01);
|
|
44
55
|
return (_jsx(Html, { fullscreen: true, calculatePosition: pinOverlayToViewport, zIndexRange: [20, 0], style: { pointerEvents: "none" }, children: _jsx("canvas", { ref: canvasRef, style: { width: "100%", height: "100%", display: "block", pointerEvents: "none" } }) }));
|
|
45
56
|
}
|
|
46
57
|
/**
|
|
@@ -51,23 +62,24 @@ export function WorldEntityBars({ statId, height = 2.2, roles, resolveRole, maxD
|
|
|
51
62
|
*
|
|
52
63
|
* @capability nameplates billboarded name + HP bar over nearby entities
|
|
53
64
|
*/
|
|
54
|
-
export function WorldNameplates({ statId = "health", height = 2.3, roles, resolveRole, maxDistance = 40, occlude =
|
|
65
|
+
export function WorldNameplates({ statId = "health", height = 2.3, roles, resolveRole, maxDistance = 40, occlude = true, tickMs = 120, maxSamples = 64, resolveName, showHealth = true, className, nameplateClassName, nameClassName, barClassName, fillClassName, renderNameplate, }) {
|
|
55
66
|
const ctx = useGameContext();
|
|
56
67
|
const camera = useThree((state) => state.camera);
|
|
57
68
|
const size = useThree((state) => state.size);
|
|
58
69
|
const [samples, setSamples] = useState([]);
|
|
59
70
|
const samplesRef = useRef([]);
|
|
60
71
|
const projectRef = useRef(new THREE.Vector3());
|
|
61
|
-
const lastTickRef = useRef(
|
|
72
|
+
const lastTickRef = useRef(-Infinity);
|
|
73
|
+
const visibility = useRenderVisibility();
|
|
62
74
|
useFrame((state) => {
|
|
63
75
|
const nowMs = state.clock.elapsedTime * 1000;
|
|
64
|
-
if (nowMs - lastTickRef.current < tickMs)
|
|
76
|
+
if (nowMs - lastTickRef.current < Math.max(0, tickMs))
|
|
65
77
|
return;
|
|
66
78
|
lastTickRef.current = nowMs;
|
|
67
79
|
camera.updateMatrixWorld();
|
|
68
|
-
collectNameplateSamples(ctx, statId, height, roles, resolveRole, camera, { width: size.width, height: size.height }, samplesRef.current, projectRef.current, maxDistance, occlude);
|
|
80
|
+
collectNameplateSamples(ctx, statId, height, roles, resolveRole, camera, { width: size.width, height: size.height }, samplesRef.current, projectRef.current, maxDistance, occlude, { maxSamples, isVisible: visibility.current, ...(resolveName === undefined ? {} : { resolveName }) });
|
|
69
81
|
setSamples([...samplesRef.current]);
|
|
70
|
-
});
|
|
82
|
+
}, CAMERA_POST_FRAME_PRIORITY + 0.01);
|
|
71
83
|
return (_jsx(Html, { fullscreen: true, calculatePosition: pinOverlayToViewport, zIndexRange: [19, 0], style: { pointerEvents: "none" }, children: _jsx("div", { className: className, "data-nameplates": true, children: samples.map((sample) => renderNameplate !== undefined ? (_jsx("div", { style: { position: "absolute", left: sample.x, top: sample.y }, children: renderNameplate(sample) }, sample.id)) : (_jsxs("div", { className: nameplateClassName, "data-nameplate": sample.id, style: {
|
|
72
84
|
position: "absolute",
|
|
73
85
|
left: sample.x,
|
|
@@ -233,6 +245,7 @@ export function CombatCameraShake() {
|
|
|
233
245
|
}
|
|
234
246
|
export function ProjectileTracers({ lifeMs = 130 }) {
|
|
235
247
|
const ctx = useGameContext();
|
|
248
|
+
const camera = useThree((state) => state.camera);
|
|
236
249
|
const [tracers, setTracers] = useState([]);
|
|
237
250
|
const nextId = useRef(0);
|
|
238
251
|
const timers = useRef(new Set());
|
|
@@ -246,7 +259,7 @@ export function ProjectileTracers({ lifeMs = 130 }) {
|
|
|
246
259
|
const id = nextId.current++;
|
|
247
260
|
const start = new THREE.Vector3(event.origin[0], event.origin[1], event.origin[2]);
|
|
248
261
|
if (event.from === ctx.player.userId)
|
|
249
|
-
readFirstPersonMuzzle(start);
|
|
262
|
+
readFirstPersonMuzzle(start, camera);
|
|
250
263
|
setTracers((current) => [
|
|
251
264
|
...current,
|
|
252
265
|
{
|
|
@@ -266,7 +279,7 @@ export function ProjectileTracers({ lifeMs = 130 }) {
|
|
|
266
279
|
clearTimeout(handle);
|
|
267
280
|
timers.current.clear();
|
|
268
281
|
};
|
|
269
|
-
}, [ctx, lifeMs]);
|
|
282
|
+
}, [ctx, camera, lifeMs]);
|
|
270
283
|
return (_jsx(_Fragment, { children: tracers.map((tracer) => (_jsx(Line, { points: tracer.points, color: "#fde68a", lineWidth: 2, transparent: true, opacity: 0.85 }, tracer.id))) }));
|
|
271
284
|
}
|
|
272
285
|
export function Reticle({ className }) {
|
|
@@ -1,31 +1,18 @@
|
|
|
1
1
|
import { type ReactElement } from "react";
|
|
2
2
|
import type { GraphicsQuality } from "@jgengine/core/settings/settingsModel";
|
|
3
|
+
import { type Vec3 } from "@jgengine/core/vfx/particles";
|
|
3
4
|
import type { ParticleEmitterSpec } from "@jgengine/core/vfx/particleDirector";
|
|
4
|
-
/**
|
|
5
|
-
* The per-effect particle pool a graphics tier allows: the requested `max`
|
|
6
|
-
* clamped to the tier cap, so lower tiers degrade density instead of dropping
|
|
7
|
-
* effects entirely.
|
|
8
|
-
*/
|
|
5
|
+
/** The requested visual pool clamped to the graphics tier. */
|
|
9
6
|
export declare function resolveParticleBudget(quality: GraphicsQuality, requested: number | undefined, particleCap?: number): number;
|
|
10
|
-
/**
|
|
11
|
-
* Whether a spec change touches something the JSX reads (blending, static offset,
|
|
12
|
-
* follow binding) and therefore needs a React re-render — config-only retunes are
|
|
13
|
-
* applied imperatively to the live system and must stay render-free.
|
|
14
|
-
*
|
|
15
|
-
* @internal WorldParticles render-boundary helper, exported for its regression test.
|
|
16
|
-
*/
|
|
7
|
+
/** @internal Config retunes stay imperative; output changes require React rendering. */
|
|
17
8
|
export declare function specNeedsRender(prev: ParticleEmitterSpec, next: ParticleEmitterSpec): boolean;
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
* {@link resolveParticleBudget}.
|
|
25
|
-
*
|
|
26
|
-
* @internal
|
|
27
|
-
*/
|
|
28
|
-
export declare function WorldParticles({ quality, particleCap }: {
|
|
9
|
+
/** @internal Transform a simulation vector through a followed yaw and origin. */
|
|
10
|
+
export declare function transformParticleVector(point: Vec3, origin: Vec3, yaw: number, inverse?: boolean, out?: [number, number, number]): Vec3;
|
|
11
|
+
/** @internal Cosmetic catch-up follows game time, with bounded work after suspension. */
|
|
12
|
+
export declare function resolveParticleDelta(previousTime: number, gameTime: number): number;
|
|
13
|
+
/** Renders the existing director and pooled simulator; named bursts reuse the standing pool. @internal */
|
|
14
|
+
export declare function WorldParticles({ quality, particleCap, diagnostics }: {
|
|
29
15
|
quality: GraphicsQuality;
|
|
30
16
|
particleCap?: number;
|
|
17
|
+
diagnostics?: boolean;
|
|
31
18
|
}): ReactElement;
|
|
@@ -1,91 +1,183 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useFrame } from "@react-three/fiber";
|
|
3
3
|
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { devtools } from "@jgengine/core/devtools/devtools";
|
|
4
5
|
import { createParticleSystem } from "@jgengine/core/vfx/particles";
|
|
5
6
|
import { useGameContext } from "@jgengine/react/provider";
|
|
6
7
|
import { ParticleField } from "../vfx/ParticleField.js";
|
|
7
8
|
const QUALITY_POOL_CAP = { low: 128, medium: 256, high: 512, ultra: 1024 };
|
|
8
|
-
/**
|
|
9
|
-
* The per-effect particle pool a graphics tier allows: the requested `max`
|
|
10
|
-
* clamped to the tier cap, so lower tiers degrade density instead of dropping
|
|
11
|
-
* effects entirely.
|
|
12
|
-
*/
|
|
9
|
+
/** The requested visual pool clamped to the graphics tier. */
|
|
13
10
|
export function resolveParticleBudget(quality, requested, particleCap) {
|
|
14
|
-
const
|
|
15
|
-
|
|
11
|
+
const rawCap = particleCap ?? QUALITY_POOL_CAP[quality];
|
|
12
|
+
const cap = Number.isFinite(rawCap) ? Math.max(1, Math.floor(rawCap)) : QUALITY_POOL_CAP[quality];
|
|
13
|
+
return Number.isFinite(requested) ? Math.max(1, Math.floor(Math.min(requested, cap))) : cap;
|
|
16
14
|
}
|
|
17
15
|
const MAX_CONCURRENT_BURSTS = 32;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
* follow binding) and therefore needs a React re-render — config-only retunes are
|
|
21
|
-
* applied imperatively to the live system and must stay render-free.
|
|
22
|
-
*
|
|
23
|
-
* @internal WorldParticles render-boundary helper, exported for its regression test.
|
|
24
|
-
*/
|
|
16
|
+
const ZERO = [0, 0, 0];
|
|
17
|
+
/** @internal Config retunes stay imperative; output changes require React rendering. */
|
|
25
18
|
export function specNeedsRender(prev, next) {
|
|
26
|
-
if (prev.blending !== next.blending || prev.follow !== next.follow)
|
|
19
|
+
if (prev.blending !== next.blending || prev.follow !== next.follow || prev.space !== next.space)
|
|
27
20
|
return true;
|
|
28
|
-
|
|
29
|
-
|
|
21
|
+
if (prev.render?.shape !== next.render?.shape || prev.render?.stretch !== next.render?.stretch)
|
|
22
|
+
return true;
|
|
23
|
+
const a = prev.offset, b = next.offset;
|
|
30
24
|
if (a === b)
|
|
31
25
|
return false;
|
|
32
26
|
if (a === undefined || b === undefined)
|
|
33
27
|
return true;
|
|
34
28
|
return a[0] !== b[0] || a[1] !== b[1] || a[2] !== b[2];
|
|
35
29
|
}
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
*
|
|
43
|
-
|
|
44
|
-
*
|
|
45
|
-
|
|
46
|
-
|
|
30
|
+
/** @internal Transform a simulation vector through a followed yaw and origin. */
|
|
31
|
+
export function transformParticleVector(point, origin, yaw, inverse = false, out = [0, 0, 0]) {
|
|
32
|
+
const c = Math.cos(yaw), s = Math.sin(yaw);
|
|
33
|
+
const x = inverse ? point[0] - origin[0] : point[0];
|
|
34
|
+
const y = inverse ? point[1] - origin[1] : point[1];
|
|
35
|
+
const z = inverse ? point[2] - origin[2] : point[2];
|
|
36
|
+
out[0] = inverse ? c * x - s * z : origin[0] + c * x + s * z;
|
|
37
|
+
out[1] = inverse ? y : origin[1] + y;
|
|
38
|
+
out[2] = inverse ? s * x + c * z : origin[2] - s * x + c * z;
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
/** @internal Cosmetic catch-up follows game time, with bounded work after suspension. */
|
|
42
|
+
export function resolveParticleDelta(previousTime, gameTime) {
|
|
43
|
+
return Number.isFinite(previousTime) && Number.isFinite(gameTime) ? Math.max(0, Math.min(0.1, gameTime - previousTime)) : 0;
|
|
44
|
+
}
|
|
45
|
+
/** Renders the existing director and pooled simulator; named bursts reuse the standing pool. @internal */
|
|
46
|
+
export function WorldParticles({ quality, particleCap, diagnostics = false }) {
|
|
47
47
|
const ctx = useGameContext();
|
|
48
48
|
const emittersRef = useRef(new Map());
|
|
49
49
|
const burstsRef = useRef([]);
|
|
50
|
+
const previousTime = useRef(ctx.time.now());
|
|
51
|
+
const timing = useRef({ poolsCreated: 0, poolsRetired: 0, updateSamples: 0, lastUpdateMs: 0, totalUpdateMs: 0, maxUpdateMs: 0, droppedCatchupSeconds: 0 });
|
|
52
|
+
const resources = useRef({ fieldsMounted: 0, fieldsUnmounted: 0, liveFields: 0, attributeBytes: 0, totalUploadMs: 0, maxUploadMs: 0, uploadSamples: 0 });
|
|
50
53
|
const [, setGeneration] = useState(0);
|
|
54
|
+
function pose(spec) {
|
|
55
|
+
const offset = spec.offset ?? [0, 0, 0];
|
|
56
|
+
const entity = spec.follow === undefined ? undefined : ctx.scene.entity.get(spec.follow);
|
|
57
|
+
if (entity === undefined || entity === null)
|
|
58
|
+
return { position: offset, yaw: 0, found: spec.follow === undefined };
|
|
59
|
+
if (spec.space === "local")
|
|
60
|
+
return { position: transformParticleVector(offset, entity.position, entity.rotationY), yaw: entity.rotationY, found: true };
|
|
61
|
+
return { position: [entity.position[0] + offset[0], entity.position[1] + offset[1], entity.position[2] + offset[2]], yaw: 0, found: true };
|
|
62
|
+
}
|
|
63
|
+
function syncPose(instance) {
|
|
64
|
+
const resolved = pose(instance.spec);
|
|
65
|
+
instance.pose = resolved;
|
|
66
|
+
if (instance.spec.space === "local" && resolved.found) {
|
|
67
|
+
instance.group?.position.set(...resolved.position);
|
|
68
|
+
if (instance.group !== null)
|
|
69
|
+
instance.group.rotation.y = resolved.yaw;
|
|
70
|
+
}
|
|
71
|
+
else if (instance.spec.space !== "local") {
|
|
72
|
+
const base = instance.spec.follow === undefined ? instance.spec.config.position ?? [0, 0, 0] : [0, 0, 0];
|
|
73
|
+
instance.system.configure({ position: [resolved.position[0] + base[0], resolved.position[1] + base[1], resolved.position[2] + base[2]] });
|
|
74
|
+
}
|
|
75
|
+
if (resolved.found && instance.spec.active !== false)
|
|
76
|
+
instance.system.start();
|
|
77
|
+
else
|
|
78
|
+
instance.system.stop();
|
|
79
|
+
}
|
|
80
|
+
function collisionFor(spec, frozenPose) {
|
|
81
|
+
return (from, to) => {
|
|
82
|
+
const current = spec();
|
|
83
|
+
const local = current.space === "local";
|
|
84
|
+
const resolved = frozenPose ?? emittersRef.current.get(current.id)?.pose ?? pose(current);
|
|
85
|
+
const start = local ? transformParticleVector(from, resolved.position, resolved.yaw) : from;
|
|
86
|
+
const end = local ? transformParticleVector(to, resolved.position, resolved.yaw) : to;
|
|
87
|
+
const direction = [end[0] - start[0], end[1] - start[1], end[2] - start[2]];
|
|
88
|
+
const maxDistance = Math.hypot(...direction);
|
|
89
|
+
if (maxDistance === 0)
|
|
90
|
+
return null;
|
|
91
|
+
const hit = ctx.scene.raycast({ origin: start, direction, maxDistance,
|
|
92
|
+
filter: { entities: false, objects: true, terrain: true, walls: true } });
|
|
93
|
+
if (hit === null)
|
|
94
|
+
return null;
|
|
95
|
+
return { position: local ? transformParticleVector(hit.point, resolved.position, resolved.yaw, true) : hit.point,
|
|
96
|
+
normal: local ? transformParticleVector(hit.normal, [0, 0, 0], resolved.yaw, true) : hit.normal };
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function accelerationFor(spec, frozenPose) {
|
|
100
|
+
const worldScratch = [0, 0, 0];
|
|
101
|
+
const forceScratch = [0, 0, 0];
|
|
102
|
+
return (position, _velocity, out) => {
|
|
103
|
+
const current = spec();
|
|
104
|
+
const influence = current.environment;
|
|
105
|
+
if (influence === undefined) {
|
|
106
|
+
out[0] = out[1] = out[2] = 0;
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
const resolved = frozenPose ?? emittersRef.current.get(current.id)?.pose ?? pose(current);
|
|
110
|
+
const local = current.space === "local";
|
|
111
|
+
const world = local ? transformParticleVector(position, resolved.position, resolved.yaw, false, worldScratch) : position;
|
|
112
|
+
const force = ctx.environment.accelerationAt(world, ctx.time.now(), influence.windResponse ?? 0, influence.maxAcceleration ?? 100, influence.mask ?? 0xffffffff, forceScratch);
|
|
113
|
+
const result = local ? transformParticleVector(force, ZERO, resolved.yaw, true, forceScratch) : force;
|
|
114
|
+
out[0] = result[0];
|
|
115
|
+
out[1] = result[1];
|
|
116
|
+
out[2] = result[2];
|
|
117
|
+
return out;
|
|
118
|
+
};
|
|
119
|
+
}
|
|
51
120
|
useEffect(() => {
|
|
52
121
|
const emitters = emittersRef.current;
|
|
53
122
|
function sync() {
|
|
54
123
|
let changed = false;
|
|
55
|
-
const specs = ctx.particles.emitters();
|
|
56
124
|
const liveIds = new Set();
|
|
57
|
-
for (const spec of
|
|
125
|
+
for (const spec of ctx.particles.emitters()) {
|
|
58
126
|
liveIds.add(spec.id);
|
|
59
127
|
const poolMax = resolveParticleBudget(quality, spec.config.max, particleCap);
|
|
60
128
|
const current = emitters.get(spec.id);
|
|
61
129
|
if (current === undefined || current.poolMax !== poolMax) {
|
|
62
|
-
|
|
63
|
-
spec,
|
|
64
|
-
|
|
65
|
-
system
|
|
66
|
-
|
|
130
|
+
const next = { spec, poolMax, group: current?.group ?? null, pose: pose(spec),
|
|
131
|
+
system: createParticleSystem({ ...spec.config, max: poolMax, seed: spec.config.seed ?? spec.id }, { collision: collisionFor(() => next.spec), acceleration: accelerationFor(() => next.spec) }) };
|
|
132
|
+
if (current !== undefined) {
|
|
133
|
+
next.system.restore(current.system.snapshot());
|
|
134
|
+
next.system.configure({ ...spec.config, max: poolMax, seed: spec.config.seed ?? spec.id }, true);
|
|
135
|
+
}
|
|
136
|
+
timing.current.poolsCreated++;
|
|
137
|
+
if (current !== undefined)
|
|
138
|
+
timing.current.poolsRetired++;
|
|
139
|
+
emitters.set(spec.id, next);
|
|
140
|
+
syncPose(next);
|
|
67
141
|
changed = true;
|
|
68
142
|
}
|
|
69
143
|
else if (current.spec !== spec) {
|
|
70
144
|
if (specNeedsRender(current.spec, spec))
|
|
71
145
|
changed = true;
|
|
72
|
-
current.system.configure({ ...spec.config, max: poolMax });
|
|
146
|
+
current.system.configure({ ...spec.config, max: poolMax, seed: spec.config.seed ?? spec.id }, true);
|
|
73
147
|
current.spec = spec;
|
|
148
|
+
syncPose(current);
|
|
74
149
|
}
|
|
75
150
|
}
|
|
76
151
|
for (const id of emitters.keys()) {
|
|
77
152
|
if (liveIds.has(id))
|
|
78
153
|
continue;
|
|
79
154
|
emitters.delete(id);
|
|
155
|
+
timing.current.poolsRetired++;
|
|
80
156
|
changed = true;
|
|
81
157
|
}
|
|
82
158
|
for (const burst of ctx.particles.drainBursts()) {
|
|
159
|
+
const named = burst.emitterId === undefined ? undefined : emitters.get(burst.emitterId);
|
|
160
|
+
if (named !== undefined) {
|
|
161
|
+
syncPose(named);
|
|
162
|
+
named.system.emit(burst.count);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
83
165
|
const poolMax = resolveParticleBudget(quality, Math.min(burst.count, burst.config.max ?? burst.count), particleCap);
|
|
84
|
-
const
|
|
166
|
+
const spec = { id: `burst:${burst.seq}`, config: burst.config, ...burst.options };
|
|
167
|
+
const resolved = pose(spec);
|
|
168
|
+
const system = createParticleSystem({ ...burst.config, rate: 0, max: poolMax, seed: burst.config.seed ?? burst.seq }, { collision: collisionFor(() => spec, resolved), acceleration: accelerationFor(() => spec, resolved) });
|
|
169
|
+
if (spec.space !== "local") {
|
|
170
|
+
const base = spec.follow === undefined ? burst.config.position ?? [0, 0, 0] : [0, 0, 0];
|
|
171
|
+
system.configure({ position: [resolved.position[0] + base[0], resolved.position[1] + base[1], resolved.position[2] + base[2]] });
|
|
172
|
+
}
|
|
85
173
|
system.emit(burst.count);
|
|
86
|
-
|
|
87
|
-
|
|
174
|
+
timing.current.poolsCreated++;
|
|
175
|
+
burstsRef.current.push({ seq: burst.seq, system, blending: burst.blending ?? "additive", render: spec.render, collisionEffect: spec.collisionEffect,
|
|
176
|
+
position: spec.space === "local" ? resolved.position : [0, 0, 0], rotationY: spec.space === "local" ? resolved.yaw : 0 });
|
|
177
|
+
if (burstsRef.current.length > MAX_CONCURRENT_BURSTS) {
|
|
88
178
|
burstsRef.current.shift();
|
|
179
|
+
timing.current.poolsRetired++;
|
|
180
|
+
}
|
|
89
181
|
changed = true;
|
|
90
182
|
}
|
|
91
183
|
if (changed)
|
|
@@ -93,36 +185,92 @@ export function WorldParticles({ quality, particleCap }) {
|
|
|
93
185
|
}
|
|
94
186
|
sync();
|
|
95
187
|
const off = ctx.particles.subscribe(sync);
|
|
188
|
+
return off;
|
|
189
|
+
}, [ctx, quality, particleCap]);
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
if (!diagnostics)
|
|
192
|
+
return;
|
|
193
|
+
return devtools.probes.register("particles", () => {
|
|
194
|
+
let count = 0, capacity = 0, renderBufferBytes = 0;
|
|
195
|
+
const read = (system) => {
|
|
196
|
+
const buffers = system.buffers();
|
|
197
|
+
count += system.count();
|
|
198
|
+
capacity += buffers.positions.length / 3;
|
|
199
|
+
renderBufferBytes += buffers.positions.byteLength + buffers.previousPositions.byteLength + buffers.velocities.byteLength + buffers.sizes.byteLength + buffers.colors.byteLength + buffers.alphas.byteLength + buffers.ids.byteLength;
|
|
200
|
+
};
|
|
201
|
+
for (const instance of emittersRef.current.values())
|
|
202
|
+
read(instance.system);
|
|
203
|
+
for (const burst of burstsRef.current)
|
|
204
|
+
read(burst.system);
|
|
205
|
+
const sample = timing.current, fields = resources.current;
|
|
206
|
+
return { scope: "WorldParticles", emitters: emittersRef.current.size, bursts: burstsRef.current.length, count, capacity, renderBufferBytes,
|
|
207
|
+
poolsCreated: sample.poolsCreated, poolsRetired: sample.poolsRetired,
|
|
208
|
+
updateSamples: sample.updateSamples, lastUpdateMs: sample.lastUpdateMs, maxUpdateMs: sample.maxUpdateMs,
|
|
209
|
+
avgUpdateMs: sample.updateSamples === 0 ? 0 : sample.totalUpdateMs / sample.updateSamples,
|
|
210
|
+
droppedCatchupSeconds: sample.droppedCatchupSeconds,
|
|
211
|
+
resources: { ...fields, avgUploadMsPerField: fields.uploadSamples === 0 ? 0 : fields.totalUploadMs / fields.uploadSamples } };
|
|
212
|
+
});
|
|
213
|
+
}, [ctx, diagnostics]);
|
|
214
|
+
useEffect(() => {
|
|
215
|
+
previousTime.current = ctx.time.now();
|
|
96
216
|
return () => {
|
|
97
|
-
|
|
98
|
-
|
|
217
|
+
timing.current.poolsRetired += emittersRef.current.size + burstsRef.current.length;
|
|
218
|
+
emittersRef.current.clear();
|
|
99
219
|
burstsRef.current = [];
|
|
100
220
|
};
|
|
101
|
-
}, [ctx
|
|
221
|
+
}, [ctx]);
|
|
102
222
|
useFrame(() => {
|
|
223
|
+
const started = diagnostics ? performance.now() : 0;
|
|
224
|
+
const gameTime = ctx.time.now();
|
|
225
|
+
const dt = resolveParticleDelta(previousTime.current, gameTime);
|
|
226
|
+
if (diagnostics)
|
|
227
|
+
timing.current.droppedCatchupSeconds += Math.max(0, gameTime - previousTime.current - dt);
|
|
228
|
+
previousTime.current = gameTime;
|
|
229
|
+
const impacts = [];
|
|
230
|
+
const collect = (id, system, effect, origin, yaw = 0) => {
|
|
231
|
+
const events = system.drainEvents();
|
|
232
|
+
for (const event of events) {
|
|
233
|
+
if (origin !== undefined) {
|
|
234
|
+
event.position = transformParticleVector(event.position, origin, yaw);
|
|
235
|
+
event.velocity = transformParticleVector(event.velocity, [0, 0, 0], yaw);
|
|
236
|
+
if (event.normal !== undefined)
|
|
237
|
+
event.normal = transformParticleVector(event.normal, [0, 0, 0], yaw);
|
|
238
|
+
}
|
|
239
|
+
if (effect !== undefined && event.type === "collision" && impacts.length < 16)
|
|
240
|
+
impacts.push({ effect, event });
|
|
241
|
+
}
|
|
242
|
+
ctx.particles.reportEvents(id, events);
|
|
243
|
+
};
|
|
103
244
|
for (const instance of emittersRef.current.values()) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
});
|
|
245
|
+
syncPose(instance);
|
|
246
|
+
instance.system.update(dt);
|
|
247
|
+
const resolved = instance.spec.space === "local" ? instance.pose : undefined;
|
|
248
|
+
collect(instance.spec.id, instance.system, instance.spec.collisionEffect, resolved?.position, resolved?.yaw);
|
|
249
|
+
}
|
|
250
|
+
let finished = false;
|
|
251
|
+
for (const burst of burstsRef.current) {
|
|
252
|
+
burst.system.update(dt);
|
|
253
|
+
collect(`burst:${burst.seq}`, burst.system, burst.collisionEffect, burst.position, burst.rotationY);
|
|
254
|
+
if (burst.system.count() === 0) {
|
|
255
|
+
finished = true;
|
|
256
|
+
timing.current.poolsRetired++;
|
|
257
|
+
}
|
|
118
258
|
}
|
|
119
|
-
|
|
120
|
-
if (finished.length > 0) {
|
|
259
|
+
if (finished) {
|
|
121
260
|
burstsRef.current = burstsRef.current.filter((burst) => burst.system.count() > 0);
|
|
122
261
|
setGeneration((n) => n + 1);
|
|
123
262
|
}
|
|
263
|
+
for (const { effect, event } of impacts) {
|
|
264
|
+
const { collision: _, ...config } = effect.config;
|
|
265
|
+
ctx.particles.burst({ ...config, position: event.position, rate: 0, deathEvents: false }, effect.count, effect.blending, { render: effect.render });
|
|
266
|
+
}
|
|
267
|
+
if (diagnostics) {
|
|
268
|
+
const elapsed = performance.now() - started, sample = timing.current;
|
|
269
|
+
sample.lastUpdateMs = elapsed;
|
|
270
|
+
sample.totalUpdateMs += elapsed;
|
|
271
|
+
sample.maxUpdateMs = Math.max(sample.maxUpdateMs, elapsed);
|
|
272
|
+
sample.updateSamples++;
|
|
273
|
+
}
|
|
124
274
|
});
|
|
125
|
-
return
|
|
126
|
-
? instance.spec.offset
|
|
127
|
-
: [0, 0, 0], children: _jsx(ParticleField, { system: instance.system, blending: instance.spec.blending ?? "normal" }) }, instance.spec.id))), burstsRef.current.map((burst) => (_jsx(ParticleField, { system: burst.system, blending: burst.blending }, burst.seq)))] }));
|
|
275
|
+
return _jsxs(_Fragment, { children: [Array.from(emittersRef.current.values()).map((instance) => _jsx("group", { ref: (group) => { instance.group = group; }, children: _jsx(ParticleField, { system: instance.system, advance: false, blending: instance.spec.blending ?? "normal", render: instance.spec.render, metrics: diagnostics ? resources.current : undefined }) }, instance.spec.id)), burstsRef.current.map((burst) => _jsx("group", { position: burst.position, rotation: [0, burst.rotationY, 0], children: _jsx(ParticleField, { system: burst.system, advance: false, blending: burst.blending, render: burst.render, metrics: diagnostics ? resources.current : undefined }) }, burst.seq))] });
|
|
128
276
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { type ComponentType, type ReactNode } from "react";
|
|
2
|
+
import { type EditorDocument } from "@jgengine/core/editor/index";
|
|
2
3
|
import type { AssetCatalog } from "@jgengine/core/scene/assetCatalog";
|
|
3
4
|
import type { SceneEntity } from "@jgengine/core/scene/entityStore";
|
|
4
5
|
import { type SceneObject } from "@jgengine/core/scene/objectStore";
|
|
5
6
|
import type { EntitySpriteConfig, ModelConfig, ObjectStyle } from "@jgengine/core/game/playableGame";
|
|
6
7
|
import type { PresencePoseRow } from "@jgengine/core/runtime/transport";
|
|
7
|
-
export declare function WorldView({ entitySprites, entityModels, objectModels, objectStyles, environment, assets, renderEntity, renderObject, selectedIds, hideLocalActor, }: {
|
|
8
|
+
export declare function WorldView({ entitySprites, entityModels, objectModels, objectStyles, environment, assets, renderEntity, renderObject, selectedIds, hideLocalActor, editorLayers, }: {
|
|
8
9
|
entitySprites: Record<string, EntitySpriteConfig> | undefined;
|
|
9
10
|
entityModels: Record<string, string | ModelConfig> | undefined;
|
|
10
11
|
objectModels: Record<string, string | ModelConfig> | undefined;
|
|
@@ -15,6 +16,7 @@ export declare function WorldView({ entitySprites, entityModels, objectModels, o
|
|
|
15
16
|
renderObject: ((object: SceneObject) => ReactNode) | undefined;
|
|
16
17
|
selectedIds: ReadonlySet<string>;
|
|
17
18
|
hideLocalActor: boolean;
|
|
19
|
+
editorLayers?: EditorDocument | undefined;
|
|
18
20
|
}): import("react").JSX.Element;
|
|
19
21
|
export declare function RemotePlayers({ rows, presenceInterpolationMs }: {
|
|
20
22
|
rows: PresencePoseRow[];
|