diff --git a/resources/[notify]/rtx_notify/.fxap b/resources/[notify]/rtx_notify/.fxap new file mode 100644 index 000000000..9268b5448 Binary files /dev/null and b/resources/[notify]/rtx_notify/.fxap differ 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 000000000..e870bbb98 Binary files /dev/null and b/resources/[notify]/rtx_notify/client/main.lua differ 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 000000000..69d71e50c Binary files /dev/null and b/resources/[notify]/rtx_notify/html/BebasNeuePro-Bold.ttf differ diff --git a/resources/[notify]/rtx_notify/html/debounce.min.js b/resources/[notify]/rtx_notify/html/debounce.min.js new file mode 100644 index 000000000..648fe5d3c --- /dev/null +++ b/resources/[notify]/rtx_notify/html/debounce.min.js @@ -0,0 +1,9 @@ +/* + * jQuery throttle / debounce - v1.1 - 3/7/2010 + * http://benalman.com/projects/jquery-throttle-debounce-plugin/ + * + * Copyright (c) 2010 "Cowboy" Ben Alman + * Dual licensed under the MIT and GPL licenses. + * http://benalman.com/about/license/ + */ +(function(b,c){var $=b.jQuery||b.Cowboy||(b.Cowboy={}),a;$.throttle=a=function(e,f,j,i){var h,d=0;if(typeof f!=="boolean"){i=j;j=f;f=c}function g(){var o=this,m=+new Date()-d,n=arguments;function l(){d=+new Date();j.apply(o,n)}function k(){h=c}if(i&&!h){l()}h&&clearTimeout(h);if(i===c&&m>e){l()}else{if(f!==true){h=setTimeout(i?k: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 000000000..5ef6454e1 Binary files /dev/null and b/resources/[notify]/rtx_notify/html/img/close.png differ diff --git a/resources/[notify]/rtx_notify/html/scripts.js b/resources/[notify]/rtx_notify/html/scripts.js new file mode 100644 index 000000000..89f007c40 --- /dev/null +++ b/resources/[notify]/rtx_notify/html/scripts.js @@ -0,0 +1,306 @@ +$(function () { + var notifyresourcename = "rtx_notify"; + var soundhandler = new Howl({ + src: "sounds/soundnotify.mp3", + volume: 0.2, + }); + + var currentscale = 1.0; + var currentvolume = 1; + var currenthide = 0; + if (localStorage.default == "false") { + } else { + localStorage.scale = 1.0; + localStorage.soundactivated = 1; + localStorage.hideactivated = 0; + } + + let defaultValues = {notifytop: "2", notifyleft: "82", scale: "1.0",}; + + var scaleslider = document.getElementById("scalesliderdata"); + + function openMain() { + $("body").css("display", "block"); + } + + function NotifyDrag() { + $(".notify-container").css("background-color", "rgba(0, 0, 0, 1.0)"); + $(".notify-container ").draggable({ + scroll: false, + axis: "x, y", + cursor: "move" + }); + }; + + function currentsliderdata() { + if (localStorage.default === "true") { + if (defaultValues.scale == 0.5) { + return 5; + } else if (defaultValues.scale == 0.6) { + return 6; + } else if (defaultValues.scale == 0.7) { + return 7; + } else if (defaultValues.scale == 0.8) { + return 8; + } else if (defaultValues.scale == 0.9) { + return 9; + } else if (defaultValues.scale == 1.0) { + return 10; + } else if (defaultValues.scale == 1.0) { + return 10; + } else if (defaultValues.scale == 1.1) { + return 11; + } else if (defaultValues.scale == 1.2) { + return 12; + } else if (defaultValues.scale == 1.3) { + return 13; + } else if (defaultValues.scale == 1.4) { + return 14; + } else if (defaultValues.scale == 1.5) { + return 15; + } + } else { + if (localStorage.scale == 0.5) { + return 5; + } else if (localStorage.scale == 0.6) { + return 6; + } else if (localStorage.scale == 0.7) { + return 7; + } else if (localStorage.scale == 0.8) { + return 8; + } else if (localStorage.scale == 0.9) { + return 9; + } else if (localStorage.scale == 1.0) { + return 10; + } else if (localStorage.scale == 1.0) { + return 10; + } else if (localStorage.scale == 1.1) { + return 11; + } else if (localStorage.scale == 1.2) { + return 12; + } else if (localStorage.scale == 1.3) { + return 13; + } else if (localStorage.scale == 1.4) { + return 14; + } else if (localStorage.scale == 1.5) { + return 15; + } + } + } + + function ChangeNotify() { + if (localStorage.default === "true") { + var notifytop = defaultValues.notifytop + "%"; + var notifyleft = defaultValues.notifyleft + '%'; + $('.notify-container').css({ + position:'absolute', + top:notifytop, + left:notifyleft + }); + localStorage.soundactivated = 1; + localStorage.hideactivated = 0; + currentvolume = 1; + currenthide = 0; + localStorage.scale = defaultValues.scale; + scaleslider.value = currentsliderdata(); + currentscale = defaultValues.scale; + $(".notify-container").css("transform", "scale(" + defaultValues.scale + ")"); + document.getElementById("notifyvolumeswitchdata").checked = true; + document.getElementById("notifyhideswitchdata").checked = false; + } else { + var notifytop = localStorage.notifytop + 'px'; + var notifyleft = localStorage.notifyleft + 'px'; + var scaledata = localStorage.scale; + var volumedata = localStorage.soundactivated; + var hidedata = localStorage.hideactivated; + $('.notify-container').css({ + position:'absolute', + top:notifytop, + left:notifyleft + }); + scaleslider.value = currentsliderdata(); + $(".notify-container").css("transform", "scale(" + scaledata + ")"); + 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; + } + } + }; + + $("#notifyvolumeswitchdata").click(function(){ + if (document.getElementById("notifyvolumeswitchdata").checked == false){ + document.getElementById("notifyvolumeswitchdata").checked = false; + currentvolume = 0; + } + else { + document.getElementById("notifyvolumeswitchdata").checked = true; + currentvolume = 1; + } + }) + + $("#notifyhideswitchdata").click(function(){ + if (document.getElementById("notifyhideswitchdata").checked == false){ + document.getElementById("notifyhideswitchdata").checked = false; + currenthide = 0; + } + else { + document.getElementById("notifyhideswitchdata").checked = true; + currenthide = 1; + } + }) + + $(".closenotifyedit").click(function(){ + ChangeNotify(); + $(".notify-container").draggable("destroy"); + $(".notify-container").css("background-color", "rgba(0, 0, 0, 0.0)"); + $("#notifysettingsshow").hide(); + $.post('http://'+notifyresourcename+'/closesettingsnotify', JSON.stringify({})); + }); + + $(".buttonsettingsreset").click(function(){ + localStorage.default = "true"; + ChangeNotify(); + }); + + $(".buttonsettingssave").click(function(){ + $(".notify-container").draggable("disable") + localStorage.default = "false"; + var notifypos = $(".notify-container").position(); + localStorage.notifytop = notifypos.top; + localStorage.notifyleft = notifypos.left; + localStorage.scale = currentscale; + localStorage.soundactivated = currentvolume; + localStorage.hideactivated = currenthide; + ChangeNotify(); + $(".notify-container").draggable("destroy"); + $(".notify-container").css("background-color", "rgba(0, 0, 0, 0.0)"); + $("#notifysettingsshow").hide(); + $.post('http://'+notifyresourcename+'/closesettingsnotify', JSON.stringify({})); + }); + + scaleslider.oninput = function() { + if (this.value == 5) { + $(".notify-container").css("transform", "scale(0.5)"); + currentscale = 0.5; + } else if (this.value == 6) { + $(".notify-container").css("transform", "scale(0.6)"); + currentscale = 0.6; + } else if (this.value == 7) { + $(".notify-container").css("transform", "scale(0.7)"); + currentscale = 0.7; + } else if (this.value == 8) { + $(".notify-container").css("transform", "scale(0.8)"); + currentscale = 0.8; + } else if (this.value == 9) { + $(".notify-container").css("transform", "scale(0.9)"); + currentscale = 0.9; + } else if (this.value == 10) { + $(".notify-container").css("transform", "scale(1.0)"); + currentscale = 1.0; + } else if (this.value == 11) { + $(".notify-container").css("transform", "scale(1.1)"); + currentscale = 1.1; + } else if (this.value == 12) { + $(".notify-container").css("transform", "scale(1.2)"); + currentscale = 1.2; + } else if (this.value == 13) { + $(".notify-container").css("transform", "scale(1.3)"); + currentscale = 1.3; + } else if (this.value == 14) { + $(".notify-container").css("transform", "scale(1.4)"); + currentscale = 1.4; + } else if (this.value == 15) { + $(".notify-container").css("transform", "scale(1.5)"); + currentscale = 1.5; + } + } + + window.addEventListener('message', function (event) { + + var item = event.data; + + if (item.message == "addnotify"){ + if (localStorage.hideactivated == 0) { + openMain(); + $( ".notifications" ).append('
' + + '
' + 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 000000000..4888383a7 Binary files /dev/null and b/resources/[notify]/rtx_notify/html/sounds/soundnotify.mp3 differ diff --git a/resources/[notify]/rtx_notify/html/styles.css b/resources/[notify]/rtx_notify/html/styles.css new file mode 100644 index 000000000..c377b1332 --- /dev/null +++ b/resources/[notify]/rtx_notify/html/styles.css @@ -0,0 +1,479 @@ +:root { + --color: var(--color); +} + +*{ + user-select: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +@font-face { + font-family: BebasNeuePro-Bold; + src: url(BebasNeuePro-Bold.ttf); +} + +html { + overflow: hidden; + font-family: 'Open Sans', sans-serif; +} + +body{ + display: none; + color: #a8a8aa; +} + +::-webkit-scrollbar +{ + width: 4px; +} + +::-webkit-scrollbar-thumb +{ + background-color: rgba(0, 0, 0, 0); +} + +.notify-container { + width: 16%; + height: 95%; + background-color: rgba(0, 0, 0, 0.0); + margin: 0; + position: absolute; + top: 2%; + left: 82%; + overflow: hidden; + z-index: 9999999; + display: block; + transform: scale(1.0); + transform-origin: 0 0; + border-radius: 10px; +} + +.notify { + display: block; + border-radius: 5px; + overflow: hidden; + margin: 0; + z-index: 9999999; + width: 100%; + background-color: rgba(0, 0, 0, 0.70); + color: #fff; + margin-bottom: 3%; + -moz-animation: fadein 1s; + -webkit-animation: fadein 1s; + -o-animation: fadein 1s; + position: relative; +} + +.notifytitle { + padding-top: 10px; + vertical-align: middle; + text-align: center; + font-size: 30px; + font-family: BebasNeuePro-Bold; +} + +.notifytext { + padding-top: 2px; + padding-bottom: 15px; + vertical-align: middle; + text-align: center; + font-size: 20px; + padding-left: 15px; + padding-right: 15px; + font-family: BebasNeuePro-Bold; +} + +.notifyprogress { + display: block; + height: 3px; + width: 100%; + text-align: center; + background-color: var(--color); + vertical-align: bottom; + transform: rotate(180deg); + float: right; +} + +.notifysettings-container { + width: 20%; + height: 30%; + background-color: rgba(0, 0, 0, 0.70); + border-bottom: 3px solid var(--color); + border-radius: 10px; + margin: 0; + position: absolute; + top: 50%; + left: 50%; + margin-right: -50%; + transform: translate(-50%, -50%); + overflow: hidden; + z-index: 99999999; + display: none; +} + +.closenotifyedit { + position: absolute; + margin: 0; + top: 10%; + left: 92%; + margin-right: -50%; + transform: translate(-50%, -50%); +} + +.notifysettingsmaintext { + position: absolute; + margin: 0; + text-decoration: none; + text-align: center; + top: 10%; + left: 50%; + margin-right: -50%; + transform: translate(-50%, -50%); + font-size: 35px; + color: var(--color); + text-align:center; + font-family: BebasNeuePro-Bold; +} + +.notifysettingsscale { + position: absolute; + margin: 0; + text-decoration: none; + text-align: center; + top: 25%; + left: 50%; + margin-right: -50%; + transform: translate(-50%, -50%); + font-size: 25px; + color: #ffffff; + text-align:center; + font-family: BebasNeuePro-Bold; +} + +.scaleslidercontainer { + position: absolute; + margin: 0; + background-color: rgba(0, 0, 0, 1); + border-bottom: 3px solid var(--color); + color: #ffffff; + font-size: 35px; + text-decoration: none; + text-align: center; + top: 37%; + left: 50%; + margin-right: -50%; + transform: translate(-50%, -50%); + overflow: hidden; + z-index: 9999999; + width: 80%; + height: 11%; + border-radius: 5px; + font-family: BebasNeueBold; +} + +.scaleslider { + -webkit-appearance: none; + appearance: none; + position: absolute; + margin: 0; + background-color: rgba(255, 255, 255, 0.0); + color: #ffffff; + font-size: 35px; + text-decoration: none; + text-align: center; + top: 50%; + left: 50%; + margin-right: -50%; + transform: translate(-50%, -50%); + overflow: hidden; + z-index: 9999999; + width: 90%; + height: 80%; + border-radius: 5px; + font-family: BebasNeueBold; + outline: none; +} + +.scalesliderline { + -webkit-appearance: none; + appearance: none; + position: absolute; + margin: 0; + background-color: rgba(255, 255, 255, 1.0); + color: #ffffff; + font-size: 35px; + text-decoration: none; + text-align: center; + top: 50%; + left: 50%; + margin-right: -50%; + transform: translate(-50%, -50%); + overflow: hidden; + z-index: 9999999; + width: 90%; + height: 8%; + border-radius: 5px; + font-family: BebasNeueBold; + outline: none; +} + +.scaleslider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 17px; + height: 17px; + border-radius: 50%; + cursor: pointer; + background-color: var(--color); +} + +.scaleslider::-moz-range-thumb { + width: 17px; + height: 17px; + border-radius: 50%; + cursor: pointer; + background-color: var(--color); +} + +input[type=range]:focus { + outline: none; +} + +.notifysettingssound { + position: absolute; + margin: 0; + text-decoration: none; + text-align: center; + top: 52%; + left: 25%; + margin-right: -50%; + transform: translate(-50%, -50%); + font-size: 25px; + color: #ffffff; + text-align:center; + font-family: BebasNeuePro-Bold; +} + +.notifysettingshide { + position: absolute; + margin: 0; + text-decoration: none; + text-align: center; + top: 52%; + left: 75%; + margin-right: -50%; + transform: translate(-50%, -50%); + font-size: 25px; + color: #ffffff; + text-align:center; + font-family: BebasNeuePro-Bold; +} + +.notifyvolumeswitch { + display: inline-block; + width: 30%; + height: 11%; + position: absolute; + margin: 0; + text-decoration: none; + text-align: center; + top: 64%; + left: 25%; + margin-right: -50%; + transform: translate(-50%, -50%); + text-align:center; +} + +.notifyvolumeswitch input { + opacity: 0; + width: 0; + height: 0; +} + +.slidervolumeswitch { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 1); + border-bottom: 3px solid var(--color); + border-radius: 5px; + -webkit-transition: .4s; + transition: .4s; +} + +.slidervolumeswitch:before { + position: absolute; + content: ""; + width: 25%; + height: 70%; + top: 50%; + left: 25%; + margin-right: -50%; + transform: translate(-50%, -50%); + background-color: #ffffff; + border-radius: 5px; + -webkit-transition: .4s; + transition: .4s; +} + +input:checked + .slidervolumeswitch:before { + top: 50%; + left: 75%; + margin-right: -50%; + transform: translate(-50%, -50%); +} + +.notifyhideswitch { + display: inline-block; + width: 30%; + height: 11%; + position: absolute; + margin: 0; + text-decoration: none; + text-align: center; + top: 64%; + left: 75%; + margin-right: -50%; + transform: translate(-50%, -50%); + text-align:center; +} + +.notifyhideswitch input { + opacity: 0; + width: 0; + height: 0; +} + +.sliderhideswitch { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 1); + border-bottom: 3px solid var(--color); + border-radius: 5px; + -webkit-transition: .4s; + transition: .4s; +} + +.sliderhideswitch:before { + position: absolute; + content: ""; + width: 25%; + height: 70%; + top: 50%; + left: 25%; + margin-right: -50%; + transform: translate(-50%, -50%); + background-color: #ffffff; + border-radius: 5px; + -webkit-transition: .4s; + transition: .4s; +} + +input:checked + .sliderhideswitch:before { + top: 50%; + left: 75%; + margin-right: -50%; + transform: translate(-50%, -50%); +} + +.buttonsettingsreset { + position: absolute; + margin: 0; + background-color: rgba(0, 0, 0, 1); + border-bottom: 3px solid var(--color); + color: #ffffff; + font-size: 27px; + padding-top: 5px; + padding-bottom: 5px; + padding-left: 5px; + padding-right: 5px; + text-decoration: none; + text-align: center; + top: 85%; + left: 25%; + margin-right: -50%; + transform: translate(-50%, -50%); + overflow: hidden; + z-index: 99999999; + width: 30%; + border-radius: 8px; + font-family: BebasNeuePro-Bold; +} + +.buttonsettingsreset:hover { + background: var(--color); + text-decoration: none; +} + +.buttonsettingssave { + position: absolute; + margin: 0; + background-color: rgba(0, 0, 0, 1); + border-bottom: 3px solid var(--color); + color: #ffffff; + font-size: 27px; + padding-top: 5px; + padding-bottom: 5px; + padding-left: 5px; + padding-right: 5px; + text-decoration: none; + text-align: center; + top: 85%; + left: 75%; + margin-right: -50%; + transform: translate(-50%, -50%); + overflow: hidden; + z-index: 99999999; + width: 30%; + border-radius: 8px; + font-family: BebasNeuePro-Bold; +} + +.buttonsettingssave:hover { + background: var(--color); + text-decoration: none; +} + +.full-screen { + width: 100%; + height:100%; + display: flex; + align-items: center; +} + +.notifications { + width: 100%; + left: 50%; + overflow: auto; + display: flex; + flex-wrap: wrap; + flex-direction: column; +} + +@keyframes progressanim { + from { + width 100%; + } + to { + width:0%; + } +} + +@keyframes fadein { + from { + opacity:0; + } + to { + opacity:1; + } +} \ No newline at end of file diff --git a/resources/[notify]/rtx_notify/html/ui.html b/resources/[notify]/rtx_notify/html/ui.html new file mode 100644 index 000000000..3e0f33a13 --- /dev/null +++ b/resources/[notify]/rtx_notify/html/ui.html @@ -0,0 +1,41 @@ + + + + + + + + + + + + +
+
+
+
+
+
+ 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)