@vgai/engine 0.2.0 → 0.4.0-canary.20260715.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.
Files changed (123) hide show
  1. package/README.md +3 -1
  2. package/package.json +24 -4
  3. package/schemas/engine-api.json +124 -0
  4. package/schemas/engine-api.md +53 -0
  5. package/schemas/engine-capabilities.json +124 -0
  6. package/schemas/inputmap.schema.json +314 -0
  7. package/schemas/mat.schema.json +286 -0
  8. package/schemas/prefab.schema.json +10148 -0
  9. package/schemas/scn2d.schema.json +475 -0
  10. package/schemas/vgai-game.schema.json +383 -0
  11. package/schemas/vscn.schema.json +11007 -0
  12. package/src/adapter/{world-kind.ts → adapter-surface.ts} +6 -6
  13. package/src/adapter/authoring.ts +77 -0
  14. package/src/adapter/first-party-systems.ts +23 -34
  15. package/src/adapter/game-adapter.ts +8 -8
  16. package/src/adapter/host-context.ts +2 -4
  17. package/src/adapter/index.ts +4 -4
  18. package/src/adapter/system-adapter.ts +88 -22
  19. package/src/adapter/vgai-scene-game-adapter.ts +244 -194
  20. package/src/animation/anim-graph-types.ts +12 -43
  21. package/src/animation/animation-clock.ts +479 -0
  22. package/src/animation/camera-ownership.ts +467 -0
  23. package/src/animation/cinematic-cues.ts +451 -0
  24. package/src/animation/clip-map.ts +41 -0
  25. package/src/animation/gsap-registration.ts +184 -0
  26. package/src/animation/theatre-clock-binding.ts +111 -0
  27. package/src/animation/theatre-director.ts +347 -0
  28. package/src/animation/theatre-object-binding.ts +661 -0
  29. package/src/animation/xstate-animation-binding.ts +436 -0
  30. package/src/animation/xstate-animation-meta.ts +319 -0
  31. package/src/audio/index.ts +39 -7
  32. package/src/audio/tone-clock-binding.ts +98 -0
  33. package/src/audio/tone-context.ts +129 -0
  34. package/src/audio/tone-offline-render.ts +167 -0
  35. package/src/audio/wav-encode.ts +119 -0
  36. package/src/character/cloth-sim.ts +533 -0
  37. package/src/character/spring-chain.ts +307 -0
  38. package/src/core/game-loop.ts +57 -2
  39. package/src/core/seeded-random.ts +161 -0
  40. package/src/core/system-runner.ts +20 -3
  41. package/src/core/types.ts +50 -0
  42. package/src/data/data-asset.ts +167 -0
  43. package/src/data/data-check-core.ts +242 -0
  44. package/src/data/data-ref.ts +145 -0
  45. package/src/data/vite-plugin-data.ts +290 -0
  46. package/src/dev/performance-profiler.ts +213 -0
  47. package/src/dev/webgl-gpu-timer.ts +53 -0
  48. package/src/ecs/component-manager.ts +45 -12
  49. package/src/ecs/game-component.ts +95 -11
  50. package/src/humanoid/bake.operation.ts +326 -0
  51. package/src/humanoid/body.ts +663 -0
  52. package/src/humanoid/clips.ts +149 -0
  53. package/src/humanoid/compose.ts +209 -0
  54. package/src/humanoid/generate.ts +189 -0
  55. package/src/humanoid/index.ts +36 -0
  56. package/src/humanoid/schema.ts +108 -0
  57. package/src/humanoid/skeleton.ts +345 -0
  58. package/src/index.ts +48 -0
  59. package/src/input/input-manager.ts +1886 -33
  60. package/src/input/input-types.ts +158 -3
  61. package/src/input/prompt-labels.ts +122 -0
  62. package/src/input/rebind-controller.ts +105 -0
  63. package/src/input/schema.ts +206 -52
  64. package/src/manifest/index.ts +5 -5
  65. package/src/manifest/load.ts +125 -72
  66. package/src/manifest/schema.ts +362 -255
  67. package/src/react/game-state.tsx +135 -32
  68. package/src/react/root-adapter.tsx +49 -0
  69. package/src/react/unmanaged-root-detector.ts +66 -0
  70. package/src/react/use-data.ts +124 -0
  71. package/src/react/use-selection.tsx +135 -0
  72. package/src/runtime/create-runtime.ts +112 -273
  73. package/src/runtime/debug-bridge.ts +483 -0
  74. package/src/runtime/debug-registry.ts +856 -0
  75. package/src/runtime/game.ts +342 -93
  76. package/src/runtime/gameplay-rng-trap.ts +134 -0
  77. package/src/runtime/input-router.ts +7 -7
  78. package/src/runtime/mount-game.ts +40 -38
  79. package/src/runtime/mount-manifest.ts +169 -37
  80. package/src/runtime/render-audio-control.ts +168 -0
  81. package/src/runtime/render-control.ts +522 -0
  82. package/src/runtime/render-seed.ts +79 -0
  83. package/src/runtime/state-bridge.ts +24 -10
  84. package/src/runtime/types.ts +110 -33
  85. package/src/scene/asset-loaders.ts +10 -36
  86. package/src/scene/asset-paths.ts +0 -2
  87. package/src/scene/asset-ref-check.ts +248 -0
  88. package/src/scene/asset-registry.ts +22 -0
  89. package/src/scene/component-registry.ts +14 -3
  90. package/src/scene/defaults.ts +1 -0
  91. package/src/scene/light-camera-factory.ts +11 -3
  92. package/src/scene/parse.ts +133 -0
  93. package/src/scene/scene-apply.ts +55 -4
  94. package/src/scene/scene-loader.ts +91 -123
  95. package/src/scene/scene-types.ts +0 -1
  96. package/src/scene/schema/animation.ts +30 -79
  97. package/src/scene/schema/entity.ts +20 -0
  98. package/src/scene/schema/index.ts +2 -46
  99. package/src/scene/schema/light.ts +16 -1
  100. package/src/scene/schema/material.ts +96 -91
  101. package/src/scene/schema/scene-file.ts +1 -7
  102. package/src/scene/user-data.ts +22 -10
  103. package/src/setup/setup-renderer.ts +10 -3
  104. package/src/tools/define-tool.ts +191 -0
  105. package/src/world2d/authoring-2d.ts +17 -1
  106. package/src/world2d/collision-2d.ts +1 -1
  107. package/src/world2d/pixi-game-adapter.ts +19 -17
  108. package/src/world2d/scene2d-loader.ts +1 -0
  109. package/src/world2d/types.ts +8 -2
  110. package/src/animation/anim-graph.ts +0 -406
  111. package/src/animation/anim-system.ts +0 -28
  112. package/src/animation/property-track.ts +0 -178
  113. package/src/animation/schema.ts +0 -204
  114. package/src/audio/ambient.ts +0 -300
  115. package/src/audio/impacts.ts +0 -212
  116. package/src/audio/movement.ts +0 -140
  117. package/src/audio/musical.ts +0 -200
  118. package/src/audio/ui-sounds.ts +0 -171
  119. package/src/audio/vehicle.ts +0 -235
  120. package/src/audio/weapons.ts +0 -152
  121. package/src/runtime/scene-ui-bridge.ts +0 -86
  122. package/src/runtime/scene-ui-data.ts +0 -119
  123. package/src/scene/schema/ui.ts +0 -602
