@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.
Files changed (52) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/GamePlayerShell.js +2 -0
  3. package/dist/Shell3dPresentation.js +1 -1
  4. package/dist/audio/AudioComponents.js +7 -0
  5. package/dist/audio/audioEngine.d.ts +59 -0
  6. package/dist/audio/audioEngine.js +334 -15
  7. package/dist/audio/audioProbe.d.ts +10 -5
  8. package/dist/audio/audioProbe.js +6 -3
  9. package/dist/audio/occlusion.d.ts +11 -0
  10. package/dist/audio/occlusion.js +20 -0
  11. package/dist/play/index.d.ts +3 -0
  12. package/dist/play/index.js +3 -0
  13. package/dist/play/run.d.ts +27 -0
  14. package/dist/play/run.js +266 -0
  15. package/dist/play/script.d.ts +103 -0
  16. package/dist/play/script.js +52 -0
  17. package/dist/play/timeline.d.ts +43 -0
  18. package/dist/play/timeline.js +27 -0
  19. package/dist/pointer/pointerService.d.ts +2 -0
  20. package/dist/pointer/pointerService.js +4 -1
  21. package/dist/postfx/PostProcessing.d.ts +15 -1
  22. package/dist/postfx/PostProcessing.js +16 -6
  23. package/dist/postfx/postfxTierPolicy.d.ts +25 -0
  24. package/dist/postfx/postfxTierPolicy.js +17 -0
  25. package/dist/render/CascadedShadows.d.ts +4 -1
  26. package/dist/render/CascadedShadows.js +15 -7
  27. package/dist/render/CharacterCrowd.d.ts +11 -7
  28. package/dist/render/CharacterCrowd.js +103 -90
  29. package/dist/render/SceneLighting.js +14 -6
  30. package/dist/render/SceneWarmupGate.d.ts +6 -1
  31. package/dist/render/SceneWarmupGate.js +33 -7
  32. package/dist/render/SkinnedInstances.d.ts +16 -8
  33. package/dist/render/SkinnedInstances.js +215 -65
  34. package/dist/render/characterLod.d.ts +7 -0
  35. package/dist/render/characterLod.js +11 -0
  36. package/dist/render/crowdBatch.d.ts +45 -0
  37. package/dist/render/crowdBatch.js +68 -0
  38. package/dist/render/crowdRegistry.d.ts +55 -5
  39. package/dist/render/crowdRegistry.js +92 -9
  40. package/dist/render/shaderWarmup.d.ts +75 -18
  41. package/dist/render/shaderWarmup.js +221 -64
  42. package/dist/render/shadowCasters.d.ts +1 -0
  43. package/dist/render/shadowCasters.js +1 -1
  44. package/dist/render/shadowTiers.d.ts +36 -0
  45. package/dist/render/shadowTiers.js +50 -0
  46. package/dist/world/WorldScene.js +28 -11
  47. package/package.json +8 -4
  48. package/skills/jgengine/api.md +3 -3
  49. package/skills/jgengine-ui/api.md +79 -9
  50. package/skills/jgengine-ui/capabilities.md +7 -2
  51. package/skills/jgengine-world/api.md +18 -2
  52. 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
@@ -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;