@vgai/engine 0.2.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/LICENSE +202 -0
- package/README.md +35 -0
- package/package.json +55 -0
- package/src/adapter/authoring.ts +402 -0
- package/src/adapter/colyseus-networking-adapter.ts +72 -0
- package/src/adapter/first-party-systems.ts +103 -0
- package/src/adapter/game-adapter.ts +151 -0
- package/src/adapter/host-context.ts +77 -0
- package/src/adapter/index.ts +85 -0
- package/src/adapter/ingest/game-contract.ts +59 -0
- package/src/adapter/ingest/overlay-applier.ts +207 -0
- package/src/adapter/ingest/overlay-apply.ts +124 -0
- package/src/adapter/ingest/overlay-file.ts +126 -0
- package/src/adapter/ingest/overlay-report.ts +176 -0
- package/src/adapter/ingest/scene-capture.ts +307 -0
- package/src/adapter/ingest/upstream-pin.ts +52 -0
- package/src/adapter/loop-gate-report.ts +54 -0
- package/src/adapter/rapier-physics-adapter.ts +56 -0
- package/src/adapter/system-adapter.ts +154 -0
- package/src/adapter/transform.ts +18 -0
- package/src/adapter/vgai-scene-game-adapter.ts +886 -0
- package/src/adapter/world-kind.ts +34 -0
- package/src/ai/navigation.ts +164 -0
- package/src/animation/anim-graph-types.ts +56 -0
- package/src/animation/anim-graph.ts +406 -0
- package/src/animation/anim-system.ts +28 -0
- package/src/animation/blend-node.ts +119 -0
- package/src/animation/property-track.ts +178 -0
- package/src/animation/schema.ts +204 -0
- package/src/assets.ts +80 -0
- package/src/audio/ambient.ts +300 -0
- package/src/audio/impacts.ts +212 -0
- package/src/audio/index.ts +7 -0
- package/src/audio/movement.ts +140 -0
- package/src/audio/musical.ts +200 -0
- package/src/audio/ui-sounds.ts +171 -0
- package/src/audio/vehicle.ts +235 -0
- package/src/audio/weapons.ts +152 -0
- package/src/core/game-loop.ts +127 -0
- package/src/core/system-runner.ts +298 -0
- package/src/core/types.ts +58 -0
- package/src/dev/console-bridge.ts +83 -0
- package/src/dev/debug-draw.ts +80 -0
- package/src/dev/logger.ts +119 -0
- package/src/ecs/component-manager.ts +748 -0
- package/src/ecs/game-component.ts +147 -0
- package/src/ecs/hmr-swap-report.ts +65 -0
- package/src/input/input-manager.ts +439 -0
- package/src/input/input-types.ts +19 -0
- package/src/input/schema.ts +129 -0
- package/src/loader.ts +70 -0
- package/src/manifest/index.ts +24 -0
- package/src/manifest/load-file.ts +16 -0
- package/src/manifest/load.ts +378 -0
- package/src/manifest/schema.ts +375 -0
- package/src/physics/collision-system.ts +76 -0
- package/src/physics/physics-registry.ts +83 -0
- package/src/physics/transform-writer.ts +41 -0
- package/src/physics/trigger-dispatch.ts +97 -0
- package/src/react/game-state.tsx +172 -0
- package/src/render/auto-batcher.ts +169 -0
- package/src/render/render-batch-system.ts +268 -0
- package/src/render/render-features.ts +146 -0
- package/src/render/render-settings.ts +72 -0
- package/src/runtime/create-runtime.ts +1152 -0
- package/src/runtime/frame-selector-cache.ts +81 -0
- package/src/runtime/game.ts +1003 -0
- package/src/runtime/input-router.ts +213 -0
- package/src/runtime/mount-game.ts +269 -0
- package/src/runtime/mount-manifest.ts +361 -0
- package/src/runtime/scene-ui-bridge.ts +86 -0
- package/src/runtime/scene-ui-data.ts +119 -0
- package/src/runtime/state-bridge.ts +79 -0
- package/src/runtime/types.ts +196 -0
- package/src/scene/asset-loaders.ts +195 -0
- package/src/scene/asset-paths.ts +123 -0
- package/src/scene/asset-registry.ts +67 -0
- package/src/scene/collider-dimensions.ts +125 -0
- package/src/scene/component-registry.ts +40 -0
- package/src/scene/defaults.ts +164 -0
- package/src/scene/geometries/index.ts +7 -0
- package/src/scene/geometries/terrain.ts +42 -0
- package/src/scene/geometry-registry.ts +42 -0
- package/src/scene/instance-registry.ts +84 -0
- package/src/scene/instancers/grid.ts +38 -0
- package/src/scene/instancers/index.ts +7 -0
- package/src/scene/light-camera-factory.ts +97 -0
- package/src/scene/material-factory.ts +211 -0
- package/src/scene/material-registry.ts +73 -0
- package/src/scene/materials/index.ts +7 -0
- package/src/scene/materials/water.ts +56 -0
- package/src/scene/parse.ts +71 -0
- package/src/scene/particles-factory.ts +383 -0
- package/src/scene/scene-apply.ts +356 -0
- package/src/scene/scene-diff-schema.ts +115 -0
- package/src/scene/scene-diff-types.ts +29 -0
- package/src/scene/scene-loader.ts +1533 -0
- package/src/scene/scene-query.ts +63 -0
- package/src/scene/scene-types.ts +34 -0
- package/src/scene/scene-version.ts +40 -0
- package/src/scene/schema/animation.ts +95 -0
- package/src/scene/schema/audio.ts +25 -0
- package/src/scene/schema/camera.ts +21 -0
- package/src/scene/schema/collider.ts +69 -0
- package/src/scene/schema/entity-ref.ts +78 -0
- package/src/scene/schema/entity.ts +169 -0
- package/src/scene/schema/environment.ts +384 -0
- package/src/scene/schema/index.ts +95 -0
- package/src/scene/schema/instances.ts +35 -0
- package/src/scene/schema/joint.ts +26 -0
- package/src/scene/schema/light.ts +38 -0
- package/src/scene/schema/material.ts +113 -0
- package/src/scene/schema/mesh.ts +108 -0
- package/src/scene/schema/particles.ts +398 -0
- package/src/scene/schema/physics.ts +49 -0
- package/src/scene/schema/scene-file.ts +299 -0
- package/src/scene/schema/shadow.ts +24 -0
- package/src/scene/schema/spline.ts +21 -0
- package/src/scene/schema/tuples.ts +21 -0
- package/src/scene/schema/ui.ts +602 -0
- package/src/scene/user-data.ts +203 -0
- package/src/setup/setup-audio.ts +60 -0
- package/src/setup/setup-particles.ts +23 -0
- package/src/setup/setup-physics.ts +67 -0
- package/src/setup/setup-renderer.ts +529 -0
- package/src/types-n8ao.d.ts +37 -0
- package/src/types-realism-effects.d.ts +61 -0
- package/src/world2d/authoring-2d.ts +208 -0
- package/src/world2d/capture-to-scene2d.ts +52 -0
- package/src/world2d/collision-2d.ts +106 -0
- package/src/world2d/components-2d.ts +86 -0
- package/src/world2d/index.ts +66 -0
- package/src/world2d/ingest-iframe-2d.ts +255 -0
- package/src/world2d/ingest2d.ts +131 -0
- package/src/world2d/physics2d-registry.ts +49 -0
- package/src/world2d/pixi-game-adapter.ts +325 -0
- package/src/world2d/pixi-surface.ts +78 -0
- package/src/world2d/scene-capture-2d.ts +117 -0
- package/src/world2d/scene2d-loader.ts +308 -0
- package/src/world2d/schema/entity2d.ts +145 -0
- package/src/world2d/schema/physics2d.ts +53 -0
- package/src/world2d/schema/sprite.ts +71 -0
- package/src/world2d/schema/tilemap.ts +22 -0
- package/src/world2d/schema/tuples2d.ts +25 -0
- package/src/world2d/system-adapters-2d.ts +49 -0
- package/src/world2d/transform-writer-2d.ts +24 -0
- package/src/world2d/types.ts +55 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ambient sounds — continuous environmental audio.
|
|
3
|
+
*
|
|
4
|
+
* Each returns play/stop/dispose. These are designed to loop indefinitely.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
function createNoiseBuffer(ctx: AudioContext, duration = 2): AudioBuffer {
|
|
8
|
+
const length = ctx.sampleRate * duration;
|
|
9
|
+
const buffer = ctx.createBuffer(1, length, ctx.sampleRate);
|
|
10
|
+
const data = buffer.getChannelData(0);
|
|
11
|
+
for (let i = 0; i < length; i++) data[i] = Math.random() * 2 - 1;
|
|
12
|
+
return buffer;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Wind — filtered noise with slow LFO modulation on cutoff frequency. */
|
|
16
|
+
export function wind(
|
|
17
|
+
ctx: AudioContext,
|
|
18
|
+
dest: AudioNode,
|
|
19
|
+
options?: { volume?: number; speed?: number },
|
|
20
|
+
) {
|
|
21
|
+
const volume = options?.volume ?? 0.15;
|
|
22
|
+
const speed = options?.speed ?? 0.3;
|
|
23
|
+
const noiseBuffer = createNoiseBuffer(ctx, 2);
|
|
24
|
+
|
|
25
|
+
const out = ctx.createGain();
|
|
26
|
+
out.gain.value = volume;
|
|
27
|
+
out.connect(dest);
|
|
28
|
+
|
|
29
|
+
const filter = ctx.createBiquadFilter();
|
|
30
|
+
filter.type = 'lowpass';
|
|
31
|
+
filter.frequency.value = 500;
|
|
32
|
+
filter.connect(out);
|
|
33
|
+
|
|
34
|
+
// LFO modulates filter cutoff for "gusts"
|
|
35
|
+
const lfo = ctx.createOscillator();
|
|
36
|
+
lfo.type = 'sine';
|
|
37
|
+
lfo.frequency.value = speed;
|
|
38
|
+
const lfoGain = ctx.createGain();
|
|
39
|
+
lfoGain.gain.value = 300; // ±300 Hz modulation
|
|
40
|
+
lfo.connect(lfoGain);
|
|
41
|
+
lfoGain.connect(filter.frequency);
|
|
42
|
+
|
|
43
|
+
let src: AudioBufferSourceNode | null = null;
|
|
44
|
+
let lfoStarted = false;
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
play() {
|
|
48
|
+
if (src) return;
|
|
49
|
+
src = ctx.createBufferSource();
|
|
50
|
+
src.buffer = noiseBuffer;
|
|
51
|
+
src.loop = true;
|
|
52
|
+
src.connect(filter);
|
|
53
|
+
src.start();
|
|
54
|
+
if (!lfoStarted) {
|
|
55
|
+
lfo.start();
|
|
56
|
+
lfoStarted = true;
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
stop() {
|
|
60
|
+
if (!src) return;
|
|
61
|
+
const ref = src;
|
|
62
|
+
src = null;
|
|
63
|
+
try {
|
|
64
|
+
ref.stop();
|
|
65
|
+
} catch {
|
|
66
|
+
/* already stopped */
|
|
67
|
+
}
|
|
68
|
+
ref.disconnect();
|
|
69
|
+
},
|
|
70
|
+
dispose() {
|
|
71
|
+
if (src) {
|
|
72
|
+
try {
|
|
73
|
+
src.stop();
|
|
74
|
+
} catch {
|
|
75
|
+
/* already stopped */
|
|
76
|
+
}
|
|
77
|
+
src.disconnect();
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
lfo.stop();
|
|
81
|
+
} catch {
|
|
82
|
+
/* already stopped */
|
|
83
|
+
}
|
|
84
|
+
lfo.disconnect();
|
|
85
|
+
lfoGain.disconnect();
|
|
86
|
+
filter.disconnect();
|
|
87
|
+
out.disconnect();
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Rain — high-frequency filtered noise. */
|
|
93
|
+
export function rain(
|
|
94
|
+
ctx: AudioContext,
|
|
95
|
+
dest: AudioNode,
|
|
96
|
+
options?: { volume?: number; intensity?: number },
|
|
97
|
+
) {
|
|
98
|
+
const volume = options?.volume ?? 0.2;
|
|
99
|
+
const intensity = options?.intensity ?? 0.5;
|
|
100
|
+
const noiseBuffer = createNoiseBuffer(ctx, 2);
|
|
101
|
+
|
|
102
|
+
const out = ctx.createGain();
|
|
103
|
+
out.gain.value = 0.05 + intensity * volume;
|
|
104
|
+
out.connect(dest);
|
|
105
|
+
|
|
106
|
+
const hp = ctx.createBiquadFilter();
|
|
107
|
+
hp.type = 'highpass';
|
|
108
|
+
hp.frequency.value = 1000 + intensity * 3000;
|
|
109
|
+
hp.connect(out);
|
|
110
|
+
|
|
111
|
+
let src: AudioBufferSourceNode | null = null;
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
play() {
|
|
115
|
+
if (src) return;
|
|
116
|
+
src = ctx.createBufferSource();
|
|
117
|
+
src.buffer = noiseBuffer;
|
|
118
|
+
src.loop = true;
|
|
119
|
+
src.connect(hp);
|
|
120
|
+
src.start();
|
|
121
|
+
},
|
|
122
|
+
stop() {
|
|
123
|
+
if (!src) return;
|
|
124
|
+
const ref = src;
|
|
125
|
+
src = null;
|
|
126
|
+
try {
|
|
127
|
+
ref.stop();
|
|
128
|
+
} catch {
|
|
129
|
+
/* already stopped */
|
|
130
|
+
}
|
|
131
|
+
ref.disconnect();
|
|
132
|
+
},
|
|
133
|
+
dispose() {
|
|
134
|
+
if (src) {
|
|
135
|
+
try {
|
|
136
|
+
src.stop();
|
|
137
|
+
} catch {
|
|
138
|
+
/* already stopped */
|
|
139
|
+
}
|
|
140
|
+
src.disconnect();
|
|
141
|
+
}
|
|
142
|
+
hp.disconnect();
|
|
143
|
+
out.disconnect();
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Hum — low continuous drone with optional harmonics. */
|
|
149
|
+
export function hum(
|
|
150
|
+
ctx: AudioContext,
|
|
151
|
+
dest: AudioNode,
|
|
152
|
+
options?: { frequency?: number; volume?: number; harmonics?: number },
|
|
153
|
+
) {
|
|
154
|
+
const frequency = options?.frequency ?? 60;
|
|
155
|
+
const volume = options?.volume ?? 0.1;
|
|
156
|
+
const harmonics = options?.harmonics ?? 2;
|
|
157
|
+
|
|
158
|
+
const out = ctx.createGain();
|
|
159
|
+
out.gain.value = 0;
|
|
160
|
+
out.connect(dest);
|
|
161
|
+
|
|
162
|
+
const oscillators: OscillatorNode[] = [];
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
play() {
|
|
166
|
+
if (oscillators.length > 0) return;
|
|
167
|
+
for (let h = 0; h <= harmonics; h++) {
|
|
168
|
+
const osc = ctx.createOscillator();
|
|
169
|
+
osc.type = 'sine';
|
|
170
|
+
osc.frequency.value = frequency * (h + 1);
|
|
171
|
+
const gain = ctx.createGain();
|
|
172
|
+
// Each harmonic is quieter
|
|
173
|
+
gain.gain.value = 1 / (h + 1);
|
|
174
|
+
osc.connect(gain);
|
|
175
|
+
gain.connect(out);
|
|
176
|
+
osc.start();
|
|
177
|
+
oscillators.push(osc);
|
|
178
|
+
}
|
|
179
|
+
out.gain.setTargetAtTime(volume, ctx.currentTime, 0.1);
|
|
180
|
+
},
|
|
181
|
+
stop() {
|
|
182
|
+
out.gain.setTargetAtTime(0, ctx.currentTime, 0.1);
|
|
183
|
+
setTimeout(() => {
|
|
184
|
+
for (const osc of oscillators) {
|
|
185
|
+
try {
|
|
186
|
+
osc.stop();
|
|
187
|
+
} catch {
|
|
188
|
+
/* already stopped */
|
|
189
|
+
}
|
|
190
|
+
osc.disconnect();
|
|
191
|
+
}
|
|
192
|
+
oscillators.length = 0;
|
|
193
|
+
}, 300);
|
|
194
|
+
},
|
|
195
|
+
dispose() {
|
|
196
|
+
for (const osc of oscillators) {
|
|
197
|
+
try {
|
|
198
|
+
osc.stop();
|
|
199
|
+
} catch {
|
|
200
|
+
/* already stopped */
|
|
201
|
+
}
|
|
202
|
+
osc.disconnect();
|
|
203
|
+
}
|
|
204
|
+
oscillators.length = 0;
|
|
205
|
+
out.disconnect();
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Nature — gentle noise base with random bird-like sine chirps. */
|
|
211
|
+
export function nature(
|
|
212
|
+
ctx: AudioContext,
|
|
213
|
+
dest: AudioNode,
|
|
214
|
+
options?: { volume?: number; birdRate?: number },
|
|
215
|
+
) {
|
|
216
|
+
const volume = options?.volume ?? 0.15;
|
|
217
|
+
const birdRate = options?.birdRate ?? 0.5;
|
|
218
|
+
const noiseBuffer = createNoiseBuffer(ctx, 2);
|
|
219
|
+
|
|
220
|
+
const out = ctx.createGain();
|
|
221
|
+
out.gain.value = volume;
|
|
222
|
+
out.connect(dest);
|
|
223
|
+
|
|
224
|
+
const lp = ctx.createBiquadFilter();
|
|
225
|
+
lp.type = 'lowpass';
|
|
226
|
+
lp.frequency.value = 400;
|
|
227
|
+
lp.connect(out);
|
|
228
|
+
|
|
229
|
+
let src: AudioBufferSourceNode | null = null;
|
|
230
|
+
let birdInterval: ReturnType<typeof setInterval> | null = null;
|
|
231
|
+
|
|
232
|
+
function chirp() {
|
|
233
|
+
const now = ctx.currentTime;
|
|
234
|
+
const freq = 2000 + Math.random() * 2000;
|
|
235
|
+
const dur = 0.05 + Math.random() * 0.05;
|
|
236
|
+
|
|
237
|
+
const osc = ctx.createOscillator();
|
|
238
|
+
osc.type = 'sine';
|
|
239
|
+
osc.frequency.setValueAtTime(freq, now);
|
|
240
|
+
osc.frequency.exponentialRampToValueAtTime(freq * 1.3, now + dur * 0.5);
|
|
241
|
+
osc.frequency.exponentialRampToValueAtTime(freq * 0.8, now + dur);
|
|
242
|
+
|
|
243
|
+
const env = ctx.createGain();
|
|
244
|
+
env.gain.setValueAtTime(0.001, now);
|
|
245
|
+
env.gain.linearRampToValueAtTime(0.6, now + dur * 0.2);
|
|
246
|
+
env.gain.exponentialRampToValueAtTime(0.001, now + dur);
|
|
247
|
+
|
|
248
|
+
osc.connect(env);
|
|
249
|
+
env.connect(out);
|
|
250
|
+
osc.start(now);
|
|
251
|
+
osc.stop(now + dur + 0.01);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
play() {
|
|
256
|
+
if (src) return;
|
|
257
|
+
src = ctx.createBufferSource();
|
|
258
|
+
src.buffer = noiseBuffer;
|
|
259
|
+
src.loop = true;
|
|
260
|
+
src.connect(lp);
|
|
261
|
+
src.start();
|
|
262
|
+
|
|
263
|
+
// Schedule random chirps
|
|
264
|
+
birdInterval = setInterval(() => {
|
|
265
|
+
if (Math.random() < birdRate) chirp();
|
|
266
|
+
}, 500);
|
|
267
|
+
},
|
|
268
|
+
stop() {
|
|
269
|
+
if (birdInterval) {
|
|
270
|
+
clearInterval(birdInterval);
|
|
271
|
+
birdInterval = null;
|
|
272
|
+
}
|
|
273
|
+
if (!src) return;
|
|
274
|
+
const ref = src;
|
|
275
|
+
src = null;
|
|
276
|
+
try {
|
|
277
|
+
ref.stop();
|
|
278
|
+
} catch {
|
|
279
|
+
/* already stopped */
|
|
280
|
+
}
|
|
281
|
+
ref.disconnect();
|
|
282
|
+
},
|
|
283
|
+
dispose() {
|
|
284
|
+
if (birdInterval) {
|
|
285
|
+
clearInterval(birdInterval);
|
|
286
|
+
birdInterval = null;
|
|
287
|
+
}
|
|
288
|
+
if (src) {
|
|
289
|
+
try {
|
|
290
|
+
src.stop();
|
|
291
|
+
} catch {
|
|
292
|
+
/* already stopped */
|
|
293
|
+
}
|
|
294
|
+
src.disconnect();
|
|
295
|
+
}
|
|
296
|
+
lp.disconnect();
|
|
297
|
+
out.disconnect();
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Impact sounds — procedural one-shot sounds for collisions and events.
|
|
3
|
+
*
|
|
4
|
+
* All functions take a Web Audio `AudioContext` and a destination `AudioNode`
|
|
5
|
+
* (typically a bus gain from setup-audio). Returns `{ play(), dispose() }`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function createNoiseBuffer(ctx: AudioContext, duration = 1): AudioBuffer {
|
|
9
|
+
const length = ctx.sampleRate * duration;
|
|
10
|
+
const buffer = ctx.createBuffer(1, length, ctx.sampleRate);
|
|
11
|
+
const data = buffer.getChannelData(0);
|
|
12
|
+
for (let i = 0; i < length; i++) data[i] = Math.random() * 2 - 1;
|
|
13
|
+
return buffer;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Short percussive thud — noise burst through a bandpass filter. */
|
|
17
|
+
export function hit(
|
|
18
|
+
ctx: AudioContext,
|
|
19
|
+
dest: AudioNode,
|
|
20
|
+
options?: { pitch?: number; volume?: number; duration?: number; brightness?: number },
|
|
21
|
+
) {
|
|
22
|
+
const pitch = options?.pitch ?? 400;
|
|
23
|
+
const volume = options?.volume ?? 0.5;
|
|
24
|
+
const duration = options?.duration ?? 0.1;
|
|
25
|
+
const brightness = options?.brightness ?? 1;
|
|
26
|
+
const noiseBuffer = createNoiseBuffer(ctx, 0.5);
|
|
27
|
+
|
|
28
|
+
const out = ctx.createGain();
|
|
29
|
+
out.gain.value = volume;
|
|
30
|
+
out.connect(dest);
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
play() {
|
|
34
|
+
const now = ctx.currentTime;
|
|
35
|
+
const src = ctx.createBufferSource();
|
|
36
|
+
src.buffer = noiseBuffer;
|
|
37
|
+
|
|
38
|
+
const filter = ctx.createBiquadFilter();
|
|
39
|
+
filter.type = 'bandpass';
|
|
40
|
+
filter.frequency.value = pitch;
|
|
41
|
+
filter.Q.value = brightness;
|
|
42
|
+
|
|
43
|
+
const env = ctx.createGain();
|
|
44
|
+
env.gain.setValueAtTime(1, now);
|
|
45
|
+
env.gain.exponentialRampToValueAtTime(0.001, now + duration);
|
|
46
|
+
|
|
47
|
+
src.connect(filter);
|
|
48
|
+
filter.connect(env);
|
|
49
|
+
env.connect(out);
|
|
50
|
+
src.start(now);
|
|
51
|
+
src.stop(now + duration + 0.01);
|
|
52
|
+
},
|
|
53
|
+
dispose() {
|
|
54
|
+
out.disconnect();
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Ball-bounce ping — sine tone with quick pitch drop. */
|
|
60
|
+
export function bounce(
|
|
61
|
+
ctx: AudioContext,
|
|
62
|
+
dest: AudioNode,
|
|
63
|
+
options?: { pitch?: number; volume?: number; duration?: number },
|
|
64
|
+
) {
|
|
65
|
+
const pitch = options?.pitch ?? 300;
|
|
66
|
+
const volume = options?.volume ?? 0.3;
|
|
67
|
+
const duration = options?.duration ?? 0.15;
|
|
68
|
+
|
|
69
|
+
const out = ctx.createGain();
|
|
70
|
+
out.gain.value = volume;
|
|
71
|
+
out.connect(dest);
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
play() {
|
|
75
|
+
const now = ctx.currentTime;
|
|
76
|
+
const osc = ctx.createOscillator();
|
|
77
|
+
osc.type = 'sine';
|
|
78
|
+
osc.frequency.setValueAtTime(pitch, now);
|
|
79
|
+
osc.frequency.exponentialRampToValueAtTime(pitch * 0.5, now + duration);
|
|
80
|
+
|
|
81
|
+
const env = ctx.createGain();
|
|
82
|
+
env.gain.setValueAtTime(1, now);
|
|
83
|
+
env.gain.exponentialRampToValueAtTime(0.001, now + duration);
|
|
84
|
+
|
|
85
|
+
osc.connect(env);
|
|
86
|
+
env.connect(out);
|
|
87
|
+
osc.start(now);
|
|
88
|
+
osc.stop(now + duration + 0.01);
|
|
89
|
+
},
|
|
90
|
+
dispose() {
|
|
91
|
+
out.disconnect();
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Heavy crash — layered noise burst with lowpass sweep + low sine thud. */
|
|
97
|
+
export function crash(
|
|
98
|
+
ctx: AudioContext,
|
|
99
|
+
dest: AudioNode,
|
|
100
|
+
options?: { volume?: number; duration?: number; lowFreq?: number },
|
|
101
|
+
) {
|
|
102
|
+
const volume = options?.volume ?? 0.7;
|
|
103
|
+
const duration = options?.duration ?? 0.5;
|
|
104
|
+
const lowFreq = options?.lowFreq ?? 80;
|
|
105
|
+
const noiseBuffer = createNoiseBuffer(ctx, 1);
|
|
106
|
+
|
|
107
|
+
const out = ctx.createGain();
|
|
108
|
+
out.gain.value = volume;
|
|
109
|
+
out.connect(dest);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
play() {
|
|
113
|
+
const now = ctx.currentTime;
|
|
114
|
+
|
|
115
|
+
// Noise layer: lowpass sweep from bright to dark
|
|
116
|
+
const noiseSrc = ctx.createBufferSource();
|
|
117
|
+
noiseSrc.buffer = noiseBuffer;
|
|
118
|
+
const lp = ctx.createBiquadFilter();
|
|
119
|
+
lp.type = 'lowpass';
|
|
120
|
+
lp.frequency.setValueAtTime(4000, now);
|
|
121
|
+
lp.frequency.exponentialRampToValueAtTime(200, now + duration);
|
|
122
|
+
const noiseEnv = ctx.createGain();
|
|
123
|
+
noiseEnv.gain.setValueAtTime(0.7, now);
|
|
124
|
+
noiseEnv.gain.exponentialRampToValueAtTime(0.001, now + duration);
|
|
125
|
+
noiseSrc.connect(lp);
|
|
126
|
+
lp.connect(noiseEnv);
|
|
127
|
+
noiseEnv.connect(out);
|
|
128
|
+
noiseSrc.start(now);
|
|
129
|
+
noiseSrc.stop(now + duration + 0.01);
|
|
130
|
+
|
|
131
|
+
// Low thud layer
|
|
132
|
+
const thud = ctx.createOscillator();
|
|
133
|
+
thud.type = 'sine';
|
|
134
|
+
thud.frequency.value = lowFreq;
|
|
135
|
+
const thudEnv = ctx.createGain();
|
|
136
|
+
thudEnv.gain.setValueAtTime(0.8, now);
|
|
137
|
+
thudEnv.gain.exponentialRampToValueAtTime(0.001, now + duration * 0.7);
|
|
138
|
+
thud.connect(thudEnv);
|
|
139
|
+
thudEnv.connect(out);
|
|
140
|
+
thud.start(now);
|
|
141
|
+
thud.stop(now + duration + 0.01);
|
|
142
|
+
},
|
|
143
|
+
dispose() {
|
|
144
|
+
out.disconnect();
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Large explosion — heavy noise burst with sub-bass tail. */
|
|
150
|
+
export function explosion(
|
|
151
|
+
ctx: AudioContext,
|
|
152
|
+
dest: AudioNode,
|
|
153
|
+
options?: { volume?: number; duration?: number; subFreq?: number },
|
|
154
|
+
) {
|
|
155
|
+
const volume = options?.volume ?? 0.8;
|
|
156
|
+
const duration = options?.duration ?? 1.0;
|
|
157
|
+
const subFreq = options?.subFreq ?? 40;
|
|
158
|
+
const noiseBuffer = createNoiseBuffer(ctx, 2);
|
|
159
|
+
|
|
160
|
+
const out = ctx.createGain();
|
|
161
|
+
out.gain.value = volume;
|
|
162
|
+
out.connect(dest);
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
play() {
|
|
166
|
+
const now = ctx.currentTime;
|
|
167
|
+
|
|
168
|
+
// Noise layer: wide lowpass sweep
|
|
169
|
+
const noiseSrc = ctx.createBufferSource();
|
|
170
|
+
noiseSrc.buffer = noiseBuffer;
|
|
171
|
+
const lp = ctx.createBiquadFilter();
|
|
172
|
+
lp.type = 'lowpass';
|
|
173
|
+
lp.frequency.setValueAtTime(6000, now);
|
|
174
|
+
lp.frequency.exponentialRampToValueAtTime(100, now + duration);
|
|
175
|
+
const noiseEnv = ctx.createGain();
|
|
176
|
+
noiseEnv.gain.setValueAtTime(1, now);
|
|
177
|
+
noiseEnv.gain.exponentialRampToValueAtTime(0.001, now + duration);
|
|
178
|
+
noiseSrc.connect(lp);
|
|
179
|
+
lp.connect(noiseEnv);
|
|
180
|
+
noiseEnv.connect(out);
|
|
181
|
+
noiseSrc.start(now);
|
|
182
|
+
noiseSrc.stop(now + duration + 0.01);
|
|
183
|
+
|
|
184
|
+
// Sub-bass layer
|
|
185
|
+
const sub = ctx.createOscillator();
|
|
186
|
+
sub.type = 'sine';
|
|
187
|
+
sub.frequency.value = subFreq;
|
|
188
|
+
const subEnv = ctx.createGain();
|
|
189
|
+
subEnv.gain.setValueAtTime(1, now);
|
|
190
|
+
subEnv.gain.exponentialRampToValueAtTime(0.001, now + duration * 0.8);
|
|
191
|
+
sub.connect(subEnv);
|
|
192
|
+
subEnv.connect(out);
|
|
193
|
+
sub.start(now);
|
|
194
|
+
sub.stop(now + duration + 0.01);
|
|
195
|
+
|
|
196
|
+
// Crunch layer: short sawtooth burst
|
|
197
|
+
const crunch = ctx.createOscillator();
|
|
198
|
+
crunch.type = 'sawtooth';
|
|
199
|
+
crunch.frequency.value = 60;
|
|
200
|
+
const crunchEnv = ctx.createGain();
|
|
201
|
+
crunchEnv.gain.setValueAtTime(0.5, now);
|
|
202
|
+
crunchEnv.gain.exponentialRampToValueAtTime(0.001, now + 0.15);
|
|
203
|
+
crunch.connect(crunchEnv);
|
|
204
|
+
crunchEnv.connect(out);
|
|
205
|
+
crunch.start(now);
|
|
206
|
+
crunch.stop(now + 0.2);
|
|
207
|
+
},
|
|
208
|
+
dispose() {
|
|
209
|
+
out.disconnect();
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { hum, nature, rain, wind } from './ambient';
|
|
2
|
+
export { bounce, crash, explosion, hit } from './impacts';
|
|
3
|
+
export { footstep, jump, whoosh } from './movement';
|
|
4
|
+
export { arpeggio, bassPulse, stinger } from './musical';
|
|
5
|
+
export { uiClick, uiError, uiHover, uiSuccess } from './ui-sounds';
|
|
6
|
+
export { brakeSqueal, engineDrone, tireSkid } from './vehicle';
|
|
7
|
+
export { chargeUp, gunshot, laser } from './weapons';
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Movement sounds — footsteps, whooshes, jumps.
|
|
3
|
+
*
|
|
4
|
+
* All one-shot: call play() to trigger, dispose() to clean up.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
function createNoiseBuffer(ctx: AudioContext, duration = 1): AudioBuffer {
|
|
8
|
+
const length = ctx.sampleRate * duration;
|
|
9
|
+
const buffer = ctx.createBuffer(1, length, ctx.sampleRate);
|
|
10
|
+
const data = buffer.getChannelData(0);
|
|
11
|
+
for (let i = 0; i < length; i++) data[i] = Math.random() * 2 - 1;
|
|
12
|
+
return buffer;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Footstep — filtered noise burst with random pitch variation per play(). */
|
|
16
|
+
export function footstep(
|
|
17
|
+
ctx: AudioContext,
|
|
18
|
+
dest: AudioNode,
|
|
19
|
+
options?: { pitch?: number; volume?: number; variation?: number },
|
|
20
|
+
) {
|
|
21
|
+
const pitch = options?.pitch ?? 250;
|
|
22
|
+
const volume = options?.volume ?? 0.3;
|
|
23
|
+
const variation = options?.variation ?? 0.2;
|
|
24
|
+
const noiseBuffer = createNoiseBuffer(ctx, 0.2);
|
|
25
|
+
|
|
26
|
+
const out = ctx.createGain();
|
|
27
|
+
out.gain.value = volume;
|
|
28
|
+
out.connect(dest);
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
play() {
|
|
32
|
+
const now = ctx.currentTime;
|
|
33
|
+
const src = ctx.createBufferSource();
|
|
34
|
+
src.buffer = noiseBuffer;
|
|
35
|
+
|
|
36
|
+
const filter = ctx.createBiquadFilter();
|
|
37
|
+
filter.type = 'bandpass';
|
|
38
|
+
filter.frequency.value = pitch * (1 + (Math.random() - 0.5) * 2 * variation);
|
|
39
|
+
filter.Q.value = 2;
|
|
40
|
+
|
|
41
|
+
const env = ctx.createGain();
|
|
42
|
+
env.gain.setValueAtTime(0, now);
|
|
43
|
+
env.gain.linearRampToValueAtTime(1, now + 0.007);
|
|
44
|
+
env.gain.exponentialRampToValueAtTime(0.001, now + 0.05);
|
|
45
|
+
|
|
46
|
+
src.connect(filter);
|
|
47
|
+
filter.connect(env);
|
|
48
|
+
env.connect(out);
|
|
49
|
+
src.start(now);
|
|
50
|
+
src.stop(now + 0.06);
|
|
51
|
+
},
|
|
52
|
+
dispose() {
|
|
53
|
+
out.disconnect();
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Whoosh — bandpass noise sweep for fast-moving objects. */
|
|
59
|
+
export function whoosh(
|
|
60
|
+
ctx: AudioContext,
|
|
61
|
+
dest: AudioNode,
|
|
62
|
+
options?: { pitch?: number; volume?: number; duration?: number; speed?: number },
|
|
63
|
+
) {
|
|
64
|
+
const pitch = options?.pitch ?? 600;
|
|
65
|
+
const volume = options?.volume ?? 0.3;
|
|
66
|
+
const duration = options?.duration ?? 0.2;
|
|
67
|
+
const speed = options?.speed ?? 1.0;
|
|
68
|
+
const noiseBuffer = createNoiseBuffer(ctx, 0.5);
|
|
69
|
+
|
|
70
|
+
const out = ctx.createGain();
|
|
71
|
+
out.gain.value = volume;
|
|
72
|
+
out.connect(dest);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
play() {
|
|
76
|
+
const now = ctx.currentTime;
|
|
77
|
+
const src = ctx.createBufferSource();
|
|
78
|
+
src.buffer = noiseBuffer;
|
|
79
|
+
|
|
80
|
+
const filter = ctx.createBiquadFilter();
|
|
81
|
+
filter.type = 'bandpass';
|
|
82
|
+
filter.Q.value = 2;
|
|
83
|
+
filter.frequency.setValueAtTime(pitch * speed * 2, now);
|
|
84
|
+
filter.frequency.exponentialRampToValueAtTime(Math.max(pitch * 0.5, 20), now + duration);
|
|
85
|
+
|
|
86
|
+
// Triangle envelope: fade in 30%, fade out 70%
|
|
87
|
+
const peak = duration * 0.3;
|
|
88
|
+
const env = ctx.createGain();
|
|
89
|
+
env.gain.setValueAtTime(0.001, now);
|
|
90
|
+
env.gain.linearRampToValueAtTime(1, now + peak);
|
|
91
|
+
env.gain.exponentialRampToValueAtTime(0.001, now + duration);
|
|
92
|
+
|
|
93
|
+
src.connect(filter);
|
|
94
|
+
filter.connect(env);
|
|
95
|
+
env.connect(out);
|
|
96
|
+
src.start(now);
|
|
97
|
+
src.stop(now + duration + 0.01);
|
|
98
|
+
},
|
|
99
|
+
dispose() {
|
|
100
|
+
out.disconnect();
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Jump — quick ascending triangle tone. */
|
|
106
|
+
export function jump(
|
|
107
|
+
ctx: AudioContext,
|
|
108
|
+
dest: AudioNode,
|
|
109
|
+
options?: { pitch?: number; volume?: number; duration?: number },
|
|
110
|
+
) {
|
|
111
|
+
const pitch = options?.pitch ?? 150;
|
|
112
|
+
const volume = options?.volume ?? 0.3;
|
|
113
|
+
const duration = options?.duration ?? 0.15;
|
|
114
|
+
|
|
115
|
+
const out = ctx.createGain();
|
|
116
|
+
out.gain.value = volume;
|
|
117
|
+
out.connect(dest);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
play() {
|
|
121
|
+
const now = ctx.currentTime;
|
|
122
|
+
const osc = ctx.createOscillator();
|
|
123
|
+
osc.type = 'triangle';
|
|
124
|
+
osc.frequency.setValueAtTime(pitch, now);
|
|
125
|
+
osc.frequency.exponentialRampToValueAtTime(pitch * 2, now + duration);
|
|
126
|
+
|
|
127
|
+
const env = ctx.createGain();
|
|
128
|
+
env.gain.setValueAtTime(0.8, now);
|
|
129
|
+
env.gain.exponentialRampToValueAtTime(0.001, now + duration);
|
|
130
|
+
|
|
131
|
+
osc.connect(env);
|
|
132
|
+
env.connect(out);
|
|
133
|
+
osc.start(now);
|
|
134
|
+
osc.stop(now + duration + 0.01);
|
|
135
|
+
},
|
|
136
|
+
dispose() {
|
|
137
|
+
out.disconnect();
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|