Advertisement
supinus

Essential

Jun 25th, 2025
646
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 112.91 KB | Gaming | 0 0
  1. ac.log('Script: Essential')
  2. local sim = ac.getSim()
  3. local car = ac.getCar(0) or error()
  4. if not car then return end
  5. local wheels = car.wheels or error()
  6. local uiState = ac.getUI()
  7.  
  8. ui.setAsynchronousImagesLoading(true)
  9.  
  10. local localTesting = ac.dirname() == 'C:\\Program Files (x86)\\Steam\\steamapps\\common\\assettocorsa\\extension\\lua\\online'
  11. local initialisation = true
  12.  
  13. -- Constants --
  14. local STEAMID = const(ac.getUserSteamID())
  15. local CSP_VERSION = const(ac.getPatchVersionCode())
  16. local CSP_MIN_VERSION = const(3044)
  17. local CAR_ID = const(ac.getCarID(0))
  18. local CAR_NAME = const(ac.getCarName(0))
  19. local DRIVER_NAME = const(ac.getDriverName(0))
  20. if CSP_VERSION < CSP_MIN_VERSION then return end
  21.  
  22. local SHARED_PLAYER_DATA = const('__ACP_SHARED_PLAYER_DATA')
  23. local SHARED_EVENT_KEY = const('__ACP_PLAYER_SHARED_UPDATE')
  24.  
  25. local DRIVER_NATION_CODE = const(ac.getDriverNationCode(0))
  26. local UNIT = "km/h"
  27. local UNIT_MULT = 1
  28. if DRIVER_NATION_CODE == "USA" or DRIVER_NATION_CODE == "GBR" then
  29.     UNIT = "mph"
  30.     UNIT_MULT = 0.621371
  31. end
  32.  
  33. local STATS_FONT_SIZE = const({
  34.     header = 30 / uiState.uiScale,
  35.     stats = 20 / uiState.uiScale,
  36. })
  37.  
  38. SECTORS_DATA = const({
  39.     [1] = {
  40.         name = "H1",
  41.         timeLimit = 0,
  42.         addTimeLimit = { 0, 0, 0 },
  43.         length = 26.5,
  44.         gates = {
  45.             { pos = { -753.56, 138.82, 3541.54 }, dir = { -0.9, 0, -0.43 }, width = 14.75, id = 1 },
  46.             { pos = { 3001.98, 72.4, 1027.23 }, dir = { -0.85, 0, 0.52 }, width = 15.65, id = 2 },
  47.         },
  48.     },
  49.     [2] = {
  50.         name = "DOUBLE TROUBLE",
  51.         timeLimit = 200,
  52.         addTimeLimit = { 0, 5, 15 },
  53.         length = 8.25,
  54.         discordLink = "https://discord.com/channels/358562025032646659/1300231481095880725",
  55.         video = "https://www.youtube.com/watch?v=FMUogCkQ1qw",
  56.         gates = {
  57.             { pos = { 767.34, 95.8, 2262.69 }, dir = { -0.82, 0, 0.56 }, width = 14.7, id = 1 },
  58.             { pos = { -3541.52, 23.48, -206.67 }, dir = { -0.87, 0, 0.49 }, width = 10.27, id = 2 },
  59.         },
  60.     },
  61.     [3] = {
  62.         name = "BOBs SCRAPYARD",
  63.         timeLimit = 200,
  64.         addTimeLimit = { 0, 5, 130 },
  65.         length = 8.25,
  66.         discordLink = "https://discord.com/channels/358562025032646659/1300207647873695755",
  67.         video = "https://www.youtube.com/watch?v=FMUogCkQ1qw",
  68.         gates = {
  69.             { pos = { 767.34, 95.8, 2262.69 }, dir = { -0.82, 0, 0.56 }, width = 14.7, id = 1 },
  70.             { pos = { -3541.52, 23.48, -206.67 }, dir = { -0.87, 0, 0.49 }, width = 10.27, id = 2 },
  71.         },
  72.     },
  73.     [4] = {
  74.         name = "BANK HEIST",
  75.         timeLimit = 475,
  76.         addTimeLimit = { 0, 40, 245 },
  77.         length = 21.5,
  78.         discordLink = "https://discord.com/channels/358562025032646659/1300207698280841266",
  79.         video = "https://www.youtube.com/watch?v=U7Kr5E_ImGI",
  80.         gates = {
  81.             { pos = { -700.04, 137.72, 3540.75 }, dir = { -1.67, 0, 1.02 }, width = 12.1, id = 1 },
  82.             { pos = { 5188.14, 58.22, -1640.53 }, dir = { -0.07, 0, -1 }, width = 5.56, id = 2 },
  83.         },
  84.     },
  85.     [5] = {
  86.         name = "DRUG DELIVERY",
  87.         timeLimit = 315,
  88.         addTimeLimit = { 0, 25, 195 },
  89.         length = 14,
  90.         discordLink = "https://discord.com/channels/358562025032646659/1300207870515744869",
  91.         video = "https://www.youtube.com/watch?v=7YKganFmzNA",
  92.         gates = {
  93.             { pos = { -395.08, 127.66, 3392.71 }, dir = { -0.7, 0, -0.72 }, width = 35.95, id = 1 },
  94.             { pos = { 585.71, -115.77, -3439.67 }, dir = { 0.99, 0, 0.03 }, width = 6.78, id = 2 },
  95.         },
  96.     },
  97. })
  98.  
  99. local POLICE_CAR = const({ "ids_2022_ford_crown", "r34police_acp24" })
  100.  
  101. local LEADERBOARDS = const({
  102.     time = {"H1", "BOBs SCRAPYARD", "DOUBLE TROUBLE", "DRUG DELIVERY", "BANK HEIST" },
  103.     score = { "arrests", "getaways", "overtake", "thefts", "heists", "deliveries", "elo" }, --, "kms", "time" },
  104. })
  105. local LEADERBOARD_NAMES = const({
  106.     { "Your Stats", "H1", "BOBs SCRAPYARD", "DOUBLE TROUBLE", "DRUG DELIVERY", "BANK HEIST", "arrests", "getaways", "overtake", "thefts", "heists", "deliveries", "elo" }, --, "kms", "time" },
  107.     { "Your Stats", "H1", "Bobs Scrapyard", "Double Trouble", "Drug Delivery", "Bank Heist", "Arrestations", "Getaways", "Overtake", "Car thefts", "Bank Heists", "Drug Deliveries", "Racing" }, --, "Distance Driven", "Time Played" },
  108. })
  109. local patchCount = 0
  110.  
  111. -- URL --
  112. local GOOGLE_APP_SCRIPT_URL = const(
  113.     'https://script.google.com/macros/s/AKfycbwenxjCAbfJA-S90VlV0y7mEH75qt3TuqAmVvlGkx-Y1TX8z5gHtvf5Vb8bOVNOA_9j/exec')
  114. local FIREBASE_URL = const('https://acp-server-97674-default-rtdb.firebaseio.com/')
  115.  
  116. -- UI --
  117. local WINDOW_WIDTH = const(sim.windowWidth / uiState.uiScale)
  118. local WIDTH_DIV = const({
  119.     _2 = WINDOW_WIDTH / 2,
  120.     _3 = WINDOW_WIDTH / 3,
  121.     _4 = WINDOW_WIDTH / 4,
  122.     _5 = WINDOW_WIDTH / 5,
  123.     _6 = WINDOW_WIDTH / 6,
  124.     _8 = WINDOW_WIDTH / 8,
  125.     _10 = WINDOW_WIDTH / 10,
  126.     _12 = WINDOW_WIDTH / 12,
  127.     _15 = WINDOW_WIDTH / 15,
  128.     _20 = WINDOW_WIDTH / 20,
  129.     _25 = WINDOW_WIDTH / 25,
  130.     _32 = WINDOW_WIDTH / 32,
  131.     _40 = WINDOW_WIDTH / 40,
  132.     _50 = WINDOW_WIDTH / 50,
  133.     _80 = WINDOW_WIDTH / 80,
  134.     _100 = WINDOW_WIDTH / 100,
  135.     _320 = WINDOW_WIDTH / 320,
  136. })
  137.  
  138. local WINDOW_HEIGHT = const(sim.windowHeight / uiState.uiScale)
  139. local HEIGHT_DIV = const({
  140.     _2 = WINDOW_HEIGHT / 2,
  141.     _3 = WINDOW_HEIGHT / 3,
  142.     _4 = WINDOW_HEIGHT / 4,
  143.     _12 = WINDOW_HEIGHT / 12,
  144.     _14 = WINDOW_HEIGHT / 14,
  145.     _20 = WINDOW_HEIGHT / 20,
  146.     _24 = WINDOW_HEIGHT / 24,
  147.     _40 = WINDOW_HEIGHT / 40,
  148.     _50 = WINDOW_HEIGHT / 50,
  149.     _60 = WINDOW_HEIGHT / 60,
  150.     _70 = WINDOW_HEIGHT / 70,
  151.     _80 = WINDOW_HEIGHT / 80,
  152.     _100 = WINDOW_HEIGHT / 100,
  153.     _320 = WINDOW_HEIGHT / 320,
  154. })
  155.  
  156. local FONT_MULT = const(WINDOW_HEIGHT / 1440)
  157.  
  158. local HUD_IMG = {}
  159. local WELCOME_NAV_IMG = {}
  160. local WELCOME_CARD_IMG = {}
  161.  
  162. local welcomeCardsToDisplayed = { 1, 2, 3, 4, 5, 6, 7 }
  163. local welcomeNavImgToDraw = {}
  164.  
  165. local IMAGES = const({
  166.     welcome = {
  167.         url = "https://github.com/ele-sage/ACP-apps/raw/refs/heads/master/images/welcome.zip",
  168.         card = {
  169.             "cartheft.jpg",
  170.             "drugdealer.jpg",
  171.             "bankheist.jpg",
  172.             -- "aboutacp.jpg",
  173.             "earnmoney.jpg",
  174.             -- "leaderboard.jpg",
  175.             "bank.jpg",
  176.             "police.jpg",
  177.             "buycars.jpg",
  178.             -- "tuning.jpg",
  179.         },
  180.         nav = {
  181.             "base.png",
  182.             "logo.png",
  183.             "leftBoxOff.png",
  184.             "leftBoxOn.png",
  185.             "centerBoxOff.png",
  186.             "centerBoxOn.png",
  187.             "rightBoxOff.png",
  188.             "rightBoxOn.png",
  189.             "leftArrowOff.png",
  190.             "leftArrowOn.png",
  191.             "rightArrowOff.png",
  192.             "rightArrowOn.png",
  193.         },
  194.     },
  195.     essential = {
  196.         url = "https://github.com/ele-sage/ACP-apps/raw/refs/heads/master/images/essential.zip",
  197.         hud = {
  198.             "base.png",
  199.             "center.png",
  200.             "left.png",
  201.             "right.png",
  202.             "countdown.png",
  203.             "menu.png",
  204.             "ranks.png",
  205.             "theft.png",
  206.         },
  207.     },
  208. })
  209.  
  210. ---@param key string
  211. local function loadImages(key)
  212.     ac.log(IMAGES[key].url)
  213.     web.loadRemoteAssets(IMAGES[key].url, function(err, data)
  214.         if err then
  215.             ac.error('Failed to load welcome images:', err)
  216.             return
  217.         end
  218.         local path = data .. '\\' .. key .. '\\'
  219.         local files = io.scanDir(path, "*")
  220.         if key == "welcome" then
  221.             for i, file in ipairs(files) do
  222.                 local cardIndex = table.indexOf(IMAGES.welcome.card, file)
  223.                 if cardIndex ~= nil then
  224.                     WELCOME_CARD_IMG[cardIndex] = path .. file
  225.                 elseif table.contains(IMAGES.welcome.nav, file) then
  226.                     local k = file:match('(.+)%..+')
  227.                     WELCOME_NAV_IMG[k] = path .. file
  228.                 end
  229.             end
  230.         elseif key == "essential" then
  231.             for i, file in ipairs(files) do
  232.                 if table.contains(IMAGES.essential.hud, file) then
  233.                     local k = file:match('(.+)%..+')
  234.                     HUD_IMG[k] = path .. file
  235.                 end
  236.             end
  237.         end
  238.     end)
  239. end
  240.  
  241. loadImages("welcome")
  242. loadImages("essential")
  243.  
  244. local WELCOME_CARD_LINK = const({
  245.     "https://www.youtube.com/watch?v=FMUogCkQ1qw", --car thefts
  246.     "https://www.youtube.com/watch?v=7YKganFmzNA", --drug dealer
  247.     "https://www.youtube.com/watch?v=U7Kr5E_ImGI", --bank heist
  248.     "https://discord.com/channels/358562025032646659/1299309514088120370", --earn
  249.     "https://discord.com/channels/358562025032646659/1299335101187883100", --bank done
  250.     "https://discord.com/channels/358562025032646659/1299310323253252117", --police done
  251.     "https://discord.com/channels/358562025032646659/1299310123482611812", --car done
  252. })
  253.  
  254. ---@param format string
  255. ---@param time number
  256. ---@return string
  257. local function formatTime(time, format)
  258.     if format == 'time played' then
  259.         local hours = math.floor(time / 3600)
  260.         local minutes = math.floor(time % 3600 / 60)
  261.         local seconds = math.floor(time % 60)
  262.         local formattedTime = ''
  263.         if hours > 0 then formattedTime = hours .. 'h ' end
  264.         if minutes > 0 then formattedTime = formattedTime .. minutes .. 'm ' end
  265.         formattedTime = formattedTime .. seconds .. 's'
  266.         return formattedTime
  267.     else
  268.         local minutes = math.floor(time / 60)
  269.         local seconds = math.floor(time % 60)
  270.         local milliseconds = math.floor((time % 1) * 1000)
  271.         return ('%02d:%02d.%03d'):format(minutes, seconds, milliseconds)
  272.     end
  273. end
  274.  
  275. local MISSIONS = const({
  276.     [1] = {
  277.         name = "BOBs SCRAPYARD",
  278.         start = { "Steal :", "Gas Station 1 TP" },
  279.         finish = { "Deliver :", "Red Car (Map)" },
  280.         levels = {
  281.             formatTime(SECTORS_DATA[3].timeLimit + SECTORS_DATA[3].addTimeLimit[3], ''),
  282.             formatTime(SECTORS_DATA[3].timeLimit + SECTORS_DATA[3].addTimeLimit[2], ''),
  283.             formatTime(SECTORS_DATA[3].timeLimit + SECTORS_DATA[3].addTimeLimit[1], ''),
  284.         },
  285.         tp= {
  286.             [1] = { pos = vec3(785.519, 95.8002, 2235.53), dir = vec3(0.51, -0.03, -0.86) },
  287.             [2] = { pos = vec3(787.707, 95.5171, 2240.88), dir = vec3(0.58, -0.03, -0.81) },
  288.             [3] = { pos = vec3(790.921, 95.1569, 2247.45), dir = vec3(0.8, -0.01, -0.60) },
  289.         },
  290.     },
  291.     [2] = {
  292.         name = "DRUG DELIVERY",
  293.         start = { "Pick Up :", "Drug Delivery TP" },
  294.         finish = { "Drop Off :", "Pink House (Map)" },
  295.         levels = {
  296.             formatTime(SECTORS_DATA[5].timeLimit + SECTORS_DATA[5].addTimeLimit[3], ''),
  297.             formatTime(SECTORS_DATA[5].timeLimit + SECTORS_DATA[5].addTimeLimit[2], ''),
  298.             formatTime(SECTORS_DATA[5].timeLimit + SECTORS_DATA[5].addTimeLimit[1], ''),
  299.         },
  300.         tp = {
  301.             [1] = { pos = vec3(-369.367, 127.557, 3405.47), dir = vec3(0.8, -0.01, 0.61) },
  302.             [2] = { pos = vec3(-374.729, 127.558, 3413.13), dir = vec3(0.69, -0.01, 0.73) },
  303.             [3] = { pos = vec3(-380.176, 127.557, 3419.49), dir = vec3(0.59, -0.01, 0.81) },
  304.         },
  305.     },
  306.     [3] = {
  307.         name = "BANK HEIST",
  308.         start = { "Rob :", "Bank TP" },
  309.         finish = { "Deliver :", "Yellow BHL (Map)" },
  310.         levels = {
  311.             formatTime(SECTORS_DATA[4].timeLimit + SECTORS_DATA[4].addTimeLimit[3], ''),
  312.             formatTime(SECTORS_DATA[4].timeLimit + SECTORS_DATA[4].addTimeLimit[2], ''),
  313.             formatTime(SECTORS_DATA[4].timeLimit + SECTORS_DATA[4].addTimeLimit[1], ''),
  314.         },
  315.         tp = {
  316.             [1] = { pos = vec3(-626.316, 135.37, 3509.81), dir = vec3(0.91, 0.03, -0.4) },
  317.             [2] = { pos = vec3(-635.369, 135.786, 3514.6), dir = vec3(0.92, 0.04, -0.39) },
  318.             [3] = { pos = vec3(-645.117, 136.215, 3518.99), dir = vec3(0.91, 0.03, -0.42) },
  319.         },
  320.     },
  321. })
  322.  
  323. local MISSION_NAMES = const({"DRUG DELIVERY", "BANK HEIST", "BOBs SCRAPYARD"})
  324. local MISSION_TEXT = const({
  325.     ["DRUG DELIVERY"] = {
  326.         chat = "* Picking up drugs *",
  327.         -- intro = { "You have ", " minutes to deliver up the drugs. Deliver the drugs to the Pink House!" },
  328.         intro = {"The deal's done! The drugs are in the car. You have ", " to drop the package at the Villa! No mistakes!"},
  329.         failed = {
  330.             "You're late! Even the drugs expired waiting for you.",
  331.             "The drugs ran out of patience, unlike your slow driving.",
  332.             "Looks like the drug deal went cold—literally.",
  333.             "Looks like the drug deal's off... thanks to you.",
  334.             "Even the cops stopped chasing—they got bored.",
  335.             "You just set a new record… for being the slowest criminal ever.",
  336.             "Hope you like walking, because you just lost your getaway ride.",
  337.             "Maybe next time, use the GPS… or learn to drive.",
  338.             "They say crime doesn't pay. Guess they were right.",
  339.             "You're late... Even the cops went home.",
  340.             "Hope you enjoyed the scenic route. Too bad it cost you the mission.",
  341.             "Crime waits for no one... except you, apparently.",
  342.             "Time's up, slowpoke! The loot's long gone.",
  343.             "I hope you enjoyed your leisurely failure.",
  344.             "You missed the mark by a mile—literally.",
  345.             "At this speed, you might as well walk.",
  346.         },
  347.     },
  348.     ["BOBs SCRAPYARD"] = {
  349.         chat = "* Stealing a " .. string.gsub(CAR_NAME, "%W", " ") .. " *",
  350.         -- intro = { "You have ", " minutes to steal the car. Deliver the car to Bobs Scrapyard!" },
  351.         intro = { "You cracked the car! Now you've got ", " to get it to Bob's Scrapyard! Don't stop, don't get caught!" },
  352.         failed = {
  353.             "Missed the car heist? Might as well try carpool karaoke next time.",
  354.             "Car theft? More like car borrowing… indefinitely.",
  355.             "Looks like the getaway car forgot to show up... oh wait, that's you.",
  356.             "You've got the speed of a parked car. Try again.",
  357.             "You've officially been overtaken... by a granny in a Prius.",
  358.             "Getaway driver? More like get-a-way-slower driver.",
  359.             "Looks like the car heist was a bust. Maybe try stealing a bike next time.",
  360.             "Time's up! Maybe you should consider Uber as a career choice.",
  361.             "Need For Speed? More like Need For a Nap.",
  362.             "You drive like my grandma, and she doesn't drive.",
  363.             "Criminal mastermind? More like criminally slow.",
  364.             "Even the cops are laughing at you.",
  365.             "Oops, looks like you lost track of time. Literally.",
  366.             "You missed the deadline… again.",
  367.             "Slow and steady doesn't win the race in this game.",
  368.             "You just got smoked—by your own bad driving.",
  369.         },
  370.     },
  371.     ["DOUBLE TROUBLE"] = {
  372.         chat = "* Stealing a " .. string.gsub(CAR_NAME, "%W", " ") .. " *",
  373.         -- intro = { "You have ", " minutes to steal the car. Deliver the car to Bobs Scrapyard!" },
  374.         intro = { "You cracked the car! Now you've got ", " to get it to Bob's Scrapyard! Don't stop, don't get caught!" },
  375.         failed = {
  376.             "Missed the car heist? Might as well try carpool karaoke next time.",
  377.             "Car theft? More like car borrowing… indefinitely.",
  378.             "Looks like the getaway car forgot to show up... oh wait, that's you.",
  379.             "You've got the speed of a parked car. Try again.",
  380.             "You've officially been overtaken... by a granny in a Prius.",
  381.             "Getaway driver? More like get-a-way-slower driver.",
  382.             "Looks like the car heist was a bust. Maybe try stealing a bike next time.",
  383.             "Time's up! Maybe you should consider Uber as a career choice.",
  384.             "Need For Speed? More like Need For a Nap.",
  385.             "You drive like my grandma, and she doesn't drive.",
  386.             "Criminal mastermind? More like criminally slow.",
  387.             "Even the cops are laughing at you.",
  388.             "Oops, looks like you lost track of time. Literally.",
  389.             "You missed the deadline… again.",
  390.             "Slow and steady doesn't win the race in this game.",
  391.             "You just got smoked—by your own bad driving.",
  392.         },
  393.     },
  394.     ["BANK HEIST"] = {
  395.         chat = "* Robbing the bank *",
  396.         -- intro = { "You have ", " minutes to rob the bank. Deliver the loot to the Yellow BHL!" },
  397.         intro = { "The bank's hit, the crew's in ! You've got ", " to get them and the loot to the BHL. Go, go, go!" },
  398.         failed = {
  399.             "At this rate, you'll be robbing piggy banks, not actual banks.",
  400.             "The bank called—they said thanks for not bothering.",
  401.             "Bank job? More like a piggy bank job.",
  402.             "You're so slow, the bank restocked its vault.",
  403.             "You've mastered the art of being fashionably late... for a robbery",
  404.             "The only thing you're robbing is your own time.",
  405.             "You'd make a great escape artist… if the art was staying put.",
  406.             "If slow and steady wins the race, you still wouldn't win.",
  407.             "That's a record! A record for being the slowest.",
  408.             "Time's up! The cops are laughing at you from the station.",
  409.             "You ran out of time... and talent.",
  410.             "Mission: Failed. Maybe consider a desk job?",
  411.             "I hope your backup plan is better than your driving.",
  412.             "You should should stick to your day job, losser.",
  413.             "Maybe you should have think before lighting up that joint.",
  414.             "You should have stayed in bed today, we would have been better off... Seriously.",
  415.         }
  416.     },
  417. });
  418.  
  419. local WELCOME_CARD_IMG_POS = const({
  420.     { vec2(70, 650),   vec2(320, 910) },
  421.     { vec2(2230, 650), vec2(2490, 910) },
  422.     { vec2(357, 325),  vec2(920, 1234) },
  423.     { vec2(993, 325),  vec2(1557, 1234) },
  424.     { vec2(1633, 325), vec2(2195, 1234) },
  425.     { vec2(31, 106),   vec2(2535, 1370) },
  426.     { vec2(2437, 48),  vec2(2510, 100) },
  427.     { vec2(2447, 58),  vec2(2500, 90) },
  428. })
  429.  
  430. local GATE_HEIGHT_OFFSET = const(0.2)
  431. local white = const(rgbm.colors.white)
  432. local gateColor = const(rgbm(0, 100, 0, 10))
  433.  
  434. local vUp = const(vec3(0, 1, 0))
  435. local vDown = const(vec3(0, -1, 0))
  436.  
  437. local menuStates = {
  438.     welcome = true,
  439.     main = false,
  440.     leaderboard = false,
  441. }
  442.  
  443. local duo = {
  444.     teammate = nil,
  445.     request = false,
  446.     onlineSender = nil,
  447.     teammateHasFinished = false,
  448.     waiting = false,
  449.     playerName = "Online Players",
  450.     sentFinish = false,
  451. }
  452.  
  453. local missionManager = {
  454.     msgTime = 0,
  455.     showIntro = false,
  456.     msgFailedIndex = os.time() % 16 + 1,
  457.     level = 3,
  458.     tp = false
  459. }
  460.  
  461. local function resetMissionManager()
  462.     if not missionManager.tp then
  463.         missionManager.msgTime = 0
  464.         missionManager.showIntro = false
  465.     else
  466.         missionManager.tp = false
  467.     end
  468.     missionManager.msgFailedIndex = os.time() % 16 + 1
  469.     missionManager.level = 3
  470. end
  471.  
  472. local menuSize = { vec2(WIDTH_DIV._5, HEIGHT_DIV._4), vec2(WIDTH_DIV._6, WINDOW_HEIGHT * 2 / 3), vec2(WIDTH_DIV._2, HEIGHT_DIV._2) }
  473. local playerStatsSubWindow = vec2(WIDTH_DIV._4 - 10, HEIGHT_DIV._2 - HEIGHT_DIV._20 - 10)
  474.  
  475. local currentTab = 1
  476.  
  477. local dataLoaded = {}
  478. dataLoaded['Settings'] = false
  479. dataLoaded['Leaderboard'] = false
  480. dataLoaded['PlayerData'] = false
  481. dataLoaded['Sectors'] = false
  482.  
  483. local openMenuKeyBind = ac.ControlButton('__ACP_OPEN_MENU_KEY_BIND', ui.KeyIndex.M)
  484.  
  485. ---@param carID string
  486. local function isPoliceCar(carID)
  487.     for _, carName in ipairs(POLICE_CAR) do
  488.         if carID == carName then
  489.             return true
  490.         end
  491.     end
  492.     return false
  493. end
  494.  
  495. ---@param key string
  496. local function removeUtf8Char(key)
  497.     local newKey = ''
  498.     for i = 1, #key do
  499.         local c = key:sub(i, i)
  500.         if c:byte() < 128 then
  501.             newKey = newKey .. c
  502.         end
  503.     end
  504.     newKey = newKey:match('^%s*(.-)%s*$')
  505.     return newKey
  506. end
  507.  
  508. local CAR_NAME_NO_UTF8 = removeUtf8Char(CAR_NAME)
  509.  
  510. if isPoliceCar(CAR_ID) then return end
  511.  
  512. --------- Utils ------------
  513. ---@param keys string[]
  514. ---@param t table
  515. local function hasKeys(keys, t)
  516.     for i = 1, #keys do
  517.         if not t[keys[i]] then
  518.             ac.error('Missing key:', keys[i])
  519.             return false
  520.         end
  521.     end
  522.     return true
  523. end
  524.  
  525. ---@param number number
  526. ---@param decimal integer
  527. ---@return number
  528. local function truncate(number, decimal)
  529.     local power = 10 ^ decimal
  530.     return math.floor(number * power) / power
  531. end
  532.  
  533.  
  534. ---@param t table
  535. local function tableToVec3(t)
  536.     return vec3(t[1], t[2], t[3])
  537. end
  538.  
  539. ---@param t table
  540. local function tableToVec2(t)
  541.     return vec2(t[1], t[2])
  542. end
  543.  
  544. ---@param t table
  545. local function tableToRGBM(t)
  546.     return rgbm(t[1], t[2], t[3], t[4])
  547. end
  548.  
  549. ---@param err string
  550. ---@param response WebResponse
  551. ---@return boolean
  552. local function canProcessRequest(err, response)
  553.     if err then
  554.         ac.error('Failed to process request:', err)
  555.         return false
  556.     end
  557.     return response.status == 200 and response.body ~= ''
  558. end
  559.  
  560. ---@param response WebResponse
  561. ---@return boolean
  562. local function hasExistingData(response)
  563.     return response.status == 200 and response.body ~= 'null'
  564. end
  565.  
  566. ---@param v vec3
  567. ---@return vec3
  568. local function snapToTrack(v)
  569.     if physics.raycastTrack(v, vDown, 20, v) == -1 then
  570.         physics.raycastTrack(v, vUp, 20, v)
  571.     end
  572.     return v
  573. end
  574.  
  575. ---@param category string
  576. ---@param rows LeaderboardRow[]
  577. ---@return LeaderboardRow[]
  578. local function sortLeaderboard(category, rows)
  579.     if category == "time" then
  580.         table.sort(rows, function(a, b)
  581.             return a[2] < b[2]
  582.         end)
  583.         for i, row in ipairs(rows) do
  584.             row[2] = formatTime(row[2], '')
  585.         end
  586.     else
  587.         table.sort(rows, function(a, b)
  588.             return a[2] > b[2]
  589.         end)
  590.     end
  591.     return rows
  592. end
  593.  
  594. ---@type Leaderboard[]
  595. local leaderboards = {}
  596. ---@type Leaderboard | Player
  597. local currentLeaderboard = nil
  598.  
  599. ---@class LeaderboardRow
  600. ---@field infos string[]
  601. local LeaderboardRow = class('LeaderboardRow')
  602.  
  603. ---@param category string
  604. ---@param data table
  605. ---@return LeaderboardRow
  606. function LeaderboardRow.allocate(category, data)
  607.     local infos = {}
  608.     if category == "time" then
  609.         table.insert(infos, data.Driver)
  610.         table.insert(infos, data.Time)
  611.         table.insert(infos, data.Car)
  612.     else
  613.         table.insert(infos, data.Driver)
  614.         table.insert(infos, data.Score)
  615.     end
  616.     setmetatable(infos, { __index = LeaderboardRow })
  617.     return infos
  618. end
  619.  
  620. ---@class Leaderboard
  621. ---@field name string
  622. ---@field category string
  623. ---@field header string[]
  624. ---@field rows LeaderboardRow[]
  625. ---@field rowCount integer
  626. ---@field nbCols integer
  627. local Leaderboard = class('Leaderboard')
  628.  
  629. ---@param name string
  630. function Leaderboard.noData(name)
  631.     local category = "score"
  632.     local header = { "Driver", "Score" }
  633.     local row = { "No Data", "No Data" }
  634.     for _, cat in ipairs(LEADERBOARDS.time) do
  635.         if cat == name then
  636.             header = { "Driver", "Time", "Car" }
  637.             category = "time"
  638.             row = { "No Data", "No Data", "No Data" }
  639.             break
  640.         end
  641.     end
  642.     local leaderboard = {
  643.         name = name,
  644.         category = category,
  645.         header = header,
  646.         rows = { row },
  647.         rowCount = 1,
  648.         nbCols = category == "time" and 3 or 2,
  649.     }
  650.     setmetatable(leaderboard, { __index = Leaderboard })
  651.     return leaderboard
  652. end
  653.  
  654. ---@param name string
  655. ---@param data table
  656. ---@return Leaderboard
  657. function Leaderboard.tryParse(name, data)
  658.     local rowCount = 0
  659.     local category = "score"
  660.     local header = { "Driver", "Score" }
  661.     for _, cat in ipairs(LEADERBOARDS.time) do
  662.         if cat == name then
  663.             header = { "Driver", "Time", "Car" }
  664.             category = "time"
  665.             break
  666.         end
  667.     end
  668.     local rows = {}
  669.     for steamID, record in pairs(data) do
  670.         local row = LeaderboardRow.allocate(category, record)
  671.         table.insert(rows, row)
  672.         rowCount = rowCount + 1
  673.     end
  674.     rows = sortLeaderboard(category, rows)
  675.     local leaderboard = {
  676.         name = name,
  677.         category = category,
  678.         header = header,
  679.         rows = rows,
  680.         rowCount = rowCount,
  681.         nbCols = category == "time" and 3 or 2,
  682.     }
  683.     setmetatable(leaderboard, { __index = Leaderboard })
  684.     return leaderboard
  685. end
  686.  
  687. ---@param name string
  688. function Leaderboard.fetch(name)
  689.     if leaderboards[name] then
  690.         currentLeaderboard = leaderboards[name]
  691.         return
  692.     end
  693.     if localTesting then
  694.         local currentPath = ac.getFolder(ac.FolderID.ScriptOrigin)
  695.         local file = io.open(currentPath .. '/response/leaderboardsResponse.json', 'r')
  696.         if not file then
  697.             ac.error('Failed to open leaderboardResponse.json')
  698.             return
  699.         end
  700.         local data = JSON.parse(file:read('*a'))
  701.         file:close()
  702.         data = data[name]
  703.         if data then
  704.             local leaderboard = Leaderboard.tryParse(name, data)
  705.             if leaderboard then
  706.                 leaderboards[name] = leaderboard
  707.                 currentLeaderboard = leaderboard
  708.                 return
  709.             end
  710.         end
  711.         local leaderboard = Leaderboard.noData(name)
  712.         leaderboards[name] = leaderboard
  713.         currentLeaderboard = leaderboard
  714.         ac.error('Failed to parse leaderboard data.')
  715.     else
  716.         local url = FIREBASE_URL .. 'Leaderboards/' .. name .. '.json'
  717.         web.get(url, function(err, response)
  718.             if canProcessRequest(err, response) then
  719.                 local data = JSON.parse(response.body)
  720.                 if data then
  721.                     local leaderboard = Leaderboard.tryParse(name, data)
  722.                     if leaderboard then
  723.                         leaderboards[name] = leaderboard
  724.                         currentLeaderboard = leaderboard
  725.                         return
  726.                     end
  727.                 end
  728.             end
  729.             local leaderboard = Leaderboard.noData(name)
  730.             leaderboards[name] = leaderboard
  731.             currentLeaderboard = leaderboard
  732.             ac.error('No leaderboard data found:', name)
  733.         end)
  734.     end
  735. end
  736.  
  737. ---@param name string
  738. function Leaderboard.allocate(name)
  739.     Leaderboard.fetch(name)
  740. end
  741.  
  742. local DEFAULT_SETTINGS = const({
  743.     essentialSize = 20,
  744.     policeSize = 20,
  745.     hudOffset = vec2(0, 0),
  746.     fontSize = 20 / uiState.uiScale,
  747.     current = 1,
  748.     colorHud = rgbm(1, 0, 0, 1),
  749.     colorHudInverted = rgbm(0, 1, 1, 1),
  750.     timeMsg = 10,
  751.     msgOffset = vec2(WIDTH_DIV._2, 10),
  752.     fontSizeMSG = 30 / uiState.uiScale,
  753.     menuPos = vec2(0, 0),
  754.     unit = UNIT,
  755.     unitMult = UNIT_MULT,
  756.     starsSize = 20,
  757.     starsPos = vec2(WIDTH_DIV._2, 0),
  758.     leaderboardWrapWidth = 20 / 1.5,
  759. })
  760.  
  761. ---@class Settings
  762. ---@field essentialSize number
  763. ---@field policeSize number
  764. ---@field hudOffset vec2
  765. ---@field fontSize number
  766. ---@field current number
  767. ---@field colorHud rgbm
  768. ---@field colorHudInverted rgbm
  769. ---@field timeMsg number
  770. ---@field msgOffset vec2
  771. ---@field fontSizeMSG number
  772. ---@field menuPos vec2
  773. ---@field unit string
  774. ---@field unitMult number
  775. ---@field starsSize number
  776. ---@field starsPos vec2
  777. local Settings = class('Settings')
  778.  
  779. ---@return Settings
  780. function Settings.new()
  781.     local settings = table.clone(DEFAULT_SETTINGS, true)
  782.     setmetatable(settings, { __index = Settings })
  783.     return settings
  784. end
  785.  
  786. ---@param data table
  787. ---@return Settings
  788. function Settings.tryParse(data)
  789.     local hudOffset = data.hudOffset and tableToVec2(data.hudOffset) or vec2(0, 0)
  790.     local colorHud = data.colorHud and tableToRGBM(data.colorHud) or rgbm(1, 0, 0, 1)
  791.     local msgOffset = data.msgOffset and tableToVec2(data.msgOffset) or vec2(WIDTH_DIV._2, 10)
  792.     local menuPos = data.menuPos and tableToVec2(data.menuPos) or vec2(0, 0)
  793.     local starsPos = data.starsPos and tableToVec2(data.starsPos) or vec2(WINDOW_WIDTH, 0)
  794.     local settings = {
  795.         essentialSize = data.essentialSize or 20,
  796.         policeSize = data.policeSize or 20,
  797.         hudOffset = hudOffset,
  798.         fontSize = data.fontSize or (20 / uiState.uiScale),
  799.         current = data.current or 1,
  800.         colorHud = colorHud,
  801.         colorHudInverted = rgbm(1 - colorHud.r, 1 - colorHud.g, 1 - colorHud.b, 1),
  802.         timeMsg = data.timeMsg or 10,
  803.         msgOffset = msgOffset,
  804.         fontSizeMSG = data.fontSizeMSG or (30 / uiState.uiScale),
  805.         menuPos = menuPos,
  806.         unit = data.unit or UNIT,
  807.         unitMult = data.unitMult or UNIT_MULT,
  808.         starsSize = data.starsSize or 20,
  809.         starsPos = starsPos,
  810.         leaderboardWrapWidth = (data.fontSize or DEFAULT_SETTINGS.fontSize) / 1.5,
  811.     }
  812.     setmetatable(settings, { __index = Settings })
  813.     return settings
  814. end
  815.  
  816. ---@param url string
  817. ---@param callback function
  818. function Settings.fetch(url, callback)
  819.     if localTesting then
  820.         local currentPath = ac.getFolder(ac.FolderID.ScriptOrigin)
  821.         local file = io.open(currentPath .. '/response/settingsResponse.json', 'r')
  822.         if not file then
  823.             ac.error('Failed to open settingsResponse.json')
  824.             callback(Settings.new())
  825.             return
  826.         end
  827.         local data = JSON.parse(file:read('*a'))
  828.         file:close()
  829.         local settings = Settings.tryParse(data)
  830.         callback(settings)
  831.     else
  832.         web.get(url, function(err, response)
  833.             if canProcessRequest(err, response) then
  834.                 if hasExistingData(response) then
  835.                     local data = JSON.parse(response.body)
  836.                     if data then
  837.                         local settings = Settings.tryParse(data)
  838.                         callback(settings)
  839.                     else
  840.                         ac.error('Failed to parse settings data.')
  841.                         callback(Settings.new())
  842.                     end
  843.                 else
  844.                     callback(Settings.new())
  845.                 end
  846.             else
  847.                 ac.error('Failed to fetch settings:', err)
  848.                 callback(Settings.new())
  849.             end
  850.         end)
  851.     end
  852. end
  853.  
  854. ---@param callback function
  855. function Settings.allocate(callback)
  856.     local url = FIREBASE_URL .. 'Settings/' .. STEAMID .. '.json'
  857.     Settings.fetch(url, function(settings)
  858.         callback(settings)
  859.     end)
  860. end
  861.  
  862. ---@return table
  863. function Settings:export()
  864.     local data = {}
  865.     if self.essentialSize ~= DEFAULT_SETTINGS.essentialSize then
  866.         data.essentialSize = self.essentialSize
  867.     end
  868.     if self.policeSize ~= DEFAULT_SETTINGS.policeSize then
  869.         data.policeSize = self.policeSize
  870.     end
  871.     if self.hudOffset ~= DEFAULT_SETTINGS.hudOffset then
  872.         data.hudOffset = { self.hudOffset.x, self.hudOffset.y }
  873.     end
  874.     if self.fontSize ~= DEFAULT_SETTINGS.fontSize then
  875.         data.fontSize = self.fontSize
  876.     end
  877.     if self.current ~= DEFAULT_SETTINGS.current then
  878.         data.current = self.current
  879.     end
  880.     if self.colorHud ~= DEFAULT_SETTINGS.colorHud then
  881.         data.colorHud = { self.colorHud.r, self.colorHud.g, self.colorHud.b, self.colorHud.mult }
  882.     end
  883.     if self.timeMsg ~= DEFAULT_SETTINGS.timeMsg then
  884.         data.timeMsg = self.timeMsg
  885.     end
  886.     if self.msgOffset ~= DEFAULT_SETTINGS.msgOffset then
  887.         data.msgOffset = { self.msgOffset.x, self.msgOffset.y }
  888.     end
  889.     if self.fontSizeMSG ~= DEFAULT_SETTINGS.fontSizeMSG then
  890.         data.fontSizeMSG = self.fontSizeMSG
  891.     end
  892.     if self.menuPos ~= DEFAULT_SETTINGS.menuPos then
  893.         data.menuPos = { self.menuPos.x, self.menuPos.y }
  894.     end
  895.     if self.unit ~= DEFAULT_SETTINGS.unit then
  896.         data.unit = self.unit
  897.     end
  898.     if self.unitMult ~= DEFAULT_SETTINGS.unitMult then
  899.         data.unitMult = self.unitMult
  900.     end
  901.     if self.starsSize ~= DEFAULT_SETTINGS.starsSize then
  902.         data.starsSize = self.starsSize
  903.     end
  904.     if self.starsPos ~= DEFAULT_SETTINGS.starsPos then
  905.         data.starsPos = { self.starsPos.x, self.starsPos.y }
  906.     end
  907.     return data
  908. end
  909.  
  910. function Settings:save()
  911.     if localTesting or patchCount > 20 then return end
  912.     patchCount = patchCount + 1
  913.     local str = '{"' .. STEAMID .. '": ' .. JSON.stringify(self:export()) .. '}'
  914.     web.request('PATCH', FIREBASE_URL .. "Settings.json", str, function(err, response)
  915.         if err then
  916.             ac.error(err)
  917.             return
  918.         end
  919.     end)
  920. end
  921.  
  922. ---@class Gate
  923. ---@field pos vec3
  924. ---@field dir vec3
  925. ---@field point1 vec3
  926. ---@field point2 vec3
  927. ---@field width number
  928. ---@field cross vec3
  929. ---@field id integer
  930. local Gate = class('Gate')
  931.  
  932. ---@param data table
  933. ---@return Gate|nil
  934. function Gate.tryParse(data)
  935.     local keys = { 'pos', 'dir', 'width', 'id' }
  936.     if not hasKeys(keys, data) then
  937.         ac.log('Missing required keys in gate data.')
  938.         return nil
  939.     end
  940.  
  941.     local pos = tableToVec3(data.pos)
  942.     local dir = tableToVec3(data.dir)
  943.     local cross = vec3(dir.z, 0, -dir.x)
  944.     local point1 = pos + cross * data.width / 2
  945.     local point2 = pos - cross * data.width / 2
  946.     return {
  947.         pos = pos,
  948.         dir = dir,
  949.         cross = cross,
  950.         point1 = snapToTrack(point1) + vec3(0, GATE_HEIGHT_OFFSET, 0),
  951.         point2 = snapToTrack(point2) + vec3(0, GATE_HEIGHT_OFFSET, 0),
  952.         width = data.width,
  953.         id = data.id,
  954.     }
  955. end
  956.  
  957. ---@param data table
  958. ---@return Gate|nil
  959. function Gate.allocate(data)
  960.     local gate = Gate.tryParse(data)
  961.     if not gate then
  962.         ac.error('Failed to allocate gate')
  963.         return nil
  964.     end
  965.     return gate
  966. end
  967.  
  968. function Gate:print()
  969.     ac.error('Gate:\npos:', self.pos, 'dir:', self.dir)
  970. end
  971.  
  972. ---@return boolean
  973. function Gate:isTooFar()
  974.     return self.pos:distanceSquared(car.position) > self.width * 3
  975. end
  976.  
  977. ---@return boolean
  978. function Gate:isCrossed()
  979.     if self:isTooFar() then
  980.         return false
  981.     end
  982.     local carHalfWidth = car.aabbSize.z / 2
  983.  
  984.     local isCrossing = vec2.intersect(vec2(self.point1.x, self.point1.z), vec2(self.point2.x, self.point2.z),
  985.         vec2(car.position.x - carHalfWidth, car.position.z - carHalfWidth),
  986.         vec2(car.position.x + carHalfWidth, car.position.z + carHalfWidth))
  987.     local goingThrough = self.dir:dot(car.look) > 0
  988.     if isCrossing and goingThrough then
  989.         return true
  990.     end
  991.     return false
  992. end
  993.  
  994. ---@class Sector
  995. ---@field name string
  996. ---@field startTime number
  997. ---@field time string
  998. ---@field timeLimit number
  999. ---@field addTimeLimit number[]
  1000. ---@field timeColor rgbm
  1001. ---@field finalTime number
  1002. ---@field startDistance number
  1003. ---@field lenght number
  1004. ---@field gateCount integer
  1005. ---@field gateIndex integer
  1006. ---@field gates Gate[]
  1007. local Sector = class('Sector')
  1008.  
  1009. ---@param data table
  1010. ---@return Sector|nil
  1011. function Sector.tryParse(data)
  1012.     local keys = { 'timeLimit', 'length', 'gates' }
  1013.     if not hasKeys(keys, data) then
  1014.         ac.error('Missing required keys in sector data.')
  1015.         return nil
  1016.     end
  1017.     local gates = {}
  1018.     for i, gateData in ipairs(data.gates) do
  1019.         local gate = Gate(gateData)
  1020.         if not gate then
  1021.             ac.error('Failed to parse gate:', i)
  1022.             return nil
  1023.         end
  1024.         table.insert(gates, gate)
  1025.     end
  1026.  
  1027.     local sector = {
  1028.         name = data.name,
  1029.         gateCount = #gates,
  1030.         gateIndex = 1,
  1031.         startTime = 0,
  1032.         time = 'Time 00:00.000',
  1033.         timeLimit = data.timeLimit,
  1034.         addTimeLimit = data.addTimeLimit,
  1035.         timeColor = white,
  1036.         startDistance = 0,
  1037.         lenght = data.length,
  1038.         gates = gates,
  1039.     }
  1040.     setmetatable(sector, { __index = Sector })
  1041.     return sector
  1042. end
  1043.  
  1044. ---@param url string
  1045. ---@param callback function
  1046. function Sector.fetch(url, callback)
  1047.     if localTesting then
  1048.         local currentPath = ac.getFolder(ac.FolderID.ScriptOrigin)
  1049.         local filename = url:match('.+/(.+)$')
  1050.         local file = io.open(currentPath .. '/response/sector' .. filename, 'r')
  1051.         if not file then
  1052.             ac.error('Failed to open response.json')
  1053.             callback(nil)
  1054.             return
  1055.         end
  1056.         local data = JSON.parse(file:read('*a'))
  1057.         file:close()
  1058.         local sector = Sector.tryParse(data)
  1059.         callback(sector)
  1060.     else
  1061.         web.get(url, function(err, response)
  1062.             if canProcessRequest(err, response) then
  1063.                 local data = JSON.parse(response.body)
  1064.                 if data then
  1065.                     local sector = Sector.tryParse(data)
  1066.                     callback(sector)
  1067.                 else
  1068.                     ac.error('Failed to parse sector data.')
  1069.                     callback(nil)
  1070.                 end
  1071.             else
  1072.                 callback(nil)
  1073.             end
  1074.         end)
  1075.     end
  1076. end
  1077.  
  1078. ---@param name string
  1079. ---@param callback function
  1080. function Sector.allocate(name, callback)
  1081.     local url = FIREBASE_URL .. 'Sectors/' .. name .. '.json'
  1082.     Sector.fetch(url, function(sector)
  1083.         if not sector then
  1084.             ac.error('Failed to allocate sector:', name)
  1085.         else
  1086.             sector.name = name
  1087.         end
  1088.         callback(sector)
  1089.     end)
  1090. end
  1091.  
  1092. function Sector:reset()
  1093.     self.gateIndex = 1
  1094.     self.startTime = 0
  1095.     self.time = 'Time - 00:00.000'
  1096.     if self.timeLimit > 0 then
  1097.         self.time = 'lvl 3 - ' .. formatTime(self.timeLimit + self.addTimeLimit[3], '')
  1098.     end
  1099.     self.timeColor = white
  1100.     self.startDistance = 0
  1101.     self.finalTime = 0
  1102. end
  1103.  
  1104. function Sector:starting()
  1105.     if self.gateIndex == 2 then
  1106.         self.time = 'Time - 00:00.000'
  1107.         if self.timeLimit > 0 then
  1108.             self.time = 'lvl 3 - ' .. formatTime(self.timeLimit + self.addTimeLimit[3], '')
  1109.         end
  1110.         self.startTime = os.preciseClock()
  1111.         self.startDistance = car.distanceDrivenTotalKm
  1112.     end
  1113. end
  1114.  
  1115. ---@return boolean
  1116. function Sector:isFinished()
  1117.     return self.gateIndex > self.gateCount and car.distanceDrivenTotalKm - self.startDistance > self.lenght
  1118. end
  1119.  
  1120. ---@return boolean
  1121. function Sector:hasStarted()
  1122.     return self.startTime > 0
  1123. end
  1124.  
  1125. function Sector:updateTime()
  1126.     if self.startTime > 0 then
  1127.         local time = os.preciseClock() - self.startTime
  1128.         local lvl = 'Time'
  1129.         if self.timeLimit ~= 0 then
  1130.             time = self.timeLimit + self.addTimeLimit[3] - time
  1131.             lvl = 'lvl' .. missionManager.level
  1132.             if time < 0 then
  1133.                 time = 0
  1134.                 lvl = 'FAIL'
  1135.             end
  1136.         end
  1137.         local minutes = math.floor(time / 60)
  1138.         local seconds = math.floor(time % 60)
  1139.         local milliseconds = math.floor((time % 1) * 1000)
  1140.         self.time = lvl .. (' - %02d:%02d.%03d'):format(minutes, seconds, milliseconds)
  1141.     end
  1142. end
  1143.  
  1144. ---@return integer
  1145. function Sector:isUnderTimeLimit()
  1146.     if self.timeLimit > 0 then
  1147.         local time = os.preciseClock() - self.startTime
  1148.         if time < self.timeLimit + self.addTimeLimit[1] then
  1149.             return 3
  1150.         elseif time < self.timeLimit + self.addTimeLimit[2] then
  1151.             return 2
  1152.         elseif time < self.timeLimit + self.addTimeLimit[3] then
  1153.             return 1
  1154.         end
  1155.         missionManager.level = 0
  1156.         return 0
  1157.     end
  1158.     return 1
  1159. end
  1160.  
  1161. function Sector:updateTimeColor()
  1162.     if self:hasStarted() then
  1163.         local underTimeLimit = self:isUnderTimeLimit()
  1164.         if underTimeLimit ~= missionManager.level then
  1165.             missionManager.level = underTimeLimit
  1166.             missionManager.msgTime = 20
  1167.         end
  1168.         if underTimeLimit == 3 or self.timeLimit == 0 then
  1169.             if self:isFinished() then
  1170.                 self.timeColor = rgbm.colors.green
  1171.             else
  1172.                 self.timeColor = rgbm.colors.white
  1173.             end
  1174.         elseif underTimeLimit == 2 then
  1175.             self.timeColor = rgbm.colors.yellow
  1176.         elseif underTimeLimit == 1 then
  1177.             self.timeColor = rgbm.colors.orange
  1178.         else
  1179.             self.timeColor = rgbm.colors.red
  1180.         end
  1181.     end
  1182. end
  1183.  
  1184. function Sector:update()
  1185.     self:updateTime()
  1186.     self:updateTimeColor()
  1187.     if self.gateIndex > self.gateCount then
  1188.         return
  1189.     end
  1190.     if self.gates[self.gateIndex]:isCrossed() then
  1191.         self.gateIndex = self.gateIndex + 1
  1192.         self:starting()
  1193.         self:updateTimeColor()
  1194.         if self:isFinished() then
  1195.             local finalTime = os.preciseClock() - self.startTime
  1196.             if finalTime < 1 then return end
  1197.             self.finalTime = finalTime
  1198.             local time = finalTime
  1199.             local lvl = 'Time'
  1200.             if self.timeLimit ~= 0 then
  1201.                 time = self.timeLimit + self.addTimeLimit[3] - time
  1202.                 lvl = 'lvl' .. missionManager.level
  1203.                 if time < 0 then
  1204.                     time = 0
  1205.                     lvl = 'FAIL'
  1206.                 end
  1207.             end
  1208.             self.time = lvl .. (' - %02d:%02d.%03d'):format(math.floor(self.finalTime / 60), math.floor(self.finalTime % 60),
  1209.                 math.floor((self.finalTime % 1) * 1000))
  1210.         end
  1211.     end
  1212. end
  1213.  
  1214. ---@class SectorStats
  1215. ---@field name string
  1216. ---@field records table<string, number>
  1217. local SectorStats = class('SectorStats')
  1218.  
  1219. ---@param name string
  1220. ---@param data table
  1221. ---@return SectorStats
  1222. function SectorStats.tryParse(name, data)
  1223.     local records = {}
  1224.     for carName, time in pairs(data) do
  1225.         local nameWithoutUtf8 = removeUtf8Char(carName)
  1226.         records[nameWithoutUtf8] = time
  1227.     end
  1228.     local sectorStats = {
  1229.         name = name,
  1230.         records = records,
  1231.     }
  1232.     setmetatable(sectorStats, { __index = SectorStats })
  1233.     return sectorStats
  1234. end
  1235.  
  1236. ---@param name string
  1237. ---@param data table
  1238. ---@return SectorStats|nil
  1239. function SectorStats.allocate(name, data)
  1240.     if type(data) == 'table' then
  1241.         local sectorStats = SectorStats.tryParse(name, data)
  1242.         if not sectorStats then
  1243.             ac.error('Failed to allocate sector stat')
  1244.             return nil
  1245.         end
  1246.         return sectorStats
  1247.     end
  1248.     if type(data) == 'number' then
  1249.         local records = {}
  1250.         records[CAR_NAME_NO_UTF8] = data
  1251.         local sectorStats = {
  1252.             name = name,
  1253.             records = records,
  1254.         }
  1255.         setmetatable(sectorStats, { __index = SectorStats })
  1256.         return sectorStats
  1257.     end
  1258.     ac.error('Failed to allocate sector stat')
  1259.     return nil
  1260. end
  1261.  
  1262. ---@param time number
  1263. ---@return boolean
  1264. function SectorStats:addRecord(time)
  1265.     if not self.records[CAR_NAME_NO_UTF8] or self.records[CAR_NAME_NO_UTF8] > time then
  1266.         self.records[CAR_NAME_NO_UTF8] = time
  1267.         return true
  1268.     end
  1269.     return false
  1270. end
  1271.  
  1272. ---@return table
  1273. function SectorStats:export()
  1274.     local records = {}
  1275.     for carName, time in pairs(self.records) do
  1276.         records[carName] = truncate(time, 3)
  1277.     end
  1278.     return {
  1279.         [self.name] = records
  1280.     }
  1281. end
  1282.  
  1283. local lastRegister = {
  1284.     kms = 0,
  1285.     time = os.clock(),
  1286. }
  1287.  
  1288. ---@class Player
  1289. ---@field name string
  1290. ---@field sectors SectorStats[]
  1291. ---@field sectorsFormated table<string, table<string, string>>
  1292. ---@field arrests integer
  1293. ---@field getaways integer
  1294. ---@field thefts integer
  1295. ---@field heists integer
  1296. ---@field deliveries integer
  1297. ---@field overtake integer
  1298. ---@field wins integer
  1299. ---@field losses integer
  1300. ---@field elo integer
  1301. ---@field kms number
  1302. ---@field time number
  1303. ---@field timePlayed string
  1304. local Player = class('Player')
  1305.  
  1306. ---@type Player | nil
  1307. local player = nil
  1308.  
  1309. local sharedPlayerLayout = {
  1310.     ac.StructItem.key(SHARED_PLAYER_DATA),
  1311.     hudColor = ac.StructItem.rgbm(),
  1312.     name = ac.StructItem.string(24),
  1313.     sectorsFormated = ac.StructItem.array(ac.StructItem.struct({
  1314.         name = ac.StructItem.string(16),
  1315.         records = ac.StructItem.array(ac.StructItem.string(50), 20)
  1316.     }), 5),
  1317.     arrests = ac.StructItem.uint16(),
  1318.     getaways = ac.StructItem.uint16(),
  1319.     thefts = ac.StructItem.uint16(),
  1320.     heists = ac.StructItem.uint16(),
  1321.     deliveries = ac.StructItem.uint16(),
  1322.     overtake = ac.StructItem.uint32(),
  1323.     wins = ac.StructItem.uint16(),
  1324.     losses = ac.StructItem.uint16(),
  1325.     elo = ac.StructItem.uint16(),
  1326.     kms = ac.StructItem.float(),
  1327.     time = ac.StructItem.float(),
  1328. }
  1329.  
  1330. ---@type Settings | nil
  1331. local settings = nil
  1332.  
  1333. local sharedPlayerData = ac.connect(sharedPlayerLayout, true, ac.SharedNamespace.ServerScript)
  1334.  
  1335. local function updateSharedPlayerData()
  1336.     if not player then return end
  1337.     local hudC = rgbm.colors.red
  1338.     if settings then
  1339.         hudC = settings.colorHud
  1340.     end
  1341.     sharedPlayerData.hudColor = hudC
  1342.     sharedPlayerData.name = player.name
  1343.     sharedPlayerData.arrests = player.arrests
  1344.     sharedPlayerData.getaways = player.getaways
  1345.     sharedPlayerData.thefts = player.thefts
  1346.     sharedPlayerData.heists = player.heists
  1347.     sharedPlayerData.deliveries = player.deliveries
  1348.     sharedPlayerData.overtake = player.overtake
  1349.     sharedPlayerData.wins = player.wins
  1350.     sharedPlayerData.losses = player.losses
  1351.     sharedPlayerData.elo = player.elo
  1352.     sharedPlayerData.kms = player.kms
  1353.     sharedPlayerData.time = player.time
  1354.     sharedPlayerData.sectorsFormated = {}
  1355.     local i = 1
  1356.     table.forEach(player.sectorsFormated, function(v, k)
  1357.         sharedPlayerData.sectorsFormated[i].name = k .. '\0'
  1358.         for j, entry in ipairs(v) do
  1359.             local carName = string.sub(entry[1], 1, 45)
  1360.             sharedPlayerData.sectorsFormated[i].records[j] = carName .. ' - ' .. entry[2] .. '\0'
  1361.         end
  1362.         i = i + 1
  1363.     end)
  1364. end
  1365.  
  1366. ---@return Player
  1367. function Player.new()
  1368.     local _player = {
  1369.         name = DRIVER_NAME,
  1370.         sectors = {},
  1371.         sectorsFormated = {},
  1372.         arrests = 0,
  1373.         getaways = 0,
  1374.         thefts = 0,
  1375.         heists = 0,
  1376.         deliveries = 0,
  1377.         overtake = 0,
  1378.         wins = 0,
  1379.         losses = 0,
  1380.         elo = 1200,
  1381.         kms = 0,
  1382.         time = 0,
  1383.         timePlayed = formatTime(0, 'time played'),
  1384.     }
  1385.     setmetatable(_player, { __index = Player })
  1386.     return _player
  1387. end
  1388.  
  1389. ---@param data table
  1390. ---@return Player
  1391. function Player.tryParse(data)
  1392.     if not data then
  1393.         return Player.new()
  1394.     end
  1395.     local sectors = {}
  1396.     if data.sectors then
  1397.         for sectorName, sectorData in pairs(data.sectors) do
  1398.             local sector = SectorStats.allocate(sectorName, sectorData)
  1399.             if sector then
  1400.                 table.insert(sectors, sector)
  1401.             end
  1402.         end
  1403.     end
  1404.     local _player = {
  1405.         name = DRIVER_NAME,
  1406.         sectors = sectors,
  1407.         sectorsFormated = {},
  1408.         arrests = data.arrests or 0,
  1409.         getaways = data.getaways or 0,
  1410.         thefts = data.thefts or 0,
  1411.         heists = data.heists or 0,
  1412.         deliveries = data.deliveries or 0,
  1413.         overtake = data.overtake or 0,
  1414.         wins = data.wins or 0,
  1415.         losses = data.losses or 0,
  1416.         elo = data.elo or 1200,
  1417.         kms = data.kms or 0,
  1418.         time = data.time or 0,
  1419.         timePlayed = formatTime(data.time or 0, 'time played'),
  1420.     }
  1421.     setmetatable(_player, { __index = Player })
  1422.     return _player
  1423. end
  1424.  
  1425. ---@param url string
  1426. ---@param callback function
  1427. function Player.fetch(url, callback)
  1428.     if localTesting then
  1429.         local currentPath = ac.getFolder(ac.FolderID.ScriptOrigin)
  1430.         local file = io.open(currentPath .. '/response/playerResponse.json', 'r')
  1431.         if not file then
  1432.             ac.error('Failed to open playerResponse.json')
  1433.             callback(Player.new())
  1434.             return
  1435.         end
  1436.         local data = JSON.parse(file:read('*a'))
  1437.         file:close()
  1438.         local _player = Player.tryParse(data)
  1439.         callback(_player)
  1440.     else
  1441.         web.get(url, function(err, response)
  1442.             if canProcessRequest(err, response) then
  1443.                 if hasExistingData(response) then
  1444.                     local data = JSON.parse(response.body)
  1445.                     if data then
  1446.                         local _player = Player.tryParse(data)
  1447.                         callback(_player)
  1448.                     else
  1449.                         ac.error('Failed to parse player data.')
  1450.                         callback(Player.new())
  1451.                     end
  1452.                 else
  1453.                     callback(Player.new())
  1454.                 end
  1455.             else
  1456.                 ac.error('Failed to fetch player:', err)
  1457.                 callback(Player.new())
  1458.             end
  1459.         end)
  1460.     end
  1461. end
  1462.  
  1463. ---@param callback function
  1464. function Player.allocate(callback)
  1465.     local url = FIREBASE_URL .. 'Players/' .. STEAMID .. '.json'
  1466.     Player.fetch(url, function(_player)
  1467.         callback(_player)
  1468.     end)
  1469. end
  1470.  
  1471. function Player:sortSectors()
  1472.     for _, sector in ipairs(self.sectors) do
  1473.         local entries = {}
  1474.         for carName, time in pairs(sector.records) do
  1475.             table.insert(entries, { carName, time })
  1476.         end
  1477.         table.sort(entries, function(a, b)
  1478.             return a[2] < b[2]
  1479.         end)
  1480.         for i, entry in ipairs(entries) do
  1481.             entries[i][2] = formatTime(entry[2], '')
  1482.         end
  1483.         self.sectorsFormated[sector.name] = entries
  1484.     end
  1485. end
  1486.  
  1487. ---@return table
  1488. function Player:export()
  1489.     local kms = truncate(car.distanceDrivenSessionKm - lastRegister.kms + self.kms, 3)
  1490.     local time = math.round(os.clock() - lastRegister.time + self.time, 0)
  1491.     local data = { name = self.name }
  1492.  
  1493.     if self.arrests > 0 then
  1494.         data.arrests = self.arrests
  1495.     end
  1496.     if self.getaways > 0 then
  1497.         data.getaways = self.getaways
  1498.     end
  1499.     if self.thefts > 0 then
  1500.         data.thefts = self.thefts
  1501.     end
  1502.     if self.heists > 0 then
  1503.         data.heists = self.heists
  1504.     end
  1505.     if self.deliveries > 0 then
  1506.         data.deliveries = self.deliveries
  1507.     end
  1508.     if self.overtake > 0 then
  1509.         data.overtake = self.overtake
  1510.     end
  1511.     if self.wins > 0 then
  1512.         data.wins = self.wins
  1513.     end
  1514.     if self.losses > 0 then
  1515.         data.losses = self.losses
  1516.     end
  1517.     if self.elo ~= 1200 then
  1518.         data.elo = self.elo
  1519.     end
  1520.     if kms > 0 then
  1521.         data.kms = kms
  1522.     end
  1523.     if time > 0 then
  1524.         data.time = time
  1525.     end
  1526.  
  1527.     lastRegister.kms = car.distanceDrivenSessionKm
  1528.     lastRegister.time = os.clock()
  1529.  
  1530.     local sectors = {}
  1531.     for _, sector in ipairs(self.sectors) do
  1532.         if not sector then
  1533.             break
  1534.         end
  1535.         local sectorData = sector:export()
  1536.         for k, v in pairs(sectorData) do
  1537.             sectors[k] = v
  1538.         end
  1539.     end
  1540.     if next(sectors) then
  1541.         data.sectors = sectors
  1542.     end
  1543.     self:sortSectors()
  1544.     updateSharedPlayerData()
  1545.     ac.broadcastSharedEvent(SHARED_EVENT_KEY, 'update')
  1546.     return data
  1547. end
  1548.  
  1549. function Player:save()
  1550.     local str = '{"' .. STEAMID .. '": ' .. JSON.stringify(self:export()) .. '}'
  1551.     if localTesting or patchCount > 40 then return end
  1552.     patchCount = patchCount + 1
  1553.     web.request('PATCH', FIREBASE_URL .. "Players.json", str, function(err, response)
  1554.         if err then
  1555.             ac.error(err)
  1556.             return
  1557.         end
  1558.     end)
  1559. end
  1560.  
  1561. ---@param sectorName string
  1562. ---@param time number
  1563. ---@return boolean
  1564. function Player:addSectorRecord(sectorName, time)
  1565.     ---@type SectorStats | nil
  1566.     local sector = nil
  1567.     for _, s in ipairs(self.sectors) do
  1568.         if s.name == sectorName then
  1569.             sector = s
  1570.             break
  1571.         end
  1572.     end
  1573.     if not sector then
  1574.         sector = SectorStats.allocate(sectorName, time)
  1575.         if not sector then return false end
  1576.         table.insert(self.sectors, sector)
  1577.         return true
  1578.     end
  1579.     return sector:addRecord(time)
  1580. end
  1581.  
  1582. ---@type Sector[]
  1583. local sectors = {}
  1584.  
  1585. local function getSectorByName(name)
  1586.     for _, sector in ipairs(sectors) do
  1587.         if sector.name == name then
  1588.             return sector
  1589.         end
  1590.     end
  1591.     return nil
  1592. end
  1593.  
  1594. ---@class SectorManager
  1595. ---@field sector Sector
  1596. ---@field started boolean
  1597. ---@field finished boolean
  1598. local SectorManager = class('SectorManager')
  1599.  
  1600. ---@return SectorManager
  1601. function SectorManager.new()
  1602.     local sm = {
  1603.         sector = nil,
  1604.         started = false,
  1605.         finished = false,
  1606.     }
  1607.     setmetatable(sm, { __index = SectorManager })
  1608.     return sm
  1609. end
  1610.  
  1611. ---@return SectorManager
  1612. function SectorManager.allocate()
  1613.     return SectorManager.new()
  1614. end
  1615.  
  1616. function SectorManager:reset()
  1617.     duo.teammateHasFinished = false
  1618.     duo.sentFinish = false
  1619.     duo.waiting = false
  1620.     duo.request = false
  1621.     duo.onlineSender = nil
  1622.     self.started = false
  1623.     self.finished = false
  1624.     self.sector:reset()
  1625. end
  1626.  
  1627. ---@param name string
  1628. function SectorManager:setSector(name)
  1629.     local sector = getSectorByName(name)
  1630.     if sector then
  1631.         self.sector = sector
  1632.         self:reset()
  1633.     end
  1634. end
  1635.  
  1636. ---@type SectorManager
  1637. local sectorManager = SectorManager()
  1638.  
  1639. local acpEvent = ac.OnlineEvent({
  1640.     message = ac.StructItem.string(110),
  1641.     messageType = ac.StructItem.int16(),
  1642.     yourIndex = ac.StructItem.int16(),
  1643. }, function(sender, data)
  1644.     if not sender then return end
  1645.     if data.yourIndex == car.sessionID and data.messageType == 5 and data.message == "Request" then
  1646.         duo.request = true
  1647.         duo.onlineSender = sender
  1648.     elseif data.yourIndex == car.sessionID and data.messageType == 5 and data.message == "Accept" then
  1649.         duo.teammate = sender
  1650.         duo.request = false
  1651.     elseif duo.teammate and data.yourIndex == car.sessionID and sender.index == duo.teammate.index and data.messageType == 5 and data.message == "Finished" then
  1652.         duo.teammateHasFinished = true
  1653.     elseif duo.teammate and data.yourIndex == car.sessionID and sender.index == duo.teammate.index and data.messageType == 5 and data.message == "Cancel" then
  1654.         duo.teammate = nil
  1655.         duo.request = false
  1656.         sectorManager:setSector('BOBs SCRAPYARD')
  1657.     end
  1658. end)
  1659.  
  1660. function SectorManager:resetDuo()
  1661.     duo.teammate = nil
  1662.     duo.request = false
  1663.     duo.onlineSender = nil
  1664.     duo.teammateHasFinished = false
  1665.     duo.waiting = false
  1666. end
  1667.  
  1668. function SectorManager:hasTeammateFinished()
  1669.     if duo.teammate and duo.teammateHasFinished then
  1670.         if not duo.sentFinish then
  1671.             acpEvent{message = "Finished", messageType = 5, yourIndex = ac.getCar(duo.teammate.index).sessionID}
  1672.             duo.sentFinish = true
  1673.         end
  1674.         return true
  1675.     end
  1676.     return false
  1677. end
  1678.  
  1679. local canRun = false
  1680. local function shouldRun()
  1681.     if canRun then return true end
  1682.     local isDataLoaded = dataLoaded['Settings'] and dataLoaded['PlayerData'] and dataLoaded['Sectors']
  1683.     local hasNecessaryData = settings and player and sectors and sectorManager.sector
  1684.     local hasMinVersion = CSP_VERSION >= CSP_MIN_VERSION
  1685.     if isDataLoaded and hasMinVersion and hasNecessaryData and not isPoliceCar(CAR_ID) then
  1686.         canRun = true
  1687.     end
  1688.     return canRun
  1689. end
  1690.  
  1691. local hud = {
  1692.     size = vec2(0, 0),
  1693.     pos = {
  1694.         countdown1 = vec2(0, 0),
  1695.         countdown2 = vec2(0, 0),
  1696.         menu1 = vec2(0, 0),
  1697.         menu2 = vec2(0, 0),
  1698.         ranks1 = vec2(0, 0),
  1699.         ranks2 = vec2(0, 0),
  1700.         theft1 = vec2(0, 0),
  1701.         theft2 = vec2(0, 0),
  1702.         left1 = vec2(0, 0),
  1703.         left2 = vec2(0, 0),
  1704.         right1 = vec2(0, 0),
  1705.         right2 = vec2(0, 0),
  1706.     },
  1707. }
  1708.  
  1709. ----------------------------------------------------------------------------------------------- Math -----------------------------------------------------------------------------------------------
  1710.  
  1711. local function calculateElo(opponentElo, youWon)
  1712.     local k = 32
  1713.     local expectedScore = 1 / (1 + 10 ^ ((opponentElo - player.elo) / 400))
  1714.     local score = youWon and 1 or 0
  1715.     local newElo = player.elo + k * (score - expectedScore)
  1716.     return math.floor(newElo)
  1717. end
  1718.  
  1719. local function cross(vector1, vector2)
  1720.     return vec2(vector1.x + vector2.x, vector1.y + vector2.y)
  1721. end
  1722.  
  1723. local function isPointInCircle(point, circle, radius)
  1724.     if math.distanceSquared(point, circle) <= radius then
  1725.         return true
  1726.     end
  1727.     return false
  1728. end
  1729.  
  1730. -------------------------------------------------------------------------------------------- Init --------------------------------------------------------------------------------------------
  1731.  
  1732. local starsUI = {
  1733.     starsPos = vec2(0, 0),
  1734.     starsSize = vec2(0, 0),
  1735.     startSpace = 0,
  1736.     full = "https://acstuff.ru/images/icons_24/star_full.png",
  1737.     empty = "https://acstuff.ru/images/icons_24/star_empty.png",
  1738. }
  1739.  
  1740. local function updateStarsPos()
  1741.     starsUI.starsPos = vec2(settings.starsPos.x - settings.starsSize / 2, settings.starsPos.y + settings.starsSize / 2)
  1742.     starsUI.starsSize = vec2(settings.starsPos.x - settings.starsSize * 2, settings.starsPos.y + settings.starsSize * 2)
  1743.     starsUI.startSpace = settings.starsSize / 1.5
  1744. end
  1745.  
  1746. local function updateHudPos()
  1747.     hud.size = vec2(HEIGHT_DIV._80 * settings.essentialSize, HEIGHT_DIV._80 * settings.essentialSize)
  1748.     hud.pos.theftPos1 = vec2(hud.size.x - hud.size.x / 1.56, hud.size.y / 1.9)
  1749.     hud.pos.theftPos2 = vec2(hud.size.x / 4.6, hud.size.y / 2.65)
  1750.     hud.pos.ranksPos1 = vec2(hud.size.x / 1.97, hud.size.y / 1.9)
  1751.     hud.pos.ranksPos2 = vec2(hud.size.x - hud.size.x / 1.56, hud.size.y / 2.65)
  1752.     hud.pos.countdownPos1 = vec2(hud.size.x / 1.53, hud.size.y / 1.9)
  1753.     hud.pos.countdownPos2 = vec2(hud.size.x - hud.size.x / 2.04, hud.size.y / 2.65)
  1754.     hud.pos.menuPos1 = vec2(hud.size.x - hud.size.x / 4.9, hud.size.y / 1.9)
  1755.     hud.pos.menuPos2 = vec2(hud.size.x / 1.53, hud.size.y / 2.65)
  1756.     hud.pos.leftPos1 = vec2(hud.size.x / 8, hud.size.y / 2.8)
  1757.     hud.pos.leftPos2 = vec2(0, hud.size.y / 4.3)
  1758.     hud.pos.rightPos1 = vec2(hud.size.x, hud.size.y / 2.8)
  1759.     hud.pos.rightPos2 = vec2(hud.size.x - hud.size.x / 8, hud.size.y / 4.3)
  1760.     settings.fontSize = settings.essentialSize * FONT_MULT
  1761. end
  1762.  
  1763. local function textWithBackground(text, sizeMult, height)
  1764.     ui.pushDWriteFont("Orbitron")
  1765.     local textSize = ui.measureDWriteText(text, settings.fontSizeMSG * sizeMult)
  1766.     local rectPos1 = settings.msgOffset - vec2(textSize.x / 2, 0)
  1767.     local rectPos2 = textSize + rectPos1
  1768.     local rectOffset = vec2(WIDTH_DIV._320, HEIGHT_DIV._320)
  1769.     if ui.time() % 1 < 0.5 then
  1770.         ui.drawRectFilled(rectPos1 - rectOffset, rectPos2 + rectOffset, COLOR_MSG_BG, 10)
  1771.     else
  1772.         ui.drawRectFilled(rectPos1 - rectOffset, rectPos2 + rectOffset, rgbm(0, 0, 0, 0.5), 10)
  1773.     end
  1774.     ui.dwriteDrawText(text, settings.fontSizeMSG * sizeMult, rectPos1, white)
  1775.     ui.popDWriteFont()
  1776. end
  1777.  
  1778. local function displayInGrid()
  1779.     local box1 = vec2(WIDTH_DIV._32, HEIGHT_DIV._70)
  1780.     local colWidth = (menuSize[currentTab].x - WIDTH_DIV._32) / currentLeaderboard.nbCols
  1781.     ui.pushDWriteFont("Orbitron;Weight=Black")
  1782.     ui.newLine()
  1783.     ui.dwriteTextWrapped("Pos", settings.leaderboardWrapWidth, settings.colorHud)
  1784.     for i = 1, #currentLeaderboard.header do
  1785.         local textLenght = ui.measureDWriteText(currentLeaderboard.header[i], settings.leaderboardWrapWidth).x
  1786.         ui.sameLine(box1.x + colWidth / 2 + colWidth * (i - 1) - textLenght / 2)
  1787.         ui.dwriteTextWrapped(currentLeaderboard.header[i], settings.leaderboardWrapWidth, settings.colorHud)
  1788.     end
  1789.     local linePos = ui.getMaxCursorY() + HEIGHT_DIV._100
  1790.     ui.drawSimpleLine(vec2(0, linePos), vec2(menuSize[currentTab].x, linePos), white, 2)
  1791.     ui.newLine()
  1792.     ui.popDWriteFont()
  1793.     ui.pushDWriteFont("Orbitron;Weight=Regular")
  1794.     for i = 1, #currentLeaderboard.rows do
  1795.         ui.dwriteTextWrapped(i, settings.leaderboardWrapWidth, white)
  1796.         for j = 1, #currentLeaderboard.rows[1] do
  1797.             local textLenght = ui.measureDWriteText(currentLeaderboard.rows[i][j], settings.leaderboardWrapWidth).x
  1798.             ui.sameLine(box1.x + colWidth / 2 + colWidth * (j - 1) - textLenght / 2)
  1799.             ui.dwriteTextWrapped(currentLeaderboard.rows[i][j], settings.leaderboardWrapWidth, white)
  1800.         end
  1801.     end
  1802.     ui.popDWriteFont()
  1803.     local lineHeight = math.max(ui.itemRectMax().y + box1.y)
  1804.     local lineOffset = box1.x * 1.5
  1805.     ui.drawSimpleLine(vec2(lineOffset, HEIGHT_DIV._20), vec2(lineOffset, lineHeight), white, 2)
  1806.     for i = 1, currentLeaderboard.nbCols - 1 do
  1807.         ui.drawSimpleLine(vec2(box1.x + colWidth * i, HEIGHT_DIV._20), vec2(box1.x + colWidth * i, lineHeight), white, 2)
  1808.     end
  1809. end
  1810.  
  1811. local function playerScores()
  1812.     ui.dwriteTextWrapped("Scores: ", STATS_FONT_SIZE.header, settings.colorHud)
  1813.     ui.newLine()
  1814.     ui.sameLine(WIDTH_DIV._100)
  1815.     ui.beginGroup()
  1816.     ui.dwriteTextWrapped("Arrests: ", STATS_FONT_SIZE.stats, settings.colorHudInverted)
  1817.     ui.sameLine(WIDTH_DIV._10)
  1818.     ui.dwriteTextWrapped(player.arrests, STATS_FONT_SIZE.stats, rgbm.colors.white)
  1819.     ui.dwriteTextWrapped("Getaways: ", STATS_FONT_SIZE.stats, settings.colorHudInverted)
  1820.     ui.sameLine(WIDTH_DIV._10)
  1821.     ui.dwriteTextWrapped(player.getaways, STATS_FONT_SIZE.stats, rgbm.colors.white)
  1822.     ui.dwriteTextWrapped("Car Thefts: ", STATS_FONT_SIZE.stats, settings.colorHudInverted)
  1823.     ui.sameLine(WIDTH_DIV._10)
  1824.     ui.dwriteTextWrapped(player.thefts, STATS_FONT_SIZE.stats, rgbm.colors.white)
  1825.     ui.dwriteTextWrapped("Bank Heists: ", STATS_FONT_SIZE.stats, settings.colorHudInverted)
  1826.     ui.sameLine(WIDTH_DIV._10)
  1827.     ui.dwriteTextWrapped(player.heists, STATS_FONT_SIZE.stats, rgbm.colors.white)
  1828.     ui.dwriteTextWrapped("Drug Deliveries: ", STATS_FONT_SIZE.stats, settings.colorHudInverted)
  1829.     ui.sameLine(WIDTH_DIV._10)
  1830.     ui.dwriteTextWrapped(player.deliveries, STATS_FONT_SIZE.stats, rgbm.colors.white)
  1831.     ui.dwriteTextWrapped("Overtake: ", STATS_FONT_SIZE.stats, settings.colorHudInverted)
  1832.     ui.sameLine(WIDTH_DIV._10)
  1833.     ui.dwriteTextWrapped(player.overtake, STATS_FONT_SIZE.stats, rgbm.colors.white)
  1834.     ui.dwriteTextWrapped("Race Wins: ", STATS_FONT_SIZE.stats, settings.colorHudInverted)
  1835.     ui.sameLine(WIDTH_DIV._10)
  1836.     ui.dwriteTextWrapped(player.wins, STATS_FONT_SIZE.stats, rgbm.colors.white)
  1837.     ui.dwriteTextWrapped("Race Losses: ", STATS_FONT_SIZE.stats, settings.colorHudInverted)
  1838.     ui.sameLine(WIDTH_DIV._10)
  1839.     ui.dwriteTextWrapped(player.losses, STATS_FONT_SIZE.stats, rgbm.colors.white)
  1840.     ui.dwriteTextWrapped("Racing Elo: ", STATS_FONT_SIZE.stats, settings.colorHudInverted)
  1841.     ui.sameLine(WIDTH_DIV._10)
  1842.     ui.dwriteTextWrapped(player.elo .. ' pts', STATS_FONT_SIZE.stats, rgbm.colors.white)
  1843.     ui.dwriteTextWrapped("Distance Driven: ", STATS_FONT_SIZE.stats, settings.colorHudInverted)
  1844.     ui.sameLine(WIDTH_DIV._10)
  1845.     ui.dwriteTextWrapped(player.kms .. ' kms', STATS_FONT_SIZE.stats, rgbm.colors.white)
  1846.     ui.dwriteTextWrapped("Time Played: ", STATS_FONT_SIZE.stats, settings.colorHudInverted)
  1847.     ui.sameLine(WIDTH_DIV._10)
  1848.     ui.dwriteTextWrapped(player.timePlayed, STATS_FONT_SIZE.stats, rgbm.colors.white)
  1849.     ui.endGroup()
  1850. end
  1851.  
  1852. local function playerTimes()
  1853.     -- ui.newLine()
  1854.     ui.dwriteTextWrapped("Sectors: ", STATS_FONT_SIZE.header, settings.colorHud)
  1855.     ui.newLine()
  1856.     ui.sameLine(WIDTH_DIV._100)
  1857.     ui.beginGroup()
  1858.     for sectorName, times in pairs(player.sectorsFormated) do
  1859.         ui.dwriteTextWrapped(sectorName .. ": ", STATS_FONT_SIZE.stats, settings.colorHud)
  1860.         ui.beginSubgroup(WIDTH_DIV._50)
  1861.         for i = 1, #times do
  1862.             ui.dwriteTextWrapped(times[i][1] .. ": ", STATS_FONT_SIZE.stats, settings.colorHudInverted)
  1863.             ui.sameLine(WIDTH_DIV._8)
  1864.             ui.dwriteTextWrapped(times[i][2], STATS_FONT_SIZE.stats, white)
  1865.         end
  1866.         ui.endSubgroup()
  1867.         ui.newLine()
  1868.     end
  1869.     ui.dummy(vec2(WIDTH_DIV._50, HEIGHT_DIV._50))
  1870.     ui.endGroup()
  1871. end
  1872.  
  1873. local function playerStats()
  1874.     ui.separator()
  1875.     ui.pushDWriteFont("Orbitron;Weight=Black")
  1876.     local topPosY = ui.getCursorY() - 5
  1877.     ui.childWindow('playerTimes', playerStatsSubWindow, true, ui.WindowFlags.ThinScrollbar, function()
  1878.         playerTimes()
  1879.     end)
  1880.     ui.drawSimpleLine(vec2(playerStatsSubWindow.x + WIDTH_DIV._100, topPosY), vec2(playerStatsSubWindow.x + WIDTH_DIV._100, ui.getCursorY()), rgbm(0.1, 0.1, 0.1, 0.3), 2)
  1881.     ui.sameLine(playerStatsSubWindow.x + WIDTH_DIV._50)
  1882.     ui.childWindow('playerScores', playerStatsSubWindow, true, ui.WindowFlags.ThinScrollbar, function()
  1883.         playerScores()
  1884.     end)
  1885.     ui.popDWriteFont()
  1886. end
  1887.  
  1888. local function showLeaderboard()
  1889.     ui.setNextItemWidth(WIDTH_DIV._12)
  1890.     ui.combo("leaderboard", currentLeaderboard.name, function()
  1891.         for i = 1, #LEADERBOARD_NAMES[2] do
  1892.             if ui.selectable(LEADERBOARD_NAMES[2][i], currentLeaderboard.name == LEADERBOARD_NAMES[2][i]) then
  1893.                 if LEADERBOARD_NAMES[1][i] == "Your Stats" then
  1894.                     currentLeaderboard = player
  1895.                 else
  1896.                     Leaderboard.allocate(LEADERBOARD_NAMES[1][i])
  1897.                 end
  1898.             end
  1899.         end
  1900.     end)
  1901.     ui.sameLine(menuSize[currentTab].x - 64)
  1902.     if ui.modernButton('', vec2(48, 32), ui.ButtonFlags.PressedOnRelease, 'EXIT', 24, nil) then menuStates.leaderboard = false end
  1903.     if not currentLeaderboard then return 3 end
  1904.     if currentLeaderboard.name == player.name then
  1905.         playerStats()
  1906.     else
  1907.         displayInGrid()
  1908.     end
  1909.     return 3
  1910. end
  1911.  
  1912. ----------------------------------------------------------------------------------------------- settings -----------------------------------------------------------------------------------------------
  1913. local PREVIEWS = const({ 'Message', 'Distance Bar', 'Stars' })
  1914.  
  1915. local preview = {
  1916.     msg = false,
  1917.     distanceBar = false,
  1918.     stars = false,
  1919. }
  1920. ---@param buttonClicked string
  1921. local function updatePreviewState(buttonClicked)
  1922.     if buttonClicked == 'Message' then
  1923.         preview.msg = not preview.msg
  1924.         preview.distanceBar = false
  1925.         preview.stars = false
  1926.     elseif buttonClicked == 'Distance Bar' then
  1927.         preview.distanceBar = not preview.distanceBar
  1928.         preview.msg = false
  1929.         preview.stars = false
  1930.     elseif buttonClicked == 'Stars' then
  1931.         preview.stars = not preview.stars
  1932.         preview.msg = false
  1933.         preview.distanceBar = false
  1934.     end
  1935. end
  1936.  
  1937. COLOR_MSG_BG = rgbm(0.5, 0.5, 0.5, 0.5)
  1938.  
  1939. local online = {
  1940.     message = "",
  1941.     messageTimer = 0,
  1942.     type = 1,
  1943.     chased = false,
  1944.     officer = nil,
  1945.     level = 0,
  1946. }
  1947.  
  1948. local function showStarsPursuit()
  1949.     local starsColor = rgbm(1, 1, 1, os.clock() % 2 + 0.3)
  1950.     updateStarsPos()
  1951.     for i = 1, 5 do
  1952.         if i > online.level / 2 then
  1953.             ui.drawImage(starsUI.empty, starsUI.starsPos, starsUI.starsSize, rgbm(1, 1, 1, 0.2))
  1954.         else
  1955.             ui.drawImage(starsUI.full, starsUI.starsPos, starsUI.starsSize, starsColor)
  1956.         end
  1957.         starsUI.starsPos.x = starsUI.starsPos.x - settings.starsSize - starsUI.startSpace
  1958.         starsUI.starsSize.x = starsUI.starsSize.x - settings.starsSize - starsUI.startSpace
  1959.     end
  1960. end
  1961.  
  1962. local function distanceBarPreview()
  1963.     ui.transparentWindow("progressBar", vec2(0, 0), vec2(WINDOW_WIDTH, WINDOW_HEIGHT), function()
  1964.         local playerInFront = "You are in front"
  1965.         local text = math.floor(50) .. "m"
  1966.         local textLenght = ui.measureDWriteText(text, 30)
  1967.         ui.newLine()
  1968.         ui.dummy(vec2(WIDTH_DIV._3, HEIGHT_DIV._40))
  1969.         ui.sameLine()
  1970.         ui.beginRotation()
  1971.         ui.progressBar(125 / 250, vec2(WIDTH_DIV._3, HEIGHT_DIV._60), playerInFront)
  1972.         ui.endRotation(90, vec2(settings.msgOffset.x - WIDTH_DIV._2 - textLenght.x / 2, settings.msgOffset.y + textLenght.y / 3))
  1973.         ui.dwriteDrawText(text, 30, vec2(settings.msgOffset.x - textLenght.x / 2, settings.msgOffset.y), white)
  1974.     end)
  1975. end
  1976.  
  1977. local function previewMSG()
  1978.     ui.transparentWindow("previewMSG", vec2(0, 0), vec2(WINDOW_WIDTH, WINDOW_HEIGHT), function()
  1979.         ui.pushDWriteFont("Orbitron;Weight=Black")
  1980.         local textSize = ui.measureDWriteText("Messages from Police when being chased", settings.fontSizeMSG)
  1981.         local uiOffsetX = settings.msgOffset.x - textSize.x / 2
  1982.         local uiOffsetY = settings.msgOffset.y
  1983.         ui.drawRectFilled(vec2(uiOffsetX - 5, uiOffsetY - 5), vec2(uiOffsetX + textSize.x + 5, uiOffsetY + textSize.y + 5), COLOR_MSG_BG)
  1984.         ui.dwriteDrawText("Messages from Police when being chased", settings.fontSizeMSG, vec2(uiOffsetX, uiOffsetY), settings.colorHud)
  1985.         ui.popDWriteFont()
  1986.     end)
  1987. end
  1988.  
  1989. local function previewStars()
  1990.     ui.transparentWindow("PreviewStars", vec2(0, 0), vec2(WINDOW_WIDTH, WINDOW_HEIGHT), function()
  1991.         showStarsPursuit()
  1992.     end)
  1993. end
  1994.  
  1995. local function uiTab()
  1996.     ui.text('On Screen Message : ')
  1997.     settings.timeMsg = ui.slider('##' .. 'Time Msg On Screen', settings.timeMsg, 1, 15, 'Time Msg On Screen' .. ': %.0fs')
  1998.     settings.fontSizeMSG = ui.slider('##' .. 'Font Size MSG', settings.fontSizeMSG, 10, 50, 'Font Size' .. ': %.0f')
  1999.     settings.msgOffset.y = ui.slider('##' .. 'Msg On Screen Offset Y', settings.msgOffset.y, 0, WINDOW_HEIGHT, 'Msg On Screen Offset Y' .. ': %.0f')
  2000.     settings.msgOffset.x = ui.slider('##' .. 'Msg On Screen Offset X', settings.msgOffset.x, 0, WINDOW_WIDTH, 'Msg On Screen Offset X' .. ': %.0f')
  2001.     if ui.button('MSG Offset X to center') then settings.msgOffset.x = WIDTH_DIV._2 end
  2002.     ui.newLine()
  2003.     ui.text('Stars : ')
  2004.     settings.starsPos.x = ui.slider('##' .. 'Stars Offset X', settings.starsPos.x, 0, WINDOW_WIDTH, 'Stars Offset X' .. ': %.0f')
  2005.     settings.starsPos.y = ui.slider('##' .. 'Stars Offset Y', settings.starsPos.y, 0, WINDOW_HEIGHT, 'Stars Offset Y' .. ': %.0f')
  2006.     settings.starsSize = ui.slider('##' .. 'Stars Size', settings.starsSize, 10, 50, 'Stars Size' .. ': %.0f')
  2007.     ui.newLine()
  2008.     ui.text('Preview : ')
  2009.     for i = 1, #PREVIEWS do
  2010.         if ui.button(PREVIEWS[i]) then
  2011.             updatePreviewState(PREVIEWS[i])
  2012.         end
  2013.         ui.sameLine()
  2014.     end
  2015.     if preview.msg then previewMSG() end
  2016.     if preview.distanceBar then distanceBarPreview() end
  2017.     if preview.stars then previewStars() end
  2018.     ui.newLine()
  2019. end
  2020.  
  2021. local function settingsWindow()
  2022.     hud.size = vec2(HEIGHT_DIV._80 * settings.essentialSize, HEIGHT_DIV._80 * settings.essentialSize)
  2023.     ui.sameLine(10)
  2024.     ui.beginGroup()
  2025.     ui.newLine(15)
  2026.  
  2027.     ui.text('Unit : ')
  2028.     ui.sameLine(160)
  2029.     if ui.selectable('mph', settings.unit == 'mph',_, ui.measureText('km/h')) then
  2030.         settings.unit = 'mph'
  2031.         settings.unitMult = 0.621371
  2032.     end
  2033.     ui.sameLine(200)
  2034.     if ui.selectable('km/h', settings.unit == 'km/h',_, ui.measureText('km/h')) then
  2035.         settings.unit = 'km/h'
  2036.         settings.unitMult = 1
  2037.     end
  2038.     ui.sameLine(menuSize[currentTab].x - 64)
  2039.     if ui.modernButton('', vec2(48, 32), ui.ButtonFlags.PressedOnRelease, 'EXIT', 24, nil) then
  2040.         menuStates.main = false
  2041.         settings:save()
  2042.     end
  2043.     ui.text('Welcome Menu Keybind : ')
  2044.     ui.sameLine()
  2045.     openMenuKeyBind:control(vec2(120, 0))
  2046.     ui.newLine()
  2047.     settings.hudOffset.x = ui.slider('##' .. 'HUD Offset X', settings.hudOffset.x, 0, WINDOW_WIDTH,'HUD Offset X' .. ': %.0f')
  2048.     settings.hudOffset.y = ui.slider('##' .. 'HUD Offset Y', settings.hudOffset.y, 0, WINDOW_HEIGHT,'HUD Offset Y' .. ': %.0f')
  2049.     settings.essentialSize = ui.slider('##' .. 'HUD Size', settings.essentialSize, 10, 50, 'HUD Size' .. ': %.0f')
  2050.     settings.fontSize = settings.essentialSize * FONT_MULT
  2051.     ui.setNextItemWidth(300)
  2052.     local colorHud = settings.colorHud
  2053.     settings.colorHudInverted = rgbm(1 - colorHud.r, 1 - colorHud.g, 1 - colorHud.b, 1)
  2054.     ui.colorPicker('Theme Color', colorHud, ui.ColorPickerFlags.AlphaBar)
  2055.     ui.newLine()
  2056.     uiTab()
  2057.     ui.endGroup()
  2058.     updateHudPos()
  2059.     return 2
  2060. end
  2061.  
  2062. local function discordLinks()
  2063.     ui.newLine(50)
  2064.     if sectorManager.sector.name ~= 'H1' then
  2065.         ui.dwriteTextWrapped("For more info about the challenge click on the Discord link :", 15, white)
  2066.         if ui.textHyperlink("Discord Channel") then
  2067.             for i = 1, #SECTORS_DATA do
  2068.                 if SECTORS_DATA[i].name == sectorManager.sector.name then
  2069.                     os.openURL(SECTORS_DATA[i].discordLink)
  2070.                 end
  2071.             end
  2072.         end
  2073.         ui.newLine()
  2074.         ui.dwriteTextWrapped("Here's a video of the challenge :", 15, white)
  2075.         if ui.textHyperlink("Demo Video") then
  2076.             for i = 1, #SECTORS_DATA do
  2077.                 if SECTORS_DATA[i].name == sectorManager.sector.name then
  2078.                     os.openURL(SECTORS_DATA[i].video)
  2079.                 end
  2080.             end
  2081.         end
  2082.     end
  2083.     ui.newLine(10)
  2084. end
  2085.  
  2086. local function doubleTrouble()
  2087.     local players = {}
  2088.     for i = ac.getSim().carsCount - 1, 0, -1 do
  2089.         local carPlayer = ac.getCar(i)
  2090.         if carPlayer and carPlayer.isConnected and (not carPlayer.isHidingLabels) then
  2091.             if carPlayer.index ~= car.index and not isPoliceCar(carPlayer:id()) then
  2092.                 table.insert(players, carPlayer)
  2093.             end
  2094.         end
  2095.     end
  2096.     if #players == 0 then
  2097.         ui.newLine()
  2098.         ui.dwriteTextWrapped("There is no other players connected", 15, white)
  2099.         ui.dwriteTextWrapped("You can't steal a car", 15, white)
  2100.     else
  2101.         if duo.teammate == nil then
  2102.             ui.setNextItemWidth(150)
  2103.             ui.combo("Teammate", duo.playerName, function()
  2104.                 for i = 1, #players do
  2105.                     if ui.selectable(ac.getDriverName(players[i].index), duo.teammate == players[i].index) then
  2106.                         acpEvent { message = "Request", messageType = 5, yourIndex = ac.getCar(players[i].index).sessionID }
  2107.                         duo.playerName = ac.getDriverName(players[i].index)
  2108.                         duo.waiting = true
  2109.                     end
  2110.                 end
  2111.             end)
  2112.             if duo.waiting then
  2113.                 ui.dwriteTextWrapped("duo.waiting for " .. duo.playerName .. " response ...", 15, rgbm.colors.yellow)
  2114.             end
  2115.         else
  2116.             ui.newLine()
  2117.             ui.dwriteTextWrapped("teammate : ", 15, white)
  2118.             ui.sameLine()
  2119.             ui.dwriteTextWrapped(ac.getDriverName(duo.teammate.index), 15, rgbm.colors.purple)
  2120.             ui.sameLine()
  2121.             if ui.button("Cancel") then
  2122.                 acpEvent { message = "Cancel", messageType = 5, yourIndex = ac.getCar(duo.teammate.index).sessionID }
  2123.                 duo.teammate = nil
  2124.             end
  2125.             duo.waiting = false
  2126.         end
  2127.     end
  2128. end
  2129.  
  2130. local function sectorSelect()
  2131.     ui.setNextItemWidth(150)
  2132.     ui.combo("Sector", sectorManager.sector.name, function()
  2133.         for i = 1, #sectors do
  2134.             if ui.selectable(sectors[i].name, sectorManager.sector == sectors[i]) then
  2135.                 sectorManager.sector = sectors[i]
  2136.                 sectorManager.sector:reset()
  2137.             end
  2138.         end
  2139.     end)
  2140.     ui.sameLine(menuSize[currentTab].x - 64)
  2141.     if ui.modernButton('', vec2(48, 32), ui.ButtonFlags.PressedOnRelease, 'EXIT', 24, nil) then
  2142.         menuStates.main = false
  2143.         settings:save()
  2144.     end
  2145. end
  2146.  
  2147. local function sectorUI()
  2148.     ui.sameLine(10)
  2149.     ui.beginGroup()
  2150.     ui.newLine(15)
  2151.     sectorSelect()
  2152.     if sectorManager.sector.name == 'DOUBLE TROUBLE' then doubleTrouble() end
  2153.     if duo.request then
  2154.         ui.newLine()
  2155.         ui.dwriteTextWrapped((ac.getDriverName(duo.onlineSender.index) .. " want to steal a car with you!"), 15, rgbm.colors.purple)
  2156.         if ui.button("Accept") then
  2157.             duo.teammate = duo.onlineSender
  2158.             acpEvent{message = "Accept", messageType = 5, yourIndex = ac.getCar(duo.teammate.index).sessionID}
  2159.             duo.request = false
  2160.             sectorManager:setSector('DOUBLE TROUBLE')
  2161.         end
  2162.         ui.sameLine()
  2163.         if ui.button("Decline") then
  2164.             duo.request = false
  2165.         end
  2166.     end
  2167.     discordLinks()
  2168.     ui.newLine()
  2169.     ui.endGroup()
  2170.     return 1
  2171. end
  2172.  
  2173. --------------------------------------------------------------------------------------- Race Opponent -----------------------------------------------------------------------------------------------
  2174.  
  2175. local horn = {
  2176.     lastState = false,
  2177.     stateChangedCount = 0,
  2178.     time = 0,
  2179.     active = false,
  2180.     resquestTime = 0,
  2181.     opponentName = "",
  2182. }
  2183.  
  2184. local raceState = {
  2185.     inRace = false,
  2186.     opponent = nil,
  2187.     inFront = nil,
  2188.     distance = 0,
  2189.     message = false,
  2190.     time = 0,
  2191.     opponentElo = 1200,
  2192. }
  2193.  
  2194. local raceFinish = {
  2195.     winner = nil,
  2196.     finished = false,
  2197.     time = 0,
  2198.     opponentName = 'None',
  2199.     messageSent = false,
  2200. }
  2201.  
  2202. local function resetHorn()
  2203.     horn.active = false
  2204.     horn.stateChangedCount = 0
  2205.     horn.time = 0
  2206. end
  2207.  
  2208. local function resetRequest()
  2209.     horn.resquestTime = 0
  2210.     raceState.opponent = nil
  2211.     raceState.opponentElo = 1200
  2212.     horn.opponentName = ""
  2213.     resetHorn()
  2214. end
  2215.  
  2216. local timeStartRace = 0
  2217.  
  2218. local function showRaceLights()
  2219.     local timing = os.clock() % 1
  2220.     if timing > 0.5 then
  2221.         ui.drawRectFilledMultiColor(vec2(0, 0), vec2(WIDTH_DIV._10, WINDOW_HEIGHT), settings.colorHud, rgbm.colors.transparent,
  2222.             rgbm.colors.transparent, settings.colorHud)
  2223.         ui.drawRectFilledMultiColor(vec2(WINDOW_WIDTH - WIDTH_DIV._10, 0), vec2(WINDOW_WIDTH, WINDOW_HEIGHT),
  2224.             rgbm.colors.transparent, settings.colorHud, settings.colorHud, rgbm.colors.transparent)
  2225.     else
  2226.         ui.drawRectFilledMultiColor(vec2(0, 0), vec2(WIDTH_DIV._10, WINDOW_HEIGHT), rgbm.colors.transparent, rgbm.colors.transparent,
  2227.             rgbm.colors.transparent, rgbm.colors.transparent)
  2228.         ui.drawRectFilledMultiColor(vec2(WINDOW_WIDTH - WIDTH_DIV._10, 0), vec2(WINDOW_WIDTH, WINDOW_HEIGHT),
  2229.             rgbm.colors.transparent, rgbm.colors.transparent, rgbm.colors.transparent, rgbm.colors.transparent)
  2230.     end
  2231. end
  2232.  
  2233. local function hasWonRace(winner)
  2234.     raceFinish.winner = winner
  2235.     raceFinish.finished = true
  2236.     raceFinish.time = 10
  2237.     raceState.inRace = false
  2238.     if winner == car then
  2239.         player.wins = player.wins + 1
  2240.         raceFinish.opponentName = ac.getDriverName(raceState.opponent.index)
  2241.         raceFinish.messageSent = false
  2242.     else
  2243.         player.losses = player.losses + 1
  2244.     end
  2245.     player.elo = calculateElo(raceState.opponentElo, winner == car)
  2246.     player:save()
  2247.     raceState.opponent = nil
  2248. end
  2249.  
  2250. local acpRace = ac.OnlineEvent({
  2251.     targetSessionID = ac.StructItem.int16(),
  2252.     messageType = ac.StructItem.int16(),
  2253.     elo = ac.StructItem.int16(),
  2254. }, function(sender, data)
  2255.     if data.targetSessionID == car.sessionID and data.messageType == 1 then
  2256.         raceState.opponent = sender
  2257.         raceState.opponentElo = data.elo
  2258.         horn.resquestTime = 7
  2259.     elseif data.targetSessionID == car.sessionID and data.messageType == 2 then
  2260.         raceState.opponent = sender
  2261.         raceState.opponentElo = data.elo
  2262.         raceState.inRace = true
  2263.         resetHorn()
  2264.         horn.resquestTime = 0
  2265.         raceState.message = true
  2266.         raceState.time = 2
  2267.         timeStartRace = 7
  2268.     elseif data.targetSessionID == car.sessionID and data.messageType == 3 then
  2269.         hasWonRace(car)
  2270.     end
  2271. end)
  2272.  
  2273. local function whosInFront()
  2274.     if raceState.opponent == nil then return end
  2275.     local direction = cross(vec2(car.velocity.x, car.velocity.z),
  2276.         vec2(raceState.opponent.velocity.x, raceState.opponent.velocity.z))
  2277.     local midBetweenPlayers = vec2((car.position.x + raceState.opponent.position.x) / 2,
  2278.         (car.position.z + raceState.opponent.position.z) / 2)
  2279.     local midPlusDirection = vec2(midBetweenPlayers.x + direction.x, midBetweenPlayers.y + direction.y)
  2280.     local youDistanceSquared = vec2(car.position.x, car.position.z):distanceSquared(midPlusDirection)
  2281.     local opponentDistanceSquared = vec2(raceState.opponent.position.x, raceState.opponent.position.z):distanceSquared(midPlusDirection)
  2282.     if youDistanceSquared < opponentDistanceSquared then
  2283.         raceState.inFront = car
  2284.     else
  2285.         raceState.inFront = raceState.opponent
  2286.     end
  2287. end
  2288.  
  2289. local function hasPit()
  2290.     if not raceState.opponent or raceState.opponent and not raceState.opponent.isConnected then
  2291.         hasWonRace(car)
  2292.         return false
  2293.     end
  2294.     if car.isInPit then
  2295.         acpRace { targetSessionID = raceState.opponent.sessionID, messageType = 3 }
  2296.         hasWonRace(raceState.opponent)
  2297.         return false
  2298.     end
  2299.     return true
  2300. end
  2301.  
  2302. local function inRace()
  2303.     if raceState.opponent == nil then return end
  2304.     raceState.distance = vec2(car.position.x, car.position.z):distance(vec2(raceState.opponent.position.x, raceState.opponent.position.z))
  2305.     if raceState.distance < 50 then
  2306.         whosInFront()
  2307.     elseif raceState.distance > 250 then
  2308.         hasWonRace(raceState.inFront)
  2309.     end
  2310. end
  2311.  
  2312. local function hornUsage()
  2313.     if horn.time < 2 then
  2314.         horn.time = horn.time + ui.deltaTime()
  2315.         if horn.lastState ~= car.hornActive then
  2316.             horn.stateChangedCount = horn.stateChangedCount + 1
  2317.             horn.lastState = car.hornActive
  2318.         end
  2319.         if horn.stateChangedCount > 3 then
  2320.             horn.active = true
  2321.             horn.stateChangedCount = 0
  2322.             horn.time = 0
  2323.         end
  2324.     else
  2325.         resetHorn()
  2326.     end
  2327. end
  2328.  
  2329. local function dot(vector1, vector2)
  2330.     return vector1.x * vector2.x + vector1.y * vector2.y
  2331. end
  2332.  
  2333. local function resquestRace()
  2334.     local opponent = ac.getCar(ac.getCarIndexInFront(0))
  2335.     if not opponent then return end
  2336.     horn.opponentName = ac.getDriverName(opponent.index)
  2337.     if opponent and (not opponent.isHidingLabels) then
  2338.         if dot(vec2(car.look.x, car.look.z), vec2(opponent.look.x, opponent.look.z)) > 0 then
  2339.             if isPoliceCar(ac.getCarID(opponent.index)) then return end
  2340.             acpRace { targetSessionID = opponent.sessionID, messageType = 1, elo = player.elo }
  2341.             horn.resquestTime = 10
  2342.         end
  2343.     end
  2344. end
  2345.  
  2346. local function acceptingRace()
  2347.     if dot(vec2(car.look.x, car.look.z), vec2(raceState.opponent.look.x, raceState.opponent.look.z)) > 0 then
  2348.         acpRace { targetSessionID = raceState.opponent.sessionID, messageType = 2, elo = player.elo }
  2349.         raceState.inRace = true
  2350.         horn.resquestTime = 0
  2351.         timeStartRace = 7
  2352.         resetHorn()
  2353.     end
  2354. end
  2355.  
  2356. local function raceUpdate(dt)
  2357.     if raceState.inRace and hasPit() then
  2358.         inRace()
  2359.         if raceState.time > 0 then
  2360.             raceState.time = raceState.time - dt
  2361.         elseif raceState.time < 0 then
  2362.             raceState.time = 0
  2363.         end
  2364.         if raceState.message and raceState.time == 0 then
  2365.             if raceState.opponent then
  2366.                 ac.sendChatMessage(DRIVER_NAME ..
  2367.                     " has started an illegal race against " .. ac.getDriverName(raceState.opponent.index) .. "!")
  2368.                 raceState.message = false
  2369.             end
  2370.         end
  2371.     else
  2372.         if raceFinish.finished then
  2373.             raceFinish.time = raceFinish.time - dt
  2374.             if raceFinish.time < 0 then
  2375.                 raceFinish.finished = false
  2376.                 raceFinish.winner = nil
  2377.             end
  2378.         else
  2379.             hornUsage()
  2380.             if horn.resquestTime > 0 then
  2381.                 horn.resquestTime = horn.resquestTime - dt
  2382.                 if horn.resquestTime < 0 then resetRequest() end
  2383.                 if horn.active and raceState.opponent then acceptingRace() end
  2384.             else
  2385.                 if horn.active then resquestRace() end
  2386.             end
  2387.         end
  2388.     end
  2389. end
  2390.  
  2391. -------------------------------------------------------------------------------- overtake --------------------------------------------------------------------------------
  2392.  
  2393. local REQUIRED_SPEED = const(80)
  2394.  
  2395. function script.prepare(dt)
  2396.     return car.speedKmh > 60
  2397. end
  2398.  
  2399. local overtake = {
  2400.     damage = {},
  2401.     timePassed = 0,
  2402.     totalScore = 0,
  2403.     comboMeter = 1,
  2404.     dangerouslySlowTimer = 0,
  2405. }
  2406.  
  2407. local carsState = {}
  2408.  
  2409. local function resetOvertake()
  2410.     for i = 0, 4 do overtake.damage[i] = car.damage[i] end
  2411.     if overtake.totalScore > player.overtake then
  2412.         player.overtake = math.floor(overtake.totalScore)
  2413.         if player.overtake > 10000 then
  2414.             ac.sendChatMessage("New highest Overtake score: " .. player.overtake .. " pts !")
  2415.             player:save()
  2416.         end
  2417.     end
  2418.     overtake.totalScore = 0
  2419.     overtake.comboMeter = 1
  2420. end
  2421.  
  2422. local function initOverTake()
  2423.     for i = 0, 4 do overtake.damage[i] = car.damage[i] end
  2424. end
  2425.  
  2426. local function overtakeUpdate(dt)
  2427.     if car.engineLifeLeft < 1 then
  2428.         resetOvertake()
  2429.         return
  2430.     end
  2431.     for i = 0, 4 do
  2432.         if car.damage[i] > overtake.damage[i] then
  2433.             resetOvertake()
  2434.             break
  2435.         end
  2436.     end
  2437.     overtake.timePassed = overtake.timePassed + dt
  2438.  
  2439.     local comboFadingRate = 0.5 * math.lerp(1, 0.1, math.lerpInvSat(car.speedKmh, 80, 200)) + car.wheelsOutside
  2440.     overtake.comboMeter = math.max(1, overtake.comboMeter - dt * comboFadingRate)
  2441.  
  2442.     while sim.carsCount > #carsState do
  2443.         carsState[#carsState + 1] = {}
  2444.     end
  2445.  
  2446.     if car.speedKmh < REQUIRED_SPEED then
  2447.         if overtake.dangerouslySlowTimer > 3 then
  2448.             resetOvertake()
  2449.             return
  2450.         end
  2451.         overtake.dangerouslySlowTimer = overtake.dangerouslySlowTimer + dt
  2452.         overtake.comboMeter = 1
  2453.         return
  2454.     else
  2455.         overtake.dangerouslySlowTimer = 0
  2456.     end
  2457.  
  2458.     for i = 1, ac.getSim().carsCount - 1 do
  2459.         local state = carsState[i]
  2460.         local otherCar = ac.getCar(i)
  2461.         if otherCar.isConnected and otherCar.position:closerToThan(car.position, 10) then
  2462.             local drivingAlong = math.dot(otherCar.look, car.look) > 0.2
  2463.             if not drivingAlong then
  2464.                 state.drivingAlong = false
  2465.  
  2466.                 if not state.nearMiss and otherCar.position:closerToThan(car.position, 3) then
  2467.                     state.nearMiss = true
  2468.  
  2469.                     if otherCar.position:closerToThan(car.position, 2.5) then
  2470.                         overtake.comboMeter = overtake.comboMeter + 3
  2471.                     else
  2472.                         overtake.comboMeter = overtake.comboMeter + 1
  2473.                     end
  2474.                 end
  2475.             end
  2476.  
  2477.             if otherCar.collidedWith == 0 then
  2478.                 state.collided = true
  2479.                 resetOvertake()
  2480.                 return
  2481.             end
  2482.  
  2483.             if not state.overtaken and not state.collided and state.drivingAlong then
  2484.                 local posDir = (otherCar.position - car.position):normalize()
  2485.                 local posDot = math.dot(posDir, otherCar.look)
  2486.                 state.maxPosDot = math.max(state.maxPosDot, posDot)
  2487.                 if posDot < -0.5 and state.maxPosDot > 0.5 then
  2488.                     overtake.totalScore = overtake.totalScore + math.ceil(10 * overtake.comboMeter)
  2489.                     overtake.comboMeter = overtake.comboMeter + 1
  2490.                     state.overtaken = true
  2491.                 end
  2492.             end
  2493.         else
  2494.             state.maxPosDot = -1
  2495.             state.overtaken = false
  2496.             state.collided = false
  2497.             state.drivingAlong = true
  2498.             state.nearMiss = false
  2499.         end
  2500.     end
  2501. end
  2502.  
  2503. local function overtakeUI(textOffset)
  2504.     local text
  2505.     local colorCombo
  2506.  
  2507.     if overtake.totalScore > 0 then
  2508.         text = overtake.totalScore .. " pts - " .. string.format("%d", overtake.comboMeter) .. "x"
  2509.         colorCombo = rgbm(0, 1, 0, 0.9)
  2510.     else
  2511.         text = "PB: " .. player.overtake .. "pts"
  2512.         colorCombo = rgbm(1, 1, 1, 0.9)
  2513.     end
  2514.     local textSize = ui.measureDWriteText(text, settings.fontSize)
  2515.     ui.dwriteDrawText(text, settings.fontSize, textOffset - vec2(textSize.x / 2, -hud.size.y / 13), colorCombo)
  2516. end
  2517.  
  2518. local function flashingAlert(intensity)
  2519.     local timing = os.clock() % 1
  2520.     if timing > 0.5 then
  2521.         ui.drawRectFilledMultiColor(vec2(0, 0), vec2(WINDOW_WIDTH / intensity, WINDOW_HEIGHT), rgbm(1, 0, 0, 0.5),
  2522.             rgbm.colors.transparent, rgbm.colors.transparent, rgbm(1, 0, 0, 0.5))
  2523.         ui.drawRectFilledMultiColor(vec2(WINDOW_WIDTH - WINDOW_WIDTH / intensity, 0), vec2(WINDOW_WIDTH, WINDOW_HEIGHT),
  2524.             rgbm.colors.transparent, rgbm(1, 0, 0, 0.5), rgbm(1, 0, 0, 0.5), rgbm.colors.transparent)
  2525.     else
  2526.         ui.drawRectFilledMultiColor(vec2(0, 0), vec2(WINDOW_WIDTH / intensity, WINDOW_HEIGHT), rgbm.colors.transparent,
  2527.             rgbm.colors.transparent, rgbm.colors.transparent, rgbm.colors.transparent)
  2528.         ui.drawRectFilledMultiColor(vec2(WINDOW_WIDTH - WINDOW_WIDTH / intensity, 0), vec2(WINDOW_WIDTH, WINDOW_HEIGHT),
  2529.             rgbm.colors.transparent, rgbm.colors.transparent, rgbm.colors.transparent, rgbm.colors.transparent)
  2530.     end
  2531. end
  2532.  
  2533. local function distanceBar()
  2534.     local playerInFront
  2535.     if raceState.inFront == car then
  2536.         playerInFront = "You are in front"
  2537.     else
  2538.         playerInFront = ac.getDriverName(raceState.inFront.index) .. " is in front"
  2539.     end
  2540.     local text = math.floor(raceState.distance) .. "m"
  2541.     local textLenght = ui.measureDWriteText(text, 30)
  2542.     ui.newLine()
  2543.     ui.dummy(vec2(WIDTH_DIV._3, HEIGHT_DIV._40))
  2544.     ui.sameLine()
  2545.     ui.beginRotation()
  2546.     ui.progressBar(raceState.distance / 250, vec2(WIDTH_DIV._3, HEIGHT_DIV._60), playerInFront)
  2547.     ui.endRotation(90, vec2(settings.msgOffset.x - WIDTH_DIV._2 - textLenght.x / 2, settings.msgOffset.y))
  2548.     ui.dwriteDrawText(text, 30, vec2(settings.msgOffset.x - textLenght.x / 2, settings.msgOffset.y), white)
  2549. end
  2550.  
  2551. local function raceUI()
  2552.     ui.pushDWriteFont("Orbitron;Weight=Black")
  2553.     local displayText = false
  2554.     local text
  2555.  
  2556.     if timeStartRace > 0 then
  2557.         timeStartRace = timeStartRace - ui.deltaTime()
  2558.         if raceState.opponent and timeStartRace - 5 > 0 then
  2559.             text = "Align yourself with " .. ac.getDriverName(raceState.opponent.index) .. " to start the race!"
  2560.             textWithBackground(text, 1, 1)
  2561.         else
  2562.             local number = math.floor(timeStartRace - 1)
  2563.             if number <= 0 then
  2564.                 text = "GO!"
  2565.             else
  2566.                 text = number .. " ..."
  2567.             end
  2568.             textWithBackground(text, 3, 1)
  2569.         end
  2570.         if timeStartRace - 6 > 0 then showRaceLights() end
  2571.         if timeStartRace < 0 then timeStartRace = 0 end
  2572.     elseif raceState.inRace and raceState.inFront then
  2573.         distanceBar()
  2574.         if raceState.inFront == raceState.opponent then
  2575.             if raceState.distance > 190 then
  2576.                 flashingAlert(math.floor((190 - raceState.distance) / 10) + 10)
  2577.             end
  2578.         end
  2579.     elseif raceFinish.finished then
  2580.         text = ac.getDriverName(raceFinish.winner.index) .. " has won the race"
  2581.         displayText = true
  2582.         if not raceFinish.messageSent and raceFinish.winner == car then
  2583.             ac.sendChatMessage(DRIVER_NAME ..
  2584.                 " has just beaten " ..
  2585.                 raceFinish.opponentName ..
  2586.                 string.format(" in an illegal race. [Win rate: %d",
  2587.                     player.wins * 100 / (player.wins + player.losses)) .. "%]")
  2588.             raceFinish.messageSent = true
  2589.             local data = {
  2590.                 ["Wins"] = player.wins,
  2591.                 ["Losses"] = player.losses,
  2592.             }
  2593.         end
  2594.     elseif horn.resquestTime > 0 and raceState.opponent then
  2595.         text = ac.getDriverName(raceState.opponent.index) ..
  2596.             " wants to challenge you to a race. To accept activate your horn twice quickly"
  2597.         displayText = true
  2598.     elseif horn.resquestTime > 0 and raceState.opponent == nil then
  2599.         text = "Waiting for " .. horn.opponentName .. " to accept the challenge"
  2600.         displayText = true
  2601.     end
  2602.     if displayText then textWithBackground(text, 1, 1) end
  2603.     ui.popDWriteFont()
  2604. end
  2605.  
  2606. --------------------------------------------------------------------------------------- Police Chase --------------------------------------------------------------------------------------------------
  2607.  
  2608. local policeLightsPos = {
  2609.     vec2(0, 0),
  2610.     vec2(WIDTH_DIV._15, WINDOW_HEIGHT),
  2611.     vec2(WINDOW_WIDTH - WIDTH_DIV._15, 0),
  2612.     vec2(WINDOW_WIDTH, WINDOW_HEIGHT)
  2613. }
  2614.  
  2615. local acpPolice = ac.OnlineEvent({
  2616.     message = ac.StructItem.string(110),
  2617.     messageType = ac.StructItem.int16(),
  2618.     yourIndex = ac.StructItem.int16(),
  2619. }, function(sender, data)
  2620.     online.type = data.messageType
  2621.     if data.yourIndex == car.sessionID and data.messageType == 0 then
  2622.         online.message = data.message
  2623.         online.chased = true
  2624.         online.officer = sender
  2625.         online.messageTimer = settings.timeMsg
  2626.         policeLightsPos[2] = vec2(WIDTH_DIV._10, WINDOW_HEIGHT)
  2627.         policeLightsPos[3] = vec2(WINDOW_WIDTH - WIDTH_DIV._10, 0)
  2628.     elseif data.yourIndex == car.sessionID and data.messageType == 1 then
  2629.         online.level = tonumber(data.message)
  2630.         online.messageTimer = settings.timeMsg
  2631.         online.message = "CHASE LEVEL " .. data.message
  2632.         if online.level > 8 then
  2633.             online.color = rgbm.colors.red
  2634.         elseif online.level > 6 then
  2635.             online.color = rgbm.colors.orange
  2636.         elseif online.level > 4 then
  2637.             online.color = rgbm.colors.yellow
  2638.         else
  2639.             online.color = white
  2640.         end
  2641.     elseif data.yourIndex == car.sessionID and data.messageType == 2 then
  2642.         physics.teleportCarTo(0, ac.SpawnSet.Pits)
  2643.         online.message = data.message
  2644.         online.messageTimer = settings.timeMsg
  2645.         online.chased = false
  2646.         online.officer = nil
  2647.         online.level = 0
  2648.         policeLightsPos[2] = vec2(WIDTH_DIV._6, WINDOW_HEIGHT)
  2649.         policeLightsPos[3] = vec2(WINDOW_WIDTH - WIDTH_DIV._6, 0)
  2650.     end
  2651. end)
  2652.  
  2653. local function showPoliceLights()
  2654.     local timing = math.floor(os.clock() * 2 % 2)
  2655.     if timing == 0 then
  2656.         ui.drawRectFilledMultiColor(policeLightsPos[1], policeLightsPos[2], rgbm(1, 0, 0, 0.5), rgbm.colors.transparent,
  2657.             rgbm.colors.transparent, rgbm(1, 0, 0, 0.5))
  2658.         ui.drawRectFilledMultiColor(policeLightsPos[3], policeLightsPos[4], rgbm.colors.transparent, rgbm(0, 0, 1, 0.5),
  2659.             rgbm(0, 0, 1, 0.5), rgbm.colors.transparent)
  2660.     else
  2661.         ui.drawRectFilledMultiColor(policeLightsPos[1], policeLightsPos[2], rgbm(0, 0, 1, 0.5), rgbm.colors.transparent,
  2662.             rgbm.colors.transparent, rgbm(0, 0, 1, 0.5))
  2663.         ui.drawRectFilledMultiColor(policeLightsPos[3], policeLightsPos[4], rgbm.colors.transparent, rgbm(1, 0, 0, 0.5),
  2664.             rgbm(1, 0, 0, 0.5), rgbm.colors.transparent)
  2665.     end
  2666. end
  2667.  
  2668. local function showArrestMSG()
  2669.     ui.pushDWriteFont("Orbitron;Weight=Black")
  2670.     local textArrest1 = "BUSTED!"
  2671.     local textArrest2 = "GGs! Please Go Back To Pits."
  2672.     local textArrestLenght1 = ui.measureDWriteText(textArrest1, settings.fontSizeMSG * 3)
  2673.     local textArrestLenght2 = ui.measureDWriteText(textArrest2, settings.fontSizeMSG * 3)
  2674.     ui.drawRectFilled(vec2(0, 0), vec2(WINDOW_WIDTH, WINDOW_HEIGHT), rgbm(0, 0, 0, 0.5))
  2675.     ui.dwriteDrawText(textArrest1, settings.fontSizeMSG * 3,
  2676.         vec2(WIDTH_DIV._2 - textArrestLenght1.x / 2, HEIGHT_DIV._4 - textArrestLenght1.y / 2), rgbm(1, 0, 0, 1))
  2677.     ui.dwriteDrawText(textArrest2, settings.fontSizeMSG * 3,
  2678.         vec2(WIDTH_DIV._2 - textArrestLenght2.x / 2, HEIGHT_DIV._4 + textArrestLenght2.y / 2), white)
  2679.     ui.popDWriteFont()
  2680. end
  2681.  
  2682.  
  2683. local function onlineEventMessageUI()
  2684.     if online.messageTimer > 0 then
  2685.         online.messageTimer = online.messageTimer - ui.deltaTime()
  2686.         local text = online.message
  2687.         if online.message ~= "BUSTED!" then textWithBackground(text, 1, 1) end
  2688.         if online.type == 2 then
  2689.             if online.message == "BUSTED!" then showArrestMSG() end
  2690.             showPoliceLights()
  2691.         end
  2692.         if online.type == 1 and online.messageTimer < 3 then
  2693.             showPoliceLights()
  2694.         end
  2695.     elseif online.messageTimer < 0 then
  2696.         online.message = ""
  2697.         online.messageTimer = 0
  2698.     end
  2699. end
  2700.  
  2701. -------------------------------------------------------------------------------------------- HUD -------------------------------------------------------------------------------------------------------
  2702.  
  2703. local statOn = {
  2704.     [1] = "Distance Driven",
  2705.     [2] = "Races",
  2706.     [3] = "Overtake",
  2707.     [4] = "Sector",
  2708. }
  2709.  
  2710. local iconsColorOn = {
  2711.     [1] = white,
  2712.     [2] = white,
  2713.     [3] = white,
  2714.     [4] = white,
  2715. }
  2716.  
  2717. local countdownTime = 0
  2718. local cooldownTime = 0
  2719. local countDownState = {
  2720.     countdownOn = false,
  2721.     ready = true,
  2722.     set = true,
  2723.     go = true
  2724. }
  2725.  
  2726. local function countdown()
  2727.     if countDownState.countdownOn then
  2728.         if countdownTime > 0 then countdownTime = countdownTime - ui.deltaTime() end
  2729.         cooldownTime = cooldownTime - ui.deltaTime()
  2730.         if cooldownTime < 0 then
  2731.             cooldownTime = 0
  2732.             countDownState.countdownOn = false
  2733.         end
  2734.         if countdownTime < 5 and countDownState.ready == true then
  2735.             ac.sendChatMessage('***GET READY***')
  2736.             countDownState.ready = false
  2737.         elseif countdownTime < 3 and countDownState.set == true then
  2738.             ac.sendChatMessage('**SET**')
  2739.             countDownState.set = false
  2740.         elseif countdownTime < 0 and countDownState.go == true then
  2741.             ac.sendChatMessage('*GO*GO*GO*')
  2742.             countDownState.go = false
  2743.         end
  2744.     end
  2745. end
  2746.  
  2747. local function drawHudText()
  2748.     ui.pushDWriteFont("Orbitron;Weight=BOLD")
  2749.     local textOffset = vec2(hud.size.x / 2, hud.size.y / 4.5)
  2750.     local textSize = ui.measureDWriteText(statOn[settings.current], settings.fontSize)
  2751.     if settings.current ~= 4 then
  2752.         ui.dwriteDrawText(statOn[settings.current], settings.fontSize,
  2753.             textOffset - vec2(textSize.x / 2, 0), settings.colorHud)
  2754.     end
  2755.     if settings.current == 1 then
  2756.         local drivenKm = car.distanceDrivenSessionKm
  2757.         if drivenKm < 0.01 then drivenKm = 0 end
  2758.         textSize = ui.measureDWriteText(string.format("%.2f", drivenKm) .. " km", settings.fontSize)
  2759.         ui.dwriteDrawText(string.format("%.2f", drivenKm) .. " km", settings.fontSize,
  2760.             textOffset - vec2(textSize.x / 2, -hud.size.y / 13), rgbm(1, 1, 1, 0.9))
  2761.     elseif settings.current == 2 then
  2762.         textSize = ui.measureDWriteText(player.wins .. "Win  -  Lost" .. player.losses, settings.fontSize / 1.1)
  2763.         ui.dwriteDrawText("Win " .. player.wins .. " - Lost " .. player.losses, settings.fontSize / 1.1,
  2764.             textOffset - vec2(textSize.x / 2, -hud.size.y / 12.5), rgbm(1, 1, 1, 0.9))
  2765.     elseif settings.current == 3 then
  2766.         overtakeUI(textOffset)
  2767.     elseif settings.current == 4 then
  2768.         textSize = ui.measureDWriteText(sectorManager.sector.name, settings.fontSize)
  2769.         ui.dwriteDrawText(sectorManager.sector.name, settings.fontSize, textOffset - vec2(textSize.x / 2, 0), settings.colorHud)
  2770.         textSize = ui.measureDWriteText('Time - 00:00.000', settings.fontSize)
  2771.         ui.dwriteDrawText(sectorManager.sector.time, settings.fontSize, textOffset - vec2(textSize.x / 2, -hud.size.y / 12.5), sectorManager.sector.timeColor)
  2772.     end
  2773.     ui.popDWriteFont()
  2774. end
  2775.  
  2776. local function getClosestMission()
  2777.     local closestMission = nil
  2778.     local closestDistance = 500
  2779.     for i = 1, #sectors do
  2780.         for j = 1, #MISSION_NAMES do
  2781.             if sectors[i].name == MISSION_NAMES[j] then
  2782.                 if car.position:distance(sectors[i].gates[1].pos) < closestDistance then
  2783.                     closestMission = sectors[i]
  2784.                     closestDistance = car.position:distance(sectors[i].gates[1].pos)
  2785.                 end
  2786.                 break
  2787.             end
  2788.         end
  2789.     end
  2790.     return closestMission
  2791. end
  2792.  
  2793. local function drawHudImages()
  2794.     iconsColorOn[1] = white
  2795.     iconsColorOn[2] = white
  2796.     iconsColorOn[3] = white
  2797.     iconsColorOn[4] = white
  2798.     local toolTipOn = false
  2799.     ui.drawImage(HUD_IMG.center, vec2(0, 0), hud.size)
  2800.     if ui.rectHovered(vec2(0, 0), vec2(hud.size.x, hud.size.y / 2)) then toolTipOn = true end
  2801.     if ui.rectHovered(hud.pos.leftPos2, hud.pos.leftPos1) then
  2802.         ui.image(HUD_IMG.left, hud.size, settings.colorHud)
  2803.         if uiState.isMouseLeftKeyClicked then
  2804.             if settings.current == 1 then settings.current = #statOn else settings.current = settings.current - 1 end
  2805.         end
  2806.     elseif ui.rectHovered(hud.pos.rightPos2, hud.pos.rightPos1) then
  2807.         ui.image(HUD_IMG.right, hud.size, settings.colorHud)
  2808.         if uiState.isMouseLeftKeyClicked then
  2809.             if settings.current == #statOn then settings.current = 1 else settings.current = settings.current + 1 end
  2810.         end
  2811.     elseif ui.rectHovered(hud.pos.theftPos2, hud.pos.theftPos1) then
  2812.         iconsColorOn[1] = settings.colorHud
  2813.         if uiState.isMouseLeftKeyClicked then
  2814.             if missionManager.msgTime == 0 then
  2815.                 local closestMission = getClosestMission()
  2816.                 if not closestMission then
  2817.                     menuStates.welcome = true
  2818.                     menuStates.main = false
  2819.                     menuStates.leaderboard = false
  2820.                     return
  2821.                 end
  2822.                 ac.sendChatMessage(MISSION_TEXT[closestMission.name].chat)
  2823.                 missionManager.msgTime = 10
  2824.                 missionManager.showIntro = true
  2825.                 if sectorManager.sector.name ~= "DOUBLE TROUBLE" then
  2826.                     sectorManager:setSector(closestMission.name)
  2827.                 elseif closestMission.name == "BOBs SCRAPYARD" then
  2828.                     sectorManager:setSector("DOUBLE TROUBLE")
  2829.                 end
  2830.                 settings.current = 4
  2831.             end
  2832.         end
  2833.     elseif ui.rectHovered(hud.pos.ranksPos2, hud.pos.ranksPos1) then
  2834.         iconsColorOn[2] = settings.colorHud
  2835.         if uiState.isMouseLeftKeyClicked then
  2836.             if menuStates.leaderboard then
  2837.                 menuStates.leaderboard = false
  2838.             else
  2839.                 if menuStates.main then
  2840.                     menuStates.main = false
  2841.                 end
  2842.                 menuStates.leaderboard = true
  2843.             end
  2844.         end
  2845.     elseif ui.rectHovered(hud.pos.countdownPos2, hud.pos.countdownPos1) then
  2846.         iconsColorOn[3] = settings.colorHud
  2847.         if not countDownState.countdownOn and uiState.isMouseLeftKeyClicked then
  2848.             if cooldownTime == 0 then
  2849.                 countdownTime = 5
  2850.                 cooldownTime = 30
  2851.                 countDownState.countdownOn = true
  2852.                 countDownState.ready = true
  2853.                 countDownState.set = true
  2854.                 countDownState.go = true
  2855.             end
  2856.             settings.current = 2
  2857.         end
  2858.     elseif ui.rectHovered(hud.pos.menuPos2, hud.pos.menuPos1) then
  2859.         iconsColorOn[4] = settings.colorHud
  2860.         if uiState.isMouseLeftKeyClicked then
  2861.             if menuStates.main then
  2862.                 menuStates.main = false
  2863.             else
  2864.                 if menuStates.leaderboard then menuStates.leaderboard = false end
  2865.                 menuStates.main = true
  2866.             end
  2867.         end
  2868.     end
  2869.     ui.image(HUD_IMG.base, hud.size, settings.colorHud)
  2870.     ui.drawImage(HUD_IMG.theft, vec2(0, 0), hud.size, iconsColorOn[1])
  2871.     ui.drawImage(HUD_IMG.ranks, vec2(0, 0), hud.size, iconsColorOn[2])
  2872.     ui.drawImage(HUD_IMG.countdown, vec2(0, 0), hud.size, iconsColorOn[3])
  2873.     ui.drawImage(HUD_IMG.menu, vec2(0, 0), hud.size, iconsColorOn[4])
  2874.     if countDownState.countdownOn then countdown() end
  2875.     if toolTipOn then
  2876.         ui.tooltip(function()
  2877.             ui.text("Click " .. openMenuKeyBind:boundTo() .. " to Bring up\nThe Welcome Menu")
  2878.         end)
  2879.     end
  2880. end
  2881.  
  2882. local lvlMSG = const("You're late! Don't even think about getting the full payout.\nLook at the new time limit and finish it, or don't bother showing up again!")
  2883.  
  2884. local function missionMsgOnScreen()
  2885.     if sectorManager.sector == nil or sectorManager.sector.name == "H1" then return end
  2886.     if sectorManager.started and missionManager.level == 0 then
  2887.         textWithBackground(MISSION_TEXT[sectorManager.sector.name].failed[missionManager.msgFailedIndex], 1, 1)
  2888.         missionManager.msgTime = 0
  2889.     elseif missionManager.msgTime > 0 then
  2890.         if missionManager.showIntro then
  2891.             textWithBackground(MISSION_TEXT[sectorManager.sector.name].intro[1] .. formatTime(sectorManager.sector.timeLimit + sectorManager.sector.addTimeLimit[3], '') .. MISSION_TEXT[sectorManager.sector.name].intro[2], 1, 1)
  2892.         else
  2893.             if not sectorManager.finished then
  2894.                 textWithBackground(lvlMSG,1,2)
  2895.             end
  2896.         end
  2897.         missionManager.msgTime = missionManager.msgTime - ui.deltaTime()
  2898.         if missionManager.msgTime < 0 then
  2899.             missionManager.msgTime = 0
  2900.             missionManager.showIntro = false
  2901.         end
  2902.     end
  2903. end
  2904.  
  2905. local function hudUI()
  2906.     missionMsgOnScreen()
  2907.     ui.transparentWindow("HUD", settings.hudOffset, hud.size, true, function()
  2908.         drawHudImages()
  2909.         drawHudText()
  2910.     end)
  2911. end
  2912.  
  2913. -------------------------------------------------------------------------------------------- Menu --------------------------------------------------------------------------------------------
  2914.  
  2915. local function menu()
  2916.     ui.tabBar('MainTabBar', ui.TabBarFlags.Reorderable, function()
  2917.         ui.tabItem('Sectors', function() currentTab = sectorUI() end)
  2918.         ui.tabItem('settings', function() currentTab = settingsWindow() end)
  2919.     end)
  2920. end
  2921.  
  2922. local windowAction = 0
  2923. local leftClickDown = false
  2924. local function moveMenu()
  2925.     if ui.windowHovered(ui.HoveredFlags.ChildWindows) then
  2926.         local mousePos = ui.mouseLocalPos()
  2927.         if not leftClickDown and ui.mouseDown() then
  2928.             leftClickDown = true
  2929.             windowAction = 3
  2930.             if mousePos.y > menuSize[currentTab].y - 50 then
  2931.                 if mousePos.x < 50 then
  2932.                     windowAction = 1
  2933.                 elseif mousePos.x > menuSize[currentTab].x - 50 then
  2934.                     windowAction = 2
  2935.                 end
  2936.             end
  2937.         end
  2938.         if mousePos.y > menuSize[currentTab].y - 50 then
  2939.             if mousePos.x < 50 then
  2940.                 ui.setMouseCursor(ui.MouseCursor.ResizeNESW)
  2941.             elseif mousePos.x > menuSize[currentTab].x - 50 then
  2942.                 ui.setMouseCursor(ui.MouseCursor.ResizeNWSE)
  2943.             end
  2944.         end
  2945.     end
  2946.     if ui.mouseReleased() then
  2947.         leftClickDown = false
  2948.         windowAction = 0
  2949.     end
  2950.  
  2951.     if leftClickDown then
  2952.         if windowAction == 1 then
  2953.             menuSize[currentTab].x = menuSize[currentTab].x - ui.mouseDelta().x
  2954.             menuSize[currentTab].y = menuSize[currentTab].y + ui.mouseDelta().y
  2955.             settings.menuPos.x = settings.menuPos.x + ui.mouseDelta().x
  2956.         elseif windowAction == 2 then
  2957.             menuSize[currentTab] = menuSize[currentTab] + ui.mouseDelta()
  2958.             if currentTab == 3 then
  2959.                 playerStatsSubWindow.x = menuSize[currentTab].x / 2
  2960.                 playerStatsSubWindow.y = menuSize[currentTab].y - HEIGHT_DIV._20 - 10
  2961.             end
  2962.         elseif windowAction == 3 then
  2963.             settings.menuPos = settings.menuPos + ui.mouseDelta()
  2964.         end
  2965.     end
  2966. end
  2967.  
  2968.  
  2969. local function leaderboardWindow()
  2970.     ui.toolWindow('LeaderboardWindow', settings.menuPos, menuSize[currentTab], false, true, function()
  2971.         currentTab = showLeaderboard()
  2972.         moveMenu()
  2973.     end)
  2974. end
  2975.  
  2976. --------------------------------------------------------------------------------- Welcome Menu ---------------------------------------------------------------------------------
  2977.  
  2978. local cardOutline = {
  2979.     white,
  2980.     white,
  2981.     white,
  2982.     white,
  2983.     white,
  2984.     white,
  2985.     white,
  2986. }
  2987.  
  2988. local welcomeWindow = {
  2989.     size = vec2(16 * WINDOW_HEIGHT / 9, WINDOW_HEIGHT),
  2990.     topLeft = vec2(0, 0),
  2991.     topRight = vec2(WINDOW_WIDTH, 0),
  2992.     offset = vec2(0, 0),
  2993.     scale = 0.9,
  2994.     fontBold = ui.DWriteFont("Orbitron;Weight=BLACK"),
  2995.     font = ui.DWriteFont("Orbitron;Weight=REGULAR"),
  2996.     closeIMG = "https://acstuff.ru/images/icons_24/cancel.png",
  2997.     fontSize = WINDOW_HEIGHT / 35,
  2998.     missionInfoFontSize = (WINDOW_HEIGHT / 35) * 0.6,
  2999. }
  3000.  
  3001.  
  3002. local function scaleWelcomeMenu()
  3003.     local aspectRatio = WINDOW_WIDTH / WINDOW_HEIGHT < 16 / 9
  3004.     local xScale = WINDOW_WIDTH / 2560
  3005.     local yScale = WINDOW_HEIGHT / 1440
  3006.     local minScale = aspectRatio and math.max(xScale, yScale) or math.min(xScale, yScale)
  3007.  
  3008.     welcomeWindow.size = welcomeWindow.size * welcomeWindow.scale
  3009.     welcomeWindow.offset = vec2((WINDOW_WIDTH - welcomeWindow.size.x) / 2, (WINDOW_HEIGHT - welcomeWindow.size.y) / 2)
  3010.     minScale = minScale * welcomeWindow.scale
  3011.     for i = 1, #WELCOME_CARD_IMG_POS do
  3012.         WELCOME_CARD_IMG_POS[i][1] = WELCOME_CARD_IMG_POS[i][1] * minScale
  3013.         WELCOME_CARD_IMG_POS[i][2] = WELCOME_CARD_IMG_POS[i][2] * minScale
  3014.     end
  3015.     welcomeWindow.topLeft = WELCOME_CARD_IMG_POS[6][1] + welcomeWindow.offset + welcomeWindow.size / 100
  3016.     welcomeWindow.topRight = vec2(WELCOME_CARD_IMG_POS[6][2].x - welcomeWindow.size.x / 100,
  3017.         WELCOME_CARD_IMG_POS[6][1].y + welcomeWindow.size.y / 100) + welcomeWindow.offset
  3018. end
  3019.  
  3020. local timeLevelsOffset = vec2(0,0)
  3021.  
  3022. local function showMissionInfo(i, id)
  3023.     local leftCorner = vec2(WELCOME_CARD_IMG_POS[i + 2][1].x, WELCOME_CARD_IMG_POS[i + 2][1].y) +
  3024.         vec2(welcomeWindow.size.x / 100, welcomeWindow.size.y / 10)
  3025.     local textPos = leftCorner + welcomeWindow.size / 100
  3026.     local margin = welcomeWindow.size.x / 100
  3027.     ui.drawRectFilled(leftCorner, vec2(WELCOME_CARD_IMG_POS[i + 2][2].x - margin, WELCOME_CARD_IMG_POS[i + 2][2].y - margin), rgbm(0, 0, 0, 0.8))
  3028.     ui.popDWriteFont()
  3029.     ui.pushDWriteFont("Orbitron;Weight=BLACK")
  3030.     local textOffsetY = ui.measureDWriteText("TEXT", welcomeWindow.missionInfoFontSize).y * 2
  3031.     local textOffsetX = ui.measureDWriteText("LEVEL 3:---", welcomeWindow.missionInfoFontSize).x
  3032.     ui.dwriteDrawText(MISSIONS[id].start[1], welcomeWindow.missionInfoFontSize, textPos, settings.colorHud)
  3033.     textPos.x = textPos.x + textOffsetX
  3034.     ui.dwriteDrawText(MISSIONS[id].start[2], welcomeWindow.missionInfoFontSize, textPos, white)
  3035.     textPos.y = textPos.y + textOffsetY
  3036.     textPos.x = textPos.x - textOffsetX
  3037.     ui.dwriteDrawText(MISSIONS[id].finish[1], welcomeWindow.missionInfoFontSize, textPos, settings.colorHud)
  3038.     textPos.x = textPos.x + textOffsetX
  3039.     ui.dwriteDrawText(MISSIONS[id].finish[2], welcomeWindow.missionInfoFontSize, textPos, white)
  3040.     textPos.y = textPos.y + textOffsetY
  3041.     textPos.x = textPos.x - textOffsetX
  3042.     ui.dwriteDrawText("Time Limits :", welcomeWindow.fontSize * 0.8, textPos, settings.colorHud)
  3043.     textPos.y = textPos.y + textOffsetY
  3044.     for j = 1, #MISSIONS[id].levels do
  3045.         ui.dwriteDrawText("LEVEL " .. j .. " :" , welcomeWindow.missionInfoFontSize, textPos, settings.colorHud)
  3046.         timeLevelsOffset.y = textPos.y
  3047.         timeLevelsOffset.x = textOffsetX + textPos.x
  3048.         ui.dwriteDrawText(MISSIONS[id].levels[j], welcomeWindow.missionInfoFontSize, timeLevelsOffset, white)
  3049.         textPos.y = textPos.y + textOffsetY
  3050.     end
  3051.     ui.popDWriteFont()
  3052. end
  3053.  
  3054. local function drawWelcomeText()
  3055.     ui.popDWriteFont()
  3056.     ui.pushDWriteFont(welcomeWindow.font)
  3057.     ui.dwriteDrawText("WELCOME BACK,", welcomeWindow.missionInfoFontSize, welcomeWindow.topLeft, white)
  3058.     ui.popDWriteFont()
  3059.     ui.pushDWriteFont(welcomeWindow.fontBold)
  3060.     ui.dwriteDrawText(DRIVER_NAME, welcomeWindow.fontSize,
  3061.         vec2(welcomeWindow.topLeft.x,
  3062.             welcomeWindow.topLeft.y + ui.measureDWriteText("WELCOME BACK,", welcomeWindow.missionInfoFontSize).y),
  3063.         settings.colorHud)
  3064.     ui.popDWriteFont()
  3065.     ui.pushDWriteFont(welcomeWindow.font)
  3066.     ui.dwriteDrawText("CURRENT CAR", welcomeWindow.missionInfoFontSize,
  3067.         vec2(welcomeWindow.topRight.x - ui.measureDWriteText("CURRENT CAR", welcomeWindow.missionInfoFontSize).x,
  3068.             welcomeWindow.topRight.y), white)
  3069.     ui.popDWriteFont()
  3070.     ui.pushDWriteFont(welcomeWindow.fontBold)
  3071.     ui.dwriteDrawText(string.gsub(string.gsub(CAR_NAME_NO_UTF8, "%W", " "), "  ", ""), welcomeWindow.fontSize,
  3072.         vec2(
  3073.             welcomeWindow.topRight.x -
  3074.             ui.measureDWriteText(string.gsub(string.gsub(CAR_NAME_NO_UTF8, "%W", " "), "  ", ""), welcomeWindow.fontSize).x,
  3075.             welcomeWindow.topRight.y + ui.measureDWriteText("CURRENT CAR", welcomeWindow.missionInfoFontSize).y),
  3076.         settings.colorHud)
  3077.     ui.popDWriteFont()
  3078. end
  3079.  
  3080. ---@param tpPos vec3
  3081. local function willCollide(tpPos)
  3082.     for i, c in ac.iterateCars.ordered() do
  3083.         if c.position:distanceSquared(tpPos) < 4 then
  3084.             return true
  3085.         end
  3086.     end
  3087.     return false
  3088. end
  3089.  
  3090. local function tpToMission(i)
  3091.     if i < 4 and car.speedKmh < 30 then
  3092.         for j = 1, #MISSIONS[i].tp do
  3093.             if not willCollide(MISSIONS[i].tp[j].pos) then
  3094.                 physics.setCarPosition(0, MISSIONS[i].tp[j].pos, MISSIONS[i].tp[j].dir)
  3095.                 settings.current = 4
  3096.                 menuStates.welcome = false
  3097.                 missionManager.tp = true
  3098.                 missionManager.msgTime = 10
  3099.                 missionManager.showIntro = true
  3100.                 if sectorManager.sector.name ~= "DOUBLE TROUBLE" then
  3101.                     sectorManager:setSector(MISSIONS[i].name)
  3102.                 elseif MISSIONS[i].name == "BOBs SCRAPYARD" then
  3103.                     sectorManager:setSector("DOUBLE TROUBLE")
  3104.                 end
  3105.                 break
  3106.             end
  3107.         end
  3108.     end
  3109. end
  3110.  
  3111. local function drawWelcomeImg()
  3112.     local iconCloseColor = white
  3113.     local toolTipOn = false
  3114.     for i = 1, #cardOutline - 1 do
  3115.         if i == #cardOutline - 1 then
  3116.             cardOutline[i] = settings.colorHud
  3117.         else
  3118.             cardOutline[i] = white
  3119.         end
  3120.     end
  3121.     welcomeNavImgToDraw[1] = WELCOME_NAV_IMG.leftArrowOff
  3122.     welcomeNavImgToDraw[2] = WELCOME_NAV_IMG.rightArrowOff
  3123.     welcomeNavImgToDraw[3] = WELCOME_NAV_IMG.leftBoxOff
  3124.     welcomeNavImgToDraw[4] = WELCOME_NAV_IMG.centerBoxOff
  3125.     welcomeNavImgToDraw[5] = WELCOME_NAV_IMG.rightBoxOff
  3126.     welcomeNavImgToDraw[6] = WELCOME_NAV_IMG.base
  3127.     welcomeNavImgToDraw[7] = WELCOME_NAV_IMG.logo
  3128.     ui.transparentWindow('WELCOME_NAV_IMG', welcomeWindow.offset, welcomeWindow.size, true, function()
  3129.         ui.childWindow('welcomeNavIMGChild', welcomeWindow.size, true, function()
  3130.             ui.drawRectFilled(WELCOME_CARD_IMG_POS[6][1], WELCOME_CARD_IMG_POS[6][2], rgbm(0, 0, 0, 0.6))
  3131.             ui.drawRectFilled(WELCOME_CARD_IMG_POS[7][1], WELCOME_CARD_IMG_POS[7][2], rgbm(0, 0, 0, 0.6))
  3132.             if ui.rectHovered(WELCOME_CARD_IMG_POS[1][1], WELCOME_CARD_IMG_POS[1][2]) then
  3133.                 cardOutline[1] = settings.colorHud
  3134.                 welcomeNavImgToDraw[1] = WELCOME_NAV_IMG.leftArrowOn
  3135.                 if uiState.isMouseLeftKeyClicked then
  3136.                     for i = 1, #welcomeCardsToDisplayed do
  3137.                         if welcomeCardsToDisplayed[i] == 1 then
  3138.                             welcomeCardsToDisplayed[i] = #WELCOME_CARD_IMG
  3139.                         else
  3140.                             welcomeCardsToDisplayed[i] = welcomeCardsToDisplayed[i] - 1
  3141.                         end
  3142.                     end
  3143.                 end
  3144.             elseif ui.rectHovered(WELCOME_CARD_IMG_POS[2][1], WELCOME_CARD_IMG_POS[2][2]) then
  3145.                 cardOutline[2] = settings.colorHud
  3146.                 welcomeNavImgToDraw[2] = WELCOME_NAV_IMG.rightArrowOn
  3147.                 if uiState.isMouseLeftKeyClicked then
  3148.                     for i = 1, #welcomeCardsToDisplayed do
  3149.                         if welcomeCardsToDisplayed[i] == #WELCOME_CARD_IMG then
  3150.                             welcomeCardsToDisplayed[i] = 1
  3151.                         else
  3152.                             welcomeCardsToDisplayed[i] = welcomeCardsToDisplayed[i] + 1
  3153.                         end
  3154.                     end
  3155.                 end
  3156.             elseif ui.rectHovered(WELCOME_CARD_IMG_POS[3][1], WELCOME_CARD_IMG_POS[3][2]) then
  3157.                 toolTipOn = true
  3158.                 cardOutline[3] = settings.colorHud
  3159.                 welcomeNavImgToDraw[3] = WELCOME_NAV_IMG.leftBoxOn
  3160.                 if uiState.isMouseLeftKeyClicked then
  3161.                     if uiState.ctrlDown then
  3162.                         os.openURL(SECTORS_DATA[welcomeCardsToDisplayed[1] + 1].video)
  3163.                     else
  3164.                         tpToMission(welcomeCardsToDisplayed[1])
  3165.                     end
  3166.                 end
  3167.             elseif ui.rectHovered(WELCOME_CARD_IMG_POS[4][1], WELCOME_CARD_IMG_POS[4][2]) then
  3168.                 toolTipOn = true
  3169.                 cardOutline[4] = settings.colorHud
  3170.                 welcomeNavImgToDraw[4] = WELCOME_NAV_IMG.centerBoxOn
  3171.                 if uiState.isMouseLeftKeyClicked then
  3172.                     if uiState.ctrlDown then
  3173.                         os.openURL(SECTORS_DATA[welcomeCardsToDisplayed[2] + 1].video)
  3174.                     else
  3175.                         tpToMission(welcomeCardsToDisplayed[2])
  3176.                     end
  3177.                 end
  3178.             elseif ui.rectHovered(WELCOME_CARD_IMG_POS[5][1], WELCOME_CARD_IMG_POS[5][2]) then
  3179.                 toolTipOn = true
  3180.                 cardOutline[5] = settings.colorHud
  3181.                 welcomeNavImgToDraw[5] = WELCOME_NAV_IMG.rightBoxOn
  3182.                 if uiState.isMouseLeftKeyClicked then
  3183.                     if uiState.ctrlDown then
  3184.                         os.openURL(SECTORS_DATA[welcomeCardsToDisplayed[3] + 1].video)
  3185.                     else
  3186.                         tpToMission(welcomeCardsToDisplayed[3])
  3187.                     end
  3188.                 end
  3189.             elseif ui.rectHovered(WELCOME_CARD_IMG_POS[7][1], WELCOME_CARD_IMG_POS[7][2]) then
  3190.                 iconCloseColor = settings.colorHud
  3191.                 if uiState.isMouseLeftKeyClicked then menuStates.welcome = false end
  3192.             end
  3193.             ui.drawImage(welcomeWindow.closeIMG, WELCOME_CARD_IMG_POS[8][1], WELCOME_CARD_IMG_POS[8][2], iconCloseColor)
  3194.             for i = 1, #welcomeNavImgToDraw do
  3195.                 ui.drawImage(welcomeNavImgToDraw[i], vec2(0, 0), welcomeWindow.size, cardOutline[i])
  3196.             end
  3197.             for i = 1, 3 do
  3198.                 if welcomeCardsToDisplayed[i] < 4 then
  3199.                     ui.drawImage(WELCOME_CARD_IMG[welcomeCardsToDisplayed[i]], WELCOME_CARD_IMG_POS[i + 2][1], WELCOME_CARD_IMG_POS[i + 2][2], white)
  3200.                     showMissionInfo(i, welcomeCardsToDisplayed[i])
  3201.                 else
  3202.                     ui.drawImage(WELCOME_CARD_IMG[welcomeCardsToDisplayed[i]], WELCOME_CARD_IMG_POS[i + 2][1], WELCOME_CARD_IMG_POS[i + 2][2], white)
  3203.                 end
  3204.             end
  3205.         end)
  3206.     end)
  3207.     if toolTipOn then
  3208.         for i = 1, 3 do
  3209.             if welcomeCardsToDisplayed[i] < 4 then
  3210.                 ui.tooltip(function()
  3211.                     ui.text("Left Click to teleport to the mission\nCTRL + Left Click to watch the DEMO")
  3212.                 end)
  3213.             end
  3214.         end
  3215.     end
  3216. end
  3217.  
  3218. local function drawWelcomeMenu()
  3219.     drawWelcomeImg()
  3220.     drawWelcomeText()
  3221. end
  3222.  
  3223. -------------------------------------------------------------------------------- UPDATE --------------------------------------------------------------------------------
  3224.  
  3225. local function missionFinishedWindow()
  3226.     ui.transparentWindow('MissionFinished', vec2(0, 0), vec2(WINDOW_WIDTH, HEIGHT_DIV._12), false, true, function()
  3227.         ui.pushDWriteFont("Orbitron;Weight=Black")
  3228.         local timeMsg = "FAILED"
  3229.         if missionManager.level ~= 0 then timeMsg = "LEVEL " .. missionManager.level end
  3230.         local text = sectorManager.sector.name .. " - " .. timeMsg .. os.date(" - %x")
  3231.         local textLenght = ui.measureDWriteText(text, settings.fontSizeMSG * 2)
  3232.         ui.drawRectFilled(vec2(0, 0), vec2(WINDOW_WIDTH, HEIGHT_DIV._12), rgbm(0, 0, 0, 0.5))
  3233.         ui.dwriteDrawText(text, settings.fontSizeMSG * 2, vec2(WIDTH_DIV._2 - textLenght.x / 2, HEIGHT_DIV._60), settings.colorHud)
  3234.         ui.popDWriteFont()
  3235.     end)
  3236. end
  3237.  
  3238. -- https://i.postimg.cc/DyKfkgBG/Boost-Meter.png V1
  3239. -- https://i.postimg.cc/pTrNt9n3/Boost-Meter.png V2
  3240. local BOOST_FRAME = const("https://i.postimg.cc/zvm1SzVM/Boost-Meter.png")
  3241. local horizontalBarParams = {
  3242.     text = '',
  3243.     pos = vec2(WIDTH_DIV._50, WINDOW_HEIGHT - HEIGHT_DIV._20),
  3244.     size = vec2(20 * 25, 20 * 5),
  3245.     delta = 0,
  3246.     activeColor = rgbm(0, 1, 0, 0.5),
  3247.     inactiveColor = rgbm(0, 0, 0, 0.3),
  3248.     total = 100,
  3249.     active = car.kersCharge * 100
  3250. }
  3251.  
  3252. local boostFrameParams = {
  3253.     image = BOOST_FRAME,
  3254.     pos = vec2(WIDTH_DIV._50, WINDOW_HEIGHT - HEIGHT_DIV._20),
  3255.     size = vec2(20 * 25, 20 * 5),
  3256.     color = white,
  3257.     uvStart = vec2(0, 0),
  3258.     uvEnd = vec2(1, 1)
  3259. }
  3260.  
  3261. local boostTextParams = {
  3262.     text = 'Boost',
  3263.     pos = vec2(WIDTH_DIV._50, WINDOW_HEIGHT - HEIGHT_DIV._20),
  3264.     letter = vec2(50, 50),
  3265.     font = 'c7_big',
  3266.     color = white,
  3267.     alignment = 0.5,
  3268.     width = 20 * 5,
  3269.     spacing = 1
  3270. }
  3271.  
  3272. local function boostBar()
  3273.     horizontalBarParams.active = car.kersCharge * 100
  3274.     horizontalBarParams.activeColor = rgbm(1 - car.kersCharge, car.kersCharge^2, 0, 0.7)
  3275.     -- display.rect()
  3276.     display.horizontalBar(horizontalBarParams)
  3277.     display.image(boostFrameParams)
  3278.     display.text(boostTextParams)
  3279. end
  3280.  
  3281. function script.drawUI()
  3282.     if not shouldRun() then return end
  3283.     -- boostBar()
  3284.     if sectorManager.sector and sectorManager.finished and sectorManager.sector.name ~= "H1" then
  3285.         missionFinishedWindow()
  3286.     end
  3287.     if menuStates.welcome then
  3288.         drawWelcomeMenu()
  3289.     else
  3290.         if online.chased then showStarsPursuit() end
  3291.         hudUI()
  3292.         onlineEventMessageUI()
  3293.         raceUI()
  3294.         if menuStates.main then
  3295.             ui.toolWindow('Menu', settings.menuPos, menuSize[currentTab], true, true, function()
  3296.                 menu()
  3297.                 moveMenu()
  3298.             end)
  3299.         end
  3300.         if menuStates.leaderboard then leaderboardWindow() end
  3301.     end
  3302. end
  3303.  
  3304. local policeCarIndex = { 0, 0, 0, 0, 0, 0 }
  3305.  
  3306. local function initPoliceCarIndex()
  3307.     local j = 1
  3308.     for i = ac.getSim().carsCount - 1, 0, -1 do
  3309.         local playerCarID = ac.getCarID(i)
  3310.         if playerCarID and isPoliceCar(playerCarID) then
  3311.             policeCarIndex[j] = i
  3312.             j = j + 1
  3313.         end
  3314.     end
  3315. end
  3316.  
  3317. local function hidePolice()
  3318.     local hideRange = 100
  3319.     for i = 1, 6 do
  3320.         local p = ac.getCar(policeCarIndex[i])
  3321.         if p and p.isConnected then
  3322.             if p.position.x > car.position.x - hideRange and p.position.z > car.position.z - hideRange and p.position.x < car.position.x + hideRange and p.position.z < car.position.z + hideRange then
  3323.                 ac.hideCarLabels(i, false)
  3324.             else
  3325.                 ac.hideCarLabels(i, true)
  3326.             end
  3327.         end
  3328.     end
  3329. end
  3330.  
  3331. ---@return boolean
  3332. local function updateThefts()
  3333.     if sectorManager.sector:isUnderTimeLimit() == 0 then
  3334.         return false
  3335.     end
  3336.     if sectorManager.sector.name == "BOBs SCRAPYARD" or sectorManager.sector.name == "DOUBLE TROUBLE" then
  3337.         player.thefts = player.thefts + 1
  3338.     elseif sectorManager.sector.name == "BANK HEIST" then
  3339.         player.heists = player.heists + 1
  3340.     elseif sectorManager.sector.name == "DRUG DELIVERY" then
  3341.         player.deliveries = player.deliveries + 1
  3342.     end
  3343.     return true
  3344. end
  3345.  
  3346. local function sectorUpdate()
  3347.     if not sectorManager.started and not sectorManager.sector:hasStarted() then
  3348.         sectorManager.started = true
  3349.         sectorManager.finished = false
  3350.     end
  3351.     if not sectorManager.finished and sectorManager.sector:isFinished() then
  3352.         if sectorManager.sector.name ~= 'DOUBLE TROUBLE' or sectorManager:hasTeammateFinished() then
  3353.             local validTheft = updateThefts()
  3354.             sectorManager.finished = true
  3355.             sectorManager.started = false
  3356.             local shouldSave = player:addSectorRecord(sectorManager.sector.name, sectorManager.sector.finalTime)
  3357.             if validTheft or shouldSave then player:save() end
  3358.         else
  3359.             if duo.teammate and not duo.sentFinish then
  3360.                 acpEvent{message = "Finished", messageType = 5, yourIndex = ac.getCar(duo.teammate.index).sessionID}
  3361.                 duo.sentFinish = true
  3362.             end
  3363.         end
  3364.     end
  3365.     if sectorManager.started and not sectorManager.finished then
  3366.         sectorManager.sector:update()
  3367.     end
  3368. end
  3369.  
  3370. local function initBoost()
  3371.     horizontalBarParams.size = vec2(settings.essentialSize * 10, settings.essentialSize)
  3372.     boostFrameParams.size = vec2(settings.essentialSize * 10, settings.essentialSize)
  3373.     boostTextParams.pos = vec2(WIDTH_DIV._50 + settings.essentialSize, WINDOW_HEIGHT - HEIGHT_DIV._20 + settings.essentialSize / 10)
  3374.     boostTextParams.letter = vec2(settings.essentialSize / 1.2, settings.essentialSize / 1.2)
  3375.     boostTextParams.width = boostFrameParams.size
  3376. end
  3377.  
  3378. local function initUI()
  3379.     updateHudPos()
  3380.     scaleWelcomeMenu()
  3381.     updateStarsPos()
  3382.     initBoost()
  3383.     dataLoaded['Settings'] = true
  3384. end
  3385.  
  3386. local function loadSettings()
  3387.     Settings.allocate(function(allocatedSetting)
  3388.         settings = allocatedSetting
  3389.         initUI()
  3390.     end)
  3391. end
  3392.  
  3393. local function loadAllSectors()
  3394.     for i = 1, #SECTORS_DATA do
  3395.         local sector = Sector.tryParse(SECTORS_DATA[i])
  3396.         if sector then
  3397.             sector.name = sector.name
  3398.             sectors[i] = sector
  3399.         end
  3400.     end
  3401.     sectorManager:setSector('H1')
  3402.     dataLoaded['Sectors'] = true
  3403. end
  3404.  
  3405. local function loadPlayerData()
  3406.     Player.allocate(function(allocatedPlayer)
  3407.         if allocatedPlayer then
  3408.             player = allocatedPlayer
  3409.             dataLoaded['PlayerData'] = true
  3410.             player:sortSectors()
  3411.             currentLeaderboard = player
  3412.             updateSharedPlayerData()
  3413.         end
  3414.     end)
  3415. end
  3416.  
  3417. local delay = 1
  3418.  
  3419. local lastTimeUpdate = os.clock()
  3420. local function updateDistanceDriven()
  3421.     if os.clock() - lastTimeUpdate > 10 then
  3422.         player.kms = truncate(car.distanceDrivenSessionKm - lastRegister.kms + player.kms, 3)
  3423.         player.time = math.round(os.clock() - lastRegister.time + player.time, 0)
  3424.         lastRegister.kms = car.distanceDrivenSessionKm
  3425.         lastRegister.time = os.clock()
  3426.         lastTimeUpdate = os.clock()
  3427.     end
  3428. end
  3429.  
  3430. function script.update(dt)
  3431.     if initialisation then
  3432.         initialisation = false
  3433.         loadSettings()
  3434.         loadAllSectors()
  3435.         loadPlayerData()
  3436.         initPoliceCarIndex()
  3437.         initOverTake()
  3438.     end
  3439.     if not shouldRun() then return end
  3440.     if delay > 0 then delay = delay - dt end
  3441.     if delay < 0 then
  3442.         delay = 0
  3443.         player:sortSectors()
  3444.         updateSharedPlayerData()
  3445.         ac.broadcastSharedEvent(SHARED_EVENT_KEY, 'update')
  3446.     end
  3447.     sectorUpdate()
  3448.     raceUpdate(dt)
  3449.     overtakeUpdate(dt)
  3450.     hidePolice()
  3451.     updateDistanceDriven()
  3452. end
  3453.  
  3454. --------------------------------------------------------------- 3D Update ---------------------------------------------------------------
  3455.  
  3456. local function drawGate()
  3457.     if sectorManager.sector and not sectorManager.sector:isFinished() then
  3458.         local gateIndex = sectorManager.sector.gateIndex
  3459.         if gateIndex > sectorManager.sector.gateCount then gateIndex = sectorManager.sector.gateCount end
  3460.         render.debugLine(sectorManager.sector.gates[gateIndex].point1,
  3461.             sectorManager.sector.gates[gateIndex].point2, gateColor)
  3462.     end
  3463. end
  3464.  
  3465. function script.draw3D()
  3466.     if not shouldRun() then return end
  3467.     render.setDepthMode(render.BlendMode.AlphaBlend)
  3468.     drawGate()
  3469. end
  3470.  
  3471. -- ui.registerOnlineExtra(ui.Icons.Menu, "Menu", nil, menu, nil, ui.OnlineExtraFlags.Tool, 'ui.WindowFlags.AlwaysAutoResize')
  3472.  
  3473. --------------------------------------------------------------- AC Callbacks --------------------------------------------------------------
  3474.  
  3475. openMenuKeyBind:onPressed(function ()
  3476.     menuStates.welcome = not menuStates.welcome
  3477. end)
  3478.  
  3479. ac.onCarJumped(0, function(carIndex)
  3480.     if carIndex ~= 0 then return end
  3481.     resetMissionManager()
  3482.     sectorManager:reset()
  3483.     if not isPoliceCar(CAR_ID) then
  3484.         if online.chased and online.officer then
  3485.             acpPolice { message = "TP", messageType = 0, yourIndex = online.officer.sessionID }
  3486.         end
  3487.     end
  3488. end)
  3489.  
  3490. ac.onClientConnected(function(carIndex)
  3491.     local newCar = ac.getCarID(carIndex)
  3492.     ac.log('New Car', newCar)
  3493.     if newCar and isPoliceCar(newCar) then
  3494.         ac.hideCarLabels(carIndex)
  3495.     end
  3496.     initPoliceCarIndex()
  3497. end)
  3498.  
  3499. ac.onClientDisconnected(function(carIndex)
  3500.     ac.hideCarLabels(carIndex, false)
  3501. end)
  3502.  
  3503. ac.onChatMessage(function(message, senderCarIndex, senderSessionID)
  3504.     if not shouldRun() then return false end
  3505.     if online.chased and online.officer then
  3506.         if (senderSessionID == online.officer.sessionID and string.find(message, 'lost')) then
  3507.             if not player.getaways then player.getaways = 0 end
  3508.             player.getaways = player.getaways + 1
  3509.             online.chased = false
  3510.             online.officer = nil
  3511.             player:save()
  3512.         end
  3513.     end
  3514.     return false
  3515. end)
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement