```lua
--[[
    DrivingAssistant.lua
    Скрипт ассистента вождения для SA-MP (San Andreas Multiplayer) с использованием MoonLoader API.
    Этот скрипт помогает игроку управлять байком, обнаруживая и обходя препятствия.
    
    Автор: [Tectrex]
    Дата: [01.10.2024]
]]--

-- ============================================
--                    Конфигурация
-- ============================================

-- Таблица виртуальных клавиш с соответствующими кодами
local VK = {
    ["S"]  = 0x53, -- Клавиша 'S'
    ["A"]  = 0x41, -- Клавиша 'A'
    ["D"]  = 0x44, -- Клавиша 'D'
    ["F5"] = 0x74  -- Клавиша 'F5'
}

-- Создание шрифта для отображения информации на экране
local font = renderCreateFont("Arial", 8, 8)

-- ============================================
--              Класс DrivingAssistant
-- ============================================

-- Создание таблицы для класса DrivingAssistant
local DrivingAssistant = {}
DrivingAssistant.__index = DrivingAssistant

--[[
    Конструктор класса DrivingAssistant
    Инициализирует новое состояние ассистента вождения.
    
    @return новый объект DrivingAssistant
]]
function DrivingAssistant:new()
    local driver = setmetatable({}, self)
    driver.drivingEnabled = false        -- Статус ассистента вождения (включен/выключен)
    driver.lastTurnDirection = nil       -- Направление последнего поворота ('left' или 'right')
    driver.userInputMultiplier = 5       -- Множитель для пользовательского ввода
    return driver
end

--[[
    Переключает состояние ассистента вождения.
    Включает или выключает ассистент и выводит соответствующее сообщение в чат.
]]
function DrivingAssistant:toggleDriving()
    self.drivingEnabled = not self.drivingEnabled
    local message = self.drivingEnabled and "Ассистент двух колес включен." or "Ассистент двух колес выключен."
    sampAddChatMessage(message, -1)
end

--[[
    Обрабатывает процесс вождения, проверяя наличие автомобиля и обнаруживая препятствия.
]]
function DrivingAssistant:processDriving()
    -- Проверка, находится ли персонаж в автомобиле
    if not isCharInAnyCar(PLAYER_PED) then
        sampAddChatMessage("Вы должны сесть на байк.", -1)
        return
    end

    -- Получение координат и направления игрока
    local playerData = self:getPlayerCoordsAndHeading()
    local playerCoords, playerHeading = playerData.coords, playerData.heading
    local radOffset = math.rad(-playerHeading)

    -- Вычисление смещения координат
    local offsetX, offsetY = self:getOffsetCoords(playerCoords, radOffset)

    -- Обнаружение препятствий
    local obstacleInfo, allLinesOccupied = self:detectObstacles(playerCoords, playerHeading, radOffset, offsetX, offsetY)

    -- Обработка обнаруженных препятствий
    self:handleObstacles(obstacleInfo, allLinesOccupied)
end

--[[
    Обнаруживает препятствия вокруг игрока.
    
    @param playerCoords таблица с координатами игрока
    @param playerHeading направление игрока в градусах
    @param radOffset смещение направления в радианах
    @param offsetX смещение по оси X
    @param offsetY смещение по оси Y
    @return obstacleInfo таблица с информацией о препятствиях
    @return allLinesOccupied булево значение, показывающее, заняты ли все линии
]]
function DrivingAssistant:detectObstacles(playerCoords, playerHeading, radOffset, offsetX, offsetY)
    local obstacleInfo = {
        detected = { ["left"] = false, ["right"] = false, ["front"] = false, ["center"] = false },
        specific = {}
    }

    local pitch = math.rad(getCarPitch(getCarCharIsUsing(PLAYER_PED))) -- Угол наклона автомобиля
    local allLinesOccupied = true

    -- Цикл для проверки различных направлений
    for i = -4, 4 do
        local radian = math.rad(-playerHeading + 2 * i)
        local sinR, cosR = math.sin(radian), math.cos(radian)
        local multiplier = self:getMultiplier(i)
        local targetX, targetY, targetZ = self:getTargetCoords(offsetX, offsetY, pitch, multiplier, sinR, cosR)
        local obstacle = self:isObstacleInFront(offsetX, offsetY, targetX, targetY, targetZ)

        if i ~= 0 then
            allLinesOccupied = allLinesOccupied and obstacle
        end

        local color = obstacle and 0xFFFF0000 or 0xFFFFFFFF

        -- Рендеринг линии на экране
        self:renderLine3D(offsetX, offsetY, 0.0, targetX, targetY, targetZ, 2, color)
        self:processObstacleDetection(obstacleInfo, obstacle, i)

        -- Отображение номера проверки на экране
        local screenX, screenY = self:getScreenCoords(targetX, targetY, targetZ)
        if screenX and screenY then
            renderFontDrawText(font, tostring(i), screenX, screenY, color)
        end
    end

    return obstacleInfo, allLinesOccupied
end

--[[
    Вычисляет множитель скорости на основе текущей скорости и индекса направления.
    
    @param index индекс направления проверки
    @return множитель скорости
]]
function DrivingAssistant:getMultiplier(index)
    local speedVector = { getCarSpeedVector(getCarCharIsUsing(PLAYER_PED), true) }
    local speed = math.sqrt(speedVector[1]^2 + speedVector[2]^2 + speedVector[3]^2)
    local speedFactor = 1 + speed / 10
    if index == 0 then
        return 0.5 * speedFactor
    else
        return (self.userInputMultiplier - 0.2 * math.abs(index)) * speedFactor
    end
end

--[[
    Вычисляет целевые координаты для проверки препятствий.
    
    @param offsetX смещение по оси X
    @param offsetY смещение по оси Y
    @param radPitch угол наклона в радианах
    @param multiplier множитель для расширения области проверки
    @param sinR синус угла направления
    @param cosR косинус угла направления
    @return целевые координаты X, Y, Z
]]
function DrivingAssistant:getTargetCoords(offsetX, offsetY, radPitch, multiplier, sinR, cosR)
    return offsetX + multiplier * sinR, offsetY + multiplier * cosR, 0.0 + multiplier * math.sin(radPitch)
end

--[[
    Обрабатывает информацию об обнаруженных препятствиях.
    
    @param obstacleInfo таблица с информацией о препятствиях
    @param obstacle булево значение, указывающее на наличие препятствия
    @param index индекс направления проверки
]]
function DrivingAssistant:processObstacleDetection(obstacleInfo, obstacle, index)
    if obstacle then
        addOneOffSound(0.0, 0.0, 0.0, 1139) -- Воспроизведение звука
        printStringNow("DETECTED", 100)       -- Вывод сообщения на экран
        obstacleInfo.detected.center = obstacleInfo.detected.center or (index == 0)
        obstacleInfo.detected.right = obstacleInfo.detected.right or (index > 0)
        obstacleInfo.detected.left = obstacleInfo.detected.left or (index < 0)
        obstacleInfo.detected.front = obstacleInfo.detected.front or (index ~= 0)
        obstacleInfo.specific[index] = true
    end
end

--[[
    Обрабатывает обнаруженные препятствия и симулирует нажатия клавиш для обхода.
    
    @param obstacleInfo таблица с информацией о препятствиях
    @param allLinesOccupied булево значение, показывающее, заняты ли все линии
]]
function DrivingAssistant:handleObstacles(obstacleInfo, allLinesOccupied)
    -- Остановка при обнаружении препятствия прямо перед автомобилем
    if obstacleInfo.detected.center then
        self:simulateKeyPress(VK["S"], 1000)
    end

    -- Обход препятствий, когда все линии заняты
    if allLinesOccupied then
        if self.lastTurnDirection == "right" then
            self:simulateKeyPress(VK["D"], 250)
        elseif self.lastTurnDirection == "left" then
            self:simulateKeyPress(VK["A"], 250)
        else
            print("Неизвестно")
        end
    elseif obstacleInfo.detected.front then
        local leftCount, rightCount = 0, 0
        for i, detected in pairs(obstacleInfo.specific) do
            if i < 0 then
                leftCount = leftCount + 1
            elseif i > 0 then
                rightCount = rightCount + 1
            end
        end

        -- Определение направления поворота на основе количества препятствий слева и справа
        if leftCount > rightCount then
            self.lastTurnDirection = "right"
            self:simulateKeyPress(VK["D"], 250)
        elseif rightCount > leftCount then
            self.lastTurnDirection = "left"
            self:simulateKeyPress(VK["A"], 250)
        end
    end
end

--[[
    Симулирует нажатие клавиши на указанное время.
    
    @param key код виртуальной клавиши
    @param duration длительность нажатия в миллисекундах
]]
function DrivingAssistant:simulateKeyPress(key, duration)
    lua_thread.create(function()
        setVirtualKeyDown(key, true)
        wait(duration)
        setVirtualKeyDown(key, false)
    end)
end

--[[
    Получает координаты и направление персонажа.
    
    @return таблица с координатами (coords) и направлением (heading)
]]
function DrivingAssistant:getPlayerCoordsAndHeading()
    return {
        coords = getCharCoordinates(PLAYER_PED),
        heading = getCharHeading(PLAYER_PED)
    }
end

--[[
    Вычисляет смещение координат игрока на основе текущего направления.
    
    @param coords таблица с координатами игрока
    @param radOffset смещение направления в радианах
    @return новые координаты X и Y
]]
function DrivingAssistant:getOffsetCoords(coords, radOffset)
    if isCharInAnyCar(PLAYER_PED) then
        return coords[1] + 0.6 * math.sin(radOffset), coords[2] + 0.6 * math.cos(radOffset)
    else
        return coords[1], coords[2]
    end
end

--[[
    Проверяет наличие препятствия впереди.
    
    @param x текущая координата X
    @param y текущая координата Y
    @param targetX целевая координата X
    @param targetY целевая координата Y
    @param targetZ целевая координата Z
    @return булево значение, указывающее на наличие препятствия
]]
function DrivingAssistant:isObstacleInFront(x, y, targetX, targetY, targetZ)
    return processLineOfSight(
        x, y, 0.0,
        targetX, targetY, targetZ,
        true, true, true, true, true, false, false, false
    )
end

--[[
    Преобразует 3D координаты в экранные координаты.
    
    @param x координата X в 3D пространстве
    @param y координата Y в 3D пространстве
    @param z координата Z в 3D пространстве
    @return экранные координаты X и Y или nil, если преобразование не удалось
]]
function DrivingAssistant:getScreenCoords(x, y, z)
    local success, screenX, screenY = convert3DCoordsToScreenEx(x, y, z)
    if success then
        return screenX, screenY
    end
end

--[[
    Рендерит линию в 3D пространстве на экране.
    
    @param x1 координата X первой точки
    @param y1 координата Y первой точки
    @param z1 координата Z первой точки
    @param x2 координата X второй точки
    @param y2 координата Y второй точки
    @param z2 координата Z второй точки
    @param width толщина линии
    @param color цвет линии
]]
function DrivingAssistant:renderLine3D(x1, y1, z1, x2, y2, z2, width, color)
    local success1, screenX1, screenY1 = convert3DCoordsToScreenEx(x1, y1, z1)
    local success2, screenX2, screenY2 = convert3DCoordsToScreenEx(x2, y2, z2)

    if success1 and success2 and isPointOnScreen(x2, y2, z2, 1.0) then
        renderDrawLine(screenX1, screenY1, screenX2, screenY2, width, color)
    end
end

-- ============================================
--                   Main
-- ============================================

--[[
    Основная функция скрипта.
    Инициализирует ассистента вождения, регистрирует команды и запускает цикл обработки.
]]
function main()
    local assistant = DrivingAssistant:new()
    local F5L = false -- Предыдущее состояние клавиши F5

    -- Ожидание доступности SAMP
    while not isSampAvailable() do
        wait(0)
    end

    -- Регистрация команды для включения/выключения ассистента вождения
    sampRegisterChatCommand("helpmedrive", function()
        assistant:toggleDriving()
    end)

    -- Регистрация команды для установки множителя дистанции
    sampRegisterChatCommand("setmultiplier", function(param)
        local value = tonumber(param)
        if value then
            assistant.userInputMultiplier = value
            sampAddChatMessage("Множитель дистанции установлен на: " .. value, -1)
        else
            sampAddChatMessage("Ошибка: Неверное значение множителя.", -1)
        end
    end)

    -- Основной цикл обработки
    while true do
        wait(0)
        local F5T = isKeyDown(VK["F5"])

        -- Переключение состояния при нажатии F5
        if F5T and not F5L then
            assistant:toggleDriving()
        end
        F5L = F5T

        -- Выполнение логики ассистента, если он включен
        if assistant.drivingEnabled then
            local status, err = pcall(function()
                assistant:processDriving()
            end)
            if not status then
                print("ERROR: " .. err)
            end
        end
    end
end
```
Можно ли улучшить логику, алгоритмы и т.д?