@jgengine/shell 0.7.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 +47 -0
- 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 +439 -28
- package/dist/behaviour.d.ts +31 -0
- package/dist/behaviour.js +63 -0
- package/dist/camera/GameCameraRig.d.ts +8 -3
- package/dist/camera/GameCameraRig.js +28 -14
- 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/cameraRigs.d.ts +5 -1
- package/dist/camera/cameraRigs.js +43 -6
- package/dist/camera/index.d.ts +4 -2
- package/dist/camera/index.js +4 -2
- package/dist/camera/inspectionCameraMath.d.ts +25 -0
- package/dist/camera/inspectionCameraMath.js +44 -0
- package/dist/camera/rigMath.d.ts +40 -1
- package/dist/camera/rigMath.js +55 -0
- package/dist/camera/rigResolve.d.ts +15 -0
- package/dist/camera/rigResolve.js +52 -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/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/PointerProbe.js +14 -1
- package/dist/pointer/pointerService.d.ts +2 -0
- package/dist/pointer/pointerService.js +45 -24
- 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/terrain/index.d.ts +1 -1
- package/dist/terrain/index.js +1 -1
- package/dist/terrain/terrainMath.d.ts +1 -0
- package/dist/terrain/terrainMath.js +8 -2
- package/dist/touch/TouchControlsOverlay.d.ts +18 -0
- package/dist/touch/TouchControlsOverlay.js +173 -0
- package/dist/water/OceanShader.d.ts +1 -1
- package/dist/water/OceanShader.js +3 -3
- 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/WorldHud.d.ts +5 -1
- package/dist/world/WorldHud.js +6 -2
- package/dist/world/WorldItems.js +21 -2
- package/llms.txt +2382 -0
- package/package.json +6 -5
- package/dist/demo/builderDemo.d.ts +0 -2
- package/dist/demo/builderDemo.js +0 -189
- package/dist/demo/demoGame.d.ts +0 -2
- package/dist/demo/demoGame.js +0 -208
- package/dist/demo/environmentShowcase.d.ts +0 -2
- package/dist/demo/environmentShowcase.js +0 -15
- package/dist/demo/mapDemo.d.ts +0 -2
- package/dist/demo/mapDemo.js +0 -206
- package/dist/demo/pointerDemo.d.ts +0 -2
- package/dist/demo/pointerDemo.js +0 -151
- package/dist/demo/sensorShowcase.d.ts +0 -2
- package/dist/demo/sensorShowcase.js +0 -131
- package/dist/demo/survivalDemo.d.ts +0 -2
- package/dist/demo/survivalDemo.js +0 -291
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Object3D } from "three";
|
|
2
|
+
import { Behaviour, type BehaviourWorld } from "@jgengine/core/behaviour/behaviour";
|
|
3
|
+
/**
|
|
4
|
+
* A core `Behaviour` bound to a three.js object. `onBeforeRender`/`onAfterRender`
|
|
5
|
+
* ride the object's own three.js render callbacks, so they only fire for renderable
|
|
6
|
+
* objects (Mesh, Line, Points, Sprite) that are visible and in frustum — attach to
|
|
7
|
+
* the mesh itself, not a parent Group, when you need them.
|
|
8
|
+
*/
|
|
9
|
+
export declare class Object3DBehaviour extends Behaviour {
|
|
10
|
+
/** @internal */ _object: Object3D | null;
|
|
11
|
+
/** The bound three.js object; available from `onAwake` onward. */
|
|
12
|
+
get object(): Object3D;
|
|
13
|
+
onBeforeRender(): void;
|
|
14
|
+
onAfterRender(): void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Binds `behaviour` to `object` and attaches it to `world` under `nodeId`
|
|
18
|
+
* (default: the object's uuid). Render hooks are chained onto the object's
|
|
19
|
+
* existing `onBeforeRender`/`onAfterRender` only when actually overridden, and
|
|
20
|
+
* are gated on the behaviour being active; the update lifecycle still flows
|
|
21
|
+
* through `world.update`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function attachObject3D<T extends Object3DBehaviour>(world: BehaviourWorld, object: Object3D, behaviour: T, nodeId?: string): T;
|
|
24
|
+
/**
|
|
25
|
+
* Bootstraps `world` on mount and dispatches `world.update` every frame at
|
|
26
|
+
* simulation priority. `scaleDt` maps the raw frame delta to the dt behaviours
|
|
27
|
+
* receive — pass the game clock's scaling to keep behaviours on game time, or
|
|
28
|
+
* omit for real-time seconds. Games driving `world.update` themselves from
|
|
29
|
+
* `loop.onTick` should not also render this.
|
|
30
|
+
*/
|
|
31
|
+
export declare function useBehaviourWorld(world: BehaviourWorld, scaleDt?: (rawDt: number) => number): void;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useFrame } from "@react-three/fiber";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
import { Behaviour } from "@jgengine/core/behaviour/behaviour";
|
|
4
|
+
/**
|
|
5
|
+
* A core `Behaviour` bound to a three.js object. `onBeforeRender`/`onAfterRender`
|
|
6
|
+
* ride the object's own three.js render callbacks, so they only fire for renderable
|
|
7
|
+
* objects (Mesh, Line, Points, Sprite) that are visible and in frustum — attach to
|
|
8
|
+
* the mesh itself, not a parent Group, when you need them.
|
|
9
|
+
*/
|
|
10
|
+
export class Object3DBehaviour extends Behaviour {
|
|
11
|
+
/** @internal */ _object = null;
|
|
12
|
+
/** The bound three.js object; available from `onAwake` onward. */
|
|
13
|
+
get object() {
|
|
14
|
+
if (this._object === null)
|
|
15
|
+
throw new Error("behaviour is not bound to an Object3D");
|
|
16
|
+
return this._object;
|
|
17
|
+
}
|
|
18
|
+
onBeforeRender() { }
|
|
19
|
+
onAfterRender() { }
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Binds `behaviour` to `object` and attaches it to `world` under `nodeId`
|
|
23
|
+
* (default: the object's uuid). Render hooks are chained onto the object's
|
|
24
|
+
* existing `onBeforeRender`/`onAfterRender` only when actually overridden, and
|
|
25
|
+
* are gated on the behaviour being active; the update lifecycle still flows
|
|
26
|
+
* through `world.update`.
|
|
27
|
+
*/
|
|
28
|
+
export function attachObject3D(world, object, behaviour, nodeId = object.uuid) {
|
|
29
|
+
behaviour._object = object;
|
|
30
|
+
world.attach(nodeId, behaviour);
|
|
31
|
+
if (behaviour.onBeforeRender !== Object3DBehaviour.prototype.onBeforeRender) {
|
|
32
|
+
const previous = object.onBeforeRender;
|
|
33
|
+
object.onBeforeRender = function chainedBeforeRender(...args) {
|
|
34
|
+
previous.apply(this, args);
|
|
35
|
+
if (behaviour.isActive)
|
|
36
|
+
behaviour.onBeforeRender();
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (behaviour.onAfterRender !== Object3DBehaviour.prototype.onAfterRender) {
|
|
40
|
+
const previous = object.onAfterRender;
|
|
41
|
+
object.onAfterRender = function chainedAfterRender(...args) {
|
|
42
|
+
previous.apply(this, args);
|
|
43
|
+
if (behaviour.isActive)
|
|
44
|
+
behaviour.onAfterRender();
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return behaviour;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Bootstraps `world` on mount and dispatches `world.update` every frame at
|
|
51
|
+
* simulation priority. `scaleDt` maps the raw frame delta to the dt behaviours
|
|
52
|
+
* receive — pass the game clock's scaling to keep behaviours on game time, or
|
|
53
|
+
* omit for real-time seconds. Games driving `world.update` themselves from
|
|
54
|
+
* `loop.onTick` should not also render this.
|
|
55
|
+
*/
|
|
56
|
+
export function useBehaviourWorld(world, scaleDt) {
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
world.start();
|
|
59
|
+
}, [world]);
|
|
60
|
+
useFrame((_state, rawDt) => {
|
|
61
|
+
world.update(scaleDt === undefined ? rawDt : scaleDt(rawDt));
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { type MutableRefObject } from "react";
|
|
2
|
-
import type {
|
|
2
|
+
import type { GameCameraConfig } from "@jgengine/core/game/playableGame";
|
|
3
|
+
import type { CameraDirector } from "@jgengine/core/runtime/cameraDirector";
|
|
4
|
+
export { resolveRigKind } from "./rigResolve.js";
|
|
3
5
|
export interface GameCameraRigProps {
|
|
4
6
|
yawRef: MutableRefObject<number>;
|
|
5
7
|
pitchRef: MutableRefObject<number>;
|
|
6
8
|
config?: GameCameraConfig;
|
|
7
9
|
onDragChange?: (dragging: boolean) => void;
|
|
8
10
|
pointerControls?: boolean;
|
|
11
|
+
/** False when the game's own input map already claims WASD, so the RTS rig should stop panning on those keys. */
|
|
12
|
+
panKeysEnabled?: boolean;
|
|
13
|
+
/** Runtime follow/cinematic override (#196.2). While present it wins over the static `config` for any field it reports non-`undefined`/non-`null`; absent (or an all-`undefined` snapshot) is a no-op. */
|
|
14
|
+
director?: CameraDirector;
|
|
9
15
|
}
|
|
10
|
-
export declare function
|
|
11
|
-
export declare function GameCameraRig({ yawRef, pitchRef, config, onDragChange, pointerControls }: GameCameraRigProps): import("react").JSX.Element;
|
|
16
|
+
export declare function GameCameraRig({ yawRef, pitchRef, config, onDragChange, pointerControls, panKeysEnabled, director, }: GameCameraRigProps): import("react").JSX.Element;
|
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useMemo } from "react";
|
|
3
|
-
import { ChaseRig, CinematicRig, LockOnRig, ObserverRig, RtsRig, ShoulderRig, TopDownRig, } from "./cameraRigs.js";
|
|
2
|
+
import { useEffect, useMemo, useReducer } from "react";
|
|
3
|
+
import { ChaseRig, CinematicRig, LockOnRig, ObserverRig, RtsRig, ShoulderRig, SideScrollRig, TopDownRig, } from "./cameraRigs.js";
|
|
4
4
|
import { GameFirstPersonCamera } from "./GameFirstPersonCamera.js";
|
|
5
|
+
import { GameInspectionCamera } from "./GameInspectionCamera.js";
|
|
5
6
|
import { GameOrbitCamera } from "./GameOrbitCamera.js";
|
|
7
|
+
import { resolveDirectedCamera } from "./rigMath.js";
|
|
8
|
+
import { resolveRigKind, turntableAsObserver } from "./rigResolve.js";
|
|
6
9
|
import { CameraShakeContext, createCameraShakeChannel } from "./shakeChannel.js";
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
return config.rig;
|
|
10
|
-
if (config?.perspective === "first")
|
|
11
|
-
return "first";
|
|
12
|
-
return "orbit";
|
|
13
|
-
}
|
|
14
|
-
export function GameCameraRig({ yawRef, pitchRef, config, onDragChange, pointerControls }) {
|
|
10
|
+
export { resolveRigKind } from "./rigResolve.js";
|
|
11
|
+
export function GameCameraRig({ yawRef, pitchRef, config, onDragChange, pointerControls, panKeysEnabled, director, }) {
|
|
15
12
|
const channel = useMemo(() => createCameraShakeChannel(config?.shake?.decayPerSecond), [config?.shake?.decayPerSecond]);
|
|
16
|
-
const
|
|
13
|
+
const [, notifyDirectorChange] = useReducer((count) => count + 1, 0);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (director === undefined)
|
|
16
|
+
return undefined;
|
|
17
|
+
return director.subscribe(notifyDirectorChange);
|
|
18
|
+
}, [director]);
|
|
19
|
+
const directed = resolveDirectedCamera(director === undefined
|
|
20
|
+
? undefined
|
|
21
|
+
: { followEntityId: director.followedEntityId(), cinematic: director.cinematic() }, { followEntityId: config?.followEntityId, cinematic: config?.cinematic });
|
|
22
|
+
const followEntityId = directed.followEntityId;
|
|
17
23
|
const rig = (() => {
|
|
18
|
-
if (
|
|
19
|
-
return _jsx(CinematicRig, { yawRef: yawRef, pitchRef: pitchRef, config: config, followEntityId: followEntityId });
|
|
24
|
+
if (directed.cinematic !== undefined) {
|
|
25
|
+
return (_jsx(CinematicRig, { yawRef: yawRef, pitchRef: pitchRef, config: { ...config, cinematic: directed.cinematic }, followEntityId: followEntityId }));
|
|
20
26
|
}
|
|
21
27
|
const kind = resolveRigKind(config);
|
|
22
28
|
switch (kind) {
|
|
@@ -25,7 +31,7 @@ export function GameCameraRig({ yawRef, pitchRef, config, onDragChange, pointerC
|
|
|
25
31
|
case "topDown":
|
|
26
32
|
return _jsx(TopDownRig, { yawRef: yawRef, pitchRef: pitchRef, config: config, followEntityId: followEntityId });
|
|
27
33
|
case "rts":
|
|
28
|
-
return _jsx(RtsRig, { yawRef: yawRef, pitchRef: pitchRef, config: config, followEntityId: followEntityId });
|
|
34
|
+
return (_jsx(RtsRig, { yawRef: yawRef, pitchRef: pitchRef, config: config, followEntityId: followEntityId, panKeysEnabled: panKeysEnabled }));
|
|
29
35
|
case "shoulder":
|
|
30
36
|
return _jsx(ShoulderRig, { yawRef: yawRef, pitchRef: pitchRef, config: config, followEntityId: followEntityId });
|
|
31
37
|
case "lockOn":
|
|
@@ -34,6 +40,14 @@ export function GameCameraRig({ yawRef, pitchRef, config, onDragChange, pointerC
|
|
|
34
40
|
return _jsx(ChaseRig, { yawRef: yawRef, pitchRef: pitchRef, config: config, followEntityId: followEntityId });
|
|
35
41
|
case "observer":
|
|
36
42
|
return _jsx(ObserverRig, { yawRef: yawRef, pitchRef: pitchRef, config: config, followEntityId: followEntityId });
|
|
43
|
+
case "turntable":
|
|
44
|
+
return (_jsx(ObserverRig, { yawRef: yawRef, pitchRef: pitchRef, config: turntableAsObserver(config), followEntityId: followEntityId }));
|
|
45
|
+
case "sideScroll":
|
|
46
|
+
return _jsx(SideScrollRig, { yawRef: yawRef, pitchRef: pitchRef, config: config, followEntityId: followEntityId });
|
|
47
|
+
case "inspection":
|
|
48
|
+
return _jsx(GameInspectionCamera, { config: config?.inspection });
|
|
49
|
+
case "none":
|
|
50
|
+
return null;
|
|
37
51
|
default:
|
|
38
52
|
return (_jsx(GameOrbitCamera, { yawRef: yawRef, pitchRef: pitchRef, config: config, followEntityId: followEntityId ?? undefined, onCameraFollow: config?.onCameraFollow, onDragChange: onDragChange, pointerControls: pointerControls }));
|
|
39
53
|
}
|
|
@@ -19,6 +19,8 @@ export function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId
|
|
|
19
19
|
const followId = followEntityId ?? userId;
|
|
20
20
|
useEffect(() => {
|
|
21
21
|
const requestLock = () => {
|
|
22
|
+
if (window.matchMedia?.("(pointer: coarse)").matches)
|
|
23
|
+
return;
|
|
22
24
|
if (document.pointerLockElement !== domElement)
|
|
23
25
|
void domElement.requestPointerLock?.();
|
|
24
26
|
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type InspectionCameraConfig } from "./inspectionCameraMath.js";
|
|
2
|
+
export interface GameInspectionCameraProps {
|
|
3
|
+
config?: InspectionCameraConfig;
|
|
4
|
+
}
|
|
5
|
+
/** Model-viewer style rig (#207.7): left-drag orbit, middle/right-drag pan, scroll zoom toward a configurable anchor. Orbits a fixed `target`; never reads player/entity state. */
|
|
6
|
+
export declare function GameInspectionCamera({ config: configPatch }: GameInspectionCameraProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { OrbitControls } from "@react-three/drei";
|
|
3
|
+
import { useThree } from "@react-three/fiber";
|
|
4
|
+
import { useEffect, useRef } from "react";
|
|
5
|
+
import { MOUSE } from "three";
|
|
6
|
+
import { resolveInspectionCameraConfig, resolveInspectionZoomToCursor, seedInspectionCamera, } from "./inspectionCameraMath.js";
|
|
7
|
+
/** Model-viewer style rig (#207.7): left-drag orbit, middle/right-drag pan, scroll zoom toward a configurable anchor. Orbits a fixed `target`; never reads player/entity state. */
|
|
8
|
+
export function GameInspectionCamera({ config: configPatch }) {
|
|
9
|
+
const config = resolveInspectionCameraConfig(configPatch);
|
|
10
|
+
const controlsRef = useRef(null);
|
|
11
|
+
const camera = useThree((state) => state.camera);
|
|
12
|
+
const seededRef = useRef(false);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (seededRef.current)
|
|
15
|
+
return;
|
|
16
|
+
const seeded = seedInspectionCamera(config);
|
|
17
|
+
camera.position.set(seeded.camera.x, seeded.camera.y, seeded.camera.z);
|
|
18
|
+
camera.lookAt(seeded.target.x, seeded.target.y, seeded.target.z);
|
|
19
|
+
controlsRef.current?.target.set(seeded.target.x, seeded.target.y, seeded.target.z);
|
|
20
|
+
controlsRef.current?.update();
|
|
21
|
+
seededRef.current = true;
|
|
22
|
+
}, [camera, config]);
|
|
23
|
+
return (_jsx(OrbitControls, { ref: controlsRef, 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
|
+
}
|
|
@@ -9,7 +9,11 @@ export interface RigProps {
|
|
|
9
9
|
followEntityId?: string | null;
|
|
10
10
|
}
|
|
11
11
|
export declare function TopDownRig(props: RigProps): null;
|
|
12
|
-
|
|
12
|
+
/** Fixed side-on 2.5D follow rig: watches the followed entity from the perpendicular axis, never reading WASD/mouse-look. */
|
|
13
|
+
export declare function SideScrollRig(props: RigProps): null;
|
|
14
|
+
export declare function RtsRig(props: RigProps & {
|
|
15
|
+
panKeysEnabled?: boolean;
|
|
16
|
+
}): null;
|
|
13
17
|
export declare function ShoulderRig(props: RigProps): null;
|
|
14
18
|
export declare function LockOnRig(props: RigProps): null;
|
|
15
19
|
export declare function ChaseRig(props: RigProps): null;
|
|
@@ -4,7 +4,7 @@ import { PerspectiveCamera, Quaternion, Vector3 } from "three";
|
|
|
4
4
|
import { usePlayer } from "@jgengine/react/hooks";
|
|
5
5
|
import { useGameContext } from "@jgengine/react/provider";
|
|
6
6
|
import { ORBIT_CAMERA_FRAME_PRIORITY } from "./orbitCameraMath.js";
|
|
7
|
-
import { chaseDesiredPosition, chaseLookAt, cinematicSample, clamp, lockOnPose, observerPose, resolveChase, resolveObserver, resolveShoulder, resolveTopDown, seatPose, shoulderPose, smoothstep, smoothYaw, speedToFov, springArmStep, topDownPose, } from "./rigMath.js";
|
|
7
|
+
import { chaseDesiredPosition, chaseLookAt, cinematicSample, clamp, lockOnPose, observerPose, resolveChase, resolveObserver, resolveShoulder, resolveSideScroll, resolveSideScrollPose, resolveTopDown, seatPose, shoulderPose, sideScrollFollowBlend, smoothstep, smoothYaw, speedToFov, springArmStep, topDownPose, } from "./rigMath.js";
|
|
8
8
|
import { useCameraShake } from "./shakeChannel.js";
|
|
9
9
|
export const CAMERA_RIG_FRAME_PRIORITY = ORBIT_CAMERA_FRAME_PRIORITY;
|
|
10
10
|
export const CAMERA_POST_FRAME_PRIORITY = ORBIT_CAMERA_FRAME_PRIORITY + 0.5;
|
|
@@ -51,6 +51,7 @@ function useCameraCommit(props, followId) {
|
|
|
51
51
|
const transitionSeconds = props.config?.transitionSeconds ?? 0.6;
|
|
52
52
|
const blendRef = useRef(null);
|
|
53
53
|
const lastPoseRef = useRef(null);
|
|
54
|
+
const farWarnedRef = useRef(false);
|
|
54
55
|
const beginTransition = () => {
|
|
55
56
|
if (transitionSeconds <= 0)
|
|
56
57
|
return;
|
|
@@ -65,6 +66,11 @@ function useCameraCommit(props, followId) {
|
|
|
65
66
|
const commit = (pose, dt) => {
|
|
66
67
|
lastPoseRef.current = pose;
|
|
67
68
|
applyPose(camera, pose);
|
|
69
|
+
const lookDistance = Math.hypot(pose.position.x - pose.lookAt.x, pose.position.y - pose.lookAt.y, pose.position.z - pose.lookAt.z);
|
|
70
|
+
if (!farWarnedRef.current && isPerspective(camera) && lookDistance > camera.far) {
|
|
71
|
+
farWarnedRef.current = true;
|
|
72
|
+
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
|
+
}
|
|
68
74
|
const blend = blendRef.current;
|
|
69
75
|
if (blend !== null) {
|
|
70
76
|
blend.elapsed += dt;
|
|
@@ -141,6 +147,32 @@ export function TopDownRig(props) {
|
|
|
141
147
|
}, CAMERA_RIG_FRAME_PRIORITY);
|
|
142
148
|
return null;
|
|
143
149
|
}
|
|
150
|
+
/** Fixed side-on 2.5D follow rig: watches the followed entity from the perpendicular axis, never reading WASD/mouse-look. */
|
|
151
|
+
export function SideScrollRig(props) {
|
|
152
|
+
const { userId } = usePlayer();
|
|
153
|
+
const ctx = useGameContext();
|
|
154
|
+
const followId = resolveFollowId(props.followEntityId, userId);
|
|
155
|
+
const config = props.config?.sideScroll;
|
|
156
|
+
const resolved = useMemo(() => resolveSideScroll(config), [config]);
|
|
157
|
+
const { camera, commit, beginTransition } = useCameraCommit(props, followId);
|
|
158
|
+
const followRef = useRef(null);
|
|
159
|
+
useEffect(beginTransition, []);
|
|
160
|
+
useFrame((_, dt) => {
|
|
161
|
+
const sample = readFollow(ctx, followId);
|
|
162
|
+
const desired = sample?.pos ?? { x: 0, y: 0, z: 0 };
|
|
163
|
+
const prev = followRef.current ?? desired;
|
|
164
|
+
const blend = sideScrollFollowBlend(resolved.followSmoothing, dt);
|
|
165
|
+
const follow = {
|
|
166
|
+
x: prev.x + (desired.x - prev.x) * blend,
|
|
167
|
+
y: prev.y + (desired.y - prev.y) * blend,
|
|
168
|
+
z: prev.z + (desired.z - prev.z) * blend,
|
|
169
|
+
};
|
|
170
|
+
followRef.current = follow;
|
|
171
|
+
const pose = resolveSideScrollPose(follow, resolved, config?.fov ?? currentFov(camera));
|
|
172
|
+
commit(pose, dt);
|
|
173
|
+
}, CAMERA_RIG_FRAME_PRIORITY);
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
144
176
|
function useHeldKeys(codes) {
|
|
145
177
|
const held = useRef(new Set());
|
|
146
178
|
useEffect(() => {
|
|
@@ -163,6 +195,7 @@ function useHeldKeys(codes) {
|
|
|
163
195
|
return held;
|
|
164
196
|
}
|
|
165
197
|
const RTS_PAN_KEYS = ["KeyW", "KeyA", "KeyS", "KeyD", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "KeyQ", "KeyE"];
|
|
198
|
+
const RTS_WASD_KEYS = new Set(["KeyW", "KeyA", "KeyS", "KeyD"]);
|
|
166
199
|
export function RtsRig(props) {
|
|
167
200
|
const { userId } = usePlayer();
|
|
168
201
|
const ctx = useGameContext();
|
|
@@ -170,7 +203,9 @@ export function RtsRig(props) {
|
|
|
170
203
|
const config = props.config?.rts;
|
|
171
204
|
const resolved = useMemo(() => resolveTopDown(config), [config]);
|
|
172
205
|
const { camera, commit, beginTransition } = useCameraCommit(props, followId);
|
|
173
|
-
const
|
|
206
|
+
const wasdPanEnabled = followId === null && props.panKeysEnabled !== false;
|
|
207
|
+
const heldCodes = useMemo(() => (wasdPanEnabled ? RTS_PAN_KEYS : RTS_PAN_KEYS.filter((code) => !RTS_WASD_KEYS.has(code))), [wasdPanEnabled]);
|
|
208
|
+
const held = useHeldKeys(heldCodes);
|
|
174
209
|
const centerRef = useRef({
|
|
175
210
|
x: config?.start?.x ?? 0,
|
|
176
211
|
y: 0,
|
|
@@ -431,8 +466,8 @@ export function ChaseRig(props) {
|
|
|
431
466
|
}, CAMERA_RIG_FRAME_PRIORITY);
|
|
432
467
|
return null;
|
|
433
468
|
}
|
|
434
|
-
function observerSubject(ctx, config) {
|
|
435
|
-
const bind = config?.bind;
|
|
469
|
+
function observerSubject(ctx, config, followId) {
|
|
470
|
+
const bind = config?.bind ?? (followId !== null ? { kind: "entity", entityId: followId } : undefined);
|
|
436
471
|
if (bind?.kind === "entity") {
|
|
437
472
|
const entity = ctx.scene.entity.get(bind.entityId);
|
|
438
473
|
if (entity !== null) {
|
|
@@ -450,15 +485,17 @@ function observerSubject(ctx, config) {
|
|
|
450
485
|
* kill-cam rig. Distinct from every other rig, which drives from mouse/keys.
|
|
451
486
|
*/
|
|
452
487
|
export function ObserverRig(props) {
|
|
488
|
+
const { userId } = usePlayer();
|
|
453
489
|
const ctx = useGameContext();
|
|
454
490
|
const config = props.config?.observer;
|
|
491
|
+
const followId = resolveFollowId(props.followEntityId, userId);
|
|
455
492
|
const resolved = useMemo(() => resolveObserver(config), [config]);
|
|
456
493
|
const angleRef = useRef(config?.startAngle ?? 0);
|
|
457
|
-
const { camera, commit, beginTransition } = useCameraCommit(props, observerSubject(ctx, config).boundEntityId);
|
|
494
|
+
const { camera, commit, beginTransition } = useCameraCommit(props, observerSubject(ctx, config, followId).boundEntityId);
|
|
458
495
|
useEffect(beginTransition, []);
|
|
459
496
|
useFrame((_, dt) => {
|
|
460
497
|
angleRef.current += resolved.orbitSpeed * dt;
|
|
461
|
-
const { subject } = observerSubject(ctx, config);
|
|
498
|
+
const { subject } = observerSubject(ctx, config, followId);
|
|
462
499
|
const pose = observerPose(subject, angleRef.current, resolved, config?.fov ?? currentFov(camera));
|
|
463
500
|
commit(pose, dt);
|
|
464
501
|
}, CAMERA_RIG_FRAME_PRIORITY);
|
package/dist/camera/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export { GameOrbitCamera, type CameraFollowListener, type GameOrbitCameraProps } from "./GameOrbitCamera.js";
|
|
2
2
|
export { GameFirstPersonCamera, type GameFirstPersonCameraProps } from "./GameFirstPersonCamera.js";
|
|
3
|
+
export { GameInspectionCamera, type GameInspectionCameraProps } from "./GameInspectionCamera.js";
|
|
3
4
|
export { GameCameraRig, resolveRigKind, type GameCameraRigProps } from "./GameCameraRig.js";
|
|
4
|
-
export { CAMERA_POST_FRAME_PRIORITY, CAMERA_RIG_FRAME_PRIORITY, ChaseRig, CinematicRig, LockOnRig, ObserverRig, RtsRig, ShoulderRig, TopDownRig, type RigProps, } from "./cameraRigs.js";
|
|
5
|
+
export { CAMERA_POST_FRAME_PRIORITY, CAMERA_RIG_FRAME_PRIORITY, ChaseRig, CinematicRig, LockOnRig, ObserverRig, RtsRig, ShoulderRig, SideScrollRig, TopDownRig, type RigProps, } from "./cameraRigs.js";
|
|
5
6
|
export { CameraShakeContext, cameraShake, createCameraShakeChannel, defaultCameraShakeChannel, useCameraShake, type CameraShakeChannel, } from "./shakeChannel.js";
|
|
6
|
-
export { addTrauma, angleDelta, blendShoulder, chaseDesiredPosition, chaseLookAt, cinematicSample, clamp, createTrauma, crossfadePose, forwardVector, lerp, lockOnPose, observerPose, resolveChase, resolveObserver, resolveShoulder, resolveTopDown, rightVector, seatPose, shakeOffset, shoulderPose, smoothstep, smoothYaw, speedToFov, springArmStep, stepTrauma, topDownPose, yawTo, type CameraPose, type ResolvedChase, type ResolvedObserver, type ResolvedShoulder, type ResolvedTopDown, type ShakeOffset, type TraumaState, } from "./rigMath.js";
|
|
7
|
+
export { addTrauma, angleDelta, blendShoulder, chaseDesiredPosition, chaseLookAt, cinematicSample, clamp, createTrauma, crossfadePose, forwardVector, lerp, lockOnPose, observerPose, resolveChase, resolveDirectedCamera, resolveObserver, resolveShoulder, resolveSideScroll, resolveSideScrollPose, resolveTopDown, rightVector, rtsPanKeysConflict, seatPose, shakeOffset, shoulderPose, sideScrollFollowBlend, smoothstep, smoothYaw, speedToFov, springArmStep, stepTrauma, topDownPose, yawTo, type CameraPose, type DirectorCameraValues, type ResolvedChase, type ResolvedDirectedCamera, type ResolvedObserver, type ResolvedShoulder, type ResolvedSideScroll, type ResolvedTopDown, type ShakeOffset, type StaticCameraValues, type TraumaState, } from "./rigMath.js";
|
|
7
8
|
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";
|
|
9
|
+
export { resolveInspectionCameraConfig, resolveInspectionZoomToCursor, seedInspectionCamera, type InspectionCameraConfig, type InspectionZoomAnchor, type ResolvedInspectionCameraConfig, } from "./inspectionCameraMath.js";
|
package/dist/camera/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export { GameOrbitCamera } from "./GameOrbitCamera.js";
|
|
2
2
|
export { GameFirstPersonCamera } from "./GameFirstPersonCamera.js";
|
|
3
|
+
export { GameInspectionCamera } from "./GameInspectionCamera.js";
|
|
3
4
|
export { GameCameraRig, resolveRigKind } from "./GameCameraRig.js";
|
|
4
|
-
export { CAMERA_POST_FRAME_PRIORITY, CAMERA_RIG_FRAME_PRIORITY, ChaseRig, CinematicRig, LockOnRig, ObserverRig, RtsRig, ShoulderRig, TopDownRig, } from "./cameraRigs.js";
|
|
5
|
+
export { CAMERA_POST_FRAME_PRIORITY, CAMERA_RIG_FRAME_PRIORITY, ChaseRig, CinematicRig, LockOnRig, ObserverRig, RtsRig, ShoulderRig, SideScrollRig, TopDownRig, } from "./cameraRigs.js";
|
|
5
6
|
export { CameraShakeContext, cameraShake, createCameraShakeChannel, defaultCameraShakeChannel, useCameraShake, } from "./shakeChannel.js";
|
|
6
|
-
export { addTrauma, angleDelta, blendShoulder, chaseDesiredPosition, chaseLookAt, cinematicSample, clamp, createTrauma, crossfadePose, forwardVector, lerp, lockOnPose, observerPose, resolveChase, resolveObserver, resolveShoulder, resolveTopDown, rightVector, seatPose, shakeOffset, shoulderPose, smoothstep, smoothYaw, speedToFov, springArmStep, stepTrauma, topDownPose, yawTo, } from "./rigMath.js";
|
|
7
|
+
export { addTrauma, angleDelta, blendShoulder, chaseDesiredPosition, chaseLookAt, cinematicSample, clamp, createTrauma, crossfadePose, forwardVector, lerp, lockOnPose, observerPose, resolveChase, resolveDirectedCamera, resolveObserver, resolveShoulder, resolveSideScroll, resolveSideScrollPose, resolveTopDown, rightVector, rtsPanKeysConflict, seatPose, shakeOffset, shoulderPose, sideScrollFollowBlend, smoothstep, smoothYaw, speedToFov, springArmStep, stepTrauma, topDownPose, yawTo, } from "./rigMath.js";
|
|
7
8
|
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";
|
|
9
|
+
export { resolveInspectionCameraConfig, resolveInspectionZoomToCursor, seedInspectionCamera, } from "./inspectionCameraMath.js";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { InspectionCameraConfig, InspectionZoomAnchor } from "@jgengine/core/game/playableGame";
|
|
2
|
+
import type { Vec3 } from "./orbitCameraMath.js";
|
|
3
|
+
export type { InspectionCameraConfig, InspectionZoomAnchor };
|
|
4
|
+
export interface ResolvedInspectionCameraConfig {
|
|
5
|
+
anchor: InspectionZoomAnchor;
|
|
6
|
+
target: Vec3;
|
|
7
|
+
initialDistance: number;
|
|
8
|
+
initialPosition: Vec3 | null;
|
|
9
|
+
minDistance: number;
|
|
10
|
+
maxDistance: number;
|
|
11
|
+
minPolarAngle: number;
|
|
12
|
+
maxPolarAngle: number;
|
|
13
|
+
pan: boolean;
|
|
14
|
+
rotateSpeed: number;
|
|
15
|
+
zoomSpeed: number;
|
|
16
|
+
dampingFactor: number;
|
|
17
|
+
}
|
|
18
|
+
export declare function resolveInspectionCameraConfig(config?: InspectionCameraConfig): ResolvedInspectionCameraConfig;
|
|
19
|
+
/** Seeds the camera/target world position before OrbitControls mounts. Falls back to `initialDistance` behind `target` on the -Z axis, raised by 40% of that distance, when `initialPosition` is unset. */
|
|
20
|
+
export declare function seedInspectionCamera(config: ResolvedInspectionCameraConfig): {
|
|
21
|
+
camera: Vec3;
|
|
22
|
+
target: Vec3;
|
|
23
|
+
};
|
|
24
|
+
/** Maps the anchor mode onto three-stdlib OrbitControls' native `zoomToCursor` flag. */
|
|
25
|
+
export declare function resolveInspectionZoomToCursor(anchor: InspectionZoomAnchor): boolean;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export function resolveInspectionCameraConfig(config) {
|
|
2
|
+
return {
|
|
3
|
+
anchor: config?.anchor ?? "target",
|
|
4
|
+
target: {
|
|
5
|
+
x: config?.target?.x ?? 0,
|
|
6
|
+
y: config?.target?.y ?? 0,
|
|
7
|
+
z: config?.target?.z ?? 0,
|
|
8
|
+
},
|
|
9
|
+
initialDistance: config?.initialDistance ?? 6,
|
|
10
|
+
initialPosition: config?.initialPosition === undefined
|
|
11
|
+
? null
|
|
12
|
+
: {
|
|
13
|
+
x: config.initialPosition.x ?? 0,
|
|
14
|
+
y: config.initialPosition.y ?? 0,
|
|
15
|
+
z: config.initialPosition.z ?? 0,
|
|
16
|
+
},
|
|
17
|
+
minDistance: config?.minDistance ?? 2,
|
|
18
|
+
maxDistance: config?.maxDistance ?? 20,
|
|
19
|
+
minPolarAngle: config?.minPolarAngle ?? 0,
|
|
20
|
+
maxPolarAngle: config?.maxPolarAngle ?? Math.PI,
|
|
21
|
+
pan: config?.pan ?? true,
|
|
22
|
+
rotateSpeed: config?.rotateSpeed ?? 0.6,
|
|
23
|
+
zoomSpeed: config?.zoomSpeed ?? 0.6,
|
|
24
|
+
dampingFactor: config?.dampingFactor ?? 0.08,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** Seeds the camera/target world position before OrbitControls mounts. Falls back to `initialDistance` behind `target` on the -Z axis, raised by 40% of that distance, when `initialPosition` is unset. */
|
|
28
|
+
export function seedInspectionCamera(config) {
|
|
29
|
+
if (config.initialPosition !== null) {
|
|
30
|
+
return { camera: config.initialPosition, target: config.target };
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
camera: {
|
|
34
|
+
x: config.target.x,
|
|
35
|
+
y: config.target.y + config.initialDistance * 0.4,
|
|
36
|
+
z: config.target.z - config.initialDistance,
|
|
37
|
+
},
|
|
38
|
+
target: config.target,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Maps the anchor mode onto three-stdlib OrbitControls' native `zoomToCursor` flag. */
|
|
42
|
+
export function resolveInspectionZoomToCursor(anchor) {
|
|
43
|
+
return anchor === "cursor";
|
|
44
|
+
}
|
package/dist/camera/rigMath.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import type { CameraKeyframe, ChaseCameraConfig, LockOnCameraConfig, ObserverCameraConfig, ShoulderCameraConfig, TopDownCameraConfig } from "@jgengine/core/game/playableGame";
|
|
1
|
+
import type { CameraKeyframe, ChaseCameraConfig, CinematicCameraConfig, LockOnCameraConfig, ObserverCameraConfig, ShoulderCameraConfig, SideScrollCameraConfig, TopDownCameraConfig } from "@jgengine/core/game/playableGame";
|
|
2
|
+
import type { ActionCodesMap } from "@jgengine/core/input/actionBindings";
|
|
2
3
|
import { type Vec3 } from "./orbitCameraMath.js";
|
|
3
4
|
export interface CameraPose {
|
|
4
5
|
position: Vec3;
|
|
5
6
|
lookAt: Vec3;
|
|
6
7
|
fov: number;
|
|
7
8
|
}
|
|
9
|
+
export declare function rtsPanKeysConflict(input: ActionCodesMap | undefined): boolean;
|
|
8
10
|
export declare function clamp(value: number, min: number, max: number): number;
|
|
9
11
|
export declare function lerp(from: number, to: number, blend: number): number;
|
|
10
12
|
export declare function smoothstep(t: number): number;
|
|
@@ -27,6 +29,22 @@ export declare function resolveTopDown(config: TopDownCameraConfig | undefined):
|
|
|
27
29
|
export declare function topDownPose(follow: Vec3, resolved: ResolvedTopDown, fov: number): CameraPose;
|
|
28
30
|
/** Exponential spring-arm approach toward a desired point (frame-rate independent). */
|
|
29
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;
|
|
30
48
|
/** Speed→FOV curve: FOV climbs from base to max as speed rises to `speedForMax`. */
|
|
31
49
|
export declare function speedToFov(speed: number, curve: {
|
|
32
50
|
base?: number;
|
|
@@ -117,6 +135,27 @@ export declare function shakeOffset(state: TraumaState, config: {
|
|
|
117
135
|
} | undefined): ShakeOffset;
|
|
118
136
|
/** Linear cross-fade between two full camera poses (position, lookAt, fov). */
|
|
119
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;
|
|
120
159
|
export interface CinematicSample {
|
|
121
160
|
pose: CameraPose;
|
|
122
161
|
done: boolean;
|
package/dist/camera/rigMath.js
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import { lerpVec3, smoothBlend } from "./orbitCameraMath.js";
|
|
2
|
+
const RTS_WASD_CODES = ["KeyW", "KeyA", "KeyS", "KeyD"];
|
|
3
|
+
function actionCodeList(codes) {
|
|
4
|
+
if (Array.isArray(codes))
|
|
5
|
+
return codes;
|
|
6
|
+
const modes = codes;
|
|
7
|
+
return [...(modes.hold ?? []), ...(modes.toggle ?? [])];
|
|
8
|
+
}
|
|
9
|
+
export function rtsPanKeysConflict(input) {
|
|
10
|
+
if (input === undefined)
|
|
11
|
+
return false;
|
|
12
|
+
return Object.values(input).some((codes) => actionCodeList(codes).some((code) => RTS_WASD_CODES.includes(code)));
|
|
13
|
+
}
|
|
2
14
|
export function clamp(value, min, max) {
|
|
3
15
|
return value < min ? min : value > max ? max : value;
|
|
4
16
|
}
|
|
@@ -56,6 +68,36 @@ export function topDownPose(follow, resolved, fov) {
|
|
|
56
68
|
export function springArmStep(current, desired, damping, dt) {
|
|
57
69
|
return lerpVec3(current, desired, smoothBlend(dt, damping));
|
|
58
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
|
+
}
|
|
59
101
|
/** Speed→FOV curve: FOV climbs from base to max as speed rises to `speedForMax`. */
|
|
60
102
|
export function speedToFov(speed, curve) {
|
|
61
103
|
const base = curve?.base ?? 55;
|
|
@@ -268,6 +310,19 @@ export function crossfadePose(from, to, t) {
|
|
|
268
310
|
fov: lerp(from.fov, to.fov, blend),
|
|
269
311
|
};
|
|
270
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
|
+
}
|
|
271
326
|
/**
|
|
272
327
|
* Sample a keyframe path at `elapsed` seconds. Each keyframe's `duration` is the
|
|
273
328
|
* travel time from the previous keyframe into it; segment easing is per the
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CameraRigKind, GameCameraConfig } from "@jgengine/core/game/playableGame";
|
|
2
|
+
/**
|
|
3
|
+
* Resolves which rig mounts from a `GameCameraConfig`. Precedence, most to least
|
|
4
|
+
* specific: an explicit `rig` field always wins; then `perspective: "first"`
|
|
5
|
+
* (the historical shorthand for `rig: "orbit" | "first"`); then the mere presence
|
|
6
|
+
* of a rig's own config block selects that rig, checked in the fixed order below
|
|
7
|
+
* (#207.8) so a config carrying more than one block resolves deterministically
|
|
8
|
+
* instead of depending on object key order. Set `rig` explicitly to break a tie.
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveRigKind(config: GameCameraConfig | undefined): CameraRigKind;
|
|
11
|
+
/**
|
|
12
|
+
* The turntable rig is a flat facade over the observer's point-orbit mode: map
|
|
13
|
+
* its `target`/`distance`/… onto an observer block so ObserverRig runs unchanged.
|
|
14
|
+
*/
|
|
15
|
+
export declare function turntableAsObserver(config: GameCameraConfig | undefined): GameCameraConfig;
|