@jgengine/shell 0.34.0 → 0.35.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 +28 -0
- package/dist/GamePlayerShell.js +2 -0
- package/dist/Shell3dPresentation.js +1 -1
- package/dist/audio/AudioComponents.js +7 -0
- package/dist/audio/audioEngine.d.ts +59 -0
- package/dist/audio/audioEngine.js +334 -15
- package/dist/audio/audioProbe.d.ts +10 -5
- package/dist/audio/audioProbe.js +6 -3
- package/dist/audio/occlusion.d.ts +11 -0
- package/dist/audio/occlusion.js +20 -0
- package/dist/play/index.d.ts +3 -0
- package/dist/play/index.js +3 -0
- package/dist/play/run.d.ts +27 -0
- package/dist/play/run.js +266 -0
- package/dist/play/script.d.ts +103 -0
- package/dist/play/script.js +52 -0
- package/dist/play/timeline.d.ts +43 -0
- package/dist/play/timeline.js +27 -0
- package/dist/pointer/pointerService.d.ts +2 -0
- package/dist/pointer/pointerService.js +4 -1
- package/dist/postfx/PostProcessing.d.ts +15 -1
- package/dist/postfx/PostProcessing.js +16 -6
- package/dist/postfx/postfxTierPolicy.d.ts +25 -0
- package/dist/postfx/postfxTierPolicy.js +17 -0
- package/dist/render/CascadedShadows.d.ts +4 -1
- package/dist/render/CascadedShadows.js +15 -7
- package/dist/render/CharacterCrowd.d.ts +11 -7
- package/dist/render/CharacterCrowd.js +103 -90
- package/dist/render/SceneLighting.js +14 -6
- package/dist/render/SceneWarmupGate.d.ts +6 -1
- package/dist/render/SceneWarmupGate.js +33 -7
- package/dist/render/SkinnedInstances.d.ts +16 -8
- package/dist/render/SkinnedInstances.js +215 -65
- package/dist/render/characterLod.d.ts +7 -0
- package/dist/render/characterLod.js +11 -0
- package/dist/render/crowdBatch.d.ts +45 -0
- package/dist/render/crowdBatch.js +68 -0
- package/dist/render/crowdRegistry.d.ts +55 -5
- package/dist/render/crowdRegistry.js +92 -9
- package/dist/render/shaderWarmup.d.ts +75 -18
- package/dist/render/shaderWarmup.js +221 -64
- package/dist/render/shadowCasters.d.ts +1 -0
- package/dist/render/shadowCasters.js +1 -1
- package/dist/render/shadowTiers.d.ts +36 -0
- package/dist/render/shadowTiers.js +50 -0
- package/dist/world/WorldScene.js +28 -11
- package/package.json +8 -4
- package/skills/jgengine/api.md +3 -3
- package/skills/jgengine-ui/api.md +79 -9
- package/skills/jgengine-ui/capabilities.md +7 -2
- package/skills/jgengine-world/api.md +18 -2
- package/skills/jgengine-world/capabilities.md +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -26,6 +26,33 @@ At publish, rename this heading to the new version and mirror the entries into
|
|
|
26
26
|
`packages/core/src/meta/changelog.ts` (the typed `CHANGELOG` export).
|
|
27
27
|
-->
|
|
28
28
|
|
|
29
|
+
## 0.35.0
|
|
30
|
+
|
|
31
|
+
### Migrate
|
|
32
|
+
|
|
33
|
+
- **Bump lockstep SDK packages to `^0.35.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.32.0`; `@jgengine/github` is `0.22.0`.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- `ModelLodConfig.crowdCapacity` declares how many characters of one model a batch draws (default 128). A character past the cap keeps its own rig at its distance tier instead of disappearing.
|
|
38
|
+
- `ModelLodConfig.crowdDistance` accepts `0`, which batches a character at every distance instead of only past 25 m. A game with a near crowd or a close-quarters army sets it to batch members in front of the camera too.
|
|
39
|
+
- `characterRigTier` (`@jgengine/shell/render/characterLod`) resolves the distance tier a character poses its own rig on, which is the fallback for an over-cap crowd.
|
|
40
|
+
- `audio.zones` (`@jgengine/core/game/playableGame`): a scene declares world-space ambience zones, one looping bed per zone. The shell crossfades bed levels from the listener position on each sound's own bus, so entering, leaving and walking between zones hand over instead of cutting.
|
|
41
|
+
- `audio.occlusion` (`@jgengine/core/audio/occlusion`): budgeted geometry occlusion for retained positional loops. A blocked line attenuates the loop and darkens it through a low-pass; the per-tick test budget and interval bound the cost, and a loop that cannot be tested stays on distance-only.
|
|
42
|
+
- `SoundDef.duck`: a cue can lower named buses for its own duration. `AudioEngine.duck` returns a release, and the player's own gain is restored exactly even when they move the slider mid-duck.
|
|
43
|
+
- `AudioEngine.ambience()`, `AudioEngine.occlusionStats()` and `window.__jgengineAudio` report the zone mix, the newest occlusion test and the measured per-test cost for capture and diagnostics.
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
|
|
47
|
+
- The character batch path now covers every drawable character model, not only skinned locomotion rigs: a node-transform character (rotating limbs, no skin) rides the node a clip moves, and a model with no clips at all batches as a static geometry. Both still bake to one geometry per material, so a crowd draws once per model and material.
|
|
48
|
+
- Batched characters are pickable: the batch carries the entity ids of the instances it drew, so `worldHit().entity` and a click on a batched character resolve to the character instead of falling through to the ground.
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
|
|
52
|
+
- Load-time shader warmup compiles what the first frame can reach, reveals as soon as those programs are linked, and finishes the rest in background rounds, so a scene no longer holds the loading cover for its whole model stream and no longer drops a compile onto a frame that is already showing. The loading cover names the shader count, the seconds waited and the work left for the background.
|
|
53
|
+
- A crowd batch silently dropped members past its fixed 128 instances while their markers still hid them, so a full scene lost characters. Members past the declared capacity now keep their own rigs.
|
|
54
|
+
- A crowd batch's clock now follows the game clock when one is bound, so a paused game no longer keeps its crowd walking.
|
|
55
|
+
|
|
29
56
|
## 0.34.0
|
|
30
57
|
|
|
31
58
|
### Migrate
|
|
@@ -34,6 +61,7 @@ At publish, rename this heading to the new version and mirror the entries into
|
|
|
34
61
|
|
|
35
62
|
### Added
|
|
36
63
|
|
|
64
|
+
- `PaceBand` on `DriverGoal` (`paceBand` + caller-measured `paceGap`) gives catch-up pacing an owner in `@jgengine/core/ai/driver`: a dead zone around `margin` (`tolerance`), `ease` shedding pace for a car ahead over `easeAt`, `push` adding it for a car behind over `pushAt`, and `scale` as the per-car share. `paceBandFactor(band, gap)` exposes the rule on its own. Deterministic scalar math, no allocation per step; the push never outdrives `DriverTuning.maxSpeed`. First adopter: Drift Foundry's rival field, which deletes its own `paceBand`; second: Harbor Heat's interceptors.
|
|
37
65
|
- `createRunLoop` (`@jgengine/core/run/runLoop`) holds a run's at-risk haul, banks it scaled by the line or loses it on a failure, and draws one authored change for each next run from a seeded table, so the same seed and run history replay the same sequence. State round-trips through `snapshot`/`restore`; the money stays in `progression/careerBank`.
|
|
38
66
|
|
|
39
67
|
## 0.33.0
|
package/dist/GamePlayerShell.js
CHANGED
|
@@ -225,6 +225,8 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
225
225
|
buses: resolvePlayableAudio(playable).buses,
|
|
226
226
|
music: playable.audio?.music,
|
|
227
227
|
musicBus: playable.audio?.musicBus,
|
|
228
|
+
zones: playable.audio?.zones,
|
|
229
|
+
occlusion: playable.audio?.occlusion,
|
|
228
230
|
}), [playable]);
|
|
229
231
|
useEffect(() => () => audioEngine.dispose(), [audioEngine]);
|
|
230
232
|
useEffect(() => {
|
|
@@ -412,7 +412,7 @@ export function Shell3dPresentation({ playable, ctx, multiplayer, tracker, point
|
|
|
412
412
|
catch (error) {
|
|
413
413
|
reportRuntimeError(error, "tick");
|
|
414
414
|
}
|
|
415
|
-
}, seatsActive: () => !gateRef.current && playControlsActive(ctx) }), _jsx(DevtoolsRendererProbe, {}), devtoolsEnabled ? (_jsx(FrameTelemetry, { game: playable.game.name, quality: graphics.quality === "low" ? "draft" : "full" })) : null, splitScreen ? (_jsx(SplitScreenRenderer, { ctx: ctx, viewports: seatViewports, config: playable.viewports, mainCamera: playable.camera })) : null, !splitScreen && resolvedLook.postProcessing !== undefined && resolvedLook.postProcessing.enabled !== false ? (_jsx(PostProcessing, { config: resolvedLook.postProcessing, quality: graphics.quality, stages: graphics.profile.postStages })) : null, poster ? null : _jsx(SceneWarmupGate, { onState: setWarmup })] }) }), _jsx(SceneWarmupCover, { state: warmup }), graphics.showFps && !poster ? _jsx(FpsCounter, { tally: frameTally }) : null, !poster &&
|
|
415
|
+
}, seatsActive: () => !gateRef.current && playControlsActive(ctx) }), _jsx(DevtoolsRendererProbe, {}), devtoolsEnabled ? (_jsx(FrameTelemetry, { game: playable.game.name, quality: graphics.quality === "low" ? "draft" : "full" })) : null, splitScreen ? (_jsx(SplitScreenRenderer, { ctx: ctx, viewports: seatViewports, config: playable.viewports, mainCamera: playable.camera })) : null, !splitScreen && resolvedLook.postProcessing !== undefined && resolvedLook.postProcessing.enabled !== false ? (_jsx(PostProcessing, { config: resolvedLook.postProcessing, quality: graphics.quality, stages: graphics.profile.postStages })) : null, poster ? null : (_jsx(SceneWarmupGate, { onState: setWarmup, sceneDrawsToTarget: !splitScreen && resolvedLook.postProcessing !== undefined && resolvedLook.postProcessing.enabled !== false }))] }) }), _jsx(SceneWarmupCover, { state: warmup }), graphics.showFps && !poster ? _jsx(FpsCounter, { tally: frameTally }) : null, !poster &&
|
|
416
416
|
!orientationGate &&
|
|
417
417
|
coarsePointer &&
|
|
418
418
|
controlsActive &&
|
|
@@ -2,14 +2,21 @@ import { useFrame, useThree } from "@react-three/fiber";
|
|
|
2
2
|
import { useEffect, useRef } from "react";
|
|
3
3
|
import { Quaternion, Vector3 } from "three";
|
|
4
4
|
import { useGameContext } from "@jgengine/react/provider";
|
|
5
|
+
import { installAudioProbe } from "./audioProbe.js";
|
|
5
6
|
import { createEmitterTracker } from "./emitterTracking.js";
|
|
6
7
|
import { groundFocusListener } from "./listenerAnchor.js";
|
|
7
8
|
import { createListenerVelocityTrack, stepListenerVelocity } from "./loopParams.js";
|
|
9
|
+
import { sceneLineBlocked } from "./occlusion.js";
|
|
8
10
|
export function AudioListener({ engine, anchor = "camera" }) {
|
|
9
11
|
const camera = useThree((state) => state.camera);
|
|
10
12
|
const ctx = useGameContext();
|
|
11
13
|
const trackRef = useRef(createListenerVelocityTrack());
|
|
12
14
|
const scratchRef = useRef({ direction: new Vector3(), up: new Vector3(), quaternion: new Quaternion() });
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
engine.setOcclusionSource((from, to) => sceneLineBlocked(ctx.scene, from, to));
|
|
17
|
+
return () => engine.setOcclusionSource(null);
|
|
18
|
+
}, [engine, ctx]);
|
|
19
|
+
useEffect(() => installAudioProbe(engine), [engine]);
|
|
13
20
|
useFrame((_, dt) => {
|
|
14
21
|
const { direction, up, quaternion } = scratchRef.current;
|
|
15
22
|
camera.getWorldDirection(direction);
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { type AmbienceZoneDef } from "@jgengine/core/audio/ambience";
|
|
1
2
|
import { type AudioBusDef, type SoundDef } from "@jgengine/core/audio/audioFalloff";
|
|
3
|
+
import { type AudioOcclusionConfig } from "@jgengine/core/audio/occlusion";
|
|
2
4
|
import type { MusicTheme } from "@jgengine/core/audio/music";
|
|
3
5
|
import { type VoiceAllocator, type VoiceAllocatorConfig } from "@jgengine/core/audio/voiceAllocator";
|
|
4
6
|
import { type CrossfadeOptions } from "./musicDirector.js";
|
|
@@ -28,6 +30,10 @@ export interface AudioSceneConfig {
|
|
|
28
30
|
music?: Record<string, MusicTheme>;
|
|
29
31
|
/** Bus id the procedural music director mixes through. Default "music". */
|
|
30
32
|
musicBus?: string;
|
|
33
|
+
/** World-space ambience zones; each becomes one retained looping bed driven by the listener position. */
|
|
34
|
+
zones?: readonly AmbienceZoneDef[];
|
|
35
|
+
/** Geometry occlusion budget/policy for retained positional loops. Unset keeps the defaults; `false` keeps distance-only. */
|
|
36
|
+
occlusion?: AudioOcclusionConfig | false;
|
|
31
37
|
}
|
|
32
38
|
export interface AudioEmitterHandle {
|
|
33
39
|
/** Whether playback is reserved or playing; false after stop, stealing, failed load, or natural end. Optional for legacy hosts. */
|
|
@@ -43,8 +49,46 @@ export interface AudioEmitterHandle {
|
|
|
43
49
|
setHighpass(hz: number): void;
|
|
44
50
|
/** Emitter world velocity for doppler; ignored unless the sound declares `doppler`. */
|
|
45
51
|
setVelocity(velocity: Vec3): void;
|
|
52
|
+
/** Geometry occlusion, 0 (clear line) to 1 (fully blocked): scales the loop's gain and darkens it, composed with the game's own gain and filter. */
|
|
53
|
+
setOcclusion(amount: number): void;
|
|
46
54
|
stop(): void;
|
|
47
55
|
}
|
|
56
|
+
/** How long and how far to duck buses for {@link AudioEngine.duck}. */
|
|
57
|
+
export interface DuckOptions {
|
|
58
|
+
/** How far to lower the buses, 0..1. Default 0.6. */
|
|
59
|
+
amount?: number;
|
|
60
|
+
/** Seconds to hold the duck; omit to hold until the returned release runs. */
|
|
61
|
+
seconds?: number;
|
|
62
|
+
/** Fade time constant in seconds. Default 0.03. */
|
|
63
|
+
fadeSeconds?: number;
|
|
64
|
+
}
|
|
65
|
+
/** Measured geometry-occlusion work, cumulative since the engine was created. */
|
|
66
|
+
export interface AudioOcclusionStats {
|
|
67
|
+
/** Geometry tests run. */
|
|
68
|
+
tests: number;
|
|
69
|
+
/** Tests that found blocking geometry. */
|
|
70
|
+
blocked: number;
|
|
71
|
+
/** Total time spent inside the injected geometry test, milliseconds. */
|
|
72
|
+
testMs: number;
|
|
73
|
+
/** Mean milliseconds per test, or 0 before the first test. */
|
|
74
|
+
perTestMs: number;
|
|
75
|
+
/** Endpoints and result of the newest test, for a probe explaining what the engine actually asked. */
|
|
76
|
+
lastTest: {
|
|
77
|
+
from: Vec3;
|
|
78
|
+
to: Vec3;
|
|
79
|
+
blocked: boolean;
|
|
80
|
+
} | null;
|
|
81
|
+
}
|
|
82
|
+
/** Current ambience mix, by zone id. */
|
|
83
|
+
export interface AudioAmbienceState {
|
|
84
|
+
/** Loudest zone, or null when the listener is outside every zone. */
|
|
85
|
+
zone: string | null;
|
|
86
|
+
/** Non-silent zone beds with their smoothed target level (`0..1`). */
|
|
87
|
+
levels: readonly {
|
|
88
|
+
id: string;
|
|
89
|
+
level: number;
|
|
90
|
+
}[];
|
|
91
|
+
}
|
|
48
92
|
export interface AudioEngine {
|
|
49
93
|
/** Full listener orientation, or a legacy position using forward -Z and up +Y. */
|
|
50
94
|
setListenerPose(pose: ListenerPose | Vec3): void;
|
|
@@ -60,6 +104,21 @@ export interface AudioEngine {
|
|
|
60
104
|
playLoop(soundId: string, position?: Vec3): AudioEmitterHandle | null;
|
|
61
105
|
/** Crossfade the procedural soundtrack to `themeId` (null fades out). No-op when no `music` catalog is configured. */
|
|
62
106
|
playMusic(themeId: string | null, options?: CrossfadeOptions): void;
|
|
107
|
+
/**
|
|
108
|
+
* Install the scene's blocking-geometry test (`from` listener, `to` emitter), or null to stop
|
|
109
|
+
* testing. The engine owns the per-tick budget; tests never run outside it.
|
|
110
|
+
*/
|
|
111
|
+
setOcclusionSource(blocked: ((from: Vec3, to: Vec3) => boolean) | null): void;
|
|
112
|
+
/** Cumulative occlusion cost, for a capture probe or a perf readout. */
|
|
113
|
+
occlusionStats(): AudioOcclusionStats;
|
|
114
|
+
/** Current zone mix, for a capture probe or a HUD readout. */
|
|
115
|
+
ambience(): AudioAmbienceState;
|
|
116
|
+
/**
|
|
117
|
+
* Lower one bus for its duration and restore exactly the player's own level, even if they move
|
|
118
|
+
* the slider while it is ducked. Returns an idempotent release.
|
|
119
|
+
* @capability audio-ducking a cue with a `duck` policy lowers the music or ambience bus for its duration and restores the player's own gain exactly, even when the slider moves mid-duck.
|
|
120
|
+
*/
|
|
121
|
+
duck(busId: string, options?: DuckOptions): () => void;
|
|
63
122
|
setBusGain(busId: string, gain: number): void;
|
|
64
123
|
/** Mute one bus without losing its gain: unmuting restores exactly the volume the player set. */
|
|
65
124
|
setBusMute(busId: string, muted: boolean): void;
|