You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

331 lines
7.6 KiB

  1. define([
  2. 'howler',
  3. 'js/misc/physics',
  4. 'js/system/events',
  5. 'js/config',
  6. 'js/system/globals'
  7. ], function (
  8. howler,
  9. physics,
  10. events,
  11. config,
  12. globals
  13. ) {
  14. const globalVolume = 0.3;
  15. let soundVolume = config.soundVolume;
  16. let musicVolume = config.musicVolume;
  17. const globalScopes = ['ui'];
  18. const minDistance = 10;
  19. const fadeDuration = 1800;
  20. window.Howler.volume(globalVolume);
  21. return {
  22. sounds: [],
  23. muted: false,
  24. currentMusic: null,
  25. init: function () {
  26. events.on('onToggleAudio', this.onToggleAudio.bind(this));
  27. events.on('onPlaySound', this.playSound.bind(this));
  28. events.on('onPlaySoundAtPosition', this.onPlaySoundAtPosition.bind(this));
  29. events.on('onManipulateVolume', this.onManipulateVolume.bind(this));
  30. const { clientConfig: { sounds: loadSounds } } = globals;
  31. Object.entries(loadSounds).forEach(([ scope, soundList ]) => {
  32. soundList.forEach(({ name: soundName, file }) => {
  33. this.addSound({
  34. name: soundName,
  35. file,
  36. scope: 'ui',
  37. autoLoad: true
  38. });
  39. });
  40. });
  41. this.onToggleAudio(config.playAudio);
  42. },
  43. //Fired when a character rezones
  44. // 'scope' is the new zone name
  45. unload: function (newScope) {
  46. const { sounds } = this;
  47. for (let i = 0; i < sounds.length; i++) {
  48. const { scope, sound } = sounds[i];
  49. if (!globalScopes.includes(scope) && scope !== newScope) {
  50. if (sound)
  51. sound.unload();
  52. sounds.splice(i, 1);
  53. i--;
  54. }
  55. }
  56. },
  57. onPlaySoundAtPosition: function ({ position: { x, y }, file, volume }) {
  58. const { player: { x: playerX, y: playerY } } = window;
  59. const dx = Math.abs(x - playerX);
  60. const dy = Math.abs(y - playerY);
  61. const distance = Math.max(dx, dy);
  62. const useVolume = volume * (1 - (Math.pow(distance, 2) / Math.pow(minDistance, 2)));
  63. //eslint-disable-next-line no-undef, no-unused-vars
  64. const sound = new Howl({
  65. src: [file],
  66. volume: useVolume,
  67. loop: false,
  68. autoplay: true,
  69. html5: false
  70. });
  71. },
  72. playSound: function (soundName) {
  73. const soundEntry = this.sounds.find(s => s.name === soundName);
  74. if (!soundEntry)
  75. return;
  76. const { sound } = soundEntry;
  77. sound.volume(soundVolume / 100);
  78. sound.play();
  79. },
  80. playSoundHelper: function (soundEntry, volume) {
  81. const { sound } = soundEntry;
  82. if (!sound) {
  83. const { file, loop } = soundEntry;
  84. soundEntry.sound = this.loadSound(file, loop, true, volume);
  85. return;
  86. }
  87. soundEntry.volume = volume;
  88. volume *= (soundVolume / 100);
  89. if (sound.playing()) {
  90. if (sound.volume() === volume)
  91. return;
  92. sound.volume(volume);
  93. } else {
  94. sound.volume(volume);
  95. sound.play();
  96. }
  97. },
  98. playMusicHelper: function (soundEntry) {
  99. const { sound } = soundEntry;
  100. if (!sound) {
  101. const { file, loop } = soundEntry;
  102. soundEntry.volume = musicVolume;
  103. soundEntry.sound = this.loadSound(file, loop, true, musicVolume / 100);
  104. return;
  105. }
  106. if (!sound.playing()) {
  107. soundEntry.volume = 0;
  108. sound.volume(0);
  109. sound.play();
  110. }
  111. if (this.currentMusic === soundEntry && sound.volume() === musicVolume / 100)
  112. return;
  113. this.currentMusic = soundEntry;
  114. sound.fade(sound.volume(), (musicVolume / 100), fadeDuration);
  115. },
  116. stopSoundHelper: function (soundEntry) {
  117. const { sound, music } = soundEntry;
  118. if (!sound || !sound.playing())
  119. return;
  120. if (music)
  121. sound.fade(sound.volume(), 0, fadeDuration);
  122. else {
  123. sound.stop();
  124. sound.volume(0);
  125. }
  126. },
  127. updateSounds: function (playerX, playerY) {
  128. this.sounds.forEach(s => {
  129. const { x, y, area, music, scope } = s;
  130. if (music || scope === 'ui')
  131. return;
  132. let distance = 0;
  133. if (!area) {
  134. let dx = Math.abs(x - playerX);
  135. let dy = Math.abs(y - playerY);
  136. distance = Math.max(dx, dy);
  137. } else if (!physics.isInPolygon(playerX, playerY, area))
  138. distance = physics.distanceToPolygon([playerX, playerY], area);
  139. if (distance > minDistance) {
  140. this.stopSoundHelper(s);
  141. return;
  142. }
  143. //Exponential fall-off
  144. const volume = s.maxVolume * (1 - (Math.pow(distance, 2) / Math.pow(minDistance, 2)));
  145. this.playSoundHelper(s, volume);
  146. });
  147. },
  148. updateMusic: function (playerX, playerY) {
  149. const sounds = this.sounds;
  150. const areaMusic = sounds.filter(s => s.music && s.area);
  151. //All music that should be playing because we're in the correct polygon
  152. const playMusic = areaMusic.filter(s => physics.isInPolygon(playerX, playerY, s.area));
  153. //All music that should stop playing because we're in the incorrect polygon
  154. const stopMusic = areaMusic.filter(s => s.sound && s.sound.playing() && !playMusic.some(m => m === s));
  155. //Stop or start defaultMusic, depending on whether anything else was found
  156. const defaultMusic = sounds.filter(a => a.defaultMusic);
  157. if (defaultMusic) {
  158. if (!playMusic.length)
  159. defaultMusic.forEach(m => this.playMusicHelper(m));
  160. else
  161. defaultMusic.forEach(m => this.stopSoundHelper(m));
  162. }
  163. //If there's a music entry in both 'play' and 'stop' that shares a fileName, we'll just ignore it. This happens when you
  164. // move to a building interior, for example. Unfortunately, we can't have different volume settings for these kinds of entries.
  165. // The one that starts playing first will get priority
  166. const filesPlaying = [...playMusic.map(p => p.file), ...stopMusic.map(p => p.file)];
  167. playMusic.spliceWhere(p => filesPlaying.filter(f => f === p.file).length > 1);
  168. stopMusic.spliceWhere(p => filesPlaying.filter(f => f === p.file).length > 1);
  169. stopMusic.forEach(m => this.stopSoundHelper(m));
  170. playMusic.forEach(m => this.playMusicHelper(m));
  171. },
  172. update: function (playerX, playerY) {
  173. this.updateSounds(playerX, playerY);
  174. this.updateMusic(playerX, playerY);
  175. },
  176. addSound: function (
  177. { name: soundName, scope, file, volume = 1, x, y, w, h, area, music, defaultMusic, autoLoad, loop }
  178. ) {
  179. if (this.sounds.some(s => s.file === file)) {
  180. if (window.player?.x !== undefined)
  181. this.update(window.player.x, window.player.y);
  182. return;
  183. }
  184. if (!area && w) {
  185. area = [
  186. [x, y],
  187. [x + w, y],
  188. [x + w, y + h],
  189. [x, y + h]
  190. ];
  191. }
  192. let sound = null;
  193. if (autoLoad)
  194. sound = this.loadSound(file, loop);
  195. if (music)
  196. volume = 0;
  197. const soundEntry = {
  198. name: soundName,
  199. sound,
  200. scope,
  201. file,
  202. loop,
  203. x,
  204. y,
  205. volume,
  206. maxVolume: volume,
  207. area,
  208. music,
  209. defaultMusic
  210. };
  211. this.sounds.push(soundEntry);
  212. if (window.player?.x !== undefined)
  213. this.update(window.player.x, window.player.y);
  214. return soundEntry;
  215. },
  216. loadSound: function (file, loop = false, autoplay = false, volume = 1) {
  217. //eslint-disable-next-line no-undef
  218. const sound = new Howl({
  219. src: [file],
  220. volume,
  221. loop,
  222. autoplay,
  223. html5: loop
  224. });
  225. return sound;
  226. },
  227. onToggleAudio: function (isAudioOn) {
  228. this.muted = !isAudioOn;
  229. this.sounds.forEach(s => {
  230. if (!s.sound)
  231. return;
  232. s.sound.mute(this.muted);
  233. });
  234. if (!window.player)
  235. return;
  236. const { player: { x, y } } = window;
  237. this.update(x, y);
  238. },
  239. onManipulateVolume: function ({ soundType, delta }) {
  240. if (soundType === 'sound')
  241. soundVolume = Math.max(0, Math.min(100, soundVolume + delta));
  242. else if (soundType === 'music')
  243. musicVolume = Math.max(0, Math.min(100, musicVolume + delta));
  244. const volume = soundType === 'sound' ? soundVolume : musicVolume;
  245. events.emit('onVolumeChange', {
  246. soundType,
  247. volume
  248. });
  249. const { player: { x, y } } = window;
  250. this.update(x, y);
  251. },
  252. destroySoundEntry: function (soundEntry) {
  253. this.sounds.spliceWhere(s => s === soundEntry);
  254. }
  255. };
  256. });