```lua
-- parkour_system.lua

if SERVER then
    print("[Parkour_Server] Parkour system loaded on server.")

    util.AddNetworkString("Parkour_LedgeGrab_Request")
    util.AddNetworkString("Parkour_LedgeGrab_Status")
    util.AddNetworkString("Parkour_LedgeGrab_PullUp")
    util.AddNetworkString("Parkour_LedgeGrab_Release")

    -- Таблица для отслеживания игроков, которые сейчас захватывают уступы
    local grabbingPlayers = {}

    -- Функция для поиска уступа перед игроком
    local function FindLedge(ply)
        print("[Parkour_Server] Attempting to find ledge for player:", ply:Nick())

        local startPos = ply:GetShootPos()
        local forward = ply:GetAimVector()
        local traceDistance = 200 -- Увеличено для большей дальности
        local traceHeight = 100 -- Увеличено для большей высоты

        -- Трассировка вперед
        local trace = util.TraceLine({
            start = startPos,
            endpos = startPos + forward * traceDistance,
            filter = ply,
            mask = MASK_SOLID
        })

        print("[Parkour_Server] Trace forward hit:", trace.Hit, " at ", trace.HitPos)

        if trace.Hit then
            -- Трассировка вверх
            local aboveTrace = util.TraceLine({
                start = trace.HitPos,
                endpos = trace.HitPos + Vector(0, 0, traceHeight),
                filter = ply,
                mask = MASK_SOLID
            })

            print("[Parkour_Server] Trace above hit:", aboveTrace.Hit)

            if not aboveTrace.Hit then
                print("[Parkour_Server] Ledge found at:", trace.HitPos)
                return trace.HitPos
            else
                print("[Parkour_Server] Ledge not valid (space above is blocked).")
            end
        else
            print("[Parkour_Server] No ledge hit detected.")
        end

        return nil
    end

    -- Хук Move для предотвращения горизонтального движения при захвате
    hook.Add("Move", "Parkour_PreventMovement", function(ply, mv)
        if grabbingPlayers[ply] then
            -- Запретить горизонтальное движение
            mv:SetForwardSpeed(0)
            mv:SetSideSpeed(0)
        end
    end)

    -- Обработка запроса захвата уступа от клиента
    net.Receive("Parkour_LedgeGrab_Request", function(len, ply)
        print("[Parkour_Server] Received Parkour_LedgeGrab_Request from:", ply:Nick())

        if not ply:Alive() then
            print("[Parkour_Server] Player is not alive. Ignoring request.")
            return
        end

        -- Предотвращение множественных захватов
        if grabbingPlayers[ply] then
            print("[Parkour_Server] Player already grabbing.")
            return
        end

        local ledgePos = FindLedge(ply)
        if ledgePos then
            -- Отправляем клиенту подтверждение захвата
            net.Start("Parkour_LedgeGrab_Status")
                net.WriteBool(true)
                net.WriteVector(ledgePos)
            net.Send(ply)
            -- Добавляем игрока в таблицу захватывающих
            grabbingPlayers[ply] = {
                ledgePos = ledgePos,
                originalPos = ply:GetPos(),
                pulling = false
            }
            print("[Parkour_Server] Sent Parkour_LedgeGrab_Status (success) to:", ply:Nick())
        else
            -- Отправляем клиенту отказ
            net.Start("Parkour_LedgeGrab_Status")
                net.WriteBool(false)
            net.Send(ply)
            print("[Parkour_Server] Sent Parkour_LedgeGrab_Status (failure) to:", ply:Nick())
        end
    end)

    -- Обработка запроса на подтягивание вверх
    net.Receive("Parkour_LedgeGrab_PullUp", function(len, ply)
        print("[Parkour_Server] Received Parkour_LedgeGrab_PullUp from:", ply:Nick())

        if grabbingPlayers[ply] then
            local ledgePos = grabbingPlayers[ply].ledgePos
            local currentPos = ply:GetPos()
            local movementDistance = 30 -- Расстояние подъема за раз

            -- Вычисление новой позиции
            local newPos = currentPos + Vector(0, 0, movementDistance)

            -- Трассировка вверх для проверки наличия пространства
            local trace = util.TraceLine({
                start = currentPos,
                endpos = newPos,
                filter = ply,
                mask = MASK_SOLID
            })

            if not trace.Hit then
                -- Перемещение игрока
                ply:SetPos(newPos)
                print("[Parkour_Server] Pulled up player to:", newPos)
            else
                print("[Parkour_Server] Cannot pull up, space is blocked at:", newPos)
            end
        else
            print("[Parkour_Server] Player is not grabbing, pull up request ignored.")
        end
    end)

    -- Обработка запроса на освобождение захвата
    net.Receive("Parkour_LedgeGrab_Release", function(len, ply)
        print("[Parkour_Server] Received Parkour_LedgeGrab_Release from:", ply:Nick())

        if grabbingPlayers[ply] then
            -- Удаляем игрока из таблицы захватывающих
            grabbingPlayers[ply] = nil
            print("[Parkour_Server] Player released grab:", ply:Nick())
        else
            print("[Parkour_Server] Player was not grabbing. Release ignored.")
        end
    end)
else
    -- CLIENT

    print("[Parkour_Client] Parkour system loaded on client.")

    -- Переменные состояния захвата уступа
    local isGrabbing = false
    local ledgePosition = Vector(0, 0, 0)
    local grabDuration = 5.0 -- Продолжительность захвата в секундах (можно настроить)
    local grabStartTime = 0
    local movementCooldown = 0.5 -- Кулдаун между подтягиваниями (в секундах)
    local lastPullTime = 0

    -- Функция для инициации захвата уступа
    local function RequestLedgeGrab()
        if isGrabbing then
            print("[Parkour_Client] Already grabbing a ledge. Request ignored.")
            return
        end

        print("[Parkour_Client] Requesting ledge grab.")
        net.Start("Parkour_LedgeGrab_Request")
        net.SendToServer()
    end

    -- Обработка ответа от сервера
    net.Receive("Parkour_LedgeGrab_Status", function()
        local success = net.ReadBool()
        if success then
            ledgePosition = net.ReadVector()
            isGrabbing = true
            grabStartTime = CurTime()

            print("[Parkour_Client] Successfully grabbed a ledge at position:", ledgePosition)

            -- Выводим HUD уведомление
            chat.AddText(Color(0, 255, 0), "[Parkour] Захват уступа успешно!")
        else
            -- Захват не удался
            chat.AddText(Color(255, 0, 0), "[Parkour] Не удалось захватить уступ.")
            print("[Parkour_Client] Ledge grab failed.")
        end
    end)

    -- Функция для отпуска захвата
    local function ReleaseLedgeGrab()
        if not isGrabbing then return end

        print("[Parkour_Client] Releasing ledge grab.")
        net.Start("Parkour_LedgeGrab_Release")
        net.SendToServer()

        isGrabbing = false
        ledgePosition = Vector(0, 0, 0)
        grabStartTime = 0

        -- Очищаем HUD или другие клиентские элементы, если необходимо
    end

    -- Функция для отправки запроса на подтягивание вверх
    local function PullUp()
        if not isGrabbing then return end

        if CurTime() - lastPullTime < movementCooldown then
            return
        end

        lastPullTime = CurTime()

        print("[Parkour_Client] Sending pull up request.")
        net.Start("Parkour_LedgeGrab_PullUp")
        net.SendToServer()
    end

    -- Обработка ввода игрока
    hook.Add("Think", "Parkour_InputHandler", function()
        if isGrabbing then
            -- Проверяем, удерживается ли клавиша захвата (E)
            if not LocalPlayer():KeyDown(IN_USE) then
                print("[Parkour_Client] Use key released. Requesting release of ledge grab.")
                -- Освобождаем захват, если отпущена клавиша захвата
                ReleaseLedgeGrab()
            end

            -- Управление вертикальным движением
            if LocalPlayer():KeyDown(IN_FORWARD) then
                -- Подтягивание вверх, используя клавишу W
                PullUp()
            end

            -- Освобождение захвата только при отпускании E
        end
    end)

    -- Рендеринг индикатора захвата уступа (опционально)
    hook.Add("HUDPaint", "Parkour_HUD", function()
        if isGrabbing then
            -- Отображение текста на экране
            draw.SimpleText("Захват уступа. Нажмите и удерживайте E для управления. Нажмите W для подтягивания вверх.", "DermaDefault", ScrW() / 2, ScrH() / 2, Color(255, 255, 255), TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
        end
    end)

    -- Обработка ввода клавиш через KeyPress
    hook.Add("KeyPress", "Parkour_KeyPress", function(ply, key)
        if ply ~= LocalPlayer() then return end

        if key == IN_USE then -- Клавиша E по умолчанию соответствует IN_USE
            print("[Parkour_Client] Use key (E) pressed.")
            RequestLedgeGrab()
        end
    end)
end
```
[{
	"resource": "/C:/Program Files (x86)/Steam/steamapps/common/GarrysMod/garrysmod/lua/autorun/parkour_system.lua",
	"owner": "glua",
	"severity": 4,
	"message": "Unused variable: ledgePos",
	"startLineNumber": 109,
	"startColumn": 19,
	"endLineNumber": 109,
	"endColumn": 27
},{
	"resource": "/C:/Program Files (x86)/Steam/steamapps/common/GarrysMod/garrysmod/lua/autorun/parkour_system.lua",
	"owner": "glua",
	"severity": 4,
	"message": "Unused variable: grabDuration",
	"startLineNumber": 156,
	"startColumn": 11,
	"endLineNumber": 156,
	"endColumn": 23
},{
	"resource": "/C:/Program Files (x86)/Steam/steamapps/common/GarrysMod/garrysmod/lua/autorun/parkour_system.lua",
	"owner": "glua",
	"severity": 4,
	"message": "Unused variable: grabStartTime",
	"startLineNumber": 157,
	"startColumn": 11,
	"endLineNumber": 157,
	"endColumn": 24
}]
Оптимизируй код, структурируй и произведи рефакторинг