@jgengine/shell 0.18.1 → 0.21.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 +504 -7
- package/dist/GameHost.d.ts +12 -1
- package/dist/GameHost.js +14 -1
- package/dist/GamePlayerShell.js +132 -17
- package/dist/Shell3dPresentation.d.ts +5 -11
- package/dist/Shell3dPresentation.js +73 -11
- package/dist/ShellChrome.d.ts +2 -1
- package/dist/ShellChrome.js +22 -6
- package/dist/ShellHudPresentation.d.ts +20 -2
- package/dist/ShellHudPresentation.js +41 -6
- package/dist/audio/AudioComponents.js +6 -2
- package/dist/audio/audioEngine.d.ts +24 -0
- package/dist/audio/audioEngine.js +231 -83
- package/dist/audio/audioWire.d.ts +3 -0
- package/dist/audio/audioWire.js +9 -3
- package/dist/audio/loopParams.d.ts +39 -0
- package/dist/audio/loopParams.js +43 -0
- package/dist/audio/synthEngine.d.ts +7 -1
- package/dist/audio/synthEngine.js +103 -38
- package/dist/camera/GameCameraRig.js +3 -2
- package/dist/camera/GameFirstPersonCamera.d.ts +6 -4
- package/dist/camera/GameFirstPersonCamera.js +68 -27
- package/dist/camera/Viewports.d.ts +34 -0
- package/dist/camera/Viewports.js +123 -0
- package/dist/camera/cameraRigs.js +76 -62
- package/dist/camera/chaseHeading.d.ts +9 -0
- package/dist/camera/chaseHeading.js +10 -0
- package/dist/camera/customMuzzle.d.ts +14 -0
- package/dist/camera/customMuzzle.js +68 -0
- package/dist/camera/index.d.ts +1 -0
- package/dist/camera/index.js +1 -0
- package/dist/camera/orbitCameraMath.js +2 -2
- package/dist/camera/rigMath.d.ts +82 -4
- package/dist/camera/rigMath.js +133 -15
- package/dist/camera/rigResolve.d.ts +3 -0
- package/dist/camera/rigResolve.js +11 -0
- package/dist/combat/ProjectileModels.d.ts +31 -0
- package/dist/combat/ProjectileModels.js +70 -0
- package/dist/commandSink.d.ts +1 -2
- package/dist/commandSink.js +3 -2
- package/dist/defineGame.d.ts +19 -3
- package/dist/defineGame.js +39 -7
- package/dist/devtools/DevtoolsOverlay.d.ts +2 -69
- package/dist/devtools/DevtoolsOverlay.js +15 -167
- package/dist/devtools/DevtoolsRuntime.d.ts +7 -0
- package/dist/devtools/DevtoolsRuntime.js +61 -0
- package/dist/devtools/agentBridge.d.ts +6 -0
- package/dist/devtools/agentBridge.js +16 -2
- package/dist/devtools/devtoolsReports.d.ts +68 -0
- package/dist/devtools/devtoolsReports.js +100 -0
- package/dist/devtools/latencyInstrumentation.d.ts +3 -0
- package/dist/devtools/latencyInstrumentation.js +11 -0
- package/dist/devtools/playtestProbe.d.ts +4 -0
- package/dist/devtools/playtestProbe.js +22 -0
- package/dist/diagnostics/FpsCounter.d.ts +17 -0
- package/dist/diagnostics/FpsCounter.js +32 -0
- package/dist/drivers/FrameDriver.js +35 -20
- package/dist/drivers/FrameRateLimiter.d.ts +9 -0
- package/dist/drivers/FrameRateLimiter.js +39 -0
- package/dist/drivers/HudOnlyDriver.d.ts +6 -1
- package/dist/drivers/HudOnlyDriver.js +32 -14
- package/dist/environment/DayNightSky.js +5 -5
- package/dist/environment/Daylight.d.ts +35 -1
- package/dist/environment/Daylight.js +98 -28
- package/dist/environment/EnvironmentScene.js +11 -10
- package/dist/environment/VolumetricClouds.js +20 -2
- package/dist/environment/daylightCycle.d.ts +20 -0
- package/dist/environment/daylightCycle.js +59 -7
- package/dist/environment/sunShadowMath.d.ts +45 -0
- package/dist/environment/sunShadowMath.js +50 -0
- package/dist/input/gamepadPoll.d.ts +47 -0
- package/dist/input/gamepadPoll.js +142 -0
- package/dist/input/gamepadSource.d.ts +11 -1
- package/dist/input/gamepadSource.js +117 -71
- package/dist/input/mouseLook.js +2 -1
- package/dist/input/padHaptics.d.ts +17 -0
- package/dist/input/padHaptics.js +32 -0
- package/dist/input/pointerLock.d.ts +11 -0
- package/dist/input/pointerLock.js +19 -0
- package/dist/inputSink.d.ts +6 -6
- package/dist/inputSink.js +60 -21
- package/dist/lazyShell3dPresentation.d.ts +9 -0
- package/dist/lazyShell3dPresentation.js +24 -0
- package/dist/materialOverride.d.ts +42 -0
- package/dist/materialOverride.js +95 -36
- package/dist/multiplayer.d.ts +33 -3
- package/dist/multiplayer.js +105 -22
- package/dist/pointer/pointerService.d.ts +2 -0
- package/dist/pointer/pointerService.js +12 -2
- package/dist/postfx/PostProcessing.d.ts +11 -1
- package/dist/postfx/PostProcessing.js +14 -42
- package/dist/postfx/gradeShader.d.ts +1 -1
- package/dist/postfx/gradeShader.js +30 -2
- package/dist/postfx/shadowPrepass.d.ts +3 -0
- package/dist/postfx/shadowPrepass.js +18 -0
- package/dist/postfx/stylizePass.d.ts +23 -0
- package/dist/postfx/stylizePass.js +216 -0
- package/dist/presentationRecovery.d.ts +75 -0
- package/dist/presentationRecovery.js +204 -0
- package/dist/presentationRecovery3d.d.ts +4 -0
- package/dist/presentationRecovery3d.js +32 -0
- package/dist/presentationResolve.d.ts +8 -1
- package/dist/presentationResolve.js +5 -2
- package/dist/render/CascadedShadows.d.ts +7 -2
- package/dist/render/CascadedShadows.js +98 -20
- package/dist/render/EnvironmentLighting.js +74 -35
- package/dist/render/HairCards.d.ts +7 -0
- package/dist/render/HairCards.js +23 -0
- package/dist/render/SceneModels.js +52 -78
- package/dist/render/SkinnedInstances.d.ts +91 -0
- package/dist/render/SkinnedInstances.js +249 -0
- package/dist/render/TextureFiltering.d.ts +16 -0
- package/dist/render/TextureFiltering.js +64 -0
- package/dist/render/authoredSurfaceMaterial.d.ts +43 -0
- package/dist/render/authoredSurfaceMaterial.js +108 -0
- package/dist/render/displayColor.d.ts +33 -0
- package/dist/render/displayColor.js +138 -0
- package/dist/render/materialAppearanceSignals.d.ts +23 -0
- package/dist/render/materialAppearanceSignals.js +58 -0
- package/dist/render/materialAsset.d.ts +98 -0
- package/dist/render/materialAsset.js +454 -0
- package/dist/render/measureBounds.js +16 -3
- package/dist/render/measureCollisionMesh.js +8 -1
- package/dist/render/modelAssets.d.ts +8 -0
- package/dist/render/modelAssets.js +43 -0
- package/dist/render/modelBindPose.d.ts +5 -0
- package/dist/render/modelBindPose.js +35 -0
- package/dist/render/modelLoad.d.ts +32 -0
- package/dist/render/modelLoad.js +70 -1
- package/dist/render/modelNodeVisibility.d.ts +6 -0
- package/dist/render/modelNodeVisibility.js +18 -0
- package/dist/render/modelRender.d.ts +18 -1
- package/dist/render/modelRender.js +81 -3
- package/dist/render/projectedOverdraw.d.ts +35 -0
- package/dist/render/projectedOverdraw.js +204 -0
- package/dist/render/resolveModel.d.ts +12 -2
- package/dist/render/resolveModel.js +25 -4
- package/dist/render/rigNode.d.ts +26 -0
- package/dist/render/rigNode.js +32 -0
- package/dist/render/spriteRender.d.ts +4 -0
- package/dist/render/spriteRender.js +5 -0
- package/dist/render/staticShapeInstances.d.ts +34 -0
- package/dist/render/staticShapeInstances.js +78 -0
- package/dist/render/useFootIk.d.ts +53 -4
- package/dist/render/useFootIk.js +262 -70
- package/dist/render/useModelAnimation.d.ts +50 -0
- package/dist/render/useModelAnimation.js +245 -99
- package/dist/render/useModelInstance.d.ts +25 -0
- package/dist/render/useModelInstance.js +43 -0
- package/dist/scatter/scatterModels.d.ts +3 -2
- package/dist/scatter/scatterModels.js +15 -7
- package/dist/scene/AuthoredScene.d.ts +23 -6
- package/dist/scene/AuthoredScene.js +46 -9
- package/dist/scene/CityRenderer.js +6 -6
- package/dist/scene/GeneratedAssetRenderer.js +11 -42
- package/dist/settings/QuickControls.js +1 -1
- package/dist/settings/SettingsMenu.js +11 -18
- package/dist/settings/SettingsRuntime.d.ts +5 -0
- package/dist/settings/SettingsRuntime.js +29 -2
- package/dist/settings/appliedSettings.d.ts +9 -3
- package/dist/settings/appliedSettings.js +6 -5
- package/dist/settings/settingsController.d.ts +3 -0
- package/dist/settings/settingsController.js +47 -3
- package/dist/shellControlSuspension.d.ts +18 -0
- package/dist/shellControlSuspension.js +28 -0
- package/dist/shellInputPublication.d.ts +28 -0
- package/dist/shellInputPublication.js +49 -0
- package/dist/shellPointerInput.d.ts +18 -0
- package/dist/shellPointerInput.js +49 -0
- package/dist/structures/BuildingKitBatch.d.ts +3 -1
- package/dist/structures/BuildingKitBatch.js +35 -56
- package/dist/structures/BuildingRenderBudget.d.ts +23 -0
- package/dist/structures/BuildingRenderBudget.js +54 -0
- package/dist/structures/GeneratedBuilding.d.ts +3 -15
- package/dist/structures/GeneratedBuilding.js +33 -22
- package/dist/structures/buildingKitMaterials.d.ts +20 -0
- package/dist/structures/buildingKitMaterials.js +70 -0
- package/dist/structures/buildingSpatialBatch.d.ts +11 -0
- package/dist/structures/buildingSpatialBatch.js +45 -0
- package/dist/structures/buildingSurface.d.ts +21 -0
- package/dist/structures/buildingSurface.js +134 -0
- package/dist/terrain/GrassField.d.ts +2 -1
- package/dist/terrain/GrassField.js +59 -5
- package/dist/terrain/grassGeometry.d.ts +10 -0
- package/dist/terrain/grassGeometry.js +55 -0
- package/dist/terrain/grassMaterial.js +2 -2
- package/dist/terrain/terrainDetailMaterial.d.ts +3 -3
- package/dist/terrain/terrainDetailMaterial.js +7 -7
- package/dist/touch/TouchControlsOverlay.js +4 -4
- package/dist/useShellMultiplayerSync.d.ts +1 -1
- package/dist/useShellMultiplayerSync.js +9 -3
- package/dist/vfx/ParticleField.d.ts +23 -20
- package/dist/vfx/ParticleField.js +168 -50
- package/dist/visibility/CullingProvider.js +2 -62
- package/dist/visibility/cullingDriver.d.ts +9 -0
- package/dist/visibility/cullingDriver.js +68 -0
- package/dist/weather/AuthoredWeatherLayer.d.ts +16 -0
- package/dist/weather/AuthoredWeatherLayer.js +31 -0
- package/dist/weather/DustField.d.ts +2 -1
- package/dist/weather/DustField.js +17 -3
- package/dist/weather/FireSpreadLayer.d.ts +4 -2
- package/dist/weather/FireSpreadLayer.js +5 -11
- package/dist/weather/LightningStrike.d.ts +4 -1
- package/dist/weather/LightningStrike.js +13 -17
- package/dist/weather/RainField.d.ts +2 -1
- package/dist/weather/RainField.js +17 -3
- package/dist/weather/RainImpactField.d.ts +16 -0
- package/dist/weather/RainImpactField.js +89 -0
- package/dist/weather/SnowField.d.ts +2 -1
- package/dist/weather/SnowField.js +17 -3
- package/dist/weather/WeatherLayer.d.ts +11 -2
- package/dist/weather/WeatherLayer.js +30 -6
- package/dist/weather/index.d.ts +2 -0
- package/dist/weather/index.js +2 -0
- package/dist/weather/weatherMath.d.ts +2 -0
- package/dist/weather/weatherMath.js +8 -0
- package/dist/weather/weatherShelter.d.ts +18 -0
- package/dist/weather/weatherShelter.js +75 -0
- package/dist/weather/weatherUniforms.d.ts +40 -1
- package/dist/weather/weatherUniforms.js +29 -9
- package/dist/world/AuthoredFlocks.d.ts +22 -0
- package/dist/world/AuthoredFlocks.js +87 -0
- package/dist/world/StaticObjectModels.d.ts +8 -0
- package/dist/world/StaticObjectModels.js +75 -0
- package/dist/world/WorldHud.d.ts +14 -6
- package/dist/world/WorldHud.js +25 -12
- package/dist/world/WorldParticles.d.ts +10 -23
- package/dist/world/WorldParticles.js +210 -62
- package/dist/world/WorldScene.d.ts +3 -1
- package/dist/world/WorldScene.js +41 -26
- package/dist/world/staticObjectBatches.d.ts +35 -0
- package/dist/world/staticObjectBatches.js +163 -0
- package/dist/world/worldBarSamples.d.ts +37 -33
- package/dist/world/worldBarSamples.js +117 -75
- package/package.json +4 -4
- package/skills/ce-handoff/SKILL.md +2 -2
- package/skills/game-design/SKILL.md +2 -2
- package/skills/jgengine/SKILL.md +28 -6
- package/skills/jgengine/api.md +139 -99
- package/skills/jgengine/capabilities.md +28 -0
- package/skills/jgengine/recipes/minimal-game.md +10 -2
- package/skills/jgengine-ui/SKILL.md +16 -17
- package/skills/jgengine-ui/api.md +603 -365
- package/skills/jgengine-ui/capabilities.md +131 -1
- package/skills/jgengine-ui/reference-primitives.md +179 -0
- package/skills/jgengine-ui/reference.md +4 -81
- package/skills/jgengine-world/SKILL.md +25 -4
- package/skills/jgengine-world/api.md +1045 -655
- package/skills/jgengine-world/capabilities.md +246 -9
- package/skills/jgengine-world/recipes/character-animation.md +62 -0
- package/skills/jgengine-world/recipes/controls.md +48 -0
- package/skills/jgengine-world/recipes/flight-feel.md +76 -0
- package/skills/jgengine-world/recipes/npc-senses.md +55 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +87 -0
- package/skills/jgengine-world/reference.md +180 -4
- package/skills/jgengine-world/visibility.md +139 -0
- package/skills/level-design/SKILL.md +1 -1
- package/dist/render/testFixtures/proceduralSpriteAtlas.d.ts +0 -15
- package/dist/render/testFixtures/proceduralSpriteAtlas.js +0 -30
- package/dist/render/testFixtures/proceduralTileLayer.d.ts +0 -13
- package/dist/render/testFixtures/proceduralTileLayer.js +0 -30
|
@@ -7,48 +7,90 @@ export function createNoiseBuffer(ctx) {
|
|
|
7
7
|
data[i] = Math.random() * 2 - 1;
|
|
8
8
|
return buf;
|
|
9
9
|
}
|
|
10
|
-
function realizeTone(ctx, out, voice, cueGain, at) {
|
|
10
|
+
function realizeTone(ctx, out, voice, cueGain, at, ended) {
|
|
11
11
|
const t = at + (voice.delay ?? 0);
|
|
12
12
|
const peak = (voice.gain ?? 1) * cueGain;
|
|
13
13
|
const attack = voice.attack ?? 0.012;
|
|
14
14
|
const osc = ctx.createOscillator();
|
|
15
|
-
|
|
16
|
-
osc
|
|
17
|
-
|
|
18
|
-
osc.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
15
|
+
const nodes = [];
|
|
16
|
+
const stop = trackVoice(osc, nodes, ended);
|
|
17
|
+
try {
|
|
18
|
+
osc.type = voice.wave ?? "sine";
|
|
19
|
+
osc.frequency.setValueAtTime(voice.freq, t);
|
|
20
|
+
if (voice.slideTo !== undefined)
|
|
21
|
+
osc.frequency.exponentialRampToValueAtTime(Math.max(1, voice.slideTo), t + voice.duration);
|
|
22
|
+
const sustainEnd = Math.max(attack, voice.sustain ?? 0);
|
|
23
|
+
const g = ctx.createGain();
|
|
24
|
+
nodes.push(g);
|
|
25
|
+
g.gain.setValueAtTime(0, t);
|
|
26
|
+
g.gain.linearRampToValueAtTime(peak, t + attack);
|
|
27
|
+
if (sustainEnd > attack)
|
|
28
|
+
g.gain.setValueAtTime(peak, t + sustainEnd);
|
|
29
|
+
g.gain.exponentialRampToValueAtTime(0.001, t + Math.max(voice.duration, sustainEnd + 0.001));
|
|
30
|
+
osc.connect(g).connect(out);
|
|
31
|
+
osc.start(t);
|
|
32
|
+
osc.stop(t + voice.duration + 0.05);
|
|
33
|
+
return stop;
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
stop();
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
29
39
|
}
|
|
30
|
-
function realizeNoise(ctx, out, noiseBuf, voice, cueGain, at) {
|
|
40
|
+
function realizeNoise(ctx, out, noiseBuf, voice, cueGain, at, ended) {
|
|
31
41
|
const t = at + (voice.delay ?? 0);
|
|
32
42
|
const peak = (voice.gain ?? 1) * cueGain;
|
|
33
43
|
const decay = voice.decay ?? 0.9;
|
|
34
44
|
const src = ctx.createBufferSource();
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
45
|
+
const nodes = [];
|
|
46
|
+
const stop = trackVoice(src, nodes, ended);
|
|
47
|
+
try {
|
|
48
|
+
src.buffer = noiseBuf;
|
|
49
|
+
src.playbackRate.value = 0.8 + Math.random() * 0.4;
|
|
50
|
+
// Loop the shared 1-second buffer so a random offset cannot truncate a longer voice.
|
|
51
|
+
src.loop = true;
|
|
52
|
+
const filter = ctx.createBiquadFilter();
|
|
53
|
+
nodes.push(filter);
|
|
54
|
+
filter.type = voice.filterType ?? "lowpass";
|
|
55
|
+
filter.frequency.value = voice.filterFreq;
|
|
56
|
+
const sustain = Math.max(0, voice.sustain ?? 0);
|
|
57
|
+
const g = ctx.createGain();
|
|
58
|
+
nodes.push(g);
|
|
59
|
+
g.gain.setValueAtTime(peak, t);
|
|
60
|
+
if (sustain > 0)
|
|
61
|
+
g.gain.setValueAtTime(peak, t + sustain);
|
|
62
|
+
g.gain.exponentialRampToValueAtTime(0.001, t + Math.max(voice.duration * decay, sustain + 0.001));
|
|
63
|
+
src.connect(filter).connect(g).connect(out);
|
|
64
|
+
src.start(t, Math.random() * 0.5, voice.duration);
|
|
65
|
+
return stop;
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
stop();
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function trackVoice(source, nodes, ended) {
|
|
73
|
+
let disposed = false;
|
|
74
|
+
function cleanup() {
|
|
75
|
+
if (disposed)
|
|
76
|
+
return;
|
|
77
|
+
disposed = true;
|
|
78
|
+
source.onended = null;
|
|
79
|
+
source.disconnect();
|
|
80
|
+
for (const node of nodes)
|
|
81
|
+
node.disconnect();
|
|
82
|
+
ended();
|
|
83
|
+
}
|
|
84
|
+
source.onended = cleanup;
|
|
85
|
+
return () => {
|
|
86
|
+
if (disposed)
|
|
87
|
+
return;
|
|
88
|
+
try {
|
|
89
|
+
source.stop();
|
|
90
|
+
}
|
|
91
|
+
catch { }
|
|
92
|
+
cleanup();
|
|
93
|
+
};
|
|
52
94
|
}
|
|
53
95
|
/**
|
|
54
96
|
* Realise a procedural cue on Web Audio: every voice is scheduled at
|
|
@@ -58,10 +100,33 @@ function realizeNoise(ctx, out, noiseBuf, voice, cueGain, at) {
|
|
|
58
100
|
export function realizeSynthPatch(ctx, out, noiseBuf, patch) {
|
|
59
101
|
const cueGain = patch.gain ?? 1;
|
|
60
102
|
const at = ctx.currentTime;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
103
|
+
let remaining = patch.voices.length;
|
|
104
|
+
const callbacks = [];
|
|
105
|
+
const stops = [];
|
|
106
|
+
function ended() {
|
|
107
|
+
remaining -= 1;
|
|
108
|
+
if (remaining === 0)
|
|
109
|
+
for (const callback of callbacks.splice(0))
|
|
110
|
+
callback();
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
for (const voice of patch.voices) {
|
|
114
|
+
stops.push(voice.kind === "tone"
|
|
115
|
+
? realizeTone(ctx, out, voice, cueGain, at, ended)
|
|
116
|
+
: realizeNoise(ctx, out, noiseBuf, voice, cueGain, at, ended));
|
|
117
|
+
}
|
|
66
118
|
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
for (const stop of stops)
|
|
121
|
+
stop();
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
stop() { for (const stop of stops)
|
|
126
|
+
stop(); },
|
|
127
|
+
onEnded(callback) { if (remaining === 0)
|
|
128
|
+
callback();
|
|
129
|
+
else
|
|
130
|
+
callbacks.push(callback); },
|
|
131
|
+
};
|
|
67
132
|
}
|
|
@@ -5,10 +5,11 @@ import { GameFirstPersonCamera } from "./GameFirstPersonCamera.js";
|
|
|
5
5
|
import { GameInspectionCamera } from "./GameInspectionCamera.js";
|
|
6
6
|
import { GameOrbitCamera } from "./GameOrbitCamera.js";
|
|
7
7
|
import { resolveDirectedCamera } from "./rigMath.js";
|
|
8
|
-
import { resolveRigKind, turntableAsObserver } from "./rigResolve.js";
|
|
8
|
+
import { resolveRigKind, resolveRuntimeCameraConfig, turntableAsObserver } from "./rigResolve.js";
|
|
9
9
|
import { CameraShakeContext, createCameraShakeChannel } from "./shakeChannel.js";
|
|
10
10
|
export { resolveRigKind } from "./rigResolve.js";
|
|
11
11
|
export function GameCameraRig({ yawRef, pitchRef, config, onDragChange, pointerControls, panKeysEnabled, director, viewmodel, }) {
|
|
12
|
+
config = resolveRuntimeCameraConfig(config, director?.rig());
|
|
12
13
|
const channel = useMemo(() => createCameraShakeChannel(config?.shake?.decayPerSecond), [config?.shake?.decayPerSecond]);
|
|
13
14
|
const directorVersionRef = useRef(0);
|
|
14
15
|
const subscribeDirector = useCallback((onStoreChange) => {
|
|
@@ -37,7 +38,7 @@ export function GameCameraRig({ yawRef, pitchRef, config, onDragChange, pointerC
|
|
|
37
38
|
const kind = resolveRigKind(config);
|
|
38
39
|
switch (kind) {
|
|
39
40
|
case "first":
|
|
40
|
-
return (_jsx(GameFirstPersonCamera, { yawRef: yawRef, pitchRef: pitchRef, config: config?.firstPerson, followEntityId: followEntityId ?? undefined, viewmodel: viewmodel }));
|
|
41
|
+
return (_jsx(GameFirstPersonCamera, { yawRef: yawRef, pitchRef: pitchRef, config: config?.firstPerson, followEntityId: followEntityId ?? undefined, viewmodel: viewmodel, weapon: config?.weapon }));
|
|
41
42
|
case "topDown":
|
|
42
43
|
return _jsx(TopDownRig, { yawRef: yawRef, pitchRef: pitchRef, config: config, followEntityId: followEntityId });
|
|
43
44
|
case "rts":
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { type ComponentType, type MutableRefObject } from "react";
|
|
2
2
|
import * as THREE from "three";
|
|
3
|
-
import type { FirstPersonCameraConfig } from "@jgengine/core/game/playableGame";
|
|
3
|
+
import type { CameraWeaponView, FirstPersonCameraConfig } from "@jgengine/core/game/playableGame";
|
|
4
4
|
import type { EntityRenderCues } from "@jgengine/core/combat/renderCues";
|
|
5
|
-
/**
|
|
6
|
-
export declare function readFirstPersonMuzzle(target: THREE.Vector3): boolean;
|
|
5
|
+
/** Presentation muzzle in world space; camera-scoped when supplied, false leaves target unchanged. */
|
|
6
|
+
export declare function readFirstPersonMuzzle(target: THREE.Vector3, camera?: THREE.Camera): boolean;
|
|
7
7
|
/** Props handed to a custom viewmodel component (#542): a live cue ref (velocity/bob/firing/reloading/recoil/hit) for the followed entity, driven from your own `useFrame` — read `cuesRef.current` there rather than storing it as render state. */
|
|
8
8
|
export interface ViewmodelProps {
|
|
9
9
|
cuesRef: MutableRefObject<EntityRenderCues>;
|
|
@@ -15,5 +15,7 @@ export interface GameFirstPersonCameraProps {
|
|
|
15
15
|
followEntityId?: string;
|
|
16
16
|
/** Custom viewmodel component replacing the built-in three-mesh gun, rendered inside the same camera-locked, muzzle-tracked anchor. Ignored when `config.viewmodel === false`. */
|
|
17
17
|
viewmodel?: ComponentType<ViewmodelProps>;
|
|
18
|
+
/** Held-weapon source (`GameCameraConfig.weapon`): poses the viewmodel and adds recoil to the look. */
|
|
19
|
+
weapon?: (entityId: string) => CameraWeaponView | null;
|
|
18
20
|
}
|
|
19
|
-
export declare function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId, viewmodel, }: GameFirstPersonCameraProps): import("react").JSX.Element | null;
|
|
21
|
+
export declare function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId, viewmodel, weapon, }: GameFirstPersonCameraProps): import("react").JSX.Element | null;
|
|
@@ -1,28 +1,25 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useFrame, useThree } from "@react-three/fiber";
|
|
3
|
-
import { useEffect, useRef } from "react";
|
|
3
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
4
4
|
import * as THREE from "three";
|
|
5
5
|
import { DEFAULT_EYE_HEIGHT } from "@jgengine/core/combat/shotOrigin";
|
|
6
|
+
import { createWeaponPresentation, viewmodelFovScale, } from "@jgengine/core/combat/weaponPresentation";
|
|
6
7
|
import { useGameContext } from "@jgengine/react/provider";
|
|
7
8
|
import { usePlayer } from "@jgengine/react/hooks";
|
|
8
9
|
import { useEntityRenderCues } from "../render/useEntityRenderCues.js";
|
|
9
10
|
import { usePlayerFov } from "./PlayerFov.js";
|
|
10
11
|
import { GAME_SIM_FRAME_PRIORITY, ORBIT_CAMERA_FRAME_PRIORITY } from "./orbitCameraMath.js";
|
|
12
|
+
import { requestRawPointerLock } from "../input/pointerLock.js";
|
|
13
|
+
import { readRegisteredFirstPersonMuzzle, registerTrackedFirstPersonMuzzle } from "./customMuzzle.js";
|
|
11
14
|
const DEFAULT_SENSITIVITY = 0.0025;
|
|
12
15
|
const DEFAULT_MAX_PITCH = 1.45;
|
|
13
16
|
const VIEWMODEL_ORIGIN = new THREE.Vector3(0.34, -0.26, -0.72);
|
|
14
17
|
const MUZZLE_TIP_LOCAL = new THREE.Vector3(0, 0.03, -0.61);
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
/** World position of the first-person weapon muzzle, or false when no viewmodel is mounted. */
|
|
19
|
-
export function readFirstPersonMuzzle(target) {
|
|
20
|
-
if (!muzzleTracked)
|
|
21
|
-
return false;
|
|
22
|
-
target.copy(muzzleWorld);
|
|
23
|
-
return true;
|
|
18
|
+
/** Presentation muzzle in world space; camera-scoped when supplied, false leaves target unchanged. */
|
|
19
|
+
export function readFirstPersonMuzzle(target, camera) {
|
|
20
|
+
return readRegisteredFirstPersonMuzzle(target, camera);
|
|
24
21
|
}
|
|
25
|
-
export function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId, viewmodel, }) {
|
|
22
|
+
export function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId, viewmodel, weapon, }) {
|
|
26
23
|
const eyeHeight = config?.eyeHeight ?? DEFAULT_EYE_HEIGHT;
|
|
27
24
|
const sensitivity = config?.sensitivity ?? DEFAULT_SENSITIVITY;
|
|
28
25
|
const maxPitch = config?.maxPitch ?? DEFAULT_MAX_PITCH;
|
|
@@ -34,12 +31,16 @@ export function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId
|
|
|
34
31
|
const followId = followEntityId ?? userId;
|
|
35
32
|
const seededRef = useRef(false);
|
|
36
33
|
const cuesRef = useEntityRenderCues(followId);
|
|
34
|
+
const presentation = useMemo(() => createWeaponPresentation(), []);
|
|
35
|
+
const tuningRef = useRef(undefined);
|
|
36
|
+
const lastLookRef = useRef(null);
|
|
37
|
+
const poseRef = useRef(null);
|
|
37
38
|
useEffect(() => {
|
|
38
39
|
const requestLock = () => {
|
|
39
40
|
if (window.matchMedia?.("(pointer: coarse)").matches)
|
|
40
41
|
return;
|
|
41
42
|
if (document.pointerLockElement !== domElement)
|
|
42
|
-
|
|
43
|
+
requestRawPointerLock(domElement);
|
|
43
44
|
};
|
|
44
45
|
const onMove = (event) => {
|
|
45
46
|
if (document.pointerLockElement !== domElement)
|
|
@@ -54,7 +55,7 @@ export function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId
|
|
|
54
55
|
window.removeEventListener("mousemove", onMove);
|
|
55
56
|
};
|
|
56
57
|
}, [domElement, sensitivity, maxPitch, yawRef, pitchRef]);
|
|
57
|
-
useFrame(() => {
|
|
58
|
+
useFrame((_, dt) => {
|
|
58
59
|
const entity = ctx.scene.entity.get(followId);
|
|
59
60
|
if (entity === null)
|
|
60
61
|
return;
|
|
@@ -62,37 +63,77 @@ export function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId
|
|
|
62
63
|
seededRef.current = true;
|
|
63
64
|
yawRef.current = entity.rotationY;
|
|
64
65
|
}
|
|
65
|
-
const
|
|
66
|
+
const view = weapon?.(followId) ?? null;
|
|
67
|
+
const last = (lastLookRef.current ??= { yaw: yawRef.current, pitch: pitchRef.current });
|
|
68
|
+
const step = Math.max(1e-4, dt);
|
|
69
|
+
let pose = null;
|
|
70
|
+
if (view !== null) {
|
|
71
|
+
if (view.presentation !== tuningRef.current) {
|
|
72
|
+
tuningRef.current = view.presentation;
|
|
73
|
+
presentation.retune(view.presentation ?? {});
|
|
74
|
+
}
|
|
75
|
+
pose = presentation.update(dt, {
|
|
76
|
+
lookYawRate: (yawRef.current - last.yaw) / step,
|
|
77
|
+
lookPitchRate: (pitchRef.current - last.pitch) / step,
|
|
78
|
+
speed: cuesRef.current.speed,
|
|
79
|
+
bobPhase: cuesRef.current.bobPhase,
|
|
80
|
+
handling: view.handling,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
last.yaw = yawRef.current;
|
|
84
|
+
last.pitch = pitchRef.current;
|
|
85
|
+
poseRef.current = pose;
|
|
86
|
+
const yaw = yawRef.current - (pose?.lookYaw ?? 0);
|
|
87
|
+
const pitch = Math.max(-maxPitch, Math.min(maxPitch, pitchRef.current + (pose?.lookPitch ?? 0)));
|
|
88
|
+
const cosPitch = Math.cos(pitch);
|
|
66
89
|
camera.position.set(entity.position[0], entity.position[1] + eyeHeight, entity.position[2]);
|
|
67
|
-
camera.lookAt(camera.position.x + Math.sin(
|
|
90
|
+
camera.lookAt(camera.position.x + Math.sin(yaw) * cosPitch, camera.position.y + Math.sin(pitch), camera.position.z + Math.cos(yaw) * cosPitch);
|
|
68
91
|
if (camera.isPerspectiveCamera === true) {
|
|
69
92
|
const perspective = camera;
|
|
70
|
-
|
|
71
|
-
|
|
93
|
+
const fov = playerFov.fov * (pose?.fovScale ?? 1);
|
|
94
|
+
if (Math.abs(perspective.fov - fov) > 0.001) {
|
|
95
|
+
perspective.fov = fov;
|
|
72
96
|
perspective.updateProjectionMatrix();
|
|
73
97
|
}
|
|
74
98
|
}
|
|
75
99
|
}, ORBIT_CAMERA_FRAME_PRIORITY);
|
|
76
100
|
if (config?.viewmodel === false)
|
|
77
101
|
return null;
|
|
78
|
-
return _jsx(FirstPersonViewmodel, { camera: camera, viewmodel: viewmodel, cuesRef: cuesRef });
|
|
102
|
+
return _jsx(FirstPersonViewmodel, { camera: camera, viewmodel: viewmodel, cuesRef: cuesRef, poseRef: poseRef });
|
|
79
103
|
}
|
|
80
|
-
function FirstPersonViewmodel({ camera, viewmodel: Viewmodel, cuesRef, }) {
|
|
104
|
+
function FirstPersonViewmodel({ camera, viewmodel: Viewmodel, cuesRef, poseRef, }) {
|
|
81
105
|
const groupRef = useRef(null);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
106
|
+
const muzzleWorld = useRef(new THREE.Vector3());
|
|
107
|
+
const muzzleTracked = useRef(false);
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
muzzleTracked.current = false;
|
|
110
|
+
return registerTrackedFirstPersonMuzzle(camera, (target) => {
|
|
111
|
+
if (!muzzleTracked.current)
|
|
112
|
+
return false;
|
|
113
|
+
target.copy(muzzleWorld.current);
|
|
114
|
+
return true;
|
|
115
|
+
});
|
|
116
|
+
}, [camera]);
|
|
85
117
|
useFrame(() => {
|
|
86
118
|
const group = groupRef.current;
|
|
87
119
|
if (group === null)
|
|
88
120
|
return;
|
|
121
|
+
const pose = poseRef.current;
|
|
122
|
+
const perspective = camera;
|
|
123
|
+
// Scaling camera-space x/y by this projects the viewmodel at its own FOV without a second render pass.
|
|
124
|
+
const squash = pose?.viewmodelFov == null || perspective.isPerspectiveCamera !== true ? 1 : viewmodelFovScale(perspective.fov, pose.viewmodelFov);
|
|
89
125
|
group.position.copy(camera.position);
|
|
90
126
|
group.quaternion.copy(camera.quaternion);
|
|
91
|
-
group.translateX(VIEWMODEL_ORIGIN.x);
|
|
92
|
-
group.translateY(VIEWMODEL_ORIGIN.y);
|
|
93
|
-
group.translateZ(VIEWMODEL_ORIGIN.z);
|
|
94
|
-
|
|
95
|
-
|
|
127
|
+
group.translateX((pose?.offset[0] ?? VIEWMODEL_ORIGIN.x) * squash);
|
|
128
|
+
group.translateY((pose?.offset[1] ?? VIEWMODEL_ORIGIN.y) * squash);
|
|
129
|
+
group.translateZ(pose?.offset[2] ?? VIEWMODEL_ORIGIN.z);
|
|
130
|
+
group.rotateX(pose?.pitch ?? 0);
|
|
131
|
+
group.rotateY(pose?.yaw ?? 0);
|
|
132
|
+
group.rotateZ(pose?.roll ?? 0);
|
|
133
|
+
group.scale.set(squash, squash, 1);
|
|
134
|
+
group.updateMatrixWorld();
|
|
135
|
+
group.localToWorld(muzzleWorld.current.copy(MUZZLE_TIP_LOCAL));
|
|
136
|
+
muzzleTracked.current = true;
|
|
96
137
|
}, GAME_SIM_FRAME_PRIORITY);
|
|
97
138
|
if (Viewmodel !== undefined) {
|
|
98
139
|
return (_jsx("group", { ref: groupRef, children: _jsx(Viewmodel, { cuesRef: cuesRef }) }));
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { GameCameraConfig } from "@jgengine/core/game/playableGame";
|
|
3
|
+
import { type ViewportDef, type ViewportsConfig } from "@jgengine/core/game/viewports";
|
|
4
|
+
import type { GameContext } from "@jgengine/core/runtime/gameContext";
|
|
5
|
+
import { type LocalPlayerSlot } from "@jgengine/core/runtime/localPlayers";
|
|
6
|
+
/** Camera yaw the split-screen renderer last used for a seat's user, so the walk controller moves that seat camera-relative. @internal */
|
|
7
|
+
export declare function seatCameraYaw(ctx: GameContext, userId: string): number | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* The viewports for the seats currently joined, re-resolved when a seat joins or leaves.
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export declare function useSeatViewports(ctx: GameContext, config: ViewportsConfig | undefined): readonly ViewportDef[];
|
|
13
|
+
/**
|
|
14
|
+
* Draws one scissored render per viewport. The primary seat keeps the main camera and its rig; every
|
|
15
|
+
* other seat gets a follow camera (chase or top-down) on its possessed entity. Mounted only while more
|
|
16
|
+
* than one viewport is drawn, since a render-priority frame callback takes over R3F's own render.
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
export declare function SplitScreenRenderer({ ctx, viewports, config, mainCamera, }: {
|
|
20
|
+
ctx: GameContext;
|
|
21
|
+
viewports: readonly ViewportDef[];
|
|
22
|
+
config: ViewportsConfig | undefined;
|
|
23
|
+
mainCamera: GameCameraConfig | undefined;
|
|
24
|
+
}): null;
|
|
25
|
+
/**
|
|
26
|
+
* One absolutely-positioned HUD root per viewport, laid over the canvas: render each seat's own
|
|
27
|
+
* health, prompts or score with `children(slot)`. Pass the same `viewports` config the game gave `defineGame`.
|
|
28
|
+
* @capability split-screen-hud Per-seat HUD roots positioned over each split-screen viewport.
|
|
29
|
+
*/
|
|
30
|
+
export declare function ViewportHuds({ ctx, config, children, }: {
|
|
31
|
+
ctx: GameContext;
|
|
32
|
+
config?: ViewportsConfig;
|
|
33
|
+
children: (slot: LocalPlayerSlot, viewport: ViewportDef) => ReactNode;
|
|
34
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useFrame, useThree } from "@react-three/fiber";
|
|
3
|
+
import { useMemo, useRef, useSyncExternalStore } from "react";
|
|
4
|
+
import * as THREE from "three";
|
|
5
|
+
import { resolveViewports, viewportPixels, } from "@jgengine/core/game/viewports";
|
|
6
|
+
import { localPlayers } from "@jgengine/core/runtime/localPlayers";
|
|
7
|
+
import { createChaseRigState, resolveChase, resolveTopDown, stepChase, topDownPose } from "./rigMath.js";
|
|
8
|
+
const SEAT_YAW = new WeakMap();
|
|
9
|
+
/** Camera yaw the split-screen renderer last used for a seat's user, so the walk controller moves that seat camera-relative. @internal */
|
|
10
|
+
export function seatCameraYaw(ctx, userId) {
|
|
11
|
+
return SEAT_YAW.get(ctx)?.get(userId);
|
|
12
|
+
}
|
|
13
|
+
function seatSlotIds(ctx) {
|
|
14
|
+
return localPlayers(ctx).slots().map((slot) => slot.slotId);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The viewports for the seats currently joined, re-resolved when a seat joins or leaves.
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
export function useSeatViewports(ctx, config) {
|
|
21
|
+
const seats = localPlayers(ctx);
|
|
22
|
+
const key = useSyncExternalStore(seats.subscribe, () => seatSlotIds(ctx).join("|"), () => seatSlotIds(ctx).join("|"));
|
|
23
|
+
return useMemo(() => resolveViewports(config, key === "" ? [] : key.split("|")), [config, key]);
|
|
24
|
+
}
|
|
25
|
+
function seatRigKind(camera, main) {
|
|
26
|
+
return camera?.rig ?? (main?.rig === "topDown" ? "topDown" : "chase");
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Draws one scissored render per viewport. The primary seat keeps the main camera and its rig; every
|
|
30
|
+
* other seat gets a follow camera (chase or top-down) on its possessed entity. Mounted only while more
|
|
31
|
+
* than one viewport is drawn, since a render-priority frame callback takes over R3F's own render.
|
|
32
|
+
* @internal
|
|
33
|
+
*/
|
|
34
|
+
export function SplitScreenRenderer({ ctx, viewports, config, mainCamera, }) {
|
|
35
|
+
const gl = useThree((state) => state.gl);
|
|
36
|
+
const scene = useThree((state) => state.scene);
|
|
37
|
+
const camera = useThree((state) => state.camera);
|
|
38
|
+
const size = useThree((state) => state.size);
|
|
39
|
+
const rigs = useRef(new Map());
|
|
40
|
+
useFrame((_state, dt) => {
|
|
41
|
+
const seats = localPlayers(ctx);
|
|
42
|
+
let yaws = SEAT_YAW.get(ctx);
|
|
43
|
+
if (yaws === undefined) {
|
|
44
|
+
yaws = new Map();
|
|
45
|
+
SEAT_YAW.set(ctx, yaws);
|
|
46
|
+
}
|
|
47
|
+
gl.setScissorTest(true);
|
|
48
|
+
for (const def of viewports) {
|
|
49
|
+
const slot = seats.slot(def.slot);
|
|
50
|
+
if (slot === null)
|
|
51
|
+
continue;
|
|
52
|
+
const pixels = viewportPixels(def.rect, size.width, size.height);
|
|
53
|
+
const view = slot.index === 0 ? camera : seatCamera(slot, def.camera ?? config?.camera, dt, yaws);
|
|
54
|
+
if (view instanceof THREE.PerspectiveCamera) {
|
|
55
|
+
view.aspect = pixels.width / pixels.height;
|
|
56
|
+
view.updateProjectionMatrix();
|
|
57
|
+
}
|
|
58
|
+
gl.setViewport(pixels.x, pixels.y, pixels.width, pixels.height);
|
|
59
|
+
gl.setScissor(pixels.x, pixels.y, pixels.width, pixels.height);
|
|
60
|
+
gl.render(scene, view);
|
|
61
|
+
}
|
|
62
|
+
gl.setScissorTest(false);
|
|
63
|
+
gl.setViewport(0, 0, size.width, size.height);
|
|
64
|
+
if (camera instanceof THREE.PerspectiveCamera) {
|
|
65
|
+
camera.aspect = size.width / size.height;
|
|
66
|
+
camera.updateProjectionMatrix();
|
|
67
|
+
}
|
|
68
|
+
}, 1);
|
|
69
|
+
function seatCamera(slot, cameraConfig, dt, yaws) {
|
|
70
|
+
let rig = rigs.current.get(slot.slotId);
|
|
71
|
+
if (rig === undefined) {
|
|
72
|
+
const created = new THREE.PerspectiveCamera();
|
|
73
|
+
if (camera instanceof THREE.PerspectiveCamera) {
|
|
74
|
+
created.near = camera.near;
|
|
75
|
+
created.far = camera.far;
|
|
76
|
+
}
|
|
77
|
+
rig = { camera: created, chase: createChaseRigState() };
|
|
78
|
+
rigs.current.set(slot.slotId, rig);
|
|
79
|
+
}
|
|
80
|
+
const entity = ctx.scene.entity.get(ctx.player.possession.active(slot.userId));
|
|
81
|
+
const follow = entity === null ? { x: 0, y: 0, z: 0 } : { x: entity.position[0], y: entity.position[1], z: entity.position[2] };
|
|
82
|
+
let pose;
|
|
83
|
+
if (seatRigKind(cameraConfig, mainCamera) === "topDown") {
|
|
84
|
+
const resolved = resolveTopDown(cameraConfig?.topDown ?? mainCamera?.topDown);
|
|
85
|
+
pose = topDownPose(follow, resolved, rig.camera.fov);
|
|
86
|
+
yaws.set(slot.userId, resolved.yaw);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const velocity = entity?.velocity;
|
|
90
|
+
const step = stepChase(rig.chase, {
|
|
91
|
+
follow,
|
|
92
|
+
yaw: entity?.rotationY ?? 0,
|
|
93
|
+
bodyPitch: entity?.rotationX ?? 0,
|
|
94
|
+
velocity: velocity === undefined ? null : { x: velocity[0], y: velocity[1], z: velocity[2] },
|
|
95
|
+
lookBack: false,
|
|
96
|
+
fovKick: 0,
|
|
97
|
+
}, resolveChase(cameraConfig?.chase ?? mainCamera?.chase), dt);
|
|
98
|
+
pose = step.pose;
|
|
99
|
+
yaws.set(slot.userId, step.anchorYaw);
|
|
100
|
+
}
|
|
101
|
+
rig.camera.position.set(pose.position.x, pose.position.y, pose.position.z);
|
|
102
|
+
rig.camera.fov = pose.fov;
|
|
103
|
+
rig.camera.lookAt(pose.lookAt.x, pose.lookAt.y, pose.lookAt.z);
|
|
104
|
+
return rig.camera;
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* One absolutely-positioned HUD root per viewport, laid over the canvas: render each seat's own
|
|
110
|
+
* health, prompts or score with `children(slot)`. Pass the same `viewports` config the game gave `defineGame`.
|
|
111
|
+
* @capability split-screen-hud Per-seat HUD roots positioned over each split-screen viewport.
|
|
112
|
+
*/
|
|
113
|
+
export function ViewportHuds({ ctx, config, children, }) {
|
|
114
|
+
const viewports = useSeatViewports(ctx, config);
|
|
115
|
+
const seats = localPlayers(ctx);
|
|
116
|
+
return (_jsx(_Fragment, { children: viewports.map((viewport) => {
|
|
117
|
+
const slot = seats.slot(viewport.slot);
|
|
118
|
+
if (slot === null)
|
|
119
|
+
return null;
|
|
120
|
+
const [x, y, width, height] = viewport.rect;
|
|
121
|
+
return (_jsx("div", { "data-jg-viewport": viewport.slot, className: "pointer-events-none absolute overflow-hidden", style: { left: `${x * 100}%`, top: `${y * 100}%`, width: `${width * 100}%`, height: `${height * 100}%` }, children: children(slot, viewport) }, viewport.slot));
|
|
122
|
+
}) }));
|
|
123
|
+
}
|