@jgengine/shell 0.8.0 → 0.10.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 +36 -0
- package/README.md +11 -0
- package/dist/GamePlayerShell.d.ts +10 -22
- package/dist/GamePlayerShell.js +709 -264
- package/dist/GameUiPreview.js +2 -1
- package/dist/audio/AudioComponents.js +25 -8
- package/dist/audio/audioEngine.d.ts +9 -0
- package/dist/audio/audioEngine.js +37 -0
- package/dist/audio/musicDirector.d.ts +31 -0
- package/dist/audio/musicDirector.js +125 -0
- package/dist/audio/musicVoices.d.ts +10 -0
- package/dist/audio/musicVoices.js +338 -0
- package/dist/audio/synthEngine.d.ts +9 -0
- package/dist/audio/synthEngine.js +57 -0
- package/dist/behaviour.d.ts +5 -24
- package/dist/behaviour.js +14 -50
- 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.js +1 -1
- package/dist/camera/GameFirstPersonCamera.d.ts +3 -0
- package/dist/camera/GameFirstPersonCamera.js +35 -4
- package/dist/camera/GameInspectionCamera.js +12 -1
- package/dist/camera/GameOrbitCamera.js +44 -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 +3 -0
- package/dist/camera/cameraRigs.js +36 -33
- package/dist/camera/fovPreference.d.ts +25 -0
- package/dist/camera/fovPreference.js +73 -0
- package/dist/camera/index.d.ts +3 -0
- package/dist/camera/index.js +3 -0
- package/dist/camera/orbitCameraMath.d.ts +18 -0
- package/dist/camera/orbitCameraMath.js +6 -1
- package/dist/camera/rigMath.d.ts +10 -0
- package/dist/camera/rigMath.js +36 -1
- 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 +246 -0
- package/dist/commandSink.d.ts +20 -0
- package/dist/commandSink.js +27 -0
- package/dist/defineGame.js +4 -1
- package/dist/devtools/CollisionDebugWorld.d.ts +5 -0
- package/dist/devtools/CollisionDebugWorld.js +181 -0
- package/dist/devtools/DevtoolsOverlay.d.ts +65 -1
- package/dist/devtools/DevtoolsOverlay.js +383 -41
- package/dist/devtools/collisionDebug.d.ts +57 -0
- package/dist/devtools/collisionDebug.js +127 -0
- package/dist/devtools/collisionDebugMath.d.ts +103 -0
- package/dist/devtools/collisionDebugMath.js +129 -0
- package/dist/environment/Daylight.d.ts +20 -8
- package/dist/environment/Daylight.js +63 -15
- package/dist/environment/EnvironmentScene.js +91 -35
- package/dist/environment/RoadRibbons.d.ts +7 -0
- package/dist/environment/RoadRibbons.js +57 -0
- package/dist/environment/groundPadMath.d.ts +2 -2
- package/dist/environment/groundPadMath.js +1 -1
- package/dist/environment/index.d.ts +2 -1
- package/dist/environment/index.js +1 -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/inputSink.d.ts +18 -0
- package/dist/inputSink.js +35 -0
- package/dist/materialOverride.d.ts +4 -6
- package/dist/materialOverride.js +12 -16
- package/dist/multiplayer.js +7 -3
- package/dist/pointer/PointerProbe.js +6 -2
- package/dist/pointer/pointerService.d.ts +3 -0
- package/dist/pointer/pointerService.js +6 -0
- package/dist/postfx/PostProcessing.d.ts +10 -0
- package/dist/postfx/PostProcessing.js +82 -0
- package/dist/postfx/gradeShader.d.ts +4 -0
- package/dist/postfx/gradeShader.js +64 -0
- package/dist/render/modelRender.d.ts +10 -1
- package/dist/render/modelRender.js +32 -5
- 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 +5 -1
- package/dist/terrain/CarvedTerrain.js +6 -5
- 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/terrainDetailMaterial.d.ts +14 -0
- package/dist/terrain/terrainDetailMaterial.js +90 -0
- package/dist/terrain/terrainMath.d.ts +8 -0
- package/dist/terrain/terrainMath.js +9 -0
- package/dist/touch/OrientationHint.d.ts +3 -0
- package/dist/touch/OrientationHint.js +13 -0
- package/dist/touch/TouchControlsOverlay.d.ts +17 -1
- package/dist/touch/TouchControlsOverlay.js +125 -9
- 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/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 +1 -1
- package/dist/world/DataObjects.js +3 -1
- package/dist/world/SpriteBatch.d.ts +44 -0
- package/dist/world/SpriteBatch.js +112 -0
- package/dist/world/WorldHud.d.ts +6 -1
- package/dist/world/WorldHud.js +95 -48
- 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 +57 -0
- package/dist/worldSync.d.ts +10 -0
- package/dist/worldSync.js +19 -0
- package/llms.txt +1522 -1143
- package/package.json +4 -4
|
@@ -2,21 +2,36 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useFrame, useThree } from "@react-three/fiber";
|
|
3
3
|
import { useEffect, useRef } from "react";
|
|
4
4
|
import * as THREE from "three";
|
|
5
|
+
import { DEFAULT_EYE_HEIGHT } from "@jgengine/core/combat/shotOrigin";
|
|
5
6
|
import { useGameContext } from "@jgengine/react/provider";
|
|
6
7
|
import { usePlayer } from "@jgengine/react/hooks";
|
|
8
|
+
import { usePlayerFov } from "./PlayerFov.js";
|
|
7
9
|
import { GAME_SIM_FRAME_PRIORITY, ORBIT_CAMERA_FRAME_PRIORITY } from "./orbitCameraMath.js";
|
|
8
|
-
const DEFAULT_EYE_HEIGHT = 1.6;
|
|
9
10
|
const DEFAULT_SENSITIVITY = 0.0025;
|
|
10
11
|
const DEFAULT_MAX_PITCH = 1.45;
|
|
12
|
+
const VIEWMODEL_ORIGIN = new THREE.Vector3(0.34, -0.26, -0.72);
|
|
13
|
+
const MUZZLE_TIP_LOCAL = new THREE.Vector3(0, 0.03, -0.61);
|
|
14
|
+
const MUZZLE_OFFSET = VIEWMODEL_ORIGIN.clone().add(MUZZLE_TIP_LOCAL);
|
|
15
|
+
const muzzleWorld = new THREE.Vector3();
|
|
16
|
+
let muzzleTracked = false;
|
|
17
|
+
/** World position of the first-person weapon muzzle, or false when no viewmodel is mounted. */
|
|
18
|
+
export function readFirstPersonMuzzle(target) {
|
|
19
|
+
if (!muzzleTracked)
|
|
20
|
+
return false;
|
|
21
|
+
target.copy(muzzleWorld);
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
11
24
|
export function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId, }) {
|
|
12
25
|
const eyeHeight = config?.eyeHeight ?? DEFAULT_EYE_HEIGHT;
|
|
13
26
|
const sensitivity = config?.sensitivity ?? DEFAULT_SENSITIVITY;
|
|
14
27
|
const maxPitch = config?.maxPitch ?? DEFAULT_MAX_PITCH;
|
|
15
28
|
const { userId } = usePlayer();
|
|
16
29
|
const ctx = useGameContext();
|
|
30
|
+
const playerFov = usePlayerFov();
|
|
17
31
|
const camera = useThree((state) => state.camera);
|
|
18
32
|
const domElement = useThree((state) => state.gl.domElement);
|
|
19
33
|
const followId = followEntityId ?? userId;
|
|
34
|
+
const seededRef = useRef(false);
|
|
20
35
|
useEffect(() => {
|
|
21
36
|
const requestLock = () => {
|
|
22
37
|
if (window.matchMedia?.("(pointer: coarse)").matches)
|
|
@@ -41,9 +56,20 @@ export function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId
|
|
|
41
56
|
const entity = ctx.scene.entity.get(followId);
|
|
42
57
|
if (entity === null)
|
|
43
58
|
return;
|
|
59
|
+
if (!seededRef.current) {
|
|
60
|
+
seededRef.current = true;
|
|
61
|
+
yawRef.current = entity.rotationY;
|
|
62
|
+
}
|
|
44
63
|
const cosPitch = Math.cos(pitchRef.current);
|
|
45
64
|
camera.position.set(entity.position[0], entity.position[1] + eyeHeight, entity.position[2]);
|
|
46
65
|
camera.lookAt(camera.position.x + Math.sin(yawRef.current) * cosPitch, camera.position.y + Math.sin(pitchRef.current), camera.position.z + Math.cos(yawRef.current) * cosPitch);
|
|
66
|
+
if (camera.isPerspectiveCamera === true) {
|
|
67
|
+
const perspective = camera;
|
|
68
|
+
if (Math.abs(perspective.fov - playerFov.fov) > 0.001) {
|
|
69
|
+
perspective.fov = playerFov.fov;
|
|
70
|
+
perspective.updateProjectionMatrix();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
47
73
|
}, ORBIT_CAMERA_FRAME_PRIORITY);
|
|
48
74
|
if (config?.viewmodel === false)
|
|
49
75
|
return null;
|
|
@@ -51,15 +77,20 @@ export function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId
|
|
|
51
77
|
}
|
|
52
78
|
function FirstPersonViewmodel({ camera }) {
|
|
53
79
|
const groupRef = useRef(null);
|
|
80
|
+
useEffect(() => () => {
|
|
81
|
+
muzzleTracked = false;
|
|
82
|
+
}, []);
|
|
54
83
|
useFrame(() => {
|
|
55
84
|
const group = groupRef.current;
|
|
56
85
|
if (group === null)
|
|
57
86
|
return;
|
|
58
87
|
group.position.copy(camera.position);
|
|
59
88
|
group.quaternion.copy(camera.quaternion);
|
|
60
|
-
group.translateX(
|
|
61
|
-
group.translateY(
|
|
62
|
-
group.translateZ(
|
|
89
|
+
group.translateX(VIEWMODEL_ORIGIN.x);
|
|
90
|
+
group.translateY(VIEWMODEL_ORIGIN.y);
|
|
91
|
+
group.translateZ(VIEWMODEL_ORIGIN.z);
|
|
92
|
+
muzzleWorld.copy(MUZZLE_OFFSET).applyQuaternion(camera.quaternion).add(camera.position);
|
|
93
|
+
muzzleTracked = true;
|
|
63
94
|
}, GAME_SIM_FRAME_PRIORITY);
|
|
64
95
|
return (_jsxs("group", { ref: groupRef, children: [_jsxs("mesh", { position: [0, 0, -0.22], children: [_jsx("boxGeometry", { args: [0.09, 0.11, 0.55] }), _jsx("meshStandardMaterial", { color: "#22262d", metalness: 0.6, roughness: 0.35 })] }), _jsxs("mesh", { position: [0, -0.13, 0.04], rotation: [0.35, 0, 0], children: [_jsx("boxGeometry", { args: [0.08, 0.18, 0.11] }), _jsx("meshStandardMaterial", { color: "#33373f", metalness: 0.4, roughness: 0.5 })] }), _jsxs("mesh", { position: [0, 0.03, -0.52], rotation: [Math.PI / 2, 0, 0], children: [_jsx("cylinderGeometry", { args: [0.022, 0.022, 0.18, 10] }), _jsx("meshStandardMaterial", { color: "#0e0f12", metalness: 0.7, roughness: 0.3 })] })] }));
|
|
65
96
|
}
|
|
@@ -20,5 +20,16 @@ export function GameInspectionCamera({ config: configPatch }) {
|
|
|
20
20
|
controlsRef.current?.update();
|
|
21
21
|
seededRef.current = true;
|
|
22
22
|
}, [camera, config]);
|
|
23
|
-
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (!seededRef.current)
|
|
25
|
+
return;
|
|
26
|
+
const controls = controlsRef.current;
|
|
27
|
+
if (controls === null)
|
|
28
|
+
return;
|
|
29
|
+
const previous = controls.target.clone();
|
|
30
|
+
controls.target.set(config.target.x, config.target.y, config.target.z);
|
|
31
|
+
camera.position.add(controls.target.clone().sub(previous));
|
|
32
|
+
controls.update();
|
|
33
|
+
}, [camera, config.target.x, config.target.y, config.target.z]);
|
|
34
|
+
return (_jsx(OrbitControls, { ref: controlsRef, makeDefault: true, enableDamping: true, dampingFactor: config.dampingFactor, rotateSpeed: config.rotateSpeed, zoomSpeed: config.zoomSpeed, zoomToCursor: resolveInspectionZoomToCursor(config.anchor), enablePan: config.pan, enableRotate: true, enableZoom: true, minDistance: config.minDistance, maxDistance: config.maxDistance, minPolarAngle: config.minPolarAngle, maxPolarAngle: config.maxPolarAngle, screenSpacePanning: true, mouseButtons: { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.PAN, RIGHT: MOUSE.PAN } }));
|
|
24
35
|
}
|
|
@@ -2,10 +2,11 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { OrbitControls } from "@react-three/drei";
|
|
3
3
|
import { useFrame, useThree } from "@react-three/fiber";
|
|
4
4
|
import { useEffect, useRef } from "react";
|
|
5
|
-
import { MOUSE, Vector3 } from "three";
|
|
5
|
+
import { MOUSE, PerspectiveCamera, Raycaster, Vector3 } from "three";
|
|
6
6
|
import { useGameContext } from "@jgengine/react/provider";
|
|
7
7
|
import { usePlayer } from "@jgengine/react/hooks";
|
|
8
8
|
import { cameraLookPitch, ORBIT_CAMERA_FRAME_PRIORITY, orbitFollowStep, orbitYawFromCamera, resolveFollowTargetFromPosition, resolveOrbitCameraConfig, seedOrbitFollowState, } from "./orbitCameraMath.js";
|
|
9
|
+
import { usePlayerFov } from "./PlayerFov.js";
|
|
9
10
|
export function GameOrbitCamera({ yawRef, pitchRef, config: configPatch, followEntityId, resolveFollowTarget, onDragChange, onCameraFollow, pointerControls = false, }) {
|
|
10
11
|
const config = resolveOrbitCameraConfig(configPatch);
|
|
11
12
|
const controlsRef = useRef(null);
|
|
@@ -13,7 +14,12 @@ export function GameOrbitCamera({ yawRef, pitchRef, config: configPatch, followE
|
|
|
13
14
|
const draggingRef = useRef(false);
|
|
14
15
|
const { userId } = usePlayer();
|
|
15
16
|
const ctx = useGameContext();
|
|
17
|
+
const playerFov = usePlayerFov();
|
|
16
18
|
const camera = useThree((state) => state.camera);
|
|
19
|
+
const scene = useThree((state) => state.scene);
|
|
20
|
+
const raycasterRef = useRef(new Raycaster());
|
|
21
|
+
const collisionDirRef = useRef(new Vector3());
|
|
22
|
+
const collisionOriginRef = useRef(new Vector3());
|
|
17
23
|
const followId = followEntityId ?? userId;
|
|
18
24
|
useEffect(() => {
|
|
19
25
|
const entity = ctx.scene.entity.get(followId);
|
|
@@ -54,6 +60,36 @@ export function GameOrbitCamera({ yawRef, pitchRef, config: configPatch, followE
|
|
|
54
60
|
camera.position.set(stepped.camera.x, stepped.camera.y, stepped.camera.z);
|
|
55
61
|
}
|
|
56
62
|
controls.update();
|
|
63
|
+
if (config.collision.enabled) {
|
|
64
|
+
const t = stepped.target;
|
|
65
|
+
const dx = camera.position.x - t.x;
|
|
66
|
+
const dy = camera.position.y - t.y;
|
|
67
|
+
const dz = camera.position.z - t.z;
|
|
68
|
+
const dist = Math.hypot(dx, dy, dz);
|
|
69
|
+
if (dist > config.collision.minTargetDistance) {
|
|
70
|
+
const dir = collisionDirRef.current.set(dx / dist, dy / dist, dz / dist);
|
|
71
|
+
const ray = raycasterRef.current;
|
|
72
|
+
ray.set(collisionOriginRef.current.set(t.x, t.y, t.z), dir);
|
|
73
|
+
ray.near = config.collision.minTargetDistance;
|
|
74
|
+
ray.far = dist;
|
|
75
|
+
let blocked = 0;
|
|
76
|
+
for (const hit of ray.intersectObjects(scene.children, true)) {
|
|
77
|
+
const obj = hit.object;
|
|
78
|
+
if (!obj.visible)
|
|
79
|
+
continue;
|
|
80
|
+
if (obj.isSprite === true)
|
|
81
|
+
continue;
|
|
82
|
+
if (obj.userData.jgCameraTransparent === true)
|
|
83
|
+
continue;
|
|
84
|
+
blocked = hit.distance;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
if (blocked > 0) {
|
|
88
|
+
const pulled = Math.max(config.collision.minTargetDistance, blocked - config.collision.padding);
|
|
89
|
+
camera.position.set(t.x + dir.x * pulled, t.y + dir.y * pulled, t.z + dir.z * pulled);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
57
93
|
runtimeRef.current = {
|
|
58
94
|
target: stepped.target,
|
|
59
95
|
camera: { x: camera.position.x, y: camera.position.y, z: camera.position.z },
|
|
@@ -64,6 +100,13 @@ export function GameOrbitCamera({ yawRef, pitchRef, config: configPatch, followE
|
|
|
64
100
|
if (pitchRef !== undefined) {
|
|
65
101
|
pitchRef.current = cameraLookPitch({ x: camera.position.x, y: camera.position.y, z: camera.position.z }, stepped.target);
|
|
66
102
|
}
|
|
103
|
+
if (camera.isPerspectiveCamera === true) {
|
|
104
|
+
const perspective = camera;
|
|
105
|
+
if (Math.abs(perspective.fov - playerFov.fov) > 0.001) {
|
|
106
|
+
perspective.fov = playerFov.fov;
|
|
107
|
+
perspective.updateProjectionMatrix();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
67
110
|
onCameraFollow?.({
|
|
68
111
|
entityId: followId,
|
|
69
112
|
target: stepped.target,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { GameCameraConfig } from "@jgengine/core/game/playableGame";
|
|
3
|
+
import { type PlayerFovBounds } from "./fovPreference.js";
|
|
4
|
+
export interface PlayerFovState {
|
|
5
|
+
fov: number;
|
|
6
|
+
bounds: PlayerFovBounds;
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
persist: boolean;
|
|
9
|
+
setFov: (value: number) => void;
|
|
10
|
+
compose: (poseFov: number, mode?: "relative" | "absolute") => number;
|
|
11
|
+
}
|
|
12
|
+
export declare function PlayerFovProvider({ config, orthographic, children, }: {
|
|
13
|
+
config?: GameCameraConfig;
|
|
14
|
+
orthographic: boolean;
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
}): import("react").JSX.Element;
|
|
17
|
+
export declare function usePlayerFov(): PlayerFovState;
|
|
18
|
+
export declare function PlayerFovSlider(): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useCallback, useContext, useMemo, useState, } from "react";
|
|
3
|
+
import { composePlayerFov, loadPlayerFov, resolvePlayerFovBounds, savePlayerFov, } from "./fovPreference.js";
|
|
4
|
+
const PlayerFovContext = createContext(null);
|
|
5
|
+
export function PlayerFovProvider({ config, orthographic, children, }) {
|
|
6
|
+
const bounds = useMemo(() => resolvePlayerFovBounds({
|
|
7
|
+
min: config?.playerFov?.min,
|
|
8
|
+
max: config?.playerFov?.max,
|
|
9
|
+
default: config?.playerFov?.default ?? config?.frustum?.fov,
|
|
10
|
+
}), [config?.playerFov?.min, config?.playerFov?.max, config?.playerFov?.default, config?.frustum?.fov]);
|
|
11
|
+
const persist = config?.playerFov?.persist !== false;
|
|
12
|
+
const showControl = !orthographic && config?.playerFov?.control !== false;
|
|
13
|
+
const [fov, setFovState] = useState(() => persist ? loadPlayerFov(bounds) : bounds.defaultFov);
|
|
14
|
+
const setFov = useCallback((value) => {
|
|
15
|
+
setFovState(persist ? savePlayerFov(value, bounds) : savePlayerFov(value, bounds, null));
|
|
16
|
+
}, [bounds, persist]);
|
|
17
|
+
const compose = useCallback((poseFov, mode = "relative") => orthographic ? poseFov : composePlayerFov(fov, poseFov, mode, bounds), [bounds, fov, orthographic]);
|
|
18
|
+
const value = useMemo(() => ({
|
|
19
|
+
fov,
|
|
20
|
+
bounds,
|
|
21
|
+
enabled: showControl,
|
|
22
|
+
persist,
|
|
23
|
+
setFov,
|
|
24
|
+
compose,
|
|
25
|
+
}), [bounds, compose, fov, persist, setFov, showControl]);
|
|
26
|
+
return _jsx(PlayerFovContext.Provider, { value: value, children: children });
|
|
27
|
+
}
|
|
28
|
+
export function usePlayerFov() {
|
|
29
|
+
const value = useContext(PlayerFovContext);
|
|
30
|
+
if (value === null) {
|
|
31
|
+
const bounds = resolvePlayerFovBounds();
|
|
32
|
+
return {
|
|
33
|
+
fov: bounds.defaultFov,
|
|
34
|
+
bounds,
|
|
35
|
+
enabled: false,
|
|
36
|
+
persist: false,
|
|
37
|
+
setFov: () => undefined,
|
|
38
|
+
compose: (poseFov) => poseFov,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
export function PlayerFovSlider() {
|
|
44
|
+
const { fov, setFov, bounds, enabled } = usePlayerFov();
|
|
45
|
+
if (!enabled)
|
|
46
|
+
return null;
|
|
47
|
+
return (_jsxs("label", { className: "pointer-events-auto absolute bottom-3 right-3 z-20 flex items-center gap-2 rounded-md bg-neutral-950/75 px-2.5 py-1.5 text-[11px] font-medium tracking-wide text-neutral-200 shadow-lg ring-1 ring-white/10 backdrop-blur-sm", children: [_jsx("span", { className: "text-neutral-400", children: "FOV" }), _jsx("input", { type: "range", min: bounds.min, max: bounds.max, step: 1, value: fov, "aria-label": "Field of view", className: "h-1 w-24 cursor-pointer accent-emerald-400", onChange: (event) => setFov(Number(event.target.value)) }), _jsx("span", { className: "w-7 tabular-nums text-neutral-100", children: Math.round(fov) })] }));
|
|
48
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Camera, Quaternion, Vector3 } from "three";
|
|
2
|
+
export interface CameraBlendScratch {
|
|
3
|
+
fromPos: Vector3;
|
|
4
|
+
fromQuat: Quaternion;
|
|
5
|
+
toPos: Vector3;
|
|
6
|
+
toQuat: Quaternion;
|
|
7
|
+
fov: number;
|
|
8
|
+
elapsed: number;
|
|
9
|
+
duration: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function createCameraBlendScratch(Vector3Ctor: new () => Vector3, QuaternionCtor: new () => Quaternion): CameraBlendScratch;
|
|
12
|
+
export declare function captureCameraBlendFrom(scratch: CameraBlendScratch, position: Vector3, quaternion: Quaternion, fov: number, duration: number): void;
|
|
13
|
+
export declare function applyCameraBlendStep(scratch: CameraBlendScratch, camera: Camera, targetFov: number, dt: number): boolean;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { smoothstep } from "./rigMath.js";
|
|
2
|
+
export function createCameraBlendScratch(Vector3Ctor, QuaternionCtor) {
|
|
3
|
+
return {
|
|
4
|
+
fromPos: new Vector3Ctor(),
|
|
5
|
+
fromQuat: new QuaternionCtor(),
|
|
6
|
+
toPos: new Vector3Ctor(),
|
|
7
|
+
toQuat: new QuaternionCtor(),
|
|
8
|
+
fov: 55,
|
|
9
|
+
elapsed: 0,
|
|
10
|
+
duration: 0,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function captureCameraBlendFrom(scratch, position, quaternion, fov, duration) {
|
|
14
|
+
scratch.fromPos.copy(position);
|
|
15
|
+
scratch.fromQuat.copy(quaternion);
|
|
16
|
+
scratch.fov = fov;
|
|
17
|
+
scratch.elapsed = 0;
|
|
18
|
+
scratch.duration = duration;
|
|
19
|
+
}
|
|
20
|
+
function isPerspective(camera) {
|
|
21
|
+
return camera.isPerspectiveCamera === true;
|
|
22
|
+
}
|
|
23
|
+
export function applyCameraBlendStep(scratch, camera, targetFov, dt) {
|
|
24
|
+
scratch.elapsed += dt;
|
|
25
|
+
const t = scratch.duration <= 0 ? 1 : Math.min(scratch.elapsed / scratch.duration, 1);
|
|
26
|
+
const eased = smoothstep(t);
|
|
27
|
+
scratch.toPos.copy(camera.position);
|
|
28
|
+
scratch.toQuat.copy(camera.quaternion);
|
|
29
|
+
camera.position.lerpVectors(scratch.fromPos, scratch.toPos, eased);
|
|
30
|
+
camera.quaternion.slerpQuaternions(scratch.fromQuat, scratch.toQuat, eased);
|
|
31
|
+
if (isPerspective(camera)) {
|
|
32
|
+
camera.fov = scratch.fov + (targetFov - scratch.fov) * eased;
|
|
33
|
+
camera.updateProjectionMatrix();
|
|
34
|
+
}
|
|
35
|
+
return t >= 1;
|
|
36
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type MutableRefObject } from "react";
|
|
2
2
|
import type { GameCameraConfig } from "@jgengine/core/game/playableGame";
|
|
3
|
+
export { applyCameraBlendStep, captureCameraBlendFrom, createCameraBlendScratch, type CameraBlendScratch, } from "./cameraBlendMath.js";
|
|
3
4
|
export declare const CAMERA_RIG_FRAME_PRIORITY = -1;
|
|
4
5
|
export declare const CAMERA_POST_FRAME_PRIORITY: number;
|
|
5
6
|
export interface RigProps {
|
|
@@ -7,6 +8,8 @@ export interface RigProps {
|
|
|
7
8
|
pitchRef: MutableRefObject<number>;
|
|
8
9
|
config?: GameCameraConfig;
|
|
9
10
|
followEntityId?: string | null;
|
|
11
|
+
/** When true, pose FOV is treated as absolute (cinematic keyframes). */
|
|
12
|
+
absoluteFov?: boolean;
|
|
10
13
|
}
|
|
11
14
|
export declare function TopDownRig(props: RigProps): null;
|
|
12
15
|
/** Fixed side-on 2.5D follow rig: watches the followed entity from the perpendicular axis, never reading WASD/mouse-look. */
|
|
@@ -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,
|
|
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
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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:
|
|
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 -
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
+
}
|
package/dist/camera/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/camera/index.js
CHANGED
|
@@ -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";
|
|
@@ -39,6 +39,23 @@ export interface OrbitCameraConfig {
|
|
|
39
39
|
/** Vertical clamp (three.js polar angle, radians): 0 = top-down over the head, PI/2 = level, >PI/2 = look up from below the target. Widen to allow top-down or vertical aim; leave unset for the standard chase feel. */
|
|
40
40
|
minPolarAngle?: number;
|
|
41
41
|
maxPolarAngle?: number;
|
|
42
|
+
/** Spring-arm collision: raycast target→camera each frame and pull the boom in past occluders (walls, terrain) so the camera never clips inside geometry. */
|
|
43
|
+
collision?: OrbitCollisionConfig;
|
|
44
|
+
}
|
|
45
|
+
/** Spring-arm occlusion config for the orbit rig. */
|
|
46
|
+
export interface OrbitCollisionConfig {
|
|
47
|
+
/** Enable the pull-in raycast. Default false (unchanged chase camera). */
|
|
48
|
+
enabled?: boolean;
|
|
49
|
+
/** Gap kept between the camera and the hit surface, world units. Default 0.35. */
|
|
50
|
+
padding?: number;
|
|
51
|
+
/** Ignore hits closer than this to the target — the player's own body. Default 1.2. */
|
|
52
|
+
minTargetDistance?: number;
|
|
53
|
+
}
|
|
54
|
+
/** An {@link OrbitCollisionConfig} with every field resolved to a concrete value. */
|
|
55
|
+
export interface ResolvedOrbitCollision {
|
|
56
|
+
enabled: boolean;
|
|
57
|
+
padding: number;
|
|
58
|
+
minTargetDistance: number;
|
|
42
59
|
}
|
|
43
60
|
/** Fully resolved shell config after merging with DEFAULT_ORBIT_CAMERA. */
|
|
44
61
|
export interface ResolvedOrbitCameraConfig {
|
|
@@ -58,6 +75,7 @@ export interface ResolvedOrbitCameraConfig {
|
|
|
58
75
|
distanceSmoothing: number;
|
|
59
76
|
minPolarAngle: number;
|
|
60
77
|
maxPolarAngle: number;
|
|
78
|
+
collision: ResolvedOrbitCollision;
|
|
61
79
|
}
|
|
62
80
|
export declare const DEFAULT_ORBIT_CAMERA: ResolvedOrbitCameraConfig;
|
|
63
81
|
/** Run simulation/movement before orbit follow so poses are current. */
|
|
@@ -25,6 +25,7 @@ export const DEFAULT_ORBIT_CAMERA = {
|
|
|
25
25
|
distanceSmoothing: 5,
|
|
26
26
|
minPolarAngle: 0.12,
|
|
27
27
|
maxPolarAngle: Math.PI / 2.05,
|
|
28
|
+
collision: { enabled: false, padding: 0.35, minTargetDistance: 1.2 },
|
|
28
29
|
};
|
|
29
30
|
/** Run simulation/movement before orbit follow so poses are current. */
|
|
30
31
|
export const GAME_SIM_FRAME_PRIORITY = 0;
|
|
@@ -35,7 +36,11 @@ export function smoothBlend(deltaSeconds, speed) {
|
|
|
35
36
|
return 1 - Math.exp(-speed * deltaSeconds);
|
|
36
37
|
}
|
|
37
38
|
export function resolveOrbitCameraConfig(patch) {
|
|
38
|
-
return {
|
|
39
|
+
return {
|
|
40
|
+
...DEFAULT_ORBIT_CAMERA,
|
|
41
|
+
...patch,
|
|
42
|
+
collision: { ...DEFAULT_ORBIT_CAMERA.collision, ...patch?.collision },
|
|
43
|
+
};
|
|
39
44
|
}
|
|
40
45
|
export function resolveTargetSmoothing(config, dragging) {
|
|
41
46
|
if (dragging) {
|