pixospritz-core 0.10.1 → 1.0.1

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 (157) hide show
  1. package/README.md +36 -286
  2. package/dist/bundle.js +13 -3
  3. package/dist/bundle.js.map +1 -1
  4. package/dist/style.css +1 -0
  5. package/package.json +43 -44
  6. package/src/components/WebGLView.jsx +318 -0
  7. package/src/css/pixos.css +372 -0
  8. package/src/engine/actions/animate.js +41 -0
  9. package/src/engine/actions/changezone.js +135 -0
  10. package/src/engine/actions/chat.js +109 -0
  11. package/src/engine/actions/dialogue.js +90 -0
  12. package/src/engine/actions/face.js +22 -0
  13. package/src/engine/actions/greeting.js +28 -0
  14. package/src/engine/actions/interact.js +86 -0
  15. package/src/engine/actions/move.js +67 -0
  16. package/src/engine/actions/patrol.js +109 -0
  17. package/src/engine/actions/prompt.js +185 -0
  18. package/src/engine/actions/script.js +42 -0
  19. package/src/engine/core/audio/AudioSystem.js +543 -0
  20. package/src/engine/core/cutscene/PxcPlayer.js +956 -0
  21. package/src/engine/core/cutscene/manager.js +243 -0
  22. package/src/engine/core/database/index.js +75 -0
  23. package/src/engine/core/debug/index.js +371 -0
  24. package/src/engine/core/hud/index.js +765 -0
  25. package/src/engine/core/index.js +540 -0
  26. package/src/engine/core/input/gamepad/Controller.js +71 -0
  27. package/src/engine/core/input/gamepad/ControllerButtons.js +231 -0
  28. package/src/engine/core/input/gamepad/ControllerStick.js +173 -0
  29. package/src/engine/core/input/gamepad/index.js +592 -0
  30. package/src/engine/core/input/keyboard.js +196 -0
  31. package/src/engine/core/input/manager.js +485 -0
  32. package/src/engine/core/input/mouse.js +203 -0
  33. package/src/engine/core/input/touch.js +175 -0
  34. package/src/engine/core/mode/manager.js +199 -0
  35. package/src/engine/core/net/manager.js +535 -0
  36. package/src/engine/core/queue/action.js +83 -0
  37. package/src/engine/core/queue/event.js +82 -0
  38. package/src/engine/core/queue/index.js +44 -0
  39. package/src/engine/core/queue/loadable.js +33 -0
  40. package/src/engine/core/render/CameraEffects.js +494 -0
  41. package/src/engine/core/render/FrustumCuller.js +417 -0
  42. package/src/engine/core/render/LODManager.js +285 -0
  43. package/src/engine/core/render/ParticleManager.js +529 -0
  44. package/src/engine/core/render/TextureAtlas.js +465 -0
  45. package/src/engine/core/render/camera.js +338 -0
  46. package/src/engine/core/render/light.js +197 -0
  47. package/src/engine/core/render/manager.js +1079 -0
  48. package/src/engine/core/render/shaders.js +110 -0
  49. package/src/engine/core/render/skybox.js +342 -0
  50. package/src/engine/core/resource/manager.js +133 -0
  51. package/src/engine/core/resource/object.js +611 -0
  52. package/src/engine/core/resource/texture.js +103 -0
  53. package/src/engine/core/resource/tileset.js +177 -0
  54. package/src/engine/core/scene/avatar.js +215 -0
  55. package/src/engine/core/scene/speech.js +138 -0
  56. package/src/engine/core/scene/sprite.js +702 -0
  57. package/src/engine/core/scene/spritz.js +189 -0
  58. package/src/engine/core/scene/world.js +681 -0
  59. package/src/engine/core/scene/zone.js +1167 -0
  60. package/src/engine/core/store/index.js +110 -0
  61. package/src/engine/dynamic/animatedSprite.js +64 -0
  62. package/src/engine/dynamic/animatedTile.js +98 -0
  63. package/src/engine/dynamic/avatar.js +110 -0
  64. package/src/engine/dynamic/map.js +174 -0
  65. package/src/engine/dynamic/sprite.js +255 -0
  66. package/src/engine/dynamic/spritz.js +119 -0
  67. package/src/engine/events/EventSystem.js +609 -0
  68. package/src/engine/events/camera.js +142 -0
  69. package/src/engine/events/chat.js +75 -0
  70. package/src/engine/events/menu.js +186 -0
  71. package/src/engine/scripting/CallbackManager.js +514 -0
  72. package/src/engine/scripting/PixoScriptInterpreter.js +81 -0
  73. package/src/engine/scripting/PixoScriptLibrary.js +704 -0
  74. package/src/engine/shaders/effects/index.js +450 -0
  75. package/src/engine/shaders/fs.js +222 -0
  76. package/src/engine/shaders/particles/fs.js +41 -0
  77. package/src/engine/shaders/particles/vs.js +61 -0
  78. package/src/engine/shaders/picker/fs.js +34 -0
  79. package/src/engine/shaders/picker/init.js +62 -0
  80. package/src/engine/shaders/picker/vs.js +42 -0
  81. package/src/engine/shaders/pxsl/README.md +250 -0
  82. package/src/engine/shaders/pxsl/index.js +25 -0
  83. package/src/engine/shaders/pxsl/library.js +608 -0
  84. package/src/engine/shaders/pxsl/manager.js +338 -0
  85. package/src/engine/shaders/pxsl/specification.js +363 -0
  86. package/src/engine/shaders/pxsl/transpiler.js +753 -0
  87. package/src/engine/shaders/skybox/cosmic/fs.js +147 -0
  88. package/src/engine/shaders/skybox/cosmic/vs.js +23 -0
  89. package/src/engine/shaders/skybox/matrix/fs.js +127 -0
  90. package/src/engine/shaders/skybox/matrix/vs.js +23 -0
  91. package/src/engine/shaders/skybox/morning/fs.js +109 -0
  92. package/src/engine/shaders/skybox/morning/vs.js +23 -0
  93. package/src/engine/shaders/skybox/neon/fs.js +119 -0
  94. package/src/engine/shaders/skybox/neon/vs.js +23 -0
  95. package/src/engine/shaders/skybox/sky/fs.js +114 -0
  96. package/src/engine/shaders/skybox/sky/vs.js +23 -0
  97. package/src/engine/shaders/skybox/sunset/fs.js +101 -0
  98. package/src/engine/shaders/skybox/sunset/vs.js +23 -0
  99. package/src/engine/shaders/transition/blur/fs.js +42 -0
  100. package/src/engine/shaders/transition/blur/vs.js +26 -0
  101. package/src/engine/shaders/transition/cross/fs.js +36 -0
  102. package/src/engine/shaders/transition/cross/vs.js +26 -0
  103. package/src/engine/shaders/transition/crossBlur/fs.js +41 -0
  104. package/src/engine/shaders/transition/crossBlur/vs.js +25 -0
  105. package/src/engine/shaders/transition/dissolve/fs.js +78 -0
  106. package/src/engine/shaders/transition/dissolve/vs.js +24 -0
  107. package/src/engine/shaders/transition/fade/fs.js +31 -0
  108. package/src/engine/shaders/transition/fade/vs.js +27 -0
  109. package/src/engine/shaders/transition/iris/fs.js +52 -0
  110. package/src/engine/shaders/transition/iris/vs.js +24 -0
  111. package/src/engine/shaders/transition/pixelate/fs.js +44 -0
  112. package/src/engine/shaders/transition/pixelate/vs.js +24 -0
  113. package/src/engine/shaders/transition/slide/fs.js +53 -0
  114. package/src/engine/shaders/transition/slide/vs.js +24 -0
  115. package/src/engine/shaders/transition/swirl/fs.js +39 -0
  116. package/src/engine/shaders/transition/swirl/vs.js +26 -0
  117. package/src/engine/shaders/transition/wipe/fs.js +50 -0
  118. package/src/engine/shaders/transition/wipe/vs.js +24 -0
  119. package/src/engine/shaders/vs.js +60 -0
  120. package/src/engine/utils/CameraController.js +506 -0
  121. package/src/engine/utils/ObjHelper.js +551 -0
  122. package/src/engine/utils/debug-logger.js +110 -0
  123. package/src/engine/utils/enums.js +305 -0
  124. package/src/engine/utils/generator.js +156 -0
  125. package/src/engine/utils/index.js +21 -0
  126. package/src/engine/utils/loaders/ActionLoader.js +77 -0
  127. package/src/engine/utils/loaders/AudioLoader.js +157 -0
  128. package/src/engine/utils/loaders/EventLoader.js +66 -0
  129. package/src/engine/utils/loaders/ObjectLoader.js +67 -0
  130. package/src/engine/utils/loaders/SpriteLoader.js +77 -0
  131. package/src/engine/utils/loaders/TilesetLoader.js +103 -0
  132. package/src/engine/utils/loaders/index.js +21 -0
  133. package/src/engine/utils/math/matrix4.js +367 -0
  134. package/src/engine/utils/math/vector.js +458 -0
  135. package/src/engine/utils/obj/_old_js/index.js +46 -0
  136. package/src/engine/utils/obj/_old_js/layout.js +308 -0
  137. package/src/engine/utils/obj/_old_js/material.js +711 -0
  138. package/src/engine/utils/obj/_old_js/mesh.js +761 -0
  139. package/src/engine/utils/obj/_old_js/utils.js +647 -0
  140. package/src/engine/utils/obj/index.js +24 -0
  141. package/src/engine/utils/obj/js/index.js +277 -0
  142. package/src/engine/utils/obj/js/loader.js +232 -0
  143. package/src/engine/utils/obj/layout.js +246 -0
  144. package/src/engine/utils/obj/material.js +665 -0
  145. package/src/engine/utils/obj/mesh.js +657 -0
  146. package/src/engine/utils/obj/ts/index.ts +72 -0
  147. package/src/engine/utils/obj/ts/layout.ts +265 -0
  148. package/src/engine/utils/obj/ts/material.ts +760 -0
  149. package/src/engine/utils/obj/ts/mesh.ts +785 -0
  150. package/src/engine/utils/obj/ts/utils.ts +501 -0
  151. package/src/engine/utils/obj/utils.js +428 -0
  152. package/src/engine/utils/resources.js +18 -0
  153. package/src/index.jsx +55 -0
  154. package/src/spritz/player.js +18 -0
  155. package/src/spritz/readme.md +18 -0
  156. package/LICENSE +0 -437
  157. package/dist/bundle.js.LICENSE.txt +0 -31
