@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,12 @@
|
|
|
1
|
+
import type { AudioEngine } from "./audioEngine.js";
|
|
2
|
+
export declare function AudioListener({ engine }: {
|
|
3
|
+
engine: AudioEngine;
|
|
4
|
+
}): null;
|
|
5
|
+
export declare function EntityAudioEmitters({ engine, entitySounds, }: {
|
|
6
|
+
engine: AudioEngine;
|
|
7
|
+
entitySounds: Record<string, string> | undefined;
|
|
8
|
+
}): null;
|
|
9
|
+
export declare function ObjectAudioEmitters({ engine, objectSounds, }: {
|
|
10
|
+
engine: AudioEngine;
|
|
11
|
+
objectSounds: Record<string, string> | undefined;
|
|
12
|
+
}): null;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useFrame, useThree } from "@react-three/fiber";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { useSceneEntities, useSceneObjects } from "@jgengine/react/hooks";
|
|
4
|
+
export function AudioListener({ engine }) {
|
|
5
|
+
const camera = useThree((state) => state.camera);
|
|
6
|
+
useFrame(() => {
|
|
7
|
+
engine.setListenerPose({ x: camera.position.x, y: camera.position.y, z: camera.position.z });
|
|
8
|
+
});
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
function useTrackedEmitters(engine, soundByKey, entries) {
|
|
12
|
+
const handles = useRef(new Map());
|
|
13
|
+
useEffect(() => () => {
|
|
14
|
+
for (const handle of handles.current.values())
|
|
15
|
+
handle.stop();
|
|
16
|
+
handles.current.clear();
|
|
17
|
+
}, [engine]);
|
|
18
|
+
useFrame(() => {
|
|
19
|
+
if (soundByKey === undefined)
|
|
20
|
+
return;
|
|
21
|
+
const live = new Set();
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
const soundId = soundByKey[entry.key];
|
|
24
|
+
if (soundId === undefined)
|
|
25
|
+
continue;
|
|
26
|
+
live.add(entry.id);
|
|
27
|
+
let handle = handles.current.get(entry.id);
|
|
28
|
+
if (handle === undefined) {
|
|
29
|
+
handle = engine.playLoop(soundId, { x: entry.position[0], y: entry.position[1], z: entry.position[2] }) ?? undefined;
|
|
30
|
+
if (handle !== undefined)
|
|
31
|
+
handles.current.set(entry.id, handle);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
handle.setPosition({ x: entry.position[0], y: entry.position[1], z: entry.position[2] });
|
|
35
|
+
}
|
|
36
|
+
for (const [id, handle] of handles.current) {
|
|
37
|
+
if (!live.has(id)) {
|
|
38
|
+
handle.stop();
|
|
39
|
+
handles.current.delete(id);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
export function EntityAudioEmitters({ engine, entitySounds, }) {
|
|
45
|
+
const entities = useSceneEntities();
|
|
46
|
+
useTrackedEmitters(engine, entitySounds, entities.map((entity) => ({ id: entity.id, key: entity.name, position: entity.position })));
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
export function ObjectAudioEmitters({ engine, objectSounds, }) {
|
|
50
|
+
const objects = useSceneObjects();
|
|
51
|
+
useTrackedEmitters(engine, objectSounds, objects.map((object) => ({ id: object.instanceId, key: object.catalogId, position: object.position })));
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type AudioBusDef, type SoundDef } from "@jgengine/core/audio/audioFalloff";
|
|
2
|
+
export interface Vec3 {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
z: number;
|
|
6
|
+
}
|
|
7
|
+
export interface AudioSceneConfig {
|
|
8
|
+
sounds?: Record<string, SoundDef>;
|
|
9
|
+
buses?: Record<string, AudioBusDef>;
|
|
10
|
+
}
|
|
11
|
+
export interface AudioEmitterHandle {
|
|
12
|
+
setPosition(position: Vec3): void;
|
|
13
|
+
stop(): void;
|
|
14
|
+
}
|
|
15
|
+
export interface AudioEngine {
|
|
16
|
+
setListenerPose(position: Vec3): void;
|
|
17
|
+
playOneShot(soundId: string, position?: Vec3): void;
|
|
18
|
+
playLoop(soundId: string, position?: Vec3): AudioEmitterHandle | null;
|
|
19
|
+
setBusGain(busId: string, gain: number): void;
|
|
20
|
+
resume(): void;
|
|
21
|
+
dispose(): void;
|
|
22
|
+
}
|
|
23
|
+
export declare function createAudioEngine(config?: AudioSceneConfig): AudioEngine;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { distance3, resolveEmitterGain } from "@jgengine/core/audio/audioFalloff";
|
|
2
|
+
function createNoopEngine() {
|
|
3
|
+
return {
|
|
4
|
+
setListenerPose: () => undefined,
|
|
5
|
+
playOneShot: () => undefined,
|
|
6
|
+
playLoop: () => null,
|
|
7
|
+
setBusGain: () => undefined,
|
|
8
|
+
resume: () => undefined,
|
|
9
|
+
dispose: () => undefined,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function resolveAudioContextCtor() {
|
|
13
|
+
if (typeof window === "undefined")
|
|
14
|
+
return undefined;
|
|
15
|
+
return (window.AudioContext ??
|
|
16
|
+
window.webkitAudioContext);
|
|
17
|
+
}
|
|
18
|
+
export function createAudioEngine(config = {}) {
|
|
19
|
+
const sounds = config.sounds ?? {};
|
|
20
|
+
const busDefs = config.buses ?? {};
|
|
21
|
+
const AudioContextCtor = resolveAudioContextCtor();
|
|
22
|
+
if (AudioContextCtor === undefined)
|
|
23
|
+
return createNoopEngine();
|
|
24
|
+
let context;
|
|
25
|
+
let masterGain;
|
|
26
|
+
try {
|
|
27
|
+
context = new AudioContextCtor();
|
|
28
|
+
masterGain = context.createGain();
|
|
29
|
+
masterGain.connect(context.destination);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return createNoopEngine();
|
|
33
|
+
}
|
|
34
|
+
const busGains = new Map();
|
|
35
|
+
function busGainNode(busId) {
|
|
36
|
+
let node = busGains.get(busId);
|
|
37
|
+
if (node === undefined) {
|
|
38
|
+
node = context.createGain();
|
|
39
|
+
node.gain.value = busDefs[busId]?.gain ?? 1;
|
|
40
|
+
node.connect(masterGain);
|
|
41
|
+
busGains.set(busId, node);
|
|
42
|
+
}
|
|
43
|
+
return node;
|
|
44
|
+
}
|
|
45
|
+
const bufferCache = new Map();
|
|
46
|
+
function loadBuffer(url) {
|
|
47
|
+
let pending = bufferCache.get(url);
|
|
48
|
+
if (pending === undefined) {
|
|
49
|
+
pending = fetch(url)
|
|
50
|
+
.then((response) => response.arrayBuffer())
|
|
51
|
+
.then((data) => context.decodeAudioData(data))
|
|
52
|
+
.catch(() => null);
|
|
53
|
+
bufferCache.set(url, pending);
|
|
54
|
+
}
|
|
55
|
+
return pending;
|
|
56
|
+
}
|
|
57
|
+
let listenerPosition = { x: 0, y: 0, z: 0 };
|
|
58
|
+
function playInternal(soundId, position, loop) {
|
|
59
|
+
const sound = sounds[soundId];
|
|
60
|
+
if (sound === undefined)
|
|
61
|
+
return null;
|
|
62
|
+
const bus = busGainNode(sound.bus);
|
|
63
|
+
let currentPosition = position ?? listenerPosition;
|
|
64
|
+
let gainNode = null;
|
|
65
|
+
let stopped = false;
|
|
66
|
+
void loadBuffer(sound.url).then((buffer) => {
|
|
67
|
+
if (stopped || buffer === null)
|
|
68
|
+
return;
|
|
69
|
+
const sourceNode = context.createBufferSource();
|
|
70
|
+
sourceNode.buffer = buffer;
|
|
71
|
+
sourceNode.loop = loop || (sound.loop ?? false);
|
|
72
|
+
gainNode = context.createGain();
|
|
73
|
+
gainNode.gain.value = resolveEmitterGain(distance3(currentPosition, listenerPosition), sound, 1);
|
|
74
|
+
sourceNode.connect(gainNode);
|
|
75
|
+
gainNode.connect(bus);
|
|
76
|
+
sourceNode.start();
|
|
77
|
+
});
|
|
78
|
+
return {
|
|
79
|
+
setPosition(next) {
|
|
80
|
+
currentPosition = next;
|
|
81
|
+
if (gainNode !== null) {
|
|
82
|
+
gainNode.gain.value = resolveEmitterGain(distance3(currentPosition, listenerPosition), sound, 1);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
stop() {
|
|
86
|
+
stopped = true;
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
setListenerPose(position) {
|
|
92
|
+
listenerPosition = position;
|
|
93
|
+
},
|
|
94
|
+
playOneShot(soundId, position) {
|
|
95
|
+
playInternal(soundId, position, false);
|
|
96
|
+
},
|
|
97
|
+
playLoop(soundId, position) {
|
|
98
|
+
return playInternal(soundId, position, true);
|
|
99
|
+
},
|
|
100
|
+
setBusGain(busId, gain) {
|
|
101
|
+
busGainNode(busId).gain.value = gain;
|
|
102
|
+
},
|
|
103
|
+
resume() {
|
|
104
|
+
void context.resume().catch(() => undefined);
|
|
105
|
+
},
|
|
106
|
+
dispose() {
|
|
107
|
+
void context.close().catch(() => undefined);
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type MutableRefObject } from "react";
|
|
2
|
+
import type { GameCameraConfig } from "@jgengine/core/game/playableGame";
|
|
3
|
+
import type { CameraDirector } from "@jgengine/core/runtime/cameraDirector";
|
|
4
|
+
export { resolveRigKind } from "./rigResolve.js";
|
|
5
|
+
export interface GameCameraRigProps {
|
|
6
|
+
yawRef: MutableRefObject<number>;
|
|
7
|
+
pitchRef: MutableRefObject<number>;
|
|
8
|
+
config?: GameCameraConfig;
|
|
9
|
+
onDragChange?: (dragging: boolean) => void;
|
|
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;
|
|
15
|
+
}
|
|
16
|
+
export declare function GameCameraRig({ yawRef, pitchRef, config, onDragChange, pointerControls, panKeysEnabled, director, }: GameCameraRigProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useReducer } from "react";
|
|
3
|
+
import { ChaseRig, CinematicRig, LockOnRig, ObserverRig, RtsRig, ShoulderRig, SideScrollRig, TopDownRig, } from "./cameraRigs.js";
|
|
4
|
+
import { GameFirstPersonCamera } from "./GameFirstPersonCamera.js";
|
|
5
|
+
import { GameInspectionCamera } from "./GameInspectionCamera.js";
|
|
6
|
+
import { GameOrbitCamera } from "./GameOrbitCamera.js";
|
|
7
|
+
import { resolveDirectedCamera } from "./rigMath.js";
|
|
8
|
+
import { resolveRigKind, turntableAsObserver } from "./rigResolve.js";
|
|
9
|
+
import { CameraShakeContext, createCameraShakeChannel } from "./shakeChannel.js";
|
|
10
|
+
export { resolveRigKind } from "./rigResolve.js";
|
|
11
|
+
export function GameCameraRig({ yawRef, pitchRef, config, onDragChange, pointerControls, panKeysEnabled, director, }) {
|
|
12
|
+
const channel = useMemo(() => createCameraShakeChannel(config?.shake?.decayPerSecond), [config?.shake?.decayPerSecond]);
|
|
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;
|
|
23
|
+
const rig = (() => {
|
|
24
|
+
if (directed.cinematic !== undefined) {
|
|
25
|
+
return (_jsx(CinematicRig, { yawRef: yawRef, pitchRef: pitchRef, config: { ...config, cinematic: directed.cinematic }, followEntityId: followEntityId }));
|
|
26
|
+
}
|
|
27
|
+
const kind = resolveRigKind(config);
|
|
28
|
+
switch (kind) {
|
|
29
|
+
case "first":
|
|
30
|
+
return (_jsx(GameFirstPersonCamera, { yawRef: yawRef, pitchRef: pitchRef, config: config?.firstPerson, followEntityId: followEntityId ?? undefined }));
|
|
31
|
+
case "topDown":
|
|
32
|
+
return _jsx(TopDownRig, { yawRef: yawRef, pitchRef: pitchRef, config: config, followEntityId: followEntityId });
|
|
33
|
+
case "rts":
|
|
34
|
+
return (_jsx(RtsRig, { yawRef: yawRef, pitchRef: pitchRef, config: config, followEntityId: followEntityId, panKeysEnabled: panKeysEnabled }));
|
|
35
|
+
case "shoulder":
|
|
36
|
+
return _jsx(ShoulderRig, { yawRef: yawRef, pitchRef: pitchRef, config: config, followEntityId: followEntityId });
|
|
37
|
+
case "lockOn":
|
|
38
|
+
return _jsx(LockOnRig, { yawRef: yawRef, pitchRef: pitchRef, config: config, followEntityId: followEntityId });
|
|
39
|
+
case "chase":
|
|
40
|
+
return _jsx(ChaseRig, { yawRef: yawRef, pitchRef: pitchRef, config: config, followEntityId: followEntityId });
|
|
41
|
+
case "observer":
|
|
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;
|
|
51
|
+
default:
|
|
52
|
+
return (_jsx(GameOrbitCamera, { yawRef: yawRef, pitchRef: pitchRef, config: config, followEntityId: followEntityId ?? undefined, onCameraFollow: config?.onCameraFollow, onDragChange: onDragChange, pointerControls: pointerControls }));
|
|
53
|
+
}
|
|
54
|
+
})();
|
|
55
|
+
return _jsx(CameraShakeContext.Provider, { value: channel, children: rig });
|
|
56
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -13,7 +13,9 @@ export interface GameOrbitCameraProps {
|
|
|
13
13
|
resolveFollowTarget?: (entity: SceneEntity) => Vec3;
|
|
14
14
|
onDragChange?: (dragging: boolean) => void;
|
|
15
15
|
onCameraFollow?: CameraFollowListener;
|
|
16
|
+
/** Free the left mouse button for pointer verbs (marquee / click-to-move); orbit moves to the middle button. */
|
|
17
|
+
pointerControls?: boolean;
|
|
16
18
|
}
|
|
17
|
-
export declare function GameOrbitCamera({ yawRef, pitchRef, config: configPatch, followEntityId, resolveFollowTarget, onDragChange, onCameraFollow, }: GameOrbitCameraProps): import("react").JSX.Element;
|
|
19
|
+
export declare function GameOrbitCamera({ yawRef, pitchRef, config: configPatch, followEntityId, resolveFollowTarget, onDragChange, onCameraFollow, pointerControls, }: GameOrbitCameraProps): import("react").JSX.Element;
|
|
18
20
|
/** Seed orbit target before controls mount (demo spawn at origin). */
|
|
19
21
|
export declare function seedOrbitCameraTarget(camera: Camera, target: Vector3, distance: number, height: number): void;
|
|
@@ -6,7 +6,7 @@ import { MOUSE, Vector3 } from "three";
|
|
|
6
6
|
import { useGameContext } from "@jgengine/react/provider";
|
|
7
7
|
import { usePlayer } from "@jgengine/react/hooks";
|
|
8
8
|
import { cameraLookPitch, ORBIT_CAMERA_FRAME_PRIORITY, orbitFollowStep, orbitYawFromCamera, resolveFollowTargetFromPosition, resolveOrbitCameraConfig, seedOrbitFollowState, } from "./orbitCameraMath.js";
|
|
9
|
-
export function GameOrbitCamera({ yawRef, pitchRef, config: configPatch, followEntityId, resolveFollowTarget, onDragChange, onCameraFollow, }) {
|
|
9
|
+
export function GameOrbitCamera({ yawRef, pitchRef, config: configPatch, followEntityId, resolveFollowTarget, onDragChange, onCameraFollow, pointerControls = false, }) {
|
|
10
10
|
const config = resolveOrbitCameraConfig(configPatch);
|
|
11
11
|
const controlsRef = useRef(null);
|
|
12
12
|
const runtimeRef = useRef(null);
|
|
@@ -71,7 +71,9 @@ export function GameOrbitCamera({ yawRef, pitchRef, config: configPatch, followE
|
|
|
71
71
|
distance: runtimeRef.current.lockedDistance ?? stepped.distance,
|
|
72
72
|
});
|
|
73
73
|
}, ORBIT_CAMERA_FRAME_PRIORITY);
|
|
74
|
-
return (_jsx(OrbitControls, { ref: controlsRef, enableDamping: true, dampingFactor: config.dampingFactor, rotateSpeed: config.rotateSpeed, zoomSpeed: config.zoomSpeed, enablePan: false, enableRotate: true, enableZoom: true, minDistance: config.minDistance, maxDistance: config.maxDistance, maxPolarAngle: config.maxPolarAngle, minPolarAngle: config.minPolarAngle, screenSpacePanning: false, mouseButtons:
|
|
74
|
+
return (_jsx(OrbitControls, { ref: controlsRef, enableDamping: true, dampingFactor: config.dampingFactor, rotateSpeed: config.rotateSpeed, zoomSpeed: config.zoomSpeed, enablePan: false, enableRotate: true, enableZoom: true, minDistance: config.minDistance, maxDistance: config.maxDistance, maxPolarAngle: config.maxPolarAngle, minPolarAngle: config.minPolarAngle, screenSpacePanning: false, mouseButtons: pointerControls
|
|
75
|
+
? { LEFT: undefined, MIDDLE: MOUSE.ROTATE, RIGHT: undefined }
|
|
76
|
+
: { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: undefined }, onStart: () => {
|
|
75
77
|
draggingRef.current = true;
|
|
76
78
|
onDragChange?.(true);
|
|
77
79
|
}, onEnd: () => {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type MutableRefObject } from "react";
|
|
2
|
+
import type { GameCameraConfig } from "@jgengine/core/game/playableGame";
|
|
3
|
+
export declare const CAMERA_RIG_FRAME_PRIORITY = -1;
|
|
4
|
+
export declare const CAMERA_POST_FRAME_PRIORITY: number;
|
|
5
|
+
export interface RigProps {
|
|
6
|
+
yawRef: MutableRefObject<number>;
|
|
7
|
+
pitchRef: MutableRefObject<number>;
|
|
8
|
+
config?: GameCameraConfig;
|
|
9
|
+
followEntityId?: string | null;
|
|
10
|
+
}
|
|
11
|
+
export declare function TopDownRig(props: RigProps): null;
|
|
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;
|
|
17
|
+
export declare function ShoulderRig(props: RigProps): null;
|
|
18
|
+
export declare function LockOnRig(props: RigProps): null;
|
|
19
|
+
export declare function ChaseRig(props: RigProps): null;
|
|
20
|
+
/**
|
|
21
|
+
* Detached spectator/photo cam (#120): binds to any entity or fixed point and
|
|
22
|
+
* auto-orbits it, reading no player input at all — the van CCTV / photo-mode /
|
|
23
|
+
* kill-cam rig. Distinct from every other rig, which drives from mouse/keys.
|
|
24
|
+
*/
|
|
25
|
+
export declare function ObserverRig(props: RigProps): null;
|
|
26
|
+
export declare function CinematicRig(props: RigProps): null;
|