@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.
- package/CHANGELOG.md +16 -0
- package/README.md +11 -0
- package/dist/GamePlayerShell.d.ts +5 -2
- package/dist/GamePlayerShell.js +358 -98
- package/dist/GameUiPreview.js +2 -1
- package/dist/audio/AudioComponents.js +25 -8
- package/dist/audio/audioEngine.d.ts +1 -0
- package/dist/audio/audioEngine.js +4 -0
- package/dist/behaviour.d.ts +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.js +14 -0
- package/dist/camera/GameOrbitCamera.js +10 -1
- package/dist/camera/PlayerFov.d.ts +18 -0
- package/dist/camera/PlayerFov.js +48 -0
- package/dist/camera/cameraBlendMath.d.ts +13 -0
- package/dist/camera/cameraBlendMath.js +36 -0
- package/dist/camera/cameraRigs.d.ts +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/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 +245 -0
- package/dist/defineGame.js +2 -1
- package/dist/devtools/CollisionDebugWorld.d.ts +5 -0
- package/dist/devtools/CollisionDebugWorld.js +180 -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 +102 -0
- package/dist/devtools/collisionDebugMath.js +128 -0
- package/dist/environment/Daylight.d.ts +13 -7
- package/dist/environment/Daylight.js +12 -11
- package/dist/environment/EnvironmentScene.js +82 -35
- 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/materialOverride.d.ts +4 -6
- package/dist/materialOverride.js +12 -16
- package/dist/pointer/PointerProbe.js +6 -2
- package/dist/pointer/pointerService.d.ts +3 -0
- package/dist/pointer/pointerService.js +6 -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 +2 -1
- package/dist/terrain/CarvedTerrain.js +3 -3
- package/dist/terrain/GrassField.d.ts +3 -1
- package/dist/terrain/GrassField.js +4 -2
- package/dist/terrain/grassBudget.d.ts +3 -0
- package/dist/terrain/grassBudget.js +6 -0
- package/dist/terrain/grassGeometry.js +1 -1
- package/dist/terrain/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 +115 -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 +3 -0
- package/dist/world/WorldHud.js +89 -42
- package/dist/world/entityPose.d.ts +14 -0
- package/dist/world/entityPose.js +10 -0
- package/dist/world/telegraphPulse.d.ts +1 -0
- package/dist/world/telegraphPulse.js +4 -0
- package/dist/world/worldBarSamples.d.ts +30 -0
- package/dist/world/worldBarSamples.js +51 -0
- package/llms.txt +1404 -1143
- 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,
|
|
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";
|
package/dist/camera/rigMath.d.ts
CHANGED
|
@@ -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;
|
package/dist/camera/rigMath.js
CHANGED
|
@@ -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:
|
|
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 {
|
|
2
|
-
export
|
|
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 {
|
|
3
|
-
export
|
|
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;
|