@@ -1,212 +0,0 @@
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
- }
@@ -1,140 +0,0 @@
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
- }
@@ -1,200 +0,0 @@
1
- /**
2
- * Musical sounds — rhythmic and melodic elements.
3
- *
4
- * bassPulse and arpeggio are continuous (play/stop).
5
- * stinger is one-shot.
6
- */
7
-
8
- /** Bass pulse — rhythmic low sine following a beat pattern. */
9
- export function bassPulse(
10
- ctx: AudioContext,
11
- dest: AudioNode,
12
- options?: { frequency?: number; bpm?: number; volume?: number; pattern?: number[] },
13
- ) {
14
- const frequency = options?.frequency ?? 55;
15
- const bpm = options?.bpm ?? 120;
16
- const volume = options?.volume ?? 0.3;
17
- const pattern = options?.pattern ?? [1, 0, 1, 0];
18
- const beatInterval = 60 / bpm;
19
-
20
- const out = ctx.createGain();
21
- out.gain.value = volume;
22
- out.connect(dest);
23
-
24
- let osc: OscillatorNode | null = null;
25
- let pulseGain: GainNode | null = null;
26
- let timer: ReturnType<typeof setInterval> | null = null;
27
- let step = 0;
28
-
29
- return {
30
- play() {
31
- if (osc) return;
32
- osc = ctx.createOscillator();
33
- osc.type = 'sine';
34
- osc.frequency.value = frequency;
35
- pulseGain = ctx.createGain();
36
- pulseGain.gain.value = 0;
37
- osc.connect(pulseGain);
38
- pulseGain.connect(out);
39
- osc.start();
40
- step = 0;
41
-
42
- const tick = () => {
43
- if (!pulseGain) return;
44
- const beat = pattern[step % pattern.length]!;
45
- const now = ctx.currentTime;
46
- if (beat) {
47
- pulseGain.gain.setValueAtTime(1, now);
48
- pulseGain.gain.exponentialRampToValueAtTime(0.01, now + beatInterval * 0.8);
49
- }
50
- step++;
51
- };
52
-
53
- tick();
54
- timer = setInterval(tick, beatInterval * 1000);
55
- },
56
- stop() {
57
- if (timer) {
58
- clearInterval(timer);
59
- timer = null;
60
- }
61
- if (!osc) return;
62
- const refs = { osc, pulseGain };
63
- osc = null;
64
- pulseGain = null;
65
- setTimeout(() => {
66
- try {
67
- refs.osc.stop();
68
- } catch {
69
- /* already stopped */
70
- }
71
- refs.osc.disconnect();
72
- refs.pulseGain?.disconnect();
73
- }, 100);
74
- },
75
- dispose() {
76
- if (timer) {
77
- clearInterval(timer);
78
- timer = null;
79
- }
80
- if (osc) {
81
- try {
82
- osc.stop();
83
- } catch {
84
- /* already stopped */
85
- }
86
- osc.disconnect();
87
- }
88
- pulseGain?.disconnect();
89
- osc = null;
90
- pulseGain = null;
91
- out.disconnect();
92
- },
93
- };
94
- }
95
-
96
- /** Arpeggio — repeating note sequence. */
97
- export function arpeggio(
98
- ctx: AudioContext,
99
- dest: AudioNode,
100
- options?: {
101
- notes?: number[];
102
- bpm?: number;
103
- waveform?: OscillatorType;
104
- volume?: number;
105
- },
106
- ) {
107
- const notes = options?.notes ?? [262, 330, 392, 523];
108
- const bpm = options?.bpm ?? 140;
109
- const waveform = options?.waveform ?? 'triangle';
110
- const volume = options?.volume ?? 0.2;
111
- const noteInterval = 60 / bpm;
112
-
113
- const out = ctx.createGain();
114
- out.gain.value = volume;
115
- out.connect(dest);
116
-
117
- let timer: ReturnType<typeof setInterval> | null = null;
118
- let step = 0;
119
-
120
- function playNote(freq: number) {
121
- const now = ctx.currentTime;
122
- const osc = ctx.createOscillator();
123
- osc.type = waveform;
124
- osc.frequency.value = freq;
125
-
126
- const env = ctx.createGain();
127
- env.gain.setValueAtTime(0.8, now);
128
- env.gain.exponentialRampToValueAtTime(0.001, now + noteInterval * 0.9);
129
-
130
- osc.connect(env);
131
- env.connect(out);
132
- osc.start(now);
133
- osc.stop(now + noteInterval);
134
- }
135
-
136
- return {
137
- play() {
138
- if (timer) return;
139
- step = 0;
140
- const tick = () => {
141
- playNote(notes[step % notes.length]!);
142
- step++;
143
- };
144
- tick();
145
- timer = setInterval(tick, noteInterval * 1000);
146
- },
147
- stop() {
148
- if (timer) {
149
- clearInterval(timer);
150
- timer = null;
151
- }
152
- },
153
- dispose() {
154
- if (timer) {
155
- clearInterval(timer);
156
- timer = null;
157
- }
158
- out.disconnect();
159
- },
160
- };
161
- }
162
-
163
- /** Stinger — one-shot ascending note sequence for events. */
164
- export function stinger(
165
- ctx: AudioContext,
166
- dest: AudioNode,
167
- options?: { notes?: number[]; volume?: number; tempo?: number },
168
- ) {
169
- const notes = options?.notes ?? [392, 494, 587, 784];
170
- const volume = options?.volume ?? 0.3;
171
- const tempo = options?.tempo ?? 0.12;
172
-
173
- const out = ctx.createGain();
174
- out.gain.value = volume;
175
- out.connect(dest);
176
-
177
- return {
178
- play() {
179
- const now = ctx.currentTime;
180
- for (let i = 0; i < notes.length; i++) {
181
- const t = now + i * tempo;
182
- const osc = ctx.createOscillator();
183
- osc.type = 'triangle';
184
- osc.frequency.value = notes[i]!;
185
-
186
- const env = ctx.createGain();
187
- env.gain.setValueAtTime(0.8, t);
188
- env.gain.exponentialRampToValueAtTime(0.001, t + tempo * 1.5);
189
-
190
- osc.connect(env);
191
- env.connect(out);
192
- osc.start(t);
193
- osc.stop(t + tempo * 1.5 + 0.01);
194
- }
195
- },
196
- dispose() {
197
- out.disconnect();
198
- },
199
- };
200
- }
@@ -1,171 +0,0 @@
1
- /**
2
- * UI sounds — short tonal feedback for interface interactions.
3
- *
4
- * All one-shot: call play() to trigger, dispose() to clean up.
5
- */
6
-
7
- /** Click — short sine pip. */
8
- export function uiClick(
9
- ctx: AudioContext,
10
- dest: AudioNode,
11
- options?: { pitch?: number; volume?: number },
12
- ) {
13
- const pitch = options?.pitch ?? 800;
14
- const volume = options?.volume ?? 0.2;
15
-
16
- const out = ctx.createGain();
17
- out.gain.value = volume;
18
- out.connect(dest);
19
-
20
- return {
21
- play() {
22
- const now = ctx.currentTime;
23
- const osc = ctx.createOscillator();
24
- osc.type = 'sine';
25
- osc.frequency.value = pitch;
26
-
27
- const env = ctx.createGain();
28
- env.gain.setValueAtTime(1, now);
29
- env.gain.exponentialRampToValueAtTime(0.001, now + 0.04);
30
-
31
- osc.connect(env);
32
- env.connect(out);
33
- osc.start(now);
34
- osc.stop(now + 0.05);
35
- },
36
- dispose() {
37
- out.disconnect();
38
- },
39
- };
40
- }
41
-
42
- /** Hover — softer, higher sine pip. */
43
- export function uiHover(
44
- ctx: AudioContext,
45
- dest: AudioNode,
46
- options?: { pitch?: number; volume?: number },
47
- ) {
48
- const pitch = options?.pitch ?? 1200;
49
- const volume = options?.volume ?? 0.1;
50
-
51
- const out = ctx.createGain();
52
- out.gain.value = volume;
53
- out.connect(dest);
54
-
55
- return {
56
- play() {
57
- const now = ctx.currentTime;
58
- const osc = ctx.createOscillator();
59
- osc.type = 'sine';
60
- osc.frequency.value = pitch;
61
-
62
- const env = ctx.createGain();
63
- env.gain.setValueAtTime(1, now);
64
- env.gain.exponentialRampToValueAtTime(0.001, now + 0.03);
65
-
66
- osc.connect(env);
67
- env.connect(out);
68
- osc.start(now);
69
- osc.stop(now + 0.04);
70
- },
71
- dispose() {
72
- out.disconnect();
73
- },
74
- };
75
- }
76
-
77
- /** Success — two ascending tones (major third interval). */
78
- export function uiSuccess(
79
- ctx: AudioContext,
80
- dest: AudioNode,
81
- options?: { pitch?: number; volume?: number },
82
- ) {
83
- const pitch = options?.pitch ?? 500;
84
- const volume = options?.volume ?? 0.3;
85
-
86
- const out = ctx.createGain();
87
- out.gain.value = volume;
88
- out.connect(dest);
89
-
90
- return {
91
- play() {
92
- const now = ctx.currentTime;
93
-
94
- // First note
95
- const osc1 = ctx.createOscillator();
96
- osc1.type = 'sine';
97
- osc1.frequency.value = pitch;
98
- const env1 = ctx.createGain();
99
- env1.gain.setValueAtTime(1, now);
100
- env1.gain.exponentialRampToValueAtTime(0.001, now + 0.15);
101
- osc1.connect(env1);
102
- env1.connect(out);
103
- osc1.start(now);
104
- osc1.stop(now + 0.16);
105
-
106
- // Second note — major third above, delayed 0.1s
107
- const osc2 = ctx.createOscillator();
108
- osc2.type = 'sine';
109
- osc2.frequency.value = pitch * (5 / 4);
110
- const env2 = ctx.createGain();
111
- env2.gain.setValueAtTime(0.001, now);
112
- env2.gain.setValueAtTime(1, now + 0.1);
113
- env2.gain.exponentialRampToValueAtTime(0.001, now + 0.25);
114
- osc2.connect(env2);
115
- env2.connect(out);
116
- osc2.start(now);
117
- osc2.stop(now + 0.26);
118
- },
119
- dispose() {
120
- out.disconnect();
121
- },
122
- };
123
- }
124
-
125
- /** Error — two descending tones (minor second). */
126
- export function uiError(
127
- ctx: AudioContext,
128
- dest: AudioNode,
129
- options?: { pitch?: number; volume?: number },
130
- ) {
131
- const pitch = options?.pitch ?? 400;
132
- const volume = options?.volume ?? 0.3;
133
-
134
- const out = ctx.createGain();
135
- out.gain.value = volume;
136
- out.connect(dest);
137
-
138
- return {
139
- play() {
140
- const now = ctx.currentTime;
141
-
142
- // First note
143
- const osc1 = ctx.createOscillator();
144
- osc1.type = 'sine';
145
- osc1.frequency.value = pitch;
146
- const env1 = ctx.createGain();
147
- env1.gain.setValueAtTime(1, now);
148
- env1.gain.exponentialRampToValueAtTime(0.001, now + 0.15);
149
- osc1.connect(env1);
150
- env1.connect(out);
151
- osc1.start(now);
152
- osc1.stop(now + 0.16);
153
-
154
- // Second note — minor second below, delayed 0.1s
155
- const osc2 = ctx.createOscillator();
156
- osc2.type = 'sine';
157
- osc2.frequency.value = pitch * 0.9;
158
- const env2 = ctx.createGain();
159
- env2.gain.setValueAtTime(0.001, now);
160
- env2.gain.setValueAtTime(1, now + 0.1);
161
- env2.gain.exponentialRampToValueAtTime(0.001, now + 0.25);
162
- osc2.connect(env2);
163
- env2.connect(out);
164
- osc2.start(now);
165
- osc2.stop(now + 0.26);
166
- },
167
- dispose() {
168
- out.disconnect();
169
- },
170
- };
171
- }