From 9c4c92e696452dbe62ed9b980a1b8b3235873b06 Mon Sep 17 00:00:00 2001 From: Jim Shield Date: Fri, 6 Jun 2025 00:45:08 +0100 Subject: [PATCH] Move GTA notify, target, skillcheck to /ui_modules I've separated the built-in custom native GTA Notifications, skillcheck and draw text targets to separate files to be loaded along side `jim_bridge` This now makes it so they only load once instead of with every single script, and each one can call on it This change also makes these systems able to be called outside of `jim_bridge` For example in my test server I'm currently using: ```lua function QBCore.Functions.Notify(text, texttype, length, icon) CreateThread(function() exports.jim_bridge:Notify(nil, text, icon) end) end ``` --- shared/contextmenus.lua | 3 +- shared/notify.lua | 58 +++---- shared/skillcheck.lua | 119 +------------ shared/targets.lua | 146 ++-------------- ui_modules/notifications.lua | 120 +++++++++++++ ui_modules/skillcheck.lua | 121 +++++++++++++ ui_modules/target.lua | 318 +++++++++++++++++++++++++++++++++++ 7 files changed, 599 insertions(+), 286 deletions(-) create mode 100644 ui_modules/notifications.lua create mode 100644 ui_modules/skillcheck.lua create mode 100644 ui_modules/target.lua diff --git a/shared/contextmenus.lua b/shared/contextmenus.lua index 48a0b39..d4d8f15 100644 --- a/shared/contextmenus.lua +++ b/shared/contextmenus.lua @@ -194,7 +194,7 @@ function openMenu(Menu, data) exports[QBMenuExport]:openMenu(Menu) elseif Config.System.Menu == "gta" then - WarMenu.CreateMenu(tostring(Menu), data.header, data.headertxt or " ", { + WarMenu.CreateMenu(tostring(Menu), data.header, " ", { titleColor = { 222, 255, 255 }, maxOptionCountOnScreen = 15, width = 0.25, @@ -232,6 +232,7 @@ function openMenu(Menu, data) if pressed and not Menu[k].isMenuHeader then WarMenu.CloseMenu() close = false + Wait(10) Menu[k].onSelect() end end diff --git a/shared/notify.lua b/shared/notify.lua index 4dc327c..9959106 100644 --- a/shared/notify.lua +++ b/shared/notify.lua @@ -50,18 +50,10 @@ function triggerNotify(title, message, type, src) TriggerClientEvent('ox_lib:notify', src, { title = title, description = message, type = type or "success" }) end elseif Config.System.Notify == "gta" then - if isStarted("jim-gtaui") then - if not src then - TriggerEvent("jim-gtaui:Notify", title, message, type) - else - TriggerClientEvent("jim-gtaui:Notify", src, title, message, type) - end + if not src then + exports.jim_bridge:Notify(title, message, type) else - if not src then - TriggerEvent(getScript()..":DisplayGTANotify", title, message) - else - TriggerClientEvent(getScript()..":DisplayGTANotify", src, title, message) - end + TriggerClientEvent("jim-bridge:Notify", src, title, message, type) end elseif Config.System.Notify == "esx" then if not src then @@ -116,25 +108,25 @@ end) --- ```lua --- TriggerEvent(getScript()..":DisplayGTANotify", "taxiname", "Taxi service has arrived.") --- ``` -RegisterNetEvent(getScript()..":DisplayGTANotify", function(title, text) - local iconTable = {} - if getScript() == "jim-npcservice" then - iconTable = { - [Loc[Config.Lan].notify["taxiname"]] = "CHAR_TAXI", - [Loc[Config.Lan].notify["limoname"]] = "CHAR_CASINO", - [Loc[Config.Lan].notify["ambiname"]] = "CHAR_CALL911", - [Loc[Config.Lan].notify["pilotname"]] = "CHAR_DEFAULT", - [Loc[Config.Lan].notify["planename"]] = "CHAR_BOATSITE2", - [Loc[Config.Lan].notify["heliname"]] = "CHAR_BOATSITE2", - } - end - - BeginTextCommandThefeedPost("STRING") - AddTextComponentSubstringKeyboardDisplay(text) - EndTextCommandThefeedPostMessagetext( - iconTable[title] or "CHAR_DEFAULT", - iconTable[title] or "CHAR_DEFAULT", - true, 1, title, nil, text - ) - EndTextCommandThefeedPostTicker(true, false) -end) \ No newline at end of file +--RegisterNetEvent(getScript()..":DisplayGTANotify", function(title, text) +-- local iconTable = {} +-- if getScript() == "jim-npcservice" then +-- iconTable = { +-- [Loc[Config.Lan].notify["taxiname"]] = "CHAR_TAXI", +-- [Loc[Config.Lan].notify["limoname"]] = "CHAR_CASINO", +-- [Loc[Config.Lan].notify["ambiname"]] = "CHAR_CALL911", +-- [Loc[Config.Lan].notify["pilotname"]] = "CHAR_DEFAULT", +-- [Loc[Config.Lan].notify["planename"]] = "CHAR_BOATSITE2", +-- [Loc[Config.Lan].notify["heliname"]] = "CHAR_BOATSITE2", +-- } +-- end +-- +-- BeginTextCommandThefeedPost("STRING") +-- AddTextComponentSubstringKeyboardDisplay(text) +-- EndTextCommandThefeedPostMessagetext( +-- iconTable[title] or "CHAR_DEFAULT", +-- iconTable[title] or "CHAR_DEFAULT", +-- true, 1, title, nil, text +-- ) +-- EndTextCommandThefeedPostTicker(true, false) +--end) \ No newline at end of file diff --git a/shared/skillcheck.lua b/shared/skillcheck.lua index 945d69a..f4d3c79 100644 --- a/shared/skillcheck.lua +++ b/shared/skillcheck.lua @@ -30,127 +30,10 @@ function skillCheck(data) result = false end elseif Config.System.skillCheck == "gta" then - loadTextureDict("timerbars") - local successes = 0 - local barsRequired = 3 + exports.jim_bridge:skillCheck() - for bar = 1, barsRequired do - debugPrint("^6Bridge^7: ^2Starting Bar ^3"..bar.."^7/^3"..barsRequired.."^7") - activeSkillCheck = true - local width, height = 0.2, 0.01 - local x, y = 0.5, 0.8 - - -- Random highlighted zone - local highlightSize = math.random(10, 20) / 100 - local highlightStart = math.random(10, 50) / 100 - local highlightEnd = highlightStart + highlightSize - local highlightAlpha = 0 - local cursorPos = 0.0 - local cursorSpeed = 0.025 - local movingRight = true - - while activeSkillCheck do - Wait(0) - makeInstructionalButtons({ - { keys = { 177 }, text = "Exit" }, - { keys = { 38 }, text = "Confirm" }, - }) - - createScaleBars(x, y, width, height) - - local pulse = (math.sin(GetGameTimer() / 250) + 1) / 2 -- Creates a pulsing effect - highlightAlpha = math.floor(150 + (pulse * 105)) -- Pulsing between 150 and 255 alpha - - -- Draw highlighted zone (success area) with pulsing effect - DrawRect(x - width / 2 + (highlightStart + highlightSize / 2) * width, y, highlightSize * width, height+0.001, 93, 182, 229, highlightAlpha ) - -- Draw moving cursor - DrawRect(x - width / 2 + cursorPos * width, y, 0.002, height + 0.01, 255, 255, 255, 255) - - -- Move cursor - if movingRight then - cursorPos += cursorSpeed - if cursorPos >= 1.0 then movingRight = false end - else - cursorPos -= cursorSpeed - if cursorPos <= 0.0 then movingRight = true end - end - - if IsControlJustPressed(0, 177) then -- Backspace to cancel - local displayTime = GetGameTimer() + 2000 - PlaySoundFrontend(-1, 'Highlight_Cancel', 'DLC_HEIST_PLANNING_BOARD_SOUNDS', 1) - while GetGameTimer() < displayTime do - Wait(0) - - createScaleBars(x, y, width, height) - - DrawRect(x - width / 2 + (highlightStart + highlightSize / 2) * width, y, highlightSize * width, height+0.001, 228, 52, 52, 255) - DrawRect(x - width / 2 + cursorPos * width, y, 0.002, height + 0.01, 255, 255, 255, 255) - - drawSuccessText(x, y, "Failed", 228, 52, 52) - end - return false - end - - -- Check for keypress (E) - if IsControlJustPressed(0, 38) then - activeSkillCheck = false - result = cursorPos >= highlightStart and cursorPos <= highlightEnd - if result then - PlaySoundFrontend(-1, "YES", "HUD_FRONTEND_DEFAULT_SOUNDSET", true) - else - PlaySoundFrontend(-1, 'Highlight_Cancel', 'DLC_HEIST_PLANNING_BOARD_SOUNDS', 1) - end - local displayTime = GetGameTimer() + 2000 - while GetGameTimer() < displayTime do - Wait(0) - - createScaleBars(x, y, width, height) - - -- Draw highlighted zone - DrawRect(x - width / 2 + (highlightStart + highlightSize / 2) * width, y, highlightSize * width, height+0.001, result and 93 or 228, result and 182 or 52, result and 229 or 52, 180) - - -- Draw stationary cursor at result position - DrawRect(x - width / 2 + cursorPos * width, y, 0.002, height + 0.01, 255, 255, 255, 255) - -- Display result text - drawSuccessText(x, y, result and "Success" or "Failed", result and 114 or 228, result and 204 or 52, result and 144 or 52) - end - if result then - successes += 1 - else - return false - end - end - end - end - activeSkillCheck = false - debugPrint("^6Bridge^7: ^2Skill Check Result^7: ^3" .. successes .. "^7/^3" .. barsRequired.."^7") - return successes == barsRequired else result = true end return result -end - -function drawSuccessText(x, y, text, r, g, b) - SetTextFont(8) - SetTextScale(0.45, 0.45) - SetTextColour(r, g, b, 255) - SetTextDropshadow(0, 0, 0, 0, 255) - SetTextEdge(2, 0, 0, 0, 150) - SetTextDropShadow() - SetTextOutline() - SetTextCentre(true) - SetTextEntry("STRING") - SetTextCentre(true) - SetTextEntry("STRING") - AddTextComponentString(text) - DrawText(x, y + 0.03) -end - -function createScaleBars(x, y, width, height) - -- Draw background box - DrawSprite("timerbars", "all_black_bg", x - (width / 4) - 0.006, y, (width / 2) + 0.08, height + 0.04, 0.0, 255, 255, 255, 255) - DrawSprite("timerbars", "all_black_bg", x + (width / 4) + 0.006, y, (width / 2) + 0.08, height + 0.04, 180.0, 255, 255, 255, 255) - -- Draw full bar (dark background) - DrawRect(x, y, width, height, 100, 100, 100, 255) end \ No newline at end of file diff --git a/shared/targets.lua b/shared/targets.lua index 5a80687..10f16c5 100644 --- a/shared/targets.lua +++ b/shared/targets.lua @@ -20,24 +20,7 @@ ------------------------------------------------------------- -- Utility Data & Tables ------------------------------------------------------------- ---- -local KEY_TABLE = { 38, 29, 47, 23, 45, 159, 162, 163 } --- Mapping of key codes to human-readable key names. -local Keys = { - [322] = "ESC", [288] = "F1", [289] = "F2", [170] = "F3", [166] = "F5", - [167] = "F6", [168] = "F7", [169] = "F8", [56] = "F9", [57] = "F10", - [243] = "~", [157] = "1", [158] = "2", [160] = "3", [164] = "4", [165] = "5", - [159] = "6", [161] = "7", [162] = "8", [163] = "9", [84] = "-", [83] = "=", - [177] = "BACKSPACE", [37] = "TAB", - [44] = "Q", [32] = "W", [38] = "E", [45] = "R", [245] = "T", [246] = "Y", - [303] = "U", [199] = "P", - [39] = "[", [40] = "]", [18] = "ENTER", [137] = "CAPS", - [34] = "A", [8] = "S", [9] = "D", [23] = "F", [47] = "G", - [74] = "H", [311] = "K", [182] = "L", [21] = "LEFTSHIFT", - [20] = "Z", [73] = "X", [26] = "C", [0] = "V", [29] = "B", [249] = "N", - [244] = "M", [82] = ",", [81] = "." -} -- Tables for storing created targets for the fallback system and zone management. local TextTargets = {} -- For fallback DrawText3D targets. @@ -82,38 +65,9 @@ function createEntityTarget(entity, opts, dist) -- Fallback: Use DrawText3D if targeting systems are disabled or unavailable. if Config.System.DontUseTarget or (not isStarted(OXTargetExport) and not isStarted(QBTargetExport)) then - local entityCoords = GetEntityCoords(entity) - debugPrint("^6Bridge^7: ^2Creating new ^3Entity^2 target with DrawText for entity ^7"..entity) - local existingTarget = nil - for _, target in pairs(TextTargets) do - if #(target.coords - entityCoords) < 0.01 then - existingTarget = target - break - end - end + debugPrint("^6Bridge^7: ^2Creating new ^3Entity ^2target with ^6"..OXTargetExport.." ^2for entity ^7"..entity) + exports.jim_bridge:createEntityTarget(entity, opts, dist) - if existingTarget then - for i = 1, #opts do - local key = KEY_TABLE[#existingTarget.options + i] - opts[i].key = key - existingTarget.buttontext[#existingTarget.buttontext + 1] = " ~b~[~w~" .. Keys[key] .. "~b~] ~w~" .. opts[i].label - existingTarget.options[#existingTarget.options + 1] = opts[i] - end - updateCachedText(existingTarget) - else - local tempText = {} - for i = 1, #opts do - opts[i].key = KEY_TABLE[i] - tempText[#tempText + 1] = " ~b~[~w~" .. Keys[opts[i].key] .. "~b~] ~w~" .. opts[i].label - end - TextTargets[entity] = { - coords = vec3(entityCoords.x, entityCoords.y, entityCoords.z), - buttontext = tempText, - options = opts, - dist = dist, - text = table.concat(tempText, "\n") - } - end elseif isStarted(OXTargetExport) then debugPrint("^6Bridge^7: ^2Creating new ^3Entity ^2target with ^6"..OXTargetExport.." ^2for entity ^7"..entity) local options = {} @@ -129,10 +83,12 @@ function createEntityTarget(entity, opts, dist) } end exports[OXTargetExport]:addLocalEntity(entity, options) + elseif isStarted(QBTargetExport) then debugPrint("^6Bridge^7: ^2Creating new ^3Entity ^2target with ^6"..QBTargetExport.." ^2for entity ^7"..entity) local options = { options = opts, distance = dist } exports[QBTargetExport]:AddTargetEntity(entity, options) + end end @@ -192,37 +148,8 @@ end function createBoxTarget(data, opts, dist) if Config.System.DontUseTarget or (not isStarted(OXTargetExport) and not isStarted(QBTargetExport)) then debugPrint("^6Bridge^7: ^2Creating new ^3Box^2 target with ^6DrawText ^2 for zone ^7"..data[1]) - local existingTarget = nil - for _, target in pairs(TextTargets) do - if #(target.coords - data[2]) < 0.01 then - existingTarget = target - break - end - end + return exports.jim_bridge:createZoneTarget(data, opts, dist) - if existingTarget then - for i = 1, #opts do - local key = KEY_TABLE[#existingTarget.options + i] - opts[i].key = key - existingTarget.buttontext[#existingTarget.buttontext + 1] = " ~b~[~w~" .. Keys[key] .. "~b~] ~w~" .. opts[i].label - existingTarget.options[#existingTarget.options + 1] = opts[i] - end - updateCachedText(existingTarget) - else - local tempText = {} - for i = 1, #opts do - opts[i].key = KEY_TABLE[i] - tempText[#tempText + 1] = " ~b~[~w~" .. Keys[opts[i].key] .. "~b~] ~w~" .. opts[i].label - end - TextTargets[data[1]] = { - coords = data[2], - buttontext = tempText, - options = opts, - dist = dist, - text = table.concat(tempText, "\n") - } - end - return data[1] elseif isStarted(OXTargetExport) then debugPrint("^6Bridge^7: ^2Creating new ^3Box^2 target with ^6"..OXTargetExport.." ^2for zone ^7"..data[1]) local options = {} @@ -250,12 +177,14 @@ function createBoxTarget(data, opts, dist) }) boxTargets[#boxTargets + 1] = target return target + elseif isStarted(QBTargetExport) then debugPrint("^6Bridge^7: ^2Creating new ^3Box^2 target with ^6"..QBTargetExport.." ^2for zone ^7"..data[1]) local options = { options = opts, distance = dist } local target = exports[QBTargetExport]:AddBoxZone(data[1], data[2], data[3], data[4], data[5], options) boxTargets[#boxTargets + 1] = target return data[1] + end end @@ -298,37 +227,8 @@ end function createCircleTarget(data, opts, dist) if Config.System.DontUseTarget then debugPrint("^6Bridge^7: ^2Creating new ^3Circle ^2target with ^6DrawText ^2for zone ^7"..data[1]) - local existingTarget = nil - for _, target in pairs(TextTargets) do - if #(target.coords - data[2]) < 0.01 then - existingTarget = target - break - end - end + return exports.jim_bridge:createZoneTarget(data, opts, dist) - if existingTarget then - for i = 1, #opts do - local key = KEY_TABLE[#existingTarget.options + i] - opts[i].key = key - existingTarget.buttontext[#existingTarget.buttontext + 1] = " ~b~[~w~" .. Keys[key] .. "~b~] ~w~" .. opts[i].label - existingTarget.options[#existingTarget.options + 1] = opts[i] - end - updateCachedText(existingTarget) - else - local tempText = {} - for i = 1, #opts do - opts[i].key = KEY_TABLE[i] - tempText[#tempText + 1] = " ~b~[~w~" .. Keys[opts[i].key] .. "~b~] ~w~" .. opts[i].label - end - TextTargets[data[1]] = { - coords = data[2], - buttontext = tempText, - options = opts, - dist = dist, - text = table.concat(tempText, "\n") - } - end - return data[1] elseif isStarted(OXTargetExport) then debugPrint("^6Bridge^7: ^2Creating new ^3Circle ^2target with ^6"..OXTargetExport.." ^2for zone ^7"..data[1]) local options = {} @@ -387,30 +287,8 @@ end ---``` function createModelTarget(models, opts, dist) if Config.System.DontUseTarget or (not isStarted(OXTargetExport) and not isStarted(QBTargetExport)) then - if type(models) ~= "table" then - models = { models } - end + return exports.jim_bridge:createModelTarget(models, opts, dist) - local tempText = {} - for i = 1, #opts do - opts[i].key = KEY_TABLE[i] - tempText[#tempText + 1] = " ~b~[~w~" .. Keys[opts[i].key] .. "~b~] ~w~" .. opts[i].label - end - - local keyStr = "" - for i, m in ipairs(models) do - keyStr = keyStr .. tostring(m) .. (i < #models and "_" or "") - end - local targetKey = "model_" .. keyStr - - TextTargets[targetKey] = { - models = models, - buttontext = tempText, - options = opts, - dist = dist, - coords = vec3(0, 0, 0), - text = table.concat(tempText, "\n") - } elseif isStarted(OXTargetExport) then debugPrint("^6Bridge^7: ^2Creating new ^3Model^2 target with ^6"..OXTargetExport) local options = {} @@ -453,7 +331,7 @@ function removeEntityTarget(entity) exports[OXTargetExport]:removeLocalEntity(entity, nil) end if Config.System.DontUseTarget or (not isStarted(OXTargetExport) and not isStarted(QBTargetExport)) then - TextTargets[entity] = nil + exports.jim_bridge:removeEntityTarget(entity) end end @@ -474,7 +352,7 @@ function removeZoneTarget(target) exports[OXTargetExport]:removeZone(target, true) end if Config.System.DontUseTarget or (not isStarted(OXTargetExport) and not isStarted(QBTargetExport)) then - TextTargets[target] = nil + exports.jim_bridge:removeZoneTarget(target) end end @@ -494,7 +372,7 @@ function removeModelTarget(model) exports[OXTargetExport]:removeModel(model, nil) end if Config.System.DontUseTarget or (not isStarted(OXTargetExport) and not isStarted(QBTargetExport)) then - TextTargets[entity] = nil + exports.jim_bridge:removeZoneTarget(target) end end ------------------------------------------------------------- diff --git a/ui_modules/notifications.lua b/ui_modules/notifications.lua new file mode 100644 index 0000000..4b54433 --- /dev/null +++ b/ui_modules/notifications.lua @@ -0,0 +1,120 @@ +local notifications = {} +local spacing = 60 -- vertical spacing between notifications +local activeDrawing = false -- flag for drawing loop state + +if not HasStreamedTextureDictLoaded("timerbars") then + while not HasStreamedTextureDictLoaded("timerbars") do + RequestStreamedTextureDict("timerbars") + Wait(5) + end +end + +local notifTypes = { + success = "✔️", + error = "❌", + warning = "⚠️", + police = "🚓", + ambulance = "🚑", +} + +function gtaNotify(title, message, emoji, src) + local notif = { + title = title, + message = message, + emoji = notifTypes[emoji] or "❔", + state = "enter", + startTime = GetGameTimer(), + progress = 0, + holdTime = 8000, + slideDuration = 100, + currentOffset = 0, + } + table.insert(notifications, notif) + + -- Activate drawing loop if not already running + -- This allows it to be silent until the first notifcation is called, then the loop starts + if not activeDrawing then + activeDrawing = true + StartDrawingLoop() + end +end + +-- Drawing loop as a separate controlled thread +function StartDrawingLoop() + CreateThread(function() + while #notifications > 0 do + local currentTime = GetGameTimer() + + -- Update notifications vertical offset + for i, notif in ipairs(notifications) do + local target = (#notifications - i) * spacing + notif.currentOffset += (target - notif.currentOffset) * 0.1 + end + + for i = #notifications, 1, -1 do + local notif = notifications[i] + + if notif.state == "enter" then + local elapsed = currentTime - notif.startTime + notif.progress = math.min(elapsed / notif.slideDuration, 1.0) + if notif.progress >= 1.0 then + notif.state = "hold" + notif.holdStart = currentTime + end + elseif notif.state == "hold" then + if currentTime - notif.holdStart >= notif.holdTime then + notif.state = "exit" + notif.exitStart = currentTime + end + elseif notif.state == "exit" then + local elapsed = currentTime - notif.exitStart + notif.progress = 1.0 - math.min(elapsed / notif.slideDuration, 1.0) + if notif.progress <= 0 then + table.remove(notifications, i) + goto continue + end + end + + local startX, targetX = 1.0, 0.8 + local posX = startX - (startX - targetX) * notif.progress + local posY = 0.05 + (notif.currentOffset / 1080) + + -- Background sprite + DrawSprite("timerbars", "all_black_bg", posX + 0.11, posY + 0.025, 0.2, 0.053, 0.0, 255, 255, 255, 255) + + -- Title text + local moveMessage = false + if not notif.title or notif.title == "" then + moveMessage = true + else + drawNotiText(8, 0.4, vec2(0.75, 0.975), notif.title, vec2(posX, posY)) + end + + -- Message text + drawNotiText(4, 0.3, vec2(0.75, 0.975), notif.message, vec2(posX, posY + (moveMessage and 0.015 or 0.03))) + + -- Emoji + drawNotiText(0, 0.3, vec2(0.75, 0.995), notif.emoji, vec2(posX, posY + 0.015)) + + ::continue:: + end + Wait(0) + end + activeDrawing = false -- No notifications left, pause drawing + end) +end + +function drawNotiText(font, scale, wrap, string, pos) + SetTextFont(font) + SetTextScale(scale, scale) + SetTextWrap(wrap.x, wrap.y) + SetTextJustification(2) + SetTextColour(255, 255, 255, 255) + SetTextOutline() + SetTextEntry("STRING") + AddTextComponentString(string) + DrawText(pos.x, pos.y) +end + +RegisterNetEvent("jim-bridge:Notify", gtaNotify) +exports("Notify", gtaNotify) \ No newline at end of file diff --git a/ui_modules/skillcheck.lua b/ui_modules/skillcheck.lua new file mode 100644 index 0000000..d2fbf30 --- /dev/null +++ b/ui_modules/skillcheck.lua @@ -0,0 +1,121 @@ +local activeSkillCheck = false + +function gtaSkillCheck() + if activeSkillCheck then return end + local result = false + local successes = 0 + local barsRequired = 3 + + for bar = 1, barsRequired do + activeSkillCheck = true + local width, height = 0.2, 0.01 + local x, y = 0.5, 0.8 + + -- Random highlighted zone + local highlightSize = math.random(10, 20) / 100 + local highlightStart = math.random(10, 50) / 100 + local highlightEnd = highlightStart + highlightSize + local highlightAlpha = 0 + local cursorPos = 0.0 + local cursorSpeed = 0.025 + local movingRight = true + + while activeSkillCheck do + Wait(0) + + createScaleBars(x, y, width, height) + + local pulse = (math.sin(GetGameTimer() / 250) + 1) / 2 -- Creates a pulsing effect + highlightAlpha = math.floor(150 + (pulse * 105)) -- Pulsing between 150 and 255 alpha + + -- Draw highlighted zone (success area) with pulsing effect + DrawRect(x - width / 2 + (highlightStart + highlightSize / 2) * width, y, highlightSize * width, height+0.001, 93, 182, 229, highlightAlpha ) + -- Draw moving cursor + DrawRect(x - width / 2 + cursorPos * width, y, 0.002, height + 0.01, 255, 255, 255, 255) + + -- Move cursor + if movingRight then + cursorPos += cursorSpeed + if cursorPos >= 1.0 then movingRight = false end + else + cursorPos -= cursorSpeed + if cursorPos <= 0.0 then movingRight = true end + end + + if IsControlJustPressed(0, 177) then -- Backspace to cancel + local displayTime = GetGameTimer() + 2000 + PlaySoundFrontend(-1, 'Highlight_Cancel', 'DLC_HEIST_PLANNING_BOARD_SOUNDS', 1) + while GetGameTimer() < displayTime do + Wait(0) + + createScaleBars(x, y, width, height) + + DrawRect(x - width / 2 + (highlightStart + highlightSize / 2) * width, y, highlightSize * width, height+0.001, 228, 52, 52, 255) + DrawRect(x - width / 2 + cursorPos * width, y, 0.002, height + 0.01, 255, 255, 255, 255) + + drawSuccessText(x, y, "Failed", 228, 52, 52) + end + return false + end + + -- Check for keypress (E) + if IsControlJustPressed(0, 38) then + activeSkillCheck = false + result = cursorPos >= highlightStart and cursorPos <= highlightEnd + if result then + PlaySoundFrontend(-1, "YES", "HUD_FRONTEND_DEFAULT_SOUNDSET", true) + else + PlaySoundFrontend(-1, 'Highlight_Cancel', 'DLC_HEIST_PLANNING_BOARD_SOUNDS', 1) + end + local displayTime = GetGameTimer() + 2000 + while GetGameTimer() < displayTime do + Wait(0) + + createScaleBars(x, y, width, height) + + -- Draw highlighted zone + DrawRect(x - width / 2 + (highlightStart + highlightSize / 2) * width, y, highlightSize * width, height+0.001, result and 93 or 228, result and 182 or 52, result and 229 or 52, 180) + + -- Draw stationary cursor at result position + DrawRect(x - width / 2 + cursorPos * width, y, 0.002, height + 0.01, 255, 255, 255, 255) + -- Display result text + drawSuccessText(x, y, result and "Success" or "Failed", result and 114 or 228, result and 204 or 52, result and 144 or 52) + end + if result then + successes += 1 + else + return false + end + end + end + end + activeSkillCheck = false + print("^5GTAUI^7: ^2Skill Check Result^7: ^3" .. successes .. "^7/^3" .. barsRequired.."^7") + return successes == barsRequired +end + +function drawSuccessText(x, y, text, r, g, b) + SetTextFont(8) + SetTextScale(0.45, 0.45) + SetTextColour(r, g, b, 255) + SetTextDropshadow(0, 0, 0, 0, 255) + SetTextEdge(2, 0, 0, 0, 150) + SetTextDropShadow() + SetTextOutline() + SetTextCentre(true) + SetTextEntry("STRING") + SetTextCentre(true) + SetTextEntry("STRING") + AddTextComponentString(text) + DrawText(x, y + 0.03) +end + +function createScaleBars(x, y, width, height) + -- Draw background box + DrawSprite("timerbars", "all_black_bg", x - (width / 4) - 0.006, y, (width / 2) + 0.08, height + 0.04, 0.0, 255, 255, 255, 255) + DrawSprite("timerbars", "all_black_bg", x + (width / 4) + 0.006, y, (width / 2) + 0.08, height + 0.04, 180.0, 255, 255, 255, 255) + -- Draw full bar (dark background) + DrawRect(x, y, width, height, 100, 100, 100, 255) +end + +exports("skillCheck", gtaSkillCheck) \ No newline at end of file diff --git a/ui_modules/target.lua b/ui_modules/target.lua new file mode 100644 index 0000000..c54bae1 --- /dev/null +++ b/ui_modules/target.lua @@ -0,0 +1,318 @@ +-- Global Key Table, defined once. +--- +local KEY_TABLE = { 38, 29, 47, 23, 45, 159, 162, 163 } + +-- Mapping of key codes to human-readable key names. +local Keys = { + [322] = "ESC", [288] = "F1", [289] = "F2", [170] = "F3", [166] = "F5", + [167] = "F6", [168] = "F7", [169] = "F8", [56] = "F9", [57] = "F10", + [243] = "~", [157] = "1", [158] = "2", [160] = "3", [164] = "4", [165] = "5", + [159] = "6", [161] = "7", [162] = "8", [163] = "9", [84] = "-", [83] = "=", + [177] = "BACKSPACE", [37] = "TAB", + [44] = "Q", [32] = "W", [38] = "E", [45] = "R", [245] = "T", [246] = "Y", + [303] = "U", [199] = "P", + [39] = "[", [40] = "]", [18] = "ENTER", [137] = "CAPS", + [34] = "A", [8] = "S", [9] = "D", [23] = "F", [47] = "G", + [74] = "H", [311] = "K", [182] = "L", [21] = "LEFTSHIFT", + [20] = "Z", [73] = "X", [26] = "C", [0] = "V", [29] = "B", [249] = "N", + [244] = "M", [82] = ",", [81] = "." +} +-- Tables for storing created targets. +local TextTargets = {} -- For fallback DrawText3D targets. +local targetEntities = {} -- For entity targets. + +function createEntityTarget(entity, opts, dist) + startTargetLoop() + targetEntities[#targetEntities + 1] = entity + local entityCoords = GetEntityCoords(entity) + + local existingTarget = nil + for _, target in pairs(TextTargets) do + if #(target.coords - entityCoords) < 0.01 then + existingTarget = target + break + end + end + + if existingTarget then + for i = 1, #opts do + local key = KEY_TABLE[#existingTarget.options + i] + opts[i].key = key + existingTarget.buttontext[#existingTarget.buttontext + 1] = " ~b~[~w~" .. Keys[key] .. "~b~] ~w~" .. opts[i].label + existingTarget.options[#existingTarget.options + 1] = opts[i] + end + updateCachedText(existingTarget) + else + local tempText = {} + for i = 1, #opts do + opts[i].key = KEY_TABLE[i] + tempText[#tempText + 1] = " ~b~[~w~" .. Keys[opts[i].key] .. "~b~] ~w~" .. opts[i].label + end + TextTargets[entity] = { + coords = vec3(entityCoords.x, entityCoords.y, entityCoords.z), + buttontext = tempText, + options = opts, + dist = dist, + text = table.concat(tempText, "\n") + } + end +end + +function createZoneTarget(data, opts, dist) + startTargetLoop() + local existingTarget = nil + for _, target in pairs(TextTargets) do + if #(target.coords - data[2]) < 0.01 then + existingTarget = target + break + end + end + + if existingTarget then + for i = 1, #opts do + local key = KEY_TABLE[#existingTarget.options + i] + opts[i].key = key + existingTarget.buttontext[#existingTarget.buttontext + 1] = " ~b~[~w~" .. Keys[key] .. "~b~] ~w~" .. opts[i].label + existingTarget.options[#existingTarget.options + 1] = opts[i] + end + updateCachedText(existingTarget) + else + local tempText = {} + for i = 1, #opts do + opts[i].key = KEY_TABLE[i] + tempText[#tempText + 1] = " ~b~[~w~" .. Keys[opts[i].key] .. "~b~] ~w~" .. opts[i].label + end + TextTargets[data[1]] = { + coords = data[2], + buttontext = tempText, + options = opts, + dist = dist, + text = table.concat(tempText, "\n") + } + end + return data[1] +end + +function createModelTarget(models, opts, dist) + startTargetLoop() + if type(models) ~= "table" then + models = { models } + end + + local tempText = {} + for i = 1, #opts do + opts[i].key = KEY_TABLE[i] + tempText[#tempText + 1] = " ~b~[~w~" .. Keys[opts[i].key] .. "~b~] ~w~" .. opts[i].label + end + + local keyStr = "" + for i, m in ipairs(models) do + keyStr = keyStr .. tostring(m) .. (i < #models and "_" or "") + end + local targetKey = "model_" .. keyStr + + TextTargets[targetKey] = { + models = models, + buttontext = tempText, + options = opts, + dist = dist, + coords = vec3(0, 0, 0), + text = table.concat(tempText, "\n") + } + + return targetKey +end + +function removeEntityTarget(entity) + TextTargets[entity] = nil +end + +function removeZoneTarget(target) + TextTargets[target] = nil +end + +function removeModelTarget(model) + TextTargets[model] = nil +end + +exports("createEntityTarget", createEntityTarget) +exports("createZoneTarget", createZoneTarget) +exports("createModelTarget", createModelTarget) + +exports("removeEntityTarget", removeEntityTarget) +exports("removeZoneTarget", removeZoneTarget) +exports("removeModelTarget", removeModelTarget) + + +------------------------------------------------------------- +-- Fallback: DrawText3D Targets (Experimental) +------------------------------------------------------------- +local started = false +function startTargetLoop() + if started then return end + Config = { + System = { + + } + } + started = true + local fileLoader = assert(load(LoadResourceFile("jim_bridge", ('starter.lua')), ('@@jim_bridge/starter.lua'))) + fileLoader() + -- Model Entity Refresher + CreateThread(function() + while true do + local pedCoords = GetEntityCoords(PlayerPedId()) + for _, target in pairs(TextTargets) do + if target.models then + for _, model in ipairs(target.models) do + local entity = GetClosestObjectOfType(pedCoords.x, pedCoords.y, pedCoords.z, target.dist, model, false, false, false) + if entity and entity ~= 0 then + target.entity = entity + target.coords = GetEntityCoords(entity) + break + end + end + end + end + Wait(3000) -- Refresh every 3s + end + end) + + -- Main Target Loop + CreateThread(function() + while true do + local ped = PlayerPedId() + local pedCoords = GetEntityCoords(ped) + local camCoords = GetGameplayCamCoord() + local camRot = GetGameplayCamRot(2) + local camForward = RotationToDirection(camRot) + + local closestTarget, closestDist, targetEntity = nil, math.huge, nil + + for _, target in pairs(TextTargets) do + local coords = target.coords + if coords then + local dist = #(pedCoords - coords) + if dist <= target.dist then + local normVec = normalizeVector(coords - camCoords) + local dot = camForward.x * normVec.x + camForward.y * normVec.y + camForward.z * normVec.z + + if dot > 0.5 and dist < closestDist then + closestTarget = target + closestDist = dist + targetEntity = target.entity + end + end + end + end + + for _, target in pairs(TextTargets) do + if not target.coords then goto continue end + local dist = #(pedCoords - target.coords) + if dist > target.dist then goto continue end + local isClosest = (target == closestTarget) + + for i, opt in ipairs(target.options) do + if IsControlJustPressed(0, opt.key) and isClosest then + if (not target.canInteract or target.canInteract()) and + (not opt.item or hasItem(opt.item)) and + (not opt.job or hasJob(opt.job, nil)) then + if opt.onSelect then opt.onSelect(targetEntity) end + if opt.action then opt.action(targetEntity) end + end + end + end + + local baseZ, lineHeight = target.coords.z + 1.0, -0.16 + local lineOffset = 0 + + for i, opt in ipairs(target.options) do + if (not target.canInteract or target.canInteract()) and + (not opt.item or hasItem(opt.item)) and + (not opt.job or hasJob(opt.job, nil)) then + DrawText3D(vec3(target.coords.x, target.coords.y, baseZ + lineHeight * lineOffset), target.buttontext[i], isClosest) + lineOffset = lineOffset + 1 + end + end + + ::continue:: + end + + Wait(1) -- Throttled + end + end) +end + + +function DrawText3D(coord, text, highlight) + SetTextScale(0.30, 0.30) + SetTextFont(0) + SetTextProportional(1) + SetTextColour(255, 255, 255, 215) + SetTextEntry("STRING") + SetTextCentre(true) + + local totalLength = string.len(text) + local textMaxLength = 99 -- max 99 + local text = totalLength > textMaxLength and text:sub(1, totalLength - (totalLength - textMaxLength)) or text + AddTextComponentString(highlight and text:gsub("%~w~", "~y~") or text) + SetDrawOrigin(coord.x, coord.y, coord.z, 0) + DrawText(0.0, 0.0) + local count, length = GetLineCountAndMaxLength(text) + + local padding = 0.005 + local heightFactor = (count / 43) + padding + local weightFactor = (length / 150) + padding + + local height = (heightFactor / 2) - padding / 1 + local width = (weightFactor / 2) - padding / 1 + + DrawRect(0.0, height, width, heightFactor, 0, 0, 0, 150) + ClearDrawOrigin() +end + +--- Calculates the number of lines and the maximum line length from the given text. +--- +--- @param text string The text to analyze. +--- @return number, number The line count and maximum line length. +--- +--- @usage +--- ```lua +--- local count, maxLen = GetLineCountAndMaxLength("Hello World") +--- ``` +function GetLineCountAndMaxLength(text) + local lineCount, maxLength = 0, 0 + for line in text:gmatch("[^\n]+") do + lineCount += 1 + local lineLength = string.len(line) + if lineLength > maxLength then + maxLength = lineLength + end + end + if lineCount == 0 then lineCount = 1 end + return lineCount, maxLength +end + + +function RotationToDirection(rot) + local adjust = math.pi / 180 + return vec3( + -math.sin(adjust * rot.z) * math.abs(math.cos(adjust * rot.x)), + math.cos(adjust * rot.z) * math.abs(math.cos(adjust * rot.x)), + math.sin(adjust * rot.x) + ) +end + +function normalizeVector(vec) + local len = math.sqrt(vec.x^2 + vec.y^2 + vec.z^2) + if len ~= 0 then + return vec3(vec.x / len, vec.y / len, vec.z / len) + else + return vec3(0, 0, 0) + end +end + +-- Helper to update cached text. +function updateCachedText(target) + target.text = table.concat(target.buttontext, "\n") +end \ No newline at end of file