@jgengine/shell 0.34.0 → 0.36.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 +60 -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,65 @@ 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.36.0
|
|
30
|
+
|
|
31
|
+
### Migrate
|
|
32
|
+
|
|
33
|
+
- **Bump lockstep SDK packages to `^0.36.0`:** `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.33.0`; `@jgengine/github` is `0.23.0`.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- `createAgentSystem` (`@jgengine/core/agents/system`) takes a game's needs, options, mood weights, members and
|
|
38
|
+
schedules once and returns a running cast: one `tick(dt)` for everyone, one target selector for the whole system,
|
|
39
|
+
a fallback declared once, and a plain-JSON `save()`/`load()` that rides the game's own save.
|
|
40
|
+
- `--quick` on `bun run shoot` and `bun run drive` is the 960x540 iteration preset: one settled frame (a 2s settle window in drive), no video, no perf pass, written as `<name>-quick.png` and timed on its own line. It refuses a 1600x900 pixel claim, a frame-time claim, a non-desktop device and a recording flag by name instead of producing weaker evidence; full stays the default for PR shots. On a warm daemon a two-shot `drive` of ember-command went 9.3s to 8.2s and 4316KB to 1383KB of PNG.
|
|
41
|
+
- `createPlayScript` / `runPlayScript` (`@jgengine/shell/play`) play a real `PlayableGame` headlessly from a tick-based script: `move`, `aim`, `fire`, `use`, `wait` and `command` steps plus `setup`/`onTick` hooks, `expect` assertions read after setup and on their own tick, a tick timeline, and an FNV digest for replay comparison. It replays the shell's own frame pipeline — held movement through the walk controller, a bound action resolving to its command on press and repeat, interact prompts, hotbar — so a script proves the same code path a player's hands do. No browser, no wall clock, fixed `dt`.
|
|
42
|
+
- `bun run simulate <game> <script>` runs a committed `<game>/scripts/*.play.ts` against a game under `./Games`, prints the timeline, names a failed assertion and exits 1. It merges the game's own tsconfig `paths` with this checkout's source packages, so the game imports and the engine under test are one module graph.
|
|
43
|
+
- `HeadlessRunner.aim()` steers the walk controller mid-run; `HeadlessRunnerOptions` gains `seed` (deterministic `ctx.rng`) and `collision` (the shell's voxel config, previously dropped on the headless path).
|
|
44
|
+
|
|
45
|
+
Play scripts prove play in seconds instead of minutes of browser play, and the three proofs land beside their games: Scrap Signal (an enemy takes damage and closes in), Beacon Bastion (a composed wave is reached, its bounty scales, a boss phase flips) and Harbor Heat (heat is witness-gated, the search area is set, a courier is swept).
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- The graphics quality tier now costs what it claims on the render path. A tier's post chain renders at its own resolution scale (`low` 0.7, `medium` 0.85, `high`/`ultra` native) instead of always at the canvas size, and a game's authored `shadowMapSize` and `cascades` are a ceiling the tier caps rather than a value it ignored — every reference game authored 3 cascades and 1024/2048, so no tier changed a shadow before this. `low` and `medium` also stretch the far cascades' refresh interval, and the smallest caster worth a depth draw grows as the map shrinks. `high` and `ultra` keep native resolution, the authored shadow sizes and the measured cadence, so their frames are unchanged.
|
|
50
|
+
- Measured uncapped at 1600x900: whole-frame draws at `low` fell 53% in harbor-heat (515 → 242) and 63% in drift-foundry (1,098 → 408), with the shadow pass there down from 911 ms to 344 ms over a 6 s window; `high` and `ultra` held their draw counts.
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- Terrain build no longer scans every road and path segment for every vertex. A baked path corridor, a draped road and a painted polyline region each answer "how far is this point?" from a precomputed segment grid, so Scrap Signal's world build drops from 16.6 s to 0.51 s on the same machine and its longest boot stall from 11.6–18.4 s to 1.2 s. Sampled heights and palettes are bit-identical to the previous full scan, and `withPathProfiles` still composes profiles in order.
|
|
55
|
+
- Load-time shader warmup compiles against the render state the first frame actually draws with. A scene whose post chain renders into its own buffer was linking two programs per material, one linear untone-mapped set the frame draws and one canvas set nothing ever drew; the cover no longer waits for the second set. Harbor Heat links 84 programs to 58 and Scrap Signal 124 to 83 before the cover lifts, with no visual change.
|
|
56
|
+
|
|
57
|
+
### Performance
|
|
58
|
+
|
|
59
|
+
- `createTerrainPaletteSampler` (`@jgengine/core/world/terrain`) tests only the material regions whose painted core or blend ring covers a sample, so a world with hundreds of authored regions no longer pays for all of them at every vertex.
|
|
60
|
+
|
|
61
|
+
## 0.35.0
|
|
62
|
+
|
|
63
|
+
### Migrate
|
|
64
|
+
|
|
65
|
+
- **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`.
|
|
66
|
+
|
|
67
|
+
### Added
|
|
68
|
+
|
|
69
|
+
- `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.
|
|
70
|
+
- `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.
|
|
71
|
+
- `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.
|
|
72
|
+
- `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.
|
|
73
|
+
- `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.
|
|
74
|
+
- `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.
|
|
75
|
+
- `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.
|
|
76
|
+
|
|
77
|
+
### Changed
|
|
78
|
+
|
|
79
|
+
- 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.
|
|
80
|
+
- 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.
|
|
81
|
+
|
|
82
|
+
### Fixed
|
|
83
|
+
|
|
84
|
+
- 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.
|
|
85
|
+
- 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.
|
|
86
|
+
- A crowd batch's clock now follows the game clock when one is bound, so a paused game no longer keeps its crowd walking.
|
|
87
|
+
|
|
29
88
|
## 0.34.0
|
|
30
89
|
|
|
31
90
|
### Migrate
|
|
@@ -34,6 +93,7 @@ At publish, rename this heading to the new version and mirror the entries into
|
|
|
34
93
|
|
|
35
94
|
### Added
|
|
36
95
|
|
|
96
|
+
- `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
97
|
- `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
98
|
|
|
39
99
|
## 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;
|