From b31336d080dc4f6789252ef478f7a0ce73c3b451 Mon Sep 17 00:00:00 2001 From: Nordi98 Date: Wed, 13 Aug 2025 18:02:58 +0200 Subject: [PATCH] ed --- resources/[notify]/rtx_notify/.fxap | Bin 0 -> 178 bytes resources/[notify]/rtx_notify/Readme.txt | 24 + resources/[notify]/rtx_notify/client/main.lua | Bin 0 -> 2684 bytes resources/[notify]/rtx_notify/config.lua | 67 + resources/[notify]/rtx_notify/fxmanifest.lua | 36 + .../rtx_notify/html/BebasNeuePro-Bold.ttf | Bin 0 -> 82676 bytes .../[notify]/rtx_notify/html/debounce.min.js | 9 + .../[notify]/rtx_notify/html/howler.core.js | 2583 +++++++++++++++++ .../[notify]/rtx_notify/html/img/close.png | Bin 0 -> 757 bytes resources/[notify]/rtx_notify/html/scripts.js | 306 ++ .../rtx_notify/html/sounds/soundnotify.mp3 | Bin 0 -> 33397 bytes resources/[notify]/rtx_notify/html/styles.css | 479 +++ resources/[notify]/rtx_notify/html/ui.html | 41 + resources/[standalone]/start_train/client.lua | 78 - .../start_train/client/client.lua | 142 + .../[standalone]/start_train/fxmanifest.lua | 11 +- .../start_train/{ => server}/server.lua | 0 .../cfx_nteam_train_scenario/fxmanifest.lua | 1 + .../train_interaction.lua | 152 + 19 files changed, 3844 insertions(+), 85 deletions(-) create mode 100644 resources/[notify]/rtx_notify/.fxap create mode 100644 resources/[notify]/rtx_notify/Readme.txt create mode 100644 resources/[notify]/rtx_notify/client/main.lua create mode 100644 resources/[notify]/rtx_notify/config.lua create mode 100644 resources/[notify]/rtx_notify/fxmanifest.lua create mode 100644 resources/[notify]/rtx_notify/html/BebasNeuePro-Bold.ttf create mode 100644 resources/[notify]/rtx_notify/html/debounce.min.js create mode 100644 resources/[notify]/rtx_notify/html/howler.core.js create mode 100644 resources/[notify]/rtx_notify/html/img/close.png create mode 100644 resources/[notify]/rtx_notify/html/scripts.js create mode 100644 resources/[notify]/rtx_notify/html/sounds/soundnotify.mp3 create mode 100644 resources/[notify]/rtx_notify/html/styles.css create mode 100644 resources/[notify]/rtx_notify/html/ui.html delete mode 100644 resources/[standalone]/start_train/client.lua create mode 100644 resources/[standalone]/start_train/client/client.lua rename resources/[standalone]/start_train/{ => server}/server.lua (100%) create mode 100644 resources/[tools]/cfx_nteam_train_scenario/train_interaction.lua diff --git a/resources/[notify]/rtx_notify/.fxap b/resources/[notify]/rtx_notify/.fxap new file mode 100644 index 0000000000000000000000000000000000000000..9268b54485aad885630b0bce44d3b97b3037a5dc GIT binary patch literal 178 zcmV;j08Rf!SV2$$00000090}_m~Zp`MVnI?Iz7vd5)8f}(9z$yF$eIhhth&BA6(to{IpFT9Cq8&F-0fJq+BNfhz%zJWxzZZJTQs}O-YFCrr zmv#!tVY-vYSFA*F(20(Z4dJ%^wx@o=WYESLv#l+cF>JB0qoSnU2b(0gYipWYU(peI gTJlSqc#B17L)hcI6bH~MQrb}vjj9p=pIzSb7;#or9{>OV literal 0 HcmV?d00001 diff --git a/resources/[notify]/rtx_notify/Readme.txt b/resources/[notify]/rtx_notify/Readme.txt new file mode 100644 index 000000000..e6e17c37a --- /dev/null +++ b/resources/[notify]/rtx_notify/Readme.txt @@ -0,0 +1,24 @@ +Thank you for purchasing rtx_notify we're grateful for your support. If you'd ever have a question and / or need our help, please reach out to us by sending an email or go ahead and create a ticket on our discord: https://discord.gg/P6KdaDpgAk + + +Install instructions: +1. Put rtx_notify folder to your resources +2. Configure your config.lua to your preferences +3. Put rtx_notify to the server.cfg + +Notify Examples: + +Client Side Notify TriggerEvent("rtx_notify:Notify", "Title", "text", time, "type") +Client Side Notify Function exports["rtx_notify"]:Notify("Title", "text", time, "type") + +Example Client Side Notify TriggerEvent("rtx_notify:Notify", "Info", "This is info notify", 2500, "info") - time 1000 = 1 seconds +Example Client Side Notify exports["rtx_notify"]:Notify("Info", "This is info notify", 2500, "info") - time 1000 = 1 seconds + +Server Side Notify TriggerClientEvent("rtx_notify:Notify", source, "Title", "text", time, "type") + +Example Server Side Notify TriggerClientEvent("rtx_notify:Notify", source, "Info", "This is info notify", 2500, "info") - time 1000 = 1 seconds + +2. Each product is to be used on a singular server, with the exception of a test server. +3. Any form of redistribution of our content is considered copyright infringement. +4. If any of these rules are broken, legal actions can be taken. +© 2025 RTX Development, all rights reserved. \ No newline at end of file diff --git a/resources/[notify]/rtx_notify/client/main.lua b/resources/[notify]/rtx_notify/client/main.lua new file mode 100644 index 0000000000000000000000000000000000000000..e870bbb987d8168560a79706b56b58b1d957a01f GIT binary patch literal 2684 zcmV-?3WN1VSV2$$000000H7C~^vGeEUU@z)bCmLj%lp@9L-@*kgp~I%-+HfDy|hUN z-1i@Wys@<1LP6hrOvhAGbNhx#uyV-L9AlGy4EY6RlyblFV^2&4LlwRHU4a*O(f@^%Xd2OG(K^h;<7||r@j{qPd8btNOZ%;dubtb*p!iU>EdVX6 zMF9^Pju#$UOw!dJLcwc8)a)CpC12!?`TCFOk<1qKD!TcE3h9r`()kj8dTnGGLI8lt z0{%ZaPL?^yg*vRZ-;_+js>UmivtrM<;hq)Xp4}zHvHIjQDYy`lO4X>xe)c16lftJb z-MtpNkO_yCu;iVoU8Uj9-QjuuUS&$`_{~+jPUynN0N2;Lzd&S!|sC7xriCl$Q+x34m9+soW*E zYa0QNiLK*vrXOB&r=ue-#o1mOD8;)R#y^-OCmI>l7D`?)Rq+nar;UvHqhS|%qpi(UYT*ou z!1|flW|Z5nEiZI-Mb(LUZvA<0RYyX_oUUoYiRK8+xd&gBW8zG@-UN7mFzJ=Uv1nZ7 zDD)L|kC4Hp-)%f5&-R&sQm>=2olg9F0(*w%W6r+xE(d4 zkd;V!r$#iH#2co$tvc5Q>a&)*fgBqf@4=UxGS&GEtr)Mbcs#4_FMCE?+~Fnu{QH{sgPR8t6k@2h*)1Yx=W* ze5V?AbA!;&9$&nmJd@xTk!5(dIyP@+dEb4Xkr&`5;Ya7R&k5wwGRT-qKLxr90}|Lo zjwic~DDR^F>{cFZ=gG_aE);H)yBUd81!&9K_NmvX)mJK#o9}iP^9rN<&C|G<( z#!ArlmX04f5)%>b=;vi67!(f_PnszIOW3L3;k!^UL%v}q=#;oQi}Tzc5dWef9ZFvO zuFyiCPjy9-##nT}T={XYBM$J%f*vTC8*xp_DwnVJw@>%{dmtke#SZiyG5T@n;Xn&z zX(;E#Tm;S4E`Fs5dv&z#-7fAg*sCPZcWwblnu?DNnoC;`qAbglNhxB;qLQ zm|8|U3+rWFau^?(VK*Bs2d4ETR)VDdYlTZ+)shig^ICd`EE6*V`{D8}dfRjt%&WLK zw>h+;6TSyR`GwBz?f5+UPcG2zrm%v99-|UcB^qv9rHn1nFj( zoQ-rK@yk>6As$hf$0aAi8FRv2N8q$);s;~OJl%O?-=v4wep*G#uGM4;!uM2Ui<&Qy zVo8at4gr3|2nskvF7FCiccujxnDGk;LTpF1tbMn)4&)h(DEh_1MOWzpmG`+;D5X;nlj*9>jQ2als{UQ39-%Qn+fM>A0yZ zhDmlpl>rUA`BM&X^ZQQlm%>n)+X8$!YXrJDaLik4wS-G4Z?7C$c;>oHi@5lGi`crA ztx!)o3UhQRs2D*-Qu-ebKTRIAYG48f;fu^W)!!ui)Kr0D>IQ!04*k@kl*dzx;*N!` z-&x7#F7-~mx`{tm9RO8ku^Mm^jv_&vB{vqnkuf4wPU^&{t6&K|@-OSHk{N5E@;Yh0 z30F_S@S|K56O?JwJEHBXKE~*Cl(KbngE6%whRcT}>p}bLodpHY_X-@w#5w$zLhGa6 z3Tr>{4&KWjz8=&c=<*wM7EubDEp%9<@3w>iK2KjLM%d~n7_S~OCXksTY~eg5=Dgx9Uv`_Vikih34)O5y5M8KfOiMR9bqPd5!?s zSn-l<`!}snbO&o^3jj?=85gt%^L@xN0SboY(*L+J?taGwRGqEy20@b_uzx zwB&%F8>xQhatOKFb3qCnNd1{&;A#bplv$I_^RSrmq=9NMEVe_Kq*bt z)Rp2(&bpyvvxYZjlZ4LXZANf)lm)H-((IYTH+f|~isUyFH+E39m0g>-8G@2K^!i4h?hjW^M=b7R!%S7e)pgqG?NMVnQ+hvH{$e~Cd@6EjichpTBkJ*K#=|6J- literal 0 HcmV?d00001 diff --git a/resources/[notify]/rtx_notify/config.lua b/resources/[notify]/rtx_notify/config.lua new file mode 100644 index 000000000..8a28b781b --- /dev/null +++ b/resources/[notify]/rtx_notify/config.lua @@ -0,0 +1,67 @@ +Config = {} + +Config.NotifySoundsEnabled = true -- true sound enabled -- false sound disabled + +Config.NotifyLocation = "right" -- left or right or middle + +Config.NotifySettings = true -- enable notify settings for players + +Config.NotifySettingsCommand = "notifysettings" -- you can also open notifysettings menu via TriggerEvent("rtx_notify:NotifySettings) + +Config.NotifySettingsInterfaceColor = "#ff66ff" -- change interface color, color must be in hex + +Config.DefaultNotifyStyle = { + defaultcolor = "#ff66ff", -- color for notify + defaulttime = 5000, -- time 1000 = 1 second + defaultnotifysoundenabled = true, -- true sound enabled -- false sound disabled + defaultnotifysound = "sounds/soundnotify.mp3", -- you can import sounds to html/sounds/ folder, +} + +--[[ + + Client Side Notify TriggerEvent("rtx_notify:Notify", "Title", "text", time, "type") + Client Side Notify Function exports["rtx_notify"]:Notify("Title", "text", time, "type") + + Example Client Side Notify TriggerEvent("rtx_notify:Notify", "Info", "This is info notify", 2500, "info") -- time 1000 = 1 seconds + Example Client Side Notify exports["rtx_notify"]:Notify("Info", "This is info notify", 2500, "info") -- time 1000 = 1 seconds + + ----- + + Server Side Notify TriggerClientEvent("rtx_notify:Notify", source, "Title", "text", time, "type") + + Example Server Side Notify TriggerClientEvent("rtx_notify:Notify", source, "Info", "This is info notify", 2500, "info") -- time 1000 = 1 seconds + +]]-- + +Config.NotifyStyles = { + ["info"] = { + notifycolor = "#1aa7ec", -- color for notify + notifytime = 5000, -- time 1000 = 1 second (default time when time is not defined in trigger) + notifysoundenabled = true, -- true sound enabled -- false sound disabled + notifysound = "sounds/soundnotify.mp3", -- you can import sounds to html/sounds/ folder + }, + ["success"] = { + notifycolor = "#07da63", -- color for notify + notifytime = 5000, -- time 1000 = 1 second (default time when time is not defined in trigger) + notifysoundenabled = true, -- true sound enabled -- false sound disabled + notifysound = "sounds/soundnotify.mp3", -- you can import sounds to html/sounds/ folder + }, + ["warning"] = { + notifycolor = "#ffe338", -- color for notify + notifytime = 5000, -- time 1000 = 1 second (default time when time is not defined in trigger) + notifysoundenabled = true, -- true sound enabled -- false sound disabled + notifysound = "sounds/soundnotify.mp3", -- you can import sounds to html/sounds/ folder + }, + ["error"] = { + notifycolor = "#ff0000", -- color for notify + notifytime = 5000, -- time 1000 = 1 second (default time when time is not defined in trigger) + notifysoundenabled = true, -- true sound enabled -- false sound disabled + notifysound = "sounds/soundnotify.mp3", -- you can import sounds to html/sounds/ folder + }, + ["custom"] = { + notifycolor = "#ff66ff", -- color for notify + notifytime = 5000, -- time 1000 = 1 second (default time when time is not defined in trigger) + notifysoundenabled = true, -- true sound enabled -- false sound disabled + notifysound = "sounds/soundnotify.mp3", -- you can import sounds to html/sounds/ folder + }, +} diff --git a/resources/[notify]/rtx_notify/fxmanifest.lua b/resources/[notify]/rtx_notify/fxmanifest.lua new file mode 100644 index 000000000..1d4f43100 --- /dev/null +++ b/resources/[notify]/rtx_notify/fxmanifest.lua @@ -0,0 +1,36 @@ +fx_version 'adamant' + +game 'gta5' + +description 'RTX NOTIFY' + +version '10.0' + +client_scripts { + 'config.lua', + 'client/main.lua' +} + +files { + 'html/ui.html', + 'html/styles.css', + 'html/scripts.js', + 'html/howler.core.js', + 'html/debounce.min.js', + 'html/BebasNeuePro-Bold.ttf', + 'html/img/*.png', + 'html/sounds/*.mp3' +} + +ui_page 'html/ui.html' + +exports { + 'Notify', +} + +lua54 'yes' + +escrow_ignore { + 'config.lua' +} +dependency '/assetpacks' \ No newline at end of file diff --git a/resources/[notify]/rtx_notify/html/BebasNeuePro-Bold.ttf b/resources/[notify]/rtx_notify/html/BebasNeuePro-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..69d71e50c31169983ff279daee950d406389860d GIT binary patch literal 82676 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:l,i===c?e-m:e)}}}if($.guid){g.guid=j.guid=j.guid||$.guid++}return g};$.debounce=function(d,e,f){return f===c?a(d,e,false):a(d,f,e!==false)}})(this); diff --git a/resources/[notify]/rtx_notify/html/howler.core.js b/resources/[notify]/rtx_notify/html/howler.core.js new file mode 100644 index 000000000..e1579ec44 --- /dev/null +++ b/resources/[notify]/rtx_notify/html/howler.core.js @@ -0,0 +1,2583 @@ +/*! + * howler.js v2.2.3 + * howlerjs.com + * + * (c) 2013-2020, James Simpson of GoldFire Studios + * goldfirestudios.com + * + * MIT License + */ + +(function() { + + 'use strict'; + + /** Global Methods **/ + /***************************************************************************/ + + /** + * Create the global controller. All contained methods and properties apply + * to all sounds that are currently playing or will be in the future. + */ + var HowlerGlobal = function() { + this.init(); + }; + HowlerGlobal.prototype = { + /** + * Initialize the global Howler object. + * @return {Howler} + */ + init: function() { + var self = this || Howler; + + // Create a global ID counter. + self._counter = 1000; + + // Pool of unlocked HTML5 Audio objects. + self._html5AudioPool = []; + self.html5PoolSize = 10; + + // Internal properties. + self._codecs = {}; + self._howls = []; + self._muted = false; + self._volume = 1; + self._canPlayEvent = 'canplaythrough'; + self._navigator = (typeof window !== 'undefined' && window.navigator) ? window.navigator : null; + + // Public properties. + self.masterGain = null; + self.noAudio = false; + self.usingWebAudio = true; + self.autoSuspend = true; + self.ctx = null; + + // Set to false to disable the auto audio unlocker. + self.autoUnlock = true; + + // Setup the various state values for global tracking. + self._setup(); + + return self; + }, + + /** + * Get/set the global volume for all sounds. + * @param {Float} vol Volume from 0.0 to 1.0. + * @return {Howler/Float} Returns self or current volume. + */ + volume: function(vol) { + var self = this || Howler; + vol = parseFloat(vol); + + // If we don't have an AudioContext created yet, run the setup. + if (!self.ctx) { + setupAudioContext(); + } + + if (typeof vol !== 'undefined' && vol >= 0 && vol <= 1) { + self._volume = vol; + + // Don't update any of the nodes if we are muted. + if (self._muted) { + return self; + } + + // When using Web Audio, we just need to adjust the master gain. + if (self.usingWebAudio) { + self.masterGain.gain.setValueAtTime(vol, Howler.ctx.currentTime); + } + + // Loop through and change volume for all HTML5 audio nodes. + for (var i=0; i=0; i--) { + self._howls[i].unload(); + } + + // Create a new AudioContext to make sure it is fully reset. + if (self.usingWebAudio && self.ctx && typeof self.ctx.close !== 'undefined') { + self.ctx.close(); + self.ctx = null; + setupAudioContext(); + } + + return self; + }, + + /** + * Check for codec support of specific extension. + * @param {String} ext Audio file extention. + * @return {Boolean} + */ + codecs: function(ext) { + return (this || Howler)._codecs[ext.replace(/^x-/, '')]; + }, + + /** + * Setup various state values for global tracking. + * @return {Howler} + */ + _setup: function() { + var self = this || Howler; + + // Keeps track of the suspend/resume state of the AudioContext. + self.state = self.ctx ? self.ctx.state || 'suspended' : 'suspended'; + + // Automatically begin the 30-second suspend process + self._autoSuspend(); + + // Check if audio is available. + if (!self.usingWebAudio) { + // No audio is available on this system if noAudio is set to true. + if (typeof Audio !== 'undefined') { + try { + var test = new Audio(); + + // Check if the canplaythrough event is available. + if (typeof test.oncanplaythrough === 'undefined') { + self._canPlayEvent = 'canplay'; + } + } catch(e) { + self.noAudio = true; + } + } else { + self.noAudio = true; + } + } + + // Test to make sure audio isn't disabled in Internet Explorer. + try { + var test = new Audio(); + if (test.muted) { + self.noAudio = true; + } + } catch (e) {} + + // Check for supported codecs. + if (!self.noAudio) { + self._setupCodecs(); + } + + return self; + }, + + /** + * Check for browser support for various codecs and cache the results. + * @return {Howler} + */ + _setupCodecs: function() { + var self = this || Howler; + var audioTest = null; + + // Must wrap in a try/catch because IE11 in server mode throws an error. + try { + audioTest = (typeof Audio !== 'undefined') ? new Audio() : null; + } catch (err) { + return self; + } + + if (!audioTest || typeof audioTest.canPlayType !== 'function') { + return self; + } + + var mpegTest = audioTest.canPlayType('audio/mpeg;').replace(/^no$/, ''); + + // Opera version <33 has mixed MP3 support, so we need to check for and block it. + var ua = self._navigator ? self._navigator.userAgent : ''; + var checkOpera = ua.match(/OPR\/([0-6].)/g); + var isOldOpera = (checkOpera && parseInt(checkOpera[0].split('/')[1], 10) < 33); + var checkSafari = ua.indexOf('Safari') !== -1 && ua.indexOf('Chrome') === -1; + var safariVersion = ua.match(/Version\/(.*?) /); + var isOldSafari = (checkSafari && safariVersion && parseInt(safariVersion[1], 10) < 15); + + self._codecs = { + mp3: !!(!isOldOpera && (mpegTest || audioTest.canPlayType('audio/mp3;').replace(/^no$/, ''))), + mpeg: !!mpegTest, + opus: !!audioTest.canPlayType('audio/ogg; codecs="opus"').replace(/^no$/, ''), + ogg: !!audioTest.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, ''), + oga: !!audioTest.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, ''), + wav: !!(audioTest.canPlayType('audio/wav; codecs="1"') || audioTest.canPlayType('audio/wav')).replace(/^no$/, ''), + aac: !!audioTest.canPlayType('audio/aac;').replace(/^no$/, ''), + caf: !!audioTest.canPlayType('audio/x-caf;').replace(/^no$/, ''), + m4a: !!(audioTest.canPlayType('audio/x-m4a;') || audioTest.canPlayType('audio/m4a;') || audioTest.canPlayType('audio/aac;')).replace(/^no$/, ''), + m4b: !!(audioTest.canPlayType('audio/x-m4b;') || audioTest.canPlayType('audio/m4b;') || audioTest.canPlayType('audio/aac;')).replace(/^no$/, ''), + mp4: !!(audioTest.canPlayType('audio/x-mp4;') || audioTest.canPlayType('audio/mp4;') || audioTest.canPlayType('audio/aac;')).replace(/^no$/, ''), + weba: !!(!isOldSafari && audioTest.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/, '')), + webm: !!(!isOldSafari && audioTest.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/, '')), + dolby: !!audioTest.canPlayType('audio/mp4; codecs="ec-3"').replace(/^no$/, ''), + flac: !!(audioTest.canPlayType('audio/x-flac;') || audioTest.canPlayType('audio/flac;')).replace(/^no$/, '') + }; + + return self; + }, + + /** + * Some browsers/devices will only allow audio to be played after a user interaction. + * Attempt to automatically unlock audio on the first user interaction. + * Concept from: http://paulbakaus.com/tutorials/html5/web-audio-on-ios/ + * @return {Howler} + */ + _unlockAudio: function() { + var self = this || Howler; + + // Only run this if Web Audio is supported and it hasn't already been unlocked. + if (self._audioUnlocked || !self.ctx) { + return; + } + + self._audioUnlocked = false; + self.autoUnlock = false; + + // Some mobile devices/platforms have distortion issues when opening/closing tabs and/or web views. + // Bugs in the browser (especially Mobile Safari) can cause the sampleRate to change from 44100 to 48000. + // By calling Howler.unload(), we create a new AudioContext with the correct sampleRate. + if (!self._mobileUnloaded && self.ctx.sampleRate !== 44100) { + self._mobileUnloaded = true; + self.unload(); + } + + // Scratch buffer for enabling iOS to dispose of web audio buffers correctly, as per: + // http://stackoverflow.com/questions/24119684 + self._scratchBuffer = self.ctx.createBuffer(1, 1, 22050); + + // Call this method on touch start to create and play a buffer, + // then check if the audio actually played to determine if + // audio has now been unlocked on iOS, Android, etc. + var unlock = function(e) { + // Create a pool of unlocked HTML5 Audio objects that can + // be used for playing sounds without user interaction. HTML5 + // Audio objects must be individually unlocked, as opposed + // to the WebAudio API which only needs a single activation. + // This must occur before WebAudio setup or the source.onended + // event will not fire. + while (self._html5AudioPool.length < self.html5PoolSize) { + try { + var audioNode = new Audio(); + + // Mark this Audio object as unlocked to ensure it can get returned + // to the unlocked pool when released. + audioNode._unlocked = true; + + // Add the audio node to the pool. + self._releaseHtml5Audio(audioNode); + } catch (e) { + self.noAudio = true; + break; + } + } + + // Loop through any assigned audio nodes and unlock them. + for (var i=0; i= 55. + if (typeof self.ctx.resume === 'function') { + self.ctx.resume(); + } + + // Setup a timeout to check that we are unlocked on the next event loop. + source.onended = function() { + source.disconnect(0); + + // Update the unlocked state and prevent this check from happening again. + self._audioUnlocked = true; + + // Remove the touch start listener. + document.removeEventListener('touchstart', unlock, true); + document.removeEventListener('touchend', unlock, true); + document.removeEventListener('click', unlock, true); + document.removeEventListener('keydown', unlock, true); + + // Let all sounds know that audio has been unlocked. + for (var i=0; i 0 ? sound._seek : self._sprite[sprite][0] / 1000); + var duration = Math.max(0, ((self._sprite[sprite][0] + self._sprite[sprite][1]) / 1000) - seek); + var timeout = (duration * 1000) / Math.abs(sound._rate); + var start = self._sprite[sprite][0] / 1000; + var stop = (self._sprite[sprite][0] + self._sprite[sprite][1]) / 1000; + sound._sprite = sprite; + + // Mark the sound as ended instantly so that this async playback + // doesn't get grabbed by another call to play while this one waits to start. + sound._ended = false; + + // Update the parameters of the sound. + var setParams = function() { + sound._paused = false; + sound._seek = seek; + sound._start = start; + sound._stop = stop; + sound._loop = !!(sound._loop || self._sprite[sprite][2]); + }; + + // End the sound instantly if seek is at the end. + if (seek >= stop) { + self._ended(sound); + return; + } + + // Begin the actual playback. + var node = sound._node; + if (self._webAudio) { + // Fire this when the sound is ready to play to begin Web Audio playback. + var playWebAudio = function() { + self._playLock = false; + setParams(); + self._refreshBuffer(sound); + + // Setup the playback params. + var vol = (sound._muted || self._muted) ? 0 : sound._volume; + node.gain.setValueAtTime(vol, Howler.ctx.currentTime); + sound._playStart = Howler.ctx.currentTime; + + // Play the sound using the supported method. + if (typeof node.bufferSource.start === 'undefined') { + sound._loop ? node.bufferSource.noteGrainOn(0, seek, 86400) : node.bufferSource.noteGrainOn(0, seek, duration); + } else { + sound._loop ? node.bufferSource.start(0, seek, 86400) : node.bufferSource.start(0, seek, duration); + } + + // Start a new timer if none is present. + if (timeout !== Infinity) { + self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout); + } + + if (!internal) { + setTimeout(function() { + self._emit('play', sound._id); + self._loadQueue(); + }, 0); + } + }; + + if (Howler.state === 'running' && Howler.ctx.state !== 'interrupted') { + playWebAudio(); + } else { + self._playLock = true; + + // Wait for the audio context to resume before playing. + self.once('resume', playWebAudio); + + // Cancel the end timer. + self._clearTimer(sound._id); + } + } else { + // Fire this when the sound is ready to play to begin HTML5 Audio playback. + var playHtml5 = function() { + node.currentTime = seek; + node.muted = sound._muted || self._muted || Howler._muted || node.muted; + node.volume = sound._volume * Howler.volume(); + node.playbackRate = sound._rate; + + // Some browsers will throw an error if this is called without user interaction. + try { + var play = node.play(); + + // Support older browsers that don't support promises, and thus don't have this issue. + if (play && typeof Promise !== 'undefined' && (play instanceof Promise || typeof play.then === 'function')) { + // Implements a lock to prevent DOMException: The play() request was interrupted by a call to pause(). + self._playLock = true; + + // Set param values immediately. + setParams(); + + // Releases the lock and executes queued actions. + play + .then(function() { + self._playLock = false; + node._unlocked = true; + if (!internal) { + self._emit('play', sound._id); + } else { + self._loadQueue(); + } + }) + .catch(function() { + self._playLock = false; + self._emit('playerror', sound._id, 'Playback was unable to start. This is most commonly an issue ' + + 'on mobile devices and Chrome where playback was not within a user interaction.'); + + // Reset the ended and paused values. + sound._ended = true; + sound._paused = true; + }); + } else if (!internal) { + self._playLock = false; + setParams(); + self._emit('play', sound._id); + } + + // Setting rate before playing won't work in IE, so we set it again here. + node.playbackRate = sound._rate; + + // If the node is still paused, then we can assume there was a playback issue. + if (node.paused) { + self._emit('playerror', sound._id, 'Playback was unable to start. This is most commonly an issue ' + + 'on mobile devices and Chrome where playback was not within a user interaction.'); + return; + } + + // Setup the end timer on sprites or listen for the ended event. + if (sprite !== '__default' || sound._loop) { + self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout); + } else { + self._endTimers[sound._id] = function() { + // Fire ended on this audio node. + self._ended(sound); + + // Clear this listener. + node.removeEventListener('ended', self._endTimers[sound._id], false); + }; + node.addEventListener('ended', self._endTimers[sound._id], false); + } + } catch (err) { + self._emit('playerror', sound._id, err); + } + }; + + // If this is streaming audio, make sure the src is set and load again. + if (node.src === 'data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA') { + node.src = self._src; + node.load(); + } + + // Play immediately if ready, or wait for the 'canplaythrough'e vent. + var loadedNoReadyState = (window && window.ejecta) || (!node.readyState && Howler._navigator.isCocoonJS); + if (node.readyState >= 3 || loadedNoReadyState) { + playHtml5(); + } else { + self._playLock = true; + self._state = 'loading'; + + var listener = function() { + self._state = 'loaded'; + + // Begin playback. + playHtml5(); + + // Clear this listener. + node.removeEventListener(Howler._canPlayEvent, listener, false); + }; + node.addEventListener(Howler._canPlayEvent, listener, false); + + // Cancel the end timer. + self._clearTimer(sound._id); + } + } + + return sound._id; + }, + + /** + * Pause playback and save current position. + * @param {Number} id The sound ID (empty to pause all in group). + * @return {Howl} + */ + pause: function(id) { + var self = this; + + // If the sound hasn't loaded or a play() promise is pending, add it to the load queue to pause when capable. + if (self._state !== 'loaded' || self._playLock) { + self._queue.push({ + event: 'pause', + action: function() { + self.pause(id); + } + }); + + return self; + } + + // If no id is passed, get all ID's to be paused. + var ids = self._getSoundIds(id); + + for (var i=0; i Returns the group's volume value. + * volume(id) -> Returns the sound id's current volume. + * volume(vol) -> Sets the volume of all sounds in this Howl group. + * volume(vol, id) -> Sets the volume of passed sound id. + * @return {Howl/Number} Returns self or current volume. + */ + volume: function() { + var self = this; + var args = arguments; + var vol, id; + + // Determine the values based on arguments. + if (args.length === 0) { + // Return the value of the groups' volume. + return self._volume; + } else if (args.length === 1 || args.length === 2 && typeof args[1] === 'undefined') { + // First check if this is an ID, and if not, assume it is a new volume. + var ids = self._getSoundIds(); + var index = ids.indexOf(args[0]); + if (index >= 0) { + id = parseInt(args[0], 10); + } else { + vol = parseFloat(args[0]); + } + } else if (args.length >= 2) { + vol = parseFloat(args[0]); + id = parseInt(args[1], 10); + } + + // Update the volume or return the current volume. + var sound; + if (typeof vol !== 'undefined' && vol >= 0 && vol <= 1) { + // If the sound hasn't loaded, add it to the load queue to change volume when capable. + if (self._state !== 'loaded'|| self._playLock) { + self._queue.push({ + event: 'volume', + action: function() { + self.volume.apply(self, args); + } + }); + + return self; + } + + // Set the group volume. + if (typeof id === 'undefined') { + self._volume = vol; + } + + // Update one or all volumes. + id = self._getSoundIds(id); + for (var i=0; i 0) ? len / steps : len); + var lastTick = Date.now(); + + // Store the value being faded to. + sound._fadeTo = to; + + // Update the volume value on each interval tick. + sound._interval = setInterval(function() { + // Update the volume based on the time since the last tick. + var tick = (Date.now() - lastTick) / len; + lastTick = Date.now(); + vol += diff * tick; + + // Round to within 2 decimal points. + vol = Math.round(vol * 100) / 100; + + // Make sure the volume is in the right bounds. + if (diff < 0) { + vol = Math.max(to, vol); + } else { + vol = Math.min(to, vol); + } + + // Change the volume. + if (self._webAudio) { + sound._volume = vol; + } else { + self.volume(vol, sound._id, true); + } + + // Set the group's volume. + if (isGroup) { + self._volume = vol; + } + + // When the fade is complete, stop it and fire event. + if ((to < from && vol <= to) || (to > from && vol >= to)) { + clearInterval(sound._interval); + sound._interval = null; + sound._fadeTo = null; + self.volume(to, sound._id); + self._emit('fade', sound._id); + } + }, stepLen); + }, + + /** + * Internal method that stops the currently playing fade when + * a new fade starts, volume is changed or the sound is stopped. + * @param {Number} id The sound id. + * @return {Howl} + */ + _stopFade: function(id) { + var self = this; + var sound = self._soundById(id); + + if (sound && sound._interval) { + if (self._webAudio) { + sound._node.gain.cancelScheduledValues(Howler.ctx.currentTime); + } + + clearInterval(sound._interval); + sound._interval = null; + self.volume(sound._fadeTo, id); + sound._fadeTo = null; + self._emit('fade', id); + } + + return self; + }, + + /** + * Get/set the loop parameter on a sound. This method can optionally take 0, 1 or 2 arguments. + * loop() -> Returns the group's loop value. + * loop(id) -> Returns the sound id's loop value. + * loop(loop) -> Sets the loop value for all sounds in this Howl group. + * loop(loop, id) -> Sets the loop value of passed sound id. + * @return {Howl/Boolean} Returns self or current loop value. + */ + loop: function() { + var self = this; + var args = arguments; + var loop, id, sound; + + // Determine the values for loop and id. + if (args.length === 0) { + // Return the grou's loop value. + return self._loop; + } else if (args.length === 1) { + if (typeof args[0] === 'boolean') { + loop = args[0]; + self._loop = loop; + } else { + // Return this sound's loop value. + sound = self._soundById(parseInt(args[0], 10)); + return sound ? sound._loop : false; + } + } else if (args.length === 2) { + loop = args[0]; + id = parseInt(args[1], 10); + } + + // If no id is passed, get all ID's to be looped. + var ids = self._getSoundIds(id); + for (var i=0; i Returns the first sound node's current playback rate. + * rate(id) -> Returns the sound id's current playback rate. + * rate(rate) -> Sets the playback rate of all sounds in this Howl group. + * rate(rate, id) -> Sets the playback rate of passed sound id. + * @return {Howl/Number} Returns self or the current playback rate. + */ + rate: function() { + var self = this; + var args = arguments; + var rate, id; + + // Determine the values based on arguments. + if (args.length === 0) { + // We will simply return the current rate of the first node. + id = self._sounds[0]._id; + } else if (args.length === 1) { + // First check if this is an ID, and if not, assume it is a new rate value. + var ids = self._getSoundIds(); + var index = ids.indexOf(args[0]); + if (index >= 0) { + id = parseInt(args[0], 10); + } else { + rate = parseFloat(args[0]); + } + } else if (args.length === 2) { + rate = parseFloat(args[0]); + id = parseInt(args[1], 10); + } + + // Update the playback rate or return the current value. + var sound; + if (typeof rate === 'number') { + // If the sound hasn't loaded, add it to the load queue to change playback rate when capable. + if (self._state !== 'loaded' || self._playLock) { + self._queue.push({ + event: 'rate', + action: function() { + self.rate.apply(self, args); + } + }); + + return self; + } + + // Set the group rate. + if (typeof id === 'undefined') { + self._rate = rate; + } + + // Update one or all volumes. + id = self._getSoundIds(id); + for (var i=0; i Returns the first sound node's current seek position. + * seek(id) -> Returns the sound id's current seek position. + * seek(seek) -> Sets the seek position of the first sound node. + * seek(seek, id) -> Sets the seek position of passed sound id. + * @return {Howl/Number} Returns self or the current seek position. + */ + seek: function() { + var self = this; + var args = arguments; + var seek, id; + + // Determine the values based on arguments. + if (args.length === 0) { + // We will simply return the current position of the first node. + if (self._sounds.length) { + id = self._sounds[0]._id; + } + } else if (args.length === 1) { + // First check if this is an ID, and if not, assume it is a new seek position. + var ids = self._getSoundIds(); + var index = ids.indexOf(args[0]); + if (index >= 0) { + id = parseInt(args[0], 10); + } else if (self._sounds.length) { + id = self._sounds[0]._id; + seek = parseFloat(args[0]); + } + } else if (args.length === 2) { + seek = parseFloat(args[0]); + id = parseInt(args[1], 10); + } + + // If there is no ID, bail out. + if (typeof id === 'undefined') { + return 0; + } + + // If the sound hasn't loaded, add it to the load queue to seek when capable. + if (typeof seek === 'number' && (self._state !== 'loaded' || self._playLock)) { + self._queue.push({ + event: 'seek', + action: function() { + self.seek.apply(self, args); + } + }); + + return self; + } + + // Get the sound. + var sound = self._soundById(id); + + if (sound) { + if (typeof seek === 'number' && seek >= 0) { + // Pause the sound and update position for restarting playback. + var playing = self.playing(id); + if (playing) { + self.pause(id, true); + } + + // Move the position of the track and cancel timer. + sound._seek = seek; + sound._ended = false; + self._clearTimer(id); + + // Update the seek position for HTML5 Audio. + if (!self._webAudio && sound._node && !isNaN(sound._node.duration)) { + sound._node.currentTime = seek; + } + + // Seek and emit when ready. + var seekAndEmit = function() { + // Restart the playback if the sound was playing. + if (playing) { + self.play(id, true); + } + + self._emit('seek', id); + }; + + // Wait for the play lock to be unset before emitting (HTML5 Audio). + if (playing && !self._webAudio) { + var emitSeek = function() { + if (!self._playLock) { + seekAndEmit(); + } else { + setTimeout(emitSeek, 0); + } + }; + setTimeout(emitSeek, 0); + } else { + seekAndEmit(); + } + } else { + if (self._webAudio) { + var realTime = self.playing(id) ? Howler.ctx.currentTime - sound._playStart : 0; + var rateSeek = sound._rateSeek ? sound._rateSeek - sound._seek : 0; + return sound._seek + (rateSeek + realTime * Math.abs(sound._rate)); + } else { + return sound._node.currentTime; + } + } + } + + return self; + }, + + /** + * Check if a specific sound is currently playing or not (if id is provided), or check if at least one of the sounds in the group is playing or not. + * @param {Number} id The sound id to check. If none is passed, the whole sound group is checked. + * @return {Boolean} True if playing and false if not. + */ + playing: function(id) { + var self = this; + + // Check the passed sound ID (if any). + if (typeof id === 'number') { + var sound = self._soundById(id); + return sound ? !sound._paused : false; + } + + // Otherwise, loop through all sounds and check if any are playing. + for (var i=0; i= 0) { + Howler._howls.splice(index, 1); + } + + // Delete this sound from the cache (if no other Howl is using it). + var remCache = true; + for (i=0; i= 0) { + remCache = false; + break; + } + } + + if (cache && remCache) { + delete cache[self._src]; + } + + // Clear global errors. + Howler.noAudio = false; + + // Clear out `self`. + self._state = 'unloaded'; + self._sounds = []; + self = null; + + return null; + }, + + /** + * Listen to a custom event. + * @param {String} event Event name. + * @param {Function} fn Listener to call. + * @param {Number} id (optional) Only listen to events for this sound. + * @param {Number} once (INTERNAL) Marks event to fire only once. + * @return {Howl} + */ + on: function(event, fn, id, once) { + var self = this; + var events = self['_on' + event]; + + if (typeof fn === 'function') { + events.push(once ? {id: id, fn: fn, once: once} : {id: id, fn: fn}); + } + + return self; + }, + + /** + * Remove a custom event. Call without parameters to remove all events. + * @param {String} event Event name. + * @param {Function} fn Listener to remove. Leave empty to remove all. + * @param {Number} id (optional) Only remove events for this sound. + * @return {Howl} + */ + off: function(event, fn, id) { + var self = this; + var events = self['_on' + event]; + var i = 0; + + // Allow passing just an event and ID. + if (typeof fn === 'number') { + id = fn; + fn = null; + } + + if (fn || id) { + // Loop through event store and remove the passed function. + for (i=0; i=0; i--) { + // Only fire the listener if the correct ID is used. + if (!events[i].id || events[i].id === id || event === 'load') { + setTimeout(function(fn) { + fn.call(this, id, msg); + }.bind(self, events[i].fn), 0); + + // If this event was setup with `once`, remove it. + if (events[i].once) { + self.off(event, events[i].fn, events[i].id); + } + } + } + + // Pass the event type into load queue so that it can continue stepping. + self._loadQueue(event); + + return self; + }, + + /** + * Queue of actions initiated before the sound has loaded. + * These will be called in sequence, with the next only firing + * after the previous has finished executing (even if async like play). + * @return {Howl} + */ + _loadQueue: function(event) { + var self = this; + + if (self._queue.length > 0) { + var task = self._queue[0]; + + // Remove this task if a matching event was passed. + if (task.event === event) { + self._queue.shift(); + self._loadQueue(); + } + + // Run the task if no event type is passed. + if (!event) { + task.action(); + } + } + + return self; + }, + + /** + * Fired when playback ends at the end of the duration. + * @param {Sound} sound The sound object to work with. + * @return {Howl} + */ + _ended: function(sound) { + var self = this; + var sprite = sound._sprite; + + // If we are using IE and there was network latency we may be clipping + // audio before it completes playing. Lets check the node to make sure it + // believes it has completed, before ending the playback. + if (!self._webAudio && sound._node && !sound._node.paused && !sound._node.ended && sound._node.currentTime < sound._stop) { + setTimeout(self._ended.bind(self, sound), 100); + return self; + } + + // Should this sound loop? + var loop = !!(sound._loop || self._sprite[sprite][2]); + + // Fire the ended event. + self._emit('end', sound._id); + + // Restart the playback for HTML5 Audio loop. + if (!self._webAudio && loop) { + self.stop(sound._id, true).play(sound._id); + } + + // Restart this timer if on a Web Audio loop. + if (self._webAudio && loop) { + self._emit('play', sound._id); + sound._seek = sound._start || 0; + sound._rateSeek = 0; + sound._playStart = Howler.ctx.currentTime; + + var timeout = ((sound._stop - sound._start) * 1000) / Math.abs(sound._rate); + self._endTimers[sound._id] = setTimeout(self._ended.bind(self, sound), timeout); + } + + // Mark the node as paused. + if (self._webAudio && !loop) { + sound._paused = true; + sound._ended = true; + sound._seek = sound._start || 0; + sound._rateSeek = 0; + self._clearTimer(sound._id); + + // Clean up the buffer source. + self._cleanBuffer(sound._node); + + // Attempt to auto-suspend AudioContext if no sounds are still playing. + Howler._autoSuspend(); + } + + // When using a sprite, end the track. + if (!self._webAudio && !loop) { + self.stop(sound._id, true); + } + + return self; + }, + + /** + * Clear the end timer for a sound playback. + * @param {Number} id The sound ID. + * @return {Howl} + */ + _clearTimer: function(id) { + var self = this; + + if (self._endTimers[id]) { + // Clear the timeout or remove the ended listener. + if (typeof self._endTimers[id] !== 'function') { + clearTimeout(self._endTimers[id]); + } else { + var sound = self._soundById(id); + if (sound && sound._node) { + sound._node.removeEventListener('ended', self._endTimers[id], false); + } + } + + delete self._endTimers[id]; + } + + return self; + }, + + /** + * Return the sound identified by this ID, or return null. + * @param {Number} id Sound ID + * @return {Object} Sound object or null. + */ + _soundById: function(id) { + var self = this; + + // Loop through all sounds and find the one with this ID. + for (var i=0; i=0; i--) { + if (cnt <= limit) { + return; + } + + if (self._sounds[i]._ended) { + // Disconnect the audio source when using Web Audio. + if (self._webAudio && self._sounds[i]._node) { + self._sounds[i]._node.disconnect(0); + } + + // Remove sounds until we have the pool size. + self._sounds.splice(i, 1); + cnt--; + } + } + }, + + /** + * Get all ID's from the sounds pool. + * @param {Number} id Only return one ID if one is passed. + * @return {Array} Array of IDs. + */ + _getSoundIds: function(id) { + var self = this; + + if (typeof id === 'undefined') { + var ids = []; + for (var i=0; i= 0; + + if (Howler._scratchBuffer && node.bufferSource) { + node.bufferSource.onended = null; + node.bufferSource.disconnect(0); + if (isIOS) { + try { node.bufferSource.buffer = Howler._scratchBuffer; } catch(e) {} + } + } + node.bufferSource = null; + + return self; + }, + + /** + * Set the source to a 0-second silence to stop any downloading (except in IE). + * @param {Object} node Audio node to clear. + */ + _clearSound: function(node) { + var checkIE = /MSIE |Trident\//.test(Howler._navigator && Howler._navigator.userAgent); + if (!checkIE) { + node.src = 'data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA'; + } + } + }; + + /** Single Sound Methods **/ + /***************************************************************************/ + + /** + * Setup the sound object, which each node attached to a Howl group is contained in. + * @param {Object} howl The Howl parent group. + */ + var Sound = function(howl) { + this._parent = howl; + this.init(); + }; + Sound.prototype = { + /** + * Initialize a new Sound object. + * @return {Sound} + */ + init: function() { + var self = this; + var parent = self._parent; + + // Setup the default parameters. + self._muted = parent._muted; + self._loop = parent._loop; + self._volume = parent._volume; + self._rate = parent._rate; + self._seek = 0; + self._paused = true; + self._ended = true; + self._sprite = '__default'; + + // Generate a unique ID for this sound. + self._id = ++Howler._counter; + + // Add itself to the parent's pool. + parent._sounds.push(self); + + // Create the new node. + self.create(); + + return self; + }, + + /** + * Create and setup a new sound object, whether HTML5 Audio or Web Audio. + * @return {Sound} + */ + create: function() { + var self = this; + var parent = self._parent; + var volume = (Howler._muted || self._muted || self._parent._muted) ? 0 : self._volume; + + if (parent._webAudio) { + // Create the gain node for controlling volume (the source will connect to this). + self._node = (typeof Howler.ctx.createGain === 'undefined') ? Howler.ctx.createGainNode() : Howler.ctx.createGain(); + self._node.gain.setValueAtTime(volume, Howler.ctx.currentTime); + self._node.paused = true; + self._node.connect(Howler.masterGain); + } else if (!Howler.noAudio) { + // Get an unlocked Audio object from the pool. + self._node = Howler._obtainHtml5Audio(); + + // Listen for errors (http://dev.w3.org/html5/spec-author-view/spec.html#mediaerror). + self._errorFn = self._errorListener.bind(self); + self._node.addEventListener('error', self._errorFn, false); + + // Listen for 'canplaythrough' event to let us know the sound is ready. + self._loadFn = self._loadListener.bind(self); + self._node.addEventListener(Howler._canPlayEvent, self._loadFn, false); + + // Listen for the 'ended' event on the sound to account for edge-case where + // a finite sound has a duration of Infinity. + self._endFn = self._endListener.bind(self); + self._node.addEventListener('ended', self._endFn, false); + + // Setup the new audio node. + self._node.src = parent._src; + self._node.preload = parent._preload === true ? 'auto' : parent._preload; + self._node.volume = volume * Howler.volume(); + + // Begin loading the source. + self._node.load(); + } + + return self; + }, + + /** + * Reset the parameters of this sound to the original state (for recycle). + * @return {Sound} + */ + reset: function() { + var self = this; + var parent = self._parent; + + // Reset all of the parameters of this sound. + self._muted = parent._muted; + self._loop = parent._loop; + self._volume = parent._volume; + self._rate = parent._rate; + self._seek = 0; + self._rateSeek = 0; + self._paused = true; + self._ended = true; + self._sprite = '__default'; + + // Generate a new ID so that it isn't confused with the previous sound. + self._id = ++Howler._counter; + + return self; + }, + + /** + * HTML5 Audio error listener callback. + */ + _errorListener: function() { + var self = this; + + // Fire an error event and pass back the code. + self._parent._emit('loaderror', self._id, self._node.error ? self._node.error.code : 0); + + // Clear the event listener. + self._node.removeEventListener('error', self._errorFn, false); + }, + + /** + * HTML5 Audio canplaythrough listener callback. + */ + _loadListener: function() { + var self = this; + var parent = self._parent; + + // Round up the duration to account for the lower precision in HTML5 Audio. + parent._duration = Math.ceil(self._node.duration * 10) / 10; + + // Setup a sprite if none is defined. + if (Object.keys(parent._sprite).length === 0) { + parent._sprite = {__default: [0, parent._duration * 1000]}; + } + + if (parent._state !== 'loaded') { + parent._state = 'loaded'; + parent._emit('load'); + parent._loadQueue(); + } + + // Clear the event listener. + self._node.removeEventListener(Howler._canPlayEvent, self._loadFn, false); + }, + + /** + * HTML5 Audio ended listener callback. + */ + _endListener: function() { + var self = this; + var parent = self._parent; + + // Only handle the `ended`` event if the duration is Infinity. + if (parent._duration === Infinity) { + // Update the parent duration to match the real audio duration. + // Round up the duration to account for the lower precision in HTML5 Audio. + parent._duration = Math.ceil(self._node.duration * 10) / 10; + + // Update the sprite that corresponds to the real duration. + if (parent._sprite.__default[1] === Infinity) { + parent._sprite.__default[1] = parent._duration * 1000; + } + + // Run the regular ended method. + parent._ended(self); + } + + // Clear the event listener since the duration is now correct. + self._node.removeEventListener('ended', self._endFn, false); + } + }; + + /** Helper Methods **/ + /***************************************************************************/ + + var cache = {}; + + /** + * Buffer a sound from URL, Data URI or cache and decode to audio source (Web Audio API). + * @param {Howl} self + */ + var loadBuffer = function(self) { + var url = self._src; + + // Check if the buffer has already been cached and use it instead. + if (cache[url]) { + // Set the duration from the cache. + self._duration = cache[url].duration; + + // Load the sound into this Howl. + loadSound(self); + + return; + } + + if (/^data:[^;]+;base64,/.test(url)) { + // Decode the base64 data URI without XHR, since some browsers don't support it. + var data = atob(url.split(',')[1]); + var dataView = new Uint8Array(data.length); + for (var i=0; i 0) { + cache[self._src] = buffer; + loadSound(self, buffer); + } else { + error(); + } + }; + + // Decode the buffer into an audio source. + if (typeof Promise !== 'undefined' && Howler.ctx.decodeAudioData.length === 1) { + Howler.ctx.decodeAudioData(arraybuffer).then(success).catch(error); + } else { + Howler.ctx.decodeAudioData(arraybuffer, success, error); + } + } + + /** + * Sound is now loaded, so finish setting everything up and fire the loaded event. + * @param {Howl} self + * @param {Object} buffer The decoded buffer sound source. + */ + var loadSound = function(self, buffer) { + // Set the duration. + if (buffer && !self._duration) { + self._duration = buffer.duration; + } + + // Setup a sprite if none is defined. + if (Object.keys(self._sprite).length === 0) { + self._sprite = {__default: [0, self._duration * 1000]}; + } + + // Fire the loaded event. + if (self._state !== 'loaded') { + self._state = 'loaded'; + self._emit('load'); + self._loadQueue(); + } + }; + + /** + * Setup the audio context when available, or switch to HTML5 Audio mode. + */ + var setupAudioContext = function() { + // If we have already detected that Web Audio isn't supported, don't run this step again. + if (!Howler.usingWebAudio) { + return; + } + + // Check if we are using Web Audio and setup the AudioContext if we are. + try { + if (typeof AudioContext !== 'undefined') { + Howler.ctx = new AudioContext(); + } else if (typeof webkitAudioContext !== 'undefined') { + Howler.ctx = new webkitAudioContext(); + } else { + Howler.usingWebAudio = false; + } + } catch(e) { + Howler.usingWebAudio = false; + } + + // If the audio context creation still failed, set using web audio to false. + if (!Howler.ctx) { + Howler.usingWebAudio = false; + } + + // Check if a webview is being used on iOS8 or earlier (rather than the browser). + // If it is, disable Web Audio as it causes crashing. + var iOS = (/iP(hone|od|ad)/.test(Howler._navigator && Howler._navigator.platform)); + var appVersion = Howler._navigator && Howler._navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/); + var version = appVersion ? parseInt(appVersion[1], 10) : null; + if (iOS && version && version < 9) { + var safari = /safari/.test(Howler._navigator && Howler._navigator.userAgent.toLowerCase()); + if (Howler._navigator && !safari) { + Howler.usingWebAudio = false; + } + } + + // Create and expose the master GainNode when using Web Audio (useful for plugins or advanced usage). + if (Howler.usingWebAudio) { + Howler.masterGain = (typeof Howler.ctx.createGain === 'undefined') ? Howler.ctx.createGainNode() : Howler.ctx.createGain(); + Howler.masterGain.gain.setValueAtTime(Howler._muted ? 0 : Howler._volume, Howler.ctx.currentTime); + Howler.masterGain.connect(Howler.ctx.destination); + } + + // Re-run the setup on Howler. + Howler._setup(); + }; + + // Add support for AMD (Asynchronous Module Definition) libraries such as require.js. + if (typeof define === 'function' && define.amd) { + define([], function() { + return { + Howler: Howler, + Howl: Howl + }; + }); + } + + // Add support for CommonJS libraries such as browserify. + if (typeof exports !== 'undefined') { + exports.Howler = Howler; + exports.Howl = Howl; + } + + // Add to global in Node.js (for testing, etc). + if (typeof global !== 'undefined') { + global.HowlerGlobal = HowlerGlobal; + global.Howler = Howler; + global.Howl = Howl; + global.Sound = Sound; + } else if (typeof window !== 'undefined') { // Define globally in case AMD is not available or unused. + window.HowlerGlobal = HowlerGlobal; + window.Howler = Howler; + window.Howl = Howl; + window.Sound = Sound; + } +})(); diff --git a/resources/[notify]/rtx_notify/html/img/close.png b/resources/[notify]/rtx_notify/html/img/close.png new file mode 100644 index 0000000000000000000000000000000000000000..5ef6454e10252ed53635dbd8f81943db546dd3f4 GIT binary patch literal 757 zcmV(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fd zMgRZ;dr3q=RCwC7m&tY__?t@dxi(80lr$>0-buPDsmsUP1!N@Ml=NH4RhBd?siEEk zPD=W$m1|zo*`x_v)at9>H-QUEPnTT@^eTOwh{g=6%J3_YC0+dhnt*=*VPs**5&S7| z8kn&0s*#KLRu!Y$N z%mX=3&!#Y86Ilxcmh})=m%uLwP51NBGSCIQwJ;MvC$Q-0`7Jb)j7y8aK~Ljl!_IoJ z#SnPyfTn>1=D)4hE?{?!`z4dal*4-zNr9wkOReT(>bzOfPf34GXRO8Su=swGlnKoM zdEb)da7>+#0}W>5O#mG=xFL)01E5^Vw8hb}o=1MwMoEPl%&hULa8Deb9n}NLEyn|o zC7qJ=(ZUoA^U&dSFK!_D4m74bko+<+%GCo2z' + + '
' + item.notifytitle + '
' + + '
' + item.notifytext + '
' + + '
' + + '
' + + '
' + + + ''); + } + } + if (item.message == "removenotify"){ + $("#" + item.notifyrandomid + "").fadeOut("slow"); + } + if (item.message == "playsound") { + if (localStorage.hideactivated == 0) { + if (localStorage.soundactivated == 1) { + soundhandler._src = item.soundsrc; + soundhandler.load(); + soundhandler.play(); + } + } + } + if (item.message == "notifysettingsload"){ + notifyresourcename = item.notifyresouredata; + let root = document.documentElement; + root.style.setProperty('--color', item.notifysettingsinterfacecolor); + if (localStorage.default == "true") { + if (item.defaultposition == "left") { + defaultValues.notifyleft = "18"; + $(".notify-container").css("left", "18%"); + } else if (item.defaultposition == "right") { + defaultValues.notifyleft = "82"; + $(".notify-container").css("left", "18%"); + } else if (item.defaultposition == "middle") { + defaultValues.notifyleft = "42"; + $(".notify-container").css("left", "42%"); + } + } else { + ChangeNotify(); + } + } + if (item.message == "notifysettingsshow"){ + var volumedata = localStorage.soundactivated; + var hidedata = localStorage.hideactivated; + if (volumedata == 1){ + document.getElementById("notifyvolumeswitchdata").checked = true; + } + else { + document.getElementById("notifyvolumeswitchdata").checked = false; + } + if (hidedata == 1){ + document.getElementById("notifyhideswitchdata").checked = true; + } + else { + document.getElementById("notifyhideswitchdata").checked = false; + } + openMain(); + $("#notifysettingsshow").show(); + NotifyDrag(); + } + document.onkeyup = function (data) { + if (open) { + if (data.which == 27) { + ChangeNotify(); + $(".notify-container").draggable("destroy"); + $(".notify-container").css("background-color", "rgba(0, 0, 0, 0.0)"); + $("#notifysettingsshow").hide(); + $.post('http://'+notifyresourcename+'/closesettingsnotify', JSON.stringify({})); + } + } + }; + }); + +}) \ No newline at end of file diff --git a/resources/[notify]/rtx_notify/html/sounds/soundnotify.mp3 b/resources/[notify]/rtx_notify/html/sounds/soundnotify.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..4888383a70dbbf6d0ebf4aa722bc74d2f47ba9f3 GIT binary patch literal 33397 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 + + + + + + + + + + + +
+
+
+
+
+
+ CloseNotifyEdit +
NOTIFY SETTINGS
+
SCALE
+
+
+
+ +
+
SOUND
+
HIDE
+ + +
RESET
+
SAVE
+
+
+ \ No newline at end of file diff --git a/resources/[standalone]/start_train/client.lua b/resources/[standalone]/start_train/client.lua deleted file mode 100644 index 2d1301a45..000000000 --- a/resources/[standalone]/start_train/client.lua +++ /dev/null @@ -1,78 +0,0 @@ -local QBCore = exports['qb-core']:GetCoreObject() - -print("^2[TRAIN-TRIGGER]^7 Client Script wird geladen...") - --- Test ob QBCore funktioniert -CreateThread(function() - Wait(2000) - if QBCore then - print("^2[TRAIN-TRIGGER]^7 QBCore erfolgreich geladen auf Client") - else - print("^1[TRAIN-TRIGGER]^7 FEHLER: QBCore nicht gefunden auf Client!") - end -end) - --- Einfache Locations (gleiche wie Server) -local trainLocations = { - {x = 215.3, y = -810.1, z = 30.7, name = "Legion Square"}, - {x = -265.0, y = -957.3, z = 31.2, name = "Pillbox Hospital"}, -} - -local showMarkers = false - --- Test Command -RegisterCommand('togglemarkers', function() - showMarkers = not showMarkers - print("^3[TRAIN-TRIGGER]^7 Markers: " .. tostring(showMarkers)) - - if QBCore and QBCore.Functions and QBCore.Functions.Notify then - QBCore.Functions.Notify('Markers: ' .. tostring(showMarkers), 'primary') - else - TriggerEvent('chatMessage', "SYSTEM", "normal", "Markers: " .. tostring(showMarkers)) - end -end, false) - --- Marker Loop -CreateThread(function() - while true do - local sleep = 1000 - - if showMarkers then - local playerCoords = GetEntityCoords(PlayerPedId()) - - for i, location in ipairs(trainLocations) do - local distance = #(playerCoords - vector3(location.x, location.y, location.z)) - - if distance < 200.0 then - sleep = 0 - -- Grüner Marker - DrawMarker(1, location.x, location.y, location.z - 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 3.0, 3.0, 2.0, 0, 255, 0, 150, false, true, 2, nil, nil, false) - - if distance < 20.0 then - -- 3D Text - local onScreen, _x, _y = World3dToScreen2d(location.x, location.y, location.z + 2.0) - if onScreen then - SetTextScale(0.4, 0.4) - SetTextFont(4) - SetTextProportional(1) - SetTextColour(255, 255, 255, 215) - SetTextEntry("STRING") - SetTextCentre(1) - AddTextComponentString("[E] " .. location.name .. "\nDistanz: " .. math.floor(distance) .. "m") - DrawText(_x, _y) - end - - if IsControlJustPressed(0, 38) then -- E - print("^3[TRAIN-TRIGGER]^7 E gedrückt bei " .. location.name) - TriggerServerEvent('train:requestStart') - end - end - end - end - end - - Wait(sleep) - end -end) - -print("^2[TRAIN-TRIGGER]^7 Client Script geladen! Commands: /togglemarkers") diff --git a/resources/[standalone]/start_train/client/client.lua b/resources/[standalone]/start_train/client/client.lua new file mode 100644 index 000000000..5b015677f --- /dev/null +++ b/resources/[standalone]/start_train/client/client.lua @@ -0,0 +1,142 @@ +local QBCore = exports['qb-core']:GetCoreObject() +local PlayerData = {} +local isInTrainingZone = false +local currentZone = nil +local isInScenario = false + +-- Events +RegisterNetEvent('QBCore:Client:OnPlayerLoaded', function() + PlayerData = QBCore.Functions.GetPlayerData() + CreateTrainingZones() +end) + +RegisterNetEvent('QBCore:Client:OnPlayerUnload', function() + PlayerData = {} +end) + +-- Erstelle Training Zones +function CreateTrainingZones() + for k, v in pairs(Config.TrainingZones) do + local blip = AddBlipForCoord(v.coords.x, v.coords.y, v.coords.z) + SetBlipSprite(blip, v.blip.sprite) + SetBlipDisplay(blip, 4) + SetBlipScale(blip, v.blip.scale) + SetBlipColour(blip, v.blip.color) + SetBlipAsShortRange(blip, true) + BeginTextCommandSetBlipName("STRING") + AddTextComponentSubstringPlayerName(v.blip.label) + EndTextCommandSetBlipName(blip) + end +end + +-- Main Thread für Zone Detection +CreateThread(function() + while true do + local sleep = 1000 + local ped = PlayerPedId() + local coords = GetEntityCoords(ped) + + for k, v in pairs(Config.TrainingZones) do + local distance = #(coords - vector3(v.coords.x, v.coords.y, v.coords.z)) + + if distance < v.radius then + sleep = 0 + if not isInTrainingZone then + isInTrainingZone = true + currentZone = k + ShowHelpText() + end + + if IsControlJustReleased(0, 38) then -- E Key + StartTrainingScenario(v.scenario) + end + elseif isInTrainingZone and currentZone == k then + isInTrainingZone = false + currentZone = nil + end + end + + Wait(sleep) + end +end) + +-- Zeige Help Text +function ShowHelpText() + CreateThread(function() + while isInTrainingZone do + local zoneData = Config.TrainingZones[currentZone] + QBCore.Functions.DrawText3D(zoneData.coords.x, zoneData.coords.y, zoneData.coords.z + 1.0, + '[E] - ' .. zoneData.label) + Wait(0) + end + end) +end + +-- Starte Training Scenario +function StartTrainingScenario(scenario) + if isInScenario then + QBCore.Functions.Notify('Du bist bereits in einem Szenario!', 'error') + return + end + + local ped = PlayerPedId() + + -- Prüfe ob Szenario existiert + if not DoesScenarioExist(scenario) then + QBCore.Functions.Notify('Szenario existiert nicht!', 'error') + return + end + + isInScenario = true + TaskStartScenarioInPlace(ped, scenario, 0, true) + + QBCore.Functions.Notify('Szenario gestartet! Drücke [X] zum Beenden', 'success') + + -- Thread zum Beenden des Szenarios + CreateThread(function() + while isInScenario do + if IsControlJustReleased(0, 73) then -- X Key + StopTrainingScenario() + break + end + Wait(0) + end + end) +end + +-- Stoppe Training Scenario +function StopTrainingScenario() + if not isInScenario then return end + + local ped = PlayerPedId() + ClearPedTasks(ped) + isInScenario = false + + QBCore.Functions.Notify('Szenario beendet!', 'primary') +end + +-- Event Handler für externe Triggers +RegisterNetEvent('train:startscenario', function(scenario) + if not scenario then + QBCore.Functions.Notify('Kein Szenario angegeben!', 'error') + return + end + + StartTrainingScenario(scenario) +end) + +-- Utility Functions +QBCore.Functions.DrawText3D = function(x, y, z, text) + SetTextScale(0.35, 0.35) + SetTextFont(4) + SetTextProportional(1) + SetTextColour(255, 255, 255, 215) + SetTextEntry("STRING") + SetTextCentre(true) + AddTextComponentString(text) + SetDrawOrigin(x, y, z, 0) + DrawText(0.0, 0.0) + local factor = (string.len(text)) / 370 + DrawRect(0.0, 0.0+0.0125, 0.017+ factor, 0.03, 0, 0, 0, 75) + ClearDrawOrigin() +end diff --git a/resources/[standalone]/start_train/fxmanifest.lua b/resources/[standalone]/start_train/fxmanifest.lua index cd37876af..783805011 100644 --- a/resources/[standalone]/start_train/fxmanifest.lua +++ b/resources/[standalone]/start_train/fxmanifest.lua @@ -1,20 +1,17 @@ fx_version 'cerulean' game 'gta5' -author 'Dein Name' -description 'QBCore Train Scenario Trigger Script' +description 'QB-TrainingScenarios' version '1.0.0' -server_scripts { - 'server.lua' +shared_scripts { + 'shared/config.lua' } client_scripts { - 'client.lua' + 'client/main.lua' } dependencies { 'qb-core' } - -lua54 'yes' diff --git a/resources/[standalone]/start_train/server.lua b/resources/[standalone]/start_train/server/server.lua similarity index 100% rename from resources/[standalone]/start_train/server.lua rename to resources/[standalone]/start_train/server/server.lua diff --git a/resources/[tools]/cfx_nteam_train_scenario/fxmanifest.lua b/resources/[tools]/cfx_nteam_train_scenario/fxmanifest.lua index ddc2f2850..99b2bdab3 100644 --- a/resources/[tools]/cfx_nteam_train_scenario/fxmanifest.lua +++ b/resources/[tools]/cfx_nteam_train_scenario/fxmanifest.lua @@ -20,6 +20,7 @@ shared_scripts { client_scripts { 'client.lua', + 'train_interaction.lua' } server_scripts { diff --git a/resources/[tools]/cfx_nteam_train_scenario/train_interaction.lua b/resources/[tools]/cfx_nteam_train_scenario/train_interaction.lua new file mode 100644 index 000000000..d1e687364 --- /dev/null +++ b/resources/[tools]/cfx_nteam_train_scenario/train_interaction.lua @@ -0,0 +1,152 @@ +-- train_interaction.lua + +-- Configuration for the interaction point +local interactionPoint = { + coords = vector3(126.0, -1037.0, 29.3), -- Change to your desired location + radius = 2.0, + text = "Press ~INPUT_CONTEXT~ to use train transportation" +} + +-- Available scenarios +local scenarios = { + {name = "Welcome", label = "City Center"}, + {name = "Jail", label = "Prison"}, + {name = "Paleto", label = "Paleto Bay"} +} + +-- Variables +local isInMarker = false +local menuOpen = false + +-- Function to draw 3D text +function Draw3DText(x, y, z, text) + local onScreen, _x, _y = World3dToScreen2d(x, y, z) + local px, py, pz = table.unpack(GetGameplayCamCoords()) + + SetTextScale(0.35, 0.35) + SetTextFont(4) + SetTextProportional(1) + SetTextColour(255, 255, 255, 215) + SetTextEntry("STRING") + SetTextCentre(1) + AddTextComponentString(text) + DrawText(_x, _y) + local factor = (string.len(text)) / 370 + DrawRect(_x, _y + 0.0125, 0.015 + factor, 0.03, 41, 11, 41, 68) +end + +-- Function to open scenario selection menu +function OpenScenarioMenu() + menuOpen = true + + -- Simple menu display + Citizen.CreateThread(function() + local selected = 1 + + while menuOpen do + Citizen.Wait(0) + + -- Draw background + DrawRect(0.5, 0.5, 0.3, 0.5, 0, 0, 0, 200) + + -- Draw title + SetTextFont(4) + SetTextScale(0.5, 0.5) + SetTextColour(255, 255, 255, 255) + SetTextCentre(true) + SetTextEntry("STRING") + AddTextComponentString("Train Transportation") + DrawText(0.5, 0.3) + + -- Draw options + for i, scenario in ipairs(scenarios) do + local y = 0.35 + (i * 0.05) + local color = {r = 255, g = 255, b = 255} + + if selected == i then + color = {r = 255, g = 255, b = 0} + DrawRect(0.5, y, 0.28, 0.04, 41, 41, 41, 200) + end + + SetTextFont(4) + SetTextScale(0.35, 0.35) + SetTextColour(color.r, color.g, color.b, 255) + SetTextCentre(true) + SetTextEntry("STRING") + AddTextComponentString(scenario.label) + DrawText(0.5, y - 0.015) + end + + -- Instructions + SetTextFont(4) + SetTextScale(0.3, 0.3) + SetTextColour(255, 255, 255, 255) + SetTextCentre(true) + SetTextEntry("STRING") + AddTextComponentString("↑/↓: Navigate | ENTER: Select | BACKSPACE: Cancel") + DrawText(0.5, 0.65) + + -- Handle controls + DisableControlAction(0, 172, true) -- UP + DisableControlAction(0, 173, true) -- DOWN + DisableControlAction(0, 176, true) -- ENTER + DisableControlAction(0, 177, true) -- BACKSPACE + + if IsDisabledControlJustPressed(0, 172) then -- UP + selected = selected - 1 + if selected < 1 then selected = #scenarios end + PlaySoundFrontend(-1, "NAV_UP_DOWN", "HUD_FRONTEND_DEFAULT_SOUNDSET", true) + elseif IsDisabledControlJustPressed(0, 173) then -- DOWN + selected = selected + 1 + if selected > #scenarios then selected = 1 end + PlaySoundFrontend(-1, "NAV_UP_DOWN", "HUD_FRONTEND_DEFAULT_SOUNDSET", true) + elseif IsDisabledControlJustPressed(0, 176) then -- ENTER + menuOpen = false + PlaySoundFrontend(-1, "SELECT", "HUD_FRONTEND_DEFAULT_SOUNDSET", true) + TriggerEvent('train:startscenario', scenarios[selected].name) + elseif IsDisabledControlJustPressed(0, 177) then -- BACKSPACE + menuOpen = false + PlaySoundFrontend(-1, "CANCEL", "HUD_FRONTEND_DEFAULT_SOUNDSET", true) + end + end + end) +end + +-- Main thread for checking player position +Citizen.CreateThread(function() + while true do + Citizen.Wait(0) + + local playerPed = PlayerPedId() + local coords = GetEntityCoords(playerPed) + local dist = #(coords - interactionPoint.coords) + + if dist < interactionPoint.radius then + isInMarker = true + Draw3DText(interactionPoint.coords.x, interactionPoint.coords.y, interactionPoint.coords.z + 1.0, interactionPoint.text) + + -- Check for E press + if IsControlJustReleased(0, 38) and not menuOpen then -- 38 is E + OpenScenarioMenu() + end + else + isInMarker = false + if menuOpen then + menuOpen = false + end + end + end +end) + +-- Create a blip on the map (optional) +Citizen.CreateThread(function() + local blip = AddBlipForCoord(interactionPoint.coords) + SetBlipSprite(blip, 513) -- Train sprite + SetBlipDisplay(blip, 4) + SetBlipScale(blip, 0.8) + SetBlipColour(blip, 2) + SetBlipAsShortRange(blip, true) + BeginTextCommandSetBlipName("STRING") + AddTextComponentString("Train Transportation") + EndTextCommandSetBlipName(blip) +end)