@jgengine/shell 0.7.0 → 0.9.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 +63 -0
- package/README.md +37 -1
- package/dist/GameHost.d.ts +12 -0
- package/dist/GameHost.js +33 -0
- package/dist/GamePlayer.d.ts +11 -0
- package/dist/GamePlayer.js +24 -0
- package/dist/GamePlayerShell.d.ts +36 -1
- package/dist/GamePlayerShell.js +751 -80
- package/dist/GameUiPreview.js +2 -1
- package/dist/audio/AudioComponents.js +25 -8
- package/dist/audio/audioEngine.d.ts +1 -0
- package/dist/audio/audioEngine.js +4 -0
- package/dist/behaviour.d.ts +12 -0
- package/dist/behaviour.js +27 -0
- package/dist/behaviourAttach.d.ts +9 -0
- package/dist/behaviourAttach.js +32 -0
- package/dist/behaviourDriver.d.ts +7 -0
- package/dist/behaviourDriver.js +21 -0
- package/dist/camera/GameCameraRig.d.ts +8 -3
- package/dist/camera/GameCameraRig.js +28 -14
- package/dist/camera/GameFirstPersonCamera.js +16 -0
- package/dist/camera/GameInspectionCamera.d.ts +6 -0
- package/dist/camera/GameInspectionCamera.js +24 -0
- package/dist/camera/GameOrbitCamera.js +10 -1
- package/dist/camera/PlayerFov.d.ts +18 -0
- package/dist/camera/PlayerFov.js +48 -0
- package/dist/camera/cameraBlendMath.d.ts +13 -0
- package/dist/camera/cameraBlendMath.js +36 -0
- package/dist/camera/cameraRigs.d.ts +8 -1
- package/dist/camera/cameraRigs.js +78 -38
- package/dist/camera/fovPreference.d.ts +25 -0
- package/dist/camera/fovPreference.js +73 -0
- package/dist/camera/index.d.ts +7 -2
- package/dist/camera/index.js +7 -2
- package/dist/camera/inspectionCameraMath.d.ts +25 -0
- package/dist/camera/inspectionCameraMath.js +44 -0
- package/dist/camera/rigMath.d.ts +50 -1
- package/dist/camera/rigMath.js +91 -1
- package/dist/camera/rigResolve.d.ts +15 -0
- package/dist/camera/rigResolve.js +52 -0
- package/dist/camera/shakeChannel.d.ts +3 -17
- package/dist/camera/shakeChannel.js +2 -21
- package/dist/camera/shakeChannelMath.d.ts +8 -0
- package/dist/camera/shakeChannelMath.js +21 -0
- package/dist/cartridge.d.ts +145 -0
- package/dist/cartridge.js +245 -0
- package/dist/defineGame.d.ts +16 -0
- package/dist/defineGame.js +52 -0
- package/dist/devtools/CollisionDebugWorld.d.ts +5 -0
- package/dist/devtools/CollisionDebugWorld.js +180 -0
- package/dist/devtools/DevtoolsOverlay.d.ts +78 -0
- package/dist/devtools/DevtoolsOverlay.js +676 -0
- package/dist/devtools/collisionDebug.d.ts +57 -0
- package/dist/devtools/collisionDebug.js +127 -0
- package/dist/devtools/collisionDebugMath.d.ts +102 -0
- package/dist/devtools/collisionDebugMath.js +128 -0
- package/dist/environment/Daylight.d.ts +56 -0
- package/dist/environment/Daylight.js +101 -0
- package/dist/environment/EnvironmentScene.js +87 -48
- package/dist/environment/GroundPad.d.ts +7 -0
- package/dist/environment/GroundPad.js +12 -0
- package/dist/environment/daylightCycle.d.ts +26 -0
- package/dist/environment/daylightCycle.js +116 -0
- package/dist/environment/groundPadMath.d.ts +13 -0
- package/dist/environment/groundPadMath.js +10 -0
- package/dist/environment/index.d.ts +3 -0
- package/dist/environment/index.js +3 -0
- package/dist/environment/skyLightingPolicy.d.ts +10 -0
- package/dist/environment/skyLightingPolicy.js +6 -0
- package/dist/input/mouseLook.d.ts +27 -0
- package/dist/input/mouseLook.js +35 -0
- package/dist/materialOverride.d.ts +6 -0
- package/dist/materialOverride.js +28 -0
- package/dist/multiplayer.d.ts +16 -9
- package/dist/multiplayer.js +62 -12
- package/dist/pointer/PointerProbe.js +20 -3
- package/dist/pointer/pointerService.d.ts +5 -0
- package/dist/pointer/pointerService.js +51 -24
- package/dist/registry.d.ts +4 -1
- package/dist/registry.js +3 -1
- package/dist/render/modelRender.d.ts +23 -0
- package/dist/render/modelRender.js +106 -0
- package/dist/render/resolveModel.d.ts +14 -0
- package/dist/render/resolveModel.js +24 -0
- package/dist/settings/QuickControls.d.ts +4 -0
- package/dist/settings/QuickControls.js +42 -0
- package/dist/settings/SettingsChrome.d.ts +1 -0
- package/dist/settings/SettingsChrome.js +10 -0
- package/dist/settings/SettingsMenu.d.ts +6 -0
- package/dist/settings/SettingsMenu.js +148 -0
- package/dist/settings/SettingsRuntime.d.ts +11 -0
- package/dist/settings/SettingsRuntime.js +19 -0
- package/dist/settings/appliedSettings.d.ts +14 -0
- package/dist/settings/appliedSettings.js +27 -0
- package/dist/settings/settingsController.d.ts +20 -0
- package/dist/settings/settingsController.js +165 -0
- package/dist/structures/GeneratedBuilding.d.ts +10 -0
- package/dist/structures/GeneratedBuilding.js +96 -8
- package/dist/structures/index.d.ts +1 -1
- package/dist/structures/index.js +1 -1
- package/dist/terrain/CarvedTerrain.d.ts +2 -1
- package/dist/terrain/CarvedTerrain.js +3 -3
- package/dist/terrain/GrassField.d.ts +3 -1
- package/dist/terrain/GrassField.js +4 -2
- package/dist/terrain/grassBudget.d.ts +3 -0
- package/dist/terrain/grassBudget.js +6 -0
- package/dist/terrain/grassGeometry.js +1 -1
- package/dist/terrain/index.d.ts +1 -1
- package/dist/terrain/index.js +1 -1
- package/dist/terrain/terrainMath.d.ts +9 -0
- package/dist/terrain/terrainMath.js +17 -2
- package/dist/touch/OrientationHint.d.ts +3 -0
- package/dist/touch/OrientationHint.js +13 -0
- package/dist/touch/TouchControlsOverlay.d.ts +34 -0
- package/dist/touch/TouchControlsOverlay.js +279 -0
- package/dist/visibility/CullingProvider.d.ts +21 -0
- package/dist/visibility/CullingProvider.js +134 -0
- package/dist/vision/FrustumSensorHud.d.ts +1 -6
- package/dist/vision/FrustumSensorHud.js +25 -27
- package/dist/vision/frustumSampleEqual.d.ts +2 -0
- package/dist/vision/frustumSampleEqual.js +10 -0
- package/dist/water/Ocean.js +1 -1
- package/dist/water/OceanConfig.d.ts +13 -0
- package/dist/water/OceanConfig.js +25 -17
- package/dist/water/OceanShader.d.ts +1 -1
- package/dist/water/OceanShader.js +3 -3
- package/dist/water/index.d.ts +1 -1
- package/dist/water/index.js +1 -1
- package/dist/weather/FireSpreadLayer.js +7 -2
- package/dist/weather/RainField.d.ts +3 -1
- package/dist/weather/RainField.js +4 -4
- package/dist/weather/SnowField.d.ts +3 -1
- package/dist/weather/SnowField.js +4 -4
- package/dist/weather/fireSpreadPose.d.ts +2 -0
- package/dist/weather/fireSpreadPose.js +4 -0
- package/dist/weather/weatherMath.d.ts +5 -1
- package/dist/weather/weatherMath.js +7 -2
- package/dist/world/DataObjects.d.ts +44 -0
- package/dist/world/DataObjects.js +77 -0
- package/dist/world/GridWorldScene.d.ts +10 -0
- package/dist/world/GridWorldScene.js +54 -0
- package/dist/world/SpriteBatch.d.ts +44 -0
- package/dist/world/SpriteBatch.js +112 -0
- package/dist/world/WorldHud.d.ts +8 -1
- package/dist/world/WorldHud.js +90 -39
- package/dist/world/WorldItems.js +21 -2
- package/dist/world/entityPose.d.ts +14 -0
- package/dist/world/entityPose.js +10 -0
- package/dist/world/telegraphPulse.d.ts +1 -0
- package/dist/world/telegraphPulse.js +4 -0
- package/dist/world/worldBarSamples.d.ts +30 -0
- package/dist/world/worldBarSamples.js +51 -0
- package/llms.txt +2643 -0
- package/package.json +6 -5
- package/dist/demo/builderDemo.d.ts +0 -2
- package/dist/demo/builderDemo.js +0 -189
- package/dist/demo/demoGame.d.ts +0 -2
- package/dist/demo/demoGame.js +0 -208
- package/dist/demo/environmentShowcase.d.ts +0 -2
- package/dist/demo/environmentShowcase.js +0 -15
- package/dist/demo/mapDemo.d.ts +0 -2
- package/dist/demo/mapDemo.js +0 -206
- package/dist/demo/pointerDemo.d.ts +0 -2
- package/dist/demo/pointerDemo.js +0 -151
- package/dist/demo/sensorShowcase.d.ts +0 -2
- package/dist/demo/sensorShowcase.js +0 -131
- package/dist/demo/survivalDemo.d.ts +0 -2
- package/dist/demo/survivalDemo.js +0 -291
|
@@ -4,8 +4,11 @@ import { PerspectiveCamera, Quaternion, Vector3 } from "three";
|
|
|
4
4
|
import { usePlayer } from "@jgengine/react/hooks";
|
|
5
5
|
import { useGameContext } from "@jgengine/react/provider";
|
|
6
6
|
import { ORBIT_CAMERA_FRAME_PRIORITY } from "./orbitCameraMath.js";
|
|
7
|
-
import { chaseDesiredPosition, chaseLookAt, cinematicSample, clamp, lockOnPose, observerPose, resolveChase, resolveObserver, resolveShoulder, resolveTopDown, seatPose, shoulderPose,
|
|
7
|
+
import { bankRollStep, chaseDesiredPosition, chaseLookAt, cinematicSample, clamp, leadFollowPoint, lockOnPose, observerPose, resolveChase, resolveObserver, resolveShoulder, resolveSideScroll, resolveSideScrollPose, resolveTopDown, seatPose, shoulderPose, sideScrollFollowBlend, smoothYaw, speedToFov, springArmStep, topDownPose, } from "./rigMath.js";
|
|
8
|
+
import { usePlayerFov } from "./PlayerFov.js";
|
|
8
9
|
import { useCameraShake } from "./shakeChannel.js";
|
|
10
|
+
import { applyCameraBlendStep, captureCameraBlendFrom, createCameraBlendScratch, } from "./cameraBlendMath.js";
|
|
11
|
+
export { applyCameraBlendStep, captureCameraBlendFrom, createCameraBlendScratch, } from "./cameraBlendMath.js";
|
|
9
12
|
export const CAMERA_RIG_FRAME_PRIORITY = ORBIT_CAMERA_FRAME_PRIORITY;
|
|
10
13
|
export const CAMERA_POST_FRAME_PRIORITY = ORBIT_CAMERA_FRAME_PRIORITY + 0.5;
|
|
11
14
|
function isPerspective(camera) {
|
|
@@ -47,39 +50,35 @@ function readFollow(ctx, followId) {
|
|
|
47
50
|
function useCameraCommit(props, followId) {
|
|
48
51
|
const camera = useThree((state) => state.camera);
|
|
49
52
|
const shake = useCameraShake();
|
|
53
|
+
const playerFov = usePlayerFov();
|
|
50
54
|
const shakeConfig = props.config?.shake;
|
|
51
55
|
const transitionSeconds = props.config?.transitionSeconds ?? 0.6;
|
|
52
|
-
const
|
|
56
|
+
const blendScratchRef = useRef(createCameraBlendScratch(Vector3, Quaternion));
|
|
57
|
+
const blendingRef = useRef(false);
|
|
53
58
|
const lastPoseRef = useRef(null);
|
|
59
|
+
const farWarnedRef = useRef(false);
|
|
54
60
|
const beginTransition = () => {
|
|
55
61
|
if (transitionSeconds <= 0)
|
|
56
62
|
return;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
quaternion: camera.quaternion.clone(),
|
|
60
|
-
fov: currentFov(camera),
|
|
61
|
-
elapsed: 0,
|
|
62
|
-
duration: transitionSeconds,
|
|
63
|
-
};
|
|
63
|
+
captureCameraBlendFrom(blendScratchRef.current, camera.position, camera.quaternion, currentFov(camera), transitionSeconds);
|
|
64
|
+
blendingRef.current = true;
|
|
64
65
|
};
|
|
65
66
|
const commit = (pose, dt) => {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
camera.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (t >= 1)
|
|
82
|
-
blendRef.current = null;
|
|
67
|
+
const composed = {
|
|
68
|
+
...pose,
|
|
69
|
+
fov: playerFov.compose(pose.fov, props.absoluteFov === true ? "absolute" : "relative"),
|
|
70
|
+
};
|
|
71
|
+
lastPoseRef.current = composed;
|
|
72
|
+
applyPose(camera, composed);
|
|
73
|
+
const lookDistance = Math.hypot(pose.position.x - pose.lookAt.x, pose.position.y - pose.lookAt.y, pose.position.z - pose.lookAt.z);
|
|
74
|
+
if (!farWarnedRef.current && isPerspective(camera) && lookDistance > camera.far) {
|
|
75
|
+
farWarnedRef.current = true;
|
|
76
|
+
console.warn(`[jgengine:camera] the "${props.config?.rig ?? "orbit"}" rig placed the camera ${Math.round(lookDistance)} world units from its look target, beyond the far plane (${camera.far}) — everything near the target is frustum-culled and the world renders empty. Check the rig config (topDown pitch is elevation: PI/2 = straight down, near 0 = grazing) or raise camera.frustum.far.`);
|
|
77
|
+
}
|
|
78
|
+
if (blendingRef.current) {
|
|
79
|
+
const done = applyCameraBlendStep(blendScratchRef.current, camera, composed.fov, dt);
|
|
80
|
+
if (done)
|
|
81
|
+
blendingRef.current = false;
|
|
83
82
|
}
|
|
84
83
|
shake.step(dt);
|
|
85
84
|
const offset = shake.sample(shakeConfig);
|
|
@@ -90,9 +89,9 @@ function useCameraCommit(props, followId) {
|
|
|
90
89
|
}
|
|
91
90
|
props.config?.onCameraFollow?.({
|
|
92
91
|
entityId: followId ?? "",
|
|
93
|
-
target:
|
|
92
|
+
target: composed.lookAt,
|
|
94
93
|
camera: { x: camera.position.x, y: camera.position.y, z: camera.position.z },
|
|
95
|
-
distance: Math.hypot(camera.position.x -
|
|
94
|
+
distance: Math.hypot(camera.position.x - composed.lookAt.x, camera.position.y - composed.lookAt.y, camera.position.z - composed.lookAt.z),
|
|
96
95
|
});
|
|
97
96
|
};
|
|
98
97
|
return { camera, commit, beginTransition };
|
|
@@ -141,6 +140,32 @@ export function TopDownRig(props) {
|
|
|
141
140
|
}, CAMERA_RIG_FRAME_PRIORITY);
|
|
142
141
|
return null;
|
|
143
142
|
}
|
|
143
|
+
/** Fixed side-on 2.5D follow rig: watches the followed entity from the perpendicular axis, never reading WASD/mouse-look. */
|
|
144
|
+
export function SideScrollRig(props) {
|
|
145
|
+
const { userId } = usePlayer();
|
|
146
|
+
const ctx = useGameContext();
|
|
147
|
+
const followId = resolveFollowId(props.followEntityId, userId);
|
|
148
|
+
const config = props.config?.sideScroll;
|
|
149
|
+
const resolved = useMemo(() => resolveSideScroll(config), [config]);
|
|
150
|
+
const { camera, commit, beginTransition } = useCameraCommit(props, followId);
|
|
151
|
+
const followRef = useRef(null);
|
|
152
|
+
useEffect(beginTransition, []);
|
|
153
|
+
useFrame((_, dt) => {
|
|
154
|
+
const sample = readFollow(ctx, followId);
|
|
155
|
+
const desired = sample?.pos ?? { x: 0, y: 0, z: 0 };
|
|
156
|
+
const prev = followRef.current ?? desired;
|
|
157
|
+
const blend = sideScrollFollowBlend(resolved.followSmoothing, dt);
|
|
158
|
+
const follow = {
|
|
159
|
+
x: prev.x + (desired.x - prev.x) * blend,
|
|
160
|
+
y: prev.y + (desired.y - prev.y) * blend,
|
|
161
|
+
z: prev.z + (desired.z - prev.z) * blend,
|
|
162
|
+
};
|
|
163
|
+
followRef.current = follow;
|
|
164
|
+
const pose = resolveSideScrollPose(follow, resolved, config?.fov ?? currentFov(camera));
|
|
165
|
+
commit(pose, dt);
|
|
166
|
+
}, CAMERA_RIG_FRAME_PRIORITY);
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
144
169
|
function useHeldKeys(codes) {
|
|
145
170
|
const held = useRef(new Set());
|
|
146
171
|
useEffect(() => {
|
|
@@ -163,6 +188,7 @@ function useHeldKeys(codes) {
|
|
|
163
188
|
return held;
|
|
164
189
|
}
|
|
165
190
|
const RTS_PAN_KEYS = ["KeyW", "KeyA", "KeyS", "KeyD", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "KeyQ", "KeyE"];
|
|
191
|
+
const RTS_WASD_KEYS = new Set(["KeyW", "KeyA", "KeyS", "KeyD"]);
|
|
166
192
|
export function RtsRig(props) {
|
|
167
193
|
const { userId } = usePlayer();
|
|
168
194
|
const ctx = useGameContext();
|
|
@@ -170,7 +196,9 @@ export function RtsRig(props) {
|
|
|
170
196
|
const config = props.config?.rts;
|
|
171
197
|
const resolved = useMemo(() => resolveTopDown(config), [config]);
|
|
172
198
|
const { camera, commit, beginTransition } = useCameraCommit(props, followId);
|
|
173
|
-
const
|
|
199
|
+
const wasdPanEnabled = followId === null && props.panKeysEnabled !== false;
|
|
200
|
+
const heldCodes = useMemo(() => (wasdPanEnabled ? RTS_PAN_KEYS : RTS_PAN_KEYS.filter((code) => !RTS_WASD_KEYS.has(code))), [wasdPanEnabled]);
|
|
201
|
+
const held = useHeldKeys(heldCodes);
|
|
174
202
|
const centerRef = useRef({
|
|
175
203
|
x: config?.start?.x ?? 0,
|
|
176
204
|
y: 0,
|
|
@@ -390,21 +418,28 @@ export function ChaseRig(props) {
|
|
|
390
418
|
const ctx = useGameContext();
|
|
391
419
|
const followId = resolveFollowId(props.followEntityId, userId);
|
|
392
420
|
const config = props.config?.chase;
|
|
393
|
-
const
|
|
394
|
-
const { commit, beginTransition } = useCameraCommit(props, followId);
|
|
421
|
+
const staticResolved = useMemo(() => resolveChase(config), [config]);
|
|
422
|
+
const { camera, commit, beginTransition } = useCameraCommit(props, followId);
|
|
395
423
|
const shake = useCameraShake();
|
|
396
424
|
const posRef = useRef(null);
|
|
397
425
|
const lastFollowRef = useRef(null);
|
|
426
|
+
const lastYawRef = useRef(null);
|
|
427
|
+
const rollRef = useRef(0);
|
|
398
428
|
const view = config?.view ?? "chase";
|
|
399
429
|
useEffect(beginTransition, [view]);
|
|
400
430
|
useFrame((_, dt) => {
|
|
431
|
+
const tuning = ctx.camera.chaseTuning();
|
|
432
|
+
const resolved = tuning === null ? staticResolved : resolveChase({ ...config, ...tuning });
|
|
401
433
|
const sample = readFollow(ctx, followId);
|
|
402
434
|
const follow = sample?.pos ?? { x: 0, y: 0, z: 0 };
|
|
403
435
|
const yaw = sample?.yaw ?? 0;
|
|
404
436
|
const last = lastFollowRef.current ?? follow;
|
|
405
437
|
const speed = dt > 0 ? Math.hypot(follow.x - last.x, follow.z - last.z) / dt : 0;
|
|
406
438
|
lastFollowRef.current = follow;
|
|
407
|
-
const
|
|
439
|
+
const lastYaw = lastYawRef.current ?? yaw;
|
|
440
|
+
lastYawRef.current = yaw;
|
|
441
|
+
rollRef.current = bankRollStep(rollRef.current, yaw, lastYaw, dt, resolved);
|
|
442
|
+
const fov = speedToFov(speed, tuning?.fov ?? config?.fov);
|
|
408
443
|
if (view !== "chase") {
|
|
409
444
|
const seat = view === "cockpit"
|
|
410
445
|
? config?.seatOffsets?.cockpit ?? { x: 0, y: 1.2, z: 0.1 }
|
|
@@ -419,20 +454,23 @@ export function ChaseRig(props) {
|
|
|
419
454
|
commit(pose, dt);
|
|
420
455
|
return;
|
|
421
456
|
}
|
|
422
|
-
const
|
|
457
|
+
const led = leadFollowPoint(follow, last, dt, resolved);
|
|
458
|
+
const desired = chaseDesiredPosition(led, yaw, resolved);
|
|
423
459
|
const prev = posRef.current ?? desired;
|
|
424
460
|
const smoothed = springArmStep(prev, desired, resolved.springDamping, dt);
|
|
425
461
|
posRef.current = smoothed;
|
|
426
462
|
if (resolved.shakePerSpeed > 0 && speed > 0) {
|
|
427
463
|
shake.shake(Math.min(resolved.shakePerSpeed * speed * dt, 0.1));
|
|
428
464
|
}
|
|
429
|
-
const pose = { position: smoothed, lookAt: chaseLookAt(
|
|
465
|
+
const pose = { position: smoothed, lookAt: chaseLookAt(led, yaw, resolved), fov };
|
|
430
466
|
commit(pose, dt);
|
|
467
|
+
if (rollRef.current !== 0)
|
|
468
|
+
camera.rotateZ(rollRef.current);
|
|
431
469
|
}, CAMERA_RIG_FRAME_PRIORITY);
|
|
432
470
|
return null;
|
|
433
471
|
}
|
|
434
|
-
function observerSubject(ctx, config) {
|
|
435
|
-
const bind = config?.bind;
|
|
472
|
+
function observerSubject(ctx, config, followId) {
|
|
473
|
+
const bind = config?.bind ?? (followId !== null ? { kind: "entity", entityId: followId } : undefined);
|
|
436
474
|
if (bind?.kind === "entity") {
|
|
437
475
|
const entity = ctx.scene.entity.get(bind.entityId);
|
|
438
476
|
if (entity !== null) {
|
|
@@ -450,15 +488,17 @@ function observerSubject(ctx, config) {
|
|
|
450
488
|
* kill-cam rig. Distinct from every other rig, which drives from mouse/keys.
|
|
451
489
|
*/
|
|
452
490
|
export function ObserverRig(props) {
|
|
491
|
+
const { userId } = usePlayer();
|
|
453
492
|
const ctx = useGameContext();
|
|
454
493
|
const config = props.config?.observer;
|
|
494
|
+
const followId = resolveFollowId(props.followEntityId, userId);
|
|
455
495
|
const resolved = useMemo(() => resolveObserver(config), [config]);
|
|
456
496
|
const angleRef = useRef(config?.startAngle ?? 0);
|
|
457
|
-
const { camera, commit, beginTransition } = useCameraCommit(props, observerSubject(ctx, config).boundEntityId);
|
|
497
|
+
const { camera, commit, beginTransition } = useCameraCommit(props, observerSubject(ctx, config, followId).boundEntityId);
|
|
458
498
|
useEffect(beginTransition, []);
|
|
459
499
|
useFrame((_, dt) => {
|
|
460
500
|
angleRef.current += resolved.orbitSpeed * dt;
|
|
461
|
-
const { subject } = observerSubject(ctx, config);
|
|
501
|
+
const { subject } = observerSubject(ctx, config, followId);
|
|
462
502
|
const pose = observerPose(subject, angleRef.current, resolved, config?.fov ?? currentFov(camera));
|
|
463
503
|
commit(pose, dt);
|
|
464
504
|
}, CAMERA_RIG_FRAME_PRIORITY);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare const PLAYER_FOV_DEFAULT: 55;
|
|
2
|
+
export declare const PLAYER_FOV_MIN = 40;
|
|
3
|
+
export declare const PLAYER_FOV_MAX = 120;
|
|
4
|
+
export declare const PLAYER_FOV_STORAGE_KEY = "jgengine:player-fov";
|
|
5
|
+
export interface PlayerFovBounds {
|
|
6
|
+
min: number;
|
|
7
|
+
max: number;
|
|
8
|
+
defaultFov: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function resolvePlayerFovBounds(options?: {
|
|
11
|
+
min?: number;
|
|
12
|
+
max?: number;
|
|
13
|
+
default?: number;
|
|
14
|
+
}): PlayerFovBounds;
|
|
15
|
+
export declare function clampPlayerFov(value: unknown, min?: number, max?: number): number;
|
|
16
|
+
export declare function loadPlayerFov(bounds?: PlayerFovBounds, storage?: Pick<Storage, "getItem"> | null | undefined): number;
|
|
17
|
+
export declare function savePlayerFov(value: number, bounds?: PlayerFovBounds, storage?: Pick<Storage, "setItem"> | null | undefined): number;
|
|
18
|
+
/**
|
|
19
|
+
* Composition model for perspective rigs:
|
|
20
|
+
* - preference is the player base FOV
|
|
21
|
+
* - poseFov is the rig's authored FOV (includes chase-speed modulation, ADS zoom, transitions)
|
|
22
|
+
* - relative mode shifts the authored FOV by (preference − default) so mods still stack
|
|
23
|
+
* - absolute mode (cinematic keyframes) keeps the authored FOV as-is
|
|
24
|
+
*/
|
|
25
|
+
export declare function composePlayerFov(preference: number, poseFov: number, mode?: "relative" | "absolute", bounds?: PlayerFovBounds): number;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { CAMERA_FRUSTUM_DEFAULTS } from "@jgengine/core/game/playableGame";
|
|
2
|
+
export const PLAYER_FOV_DEFAULT = CAMERA_FRUSTUM_DEFAULTS.fov;
|
|
3
|
+
export const PLAYER_FOV_MIN = 40;
|
|
4
|
+
export const PLAYER_FOV_MAX = 120;
|
|
5
|
+
export const PLAYER_FOV_STORAGE_KEY = "jgengine:player-fov";
|
|
6
|
+
export function resolvePlayerFovBounds(options) {
|
|
7
|
+
const min = Number.isFinite(options?.min) ? options.min : PLAYER_FOV_MIN;
|
|
8
|
+
const max = Number.isFinite(options?.max) ? options.max : PLAYER_FOV_MAX;
|
|
9
|
+
const lo = Math.min(min, max);
|
|
10
|
+
const hi = Math.max(min, max);
|
|
11
|
+
const defaultFov = clampPlayerFov(options?.default ?? PLAYER_FOV_DEFAULT, lo, hi);
|
|
12
|
+
return { min: lo, max: hi, defaultFov };
|
|
13
|
+
}
|
|
14
|
+
export function clampPlayerFov(value, min = PLAYER_FOV_MIN, max = PLAYER_FOV_MAX) {
|
|
15
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
16
|
+
return clampPlayerFov(PLAYER_FOV_DEFAULT, min, max);
|
|
17
|
+
if (value < min)
|
|
18
|
+
return min;
|
|
19
|
+
if (value > max)
|
|
20
|
+
return max;
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
export function loadPlayerFov(bounds = resolvePlayerFovBounds(), storage = defaultStorage()) {
|
|
24
|
+
if (storage == null)
|
|
25
|
+
return bounds.defaultFov;
|
|
26
|
+
try {
|
|
27
|
+
const raw = storage.getItem(PLAYER_FOV_STORAGE_KEY);
|
|
28
|
+
if (raw === null)
|
|
29
|
+
return bounds.defaultFov;
|
|
30
|
+
return clampPlayerFov(Number(raw), bounds.min, bounds.max);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return bounds.defaultFov;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function savePlayerFov(value, bounds = resolvePlayerFovBounds(), storage = defaultStorage()) {
|
|
37
|
+
const next = clampPlayerFov(value, bounds.min, bounds.max);
|
|
38
|
+
if (storage == null)
|
|
39
|
+
return next;
|
|
40
|
+
try {
|
|
41
|
+
storage.setItem(PLAYER_FOV_STORAGE_KEY, String(next));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
/* ignore quota / private-mode failures */
|
|
45
|
+
}
|
|
46
|
+
return next;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Composition model for perspective rigs:
|
|
50
|
+
* - preference is the player base FOV
|
|
51
|
+
* - poseFov is the rig's authored FOV (includes chase-speed modulation, ADS zoom, transitions)
|
|
52
|
+
* - relative mode shifts the authored FOV by (preference − default) so mods still stack
|
|
53
|
+
* - absolute mode (cinematic keyframes) keeps the authored FOV as-is
|
|
54
|
+
*/
|
|
55
|
+
export function composePlayerFov(preference, poseFov, mode = "relative", bounds = resolvePlayerFovBounds()) {
|
|
56
|
+
if (mode === "absolute")
|
|
57
|
+
return clampPlayerFov(poseFov, bounds.min, bounds.max);
|
|
58
|
+
const pref = clampPlayerFov(preference, bounds.min, bounds.max);
|
|
59
|
+
if (!Number.isFinite(poseFov))
|
|
60
|
+
return pref;
|
|
61
|
+
return clampPlayerFov(pref + (poseFov - PLAYER_FOV_DEFAULT), bounds.min, bounds.max);
|
|
62
|
+
}
|
|
63
|
+
function defaultStorage() {
|
|
64
|
+
try {
|
|
65
|
+
if (typeof globalThis !== "undefined" && "localStorage" in globalThis) {
|
|
66
|
+
return globalThis.localStorage ?? null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
package/dist/camera/index.d.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
export { GameOrbitCamera, type CameraFollowListener, type GameOrbitCameraProps } from "./GameOrbitCamera.js";
|
|
2
2
|
export { GameFirstPersonCamera, type GameFirstPersonCameraProps } from "./GameFirstPersonCamera.js";
|
|
3
|
+
export { GameInspectionCamera, type GameInspectionCameraProps } from "./GameInspectionCamera.js";
|
|
3
4
|
export { GameCameraRig, resolveRigKind, type GameCameraRigProps } from "./GameCameraRig.js";
|
|
4
|
-
export {
|
|
5
|
+
export { PlayerFovProvider, PlayerFovSlider, usePlayerFov, type PlayerFovState, } from "./PlayerFov.js";
|
|
6
|
+
export { PLAYER_FOV_DEFAULT, PLAYER_FOV_MAX, PLAYER_FOV_MIN, PLAYER_FOV_STORAGE_KEY, clampPlayerFov, composePlayerFov, loadPlayerFov, resolvePlayerFovBounds, savePlayerFov, type PlayerFovBounds, } from "./fovPreference.js";
|
|
7
|
+
export { CAMERA_POST_FRAME_PRIORITY, CAMERA_RIG_FRAME_PRIORITY, ChaseRig, CinematicRig, LockOnRig, ObserverRig, RtsRig, ShoulderRig, SideScrollRig, TopDownRig, type RigProps, } from "./cameraRigs.js";
|
|
8
|
+
export { applyCameraBlendStep, captureCameraBlendFrom, createCameraBlendScratch, type CameraBlendScratch, } from "./cameraBlendMath.js";
|
|
5
9
|
export { CameraShakeContext, cameraShake, createCameraShakeChannel, defaultCameraShakeChannel, useCameraShake, type CameraShakeChannel, } from "./shakeChannel.js";
|
|
6
|
-
export { addTrauma, angleDelta, blendShoulder, chaseDesiredPosition, chaseLookAt, cinematicSample, clamp, createTrauma, crossfadePose, forwardVector, lerp, lockOnPose, observerPose, resolveChase, resolveObserver, resolveShoulder, resolveTopDown, rightVector, seatPose, shakeOffset, shoulderPose, smoothstep, smoothYaw, speedToFov, springArmStep, stepTrauma, topDownPose, yawTo, type CameraPose, type ResolvedChase, type ResolvedObserver, type ResolvedShoulder, type ResolvedTopDown, type ShakeOffset, type TraumaState, } from "./rigMath.js";
|
|
10
|
+
export { addTrauma, angleDelta, blendShoulder, chaseDesiredPosition, chaseLookAt, cinematicSample, clamp, createTrauma, crossfadePose, forwardVector, lerp, lockOnPose, observerPose, resolveChase, resolveDirectedCamera, resolveObserver, resolveShoulder, resolveSideScroll, resolveSideScrollPose, resolveTopDown, rightVector, rtsPanKeysConflict, seatPose, shakeOffset, shoulderPose, sideScrollFollowBlend, smoothstep, smoothYaw, speedToFov, springArmStep, stepTrauma, topDownPose, yawTo, type CameraPose, type DirectorCameraValues, type ResolvedChase, type ResolvedDirectedCamera, type ResolvedObserver, type ResolvedShoulder, type ResolvedSideScroll, type ResolvedTopDown, type ShakeOffset, type StaticCameraValues, type TraumaState, } from "./rigMath.js";
|
|
7
11
|
export { cameraFollowStep, cameraLookPitch, DEFAULT_ORBIT_CAMERA, distanceBetween, GAME_SIM_FRAME_PRIORITY, lerpVec3, ORBIT_CAMERA_FRAME_PRIORITY, orbitFollowStep, orbitYawFromCamera, resolveFollowTargetFromPosition, resolveOrbitCameraConfig, resolveTargetSmoothing, seedOrbitFollowState, smoothBlend, type CameraFollowState, type OrbitCameraConfig, type OrbitFollowRuntimeState, type ResolvedOrbitCameraConfig, type Vec3, } from "./orbitCameraMath.js";
|
|
12
|
+
export { resolveInspectionCameraConfig, resolveInspectionZoomToCursor, seedInspectionCamera, type InspectionCameraConfig, type InspectionZoomAnchor, type ResolvedInspectionCameraConfig, } from "./inspectionCameraMath.js";
|
package/dist/camera/index.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
export { GameOrbitCamera } from "./GameOrbitCamera.js";
|
|
2
2
|
export { GameFirstPersonCamera } from "./GameFirstPersonCamera.js";
|
|
3
|
+
export { GameInspectionCamera } from "./GameInspectionCamera.js";
|
|
3
4
|
export { GameCameraRig, resolveRigKind } from "./GameCameraRig.js";
|
|
4
|
-
export {
|
|
5
|
+
export { PlayerFovProvider, PlayerFovSlider, usePlayerFov, } from "./PlayerFov.js";
|
|
6
|
+
export { PLAYER_FOV_DEFAULT, PLAYER_FOV_MAX, PLAYER_FOV_MIN, PLAYER_FOV_STORAGE_KEY, clampPlayerFov, composePlayerFov, loadPlayerFov, resolvePlayerFovBounds, savePlayerFov, } from "./fovPreference.js";
|
|
7
|
+
export { CAMERA_POST_FRAME_PRIORITY, CAMERA_RIG_FRAME_PRIORITY, ChaseRig, CinematicRig, LockOnRig, ObserverRig, RtsRig, ShoulderRig, SideScrollRig, TopDownRig, } from "./cameraRigs.js";
|
|
8
|
+
export { applyCameraBlendStep, captureCameraBlendFrom, createCameraBlendScratch, } from "./cameraBlendMath.js";
|
|
5
9
|
export { CameraShakeContext, cameraShake, createCameraShakeChannel, defaultCameraShakeChannel, useCameraShake, } from "./shakeChannel.js";
|
|
6
|
-
export { addTrauma, angleDelta, blendShoulder, chaseDesiredPosition, chaseLookAt, cinematicSample, clamp, createTrauma, crossfadePose, forwardVector, lerp, lockOnPose, observerPose, resolveChase, resolveObserver, resolveShoulder, resolveTopDown, rightVector, seatPose, shakeOffset, shoulderPose, smoothstep, smoothYaw, speedToFov, springArmStep, stepTrauma, topDownPose, yawTo, } from "./rigMath.js";
|
|
10
|
+
export { addTrauma, angleDelta, blendShoulder, chaseDesiredPosition, chaseLookAt, cinematicSample, clamp, createTrauma, crossfadePose, forwardVector, lerp, lockOnPose, observerPose, resolveChase, resolveDirectedCamera, resolveObserver, resolveShoulder, resolveSideScroll, resolveSideScrollPose, resolveTopDown, rightVector, rtsPanKeysConflict, seatPose, shakeOffset, shoulderPose, sideScrollFollowBlend, smoothstep, smoothYaw, speedToFov, springArmStep, stepTrauma, topDownPose, yawTo, } from "./rigMath.js";
|
|
7
11
|
export { cameraFollowStep, cameraLookPitch, DEFAULT_ORBIT_CAMERA, distanceBetween, GAME_SIM_FRAME_PRIORITY, lerpVec3, ORBIT_CAMERA_FRAME_PRIORITY, orbitFollowStep, orbitYawFromCamera, resolveFollowTargetFromPosition, resolveOrbitCameraConfig, resolveTargetSmoothing, seedOrbitFollowState, smoothBlend, } from "./orbitCameraMath.js";
|
|
12
|
+
export { resolveInspectionCameraConfig, resolveInspectionZoomToCursor, seedInspectionCamera, } from "./inspectionCameraMath.js";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { InspectionCameraConfig, InspectionZoomAnchor } from "@jgengine/core/game/playableGame";
|
|
2
|
+
import type { Vec3 } from "./orbitCameraMath.js";
|
|
3
|
+
export type { InspectionCameraConfig, InspectionZoomAnchor };
|
|
4
|
+
export interface ResolvedInspectionCameraConfig {
|
|
5
|
+
anchor: InspectionZoomAnchor;
|
|
6
|
+
target: Vec3;
|
|
7
|
+
initialDistance: number;
|
|
8
|
+
initialPosition: Vec3 | null;
|
|
9
|
+
minDistance: number;
|
|
10
|
+
maxDistance: number;
|
|
11
|
+
minPolarAngle: number;
|
|
12
|
+
maxPolarAngle: number;
|
|
13
|
+
pan: boolean;
|
|
14
|
+
rotateSpeed: number;
|
|
15
|
+
zoomSpeed: number;
|
|
16
|
+
dampingFactor: number;
|
|
17
|
+
}
|
|
18
|
+
export declare function resolveInspectionCameraConfig(config?: InspectionCameraConfig): ResolvedInspectionCameraConfig;
|
|
19
|
+
/** Seeds the camera/target world position before OrbitControls mounts. Falls back to `initialDistance` behind `target` on the -Z axis, raised by 40% of that distance, when `initialPosition` is unset. */
|
|
20
|
+
export declare function seedInspectionCamera(config: ResolvedInspectionCameraConfig): {
|
|
21
|
+
camera: Vec3;
|
|
22
|
+
target: Vec3;
|
|
23
|
+
};
|
|
24
|
+
/** Maps the anchor mode onto three-stdlib OrbitControls' native `zoomToCursor` flag. */
|
|
25
|
+
export declare function resolveInspectionZoomToCursor(anchor: InspectionZoomAnchor): boolean;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export function resolveInspectionCameraConfig(config) {
|
|
2
|
+
return {
|
|
3
|
+
anchor: config?.anchor ?? "target",
|
|
4
|
+
target: {
|
|
5
|
+
x: config?.target?.x ?? 0,
|
|
6
|
+
y: config?.target?.y ?? 0,
|
|
7
|
+
z: config?.target?.z ?? 0,
|
|
8
|
+
},
|
|
9
|
+
initialDistance: config?.initialDistance ?? 6,
|
|
10
|
+
initialPosition: config?.initialPosition === undefined
|
|
11
|
+
? null
|
|
12
|
+
: {
|
|
13
|
+
x: config.initialPosition.x ?? 0,
|
|
14
|
+
y: config.initialPosition.y ?? 0,
|
|
15
|
+
z: config.initialPosition.z ?? 0,
|
|
16
|
+
},
|
|
17
|
+
minDistance: config?.minDistance ?? 2,
|
|
18
|
+
maxDistance: config?.maxDistance ?? 20,
|
|
19
|
+
minPolarAngle: config?.minPolarAngle ?? 0,
|
|
20
|
+
maxPolarAngle: config?.maxPolarAngle ?? Math.PI,
|
|
21
|
+
pan: config?.pan ?? true,
|
|
22
|
+
rotateSpeed: config?.rotateSpeed ?? 0.6,
|
|
23
|
+
zoomSpeed: config?.zoomSpeed ?? 0.6,
|
|
24
|
+
dampingFactor: config?.dampingFactor ?? 0.08,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** Seeds the camera/target world position before OrbitControls mounts. Falls back to `initialDistance` behind `target` on the -Z axis, raised by 40% of that distance, when `initialPosition` is unset. */
|
|
28
|
+
export function seedInspectionCamera(config) {
|
|
29
|
+
if (config.initialPosition !== null) {
|
|
30
|
+
return { camera: config.initialPosition, target: config.target };
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
camera: {
|
|
34
|
+
x: config.target.x,
|
|
35
|
+
y: config.target.y + config.initialDistance * 0.4,
|
|
36
|
+
z: config.target.z - config.initialDistance,
|
|
37
|
+
},
|
|
38
|
+
target: config.target,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Maps the anchor mode onto three-stdlib OrbitControls' native `zoomToCursor` flag. */
|
|
42
|
+
export function resolveInspectionZoomToCursor(anchor) {
|
|
43
|
+
return anchor === "cursor";
|
|
44
|
+
}
|
package/dist/camera/rigMath.d.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
import type { CameraKeyframe, ChaseCameraConfig, LockOnCameraConfig, ObserverCameraConfig, ShoulderCameraConfig, TopDownCameraConfig } from "@jgengine/core/game/playableGame";
|
|
1
|
+
import type { CameraKeyframe, ChaseCameraConfig, CinematicCameraConfig, LockOnCameraConfig, ObserverCameraConfig, ShoulderCameraConfig, SideScrollCameraConfig, TopDownCameraConfig } from "@jgengine/core/game/playableGame";
|
|
2
|
+
import type { ActionCodesMap } from "@jgengine/core/input/actionBindings";
|
|
2
3
|
import { type Vec3 } from "./orbitCameraMath.js";
|
|
3
4
|
export interface CameraPose {
|
|
4
5
|
position: Vec3;
|
|
5
6
|
lookAt: Vec3;
|
|
6
7
|
fov: number;
|
|
7
8
|
}
|
|
9
|
+
export declare function rtsPanKeysConflict(input: ActionCodesMap | undefined): boolean;
|
|
8
10
|
export declare function clamp(value: number, min: number, max: number): number;
|
|
9
11
|
export declare function lerp(from: number, to: number, blend: number): number;
|
|
10
12
|
export declare function smoothstep(t: number): number;
|
|
11
13
|
export declare function forwardVector(yaw: number): Vec3;
|
|
14
|
+
/** Screen-right of a yaw: `forward × up` with up = +Y. */
|
|
12
15
|
export declare function rightVector(yaw: number): Vec3;
|
|
13
16
|
export interface ResolvedTopDown {
|
|
14
17
|
height: number;
|
|
@@ -27,6 +30,22 @@ export declare function resolveTopDown(config: TopDownCameraConfig | undefined):
|
|
|
27
30
|
export declare function topDownPose(follow: Vec3, resolved: ResolvedTopDown, fov: number): CameraPose;
|
|
28
31
|
/** Exponential spring-arm approach toward a desired point (frame-rate independent). */
|
|
29
32
|
export declare function springArmStep(current: Vec3, desired: Vec3, damping: number, dt: number): Vec3;
|
|
33
|
+
export interface ResolvedSideScroll {
|
|
34
|
+
axis: "x" | "z";
|
|
35
|
+
distance: number;
|
|
36
|
+
height: number;
|
|
37
|
+
lookHeight: number;
|
|
38
|
+
followSmoothing: number;
|
|
39
|
+
}
|
|
40
|
+
export declare function resolveSideScroll(config: SideScrollCameraConfig | undefined): ResolvedSideScroll;
|
|
41
|
+
/** Frame-rate independent follow blend for the side-scroll rig; `followSmoothing <= 0` hard-locks (blend 1) instead of freezing. */
|
|
42
|
+
export declare function sideScrollFollowBlend(followSmoothing: number, dt: number): number;
|
|
43
|
+
/**
|
|
44
|
+
* Fixed lateral 2.5D follow pose: the camera sits perpendicular to the travel
|
|
45
|
+
* axis at `distance`, above the entity by `height`, and looks at the entity
|
|
46
|
+
* raised by `lookHeight`. Axis "x" watches from +z; axis "z" watches from +x.
|
|
47
|
+
*/
|
|
48
|
+
export declare function resolveSideScrollPose(entityPos: Vec3, resolved: ResolvedSideScroll, fov: number): CameraPose;
|
|
30
49
|
/** Speed→FOV curve: FOV climbs from base to max as speed rises to `speedForMax`. */
|
|
31
50
|
export declare function speedToFov(speed: number, curve: {
|
|
32
51
|
base?: number;
|
|
@@ -69,8 +88,17 @@ export interface ResolvedChase {
|
|
|
69
88
|
lookHeight: number;
|
|
70
89
|
springDamping: number;
|
|
71
90
|
shakePerSpeed: number;
|
|
91
|
+
leadTime: number;
|
|
92
|
+
leadMax: number;
|
|
93
|
+
bankPerYawRate: number;
|
|
94
|
+
bankMax: number;
|
|
95
|
+
bankDamping: number;
|
|
72
96
|
}
|
|
73
97
|
export declare function resolveChase(config: ChaseCameraConfig | undefined): ResolvedChase;
|
|
98
|
+
/** Velocity-lead the follow point (#286.9): aim `leadTime` seconds along the target's frame velocity, clamped to `leadMax`. */
|
|
99
|
+
export declare function leadFollowPoint(follow: Vec3, previous: Vec3, dt: number, resolved: ResolvedChase): Vec3;
|
|
100
|
+
/** One smoothing step of the chase rig's turn-bank roll (#286.10): roll opposes yaw rate, clamped and exponentially damped. */
|
|
101
|
+
export declare function bankRollStep(currentRoll: number, yaw: number, previousYaw: number, dt: number, resolved: ResolvedChase): number;
|
|
74
102
|
export interface ResolvedObserver {
|
|
75
103
|
distance: number;
|
|
76
104
|
height: number;
|
|
@@ -117,6 +145,27 @@ export declare function shakeOffset(state: TraumaState, config: {
|
|
|
117
145
|
} | undefined): ShakeOffset;
|
|
118
146
|
/** Linear cross-fade between two full camera poses (position, lookAt, fov). */
|
|
119
147
|
export declare function crossfadePose(from: CameraPose, to: CameraPose, t: number): CameraPose;
|
|
148
|
+
export interface DirectorCameraValues {
|
|
149
|
+
/** `undefined` = no runtime override (fall back to static); `null` = explicitly follow nothing. */
|
|
150
|
+
followEntityId?: string | null;
|
|
151
|
+
/** `null` = no runtime cinematic active (fall back to static). */
|
|
152
|
+
cinematic?: CinematicCameraConfig | null;
|
|
153
|
+
}
|
|
154
|
+
export interface StaticCameraValues {
|
|
155
|
+
followEntityId?: string | null;
|
|
156
|
+
cinematic?: CinematicCameraConfig;
|
|
157
|
+
}
|
|
158
|
+
export interface ResolvedDirectedCamera {
|
|
159
|
+
followEntityId: string | null | undefined;
|
|
160
|
+
cinematic: CinematicCameraConfig | undefined;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Merges a `CameraDirector` runtime snapshot over the static `GameCameraConfig`
|
|
164
|
+
* (#196.2). `director` omitted, or its fields `undefined`/`null`, is a pure
|
|
165
|
+
* passthrough to `staticConfig` so mounting a director with no active override
|
|
166
|
+
* changes nothing.
|
|
167
|
+
*/
|
|
168
|
+
export declare function resolveDirectedCamera(director: DirectorCameraValues | undefined, staticConfig: StaticCameraValues): ResolvedDirectedCamera;
|
|
120
169
|
export interface CinematicSample {
|
|
121
170
|
pose: CameraPose;
|
|
122
171
|
done: boolean;
|
package/dist/camera/rigMath.js
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import { lerpVec3, smoothBlend } from "./orbitCameraMath.js";
|
|
2
|
+
const RTS_WASD_CODES = ["KeyW", "KeyA", "KeyS", "KeyD"];
|
|
3
|
+
function actionCodeList(codes) {
|
|
4
|
+
if (Array.isArray(codes))
|
|
5
|
+
return codes;
|
|
6
|
+
const modes = codes;
|
|
7
|
+
return [...(modes.hold ?? []), ...(modes.toggle ?? [])];
|
|
8
|
+
}
|
|
9
|
+
export function rtsPanKeysConflict(input) {
|
|
10
|
+
if (input === undefined)
|
|
11
|
+
return false;
|
|
12
|
+
return Object.values(input).some((codes) => actionCodeList(codes).some((code) => RTS_WASD_CODES.includes(code)));
|
|
13
|
+
}
|
|
2
14
|
export function clamp(value, min, max) {
|
|
3
15
|
return value < min ? min : value > max ? max : value;
|
|
4
16
|
}
|
|
@@ -12,8 +24,9 @@ export function smoothstep(t) {
|
|
|
12
24
|
export function forwardVector(yaw) {
|
|
13
25
|
return { x: Math.sin(yaw), y: 0, z: Math.cos(yaw) };
|
|
14
26
|
}
|
|
27
|
+
/** Screen-right of a yaw: `forward × up` with up = +Y. */
|
|
15
28
|
export function rightVector(yaw) {
|
|
16
|
-
return { x: Math.cos(yaw), y: 0, z:
|
|
29
|
+
return { x: -Math.cos(yaw), y: 0, z: Math.sin(yaw) };
|
|
17
30
|
}
|
|
18
31
|
export function resolveTopDown(config) {
|
|
19
32
|
return {
|
|
@@ -56,6 +69,36 @@ export function topDownPose(follow, resolved, fov) {
|
|
|
56
69
|
export function springArmStep(current, desired, damping, dt) {
|
|
57
70
|
return lerpVec3(current, desired, smoothBlend(dt, damping));
|
|
58
71
|
}
|
|
72
|
+
export function resolveSideScroll(config) {
|
|
73
|
+
return {
|
|
74
|
+
axis: config?.axis ?? "x",
|
|
75
|
+
distance: config?.distance ?? 10,
|
|
76
|
+
height: config?.height ?? 3,
|
|
77
|
+
lookHeight: config?.lookHeight ?? 1,
|
|
78
|
+
followSmoothing: config?.followSmoothing ?? 8,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/** Frame-rate independent follow blend for the side-scroll rig; `followSmoothing <= 0` hard-locks (blend 1) instead of freezing. */
|
|
82
|
+
export function sideScrollFollowBlend(followSmoothing, dt) {
|
|
83
|
+
return followSmoothing <= 0 ? 1 : smoothBlend(dt, followSmoothing);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Fixed lateral 2.5D follow pose: the camera sits perpendicular to the travel
|
|
87
|
+
* axis at `distance`, above the entity by `height`, and looks at the entity
|
|
88
|
+
* raised by `lookHeight`. Axis "x" watches from +z; axis "z" watches from +x.
|
|
89
|
+
*/
|
|
90
|
+
export function resolveSideScrollPose(entityPos, resolved, fov) {
|
|
91
|
+
const perpendicular = resolved.axis === "x" ? { x: 0, y: 0, z: 1 } : { x: 1, y: 0, z: 0 };
|
|
92
|
+
return {
|
|
93
|
+
position: {
|
|
94
|
+
x: entityPos.x + perpendicular.x * resolved.distance,
|
|
95
|
+
y: entityPos.y + resolved.height,
|
|
96
|
+
z: entityPos.z + perpendicular.z * resolved.distance,
|
|
97
|
+
},
|
|
98
|
+
lookAt: { x: entityPos.x, y: entityPos.y + resolved.lookHeight, z: entityPos.z },
|
|
99
|
+
fov,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
59
102
|
/** Speed→FOV curve: FOV climbs from base to max as speed rises to `speedForMax`. */
|
|
60
103
|
export function speedToFov(speed, curve) {
|
|
61
104
|
const base = curve?.base ?? 55;
|
|
@@ -165,8 +208,42 @@ export function resolveChase(config) {
|
|
|
165
208
|
lookHeight: config?.lookHeight ?? 1.2,
|
|
166
209
|
springDamping: config?.springDamping ?? 6,
|
|
167
210
|
shakePerSpeed: config?.shakePerSpeed ?? 0,
|
|
211
|
+
leadTime: config?.lead?.time ?? 0,
|
|
212
|
+
leadMax: config?.lead?.max ?? 4,
|
|
213
|
+
bankPerYawRate: config?.bank?.perYawRate ?? 0,
|
|
214
|
+
bankMax: config?.bank?.max ?? 0.35,
|
|
215
|
+
bankDamping: config?.bank?.damping ?? 8,
|
|
168
216
|
};
|
|
169
217
|
}
|
|
218
|
+
/** Velocity-lead the follow point (#286.9): aim `leadTime` seconds along the target's frame velocity, clamped to `leadMax`. */
|
|
219
|
+
export function leadFollowPoint(follow, previous, dt, resolved) {
|
|
220
|
+
if (resolved.leadTime <= 0 || dt <= 0)
|
|
221
|
+
return follow;
|
|
222
|
+
let leadX = ((follow.x - previous.x) / dt) * resolved.leadTime;
|
|
223
|
+
let leadZ = ((follow.z - previous.z) / dt) * resolved.leadTime;
|
|
224
|
+
const magnitude = Math.hypot(leadX, leadZ);
|
|
225
|
+
if (magnitude > resolved.leadMax) {
|
|
226
|
+
leadX *= resolved.leadMax / magnitude;
|
|
227
|
+
leadZ *= resolved.leadMax / magnitude;
|
|
228
|
+
}
|
|
229
|
+
return { x: follow.x + leadX, y: follow.y, z: follow.z + leadZ };
|
|
230
|
+
}
|
|
231
|
+
function wrapAngle(angle) {
|
|
232
|
+
let wrapped = angle;
|
|
233
|
+
while (wrapped > Math.PI)
|
|
234
|
+
wrapped -= Math.PI * 2;
|
|
235
|
+
while (wrapped < -Math.PI)
|
|
236
|
+
wrapped += Math.PI * 2;
|
|
237
|
+
return wrapped;
|
|
238
|
+
}
|
|
239
|
+
/** One smoothing step of the chase rig's turn-bank roll (#286.10): roll opposes yaw rate, clamped and exponentially damped. */
|
|
240
|
+
export function bankRollStep(currentRoll, yaw, previousYaw, dt, resolved) {
|
|
241
|
+
if (resolved.bankPerYawRate <= 0)
|
|
242
|
+
return 0;
|
|
243
|
+
const yawRate = dt > 0 ? wrapAngle(yaw - previousYaw) / dt : 0;
|
|
244
|
+
const target = clamp(-yawRate * resolved.bankPerYawRate, -resolved.bankMax, resolved.bankMax);
|
|
245
|
+
return currentRoll + (target - currentRoll) * Math.min(1, resolved.bankDamping * dt);
|
|
246
|
+
}
|
|
170
247
|
export function resolveObserver(config) {
|
|
171
248
|
return {
|
|
172
249
|
distance: config?.distance ?? 8,
|
|
@@ -268,6 +345,19 @@ export function crossfadePose(from, to, t) {
|
|
|
268
345
|
fov: lerp(from.fov, to.fov, blend),
|
|
269
346
|
};
|
|
270
347
|
}
|
|
348
|
+
/**
|
|
349
|
+
* Merges a `CameraDirector` runtime snapshot over the static `GameCameraConfig`
|
|
350
|
+
* (#196.2). `director` omitted, or its fields `undefined`/`null`, is a pure
|
|
351
|
+
* passthrough to `staticConfig` so mounting a director with no active override
|
|
352
|
+
* changes nothing.
|
|
353
|
+
*/
|
|
354
|
+
export function resolveDirectedCamera(director, staticConfig) {
|
|
355
|
+
const directedFollow = director?.followEntityId;
|
|
356
|
+
const followEntityId = directedFollow === undefined ? staticConfig.followEntityId : directedFollow;
|
|
357
|
+
const directedCinematic = director?.cinematic;
|
|
358
|
+
const cinematic = directedCinematic === undefined || directedCinematic === null ? staticConfig.cinematic : directedCinematic;
|
|
359
|
+
return { followEntityId, cinematic };
|
|
360
|
+
}
|
|
271
361
|
/**
|
|
272
362
|
* Sample a keyframe path at `elapsed` seconds. Each keyframe's `duration` is the
|
|
273
363
|
* travel time from the previous keyframe into it; segment easing is per the
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CameraRigKind, GameCameraConfig } from "@jgengine/core/game/playableGame";
|
|
2
|
+
/**
|
|
3
|
+
* Resolves which rig mounts from a `GameCameraConfig`. Precedence, most to least
|
|
4
|
+
* specific: an explicit `rig` field always wins; then `perspective: "first"`
|
|
5
|
+
* (the historical shorthand for `rig: "orbit" | "first"`); then the mere presence
|
|
6
|
+
* of a rig's own config block selects that rig, checked in the fixed order below
|
|
7
|
+
* (#207.8) so a config carrying more than one block resolves deterministically
|
|
8
|
+
* instead of depending on object key order. Set `rig` explicitly to break a tie.
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveRigKind(config: GameCameraConfig | undefined): CameraRigKind;
|
|
11
|
+
/**
|
|
12
|
+
* The turntable rig is a flat facade over the observer's point-orbit mode: map
|
|
13
|
+
* its `target`/`distance`/… onto an observer block so ObserverRig runs unchanged.
|
|
14
|
+
*/
|
|
15
|
+
export declare function turntableAsObserver(config: GameCameraConfig | undefined): GameCameraConfig;
|