@jgengine/shell 0.31.0 → 0.33.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 +67 -0
- package/dist/GamePlayerShell.js +7 -0
- package/dist/Shell3dPresentation.js +6 -4
- package/dist/ShellChrome.d.ts +3 -1
- package/dist/ShellChrome.js +3 -2
- package/dist/ShellHudPresentation.js +1 -1
- package/dist/audio/AudioComponents.js +61 -51
- package/dist/audio/CaptionOverlay.d.ts +15 -0
- package/dist/audio/CaptionOverlay.js +50 -0
- package/dist/audio/FeedbackHost.d.ts +14 -0
- package/dist/audio/FeedbackHost.js +46 -0
- package/dist/audio/audioEngine.d.ts +25 -0
- package/dist/audio/audioEngine.js +267 -32
- package/dist/audio/audioProbe.d.ts +22 -0
- package/dist/audio/audioProbe.js +20 -0
- package/dist/audio/busMix.d.ts +4 -0
- package/dist/audio/busMix.js +7 -0
- package/dist/audio/captions.d.ts +48 -0
- package/dist/audio/captions.js +77 -0
- package/dist/audio/emitterTracking.d.ts +40 -0
- package/dist/audio/emitterTracking.js +153 -0
- package/dist/audio/eventFeedback.d.ts +137 -0
- package/dist/audio/eventFeedback.js +273 -0
- package/dist/audio/gestureResume.d.ts +10 -0
- package/dist/audio/gestureResume.js +25 -0
- package/dist/audio/hapticsRoute.d.ts +28 -0
- package/dist/audio/hapticsRoute.js +59 -0
- package/dist/audio/loopParams.js +20 -10
- package/dist/audio/playableAudio.d.ts +1 -1
- package/dist/audio/playableAudio.js +3 -1
- package/dist/devtools/FrameTelemetry.d.ts +1 -1
- package/dist/devtools/FrameTelemetry.js +4 -2
- package/dist/devtools/agentBridge.js +13 -0
- package/dist/devtools/frameSampler.js +4 -0
- package/dist/drivers/CanvasCoverFrameloop.d.ts +13 -0
- package/dist/drivers/CanvasCoverFrameloop.js +22 -0
- package/dist/drivers/FrameRateLimiter.d.ts +2 -1
- package/dist/drivers/FrameRateLimiter.js +6 -3
- package/dist/postfx/PostProcessing.d.ts +78 -2
- package/dist/postfx/PostProcessing.js +149 -76
- package/dist/postfx/postfxOverlay.d.ts +14 -4
- package/dist/postfx/postfxOverlay.js +90 -10
- package/dist/postfx/shadowPrepass.d.ts +6 -2
- package/dist/postfx/shadowPrepass.js +7 -3
- package/dist/render/characterLod.js +6 -0
- package/dist/render/useFootIk.js +54 -15
- package/dist/render/useModelAnimation.js +22 -5
- package/dist/settings/appliedSettings.js +3 -0
- package/dist/settings/settingsController.d.ts +10 -1
- package/dist/settings/settingsController.js +94 -67
- package/dist/vision/RevealVision.d.ts +3 -1
- package/dist/vision/RevealVision.js +34 -15
- package/dist/world/WorldItems.js +11 -3
- package/package.json +4 -4
- package/skills/jgengine/api.md +3 -2
- package/skills/jgengine-ui/SKILL.md +1 -0
- package/skills/jgengine-ui/api.md +94 -24
- package/skills/jgengine-ui/capabilities.md +40 -0
- package/skills/jgengine-ui/reference-primitives.md +2 -0
- package/skills/jgengine-ui/reference.md +1 -1
- package/skills/jgengine-world/api.md +18 -8
- package/skills/jgengine-world/capabilities.md +4 -0
|
@@ -15,10 +15,20 @@ export declare function isPostfxOverlay(object: {
|
|
|
15
15
|
userData?: Record<string, unknown>;
|
|
16
16
|
}): boolean;
|
|
17
17
|
/**
|
|
18
|
-
* Hide every currently visible
|
|
19
|
-
* `out` (cleared first, no allocation on the hot path) so {@link restorePostfxOverlays} can
|
|
20
|
-
*
|
|
18
|
+
* Hide every currently visible object `scene`'s prepasses must skip, recording what was hidden into
|
|
19
|
+
* `out` (cleared first, no allocation on the hot path) so {@link restorePostfxOverlays} can undo
|
|
20
|
+
* exactly that set. `withUnmodelled` also hides points and lines, which three's GTAO pass excludes
|
|
21
|
+
* from its normal buffer but a depth prepass keeps.
|
|
22
|
+
*
|
|
23
|
+
* The set is cached and rescanned only when the scene graph changes: walking the whole scene per
|
|
24
|
+
* prepass cost 2.4 ms/frame in Drift Foundry and 0.6 ms/frame in Harbor Heat, and the graph changes
|
|
25
|
+
* when something mounts, not between frames.
|
|
21
26
|
*/
|
|
22
|
-
export declare function hidePostfxOverlays(scene: THREE.Object3D, out: THREE.Object3D[]): void;
|
|
27
|
+
export declare function hidePostfxOverlays(scene: THREE.Object3D, out: THREE.Object3D[], withUnmodelled?: boolean): void;
|
|
23
28
|
/** Restore visibility for the objects hidden by {@link hidePostfxOverlays} and clear the list. */
|
|
24
29
|
export declare function restorePostfxOverlays(hidden: THREE.Object3D[]): void;
|
|
30
|
+
/**
|
|
31
|
+
* Drop the cached set and its listeners for a scene that is going away, so a torn-down world does
|
|
32
|
+
* not keep its node graph reachable through this module.
|
|
33
|
+
*/
|
|
34
|
+
export declare function releasePostfxOverlayTracking(scene: THREE.Object3D): void;
|
|
@@ -13,19 +13,84 @@ export const POSTFX_OVERLAY_USERDATA = {
|
|
|
13
13
|
export function isPostfxOverlay(object) {
|
|
14
14
|
return object.userData?.jgPostfxOverlay === true;
|
|
15
15
|
}
|
|
16
|
+
/** Whether a normal-material prepass must leave `object` out: three's AO pass cannot describe these. */
|
|
17
|
+
function isUnmodelledDrawable(object) {
|
|
18
|
+
const drawable = object;
|
|
19
|
+
return drawable.isPoints === true || drawable.isLine === true || drawable.isLine2 === true;
|
|
20
|
+
}
|
|
21
|
+
const tracks = new WeakMap();
|
|
22
|
+
function unwatchAll(track, node) {
|
|
23
|
+
track.listeners.get(node)?.();
|
|
24
|
+
track.listeners.delete(node);
|
|
25
|
+
for (const child of node.children)
|
|
26
|
+
unwatchAll(track, child);
|
|
27
|
+
}
|
|
28
|
+
function watch(track, node) {
|
|
29
|
+
if (track.listeners.has(node))
|
|
30
|
+
return;
|
|
31
|
+
const added = ({ child }) => {
|
|
32
|
+
track.revision += 1;
|
|
33
|
+
watch(track, child);
|
|
34
|
+
};
|
|
35
|
+
const removed = ({ child }) => {
|
|
36
|
+
track.revision += 1;
|
|
37
|
+
unwatchAll(track, child);
|
|
38
|
+
};
|
|
39
|
+
node.addEventListener("childadded", added);
|
|
40
|
+
node.addEventListener("childremoved", removed);
|
|
41
|
+
track.listeners.set(node, () => {
|
|
42
|
+
node.removeEventListener("childadded", added);
|
|
43
|
+
node.removeEventListener("childremoved", removed);
|
|
44
|
+
});
|
|
45
|
+
for (const child of node.children)
|
|
46
|
+
watch(track, child);
|
|
47
|
+
}
|
|
48
|
+
function trackFor(scene) {
|
|
49
|
+
const existing = tracks.get(scene);
|
|
50
|
+
if (existing !== undefined)
|
|
51
|
+
return existing;
|
|
52
|
+
const track = { revision: 0, scanned: -1, overlays: [], drawables: [], listeners: new Map() };
|
|
53
|
+
tracks.set(scene, track);
|
|
54
|
+
watch(track, scene);
|
|
55
|
+
return track;
|
|
56
|
+
}
|
|
57
|
+
function rescan(scene, track) {
|
|
58
|
+
track.overlays.length = 0;
|
|
59
|
+
track.drawables.length = 0;
|
|
60
|
+
scene.traverse((object) => {
|
|
61
|
+
if (isPostfxOverlay(object))
|
|
62
|
+
track.overlays.push(object);
|
|
63
|
+
else if (isUnmodelledDrawable(object))
|
|
64
|
+
track.drawables.push(object);
|
|
65
|
+
});
|
|
66
|
+
track.scanned = track.revision;
|
|
67
|
+
}
|
|
68
|
+
function hideAll(objects, out) {
|
|
69
|
+
for (const object of objects) {
|
|
70
|
+
if (!object.visible)
|
|
71
|
+
continue;
|
|
72
|
+
object.visible = false;
|
|
73
|
+
out.push(object);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
16
76
|
/**
|
|
17
|
-
* Hide every currently visible
|
|
18
|
-
* `out` (cleared first, no allocation on the hot path) so {@link restorePostfxOverlays} can
|
|
19
|
-
*
|
|
77
|
+
* Hide every currently visible object `scene`'s prepasses must skip, recording what was hidden into
|
|
78
|
+
* `out` (cleared first, no allocation on the hot path) so {@link restorePostfxOverlays} can undo
|
|
79
|
+
* exactly that set. `withUnmodelled` also hides points and lines, which three's GTAO pass excludes
|
|
80
|
+
* from its normal buffer but a depth prepass keeps.
|
|
81
|
+
*
|
|
82
|
+
* The set is cached and rescanned only when the scene graph changes: walking the whole scene per
|
|
83
|
+
* prepass cost 2.4 ms/frame in Drift Foundry and 0.6 ms/frame in Harbor Heat, and the graph changes
|
|
84
|
+
* when something mounts, not between frames.
|
|
20
85
|
*/
|
|
21
|
-
export function hidePostfxOverlays(scene, out) {
|
|
86
|
+
export function hidePostfxOverlays(scene, out, withUnmodelled = false) {
|
|
22
87
|
out.length = 0;
|
|
23
|
-
scene
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
88
|
+
const track = trackFor(scene);
|
|
89
|
+
if (track.scanned !== track.revision)
|
|
90
|
+
rescan(scene, track);
|
|
91
|
+
hideAll(track.overlays, out);
|
|
92
|
+
if (withUnmodelled)
|
|
93
|
+
hideAll(track.drawables, out);
|
|
29
94
|
}
|
|
30
95
|
/** Restore visibility for the objects hidden by {@link hidePostfxOverlays} and clear the list. */
|
|
31
96
|
export function restorePostfxOverlays(hidden) {
|
|
@@ -33,3 +98,18 @@ export function restorePostfxOverlays(hidden) {
|
|
|
33
98
|
object.visible = true;
|
|
34
99
|
hidden.length = 0;
|
|
35
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Drop the cached set and its listeners for a scene that is going away, so a torn-down world does
|
|
103
|
+
* not keep its node graph reachable through this module.
|
|
104
|
+
*/
|
|
105
|
+
export function releasePostfxOverlayTracking(scene) {
|
|
106
|
+
const track = tracks.get(scene);
|
|
107
|
+
if (track === undefined)
|
|
108
|
+
return;
|
|
109
|
+
tracks.delete(scene);
|
|
110
|
+
for (const off of track.listeners.values())
|
|
111
|
+
off();
|
|
112
|
+
track.listeners.clear();
|
|
113
|
+
track.overlays.length = 0;
|
|
114
|
+
track.drawables.length = 0;
|
|
115
|
+
}
|
|
@@ -6,5 +6,9 @@ import type * as THREE from "three";
|
|
|
6
6
|
* @internal
|
|
7
7
|
*/
|
|
8
8
|
export declare function renderWithCurrentMatrices<T>(scene: THREE.Object3D, render: () => T): T;
|
|
9
|
-
/**
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Reuse the beauty pass's shadow maps and world matrices while drawing a scene prepass.
|
|
11
|
+
* `withUnmodelled` also hides points and lines, which GTAO's normal buffer excludes anyway.
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
export declare function renderShadowlessPrepass<T>(renderer: Pick<THREE.WebGLRenderer, "shadowMap">, scene: THREE.Object3D, hiddenOverlays: THREE.Object3D[], render: () => T, withUnmodelled?: boolean): T;
|
|
@@ -15,12 +15,16 @@ export function renderWithCurrentMatrices(scene, render) {
|
|
|
15
15
|
scene.matrixWorldAutoUpdate = autoUpdate;
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
-
/**
|
|
19
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Reuse the beauty pass's shadow maps and world matrices while drawing a scene prepass.
|
|
20
|
+
* `withUnmodelled` also hides points and lines, which GTAO's normal buffer excludes anyway.
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
export function renderShadowlessPrepass(renderer, scene, hiddenOverlays, render, withUnmodelled = false) {
|
|
20
24
|
const shadowMap = renderer.shadowMap;
|
|
21
25
|
const autoUpdate = shadowMap.autoUpdate;
|
|
22
26
|
const needsUpdate = shadowMap.needsUpdate;
|
|
23
|
-
hidePostfxOverlays(scene, hiddenOverlays);
|
|
27
|
+
hidePostfxOverlays(scene, hiddenOverlays, withUnmodelled);
|
|
24
28
|
shadowMap.autoUpdate = false;
|
|
25
29
|
shadowMap.needsUpdate = false;
|
|
26
30
|
try {
|
|
@@ -94,6 +94,12 @@ export function noteCharacterPose(instanceId) {
|
|
|
94
94
|
}
|
|
95
95
|
/** Publish the clip and time a rig is posing at, so the crowd can continue the same pose at the swap. @internal */
|
|
96
96
|
export function noteAnimationPhase(instanceId, clip, time) {
|
|
97
|
+
const published = phases.get(instanceId);
|
|
98
|
+
if (published !== undefined) {
|
|
99
|
+
published.clip = clip;
|
|
100
|
+
published.time = time;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
97
103
|
phases.set(instanceId, { clip, time });
|
|
98
104
|
}
|
|
99
105
|
/** The clip phase a character was last posed at, or `undefined` when it never posed. @internal */
|
package/dist/render/useFootIk.js
CHANGED
|
@@ -174,6 +174,14 @@ function restoreFootIkPose(rig) {
|
|
|
174
174
|
record.bone.position.copy(record.animated.p);
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Reused per frame by {@link applyFootIk}: the ground probes, their hits and the solver input are read
|
|
179
|
+
* synchronously inside one call, so a rig posing every frame allocates nothing.
|
|
180
|
+
*/
|
|
181
|
+
const probeInput = { origin: [0, 0, 0], direction: [0, -1, 0], maxDistance: 0 };
|
|
182
|
+
const samplesScratch = [];
|
|
183
|
+
const normalsScratch = [];
|
|
184
|
+
const samplePool = [];
|
|
177
185
|
/**
|
|
178
186
|
* Applies one frame of foot IK after the animation mixer: probes the ground under each foot,
|
|
179
187
|
* resolves targets with `placeFeet`, lowers the pelvis, solves each leg with `solveTwoBone` bending
|
|
@@ -187,8 +195,8 @@ export function applyFootIk(rig, originY, probe, state, delta, lookAtTarget) {
|
|
|
187
195
|
record.animated.q.copy(record.bone.quaternion);
|
|
188
196
|
record.animated.p.copy(record.bone.position);
|
|
189
197
|
}
|
|
190
|
-
|
|
191
|
-
|
|
198
|
+
samplesScratch.length = 0;
|
|
199
|
+
normalsScratch.length = 0;
|
|
192
200
|
let length = 0;
|
|
193
201
|
let ankleHeight = 0;
|
|
194
202
|
for (const leg of rig.legs) {
|
|
@@ -197,11 +205,19 @@ export function applyFootIk(rig, originY, probe, state, delta, lookAtTarget) {
|
|
|
197
205
|
length = Math.max(length, legSize);
|
|
198
206
|
ankleHeight = Math.max(ankleHeight, leg.ankleRatio * legSize);
|
|
199
207
|
const from = Math.max(scratch.tip.y, originY) + legSize * 0.5;
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
208
|
+
probeInput.origin[0] = scratch.tip.x;
|
|
209
|
+
probeInput.origin[1] = from;
|
|
210
|
+
probeInput.origin[2] = scratch.tip.z;
|
|
211
|
+
probeInput.maxDistance = legSize * 2;
|
|
212
|
+
const hit = probe(probeInput);
|
|
213
|
+
const index = samplesScratch.length;
|
|
214
|
+
const sample = samplePool[index] ?? (samplePool[index] = { ankleY: 0, groundY: null });
|
|
215
|
+
sample.ankleY = scratch.tip.y;
|
|
216
|
+
sample.groundY = hit === null ? null : hit.point[1];
|
|
217
|
+
samplesScratch.push(sample);
|
|
218
|
+
normalsScratch.push(hit === null ? null : hit.normal);
|
|
203
219
|
}
|
|
204
|
-
const placement = placeFeet({ feet:
|
|
220
|
+
const placement = placeFeet({ feet: samplesScratch, originY, ankleHeight, maxAdjust: rig.maxAdjust * length, airborneGap: length * 0.3 });
|
|
205
221
|
const blend = Math.min(1, delta * (placement.grounded ? 12 : 6));
|
|
206
222
|
state.weight += ((placement.grounded ? 1 : 0) - state.weight) * blend;
|
|
207
223
|
state.pelvis += (placement.pelvisOffset - state.pelvis) * Math.min(1, delta * 15);
|
|
@@ -230,9 +246,9 @@ export function applyFootIk(rig, originY, probe, state, delta, lookAtTarget) {
|
|
|
230
246
|
aimSegment(leg.root, root, mid, solved.mid);
|
|
231
247
|
// Rotating the thigh also moved the tip; aim the shin from that updated pose.
|
|
232
248
|
aimSegment(leg.mid, leg.mid.getWorldPosition(scratch.mid), leg.tip.getWorldPosition(scratch.tip), solved.tip);
|
|
233
|
-
const normal =
|
|
249
|
+
const normal = normalsScratch[i];
|
|
234
250
|
if (normal !== null && normal !== undefined && rig.alignToGround > 0) {
|
|
235
|
-
const planted = 1 - Math.min(1, Math.max(0,
|
|
251
|
+
const planted = 1 - Math.min(1, Math.max(0, samplesScratch[i].ankleY - ankleHeight - samplesScratch[i].groundY) / Math.max(ankleHeight, length * 0.05));
|
|
236
252
|
const tilt = scratch.q.setFromUnitVectors(UP, scratch.b.set(normal[0], normal[1], normal[2]).normalize());
|
|
237
253
|
const current = leg.tip.getWorldQuaternion(scratch.q2);
|
|
238
254
|
writeWorldQuaternion(leg.tip, current.slerp(tilt.multiply(current), planted * weight * rig.alignToGround));
|
|
@@ -251,6 +267,8 @@ export function applyFootIk(rig, originY, probe, state, delta, lookAtTarget) {
|
|
|
251
267
|
}
|
|
252
268
|
return placement.grounded;
|
|
253
269
|
}
|
|
270
|
+
/** Materials a foot probe may hit: blocking physical objects and terrain, never the model's own entity. */
|
|
271
|
+
const IK_FILTER = { entities: false, objects: true, walls: false, terrain: true };
|
|
254
272
|
/**
|
|
255
273
|
* Runs foot IK after the model's animation mixer for `ModelConfig.ik`. Ground probes hit terrain and
|
|
256
274
|
* blocking physical objects, never the model's own entity. `groundOffset` is the model-space height
|
|
@@ -262,6 +280,16 @@ export function useFootIk(scene, config, ctx, instanceId, groundOffset = 0) {
|
|
|
262
280
|
const state = useRef({ weight: 0, pelvis: 0 });
|
|
263
281
|
const lookAtConfig = config === undefined || config === "auto" ? undefined : config.lookAt;
|
|
264
282
|
const target = useRef(null);
|
|
283
|
+
// One probe input and one look-at triple per mount: a rig that poses every frame allocates nothing.
|
|
284
|
+
const probe = useMemo(() => ({
|
|
285
|
+
origin: [0, 0, 0],
|
|
286
|
+
direction: [0, -1, 0],
|
|
287
|
+
maxDistance: 0,
|
|
288
|
+
excludeInstanceIds: instanceId === undefined ? [] : [instanceId],
|
|
289
|
+
filter: IK_FILTER,
|
|
290
|
+
accept: (hit) => hit.blocks && hit.purpose === "physical",
|
|
291
|
+
}), [instanceId]);
|
|
292
|
+
const lookAtPoint = useMemo(() => [0, 0, 0], []);
|
|
265
293
|
useLayoutEffect(() => {
|
|
266
294
|
state.current = { weight: 0, pelvis: 0 };
|
|
267
295
|
target.current = null;
|
|
@@ -277,15 +305,26 @@ export function useFootIk(scene, config, ctx, instanceId, groundOffset = 0) {
|
|
|
277
305
|
return;
|
|
278
306
|
const parent = scene.parent;
|
|
279
307
|
const originY = parent === null ? scene.position.y : parent.localToWorld(scratch.a.set(0, groundOffset, 0)).y;
|
|
280
|
-
const exclude = [instanceId];
|
|
281
308
|
const aim = resolveLookAtTarget(lookAtConfig?.target, ctx, target.current, lookAtConfig?.entityEyeHeight ?? DEFAULT_EYE_HEIGHT);
|
|
282
309
|
target.current = aim;
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
}
|
|
310
|
+
if (aim === null) {
|
|
311
|
+
const world = camera.getWorldPosition(scratch.b);
|
|
312
|
+
lookAtPoint[0] = world.x;
|
|
313
|
+
lookAtPoint[1] = world.y;
|
|
314
|
+
lookAtPoint[2] = world.z;
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
lookAtPoint[0] = aim.x;
|
|
318
|
+
lookAtPoint[1] = aim.y;
|
|
319
|
+
lookAtPoint[2] = aim.z;
|
|
320
|
+
}
|
|
321
|
+
applyFootIk(rig, originY, (input) => {
|
|
322
|
+
probe.origin[0] = input.origin[0];
|
|
323
|
+
probe.origin[1] = input.origin[1];
|
|
324
|
+
probe.origin[2] = input.origin[2];
|
|
325
|
+
probe.maxDistance = input.maxDistance;
|
|
326
|
+
return ctx.scene.raycast(probe);
|
|
327
|
+
}, state.current, delta, lookAtPoint);
|
|
289
328
|
});
|
|
290
329
|
}
|
|
291
330
|
const DEFAULT_EYE_HEIGHT = 1.5;
|
|
@@ -187,13 +187,17 @@ function applyGraphClips(actions, clips) {
|
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
189
|
/** The clip with the largest weight in one graph output: the pose the rig is mostly showing. */
|
|
190
|
-
function dominantPhase(clips) {
|
|
190
|
+
function dominantPhase(clips, out) {
|
|
191
191
|
let best;
|
|
192
192
|
for (const entry of clips) {
|
|
193
193
|
if (entry.weight > 0 && (best === undefined || entry.weight > best.weight))
|
|
194
194
|
best = entry;
|
|
195
195
|
}
|
|
196
|
-
|
|
196
|
+
if (best === undefined)
|
|
197
|
+
return undefined;
|
|
198
|
+
out.clip = best.clip;
|
|
199
|
+
out.time = best.time;
|
|
200
|
+
return out;
|
|
197
201
|
}
|
|
198
202
|
/**
|
|
199
203
|
* Binds a graph's clips to a rig exactly as `useModelAnimation` does (masked layers get filtered
|
|
@@ -282,6 +286,8 @@ function buildGraphPlayback(scene, mixer, graph, clips, rng) {
|
|
|
282
286
|
lastYaw: null,
|
|
283
287
|
smoothedSpeed: 0,
|
|
284
288
|
params: {},
|
|
289
|
+
step: { dx: 0, dz: 0, dyaw: 0, seconds: 0 },
|
|
290
|
+
phase: { clip: "", time: 0 },
|
|
285
291
|
};
|
|
286
292
|
}
|
|
287
293
|
/**
|
|
@@ -465,10 +471,21 @@ export function useModelAnimation(scene, clips, animationInput, instanceId) {
|
|
|
465
471
|
else {
|
|
466
472
|
const instantSpeed = Math.hypot(dx, dz) / playbackDelta;
|
|
467
473
|
playback.smoothedSpeed += (instantSpeed - playback.smoothedSpeed) * Math.min(1, playbackDelta * 12);
|
|
468
|
-
|
|
474
|
+
const sample = playback.step;
|
|
475
|
+
sample.dx = dx;
|
|
476
|
+
sample.dz = dz;
|
|
477
|
+
sample.dyaw = playback.lastYaw === null ? 0 : shortestAngle(yaw - playback.lastYaw);
|
|
478
|
+
sample.seconds = playbackDelta;
|
|
479
|
+
step = sample;
|
|
469
480
|
}
|
|
470
481
|
}
|
|
471
|
-
playback.lastPos
|
|
482
|
+
if (playback.lastPos === null)
|
|
483
|
+
playback.lastPos = [x, entity.position[1], z];
|
|
484
|
+
else {
|
|
485
|
+
playback.lastPos[0] = x;
|
|
486
|
+
playback.lastPos[1] = entity.position[1];
|
|
487
|
+
playback.lastPos[2] = z;
|
|
488
|
+
}
|
|
472
489
|
playback.lastYaw = yaw;
|
|
473
490
|
readModelAnimationParams(ctx, instanceId, playback.smoothedSpeed, params, step);
|
|
474
491
|
}
|
|
@@ -487,7 +504,7 @@ export function useModelAnimation(scene, clips, animationInput, instanceId) {
|
|
|
487
504
|
mixerRef.current.update(0);
|
|
488
505
|
if (instanceId !== undefined) {
|
|
489
506
|
noteCharacterPose(instanceId);
|
|
490
|
-
const phase = dominantPhase(out.clips);
|
|
507
|
+
const phase = dominantPhase(out.clips, playback.phase);
|
|
491
508
|
if (phase !== undefined)
|
|
492
509
|
noteAnimationPhase(instanceId, phase.clip, phase.time);
|
|
493
510
|
}
|
|
@@ -3,6 +3,7 @@ import { busVolumeSettingId, DEFAULT_MASTER_VOLUME, DEFAULT_UI_SCALE, SETTING_ID
|
|
|
3
3
|
import { readGraphicsSettings } from "@jgengine/core/settings/graphicsSettings";
|
|
4
4
|
import { readFrameRateLimit } from "@jgengine/core/settings/frameRateLimit";
|
|
5
5
|
import { DEFAULT_TOUCH_JOYSTICK_VARIANT, TOUCH_JOYSTICK_VARIANTS, TOUCH_STYLES, } from "@jgengine/core/input/touchScheme";
|
|
6
|
+
import { busMuteSettingId, MASTER_MUTE_SETTING_ID } from "../audio/busMix.js";
|
|
6
7
|
/** Sentinel Controls value meaning "defer to the game's suggested touch skin". */
|
|
7
8
|
export const TOUCH_STYLE_AUTO = "auto";
|
|
8
9
|
/** Shell-internal: binds the SettingsStore touch-style choice to the touch renderer. @internal */
|
|
@@ -49,8 +50,10 @@ export function AudioSettingsBridge({ store, engine, buses, }) {
|
|
|
49
50
|
const rev = useSettingsRevision(store);
|
|
50
51
|
useEffect(() => {
|
|
51
52
|
engine.setMasterGain(store.get(SETTING_IDS.masterVolume, DEFAULT_MASTER_VOLUME));
|
|
53
|
+
engine.setMasterMute(Boolean(store.get(MASTER_MUTE_SETTING_ID, false)));
|
|
52
54
|
for (const bus of Object.values(buses ?? {})) {
|
|
53
55
|
engine.setBusGain(bus.id, store.get(busVolumeSettingId(bus.id), bus.gain ?? 1));
|
|
56
|
+
engine.setBusMute(bus.id, Boolean(store.get(busMuteSettingId(bus.id), false)));
|
|
54
57
|
}
|
|
55
58
|
}, [store, engine, buses, rev]);
|
|
56
59
|
return null;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { bindingLabel, type ActionCodesMap } from "@jgengine/core/input/actionBindings";
|
|
2
2
|
import type { BindingOverrides } from "@jgengine/core/input/bindingOverrides";
|
|
3
3
|
import type { AudioBusDef } from "@jgengine/core/audio/audioFalloff";
|
|
4
|
-
import { type GameSettingDef, type SettingCategory, type SettingCategoryDef } from "@jgengine/core/settings/settingsModel";
|
|
4
|
+
import { type GameSettingDef, type SettingCategory, type SettingCategoryDef, type SettingsStore } from "@jgengine/core/settings/settingsModel";
|
|
5
5
|
import { type GraphicsProfileOverrides } from "@jgengine/core/settings/graphicsSettings";
|
|
6
|
+
import type { PostProcessingConfig } from "@jgengine/core/render/postProcessing";
|
|
6
7
|
import { type SettingsActionView, type SettingsKeybindRow, type SettingsCategoryView, type SettingsController, type SettingsRow } from "@jgengine/react/settings";
|
|
7
8
|
export type { SettingsActionView, SettingsKeybindRow, SettingsCategoryView, SettingsController, SettingsRow };
|
|
8
9
|
export interface SettingsControllerInput {
|
|
@@ -14,6 +15,8 @@ export interface SettingsControllerInput {
|
|
|
14
15
|
fovEnabled: boolean;
|
|
15
16
|
/** The game's per-tier profile overrides (`defineGame({ graphics })`), so tier defaults match what renders. */
|
|
16
17
|
graphics?: GraphicsProfileOverrides;
|
|
18
|
+
/** The post chain this game actually runs, so a stage row never outlives its pass. */
|
|
19
|
+
postProcessing?: PostProcessingConfig;
|
|
17
20
|
hideBindings: readonly string[];
|
|
18
21
|
/** Show the touch-controls skin selector in Controls (touch device with a dock). */
|
|
19
22
|
touchStyle: boolean;
|
|
@@ -22,4 +25,10 @@ export interface SettingsControllerInput {
|
|
|
22
25
|
resetBinding: (action: string) => void;
|
|
23
26
|
}
|
|
24
27
|
export declare function useSettingsCategories(config: SettingsControllerInput): SettingsCategoryView[];
|
|
28
|
+
/**
|
|
29
|
+
* The built-in Graphics rows: tier, scale, filtering, shadows, one toggle per postfx stage this
|
|
30
|
+
* game's chain can build, then UI scale and FPS.
|
|
31
|
+
* @internal
|
|
32
|
+
*/
|
|
33
|
+
export declare function graphicsSettingRows(store: SettingsStore, overrides: GraphicsProfileOverrides | undefined, postProcessing: PostProcessingConfig | undefined): SettingsRow[];
|
|
25
34
|
export { bindingLabel };
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { actionLabel, bindingLabel } from "@jgengine/core/input/actionBindings";
|
|
2
2
|
import { BUILT_IN_SETTING_CATEGORIES, busVolumeSettingId, DEFAULT_GRAPHICS_SHADOWS, DEFAULT_MASTER_VOLUME, DEFAULT_UI_SCALE, GRAPHICS_QUALITY_OPTIONS, SETTING_IDS, UI_SCALE_MAX, UI_SCALE_MIN, } from "@jgengine/core/settings/settingsModel";
|
|
3
|
-
import { ANISOTROPY_OPTIONS, applyGraphicsQuality, GRAPHICS_POST_STAGE_LABELS,
|
|
3
|
+
import { ANISOTROPY_OPTIONS, applyGraphicsQuality, GRAPHICS_POST_STAGE_LABELS, graphicsPostStageSettingId, readGraphicsSettings, RENDER_SCALE_MAX, RENDER_SCALE_MIN, RENDER_SCALE_STEP, } from "@jgengine/core/settings/graphicsSettings";
|
|
4
4
|
import { FRAME_RATE_LIMIT_DISPLAY, FRAME_RATE_LIMIT_OPTIONS } from "@jgengine/core/settings/frameRateLimit";
|
|
5
5
|
import { accessibilitySettingRows } from "@jgengine/core/settings/accessibilitySettings";
|
|
6
|
+
import { busMuteSettingId, MASTER_MUTE_SETTING_ID } from "../audio/busMix.js";
|
|
7
|
+
import { availablePostfxStages } from "../postfx/PostProcessing.js";
|
|
6
8
|
import { DEFAULT_TOUCH_JOYSTICK_VARIANT, TOUCH_JOYSTICK_VARIANT_OPTIONS, TOUCH_STYLE_OPTIONS, } from "@jgengine/core/input/touchScheme";
|
|
7
9
|
import { TOUCH_STYLE_AUTO, useSettingsRevision } from "./appliedSettings.js";
|
|
8
10
|
import { useSettingsStore, } from "@jgengine/react/settings";
|
|
@@ -63,77 +65,24 @@ export function useSettingsCategories(config) {
|
|
|
63
65
|
set: (value) => store.set(id, value),
|
|
64
66
|
};
|
|
65
67
|
}),
|
|
66
|
-
...
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const graphicsRows = [
|
|
70
|
-
{
|
|
71
|
-
id: SETTING_IDS.graphicsQuality,
|
|
72
|
-
label: "Quality",
|
|
73
|
-
kind: "select",
|
|
74
|
-
value: graphics.quality,
|
|
75
|
-
options: GRAPHICS_QUALITY_OPTIONS,
|
|
76
|
-
set: (value) => applyGraphicsQuality(store, String(value), config.graphics),
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
id: SETTING_IDS.graphicsRenderScale,
|
|
80
|
-
label: "Render scale",
|
|
81
|
-
kind: "slider",
|
|
82
|
-
value: graphics.profile.renderScale,
|
|
83
|
-
min: RENDER_SCALE_MIN,
|
|
84
|
-
max: RENDER_SCALE_MAX,
|
|
85
|
-
step: RENDER_SCALE_STEP,
|
|
86
|
-
format: percent,
|
|
87
|
-
set: (value) => store.set(SETTING_IDS.graphicsRenderScale, value),
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
id: SETTING_IDS.graphicsFrameRateLimit,
|
|
91
|
-
label: "Frame rate limit",
|
|
92
|
-
kind: "select",
|
|
93
|
-
value: store.get(SETTING_IDS.graphicsFrameRateLimit, FRAME_RATE_LIMIT_DISPLAY),
|
|
94
|
-
options: FRAME_RATE_LIMIT_OPTIONS,
|
|
95
|
-
set: (value) => store.set(SETTING_IDS.graphicsFrameRateLimit, String(value)),
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
id: SETTING_IDS.graphicsAnisotropy,
|
|
99
|
-
label: "Texture filtering",
|
|
100
|
-
kind: "select",
|
|
101
|
-
value: String(graphics.profile.anisotropy),
|
|
102
|
-
options: ANISOTROPY_OPTIONS,
|
|
103
|
-
set: (value) => store.set(SETTING_IDS.graphicsAnisotropy, String(value)),
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
id: SETTING_IDS.graphicsShadows,
|
|
107
|
-
label: "Shadows",
|
|
68
|
+
...Object.values(config.buses ?? {}).map((bus) => ({
|
|
69
|
+
id: busMuteSettingId(bus.id),
|
|
70
|
+
label: bus.id === "ui" ? "Mute UI" : `Mute ${bus.id}`,
|
|
108
71
|
kind: "toggle",
|
|
109
|
-
value: store.get(
|
|
110
|
-
set: (value) => store.set(
|
|
111
|
-
},
|
|
112
|
-
...GRAPHICS_POST_STAGES.map((stage) => ({
|
|
113
|
-
id: graphicsPostStageSettingId(stage),
|
|
114
|
-
label: GRAPHICS_POST_STAGE_LABELS[stage],
|
|
115
|
-
kind: "toggle",
|
|
116
|
-
value: graphics.profile.postStages[stage],
|
|
117
|
-
set: (value) => store.set(graphicsPostStageSettingId(stage), value),
|
|
72
|
+
value: Boolean(store.get(busMuteSettingId(bus.id), false)),
|
|
73
|
+
set: (value) => store.set(busMuteSettingId(bus.id), Boolean(value)),
|
|
118
74
|
})),
|
|
119
75
|
{
|
|
120
|
-
id:
|
|
121
|
-
label: "
|
|
122
|
-
kind: "slider",
|
|
123
|
-
value: store.get(SETTING_IDS.graphicsUiScale, DEFAULT_UI_SCALE),
|
|
124
|
-
min: UI_SCALE_MIN,
|
|
125
|
-
max: UI_SCALE_MAX,
|
|
126
|
-
step: 0.05,
|
|
127
|
-
format: percent,
|
|
128
|
-
set: (value) => store.set(SETTING_IDS.graphicsUiScale, value),
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
id: SETTING_IDS.graphicsShowFps,
|
|
132
|
-
label: "Show FPS",
|
|
76
|
+
id: MASTER_MUTE_SETTING_ID,
|
|
77
|
+
label: "Mute all",
|
|
133
78
|
kind: "toggle",
|
|
134
|
-
value: store.get(
|
|
135
|
-
set: (value) => store.set(
|
|
79
|
+
value: Boolean(store.get(MASTER_MUTE_SETTING_ID, false)),
|
|
80
|
+
set: (value) => store.set(MASTER_MUTE_SETTING_ID, Boolean(value)),
|
|
136
81
|
},
|
|
82
|
+
...extrasFor("sound"),
|
|
83
|
+
];
|
|
84
|
+
const graphicsRows = [
|
|
85
|
+
...graphicsSettingRows(store, config.graphics, config.postProcessing),
|
|
137
86
|
...extrasFor("graphics"),
|
|
138
87
|
];
|
|
139
88
|
const gameplayRows = [
|
|
@@ -225,6 +174,84 @@ export function useSettingsCategories(config) {
|
|
|
225
174
|
views.sort((a, b) => (declaredOrder.get(a.id) ?? indexOrder(order, a.id)) - (declaredOrder.get(b.id) ?? indexOrder(order, b.id)));
|
|
226
175
|
return views;
|
|
227
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* The built-in Graphics rows: tier, scale, filtering, shadows, one toggle per postfx stage this
|
|
179
|
+
* game's chain can build, then UI scale and FPS.
|
|
180
|
+
* @internal
|
|
181
|
+
*/
|
|
182
|
+
export function graphicsSettingRows(store, overrides, postProcessing) {
|
|
183
|
+
const graphics = readGraphicsSettings(store, overrides);
|
|
184
|
+
const stages = postProcessing === undefined || postProcessing.enabled === false ? [] : availablePostfxStages(postProcessing);
|
|
185
|
+
return [
|
|
186
|
+
{
|
|
187
|
+
id: SETTING_IDS.graphicsQuality,
|
|
188
|
+
label: "Quality",
|
|
189
|
+
kind: "select",
|
|
190
|
+
value: graphics.quality,
|
|
191
|
+
options: GRAPHICS_QUALITY_OPTIONS,
|
|
192
|
+
set: (value) => applyGraphicsQuality(store, String(value), overrides),
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
id: SETTING_IDS.graphicsRenderScale,
|
|
196
|
+
label: "Render scale",
|
|
197
|
+
kind: "slider",
|
|
198
|
+
value: graphics.profile.renderScale,
|
|
199
|
+
min: RENDER_SCALE_MIN,
|
|
200
|
+
max: RENDER_SCALE_MAX,
|
|
201
|
+
step: RENDER_SCALE_STEP,
|
|
202
|
+
format: percent,
|
|
203
|
+
set: (value) => store.set(SETTING_IDS.graphicsRenderScale, value),
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: SETTING_IDS.graphicsFrameRateLimit,
|
|
207
|
+
label: "Frame rate limit",
|
|
208
|
+
kind: "select",
|
|
209
|
+
value: store.get(SETTING_IDS.graphicsFrameRateLimit, FRAME_RATE_LIMIT_DISPLAY),
|
|
210
|
+
options: FRAME_RATE_LIMIT_OPTIONS,
|
|
211
|
+
set: (value) => store.set(SETTING_IDS.graphicsFrameRateLimit, String(value)),
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
id: SETTING_IDS.graphicsAnisotropy,
|
|
215
|
+
label: "Texture filtering",
|
|
216
|
+
kind: "select",
|
|
217
|
+
value: String(graphics.profile.anisotropy),
|
|
218
|
+
options: ANISOTROPY_OPTIONS,
|
|
219
|
+
set: (value) => store.set(SETTING_IDS.graphicsAnisotropy, String(value)),
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
id: SETTING_IDS.graphicsShadows,
|
|
223
|
+
label: "Shadows",
|
|
224
|
+
kind: "toggle",
|
|
225
|
+
value: store.get(SETTING_IDS.graphicsShadows, DEFAULT_GRAPHICS_SHADOWS),
|
|
226
|
+
set: (value) => store.set(SETTING_IDS.graphicsShadows, value),
|
|
227
|
+
},
|
|
228
|
+
...stages.map((stage) => ({
|
|
229
|
+
id: graphicsPostStageSettingId(stage),
|
|
230
|
+
label: GRAPHICS_POST_STAGE_LABELS[stage],
|
|
231
|
+
kind: "toggle",
|
|
232
|
+
value: graphics.profile.postStages[stage],
|
|
233
|
+
set: (value) => store.set(graphicsPostStageSettingId(stage), value),
|
|
234
|
+
})),
|
|
235
|
+
{
|
|
236
|
+
id: SETTING_IDS.graphicsUiScale,
|
|
237
|
+
label: "UI scale",
|
|
238
|
+
kind: "slider",
|
|
239
|
+
value: store.get(SETTING_IDS.graphicsUiScale, DEFAULT_UI_SCALE),
|
|
240
|
+
min: UI_SCALE_MIN,
|
|
241
|
+
max: UI_SCALE_MAX,
|
|
242
|
+
step: 0.05,
|
|
243
|
+
format: percent,
|
|
244
|
+
set: (value) => store.set(SETTING_IDS.graphicsUiScale, value),
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
id: SETTING_IDS.graphicsShowFps,
|
|
248
|
+
label: "Show FPS",
|
|
249
|
+
kind: "toggle",
|
|
250
|
+
value: store.get(SETTING_IDS.graphicsShowFps, false),
|
|
251
|
+
set: (value) => store.set(SETTING_IDS.graphicsShowFps, value),
|
|
252
|
+
},
|
|
253
|
+
];
|
|
254
|
+
}
|
|
228
255
|
function indexOrder(order, id) {
|
|
229
256
|
const index = order.indexOf(id);
|
|
230
257
|
return index === -1 ? 999 : index;
|
|
@@ -6,7 +6,7 @@ export interface RevealVisionOptions {
|
|
|
6
6
|
tags: readonly string[];
|
|
7
7
|
resolveTags: (entity: SceneEntity) => readonly string[];
|
|
8
8
|
}
|
|
9
|
-
/** Occlusion-ignoring tagged-entity radius query (#115), bound to the live scene. */
|
|
9
|
+
/** Occlusion-ignoring tagged-entity radius query (#115), bound to the live scene and refreshed on render. */
|
|
10
10
|
export declare function useRevealHits(options: RevealVisionOptions): readonly RevealHit[];
|
|
11
11
|
export interface RevealHighlightsProps extends RevealVisionOptions {
|
|
12
12
|
enabled: boolean;
|
|
@@ -17,6 +17,8 @@ export interface RevealHighlightsProps extends RevealVisionOptions {
|
|
|
17
17
|
* occluders (Dark Sight / detective-vision / wallhack style). Renders with
|
|
18
18
|
* `depthTest: false` so the highlight draws over any wall standing between
|
|
19
19
|
* the origin and the revealed entity, rather than the usual depth-sorted scene.
|
|
20
|
+
* Highlights track the origin and their targets every frame while enabled, so
|
|
21
|
+
* a disabled mode costs no renders.
|
|
20
22
|
*/
|
|
21
23
|
export declare function RevealHighlights(props: RevealHighlightsProps): import("react").JSX.Element | null;
|
|
22
24
|
export interface RevealScreenTintProps {
|