@jgengine/shell 0.33.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 +38 -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 +21 -3
- package/skills/jgengine-world/capabilities.md +8 -0
- package/skills/jgengine-world/reference.md +1 -1
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { GraphicsProfile } from "@jgengine/core/settings/graphicsProfile";
|
|
2
|
+
/** Shadow map size when neither the game nor a profile names one. @internal */
|
|
3
|
+
export declare const DEFAULT_SHADOW_MAP_SIZE = 1024;
|
|
4
|
+
/** Cascades when neither the game nor a profile names a count. @internal */
|
|
5
|
+
export declare const DEFAULT_CASCADES = 3;
|
|
6
|
+
/**
|
|
7
|
+
* Shadow map size a tier will pay for. A game's authored size is a ceiling for its art, not a floor
|
|
8
|
+
* under the player's tier, so the profile caps it: a game built for 2048 renders 512 on `low`.
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
export declare function resolveShadowMapSize(profile?: GraphicsProfile, authored?: number): number;
|
|
12
|
+
/**
|
|
13
|
+
* Cascades a tier will pay for, capped the same way. `low` asks for one, which the lighting tree
|
|
14
|
+
* mounts as a single shadow-casting light instead of a cascade rig.
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveCascadeCount(profile?: GraphicsProfile, authored?: number): number;
|
|
18
|
+
/**
|
|
19
|
+
* Multiplier on the far cascades' refresh interval, by shadow map size. A tier that halves its map
|
|
20
|
+
* also accepts a far cascade lagging a frame or two longer: at 1024 and below the depth pass is
|
|
21
|
+
* cheap per cascade but there are several of them, and a half-res silhouette moving is harder to
|
|
22
|
+
* see than a full-res one. `high` and `ultra` keep the cadence they had.
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
25
|
+
export declare const SHADOW_REFRESH_SCALE_BY_MAP_SIZE: Readonly<Record<number, number>>;
|
|
26
|
+
/** Refresh-interval multiplier for a shadow map of `mapSize`. @internal */
|
|
27
|
+
export declare function shadowRefreshScale(mapSize: number): number;
|
|
28
|
+
/**
|
|
29
|
+
* Map size at which a caster's silhouette is still a few texels wide, the reference
|
|
30
|
+
* {@link MIN_SHADOW_CASTER_RADIUS} was measured at. A smaller map resolves less, so the smallest
|
|
31
|
+
* shadow worth a depth draw grows with it.
|
|
32
|
+
* @internal
|
|
33
|
+
*/
|
|
34
|
+
export declare const SHADOW_CASTER_RADIUS_REFERENCE_MAP = 2048;
|
|
35
|
+
/** Smallest caster radius worth a depth draw at `mapSize`, in metres. @internal */
|
|
36
|
+
export declare function minShadowCasterRadius(mapSize: number, reference?: number): number;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Shadow map size when neither the game nor a profile names one. @internal */
|
|
2
|
+
export const DEFAULT_SHADOW_MAP_SIZE = 1024;
|
|
3
|
+
/** Cascades when neither the game nor a profile names a count. @internal */
|
|
4
|
+
export const DEFAULT_CASCADES = 3;
|
|
5
|
+
/**
|
|
6
|
+
* Shadow map size a tier will pay for. A game's authored size is a ceiling for its art, not a floor
|
|
7
|
+
* under the player's tier, so the profile caps it: a game built for 2048 renders 512 on `low`.
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
export function resolveShadowMapSize(profile, authored) {
|
|
11
|
+
const wanted = authored ?? profile?.shadowMapSize ?? DEFAULT_SHADOW_MAP_SIZE;
|
|
12
|
+
return profile === undefined ? wanted : Math.min(wanted, profile.shadowMapSize);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Cascades a tier will pay for, capped the same way. `low` asks for one, which the lighting tree
|
|
16
|
+
* mounts as a single shadow-casting light instead of a cascade rig.
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
export function resolveCascadeCount(profile, authored) {
|
|
20
|
+
const wanted = authored ?? profile?.cascades ?? DEFAULT_CASCADES;
|
|
21
|
+
return profile === undefined ? wanted : Math.min(wanted, profile.cascades);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Multiplier on the far cascades' refresh interval, by shadow map size. A tier that halves its map
|
|
25
|
+
* also accepts a far cascade lagging a frame or two longer: at 1024 and below the depth pass is
|
|
26
|
+
* cheap per cascade but there are several of them, and a half-res silhouette moving is harder to
|
|
27
|
+
* see than a full-res one. `high` and `ultra` keep the cadence they had.
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
export const SHADOW_REFRESH_SCALE_BY_MAP_SIZE = {
|
|
31
|
+
512: 2,
|
|
32
|
+
1024: 2,
|
|
33
|
+
2048: 1,
|
|
34
|
+
4096: 1,
|
|
35
|
+
};
|
|
36
|
+
/** Refresh-interval multiplier for a shadow map of `mapSize`. @internal */
|
|
37
|
+
export function shadowRefreshScale(mapSize) {
|
|
38
|
+
return SHADOW_REFRESH_SCALE_BY_MAP_SIZE[mapSize] ?? 1;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Map size at which a caster's silhouette is still a few texels wide, the reference
|
|
42
|
+
* {@link MIN_SHADOW_CASTER_RADIUS} was measured at. A smaller map resolves less, so the smallest
|
|
43
|
+
* shadow worth a depth draw grows with it.
|
|
44
|
+
* @internal
|
|
45
|
+
*/
|
|
46
|
+
export const SHADOW_CASTER_RADIUS_REFERENCE_MAP = 2048;
|
|
47
|
+
/** Smallest caster radius worth a depth draw at `mapSize`, in metres. @internal */
|
|
48
|
+
export function minShadowCasterRadius(mapSize, reference = 0.5) {
|
|
49
|
+
return reference * Math.max(1, SHADOW_CASTER_RADIUS_REFERENCE_MAP / mapSize);
|
|
50
|
+
}
|
package/dist/world/WorldScene.js
CHANGED
|
@@ -23,7 +23,7 @@ import { MeasuredBoundsGroup } from "../render/measureBounds.js";
|
|
|
23
23
|
import { EntitySprite, IsolatedEntityModel } from "../render/SceneModels.js";
|
|
24
24
|
import { CharacterCrowd } from "../render/CharacterCrowd.js";
|
|
25
25
|
import { CharacterCrowdRegistry, crowdEligible, crowdKindOf, crowdKindReady, } from "../render/crowdRegistry.js";
|
|
26
|
-
import { characterLodTier, clearCharacterLodTier, setCharacterLodTier, } from "../render/characterLod.js";
|
|
26
|
+
import { characterLodTier, characterRigTier, clearCharacterLodTier, setCharacterLodTier, } from "../render/characterLod.js";
|
|
27
27
|
import { resolveEntityModel, resolveObjectModel } from "../render/resolveModel.js";
|
|
28
28
|
import { useRenderVisibility } from "../visibility/CullingProvider.js";
|
|
29
29
|
import { writePoseIfChanged, writeRenderPoseIfChanged, emptyPoseMemo } from "./entityPose.js";
|
|
@@ -34,6 +34,7 @@ const GROUND_SIZE = 160;
|
|
|
34
34
|
const GROUND_SEGMENTS = 80;
|
|
35
35
|
function EntityMarker({ entity, custom, model, sprite, isLocal, targeted, selected, crowd, onSelect, }) {
|
|
36
36
|
const groupRef = useRef(null);
|
|
37
|
+
const modelRef = useRef(null);
|
|
37
38
|
const ctx = useGameContext();
|
|
38
39
|
const visibleRef = useRenderVisibility();
|
|
39
40
|
const entityId = entity.id;
|
|
@@ -53,21 +54,33 @@ function EntityMarker({ entity, custom, model, sprite, isLocal, targeted, select
|
|
|
53
54
|
if (live === null)
|
|
54
55
|
return;
|
|
55
56
|
writeRenderPoseIfChanged(group, ctx, entityId, live, scratchRef.current, memoRef.current);
|
|
57
|
+
const shown = live.hidden !== true && visibleRef.current(entityId);
|
|
56
58
|
let tier;
|
|
57
|
-
|
|
59
|
+
let batched = false;
|
|
60
|
+
if (crowdModel !== undefined && crowd !== undefined && crowdKind !== undefined && crowdKindReady(crowdKind) && shown) {
|
|
58
61
|
const [x, y, z] = live.position;
|
|
59
62
|
const distance = Math.sqrt((state.camera.position.x - x) ** 2 + (state.camera.position.y - y) ** 2 + (state.camera.position.z - z) ** 2);
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
const lod = crowdModel.lod === false ? undefined : crowdModel.lod;
|
|
64
|
+
tier = characterLodTier(distance, lod, tierRef.current);
|
|
62
65
|
if (tier === "crowd") {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
// A painted character keeps its own rig: its strokes live on its materials, not in the batch.
|
|
67
|
+
const painted = ctx.scene.entity.paint.version(entityId) !== 0;
|
|
68
|
+
batched = !painted && crowd.claim(entityId, crowdKind, { x, y, z, rotationY: live.rotationY ?? 0, speed: groundSpeed(live) });
|
|
69
|
+
// An over-cap batch leaves the rest of its kind on their own rigs at the tier their distance
|
|
70
|
+
// earns, so a crowded scene loses draws rather than characters.
|
|
71
|
+
if (!batched)
|
|
72
|
+
tier = characterRigTier(distance, lod);
|
|
67
73
|
}
|
|
74
|
+
setCharacterLodTier(entityId, tier);
|
|
68
75
|
}
|
|
76
|
+
if (!batched)
|
|
77
|
+
crowd?.delete(entityId);
|
|
69
78
|
tierRef.current = tier;
|
|
70
|
-
|
|
79
|
+
// The marker itself stays mounted and visible while a batch draws the character: its status
|
|
80
|
+
// rings and the hit test on its rig have to behave as they do when it draws itself.
|
|
81
|
+
if (modelRef.current !== null)
|
|
82
|
+
modelRef.current.visible = !batched;
|
|
83
|
+
group.visible = shown;
|
|
71
84
|
});
|
|
72
85
|
useEffect(() => () => {
|
|
73
86
|
crowd?.delete(entityId);
|
|
@@ -80,7 +93,7 @@ function EntityMarker({ entity, custom, model, sprite, isLocal, targeted, select
|
|
|
80
93
|
const live = ctx.scene.entity.get(entityId);
|
|
81
94
|
if (live !== null)
|
|
82
95
|
onSelect(live);
|
|
83
|
-
}, children: [selected ? (_jsxs("mesh", { "rotation-x": -Math.PI / 2, "position-y": 0.02, children: [_jsx("ringGeometry", { args: [0.8, 0.95, 32] }), _jsx("meshBasicMaterial", { color: "#34d399", transparent: true, opacity: 0.9 })] })) : null, custom !== undefined && custom !== null ? (_jsx(MeasuredBoundsGroup, { target: "entity", measureKey: name, children: custom })) : model !== undefined ? (_jsx(IsolatedEntityModel, { model: model, instanceId: entityId, measure: { target: "entity", key: name }, fallback: sprite !== undefined ? _jsx(EntitySprite, { sprite: sprite }) : undefined })) : sprite !== undefined ? (_jsx(EntitySprite, { sprite: sprite })) : role === "prop" ? (_jsxs("mesh", { "position-y": 0.5, castShadow: true, receiveShadow: true, children: [_jsx("sphereGeometry", { args: [0.45, 24, 24] }), _jsx(DefaultSurface, { color: color })] })) : (_jsxs("group", { scale: ctx.scene.entity.visualScaleOf(entityId), children: [_jsxs("mesh", { "position-y": 0.95, castShadow: true, receiveShadow: true, children: [_jsx("capsuleGeometry", { args: [0.35, 1.1, 6, 14] }), _jsx(DefaultSurface, { color: color, roughness: 0.6 })] }), _jsxs("mesh", { position: [0, 1.35, 0.32], children: [_jsx("boxGeometry", { args: [0.16, 0.16, 0.16] }), _jsx("meshStandardMaterial", { color: "#f8fafc" })] })] })), targeted ? (_jsxs("mesh", { "rotation-x": -Math.PI / 2, "position-y": 0.03, children: [_jsx("ringGeometry", { args: [0.6, 0.75, 28] }), _jsx("meshBasicMaterial", { color: "#f87171" })] })) : null] }));
|
|
96
|
+
}, children: [selected ? (_jsxs("mesh", { "rotation-x": -Math.PI / 2, "position-y": 0.02, children: [_jsx("ringGeometry", { args: [0.8, 0.95, 32] }), _jsx("meshBasicMaterial", { color: "#34d399", transparent: true, opacity: 0.9 })] })) : null, _jsx("group", { ref: modelRef, children: custom !== undefined && custom !== null ? (_jsx(MeasuredBoundsGroup, { target: "entity", measureKey: name, children: custom })) : model !== undefined ? (_jsx(IsolatedEntityModel, { model: model, instanceId: entityId, measure: { target: "entity", key: name }, fallback: sprite !== undefined ? _jsx(EntitySprite, { sprite: sprite }) : undefined })) : sprite !== undefined ? (_jsx(EntitySprite, { sprite: sprite })) : role === "prop" ? (_jsxs("mesh", { "position-y": 0.5, castShadow: true, receiveShadow: true, children: [_jsx("sphereGeometry", { args: [0.45, 24, 24] }), _jsx(DefaultSurface, { color: color })] })) : (_jsxs("group", { scale: ctx.scene.entity.visualScaleOf(entityId), children: [_jsxs("mesh", { "position-y": 0.95, castShadow: true, receiveShadow: true, children: [_jsx("capsuleGeometry", { args: [0.35, 1.1, 6, 14] }), _jsx(DefaultSurface, { color: color, roughness: 0.6 })] }), _jsxs("mesh", { position: [0, 1.35, 0.32], children: [_jsx("boxGeometry", { args: [0.16, 0.16, 0.16] }), _jsx("meshStandardMaterial", { color: "#f8fafc" })] })] })) }), targeted ? (_jsxs("mesh", { "rotation-x": -Math.PI / 2, "position-y": 0.03, children: [_jsx("ringGeometry", { args: [0.6, 0.75, 28] }), _jsx("meshBasicMaterial", { color: "#f87171" })] })) : null] }));
|
|
84
97
|
}
|
|
85
98
|
function ObjectMarker({ object, custom, model, style, }) {
|
|
86
99
|
const groupRef = useRef(null);
|
|
@@ -237,7 +250,11 @@ function WorldActors({ entitySprites, entityModels, objectModels, objectStyles,
|
|
|
237
250
|
if (crowdEligible(model))
|
|
238
251
|
crowdModels.set(crowdKindOf(model), model);
|
|
239
252
|
return (_jsx(EntityMarker, { entity: entity, custom: custom, model: model, sprite: sprite, isLocal: entityId === controlledId, targeted: entityId === targetId, selected: selectedIds.has(entityId), crowd: crowdRegistry, onSelect: handleSelect }, entityId));
|
|
240
|
-
}), crowdModels.size > 0 ? _jsx(CharacterCrowd, { models: [...crowdModels.values()], registry: crowdRegistry
|
|
253
|
+
}), crowdModels.size > 0 ? (_jsx(CharacterCrowd, { models: [...crowdModels.values()], registry: crowdRegistry, onPick: (entityId) => {
|
|
254
|
+
const live = ctx.scene.entity.get(entityId);
|
|
255
|
+
if (live !== null)
|
|
256
|
+
handleSelect(live);
|
|
257
|
+
} })) : null, grouped.singles.map((candidate) => renderSingle(candidate.object)), grouped.batches.map((batch) => _jsx(StaticObjectModels, { batch: batch, single: renderSingle }, batch.key))] }));
|
|
241
258
|
}
|
|
242
259
|
export function WorldView({ entitySprites, entityModels, objectModels, objectStyles, environment, assets, renderEntity, renderObject, selectedIds, hideLocalActor, editorLayers, }) {
|
|
243
260
|
return (_jsxs(_Fragment, { children: [_jsx(WindClock, {}), _jsx(WorldEnvironment, { environment: environment }), _jsx(WorldActors, { entitySprites: entitySprites, entityModels: entityModels, objectModels: objectModels, objectStyles: objectStyles, assets: assets, renderEntity: renderEntity, renderObject: renderObject, selectedIds: selectedIds, hideLocalActor: hideLocalActor, editorLayers: editorLayers })] }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgengine/shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.0",
|
|
4
4
|
"description": "Game player shell for JGengine: React Three Fiber canvas, orbit camera, input tracking, HUD mounting, GameUiPreview, and a demo game. Consumers supply a GameRegistry.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
"skills"
|
|
17
17
|
],
|
|
18
18
|
"exports": {
|
|
19
|
+
"./play": {
|
|
20
|
+
"types": "./dist/play/index.d.ts",
|
|
21
|
+
"default": "./dist/play/index.js"
|
|
22
|
+
},
|
|
19
23
|
"./weather": {
|
|
20
24
|
"types": "./dist/weather/index.d.ts",
|
|
21
25
|
"default": "./dist/weather/index.js"
|
|
@@ -35,9 +39,9 @@
|
|
|
35
39
|
"test": "bun test src"
|
|
36
40
|
},
|
|
37
41
|
"dependencies": {
|
|
38
|
-
"@jgengine/core": "^0.
|
|
39
|
-
"@jgengine/react": "^0.
|
|
40
|
-
"@jgengine/ws": "^0.
|
|
42
|
+
"@jgengine/core": "^0.35.0",
|
|
43
|
+
"@jgengine/react": "^0.35.0",
|
|
44
|
+
"@jgengine/ws": "^0.35.0"
|
|
41
45
|
},
|
|
42
46
|
"peerDependencies": {
|
|
43
47
|
"@react-three/drei": "^10.0.0",
|
package/skills/jgengine/api.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
- `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
|
|
8
8
|
- `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
|
|
9
|
-
- `VERSION` (const): const VERSION: "0.
|
|
9
|
+
- `VERSION` (const): const VERSION: "0.35.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
10
10
|
|
|
11
11
|
## @jgengine/core/authoring
|
|
12
12
|
|
|
@@ -285,7 +285,7 @@
|
|
|
285
285
|
|
|
286
286
|
- `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
|
|
287
287
|
- `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
|
|
288
|
-
- `VERSION` (const): const VERSION: "0.
|
|
288
|
+
- `VERSION` (const): const VERSION: "0.35.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
289
289
|
|
|
290
290
|
## @jgengine/core/runtime/adapter
|
|
291
291
|
|
|
@@ -436,7 +436,7 @@
|
|
|
436
436
|
- `HeadlessCommandInterface` (interface): interface HeadlessCommandInterface — Canvas-free command/intent surface for {@link HeadlessRunner} — invoke a game's registered UI commands and read the resulting reactive state off `ctx`.
|
|
437
437
|
- `HeadlessInput` (interface): interface HeadlessInput — One step's worth of player intent handed to {@link HeadlessRunner.step} — the held-action set and pointer state the shell would otherwise publish from the browser each frame.
|
|
438
438
|
- `HeadlessRunner` (interface): interface HeadlessRunner — A renderer-free driver for a game loop: builds a {@link GameContext} from a {@link GameDefinition}, runs the init hooks, then advances the simulation one step at a time from injected input. No React, R3F, or three.js — the whole play path (time, input, `onTick`, behaviour nav, optional player movement) runs from `core` primitives alone, so a non-React host (a server tick, a test, a CLI replay) can play a real game and read its world snapshot. The shell's FrameDriver is one such driver bolted to `useFrame`; this is the same step distilled out of the render tree.
|
|
439
|
-
- `HeadlessRunnerOptions` (interface): interface HeadlessRunnerOptions<TAssetRef extends ModelAssetRef, TMultiplayer> { definition: GameDefinition<TAssetRef, TMultiplayer>; content?: GameContextContent; loop?: GameLoop<GameContext>; player?: {userId: string; isNew: boolean}; now?: () => number;
|
|
439
|
+
- `HeadlessRunnerOptions` (interface): interface HeadlessRunnerOptions<TAssetRef extends ModelAssetRef, TMultiplayer> { definition: GameDefinition<TAssetRef, TMultiplayer>; content?: GameContextContent; loop?: GameLoop<GameContext>; player?: {userId: string; isNew: boolean}; now?: () => number; seed?: string | number; maxStepSeconds?: number; playerMovemen… — ⚠ undocumented
|
|
440
440
|
- `createHeadlessRunner` (function): function createHeadlessRunner<TAssetRef extends ModelAssetRef, TMultiplayer>(options: HeadlessRunnerOptions<TAssetRef, TMultiplayer>): HeadlessRunner — ⚠ undocumented
|
|
441
441
|
|
|
442
442
|
## @jgengine/core/runtime/hostPersistence
|
|
@@ -2162,10 +2162,13 @@
|
|
|
2162
2162
|
|
|
2163
2163
|
## @jgengine/shell/audio/audioEngine
|
|
2164
2164
|
|
|
2165
|
-
- `
|
|
2165
|
+
- `AudioAmbienceState` (interface): interface AudioAmbienceState — Current ambience mix, by zone id.
|
|
2166
|
+
- `AudioEmitterHandle` (interface): interface AudioEmitterHandle { isPlaying?(): boolean; setPosition(position: Vec3): void; setRate(rate: number): void; setGain(gain: number): void; setLowpass(hz: number): void; setHighpass(hz: number): void; setVelocity(velocity: Vec3): void; setOcclusion(amount: numbe… — ⚠ undocumented
|
|
2166
2167
|
- `AudioEngine` (interface): interface AudioEngine { setListenerPose(pose: ListenerPose | Vec3): void; setVoiceLimit(maxTotal: number, overflow?: VoiceAllocatorConfig["overflow"]): void; play(soundId: string, position?: Vec3): void; playOneShot(soundId: string, position?: Vec3): void; playL… — ⚠ undocumented · used by `createAudioEngine`: Game-owned sound catalog playback with bounded SFX reservations, spatial panning, and shared buses.
|
|
2167
2168
|
- `AudioLevel` (interface): interface AudioLevel — Master-output level, `0..1`. Pooled by {@link AudioEngine.measure} - read it, don't retain it.
|
|
2168
|
-
- `
|
|
2169
|
+
- `AudioOcclusionStats` (interface): interface AudioOcclusionStats — Measured geometry-occlusion work, cumulative since the engine was created.
|
|
2170
|
+
- `AudioSceneConfig` (interface): interface AudioSceneConfig { sounds?: Record<string, SoundDef>; maxVoices?: number; voiceOverflow?: VoiceAllocatorConfig["overflow"]; voiceAllocator?: VoiceAllocator; buses?: Record<string, AudioBusDef>; music?: Record<string, MusicTheme>; musicBus?: string; zones?: … — ⚠ undocumented · used by `createAudioEngine`: Game-owned sound catalog playback with bounded SFX reservations, spatial panning, and shared buses.
|
|
2171
|
+
- `DuckOptions` (interface): interface DuckOptions — How long and how far to duck buses for {@link AudioEngine.duck}.
|
|
2169
2172
|
- `ListenerPose` (interface): interface ListenerPose — Position and orientation for a spatial-audio listener.
|
|
2170
2173
|
- `MAX_PENDING_CUES` (const): const MAX_PENDING_CUES: 16 — Cues held while the browser keeps the audio context suspended, before any user gesture.
|
|
2171
2174
|
- `Vec3` (interface): interface Vec3 { x: number; y: number; z: number } — ⚠ undocumented · used by `coverPoints` (@jgengine/core/ai/tacticalQueries): Returns boundary locations that are hidden from a threat.
|
|
@@ -2174,7 +2177,7 @@
|
|
|
2174
2177
|
## @jgengine/shell/audio/audioProbe
|
|
2175
2178
|
|
|
2176
2179
|
- `AudioProbe` (interface): interface AudioProbe — The read-only handle a capture script or devtools uses to prove the mix is producing sound.
|
|
2177
|
-
- `installAudioProbe` (function): function installAudioProbe(engine: AudioEngine): () => void — Publish the audio engine's own level
|
|
2180
|
+
- `installAudioProbe` (function): function installAudioProbe(engine: AudioEngine): () => void — Publish the audio engine's own level, ambience and occlusion readouts on `window.__jgengineAudio`, so a headless capture can state measured values instead of guessing from a screenshot. Reads only; it never plays or changes a bus.
|
|
2178
2181
|
|
|
2179
2182
|
## @jgengine/shell/audio/busMix
|
|
2180
2183
|
|
|
@@ -2233,6 +2236,11 @@
|
|
|
2233
2236
|
|
|
2234
2237
|
- `playMusicNote` (function): function playMusicNote(ctx: Ctx, event: NoteEvent, when: number, spb: number, out: GainNode, transpose = 0): void — Play one theme note through its instrument voice into `out` (a per-layer gain node the director wires to master + reverb). `spb` is seconds-per-beat and `transpose` shifts the note in semitones for per-zone key changes. This is the engine's reusable instrument library; unknown instruments fall back to a sine.
|
|
2235
2238
|
|
|
2239
|
+
## @jgengine/shell/audio/occlusion
|
|
2240
|
+
|
|
2241
|
+
- `BlockingRaycastSource` (interface): interface BlockingRaycastSource — The narrow slice of the scene a line-of-hearing test needs.
|
|
2242
|
+
- `sceneLineBlocked` (function): function sceneLineBlocked(source: BlockingRaycastSource, from: Vec3, to: Vec3): boolean — Whether blocking world geometry (objects and wall segments, never creatures or terrain) lies between the listener and an emitter. One scene query per call; the engine owns how often it runs.
|
|
2243
|
+
|
|
2236
2244
|
## @jgengine/shell/audio/synthEngine
|
|
2237
2245
|
|
|
2238
2246
|
- `SynthPlayback` (interface): interface SynthPlayback — Scheduled procedural sources; stopping releases their source/filter/envelope graph.
|
|
@@ -2713,6 +2721,62 @@
|
|
|
2713
2721
|
- `resolvePeerShellMultiplayer` (function): function resolvePeerShellMultiplayer(args: ResolvePeerShellMultiplayerArgs): Promise<ShellMultiplayer & { close: () => Promise<void> }> — Opens a peer session. With `playable`, one host runs the game loop and guests mirror its shared world. Await `close()` to finish authoritative persistence. Same-origin rooms use BroadcastChannel signaling.
|
|
2714
2722
|
- `resolveShellMultiplayer` (function): function resolveShellMultiplayer(args: ResolveShellMultiplayerArgs): ShellMultiplayer | null — ⚠ undocumented
|
|
2715
2723
|
|
|
2724
|
+
## @jgengine/shell/play
|
|
2725
|
+
|
|
2726
|
+
- `PLAY_MOVE_ACTIONS` (const): const PLAY_MOVE_ACTIONS: Readonly<Record<PlayMove, string>> — Movement action ids a {@link PlayMove} holds, so a script walks through the same controller a player does.
|
|
2727
|
+
- `PlayAimStep` (interface): interface PlayAimStep extends PlayStepBase — Point the walk controller and the aim payload at a yaw/pitch, in radians.
|
|
2728
|
+
- `PlayCommandStep` (interface): interface PlayCommandStep extends PlayStepBase — Run a registered command directly, with whatever input it declares.
|
|
2729
|
+
- `PlayExpectation` (interface): interface PlayExpectation — One assertion about world state. Read happens twice — right after `setup` and on the assertion's own tick — so a script can prove change ("health dropped") as well as a level ("wave is 2").
|
|
2730
|
+
- `PlayExpectationResult` (interface): interface PlayExpectationResult — One assertion's verdict, with both readings so a failure says what changed and by how much.
|
|
2731
|
+
- `PlayFireStep` (interface): interface PlayFireStep extends PlayStepBase — Hold the `fire` action: the bound `fire` command runs on the press tick, then on its repeat cadence.
|
|
2732
|
+
- `PlayMove` (type): type PlayMove = "forward" | "back" | "left" | "right" — Direction a scripted step walks, using the shell's own movement actions.
|
|
2733
|
+
- `PlayMoveStep` (interface): interface PlayMoveStep extends PlayStepBase — Hold a movement direction for `ticks` — the shell walk controller's own held actions.
|
|
2734
|
+
- `PlayRunOptions` (interface): interface PlayRunOptions — Options for one scripted play run.
|
|
2735
|
+
- `PlayRunResult` (interface): interface PlayRunResult — Everything a scripted run produced: the timeline, the assertion verdicts, and a digest of the timeline for replay comparison.
|
|
2736
|
+
- `PlayScript` (interface): interface PlayScript — A script to play against a real game: what to do, on which tick, and what must be true afterwards.
|
|
2737
|
+
- `PlayScriptSpec` (interface): interface PlayScriptSpec — What {@link createPlayScript} accepts: the same script with the derived fields optional.
|
|
2738
|
+
- `PlayStep` (type): type PlayStep = PlayMoveStep | PlayAimStep | PlayFireStep | PlayUseStep | PlayWaitStep | PlayCommandStep — One tick-based action in a script.
|
|
2739
|
+
- `PlayTimelineEntry` (interface): interface PlayTimelineEntry — One row of what happened, on the tick it happened.
|
|
2740
|
+
- `PlayTimelineKind` (type): type PlayTimelineKind = "step" | "command" | "event" | "check" — What a timeline row records: a scripted action, a command result, a world event, or an assertion reading.
|
|
2741
|
+
- `PlayUseStep` (interface): interface PlayUseStep extends PlayStepBase — Press one bound action id once (default one tick) — `interact`, `reload`, `useAbility`, a game's own action.
|
|
2742
|
+
- `PlayValue` (type): type PlayValue = number | string | boolean | null — A reading an assertion compares: a stat, a distance, a count, a phase id.
|
|
2743
|
+
- `PlayWaitStep` (interface): interface PlayWaitStep extends PlayStepBase — Hold nothing for `ticks`, letting the world run.
|
|
2744
|
+
- `createPlayScript` (function): function createPlayScript(spec: PlayScriptSpec): PlayScript — Validate and normalize a script: whole-tick timings, sorted steps, a derived run length.
|
|
2745
|
+
- `formatPlayTimelineEntry` (function): function formatPlayTimelineEntry(entry: PlayTimelineEntry): string — One compact timeline line: `t=120 event combat.damage to=bruiser amount=12 remaining=54`.
|
|
2746
|
+
- `playTimelineDigest` (function): function playTimelineDigest(timeline: readonly PlayTimelineEntry[]): string — FNV-1a over the timeline's canonical JSON. Cheap, dependency-free and stable across runs and machines, which is all a replay comparison needs.
|
|
2747
|
+
- `runPlayScript` (function): function runPlayScript(playable: PlayableGame, spec: PlayScript | PlayScriptSpec, options: PlayRunOptions = {}): PlayRunResult — Play a real game without a browser: boot its `PlayableGame` through the headless runner, apply a tick-based {@link PlayScript}, and return what happened plus whether the script's assertions held.
|
|
2748
|
+
|
|
2749
|
+
## @jgengine/shell/play/run
|
|
2750
|
+
|
|
2751
|
+
- `PlayRunOptions` (interface): interface PlayRunOptions — Options for one scripted play run.
|
|
2752
|
+
- `runPlayScript` (function): function runPlayScript(playable: PlayableGame, spec: PlayScript | PlayScriptSpec, options: PlayRunOptions = {}): PlayRunResult — Play a real game without a browser: boot its `PlayableGame` through the headless runner, apply a tick-based {@link PlayScript}, and return what happened plus whether the script's assertions held.
|
|
2753
|
+
|
|
2754
|
+
## @jgengine/shell/play/script
|
|
2755
|
+
|
|
2756
|
+
- `PLAY_MOVE_ACTIONS` (const): const PLAY_MOVE_ACTIONS: Readonly<Record<PlayMove, string>> — Movement action ids a {@link PlayMove} holds, so a script walks through the same controller a player does.
|
|
2757
|
+
- `PlayAimStep` (interface): interface PlayAimStep extends PlayStepBase — Point the walk controller and the aim payload at a yaw/pitch, in radians.
|
|
2758
|
+
- `PlayCommandStep` (interface): interface PlayCommandStep extends PlayStepBase — Run a registered command directly, with whatever input it declares.
|
|
2759
|
+
- `PlayExpectation` (interface): interface PlayExpectation — One assertion about world state. Read happens twice — right after `setup` and on the assertion's own tick — so a script can prove change ("health dropped") as well as a level ("wave is 2").
|
|
2760
|
+
- `PlayFireStep` (interface): interface PlayFireStep extends PlayStepBase — Hold the `fire` action: the bound `fire` command runs on the press tick, then on its repeat cadence.
|
|
2761
|
+
- `PlayMove` (type): type PlayMove = "forward" | "back" | "left" | "right" — Direction a scripted step walks, using the shell's own movement actions.
|
|
2762
|
+
- `PlayMoveStep` (interface): interface PlayMoveStep extends PlayStepBase — Hold a movement direction for `ticks` — the shell walk controller's own held actions.
|
|
2763
|
+
- `PlayScript` (interface): interface PlayScript — A script to play against a real game: what to do, on which tick, and what must be true afterwards.
|
|
2764
|
+
- `PlayScriptSpec` (interface): interface PlayScriptSpec — What {@link createPlayScript} accepts: the same script with the derived fields optional.
|
|
2765
|
+
- `PlayStep` (type): type PlayStep = PlayMoveStep | PlayAimStep | PlayFireStep | PlayUseStep | PlayWaitStep | PlayCommandStep — One tick-based action in a script.
|
|
2766
|
+
- `PlayUseStep` (interface): interface PlayUseStep extends PlayStepBase — Press one bound action id once (default one tick) — `interact`, `reload`, `useAbility`, a game's own action.
|
|
2767
|
+
- `PlayValue` (type): type PlayValue = number | string | boolean | null — A reading an assertion compares: a stat, a distance, a count, a phase id.
|
|
2768
|
+
- `PlayWaitStep` (interface): interface PlayWaitStep extends PlayStepBase — Hold nothing for `ticks`, letting the world run.
|
|
2769
|
+
- `createPlayScript` (function): function createPlayScript(spec: PlayScriptSpec): PlayScript — Validate and normalize a script: whole-tick timings, sorted steps, a derived run length.
|
|
2770
|
+
|
|
2771
|
+
## @jgengine/shell/play/timeline
|
|
2772
|
+
|
|
2773
|
+
- `PlayExpectationResult` (interface): interface PlayExpectationResult — One assertion's verdict, with both readings so a failure says what changed and by how much.
|
|
2774
|
+
- `PlayRunResult` (interface): interface PlayRunResult — Everything a scripted run produced: the timeline, the assertion verdicts, and a digest of the timeline for replay comparison.
|
|
2775
|
+
- `PlayTimelineEntry` (interface): interface PlayTimelineEntry — One row of what happened, on the tick it happened.
|
|
2776
|
+
- `PlayTimelineKind` (type): type PlayTimelineKind = "step" | "command" | "event" | "check" — What a timeline row records: a scripted action, a command result, a world event, or an assertion reading.
|
|
2777
|
+
- `formatPlayTimelineEntry` (function): function formatPlayTimelineEntry(entry: PlayTimelineEntry): string — One compact timeline line: `t=120 event combat.damage to=bruiser amount=12 remaining=54`.
|
|
2778
|
+
- `playTimelineDigest` (function): function playTimelineDigest(timeline: readonly PlayTimelineEntry[]): string — FNV-1a over the timeline's canonical JSON. Cheap, dependency-free and stable across runs and machines, which is all a replay comparison needs.
|
|
2779
|
+
|
|
2716
2780
|
## @jgengine/shell/pointer/PointerOverlays
|
|
2717
2781
|
|
|
2718
2782
|
- `ContextMenuView` (function): function ContextMenuView({ menu, x, y, onPick, onClose, }: { menu: ContextMenu; x: number; y: number; onPick: (verb: ContextVerb) => void; onClose: () => void; }): React.JSX.Element — ⚠ undocumented
|
|
@@ -2770,7 +2834,7 @@
|
|
|
2770
2834
|
|
|
2771
2835
|
## @jgengine/shell/render/CharacterCrowd
|
|
2772
2836
|
|
|
2773
|
-
- `CharacterCrowd` (function): function CharacterCrowd({ models, registry, fps = 24 }: CharacterCrowdProps): React.JSX.Element — Draws
|
|
2837
|
+
- `CharacterCrowd` (function): function CharacterCrowd({ models, registry, fps = 24, onPick }: CharacterCrowdProps): React.JSX.Element — Draws characters through the GPU batch path: one baked rig per character kind, one instanced draw per material, every member's clip, time offset and speed sampled in the vertex shader. The entity markers decide who is batched (their `lod` tiers and the batch capacity) and write poses into `registry`; this layer owns the bakes, the materials and the instance buffers. Mount it in the same canvas as the entity markers; unmount it to fall back to per-entity rigs.
|
|
2774
2838
|
- `CharacterCrowdProps` (interface): interface CharacterCrowdProps — Props for {@link CharacterCrowd}.
|
|
2775
2839
|
|
|
2776
2840
|
## @jgengine/shell/render/EntityPreview
|
|
@@ -2809,7 +2873,7 @@
|
|
|
2809
2873
|
- `SkinnedCrowdSource` (interface): interface SkinnedCrowdSource — A rig to bake: a loaded scene with skinned meshes and its clips.
|
|
2810
2874
|
- `SkinnedInstances` (function): function SkinnedInstances({ url, instances, clips, once, fps, modelScale = 1, castShadow = true, receiveShadow = true, timeScale = 1, }: SkinnedInstancesProps): React.JSX.Element — Renders many animated copies of one rigged model in a single instanced draw per material: the rig's clips are baked to a bone-matrix texture once, and each instance samples its own clip, time offset and speed on the GPU. For crowds, spectators and distant NPCs; a character that needs blending, IK or attachments stays an entity model.
|
|
2811
2875
|
- `SkinnedInstancesProps` (interface): interface SkinnedInstancesProps — Props for {@link SkinnedInstances}.
|
|
2812
|
-
- `bakeSkinnedCrowd` (function): function bakeSkinnedCrowd(source: SkinnedCrowdSource, options: BakeSkinnedCrowdOptions = {}): BakedSkinnedCrowd — Bakes a rig for {@link SkinnedInstances}: merges its
|
|
2876
|
+
- `bakeSkinnedCrowd` (function): function bakeSkinnedCrowd(source: SkinnedCrowdSource, options: BakeSkinnedCrowdOptions = {}): BakedSkinnedCrowd — Bakes a rig for {@link SkinnedInstances}: merges its meshes into one geometry per material, samples every requested clip at `fps`, and writes each slot's model-space matrix into a float texture laid out by `planBoneTexture`. A skinned mesh contributes its bones as slots; a rig-less part that a clip moves contributes that node, with its vertices weighted fully to it, so a node-transform character (a walking miniature with rotating limbs) batches through the same shader. A model with no clips at all bakes to `animated: false`: one merged geometry per material and no texture. Meshes with several materials keep only the first. Exported for tests and custom crowd renderers.
|
|
2813
2877
|
- `createCrowdUniforms` (function): function createCrowdUniforms(baked: BakedSkinnedCrowd): CrowdUniforms — Builds the uniforms a crowd material reads: the texture and per-clip start, frames, fps and loop.
|
|
2814
2878
|
- `patchCrowdMaterial` (function): function patchCrowdMaterial<T extends THREE.Material>(material: T, uniforms: CrowdUniforms): T — Patches a material so each instance skins itself from the crowd texture: per-instance `crowdPlay` is (clip index, time offset, speed). Works on any built-in material, the depth material for shadows included.
|
|
2815
2879
|
|
|
@@ -2833,14 +2897,20 @@
|
|
|
2833
2897
|
- `characterLodInterval` (function): function characterLodInterval(tier: CharacterLodTier): number — Frames between pose updates for a tier: 1, 2, 4, and `0` meaning "never" for the crowd tier.
|
|
2834
2898
|
- `characterLodRate` (function): function characterLodRate(tier: CharacterLodTier): number — Share of frames a tier poses the rig: 1, 0.5, 0.25, and 0 once the GPU crowd draws it.
|
|
2835
2899
|
- `characterLodTier` (function): function characterLodTier(distance: number, config: ModelLodConfig | undefined, previous?: CharacterLodTier): CharacterLodTier — The tier a character at `distance` falls in. Passing the tier it held last frame applies the crowd hysteresis, so a character walking along the crowd boundary does not swap representation every frame.
|
|
2900
|
+
- `characterRigTier` (function): function characterRigTier(distance: number, config: ModelLodConfig | undefined): CharacterLodTier — The tier a character poses its own rig on, ignoring the crowd path. A crowd character whose batch is full falls back to this, so an over-cap crowd draws every member on its own rig instead of dropping the ones past the cap.
|
|
2901
|
+
|
|
2902
|
+
## @jgengine/shell/render/crowdBatch
|
|
2903
|
+
|
|
2904
|
+
- `poseTimeOffset` (function): function poseTimeOffset(id: string): number — A stable non-zero time offset for a member with no recorded pose, so a crowd is not in lockstep.
|
|
2836
2905
|
|
|
2837
2906
|
## @jgengine/shell/render/crowdRegistry
|
|
2838
2907
|
|
|
2839
2908
|
- `CharacterCrowdRegistry` (class): class CharacterCrowdRegistry — The characters the crowd layer is currently drawing, written by their entity markers each frame. It holds poses rather than React state: 71 pedestrians writing matrices must not re-render the world, and the crowd batch reads the same entries back to fill its instance buffers.
|
|
2840
|
-
- `crowdClipForSpeed` (function): function crowdClipForSpeed(clips: readonly string[], speed: number): string | undefined — The baked clip a crowd member at `speed` plays; clips are `[idle, walk, run]` as {@link crowdClipsOf} orders them.
|
|
2841
|
-
- `crowdClipsOf` (function): function crowdClipsOf(model: ModelConfig, clipNames: readonly string[]): string[] — The clips a crowd batch bakes for a model: its resolved idle, walk and run, in that order. Empty when the rig's clip names do not resolve to locomotion roles, which leaves the character on its own rig instead of drawing a frozen crowd copy.
|
|
2842
|
-
- `crowdEligible` (function): function crowdEligible(model: ModelConfig | undefined): model is ModelConfig — Whether a model can leave its own rig for the GPU crowd path: a rigged, clip-played, plain model.
|
|
2909
|
+
- `crowdClipForSpeed` (function): function crowdClipForSpeed(clips: readonly string[], speed: number, bands?: CrowdSpeedBands): string | undefined — The baked clip a crowd member at `speed` plays; clips are `[idle, walk, run]` as {@link crowdClipsOf} orders them. `bands` is the model's own walk/run speed.
|
|
2910
|
+
- `crowdClipsOf` (function): function crowdClipsOf(model: ModelConfig, clipNames: readonly string[]): string[] — The clips a crowd batch bakes for a model: its resolved idle, walk and run, in that order. Empty when the model plays no clips at all (a static character) or when the rig's clip names do not resolve to locomotion roles, which leaves the character on its own rig instead of drawing a frozen crowd copy.
|
|
2911
|
+
- `crowdEligible` (function): function crowdEligible(model: ModelConfig | undefined): model is ModelConfig — Whether a model can leave its own rig for the GPU crowd path: one whose drawn result a batch can reproduce. A rigged, clip-played, plain model qualifies, and so does a model with no animation at all (a static crowd character) — it has no playback to lose. A model that needs per-entity materials, wind, hidden nodes, IK, attachments or a part composition keeps its own rig.
|
|
2843
2912
|
- `crowdKindOf` (function): function crowdKindOf(model: ModelConfig): string — The identity a character model shares with every other model the GPU crowd can draw: rig url, height normalization and material tint. Two entity kinds with the same key can share one instanced draw; a different tint or height needs its own.
|
|
2913
|
+
- `crowdSpeedBandsOf` (function): function crowdSpeedBandsOf(model: ModelConfig, clipNames: readonly string[]): CrowdSpeedBands — The ground speeds the model's own locomotion graph switches clips at, so a batched character walks and idles at the same speed its own rig would. A model that declares none keeps the engine's defaults.
|
|
2844
2914
|
|
|
2845
2915
|
## @jgengine/shell/render/decalGeometry
|
|
2846
2916
|
|
|
@@ -2944,7 +3014,7 @@
|
|
|
2944
3014
|
|
|
2945
3015
|
## @jgengine/shell/render/shaderWarmup
|
|
2946
3016
|
|
|
2947
|
-
- `SHADER_WARMUP_DEFAULTS` (const): const SHADER_WARMUP_DEFAULTS: { readonly quietMs:
|
|
3017
|
+
- `SHADER_WARMUP_DEFAULTS` (const): const SHADER_WARMUP_DEFAULTS: { readonly quietMs: 250; readonly planBudgetMs: 12000; readonly planFloorMs: 1500; readonly pollMs: 100; readonly backgroundMs: 2000; readonly backgroundEveryMs: 250; } — Defaults sized to cover one load: the covered wait is the driver's link time, not a guess.
|
|
2948
3018
|
- `ShaderWarmup` (interface): interface ShaderWarmup — A warmup in flight: its promise, its latest state, and a way to stop it.
|
|
2949
3019
|
- `ShaderWarmupOptions` (interface): interface ShaderWarmupOptions — Options for {@link createShaderWarmup}; the clock, sleep and loader signal are injectable for tests.
|
|
2950
3020
|
- `ShaderWarmupRenderer` (interface): interface ShaderWarmupRenderer — The renderer surface the warmup needs; a real `WebGLRenderer` satisfies it.
|
|
@@ -72,7 +72,7 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
72
72
|
|
|
73
73
|
- `attachWaveFeedback` (function) · `import { attachWaveFeedback } from "@jgengine/shell/audio/eventFeedback"`
|
|
74
74
|
|
|
75
|
-
## audio-probe — read the live master RMS/peak
|
|
75
|
+
## audio-probe — read the live master RMS/peak, bus gains, ambience zone mix and occlusion cost from a running page for capture and diagnostics
|
|
76
76
|
|
|
77
77
|
- `installAudioProbe` (function) · `import { installAudioProbe } from "@jgengine/shell/audio/audioProbe"`
|
|
78
78
|
|
|
@@ -142,7 +142,7 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
142
142
|
|
|
143
143
|
- `captureCanvas` (function) · `import { captureCanvas } from "@jgengine/shell/render/sceneCapture"`
|
|
144
144
|
|
|
145
|
-
## character-crowd — draw
|
|
145
|
+
## character-crowd — draw animated or static characters as batched instanced draws instead of posing each skeleton
|
|
146
146
|
|
|
147
147
|
- `CharacterCrowd` (function) · `import { CharacterCrowd } from "@jgengine/shell/render/CharacterCrowd"`
|
|
148
148
|
|
|
@@ -736,6 +736,11 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
736
736
|
|
|
737
737
|
- `PlacementGhost` (function) · `import { PlacementGhost } from "@jgengine/shell/structures"`
|
|
738
738
|
|
|
739
|
+
## play-script — write a game's behaviour as a tick-based script with declarative assertions instead of playing it in a browser
|
|
740
|
+
|
|
741
|
+
- `createPlayScript` (function) · `import { createPlayScript } from "@jgengine/shell/play"`
|
|
742
|
+
- `runPlayScript` (function) · `import { runPlayScript } from "@jgengine/shell/play"`
|
|
743
|
+
|
|
739
744
|
## popover — viewport-flipping, viewport-clamped anchored popover/tooltip shell
|
|
740
745
|
|
|
741
746
|
- `Popover` (function) · `import { Popover } from "@jgengine/react"`
|
|
@@ -106,8 +106,10 @@
|
|
|
106
106
|
- `DriverState` (interface): interface DriverState — Serializable per-driver state advanced in place by {@link driveStep}.
|
|
107
107
|
- `DriverStep` (interface): interface DriverStep extends AxisInput — {@link driveStep}'s result: the axis sample to feed the vehicle sim, plus arrival.
|
|
108
108
|
- `DriverTuning` (interface): interface DriverTuning — Vehicle-feel numbers the game supplies; everything has a sensible default except `maxSpeed`.
|
|
109
|
+
- `PaceBand` (interface): interface PaceBand — Catch-up pacing for one car in a field: the gap it tries to hold to a reference car (a rival, the player, a convoy lead) and how its pace bends when it drifts off that gap. Past `margin` by `tolerance` it sheds up to `ease` of its pace; short of it by `tolerance` it gains up to `push`; inside that tolerance it leaves the car alone. The push never outdrives `DriverTuning.maxSpeed`, so a band changes how hard a car tries, never what it is built to do.
|
|
109
110
|
- `createDriverState` (function): function createDriverState(targetX = 0, targetZ = 0): DriverState — Fresh driver state committed to an initial target (usually the spawn-time goal or the spawn position itself).
|
|
110
|
-
- `driveStep` (function): function driveStep(state: DriverState, dt: number, pose: DriverPose, goal: DriverGoal, profile: DifficultyProfile, tuning: DriverTuning, rng: () => number, obstacleAhead: number | null = null): DriverStep — Advance one driving step, mutating `state`, and return the {@link AxisInput} to feed the vehicle sim. `obstacleAhead` is the caller-measured distance to the nearest blocker in the driving corridor this step (`null`/omitted when clear) — the driver perceives it late by `speed * reactionSeconds`, so easy tiers brake at the last moment and expert tiers with proper margin. The steering target is a delay-line over the goal — resampled every `reactionSeconds`, so it trails a moving goal by up to that long, which is what makes an easy chaser visibly cut toward where the target *was*. When throttle produces no motion for a tier-scaled while the driver reverses out with counter-steer (brake past standstill — the shared sim's reverse).
|
|
111
|
+
- `driveStep` (function): function driveStep(state: DriverState, dt: number, pose: DriverPose, goal: DriverGoal, profile: DifficultyProfile, tuning: DriverTuning, rng: () => number, obstacleAhead: number | null = null): DriverStep — Advance one driving step, mutating `state`, and return the {@link AxisInput} to feed the vehicle sim. `obstacleAhead` is the caller-measured distance to the nearest blocker in the driving corridor this step (`null`/omitted when clear) — the driver perceives it late by `speed * reactionSeconds`, so easy tiers brake at the last moment and expert tiers with proper margin. The steering target is a delay-line over the goal — resampled every `reactionSeconds`, so it trails a moving goal by up to that long, which is what makes an easy chaser visibly cut toward where the target *was*. A {@link PaceBand} on the goal eases a car ahead of its margin and pushes one behind it, bounded by the car's own top speed. When throttle produces no motion for a tier-scaled while the driver reverses out with counter-steer (brake past standstill — the shared sim's reverse).
|
|
112
|
+
- `paceBandFactor` (function): function paceBandFactor(band: PaceBand, gap: number): number — The pace multiplier a {@link PaceBand} asks for at `gap`: `1` inside the tolerance, `1 - ease * scale` fully ahead of it, `1 + push * scale` fully behind it, ramped linearly across `easeAt` / `pushAt` in between. Pure scalar math, so a band is deterministic and costs no allocation per step; the result never goes negative.
|
|
111
113
|
- `pathTargetAhead` (function): function pathTargetAhead(path: readonly DrivePathPoint[], x: number, z: number, lookahead: number, loop = false): DrivePathPoint | null — The pure-pursuit target for street/route following: the point `lookahead` meters further along the polyline from wherever the vehicle currently projects onto it. Feed the result to {@link driveStep} as the goal each tick and the driver follows the road; `loop` wraps a circuit (traffic block, race lap) instead of pinning at the final point. Returns `null` for a degenerate path. Give sharper tiers a longer `lookahead` to cut corners cleanly and duller tiers a short one so they steer segment-to-segment.
|
|
112
114
|
|
|
113
115
|
## @jgengine/core/ai/flock
|
|
@@ -453,13 +455,22 @@
|
|
|
453
455
|
- `sumMagnitude` (function): function sumMagnitude<P>(memberships: readonly AreaMembership<P>[], magnitudeOf: MagnitudeOf<P>): number — Sum a numeric magnitude across memberships — additive aggregation for a total field strength (total damage per tick, total slow). A terminal reducer, not a filter, so it returns the number.
|
|
454
456
|
- `uniqueByStackKey` (function): function uniqueByStackKey<P>(magnitudeOf?: MagnitudeOf<P>): AreaStackPolicy<P> — Keep at most one membership per `stackKey`. With `magnitudeOf` the strongest per key wins (ties broken by `sourceId` for determinism); without it the first-seen per key wins. Use for unique-by-key buffs where reapplying the same aura should not stack.
|
|
455
457
|
|
|
458
|
+
## @jgengine/core/audio/ambience
|
|
459
|
+
|
|
460
|
+
- `AmbienceWeight` (interface): interface AmbienceWeight — One zone's share of the ambience mix at a listener position, 0..1.
|
|
461
|
+
- `AmbienceZoneDef` (interface): interface AmbienceZoneDef — One ambience zone: a sound bed keyed to a volume of the scene.
|
|
462
|
+
- `AmbienceZoneShape` (type): type AmbienceZoneShape = | { kind: "sphere"; center: readonly [number, number, number]; radius: number } | { kind: "box"; min: readonly [number, number, number]; max: readonly [number, number, number] } — A world-space volume whose sound bed plays while the listener is inside it.
|
|
463
|
+
- `activeAmbienceZone` (function): function activeAmbienceZone(weights: readonly AmbienceWeight[]): string | null — The loudest zone in a weight list, or null when every zone is silent.
|
|
464
|
+
- `ambienceWeights` (function): function ambienceWeights(zones: readonly AmbienceZoneDef[], x: number, y: number, z: number, out: AmbienceWeight[] = []): AmbienceWeight[] — Every zone's mix weight at a listener position. A zone is at full level `crossfade` units inside its boundary, at half level on the boundary and silent `crossfade` units outside, so a listener walking between two zones that share a boundary hears a constant-sum handover. Weights that would sum above 1 (overlapping zones) are normalized so total bed level never exceeds unity. Writes into `out` when given, so a per-frame caller allocates nothing.
|
|
465
|
+
- `zoneSignedDistance` (function): function zoneSignedDistance(shape: AmbienceZoneShape, x: number, y: number, z: number): number — Distance from a point to a zone boundary, positive inside and negative outside. Spheres measure from the surface; boxes use the nearest face.
|
|
466
|
+
|
|
456
467
|
## @jgengine/core/audio/audioFalloff
|
|
457
468
|
|
|
458
469
|
- `AudioBusDef` (interface): interface AudioBusDef { id: AudioBusId; gain?: number } — ⚠ undocumented · used by `withFeedbackCueSounds` (@jgengine/core/audio/feedbackCues): Merge the default feedback reactions into a game's sound catalog.
|
|
459
470
|
- `AudioBusId` (type): type AudioBusId = string — ⚠ undocumented
|
|
460
471
|
- `AudioFalloffConfig` (interface): interface AudioFalloffConfig { minDistance?: number; maxDistance?: number; curve?: FalloffCurve } — ⚠ undocumented
|
|
461
472
|
- `FalloffCurve` (type): type FalloffCurve = "linear" | "inverse" | "none" — ⚠ undocumented
|
|
462
|
-
- `SoundDef` (interface): interface SoundDef { id: string; url?: string; synth?: SynthPatch; bus: AudioBusId; gain?: number; loop?: boolean; maxVoices?: number; priority?: number;
|
|
473
|
+
- `SoundDef` (interface): interface SoundDef { id: string; url?: string; synth?: SynthPatch; bus: AudioBusId; gain?: number; loop?: boolean; maxVoices?: number; priority?: number; duck?: {buses?: readonly string[]; amount?: number; seconds?: number;}; positional?: boolean; falloff?: A… — ⚠ undocumented · used by `DEFAULT_FEEDBACK_CUE_SOUNDS` (@jgengine/core/audio/feedbackCues): Short procedural reactions - no samples to load, so every game has impact feedback without authoring audio.
|
|
463
474
|
- `computeFalloffGain` (function): function computeFalloffGain(distance: number, config: AudioFalloffConfig = {}): number — ⚠ undocumented
|
|
464
475
|
- `distance3` (function): function distance3(a: { x: number; y: number; z: number }, b: { x: number; y: number; z: number }): number — ⚠ undocumented
|
|
465
476
|
- `resolveEmitterGain` (function): function resolveEmitterGain(distance: number, sound: Pick<SoundDef, "gain" | "positional" | "falloff">, busGain: number): number — ⚠ undocumented
|
|
@@ -511,6 +522,13 @@
|
|
|
511
522
|
- `createMusicState` (function): function createMusicState(): MusicStateSnapshot — Fresh state with no tier entered, so the first resolve always settles and reports `changed`.
|
|
512
523
|
- `resolveMusicState` (function): function resolveMusicState(state: MusicStateSnapshot, input: MusicStateInput, config: MusicStateConfig): MusicStateResult — Pick the theme for the current intensity. Escalation is immediate; de-escalation waits for the tier's `holdMs` and needs the intensity to fall a `release` margin below the tier it is leaving.
|
|
513
524
|
|
|
525
|
+
## @jgengine/core/audio/occlusion
|
|
526
|
+
|
|
527
|
+
- `AUDIO_OCCLUSION_DEFAULTS` (const): const AUDIO_OCCLUSION_DEFAULTS: { readonly maxTestsPerTick: 8; readonly intervalMs: 150; readonly attenuation: 0.55; readonly lowpassHz: 1200; readonly maxDistance: 60; } — Defaults for {@link AudioOcclusionConfig}.
|
|
528
|
+
- `AudioOcclusionConfig` (interface): interface AudioOcclusionConfig — Geometry occlusion for retained positional loops: a cheap line test between the listener and an emitter behind blocking geometry, bounded so a busy scene can never turn audio into a raycast pass. The shell runs the test against the scene's blockable colliders; the engine applies the resulting attenuation and low-pass to the loop it already owns.
|
|
529
|
+
- `ResolvedAudioOcclusionConfig` (interface): interface ResolvedAudioOcclusionConfig — A fully specified occlusion policy, with every unset field replaced by its default.
|
|
530
|
+
- `resolveOcclusionConfig` (function): function resolveOcclusionConfig(config: AudioOcclusionConfig | false = {}): ResolvedAudioOcclusionConfig — Fill in the occlusion defaults and clamp each field to a workable range: a finite test budget, a non-negative retest interval and distance cap, and an attenuation in `0..1`.
|
|
531
|
+
|
|
514
532
|
## @jgengine/core/audio/stepCadence
|
|
515
533
|
|
|
516
534
|
- `StepCadenceConfig` (interface): interface StepCadenceConfig — Distance-driven footstep cadence. Pure state-in/state-out: feed it how far a mover travelled this frame and it tells you how many steps landed, so foley stays locked to ground distance instead of a wall-clock timer that desyncs when speed changes.
|
|
@@ -2857,7 +2875,7 @@
|
|
|
2857
2875
|
- `SnapMode` (type): type SnapMode = "grid" | "free" | "surface" — ⚠ undocumented
|
|
2858
2876
|
- `SnowEnvironmentDescriptor` (type): type SnowEnvironmentDescriptor = { kind: "snow" } & Required< Pick<SnowEnvironmentConfig, "area" | "density" | "speed" | "flakeSize" | "drift" | "wind" | "color" | "opacity"> > — ⚠ undocumented · used by `snow`: Declares a snowfall weather effect for `environment()` — area, density, drift, wind, and flake opacity.
|
|
2859
2877
|
- `SoilRules` (interface): interface SoilRules — Fully-defaulted soil params parsed from a volume's `meta`.
|
|
2860
|
-
- `SoundDef` (interface): interface SoundDef { id: string; url?: string; synth?: SynthPatch; bus: AudioBusId; gain?: number; loop?: boolean; maxVoices?: number; priority?: number;
|
|
2878
|
+
- `SoundDef` (interface): interface SoundDef { id: string; url?: string; synth?: SynthPatch; bus: AudioBusId; gain?: number; loop?: boolean; maxVoices?: number; priority?: number; duck?: {buses?: readonly string[]; amount?: number; seconds?: number;}; positional?: boolean; falloff?: A… — ⚠ undocumented · used by `DEFAULT_FEEDBACK_CUE_SOUNDS` (@jgengine/core/audio/feedbackCues): Short procedural reactions - no samples to load, so every game has impact feedback without authoring audio.
|
|
2861
2879
|
- `SpatialGrid` (class): class SpatialGrid — A uniform-grid broad-phase over the x/z plane, separate from the rigid-body sim, for cheap same-tick proximity across hundreds–thousands of simple movers (swarm enemies). Rebuild each tick from the caller's own position arrays, then `queryCircle` (enemies hitting the player / an AoE) or `forEachPair` (mutual separation). Both are precise: no false negatives, no false positives beyond the exact distance test.
|
|
2862
2880
|
- `SpawnDirectorConfig` (interface): interface SpawnDirectorConfig { waves: readonly WaveManifest[]; maxAlive?: number; escalationPerSecond?: number; alertBudgetPerSecond?: number; alertDecayPerSecond?: number; playerBudgetPerSecond?: number; maxSpawnsPerTick?: number; loop?: boolean; seed?: number; spawnP… — ⚠ undocumented
|
|
2863
2881
|
- `SpawnDirectorState` (interface): interface SpawnDirectorState { wave: number; elapsed: number; waveElapsed: number; budget: number; alert: number; spawnedThisWave: number; spawnedTotal: number; rng: RandomSeed; done: boolean } — ⚠ undocumented
|
|
@@ -28,6 +28,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
28
28
|
|
|
29
29
|
- `measureAir` (function) · `import { measureAir } from "@jgengine/core/physics/handlingProbe"`
|
|
30
30
|
|
|
31
|
+
## ambience-zones — a scene declares looping sound beds keyed to world volumes; walking between them crossfades bed levels on the existing buses instead of cutting.
|
|
32
|
+
|
|
33
|
+
- `AmbienceZoneDef` (interface) · `import { AmbienceZoneDef } from "@jgengine/core/audio/ambience"`
|
|
34
|
+
|
|
31
35
|
## anim-graph — data-first animation state machine with blend trees, crossfades, layers, and clip events
|
|
32
36
|
|
|
33
37
|
- `createAnimGraphRuntime` (function) · `import { createAnimGraphRuntime } from "@jgengine/core/anim/animGraph"`
|
|
@@ -109,6 +113,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
109
113
|
|
|
110
114
|
- `defineAttackMoveOrder` (function) · `import { defineAttackMoveOrder } from "@jgengine/core/world"`
|
|
111
115
|
|
|
116
|
+
## audio-occlusion — an emitter behind blocking world geometry is attenuated and darkened by a budgeted line test, and falls back to distance-only once the tick's budget is spent.
|
|
117
|
+
|
|
118
|
+
- `AudioOcclusionConfig` (interface) · `import { AudioOcclusionConfig } from "@jgengine/core/audio/occlusion"`
|
|
119
|
+
|
|
112
120
|
## audio-voice-budget — bound simultaneous sound playback with per-sound caps, priorities, and deterministic stealing
|
|
113
121
|
|
|
114
122
|
- `createVoiceAllocator` (function) · `import { createVoiceAllocator } from "@jgengine/core/audio/voiceAllocator"`
|
|
@@ -413,7 +413,7 @@ Replace behavior data through `ctx.scene.entity.update(id, { behaviors })` or en
|
|
|
413
413
|
|
|
414
414
|
Compose, don't replace: scale `MobBrainConfig.aggroRadius` by `perceptionScale`, route `acquireTarget` scoring through `pickScored`, gate `advancePursuit` target swaps behind the reaction gate.
|
|
415
415
|
|
|
416
|
-
**Drive** — `ai/driver` is the vehicle consumer of the profile: `driveStep(state, dt, pose, goal, profile, tuning, rng, obstacleAhead?)` turns a pose + goal into the shared `AxisInput` (`throttle`/`brake`/`steer`) for `tickDrivableVehicle`. The profile decides how well the car drives: the steering target is a delay-line resampled every `reactionSeconds` (easy chasers cut to where the target *was*), corners shed speed instead of orbiting at full throttle, obstacle distance is perceived late by `speed × reactionSeconds`, the wheel wobbles by `executionJitter` (resampled on an interval, so it reads as a loose wheel), and a car grinding a wall backs out with counter-steer after a tier-scaled delay (brake past standstill is the sim's reverse). `createDriverState` is one small serializable object; the caller owns the sim, collision, and all spatial queries. For streets and circuits, `pathTargetAhead(path, x, z, lookahead, loop?)` walks a road/route polyline to the pure-pursuit point ahead — feed it back in as the goal each tick and the same brain follows authored roads; give sharper tiers a longer lookahead.
|
|
416
|
+
**Drive** — `ai/driver` is the vehicle consumer of the profile: `driveStep(state, dt, pose, goal, profile, tuning, rng, obstacleAhead?)` turns a pose + goal into the shared `AxisInput` (`throttle`/`brake`/`steer`) for `tickDrivableVehicle`. The profile decides how well the car drives: the steering target is a delay-line resampled every `reactionSeconds` (easy chasers cut to where the target *was*), corners shed speed instead of orbiting at full throttle, obstacle distance is perceived late by `speed × reactionSeconds`, the wheel wobbles by `executionJitter` (resampled on an interval, so it reads as a loose wheel), and a car grinding a wall backs out with counter-steer after a tier-scaled delay (brake past standstill is the sim's reverse). `createDriverState` is one small serializable object; the caller owns the sim, collision, and all spatial queries. For streets and circuits, `pathTargetAhead(path, x, z, lookahead, loop?)` walks a road/route polyline to the pure-pursuit point ahead — feed it back in as the goal each tick and the same brain follows authored roads; give sharper tiers a longer lookahead. Catch-up pacing is a `PaceBand` on the goal: the caller measures `paceGap` (positive = ahead of the reference) and the band's `margin`/`tolerance` leave a dead zone, `ease` sheds pace for a leader over `easeAt`, `push` adds it for a chaser over `pushAt`, and `scale` is the per-car share. `paceBandFactor(band, gap)` is the same rule on its own; the push never outdrives `tuning.maxSpeed`.
|
|
417
417
|
|
|
418
418
|
### Snapshot and restore on movement and visibility handles
|
|
419
419
|
|