@jgengine/shell 0.10.0 → 0.12.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 +39 -2
- package/LICENSE +203 -661
- package/README.md +1 -1
- package/dist/CombatPresentation.d.ts +5 -0
- package/dist/CombatPresentation.js +8 -0
- package/dist/GameHost.d.ts +25 -1
- package/dist/GameHost.js +60 -4
- package/dist/GamePhaseStamp.d.ts +1 -0
- package/dist/GamePhaseStamp.js +9 -0
- package/dist/GamePlayer.d.ts +5 -0
- package/dist/GamePlayer.js +5 -0
- package/dist/GamePlayerShell.d.ts +5 -12
- package/dist/GamePlayerShell.js +67 -1332
- package/dist/GameUiPreview.js +3 -2
- package/dist/Shell3dPresentation.d.ts +63 -0
- package/dist/Shell3dPresentation.js +302 -0
- package/dist/ShellChrome.d.ts +51 -0
- package/dist/ShellChrome.js +51 -0
- package/dist/ShellHudPresentation.d.ts +41 -0
- package/dist/ShellHudPresentation.js +22 -0
- package/dist/audio/audioEngine.d.ts +4 -0
- package/dist/audio/audioEngine.js +128 -21
- package/dist/audio/audioWire.d.ts +40 -0
- package/dist/audio/audioWire.js +62 -0
- package/dist/audio/loopParams.d.ts +20 -0
- package/dist/audio/loopParams.js +28 -0
- package/dist/behaviour.d.ts +2 -1
- package/dist/behaviour.js +2 -1
- package/dist/behaviourAttach.d.ts +1 -0
- package/dist/behaviourAttach.js +1 -0
- package/dist/behaviourDriver.d.ts +1 -0
- package/dist/behaviourDriver.js +1 -0
- package/dist/boundActionDispatch.d.ts +13 -0
- package/dist/boundActionDispatch.js +37 -0
- package/dist/camera/GameCameraRig.d.ts +5 -2
- package/dist/camera/GameCameraRig.js +8 -3
- package/dist/camera/GameFirstPersonCamera.d.ts +9 -2
- package/dist/camera/GameFirstPersonCamera.js +8 -3
- package/dist/camera/GameInspectionCamera.d.ts +1 -1
- package/dist/camera/GameInspectionCamera.js +2 -2
- package/dist/camera/GameOrbitCamera.js +24 -9
- package/dist/camera/cameraBlendMath.d.ts +3 -0
- package/dist/camera/cameraBlendMath.js +3 -0
- package/dist/camera/cameraRigs.d.ts +3 -1
- package/dist/camera/cameraRigs.js +21 -3
- package/dist/camera/fovPreference.d.ts +6 -1
- package/dist/camera/fovPreference.js +6 -1
- package/dist/camera/index.d.ts +4 -7
- package/dist/camera/index.js +4 -7
- package/dist/camera/inspectionCameraMath.d.ts +7 -2
- package/dist/camera/inspectionCameraMath.js +7 -2
- package/dist/camera/orbitCameraMath.d.ts +36 -6
- package/dist/camera/orbitCameraMath.js +56 -6
- package/dist/camera/rigMath.d.ts +114 -24
- package/dist/camera/rigMath.js +129 -24
- package/dist/camera/rigResolve.d.ts +4 -2
- package/dist/camera/rigResolve.js +4 -2
- package/dist/camera/shakeChannel.d.ts +6 -2
- package/dist/camera/shakeChannel.js +6 -2
- package/dist/camera/shakeChannelMath.d.ts +1 -0
- package/dist/camera/shakeChannelMath.js +1 -0
- package/dist/commandSink.d.ts +8 -3
- package/dist/commandSink.js +8 -3
- package/dist/defineGame.d.ts +15 -1
- package/dist/defineGame.js +50 -9
- package/dist/devtools/ColPanel.d.ts +1 -0
- package/dist/devtools/ColPanel.js +14 -0
- package/dist/devtools/DevtoolsOverlay.d.ts +11 -7
- package/dist/devtools/DevtoolsOverlay.js +47 -462
- package/dist/devtools/KeysPanel.d.ts +4 -0
- package/dist/devtools/KeysPanel.js +31 -0
- package/dist/devtools/LogsPanel.d.ts +1 -0
- package/dist/devtools/LogsPanel.js +12 -0
- package/dist/devtools/NetPanel.d.ts +4 -0
- package/dist/devtools/NetPanel.js +10 -0
- package/dist/devtools/PerfPanel.d.ts +4 -0
- package/dist/devtools/PerfPanel.js +53 -0
- package/dist/devtools/TunePanel.d.ts +3 -0
- package/dist/devtools/TunePanel.js +309 -0
- package/dist/devtools/agentBridge.d.ts +41 -0
- package/dist/devtools/agentBridge.js +266 -0
- package/dist/devtools/collisionDebug.d.ts +6 -0
- package/dist/devtools/collisionDebug.js +6 -0
- package/dist/devtools/collisionDebugMath.d.ts +8 -2
- package/dist/devtools/collisionDebugMath.js +24 -5
- package/dist/devtools/devtoolsOverrides.d.ts +6 -0
- package/dist/devtools/devtoolsOverrides.js +48 -0
- package/dist/devtools/panelAtoms.d.ts +9 -0
- package/dist/devtools/panelAtoms.js +10 -0
- package/dist/devtools/perfDiagnose.d.ts +2 -0
- package/dist/devtools/perfDiagnose.js +43 -0
- package/dist/diagnostics/RuntimeDiagnostics.d.ts +36 -0
- package/dist/diagnostics/RuntimeDiagnostics.js +80 -0
- package/dist/drivers/FrameDriver.d.ts +37 -0
- package/dist/drivers/FrameDriver.js +232 -0
- package/dist/drivers/HudOnlyDriver.d.ts +16 -0
- package/dist/drivers/HudOnlyDriver.js +60 -0
- package/dist/environment/Daylight.d.ts +23 -7
- package/dist/environment/Daylight.js +106 -13
- package/dist/environment/EnvironmentScene.js +44 -9
- package/dist/environment/VolumetricClouds.d.ts +17 -0
- package/dist/environment/VolumetricClouds.js +202 -0
- package/dist/environment/daylightCycle.d.ts +3 -1
- package/dist/environment/daylightCycle.js +3 -1
- package/dist/environment/groundPadMath.d.ts +3 -0
- package/dist/environment/groundPadMath.js +3 -0
- package/dist/environment/index.d.ts +1 -0
- package/dist/environment/index.js +1 -0
- package/dist/environment/skyLightingPolicy.d.ts +2 -0
- package/dist/environment/skyLightingPolicy.js +2 -0
- package/dist/environment/terrainGroundColor.d.ts +9 -0
- package/dist/environment/terrainGroundColor.js +22 -0
- package/dist/gameKit.d.ts +27 -0
- package/dist/gameKit.js +29 -0
- package/dist/hotbarActions.d.ts +26 -0
- package/dist/hotbarActions.js +27 -0
- package/dist/inputSink.d.ts +16 -4
- package/dist/inputSink.js +57 -7
- package/dist/map/MapMarkerBeacons.d.ts +3 -2
- package/dist/materialOverride.d.ts +10 -0
- package/dist/materialOverride.js +15 -3
- package/dist/multiplayer.js +8 -2
- package/dist/postfx/PostProcessing.d.ts +9 -1
- package/dist/postfx/PostProcessing.js +56 -11
- package/dist/postfx/gradeShader.js +9 -1
- package/dist/presentationResolve.d.ts +35 -0
- package/dist/presentationResolve.js +51 -0
- package/dist/registry.d.ts +4 -2
- package/dist/render/EnvironmentLighting.d.ts +10 -0
- package/dist/render/EnvironmentLighting.js +34 -0
- package/dist/render/SceneLighting.d.ts +7 -0
- package/dist/render/SceneLighting.js +18 -0
- package/dist/render/SceneModels.d.ts +14 -0
- package/dist/render/SceneModels.js +337 -0
- package/dist/render/assetBase.d.ts +17 -0
- package/dist/render/assetBase.js +35 -0
- package/dist/render/defaultSurface.d.ts +29 -0
- package/dist/render/defaultSurface.js +101 -0
- package/dist/render/modelLoad.d.ts +24 -0
- package/dist/render/modelLoad.js +76 -0
- package/dist/render/modelRender.d.ts +11 -0
- package/dist/render/modelRender.js +15 -0
- package/dist/render/resolveModel.d.ts +54 -3
- package/dist/render/resolveModel.js +116 -4
- package/dist/render/useEntityRenderCues.d.ts +13 -0
- package/dist/render/useEntityRenderCues.js +62 -0
- package/dist/scatter/InstancedScatter.d.ts +24 -0
- package/dist/scatter/InstancedScatter.js +135 -0
- package/dist/scatter/scatterModels.d.ts +25 -0
- package/dist/scatter/scatterModels.js +61 -0
- package/dist/scatter/scatterProxies.d.ts +6 -0
- package/dist/scatter/scatterProxies.js +118 -0
- package/dist/scatter.d.ts +1 -0
- package/dist/scatter.js +1 -0
- package/dist/scene/AuthoredScene.d.ts +84 -0
- package/dist/scene/AuthoredScene.js +178 -0
- package/dist/scene/CityRenderer.d.ts +2 -0
- package/dist/scene/CityRenderer.js +1031 -0
- package/dist/scene/GeneratedAssetRenderer.d.ts +14 -0
- package/dist/scene/GeneratedAssetRenderer.js +59 -0
- package/dist/scene/GrassFieldRenderer.d.ts +2 -0
- package/dist/scene/GrassFieldRenderer.js +73 -0
- package/dist/scene/PoleLineRenderer.d.ts +2 -0
- package/dist/scene/PoleLineRenderer.js +287 -0
- package/dist/scene/SoilPatchRenderer.d.ts +2 -0
- package/dist/scene/SoilPatchRenderer.js +84 -0
- package/dist/scene/StudioStage.d.ts +37 -0
- package/dist/scene/StudioStage.js +42 -0
- package/dist/scene/WaterRenderer.d.ts +2 -0
- package/dist/scene/WaterRenderer.js +61 -0
- package/dist/scene/builtinSceneKindRenderers.d.ts +2 -0
- package/dist/scene/builtinSceneKindRenderers.js +26 -0
- package/dist/scene/cityWindowMaterial.d.ts +16 -0
- package/dist/scene/cityWindowMaterial.js +81 -0
- package/dist/scene/sceneKindRenderers.d.ts +30 -0
- package/dist/scene/sceneKindRenderers.js +17 -0
- package/dist/scene.d.ts +1 -0
- package/dist/scene.js +1 -0
- package/dist/settings/appliedSettings.d.ts +7 -1
- package/dist/settings/appliedSettings.js +13 -2
- package/dist/settings/settingsController.d.ts +2 -0
- package/dist/settings/settingsController.js +22 -1
- package/dist/shellConstants.d.ts +4 -0
- package/dist/shellConstants.js +4 -0
- package/dist/shellMovement.d.ts +8 -0
- package/dist/shellMovement.js +11 -0
- package/dist/shellPointer.d.ts +20 -0
- package/dist/shellPointer.js +28 -0
- package/dist/structures/PlacementGhost.d.ts +4 -0
- package/dist/structures/PlacementGhost.js +4 -0
- package/dist/structures/TransformGizmo.d.ts +53 -0
- package/dist/structures/TransformGizmo.js +59 -0
- package/dist/structures/index.d.ts +1 -0
- package/dist/structures/index.js +1 -0
- package/dist/terrain/GrassField.d.ts +11 -3
- package/dist/terrain/GrassField.js +16 -6
- package/dist/terrain/TerraformBrushCursor.js +3 -0
- package/dist/terrain/grassBudget.d.ts +11 -2
- package/dist/terrain/grassBudget.js +20 -4
- package/dist/terrain/grassGeometry.d.ts +35 -0
- package/dist/terrain/grassGeometry.js +85 -20
- package/dist/terrain/grassMaterial.d.ts +20 -0
- package/dist/terrain/grassMaterial.js +68 -13
- package/dist/terrain/heightfieldGeometry.d.ts +34 -0
- package/dist/terrain/heightfieldGeometry.js +109 -0
- package/dist/terrain/index.d.ts +1 -0
- package/dist/terrain/index.js +1 -0
- package/dist/terrain/random.d.ts +3 -0
- package/dist/terrain/random.js +3 -0
- package/dist/terrain/soilPatchMaterial.d.ts +23 -0
- package/dist/terrain/soilPatchMaterial.js +147 -0
- package/dist/terrain/terrainDetailMaterial.d.ts +19 -2
- package/dist/terrain/terrainDetailMaterial.js +87 -7
- package/dist/terrain/terrainMath.d.ts +8 -1
- package/dist/terrain/terrainMath.js +8 -1
- package/dist/touch/TouchControlsOverlay.d.ts +9 -5
- package/dist/touch/TouchControlsOverlay.js +201 -22
- package/dist/useShellMultiplayerSync.d.ts +9 -0
- package/dist/useShellMultiplayerSync.js +98 -0
- package/dist/visibility/CullingProvider.js +1 -1
- package/dist/water/Ocean.d.ts +7 -1
- package/dist/water/Ocean.js +16 -3
- package/dist/water/OceanConfig.d.ts +11 -0
- package/dist/water/OceanConfig.js +8 -0
- package/dist/water/OceanMaterial.d.ts +12 -0
- package/dist/water/OceanMaterial.js +9 -0
- package/dist/water/OceanShader.d.ts +2 -2
- package/dist/water/OceanShader.js +78 -10
- package/dist/weather/fireSpreadPose.d.ts +1 -0
- package/dist/weather/fireSpreadPose.js +1 -0
- package/dist/weather/weatherGeometry.d.ts +1 -0
- package/dist/weather/weatherGeometry.js +1 -0
- package/dist/weather/weatherMath.d.ts +3 -0
- package/dist/weather/weatherMath.js +3 -0
- package/dist/weather/weatherUniforms.d.ts +3 -0
- package/dist/weather/weatherUniforms.js +3 -0
- package/dist/world/PlaceScene.d.ts +11 -0
- package/dist/world/PlaceScene.js +27 -0
- package/dist/world/RetainedVfx.d.ts +10 -0
- package/dist/world/RetainedVfx.js +136 -0
- package/dist/world/WorldHud.d.ts +49 -2
- package/dist/world/WorldHud.js +87 -2
- package/dist/world/WorldScene.d.ts +21 -0
- package/dist/world/WorldScene.js +186 -0
- package/dist/world/WorldVfx.d.ts +9 -0
- package/dist/world/WorldVfx.js +184 -0
- package/dist/world/entityPose.d.ts +2 -0
- package/dist/world/entityPose.js +2 -0
- package/dist/world/floatTextStyle.d.ts +1 -0
- package/dist/world/floatTextStyle.js +1 -0
- package/dist/world/telegraphPulse.d.ts +1 -0
- package/dist/world/telegraphPulse.js +1 -0
- package/dist/world/worldBarSamples.d.ts +25 -0
- package/dist/world/worldBarSamples.js +38 -0
- package/dist/worldSky.d.ts +11 -0
- package/dist/worldSky.js +21 -0
- package/dist/worldSync.d.ts +2 -1
- package/dist/worldSync.js +2 -1
- package/package.json +6 -6
- package/skills/game-design/SKILL.md +28 -0
- package/skills/game-design/agents/openai.yaml +4 -0
- package/skills/game-design/references/application-playbook.md +170 -0
- package/skills/game-design/references/field-guide.md +288 -0
- package/skills/jgengine/SKILL.md +54 -0
- package/skills/jgengine/api.md +527 -0
- package/skills/jgengine/capabilities.md +131 -0
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-ui/SKILL.md +41 -0
- package/skills/jgengine-ui/api.md +2007 -0
- package/skills/jgengine-ui/capabilities.md +211 -0
- package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
- package/skills/jgengine-ui/reference.md +513 -0
- package/skills/jgengine-ui/references/visual-scorecard.md +73 -0
- package/skills/jgengine-world/SKILL.md +51 -0
- package/skills/jgengine-world/api.md +3056 -0
- package/skills/jgengine-world/capabilities.md +476 -0
- package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
- package/skills/jgengine-world/reference.md +261 -0
- package/skills/level-design/SKILL.md +54 -0
- package/skills/level-design/agents/openai.yaml +4 -0
- package/skills/level-design/references/application-playbook.md +192 -0
- package/skills/level-design/references/field-guide.md +337 -0
- package/skills/level-design/references/live-editor-loop.md +179 -0
- package/dist/cartridge.d.ts +0 -145
- package/dist/cartridge.js +0 -246
- package/llms.txt +0 -2761
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { distance3, resolveEmitterGain } from "@jgengine/core/audio/audioFalloff";
|
|
2
|
+
import { patchDuration } from "@jgengine/core/audio/synth";
|
|
3
|
+
import { createDisposer } from "@jgengine/core/game/defineGame";
|
|
4
|
+
import { clampLoopGain, clampLoopRate } from "./loopParams.js";
|
|
2
5
|
import { MusicDirector } from "./musicDirector.js";
|
|
3
6
|
import { createNoiseBuffer, realizeSynthPatch } from "./synthEngine.js";
|
|
7
|
+
/** setTargetAtTime time constant (~20 ms) for zipper-free live rate/gain ramps on retained loops (#1051). */
|
|
8
|
+
const LOOP_PARAM_SMOOTH_TC = 0.02;
|
|
4
9
|
function createNoopEngine() {
|
|
5
10
|
return {
|
|
6
11
|
setListenerPose: () => undefined,
|
|
@@ -19,6 +24,12 @@ function resolveAudioContextCtor() {
|
|
|
19
24
|
return (window.AudioContext ??
|
|
20
25
|
window.webkitAudioContext);
|
|
21
26
|
}
|
|
27
|
+
function resolveOfflineAudioContextCtor() {
|
|
28
|
+
if (typeof window === "undefined")
|
|
29
|
+
return undefined;
|
|
30
|
+
return (window.OfflineAudioContext ??
|
|
31
|
+
window.webkitOfflineAudioContext);
|
|
32
|
+
}
|
|
22
33
|
export function createAudioEngine(config = {}) {
|
|
23
34
|
const sounds = config.sounds ?? {};
|
|
24
35
|
const busDefs = config.buses ?? {};
|
|
@@ -73,51 +84,144 @@ export function createAudioEngine(config = {}) {
|
|
|
73
84
|
}
|
|
74
85
|
return pending;
|
|
75
86
|
}
|
|
87
|
+
// Retained synth loops render the procedural patch to one cached buffer, then loop it as a plain
|
|
88
|
+
// BufferSource — so live pitch is `playbackRate` and live gain is a gain node, exactly like sample loops (#1051).
|
|
89
|
+
const synthBufferCache = new Map();
|
|
90
|
+
function renderSynthBuffer(soundId, patch) {
|
|
91
|
+
let pending = synthBufferCache.get(soundId);
|
|
92
|
+
if (pending === undefined) {
|
|
93
|
+
const OfflineCtor = resolveOfflineAudioContextCtor();
|
|
94
|
+
if (OfflineCtor === undefined) {
|
|
95
|
+
pending = Promise.resolve(null);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
try {
|
|
99
|
+
const seconds = Math.max(patchDuration(patch), 0.05);
|
|
100
|
+
const frames = Math.max(1, Math.ceil(seconds * context.sampleRate));
|
|
101
|
+
const offline = new OfflineCtor(1, frames, context.sampleRate);
|
|
102
|
+
realizeSynthPatch(offline, offline.destination, createNoiseBuffer(offline), patch);
|
|
103
|
+
pending = offline.startRendering().catch(() => null);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
pending = Promise.resolve(null);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
synthBufferCache.set(soundId, pending);
|
|
110
|
+
}
|
|
111
|
+
return pending;
|
|
112
|
+
}
|
|
76
113
|
let listenerPosition = { x: 0, y: 0, z: 0 };
|
|
114
|
+
const activeSpatialUpdaters = new Set();
|
|
115
|
+
const activeLoops = new Set();
|
|
116
|
+
const NOOP_HANDLE = {
|
|
117
|
+
setPosition: () => undefined,
|
|
118
|
+
setRate: () => undefined,
|
|
119
|
+
setGain: () => undefined,
|
|
120
|
+
stop: () => undefined,
|
|
121
|
+
};
|
|
77
122
|
function playInternal(soundId, position, loop) {
|
|
78
123
|
const sound = sounds[soundId];
|
|
79
124
|
if (sound === undefined)
|
|
80
125
|
return null;
|
|
81
126
|
const bus = busGainNode(sound.bus);
|
|
82
|
-
|
|
83
|
-
|
|
127
|
+
const currentPosition = position ?? listenerPosition;
|
|
128
|
+
// A one-shot synth cue realises its decaying voices live and is fire-and-forget.
|
|
129
|
+
if (sound.synth !== undefined && !loop) {
|
|
84
130
|
const cueGain = context.createGain();
|
|
85
131
|
cueGain.gain.value = resolveEmitterGain(distance3(currentPosition, listenerPosition), sound, 1);
|
|
86
132
|
cueGain.connect(bus);
|
|
87
133
|
realizeSynthPatch(context, cueGain, sharedNoiseBuffer(), sound.synth);
|
|
88
|
-
return
|
|
134
|
+
return NOOP_HANDLE;
|
|
89
135
|
}
|
|
90
|
-
|
|
136
|
+
// Everything else resolves to one AudioBuffer we loop/play: sample URL, or a synth patch rendered once.
|
|
137
|
+
let bufferPromise;
|
|
138
|
+
if (sound.synth !== undefined)
|
|
139
|
+
bufferPromise = renderSynthBuffer(soundId, sound.synth);
|
|
140
|
+
else if (sound.url !== undefined)
|
|
141
|
+
bufferPromise = loadBuffer(sound.url);
|
|
142
|
+
else
|
|
91
143
|
return null;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
144
|
+
return playBufferSource(sound, bus, bufferPromise, currentPosition, loop);
|
|
145
|
+
}
|
|
146
|
+
function playBufferSource(sound, bus, bufferPromise, initialPosition, loop) {
|
|
147
|
+
const disposer = createDisposer();
|
|
148
|
+
let currentPosition = initialPosition;
|
|
149
|
+
// Live control state, applied on source creation so updates that race the async buffer load stick.
|
|
150
|
+
let currentRate = 1;
|
|
151
|
+
let currentUserGain = 1;
|
|
152
|
+
let sourceNode = null;
|
|
153
|
+
let userGainNode = null;
|
|
154
|
+
let falloffGain = null;
|
|
155
|
+
function updateFalloff() {
|
|
156
|
+
if (falloffGain !== null) {
|
|
157
|
+
falloffGain.gain.value = resolveEmitterGain(distance3(currentPosition, listenerPosition), sound, 1);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
void bufferPromise.then((buffer) => {
|
|
161
|
+
if (buffer === null)
|
|
96
162
|
return;
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
163
|
+
const src = context.createBufferSource();
|
|
164
|
+
src.buffer = buffer;
|
|
165
|
+
src.loop = loop || (sound.loop ?? false);
|
|
166
|
+
src.playbackRate.value = currentRate;
|
|
167
|
+
// source → per-loop user gain (setGain) → distance falloff → bus
|
|
168
|
+
const uGain = context.createGain();
|
|
169
|
+
uGain.gain.value = currentUserGain;
|
|
170
|
+
const fGain = context.createGain();
|
|
171
|
+
fGain.gain.value = resolveEmitterGain(distance3(currentPosition, listenerPosition), sound, 1);
|
|
172
|
+
src.connect(uGain);
|
|
173
|
+
uGain.connect(fGain);
|
|
174
|
+
fGain.connect(bus);
|
|
175
|
+
src.start();
|
|
176
|
+
sourceNode = src;
|
|
177
|
+
userGainNode = uGain;
|
|
178
|
+
falloffGain = fGain;
|
|
179
|
+
disposer.onDispose(() => {
|
|
180
|
+
try {
|
|
181
|
+
src.stop();
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
}
|
|
185
|
+
src.disconnect();
|
|
186
|
+
uGain.disconnect();
|
|
187
|
+
fGain.disconnect();
|
|
188
|
+
sourceNode = null;
|
|
189
|
+
userGainNode = null;
|
|
190
|
+
falloffGain = null;
|
|
191
|
+
});
|
|
105
192
|
});
|
|
106
|
-
|
|
193
|
+
if (loop)
|
|
194
|
+
activeSpatialUpdaters.add(updateFalloff);
|
|
195
|
+
const handle = {
|
|
107
196
|
setPosition(next) {
|
|
108
197
|
currentPosition = next;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
198
|
+
updateFalloff();
|
|
199
|
+
},
|
|
200
|
+
setRate(rate) {
|
|
201
|
+
currentRate = clampLoopRate(rate);
|
|
202
|
+
if (sourceNode !== null)
|
|
203
|
+
sourceNode.playbackRate.setTargetAtTime(currentRate, context.currentTime, LOOP_PARAM_SMOOTH_TC);
|
|
204
|
+
},
|
|
205
|
+
setGain(gain) {
|
|
206
|
+
currentUserGain = clampLoopGain(gain);
|
|
207
|
+
if (userGainNode !== null)
|
|
208
|
+
userGainNode.gain.setTargetAtTime(currentUserGain, context.currentTime, LOOP_PARAM_SMOOTH_TC);
|
|
112
209
|
},
|
|
113
210
|
stop() {
|
|
114
|
-
|
|
211
|
+
activeSpatialUpdaters.delete(updateFalloff);
|
|
212
|
+
activeLoops.delete(handle);
|
|
213
|
+
disposer.dispose();
|
|
115
214
|
},
|
|
116
215
|
};
|
|
216
|
+
if (loop)
|
|
217
|
+
activeLoops.add(handle);
|
|
218
|
+
return handle;
|
|
117
219
|
}
|
|
118
220
|
return {
|
|
119
221
|
setListenerPose(position) {
|
|
120
222
|
listenerPosition = position;
|
|
223
|
+
for (const updateGain of activeSpatialUpdaters)
|
|
224
|
+
updateGain();
|
|
121
225
|
},
|
|
122
226
|
playOneShot(soundId, position) {
|
|
123
227
|
void context.resume().catch(() => undefined);
|
|
@@ -140,6 +244,9 @@ export function createAudioEngine(config = {}) {
|
|
|
140
244
|
void context.resume().catch(() => undefined);
|
|
141
245
|
},
|
|
142
246
|
dispose() {
|
|
247
|
+
for (const handle of [...activeLoops])
|
|
248
|
+
handle.stop();
|
|
249
|
+
activeSpatialUpdaters.clear();
|
|
143
250
|
director?.dispose();
|
|
144
251
|
void context.close().catch(() => undefined);
|
|
145
252
|
},
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { AudioEngine } from "./audioEngine.js";
|
|
2
|
+
type AudioPlayPayload = {
|
|
3
|
+
sound: string;
|
|
4
|
+
at?: readonly [number, number, number];
|
|
5
|
+
};
|
|
6
|
+
type AudioMusicPayload = {
|
|
7
|
+
theme: string | null;
|
|
8
|
+
transpose?: number;
|
|
9
|
+
};
|
|
10
|
+
type AudioLoopStartPayload = {
|
|
11
|
+
id: string;
|
|
12
|
+
sound: string;
|
|
13
|
+
at?: readonly [number, number, number];
|
|
14
|
+
};
|
|
15
|
+
type AudioLoopSetPayload = {
|
|
16
|
+
id: string;
|
|
17
|
+
rate?: number;
|
|
18
|
+
gain?: number;
|
|
19
|
+
at?: readonly [number, number, number];
|
|
20
|
+
};
|
|
21
|
+
type AudioLoopStopPayload = {
|
|
22
|
+
id: string;
|
|
23
|
+
};
|
|
24
|
+
/** Minimal event bus the shell wires to the audio engine, including retained id-keyed loops (#1051). @internal */
|
|
25
|
+
export type AudioEventBus = {
|
|
26
|
+
on(event: "audio.play", handler: (payload: AudioPlayPayload) => void): () => void;
|
|
27
|
+
on(event: "audio.music", handler: (payload: AudioMusicPayload) => void): () => void;
|
|
28
|
+
on(event: "audio.resume", handler: () => void): () => void;
|
|
29
|
+
on(event: "audio.loopStart", handler: (payload: AudioLoopStartPayload) => void): () => void;
|
|
30
|
+
on(event: "audio.loopSet", handler: (payload: AudioLoopSetPayload) => void): () => void;
|
|
31
|
+
on(event: "audio.loopStop", handler: (payload: AudioLoopStopPayload) => void): () => void;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Subscribe game audio events to the shell audio engine. Manages the retained-loop registry
|
|
35
|
+
* (`audio.loopStart`/`audio.loopSet`/`audio.loopStop`), keyed by stable id. Returns a single
|
|
36
|
+
* unsubscribe that tears every listener down and stops any loops still playing.
|
|
37
|
+
* @internal
|
|
38
|
+
*/
|
|
39
|
+
export declare function attachAudioEventWire(events: AudioEventBus, audioEngine: Pick<AudioEngine, "playOneShot" | "playLoop" | "playMusic" | "resume">): () => void;
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
function toVec(at) {
|
|
2
|
+
return at === undefined ? undefined : { x: at[0], y: at[1], z: at[2] };
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Subscribe game audio events to the shell audio engine. Manages the retained-loop registry
|
|
6
|
+
* (`audio.loopStart`/`audio.loopSet`/`audio.loopStop`), keyed by stable id. Returns a single
|
|
7
|
+
* unsubscribe that tears every listener down and stops any loops still playing.
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
export function attachAudioEventWire(events, audioEngine) {
|
|
11
|
+
const loops = new Map();
|
|
12
|
+
const offPlay = events.on("audio.play", ({ sound, at }) => {
|
|
13
|
+
audioEngine.playOneShot(sound, toVec(at));
|
|
14
|
+
});
|
|
15
|
+
const offMusic = events.on("audio.music", ({ theme, transpose }) => {
|
|
16
|
+
audioEngine.playMusic(theme, transpose === undefined ? undefined : { transpose });
|
|
17
|
+
});
|
|
18
|
+
const offResume = events.on("audio.resume", () => audioEngine.resume());
|
|
19
|
+
const offLoopStart = events.on("audio.loopStart", ({ id, sound, at }) => {
|
|
20
|
+
const existing = loops.get(id);
|
|
21
|
+
if (existing !== undefined) {
|
|
22
|
+
// Same sound: idempotent — keep the running source so a per-tick re-issue never clicks.
|
|
23
|
+
if (existing.sound === sound)
|
|
24
|
+
return;
|
|
25
|
+
existing.handle.stop();
|
|
26
|
+
loops.delete(id);
|
|
27
|
+
}
|
|
28
|
+
const handle = audioEngine.playLoop(sound, toVec(at));
|
|
29
|
+
if (handle !== null)
|
|
30
|
+
loops.set(id, { sound, handle });
|
|
31
|
+
});
|
|
32
|
+
const offLoopSet = events.on("audio.loopSet", ({ id, rate, gain, at }) => {
|
|
33
|
+
const entry = loops.get(id);
|
|
34
|
+
// Unknown id: a live update may race a stop — silently drop it.
|
|
35
|
+
if (entry === undefined)
|
|
36
|
+
return;
|
|
37
|
+
if (rate !== undefined)
|
|
38
|
+
entry.handle.setRate(rate);
|
|
39
|
+
if (gain !== undefined)
|
|
40
|
+
entry.handle.setGain(gain);
|
|
41
|
+
if (at !== undefined)
|
|
42
|
+
entry.handle.setPosition({ x: at[0], y: at[1], z: at[2] });
|
|
43
|
+
});
|
|
44
|
+
const offLoopStop = events.on("audio.loopStop", ({ id }) => {
|
|
45
|
+
const entry = loops.get(id);
|
|
46
|
+
if (entry === undefined)
|
|
47
|
+
return;
|
|
48
|
+
entry.handle.stop();
|
|
49
|
+
loops.delete(id);
|
|
50
|
+
});
|
|
51
|
+
return () => {
|
|
52
|
+
offPlay();
|
|
53
|
+
offMusic();
|
|
54
|
+
offResume();
|
|
55
|
+
offLoopStart();
|
|
56
|
+
offLoopSet();
|
|
57
|
+
offLoopStop();
|
|
58
|
+
for (const { handle } of loops.values())
|
|
59
|
+
handle.stop();
|
|
60
|
+
loops.clear();
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure clamps for retained-loop live control (#1051). Kept dependency-free so the
|
|
3
|
+
* playback-rate/gain windows are one testable source of truth the audio engine reads.
|
|
4
|
+
*/
|
|
5
|
+
/** Lowest live playback-rate multiplier (two octaves below authored pitch). */
|
|
6
|
+
export declare const MIN_LOOP_RATE = 0.25;
|
|
7
|
+
/** Highest live playback-rate multiplier (two octaves above authored pitch). */
|
|
8
|
+
export declare const MAX_LOOP_RATE = 4;
|
|
9
|
+
/**
|
|
10
|
+
* Clamp a loop playback-rate multiplier into the safe pitch window `MIN_LOOP_RATE`–`MAX_LOOP_RATE`
|
|
11
|
+
* (1 = authored pitch). Non-finite input (NaN/±Infinity) falls back to the authored `1`.
|
|
12
|
+
* @internal — the audio engine applies this; game code sets rate through `ctx.game.audio.setLoop`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function clampLoopRate(rate: number): number;
|
|
15
|
+
/**
|
|
16
|
+
* Clamp a loop gain multiplier into `0`–`1`. Non-finite input falls back to `0` (silence) so a
|
|
17
|
+
* stray NaN never blasts a loop to full volume.
|
|
18
|
+
* @internal — the audio engine applies this; game code sets gain through `ctx.game.audio.setLoop`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function clampLoopGain(gain: number): number;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure clamps for retained-loop live control (#1051). Kept dependency-free so the
|
|
3
|
+
* playback-rate/gain windows are one testable source of truth the audio engine reads.
|
|
4
|
+
*/
|
|
5
|
+
/** Lowest live playback-rate multiplier (two octaves below authored pitch). */
|
|
6
|
+
export const MIN_LOOP_RATE = 0.25;
|
|
7
|
+
/** Highest live playback-rate multiplier (two octaves above authored pitch). */
|
|
8
|
+
export const MAX_LOOP_RATE = 4;
|
|
9
|
+
/**
|
|
10
|
+
* Clamp a loop playback-rate multiplier into the safe pitch window `MIN_LOOP_RATE`–`MAX_LOOP_RATE`
|
|
11
|
+
* (1 = authored pitch). Non-finite input (NaN/±Infinity) falls back to the authored `1`.
|
|
12
|
+
* @internal — the audio engine applies this; game code sets rate through `ctx.game.audio.setLoop`.
|
|
13
|
+
*/
|
|
14
|
+
export function clampLoopRate(rate) {
|
|
15
|
+
if (!Number.isFinite(rate))
|
|
16
|
+
return 1;
|
|
17
|
+
return Math.min(MAX_LOOP_RATE, Math.max(MIN_LOOP_RATE, rate));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Clamp a loop gain multiplier into `0`–`1`. Non-finite input falls back to `0` (silence) so a
|
|
21
|
+
* stray NaN never blasts a loop to full volume.
|
|
22
|
+
* @internal — the audio engine applies this; game code sets gain through `ctx.game.audio.setLoop`.
|
|
23
|
+
*/
|
|
24
|
+
export function clampLoopGain(gain) {
|
|
25
|
+
if (!Number.isFinite(gain))
|
|
26
|
+
return 0;
|
|
27
|
+
return Math.min(1, Math.max(0, gain));
|
|
28
|
+
}
|
package/dist/behaviour.d.ts
CHANGED
|
@@ -8,5 +8,6 @@ export { attachObject3D, Object3DBehaviour } from "./behaviourAttach.js";
|
|
|
8
8
|
* receive — pass the game clock's scaling to keep behaviours on game time, or
|
|
9
9
|
* omit for real-time seconds. Games driving `world.update` themselves from
|
|
10
10
|
* `loop.onTick` should not also render this.
|
|
11
|
-
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
12
13
|
export declare function useBehaviourWorld(world: BehaviourWorld, scaleDt?: (rawDt: number) => number): void;
|
package/dist/behaviour.js
CHANGED
|
@@ -10,7 +10,8 @@ export { attachObject3D, Object3DBehaviour } from "./behaviourAttach.js";
|
|
|
10
10
|
* receive — pass the game clock's scaling to keep behaviours on game time, or
|
|
11
11
|
* omit for real-time seconds. Games driving `world.update` themselves from
|
|
12
12
|
* `loop.onTick` should not also render this.
|
|
13
|
-
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
14
15
|
export function useBehaviourWorld(world, scaleDt) {
|
|
15
16
|
const driverRef = useRef(createBehaviourWorldDriver(world));
|
|
16
17
|
useEffect(() => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Object3D } from "three";
|
|
2
2
|
import { Behaviour, type BehaviourWorld } from "@jgengine/core/behaviour/behaviour";
|
|
3
|
+
/** @internal */
|
|
3
4
|
export declare class Object3DBehaviour extends Behaviour {
|
|
4
5
|
/** @internal */ _object: Object3D | null;
|
|
5
6
|
get object(): Object3D;
|
package/dist/behaviourAttach.js
CHANGED
package/dist/behaviourDriver.js
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ActionStateTracker } from "@jgengine/core/input/actionBindings";
|
|
2
|
+
import type { Aim } from "@jgengine/core/scene/spatial";
|
|
3
|
+
import type { GameContext } from "@jgengine/core/runtime/gameContext";
|
|
4
|
+
import type { ActionCodesMap } from "@jgengine/core/input/actionBindings";
|
|
5
|
+
import { type CommandSink } from "./commandSink.js";
|
|
6
|
+
/** Actions the shell consumes natively and never routes to a same-named command. @internal */
|
|
7
|
+
export declare const RESERVED_INPUT_ACTIONS: ReadonlySet<string>;
|
|
8
|
+
/** Actions from `input` currently held down, for `ctx.input.publish` (#164.1); includes reserved movement/jump actions. @internal */
|
|
9
|
+
export declare function heldActionsFor(tracker: Pick<ActionStateTracker<string>, "isDown">, actions: readonly string[]): string[];
|
|
10
|
+
/** Whether a bound action should fire this frame: on press, or on repeat interval while held (shared by `FrameDriver` and `HudOnlyDriver`). @internal */
|
|
11
|
+
export declare function shouldFireBoundAction(tracker: Pick<ActionStateTracker<string>, "isDown" | "wasPressed">, action: string, input: ActionCodesMap | undefined, repeatFiredAt: ReadonlyMap<string, number>, now: number): boolean;
|
|
12
|
+
/** Resolves and runs the command bound to `action` via the shell's action→command convention (shared by `FrameDriver` and `HudOnlyDriver`). @internal */
|
|
13
|
+
export declare function dispatchBoundAction(ctx: GameContext, action: string, yaw: number, pitch: number, aim: Aim, reserved?: ReadonlySet<string>, sink?: CommandSink): void;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { actionRepeatMs, resolveActionCommand, shouldDispatchAction, } from "@jgengine/core/input/actionBindings";
|
|
2
|
+
import { localCommandSink } from "./commandSink.js";
|
|
3
|
+
/** Actions the shell consumes natively and never routes to a same-named command. @internal */
|
|
4
|
+
export const RESERVED_INPUT_ACTIONS = new Set([
|
|
5
|
+
"moveForward",
|
|
6
|
+
"moveBack",
|
|
7
|
+
"moveLeft",
|
|
8
|
+
"moveRight",
|
|
9
|
+
"turnLeft",
|
|
10
|
+
"turnRight",
|
|
11
|
+
"sprint",
|
|
12
|
+
"jump",
|
|
13
|
+
"tabTarget",
|
|
14
|
+
"clearTarget",
|
|
15
|
+
"useAbility",
|
|
16
|
+
"interact",
|
|
17
|
+
]);
|
|
18
|
+
/** Actions from `input` currently held down, for `ctx.input.publish` (#164.1); includes reserved movement/jump actions. @internal */
|
|
19
|
+
export function heldActionsFor(tracker, actions) {
|
|
20
|
+
return actions.filter((action) => tracker.isDown(action));
|
|
21
|
+
}
|
|
22
|
+
/** Whether a bound action should fire this frame: on press, or on repeat interval while held (shared by `FrameDriver` and `HudOnlyDriver`). @internal */
|
|
23
|
+
export function shouldFireBoundAction(tracker, action, input, repeatFiredAt, now) {
|
|
24
|
+
return shouldDispatchAction({
|
|
25
|
+
pressed: tracker.wasPressed(action),
|
|
26
|
+
down: tracker.isDown(action),
|
|
27
|
+
repeatMs: actionRepeatMs(input?.[action]),
|
|
28
|
+
lastFiredAt: repeatFiredAt.get(action) ?? null,
|
|
29
|
+
now,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/** Resolves and runs the command bound to `action` via the shell's action→command convention (shared by `FrameDriver` and `HudOnlyDriver`). @internal */
|
|
33
|
+
export function dispatchBoundAction(ctx, action, yaw, pitch, aim, reserved = RESERVED_INPUT_ACTIONS, sink = localCommandSink(ctx)) {
|
|
34
|
+
const command = resolveActionCommand(action, (name) => ctx.game.commands.has(name), reserved);
|
|
35
|
+
if (command !== null)
|
|
36
|
+
sink.run(command, { yaw, pitch, aim });
|
|
37
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { type MutableRefObject } from "react";
|
|
1
|
+
import { type ComponentType, type MutableRefObject } from "react";
|
|
2
2
|
import type { GameCameraConfig } from "@jgengine/core/game/playableGame";
|
|
3
3
|
import type { CameraDirector } from "@jgengine/core/runtime/cameraDirector";
|
|
4
|
+
import { type ViewmodelProps } from "./GameFirstPersonCamera.js";
|
|
4
5
|
export { resolveRigKind } from "./rigResolve.js";
|
|
5
6
|
export interface GameCameraRigProps {
|
|
6
7
|
yawRef: MutableRefObject<number>;
|
|
@@ -12,5 +13,7 @@ export interface GameCameraRigProps {
|
|
|
12
13
|
panKeysEnabled?: boolean;
|
|
13
14
|
/** 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
15
|
director?: CameraDirector;
|
|
16
|
+
/** Custom first-person viewmodel (#542); read only when the resolved rig is `"first"`. */
|
|
17
|
+
viewmodel?: ComponentType<ViewmodelProps>;
|
|
15
18
|
}
|
|
16
|
-
export declare function GameCameraRig({ yawRef, pitchRef, config, onDragChange, pointerControls, panKeysEnabled, director, }: GameCameraRigProps): import("react").JSX.Element;
|
|
19
|
+
export declare function GameCameraRig({ yawRef, pitchRef, config, onDragChange, pointerControls, panKeysEnabled, director, viewmodel, }: GameCameraRigProps): import("react").JSX.Element;
|
|
@@ -8,7 +8,7 @@ import { resolveDirectedCamera } from "./rigMath.js";
|
|
|
8
8
|
import { resolveRigKind, turntableAsObserver } from "./rigResolve.js";
|
|
9
9
|
import { CameraShakeContext, createCameraShakeChannel } from "./shakeChannel.js";
|
|
10
10
|
export { resolveRigKind } from "./rigResolve.js";
|
|
11
|
-
export function GameCameraRig({ yawRef, pitchRef, config, onDragChange, pointerControls, panKeysEnabled, director, }) {
|
|
11
|
+
export function GameCameraRig({ yawRef, pitchRef, config, onDragChange, pointerControls, panKeysEnabled, director, viewmodel, }) {
|
|
12
12
|
const channel = useMemo(() => createCameraShakeChannel(config?.shake?.decayPerSecond), [config?.shake?.decayPerSecond]);
|
|
13
13
|
const [, notifyDirectorChange] = useReducer((count) => count + 1, 0);
|
|
14
14
|
useEffect(() => {
|
|
@@ -22,12 +22,17 @@ export function GameCameraRig({ yawRef, pitchRef, config, onDragChange, pointerC
|
|
|
22
22
|
const followEntityId = directed.followEntityId;
|
|
23
23
|
const rig = (() => {
|
|
24
24
|
if (directed.cinematic !== undefined) {
|
|
25
|
-
|
|
25
|
+
// A director-set cinematic auto-clears when its keyframe path completes, so a
|
|
26
|
+
// one-shot flyover can never strand the camera if a game-side clear timer drifts
|
|
27
|
+
// (the rig plays in real time; sim-clock timers do not). Static config cinematics
|
|
28
|
+
// hold their final frame as before.
|
|
29
|
+
const owner = director !== undefined && director.cinematic() !== null ? director : undefined;
|
|
30
|
+
return (_jsx(CinematicRig, { yawRef: yawRef, pitchRef: pitchRef, config: { ...config, cinematic: directed.cinematic }, followEntityId: followEntityId, absoluteFov: true, ...(owner === undefined ? {} : { onComplete: () => owner.setCinematic(null) }) }));
|
|
26
31
|
}
|
|
27
32
|
const kind = resolveRigKind(config);
|
|
28
33
|
switch (kind) {
|
|
29
34
|
case "first":
|
|
30
|
-
return (_jsx(GameFirstPersonCamera, { yawRef: yawRef, pitchRef: pitchRef, config: config?.firstPerson, followEntityId: followEntityId ?? undefined }));
|
|
35
|
+
return (_jsx(GameFirstPersonCamera, { yawRef: yawRef, pitchRef: pitchRef, config: config?.firstPerson, followEntityId: followEntityId ?? undefined, viewmodel: viewmodel }));
|
|
31
36
|
case "topDown":
|
|
32
37
|
return _jsx(TopDownRig, { yawRef: yawRef, pitchRef: pitchRef, config: config, followEntityId: followEntityId });
|
|
33
38
|
case "rts":
|
|
@@ -1,12 +1,19 @@
|
|
|
1
|
-
import { type MutableRefObject } from "react";
|
|
1
|
+
import { type ComponentType, type MutableRefObject } from "react";
|
|
2
2
|
import * as THREE from "three";
|
|
3
3
|
import type { FirstPersonCameraConfig } from "@jgengine/core/game/playableGame";
|
|
4
|
+
import type { EntityRenderCues } from "@jgengine/core/combat/renderCues";
|
|
4
5
|
/** World position of the first-person weapon muzzle, or false when no viewmodel is mounted. */
|
|
5
6
|
export declare function readFirstPersonMuzzle(target: THREE.Vector3): boolean;
|
|
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
|
+
export interface ViewmodelProps {
|
|
9
|
+
cuesRef: MutableRefObject<EntityRenderCues>;
|
|
10
|
+
}
|
|
6
11
|
export interface GameFirstPersonCameraProps {
|
|
7
12
|
yawRef: MutableRefObject<number>;
|
|
8
13
|
pitchRef: MutableRefObject<number>;
|
|
9
14
|
config?: FirstPersonCameraConfig;
|
|
10
15
|
followEntityId?: string;
|
|
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
|
+
viewmodel?: ComponentType<ViewmodelProps>;
|
|
11
18
|
}
|
|
12
|
-
export declare function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId, }: GameFirstPersonCameraProps): import("react").JSX.Element | null;
|
|
19
|
+
export declare function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId, viewmodel, }: GameFirstPersonCameraProps): import("react").JSX.Element | null;
|
|
@@ -5,6 +5,7 @@ import * as THREE from "three";
|
|
|
5
5
|
import { DEFAULT_EYE_HEIGHT } from "@jgengine/core/combat/shotOrigin";
|
|
6
6
|
import { useGameContext } from "@jgengine/react/provider";
|
|
7
7
|
import { usePlayer } from "@jgengine/react/hooks";
|
|
8
|
+
import { useEntityRenderCues } from "../render/useEntityRenderCues.js";
|
|
8
9
|
import { usePlayerFov } from "./PlayerFov.js";
|
|
9
10
|
import { GAME_SIM_FRAME_PRIORITY, ORBIT_CAMERA_FRAME_PRIORITY } from "./orbitCameraMath.js";
|
|
10
11
|
const DEFAULT_SENSITIVITY = 0.0025;
|
|
@@ -21,7 +22,7 @@ export function readFirstPersonMuzzle(target) {
|
|
|
21
22
|
target.copy(muzzleWorld);
|
|
22
23
|
return true;
|
|
23
24
|
}
|
|
24
|
-
export function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId, }) {
|
|
25
|
+
export function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId, viewmodel, }) {
|
|
25
26
|
const eyeHeight = config?.eyeHeight ?? DEFAULT_EYE_HEIGHT;
|
|
26
27
|
const sensitivity = config?.sensitivity ?? DEFAULT_SENSITIVITY;
|
|
27
28
|
const maxPitch = config?.maxPitch ?? DEFAULT_MAX_PITCH;
|
|
@@ -32,6 +33,7 @@ export function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId
|
|
|
32
33
|
const domElement = useThree((state) => state.gl.domElement);
|
|
33
34
|
const followId = followEntityId ?? userId;
|
|
34
35
|
const seededRef = useRef(false);
|
|
36
|
+
const cuesRef = useEntityRenderCues(followId);
|
|
35
37
|
useEffect(() => {
|
|
36
38
|
const requestLock = () => {
|
|
37
39
|
if (window.matchMedia?.("(pointer: coarse)").matches)
|
|
@@ -73,9 +75,9 @@ export function GameFirstPersonCamera({ yawRef, pitchRef, config, followEntityId
|
|
|
73
75
|
}, ORBIT_CAMERA_FRAME_PRIORITY);
|
|
74
76
|
if (config?.viewmodel === false)
|
|
75
77
|
return null;
|
|
76
|
-
return _jsx(FirstPersonViewmodel, { camera: camera });
|
|
78
|
+
return _jsx(FirstPersonViewmodel, { camera: camera, viewmodel: viewmodel, cuesRef: cuesRef });
|
|
77
79
|
}
|
|
78
|
-
function FirstPersonViewmodel({ camera }) {
|
|
80
|
+
function FirstPersonViewmodel({ camera, viewmodel: Viewmodel, cuesRef, }) {
|
|
79
81
|
const groupRef = useRef(null);
|
|
80
82
|
useEffect(() => () => {
|
|
81
83
|
muzzleTracked = false;
|
|
@@ -92,5 +94,8 @@ function FirstPersonViewmodel({ camera }) {
|
|
|
92
94
|
muzzleWorld.copy(MUZZLE_OFFSET).applyQuaternion(camera.quaternion).add(camera.position);
|
|
93
95
|
muzzleTracked = true;
|
|
94
96
|
}, GAME_SIM_FRAME_PRIORITY);
|
|
97
|
+
if (Viewmodel !== undefined) {
|
|
98
|
+
return (_jsx("group", { ref: groupRef, children: _jsx(Viewmodel, { cuesRef: cuesRef }) }));
|
|
99
|
+
}
|
|
95
100
|
return (_jsxs("group", { ref: groupRef, children: [_jsxs("mesh", { position: [0, 0, -0.22], children: [_jsx("boxGeometry", { args: [0.09, 0.11, 0.55] }), _jsx("meshStandardMaterial", { color: "#22262d", metalness: 0.6, roughness: 0.35 })] }), _jsxs("mesh", { position: [0, -0.13, 0.04], rotation: [0.35, 0, 0], children: [_jsx("boxGeometry", { args: [0.08, 0.18, 0.11] }), _jsx("meshStandardMaterial", { color: "#33373f", metalness: 0.4, roughness: 0.5 })] }), _jsxs("mesh", { position: [0, 0.03, -0.52], rotation: [Math.PI / 2, 0, 0], children: [_jsx("cylinderGeometry", { args: [0.022, 0.022, 0.18, 10] }), _jsx("meshStandardMaterial", { color: "#0e0f12", metalness: 0.7, roughness: 0.3 })] })] }));
|
|
96
101
|
}
|
|
@@ -2,5 +2,5 @@ import { type InspectionCameraConfig } from "./inspectionCameraMath.js";
|
|
|
2
2
|
export interface GameInspectionCameraProps {
|
|
3
3
|
config?: InspectionCameraConfig;
|
|
4
4
|
}
|
|
5
|
-
/** Model-viewer
|
|
5
|
+
/** Model-viewer / editor rig (#207.7, #866): left-click selects (editor), middle-drag pans, right-drag orbits, scroll zooms toward a configurable anchor. Orbits a fixed `target`; never reads player/entity state. */
|
|
6
6
|
export declare function GameInspectionCamera({ config: configPatch }: GameInspectionCameraProps): import("react").JSX.Element;
|
|
@@ -4,7 +4,7 @@ import { useThree } from "@react-three/fiber";
|
|
|
4
4
|
import { useEffect, useRef } from "react";
|
|
5
5
|
import { MOUSE } from "three";
|
|
6
6
|
import { resolveInspectionCameraConfig, resolveInspectionZoomToCursor, seedInspectionCamera, } from "./inspectionCameraMath.js";
|
|
7
|
-
/** Model-viewer
|
|
7
|
+
/** Model-viewer / editor rig (#207.7, #866): left-click selects (editor), middle-drag pans, right-drag orbits, scroll zooms toward a configurable anchor. Orbits a fixed `target`; never reads player/entity state. */
|
|
8
8
|
export function GameInspectionCamera({ config: configPatch }) {
|
|
9
9
|
const config = resolveInspectionCameraConfig(configPatch);
|
|
10
10
|
const controlsRef = useRef(null);
|
|
@@ -31,5 +31,5 @@ export function GameInspectionCamera({ config: configPatch }) {
|
|
|
31
31
|
camera.position.add(controls.target.clone().sub(previous));
|
|
32
32
|
controls.update();
|
|
33
33
|
}, [camera, config.target.x, config.target.y, config.target.z]);
|
|
34
|
-
return (_jsx(OrbitControls, { ref: controlsRef, makeDefault: true, 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:
|
|
34
|
+
return (_jsx(OrbitControls, { ref: controlsRef, makeDefault: true, 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: undefined, MIDDLE: MOUSE.PAN, RIGHT: MOUSE.ROTATE } }));
|
|
35
35
|
}
|