@jgengine/shell 0.8.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.
Files changed (127) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +11 -0
  3. package/dist/GamePlayerShell.d.ts +5 -2
  4. package/dist/GamePlayerShell.js +358 -98
  5. package/dist/GameUiPreview.js +2 -1
  6. package/dist/audio/AudioComponents.js +25 -8
  7. package/dist/audio/audioEngine.d.ts +1 -0
  8. package/dist/audio/audioEngine.js +4 -0
  9. package/dist/behaviour.d.ts +5 -24
  10. package/dist/behaviour.js +14 -50
  11. package/dist/behaviourAttach.d.ts +9 -0
  12. package/dist/behaviourAttach.js +32 -0
  13. package/dist/behaviourDriver.d.ts +7 -0
  14. package/dist/behaviourDriver.js +21 -0
  15. package/dist/camera/GameCameraRig.js +1 -1
  16. package/dist/camera/GameFirstPersonCamera.js +14 -0
  17. package/dist/camera/GameOrbitCamera.js +10 -1
  18. package/dist/camera/PlayerFov.d.ts +18 -0
  19. package/dist/camera/PlayerFov.js +48 -0
  20. package/dist/camera/cameraBlendMath.d.ts +13 -0
  21. package/dist/camera/cameraBlendMath.js +36 -0
  22. package/dist/camera/cameraRigs.d.ts +3 -0
  23. package/dist/camera/cameraRigs.js +36 -33
  24. package/dist/camera/fovPreference.d.ts +25 -0
  25. package/dist/camera/fovPreference.js +73 -0
  26. package/dist/camera/index.d.ts +3 -0
  27. package/dist/camera/index.js +3 -0
  28. package/dist/camera/rigMath.d.ts +10 -0
  29. package/dist/camera/rigMath.js +36 -1
  30. package/dist/camera/shakeChannel.d.ts +3 -17
  31. package/dist/camera/shakeChannel.js +2 -21
  32. package/dist/camera/shakeChannelMath.d.ts +8 -0
  33. package/dist/camera/shakeChannelMath.js +21 -0
  34. package/dist/cartridge.d.ts +145 -0
  35. package/dist/cartridge.js +245 -0
  36. package/dist/defineGame.js +2 -1
  37. package/dist/devtools/CollisionDebugWorld.d.ts +5 -0
  38. package/dist/devtools/CollisionDebugWorld.js +180 -0
  39. package/dist/devtools/DevtoolsOverlay.d.ts +65 -1
  40. package/dist/devtools/DevtoolsOverlay.js +383 -41
  41. package/dist/devtools/collisionDebug.d.ts +57 -0
  42. package/dist/devtools/collisionDebug.js +127 -0
  43. package/dist/devtools/collisionDebugMath.d.ts +102 -0
  44. package/dist/devtools/collisionDebugMath.js +128 -0
  45. package/dist/environment/Daylight.d.ts +13 -7
  46. package/dist/environment/Daylight.js +12 -11
  47. package/dist/environment/EnvironmentScene.js +82 -35
  48. package/dist/environment/groundPadMath.d.ts +2 -2
  49. package/dist/environment/groundPadMath.js +1 -1
  50. package/dist/environment/index.d.ts +2 -1
  51. package/dist/environment/index.js +1 -0
  52. package/dist/environment/skyLightingPolicy.d.ts +10 -0
  53. package/dist/environment/skyLightingPolicy.js +6 -0
  54. package/dist/input/mouseLook.d.ts +27 -0
  55. package/dist/input/mouseLook.js +35 -0
  56. package/dist/materialOverride.d.ts +4 -6
  57. package/dist/materialOverride.js +12 -16
  58. package/dist/pointer/PointerProbe.js +6 -2
  59. package/dist/pointer/pointerService.d.ts +3 -0
  60. package/dist/pointer/pointerService.js +6 -0
  61. package/dist/render/modelRender.d.ts +10 -1
  62. package/dist/render/modelRender.js +32 -5
  63. package/dist/render/resolveModel.d.ts +14 -0
  64. package/dist/render/resolveModel.js +24 -0
  65. package/dist/settings/QuickControls.d.ts +4 -0
  66. package/dist/settings/QuickControls.js +42 -0
  67. package/dist/settings/SettingsChrome.d.ts +1 -0
  68. package/dist/settings/SettingsChrome.js +10 -0
  69. package/dist/settings/SettingsMenu.d.ts +6 -0
  70. package/dist/settings/SettingsMenu.js +148 -0
  71. package/dist/settings/SettingsRuntime.d.ts +11 -0
  72. package/dist/settings/SettingsRuntime.js +19 -0
  73. package/dist/settings/appliedSettings.d.ts +14 -0
  74. package/dist/settings/appliedSettings.js +27 -0
  75. package/dist/settings/settingsController.d.ts +20 -0
  76. package/dist/settings/settingsController.js +165 -0
  77. package/dist/structures/GeneratedBuilding.d.ts +10 -0
  78. package/dist/structures/GeneratedBuilding.js +96 -8
  79. package/dist/structures/index.d.ts +1 -1
  80. package/dist/structures/index.js +1 -1
  81. package/dist/terrain/CarvedTerrain.d.ts +2 -1
  82. package/dist/terrain/CarvedTerrain.js +3 -3
  83. package/dist/terrain/GrassField.d.ts +3 -1
  84. package/dist/terrain/GrassField.js +4 -2
  85. package/dist/terrain/grassBudget.d.ts +3 -0
  86. package/dist/terrain/grassBudget.js +6 -0
  87. package/dist/terrain/grassGeometry.js +1 -1
  88. package/dist/terrain/terrainMath.d.ts +8 -0
  89. package/dist/terrain/terrainMath.js +9 -0
  90. package/dist/touch/OrientationHint.d.ts +3 -0
  91. package/dist/touch/OrientationHint.js +13 -0
  92. package/dist/touch/TouchControlsOverlay.d.ts +17 -1
  93. package/dist/touch/TouchControlsOverlay.js +115 -9
  94. package/dist/visibility/CullingProvider.d.ts +21 -0
  95. package/dist/visibility/CullingProvider.js +134 -0
  96. package/dist/vision/FrustumSensorHud.d.ts +1 -6
  97. package/dist/vision/FrustumSensorHud.js +25 -27
  98. package/dist/vision/frustumSampleEqual.d.ts +2 -0
  99. package/dist/vision/frustumSampleEqual.js +10 -0
  100. package/dist/water/Ocean.js +1 -1
  101. package/dist/water/OceanConfig.d.ts +13 -0
  102. package/dist/water/OceanConfig.js +25 -17
  103. package/dist/water/index.d.ts +1 -1
  104. package/dist/water/index.js +1 -1
  105. package/dist/weather/FireSpreadLayer.js +7 -2
  106. package/dist/weather/RainField.d.ts +3 -1
  107. package/dist/weather/RainField.js +4 -4
  108. package/dist/weather/SnowField.d.ts +3 -1
  109. package/dist/weather/SnowField.js +4 -4
  110. package/dist/weather/fireSpreadPose.d.ts +2 -0
  111. package/dist/weather/fireSpreadPose.js +4 -0
  112. package/dist/weather/weatherMath.d.ts +5 -1
  113. package/dist/weather/weatherMath.js +7 -2
  114. package/dist/world/DataObjects.d.ts +1 -1
  115. package/dist/world/DataObjects.js +3 -1
  116. package/dist/world/SpriteBatch.d.ts +44 -0
  117. package/dist/world/SpriteBatch.js +112 -0
  118. package/dist/world/WorldHud.d.ts +3 -0
  119. package/dist/world/WorldHud.js +89 -42
  120. package/dist/world/entityPose.d.ts +14 -0
  121. package/dist/world/entityPose.js +10 -0
  122. package/dist/world/telegraphPulse.d.ts +1 -0
  123. package/dist/world/telegraphPulse.js +4 -0
  124. package/dist/world/worldBarSamples.d.ts +30 -0
  125. package/dist/world/worldBarSamples.js +51 -0
  126. package/llms.txt +1404 -1143
  127. package/package.json +4 -4
