Brookhaven Studio
Key RequiredUniversal

Brookhaven Studio

Universal ScriptObsidianWorksObsidianWorks 447 views 1.2K executesUpdated Oct 2, 2026
Unrated
00
Lua Script•3636 lines (136168 chars)
--[[
    Brookhaven Studio  v6
    Car, stunt, RGB, player and house suite for Brookhaven RP, in the Snitch interface
    (black and white).

    Every server action goes through Brookhaven's own requests, so it only does what your
    gamepasses already unlock. Handling, braking and stunts are physics on the car you are
    driving (your client simulates it); top speed stays at the server's cap for your passes.

    RightShift: open / close the menu.   X: stunt.   C: car hop.   Hold Q: drift.
    Any jump and the car does a trick by itself (Stunts -> Air Stunts / Every Jump).
]]

local ENV = getgenv()
-- Re-running keeps the window hidden if you had hidden it.
local startShown = not (ENV.BrookhavenStudio and ENV.BrookhavenStudio.IsShown and not ENV.BrookhavenStudio.IsShown())
if ENV.BrookhavenStudio and ENV.BrookhavenStudio.Unload then
    local ok, err = pcall(ENV.BrookhavenStudio.Unload)
    if not ok then
        warn("[Brookhaven Studio] previous instance failed to unload: " .. tostring(err))
    end
end

local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TweenService = game:GetService("TweenService")
local UserInputService = game:GetService("UserInputService")
local RunService = game:GetService("RunService")
local MarketplaceService = game:GetService("MarketplaceService")
local HttpService = game:GetService("HttpService")
local Lighting = game:GetService("Lighting")
local VirtualUser = game:GetService("VirtualUser")

local LocalPlayer = Players.LocalPlayer
local Remotes = ReplicatedStorage:WaitForChild("Remotes")
local LegacyRemotes = ReplicatedStorage:WaitForChild("RE")
local Gamepasses = require(ReplicatedStorage.Modules.Shared.PlayerData.Gamepasses)

---------------------------------------------------------------------------------------------
-- Game bridge
---------------------------------------------------------------------------------------------

-- Brookhaven pads its legacy remote names with "1"s (e.g. "1Ca1r"), so match on the cleaned name.
local function legacyRemote(cleanName)
    for _, child in ipairs(LegacyRemotes:GetChildren()) do
        if (child.Name:gsub("1", "")) == cleanName then
            return child
        end
    end
    error("[Brookhaven Studio] RE." .. cleanName .. " not found - the game may have updated")
end

local function remote(name)
    local r = Remotes:FindFirstChild(name)
    if not r then
        error("[Brookhaven Studio] Remotes." .. name .. " not found - the game may have updated")
    end
    return r
end

local CarEvent = legacyRemote("Car")
local HouseEvent = legacyRemote("PlayersHouse")
local NameColorEvent = legacyRemote("RPNameColor")
local NameTextEvent = legacyRemote("RPNameText")
local PropertySetColor = remote("Property:SetColor")
local HouseNumber = LocalPlayer:WaitForChild("PlayersBag"):WaitForChild("HouseNumber")

-- pcall-style: called (did the request itself run), then the server's own return values.
local function invoke(name, ...)
    local args = table.pack(...)
    return pcall(function()
        return remote(name):InvokeServer(table.unpack(args, 1, args.n))
    end)
end

local ownedCache = {}
local function owns(key)
    if ownedCache[key] ~= nil then
        return ownedCache[key]
    end
    local pass = Gamepasses[key]
    if pass == nil then
        error("[Brookhaven Studio] unknown gamepass " .. key)
    end
    local ok, result = pcall(function()
        return MarketplaceService:UserOwnsGamePassAsync(LocalPlayer.UserId, Gamepasses.GetId(pass))
    end)
    if not ok then
        warn("[Brookhaven Studio] couldn't check " .. key .. ": " .. tostring(result))
        return false
    end
    ownedCache[key] = result == true
    return ownedCache[key]
end

-- Mirrors Brookhaven's VehicleSpeedUtil.GetMaxSpeedForPlayer.
local function topSpeed()
    if owns("VEHICLE_SPEED_UNLOCKED") then return 200 end
    if owns("VEHICLE_UPGRADE") then return 100 end
    if owns("PREMIUM") then return 75 end
    return 55
end

-- Best-feeling cars first. pass = the gamepass Brookhaven's catalog requires for it.
local CARS = {
    { id = "Bugatti", label = "Bugatti", pass = "PREMIUM" },
    { id = "Hypercar", label = "Hypercar", pass = "PREMIUM" },
    { id = "Ferrari", label = "Ferrari", pass = "PREMIUM" },
    { id = "PremiumRacer", label = "Premium Racer", pass = "PREMIUM" },
    { id = "Formula1", label = "Formula 1", pass = "VEHICLE_PACK" },
    { id = "SuperCar", label = "Super Car", pass = "VEHICLE_PACK" },
    { id = "Lamboego", label = "Lambo", pass = "VEHICLE_PACK" },
    { id = "Skyline", label = "Skyline", pass = "VEHICLE_PACK" },
    { id = "CyberTruck", label = "Cybertruck", pass = "VEHICLE_PACK" },
    { id = "BatMobile", label = "Batmobile", pass = "VIP" },
    { id = "FordGT", label = "Ford GT" },
    { id = "350Z", label = "350Z" },
    { id = "Challenger", label = "Challenger" },
}

local UNDERGLOWS = { "Scrolling Beams", "Scrolling 3 Colors", "Scrolling Split", "One Color", "Front Back", "XGlow", "ShockWave", "Futuristic" }
local RGB_MODES = { "Rainbow", "Pastel", "Neon pulse", "Ocean", "Sunset", "Galaxy", "Fire", "Police" }

-- Gradient modes: the color glides through these stops and loops back to the first.
local RGB_PALETTES = {
    Ocean = { Color3.fromRGB(0, 90, 255), Color3.fromRGB(0, 200, 255), Color3.fromRGB(0, 255, 200), Color3.fromRGB(0, 140, 255) },
    Sunset = { Color3.fromRGB(255, 94, 58), Color3.fromRGB(255, 149, 0), Color3.fromRGB(255, 59, 130), Color3.fromRGB(140, 60, 255) },
    Galaxy = { Color3.fromRGB(120, 40, 255), Color3.fromRGB(255, 60, 200), Color3.fromRGB(40, 80, 255), Color3.fromRGB(180, 90, 255) },
    Fire = { Color3.fromRGB(255, 30, 0), Color3.fromRGB(255, 120, 0), Color3.fromRGB(255, 200, 0), Color3.fromRGB(255, 80, 0) },
}

local SWATCHES = {
    Color3.fromRGB(245, 245, 247),
    Color3.fromRGB(24, 24, 28),
    Color3.fromRGB(77, 201, 240),
    Color3.fromRGB(155, 120, 255),
    Color3.fromRGB(255, 120, 170),
    Color3.fromRGB(255, 196, 92),
    Color3.fromRGB(96, 214, 150),
}

-- Stunts rotate the car around one of its own axes (X = pitch, Y = yaw, Z = roll).
-- Positive X turns = nose up and over the back; positive Z turns = rolling to the left.
local STUNTS = {
    { name = "Backflip", axis = "X", turns = 1 },
    { name = "Frontflip", axis = "X", turns = -1 },
    { name = "Barrel roll left", axis = "Z", turns = 1 },
    { name = "Barrel roll right", axis = "Z", turns = -1 },
    { name = "360 spin", axis = "Y", turns = 1 },
    { name = "Double backflip", axis = "X", turns = 2 },
    { name = "Corkscrew", axis = "YZ", turns = 1 },
}

---------------------------------------------------------------------------------------------
-- Settings (persisted in Solara's workspace folder)
---------------------------------------------------------------------------------------------

local SETTINGS_FILE = "BrookhavenStudio.json"
local Settings = {
    car = "Bugatti",
    autoTune = true,
    performance = true,
    accel = 2.0,
    steer = 1.4,
    handling = true,
    grip = true,
    tireGrip = 2.0,
    traction = 70,
    antiRoll = true,
    brakeAssist = true,
    brakeStrength = 1.2,
    autoFlip = false,
    rideHeight = 0,
    underglow = "Scrolling Beams",
    glowLength = 1.0,
    glowWidth = 1.3,
    rainbowGlow = false,
    carRGB = false,
    rgbMode = "Rainbow",
    rgbCycle = 6,
    stunt = "Backflip",
    stuntAirtime = 1.2,
    hopAirtime = 0.6,
    rpName = "",
    rpBio = "",
    rainbowName = false,
    antiAfk = true,
    speedo = true,
    lockTime = false,
    hour = 14,
    fullbright = false,
    fovOn = false,
    fov = 85,
    unlockZoom = false,
    houseRainbow = true,
    houseCycle = 20,
    loadBuild = false,
    buildSlot = "",
    airStunts = true,
    airTrick = "Backflip",
    airAlways = true,
    airMin = 0.25,
    landAssist = true,
    stuntTrail = true,
    drift = true,
    driftGrip = 0.35,
    autoLights = false,
    arraylist = true,
    toggleNotify = true,
    blur = true,
    binds = { Stunt = "X", ["Car Hop"] = "C", Drift = "Q" },
    schema = 0,
}

local SETTINGS_SCHEMA = 4

if isfile and readfile and isfile(SETTINGS_FILE) then
    local ok, data = pcall(function()
        return HttpService:JSONDecode(readfile(SETTINGS_FILE))
    end)
    if ok and type(data) == "table" then
        for key, value in pairs(data) do
            if Settings[key] ~= nil and type(value) == type(Settings[key]) then
                Settings[key] = value
            end
        end
    else
        warn("[Brookhaven Studio] settings file unreadable, using defaults")
    end
end

-- Older saves forced ride height level 1 on every car (it made cars scrape and flip), and
-- predate some keybinds; move them over once.
if Settings.schema < 3 then
    Settings.rideHeight = 0
end
if Settings.schema < SETTINGS_SCHEMA then
    for name, key in pairs({ Stunt = "X", ["Car Hop"] = "C", Drift = "Q" }) do
        if Settings.binds[name] == nil then
            Settings.binds[name] = key
        end
    end
    Settings.schema = SETTINGS_SCHEMA
end

local alive = true
local savePending = false
local function saveSoon()
    if savePending or not writefile then return end
    savePending = true
    task.delay(0.8, function()
        savePending = false
        if alive then
            writefile(SETTINGS_FILE, HttpService:JSONEncode(Settings))
        end
    end)
end

---------------------------------------------------------------------------------------------
-- Interface toolkit: the Snitch look (flat black box, white type, grey hierarchy, no color)
---------------------------------------------------------------------------------------------

local T = {
    bg = Color3.fromRGB(11, 11, 12),
    side = Color3.fromRGB(14, 14, 16),
    raised = Color3.fromRGB(20, 20, 22),
    line = Color3.fromRGB(30, 30, 33),
    off = Color3.fromRGB(44, 44, 48),
    white = Color3.fromRGB(245, 245, 245),
    text = Color3.fromRGB(205, 205, 208),
    sub = Color3.fromRGB(120, 120, 124),
    dim = Color3.fromRGB(72, 72, 76),
    black = Color3.fromRGB(0, 0, 0),
}
local FONT = Enum.Font.BuilderSansMedium
local FONT_BOLD = Enum.Font.BuilderSansBold
local FONT_BLACK = Enum.Font.BuilderSansExtraBold
local EASE = TweenInfo.new(0.16, Enum.EasingStyle.Quint, Enum.EasingDirection.Out)

local connections = {}
local function bind(signal, fn)
    local c = signal:Connect(fn)
    table.insert(connections, c)
    return c
end

local order = 0
local function nextOrder()
    order += 1
    return order
end

local function make(className, props, children)
    local inst = Instance.new(className)
    for key, value in pairs(props or {}) do
        if key ~= "Parent" then
            inst[key] = value
        end
    end
    for _, child in ipairs(children or {}) do
        child.Parent = inst
    end
    inst.Parent = props and props.Parent
    return inst
end

local function tween(inst, props, info)
    local tw = TweenService:Create(inst, info or EASE, props)
    tw:Play()
    return tw
end

local function label(props)
    props.BackgroundTransparency = 1
    props.Font = props.Font or FONT
    props.TextColor3 = props.TextColor3 or T.text
    props.TextXAlignment = props.TextXAlignment or Enum.TextXAlignment.Left
    return make("TextLabel", props)
end

local function corner(radius)
    return make("UICorner", { CornerRadius = radius and UDim.new(0, radius) or UDim.new(1, 0) })
end

local function pad(left, right, top, bottom)
    return make("UIPadding", {
        PaddingLeft = UDim.new(0, left or 0),
        PaddingRight = UDim.new(0, right or 0),
        PaddingTop = UDim.new(0, top or 0),
        PaddingBottom = UDim.new(0, bottom or 0),
    })
end

do
    local oldBlur = Lighting:FindFirstChild("BrookhavenStudioBlur")
    if oldBlur then
        oldBlur:Destroy()
    end
end

local Gui = make("ScreenGui", {
    Name = "BrookhavenStudio",
    ResetOnSpawn = false,
    IgnoreGuiInset = true,
    ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
    DisplayOrder = 60,
    Parent = (gethui and gethui()) or game:GetService("CoreGui"),
})

-- Everything is designed at 1100x720 and shrinks on smaller screens.
local function computeScale()
    local viewport = workspace.CurrentCamera.ViewportSize
    return math.clamp(math.min(viewport.X / 1100, viewport.Y / 720), 0.6, 1)
end
local guiScale = computeScale()
local scaleObjects = {}
local function scaled(inst)
    table.insert(scaleObjects, make("UIScale", { Scale = guiScale, Parent = inst }))
    return inst
end

-- Notifications (bottom right). Fixed-width cards: title, detail line, draining timer line.
-- Each sits in a slot whose height collapses on exit, so the stack reflows smoothly.
-- Mark: solid white dot = done, hollow ring = refused or a problem, none = info.
local TOAST_W, TOAST_H, TOAST_TIME = 316, 66, 4
local toastHolder = scaled(make("Frame", {
    AnchorPoint = Vector2.new(1, 1),
    Position = UDim2.new(1, -20, 1, -20),
    Size = UDim2.fromOffset(TOAST_W, 420),
    BackgroundTransparency = 1,
    Parent = Gui,
}, {
    make("UIListLayout", { VerticalAlignment = Enum.VerticalAlignment.Bottom, SortOrder = Enum.SortOrder.LayoutOrder }),
}))
local toastCount = 0

local function notify(title, message, kind)
    print("[Brookhaven Studio] " .. title .. ": " .. message)
    if not alive then return end
    toastCount += 1
    local slot = make("Frame", {
        LayoutOrder = toastCount,
        Size = UDim2.fromOffset(TOAST_W, 0),
        BackgroundTransparency = 1,
        Parent = toastHolder,
    })
    local card = make("CanvasGroup", {
        AnchorPoint = Vector2.new(0, 1),
        Position = UDim2.new(0, 48, 1, 0),
        Size = UDim2.fromOffset(TOAST_W, TOAST_H),
        BackgroundColor3 = T.bg,
        BackgroundTransparency = 0.04,
        BorderSizePixel = 0,
        GroupTransparency = 1,
        Parent = slot,
    }, {
        corner(10),
        make("UIStroke", { Color = T.line, ApplyStrokeMode = Enum.ApplyStrokeMode.Border }),
    })
    local textX = 18
    if kind == "good" or kind == "bad" then
        local dot = make("Frame", {
            AnchorPoint = Vector2.new(0, 0.5),
            Position = UDim2.new(0, 18, 0.5, 0),
            Size = UDim2.fromOffset(8, 8),
            BackgroundColor3 = kind == "good" and T.white or T.bg,
            BorderSizePixel = 0,
            Parent = card,
        }, { corner() })
        if kind == "bad" then
            make("UIStroke", { Color = T.white, Thickness = 1.5, Parent = dot })
        end
        textX = 38
    end
    label({
        Text = title,
        Font = FONT_BOLD,
        TextSize = 16,
        TextColor3 = T.white,
        Position = UDim2.new(0, textX, 0, 14),
        Size = UDim2.new(1, -textX - 16, 0, 20),
        TextTruncate = Enum.TextTruncate.AtEnd,
        Parent = card,
    })
    label({
        Text = message,
        TextSize = 14,
        TextColor3 = T.sub,
        Position = UDim2.new(0, textX, 0, 34),
        Size = UDim2.new(1, -textX - 16, 0, 18),
        TextTruncate = Enum.TextTruncate.AtEnd,
        Parent = card,
    })
    local timer = make("Frame", {
        AnchorPoint = Vector2.new(0, 1),
        Position = UDim2.new(0, 0, 1, 0),
        Size = UDim2.new(1, 0, 0, 2),
        BackgroundColor3 = T.white,
        BackgroundTransparency = 0.35,
        BorderSizePixel = 0,
        Parent = card,
    })
    tween(slot, { Size = UDim2.fromOffset(TOAST_W, TOAST_H + 10) }, TweenInfo.new(0.3, Enum.EasingStyle.Quint, Enum.EasingDirection.Out))
    tween(card, { Position = UDim2.new(0, 0, 1, 0), GroupTransparency = 0 }, TweenInfo.new(0.38, Enum.EasingStyle.Quint, Enum.EasingDirection.Out))
    tween(timer, { Size = UDim2.new(0, 0, 0, 2) }, TweenInfo.new(TOAST_TIME, Enum.EasingStyle.Linear))
    task.delay(TOAST_TIME, function()
        if not card.Parent then return end
        tween(card, { Position = UDim2.new(0, 48, 1, 0), GroupTransparency = 1 }, TweenInfo.new(0.3, Enum.EasingStyle.Quint, Enum.EasingDirection.In))
        task.wait(0.28)
        if not slot.Parent then return end
        tween(slot, { Size = UDim2.fromOffset(TOAST_W, 0) }, TweenInfo.new(0.28, Enum.EasingStyle.Quint, Enum.EasingDirection.Out))
        task.wait(0.3)
        slot:Destroy()
    end)
end

---------------------------------------------------------------------------------------------
-- Cars
---------------------------------------------------------------------------------------------

local function vehiclesFolder()
    return workspace:FindFirstChild("Vehicles")
end

local function isMine(car)
    local owner = car and car:FindFirstChild("Owner")
    return owner ~= nil and owner:IsA("StringValue") and owner.Value == LocalPlayer.Name
end

local function myCar()
    local folder = vehiclesFolder()
    if not folder then return nil end
    for _, model in ipairs(folder:GetChildren()) do
        if isMine(model) then return model end
    end
    return nil
end

local function carFromPart(part)
    local folder = vehiclesFolder()
    if not folder then return nil end
    local node = part
    while node and node.Parent ~= folder do
        node = node.Parent
    end
    return node
end

-- Brookhaven paints the parts inside Body.Color; fall back to the car's configured default.
local function currentPaint(car)
    local body = car:FindFirstChild("Body")
    local colorModel = body and body:FindFirstChild("Color")
    if colorModel then
        for _, part in ipairs(colorModel:GetDescendants()) do
            if part:IsA("BasePart") then return part.Color end
        end
    end
    local config = car:FindFirstChild("Config")
    local default = config and config:FindFirstChild("DefaultColor")
    return default and default.Value or nil
end

local Session = nil
-- Your car's own paint, remembered per car so Car RGB can put it back after the car has been
-- recolored for a while (driving or parked).
local originalPaints = setmetatable({}, { __mode = "k" })

-- Your car, driven or parked. Parked cars are looked up at most once a second.
local cachedCar, nextCarScan = nil, 0
local function ownCar()
    if Session then
        return Session.car
    end
    if cachedCar and cachedCar.Parent and isMine(cachedCar) then
        return cachedCar
    end
    cachedCar = nil
    if os.clock() >= nextCarScan then
        nextCarScan = os.clock() + 1
        cachedCar = myCar()
        if cachedCar and originalPaints[cachedCar] == nil then
            originalPaints[cachedCar] = currentPaint(cachedCar)
        end
    end
    return cachedCar
end

-- Paint requests carry the car's id, so they work from inside or outside the car.
local function paintCar(car, color)
    local called, ok, msg = invoke("SetColor", car:GetAttribute("VehicleUuid"), color)
    if not called then
        return false, tostring(ok)
    end
    return ok == true, msg
end
local tunedCars = setmetatable({}, { __mode = "k" })
local refreshModule -- set once the module system exists
local setVehicleStatus

-- Handling tweaks only go on regular 4-wheel cars (two steering wheels at the front, two
-- driven wheels at the back). Bikes, boats and odd vehicles keep Brookhaven's stock physics,
-- because car tuning on them causes wheelies and flips.
local function accelFor(s)
    return (s.geom.valid and Settings.performance) and Settings.accel or 1
end

local function steerFor()
    return Settings.performance and Settings.steer or 1
end

local function applyPhysics()
    local s = Session
    if not s then return end
    local accel = accelFor(s)
    s.seat.Torque = s.baseTorque * accel
    s.seat.TurnSpeed = s.baseTurn * steerFor()
    local baseTurbo = s.turbo and tonumber(s.baseTurbo)
    if baseTurbo then
        s.turboExpected = string.format("%.3f", baseTurbo * accel)
        s.turbo.Value = s.turboExpected
    end
end

-- Tires: Brookhaven's "drift strength" is literally wheel friction (0 = ice, 1.5 = its max).
-- While you drive, your client simulates the car, so we raise friction and its weight
-- locally: the tire, not the road surface, then decides how much grip there is. Grip is
-- capped per car (see analyzeCar) so tall vehicles can't out-grip their own balance and flip.
local TIRE_FRICTION_WEIGHT = 10
local STOCK_FRICTION = 1

local function tireFriction(s)
    return math.min(Settings.tireGrip, math.max(s.geom.gripLimit, STOCK_FRICTION))
end

local function tuningTires(s)
    return s.geom.valid and Settings.handling
end

-- The friction a wheel should have right now, or nil for the car's own tire. Drift Mode drops
-- the rear wheels' grip while the drift key is held.
local function wheelFriction(s, wheel)
    if s.drifting and s.rearWheels[wheel] then
        return Settings.driftGrip
    end
    return tuningTires(s) and tireFriction(s) or nil
end

local function applyTires()
    local s = Session
    if not s then return end
    for wheel, original in pairs(s.wheelProps) do
        if wheel.Parent then
            local friction = wheelFriction(s, wheel)
            if friction then
                wheel.CustomPhysicalProperties = PhysicalProperties.new(
                    original.Density, friction, original.Elasticity, TIRE_FRICTION_WEIGHT, original.ElasticityWeight
                )
            else
                wheel.CustomPhysicalProperties = original
            end
        end
    end
end

local groundParams = RaycastParams.new()
groundParams.FilterType = Enum.RaycastFilterType.Exclude

-- Brookhaven's own reference frame for a car is Chassis.Platform (its drive code measures
-- speed along Platform.LookVector). Seats can be tilted or turned, so never use them for axes.
local function carFrame(s)
    local platform = s.geom.platform
    if platform and platform.Parent then
        return platform.CFrame
    end
    return s.seat.CFrame
