@jgengine/shell 0.19.0 → 0.22.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 +364 -0
- package/dist/GameHost.d.ts +12 -1
- package/dist/GameHost.js +14 -1
- package/dist/GamePlayerShell.js +113 -15
- package/dist/Shell3dPresentation.d.ts +2 -2
- package/dist/Shell3dPresentation.js +49 -7
- package/dist/ShellChrome.d.ts +2 -1
- package/dist/ShellChrome.js +22 -6
- package/dist/ShellHudPresentation.d.ts +11 -1
- package/dist/ShellHudPresentation.js +20 -6
- package/dist/audio/audioEngine.d.ts +16 -0
- package/dist/audio/audioEngine.js +178 -89
- package/dist/audio/audioWire.js +2 -2
- package/dist/audio/synthEngine.d.ts +7 -1
- package/dist/audio/synthEngine.js +103 -38
- package/dist/camera/GameCameraRig.js +2 -1
- package/dist/camera/GameFirstPersonCamera.d.ts +2 -2
- package/dist/camera/GameFirstPersonCamera.js +17 -13
- package/dist/camera/cameraRigs.js +8 -2
- 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/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 +12 -5
- package/dist/defineGame.js +25 -10
- package/dist/devtools/DevtoolsOverlay.d.ts +2 -69
- package/dist/devtools/DevtoolsOverlay.js +14 -168
- package/dist/devtools/DevtoolsRuntime.d.ts +7 -0
- package/dist/devtools/DevtoolsRuntime.js +61 -0
- package/dist/devtools/agentBridge.js +1 -1
- 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/drivers/FrameDriver.js +25 -21
- package/dist/drivers/HudOnlyDriver.d.ts +6 -1
- package/dist/drivers/HudOnlyDriver.js +32 -14
- package/dist/environment/EnvironmentScene.js +19 -15
- package/dist/environment/terrainGroundColor.d.ts +5 -4
- package/dist/environment/terrainGroundColor.js +19 -8
- 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 +10 -0
- package/dist/postfx/PostProcessing.js +9 -41
- 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/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 +51 -77
- package/dist/render/authoredSurfaceMaterial.d.ts +43 -0
- package/dist/render/authoredSurfaceMaterial.js +108 -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 +3 -0
- package/dist/render/modelLoad.js +9 -0
- 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 +21 -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.js +76 -11
- package/dist/render/useModelAnimation.d.ts +28 -2
- package/dist/render/useModelAnimation.js +194 -69
- 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 +14 -6
- package/dist/scene/AuthoredScene.js +30 -9
- package/dist/scene/GeneratedAssetRenderer.js +11 -42
- package/dist/scene/GrassFieldRenderer.js +20 -2
- package/dist/settings/SettingsMenu.js +11 -18
- package/dist/settings/SettingsRuntime.d.ts +5 -0
- package/dist/settings/SettingsRuntime.js +29 -2
- 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 +34 -59
- package/dist/structures/BuildingRenderBudget.d.ts +23 -0
- package/dist/structures/BuildingRenderBudget.js +54 -0
- package/dist/structures/GeneratedBuilding.js +13 -8
- 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/terrain/CarvedTerrain.d.ts +2 -1
- package/dist/terrain/CarvedTerrain.js +2 -2
- package/dist/terrain/GrassField.d.ts +3 -1
- package/dist/terrain/GrassField.js +61 -6
- package/dist/terrain/grassGeometry.d.ts +13 -0
- package/dist/terrain/grassGeometry.js +57 -1
- package/dist/terrain/grassMaterial.js +2 -2
- package/dist/terrain/index.d.ts +1 -0
- package/dist/terrain/index.js +1 -0
- package/dist/terrain/terrainDetailMaterial.d.ts +3 -3
- package/dist/terrain/terrainDetailMaterial.js +7 -7
- package/dist/terrain/terrainMath.d.ts +2 -0
- package/dist/terrain/terrainMath.js +2 -0
- package/dist/terrain/terrainPaintColor.d.ts +11 -0
- package/dist/terrain/terrainPaintColor.js +47 -0
- package/dist/touch/TouchControlsOverlay.js +4 -4
- package/dist/useShellMultiplayerSync.js +4 -0
- 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 +209 -61
- 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 +1 -1
- package/skills/game-design/SKILL.md +2 -0
- package/skills/jgengine/SKILL.md +23 -2
- package/skills/jgengine/api.md +28 -17
- package/skills/jgengine/recipes/minimal-game.md +7 -2
- package/skills/jgengine-ui/SKILL.md +3 -1
- package/skills/jgengine-ui/api.md +219 -70
- package/skills/jgengine-ui/capabilities.md +119 -1
- package/skills/jgengine-ui/reference-primitives.md +93 -3
- package/skills/jgengine-ui/reference.md +2 -0
- package/skills/jgengine-world/SKILL.md +12 -3
- package/skills/jgengine-world/api.md +201 -58
- package/skills/jgengine-world/capabilities.md +116 -3
- package/skills/jgengine-world/recipes/character-animation.md +31 -8
- package/skills/jgengine-world/recipes/controls.md +4 -0
- package/skills/jgengine-world/recipes/npc-senses.md +12 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +3 -1
- package/skills/jgengine-world/reference.md +167 -5
- package/skills/jgengine-world/visibility.md +28 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { distance3, resolveEmitterGain } from "@jgengine/core/audio/audioFalloff";
|
|
2
2
|
import { dopplerRate } from "@jgengine/core/audio/doppler";
|
|
3
3
|
import { patchDuration } from "@jgengine/core/audio/synth";
|
|
4
|
+
import { createVoiceAllocator } from "@jgengine/core/audio/voiceAllocator";
|
|
4
5
|
import { createDisposer } from "@jgengine/core/game/defineGame";
|
|
5
6
|
import { clampLoopCutoff, clampLoopGain, clampLoopRate, MIN_LOOP_CUTOFF, MAX_LOOP_CUTOFF } from "./loopParams.js";
|
|
6
7
|
import { MusicDirector } from "./musicDirector.js";
|
|
@@ -11,6 +12,7 @@ const ZERO_VELOCITY = { x: 0, y: 0, z: 0 };
|
|
|
11
12
|
function createNoopEngine() {
|
|
12
13
|
return {
|
|
13
14
|
setListenerPose: () => undefined,
|
|
15
|
+
setVoiceLimit: () => undefined,
|
|
14
16
|
playOneShot: () => undefined,
|
|
15
17
|
playLoop: () => null,
|
|
16
18
|
playMusic: () => undefined,
|
|
@@ -44,25 +46,38 @@ function setPannerPosition(panner, position) {
|
|
|
44
46
|
}
|
|
45
47
|
function createPanner(context, spatial, position) {
|
|
46
48
|
const panner = context.createPanner();
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
49
|
+
try {
|
|
50
|
+
panner.panningModel = spatial.panning === "hrtf" ? "HRTF" : "equalpower";
|
|
51
|
+
panner.distanceModel = "inverse";
|
|
52
|
+
if (spatial.refDistance !== undefined)
|
|
53
|
+
panner.refDistance = spatial.refDistance;
|
|
54
|
+
if (spatial.maxDistance !== undefined)
|
|
55
|
+
panner.maxDistance = spatial.maxDistance;
|
|
56
|
+
if (spatial.rolloff !== undefined)
|
|
57
|
+
panner.rolloffFactor = spatial.rolloff;
|
|
58
|
+
if (spatial.coneInner !== undefined)
|
|
59
|
+
panner.coneInnerAngle = spatial.coneInner;
|
|
60
|
+
if (spatial.coneOuter !== undefined)
|
|
61
|
+
panner.coneOuterAngle = spatial.coneOuter;
|
|
62
|
+
if (spatial.coneOuterGain !== undefined)
|
|
63
|
+
panner.coneOuterGain = spatial.coneOuterGain;
|
|
64
|
+
setPannerPosition(panner, position);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
panner.disconnect();
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
62
70
|
return panner;
|
|
63
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Game-owned sound catalog playback with bounded SFX reservations, spatial panning, and shared buses.
|
|
74
|
+
* @capability bounded-spatial-audio play positional or flat cues with listener orientation, voice budgets, and per-source cleanup
|
|
75
|
+
*/
|
|
64
76
|
export function createAudioEngine(config = {}) {
|
|
65
77
|
const sounds = config.sounds ?? {};
|
|
78
|
+
const allocator = config.voiceAllocator ?? createVoiceAllocator({ maxTotal: config.maxVoices, overflow: config.voiceOverflow });
|
|
79
|
+
let disposed = false;
|
|
80
|
+
const activeVoices = new Map();
|
|
66
81
|
const busDefs = config.buses ?? {};
|
|
67
82
|
const AudioContextCtor = resolveAudioContextCtor();
|
|
68
83
|
if (AudioContextCtor === undefined)
|
|
@@ -82,9 +97,15 @@ export function createAudioEngine(config = {}) {
|
|
|
82
97
|
let node = busGains.get(busId);
|
|
83
98
|
if (node === undefined) {
|
|
84
99
|
node = context.createGain();
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
100
|
+
try {
|
|
101
|
+
node.gain.value = busDefs[busId]?.gain ?? 1;
|
|
102
|
+
node.connect(masterGain);
|
|
103
|
+
busGains.set(busId, node);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
node.disconnect();
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
88
109
|
}
|
|
89
110
|
return node;
|
|
90
111
|
}
|
|
@@ -145,38 +166,66 @@ export function createAudioEngine(config = {}) {
|
|
|
145
166
|
let listenerVelocity = { x: 0, y: 0, z: 0 };
|
|
146
167
|
const nyquist = context.sampleRate / 2;
|
|
147
168
|
const activeSpatialUpdaters = new Set();
|
|
148
|
-
const activeLoops = new Set();
|
|
149
|
-
const NOOP_HANDLE = {
|
|
150
|
-
setPosition: () => undefined,
|
|
151
|
-
setRate: () => undefined,
|
|
152
|
-
setGain: () => undefined,
|
|
153
|
-
setLowpass: () => undefined,
|
|
154
|
-
setHighpass: () => undefined,
|
|
155
|
-
setVelocity: () => undefined,
|
|
156
|
-
stop: () => undefined,
|
|
157
|
-
};
|
|
158
169
|
function playInternal(soundId, position, loop) {
|
|
170
|
+
if (disposed)
|
|
171
|
+
return null;
|
|
159
172
|
const sound = sounds[soundId];
|
|
160
|
-
if (sound === undefined)
|
|
173
|
+
if (sound === undefined || (sound.synth === undefined && sound.url === undefined))
|
|
161
174
|
return null;
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
175
|
+
const allocation = allocator.request(soundId, sound.priority, sound.maxVoices);
|
|
176
|
+
if (!allocation.ok)
|
|
177
|
+
return null;
|
|
178
|
+
for (const id of allocation.stolen)
|
|
179
|
+
activeVoices.get(id)?.();
|
|
180
|
+
const voiceId = allocation.voiceId;
|
|
181
|
+
let bus;
|
|
182
|
+
try {
|
|
183
|
+
bus = busGainNode(sound.bus);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
allocator.release(voiceId);
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
const currentPosition = { ...(position ?? listenerPosition) };
|
|
165
190
|
if (sound.synth !== undefined && !loop) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
panner
|
|
191
|
+
let cueGain = null;
|
|
192
|
+
let panner = null;
|
|
193
|
+
let playback = null;
|
|
194
|
+
let stopped = false;
|
|
195
|
+
function updateGain() {
|
|
196
|
+
if (cueGain === null)
|
|
197
|
+
return;
|
|
198
|
+
cueGain.gain.value = panner === null
|
|
199
|
+
? resolveEmitterGain(distance3(currentPosition, listenerPosition), sound, 1)
|
|
200
|
+
: sound.gain ?? 1;
|
|
174
201
|
}
|
|
175
|
-
|
|
176
|
-
|
|
202
|
+
function stop() {
|
|
203
|
+
if (stopped)
|
|
204
|
+
return;
|
|
205
|
+
stopped = true;
|
|
206
|
+
activeVoices.delete(voiceId);
|
|
207
|
+
activeSpatialUpdaters.delete(updateGain);
|
|
208
|
+
allocator.release(voiceId);
|
|
209
|
+
playback?.stop();
|
|
210
|
+
cueGain?.disconnect();
|
|
211
|
+
panner?.disconnect();
|
|
177
212
|
}
|
|
178
|
-
|
|
179
|
-
|
|
213
|
+
activeVoices.set(voiceId, stop);
|
|
214
|
+
try {
|
|
215
|
+
cueGain = context.createGain();
|
|
216
|
+
const spatial = sound.positional === false ? undefined : sound.spatial;
|
|
217
|
+
panner = spatial === undefined ? null : createPanner(context, spatial, currentPosition);
|
|
218
|
+
updateGain();
|
|
219
|
+
cueGain.connect(panner ?? bus);
|
|
220
|
+
panner?.connect(bus);
|
|
221
|
+
playback = realizeSynthPatch(context, cueGain, sharedNoiseBuffer(), sound.synth);
|
|
222
|
+
activeSpatialUpdaters.add(updateGain);
|
|
223
|
+
playback.onEnded(stop);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
stop();
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
180
229
|
}
|
|
181
230
|
// Everything else resolves to one AudioBuffer we loop/play: sample URL, or a synth patch rendered once.
|
|
182
231
|
let bufferPromise;
|
|
@@ -186,11 +235,13 @@ export function createAudioEngine(config = {}) {
|
|
|
186
235
|
bufferPromise = loadBuffer(sound.url);
|
|
187
236
|
else
|
|
188
237
|
return null;
|
|
189
|
-
return playBufferSource(sound, bus, bufferPromise, currentPosition, loop);
|
|
238
|
+
return playBufferSource(sound, bus, bufferPromise, currentPosition, loop, voiceId);
|
|
190
239
|
}
|
|
191
|
-
function playBufferSource(sound, bus, bufferPromise, initialPosition, loop) {
|
|
240
|
+
function playBufferSource(sound, bus, bufferPromise, initialPosition, loop, voiceId) {
|
|
192
241
|
const disposer = createDisposer();
|
|
193
|
-
let currentPosition = initialPosition;
|
|
242
|
+
let currentPosition = { ...initialPosition };
|
|
243
|
+
let stopped = false;
|
|
244
|
+
const spatial = sound.positional === false ? undefined : sound.spatial;
|
|
194
245
|
// Live control state, applied on source creation so updates that race the async buffer load stick.
|
|
195
246
|
let currentRate = 1;
|
|
196
247
|
let currentUserGain = 1;
|
|
@@ -215,7 +266,7 @@ export function createAudioEngine(config = {}) {
|
|
|
215
266
|
sourceNode.playbackRate.setTargetAtTime(effectiveRate(), context.currentTime, LOOP_PARAM_SMOOTH_TC);
|
|
216
267
|
}
|
|
217
268
|
function updateFalloff() {
|
|
218
|
-
if (falloffGain !== null &&
|
|
269
|
+
if (falloffGain !== null && spatial === undefined) {
|
|
219
270
|
falloffGain.gain.value = resolveEmitterGain(distance3(currentPosition, listenerPosition), sound, 1);
|
|
220
271
|
}
|
|
221
272
|
if (pannerNode !== null)
|
|
@@ -223,73 +274,83 @@ export function createAudioEngine(config = {}) {
|
|
|
223
274
|
if (dopplerFactor !== 0)
|
|
224
275
|
applyRate();
|
|
225
276
|
}
|
|
277
|
+
disposer.onDispose(() => {
|
|
278
|
+
if (sourceNode !== null)
|
|
279
|
+
sourceNode.onended = null;
|
|
280
|
+
try {
|
|
281
|
+
sourceNode?.stop();
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
}
|
|
285
|
+
sourceNode?.disconnect();
|
|
286
|
+
lowpassNode?.disconnect();
|
|
287
|
+
highpassNode?.disconnect();
|
|
288
|
+
userGainNode?.disconnect();
|
|
289
|
+
falloffGain?.disconnect();
|
|
290
|
+
pannerNode?.disconnect();
|
|
291
|
+
sourceNode = null;
|
|
292
|
+
lowpassNode = null;
|
|
293
|
+
highpassNode = null;
|
|
294
|
+
userGainNode = null;
|
|
295
|
+
falloffGain = null;
|
|
296
|
+
pannerNode = null;
|
|
297
|
+
});
|
|
226
298
|
void bufferPromise.then((buffer) => {
|
|
227
|
-
if (
|
|
299
|
+
if (stopped || disposed)
|
|
300
|
+
return;
|
|
301
|
+
if (buffer === null) {
|
|
302
|
+
handle.stop();
|
|
228
303
|
return;
|
|
304
|
+
}
|
|
229
305
|
const src = context.createBufferSource();
|
|
306
|
+
sourceNode = src;
|
|
230
307
|
src.buffer = buffer;
|
|
231
308
|
src.loop = loop || (sound.loop ?? false);
|
|
232
309
|
src.playbackRate.value = effectiveRate();
|
|
233
|
-
//
|
|
310
|
+
// Spatial panners own distance attenuation; the scalar stage keeps only authored gain.
|
|
234
311
|
const uGain = context.createGain();
|
|
312
|
+
userGainNode = uGain;
|
|
235
313
|
uGain.gain.value = currentUserGain;
|
|
236
314
|
const fGain = context.createGain();
|
|
315
|
+
falloffGain = fGain;
|
|
237
316
|
fGain.gain.value = resolveEmitterGain(distance3(currentPosition, listenerPosition), sound, 1);
|
|
238
317
|
if (loop) {
|
|
239
318
|
const lp = context.createBiquadFilter();
|
|
319
|
+
lowpassNode = lp;
|
|
240
320
|
lp.type = "lowpass";
|
|
241
321
|
lp.frequency.value = clampLoopCutoff(currentLowpass, MAX_LOOP_CUTOFF, nyquist);
|
|
242
322
|
const hp = context.createBiquadFilter();
|
|
323
|
+
highpassNode = hp;
|
|
243
324
|
hp.type = "highpass";
|
|
244
325
|
hp.frequency.value = clampLoopCutoff(currentHighpass, MIN_LOOP_CUTOFF, nyquist);
|
|
245
326
|
src.connect(lp);
|
|
246
327
|
lp.connect(hp);
|
|
247
328
|
hp.connect(uGain);
|
|
248
|
-
lowpassNode = lp;
|
|
249
|
-
highpassNode = hp;
|
|
250
329
|
}
|
|
251
330
|
else {
|
|
252
331
|
src.connect(uGain);
|
|
253
332
|
}
|
|
254
333
|
uGain.connect(fGain);
|
|
255
|
-
if (
|
|
256
|
-
const panner = createPanner(context,
|
|
257
|
-
|
|
334
|
+
if (spatial !== undefined) {
|
|
335
|
+
const panner = createPanner(context, spatial, currentPosition);
|
|
336
|
+
pannerNode = panner;
|
|
337
|
+
fGain.gain.value = sound.gain ?? 1;
|
|
258
338
|
fGain.connect(panner);
|
|
259
339
|
panner.connect(bus);
|
|
260
|
-
pannerNode = panner;
|
|
261
340
|
}
|
|
262
341
|
else {
|
|
263
342
|
fGain.connect(bus);
|
|
264
343
|
}
|
|
344
|
+
src.onended = () => handle.stop();
|
|
265
345
|
src.start();
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
falloffGain = fGain;
|
|
269
|
-
disposer.onDispose(() => {
|
|
270
|
-
try {
|
|
271
|
-
src.stop();
|
|
272
|
-
}
|
|
273
|
-
catch {
|
|
274
|
-
}
|
|
275
|
-
src.disconnect();
|
|
276
|
-
lowpassNode?.disconnect();
|
|
277
|
-
highpassNode?.disconnect();
|
|
278
|
-
uGain.disconnect();
|
|
279
|
-
fGain.disconnect();
|
|
280
|
-
pannerNode?.disconnect();
|
|
281
|
-
sourceNode = null;
|
|
282
|
-
lowpassNode = null;
|
|
283
|
-
highpassNode = null;
|
|
284
|
-
userGainNode = null;
|
|
285
|
-
falloffGain = null;
|
|
286
|
-
});
|
|
287
|
-
});
|
|
288
|
-
if (loop)
|
|
289
|
-
activeSpatialUpdaters.add(updateFalloff);
|
|
346
|
+
}).catch(() => handle.stop());
|
|
347
|
+
activeSpatialUpdaters.add(updateFalloff);
|
|
290
348
|
const handle = {
|
|
349
|
+
isPlaying: () => !stopped,
|
|
291
350
|
setPosition(next) {
|
|
292
|
-
|
|
351
|
+
if (stopped)
|
|
352
|
+
return;
|
|
353
|
+
currentPosition = { ...next };
|
|
293
354
|
updateFalloff();
|
|
294
355
|
},
|
|
295
356
|
setRate(rate) {
|
|
@@ -307,7 +368,7 @@ export function createAudioEngine(config = {}) {
|
|
|
307
368
|
highpassNode.frequency.setTargetAtTime(currentHighpass, context.currentTime, LOOP_PARAM_SMOOTH_TC);
|
|
308
369
|
},
|
|
309
370
|
setVelocity(velocity) {
|
|
310
|
-
currentVelocity = velocity;
|
|
371
|
+
currentVelocity = { ...velocity };
|
|
311
372
|
if (dopplerFactor !== 0)
|
|
312
373
|
applyRate();
|
|
313
374
|
},
|
|
@@ -317,20 +378,31 @@ export function createAudioEngine(config = {}) {
|
|
|
317
378
|
userGainNode.gain.setTargetAtTime(currentUserGain, context.currentTime, LOOP_PARAM_SMOOTH_TC);
|
|
318
379
|
},
|
|
319
380
|
stop() {
|
|
381
|
+
if (stopped)
|
|
382
|
+
return;
|
|
383
|
+
stopped = true;
|
|
320
384
|
activeSpatialUpdaters.delete(updateFalloff);
|
|
321
|
-
|
|
385
|
+
activeVoices.delete(voiceId);
|
|
386
|
+
allocator.release(voiceId);
|
|
322
387
|
disposer.dispose();
|
|
323
388
|
},
|
|
324
389
|
};
|
|
325
|
-
|
|
326
|
-
activeLoops.add(handle);
|
|
390
|
+
activeVoices.set(voiceId, () => handle.stop());
|
|
327
391
|
return handle;
|
|
328
392
|
}
|
|
329
393
|
return {
|
|
394
|
+
setVoiceLimit(maxTotal, overflow) {
|
|
395
|
+
if (disposed)
|
|
396
|
+
return;
|
|
397
|
+
for (const id of allocator.retune({ maxTotal, overflow }))
|
|
398
|
+
activeVoices.get(id)?.();
|
|
399
|
+
},
|
|
330
400
|
setListenerPose(position) {
|
|
401
|
+
if (disposed)
|
|
402
|
+
return;
|
|
331
403
|
const pose = "position" in position ? position : { position, forward: { x: 0, y: 0, z: -1 }, up: { x: 0, y: 1, z: 0 } };
|
|
332
|
-
listenerPosition = pose.position;
|
|
333
|
-
listenerVelocity = "velocity" in pose && pose.velocity !== undefined ? pose.velocity : ZERO_VELOCITY;
|
|
404
|
+
listenerPosition = { ...pose.position };
|
|
405
|
+
listenerVelocity = "velocity" in pose && pose.velocity !== undefined ? { ...pose.velocity } : ZERO_VELOCITY;
|
|
334
406
|
const listener = context.listener;
|
|
335
407
|
setAudioParam(listener.positionX, pose.position.x);
|
|
336
408
|
setAudioParam(listener.positionY, pose.position.y);
|
|
@@ -345,6 +417,8 @@ export function createAudioEngine(config = {}) {
|
|
|
345
417
|
updateGain();
|
|
346
418
|
},
|
|
347
419
|
playOneShot(soundId, position) {
|
|
420
|
+
if (disposed)
|
|
421
|
+
return;
|
|
348
422
|
void context.resume().catch(() => undefined);
|
|
349
423
|
playInternal(soundId, position, false);
|
|
350
424
|
},
|
|
@@ -352,23 +426,38 @@ export function createAudioEngine(config = {}) {
|
|
|
352
426
|
return playInternal(soundId, position, true);
|
|
353
427
|
},
|
|
354
428
|
playMusic(themeId, options) {
|
|
429
|
+
if (disposed)
|
|
430
|
+
return;
|
|
355
431
|
void context.resume().catch(() => undefined);
|
|
356
432
|
musicDirector()?.crossfadeTo(themeId, options);
|
|
357
433
|
},
|
|
358
434
|
setBusGain(busId, gain) {
|
|
435
|
+
if (disposed)
|
|
436
|
+
return;
|
|
359
437
|
busGainNode(busId).gain.value = gain;
|
|
360
438
|
},
|
|
361
439
|
setMasterGain(gain) {
|
|
440
|
+
if (disposed)
|
|
441
|
+
return;
|
|
362
442
|
masterGain.gain.value = gain;
|
|
363
443
|
},
|
|
364
444
|
resume() {
|
|
445
|
+
if (disposed)
|
|
446
|
+
return;
|
|
365
447
|
void context.resume().catch(() => undefined);
|
|
366
448
|
},
|
|
367
449
|
dispose() {
|
|
368
|
-
|
|
369
|
-
|
|
450
|
+
if (disposed)
|
|
451
|
+
return;
|
|
452
|
+
disposed = true;
|
|
453
|
+
for (const stop of [...activeVoices.values()])
|
|
454
|
+
stop();
|
|
370
455
|
activeSpatialUpdaters.clear();
|
|
371
456
|
director?.dispose();
|
|
457
|
+
for (const node of busGains.values())
|
|
458
|
+
node.disconnect();
|
|
459
|
+
busGains.clear();
|
|
460
|
+
masterGain.disconnect();
|
|
372
461
|
void context.close().catch(() => undefined);
|
|
373
462
|
},
|
|
374
463
|
};
|
package/dist/audio/audioWire.js
CHANGED
|
@@ -19,8 +19,8 @@ export function attachAudioEventWire(events, audioEngine) {
|
|
|
19
19
|
const offLoopStart = events.on("audio.loopStart", ({ id, sound, at }) => {
|
|
20
20
|
const existing = loops.get(id);
|
|
21
21
|
if (existing !== undefined) {
|
|
22
|
-
//
|
|
23
|
-
if (existing.sound === sound)
|
|
22
|
+
// Keep active/pending loops; a stolen voice can retry on the next explicit start.
|
|
23
|
+
if (existing.sound === sound && existing.handle.isPlaying?.() !== false)
|
|
24
24
|
return;
|
|
25
25
|
existing.handle.stop();
|
|
26
26
|
loops.delete(id);
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import type { SynthPatch } from "@jgengine/core/audio/synth";
|
|
2
2
|
/** Build the shared 1-second mono white-noise buffer every noise voice samples from. */
|
|
3
3
|
export declare function createNoiseBuffer(ctx: BaseAudioContext): AudioBuffer;
|
|
4
|
+
/** Scheduled procedural sources; stopping releases their source/filter/envelope graph. */
|
|
5
|
+
export interface SynthPlayback {
|
|
6
|
+
stop(): void;
|
|
7
|
+
/** Called once after every source ends; late subscribers are notified immediately. */
|
|
8
|
+
onEnded(callback: () => void): void;
|
|
9
|
+
}
|
|
4
10
|
/**
|
|
5
11
|
* Realise a procedural cue on Web Audio: every voice is scheduled at
|
|
6
12
|
* `ctx.currentTime + delay` into `out`, summed into one one-shot. `noiseBuf` is
|
|
7
13
|
* the shared buffer from {@link createNoiseBuffer}.
|
|
8
14
|
*/
|
|
9
|
-
export declare function realizeSynthPatch(ctx: BaseAudioContext, out: AudioNode, noiseBuf: AudioBuffer, patch: SynthPatch):
|
|
15
|
+
export declare function realizeSynthPatch(ctx: BaseAudioContext, out: AudioNode, noiseBuf: AudioBuffer, patch: SynthPatch): SynthPlayback;
|
|
@@ -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) => {
|
|
@@ -2,8 +2,8 @@ import { type ComponentType, type MutableRefObject } from "react";
|
|
|
2
2
|
import * as THREE from "three";
|
|
3
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>;
|