@jgengine/shell 0.19.0 → 0.21.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 +346 -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 +24 -9
- 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 +11 -10
- 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/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/GrassField.d.ts +2 -1
- package/dist/terrain/GrassField.js +59 -5
- package/dist/terrain/grassGeometry.d.ts +10 -0
- package/dist/terrain/grassGeometry.js +55 -0
- package/dist/terrain/grassMaterial.js +2 -2
- package/dist/terrain/terrainDetailMaterial.d.ts +3 -3
- package/dist/terrain/terrainDetailMaterial.js +7 -7
- 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/jgengine/SKILL.md +22 -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 +204 -67
- package/skills/jgengine-ui/capabilities.md +115 -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 +200 -57
- 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 +152 -5
- package/skills/jgengine-world/visibility.md +28 -1
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
/** Cosmetic roof clipping is bounded independently of authoritative shelter queries. */
|
|
3
|
+
export const WEATHER_SHELTER_BUDGET = 32;
|
|
4
|
+
/** @internal */
|
|
5
|
+
export function createWeatherShelterUniforms() {
|
|
6
|
+
return {
|
|
7
|
+
shelterCenters: { value: Array.from({ length: WEATHER_SHELTER_BUDGET }, () => new THREE.Vector4()) },
|
|
8
|
+
shelterShapes: { value: Array.from({ length: WEATHER_SHELTER_BUDGET }, () => new THREE.Vector4()) },
|
|
9
|
+
shelterCount: { value: 0 },
|
|
10
|
+
shelterIndices: new Int32Array(WEATHER_SHELTER_BUDGET),
|
|
11
|
+
shelterDistances: new Float64Array(WEATHER_SHELTER_BUDGET),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/** @internal */
|
|
15
|
+
export function updateWeatherShelters(uniforms, shelters, x, y, z) {
|
|
16
|
+
const indices = uniforms.shelterIndices;
|
|
17
|
+
const distances = uniforms.shelterDistances;
|
|
18
|
+
distances.fill(Infinity);
|
|
19
|
+
indices.fill(-1);
|
|
20
|
+
for (let index = 0; index < shelters.length; index += 1) {
|
|
21
|
+
const shelter = shelters[index];
|
|
22
|
+
if (shelter.kind !== "shelter")
|
|
23
|
+
continue;
|
|
24
|
+
const hx = shelter.shape === "box" ? shelter.halfExtents?.x ?? 0 : shelter.radius ?? 0;
|
|
25
|
+
const hy = shelter.shape === "box" ? shelter.halfExtents?.y ?? 0 : shelter.shape === "sphere" ? shelter.radius ?? 0 : (shelter.height ?? 0) * 0.5;
|
|
26
|
+
const hz = shelter.shape === "box" ? shelter.halfExtents?.z ?? 0 : shelter.radius ?? 0;
|
|
27
|
+
const dx = Math.max(0, Math.abs(x - shelter.center.x) - hx);
|
|
28
|
+
const dy = Math.max(0, Math.abs(y - shelter.center.y) - hy);
|
|
29
|
+
const dz = Math.max(0, Math.abs(z - shelter.center.z) - hz);
|
|
30
|
+
const distance = dx * dx + dy * dy + dz * dz;
|
|
31
|
+
for (let slot = 0; slot < WEATHER_SHELTER_BUDGET; slot += 1) {
|
|
32
|
+
if (distance >= distances[slot])
|
|
33
|
+
continue;
|
|
34
|
+
for (let move = WEATHER_SHELTER_BUDGET - 1; move > slot; move -= 1) {
|
|
35
|
+
distances[move] = distances[move - 1];
|
|
36
|
+
indices[move] = indices[move - 1];
|
|
37
|
+
}
|
|
38
|
+
distances[slot] = distance;
|
|
39
|
+
indices[slot] = index;
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
let count = 0;
|
|
44
|
+
for (let slot = 0; slot < WEATHER_SHELTER_BUDGET; slot += 1) {
|
|
45
|
+
const index = indices[slot];
|
|
46
|
+
if (index < 0)
|
|
47
|
+
break;
|
|
48
|
+
const shelter = shelters[index];
|
|
49
|
+
const center = shelter.center;
|
|
50
|
+
uniforms.shelterCenters.value[slot].set(center.x, center.y, center.z, shelter.shape === "box" ? 0 : shelter.shape === "sphere" ? 1 : 2);
|
|
51
|
+
const exposure = typeof shelter.meta?.exposure === "number" ? Math.max(0, Math.min(1, shelter.meta.exposure)) : 0;
|
|
52
|
+
const extents = shelter.halfExtents;
|
|
53
|
+
uniforms.shelterShapes.value[slot].set(shelter.shape === "box" ? extents?.x ?? 0 : shelter.radius ?? 0, shelter.shape === "box" ? extents?.y ?? 0 : (shelter.height ?? 0) * 0.5, shelter.shape === "box" ? extents?.z ?? 0 : 0, exposure);
|
|
54
|
+
count += 1;
|
|
55
|
+
}
|
|
56
|
+
uniforms.shelterCount.value = count;
|
|
57
|
+
}
|
|
58
|
+
/** @internal */
|
|
59
|
+
export const WEATHER_SHELTER_SHADER = `
|
|
60
|
+
uniform vec4 uShelterCenters[32];
|
|
61
|
+
uniform vec4 uShelterShapes[32];
|
|
62
|
+
uniform int uShelterCount;
|
|
63
|
+
float weatherExposure(vec3 point) {
|
|
64
|
+
float exposure = 1.0;
|
|
65
|
+
for (int i = 0; i < 32; i++) {
|
|
66
|
+
if (i >= uShelterCount) break;
|
|
67
|
+
vec4 center = uShelterCenters[i];
|
|
68
|
+
vec4 shape = uShelterShapes[i];
|
|
69
|
+
vec3 delta = abs(point - center.xyz);
|
|
70
|
+
bool inside = center.w < 0.5 ? all(lessThanEqual(delta, shape.xyz)) : center.w < 1.5 ? length(delta) <= shape.x : length(delta.xz) <= shape.x && delta.y <= shape.y;
|
|
71
|
+
if (inside) exposure = min(exposure, shape.w);
|
|
72
|
+
}
|
|
73
|
+
return exposure;
|
|
74
|
+
}
|
|
75
|
+
`;
|
|
@@ -1,19 +1,58 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
import * as THREE from "three";
|
|
3
|
+
import type { EditorVolume } from "@jgengine/core/editor/types";
|
|
4
|
+
import { type WeatherShelterUniforms } from "./weatherShelter.js";
|
|
5
|
+
import type { AuthoredWeatherSample } from "@jgengine/core/world/authoredWeather";
|
|
3
6
|
export type WeatherVector = readonly [number, number, number];
|
|
4
|
-
|
|
7
|
+
/** Actual procedural weather submissions and per-frame impact query work. */
|
|
8
|
+
export interface WeatherParticleMetrics {
|
|
9
|
+
rain: {
|
|
10
|
+
count: number;
|
|
11
|
+
capacity: number;
|
|
12
|
+
};
|
|
13
|
+
snow: {
|
|
14
|
+
count: number;
|
|
15
|
+
capacity: number;
|
|
16
|
+
};
|
|
17
|
+
dust: {
|
|
18
|
+
count: number;
|
|
19
|
+
capacity: number;
|
|
20
|
+
};
|
|
21
|
+
impacts: {
|
|
22
|
+
count: number;
|
|
23
|
+
capacity: number;
|
|
24
|
+
heightQueries: number;
|
|
25
|
+
exposureQueries: number;
|
|
26
|
+
queryBudget: number;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export interface WeatherUniformSet extends WeatherShelterUniforms {
|
|
5
30
|
time: THREE.IUniform<number>;
|
|
6
31
|
wind: THREE.IUniform<THREE.Vector3>;
|
|
7
32
|
lightning: THREE.IUniform<number>;
|
|
33
|
+
rain: THREE.IUniform<number>;
|
|
34
|
+
snow: THREE.IUniform<number>;
|
|
35
|
+
dust: THREE.IUniform<number>;
|
|
36
|
+
controlsWind: boolean;
|
|
37
|
+
metrics?: WeatherParticleMetrics;
|
|
8
38
|
}
|
|
9
39
|
export interface WeatherUniformOptions {
|
|
10
40
|
wind?: WeatherVector;
|
|
11
41
|
lightning?: number;
|
|
12
42
|
timeScale?: number;
|
|
43
|
+
/** Absolute authoritative simulation seconds; no render delta is accumulated when supplied. */
|
|
44
|
+
timeSeconds?: number | (() => number);
|
|
45
|
+
sample?: (x: number, z: number) => AuthoredWeatherSample;
|
|
46
|
+
/** The closest 32 authored shelter volumes clip cosmetic particles. */
|
|
47
|
+
shelters?: readonly EditorVolume[];
|
|
48
|
+
/** Opt-in counters read from submitted geometry and actual query calls. */
|
|
49
|
+
metrics?: WeatherParticleMetrics;
|
|
13
50
|
}
|
|
14
51
|
/** @internal */
|
|
15
52
|
export declare function createWeatherUniformSet(options?: WeatherUniformOptions): WeatherUniformSet;
|
|
16
53
|
/** @internal */
|
|
54
|
+
export declare function updateWeatherUniformSet(uniforms: WeatherUniformSet, options: WeatherUniformOptions, delta: number, x?: number, z?: number, y?: number): void;
|
|
55
|
+
/** @internal */
|
|
17
56
|
export declare function WeatherUniformProvider({ children, ...options }: WeatherUniformOptions & {
|
|
18
57
|
children: ReactNode;
|
|
19
58
|
}): import("react").JSX.Element;
|
|
@@ -2,25 +2,44 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { createContext, useContext, useMemo } from "react";
|
|
3
3
|
import { useFrame } from "@react-three/fiber";
|
|
4
4
|
import * as THREE from "three";
|
|
5
|
+
import { createWeatherShelterUniforms, updateWeatherShelters } from "./weatherShelter.js";
|
|
5
6
|
const WeatherUniformContext = createContext(null);
|
|
6
7
|
/** @internal */
|
|
7
8
|
export function createWeatherUniformSet(options = {}) {
|
|
8
9
|
const wind = options.wind ?? [0, 0, 0];
|
|
9
10
|
return {
|
|
10
|
-
|
|
11
|
+
...createWeatherShelterUniforms(),
|
|
12
|
+
time: { value: typeof options.timeSeconds === "function" ? options.timeSeconds() : options.timeSeconds ?? 0 },
|
|
11
13
|
wind: { value: new THREE.Vector3(wind[0], wind[1], wind[2]) },
|
|
12
14
|
lightning: { value: options.lightning ?? 0 },
|
|
15
|
+
rain: { value: options.sample === undefined ? 1 : 0 },
|
|
16
|
+
snow: { value: options.sample === undefined ? 1 : 0 },
|
|
17
|
+
dust: { value: options.sample === undefined ? 1 : 0 },
|
|
18
|
+
controlsWind: options.wind !== undefined || options.sample !== undefined,
|
|
19
|
+
metrics: options.metrics,
|
|
13
20
|
};
|
|
14
21
|
}
|
|
22
|
+
/** @internal */
|
|
23
|
+
export function updateWeatherUniformSet(uniforms, options, delta, x = 0, z = 0, y = 0) {
|
|
24
|
+
const sample = options.sample?.(x, z);
|
|
25
|
+
const explicitTime = sample?.timeSeconds ?? (typeof options.timeSeconds === "function" ? options.timeSeconds() : options.timeSeconds);
|
|
26
|
+
uniforms.time.value = explicitTime ?? uniforms.time.value + delta * (options.timeScale ?? 1);
|
|
27
|
+
const wind = sample === undefined ? options.wind ?? [0, 0, 0] : [sample.wind[0], 0, sample.wind[1]];
|
|
28
|
+
uniforms.wind.value.set(wind[0], wind[1], wind[2]);
|
|
29
|
+
uniforms.lightning.value = sample?.lightning ?? options.lightning ?? 0;
|
|
30
|
+
uniforms.rain.value = sample?.rain ?? 1;
|
|
31
|
+
uniforms.snow.value = sample?.snow ?? 1;
|
|
32
|
+
uniforms.dust.value = sample?.dust ?? 1;
|
|
33
|
+
uniforms.controlsWind = options.wind !== undefined || options.sample !== undefined;
|
|
34
|
+
uniforms.metrics = options.metrics;
|
|
35
|
+
updateWeatherShelters(uniforms, options.shelters ?? [], x, y, z);
|
|
36
|
+
}
|
|
15
37
|
function useUniformTicker(uniforms, options, enabled = true) {
|
|
16
|
-
useFrame((
|
|
38
|
+
useFrame((state, delta) => {
|
|
17
39
|
if (!enabled)
|
|
18
40
|
return;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
uniforms.wind.value.set(wind[0], wind[1], wind[2]);
|
|
22
|
-
uniforms.lightning.value = options.lightning ?? 0;
|
|
23
|
-
});
|
|
41
|
+
updateWeatherUniformSet(uniforms, options, delta, state.camera.position.x, state.camera.position.z, state.camera.position.y);
|
|
42
|
+
}, -1);
|
|
24
43
|
}
|
|
25
44
|
/** @internal */
|
|
26
45
|
export function WeatherUniformProvider({ children, ...options }) {
|
|
@@ -32,6 +51,7 @@ export function WeatherUniformProvider({ children, ...options }) {
|
|
|
32
51
|
export function useWeatherUniformSet(options = {}) {
|
|
33
52
|
const shared = useContext(WeatherUniformContext);
|
|
34
53
|
const local = useMemo(() => createWeatherUniformSet(options), []);
|
|
35
|
-
useUniformTicker(local, options, shared === null);
|
|
36
|
-
|
|
54
|
+
useUniformTicker(local, options, shared === null || (!shared.controlsWind && options.wind !== undefined));
|
|
55
|
+
const inherited = useMemo(() => shared === null ? local : { ...shared, wind: local.wind }, [shared, local]);
|
|
56
|
+
return shared === null ? local : !shared.controlsWind && options.wind !== undefined ? inherited : shared;
|
|
37
57
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ReactElement } from "react";
|
|
2
|
+
import * as THREE from "three";
|
|
3
|
+
import type { FlockStepAgent } from "@jgengine/core/ai/flock";
|
|
4
|
+
import type { ModelConfig } from "@jgengine/core/game/playableGame";
|
|
5
|
+
import type { AssetCatalog } from "@jgengine/core/scene/assetCatalog";
|
|
6
|
+
/** Caller-owned species models for authored habitat agents. */
|
|
7
|
+
export interface AuthoredFlocksProps {
|
|
8
|
+
models: Record<string, string | ModelConfig>;
|
|
9
|
+
assets?: AssetCatalog;
|
|
10
|
+
/** Model nose axis. Default -Z. */
|
|
11
|
+
forward?: readonly [number, number, number];
|
|
12
|
+
maxInstances?: number;
|
|
13
|
+
}
|
|
14
|
+
/** @internal Orient habitat agents from their full authoritative velocity. */
|
|
15
|
+
export declare function applyFlockPose(group: THREE.Group, agent: FlockStepAgent, forward: THREE.Vector3, direction: THREE.Vector3): void;
|
|
16
|
+
/**
|
|
17
|
+
* Render bounded authored habitat agents with game-selected species models. The
|
|
18
|
+
* environment owns movement and lifetime; existing model owners load, animate,
|
|
19
|
+
* and dispose the native GLB. Unmapped species have no substitute geometry.
|
|
20
|
+
* @capability authored-flocks render authored habitat agents through caller-selected animated species models with velocity orientation and bounded model lifetime
|
|
21
|
+
*/
|
|
22
|
+
export declare function AuthoredFlocks({ models, assets, forward, maxInstances }: AuthoredFlocksProps): ReactElement;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useFrame } from "@react-three/fiber";
|
|
3
|
+
import { useMemo, useRef, useState } from "react";
|
|
4
|
+
import * as THREE from "three";
|
|
5
|
+
import { useGameContext } from "@jgengine/react/provider";
|
|
6
|
+
import { IsolatedEntityModel } from "../render/SceneModels.js";
|
|
7
|
+
import { resolveModel } from "../render/resolveModel.js";
|
|
8
|
+
/** @internal Orient habitat agents from their full authoritative velocity. */
|
|
9
|
+
export function applyFlockPose(group, agent, forward, direction) {
|
|
10
|
+
group.position.set(...agent.position);
|
|
11
|
+
direction.set(...agent.velocity);
|
|
12
|
+
if (direction.lengthSq() > 1e-12)
|
|
13
|
+
group.quaternion.setFromUnitVectors(forward, direction.normalize());
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Render bounded authored habitat agents with game-selected species models. The
|
|
17
|
+
* environment owns movement and lifetime; existing model owners load, animate,
|
|
18
|
+
* and dispose the native GLB. Unmapped species have no substitute geometry.
|
|
19
|
+
* @capability authored-flocks render authored habitat agents through caller-selected animated species models with velocity orientation and bounded model lifetime
|
|
20
|
+
*/
|
|
21
|
+
export function AuthoredFlocks({ models, assets, forward = [0, 0, -1], maxInstances = 256 }) {
|
|
22
|
+
const ctx = useGameContext();
|
|
23
|
+
const speciesModels = useMemo(() => {
|
|
24
|
+
const resolved = new Map();
|
|
25
|
+
for (const [species, value] of Object.entries(models)) {
|
|
26
|
+
if (typeof value === "string" && assets === undefined)
|
|
27
|
+
throw new Error(`[jgengine] flock species ${species} requires an asset catalog to resolve ${value}`);
|
|
28
|
+
const model = typeof value === "string" ? resolveModel(value, assets) : value;
|
|
29
|
+
resolved.set(species, model.animation === undefined ? { ...model, animation: "auto" } : model);
|
|
30
|
+
}
|
|
31
|
+
return resolved;
|
|
32
|
+
}, [models, assets]);
|
|
33
|
+
const entries = useRef(new Map());
|
|
34
|
+
const seen = useRef(new Set());
|
|
35
|
+
const direction = useRef(new THREE.Vector3());
|
|
36
|
+
const nose = useRef(new THREE.Vector3());
|
|
37
|
+
const [, setGeneration] = useState(0);
|
|
38
|
+
nose.current.set(...forward).normalize();
|
|
39
|
+
if (nose.current.lengthSq() === 0)
|
|
40
|
+
nose.current.set(0, 0, -1);
|
|
41
|
+
const limit = Number.isFinite(maxInstances) ? Math.max(0, Math.min(1024, Math.floor(maxInstances))) : 256;
|
|
42
|
+
useFrame(() => {
|
|
43
|
+
let changed = false;
|
|
44
|
+
let count = 0;
|
|
45
|
+
seen.current.clear();
|
|
46
|
+
for (const flock of ctx.environment.flocks()) {
|
|
47
|
+
if (flock.role !== "cosmetic")
|
|
48
|
+
continue;
|
|
49
|
+
const model = speciesModels.get(flock.species);
|
|
50
|
+
if (model === undefined)
|
|
51
|
+
continue;
|
|
52
|
+
for (let i = 0; i < flock.agents.length && count < limit; i++, count++) {
|
|
53
|
+
const key = `${flock.id}:${i}`;
|
|
54
|
+
const agent = flock.agents[i];
|
|
55
|
+
seen.current.add(key);
|
|
56
|
+
const current = entries.current.get(key);
|
|
57
|
+
if (current === undefined) {
|
|
58
|
+
entries.current.set(key, { agent, model, group: null });
|
|
59
|
+
changed = true;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
current.agent = agent;
|
|
63
|
+
if (current.model !== model) {
|
|
64
|
+
current.model = model;
|
|
65
|
+
changed = true;
|
|
66
|
+
}
|
|
67
|
+
if (current.group !== null)
|
|
68
|
+
applyFlockPose(current.group, agent, nose.current, direction.current);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (count >= limit)
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
for (const key of entries.current.keys())
|
|
75
|
+
if (!seen.current.has(key)) {
|
|
76
|
+
entries.current.delete(key);
|
|
77
|
+
changed = true;
|
|
78
|
+
}
|
|
79
|
+
if (changed)
|
|
80
|
+
setGeneration((n) => n + 1);
|
|
81
|
+
});
|
|
82
|
+
return _jsx(_Fragment, { children: Array.from(entries.current, ([key, entry]) => _jsx("group", { ref: (group) => {
|
|
83
|
+
entry.group = group;
|
|
84
|
+
if (group !== null)
|
|
85
|
+
applyFlockPose(group, entry.agent, nose.current, direction.current);
|
|
86
|
+
}, children: _jsx(IsolatedEntityModel, { model: entry.model }) }, key)) });
|
|
87
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { SceneObject } from "@jgengine/core/scene/objectStore";
|
|
3
|
+
import { type StaticObjectBatch } from "./staticObjectBatches.js";
|
|
4
|
+
/** Render compatible authored props as shared model draws, preserving unsupported models. @internal */
|
|
5
|
+
export declare function StaticObjectModels({ batch, single }: {
|
|
6
|
+
batch: StaticObjectBatch;
|
|
7
|
+
single: (object: SceneObject) => ReactNode;
|
|
8
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useFrame, useLoader } from "@react-three/fiber";
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import * as THREE from "three";
|
|
5
|
+
import { useGameContext } from "@jgengine/react/provider";
|
|
6
|
+
import { sharedGltfLoader } from "../render/modelLoad.js";
|
|
7
|
+
import { IsolatedModelPart } from "../render/SceneModels.js";
|
|
8
|
+
import { measureLocalBounds, reportMeasuredBounds } from "../render/measureBounds.js";
|
|
9
|
+
import { measureLocalCollisionTriangles, reportMeasuredCollisionMesh } from "../render/measureCollisionMesh.js";
|
|
10
|
+
import { buildScatterModelSources, disposeScatterModelSources } from "../scatter/scatterModels.js";
|
|
11
|
+
import { useRenderVisibility } from "../visibility/CullingProvider.js";
|
|
12
|
+
import { CAMERA_POST_FRAME_PRIORITY } from "../camera/cameraRigs.js";
|
|
13
|
+
import { canInstanceStaticScene, incompatibleStaticObjectIds, isCompatibleStaticObject, syncStaticObjectSource } from "./staticObjectBatches.js";
|
|
14
|
+
function ObjectSource({ source, objects, batch }) {
|
|
15
|
+
const ctx = useGameContext();
|
|
16
|
+
const visibility = useRenderVisibility();
|
|
17
|
+
const ref = useRef(null);
|
|
18
|
+
const scratch = useMemo(() => new THREE.Object3D(), []);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const mesh = ref.current;
|
|
21
|
+
return () => { mesh?.dispose(); };
|
|
22
|
+
}, [objects.length, source]);
|
|
23
|
+
useFrame(() => {
|
|
24
|
+
if (ref.current !== null)
|
|
25
|
+
syncStaticObjectSource(ref.current, source, objects, (id) => {
|
|
26
|
+
const object = ctx.scene.object.get(id);
|
|
27
|
+
return object !== null && object.catalogId === batch.objects[0].catalogId &&
|
|
28
|
+
isCompatibleStaticObject(object, batch.model, ctx.scene.object.catalog(id)) ? object : null;
|
|
29
|
+
}, visibility.current, scratch);
|
|
30
|
+
}, CAMERA_POST_FRAME_PRIORITY + 0.1);
|
|
31
|
+
return _jsx("instancedMesh", { ref: ref, args: [source.geometry, source.material, objects.length], castShadow: source.castShadow, receiveShadow: source.receiveShadow, dispose: null });
|
|
32
|
+
}
|
|
33
|
+
function LoadedStaticObjects({ batch, single }) {
|
|
34
|
+
const ctx = useGameContext();
|
|
35
|
+
const [excluded, setExcluded] = useState([]);
|
|
36
|
+
const fallbackObjects = useRef(new Map());
|
|
37
|
+
useFrame(() => {
|
|
38
|
+
const next = incompatibleStaticObjectIds(batch, ctx.scene.object.get, ctx.scene.object.catalog, excluded, fallbackObjects.current);
|
|
39
|
+
if (next !== excluded)
|
|
40
|
+
setExcluded(next);
|
|
41
|
+
});
|
|
42
|
+
const objects = useMemo(() => batch.objects.filter((object) => !excluded.includes(object.instanceId)), [batch, excluded]);
|
|
43
|
+
const gltf = useLoader(sharedGltfLoader, batch.model.url);
|
|
44
|
+
const supported = useMemo(() => canInstanceStaticScene(gltf.scene, gltf.animations), [gltf]);
|
|
45
|
+
const harvested = useMemo(() => supported ? buildScatterModelSources(gltf.scene, batch.model) : null, [supported, gltf, batch.model]);
|
|
46
|
+
useEffect(() => () => { if (harvested !== null)
|
|
47
|
+
disposeScatterModelSources(harvested.root); }, [harvested]);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (harvested === null)
|
|
50
|
+
return;
|
|
51
|
+
const root = new THREE.Group();
|
|
52
|
+
for (const source of harvested.sources) {
|
|
53
|
+
const mesh = new THREE.Mesh(source.geometry, source.material);
|
|
54
|
+
mesh.matrixAutoUpdate = false;
|
|
55
|
+
mesh.matrix.copy(source.localMatrix);
|
|
56
|
+
root.add(mesh);
|
|
57
|
+
}
|
|
58
|
+
const key = batch.objects[0].catalogId;
|
|
59
|
+
if (batch.model.dims?.maxY === undefined) {
|
|
60
|
+
const bounds = measureLocalBounds(root);
|
|
61
|
+
if (bounds !== null)
|
|
62
|
+
reportMeasuredBounds(ctx, "object", key, bounds);
|
|
63
|
+
}
|
|
64
|
+
const triangles = measureLocalCollisionTriangles(root);
|
|
65
|
+
if (triangles !== null)
|
|
66
|
+
reportMeasuredCollisionMesh(ctx, "object", key, triangles);
|
|
67
|
+
}, [ctx, harvested, batch]);
|
|
68
|
+
if (harvested === null)
|
|
69
|
+
return _jsx(_Fragment, { children: batch.objects.map(single) });
|
|
70
|
+
return _jsxs(_Fragment, { children: [harvested.sources.map((source, index) => objects.length === 0 ? null : _jsx(ObjectSource, { source: source, objects: objects, batch: batch }, index)), excluded.map((id) => { const live = ctx.scene.object.get(id); return live === null ? null : single(live); })] });
|
|
71
|
+
}
|
|
72
|
+
/** Render compatible authored props as shared model draws, preserving unsupported models. @internal */
|
|
73
|
+
export function StaticObjectModels({ batch, single }) {
|
|
74
|
+
return _jsx(IsolatedModelPart, { model: batch.model, seam: "object", children: _jsx(LoadedStaticObjects, { batch: batch, single: single }) });
|
|
75
|
+
}
|
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;
|