@jgengine/shell 0.18.1 → 0.19.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 +158 -7
- package/dist/GamePlayerShell.js +20 -3
- package/dist/Shell3dPresentation.d.ts +3 -9
- package/dist/Shell3dPresentation.js +26 -6
- package/dist/ShellHudPresentation.d.ts +9 -1
- package/dist/ShellHudPresentation.js +21 -0
- package/dist/audio/AudioComponents.js +6 -2
- package/dist/audio/audioEngine.d.ts +8 -0
- package/dist/audio/audioEngine.js +65 -6
- package/dist/audio/audioWire.d.ts +3 -0
- package/dist/audio/audioWire.js +7 -1
- package/dist/audio/loopParams.d.ts +39 -0
- package/dist/audio/loopParams.js +43 -0
- package/dist/camera/GameCameraRig.js +1 -1
- package/dist/camera/GameFirstPersonCamera.d.ts +4 -2
- package/dist/camera/GameFirstPersonCamera.js +52 -15
- package/dist/camera/Viewports.d.ts +34 -0
- package/dist/camera/Viewports.js +123 -0
- package/dist/camera/cameraRigs.js +70 -62
- package/dist/camera/orbitCameraMath.js +2 -2
- package/dist/camera/rigMath.d.ts +82 -4
- package/dist/camera/rigMath.js +133 -15
- package/dist/defineGame.d.ts +9 -0
- package/dist/defineGame.js +20 -3
- package/dist/devtools/DevtoolsOverlay.js +2 -0
- package/dist/devtools/agentBridge.d.ts +6 -0
- package/dist/devtools/agentBridge.js +15 -1
- package/dist/diagnostics/FpsCounter.d.ts +17 -0
- package/dist/diagnostics/FpsCounter.js +32 -0
- package/dist/drivers/FrameDriver.js +13 -2
- package/dist/drivers/FrameRateLimiter.d.ts +9 -0
- package/dist/drivers/FrameRateLimiter.js +39 -0
- package/dist/drivers/HudOnlyDriver.js +1 -1
- package/dist/environment/DayNightSky.js +5 -5
- package/dist/environment/Daylight.d.ts +35 -1
- package/dist/environment/Daylight.js +98 -28
- package/dist/environment/VolumetricClouds.js +20 -2
- package/dist/environment/daylightCycle.d.ts +20 -0
- package/dist/environment/daylightCycle.js +59 -7
- package/dist/environment/sunShadowMath.d.ts +45 -0
- package/dist/environment/sunShadowMath.js +50 -0
- package/dist/input/gamepadPoll.d.ts +47 -0
- package/dist/input/gamepadPoll.js +142 -0
- package/dist/input/gamepadSource.d.ts +11 -1
- package/dist/input/gamepadSource.js +117 -71
- package/dist/input/mouseLook.js +2 -1
- package/dist/input/padHaptics.d.ts +17 -0
- package/dist/input/padHaptics.js +32 -0
- package/dist/input/pointerLock.d.ts +11 -0
- package/dist/input/pointerLock.js +19 -0
- package/dist/postfx/PostProcessing.d.ts +1 -1
- package/dist/postfx/PostProcessing.js +5 -1
- package/dist/postfx/stylizePass.d.ts +23 -0
- package/dist/postfx/stylizePass.js +216 -0
- package/dist/render/SceneModels.js +1 -1
- package/dist/render/SkinnedInstances.d.ts +91 -0
- package/dist/render/SkinnedInstances.js +249 -0
- package/dist/render/TextureFiltering.d.ts +16 -0
- package/dist/render/TextureFiltering.js +64 -0
- package/dist/render/displayColor.d.ts +33 -0
- package/dist/render/displayColor.js +138 -0
- package/dist/render/modelLoad.d.ts +29 -0
- package/dist/render/modelLoad.js +61 -1
- package/dist/render/resolveModel.js +4 -0
- package/dist/render/useFootIk.d.ts +53 -4
- package/dist/render/useFootIk.js +197 -70
- package/dist/render/useModelAnimation.d.ts +24 -0
- package/dist/render/useModelAnimation.js +61 -40
- package/dist/scene/AuthoredScene.d.ts +9 -0
- package/dist/scene/AuthoredScene.js +17 -1
- package/dist/scene/CityRenderer.js +6 -6
- package/dist/settings/QuickControls.js +1 -1
- package/dist/settings/SettingsMenu.js +1 -1
- package/dist/settings/appliedSettings.d.ts +9 -3
- package/dist/settings/appliedSettings.js +6 -5
- package/dist/settings/settingsController.d.ts +3 -0
- package/dist/settings/settingsController.js +47 -3
- package/dist/structures/BuildingKitBatch.js +26 -22
- package/dist/structures/GeneratedBuilding.d.ts +3 -15
- package/dist/structures/GeneratedBuilding.js +21 -15
- package/dist/structures/buildingSurface.d.ts +21 -0
- package/dist/structures/buildingSurface.js +134 -0
- package/dist/useShellMultiplayerSync.d.ts +1 -1
- package/dist/useShellMultiplayerSync.js +5 -3
- package/dist/world/WorldParticles.js +1 -1
- package/package.json +4 -4
- package/skills/ce-handoff/SKILL.md +1 -1
- package/skills/game-design/SKILL.md +2 -2
- package/skills/jgengine/SKILL.md +7 -5
- package/skills/jgengine/api.md +118 -89
- package/skills/jgengine/capabilities.md +28 -0
- package/skills/jgengine/recipes/minimal-game.md +3 -0
- package/skills/jgengine-ui/SKILL.md +13 -16
- package/skills/jgengine-ui/api.md +418 -317
- package/skills/jgengine-ui/capabilities.md +16 -0
- package/skills/jgengine-ui/reference-primitives.md +89 -0
- package/skills/jgengine-ui/reference.md +2 -81
- package/skills/jgengine-world/SKILL.md +15 -3
- package/skills/jgengine-world/api.md +873 -626
- package/skills/jgengine-world/capabilities.md +130 -6
- package/skills/jgengine-world/recipes/character-animation.md +39 -0
- package/skills/jgengine-world/recipes/controls.md +44 -0
- package/skills/jgengine-world/recipes/flight-feel.md +76 -0
- package/skills/jgengine-world/recipes/npc-senses.md +43 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +85 -0
- package/skills/jgengine-world/reference.md +31 -2
- package/skills/jgengine-world/visibility.md +112 -0
- package/skills/level-design/SKILL.md +1 -1
- package/dist/render/testFixtures/proceduralSpriteAtlas.d.ts +0 -15
- package/dist/render/testFixtures/proceduralSpriteAtlas.js +0 -30
- package/dist/render/testFixtures/proceduralTileLayer.d.ts +0 -13
- package/dist/render/testFixtures/proceduralTileLayer.js +0 -30
|
@@ -5,10 +5,11 @@ import { snapPixelPerfectPosition } from "@jgengine/core/game/pixelPerfect";
|
|
|
5
5
|
import { usePlayer } from "@jgengine/react/hooks";
|
|
6
6
|
import { useGameContext } from "@jgengine/react/provider";
|
|
7
7
|
import { ORBIT_CAMERA_FRAME_PRIORITY } from "./orbitCameraMath.js";
|
|
8
|
-
import {
|
|
8
|
+
import { cinematicSample, clamp, clampBoomToHit, createChaseRigState, lockOnPose, observerPose, resolveChase, resolveObserver, resolveShoulder, resolveSideScroll, resolveSideScrollPose, resolveTopDown, rtsPanWorldDir, seatPose, shoulderPose, sideScrollFollowBlend, smoothYaw, stepChase, topDownPose, } from "./rigMath.js";
|
|
9
9
|
import { usePlayerFov } from "./PlayerFov.js";
|
|
10
10
|
import { useCameraShake } from "./shakeChannel.js";
|
|
11
11
|
import { applyCameraBlendStep, captureCameraBlendFrom, createCameraBlendScratch, } from "./cameraBlendMath.js";
|
|
12
|
+
import { requestRawPointerLock } from "../input/pointerLock.js";
|
|
12
13
|
export { applyCameraBlendStep, captureCameraBlendFrom, createCameraBlendScratch, } from "./cameraBlendMath.js";
|
|
13
14
|
export const CAMERA_RIG_FRAME_PRIORITY = ORBIT_CAMERA_FRAME_PRIORITY;
|
|
14
15
|
export const CAMERA_POST_FRAME_PRIORITY = ORBIT_CAMERA_FRAME_PRIORITY + 0.5;
|
|
@@ -306,7 +307,7 @@ export function ShoulderRig(props) {
|
|
|
306
307
|
useEffect(() => {
|
|
307
308
|
const requestLock = () => {
|
|
308
309
|
if (document.pointerLockElement !== domElement)
|
|
309
|
-
|
|
310
|
+
requestRawPointerLock(domElement);
|
|
310
311
|
};
|
|
311
312
|
const onMove = (event) => {
|
|
312
313
|
if (document.pointerLockElement !== domElement)
|
|
@@ -345,8 +346,13 @@ export function ShoulderRig(props) {
|
|
|
345
346
|
useFrame((_, dt) => {
|
|
346
347
|
const sample = readFollow(ctx, followId);
|
|
347
348
|
const follow = sample?.pos ?? { x: 0, y: 0, z: 0 };
|
|
348
|
-
const
|
|
349
|
-
|
|
349
|
+
const weapon = followId === null ? null : (props.config?.weapon?.(followId) ?? null);
|
|
350
|
+
if (weapon === null) {
|
|
351
|
+
const adsTarget = aimingRef.current ? 1 : 0;
|
|
352
|
+
aimRef.current += (adsTarget - aimRef.current) * (1 - Math.exp(-(config?.adsTransitionSpeed ?? 10) * dt));
|
|
353
|
+
}
|
|
354
|
+
else
|
|
355
|
+
aimRef.current = weapon.handling.adsProgress;
|
|
350
356
|
const hip = resolveShoulder(config, false);
|
|
351
357
|
const ads = resolveShoulder(config, true);
|
|
352
358
|
const blended = {
|
|
@@ -355,7 +361,11 @@ export function ShoulderRig(props) {
|
|
|
355
361
|
distance: hip.distance + (ads.distance - hip.distance) * aimRef.current,
|
|
356
362
|
fov: hip.fov + (ads.fov - hip.fov) * aimRef.current,
|
|
357
363
|
};
|
|
358
|
-
const
|
|
364
|
+
const handling = weapon?.handling;
|
|
365
|
+
const yaw = props.yawRef.current - (handling === undefined ? 0 : handling.aimYaw + handling.cameraYaw);
|
|
366
|
+
// This rig's pitch is camera elevation, so a muzzle climb lowers it.
|
|
367
|
+
const pitch = props.pitchRef.current - (handling === undefined ? 0 : handling.aimPitch + handling.cameraPitch);
|
|
368
|
+
const pose = shoulderPose(follow, yaw, pitch, sideRef.current, blended);
|
|
359
369
|
commit(pose, dt);
|
|
360
370
|
}, CAMERA_RIG_FRAME_PRIORITY);
|
|
361
371
|
return null;
|
|
@@ -424,73 +434,71 @@ export function ChaseRig(props) {
|
|
|
424
434
|
const staticResolved = useMemo(() => resolveChase(config), [config]);
|
|
425
435
|
const { camera, commit, beginTransition } = useCameraCommit(props, followId);
|
|
426
436
|
const shake = useCameraShake();
|
|
427
|
-
const
|
|
428
|
-
const
|
|
429
|
-
const lastYawRef = useRef(null);
|
|
430
|
-
const anchorYawRef = useRef(null);
|
|
431
|
-
const rollRef = useRef(0);
|
|
432
|
-
const view = config?.view ?? "chase";
|
|
433
|
-
useEffect(beginTransition, [view]);
|
|
437
|
+
const stateRef = useRef(createChaseRigState());
|
|
438
|
+
const viewRef = useRef(null);
|
|
434
439
|
useFrame((_, dt) => {
|
|
435
440
|
const tuning = ctx.camera.chaseTuning();
|
|
436
|
-
const
|
|
437
|
-
const
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
const
|
|
441
|
+
const merged = tuning === null ? config : { ...config, ...tuning };
|
|
442
|
+
const resolved = tuning === null ? staticResolved : resolveChase(merged);
|
|
443
|
+
const view = merged?.view ?? "chase";
|
|
444
|
+
if (viewRef.current !== view) {
|
|
445
|
+
beginTransition();
|
|
446
|
+
viewRef.current = view;
|
|
447
|
+
stateRef.current.offset = null;
|
|
448
|
+
}
|
|
449
|
+
const entity = followId === null ? null : ctx.scene.entity.get(followId);
|
|
450
|
+
const follow = entity === null ? { x: 0, y: 0, z: 0 } : { x: entity.position[0], y: entity.position[1], z: entity.position[2] };
|
|
451
|
+
const yaw = entity?.rotationY ?? 0;
|
|
452
|
+
const v = entity?.velocity;
|
|
453
|
+
const published = v !== undefined && (v[0] !== 0 || v[1] !== 0 || v[2] !== 0);
|
|
454
|
+
const lookBack = resolved.lookBackAction !== null && ctx.input.isDown(resolved.lookBackAction);
|
|
455
|
+
const clampBoom = resolved.collision
|
|
456
|
+
? (pivot, desired) => {
|
|
457
|
+
const dx = desired.x - pivot.x;
|
|
458
|
+
const dy = desired.y - pivot.y;
|
|
459
|
+
const dz = desired.z - pivot.z;
|
|
460
|
+
const length = Math.hypot(dx, dy, dz);
|
|
461
|
+
if (length <= 1e-6)
|
|
462
|
+
return desired;
|
|
463
|
+
const hit = ctx.scene.raycast({
|
|
464
|
+
origin: [pivot.x, pivot.y, pivot.z],
|
|
465
|
+
direction: [dx / length, dy / length, dz / length],
|
|
466
|
+
maxDistance: length + resolved.collisionRadius,
|
|
467
|
+
excludeInstanceIds: followId === null ? undefined : [followId],
|
|
468
|
+
filter: { entities: false },
|
|
469
|
+
});
|
|
470
|
+
return clampBoomToHit(pivot, desired, hit?.distance ?? null, resolved.collisionRadius, resolved.collisionMinDistance);
|
|
471
|
+
}
|
|
472
|
+
: undefined;
|
|
473
|
+
const step = stepChase(stateRef.current, {
|
|
474
|
+
follow,
|
|
475
|
+
yaw,
|
|
476
|
+
bodyPitch: entity?.rotationX ?? 0,
|
|
477
|
+
velocity: published ? { x: v[0], y: v[1], z: v[2] } : null,
|
|
478
|
+
lookBack: lookBack && view === "chase",
|
|
479
|
+
fovKick: ctx.camera.takeFovKick(),
|
|
480
|
+
}, resolved, dt, view === "chase" ? clampBoom : undefined);
|
|
481
|
+
props.yawRef.current = step.anchorYaw;
|
|
447
482
|
if (view !== "chase") {
|
|
448
483
|
const seat = view === "cockpit"
|
|
449
|
-
?
|
|
484
|
+
? merged?.seatOffsets?.cockpit ?? { x: 0, y: 1.2, z: 0.1 }
|
|
450
485
|
: view === "hood"
|
|
451
|
-
?
|
|
452
|
-
:
|
|
453
|
-
const
|
|
454
|
-
const pose = seatPose(follow,
|
|
455
|
-
if (
|
|
456
|
-
pose.position = seatPose(follow, yaw, seat, fov).position;
|
|
457
|
-
}
|
|
486
|
+
? merged?.seatOffsets?.hood ?? { x: 0, y: 1, z: 1.4 }
|
|
487
|
+
: merged?.seatOffsets?.rear ?? { x: 0, y: 1.6, z: -1.5 };
|
|
488
|
+
const seatLooksBack = view === "rear" || lookBack;
|
|
489
|
+
const pose = seatPose(follow, seatLooksBack ? yaw + Math.PI : yaw, seat, step.pose.fov);
|
|
490
|
+
if (seatLooksBack)
|
|
491
|
+
pose.position = seatPose(follow, yaw, seat, step.pose.fov).position;
|
|
458
492
|
props.yawRef.current = yaw;
|
|
459
493
|
commit(pose, dt);
|
|
460
494
|
return;
|
|
461
495
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
// yawResponse — so a strafe-flipped facing arcs the boom around the character instead of
|
|
465
|
-
// teleporting it to the far side. yawResponse: Infinity restores the legacy rigid follow.
|
|
466
|
-
let targetYaw = yaw;
|
|
467
|
-
let response = resolved.yawResponse;
|
|
468
|
-
if (resolved.velocityYawBlend > 0) {
|
|
469
|
-
const velocity = dt > 0
|
|
470
|
-
? { x: (follow.x - last.x) / dt, y: 0, z: (follow.z - last.z) / dt }
|
|
471
|
-
: { x: 0, y: 0, z: 0 };
|
|
472
|
-
targetYaw = velocityYawTarget(yaw, velocity, resolved);
|
|
473
|
-
response = resolved.velocityYawResponse;
|
|
496
|
+
if (resolved.shakePerSpeed > 0 && step.speed > 0) {
|
|
497
|
+
shake.shake(Math.min(resolved.shakePerSpeed * step.speed * dt, 0.1));
|
|
474
498
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
anchorYawRef.current = anchorYaw;
|
|
479
|
-
// Report camera yaw back to the shell like every other player-facing rig, so
|
|
480
|
-
// on-foot movement and aim stay camera-relative instead of frozen at yaw 0.
|
|
481
|
-
props.yawRef.current = anchorYaw;
|
|
482
|
-
const led = leadFollowPoint(follow, last, dt, resolved);
|
|
483
|
-
const desired = chaseDesiredPosition(led, anchorYaw, resolved);
|
|
484
|
-
const prev = posRef.current ?? desired;
|
|
485
|
-
const smoothed = springArmStep(prev, desired, resolved.springDamping, dt);
|
|
486
|
-
posRef.current = smoothed;
|
|
487
|
-
if (resolved.shakePerSpeed > 0 && speed > 0) {
|
|
488
|
-
shake.shake(Math.min(resolved.shakePerSpeed * speed * dt, 0.1));
|
|
489
|
-
}
|
|
490
|
-
const pose = { position: smoothed, lookAt: chaseLookAt(led, anchorYaw, resolved), fov };
|
|
491
|
-
commit(pose, dt);
|
|
492
|
-
if (rollRef.current !== 0)
|
|
493
|
-
camera.rotateZ(rollRef.current);
|
|
499
|
+
commit(step.pose, dt);
|
|
500
|
+
if (step.roll !== 0)
|
|
501
|
+
camera.rotateZ(step.roll);
|
|
494
502
|
}, CAMERA_RIG_FRAME_PRIORITY);
|
|
495
503
|
return null;
|
|
496
504
|
}
|
|
@@ -17,8 +17,8 @@ export const DEFAULT_ORBIT_CAMERA = {
|
|
|
17
17
|
minDistance: 4,
|
|
18
18
|
maxDistance: 28,
|
|
19
19
|
targetHeight: 1.2,
|
|
20
|
-
initialDistance:
|
|
21
|
-
initialHeight:
|
|
20
|
+
initialDistance: 7,
|
|
21
|
+
initialHeight: 2.4,
|
|
22
22
|
followLock: true,
|
|
23
23
|
followEnabled: true,
|
|
24
24
|
rotateSpeed: 0.22,
|
package/dist/camera/rigMath.d.ts
CHANGED
|
@@ -136,6 +136,23 @@ export interface ResolvedChase {
|
|
|
136
136
|
velocityYawResponse: number;
|
|
137
137
|
/** Exponential smoothing rate the anchor yaw follows the body's facing with (#1370). `Infinity` restores the legacy rigid follow. */
|
|
138
138
|
yawResponse: number;
|
|
139
|
+
fovBase: number;
|
|
140
|
+
fovMax: number;
|
|
141
|
+
fovSpeedForMax: number;
|
|
142
|
+
/** Exponential FOV smoothing rate; `Infinity` applies the curve unsmoothed. */
|
|
143
|
+
fovResponse: number;
|
|
144
|
+
distanceExtra: number;
|
|
145
|
+
distanceSpeedForMax: number;
|
|
146
|
+
/** Fraction of body pitch the boom follows; 0 keeps a level boom. */
|
|
147
|
+
pitchBlend: number;
|
|
148
|
+
pitchMax: number;
|
|
149
|
+
pitchResponse: number;
|
|
150
|
+
fovKickDecay: number;
|
|
151
|
+
fovKickMax: number;
|
|
152
|
+
lookBackAction: string | null;
|
|
153
|
+
collision: boolean;
|
|
154
|
+
collisionRadius: number;
|
|
155
|
+
collisionMinDistance: number;
|
|
139
156
|
}
|
|
140
157
|
/** @internal */
|
|
141
158
|
export declare function resolveChase(config: ChaseCameraConfig | undefined): ResolvedChase;
|
|
@@ -143,6 +160,10 @@ export declare function resolveChase(config: ChaseCameraConfig | undefined): Res
|
|
|
143
160
|
* @internal
|
|
144
161
|
*/
|
|
145
162
|
export declare function leadFollowPoint(follow: Vec3, previous: Vec3, dt: number, resolved: ResolvedChase): Vec3;
|
|
163
|
+
/** Velocity-lead the follow point along a known planar velocity, clamped to `leadMax`.
|
|
164
|
+
* @internal
|
|
165
|
+
*/
|
|
166
|
+
export declare function leadFromVelocity(follow: Vec3, velocity: Vec3, resolved: ResolvedChase): Vec3;
|
|
146
167
|
/** One smoothing step of the chase rig's turn-bank roll (#286.10): roll opposes yaw rate, clamped and exponentially damped.
|
|
147
168
|
* @internal
|
|
148
169
|
*/
|
|
@@ -159,14 +180,71 @@ export declare function resolveObserver(config: ObserverCameraConfig | undefined
|
|
|
159
180
|
* @internal
|
|
160
181
|
*/
|
|
161
182
|
export declare function observerPose(subject: Vec3, angle: number, resolved: ResolvedObserver, fov: number): CameraPose;
|
|
162
|
-
/** Desired chase-camera position behind a vehicle facing `yaw` (before spring smoothing).
|
|
183
|
+
/** Desired chase-camera position behind a vehicle facing `yaw` (before spring smoothing). `pitch` (nose-up positive) tilts the boom around the vehicle's right axis.
|
|
184
|
+
* @internal
|
|
185
|
+
*/
|
|
186
|
+
export declare function chaseDesiredPosition(follow: Vec3, yaw: number, resolved: ResolvedChase, pitch?: number, distance?: number): Vec3;
|
|
187
|
+
/** Camera offset from the follow point for a boom `distance` back and `height` up, tilted by `pitch` (nose-up positive).
|
|
188
|
+
* @internal
|
|
189
|
+
*/
|
|
190
|
+
export declare function chaseBoomOffset(yaw: number, pitch: number, distance: number, height: number): Vec3;
|
|
191
|
+
/** Look point ahead of / above a chased vehicle, tilted by `pitch` like the boom.
|
|
163
192
|
* @internal
|
|
164
193
|
*/
|
|
165
|
-
export declare function
|
|
166
|
-
/**
|
|
194
|
+
export declare function chaseLookAt(follow: Vec3, yaw: number, resolved: ResolvedChase, pitch?: number): Vec3;
|
|
195
|
+
/** Mutable per-rig chase state; {@link stepChase} advances it in place. */
|
|
196
|
+
export interface ChaseRigState {
|
|
197
|
+
/** Smoothed camera offset from the led follow point; springing the offset, not the world position, keeps the boom length at any constant speed. */
|
|
198
|
+
offset: Vec3 | null;
|
|
199
|
+
anchorYaw: number | null;
|
|
200
|
+
lastYaw: number | null;
|
|
201
|
+
lastFollow: Vec3 | null;
|
|
202
|
+
roll: number;
|
|
203
|
+
pitch: number;
|
|
204
|
+
fov: number | null;
|
|
205
|
+
kick: number;
|
|
206
|
+
lookingBack: boolean;
|
|
207
|
+
}
|
|
208
|
+
/** @internal */
|
|
209
|
+
export declare function createChaseRigState(): ChaseRigState;
|
|
210
|
+
/** What the chase rig reads from its target each frame. */
|
|
211
|
+
export interface ChaseSample {
|
|
212
|
+
follow: Vec3;
|
|
213
|
+
/** Body heading (radians). */
|
|
214
|
+
yaw: number;
|
|
215
|
+
/** Body `rotationX` (three.js convention: positive = nose down). */
|
|
216
|
+
bodyPitch: number;
|
|
217
|
+
/** Sim velocity when the entity publishes one; `null` falls back to the frame-to-frame position delta. */
|
|
218
|
+
velocity: Vec3 | null;
|
|
219
|
+
lookBack: boolean;
|
|
220
|
+
/** FOV impulse (degrees) queued since the last frame. */
|
|
221
|
+
fovKick: number;
|
|
222
|
+
}
|
|
223
|
+
/** One frame of chase output from {@link stepChase}. */
|
|
224
|
+
export interface ChaseStepResult {
|
|
225
|
+
pose: CameraPose;
|
|
226
|
+
/** Camera roll to apply after the pose, radians. */
|
|
227
|
+
roll: number;
|
|
228
|
+
/** Forward chase yaw to report to the shell; unaffected by look-back. */
|
|
229
|
+
anchorYaw: number;
|
|
230
|
+
/** Planar speed used for FOV, distance and shake. */
|
|
231
|
+
speed: number;
|
|
232
|
+
}
|
|
233
|
+
/** Pulls a desired camera position in toward `pivot` when something blocks the boom. */
|
|
234
|
+
export type ChaseBoomClamp = (pivot: Vec3, desired: Vec3) => Vec3;
|
|
235
|
+
/**
|
|
236
|
+
* One frame of the chase rig: speed from sim velocity, smoothed FOV plus decaying kicks, drift-lag
|
|
237
|
+
* anchor yaw, pitch follow, distance by speed, look-back, and a boom spring that eases the offset
|
|
238
|
+
* from the target rather than its world position, so the camera holds its distance at speed.
|
|
239
|
+
* @internal
|
|
240
|
+
*/
|
|
241
|
+
export declare function stepChase(state: ChaseRigState, sample: ChaseSample, resolved: ResolvedChase, dt: number, clampBoom?: ChaseBoomClamp): ChaseStepResult;
|
|
242
|
+
/**
|
|
243
|
+
* Where a blocked boom should put the camera: `hitDistance` along the pivot→desired ray, less
|
|
244
|
+
* `radius`, but never nearer the pivot than `minDistance`. `null` hit returns `desired` unchanged.
|
|
167
245
|
* @internal
|
|
168
246
|
*/
|
|
169
|
-
export declare function
|
|
247
|
+
export declare function clampBoomToHit(pivot: Vec3, desired: Vec3, hitDistance: number | null, radius: number, minDistance: number): Vec3;
|
|
170
248
|
/**
|
|
171
249
|
* Drift-lag (#1051): the yaw the chase anchor should aim before temporal
|
|
172
250
|
* smoothing. When the target slides, its planar `velocity` diverges from its
|
package/dist/camera/rigMath.js
CHANGED
|
@@ -253,6 +253,9 @@ export function lockOnPose(player, target, config, fov) {
|
|
|
253
253
|
const DEFAULT_CHASE_YAW_RESPONSE = 5;
|
|
254
254
|
/** @internal */
|
|
255
255
|
export function resolveChase(config) {
|
|
256
|
+
const fovSpeedForMax = config?.fov?.speedForMax ?? 24;
|
|
257
|
+
const collision = config?.collision ?? true;
|
|
258
|
+
const collisionBlock = typeof collision === "object" ? collision : undefined;
|
|
256
259
|
return {
|
|
257
260
|
distance: config?.distance ?? 6,
|
|
258
261
|
height: config?.height ?? 2.6,
|
|
@@ -268,16 +271,39 @@ export function resolveChase(config) {
|
|
|
268
271
|
velocityYawMinSpeed: config?.velocityYaw?.minSpeed ?? 4,
|
|
269
272
|
velocityYawResponse: config?.velocityYaw?.response ?? 6,
|
|
270
273
|
yawResponse: config?.yawResponse ?? DEFAULT_CHASE_YAW_RESPONSE,
|
|
274
|
+
fovBase: config?.fov?.base ?? 55,
|
|
275
|
+
fovMax: config?.fov?.max ?? 78,
|
|
276
|
+
fovSpeedForMax,
|
|
277
|
+
fovResponse: config?.fov?.response ?? 6,
|
|
278
|
+
distanceExtra: config?.distanceBySpeed?.extra ?? 0,
|
|
279
|
+
distanceSpeedForMax: config?.distanceBySpeed?.speedForMax ?? fovSpeedForMax,
|
|
280
|
+
pitchBlend: config?.pitchFollow === undefined ? 0 : config.pitchFollow.blend ?? 1,
|
|
281
|
+
pitchMax: config?.pitchFollow?.max ?? 0.6,
|
|
282
|
+
pitchResponse: config?.pitchFollow?.response ?? 5,
|
|
283
|
+
fovKickDecay: config?.fovKick?.decay ?? 7,
|
|
284
|
+
fovKickMax: config?.fovKick?.max ?? 20,
|
|
285
|
+
lookBackAction: config?.lookBackAction ?? null,
|
|
286
|
+
collision: collision !== false,
|
|
287
|
+
collisionRadius: collisionBlock?.radius ?? 0.3,
|
|
288
|
+
collisionMinDistance: collisionBlock?.minDistance ?? 0.8,
|
|
271
289
|
};
|
|
272
290
|
}
|
|
273
291
|
/** Velocity-lead the follow point (#286.9): aim `leadTime` seconds along the target's frame velocity, clamped to `leadMax`.
|
|
274
292
|
* @internal
|
|
275
293
|
*/
|
|
276
294
|
export function leadFollowPoint(follow, previous, dt, resolved) {
|
|
277
|
-
if (
|
|
295
|
+
if (dt <= 0)
|
|
278
296
|
return follow;
|
|
279
|
-
|
|
280
|
-
|
|
297
|
+
return leadFromVelocity(follow, { x: (follow.x - previous.x) / dt, y: 0, z: (follow.z - previous.z) / dt }, resolved);
|
|
298
|
+
}
|
|
299
|
+
/** Velocity-lead the follow point along a known planar velocity, clamped to `leadMax`.
|
|
300
|
+
* @internal
|
|
301
|
+
*/
|
|
302
|
+
export function leadFromVelocity(follow, velocity, resolved) {
|
|
303
|
+
if (resolved.leadTime <= 0)
|
|
304
|
+
return follow;
|
|
305
|
+
let leadX = velocity.x * resolved.leadTime;
|
|
306
|
+
let leadZ = velocity.z * resolved.leadTime;
|
|
281
307
|
const magnitude = Math.hypot(leadX, leadZ);
|
|
282
308
|
if (magnitude > resolved.leadMax) {
|
|
283
309
|
leadX *= resolved.leadMax / magnitude;
|
|
@@ -327,28 +353,120 @@ export function observerPose(subject, angle, resolved, fov) {
|
|
|
327
353
|
fov,
|
|
328
354
|
};
|
|
329
355
|
}
|
|
330
|
-
/** Desired chase-camera position behind a vehicle facing `yaw` (before spring smoothing).
|
|
356
|
+
/** Desired chase-camera position behind a vehicle facing `yaw` (before spring smoothing). `pitch` (nose-up positive) tilts the boom around the vehicle's right axis.
|
|
331
357
|
* @internal
|
|
332
358
|
*/
|
|
333
|
-
export function chaseDesiredPosition(follow, yaw, resolved) {
|
|
334
|
-
const
|
|
359
|
+
export function chaseDesiredPosition(follow, yaw, resolved, pitch = 0, distance = resolved.distance) {
|
|
360
|
+
const offset = chaseBoomOffset(yaw, pitch, distance, resolved.height);
|
|
361
|
+
return { x: follow.x + offset.x, y: follow.y + offset.y, z: follow.z + offset.z };
|
|
362
|
+
}
|
|
363
|
+
/** Camera offset from the follow point for a boom `distance` back and `height` up, tilted by `pitch` (nose-up positive).
|
|
364
|
+
* @internal
|
|
365
|
+
*/
|
|
366
|
+
export function chaseBoomOffset(yaw, pitch, distance, height) {
|
|
367
|
+
const forward = forwardVector(yaw);
|
|
368
|
+
const cos = Math.cos(pitch);
|
|
369
|
+
const sin = Math.sin(pitch);
|
|
370
|
+
const along = -distance * cos - height * sin;
|
|
371
|
+
return { x: forward.x * along, y: -distance * sin + height * cos, z: forward.z * along };
|
|
372
|
+
}
|
|
373
|
+
/** Look point ahead of / above a chased vehicle, tilted by `pitch` like the boom.
|
|
374
|
+
* @internal
|
|
375
|
+
*/
|
|
376
|
+
export function chaseLookAt(follow, yaw, resolved, pitch = 0) {
|
|
377
|
+
const forward = forwardVector(yaw);
|
|
378
|
+
const cos = Math.cos(pitch);
|
|
379
|
+
const sin = Math.sin(pitch);
|
|
380
|
+
const along = 2 * cos - resolved.lookHeight * sin;
|
|
335
381
|
return {
|
|
336
|
-
x: follow.x
|
|
337
|
-
y: follow.y + resolved.
|
|
338
|
-
z: follow.z
|
|
382
|
+
x: follow.x + forward.x * along,
|
|
383
|
+
y: follow.y + 2 * sin + resolved.lookHeight * cos,
|
|
384
|
+
z: follow.z + forward.z * along,
|
|
339
385
|
};
|
|
340
386
|
}
|
|
341
|
-
/**
|
|
387
|
+
/** @internal */
|
|
388
|
+
export function createChaseRigState() {
|
|
389
|
+
return { offset: null, anchorYaw: null, lastYaw: null, lastFollow: null, roll: 0, pitch: 0, fov: null, kick: 0, lookingBack: false };
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* One frame of the chase rig: speed from sim velocity, smoothed FOV plus decaying kicks, drift-lag
|
|
393
|
+
* anchor yaw, pitch follow, distance by speed, look-back, and a boom spring that eases the offset
|
|
394
|
+
* from the target rather than its world position, so the camera holds its distance at speed.
|
|
342
395
|
* @internal
|
|
343
396
|
*/
|
|
344
|
-
export function
|
|
345
|
-
const
|
|
397
|
+
export function stepChase(state, sample, resolved, dt, clampBoom) {
|
|
398
|
+
const { follow, yaw } = sample;
|
|
399
|
+
const last = state.lastFollow ?? follow;
|
|
400
|
+
const velocity = sample.velocity ?? (dt > 0 ? { x: (follow.x - last.x) / dt, y: 0, z: (follow.z - last.z) / dt } : { x: 0, y: 0, z: 0 });
|
|
401
|
+
state.lastFollow = follow;
|
|
402
|
+
const speed = Math.hypot(velocity.x, velocity.z);
|
|
403
|
+
const lastYaw = state.lastYaw ?? yaw;
|
|
404
|
+
state.lastYaw = yaw;
|
|
405
|
+
state.roll = bankRollStep(state.roll, yaw, lastYaw, dt, resolved);
|
|
406
|
+
const targetFov = speedToFov(speed, { base: resolved.fovBase, max: resolved.fovMax, speedForMax: resolved.fovSpeedForMax });
|
|
407
|
+
state.fov =
|
|
408
|
+
state.fov === null || !Number.isFinite(resolved.fovResponse)
|
|
409
|
+
? targetFov
|
|
410
|
+
: state.fov + (targetFov - state.fov) * smoothBlend(dt, resolved.fovResponse);
|
|
411
|
+
state.kick = clamp(state.kick * Math.exp(-resolved.fovKickDecay * dt) + sample.fovKick, -resolved.fovKickMax, resolved.fovKickMax);
|
|
412
|
+
let targetYaw = yaw;
|
|
413
|
+
let response = resolved.yawResponse;
|
|
414
|
+
if (resolved.velocityYawBlend > 0) {
|
|
415
|
+
targetYaw = velocityYawTarget(yaw, velocity, resolved);
|
|
416
|
+
response = resolved.velocityYawResponse;
|
|
417
|
+
}
|
|
418
|
+
const anchorYaw = Number.isFinite(response)
|
|
419
|
+
? smoothYaw(state.anchorYaw ?? targetYaw, targetYaw, response, dt)
|
|
420
|
+
: targetYaw;
|
|
421
|
+
state.anchorYaw = anchorYaw;
|
|
422
|
+
const pitchTarget = clamp(-sample.bodyPitch * resolved.pitchBlend, -resolved.pitchMax, resolved.pitchMax);
|
|
423
|
+
state.pitch =
|
|
424
|
+
resolved.pitchBlend <= 0 ? 0 : state.pitch + (pitchTarget - state.pitch) * smoothBlend(dt, resolved.pitchResponse);
|
|
425
|
+
const snap = state.offset === null || sample.lookBack !== state.lookingBack;
|
|
426
|
+
state.lookingBack = sample.lookBack;
|
|
427
|
+
const boomYaw = sample.lookBack ? anchorYaw + Math.PI : anchorYaw;
|
|
428
|
+
const boomPitch = sample.lookBack ? -state.pitch : state.pitch;
|
|
429
|
+
const extra = resolved.distanceSpeedForMax > 0 ? resolved.distanceExtra * clamp(speed / resolved.distanceSpeedForMax, 0, 1) : 0;
|
|
430
|
+
const led = sample.lookBack ? follow : leadFromVelocity(follow, velocity, resolved);
|
|
431
|
+
const desiredOffset = chaseBoomOffset(boomYaw, boomPitch, resolved.distance + extra, resolved.height);
|
|
432
|
+
let offset = snap || state.offset === null ? desiredOffset : springArmStep(state.offset, desiredOffset, resolved.springDamping, dt);
|
|
433
|
+
if (clampBoom !== undefined) {
|
|
434
|
+
const up = chaseLookAt(led, boomYaw, resolved, boomPitch);
|
|
435
|
+
const pivot = { x: led.x, y: up.y, z: led.z };
|
|
436
|
+
const wanted = { x: led.x + offset.x, y: led.y + offset.y, z: led.z + offset.z };
|
|
437
|
+
const allowed = clampBoom(pivot, wanted);
|
|
438
|
+
offset = { x: allowed.x - led.x, y: allowed.y - led.y, z: allowed.z - led.z };
|
|
439
|
+
}
|
|
440
|
+
state.offset = offset;
|
|
346
441
|
return {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
442
|
+
pose: {
|
|
443
|
+
position: { x: led.x + offset.x, y: led.y + offset.y, z: led.z + offset.z },
|
|
444
|
+
lookAt: chaseLookAt(led, boomYaw, resolved, boomPitch),
|
|
445
|
+
fov: state.fov + state.kick,
|
|
446
|
+
},
|
|
447
|
+
roll: sample.lookBack ? -state.roll : state.roll,
|
|
448
|
+
anchorYaw,
|
|
449
|
+
speed,
|
|
350
450
|
};
|
|
351
451
|
}
|
|
452
|
+
/**
|
|
453
|
+
* Where a blocked boom should put the camera: `hitDistance` along the pivot→desired ray, less
|
|
454
|
+
* `radius`, but never nearer the pivot than `minDistance`. `null` hit returns `desired` unchanged.
|
|
455
|
+
* @internal
|
|
456
|
+
*/
|
|
457
|
+
export function clampBoomToHit(pivot, desired, hitDistance, radius, minDistance) {
|
|
458
|
+
if (hitDistance === null)
|
|
459
|
+
return desired;
|
|
460
|
+
const dx = desired.x - pivot.x;
|
|
461
|
+
const dy = desired.y - pivot.y;
|
|
462
|
+
const dz = desired.z - pivot.z;
|
|
463
|
+
const length = Math.hypot(dx, dy, dz);
|
|
464
|
+
if (length <= 1e-6)
|
|
465
|
+
return desired;
|
|
466
|
+
const allowed = clamp(hitDistance - radius, Math.min(minDistance, length), length);
|
|
467
|
+
const scale = allowed / length;
|
|
468
|
+
return { x: pivot.x + dx * scale, y: pivot.y + dy * scale, z: pivot.z + dz * scale };
|
|
469
|
+
}
|
|
352
470
|
/**
|
|
353
471
|
* Drift-lag (#1051): the yaw the chase anchor should aim before temporal
|
|
354
472
|
* smoothing. When the target slides, its planar `velocity` diverges from its
|
package/dist/defineGame.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type ComponentType } from "react";
|
|
2
|
+
import type { EditorDocument } from "@jgengine/core/editor/types";
|
|
2
3
|
import { type GameDefinitionConfig, type GameLoop } from "@jgengine/core/game/defineGame";
|
|
4
|
+
import type { BackdropConfig } from "@jgengine/core/game/playableGame";
|
|
3
5
|
import type { GameContext, GameContextContent } from "@jgengine/core/runtime/gameContext";
|
|
4
6
|
import type { ModelAssetRef } from "@jgengine/core/scene/assetCatalog";
|
|
5
7
|
import type { ModelConfig } from "@jgengine/core/game/playableGame";
|
|
@@ -19,6 +21,13 @@ type PresentationFields = Omit<PlayableGame, "game" | "content" | "loop" | "Game
|
|
|
19
21
|
sceneScatterModels?: Record<string, string | ModelConfig>;
|
|
20
22
|
};
|
|
21
23
|
export type GameConfig<TAssetRef extends ModelAssetRef = ModelAssetRef> = EngineFields<TAssetRef> & PresentationFields;
|
|
24
|
+
/**
|
|
25
|
+
* The scene document's authored sky fills `backdrop.sky` when the game did not set one, so editor
|
|
26
|
+
* lighting-workspace edits render in `place()` worlds too. A document environment carrying only
|
|
27
|
+
* point lights yields no sky fields and leaves the world's sky alone.
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
export declare function withDocumentSky(backdrop: BackdropConfig | undefined, doc: EditorDocument | undefined): BackdropConfig | undefined;
|
|
22
31
|
/**
|
|
23
32
|
* The one public authoring entry point: compose engine fields (systems, world, physics, input) and
|
|
24
33
|
* presentation fields (camera, HUD, audio, authored scene) into a `PlayableGame` ready for `GameHost`.
|
package/dist/defineGame.js
CHANGED
|
@@ -3,7 +3,7 @@ import { useMemo } from "react";
|
|
|
3
3
|
import { defineGameDefinition as defineEngineGame, } from "@jgengine/core/game/defineGame";
|
|
4
4
|
import { syncLifecyclePhase } from "@jgengine/core/game/gamePhase";
|
|
5
5
|
import { offline } from "@jgengine/core/runtime/adapter";
|
|
6
|
-
import { lightingFromDocument } from "@jgengine/core/editor/environment";
|
|
6
|
+
import { lightingFromDocument, skyFromDocument } from "@jgengine/core/editor/environment";
|
|
7
7
|
import { resolveGameLook } from "@jgengine/core/render/lookPreset";
|
|
8
8
|
import { EnvironmentScene } from "./environment.js";
|
|
9
9
|
import { terrainGroundColorSampler } from "./environment/terrainGroundColor.js";
|
|
@@ -14,6 +14,20 @@ function worldBackdrop(feature) {
|
|
|
14
14
|
return _jsx(EnvironmentScene, { feature: feature });
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* The scene document's authored sky fills `backdrop.sky` when the game did not set one, so editor
|
|
19
|
+
* lighting-workspace edits render in `place()` worlds too. A document environment carrying only
|
|
20
|
+
* point lights yields no sky fields and leaves the world's sky alone.
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
export function withDocumentSky(backdrop, doc) {
|
|
24
|
+
if (backdrop?.sky !== undefined || doc === undefined)
|
|
25
|
+
return backdrop;
|
|
26
|
+
const sky = skyFromDocument(doc);
|
|
27
|
+
if (sky === undefined || Object.keys(sky).length === 0)
|
|
28
|
+
return backdrop;
|
|
29
|
+
return { ...backdrop, sky };
|
|
30
|
+
}
|
|
17
31
|
function isEnvironmentSource(value) {
|
|
18
32
|
return typeof value === "object" && value !== null && "kind" in value && ["gradient", "hdri", "cube"].includes(value.kind);
|
|
19
33
|
}
|
|
@@ -33,7 +47,7 @@ const emptyUi = () => null;
|
|
|
33
47
|
* @capability define-game single public game-authoring path — compose systems, world, presentation, and authored scene in one definition
|
|
34
48
|
*/
|
|
35
49
|
export function defineGame(config) {
|
|
36
|
-
const { content, loop, GameUI, environment, camera, multiplayer, editorLayers, editorCatalogs, scenePlacement, sceneScatterModels, WorldOverlay, viewmodel, renderEntity, renderObject, entitySprites, entityModels, objectModels, hotbarSelection, prompts, pointer, touch, orientation, worldHealthBars, nameplates, presentationEffects, audio, entitySounds, objectSounds, worldItem, collision, movement, lighting: authoredLighting, environmentSource, backdrop, look, postProcessing, shadows, graphics, presentation, objectStyles, devtools, capture, platforms, hudFit, ...engineFields } = config;
|
|
50
|
+
const { content, loop, GameUI, environment, camera, multiplayer, editorLayers, editorCatalogs, scenePlacement, sceneScatterModels, WorldOverlay, viewmodel, renderEntity, renderObject, entitySprites, entityModels, objectModels, hotbarSelection, prompts, pointer, touch, gamepad, localPlayers, viewports, orientation, worldHealthBars, nameplates, presentationEffects, audio, entitySounds, objectSounds, worldItem, collision, movement, lighting: authoredLighting, environmentSource, backdrop, look, postProcessing, shadows, graphics, presentation, objectStyles, devtools, capture, platforms, hudFit, ...engineFields } = config;
|
|
37
51
|
const game = defineEngineGame({
|
|
38
52
|
...engineFields,
|
|
39
53
|
multiplayer: multiplayer ?? offline(),
|
|
@@ -42,7 +56,7 @@ export function defineGame(config) {
|
|
|
42
56
|
const resolvedLook = resolveGameLook({
|
|
43
57
|
look,
|
|
44
58
|
lighting: authoredLighting,
|
|
45
|
-
backdrop,
|
|
59
|
+
backdrop: withDocumentSky(backdrop, editorLayers),
|
|
46
60
|
postProcessing,
|
|
47
61
|
hasWorldSky: editorLayers?.environment?.preset !== undefined,
|
|
48
62
|
});
|
|
@@ -84,6 +98,9 @@ export function defineGame(config) {
|
|
|
84
98
|
prompts,
|
|
85
99
|
pointer,
|
|
86
100
|
touch,
|
|
101
|
+
gamepad,
|
|
102
|
+
localPlayers,
|
|
103
|
+
viewports,
|
|
87
104
|
orientation,
|
|
88
105
|
worldHealthBars,
|
|
89
106
|
nameplates,
|
|
@@ -5,6 +5,7 @@ import { devtools, instrumentLatency, } from "@jgengine/core/devtools/devtools";
|
|
|
5
5
|
import { fallbackSeamsSnapshot } from "@jgengine/core/devtools/fallbackSeams";
|
|
6
6
|
import { textureErrorsSnapshot } from "@jgengine/core/devtools/textureErrors";
|
|
7
7
|
import { MOVEMENT_TUNING } from "@jgengine/core/movement/movementModel";
|
|
8
|
+
import { heavyModels } from "../render/modelLoad.js";
|
|
8
9
|
import { collisionDebug } from "./collisionDebug.js";
|
|
9
10
|
import { readStoredOverrides } from "./devtoolsOverrides.js";
|
|
10
11
|
import { diagnose } from "./perfDiagnose.js";
|
|
@@ -212,6 +213,7 @@ export function DevtoolsOverlay({ open, ctx, playable, multiplayer, }) {
|
|
|
212
213
|
devtools.probes.register("objects", () => ctx.scene.object.list().length),
|
|
213
214
|
devtools.probes.register("fallbacks", () => fallbackSeamsSnapshot()),
|
|
214
215
|
devtools.probes.register("textureErrors", () => textureErrorsSnapshot()),
|
|
216
|
+
devtools.probes.register("heavyModels", () => heavyModels()),
|
|
215
217
|
];
|
|
216
218
|
return () => {
|
|
217
219
|
for (const dispose of disposers)
|
|
@@ -15,6 +15,12 @@ type EditorHost = {
|
|
|
15
15
|
gameId: string;
|
|
16
16
|
handle(request: AgentBridgeRequest): unknown;
|
|
17
17
|
};
|
|
18
|
+
/**
|
|
19
|
+
* @internal
|
|
20
|
+
* Resolves true once `isLive()` reports a mounted editor host, polling every `intervalMs` up to
|
|
21
|
+
* `timeoutMs`. `editor_summon` awaits this so the next RPC verb in a `drive` batch finds the editor.
|
|
22
|
+
*/
|
|
23
|
+
export declare function waitForEditorHost(isLive: () => boolean, timeoutMs?: number, intervalMs?: number, sleep?: (ms: number) => Promise<void>): Promise<boolean>;
|
|
18
24
|
declare global {
|
|
19
25
|
interface Window {
|
|
20
26
|
__jgengineAgent?: {
|
|
@@ -3,6 +3,20 @@ import { getSaveEndpoint } from "@jgengine/core/devtools/saveEndpoint";
|
|
|
3
3
|
import { isHudAnchor, listActiveHudLayouts, } from "@jgengine/core/ui/hudLayout";
|
|
4
4
|
import { listHudPanelTypes, resolvePanelResize, resizePanelSize, } from "@jgengine/core/ui/hudDocument";
|
|
5
5
|
import { buildFullReport, buildLeanReport } from "./DevtoolsOverlay.js";
|
|
6
|
+
/**
|
|
7
|
+
* @internal
|
|
8
|
+
* Resolves true once `isLive()` reports a mounted editor host, polling every `intervalMs` up to
|
|
9
|
+
* `timeoutMs`. `editor_summon` awaits this so the next RPC verb in a `drive` batch finds the editor.
|
|
10
|
+
*/
|
|
11
|
+
export async function waitForEditorHost(isLive, timeoutMs = 15_000, intervalMs = 100, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
|
|
12
|
+
for (let waited = 0;; waited += intervalMs) {
|
|
13
|
+
if (isLive())
|
|
14
|
+
return true;
|
|
15
|
+
if (waited >= timeoutMs)
|
|
16
|
+
return false;
|
|
17
|
+
await sleep(intervalMs);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
6
20
|
async function saveScene(editorHost) {
|
|
7
21
|
const endpoint = getSaveEndpoint();
|
|
8
22
|
if (endpoint === null)
|
|
@@ -239,7 +253,7 @@ export function installAgentBridge(options) {
|
|
|
239
253
|
return { ok: false, error: "no editor summoner on this page — open with ?mode=editor" };
|
|
240
254
|
}
|
|
241
255
|
window.__jgengineSummonEditor();
|
|
242
|
-
return { ok: true, live: false };
|
|
256
|
+
return waitForEditorHost(() => window.__jgengineEditorHost !== undefined).then((live) => live ? { ok: true, live: true } : { ok: false, error: "editor did not mount within 15s" });
|
|
243
257
|
}
|
|
244
258
|
case "save_scene": {
|
|
245
259
|
const editorHost = window.__jgengineEditorHost;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ReactElement } from "react";
|
|
2
|
+
/** Mutable frame tally shared between the in-Canvas probe and the DOM readout. */
|
|
3
|
+
export interface FrameTally {
|
|
4
|
+
frames: number;
|
|
5
|
+
}
|
|
6
|
+
/** Counts rendered frames. Mount inside the Canvas. @internal */
|
|
7
|
+
export declare function FpsProbe({ tally }: {
|
|
8
|
+
tally: FrameTally;
|
|
9
|
+
}): null;
|
|
10
|
+
/**
|
|
11
|
+
* Player-facing frames-per-second readout, sampled twice a second from rendered frames (not
|
|
12
|
+
* display refreshes), so it reflects the frame-rate limit and GPU load.
|
|
13
|
+
* @internal shell-internal; shown by the `graphics.showFps` setting.
|
|
14
|
+
*/
|
|
15
|
+
export declare function FpsCounter({ tally }: {
|
|
16
|
+
tally: FrameTally;
|
|
17
|
+
}): ReactElement;
|