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