@jgengine/shell 0.6.0 → 0.8.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 +214 -2
- package/README.md +26 -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 +33 -1
- package/dist/GamePlayerShell.js +641 -59
- package/dist/audio/AudioComponents.d.ts +12 -0
- package/dist/audio/AudioComponents.js +53 -0
- package/dist/audio/audioEngine.d.ts +23 -0
- package/dist/audio/audioEngine.js +110 -0
- package/dist/behaviour.d.ts +31 -0
- package/dist/behaviour.js +63 -0
- package/dist/camera/GameCameraRig.d.ts +16 -0
- package/dist/camera/GameCameraRig.js +56 -0
- package/dist/camera/GameFirstPersonCamera.js +2 -0
- package/dist/camera/GameInspectionCamera.d.ts +6 -0
- package/dist/camera/GameInspectionCamera.js +24 -0
- package/dist/camera/GameOrbitCamera.d.ts +3 -1
- package/dist/camera/GameOrbitCamera.js +4 -2
- package/dist/camera/cameraRigs.d.ts +26 -0
- package/dist/camera/cameraRigs.js +522 -0
- package/dist/camera/index.d.ts +7 -0
- package/dist/camera/index.js +7 -0
- package/dist/camera/inspectionCameraMath.d.ts +25 -0
- package/dist/camera/inspectionCameraMath.js +44 -0
- package/dist/camera/rigMath.d.ts +169 -0
- package/dist/camera/rigMath.js +393 -0
- package/dist/camera/rigResolve.d.ts +15 -0
- package/dist/camera/rigResolve.js +52 -0
- package/dist/camera/shakeChannel.d.ts +29 -0
- package/dist/camera/shakeChannel.js +38 -0
- package/dist/defineGame.d.ts +16 -0
- package/dist/defineGame.js +51 -0
- package/dist/devtools/DevtoolsOverlay.d.ts +14 -0
- package/dist/devtools/DevtoolsOverlay.js +334 -0
- package/dist/environment/Daylight.d.ts +50 -0
- package/dist/environment/Daylight.js +100 -0
- package/dist/environment/EnvironmentScene.js +18 -26
- 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 +2 -0
- package/dist/environment/index.js +2 -0
- package/dist/map/MapMarkerBeacons.d.ts +12 -0
- package/dist/map/MapMarkerBeacons.js +16 -0
- package/dist/map/index.d.ts +2 -0
- package/dist/map/index.js +2 -0
- package/dist/map/terrainMap.d.ts +23 -0
- package/dist/map/terrainMap.js +79 -0
- package/dist/materialOverride.d.ts +8 -0
- package/dist/materialOverride.js +32 -0
- package/dist/multiplayer.d.ts +16 -9
- package/dist/multiplayer.js +62 -12
- package/dist/pointer/PointerOverlays.d.ts +12 -0
- package/dist/pointer/PointerOverlays.js +17 -0
- package/dist/pointer/PointerProbe.d.ts +4 -0
- package/dist/pointer/PointerProbe.js +40 -0
- package/dist/pointer/pointerService.d.ts +26 -0
- package/dist/pointer/pointerService.js +89 -0
- package/dist/registry.d.ts +4 -1
- package/dist/registry.js +3 -1
- package/dist/render/modelRender.d.ts +14 -0
- package/dist/render/modelRender.js +79 -0
- package/dist/replay/useSessionRecorder.d.ts +14 -0
- package/dist/replay/useSessionRecorder.js +22 -0
- package/dist/structures/PlacementGhost.d.ts +8 -0
- package/dist/structures/PlacementGhost.js +11 -0
- package/dist/structures/index.d.ts +2 -0
- package/dist/structures/index.js +2 -0
- package/dist/structures.d.ts +1 -0
- package/dist/structures.js +1 -0
- package/dist/terrain/CarvedTerrain.d.ts +22 -0
- package/dist/terrain/CarvedTerrain.js +19 -0
- package/dist/terrain/EditableGround.d.ts +11 -0
- package/dist/terrain/EditableGround.js +38 -0
- package/dist/terrain/TerraformBrushCursor.d.ts +8 -0
- package/dist/terrain/TerraformBrushCursor.js +13 -0
- package/dist/terrain/index.d.ts +4 -1
- package/dist/terrain/index.js +4 -1
- package/dist/terrain/terrainMath.d.ts +15 -1
- package/dist/terrain/terrainMath.js +87 -1
- package/dist/touch/TouchControlsOverlay.d.ts +18 -0
- package/dist/touch/TouchControlsOverlay.js +173 -0
- package/dist/vision/FrustumSensorHud.d.ts +21 -0
- package/dist/vision/FrustumSensorHud.js +67 -0
- package/dist/vision/HiddenStateProbeHud.d.ts +11 -0
- package/dist/vision/HiddenStateProbeHud.js +11 -0
- package/dist/vision/RevealVision.d.ts +28 -0
- package/dist/vision/RevealVision.js +53 -0
- package/dist/water/OceanShader.d.ts +1 -1
- package/dist/water/OceanShader.js +3 -3
- package/dist/weather/FireSpreadLayer.d.ts +14 -0
- package/dist/weather/FireSpreadLayer.js +68 -0
- package/dist/weather/index.d.ts +1 -0
- package/dist/weather/index.js +1 -0
- package/dist/world/DataObjects.d.ts +44 -0
- package/dist/world/DataObjects.js +75 -0
- package/dist/world/GridWorldScene.d.ts +10 -0
- package/dist/world/GridWorldScene.js +54 -0
- package/dist/world/InstancedJoints.d.ts +14 -0
- package/dist/world/InstancedJoints.js +32 -0
- package/dist/world/WorldHud.d.ts +7 -1
- package/dist/world/WorldHud.js +76 -14
- package/dist/world/WorldItems.d.ts +5 -0
- package/dist/world/WorldItems.js +50 -0
- package/dist/world/floatTextStyle.d.ts +14 -0
- package/dist/world/floatTextStyle.js +39 -0
- package/llms.txt +2382 -0
- package/package.json +10 -5
- 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
|
@@ -0,0 +1,169 @@
|
|
|
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";
|
|
3
|
+
import { type Vec3 } from "./orbitCameraMath.js";
|
|
4
|
+
export interface CameraPose {
|
|
5
|
+
position: Vec3;
|
|
6
|
+
lookAt: Vec3;
|
|
7
|
+
fov: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function rtsPanKeysConflict(input: ActionCodesMap | undefined): boolean;
|
|
10
|
+
export declare function clamp(value: number, min: number, max: number): number;
|
|
11
|
+
export declare function lerp(from: number, to: number, blend: number): number;
|
|
12
|
+
export declare function smoothstep(t: number): number;
|
|
13
|
+
export declare function forwardVector(yaw: number): Vec3;
|
|
14
|
+
export declare function rightVector(yaw: number): Vec3;
|
|
15
|
+
export interface ResolvedTopDown {
|
|
16
|
+
height: number;
|
|
17
|
+
pitch: number;
|
|
18
|
+
yaw: number;
|
|
19
|
+
offset: Vec3;
|
|
20
|
+
followSmoothing: number;
|
|
21
|
+
}
|
|
22
|
+
export declare function resolveTopDown(config: TopDownCameraConfig | undefined): ResolvedTopDown;
|
|
23
|
+
/**
|
|
24
|
+
* Camera pose for a fixed top-down / isometric rig. `pitch` is the elevation of
|
|
25
|
+
* the camera→target ray above the ground (PI/2 = straight down); `yaw` is the
|
|
26
|
+
* fixed azimuth (PI/4 reads isometric). Height sets zoom; horizontal boom is
|
|
27
|
+
* derived so the look angle stays constant as height changes.
|
|
28
|
+
*/
|
|
29
|
+
export declare function topDownPose(follow: Vec3, resolved: ResolvedTopDown, fov: number): CameraPose;
|
|
30
|
+
/** Exponential spring-arm approach toward a desired point (frame-rate independent). */
|
|
31
|
+
export declare function springArmStep(current: Vec3, desired: Vec3, damping: number, dt: number): Vec3;
|
|
32
|
+
export interface ResolvedSideScroll {
|
|
33
|
+
axis: "x" | "z";
|
|
34
|
+
distance: number;
|
|
35
|
+
height: number;
|
|
36
|
+
lookHeight: number;
|
|
37
|
+
followSmoothing: number;
|
|
38
|
+
}
|
|
39
|
+
export declare function resolveSideScroll(config: SideScrollCameraConfig | undefined): ResolvedSideScroll;
|
|
40
|
+
/** Frame-rate independent follow blend for the side-scroll rig; `followSmoothing <= 0` hard-locks (blend 1) instead of freezing. */
|
|
41
|
+
export declare function sideScrollFollowBlend(followSmoothing: number, dt: number): number;
|
|
42
|
+
/**
|
|
43
|
+
* Fixed lateral 2.5D follow pose: the camera sits perpendicular to the travel
|
|
44
|
+
* axis at `distance`, above the entity by `height`, and looks at the entity
|
|
45
|
+
* raised by `lookHeight`. Axis "x" watches from +z; axis "z" watches from +x.
|
|
46
|
+
*/
|
|
47
|
+
export declare function resolveSideScrollPose(entityPos: Vec3, resolved: ResolvedSideScroll, fov: number): CameraPose;
|
|
48
|
+
/** Speed→FOV curve: FOV climbs from base to max as speed rises to `speedForMax`. */
|
|
49
|
+
export declare function speedToFov(speed: number, curve: {
|
|
50
|
+
base?: number;
|
|
51
|
+
max?: number;
|
|
52
|
+
speedForMax?: number;
|
|
53
|
+
} | undefined): number;
|
|
54
|
+
/** Yaw that points from `from` toward `to` on the XZ plane (matches shell forward = (sin, cos)). */
|
|
55
|
+
export declare function yawTo(from: Vec3, to: Vec3): number;
|
|
56
|
+
/** Shortest signed angular delta from `a` to `b`, wrapped to (-PI, PI]. */
|
|
57
|
+
export declare function angleDelta(a: number, b: number): number;
|
|
58
|
+
/** Exponential yaw smoothing that respects wrap-around. */
|
|
59
|
+
export declare function smoothYaw(current: number, desired: number, speed: number, dt: number): number;
|
|
60
|
+
export interface ResolvedShoulder {
|
|
61
|
+
shoulderOffset: number;
|
|
62
|
+
heightOffset: number;
|
|
63
|
+
distance: number;
|
|
64
|
+
fov: number;
|
|
65
|
+
}
|
|
66
|
+
export declare function resolveShoulder(config: ShoulderCameraConfig | undefined, aiming: boolean): ResolvedShoulder;
|
|
67
|
+
/** Blend two shoulder framings by an ADS factor in [0,1]. */
|
|
68
|
+
export declare function blendShoulder(hip: ResolvedShoulder, ads: ResolvedShoulder, t: number): ResolvedShoulder;
|
|
69
|
+
/**
|
|
70
|
+
* Over-the-shoulder camera pose. The boom sits behind the follow point along
|
|
71
|
+
* `yaw`, raised by `heightOffset`, and pushed sideways by `shoulderOffset`
|
|
72
|
+
* (sign set by `sideSign`: +1 right, -1 left). `pitch` tilts the look point.
|
|
73
|
+
*/
|
|
74
|
+
export declare function shoulderPose(follow: Vec3, yaw: number, pitch: number, sideSign: number, shoulder: ResolvedShoulder): CameraPose;
|
|
75
|
+
/**
|
|
76
|
+
* Lock-on pose: camera sits behind the player along the player→target vector so
|
|
77
|
+
* the target stays framed. The look point is biased between player and target
|
|
78
|
+
* by `framingBias`.
|
|
79
|
+
*/
|
|
80
|
+
export declare function lockOnPose(player: Vec3, target: Vec3, config: LockOnCameraConfig | undefined, fov: number): {
|
|
81
|
+
pose: CameraPose;
|
|
82
|
+
yaw: number;
|
|
83
|
+
};
|
|
84
|
+
export interface ResolvedChase {
|
|
85
|
+
distance: number;
|
|
86
|
+
height: number;
|
|
87
|
+
lookHeight: number;
|
|
88
|
+
springDamping: number;
|
|
89
|
+
shakePerSpeed: number;
|
|
90
|
+
}
|
|
91
|
+
export declare function resolveChase(config: ChaseCameraConfig | undefined): ResolvedChase;
|
|
92
|
+
export interface ResolvedObserver {
|
|
93
|
+
distance: number;
|
|
94
|
+
height: number;
|
|
95
|
+
lookHeight: number;
|
|
96
|
+
orbitSpeed: number;
|
|
97
|
+
}
|
|
98
|
+
export declare function resolveObserver(config: ObserverCameraConfig | undefined): ResolvedObserver;
|
|
99
|
+
/** Detached spectator pose: orbits `subject` at a fixed distance/height, never reading player input. */
|
|
100
|
+
export declare function observerPose(subject: Vec3, angle: number, resolved: ResolvedObserver, fov: number): CameraPose;
|
|
101
|
+
/** Desired chase-camera position behind a vehicle facing `yaw` (before spring smoothing). */
|
|
102
|
+
export declare function chaseDesiredPosition(follow: Vec3, yaw: number, resolved: ResolvedChase): Vec3;
|
|
103
|
+
/** Look point ahead of / above a chased vehicle. */
|
|
104
|
+
export declare function chaseLookAt(follow: Vec3, yaw: number, resolved: ResolvedChase): Vec3;
|
|
105
|
+
/** World-space seat pose (cockpit/hood/rear) rigidly attached to the vehicle. */
|
|
106
|
+
export declare function seatPose(follow: Vec3, yaw: number, local: {
|
|
107
|
+
x?: number;
|
|
108
|
+
y?: number;
|
|
109
|
+
z?: number;
|
|
110
|
+
}, fov: number): CameraPose;
|
|
111
|
+
export interface TraumaState {
|
|
112
|
+
trauma: number;
|
|
113
|
+
time: number;
|
|
114
|
+
}
|
|
115
|
+
export declare function createTrauma(): TraumaState;
|
|
116
|
+
/** Add trauma (0..1) and clamp; larger hits raise the ceiling toward 1. */
|
|
117
|
+
export declare function addTrauma(state: TraumaState, amount: number): void;
|
|
118
|
+
/** Decay trauma linearly toward 0 and advance the shake clock. */
|
|
119
|
+
export declare function stepTrauma(state: TraumaState, decayPerSecond: number, dt: number): void;
|
|
120
|
+
export interface ShakeOffset {
|
|
121
|
+
x: number;
|
|
122
|
+
y: number;
|
|
123
|
+
roll: number;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Positional + roll shake for the current trauma. Shake magnitude is
|
|
127
|
+
* `trauma^exponent` so small hits stay subtle; the oscillation is deterministic
|
|
128
|
+
* per-seed pseudo-noise (no per-frame RNG, so identical inputs reproduce).
|
|
129
|
+
*/
|
|
130
|
+
export declare function shakeOffset(state: TraumaState, config: {
|
|
131
|
+
maxOffset?: number;
|
|
132
|
+
maxRoll?: number;
|
|
133
|
+
exponent?: number;
|
|
134
|
+
frequency?: number;
|
|
135
|
+
} | undefined): ShakeOffset;
|
|
136
|
+
/** Linear cross-fade between two full camera poses (position, lookAt, fov). */
|
|
137
|
+
export declare function crossfadePose(from: CameraPose, to: CameraPose, t: number): CameraPose;
|
|
138
|
+
export interface DirectorCameraValues {
|
|
139
|
+
/** `undefined` = no runtime override (fall back to static); `null` = explicitly follow nothing. */
|
|
140
|
+
followEntityId?: string | null;
|
|
141
|
+
/** `null` = no runtime cinematic active (fall back to static). */
|
|
142
|
+
cinematic?: CinematicCameraConfig | null;
|
|
143
|
+
}
|
|
144
|
+
export interface StaticCameraValues {
|
|
145
|
+
followEntityId?: string | null;
|
|
146
|
+
cinematic?: CinematicCameraConfig;
|
|
147
|
+
}
|
|
148
|
+
export interface ResolvedDirectedCamera {
|
|
149
|
+
followEntityId: string | null | undefined;
|
|
150
|
+
cinematic: CinematicCameraConfig | undefined;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Merges a `CameraDirector` runtime snapshot over the static `GameCameraConfig`
|
|
154
|
+
* (#196.2). `director` omitted, or its fields `undefined`/`null`, is a pure
|
|
155
|
+
* passthrough to `staticConfig` so mounting a director with no active override
|
|
156
|
+
* changes nothing.
|
|
157
|
+
*/
|
|
158
|
+
export declare function resolveDirectedCamera(director: DirectorCameraValues | undefined, staticConfig: StaticCameraValues): ResolvedDirectedCamera;
|
|
159
|
+
export interface CinematicSample {
|
|
160
|
+
pose: CameraPose;
|
|
161
|
+
done: boolean;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Sample a keyframe path at `elapsed` seconds. Each keyframe's `duration` is the
|
|
165
|
+
* travel time from the previous keyframe into it; segment easing is per the
|
|
166
|
+
* destination keyframe. Reports `done` once past the final keyframe (unless
|
|
167
|
+
* looping, which wraps `elapsed` into the total path duration).
|
|
168
|
+
*/
|
|
169
|
+
export declare function cinematicSample(keyframes: readonly CameraKeyframe[], elapsed: number, loop: boolean, fallbackFov: number): CinematicSample;
|
|
@@ -0,0 +1,393 @@
|
|
|
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
|
+
}
|
|
14
|
+
export function clamp(value, min, max) {
|
|
15
|
+
return value < min ? min : value > max ? max : value;
|
|
16
|
+
}
|
|
17
|
+
export function lerp(from, to, blend) {
|
|
18
|
+
return from + (to - from) * blend;
|
|
19
|
+
}
|
|
20
|
+
export function smoothstep(t) {
|
|
21
|
+
const clamped = clamp(t, 0, 1);
|
|
22
|
+
return clamped * clamped * (3 - 2 * clamped);
|
|
23
|
+
}
|
|
24
|
+
export function forwardVector(yaw) {
|
|
25
|
+
return { x: Math.sin(yaw), y: 0, z: Math.cos(yaw) };
|
|
26
|
+
}
|
|
27
|
+
export function rightVector(yaw) {
|
|
28
|
+
return { x: Math.cos(yaw), y: 0, z: -Math.sin(yaw) };
|
|
29
|
+
}
|
|
30
|
+
export function resolveTopDown(config) {
|
|
31
|
+
return {
|
|
32
|
+
height: config?.height ?? 18,
|
|
33
|
+
pitch: config?.pitch ?? 1.02,
|
|
34
|
+
yaw: config?.yaw ?? Math.PI / 4,
|
|
35
|
+
offset: {
|
|
36
|
+
x: config?.targetOffset?.x ?? 0,
|
|
37
|
+
y: config?.targetOffset?.y ?? 0,
|
|
38
|
+
z: config?.targetOffset?.z ?? 0,
|
|
39
|
+
},
|
|
40
|
+
followSmoothing: config?.followSmoothing ?? 8,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Camera pose for a fixed top-down / isometric rig. `pitch` is the elevation of
|
|
45
|
+
* the camera→target ray above the ground (PI/2 = straight down); `yaw` is the
|
|
46
|
+
* fixed azimuth (PI/4 reads isometric). Height sets zoom; horizontal boom is
|
|
47
|
+
* derived so the look angle stays constant as height changes.
|
|
48
|
+
*/
|
|
49
|
+
export function topDownPose(follow, resolved, fov) {
|
|
50
|
+
const lookAt = {
|
|
51
|
+
x: follow.x + resolved.offset.x,
|
|
52
|
+
y: follow.y + resolved.offset.y,
|
|
53
|
+
z: follow.z + resolved.offset.z,
|
|
54
|
+
};
|
|
55
|
+
const pitch = clamp(resolved.pitch, 0.05, Math.PI / 2);
|
|
56
|
+
const horizontal = resolved.height / Math.tan(pitch);
|
|
57
|
+
return {
|
|
58
|
+
position: {
|
|
59
|
+
x: lookAt.x - Math.sin(resolved.yaw) * horizontal,
|
|
60
|
+
y: lookAt.y + resolved.height,
|
|
61
|
+
z: lookAt.z - Math.cos(resolved.yaw) * horizontal,
|
|
62
|
+
},
|
|
63
|
+
lookAt,
|
|
64
|
+
fov,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** Exponential spring-arm approach toward a desired point (frame-rate independent). */
|
|
68
|
+
export function springArmStep(current, desired, damping, dt) {
|
|
69
|
+
return lerpVec3(current, desired, smoothBlend(dt, damping));
|
|
70
|
+
}
|
|
71
|
+
export function resolveSideScroll(config) {
|
|
72
|
+
return {
|
|
73
|
+
axis: config?.axis ?? "x",
|
|
74
|
+
distance: config?.distance ?? 10,
|
|
75
|
+
height: config?.height ?? 3,
|
|
76
|
+
lookHeight: config?.lookHeight ?? 1,
|
|
77
|
+
followSmoothing: config?.followSmoothing ?? 8,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** Frame-rate independent follow blend for the side-scroll rig; `followSmoothing <= 0` hard-locks (blend 1) instead of freezing. */
|
|
81
|
+
export function sideScrollFollowBlend(followSmoothing, dt) {
|
|
82
|
+
return followSmoothing <= 0 ? 1 : smoothBlend(dt, followSmoothing);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Fixed lateral 2.5D follow pose: the camera sits perpendicular to the travel
|
|
86
|
+
* axis at `distance`, above the entity by `height`, and looks at the entity
|
|
87
|
+
* raised by `lookHeight`. Axis "x" watches from +z; axis "z" watches from +x.
|
|
88
|
+
*/
|
|
89
|
+
export function resolveSideScrollPose(entityPos, resolved, fov) {
|
|
90
|
+
const perpendicular = resolved.axis === "x" ? { x: 0, y: 0, z: 1 } : { x: 1, y: 0, z: 0 };
|
|
91
|
+
return {
|
|
92
|
+
position: {
|
|
93
|
+
x: entityPos.x + perpendicular.x * resolved.distance,
|
|
94
|
+
y: entityPos.y + resolved.height,
|
|
95
|
+
z: entityPos.z + perpendicular.z * resolved.distance,
|
|
96
|
+
},
|
|
97
|
+
lookAt: { x: entityPos.x, y: entityPos.y + resolved.lookHeight, z: entityPos.z },
|
|
98
|
+
fov,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/** Speed→FOV curve: FOV climbs from base to max as speed rises to `speedForMax`. */
|
|
102
|
+
export function speedToFov(speed, curve) {
|
|
103
|
+
const base = curve?.base ?? 55;
|
|
104
|
+
const max = curve?.max ?? 78;
|
|
105
|
+
const speedForMax = curve?.speedForMax ?? 24;
|
|
106
|
+
if (speedForMax <= 0)
|
|
107
|
+
return base;
|
|
108
|
+
return lerp(base, max, clamp(speed / speedForMax, 0, 1));
|
|
109
|
+
}
|
|
110
|
+
/** Yaw that points from `from` toward `to` on the XZ plane (matches shell forward = (sin, cos)). */
|
|
111
|
+
export function yawTo(from, to) {
|
|
112
|
+
return Math.atan2(to.x - from.x, to.z - from.z);
|
|
113
|
+
}
|
|
114
|
+
/** Shortest signed angular delta from `a` to `b`, wrapped to (-PI, PI]. */
|
|
115
|
+
export function angleDelta(a, b) {
|
|
116
|
+
let delta = (b - a) % (Math.PI * 2);
|
|
117
|
+
if (delta > Math.PI)
|
|
118
|
+
delta -= Math.PI * 2;
|
|
119
|
+
if (delta < -Math.PI)
|
|
120
|
+
delta += Math.PI * 2;
|
|
121
|
+
return delta;
|
|
122
|
+
}
|
|
123
|
+
/** Exponential yaw smoothing that respects wrap-around. */
|
|
124
|
+
export function smoothYaw(current, desired, speed, dt) {
|
|
125
|
+
return current + angleDelta(current, desired) * smoothBlend(dt, speed);
|
|
126
|
+
}
|
|
127
|
+
export function resolveShoulder(config, aiming) {
|
|
128
|
+
const hip = {
|
|
129
|
+
shoulderOffset: config?.shoulderOffset ?? 0.6,
|
|
130
|
+
heightOffset: config?.heightOffset ?? 1.6,
|
|
131
|
+
distance: config?.distance ?? 3.2,
|
|
132
|
+
fov: config?.fov ?? 55,
|
|
133
|
+
};
|
|
134
|
+
if (!aiming)
|
|
135
|
+
return hip;
|
|
136
|
+
const ads = config?.ads;
|
|
137
|
+
return {
|
|
138
|
+
shoulderOffset: ads?.shoulderOffset ?? hip.shoulderOffset * 0.4,
|
|
139
|
+
heightOffset: ads?.heightOffset ?? hip.heightOffset,
|
|
140
|
+
distance: ads?.distance ?? hip.distance * 0.55,
|
|
141
|
+
fov: ads?.fov ?? hip.fov * 0.75,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/** Blend two shoulder framings by an ADS factor in [0,1]. */
|
|
145
|
+
export function blendShoulder(hip, ads, t) {
|
|
146
|
+
return {
|
|
147
|
+
shoulderOffset: lerp(hip.shoulderOffset, ads.shoulderOffset, t),
|
|
148
|
+
heightOffset: lerp(hip.heightOffset, ads.heightOffset, t),
|
|
149
|
+
distance: lerp(hip.distance, ads.distance, t),
|
|
150
|
+
fov: lerp(hip.fov, ads.fov, t),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Over-the-shoulder camera pose. The boom sits behind the follow point along
|
|
155
|
+
* `yaw`, raised by `heightOffset`, and pushed sideways by `shoulderOffset`
|
|
156
|
+
* (sign set by `sideSign`: +1 right, -1 left). `pitch` tilts the look point.
|
|
157
|
+
*/
|
|
158
|
+
export function shoulderPose(follow, yaw, pitch, sideSign, shoulder) {
|
|
159
|
+
const forward = forwardVector(yaw);
|
|
160
|
+
const right = rightVector(yaw);
|
|
161
|
+
const anchor = {
|
|
162
|
+
x: follow.x + right.x * shoulder.shoulderOffset * sideSign,
|
|
163
|
+
y: follow.y + shoulder.heightOffset,
|
|
164
|
+
z: follow.z + right.z * shoulder.shoulderOffset * sideSign,
|
|
165
|
+
};
|
|
166
|
+
const cosPitch = Math.cos(pitch);
|
|
167
|
+
const position = {
|
|
168
|
+
x: anchor.x - forward.x * shoulder.distance * cosPitch,
|
|
169
|
+
y: anchor.y + Math.sin(pitch) * shoulder.distance,
|
|
170
|
+
z: anchor.z - forward.z * shoulder.distance * cosPitch,
|
|
171
|
+
};
|
|
172
|
+
const lookAt = {
|
|
173
|
+
x: anchor.x + forward.x * 4 * cosPitch,
|
|
174
|
+
y: anchor.y - Math.sin(pitch) * 4,
|
|
175
|
+
z: anchor.z + forward.z * 4 * cosPitch,
|
|
176
|
+
};
|
|
177
|
+
return { position, lookAt, fov: shoulder.fov };
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Lock-on pose: camera sits behind the player along the player→target vector so
|
|
181
|
+
* the target stays framed. The look point is biased between player and target
|
|
182
|
+
* by `framingBias`.
|
|
183
|
+
*/
|
|
184
|
+
export function lockOnPose(player, target, config, fov) {
|
|
185
|
+
const distance = config?.distance ?? 5;
|
|
186
|
+
const height = config?.height ?? 2.4;
|
|
187
|
+
const lookHeight = config?.lookHeight ?? 1.2;
|
|
188
|
+
const bias = clamp(config?.framingBias ?? 0.5, 0, 1);
|
|
189
|
+
const yaw = yawTo(player, target);
|
|
190
|
+
const back = forwardVector(yaw);
|
|
191
|
+
const focus = {
|
|
192
|
+
x: lerp(player.x, target.x, bias),
|
|
193
|
+
y: lerp(player.y, target.y, bias) + lookHeight,
|
|
194
|
+
z: lerp(player.z, target.z, bias),
|
|
195
|
+
};
|
|
196
|
+
const position = {
|
|
197
|
+
x: player.x - back.x * distance,
|
|
198
|
+
y: player.y + height,
|
|
199
|
+
z: player.z - back.z * distance,
|
|
200
|
+
};
|
|
201
|
+
return { pose: { position, lookAt: focus, fov }, yaw };
|
|
202
|
+
}
|
|
203
|
+
export function resolveChase(config) {
|
|
204
|
+
return {
|
|
205
|
+
distance: config?.distance ?? 6,
|
|
206
|
+
height: config?.height ?? 2.6,
|
|
207
|
+
lookHeight: config?.lookHeight ?? 1.2,
|
|
208
|
+
springDamping: config?.springDamping ?? 6,
|
|
209
|
+
shakePerSpeed: config?.shakePerSpeed ?? 0,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
export function resolveObserver(config) {
|
|
213
|
+
return {
|
|
214
|
+
distance: config?.distance ?? 8,
|
|
215
|
+
height: config?.height ?? 3,
|
|
216
|
+
lookHeight: config?.lookHeight ?? 1.2,
|
|
217
|
+
orbitSpeed: config?.orbitSpeed ?? 0.2,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
/** Detached spectator pose: orbits `subject` at a fixed distance/height, never reading player input. */
|
|
221
|
+
export function observerPose(subject, angle, resolved, fov) {
|
|
222
|
+
const position = {
|
|
223
|
+
x: subject.x + Math.sin(angle) * resolved.distance,
|
|
224
|
+
y: subject.y + resolved.height,
|
|
225
|
+
z: subject.z + Math.cos(angle) * resolved.distance,
|
|
226
|
+
};
|
|
227
|
+
return {
|
|
228
|
+
position,
|
|
229
|
+
lookAt: { x: subject.x, y: subject.y + resolved.lookHeight, z: subject.z },
|
|
230
|
+
fov,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
/** Desired chase-camera position behind a vehicle facing `yaw` (before spring smoothing). */
|
|
234
|
+
export function chaseDesiredPosition(follow, yaw, resolved) {
|
|
235
|
+
const back = forwardVector(yaw);
|
|
236
|
+
return {
|
|
237
|
+
x: follow.x - back.x * resolved.distance,
|
|
238
|
+
y: follow.y + resolved.height,
|
|
239
|
+
z: follow.z - back.z * resolved.distance,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
/** Look point ahead of / above a chased vehicle. */
|
|
243
|
+
export function chaseLookAt(follow, yaw, resolved) {
|
|
244
|
+
const forward = forwardVector(yaw);
|
|
245
|
+
return {
|
|
246
|
+
x: follow.x + forward.x * 2,
|
|
247
|
+
y: follow.y + resolved.lookHeight,
|
|
248
|
+
z: follow.z + forward.z * 2,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
/** World-space seat pose (cockpit/hood/rear) rigidly attached to the vehicle. */
|
|
252
|
+
export function seatPose(follow, yaw, local, fov) {
|
|
253
|
+
const forward = forwardVector(yaw);
|
|
254
|
+
const right = rightVector(yaw);
|
|
255
|
+
const lx = local.x ?? 0;
|
|
256
|
+
const ly = local.y ?? 1.1;
|
|
257
|
+
const lz = local.z ?? 0.2;
|
|
258
|
+
const position = {
|
|
259
|
+
x: follow.x + right.x * lx + forward.x * lz,
|
|
260
|
+
y: follow.y + ly,
|
|
261
|
+
z: follow.z + right.z * lx + forward.z * lz,
|
|
262
|
+
};
|
|
263
|
+
return {
|
|
264
|
+
position,
|
|
265
|
+
lookAt: { x: position.x + forward.x * 6, y: position.y, z: position.z + forward.z * 6 },
|
|
266
|
+
fov,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
export function createTrauma() {
|
|
270
|
+
return { trauma: 0, time: 0 };
|
|
271
|
+
}
|
|
272
|
+
/** Add trauma (0..1) and clamp; larger hits raise the ceiling toward 1. */
|
|
273
|
+
export function addTrauma(state, amount) {
|
|
274
|
+
state.trauma = clamp(state.trauma + amount, 0, 1);
|
|
275
|
+
}
|
|
276
|
+
/** Decay trauma linearly toward 0 and advance the shake clock. */
|
|
277
|
+
export function stepTrauma(state, decayPerSecond, dt) {
|
|
278
|
+
state.time += dt;
|
|
279
|
+
state.trauma = clamp(state.trauma - decayPerSecond * dt, 0, 1);
|
|
280
|
+
}
|
|
281
|
+
function hashNoise(seed, time, frequency) {
|
|
282
|
+
const phase = time * frequency + seed * 12.9898;
|
|
283
|
+
return Math.sin(phase) * 0.5 + Math.sin(phase * 2.137 + 1.7) * 0.5;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Positional + roll shake for the current trauma. Shake magnitude is
|
|
287
|
+
* `trauma^exponent` so small hits stay subtle; the oscillation is deterministic
|
|
288
|
+
* per-seed pseudo-noise (no per-frame RNG, so identical inputs reproduce).
|
|
289
|
+
*/
|
|
290
|
+
export function shakeOffset(state, config) {
|
|
291
|
+
const maxOffset = config?.maxOffset ?? 0.5;
|
|
292
|
+
const maxRoll = config?.maxRoll ?? 0.06;
|
|
293
|
+
const exponent = config?.exponent ?? 2;
|
|
294
|
+
const frequency = config?.frequency ?? 24;
|
|
295
|
+
const shake = Math.pow(state.trauma, exponent);
|
|
296
|
+
if (shake <= 0)
|
|
297
|
+
return { x: 0, y: 0, roll: 0 };
|
|
298
|
+
return {
|
|
299
|
+
x: maxOffset * shake * hashNoise(1, state.time, frequency),
|
|
300
|
+
y: maxOffset * shake * hashNoise(2, state.time, frequency),
|
|
301
|
+
roll: maxRoll * shake * hashNoise(3, state.time, frequency),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
/** Linear cross-fade between two full camera poses (position, lookAt, fov). */
|
|
305
|
+
export function crossfadePose(from, to, t) {
|
|
306
|
+
const blend = clamp(t, 0, 1);
|
|
307
|
+
return {
|
|
308
|
+
position: lerpVec3(from.position, to.position, blend),
|
|
309
|
+
lookAt: lerpVec3(from.lookAt, to.lookAt, blend),
|
|
310
|
+
fov: lerp(from.fov, to.fov, blend),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Merges a `CameraDirector` runtime snapshot over the static `GameCameraConfig`
|
|
315
|
+
* (#196.2). `director` omitted, or its fields `undefined`/`null`, is a pure
|
|
316
|
+
* passthrough to `staticConfig` so mounting a director with no active override
|
|
317
|
+
* changes nothing.
|
|
318
|
+
*/
|
|
319
|
+
export function resolveDirectedCamera(director, staticConfig) {
|
|
320
|
+
const directedFollow = director?.followEntityId;
|
|
321
|
+
const followEntityId = directedFollow === undefined ? staticConfig.followEntityId : directedFollow;
|
|
322
|
+
const directedCinematic = director?.cinematic;
|
|
323
|
+
const cinematic = directedCinematic === undefined || directedCinematic === null ? staticConfig.cinematic : directedCinematic;
|
|
324
|
+
return { followEntityId, cinematic };
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Sample a keyframe path at `elapsed` seconds. Each keyframe's `duration` is the
|
|
328
|
+
* travel time from the previous keyframe into it; segment easing is per the
|
|
329
|
+
* destination keyframe. Reports `done` once past the final keyframe (unless
|
|
330
|
+
* looping, which wraps `elapsed` into the total path duration).
|
|
331
|
+
*/
|
|
332
|
+
export function cinematicSample(keyframes, elapsed, loop, fallbackFov) {
|
|
333
|
+
if (keyframes.length === 0) {
|
|
334
|
+
const zero = { x: 0, y: 0, z: 0 };
|
|
335
|
+
return { pose: { position: zero, lookAt: zero, fov: fallbackFov }, done: true };
|
|
336
|
+
}
|
|
337
|
+
const first = keyframes[0];
|
|
338
|
+
const firstPose = {
|
|
339
|
+
position: { ...first.position },
|
|
340
|
+
lookAt: { ...first.lookAt },
|
|
341
|
+
fov: first.fov ?? fallbackFov,
|
|
342
|
+
};
|
|
343
|
+
if (keyframes.length === 1)
|
|
344
|
+
return { pose: firstPose, done: true };
|
|
345
|
+
let total = 0;
|
|
346
|
+
for (let i = 1; i < keyframes.length; i += 1)
|
|
347
|
+
total += keyframes[i].duration ?? 1;
|
|
348
|
+
if (total <= 0)
|
|
349
|
+
return { pose: firstPose, done: true };
|
|
350
|
+
let time = elapsed;
|
|
351
|
+
let done = false;
|
|
352
|
+
if (loop) {
|
|
353
|
+
time = ((elapsed % total) + total) % total;
|
|
354
|
+
}
|
|
355
|
+
else if (elapsed >= total) {
|
|
356
|
+
const last = keyframes[keyframes.length - 1];
|
|
357
|
+
return {
|
|
358
|
+
pose: { position: { ...last.position }, lookAt: { ...last.lookAt }, fov: last.fov ?? fallbackFov },
|
|
359
|
+
done: true,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
else if (elapsed <= 0) {
|
|
363
|
+
return { pose: firstPose, done: false };
|
|
364
|
+
}
|
|
365
|
+
let acc = 0;
|
|
366
|
+
for (let i = 1; i < keyframes.length; i += 1) {
|
|
367
|
+
const to = keyframes[i];
|
|
368
|
+
const from = keyframes[i - 1];
|
|
369
|
+
const duration = to.duration ?? 1;
|
|
370
|
+
if (time <= acc + duration || i === keyframes.length - 1) {
|
|
371
|
+
const localT = duration <= 0 ? 1 : clamp((time - acc) / duration, 0, 1);
|
|
372
|
+
const eased = to.ease === "linear" ? localT : smoothstep(localT);
|
|
373
|
+
const fromPose = {
|
|
374
|
+
position: { ...from.position },
|
|
375
|
+
lookAt: { ...from.lookAt },
|
|
376
|
+
fov: from.fov ?? fallbackFov,
|
|
377
|
+
};
|
|
378
|
+
const toPose = {
|
|
379
|
+
position: { ...to.position },
|
|
380
|
+
lookAt: { ...to.lookAt },
|
|
381
|
+
fov: to.fov ?? fallbackFov,
|
|
382
|
+
};
|
|
383
|
+
return { pose: crossfadePose(fromPose, toPose, eased), done };
|
|
384
|
+
}
|
|
385
|
+
acc += duration;
|
|
386
|
+
}
|
|
387
|
+
done = true;
|
|
388
|
+
const last = keyframes[keyframes.length - 1];
|
|
389
|
+
return {
|
|
390
|
+
pose: { position: { ...last.position }, lookAt: { ...last.lookAt }, fov: last.fov ?? fallbackFov },
|
|
391
|
+
done,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
@@ -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;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves which rig mounts from a `GameCameraConfig`. Precedence, most to least
|
|
3
|
+
* specific: an explicit `rig` field always wins; then `perspective: "first"`
|
|
4
|
+
* (the historical shorthand for `rig: "orbit" | "first"`); then the mere presence
|
|
5
|
+
* of a rig's own config block selects that rig, checked in the fixed order below
|
|
6
|
+
* (#207.8) so a config carrying more than one block resolves deterministically
|
|
7
|
+
* instead of depending on object key order. Set `rig` explicitly to break a tie.
|
|
8
|
+
*/
|
|
9
|
+
export function resolveRigKind(config) {
|
|
10
|
+
if (config?.rig !== undefined)
|
|
11
|
+
return config.rig;
|
|
12
|
+
if (config?.perspective === "first")
|
|
13
|
+
return "first";
|
|
14
|
+
if (config?.topDown !== undefined)
|
|
15
|
+
return "topDown";
|
|
16
|
+
if (config?.rts !== undefined)
|
|
17
|
+
return "rts";
|
|
18
|
+
if (config?.shoulder !== undefined)
|
|
19
|
+
return "shoulder";
|
|
20
|
+
if (config?.lockOn !== undefined)
|
|
21
|
+
return "lockOn";
|
|
22
|
+
if (config?.chase !== undefined)
|
|
23
|
+
return "chase";
|
|
24
|
+
if (config?.observer !== undefined)
|
|
25
|
+
return "observer";
|
|
26
|
+
if (config?.turntable !== undefined)
|
|
27
|
+
return "turntable";
|
|
28
|
+
if (config?.sideScroll !== undefined)
|
|
29
|
+
return "sideScroll";
|
|
30
|
+
if (config?.inspection !== undefined)
|
|
31
|
+
return "inspection";
|
|
32
|
+
return "orbit";
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The turntable rig is a flat facade over the observer's point-orbit mode: map
|
|
36
|
+
* its `target`/`distance`/… onto an observer block so ObserverRig runs unchanged.
|
|
37
|
+
*/
|
|
38
|
+
export function turntableAsObserver(config) {
|
|
39
|
+
const t = config?.turntable;
|
|
40
|
+
if (t === undefined)
|
|
41
|
+
return config ?? {};
|
|
42
|
+
const observer = {
|
|
43
|
+
...(t.target !== undefined ? { bind: { kind: "point", position: t.target } } : {}),
|
|
44
|
+
...(t.distance !== undefined ? { distance: t.distance } : {}),
|
|
45
|
+
...(t.height !== undefined ? { height: t.height } : {}),
|
|
46
|
+
...(t.lookHeight !== undefined ? { lookHeight: t.lookHeight } : {}),
|
|
47
|
+
...(t.orbitSpeed !== undefined ? { orbitSpeed: t.orbitSpeed } : {}),
|
|
48
|
+
...(t.startAngle !== undefined ? { startAngle: t.startAngle } : {}),
|
|
49
|
+
...(t.fov !== undefined ? { fov: t.fov } : {}),
|
|
50
|
+
};
|
|
51
|
+
return { ...config, observer };
|
|
52
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
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;
|
|
18
|
+
/**
|
|
19
|
+
* Process-wide default channel. A shell mounts its own channel via
|
|
20
|
+
* `CameraShakeContext`, but game systems that have no React context (e.g. a
|
|
21
|
+
* `loop.onTick` reacting to `entity.died`) can import `cameraShake` and feed the
|
|
22
|
+
* default channel directly.
|
|
23
|
+
*/
|
|
24
|
+
export declare const defaultCameraShakeChannel: CameraShakeChannel;
|
|
25
|
+
/** Feed the default camera-shake channel from anywhere (see G7 hitstop cross-cut). */
|
|
26
|
+
export declare function cameraShake(amplitude: number, decayPerSecond?: number): void;
|
|
27
|
+
export declare const CameraShakeContext: import("react").Context<CameraShakeChannel>;
|
|
28
|
+
/** The active rig's shake channel — call `.shake(...)` to add trauma from React UI. */
|
|
29
|
+
export declare function useCameraShake(): CameraShakeChannel;
|