--[[ __ __ /\ `\ ____ __ __ __/\_\ ___ ___ \ `\ `\ /',__\/\ \/\ \/\ \/\ \ /' __` __`\ `\ > > /\__, `\ \ \_/ \_/ \ \ \/\ \/\ \/\ \ / / \/\____/\ \___x___/'\ \_\ \_\ \_\ \_\/\_/ \/___/ \/__//__/ \/_/\/_/\/_/\/_/\// Custom License — see original for terms. ]] -- -- SWIM LIB -- reused functions from multiple swim scripts -- memoize function Memoize(f) local mem = {} -- memoizing table setmetatable(mem, { __mode = "kv" }) -- make it weak return function(x) -- new version of ’f’, with memoizing local r = mem[x] if r == nil then -- no previous result? r = f(x) -- calls original function mem[x] = r -- store result for reuse end return r end end -- queue object List = {} function List.new() return { first = 0, last = -1 } end function List.pushLeft(list, value) local first = list.first - 1 list.first = first list[first] = value end function List.pushRight(list, value) local last = list.last + 1 list.last = last list[last] = value end function List.popLeft(list) local first = list.first if first > list.last then error("list is empty") end local value = list[first] list[first] = nil -- to allow garbage collection list.first = first + 1 return value end function List.popRight(list) local last = list.last if list.first > last then error("list is empty") end local value = list[last] list[last] = nil -- to allow garbage collection list.last = last - 1 return value end function List.length(list) return list.last - list.first + 1 end function RainbowColor(value) -- Calculate the hue value based on the numeric value local hue = math.floor(value % 360) -- Convert the hue value to RGB values local function hslToRgb(h, s, l) local r, g, b if s == 0 then r, g, b = l, l, l -- achromatic else local function hue2rgb(p, q, t) if t < 0 then t = t + 1 end if t > 1 then t = t - 1 end if t < 1 / 6 then return p + (q - p) * 6 * t end if t < 1 / 2 then return q end if t < 2 / 3 then return p + (q - p) * (2 / 3 - t) * 6 end return p end local q = l < 0.5 and l * (1 + s) or l + s - l * s local p = 2 * l - q r = hue2rgb(p, q, h + 1 / 3) g = hue2rgb(p, q, h) b = hue2rgb(p, q, h - 1 / 3) end return r, g, b end local r, g, b = hslToRgb(hue / 360, 1, 0.5) return rgbm(r, g, b, 1) end function IsPlayerBetweenCars(car1, car2, player) -- Calculate the distances local distanceCar1ToPlayer = vec3.distance(car1.pos, player.pos) local distanceCar2ToPlayer = vec3.distance(car2.pos, player.pos) local distanceCar1ToCar2 = vec3.distance(car1.pos, car2.pos) if distanceCar1ToCar2 > 20 then return false end local tolerance = 0.5 if math.abs((distanceCar1ToPlayer + distanceCar2ToPlayer) - distanceCar1ToCar2) <= tolerance then return true else return false end end -------------- -- THEME (Obsidian Glass) local Theme = { -- Obsidian glass layers glass = rgbm(0.047, 0.047, 0.059, 0.85), glassHighlight = rgbm(1, 1, 1, 0.04), glassEdge = rgbm(1, 1, 1, 0.08), glassSurface = rgbm(0.071, 0.071, 0.086, 0.72), -- Text hierarchy textHero = rgbm(1, 1, 1, 1), textPrimary = rgbm(0.941, 0.941, 0.949, 1), textSecondary = rgbm(0.659, 0.659, 0.690, 1), textMuted = rgbm(0.376, 0.376, 0.408, 1), textGhost = rgbm(0.251, 0.251, 0.282, 1), -- Accent (pale blue) accent = rgbm(0.647, 0.769, 0.831, 1), accentBright = rgbm(0.761, 0.855, 0.902, 1), accentGlow = rgbm(0.647, 0.769, 0.831, 0.25), accentSubtle = rgbm(0.647, 0.769, 0.831, 0.08), -- Semantic success = rgbm(0.639, 0.902, 0.208, 1), successGlow = rgbm(0.639, 0.902, 0.208, 0.2), error = rgbm(0.973, 0.443, 0.443, 1), errorGlow = rgbm(0.973, 0.443, 0.443, 0.2), } local UI = { width = 260, height = 380, x = 32, y = 32, padding = 18, cornerRadius = 16, innerRadius = 10, -- Animation state pulsePhase = 0, statusPhase = 0, } -------------- -- SETTINGS — two-layer scheme. THIS SCRIPT is the sole disk owner. -- -- 1. Persisted (ac.storage) ─ disk, debounced auto-save (this script only). -- 2. Settings (ac.connect) ─ shared memory, visible to every Lua script -- in the session (zero-cost reads). -- -- The pause-menu script (test-pause-menu.lua) declares the same ac.connect -- layout and writes to it directly when the user flips a settings pill, but -- it does NOT touch disk. Our setInterval watcher below catches those writes -- and persists them, so all settings changes are durable regardless of which -- script made the edit. -- -- IMPORTANT: the layout below must be byte-for-byte identical across scripts. local Persisted = ac.storage{ hudEnabled = true, hudPosition = 1, -- 1=TL, 2=TR, 3=BL, 4=BR compactMode = false, } local Settings = ac.connect({ ac.StructItem.key('swim.settings.v1'), hudEnabled = ac.StructItem.boolean(), hudPosition = ac.StructItem.int32(), compactMode = ac.StructItem.boolean(), }, true) -- keepLive=true so the struct outlives any single script unload -- Hydrate the shared struct from this script's disk on startup. If the -- pause-menu script also runs ac.connect with the same key, whichever loads -- second overwrites — but both load from disks that should agree because of -- the change-watcher below. Settings.hudEnabled = Persisted.hudEnabled Settings.hudPosition = Persisted.hudPosition Settings.compactMode = Persisted.compactMode -- Mirror local writes (from this script's settings panel) to both layers. local function setPref(key, value) Settings[key] = value Persisted[key] = value end -- Watch for external writes (from the pause menu) and persist them so the -- two scripts stay disk-synced regardless of which one made the change. setInterval(function() if Settings.hudEnabled ~= Persisted.hudEnabled then Persisted.hudEnabled = Settings.hudEnabled end if Settings.hudPosition ~= Persisted.hudPosition then Persisted.hudPosition = Settings.hudPosition end if Settings.compactMode ~= Persisted.compactMode then Persisted.compactMode = Settings.compactMode end end, 0.5) local PositionNames = { "Top-Left", "Top-Right", "Bottom-Left", "Bottom-Right" } local settingsOpen = false -- Rounded rectangle with optional border local function drawPanel(x, y, w, h, radius, fillColor, borderColor, borderWidth) local p1, p2 = vec2(x, y), vec2(x + w, y + h) ui.drawRectFilled(p1, p2, fillColor, radius) if borderColor then ui.drawRect(p1, p2, borderColor, radius, borderWidth or 1) end end -- Horizontal gradient glow strip local function drawGlowStrip(x, y, width, color) local centerX = x + width / 2 local stripWidth = width * 0.6 ui.drawRectFilled(vec2(centerX - stripWidth / 2, y), vec2(centerX + stripWidth / 2, y + 2), color) end -- Message card with thin border around the whole thing local function drawMessage(x, y, width, text, points, mood, alpha) local height = 20 -- Determine accent color by mood local accentCol if mood == 1 then accentCol = Theme.success elseif mood == -1 then accentCol = Theme.error else accentCol = Theme.accent end -- Apply alpha fade accentCol = rgbm(accentCol.r, accentCol.g, accentCol.b, alpha) local textCol = rgbm(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, alpha) -- Text content ui.pushFont(ui.Font.Small) ui.setCursor(vec2(x, y)) ui.textColored(text, textCol) -- Points (right-aligned) local pointsText = (points >= 0 and "+" or "") .. tostring(points) ui.setCursor(vec2(x + width - 36, y)) ui.textColored(pointsText, accentCol) ui.popFont() -- Colored underline ui.drawRectFilled(vec2(x, y + height - 2), vec2(x + width, y + height), accentCol) end -------------- -- SETTINGS PANEL (Obsidian Glass themed overlay) local function drawSettingsPanel() local uiState = ac.getUI() local screenW, screenH = uiState.windowSize.x, uiState.windowSize.y local settingsW = 300 local settingsH = 320 local panelX = (screenW - settingsW) / 2 local panelY = (screenH - settingsH) / 2 local pad = UI.padding ui.beginTransparentWindow("swimSettings", vec2(panelX, panelY), vec2(settingsW, settingsH), true, true) -- Background drawPanel(0, 0, settingsW, settingsH, UI.cornerRadius, Theme.glass, Theme.glassEdge, 1) ui.drawRectFilled(vec2(0, 0), vec2(settingsW, 48), Theme.glassHighlight, UI.cornerRadius) drawGlowStrip(0, 0, settingsW, Theme.accent) -- Header: "swim> Settings" local cy = 12 ui.setCursor(vec2(pad, cy)) ui.pushFont(ui.Font.Main) ui.textColored("swim", Theme.accent) ui.sameLine(0, 0) ui.textColored(">", Theme.textMuted) ui.sameLine(0, 6) ui.textColored("Settings", Theme.textPrimary) ui.popFont() -- Close button (X) top-right local closeBtnSize = 24 local closeX = settingsW - closeBtnSize - 10 local closeY = 10 ui.setCursor(vec2(closeX, closeY)) if ui.invisibleButton("##settingsClose", vec2(closeBtnSize, closeBtnSize)) then settingsOpen = false end local xHovered = ui.itemHovered() local xColor = xHovered and Theme.textHero or Theme.textSecondary local xPad = 6 ui.drawLine( vec2(closeX + xPad, closeY + xPad), vec2(closeX + closeBtnSize - xPad, closeY + closeBtnSize - xPad), xColor, 2 ) ui.drawLine( vec2(closeX + closeBtnSize - xPad, closeY + xPad), vec2(closeX + xPad, closeY + closeBtnSize - xPad), xColor, 2 ) -- Divider cy = 44 ui.drawRectFilled(vec2(pad, cy), vec2(settingsW - pad, cy + 1), Theme.textGhost) cy = cy + 16 -- HUD Toggle ui.pushFont(ui.Font.Small) ui.setCursor(vec2(pad, cy)) ui.textColored("HUD", Theme.textGhost) ui.popFont() cy = cy + 18 local hudLabel = Settings.hudEnabled and "Hide HUD" or "Show HUD" drawPanel(pad, cy, settingsW - pad * 2, 30, UI.innerRadius, Theme.glassSurface, Theme.glassEdge, 1) ui.setCursor(vec2(pad, cy)) if ui.invisibleButton("##toggleHud", vec2(settingsW - pad * 2, 30)) then setPref('hudEnabled', not Settings.hudEnabled) end local hudColor = ui.itemHovered() and Theme.textHero or Theme.textPrimary ui.setCursor(vec2(pad + 10, cy + 6)) ui.textColored(hudLabel, hudColor) cy = cy + 42 -- Compact Mode Toggle ui.pushFont(ui.Font.Small) ui.setCursor(vec2(pad, cy)) ui.textColored("DISPLAY", Theme.textGhost) ui.popFont() cy = cy + 18 local compactLabel = Settings.compactMode and "Full Mode" or "Compact Mode" drawPanel(pad, cy, settingsW - pad * 2, 30, UI.innerRadius, Theme.glassSurface, Theme.glassEdge, 1) ui.setCursor(vec2(pad, cy)) if ui.invisibleButton("##toggleCompact", vec2(settingsW - pad * 2, 30)) then setPref('compactMode', not Settings.compactMode) end local compactColor = ui.itemHovered() and Theme.textHero or Theme.textPrimary ui.setCursor(vec2(pad + 10, cy + 6)) ui.textColored(compactLabel, compactColor) cy = cy + 42 -- Position Selector (2x2 grid) ui.pushFont(ui.Font.Small) ui.setCursor(vec2(pad, cy)) ui.textColored("POSITION", Theme.textGhost) ui.popFont() cy = cy + 18 local btnW = (settingsW - pad * 2 - 8) / 2 local btnH = 30 for idx = 1, 4 do local col = (idx - 1) % 2 local row = math.floor((idx - 1) / 2) local bx = pad + col * (btnW + 8) local by = cy + row * (btnH + 6) local isActive = (Settings.hudPosition == idx) local bgColor = isActive and Theme.accentSubtle or Theme.glassSurface local borderColor = isActive and Theme.accent or Theme.glassEdge drawPanel(bx, by, btnW, btnH, UI.innerRadius, bgColor, borderColor, 1) ui.setCursor(vec2(bx, by)) if ui.invisibleButton("##pos" .. idx, vec2(btnW, btnH)) then setPref('hudPosition', idx) end local textColor if isActive then textColor = Theme.accent elseif ui.itemHovered() then textColor = Theme.textHero else textColor = Theme.textPrimary end local textWidth = ui.measureText(PositionNames[idx]).x ui.setCursor(vec2(bx + (btnW - textWidth) / 2, by + 7)) ui.textColored(PositionNames[idx], textColor) end ui.endTransparentWindow() end -- EXTRAS MENU (accessible via AC chat app Extras button) local function swimExtrasUI() settingsOpen = not settingsOpen ac.setOnlineExtra(false) end ui.registerOnlineExtra(ui.Icons.Settings, "Settings", nil, swimExtrasUI, nil) -------------- -- GLOBAL VARS local overtakeDistance = 5 -- max distance away from player for overtake to count local playerDistance = 150 -- max distance from player for other players to count to the multiplier -- event state local timePassed = 0 local totalScore = 0 local highestScore = 0 local comboMeter = 1 local nearbyPlayers = -1 local playerMultiplier = 0 local carsState = {} local totalPasses = 0 -- ui state local messageQueue = List.new() local highscoreEvent = ac.OnlineEvent({ ac.StructItem.key("SwimCutupMsg"), MsgType = ac.StructItem.int64(), Payload = ac.StructItem.int64(), }, function(sender, message) if message.MsgType == 1 then recvScore = tonumber(message.Payload) if recvScore > 9999999 then highestScore = 0 else highestScore = recvScore end end end) highscoreEvent({ MsgType = 1, Payload = 0 }) function AddMessage(text, mood, duration, points) local initialDuration = duration local message = { text = text, duration = duration, mood = mood, -- 1=success, -1=error, 0=neutral points = points or 0, alpha = 1, } -- Ease-out fade update function message.update = function(dt) message.duration = message.duration - dt -- Ease-out fade local progress = math.max(0, message.duration / initialDuration) message.alpha = progress * progress if message.alpha <= 0.01 then List.popLeft(messageQueue) end end List.pushRight(messageQueue, message) end function AddCombo(amt) if comboMeter + amt > 20 then comboMeter = 20 else comboMeter = comboMeter + amt end end function OnTeleportOrPits(carId) AddMessage("You teleported!", -1, 8, 0) ResetPoints() end -- reset points, save highscore function ResetPoints() if totalScore > highestScore then highestScore = math.floor(totalScore) ac.sendChatMessage("scored " .. totalScore .. " points.") end highscoreEvent({ MsgType = 2, Payload = totalScore }) totalScore = 0 comboMeter = 1 end function GetCarAngle(car1, car2) local car1Tocar2 = (car1.pos - car2.pos):normalize() -- Calculate angles local car2Angle = math.acos(math.dot(car1Tocar2, car1.look)) * (180 / math.pi) -- Calculate cross products local crossProduct = math.cross(car1.look, car1Tocar2) -- Adjust angles based on the sign of the cross product if crossProduct.y < 0 then car2Angle = 360 - car2Angle end return car2Angle end ac.onCarJumped(0, OnTeleportOrPits) -- If player teleports, callback registered once function script.update(dt) -- Toggle settings overlay with End key local uiCheck = ac.getUI() if not uiCheck.wantCaptureKeyboard and ac.isKeyPressed(ui.KeyIndex.End) then settingsOpen = not settingsOpen end local player = ac.getCar(0) -- Get player state if player == nil then return end local sim = ac.getSim() -- get sim state timePassed = timePassed + dt -- update time if sim.carsCount > #carsState then for i = 1, sim.carsCount do carsState[i] = {} end end -- handle totaled car (only works if server has damaged enabled) if player.engineLifeLeft < 1 then ResetPoints() end -- define combo fading rate local comboFadingRate = 0.05 if player.speedKmh < 70 then comboMeter = 1 return else if comboMeter - comboFadingRate * dt < 1 then comboMeter = 1 else comboMeter = comboMeter - comboFadingRate * dt end end local angle = player.localAngularVelocity:length() / math.sqrt(3) -- calculates angle on a scale of 0 - 1 playerMultiplier = -1 -- is increased for each car that is a nearby player nearbyPlayers = -1 -- reset nearby players local nearbyCars = {} -- nearby cars -- loop through the cars to check for overtakes, (near) collisions for i = 1, sim.carsCount do -- i = 1 because lua lists start at 1 local car = ac.getCar(i - 1) -- subtracting 1 beacuse getCar has a zero based index if car == nil then return end local distance = (car.pos - player.pos):length() -- distance between car and player local posDir = (car.pos - player.pos):normalize() -- relative position vector (normalized) local posDot = math.dot(posDir, car.look) -- dot product, where car is in relation to player local state = carsState[i] -- get state of nearby car -- nearby player score multiplier if distance < playerDistance then if string.sub(ac.getDriverName(i - 1), 1, 7) ~= "Traffic" then if playerMultiplier < 4 then playerMultiplier = playerMultiplier + 1 end nearbyPlayers = nearbyPlayers + 1 end end if distance < 30 then table.insert(nearbyCars, car) end -- only check for collisions and overtakes if car is nearby if distance < 15 then -- check direction of travel local drivingAlong = math.dot(car.look, player.look) > 0.2 -- check if state exists if state.maxPosDot == nil then state.collided = false state.overtaken = false state.maxPosDot = -1 state.whitelined = false state.cut = false state.movin = false end -- check for collision with the player if car.collidedWith == 0 then if List.length(messageQueue) > 0 then if messageQueue[messageQueue.last].text == "Collision!" then messageQueue[messageQueue.last].duration = 5 else AddMessage("Collision!", -1, 8, 0) end else AddMessage("Collision!", -1, 8, 0) end ResetPoints() state.collided = true collectgarbage("collect") end -- check for overtakes if not state.overtaken and not state.collided and drivingAlong then state.maxPosDot = math.max(state.maxPosDot, posDot) if posDot < -0.2 and state.maxPosDot > 0.5 and distance < overtakeDistance then local pts = math.round(10 * (comboMeter + (playerMultiplier * 5))) AddMessage("Overtake!", 0, 2, pts) totalPasses = totalPasses + 1 totalScore = totalScore + pts state.overtaken = true end end else state.maxPosDot = -1 state.overtaken = false state.collided = false state.whitelined = false state.cut = false state.movin = false end end -- checks and balances (make sure values aren't negative) if playerMultiplier < 0 then playerMultiplier = 0 end if nearbyPlayers < 0 then nearbyPlayers = 0 end if comboMeter < 1 then comboMeter = 1 end for i, car1 in ipairs(nearbyCars) do local state1 = carsState[car1.index + 1] for j, car2 in ipairs(nearbyCars) do local state2 = carsState[car2.index + 1] if i == j then goto continue end if state1.cut or state2.cut or state1.whitelined or state2.whitelined then goto continue end if car1.index == 0 or car2.index == 0 then goto continue end if IsPlayerBetweenCars(car1, car2, player) then local car1Dot = math.dot((player.pos - car1.pos):normalize(), player.look) local car2Dot = math.dot((player.pos - car2.pos):normalize(), player.look) local car1ToCar2 = (car2.pos - car1.pos):normalize() -- Direction from car1 to car2 local dotProduct = math.dot(car1ToCar2, car1.look) local aiDot = math.dot((car1.pos - car2.pos):normalize(), car1.look) local distance = (car1.pos - car2.pos):length() if car1Dot < 0.3 and car1Dot > -0.7 and car2Dot < 0.3 and car2Dot > -0.7 and distance < 6 then local pts = math.round(100 * (comboMeter + (playerMultiplier * 5))) AddMessage("Whiteline!", 1, 4, pts) totalScore = totalScore + pts AddCombo(3) state2.whitelined = true elseif ((car1Dot > 0.5 and car2Dot < -0.5) or (car2Dot > 0.5 and car1Dot < -0.5)) and dotProduct > 0.91 and distance < 18 then local pts = math.round(30 * (comboMeter + (playerMultiplier * 5))) AddMessage("Cut!", 1, 12, pts) totalScore = totalScore + pts state1.cut = true state2.cut = true AddCombo(1) elseif state1.movin == false and state2.movin == false and dotProduct > 0.7 then local pts = math.round(20 * (comboMeter + (playerMultiplier * 5))) AddMessage("Movin!", 0, 4, pts) totalScore = totalScore + pts state1.movin = true state2.movin = true AddCombo(0.5) end end ::continue:: end end for i = messageQueue.first, messageQueue.last do local message = messageQueue[i] if message then message.update(dt) end end -- print debug info ac.debug("total passes", tostring(totalPasses)) ac.debug("angle", tostring(angle)) ac.debug("score", tostring(totalScore)) ac.debug("nearby players", tostring(nearbyPlayers)) ac.debug("combo", tostring(comboMeter)) ac.debug("fading combo", tostring(comboFadingRate)) collectgarbage("step") end function script.drawUI() -- Draw settings overlay (always available, independent of HUD toggle) if settingsOpen then drawSettingsPanel() end -- Skip HUD if disabled if not Settings.hudEnabled then return end local uiState = ac.getUI() -- Update animations UI.pulsePhase = (UI.pulsePhase + uiState.dt * 1.5) % (math.pi * 2) UI.statusPhase = (UI.statusPhase + uiState.dt * 2) % (math.pi * 2) -- Calculate accent pulse brightness local accentPulse = 0.9 + math.sin(UI.pulsePhase) * 0.1 local accentColor = rgbm(Theme.accent.r * accentPulse, Theme.accent.g * accentPulse, Theme.accent.b * accentPulse, 1) -- Trim old messages (max 3) if List.length(messageQueue) > 0 then while List.length(messageQueue) > 3 or (messageQueue[messageQueue.first] and messageQueue[messageQueue.first].alpha < 0.01) do if List.length(messageQueue) == 0 then break end List.popLeft(messageQueue) end end -- Fixed height based on compact mode (no longer grows with messages) local dynamicHeight if Settings.compactMode then dynamicHeight = 170 -- Compact height (no messages) else dynamicHeight = 300 -- Fixed height with messages section end -- Calculate position based on Settings.hudPosition (1-4, Lua 1-indexed) local screenW, screenH = uiState.windowSize.x, uiState.windowSize.y local margin = 32 local posX, posY if Settings.hudPosition == 1 then -- Top-Left posX, posY = margin, margin elseif Settings.hudPosition == 2 then -- Top-Right posX, posY = screenW - UI.width - margin, margin elseif Settings.hudPosition == 3 then -- Bottom-Left posX, posY = margin, screenH - dynamicHeight - margin else -- Bottom-Right posX, posY = screenW - UI.width - margin, screenH - dynamicHeight - margin end -- Window setup local windowPos = vec2(posX, posY) local windowSize = vec2(UI.width, dynamicHeight) ui.beginTransparentWindow("swimHUD", windowPos, windowSize, true) -- === MEASURE FONTS === local cx = UI.padding ui.pushFont(ui.Font.Huge) local hugeHeight = ui.measureText("0").y ui.popFont() ui.pushFont(ui.Font.Main) local mainHeight = ui.measureText("0").y ui.popFont() ui.pushFont(ui.Font.Small) local smallHeight = ui.measureText("0").y ui.popFont() -- === CALCULATE LAYOUT POSITIONS === local cy = 6 local headerY = cy cy = cy + hugeHeight + 2 local scoreY = cy cy = cy + hugeHeight + 6 local pillY = cy local pillHeight = 24 local highlightEndY = pillY + pillHeight + 4 cy = cy + pillHeight + 8 local metaY = cy cy = cy + (smallHeight * 2) + 6 -- === PANEL BACKGROUNDS === drawPanel(0, 0, UI.width, dynamicHeight, UI.cornerRadius, Theme.glass, nil) -- Top highlight sheen (covers header, score, and multipliers only) ui.drawRectFilled(vec2(0, 0), vec2(UI.width, highlightEndY), Theme.glassHighlight, UI.cornerRadius) -- Accent glow strip at top drawGlowStrip(0, 0, UI.width, accentColor) -- === CONTENT === -- Header: brand ui.setCursor(vec2(cx, headerY)) ui.pushFont(ui.Font.Huge) ui.textColored("swim", accentColor) ui.sameLine(0, 0) ui.textColored(">", Theme.textMuted) ui.popFont() -- Score ui.setCursor(vec2(cx, scoreY)) ui.pushFont(ui.Font.Huge) ui.textColored(tostring(math.floor(totalScore)), Theme.textHero) ui.sameLine(0, 6) ui.pushFont(ui.Font.Main) ui.textColored("pts", Theme.textMuted) ui.popFont() ui.popFont() -- Multipliers (pills) local comboText = string.format("%.1fx", comboMeter) local bonusText = string.format("%dx", nearbyPlayers * 5) -- Combo pill drawPanel(cx, pillY, 60, pillHeight, 8, Theme.glassSurface, nil) ui.setCursor(vec2(cx + 10, pillY + 4)) ui.textColored(comboText, Theme.textPrimary) -- Plus sign ui.setCursor(vec2(cx + 70, pillY + 4)) ui.textColored("+", Theme.textGhost) -- Bonus pill (accent tinted) drawPanel(cx + 86, pillY, 50, pillHeight, 8, Theme.accentSubtle, nil) ui.setCursor(vec2(cx + 96, pillY + 4)) ui.textColored(bonusText, accentColor) -- Meta row: best + nearby ui.pushFont(ui.Font.Small) ui.setCursor(vec2(cx, metaY)) ui.textColored("BEST", Theme.textGhost) ui.setCursor(vec2(cx, metaY + smallHeight + 2)) ui.textColored(tostring(highestScore), Theme.textSecondary) ui.setCursor(vec2(cx + 110, metaY)) ui.textColored("NEARBY", Theme.textGhost) ui.setCursor(vec2(cx + 110, metaY + smallHeight + 2)) ui.textColored(tostring(nearbyPlayers) .. " drivers", Theme.textSecondary) ui.popFont() -- Messages section (hidden in compact mode) if not Settings.compactMode then -- Divider ui.drawRectFilled(vec2(cx, cy), vec2(UI.width - cx, cy + 1), Theme.textGhost) cy = cy + 8 -- Messages (fixed area, max 3 messages) local msgWidth = UI.width - (cx * 2) local msgCount = 0 for i = messageQueue.first, messageQueue.last do local msg = messageQueue[i] if msg and msg.alpha > 0.01 and msgCount < 3 then drawMessage(cx, cy, msgWidth, msg.text, msg.points or 0, msg.mood or 0, msg.alpha) cy = cy + 24 msgCount = msgCount + 1 end end end ui.endTransparentWindow() end