@@ -0,0 +1,543 @@
1
+ /* *\
2
+ ** ----------------------------------------------- **
3
+ ** Calliope - Pixos Game Engine **
4
+ ** ----------------------------------------------- **
5
+ ** Copyright (c) 2020-2025 - Kyle Derby MacInnis **
6
+ ** **
7
+ ** Any unauthorized distribution or transfer **
8
+ ** of this work is strictly prohibited. **
9
+ ** **
10
+ ** All Rights Reserved. **
11
+ ** ----------------------------------------------- **
12
+ \* */
13
+
14
+ /**
15
+ * AudioSystem - Advanced audio management with mixing, effects, and spatial audio.
16
+ * Provides professional-quality audio experience for games.
17
+ */
18
+
19
+ /**
20
+ * @typedef {object} AudioChannel
21
+ * @property {string} name - Channel name
22
+ * @property {GainNode} gainNode - Volume control node
23
+ * @property {number} volume - Current volume (0-1)
24
+ * @property {boolean} muted - Whether channel is muted
25
+ */
26
+
27
+ /**
28
+ * @typedef {object} SpatialAudioConfig
29
+ * @property {number} maxDistance - Maximum audible distance
30
+ * @property {number} refDistance - Reference distance for attenuation
31
+ * @property {number} rolloffFactor - How quickly sound attenuates
32
+ * @property {string} distanceModel - 'linear', 'inverse', or 'exponential'
33
+ */
34
+
35
+ export default class AudioSystem {
36
+ /**
37
+ * Creates an instance of AudioSystem.
38
+ * @param {import('../index.js').default} engine - The game engine instance.
39
+ */
40
+ constructor(engine) {
41
+ /** @type {import('../index.js').default} */
42
+ this.engine = engine;
43
+
44
+ /** @type {AudioContext|null} */
45
+ this.context = null;
46
+
47
+ /** @type {GainNode|null} */
48
+ this.masterGain = null;
49
+
50
+ /** @type {Map<string, AudioChannel>} */
51
+ this.channels = new Map();
52
+
53
+ /** @type {Map<string, AudioBuffer>} */
54
+ this.bufferCache = new Map();
55
+
56
+ /** @type {Map<string, AudioBufferSourceNode>} */
57
+ this.activeSources = new Map();
58
+
59
+ /** @type {DynamicsCompressorNode|null} */
60
+ this.compressor = null;
61
+
62
+ /** @type {ConvolverNode|null} */
63
+ this.reverb = null;
64
+
65
+ /** @type {GainNode|null} */
66
+ this.reverbGain = null;
67
+
68
+ /** @type {boolean} */
69
+ this.initialized = false;
70
+
71
+ /** @type {SpatialAudioConfig} */
72
+ this.spatialConfig = {
73
+ maxDistance: 100,
74
+ refDistance: 1,
75
+ rolloffFactor: 1,
76
+ distanceModel: 'inverse',
77
+ };
78
+
79
+ /** @type {{ x: number, y: number, z: number }} */
80
+ this.listenerPosition = { x: 0, y: 0, z: 0 };
81
+
82
+ // Crossfade state
83
+ /** @type {Map<string, { source: AudioBufferSourceNode, gain: GainNode }>} */
84
+ this.musicTracks = new Map();
85
+ }
86
+
87
+ /**
88
+ * Initializes the audio system. Must be called after user interaction.
89
+ * @returns {Promise<void>}
90
+ */
91
+ async init() {
92
+ if (this.initialized) return;
93
+
94
+ try {
95
+ this.context = new (window.AudioContext || window.webkitAudioContext)();
96
+
97
+ // Create master gain
98
+ this.masterGain = this.context.createGain();
99
+ this.masterGain.gain.value = 1.0;
100
+
101
+ // Create compressor to prevent clipping
102
+ this.compressor = this.context.createDynamicsCompressor();
103
+ this.compressor.threshold.value = -24;
104
+ this.compressor.knee.value = 30;
105
+ this.compressor.ratio.value = 12;
106
+ this.compressor.attack.value = 0.003;
107
+ this.compressor.release.value = 0.25;
108
+
109
+ // Create reverb (dry by default)
110
+ this.reverbGain = this.context.createGain();
111
+ this.reverbGain.gain.value = 0; // Reverb off by default
112
+
113
+ // Connect: master -> compressor -> destination
114
+ this.masterGain.connect(this.compressor);
115
+ this.compressor.connect(this.context.destination);
116
+
117
+ // Create default channels
118
+ this.createChannel('master', 1.0);
119
+ this.createChannel('music', 0.7);
120
+ this.createChannel('sfx', 1.0);
121
+ this.createChannel('dialogue', 1.0);
122
+ this.createChannel('ambient', 0.5);
123
+
124
+ this.initialized = true;
125
+ } catch (e) {
126
+ console.error('Failed to initialize AudioSystem:', e);
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Creates an audio channel for mixing.
132
+ * @param {string} name - Channel name
133
+ * @param {number} [volume=1.0] - Initial volume
134
+ * @returns {AudioChannel}
135
+ */
136
+ createChannel(name, volume = 1.0) {
137
+ if (!this.context) return null;
138
+
139
+ const gainNode = this.context.createGain();
140
+ gainNode.gain.value = volume;
141
+ gainNode.connect(this.masterGain);
142
+
143
+ const channel = {
144
+ name,
145
+ gainNode,
146
+ volume,
147
+ muted: false,
148
+ };
149
+
150
+ this.channels.set(name, channel);
151
+ return channel;
152
+ }
153
+
154
+ /**
155
+ * Sets volume for a channel.
156
+ * @param {string} channelName - Channel name
157
+ * @param {number} volume - Volume (0-1)
158
+ * @param {number} [fadeTime=0] - Fade time in seconds
159
+ */
160
+ setChannelVolume(channelName, volume, fadeTime = 0) {
161
+ const channel = this.channels.get(channelName);
162
+ if (!channel) return;
163
+
164
+ const clampedVolume = Math.max(0, Math.min(1, volume));
165
+ channel.volume = clampedVolume;
166
+
167
+ if (fadeTime > 0 && this.context) {
168
+ channel.gainNode.gain.linearRampToValueAtTime(
169
+ channel.muted ? 0 : clampedVolume,
170
+ this.context.currentTime + fadeTime
171
+ );
172
+ } else {
173
+ channel.gainNode.gain.value = channel.muted ? 0 : clampedVolume;
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Mutes or unmutes a channel.
179
+ * @param {string} channelName - Channel name
180
+ * @param {boolean} muted - Whether to mute
181
+ */
182
+ setChannelMuted(channelName, muted) {
183
+ const channel = this.channels.get(channelName);
184
+ if (!channel) return;
185
+
186
+ channel.muted = muted;
187
+ channel.gainNode.gain.value = muted ? 0 : channel.volume;
188
+ }
189
+
190
+ /**
191
+ * Sets master volume.
192
+ * @param {number} volume - Volume (0-1)
193
+ */
194
+ setMasterVolume(volume) {
195
+ if (!this.masterGain) return;
196
+ this.masterGain.gain.value = Math.max(0, Math.min(1, volume));
197
+ }
198
+
199
+ /**
200
+ * Loads an audio buffer from URL.
201
+ * @param {string} url - Audio file URL
202
+ * @returns {Promise<AudioBuffer>}
203
+ */
204
+ async loadBuffer(url) {
205
+ if (this.bufferCache.has(url)) {
206
+ return this.bufferCache.get(url);
207
+ }
208
+
209
+ if (!this.context) await this.init();
210
+
211
+ const response = await fetch(url);
212
+ const arrayBuffer = await response.arrayBuffer();
213
+ const audioBuffer = await this.context.decodeAudioData(arrayBuffer);
214
+
215
+ this.bufferCache.set(url, audioBuffer);
216
+ return audioBuffer;
217
+ }
218
+
219
+ /**
220
+ * Plays a sound effect.
221
+ * @param {string|AudioBuffer} sound - URL or AudioBuffer
222
+ * @param {object} [options] - Playback options
223
+ * @param {string} [options.channel='sfx'] - Channel to play on
224
+ * @param {number} [options.volume=1] - Volume multiplier
225
+ * @param {number} [options.pitch=1] - Playback rate
226
+ * @param {boolean} [options.loop=false] - Whether to loop
227
+ * @param {number[]} [options.position] - 3D position [x, y, z] for spatial audio
228
+ * @returns {Promise<string>} Source ID for stopping
229
+ */
230
+ async play(sound, options = {}) {
231
+ if (!this.context) await this.init();
232
+
233
+ const {
234
+ channel = 'sfx',
235
+ volume = 1,
236
+ pitch = 1,
237
+ loop = false,
238
+ position = null,
239
+ } = options;
240
+
241
+ const buffer = typeof sound === 'string' ? await this.loadBuffer(sound) : sound;
242
+ const channelObj = this.channels.get(channel);
243
+ if (!channelObj) return null;
244
+
245
+ const source = this.context.createBufferSource();
246
+ source.buffer = buffer;
247
+ source.loop = loop;
248
+ source.playbackRate.value = pitch;
249
+
250
+ // Create gain for this sound
251
+ const gainNode = this.context.createGain();
252
+ gainNode.gain.value = volume;
253
+
254
+ // Connect with optional spatial audio
255
+ if (position) {
256
+ const panner = this.context.createPanner();
257
+ this.configurePanner(panner, position);
258
+ source.connect(gainNode);
259
+ gainNode.connect(panner);
260
+ panner.connect(channelObj.gainNode);
261
+ } else {
262
+ source.connect(gainNode);
263
+ gainNode.connect(channelObj.gainNode);
264
+ }
265
+
266
+ // Generate unique ID
267
+ const id = `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
268
+ this.activeSources.set(id, source);
269
+
270
+ source.onended = () => {
271
+ this.activeSources.delete(id);
272
+ };
273
+
274
+ source.start();
275
+ return id;
276
+ }
277
+
278
+ /**
279
+ * Stops a playing sound.
280
+ * @param {string} id - Source ID from play()
281
+ */
282
+ stop(id) {
283
+ const source = this.activeSources.get(id);
284
+ if (source) {
285
+ source.stop();
286
+ this.activeSources.delete(id);
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Stops all sounds on a channel.
292
+ * @param {string} channelName - Channel name
293
+ */
294
+ stopChannel(channelName) {
295
+ // Note: This stops ALL sounds since we'd need to track by channel
296
+ // A more complete implementation would track sources per channel
297
+ this.activeSources.forEach((source, id) => {
298
+ try {
299
+ source.stop();
300
+ } catch (e) {
301
+ // Already stopped
302
+ }
303
+ });
304
+ this.activeSources.clear();
305
+ }
306
+
307
+ /**
308
+ * Plays music with optional crossfade from current track.
309
+ * @param {string|AudioBuffer} music - URL or AudioBuffer
310
+ * @param {object} [options] - Playback options
311
+ * @param {number} [options.fadeIn=2] - Fade in duration in seconds
312
+ * @param {number} [options.fadeOut=2] - Fade out duration for current track
313
+ * @param {boolean} [options.loop=true] - Whether to loop
314
+ * @returns {Promise<void>}
315
+ */
316
+ async playMusic(music, options = {}) {
317
+ if (!this.context) await this.init();
318
+
319
+ const { fadeIn = 2, fadeOut = 2, loop = true } = options;
320
+
321
+ const buffer = typeof music === 'string' ? await this.loadBuffer(music) : music;
322
+ const channel = this.channels.get('music');
323
+ if (!channel) return;
324
+
325
+ // Fade out current music
326
+ if (this.musicTracks.size > 0) {
327
+ for (const [id, track] of this.musicTracks) {
328
+ track.gain.gain.linearRampToValueAtTime(0, this.context.currentTime + fadeOut);
329
+ setTimeout(() => {
330
+ try {
331
+ track.source.stop();
332
+ } catch (e) {}
333
+ this.musicTracks.delete(id);
334
+ }, fadeOut * 1000);
335
+ }
336
+ }
337
+
338
+ // Create new music source
339
+ const source = this.context.createBufferSource();
340
+ source.buffer = buffer;
341
+ source.loop = loop;
342
+
343
+ const gainNode = this.context.createGain();
344
+ gainNode.gain.value = 0;
345
+
346
+ source.connect(gainNode);
347
+ gainNode.connect(channel.gainNode);
348
+
349
+ const id = `music_${Date.now()}`;
350
+ this.musicTracks.set(id, { source, gain: gainNode });
351
+
352
+ source.start();
353
+
354
+ // Fade in
355
+ gainNode.gain.linearRampToValueAtTime(1, this.context.currentTime + fadeIn);
356
+ }
357
+
358
+ /**
359
+ * Stops all music with fade out.
360
+ * @param {number} [fadeOut=2] - Fade out duration
361
+ */
362
+ stopMusic(fadeOut = 2) {
363
+ if (!this.context) return;
364
+
365
+ for (const [id, track] of this.musicTracks) {
366
+ track.gain.gain.linearRampToValueAtTime(0, this.context.currentTime + fadeOut);
367
+ setTimeout(() => {
368
+ try {
369
+ track.source.stop();
370
+ } catch (e) {}
371
+ this.musicTracks.delete(id);
372
+ }, fadeOut * 1000);
373
+ }
374
+ }
375
+
376
+ /**
377
+ * Configures a panner node for spatial audio.
378
+ * @param {PannerNode} panner - The panner node
379
+ * @param {number[]} position - [x, y, z] position
380
+ */
381
+ configurePanner(panner, position) {
382
+ panner.panningModel = 'HRTF';
383
+ panner.distanceModel = this.spatialConfig.distanceModel;
384
+ panner.maxDistance = this.spatialConfig.maxDistance;
385
+ panner.refDistance = this.spatialConfig.refDistance;
386
+ panner.rolloffFactor = this.spatialConfig.rolloffFactor;
387
+ panner.setPosition(position[0], position[1], position[2]);
388
+ }
389
+
390
+ /**
391
+ * Updates the listener position for spatial audio.
392
+ * @param {number} x - X position
393
+ * @param {number} y - Y position
394
+ * @param {number} z - Z position
395
+ */
396
+ setListenerPosition(x, y, z) {
397
+ if (!this.context) return;
398
+
399
+ this.listenerPosition = { x, y, z };
400
+ const listener = this.context.listener;
401
+
402
+ if (listener.positionX) {
403
+ // Modern API
404
+ listener.positionX.value = x;
405
+ listener.positionY.value = y;
406
+ listener.positionZ.value = z;
407
+ } else {
408
+ // Legacy API
409
+ listener.setPosition(x, y, z);
410
+ }
411
+ }
412
+
413
+ /**
414
+ * Sets the listener orientation.
415
+ * @param {number} fx - Forward X
416
+ * @param {number} fy - Forward Y
417
+ * @param {number} fz - Forward Z
418
+ * @param {number} ux - Up X
419
+ * @param {number} uy - Up Y
420
+ * @param {number} uz - Up Z
421
+ */
422
+ setListenerOrientation(fx, fy, fz, ux = 0, uy = 1, uz = 0) {
423
+ if (!this.context) return;
424
+
425
+ const listener = this.context.listener;
426
+
427
+ if (listener.forwardX) {
428
+ listener.forwardX.value = fx;
429
+ listener.forwardY.value = fy;
430
+ listener.forwardZ.value = fz;
431
+ listener.upX.value = ux;
432
+ listener.upY.value = uy;
433
+ listener.upZ.value = uz;
434
+ } else {
435
+ listener.setOrientation(fx, fy, fz, ux, uy, uz);
436
+ }
437
+ }
438
+
439
+ /**
440
+ * Sets reverb amount (wet/dry mix).
441
+ * @param {number} amount - Reverb amount (0-1)
442
+ */
443
+ setReverbAmount(amount) {
444
+ if (this.reverbGain) {
445
+ this.reverbGain.gain.value = Math.max(0, Math.min(1, amount));
446
+ }
447
+ }
448
+
449
+ /**
450
+ * Creates a simple impulse response for reverb.
451
+ * @param {number} [duration=2] - Reverb duration in seconds
452
+ * @param {number} [decay=2] - Decay rate
453
+ * @returns {AudioBuffer}
454
+ */
455
+ createImpulseResponse(duration = 2, decay = 2) {
456
+ if (!this.context) return null;
457
+
458
+ const sampleRate = this.context.sampleRate;
459
+ const length = sampleRate * duration;
460
+ const impulse = this.context.createBuffer(2, length, sampleRate);
461
+
462
+ for (let channel = 0; channel < 2; channel++) {
463
+ const channelData = impulse.getChannelData(channel);
464
+ for (let i = 0; i < length; i++) {
465
+ channelData[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / length, decay);
466
+ }
467
+ }
468
+
469
+ return impulse;
470
+ }
471
+
472
+ /**
473
+ * Enables reverb with custom impulse response.
474
+ * @param {AudioBuffer} [impulse] - Custom impulse response
475
+ */
476
+ enableReverb(impulse = null) {
477
+ if (!this.context) return;
478
+
479
+ this.reverb = this.context.createConvolver();
480
+ this.reverb.buffer = impulse || this.createImpulseResponse();
481
+
482
+ // Insert reverb into chain
483
+ this.reverbGain.gain.value = 0.3;
484
+ this.masterGain.connect(this.reverbGain);
485
+ this.reverbGain.connect(this.reverb);
486
+ this.reverb.connect(this.compressor);
487
+ }
488
+
489
+ /**
490
+ * Disables reverb.
491
+ */
492
+ disableReverb() {
493
+ if (this.reverbGain) {
494
+ this.reverbGain.gain.value = 0;
495
+ }
496
+ }
497
+
498
+ /**
499
+ * Gets volume levels for visualization.
500
+ * @returns {{ left: number, right: number }}
501
+ */
502
+ getVolumeLevels() {
503
+ // This would require an AnalyserNode to implement properly
504
+ return { left: 0, right: 0 };
505
+ }
506
+
507
+ /**
508
+ * Resumes audio context if suspended.
509
+ * @returns {Promise<void>}
510
+ */
511
+ async resume() {
512
+ if (this.context && this.context.state === 'suspended') {
513
+ await this.context.resume();
514
+ }
515
+ }
516
+
517
+ /**
518
+ * Suspends audio context.
519
+ * @returns {Promise<void>}
520
+ */
521
+ async suspend() {
522
+ if (this.context && this.context.state === 'running') {
523
+ await this.context.suspend();
524
+ }
525
+ }
526
+
527
+ /**
528
+ * Cleans up all audio resources.
529
+ */
530
+ dispose() {
531
+ this.stopChannel('all');
532
+ this.stopMusic(0);
533
+ this.bufferCache.clear();
534
+ this.channels.clear();
535
+
536
+ if (this.context) {
537
+ this.context.close();
538
+ this.context = null;
539
+ }
540
+
541
+ this.initialized = false;
542
+ }
543
+ }