end

-- Measures the car: wheel layout, centre-of-mass height, and how much tire grip it can take
-- before braking, accelerating or cornering would tip it over.
local function analyzeCar(car, seat, root)
    local chassis = car:FindFirstChild("Chassis")
    local platform = chassis and chassis:FindFirstChild("Platform")
    if platform and not platform:IsA("BasePart") then
        platform = nil
    end
    local frame = platform and platform.CFrame or seat.CFrame
    local front, rear, all = {}, {}, {}
    for _, d in ipairs(car:GetDescendants()) do
        local steering = d:HasTag("WheelSteering")
        if steering or d:HasTag("Wheel") then
            local holder = d:FindFirstChild("AttachmentHolder") or d:FindFirstChild("PhysicalWheel")
            local physical = d:FindFirstChild("PhysicalWheel")
            if holder and holder:IsA("BasePart") then
                local wheel = {
                    frameLocal = frame:PointToObjectSpace(holder.Position),
                    radius = physical and physical:IsA("BasePart") and physical.Size.Y / 2 or 1,
                    -- Bottom of the real tire (AttachmentHolder sits up at the suspension mount).
                    contact = physical and physical:IsA("BasePart") and frame:PointToObjectSpace(physical.Position).Y - physical.Size.Y / 2 or nil,
                }
                table.insert(all, wheel)
                table.insert(steering and front or rear, wheel)
            end
        end
    end
    local extents = car:GetExtentsSize()
    local geom = { valid = false, platform = platform, wheels = #all, gripLimit = STOCK_FRICTION, probe = extents.Y + 3 }
    if #all < 4 or #front < 2 or #rear < 2 then
        return geom
    end
    local function averageOf(list)
        local sum = Vector3.zero
        for _, w in ipairs(list) do
            sum += w.frameLocal
        end
        return sum / #list
    end
    local frontLocal, rearLocal = averageOf(front), averageOf(rear)
    local minX, maxX, contactY, radius = math.huge, -math.huge, 0, 0
    for _, w in ipairs(all) do
        minX = math.min(minX, w.frameLocal.X)
        maxX = math.max(maxX, w.frameLocal.X)
        contactY += w.frameLocal.Y - w.radius
        radius += w.radius
    end
    contactY /= #all
    radius /= #all
    local com = frame:PointToObjectSpace(root.AssemblyCenterOfMass)
    local cgHeight = math.max(com.Y - contactY, 0.25)
    local halfTrack = (maxX - minX) / 2
    local frontDist = math.abs(com.Z - frontLocal.Z)
    local rearDist = math.abs(rearLocal.Z - com.Z)
    -- Static stability: once tire friction exceeds min(half track, axle distance) / CG height,
    -- the tires out-grip the car's balance and it flips. Anti-flip covers the margin above it.
    local stability = math.min(halfTrack, frontDist, rearDist) / cgHeight
    geom.valid = true
    geom.stability = stability
    geom.gripLimit = stability
    geom.halfTrack = halfTrack
    geom.frontDist = frontDist
    geom.rearDist = rearDist
    geom.cgHeight = cgHeight
    geom.wheelbase = frontDist + rearDist
    geom.radius = radius
    geom.probe = radius * 2 + 3
    -- How high the root part rides above the tire contact patches.
    local contactSum, contactCount = 0, 0
    for _, w in ipairs(all) do
        if w.contact then
            contactSum += w.contact
            contactCount += 1
        end
    end
    if contactCount > 0 then
        geom.rootHeight = frame:PointToObjectSpace(root.Position).Y - contactSum / contactCount
    end
    -- Axle centres, relative to the root part so forces can be attached there.
    geom.frontRoot = root.CFrame:PointToObjectSpace(frame:PointToWorldSpace(frontLocal))
    geom.rearRoot = root.CFrame:PointToObjectSpace(frame:PointToWorldSpace(rearLocal))
    return geom
end

local function ensureHandlingParts(s, root)
    if s.root == root and s.force and s.force.Parent == root then return end
    for _, inst in ipairs({ s.force, s.attach, s.align, s.alignAttach, s.rearForce, s.rearAttach }) do
        inst:Destroy()
    end
    s.root = root
    local frame = carFrame(s)
    s.attach = make("Attachment", { Name = "BHS_Handling", Parent = root })
    s.force = make("VectorForce", {
        Name = "BHS_Handling",
        Attachment0 = s.attach,
        RelativeTo = Enum.ActuatorRelativeTo.World,
        ApplyAtCenterOfMass = true,
        Force = Vector3.zero,
        Parent = root,
    })
    -- Stability control: a sideways force applied at the rear axle. Cancelling the rear tires'
    -- slide there also creates the yaw torque that stops the tail swinging out (no spin-outs),
    -- without fighting the steering the way a force at the car's centre does.
    s.rearAttach = make("Attachment", { Name = "BHS_RearAxle", Position = s.geom.rearRoot, Parent = root })
    s.rearForce = make("VectorForce", {
        Name = "BHS_RearAxle",
        Attachment0 = s.rearAttach,
        RelativeTo = Enum.ActuatorRelativeTo.World,
        ApplyAtCenterOfMass = false,
        Force = Vector3.zero,
        Parent = root,
    })
    -- Anti-flip: aligns the car's up axis with the road; yaw (steering) stays free. Its torque
    -- is zero until the car tilts past normal body roll/pitch (see updateHandling).
    s.alignAttach = make("Attachment", { Name = "BHS_Stability", Parent = root })
    s.alignAttach.WorldAxis = frame.UpVector
    s.alignAttach.WorldSecondaryAxis = frame.LookVector
    s.align = make("AlignOrientation", {
        Name = "BHS_Stability",
        Mode = Enum.OrientationAlignmentMode.OneAttachment,
        Attachment0 = s.alignAttach,
        AlignType = Enum.AlignType.PrimaryAxisParallel,
        PrimaryAxis = Vector3.yAxis,
        RigidityEnabled = false,
        Responsiveness = 15,
        MaxTorque = 0,
        MaxAngularVelocity = 6,
        Enabled = false,
        Parent = root,
    })
end

local ANTI_FLIP_DEADZONE = 5 -- degrees of tilt allowed before it starts helping
local ANTI_FLIP_RAMP = 10 -- degrees over which it ramps to full strength

local function updateHandling(dt)
    local s = Session
    if not s or not s.geom.valid then return end
    local root = s.seat.AssemblyRootPart
    if not root then return end
    ensureHandlingParts(s, root)
    if s.stunting then
        -- The stunt controller owns the car's motion until it lands.
        s.force.Force = Vector3.zero
        s.rearForce.Force = Vector3.zero
        s.align.Enabled = false
        return
    end

    local frame = carFrame(s)
    local up, right, look = frame.UpVector, frame.RightVector, frame.LookVector
    local gravity = workspace.Gravity
    groundParams.FilterDescendantsInstances = { s.car, LocalPlayer.Character }
    local frontPoint = root.CFrame:PointToWorldSpace(s.geom.frontRoot)
    local rearPoint = root.CFrame:PointToWorldSpace(s.geom.rearRoot)
    local frontHit = workspace:Raycast(frontPoint, -up * s.geom.probe, groundParams)
    local rearHit = workspace:Raycast(rearPoint, -up * s.geom.probe, groundParams)
    local grounded = frontHit ~= nil or rearHit ~= nil
    local v = root.AssemblyLinearVelocity

    local force = Vector3.zero
    if Settings.handling and Settings.grip and grounded then
        -- Downforce that builds with speed, only while the wheels are on the road.
        local flatSpeed = Vector3.new(v.X, 0, v.Z).Magnitude
        force += Vector3.new(0, -root.AssemblyMass * gravity * math.clamp(flatSpeed / 120, 0, 1) * 0.5, 0)
    end
    if Settings.brakeAssist and grounded then
        -- Brake assist: when you press against the direction you're moving, add braking force,
        -- capped by how far this car's weight can shift before the rear lifts (no stoppies).
        local forward = v:Dot(look)
        local throttle = s.seat.ThrottleFloat
        if (forward > 4 and throttle < -0.1) or (forward < -4 and throttle > 0.1) then
            local balance = math.min(s.geom.frontDist, s.geom.rearDist) / s.geom.cgHeight
            local decel = gravity * math.min(Settings.brakeStrength, balance * 0.9) * math.abs(throttle)
            -- Never brake harder than it takes to stop this frame, so it can't push backwards.
            decel = math.min(decel, math.abs(forward) / math.max(dt, 1 / 240))
            force += look * (forward > 0 and -1 or 1) * s.mass * decel
        end
    end
    s.force.Force = force

    if Settings.handling and rearHit and Settings.traction > 0 and not s.drifting then
        local lateral = root:GetVelocityAtPosition(rearPoint):Dot(right)
        -- Never push harder sideways than this car's balance allows (tall vehicles tip).
        local cap = s.mass * gravity * math.min(1.2, s.geom.stability)
        -- Gain up to 5/s keeps the correction stable even at 30 fps.
        local push = math.clamp(-lateral * s.mass * (Settings.traction / 100) * 5, -cap, cap)
        s.rearForce.Force = right * push
    else
        s.rearForce.Force = Vector3.zero
    end

    -- Target "up": no roll against world up, pitch following the road slope measured under the
    -- front and rear axles (not the normal of whatever face a single ray hits).
    local strength = 0
    if Settings.handling and Settings.antiRoll and grounded then
        local roadForward = frontHit and rearHit and (frontHit.Position - rearHit.Position) or nil
        if not roadForward or roadForward.Magnitude < 0.5 or math.abs(roadForward.Unit.Y) > 0.6 then
            roadForward = Vector3.new(look.X, 0, look.Z)
        end
        if roadForward.Magnitude > 1e-3 then
            local sideways = roadForward.Unit:Cross(Vector3.yAxis)
            if sideways.Magnitude > 1e-3 then
                local targetUp = sideways.Unit:Cross(roadForward.Unit)
                local tilt = math.deg(math.acos(math.clamp(up:Dot(targetUp), -1, 1)))
                strength = math.clamp((tilt - ANTI_FLIP_DEADZONE) / ANTI_FLIP_RAMP, 0, 1)
                s.align.PrimaryAxis = targetUp
            end
        end
    end
    s.align.MaxTorque = s.flipTorque * strength
    s.align.Enabled = strength > 0
end

-- Brookhaven marks one SuspensionHeights entry "isDefault"; its level is its sorted position.
local function defaultRideLevel(car)
    local config = car:FindFirstChild("Config")
    local heights = config and config:FindFirstChild("SuspensionHeights")
    if not heights then return nil end
    local values, default = {}, nil
    for _, v in ipairs(heights:GetChildren()) do
        if v:IsA("NumberValue") then
            table.insert(values, v.Value)
            if v:GetAttribute("isDefault") == true then
                default = v.Value
            end
        end
    end
    table.sort(values)
    for i, value in ipairs(values) do
        if default and math.abs(value - default) < 0.001 then
            return i
        end
    end
    return nil
end

-- Requests that need you in the driver's seat. Returns success plus the server's message.
local function carRequest(name, ...)
    if not Session then
        return false, "Get in your car first"
    end
    local called, ok, msg = invoke(name, ...)
    if not called then
        return false, tostring(ok)
    end
    return ok == true, msg
end

local function autoTune(s)
    task.wait(0.6)
    if Session ~= s then return end
    local parts = {}
    local speed = topSpeed()
    local called, ok, _, value = invoke("SetMaxSpeed", speed)
    if called and ok then
        table.insert(parts, "top speed " .. tostring(value or speed))
    else
        table.insert(parts, "top speed refused")
    end
    if carRequest("SetTurboAppearance", 3) then
        table.insert(parts, "turbo 3")
    end
    if tuningTires(s) then
        -- Server-side grip too (Brookhaven caps it at 1.5), within this car's balance limit.
        local grip = tireFriction(s)
        if carRequest("SetDriftStrength", math.min(grip, 1.5)) then
            table.insert(parts, grip < Settings.tireGrip and string.format("grip %.1f (capped for balance)", grip) or string.format("grip %.1f", grip))
        end
    elseif not s.geom.valid then
        table.insert(parts, "stock handling (not a 4-wheel car)")
    end
    -- 0 = leave the car's own ride height alone (forcing the lowest level made cars scrape).
    if Settings.rideHeight > 0 and carRequest("SetSuspensionLevel", Settings.rideHeight) then
        table.insert(parts, "ride " .. Settings.rideHeight)
    end
    if owns("VEHICLE_CUSTOMIZATION") and carRequest("SetUnderglow", Settings.underglow) then
        table.insert(parts, string.lower(Settings.underglow))
        carRequest("SetUnderglowSize", Settings.glowLength, Settings.glowWidth)
    end
    if Session == s then
        s.ready = true
    end
    notify("Auto-tune", table.concat(parts, " · "), "good")
end

local function syncEffectModules()
    if refreshModule then
        refreshModule("Fire")
        refreshModule("Engine Smoke")
    end
end

local function refreshVehicleStatus()
    if not setVehicleStatus then return end
    if Session then
        local name = Session.car:GetAttribute("vehicleName") or Session.car.Name
        setVehicleStatus(name, Session.geom.valid and "" or "Stock handling", true)
    else
        local parked = myCar()
        if parked then
            setVehicleStatus(parked:GetAttribute("vehicleName") or parked.Name, "Parked", false)
        else
            setVehicleStatus("On foot", "No car out", false)
        end
    end
end

local function endSession()
    local s = Session
    if not s then return end
    Session = nil
    for _, c in ipairs(s.conns) do
        c:Disconnect()
    end
    if s.seat.Parent then
        s.seat.Torque = s.baseTorque
        s.seat.TurnSpeed = s.baseTurn
    end
    if s.turbo and s.turbo.Parent and s.baseTurbo then
        s.turbo.Value = s.baseTurbo
    end
    for wheel, original in pairs(s.wheelProps) do
        if wheel.Parent then
            wheel.CustomPhysicalProperties = original
        end
    end
    for _, inst in ipairs({ s.force, s.attach, s.align, s.alignAttach, s.rearForce, s.rearAttach }) do
        inst:Destroy()
    end
    syncEffectModules()
end

local function startSession(seat, car)
    local turbo = seat:FindFirstChild("Turbo")
    local s = {
        seat = seat,
        car = car,
        uuid = car:GetAttribute("VehicleUuid"),
        baseTorque = seat.Torque,
        baseTurn = seat.TurnSpeed,
        turbo = turbo and turbo:IsA("StringValue") and turbo or nil,
        baseTurbo = turbo and turbo:IsA("StringValue") and turbo.Value or nil,
        ready = false,
        stunting = false,
        drifting = false,
        airArmed = false,
        groundTime = 0,
        conns = {},
        wheelProps = {},
        rearWheels = {},
        mass = 0,
    }
    for _, part in ipairs(car:GetDescendants()) do
        if part:IsA("BasePart") then
            if not part.Massless then
                s.mass += part:GetMass()
            end
            if part.Name == "PhysicalWheel" then
                s.wheelProps[part] = part.CustomPhysicalProperties or PhysicalProperties.new(part.Material)
                -- Brookhaven tags the driven (rear) wheel models "Wheel" and the front ones "WheelSteering".
                s.rearWheels[part] = part.Parent ~= nil and part.Parent:HasTag("Wheel")
                -- If the server re-sets friction (e.g. a grip change), keep its value as the restore
                -- point and put our tire setting back on top.
                table.insert(s.conns, part:GetPropertyChangedSignal("CustomPhysicalProperties"):Connect(function()
                    local p = part.CustomPhysicalProperties
                    local want = Session == s and wheelFriction(s, part)
                    if want and p and (math.abs(p.Friction - want) > 0.01 or math.abs(p.FrictionWeight - TIRE_FRICTION_WEIGHT) > 0.01) then
                        s.wheelProps[part] = p
                        applyTires()
                    end
                end))
            end
        end
    end
    if originalPaints[car] == nil then
        originalPaints[car] = currentPaint(car)
    end
    s.geom = analyzeCar(car, seat, seat.AssemblyRootPart)
    -- How high the root part rides above the road, for telling when the car is in the air.
    do
        local root = seat.AssemblyRootPart
        groundParams.FilterDescendantsInstances = { car, LocalPlayer.Character }
        local below = workspace:Raycast(root.Position, Vector3.new(0, -30, 0), groundParams)
        s.clearance = math.clamp(s.geom.rootHeight or (below and root.Position.Y - below.Position.Y) or 2, 0.3, 15)
    end
    if s.geom.valid then
        s.flipTorque = s.mass * workspace.Gravity * math.max(s.geom.halfTrack, s.geom.wheelbase / 2) * 2
    end
    Session = s
    -- The server can change these (e.g. a turbo upgrade); adopt its value as the new base.
    table.insert(s.conns, seat:GetPropertyChangedSignal("Torque"):Connect(function()
        if Session == s and math.abs(seat.Torque - s.baseTorque * accelFor(s)) > 0.5 then
            s.baseTorque = seat.Torque
            applyPhysics()
        end
    end))
    table.insert(s.conns, seat:GetPropertyChangedSignal("TurnSpeed"):Connect(function()
        if Session == s and math.abs(seat.TurnSpeed - s.baseTurn * steerFor()) > 0.01 then
            s.baseTurn = seat.TurnSpeed
            applyPhysics()
        end
    end))
    if s.turbo then
        table.insert(s.conns, s.turbo.Changed:Connect(function(newValue)
            if Session == s and newValue ~= s.turboExpected then
                s.baseTurbo = newValue
                applyPhysics()
            end
        end))
    end
    table.insert(s.conns, car:GetAttributeChangedSignal("OnFire"):Connect(syncEffectModules))
    table.insert(s.conns, car:GetAttributeChangedSignal("Smoking"):Connect(syncEffectModules))
    table.insert(s.conns, RunService.Heartbeat:Connect(updateHandling))
    applyPhysics()
    applyTires()
    syncEffectModules()
    if Settings.autoTune and not tunedCars[car] then
        tunedCars[car] = true
        task.spawn(autoTune, s)
    else
        s.ready = true
    end
end

local function onSeatChanged(humanoid)
    local seat = humanoid.SeatPart
    if Session and Session.seat ~= seat then
        endSession()
    end
    if seat and seat:IsA("VehicleSeat") and not Session then
        local car = carFromPart(seat)
        if car and isMine(car) then
            startSession(seat, car)
        end
    end
    refreshVehicleStatus()
end

local function hookCharacter(character)
    local humanoid = character:WaitForChild("Humanoid", 10)
    if not humanoid or not alive then return end
    bind(humanoid:GetPropertyChangedSignal("SeatPart"), function()
        onSeatChanged(humanoid)
    end)
    onSeatChanged(humanoid)
end

local spawning = false
local function spawnCar()
    if spawning then return end
    local entry
    for _, car in ipairs(CARS) do
        if car.id == Settings.car then entry = car end
    end
    if not entry then
        notify("Spawn", "Pick a car first", "bad")
        return
    end
    local character = LocalPlayer.Character
    local humanoid = character and character:FindFirstChildOfClass("Humanoid")
    if not humanoid or humanoid.Health <= 0 then
        notify("Spawn", "Respawn first", "bad")
        return
    end
    if humanoid.SeatPart or humanoid.Sit then
        notify("Spawn", "Get out of your seat or vehicle first", "bad")
        return
    end
    spawning = true
    local before = myCar()
    CarEvent:FireServer("PickingCar", entry.id)
    local spawned
    local started = os.clock()
    repeat
        task.wait(0.2)
        local current = myCar()
        if current and current ~= before then spawned = current end
    until spawned or os.clock() - started > 8 or not alive
    spawning = false
    if spawned then
        notify("Spawn", entry.label .. " is out · hop in to apply your tune", "good")
    else
        notify("Spawn", "Didn't go through · wait for the cooldown or move off roads/water", "bad")
    end
    refreshVehicleStatus()
end

-- Fire and smoke are server toggles; compare with the car's real state so a dropped
-- request can never leave them inverted.
local function setCarEffect(attribute, remoteName, wanted)
    local s = Session
    if not s then
        notify(attribute == "OnFire" and "Fire" or "Engine Smoke", "Get in your car first", "bad")
        return
    end
    if (s.car:GetAttribute(attribute) == true) ~= wanted then
        local ok, msg = carRequest(remoteName)
        if not ok then
            notify("Effects", "Refused: " .. tostring(msg), "bad")
        end
    end
    task.delay(0.5, syncEffectModules)
end

---------------------------------------------------------------------------------------------
-- Stunts. The car's orientation follows a scripted path through a rigid AlignOrientation: it
-- settles level, turns exactly the chosen number of times, then is held level until it touches
-- down, so it always lands on its wheels facing the way it was going.
-- Key stunts launch the car first; Air Stunts use the airtime of a jump you're already in.
---------------------------------------------------------------------------------------------

local stuntBusy = false
local stuntCount = 0
local HOP = { name = "Car Hop", axis = "X", turns = 0 }
local LANDING = { name = "Landing assist", axis = "X", turns = 0 }

local function stuntRotation(def, angle)
    if def.axis == "X" then
        return CFrame.fromAxisAngle(Vector3.xAxis, angle)
    elseif def.axis == "Y" then
        return CFrame.fromAxisAngle(Vector3.yAxis, angle)
    elseif def.axis == "Z" then
        return CFrame.fromAxisAngle(Vector3.zAxis, angle)
    end
    return CFrame.fromAxisAngle(Vector3.yAxis, angle) * CFrame.fromAxisAngle(Vector3.zAxis, angle)
end

local function findStunt(name)
    for _, def in ipairs(STUNTS) do
        if def.name == name then return def end
    end
    return STUNTS[1]
end

local function smoothstep(x)
    return x * x * (3 - 2 * x)
end

-- Wheels down = the root part is within its normal ride height of the ground.
local function isGrounded(s, root)
    groundParams.FilterDescendantsInstances = { s.car, LocalPlayer.Character }
    return workspace:Raycast(root.Position, Vector3.new(0, -(s.clearance + 1.2), 0), groundParams) ~= nil
end

-- Seconds until the car comes down, from its height and vertical speed. The ground is checked
-- straight below and where the car is heading, since a roof or a hill ahead lands it sooner.
local function airtimeLeft(s, root)
    local gravity = workspace.Gravity
    local pos, v = root.Position, root.AssemblyLinearVelocity
    groundParams.FilterDescendantsInstances = { s.car, LocalPlayer.Character }
    local below = workspace:Raycast(pos, Vector3.new(0, -2000, 0), groundParams)
    if not below then return nil end
    local groundY = below.Position.Y
    local function timeToGround()
        local height = pos.Y - s.clearance - groundY
        if height <= 0 and v.Y <= 0 then return 0 end
        return (v.Y + math.sqrt(math.max(v.Y * v.Y + 2 * gravity * height, 0))) / gravity
    end
    local t = timeToGround()
    for _ = 1, 2 do
        local ahead = pos + Vector3.new(v.X, 0, v.Z) * t
        local hit = workspace:Raycast(Vector3.new(ahead.X, pos.Y + 4, ahead.Z), Vector3.new(0, -2000, 0), groundParams)
        if hit and hit.Position.Y > groundY then
            groundY = hit.Position.Y
            t = timeToGround()
        end
    end
    return t
end

-- Air needed to finish the trick and still be level before touchdown.
local function airNeeded(def)
    local turns = math.abs(def.turns)
    return turns == 0 and 0.4 or 0.35 + 0.45 * turns
end

local function airRotateTime(def, left)
    local turns = math.abs(def.turns)
    return turns == 0 and 0 or math.min(left - 0.3, 0.85 * turns + 0.15)
end

-- Two white ribbons off the rear corners while the car is in the air. Created by your client,
-- so only you see them.
local function addTrails(s, root)
    local frame = carFrame(s)
    local right = root.CFrame:VectorToObjectSpace(frame.RightVector)
    local up = root.CFrame:VectorToObjectSpace(frame.UpVector)
    local rear = s.geom.valid and s.geom.rearRoot or root.CFrame:PointToObjectSpace(root.Position - frame.LookVector * 4)
    local half = s.geom.valid and s.geom.halfTrack or 2.5
    local made = {}
    for _, side in ipairs({ -1, 1 }) do
        local base = rear + right * half * side
        local a0 = make("Attachment", { Name = "BHS_Trail", Position = base, Parent = root })
        local a1 = make("Attachment", { Name = "BHS_Trail", Position = base + up * 0.7, Parent = root })
        local trail = make("Trail", {
            Name = "BHS_Trail",
            Attachment0 = a0,
            Attachment1 = a1,
            Lifetime = 0.45,
            MinLength = 0.05,
            FaceCamera = true,
            LightEmission = 0.5,
            Color = ColorSequence.new(Color3.new(1, 1, 1)),
            Transparency = NumberSequence.new({ NumberSequenceKeypoint.new(0, 0.2), NumberSequenceKeypoint.new(1, 1) }),
            WidthScale = NumberSequence.new({ NumberSequenceKeypoint.new(0, 1), NumberSequenceKeypoint.new(1, 0.1) }),
            Parent = root,
        })
        table.insert(made, trail)
        table.insert(made, a0)
        table.insert(made, a1)
    end
    return made
end

-- flight: expected seconds in the air; rotateTime: seconds the turns take; launch: upward
-- speed to add first (0 when the car is already in the air).
local function runStunt(s, def, flight, rotateTime, launch)
    local root = s.seat.AssemblyRootPart
    if stuntBusy or not root then return false end
    stuntBusy = true
    s.stunting = true

    local frame = carFrame(s)
    local flatLook = Vector3.new(frame.LookVector.X, 0, frame.LookVector.Z)
    if flatLook.Magnitude < 0.1 then
        -- Nose straight up or down: use the direction of travel instead.
        local v = root.AssemblyLinearVelocity
        flatLook = Vector3.new(v.X, 0, v.Z)
        if flatLook.Magnitude < 0.1 then
            flatLook = Vector3.new(0, 0, -1)
        end
    end
    local level = CFrame.lookAt(Vector3.zero, flatLook.Unit)
    local startRot = frame.Rotation

    local attach = make("Attachment", { Name = "BHS_Stunt", Parent = root })
    attach.WorldCFrame = CFrame.new(root.Position) * frame.Rotation
    local align = make("AlignOrientation", {
        Name = "BHS_Stunt",
        Mode = Enum.OrientationAlignmentMode.OneAttachment,
        Attachment0 = attach,
        RigidityEnabled = true,
        CFrame = startRot,
        Parent = root,
    })

    if launch > 0 then
        -- Lift every part of the car (wheels are separate physics assemblies) by the same amount.
        local lifted = {}
        for _, part in ipairs(s.car:GetDescendants()) do
            if part:IsA("BasePart") and not part.Anchored then
                local assembly = part.AssemblyRootPart
                if assembly and not lifted[assembly] then
                    lifted[assembly] = true
                    assembly.AssemblyLinearVelocity += Vector3.new(0, launch, 0)
                end
            end
        end
    end
    local trails = Settings.stuntTrail and addTrails(s, root) or {}

    local started = os.clock()
    local landedAt
    local conn
    local function finish()
        if conn then conn:Disconnect() end
        if root.Parent then
            local tilt = math.deg(math.acos(math.clamp(carFrame(s).UpVector.Y, -1, 1)))
            print(string.format("[Brookhaven Studio] %s landed: %.1f° tilt after %.2fs", def.name, tilt, os.clock() - started))
        end
        if def.turns ~= 0 then
            stuntCount += 1
        end
        align:Destroy()
        attach:Destroy()
        for _, inst in ipairs(trails) do
            if inst:IsA("Trail") then
                inst.Enabled = false
            end
        end
        task.delay(0.5, function()
            for _, inst in ipairs(trails) do
                inst:Destroy()
            end
        end)
        s.stunting = false
        stuntBusy = false
    end
    conn = RunService.Heartbeat:Connect(function()
        if Session ~= s or not root.Parent or not alive then
            finish()
            return
        end
        local t = os.clock() - started
        local base = startRot:Lerp(level, smoothstep(math.clamp(t / 0.2, 0, 1)))
        local progress = rotateTime > 0 and math.clamp(t / rotateTime, 0, 1) or 1
        align.CFrame = base * stuntRotation(def, def.turns * 2 * math.pi * smoothstep(progress))
        if progress >= 1 and t > flight * 0.5 then
            if not landedAt and isGrounded(s, root) then
                landedAt = t
            end
            -- Hold level briefly after touchdown so the suspension settles, then hand back.
            if (landedAt and t - landedAt > 0.2) or t > flight + 2 then
                finish()
            end
        end
    end)
    return true
end

-- Do the trick from mid-air. If the jump is too short for it, add just enough lift that the
-- rest of the flight lasts as long as the trick needs.
local function stuntFromAir(s, root, def, left)
    local need = airNeeded(def)
    if left >= need then
        return runStunt(s, def, left, airRotateTime(def, left), 0)
    end
    local gravity = workspace.Gravity
    local vy = root.AssemblyLinearVelocity.Y
    -- Height above the landing spot right now (from 0 = h + vy*t - g*t^2/2 at t = left).
    local height = gravity * left * left / 2 - vy * left
    local boost = (gravity * need * need / 2 - height) / need - vy
    return runStunt(s, def, need, airRotateTime(def, need), math.max(boost, 0))
end

local function performStunt(def, airtime, title)
    local s = Session
    if not s then
        notify(title, "Get in your car first", "bad")
        return
    end
    if stuntBusy then return end
    local root = s.seat.AssemblyRootPart
    if not root then return end
    local turns = math.abs(def.turns)
    if isGrounded(s, root) then
        -- Enough airtime to finish the rotation comfortably before touchdown.
        airtime = math.max(airtime, 0.45 + 0.55 * turns)
        runStunt(s, def, airtime, turns > 0 and airtime * 0.8 or 0, workspace.Gravity * airtime / 2)
        return
    end
    -- Already in the air: use the airtime that's left, topped up if it's too short.
    local left = airtimeLeft(s, root)
    if left then
        stuntFromAir(s, root, def, left)
    else
        notify(title, "No ground below to land on", "bad")
    end
end

-- Air Stunts and Landing Assist: one decision per jump, made the moment the wheels leave the
-- ground, from the predicted airtime. With Every Jump on, any jump longer than the minimum gets
-- the trick (topped up with lift); shorter bumps and curbs never trigger anything.
local function airMonitor(dt)
    local s = Session
    if not s or s.stunting then return end
    local root = s.seat.AssemblyRootPart
    if not root then return end
    if isGrounded(s, root) then
        s.groundTime += dt
        if s.groundTime > 0.25 then
            s.airArmed = true
        end
        return
    end
    s.groundTime = 0
    if not s.airArmed then return end
    s.airArmed = false
    if not (Settings.airStunts or Settings.landAssist) then return end
    local left = airtimeLeft(s, root)
    if not left then return end
    if Settings.airStunts then
        local def = Settings.airTrick == "Random" and STUNTS[math.random(#STUNTS)] or findStunt(Settings.airTrick)
        if left >= airNeeded(def) or (Settings.airAlways and left >= Settings.airMin) then
            stuntFromAir(s, root, def, left)
            return
        end
    end
    if Settings.landAssist and left >= airNeeded(LANDING) then
        runStunt(s, LANDING, left, 0, 0)
    end
end

---------------------------------------------------------------------------------------------
-- RGB
---------------------------------------------------------------------------------------------

-- Every mode is a continuous function of time, so the color glides instead of jumping.
local function paletteColor(stops, phase)
    local scaledPhase = phase * #stops
    local index = math.floor(scaledPhase)
    local frac = scaledPhase - index
    local eased = frac * frac * (3 - 2 * frac)
    local from = stops[index % #stops + 1]
    local to = stops[(index + 1) % #stops + 1]
    return from:Lerp(to, eased)
end

local function rgbColor(mode, t, cycle)
    local phase = (t % cycle) / cycle
    if mode == "Pastel" then
        return Color3.fromHSV(phase, 0.42, 1)
    elseif mode == "Neon pulse" then
        local beat = 0.5 + 0.5 * math.sin(t * math.pi * 2 / math.max(cycle / 4, 0.5))
        return Color3.fromHSV(phase, 1, 0.35 + 0.65 * beat)
    elseif mode == "Police" then
        local sweep = 0.5 + 0.5 * math.sin(t * math.pi * 2 / 0.9)
        return Color3.fromRGB(255, 40, 40):Lerp(Color3.fromRGB(40, 90, 255), sweep)
    elseif RGB_PALETTES[mode] then
        return paletteColor(RGB_PALETTES[mode], phase)
    end
    return Color3.fromHSV(phase, 1, 1)
end

-- What the underglow shows: the paint color when Car RGB is on, otherwise its own rainbow.
local function glowColor(t)
    if Settings.carRGB then
        return rgbColor(Settings.rgbMode, t, Settings.rgbCycle)
    end
    return rgbColor("Rainbow", t, Settings.rgbCycle)
end

local function paintParts(car)
    local body = car:FindFirstChild("Body")
    local colorModel = body and body:FindFirstChild("Color")
    local parts = {}
    if colorModel then
        for _, d in ipairs(colorModel:GetDescendants()) do
            if d:IsA("BasePart") then table.insert(parts, d) end
        end
    end
    return parts
end

local function glowObjects(car)
    local body = car:FindFirstChild("Body")
    local collide = body and body:FindFirstChild("VehicleCollide")
    local objects = {}
    if collide then
        for _, d in ipairs(collide:GetDescendants()) do
            if d.Name == "ColorPrimary" and (d:IsA("Beam") or d:IsA("Light")) then
                table.insert(objects, d)
            end
        end
    end
    return objects
end

local function restorePaint()
    local car = ownCar()
    local paint = car and originalPaints[car]
    if paint then
        task.wait(0.4) -- let any in-flight RGB step land first
        if not Settings.carRGB and car.Parent then
            paintCar(car, paint)
        end
    end
end

-- Single entry point for Car RGB so the module, swatches and console stay in sync.
local function setCarRGB(v)
    Settings.carRGB = v
    saveSoon()
    if refreshModule then refreshModule("Car RGB") end
    if not v then
        task.spawn(restorePaint)
    end
end

---------------------------------------------------------------------------------------------
-- House
---------------------------------------------------------------------------------------------

local function setHouseColor(color)
    PropertySetColor:FireServer(color)
    HouseEvent:FireServer("ColorPickHouse", color)
end

local function loadBuild(slot)
    if HouseNumber.Value <= 0 then
        notify("Saved Props", "Claim a house first, then load props", "bad")
        return
    end
    if slot == "" then
        notify("Saved Props", "Pick a saved build first", "bad")
        return
    end
    local called, result, message = invoke("HS_PropBuildLoad", slot, "MainButton")
    print("[Brookhaven Studio] HS_PropBuildLoad ->", called, result, message)
    if not called then
        notify("Saved Props", "Didn't load: " .. tostring(result), "bad")
    elseif result == false then
        notify("Saved Props", "Server refused the build" .. (message and (": " .. tostring(message)) or ""), "bad")
    else
        notify("Saved Props", "Props requested from save " .. slot, "good")
    end
end

local function fetchSaves()
    local called, saves = invoke("HS_HouseSaves")
    if not called or type(saves) ~= "table" then
        return nil, tostring(saves)
    end
    local options = {}
    for slot, data in pairs(saves) do
        table.insert(options, {
            value = slot,
            label = string.format("%s · %d props", tostring(data.Name or slot), tonumber(data.PropCount) or 0),
        })
    end
    table.sort(options, function(a, b) return a.value < b.value end)
    return options
end

local function houseColorAt(t)
    return Color3.fromHSV((t % Settings.houseCycle) / Settings.houseCycle, 0.72, 1)
end

local function colorClose(a, b)
    return math.abs(a.R - b.R) + math.abs(a.G - b.G) + math.abs(a.B - b.B) < 0.04
end

local housePaint = { parts = {}, recent = {}, refreshAt = 0 }

-- The painted parts are the ones in your lot currently showing a color we sent (or the color
-- we're drawing locally), so this matches exactly what the server recolors.
local function collectHouseParts()
    local lots = workspace:FindFirstChild("001_Lots")
    local lot = lots and lots:FindFirstChild(LocalPlayer.Name .. "House")
    local found = {}
    if not lot then return found end
    local candidates = table.clone(housePaint.recent)
    table.insert(candidates, houseColorAt(os.clock()))
    for _, d in ipairs(lot:GetDescendants()) do
        if d:IsA("BasePart") then
            for _, color in ipairs(candidates) do
                if colorClose(d.Color, color) then
                    table.insert(found, d)
                    break
                end
            end
        end
    end
    return found
end

---------------------------------------------------------------------------------------------
-- Player and world (lighting, FOV and zoom are local to your screen)
---------------------------------------------------------------------------------------------

local LIGHTING_KEYS = { "ClockTime", "Brightness", "Ambient", "OutdoorAmbient", "GlobalShadows", "FogEnd" }
local savedLighting = nil

local function syncLightingSnapshot()
    local active = Settings.lockTime or Settings.fullbright
    if active and not savedLighting then
        savedLighting = {}
        for _, key in ipairs(LIGHTING_KEYS) do
            savedLighting[key] = Lighting[key]
        end
    elseif not active and savedLighting then
        for key, value in pairs(savedLighting) do
            Lighting[key] = value
        end
        savedLighting = nil
    end
end

local savedFov
local function syncFov()
    local camera = workspace.CurrentCamera
    if Settings.fovOn and not savedFov then
        savedFov = camera.FieldOfView
    elseif not Settings.fovOn and savedFov then
        camera.FieldOfView = savedFov
        savedFov = nil
    end
end

local savedZoom
local function syncZoom()
    if Settings.unlockZoom and not savedZoom then
        savedZoom = LocalPlayer.CameraMaxZoomDistance
        LocalPlayer.CameraMaxZoomDistance = 1000
    elseif not Settings.unlockZoom and savedZoom then
        LocalPlayer.CameraMaxZoomDistance = savedZoom
        savedZoom = nil
    end
end

RunService:BindToRenderStep("BrookhavenStudioView", Enum.RenderPriority.Last.Value, function()
    if Settings.fovOn then
        workspace.CurrentCamera.FieldOfView = Settings.fov
    end
    if not savedLighting then return end
    if Settings.lockTime then
        Lighting.ClockTime = Settings.hour
    elseif Lighting.ClockTime ~= savedLighting.ClockTime then
        savedLighting.ClockTime = Lighting.ClockTime
    end
    if Settings.fullbright then
        Lighting.Brightness = 2
        Lighting.Ambient = Color3.fromRGB(178, 178, 178)
        Lighting.OutdoorAmbient = Color3.fromRGB(178, 178, 178)
        Lighting.GlobalShadows = false
        Lighting.FogEnd = 1e6
    end
end)

bind(LocalPlayer.Idled, function()
    if Settings.antiAfk then
        VirtualUser:CaptureController()
        VirtualUser:ClickButton2(Vector2.new())
    end
end)

---------------------------------------------------------------------------------------------
-- Menu: one flat box (RightShift). Sidebar tabs on the left, the page on the right.
---------------------------------------------------------------------------------------------

local BOX_W, BOX_H = 780, 540
local SIDEBAR_W = 200
local HEADER_H = 58
local TAB_H, TAB_GAP, TAB_TOP = 44, 4, 12
local PAD = 22
local GLIDE = TweenInfo.new(0.32, Enum.EasingStyle.Quint, Enum.EasingDirection.Out)

local menuOpen = false
-- Frees the mouse from first person / shift lock while the menu is open.
local modal = make("TextButton", {
    Text = "",
    Size = UDim2.fromOffset(0, 0),
    BackgroundTransparency = 1,
    Modal = false,
    Visible = false,
    Parent = Gui,
})

local box = make("CanvasGroup", {
    AnchorPoint = Vector2.new(0.5, 0.5),
    Position = UDim2.fromScale(0.5, 0.5),
    Size = UDim2.fromOffset(BOX_W, BOX_H),
    BackgroundColor3 = T.bg,
    BorderSizePixel = 0,
    GroupTransparency = 1,
    Visible = false,
    Parent = Gui,
})
local basePos = box.Position
local boxScale = make("UIScale", { Scale = guiScale * 0.94, Parent = box })
local menuBlur = make("BlurEffect", { Name = "BrookhavenStudioBlur", Size = 0, Parent = Lighting })

local function hline(parent, y)
    return make("Frame", {
        Position = UDim2.new(0, 0, 0, y),
        Size = UDim2.new(1, 0, 0, 1),
        BackgroundColor3 = T.line,
        BorderSizePixel = 0,
        Parent = parent,
    })
end

-- Header (drag handle), full width.
local header = make("Frame", {
    Size = UDim2.new(1, 0, 0, HEADER_H),
    BackgroundTransparency = 1,
    Parent = box,
}, { pad(24, 24) })
label({
    Text = "BROOKHAVEN STUDIO   <font size=\"13\" weight=\"medium\" color=\"#5A5A5E\">by Obsidian Works</font>",
    RichText = true,
    Font = FONT_BLACK,
    TextSize = 20,
    TextColor3 = T.white,
    Size = UDim2.fromScale(0.75, 1),
    Parent = header,
})
label({
    Text = "RShift to close",
    TextSize = 13,
    TextColor3 = T.dim,
    AnchorPoint = Vector2.new(1, 0),
    Position = UDim2.fromScale(1, 0),
    Size = UDim2.fromScale(0.25, 1),
    TextXAlignment = Enum.TextXAlignment.Right,
    Parent = header,
})
hline(box, HEADER_H)

-- Sidebar: tabs on top, your vehicle card and Unload at the bottom.
local sidebar = make("Frame", {
    Position = UDim2.fromOffset(0, HEADER_H + 1),
    Size = UDim2.new(0, SIDEBAR_W, 1, -(HEADER_H + 1)),
    BackgroundColor3 = T.side,
    BorderSizePixel = 0,
    Parent = box,
})
make("Frame", {
    AnchorPoint = Vector2.new(1, 0),
    Position = UDim2.fromScale(1, 0),
    Size = UDim2.new(0, 1, 1, 0),
    BackgroundColor3 = T.line,
    BorderSizePixel = 0,
    Parent = sidebar,
})
-- One highlight and indicator that glide to the active tab.
local tabHighlight = make("Frame", {
    Position = UDim2.fromOffset(10, TAB_TOP),
    Size = UDim2.new(1, -21, 0, TAB_H),
    BackgroundColor3 = T.raised,
    BorderSizePixel = 0,
    Parent = sidebar,
}, { corner(6) })
local tabIndicator = make("Frame", {
    Position = UDim2.fromOffset(10, TAB_TOP + (TAB_H - 18) / 2),
    Size = UDim2.fromOffset(3, 18),
    BackgroundColor3 = T.white,
    BorderSizePixel = 0,
    ZIndex = 2,
    Parent = sidebar,
}, { corner() })
local tabList = make("Frame", {
    Size = UDim2.new(1, -1, 0, 0),
    AutomaticSize = Enum.AutomaticSize.Y,
    BackgroundTransparency = 1,
    ZIndex = 3,
    Parent = sidebar,
}, {
    make("UIListLayout", { SortOrder = Enum.SortOrder.LayoutOrder, Padding = UDim.new(0, TAB_GAP) }),
    pad(10, 10, TAB_TOP, 0),
})

local cardName, cardLine, cardStroke
local unloadHolder
do
    local sideBottom = make("Frame", {
        AnchorPoint = Vector2.new(0, 1),
        Position = UDim2.new(0, 0, 1, 0),
        Size = UDim2.new(1, -1, 0, 156),
        BackgroundTransparency = 1,
        Parent = sidebar,
    }, { pad(18, 18, 0, 16) })
    make("Frame", {
        Position = UDim2.fromOffset(-18, 0),
        Size = UDim2.new(1, 36, 0, 1),
        BackgroundColor3 = T.line,
        BorderSizePixel = 0,
        Parent = sideBottom,
    })
    label({
        Text = "VEHICLE",
        Font = FONT_BOLD,
        TextSize = 12,
        TextColor3 = T.sub,
        Position = UDim2.fromOffset(0, 16),
        Size = UDim2.new(1, 0, 0, 14),
        Parent = sideBottom,
    })
    local ring = make("Frame", {
        Position = UDim2.fromOffset(0, 36),
        Size = UDim2.fromOffset(42, 42),
        BackgroundColor3 = T.raised,
        BorderSizePixel = 0,
        Parent = sideBottom,
    }, { corner() })
    cardStroke = make("UIStroke", { Color = T.line, Thickness = 1.5, Parent = ring })
    make("ImageLabel", {
        Size = UDim2.fromScale(1, 1),
        BackgroundTransparency = 1,
        Image = "rbxthumb://type=AvatarHeadShot&id=" .. LocalPlayer.UserId .. "&w=150&h=150",
        Parent = ring,
    }, { corner() })
    cardName = label({
        Text = "On foot",
        Font = FONT_BLACK,
        TextSize = 21,
        TextColor3 = T.dim,
        Position = UDim2.fromOffset(52, 34),
        Size = UDim2.new(1, -52, 0, 24),
        TextTruncate = Enum.TextTruncate.AtEnd,
        Parent = sideBottom,
    })
    cardLine = label({
        Text = "",
        TextSize = 14,
        TextColor3 = T.sub,
        Position = UDim2.fromOffset(52, 60),
        Size = UDim2.new(1, -52, 0, 18),
        TextTruncate = Enum.TextTruncate.AtEnd,
        Parent = sideBottom,
    })
    unloadHolder = make("Frame", {
        AnchorPoint = Vector2.new(0, 1),
        Position = UDim2.new(0, 0, 1, 0),
        Size = UDim2.new(1, 0, 0, 40),
        BackgroundTransparency = 1,
        Parent = sideBottom,
    })
end

-- While you drive, the card's second line shows live speed and your stunt count.
local cardNote, cardActive = "", false
setVehicleStatus = function(name, note, active)
    cardName.Text = name
    cardNote = note
    cardActive = active
    cardName.TextColor3 = active and T.white or T.dim
    cardStroke.Color = active and T.white or T.line
    if not active then
        cardLine.Text = note
    end
end

-- Content: page title, subtitle and the scrolling page.
local CONTENT_POS = UDim2.fromOffset(SIDEBAR_W, HEADER_H + 1)
local content = make("CanvasGroup", {
    Position = CONTENT_POS,
    Size = UDim2.new(1, -SIDEBAR_W, 1, -(HEADER_H + 1)),
    BackgroundTransparency = 1,
    Parent = box,
})
local pageTitle = label({
    Text = "",
    Font = FONT_BLACK,
    TextSize = 30,
    TextColor3 = T.white,
    Position = UDim2.fromOffset(PAD, 20),
    Size = UDim2.new(1, -PAD * 2, 0, 34),
    Parent = content,
})
local pageSub = label({
    Text = "",
    TextSize = 15,
    TextColor3 = T.sub,
    Position = UDim2.fromOffset(PAD, 56),
    Size = UDim2.new(1, -PAD * 2, 0, 18),
    Parent = content,
})
local scroller = make("ScrollingFrame", {
    Position = UDim2.fromOffset(0, 88),
    Size = UDim2.new(1, 0, 1, -88),
    BackgroundTransparency = 1,
    BorderSizePixel = 0,
    CanvasSize = UDim2.new(),
    AutomaticCanvasSize = Enum.AutomaticSize.Y,
    ScrollBarThickness = 2,
    ScrollBarImageColor3 = T.dim,
    ScrollingDirection = Enum.ScrollingDirection.Y,
    Parent = content,
}, { pad(0, 0, 0, 12) })

local TABS = { "Vehicle", "Stunts", "Visuals", "Player", "House", "Settings" }
local TAB_SUB = {
    Vehicle = "Spawn, tune and drive",
    Stunts = "Tricks on a key or in mid-air",
    Visuals = "Paint, glow and camera",
    Player = "Your roleplay name and session",
    House = "Color and saved props",
    Settings = "Interface and keybinds",
}
local pages, tabButtons = {}, {}
local activeTab
local function selectTab(name, instant)
    if name == activeTab then return end
    activeTab = name
    pageTitle.Text = name
    pageSub.Text = TAB_SUB[name]
    scroller.CanvasPosition = Vector2.zero
    for i, tabName in ipairs(TABS) do
        local on = tabName == name
        local tab = tabButtons[tabName]
        pages[tabName].Visible = on
        tab.label.Font = on and FONT_BOLD or FONT
        tween(tab.label, { TextColor3 = on and T.white or T.sub, Position = UDim2.fromOffset(on and 20 or 16, 0) })
        if on then
            local y = TAB_TOP + (i - 1) * (TAB_H + TAB_GAP)
            local info = instant and TweenInfo.new(0) or GLIDE
            tween(tabHighlight, { Position = UDim2.fromOffset(10, y) }, info)
            tween(tabIndicator, { Position = UDim2.fromOffset(10, y + (TAB_H - 18) / 2) }, info)
        end
    end
    if not instant then
        content.GroupTransparency = 1
        content.Position = CONTENT_POS + UDim2.fromOffset(16, 0)
        tween(content, { GroupTransparency = 0, Position = CONTENT_POS }, GLIDE)
    end
end
for i, tabName in ipairs(TABS) do
    pages[tabName] = make("Frame", {
        Size = UDim2.new(1, 0, 0, 0),
        AutomaticSize = Enum.AutomaticSize.Y,
        BackgroundTransparency = 1,
        Visible = false,
        Parent = scroller,
    }, { make("UIListLayout", { SortOrder = Enum.SortOrder.LayoutOrder }) })
    local btn = make("TextButton", {
        LayoutOrder = i,
        Text = "",
        AutoButtonColor = false,
        Size = UDim2.new(1, 0, 0, TAB_H),
        BackgroundTransparency = 1,
        BorderSizePixel = 0,
        ZIndex = 3,
        Parent = tabList,
    })
    local text = label({
        Text = tabName,
        TextSize = 16,
        TextColor3 = T.sub,
        Position = UDim2.fromOffset(16, 0),
        Size = UDim2.new(1, -20, 1, 0),
        ZIndex = 3,
        Parent = btn,
    })
    bind(btn.MouseEnter, function()
        if activeTab ~= tabName then tween(text, { TextColor3 = T.text }) end
    end)
    bind(btn.MouseLeave, function()
        if activeTab ~= tabName then tween(text, { TextColor3 = T.sub }) end
    end)
    bind(btn.MouseButton1Click, function()
        selectTab(tabName)
    end)
    tabButtons[tabName] = { btn = btn, label = text }
end

-- Rows ---------------------------------------------------------------------------------------

local Modules, ModuleByName = {}, {}
local bindingKey -- keybind waiting for a key press
local keyRows = {}
local function renderKeyRows()
    for _, render in pairs(keyRows) do
        render()
    end
end

local carPicker, buildPicker, passNote
local unloadRef

-- The row builders and pages live in their own scope (Luau allows 200 locals per function).
do
    local current -- page rows are added to

    local function runSafely(title, fn)
        task.spawn(function()
            local ok, err = xpcall(fn, debug.traceback)
            if not ok then
                warn(err)
                notify(title, "Something broke: " .. tostring(err):match("^[^\n]*"), "bad")
            end
        end)
    end

    local function hoverRow(btn)
        bind(btn.MouseEnter, function() tween(btn, { BackgroundTransparency = 0 }) end)
        bind(btn.MouseLeave, function() tween(btn, { BackgroundTransparency = 1 }) end)
    end

    -- Name on the left; an optional dim hint line under it.
    local function rowText(parent, text, hint, rightSpace)
        local nameLabel = label({
            Text = text,
            TextSize = 17,
            Position = UDim2.fromOffset(0, hint and 10 or 0),
            Size = UDim2.new(1, -rightSpace, 0, hint and 22 or 46),
            TextTruncate = Enum.TextTruncate.AtEnd,
            Parent = parent,
        })
        if hint then
            label({
                Text = hint,
                TextSize = 13,
                TextColor3 = T.dim,
                Position = UDim2.fromOffset(0, 32),
                Size = UDim2.new(1, -rightSpace, 0, 16),
                TextTruncate = Enum.TextTruncate.AtEnd,
                Parent = parent,
            })
        end
        return nameLabel
    end

    local function sectionHeader(text)
        local row = make("Frame", {
            LayoutOrder = nextOrder(),
            Size = UDim2.new(1, 0, 0, 34),
            BackgroundTransparency = 1,
            Parent = current,
        }, { pad(PAD, PAD) })
        local title = label({
            Text = string.upper(text),
            Font = FONT_BOLD,
            TextSize = 12,
            TextColor3 = T.sub,
            Size = UDim2.fromScale(1, 1),
            TextYAlignment = Enum.TextYAlignment.Bottom,
            Parent = row,
        })
        pad(0, 0, 0, 4).Parent = title
    end

    -- def: { name, get, set, hint?, suffix?, array? (false = keep off the HUD list), quiet? }
    local function toggleRow(def)
        local btn = make("TextButton", {
            LayoutOrder = nextOrder(),
            Text = "",
            AutoButtonColor = false,
            Size = UDim2.new(1, 0, 0, def.hint and 60 or 46),
            BackgroundColor3 = T.raised,
            BackgroundTransparency = 1,
            BorderSizePixel = 0,
            Parent = current,
        }, { pad(PAD, PAD) })
        local nameLabel = rowText(btn, def.name, def.hint, 56)
        local track = make("Frame", {
            AnchorPoint = Vector2.new(1, 0.5),
            Position = UDim2.new(1, 0, 0.5, 0),
            Size = UDim2.fromOffset(40, 22),
            BackgroundColor3 = T.off,
            BorderSizePixel = 0,
            Parent = btn,
        }, { corner() })
        local knob = make("Frame", {
            AnchorPoint = Vector2.new(0, 0.5),
            Position = UDim2.new(0, 3, 0.5, 0),
            Size = UDim2.fromOffset(16, 16),
            BackgroundColor3 = T.sub,
            BorderSizePixel = 0,
            Parent = track,
        }, { corner() })

        local m = { def = def, name = def.name }
        function m.render(instant)
            local on = def.get() == true
            local info = instant and TweenInfo.new(0) or EASE
            tween(nameLabel, { TextColor3 = on and T.white or T.text }, info)
            tween(track, { BackgroundColor3 = on and T.white or T.off }, info)
            tween(knob, {
                Position = on and UDim2.new(1, -19, 0.5, 0) or UDim2.new(0, 3, 0.5, 0),
                BackgroundColor3 = on and T.black or T.sub,
            }, info)
        end
        hoverRow(btn)
        bind(btn.MouseButton1Click, function()
            local enabled = def.get() ~= true
            def.set(enabled)
            saveSoon()
            m.render()
            if Settings.toggleNotify and not def.quiet then
                notify(def.name, enabled and "Enabled" or "Disabled", enabled and "good" or nil)
            end
        end)
        m.render(true)
        table.insert(Modules, m)
        ModuleByName[def.name] = m
        return m
    end

    local function setting(name, key, extra)
        local def = {
            name = name,
            get = function() return Settings[key] end,
            set = function(v) Settings[key] = v end,
        }
        for k, v in pairs(extra or {}) do
            def[k] = v
        end
        return def
    end

    local function sliderRow(text, min, max, step, initial, format, onChange, onRelease)
        local row = make("Frame", {
            LayoutOrder = nextOrder(),
            Size = UDim2.new(1, 0, 0, 62),
            BackgroundTransparency = 1,
            Parent = current,
        }, { pad(PAD, PAD) })
        label({ Text = text, TextSize = 16, TextColor3 = T.sub, Size = UDim2.new(1, -90, 0, 32), Parent = row })
        local valueText = label({
            Font = FONT_BOLD,
            TextSize = 16,
            TextColor3 = T.white,
            AnchorPoint = Vector2.new(1, 0),
            Position = UDim2.fromScale(1, 0),
            Size = UDim2.fromOffset(90, 32),
            TextXAlignment = Enum.TextXAlignment.Right,
            Parent = row,
        })
        -- Tall invisible hit area around the thin track.
        local hit = make("TextButton", {
            Text = "",
            AutoButtonColor = false,
            Position = UDim2.fromOffset(0, 34),
            Size = UDim2.new(1, 0, 0, 18),
            BackgroundTransparency = 1,
            Parent = row,
        })
        local track = make("Frame", {
            AnchorPoint = Vector2.new(0, 0.5),
            Position = UDim2.fromScale(0, 0.5),
            Size = UDim2.new(1, 0, 0, 4),
            BackgroundColor3 = T.off,
            BorderSizePixel = 0,
            Parent = hit,
        }, { corner() })
        local fill = make("Frame", { Size = UDim2.fromScale(0, 1), BackgroundColor3 = T.white, BorderSizePixel = 0, Parent = track }, { corner() })
        local knob = make("Frame", {
            AnchorPoint = Vector2.new(0.5, 0.5),
            Position = UDim2.fromScale(0, 0.5),
            Size = UDim2.fromOffset(16, 16),
            BackgroundColor3 = T.white,
            BorderSizePixel = 0,
            Parent = track,
        }, { corner() })

        local value = math.clamp(initial, min, max)
        local function render()
            local a = (value - min) / (max - min)
            fill.Size = UDim2.fromScale(a, 1)
            knob.Position = UDim2.fromScale(a, 0.5)
            valueText.Text = format(value)
        end
        local control = {
            set = function(v)
                value = math.clamp(v, min, max)
                render()
                onChange(value)
            end,
        }
        local held = false
        local function setFromX(x)
            local a = math.clamp((x - track.AbsolutePosition.X) / track.AbsoluteSize.X, 0, 1)
            local v = math.clamp(math.floor((min + a * (max - min)) / step + 0.5) * step, min, max)
            if math.abs(v - value) > step / 2 then
                value = v
                render()
                onChange(v)
            end
        end
        local function isPress(input)
            return input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch
        end
        bind(hit.InputBegan, function(input)
            if isPress(input) then
                held = true
                setFromX(input.Position.X)
            end
        end)
        bind(UserInputService.InputChanged, function(input)
            if held and (input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch) then
                setFromX(input.Position.X)
            end
        end)
        bind(UserInputService.InputEnded, function(input)
            if held and isPress(input) then
                held = false
                saveSoon()
                if onRelease then
                    runSafely(text, function() onRelease(value) end)
                end
            end
        end)
        render()
        return control
    end

    -- A value on the right; click to open the options underneath.
    local function choiceRow(text, initial, onChange)
        local btn = make("TextButton", {
            LayoutOrder = nextOrder(),
            Text = "",
            AutoButtonColor = false,
            Size = UDim2.new(1, 0, 0, 46),
            BackgroundColor3 = T.raised,
            BackgroundTransparency = 1,
            BorderSizePixel = 0,
            Parent = current,
        }, { pad(PAD, PAD) })
        label({ Text = text, TextSize = 17, Size = UDim2.new(0.45, 0, 1, 0), Parent = btn })
        local valueText = label({
            Font = FONT_BOLD,
            TextSize = 16,
            TextColor3 = T.white,
            AnchorPoint = Vector2.new(1, 0),
            Position = UDim2.new(1, -20, 0, 0),
            Size = UDim2.new(0.55, -20, 1, 0),
            TextXAlignment = Enum.TextXAlignment.Right,
            TextTruncate = Enum.TextTruncate.AtEnd,
            Parent = btn,
        })
        local arrow = label({
            Text = "›",
            Font = FONT_BOLD,
            TextSize = 22,
            TextColor3 = T.sub,
            AnchorPoint = Vector2.new(1, 0.5),
            Position = UDim2.new(1, 0, 0.5, -2),
            Size = UDim2.fromOffset(12, 22),
            TextXAlignment = Enum.TextXAlignment.Center,
            Parent = btn,
        })
        local list = make("Frame", {
            LayoutOrder = nextOrder(),
            Size = UDim2.new(1, 0, 0, 0),
            AutomaticSize = Enum.AutomaticSize.Y,
            BackgroundColor3 = T.side,
            BorderSizePixel = 0,
            Visible = false,
            Parent = current,
        }, {
            make("UIListLayout", { SortOrder = Enum.SortOrder.LayoutOrder }),
            pad(0, 0, 4, 4),
        })
        hoverRow(btn)

        local selected = initial
        local options = {}
        local function setOpen(open)
            list.Visible = open
            tween(arrow, { Rotation = open and 90 or 0 })
        end
        local function render()
            for _, child in ipairs(list:GetChildren()) do
                if child:IsA("TextButton") then child:Destroy() end
            end
            local selectedLabel
            for i, option in ipairs(options) do
                local isSelected = option.value == selected
                if isSelected then selectedLabel = option.label end
                local b = make("TextButton", {
                    LayoutOrder = i,
                    Text = "",
                    AutoButtonColor = false,
                    Size = UDim2.new(1, 0, 0, 34),
                    BackgroundColor3 = T.raised,
                    BackgroundTransparency = 1,
                    BorderSizePixel = 0,
                    Parent = list,
                }, { pad(PAD + 14, PAD) })
                label({
                    Text = option.label,
                    Font = isSelected and FONT_BOLD or FONT,
                    TextSize = 15,
                    TextColor3 = isSelected and T.white or T.sub,
                    Size = UDim2.fromScale(1, 1),
                    Parent = b,
                })
                if isSelected then
                    make("Frame", {
                        AnchorPoint = Vector2.new(0, 0.5),
                        Position = UDim2.new(0, -12, 0.5, 0),
                        Size = UDim2.fromOffset(3, 14),
                        BackgroundColor3 = T.white,
                        BorderSizePixel = 0,
                        Parent = b,
                    }, { corner() })
                end
                hoverRow(b)
                bind(b.MouseButton1Click, function()
                    selected = option.value
                    setOpen(false)
                    render()
                    onChange(option.value)
                    saveSoon()
                end)
            end
            valueText.Text = selectedLabel or (#options > 0 and "Choose" or "Loading…")
        end
        bind(btn.MouseButton1Click, function()
            if #options > 0 then setOpen(not list.Visible) end
        end)
        render()
        return {
            setOptions = function(newOptions, newSelected)
                options = newOptions
                if newSelected ~= nil then selected = newSelected end
                render()
            end,
        }
    end

    local function namesToOptions(names)
        local options = {}
        for _, name in ipairs(names) do
            table.insert(options, { value = name, label = name })
        end
        return options
    end

    -- A pill button on the right, styled like Unload: white on hover.
    local function actionRow(text, buttonText, fn, hint)
        local row = make("Frame", {
            LayoutOrder = nextOrder(),
            Size = UDim2.new(1, 0, 0, hint and 60 or 46),
            BackgroundTransparency = 1,
            Parent = current,
        }, { pad(PAD, PAD) })
        rowText(row, text, hint, 130)
        local button = make("TextButton", {
            Text = buttonText,
            Font = FONT_BOLD,
            TextSize = 14,
            TextColor3 = T.text,
            AutoButtonColor = false,
            AnchorPoint = Vector2.new(1, 0.5),
            Position = UDim2.new(1, 0, 0.5, 0),
            Size = UDim2.fromOffset(0, 30),
            AutomaticSize = Enum.AutomaticSize.X,
            BackgroundColor3 = T.raised,
            BorderSizePixel = 0,
            Parent = row,
        }, { corner(6), pad(16, 16) })
        bind(button.MouseEnter, function() tween(button, { BackgroundColor3 = T.white, TextColor3 = T.black }) end)
        bind(button.MouseLeave, function() tween(button, { BackgroundColor3 = T.raised, TextColor3 = T.text }) end)
        bind(button.MouseButton1Click, function()
            runSafely(text, fn)
        end)
        return button
    end

    local function inputRow(text, placeholder, initial, onCommit)
        local row = make("Frame", {
            LayoutOrder = nextOrder(),
            Size = UDim2.new(1, 0, 0, 50),
            BackgroundTransparency = 1,
            Parent = current,
        }, { pad(PAD, PAD) })
        rowText(row, text, nil, 260)
        local boxField = make("TextBox", {
            Text = initial,
            PlaceholderText = placeholder,
            PlaceholderColor3 = T.dim,
            ClearTextOnFocus = false,
            Font = FONT,
            TextSize = 15,
            TextColor3 = T.white,
            TextXAlignment = Enum.TextXAlignment.Left,
            TextTruncate = Enum.TextTruncate.AtEnd,
            AnchorPoint = Vector2.new(1, 0.5),
            Position = UDim2.new(1, 0, 0.5, 0),
            Size = UDim2.fromOffset(250, 32),
            BackgroundColor3 = T.raised,
            BorderSizePixel = 0,
            Parent = row,
        }, { corner(6), pad(12, 12) })
        local stroke = make("UIStroke", { Color = T.line, ApplyStrokeMode = Enum.ApplyStrokeMode.Border, Parent = boxField })
        local committed = initial
        bind(boxField.Focused, function() tween(stroke, { Color = T.sub }) end)
        bind(boxField.FocusLost, function()
            tween(stroke, { Color = T.line })
            if boxField.Text ~= committed then
                committed = boxField.Text
                onCommit(boxField.Text)
            end
        end)
    end

    local function swatchRow(text, colors, onPick)
        local row = make("Frame", {
            LayoutOrder = nextOrder(),
            Size = UDim2.new(1, 0, 0, 46),
            BackgroundTransparency = 1,
            Parent = current,
        }, { pad(PAD, PAD) })
        rowText(row, text, nil, 240)
        local holder = make("Frame", {
            AnchorPoint = Vector2.new(1, 0.5),
            Position = UDim2.new(1, 0, 0.5, 0),
            Size = UDim2.fromOffset(230, 22),
            BackgroundTransparency = 1,
            Parent = row,
        }, {
            make("UIListLayout", {
                FillDirection = Enum.FillDirection.Horizontal,
                HorizontalAlignment = Enum.HorizontalAlignment.Right,
                VerticalAlignment = Enum.VerticalAlignment.Center,
                SortOrder = Enum.SortOrder.LayoutOrder,
                Padding = UDim.new(0, 8),
            }),
        })
        for i, color in ipairs(colors) do
            local swatch = make("TextButton", {
                LayoutOrder = i,
                Text = "",
                AutoButtonColor = false,
                Size = UDim2.fromOffset(22, 22),
                BackgroundColor3 = color,
                BorderSizePixel = 0,
                Parent = holder,
            }, { corner() })
            local stroke = make("UIStroke", { Color = T.off, Thickness = 1.5, Parent = swatch })
            bind(swatch.MouseEnter, function() tween(stroke, { Color = T.white }) end)
            bind(swatch.MouseLeave, function() tween(stroke, { Color = T.off }) end)
            bind(swatch.MouseButton1Click, function()
                runSafely(text, function() onPick(color) end)
            end)
        end
    end

    local function keyRow(text, bindName, hint)
        local btn = make("TextButton", {
            LayoutOrder = nextOrder(),
            Text = "",
            AutoButtonColor = false,
            Size = UDim2.new(1, 0, 0, hint and 60 or 46),
            BackgroundColor3 = T.raised,
            BackgroundTransparency = 1,
            BorderSizePixel = 0,
            Parent = current,
        }, { pad(PAD, PAD) })
        rowText(btn, text, hint, 150)
        local pill = make("TextLabel", {
            AnchorPoint = Vector2.new(1, 0.5),
            Position = UDim2.new(1, 0, 0.5, 0),
            Size = UDim2.fromOffset(0, 30),
            AutomaticSize = Enum.AutomaticSize.X,
            BackgroundColor3 = T.raised,
            Font = FONT_BOLD,
            TextSize = 14,
            TextColor3 = T.white,
            Text = "",
            Parent = btn,
        }, { corner(6), pad(14, 14) })
        keyRows[bindName] = function()
            local waiting = bindingKey == bindName
            pill.Text = waiting and "Press a key…" or (Settings.binds[bindName] or "None")
            pill.BackgroundColor3 = waiting and T.white or T.raised
            pill.TextColor3 = waiting and T.black or T.white
        end
        hoverRow(btn)
        bind(btn.MouseButton1Click, function()
            bindingKey = bindName
            renderKeyRows()
        end)
        keyRows[bindName]()
    end

    local function note(text)
        return label({
            LayoutOrder = nextOrder(),
            Text = text,
            TextSize = 13,
            TextColor3 = T.dim,
            Size = UDim2.new(1, 0, 0, 36),
            TextXAlignment = Enum.TextXAlignment.Center,
            TextWrapped = true,
            Parent = current,
        })
    end

    refreshModule = function(name)
        local m = ModuleByName[name]
        if m then
            m.render()
        end
    end

    local function fmtTimes(v) return string.format("%.1fx", v) end

    ---------------------------------------------------------------------------------------------
    -- Pages
    ---------------------------------------------------------------------------------------------

    -- Vehicle -------------------------------------------------------------------------------------
    current = pages.Vehicle
    passNote = note("Checking your gamepasses…")
    sectionHeader("Spawn")
    carPicker = choiceRow("Car", Settings.car, function(value)
        Settings.car = value
    end)
    actionRow("Spawn car", "Spawn", spawnCar, "Get out of any seat first")
    toggleRow(setting("Auto-tune", "autoTune", {
        hint = "Top speed, turbo, grip and underglow as soon as you get in",
        array = false,
    }))

    sectionHeader("Handling")
    toggleRow(setting("Performance", "performance", {
        hint = "Harder launches and quicker steering; top speed stays at your cap",
        set = function(v)
            Settings.performance = v
            applyPhysics()
        end,
        suffix = function() return fmtTimes(Settings.accel) end,
    }))
    -- Brookhaven turns the wheels toward the target angle at TurnSpeed per second (about 60 is
    -- instant), and ramps throttle and power from Turbo and Torque, which Acceleration scales.
    local accelSlider = sliderRow("Acceleration", 1, 5, 0.1, Settings.accel, fmtTimes, function(v)
        Settings.accel = v
        applyPhysics()
    end)
    local steerSlider = sliderRow("Steering response", 1, 6, 0.1, Settings.steer, fmtTimes, function(v)
        Settings.steer = v
        applyPhysics()
    end)
    actionRow("Max response", "Max out", function()
        Settings.performance = true
        refreshModule("Performance")
        accelSlider.set(5)
        steerSlider.set(6)
        saveSoon()
        notify("Max response", "Acceleration 5x · steering 6x", "good")
    end, "Fastest launches and near-instant steering")
    toggleRow(setting("Grip", "handling", {
        hint = "No sliding in sharp turns; capped per car so tall ones can't flip",
        set = function(v)
            Settings.handling = v
            applyTires()
        end,
    }))
    sliderRow("Tire grip", 0.5, 2, 0.05, Settings.tireGrip, function(v) return string.format("%.2f", v) end, function(v)
        Settings.tireGrip = v
        applyTires()
    end, function()
        local s = Session
        if s and tuningTires(s) then
            local grip = tireFriction(s)
            local ok, msg = carRequest("SetDriftStrength", math.min(grip, 1.5))
            if not ok then
                notify("Grip", "Refused: " .. tostring(msg), "bad")
            elseif grip < Settings.tireGrip then
                notify("Grip", string.format("Capped at %.2f so this car can't flip", grip))
            end
        end
    end)
    sliderRow("Stability control", 0, 100, 5, Settings.traction, function(v) return string.format("%d%%", v) end, function(v)
        Settings.traction = v
    end)
    toggleRow(setting("Anti-flip", "antiRoll", { array = false }))
    toggleRow(setting("Downforce", "grip", { array = false }))
    toggleRow(setting("Brake Assist", "brakeAssist", {
        hint = "Much harder braking when you press against your direction",
        suffix = function() return string.format("%.1fg", Settings.brakeStrength) end,
    }))
    sliderRow("Brake strength", 0.5, 2, 0.1, Settings.brakeStrength, function(v) return string.format("%.1fg", v) end, function(v)
        Settings.brakeStrength = v
    end)
    toggleRow(setting("Drift Mode", "drift", {
        hint = "Hold the drift key (Settings → Keybinds) to let the rear slide",
        set = function(v)
            Settings.drift = v
            if not v and Session and Session.drifting then
                Session.drifting = false
                applyTires()
            end
        end,
    }))
    sliderRow("Drift grip", 0.1, 1, 0.05, Settings.driftGrip, function(v) return string.format("%.2f", v) end, function(v)
        Settings.driftGrip = v
        if Session and Session.drifting then applyTires() end
    end)

    sectionHeader("Car")
    toggleRow(setting("Auto Flip", "autoFlip", { hint = "Puts the car back on its wheels if it ends up on its roof" }))
    toggleRow(setting("Auto Headlights", "autoLights", { hint = "Lights on at night and off in the day; V still works" }))
    sliderRow("Ride height", 0, 4, 1, Settings.rideHeight, function(v)
        return v == 0 and "Car default" or ("Level " .. v)
    end, function(v)
        Settings.rideHeight = v
    end)
    actionRow("Apply ride height", "Apply", function()
        if not Session then
            notify("Ride height", "Get in your car first", "bad")
            return
        end
        local level = Settings.rideHeight > 0 and Settings.rideHeight or defaultRideLevel(Session.car)
        if not level then
            notify("Ride height", "This car has no ride height settings", "bad")
            return
        end
        local ok, msg = carRequest("SetSuspensionLevel", level)
        notify("Ride height", ok and ("Level " .. level) or ("Refused: " .. tostring(msg)), ok and "good" or "bad")
    end)
    actionRow("Flip upright", "Flip", function()
        local ok, msg = carRequest("RepositionVehicle")
        notify("Flip upright", ok and "Car repositioned" or ("Refused: " .. tostring(msg)), ok and "good" or "bad")
    end)
    actionRow("Headlights", "Toggle", function()
        local s = Session
        if not s then
            notify("Headlights", "Get in your car first", "bad")
            return
        end
        -- The server answers with the new light state, not a success flag.
        local called, lightsOn = invoke("ToggleHeadLights")
        if called and type(lightsOn) == "boolean" then
            s.lightsOn = lightsOn
            notify("Headlights", lightsOn and "On" or "Off", "good")
        else
            notify("Headlights", "Request failed: " .. tostring(lightsOn), "bad")
        end
    end)
    toggleRow({
        name = "Fire",
        quiet = true,
        array = false,
        get = function() return Session ~= nil and Session.car:GetAttribute("OnFire") == true end,
        set = function(v) task.spawn(setCarEffect, "OnFire", "ToggleOnFire", v) end,
    })
    toggleRow({
        name = "Engine Smoke",
        quiet = true,
        array = false,
        get = function() return Session ~= nil and Session.car:GetAttribute("Smoking") == true end,
        set = function(v) task.spawn(setCarEffect, "Smoking", "ToggleEngineSmoke", v) end,
    })

    -- Stunts --------------------------------------------------------------------------------------
    current = pages.Stunts
    local stuntNames = {}
    for _, def in ipairs(STUNTS) do
        table.insert(stuntNames, def.name)
    end
    sectionHeader("On a key")
    choiceRow("Trick", Settings.stunt, function(value)
        Settings.stunt = value
    end).setOptions(namesToOptions(stuntNames))
    sliderRow("Airtime", 0.8, 2.5, 0.1, Settings.stuntAirtime, function(v) return string.format("%.1fs", v) end, function(v)
        Settings.stuntAirtime = v
    end)
    actionRow("Do the trick", "Go", function()
        performStunt(findStunt(Settings.stunt), Settings.stuntAirtime, "Stunt")
    end, "Or press your Stunt key while driving, even mid-jump")
    actionRow("Car hop", "Hop", function()
        performStunt(HOP, Settings.hopAirtime, "Car Hop")
    end, "A quick hop that lands flat; or press your Car Hop key")
    sliderRow("Hop airtime", 0.3, 1.2, 0.05, Settings.hopAirtime, function(v) return string.format("%.2fs", v) end, function(v)
        Settings.hopAirtime = v
    end)

    sectionHeader("In the air")
    toggleRow(setting("Air Stunts", "airStunts", {
        hint = "Drive off something high and the car does the trick by itself",
        suffix = function() return Settings.airTrick end,
    }))
    do
        local options = namesToOptions(stuntNames)
        table.insert(options, { value = "Random", label = "Random" })
        choiceRow("Air trick", Settings.airTrick, function(value)
            Settings.airTrick = value
        end).setOptions(options)
    end
    toggleRow(setting("Every Jump", "airAlways", {
        hint = "Any jump gets the trick: short ones get a boost so it always finishes",
        array = false,
    }))
    sliderRow("Minimum jump", 0.1, 0.8, 0.05, Settings.airMin, function(v) return string.format("%.2fs", v) end, function(v)
        Settings.airMin = v
    end)
    note("Jumps shorter than this are ignored, so curbs and bumps never flip you.")
    toggleRow(setting("Landing Assist", "landAssist", { hint = "Jumps too short for a trick still land flat on four wheels" }))
    toggleRow(setting("Stunt Trail", "stuntTrail", { hint = "White streaks behind the car in the air (only you see them)" }))

    -- Visuals -------------------------------------------------------------------------------------
    current = pages.Visuals
    sectionHeader("Paint")
    toggleRow(setting("Car RGB", "carRGB", {
        hint = "Smooth color fade, driving or parked; off puts your color back",
        set = function(v) setCarRGB(v) end,
        suffix = function() return Settings.rgbMode end,
    }))
    choiceRow("Mode", Settings.rgbMode, function(value)
        Settings.rgbMode = value
    end).setOptions(namesToOptions(RGB_MODES))
    sliderRow("Cycle length", 1, 20, 0.5, Settings.rgbCycle, function(v) return string.format("%.1fs", v) end, function(v)
        Settings.rgbCycle = v
    end)
    swatchRow("Paint once", SWATCHES, function(color)
        local car = ownCar()
        if not car then
            notify("Paint", "Spawn a car first", "bad")
            return
        end
        if Settings.carRGB then
            Settings.carRGB = false
            saveSoon()
            refreshModule("Car RGB")
            task.wait(0.4) -- let any in-flight RGB step land first
        end
        local ok, msg = paintCar(car, color)
        if ok then
            originalPaints[car] = color
            notify("Paint", "Car painted", "good")
        else
            notify("Paint", "Refused: " .. tostring(msg), "bad")
        end
    end)

    sectionHeader("Underglow")
    toggleRow(setting("RGB Underglow", "rainbowGlow", { hint = "Glow follows your paint, or rainbows on its own" }))
    choiceRow("Style", Settings.underglow, function(value)
        Settings.underglow = value
    end).setOptions(namesToOptions(UNDERGLOWS))
    sliderRow("Length", 0.1, 1.5, 0.05, Settings.glowLength, function(v) return string.format("%.2f", v) end, function(v)
        Settings.glowLength = v
    end)
    sliderRow("Width", 0.1, 1.5, 0.05, Settings.glowWidth, function(v) return string.format("%.2f", v) end, function(v)
        Settings.glowWidth = v
    end)
    actionRow("Apply underglow", "Apply", function()
        local ok, msg = carRequest("SetUnderglow", Settings.underglow)
        if ok then
            carRequest("SetUnderglowSize", Settings.glowLength, Settings.glowWidth)
            notify("Underglow", Settings.underglow, "good")
        else
            notify("Underglow", "Refused: " .. tostring(msg), "bad")
        end
    end)

    sectionHeader("Camera and world")
    toggleRow(setting("Speedometer", "speedo", {}))
    toggleRow(setting("Fullbright", "fullbright", {
        set = function(v)
            Settings.fullbright = v
            syncLightingSnapshot()
        end,
    }))
    local function hourText(v)
        return string.format("%02d:%s", math.floor(v) % 24, v % 1 >= 0.5 and "30" or "00")
    end
    toggleRow(setting("Time Lock", "lockTime", {
        set = function(v)
            Settings.lockTime = v
            syncLightingSnapshot()
        end,
        suffix = function() return hourText(Settings.hour) end,
    }))
    sliderRow("Hour", 0, 24, 0.5, Settings.hour, hourText, function(v)
        Settings.hour = v
    end)
    toggleRow(setting("FOV", "fovOn", {
        set = function(v)
            Settings.fovOn = v
            syncFov()
        end,
        suffix = function() return tostring(Settings.fov) end,
    }))
    sliderRow("Field of view", 50, 120, 1, Settings.fov, tostring, function(v)
        Settings.fov = v
    end)
    toggleRow(setting("Unlock Zoom", "unlockZoom", {
        set = function(v)
            Settings.unlockZoom = v
            syncZoom()
        end,
    }))

    -- Player --------------------------------------------------------------------------------------
    current = pages.Player
    sectionHeader("Roleplay")
    -- Brookhaven ignores blank names and bios (tested), so an empty box only stops re-applying it.
    inputRow("Name", "Shown above your head", Settings.rpName, function(text)
        Settings.rpName = text
        saveSoon()
        if text:gsub("%s", "") == "" then
            notify("Roleplay name", "Brookhaven won't blank a name; it won't be re-applied")
            return
        end
        NameTextEvent:FireServer("RolePlayName", text)
        notify("Roleplay name", "Set to " .. text, "good")
    end)
    inputRow("Bio", "Shown under your name", Settings.rpBio, function(text)
        Settings.rpBio = text
        saveSoon()
        if text:gsub("%s", "") == "" then
            notify("Roleplay bio", "Brookhaven won't blank a bio; it won't be re-applied")
            return
        end
        NameTextEvent:FireServer("RolePlayBio", text)
        notify("Roleplay bio", "Bio updated", "good")
    end)
    toggleRow(setting("Rainbow Name", "rainbowName", {
        hint = "Cycles your roleplay name's color (needs a name set)",
        set = function(v)
            Settings.rainbowName = v
            if not v then
                task.delay(1.2, function()
                    if alive and not Settings.rainbowName then
                        NameColorEvent:FireServer("PickingRPNameColor", Color3.new(1, 1, 1))
                    end
                end)
            end
        end,
    }))
    sectionHeader("Session")
    toggleRow(setting("Anti-AFK", "antiAfk", { hint = "Stops the 20-minute idle kick" }))

    -- House ---------------------------------------------------------------------------------------
    current = pages.House
    sectionHeader("Color")
    toggleRow(setting("Rainbow House", "houseRainbow", {
        hint = "Your house fades through the colors; starts when you claim one",
        suffix = function() return string.format("%ds", Settings.houseCycle) end,
    }))
    sliderRow("Cycle length", 6, 60, 1, Settings.houseCycle, function(v) return string.format("%ds", v) end, function(v)
        Settings.houseCycle = v
    end)
    swatchRow("Paint once", SWATCHES, function(color)
        if HouseNumber.Value <= 0 then
            notify("House paint", "Claim a house first", "bad")
            return
        end
        Settings.houseRainbow = false
        saveSoon()
        refreshModule("Rainbow House")
        setHouseColor(color)
        notify("House paint", "House painted", "good")
    end)
    sectionHeader("Saved props")
    buildPicker = choiceRow("Build", Settings.buildSlot, function(value)
        Settings.buildSlot = value
    end)
    actionRow("Load props", "Load", function()
        loadBuild(Settings.buildSlot)
    end, "Into the house you own")
    toggleRow(setting("Load on claim", "loadBuild", {
        hint = "Loads the build when you claim a house",
        array = false,
    }))

    -- Settings ------------------------------------------------------------------------------------
    current = pages.Settings
    sectionHeader("Interface")
    toggleRow(setting("ArrayList", "arraylist", { hint = "Enabled modules in the top-right corner", array = false }))
    toggleRow(setting("Toggle alerts", "toggleNotify", { hint = "A notification when you switch something on or off", array = false, quiet = true }))
    toggleRow(setting("Background blur", "blur", {
        array = false,
        set = function(v)
            Settings.blur = v
            tween(menuBlur, { Size = (v and menuOpen) and 14 or 0 })
        end,
    }))
    actionRow("Recenter menu", "Reset", function()
        basePos = UDim2.fromScale(0.5, 0.5)
        tween(box, { Position = basePos }, GLIDE)
    end)
    sectionHeader("Keybinds")
    keyRow("Stunt", "Stunt")
    keyRow("Car hop", "Car Hop")
    keyRow("Drift (hold)", "Drift")
    note("Click a key, then press the new one. Esc clears it. RightShift opens this menu.")

    -- Unload button (bottom of the sidebar), styled like Snitch's.
    do
        local btn = make("TextButton", {
            Text = "Unload",
            Font = FONT_BOLD,
            TextSize = 15,
            TextColor3 = T.text,
            AutoButtonColor = false,
            Size = UDim2.fromScale(1, 1),
            BackgroundColor3 = T.raised,
            BorderSizePixel = 0,
            Parent = unloadHolder,
        }, { corner(6) })
        bind(btn.MouseEnter, function() tween(btn, { BackgroundColor3 = T.white, TextColor3 = T.black }) end)
        bind(btn.MouseLeave, function() tween(btn, { BackgroundColor3 = T.raised, TextColor3 = T.text }) end)
        bind(btn.MouseButton1Click, function()
            if unloadRef then unloadRef() end
        end)
    end
end

---------------------------------------------------------------------------------------------
-- HUD (always on screen): Minecraft-client arraylist, top right, sorted longest first
---------------------------------------------------------------------------------------------

local placeArrayList
do
    local HUD_FONT = Enum.Font.Arcade
    local HUD_SIZE = 15
    local HUD_LINE = 23
    local SHADOW = Color3.fromRGB(38, 38, 40)

    local hud = make("Frame", {
        AnchorPoint = Vector2.new(1, 0),
        Position = UDim2.new(1, -4, 0, 4),
        Size = UDim2.fromOffset(400, 560),
        BackgroundTransparency = 1,
        Parent = Gui,
    }, {
        make("UIListLayout", { SortOrder = Enum.SortOrder.LayoutOrder, HorizontalAlignment = Enum.HorizontalAlignment.Right }),
    })

    -- Text with a 1px Minecraft shadow; returns the white label (the shadow follows its Text).
    local function shadowText(parent, layoutOrder, size, color, height)
        local holder = make("Frame", {
            LayoutOrder = layoutOrder,
            Size = UDim2.fromOffset(0, height),
            AutomaticSize = Enum.AutomaticSize.X,
            BackgroundTransparency = 1,
            Parent = parent,
        })
        local shadow = label({
            Text = "",
            Font = HUD_FONT,
            TextSize = size,
            TextColor3 = SHADOW,
            Position = UDim2.fromOffset(1, 1),
            Size = UDim2.fromOffset(0, height),
            AutomaticSize = Enum.AutomaticSize.X,
            Parent = holder,
        })
        local main = label({
            Text = "",
            Font = HUD_FONT,
            TextSize = size,
            TextColor3 = color,
            Size = UDim2.fromOffset(0, height),
            AutomaticSize = Enum.AutomaticSize.X,
            Parent = holder,
        })
        bind(main:GetPropertyChangedSignal("Text"), function()
            shadow.Text = main.Text
        end)
        return main, holder
    end

    local watermark = make("Frame", {
        LayoutOrder = -1000000,
        Size = UDim2.fromOffset(0, 30),
        AutomaticSize = Enum.AutomaticSize.X,
        BackgroundTransparency = 1,
        Parent = hud,
    }, { pad(0, 4) })
    local wmText = shadowText(watermark, 1, 22, T.white, 30)
    wmText.Text = "BH STUDIO"

    -- Lines: [name][suffix][gap][bar]. The list layout places every child, so the white bar is
    -- the last item in the row rather than an absolutely positioned frame.
    local hudEntries = {}
    for _, m in ipairs(Modules) do
        if m.def.array ~= false then
            local frame = make("Frame", {
                Size = UDim2.fromOffset(0, HUD_LINE),
                AutomaticSize = Enum.AutomaticSize.X,
                BackgroundColor3 = T.black,
                BackgroundTransparency = 0.5,
                BorderSizePixel = 0,
                Visible = false,
                Parent = hud,
            }, {
                pad(6, 0),
                make("UIListLayout", {
                    FillDirection = Enum.FillDirection.Horizontal,
                    VerticalAlignment = Enum.VerticalAlignment.Center,
                    SortOrder = Enum.SortOrder.LayoutOrder,
                    Padding = UDim.new(0, 7),
                }),
            })
            local main = shadowText(frame, 1, HUD_SIZE, T.white, HUD_LINE)
            main.Text = m.name
            local suffix, suffixHolder = shadowText(frame, 2, HUD_SIZE, T.sub, HUD_LINE)
            local bar = make("Frame", {
                LayoutOrder = 3,
                Size = UDim2.fromOffset(2, HUD_LINE),
                BackgroundColor3 = T.white,
                BorderSizePixel = 0,
                Parent = frame,
            })
            table.insert(hudEntries, { m = m, frame = frame, main = main, suffix = suffix, suffixHolder = suffixHolder, bar = bar })
        end
    end

    -- Brookhaven's HUD owns the top-right corner (clock and ad icons along the top, the menu
    -- column down the right edge), so the list sits just inside it. Measured from the live HUD
    -- so it fits any screen size.
    local function shownOnScreen(inst, screenGui)
        local node = inst
        while node and node ~= screenGui do
            if node:IsA("GuiObject") and not node.Visible then return false end
            node = node.Parent
        end
        return true
    end

    placeArrayList = function()
        local screen = Gui.AbsoluteSize
        local origin = Gui.AbsolutePosition
        local column, topRow = {}, {}
        for _, screenGui in ipairs(LocalPlayer.PlayerGui:GetChildren()) do
            if screenGui:IsA("ScreenGui") and screenGui.Enabled then
                for _, d in ipairs(screenGui:GetDescendants()) do
                    if (d:IsA("GuiButton") or d:IsA("ImageLabel") or d:IsA("TextLabel")) and d.Visible and d.AbsoluteSize.X > 20 then
                        local x = d.AbsolutePosition.X - origin.X
                        local y = d.AbsolutePosition.Y - origin.Y
                        local w, h = d.AbsoluteSize.X, d.AbsoluteSize.Y
                        if d:IsA("GuiButton") and x + w > screen.X - 24 and y < screen.Y * 0.7 and shownOnScreen(d, screenGui) then
                            table.insert(column, x)
                        elseif x + w > screen.X - 480 and y < 80 and h < 80 and shownOnScreen(d, screenGui) then
                            table.insert(topRow, { x + w, y + h })
                        end
                    end
                end
            end
        end
        local right = 4
        for _, x in ipairs(column) do
            right = math.max(right, screen.X - x + 8)
        end
        -- Only top-row items above the list's own span (entries are under ~220 px wide) push it down.
        local top = 4
        for _, item in ipairs(topRow) do
            if item[1] > screen.X - right - 220 then
                top = math.max(top, item[2] + 6)
            end
        end
        hud.Position = UDim2.new(1, -right, 0, top)
    end

    -- Every line sorted by its rendered width, widest on top; a slow white-to-grey wave rolls
    -- down the list (the monochrome take on the client rainbow).
    bind(RunService.RenderStepped, function()
        hud.Visible = Settings.arraylist
        if not Settings.arraylist then return end
        local visible = {}
        for _, e in ipairs(hudEntries) do
            local on = e.m.def.get() == true
            if on and not e.frame.Visible then
                e.frame.Visible = true
                e.main.TextTransparency = 1
                tween(e.main, { TextTransparency = 0 })
            elseif not on and e.frame.Visible then
                e.frame.Visible = false
            end
            if on then
                local text = e.m.def.suffix and e.m.def.suffix() or ""
                if e.suffix.Text ~= text then e.suffix.Text = text end
                e.suffixHolder.Visible = text ~= ""
                table.insert(visible, e)
            end
        end
        table.sort(visible, function(a, b)
            local wa, wb = a.frame.AbsoluteSize.X, b.frame.AbsoluteSize.X
            if wa ~= wb then return wa > wb end
            return a.m.name < b.m.name
        end)
        local t = os.clock()
        local function wave(i)
            local k = 0.5 + 0.5 * math.sin(t * 2.2 - i * 0.55)
            return Color3.fromRGB(245, 245, 245):Lerp(Color3.fromRGB(150, 150, 154), k)
        end
        wmText.TextColor3 = wave(0)
        for i, e in ipairs(visible) do
            e.frame.LayoutOrder = i
            local color = wave(i)
            e.main.TextColor3 = color
            e.bar.BackgroundColor3 = color
        end
    end)
end

---------------------------------------------------------------------------------------------
-- Speedometer (your screen only). Speed is studs/s converted at 1 stud = 0.28 m.
---------------------------------------------------------------------------------------------

do
    local Speedo = scaled(make("Frame", {
        Name = "Speedometer",
        AnchorPoint = Vector2.new(0.5, 1),
        Position = UDim2.new(0.5, 0, 1, -24),
        Size = UDim2.fromOffset(230, 64),
        BackgroundColor3 = T.bg,
        BackgroundTransparency = 0.04,
        BorderSizePixel = 0,
        Visible = false,
        Parent = Gui,
    }, {
        corner(10),
        make("UIStroke", { Color = T.line, ApplyStrokeMode = Enum.ApplyStrokeMode.Border }),
    }))
    local speedNumber = label({
        Text = "0",
        Font = FONT_BLACK,
        TextSize = 30,
        TextColor3 = T.white,
        Position = UDim2.fromOffset(16, 7),
        Size = UDim2.fromOffset(90, 32),
        Parent = Speedo,
    })
    label({
        Text = "MPH",
        Font = FONT_BOLD,
        TextSize = 11,
        TextColor3 = T.sub,
        Position = UDim2.fromOffset(17, 40),
        Size = UDim2.fromOffset(60, 12),
        Parent = Speedo,
    })
    local speedCar = label({
        Text = "",
        TextSize = 13,
        TextColor3 = T.sub,
        Position = UDim2.fromOffset(104, 13),
        Size = UDim2.new(1, -120, 0, 16),
        TextXAlignment = Enum.TextXAlignment.Right,
        TextTruncate = Enum.TextTruncate.AtEnd,
        Parent = Speedo,
    })
    local speedTrack = make("Frame", {
        Position = UDim2.fromOffset(104, 39),
        Size = UDim2.new(1, -120, 0, 4),
        BackgroundColor3 = T.off,
        BorderSizePixel = 0,
        Parent = Speedo,
    }, { corner() })
    local speedFill = make("Frame", { Size = UDim2.new(0, 0, 1, 0), BackgroundColor3 = T.white, BorderSizePixel = 0, Parent = speedTrack }, { corner() })

    local shownSpeed = 0
    bind(RunService.RenderStepped, function(dt)
        local s = Session
        local driving = s ~= nil
        Speedo.Visible = Settings.speedo and driving
        if not driving then
            shownSpeed = 0
            return
        end
        local root = s.seat.AssemblyRootPart
        local mph = root and root.AssemblyLinearVelocity.Magnitude * 0.28 * 2.237 or 0
        shownSpeed += (mph - shownSpeed) * math.min(dt * 8, 1)
        local whole = math.floor(shownSpeed + 0.5)
        if Settings.speedo then
            speedNumber.Text = tostring(whole)
            speedFill.Size = UDim2.new(math.clamp(shownSpeed / 160, 0, 1), 0, 1, 0)
            speedCar.Text = s.car:GetAttribute("vehicleName") or ""
        end
        if menuOpen and cardActive then
            local parts = { whole .. " mph", stuntCount .. (stuntCount == 1 and " stunt" or " stunts") }
            if cardNote ~= "" then
                table.insert(parts, 1, cardNote)
            end
            cardLine.Text = table.concat(parts, " · ")
        end
    end)
end

---------------------------------------------------------------------------------------------
-- Opening, closing, dragging and keys
---------------------------------------------------------------------------------------------

local dragging = false
do
    local dragStart, startPos
    bind(header.InputBegan, function(input)
        if menuOpen and (input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch) then
            dragging, dragStart, startPos = true, input.Position, box.Position
        end
    end)
    bind(UserInputService.InputChanged, function(input)
        if dragging and (input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch) then
            local d = input.Position - dragStart
            box.Position = UDim2.new(startPos.X.Scale, startPos.X.Offset + d.X, startPos.Y.Scale, startPos.Y.Offset + d.Y)
            basePos = box.Position
        end
    end)
    bind(UserInputService.InputEnded, function(input)
        if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
            dragging = false
        end
    end)
end

local function setMenu(open)
    menuOpen = open
    modal.Modal = open
    modal.Visible = open
    dragging = false
    if open then
        box.Visible = true
        box.Position = basePos
        -- Never open partly off-screen.
        local pos, size, screen = box.AbsolutePosition, box.AbsoluteSize, Gui.AbsoluteSize
        if pos.X < 0 or pos.Y < 0 or pos.X + size.X > screen.X or pos.Y + size.Y > screen.Y then
            basePos = UDim2.fromScale(0.5, 0.5)
            box.Position = basePos
        end
        boxScale.Scale = guiScale * 0.94
        local info = TweenInfo.new(0.34, Enum.EasingStyle.Quint, Enum.EasingDirection.Out)
        tween(box, { GroupTransparency = 0 }, info)
        tween(boxScale, { Scale = guiScale }, info)
        tween(menuBlur, { Size = Settings.blur and 14 or 0 }, info)
        refreshVehicleStatus()
    else
        if bindingKey then
            bindingKey = nil
            renderKeyRows()
        end
        local info = TweenInfo.new(0.18, Enum.EasingStyle.Quad, Enum.EasingDirection.In)
        tween(box, { GroupTransparency = 1 }, info)
        tween(boxScale, { Scale = guiScale * 0.96 }, info)
        tween(menuBlur, { Size = 0 }, info)
        task.delay(0.2, function()
            if not menuOpen then box.Visible = false end
        end)
    end
end

local function setDrift(on)
    local s = Session
    if not s or not s.geom.valid or s.drifting == on then return end
    s.drifting = on
    applyTires()
end

-- The game binds Shift itself, so RightShift can arrive as "processed": only ignore keys
-- while you're typing in a text box (chat etc.).
bind(UserInputService.InputBegan, function(input)
    if input.UserInputType ~= Enum.UserInputType.Keyboard then return end
    if bindingKey then
        local name = bindingKey
        bindingKey = nil
        if input.KeyCode == Enum.KeyCode.Escape or input.KeyCode == Enum.KeyCode.Backspace then
            Settings.binds[name] = nil
            notify("Keybinds", name .. " cleared")
        elseif input.KeyCode ~= Enum.KeyCode.RightShift then
            Settings.binds[name] = input.KeyCode.Name
            notify("Keybinds", name .. " is now " .. input.KeyCode.Name, "good")
        end
        saveSoon()
        renderKeyRows()
        return
    end
    if UserInputService:GetFocusedTextBox() then return end
    if input.KeyCode == Enum.KeyCode.RightShift then
        setMenu(not menuOpen)
        return
    end
    local key = input.KeyCode.Name
    if key == Settings.binds.Stunt then
        performStunt(findStunt(Settings.stunt), Settings.stuntAirtime, "Stunt")
    elseif key == Settings.binds["Car Hop"] then
        performStunt(HOP, Settings.hopAirtime, "Car Hop")
    end
    if key == Settings.binds.Drift and Settings.drift then
        setDrift(true)
    end
end)
bind(UserInputService.InputEnded, function(input)
    if input.UserInputType == Enum.UserInputType.Keyboard and input.KeyCode.Name == Settings.binds.Drift then
        setDrift(false)
    end
end)

local function unload()
    if not alive then return end
    alive = false
    local rgbCar = Settings.carRGB and ownCar()
    if rgbCar and originalPaints[rgbCar] then
        task.spawn(paintCar, rgbCar, originalPaints[rgbCar])
    end
    endSession()
    RunService:UnbindFromRenderStep("BrookhavenStudioView")
    if savedLighting then
        for key, value in pairs(savedLighting) do
            Lighting[key] = value
        end
        savedLighting = nil
    end
    if savedFov then
        workspace.CurrentCamera.FieldOfView = savedFov
    end
    if savedZoom then
        LocalPlayer.CameraMaxZoomDistance = savedZoom
    end
    for _, c in ipairs(connections) do
        c:Disconnect()
    end
    table.clear(connections)
    modal.Modal = false
    menuBlur:Destroy()
    Gui:Destroy()
    if ENV.BrookhavenStudio and ENV.BrookhavenStudio.Unload == unload then
        ENV.BrookhavenStudio = nil
    end
    print("[Brookhaven Studio] unloaded")
end
unloadRef = unload

bind(workspace.CurrentCamera:GetPropertyChangedSignal("ViewportSize"), function()
    guiScale = computeScale()
    for _, sc in ipairs(scaleObjects) do
        sc.Scale = guiScale
    end
    if menuOpen then
        boxScale.Scale = guiScale
    end
    task.defer(placeArrayList)
end)

ENV.BrookhavenStudio = {
    Unload = unload,
    Settings = Settings,
    Modules = ModuleByName,
    SetShown = setMenu,
    IsShown = function() return menuOpen end,
    SelectTab = selectTab,
    SetCarRGB = setCarRGB,
    SyncLighting = syncLightingSnapshot,
    ApplyCarSettings = function()
        applyPhysics()
        applyTires()
    end,
    Stunt = function(name)
        performStunt(findStunt(name), Settings.stuntAirtime, "Stunt")
    end,
    SetDrift = setDrift,
    Session = function() return Session end,
    StuntCount = function() return stuntCount end,
}

---------------------------------------------------------------------------------------------
-- Start
---------------------------------------------------------------------------------------------

syncLightingSnapshot()
syncFov()
syncZoom()
selectTab("Vehicle", true)
setMenu(startShown)
refreshVehicleStatus()

-- Gamepass checks hit Roblox's web API, so they run after the menu is up.
task.spawn(function()
    local available = {}
    for _, car in ipairs(CARS) do
        if not car.pass or owns(car.pass) then
            table.insert(available, { value = car.id, label = car.label })
        end
    end
    if not alive then return end
    local selected = Settings.car
    local found = false
    for _, option in ipairs(available) do
        if option.value == selected then found = true end
    end
    if not found and available[1] then
        selected = available[1].value
        Settings.car = selected
    end
    carPicker.setOptions(available, selected)
    local headline = owns("VIP") and "VIP" or owns("PREMIUM") and "Premium" or "Standard"
    passNote.Text = string.format("%s · %d of %d cars · top speed %d", headline, #available, #CARS, topSpeed())
    if LocalPlayer.Character then
        task.spawn(hookCharacter, LocalPlayer.Character)
    end
end)
bind(LocalPlayer.CharacterAdded, function(character)
    endSession()
    hookCharacter(character)
end)

if Settings.rpName:gsub("%s", "") ~= "" then
    NameTextEvent:FireServer("RolePlayName", Settings.rpName)
end
if Settings.rpBio:gsub("%s", "") ~= "" then
    NameTextEvent:FireServer("RolePlayBio", Settings.rpBio)
end

task.spawn(function()
    local options, err = fetchSaves()
    if not alive then return end
    if not options then
        notify("Saved props", "Couldn't read your house saves: " .. tostring(err), "bad")
        buildPicker.setOptions({})
        return
    end
    local selected = Settings.buildSlot
    if selected == "" and options[1] then
        selected = options[1].value
        Settings.buildSlot = selected
    end
    buildPicker.setOptions(options, selected)
end)

bind(HouseNumber.Changed, function(number)
    if number > 0 then
        notify("House", "House #" .. number .. " claimed" .. (Settings.houseRainbow and " · color cycle on" or ""), "good")
        if Settings.loadBuild and Settings.buildSlot ~= "" then
            task.delay(3, function()
                if alive and HouseNumber.Value == number then
                    loadBuild(Settings.buildSlot)
                end
            end)
        end
    end
end)

-- House color cycle. Each server update repaints hundreds of parts for everyone, so the server
-- only gets a step every 0.6s; your own screen is smoothed below.
task.spawn(function()
    while alive do
        if Settings.houseRainbow and HouseNumber.Value > 0 then
            local color = houseColorAt(os.clock() + 0.05)
            setHouseColor(color)
            table.insert(housePaint.recent, 1, color)
            housePaint.recent[4] = nil
            if os.clock() >= housePaint.refreshAt then
                housePaint.refreshAt = os.clock() + 8
                task.delay(0.8, function()
                    if alive then
                        housePaint.parts = collectHouseParts()
                    end
                end)
            end
            task.wait(0.6)
        else
            table.clear(housePaint.parts)
            housePaint.refreshAt = 0
            task.wait(0.3)
        end
    end
end)

do
    -- Repaint 20 times a second, and also immediately in any frame where a server update just
    -- landed (the first part no longer shows what we last wrote), so it never flickers.
    local nextFrame = 0
    local lastWritten
    bind(RunService.RenderStepped, function()
        local parts = housePaint.parts
        if not Settings.houseRainbow or HouseNumber.Value <= 0 or #parts == 0 then return end
        local now = os.clock()
        local overwritten = lastWritten ~= nil and parts[1].Color ~= lastWritten
        if now < nextFrame and not overwritten then return end
        nextFrame = now + 0.05
        local color = houseColorAt(now)
        for _, part in ipairs(parts) do
            part.Color = color
        end
        lastWritten = parts[1].Color
    end)
end

-- RGB streaming. The server gets ~10 small steps a second (tested: 28/28 accepted at this
-- rate), sent without waiting on each reply, so other players see a smooth fade too.
-- Three refusals in a row (e.g. a car that can't be painted) switch the feature off.
local RGB_SEND_INTERVAL = 0.1
local RGB_MAX_IN_FLIGHT = 2

-- getTarget returns what to recolor right now, or nil to pause.
local function rgbStream(getTarget, send, onFail)
    task.spawn(function()
        local inFlight, failures = 0, 0
        while alive do
            local target = getTarget()
            if target and inFlight < RGB_MAX_IN_FLIGHT then
                inFlight += 1
                task.spawn(function()
                    local ok, msg = send(target, os.clock() + RGB_SEND_INTERVAL)
                    inFlight -= 1
                    if ok or getTarget() ~= target then
                        failures = 0
                    else
                        failures += 1
                        if failures >= 3 then
                            failures = 0
                            onFail(msg)
                        end
                    end
                end)
            end
            task.wait(target and RGB_SEND_INTERVAL or 0.25)
        end
    end)
end

-- Car RGB keeps going whether you're driving the car or it's parked (the game accepts paint
-- for your car from anywhere). While you drive, it waits for auto-tune to finish first.
local function rgbCar()
    if not Settings.carRGB then return nil end
    local s = Session
    if s and not s.ready then return nil end
    return ownCar()
end

rgbStream(rgbCar, function(car, t)
    return paintCar(car, rgbColor(Settings.rgbMode, t, Settings.rgbCycle))
end, function(msg)
    setCarRGB(false)
    notify("Car RGB", "Stopped: " .. tostring(msg), "bad")
end)

-- The underglow request has no car id, so it only works for the car you're in.
rgbStream(function()
    local s = Session
    return Settings.rainbowGlow and s and s.ready and s or nil
end, function(_, t)
    local ok, msg = carRequest("SetUnderglowColor", 1, glowColor(t))
    -- Usually the glow is just switched off; turn the chosen style back on and keep going.
    if not ok and tostring(msg):lower():find("select an underglow") then
        carRequest("SetUnderglow", Settings.underglow)
    end
    return ok, msg
end, function(msg)
    Settings.rainbowGlow = false
    saveSoon()
    refreshModule("RGB Underglow")
    notify("RGB Underglow", "Stopped: " .. tostring(msg), "bad")
end)

-- Your own screen: recolor every frame so the fade is perfectly smooth between the server's
-- steps (replicated values land before RenderStepped, so these always win on screen).
bind(RunService.RenderStepped, function()
    local t = os.clock()
    local car = rgbCar()
    if car then
        local color = rgbColor(Settings.rgbMode, t, Settings.rgbCycle)
        for _, part in ipairs(paintParts(car)) do
            part.Color = color
        end
    end
    local s = Session
    if not s or not s.ready then return end
    if Settings.rainbowGlow then
        local color = glowColor(t)
        for _, obj in ipairs(glowObjects(s.car)) do
            if obj:IsA("Beam") then
                obj.Color = ColorSequence.new(color)
            else
                obj.Color = color
            end
        end
    end
end)

-- Rainbow RP name. Brookhaven waits 1s between name color changes, so match that.
task.spawn(function()
    local hue = 0
    while alive do
        if Settings.rainbowName then
            hue = (hue + 0.08) % 1
            NameColorEvent:FireServer("PickingRPNameColor", Color3.fromHSV(hue, 0.85, 1))
            task.wait(1.1)
        else
            task.wait(0.3)
        end
    end
end)

-- Auto Flip: if the car stays on its roof or side for a moment, ask the game to set it upright.
task.spawn(function()
    local tippedSince
    local lastFlip = 0
    while alive do
        local s = Session
        if Settings.autoFlip and s and not s.stunting and s.geom then
            local up = carFrame(s).UpVector
            if up.Y < 0.35 then
                tippedSince = tippedSince or os.clock()
                if os.clock() - tippedSince > 1.2 and os.clock() - lastFlip > 5 then
                    lastFlip = os.clock()
                    tippedSince = nil
                    local ok = carRequest("RepositionVehicle")
                    notify("Auto Flip", ok and "Back on your wheels" or "Couldn't flip the car", ok and "good" or "bad")
                end
            else
                tippedSince = nil
            end
        else
            tippedSince = nil
        end
        task.wait(0.2)
    end
end)

-- Air Stunts and Landing Assist watch every physics step.
bind(RunService.Heartbeat, airMonitor)

-- Auto Headlights. The game only reports the light state when it's toggled, so this switches
-- at dusk and dawn (and when you get in) and checks the answer; pressing V yourself still works
-- in between.
task.spawn(function()
    while alive do
        local s = Session
        if Settings.autoLights and s and s.ready then
            local clock = savedLighting and savedLighting.ClockTime or Lighting.ClockTime
            local night = clock < 6.3 or clock >= 17.7
            if s.lightsFor ~= night then
                s.lightsFor = night
                if s.lightsOn == nil and not night then
                    s.lightsOn = false -- a freshly spawned car has its lights off
                end
                for _ = 1, 2 do
                    if s.lightsOn == night or Session ~= s then break end
                    local called, state = invoke("ToggleHeadLights")
                    if not called or type(state) ~= "boolean" then
                        notify("Auto Headlights", "Couldn't switch the lights: " .. tostring(state), "bad")
                        break
                    end
                    s.lightsOn = state
                end
            end
        end
        task.wait(1)
    end
end)

-- Periodic refresh for things that change on their own.
task.spawn(function()
    local tick = 0
    while alive do
        tick += 1
        if tick % 10 == 0 then
            refreshVehicleStatus()
            for _, name in ipairs({ "Fire", "Engine Smoke" }) do
                refreshModule(name)
            end
        end
        -- Brookhaven's HUD changes with what you're doing, so re-measure it every few seconds.
        if tick % 50 == 1 then
            placeArrayList()
        end
        task.wait(0.1)
    end
end)

notify("Brookhaven Studio", "v6 loaded · RightShift opens the menu", "good")

Features & Information

Car Stunts, Car Speed, RGB House, RGB Car

Community Comments (1)

Gamer_291214Gamer_291214Oct 7, 2026

Is okay (no key)

More Scripts for Universal Scripts