@@ -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, resolveSideScroll, resolveSideScrollPose, resolveTopDown, seatPose, shoulderPose, sideScrollFollowBlend, smoothstep, smoothYaw, speedToFov, springArmStep, topDownPose, } from "./rigMath.js";
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,45 +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 blendRef = useRef(null);
56
+ const blendScratchRef = useRef(createCameraBlendScratch(Vector3, Quaternion));
57
+ const blendingRef = useRef(false);
53
58
  const lastPoseRef = useRef(null);
54
59
  const farWarnedRef = useRef(false);
55
60
  const beginTransition = () => {
56
61
  if (transitionSeconds <= 0)
57
62
  return;
58
- blendRef.current = {
59
- position: camera.position.clone(),
60
- quaternion: camera.quaternion.clone(),
61
- fov: currentFov(camera),
62
- elapsed: 0,
63
- duration: transitionSeconds,
64
- };
63
+ captureCameraBlendFrom(blendScratchRef.current, camera.position, camera.quaternion, currentFov(camera), transitionSeconds);
64
+ blendingRef.current = true;
65
65
  };
66
66
  const commit = (pose, dt) => {
67
- lastPoseRef.current = pose;
68
- applyPose(camera, pose);
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);
69
73
  const lookDistance = Math.hypot(pose.position.x - pose.lookAt.x, pose.position.y - pose.lookAt.y, pose.position.z - pose.lookAt.z);
70
74
  if (!farWarnedRef.current && isPerspective(camera) && lookDistance > camera.far) {
71
75
  farWarnedRef.current = true;
72
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.`);
73
77
  }
74
- const blend = blendRef.current;
75
- if (blend !== null) {
76
- blend.elapsed += dt;
77
- const t = blend.duration <= 0 ? 1 : Math.min(blend.elapsed / blend.duration, 1);
78
- const eased = smoothstep(t);
79
- const targetPos = camera.position.clone();
80
- const targetQuat = camera.quaternion.clone();
81
- camera.position.lerpVectors(blend.position, targetPos, eased);
82
- camera.quaternion.slerpQuaternions(blend.quaternion, targetQuat, eased);
83
- if (isPerspective(camera)) {
84
- camera.fov = blend.fov + (pose.fov - blend.fov) * eased;
85
- camera.updateProjectionMatrix();
86
- }
87
- if (t >= 1)
88
- blendRef.current = null;
78
+ if (blendingRef.current) {
79
+ const done = applyCameraBlendStep(blendScratchRef.current, camera, composed.fov, dt);
80
+ if (done)
81
+ blendingRef.current = false;
89
82
  }
90
83
  shake.step(dt);
91
84
  const offset = shake.sample(shakeConfig);
@@ -96,9 +89,9 @@ function useCameraCommit(props, followId) {
96
89
  }
97
90
  props.config?.onCameraFollow?.({
98
91
  entityId: followId ?? "",
99
- target: pose.lookAt,
92
+ target: composed.lookAt,
100
93
  camera: { x: camera.position.x, y: camera.position.y, z: camera.position.z },
101
- distance: Math.hypot(camera.position.x - pose.lookAt.x, camera.position.y - pose.lookAt.y, camera.position.z - pose.lookAt.z),
94
+ distance: Math.hypot(camera.position.x - composed.lookAt.x, camera.position.y - composed.lookAt.y, camera.position.z - composed.lookAt.z),
102
95
  });
103
96
  };
104
97
  return { camera, commit, beginTransition };
@@ -425,21 +418,28 @@ export function ChaseRig(props) {
425
418
  const ctx = useGameContext();
426
419
  const followId = resolveFollowId(props.followEntityId, userId);
427
420
  const config = props.config?.chase;
428
- const resolved = useMemo(() => resolveChase(config), [config]);
429
- const { commit, beginTransition } = useCameraCommit(props, followId);
421
+ const staticResolved = useMemo(() => resolveChase(config), [config]);
422
+ const { camera, commit, beginTransition } = useCameraCommit(props, followId);
430
423
  const shake = useCameraShake();
431
424
  const posRef = useRef(null);
432
425
  const lastFollowRef = useRef(null);
426
+ const lastYawRef = useRef(null);
427
+ const rollRef = useRef(0);
433
428
  const view = config?.view ?? "chase";
434
429
  useEffect(beginTransition, [view]);
435
430
  useFrame((_, dt) => {
431
+ const tuning = ctx.camera.chaseTuning();
432
+ const resolved = tuning === null ? staticResolved : resolveChase({ ...config, ...tuning });
436
433
  const sample = readFollow(ctx, followId);
437
434
  const follow = sample?.pos ?? { x: 0, y: 0, z: 0 };
438
435
  const yaw = sample?.yaw ?? 0;
439
436
  const last = lastFollowRef.current ?? follow;
440
437
  const speed = dt > 0 ? Math.hypot(follow.x - last.x, follow.z - last.z) / dt : 0;
441
438
  lastFollowRef.current = follow;
442
- const fov = speedToFov(speed, config?.fov);
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);
443
443
  if (view !== "chase") {
444
444
  const seat = view === "cockpit"
445
445
  ? config?.seatOffsets?.cockpit ?? { x: 0, y: 1.2, z: 0.1 }
@@ -454,15 +454,18 @@ export function ChaseRig(props) {
454
454
  commit(pose, dt);
455
455
  return;
456
456
  }
457
- const desired = chaseDesiredPosition(follow, yaw, resolved);
457
+ const led = leadFollowPoint(follow, last, dt, resolved);
458
+ const desired = chaseDesiredPosition(led, yaw, resolved);
458
459
  const prev = posRef.current ?? desired;
459
460
  const smoothed = springArmStep(prev, desired, resolved.springDamping, dt);
460
461
  posRef.current = smoothed;
461
462
  if (resolved.shakePerSpeed > 0 && speed > 0) {
462
463
  shake.shake(Math.min(resolved.shakePerSpeed * speed * dt, 0.1));
463
464
  }
464
- const pose = { position: smoothed, lookAt: chaseLookAt(follow, yaw, resolved), fov };
465
+ const pose = { position: smoothed, lookAt: chaseLookAt(led, yaw, resolved), fov };
465
466
  commit(pose, dt);
467
+ if (rollRef.current !== 0)
468
+ camera.rotateZ(rollRef.current);
466
469
  }, CAMERA_RIG_FRAME_PRIORITY);
467
470
  return null;
468
471
  }
@@ -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
+ }
@@ -2,7 +2,10 @@ export { GameOrbitCamera, type CameraFollowListener, type GameOrbitCameraProps }
2
2
  export { GameFirstPersonCamera, type GameFirstPersonCameraProps } from "./GameFirstPersonCamera.js";
3
3
  export { GameInspectionCamera, type GameInspectionCameraProps } from "./GameInspectionCamera.js";
4
4
  export { GameCameraRig, resolveRigKind, type GameCameraRigProps } from "./GameCameraRig.js";
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";
5
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";
6
9
  export { CameraShakeContext, cameraShake, createCameraShakeChannel, defaultCameraShakeChannel, useCameraShake, type CameraShakeChannel, } from "./shakeChannel.js";
7
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";
8
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";
@@ -2,7 +2,10 @@ export { GameOrbitCamera } from "./GameOrbitCamera.js";
2
2
  export { GameFirstPersonCamera } from "./GameFirstPersonCamera.js";
3
3
  export { GameInspectionCamera } from "./GameInspectionCamera.js";
4
4
  export { GameCameraRig, resolveRigKind } from "./GameCameraRig.js";
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";
5
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";
6
9
  export { CameraShakeContext, cameraShake, createCameraShakeChannel, defaultCameraShakeChannel, useCameraShake, } from "./shakeChannel.js";
7
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";
8
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";
@@ -11,6 +11,7 @@ export declare function clamp(value: number, min: number, max: number): number;
11
11
  export declare function lerp(from: number, to: number, blend: number): number;
12
12
  export declare function smoothstep(t: number): number;
13
13
  export declare function forwardVector(yaw: number): Vec3;
14
+ /** Screen-right of a yaw: `forward × up` with up = +Y. */
14
15
  export declare function rightVector(yaw: number): Vec3;
15
16
  export interface ResolvedTopDown {
16
17
  height: number;
@@ -87,8 +88,17 @@ export interface ResolvedChase {
87
88
  lookHeight: number;
88
89
  springDamping: number;
89
90
  shakePerSpeed: number;
91
+ leadTime: number;
92
+ leadMax: number;
93
+ bankPerYawRate: number;
94
+ bankMax: number;
95
+ bankDamping: number;
90
96
  }
91
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;
92
102
  export interface ResolvedObserver {
93
103
  distance: number;
94
104
  height: number;
@@ -24,8 +24,9 @@ export function smoothstep(t) {
24
24
  export function forwardVector(yaw) {
25
25
  return { x: Math.sin(yaw), y: 0, z: Math.cos(yaw) };
26
26
  }
27
+ /** Screen-right of a yaw: `forward × up` with up = +Y. */
27
28
  export function rightVector(yaw) {
28
- return { x: Math.cos(yaw), y: 0, z: -Math.sin(yaw) };
29
+ return { x: -Math.cos(yaw), y: 0, z: Math.sin(yaw) };
29
30
  }
30
31
  export function resolveTopDown(config) {
31
32
  return {
@@ -207,8 +208,42 @@ export function resolveChase(config) {
207
208
  lookHeight: config?.lookHeight ?? 1.2,
208
209
  springDamping: config?.springDamping ?? 6,
209
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,
210
216
  };
211
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
+ }
212
247
  export function resolveObserver(config) {
213
248
  return {
214
249
  distance: config?.distance ?? 8,
@@ -1,20 +1,6 @@
1
- import { shakeOffset, type ShakeOffset } from "./rigMath.js";
2
- export interface CameraShakeChannel {
3
- /**
4
- * Add a trauma impulse. `amplitude` is a 0..1 fraction (1 = a full boss slam);
5
- * `decayPerSecond` optionally overrides how fast this and subsequent trauma
6
- * bleed off. Any gameplay system (combat hitstop, explosions, vehicle crashes)
7
- * can call this — the active rig reads the resulting shake every frame.
8
- */
9
- shake(amplitude: number, decayPerSecond?: number): void;
10
- /** Advance decay + the shake clock (called once per frame by the mounted rig). */
11
- step(dt: number): void;
12
- /** Current positional + roll shake for the given tuning. */
13
- sample(config?: Parameters<typeof shakeOffset>[1]): ShakeOffset;
14
- /** Live trauma in [0,1] (for HUD/debug). */
15
- trauma(): number;
16
- }
17
- export declare function createCameraShakeChannel(defaultDecayPerSecond?: number): CameraShakeChannel;
1
+ import { type CameraShakeChannel } from "./shakeChannelMath.js";
2
+ export type { CameraShakeChannel } from "./shakeChannelMath.js";
3
+ export { createCameraShakeChannel } from "./shakeChannelMath.js";
18
4
  /**
19
5
  * Process-wide default channel. A shell mounts its own channel via
20
6
  * `CameraShakeContext`, but game systems that have no React context (e.g. a
@@ -1,25 +1,6 @@
1
1
  import { createContext, useContext } from "react";
2
- import { addTrauma, createTrauma, shakeOffset, stepTrauma } from "./rigMath.js";
3
- export function createCameraShakeChannel(defaultDecayPerSecond = 1.6) {
4
- const state = createTrauma();
5
- let decay = defaultDecayPerSecond;
6
- return {
7
- shake(amplitude, decayPerSecond) {
8
- if (decayPerSecond !== undefined)
9
- decay = decayPerSecond;
10
- addTrauma(state, amplitude);
11
- },
12
- step(dt) {
13
- stepTrauma(state, decay, dt);
14
- },
15
- sample(config) {
16
- return shakeOffset(state, config);
17
- },
18
- trauma() {
19
- return state.trauma;
20
- },
21
- };
22
- }
2
+ import { createCameraShakeChannel, } from "./shakeChannelMath.js";
3
+ export { createCameraShakeChannel } from "./shakeChannelMath.js";
23
4
  /**
24
5
  * Process-wide default channel. A shell mounts its own channel via
25
6
  * `CameraShakeContext`, but game systems that have no React context (e.g. a
@@ -0,0 +1,8 @@
1
+ import { shakeOffset, type ShakeOffset } from "./rigMath.js";
2
+ export interface CameraShakeChannel {
3
+ shake(amplitude: number, decayPerSecond?: number): void;
4
+ step(dt: number): void;
5
+ sample(config?: Parameters<typeof shakeOffset>[1]): ShakeOffset;
6
+ trauma(): number;
7
+ }
8
+ export declare function createCameraShakeChannel(defaultDecayPerSecond?: number): CameraShakeChannel;
@@ -0,0 +1,21 @@
1
+ import { addTrauma, createTrauma, shakeOffset, stepTrauma } from "./rigMath.js";
2
+ export function createCameraShakeChannel(defaultDecayPerSecond = 1.6) {
3
+ const state = createTrauma();
4
+ let decay = defaultDecayPerSecond;
5
+ return {
6
+ shake(amplitude, decayPerSecond) {
7
+ if (decayPerSecond !== undefined)
8
+ decay = decayPerSecond;
9
+ addTrauma(state, amplitude);
10
+ },
11
+ step(dt) {
12
+ stepTrauma(state, decay, dt);
13
+ },
14
+ sample(config) {
15
+ return shakeOffset(state, config);
16
+ },
17
+ trauma() {
18
+ return state.trauma;
19
+ },
20
+ };
21
+ }
@@ -0,0 +1,145 @@
1
+ import { type ComponentType } from "react";
2
+ import { type CartridgeSpec } from "@jgengine/core/cartridge/spec";
3
+ import type { AbilitySlotState } from "@jgengine/core/combat/abilityKit";
4
+ import type { HudAnchor } from "@jgengine/core/ui/hudLayout";
5
+ import { type GameConfig } from "./defineGame.js";
6
+ import type { PlayableGame } from "./registry.js";
7
+ export type CartridgePanelItem = {
8
+ kind: "vital";
9
+ stat: string;
10
+ label: string;
11
+ tone?: string;
12
+ width?: number;
13
+ } | {
14
+ kind: "xp";
15
+ width?: number;
16
+ } | {
17
+ kind: "timer";
18
+ label: string;
19
+ } | {
20
+ kind: "score";
21
+ source: "kills";
22
+ label: string;
23
+ digits?: number;
24
+ } | {
25
+ kind: "abilityBar";
26
+ icons: Record<string, string>;
27
+ } | {
28
+ kind: "component";
29
+ Component: ComponentType;
30
+ };
31
+ export interface CartridgeHudPanelSpec {
32
+ id: string;
33
+ anchor: HudAnchor;
34
+ inset?: {
35
+ x: number;
36
+ y: number;
37
+ };
38
+ items: readonly CartridgePanelItem[];
39
+ }
40
+ export type CartridgeResultLine = {
41
+ label: string;
42
+ accent?: boolean;
43
+ } & ({
44
+ source: "kills" | "level";
45
+ } | {
46
+ value: string | number;
47
+ });
48
+ export interface CartridgeScreens {
49
+ start?: {
50
+ title: string;
51
+ subtitle?: string;
52
+ buttonLabel?: string;
53
+ };
54
+ win?: {
55
+ title: string;
56
+ lines: readonly CartridgeResultLine[];
57
+ };
58
+ lose?: {
59
+ title: string;
60
+ subtitle?: string;
61
+ };
62
+ }
63
+ export interface CartridgeAbilitySlot {
64
+ id: string;
65
+ icon: string;
66
+ state: AbilitySlotState;
67
+ cooldownFraction: number;
68
+ justCast: boolean;
69
+ }
70
+ export interface CartridgePanels {
71
+ Vital: ComponentType<{
72
+ value: {
73
+ current: number;
74
+ max: number;
75
+ min?: number;
76
+ };
77
+ label: string;
78
+ tone?: string;
79
+ width?: number;
80
+ }>;
81
+ Xp: ComponentType<{
82
+ fraction: number;
83
+ level?: number;
84
+ width?: number;
85
+ }>;
86
+ Timer: ComponentType<{
87
+ seconds: number;
88
+ label: string;
89
+ }>;
90
+ Score: ComponentType<{
91
+ value: number;
92
+ label: string;
93
+ digits?: number;
94
+ }>;
95
+ AbilityBar: ComponentType<{
96
+ slots: readonly CartridgeAbilitySlot[];
97
+ }>;
98
+ DraftModal: ComponentType<{
99
+ offers: readonly {
100
+ id: string;
101
+ label: string;
102
+ }[];
103
+ choose(id: string): void;
104
+ }>;
105
+ StartScreen: ComponentType<{
106
+ title: string;
107
+ subtitle?: string;
108
+ buttonLabel?: string;
109
+ begin(): void;
110
+ }>;
111
+ Countdown: ComponentType<{
112
+ seconds: number;
113
+ }>;
114
+ WinScreen: ComponentType<{
115
+ title: string;
116
+ lines: readonly {
117
+ label: string;
118
+ value: string | number;
119
+ accent?: boolean;
120
+ }[];
121
+ }>;
122
+ LoseScreen: ComponentType<{
123
+ title: string;
124
+ subtitle?: string;
125
+ }>;
126
+ }
127
+ export type CartridgeConfig = CartridgeSpec & {
128
+ name: string;
129
+ panels: CartridgePanels;
130
+ hud: {
131
+ storageKey?: string;
132
+ panels: readonly CartridgeHudPanelSpec[];
133
+ };
134
+ screens: CartridgeScreens;
135
+ theme?: Record<string, string>;
136
+ world?: GameConfig["world"];
137
+ physics?: GameConfig["physics"];
138
+ assets?: GameConfig["assets"];
139
+ save?: GameConfig["save"];
140
+ camera?: GameConfig["camera"];
141
+ entitySprites?: GameConfig["entitySprites"];
142
+ worldItem?: GameConfig["worldItem"];
143
+ settings?: GameConfig["settings"];
144
+ };
145
+ export declare function cartridge(config: CartridgeConfig): PlayableGame;