Почему описание основных кнопок отображается как table: xx0001, а описание подкнопок нормально?
```lua
if CLIENT then

    -- =========================
    -- DescriptionPanel CLASS
    -- =========================
    local DescriptionPanel = {}
    DescriptionPanel.__index = DescriptionPanel

    function DescriptionPanel:New()
        local self = setmetatable({}, DescriptionPanel)

        self.panel = vgui.Create("DPanel")
        self.panel:SetSize(ScrW(), 40)
        self.panel:SetPos(0, 0)
        self.panel:SetZPos(9999)
        self.panel.Paint = function(_, w, h)
            surface.SetDrawColor(30, 30, 30, 220)
            surface.DrawRect(0, 0, w, h)
        end

        self.label = vgui.Create("DLabel", self.panel)
        self.label:SetFont("DermaLarge")
        self.label:SetTextColor(Color(255, 255, 255))
        self.label:SetPos(20, 6)
        self.label:SetSize(ScrW() - 40, 32)

        self.panel:SetVisible(false)

        return self
    end

    function DescriptionPanel:Show(text)
        if IsValid(self.panel) then
            self.label:SetText(tostring(text or ""))
            self.panel:SetVisible(true)
        end
    end

    function DescriptionPanel:Hide()
        if IsValid(self.panel) then
            self.panel:SetVisible(false)
        end
    end

    function DescriptionPanel:IsValid()
        return IsValid(self.panel)
    end

    function DescriptionPanel:Remove()
        if IsValid(self.panel) then
            self.panel:Remove()
            self.panel = nil
            self.label = nil
        end
    end


    -- =========================
    -- CircleButton CLASS
    -- =========================
    local CircleButton = {}
    CircleButton.__index = CircleButton

    -- Static utility method to draw a circular button with hover effects
    function CircleButton.PaintButton(s, w, h, hovered, text, alphaProgress)
        alphaProgress = alphaProgress or 1
        local baseAlpha = hovered and 180 or 120
        local borderAlpha = hovered and 220 or 150
        local textAlpha = hovered and 255 or 200

        baseAlpha = baseAlpha * alphaProgress
        borderAlpha = borderAlpha * alphaProgress
        textAlpha = textAlpha * alphaProgress

        surface.SetDrawColor(80, 80, 80, baseAlpha)
        draw.NoTexture()
        surface.DrawCircle(w / 2, h / 2, (w / 2) - 2, 80, 80, 80, baseAlpha)

        surface.SetDrawColor(180, 180, 180, borderAlpha)
        surface.DrawCircle(w / 2, h / 2, (w / 2) - 4, 180, 180, 180, borderAlpha)

        if hovered then
            surface.SetDrawColor(255, 255, 255, 30 * alphaProgress)
            surface.DrawCircle(w / 2, h / 2, (w / 2) - 2, 255, 255, 255, 30 * alphaProgress)
        end

        draw.SimpleText(text or "", "DermaLarge", w / 2, h / 2, Color(220, 220, 220, textAlpha), TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
    end

    -- Создает новую кнопку с параметрами
    function CircleButton:New(parent, idx, pos, size, data)
        local self = setmetatable({}, CircleButton)

        self.idx = idx
        self.data = data or {}
        self.position = pos or { x = 0, y = 0 }
        self.size = size or 64
        self.animProgress = 1 -- 0 .. 1 анимация появления
        self.alpha = 255

        self.btn = vgui.Create("DButton", parent)
        self.btn:SetSize(self.size, self.size)
        self.btn:SetPos(self.position.x, self.position.y)
        self.btn:SetText("")
        self.btn:SetFont("DermaLarge")
        self.btn:SetZPos(idx or 1)

        -- Связываем события кнопки с методом объекта
        self.btn.Paint = function(s, w, h)
            local hovered = s:IsHovered()
            CircleButton.PaintButton(s, w, h, hovered, self.data.text or tostring(self.idx), self.animProgress)
        end

        self.btn.DoClick = function()
            if self.data.action then
                self.data.action()
            end
            if self.OnClick then
                self:OnClick()
            end
        end

        self.btn.OnCursorEntered = function()
            if self.OnHover then
                self:OnHover()
            end
            if self.data.desc and self.OnShowDescription then
                self:OnShowDescription(tostring(self.data.desc))
            end
        end

        self.btn.OnCursorExited = function()
            if self.OnHideDescription then
                self:OnHideDescription()
            end
        end

        return self
    end

    function CircleButton:SetPos(x, y)
        self.position.x = x
        self.position.y = y
        if IsValid(self.btn) then
            self.btn:SetPos(x, y)
        end
    end

    function CircleButton:GetPos()
        if IsValid(self.btn) then
            return self.btn:GetPos()
        end
        return self.position.x, self.position.y
    end

    function CircleButton:GetSize()
        if IsValid(self.btn) then
            return self.btn:GetSize()
        end
        return self.size, self.size
    end

    function CircleButton:IsHovered()
        return IsValid(self.btn) and self.btn:IsHovered()
    end

    function CircleButton:SetAlpha(a)
        self.alpha = a
        if IsValid(self.btn) then
            self.btn:SetAlpha(a)
        end
    end

    function CircleButton:Remove()
        if IsValid(self.btn) then
            self.btn:Remove()
            self.btn = nil
        end
    end

    function CircleButton:IsValid()
        return IsValid(self.btn)
    end


    -- =========================
    -- CircularMenu CLASS
    -- =========================
    local CircularMenu = {}
    CircularMenu.__index = CircularMenu

    -- Static utility function: проверка попадания точки в круг
    function CircularMenu.PointInCircle(px, py, cx, cy, radius)
        local dx, dy = px - cx, py - cy
        return (dx * dx + dy * dy) <= (radius * radius)
    end

    function CircularMenu:New(buttonData, subButtonData)
        local self = setmetatable({}, CircularMenu)

        self.buttonData = buttonData or {}
        self.subButtonData = subButtonData or {}

        self.panel = nil
        self.descriptionPanel = DescriptionPanel:New()

        self.buttons = {}
        self.subButtons = {}

        self.animDuration = 0.5
        self.animDelay = 0.08
        self.radius = 150

        self.animations = {} -- для анимаций основных кнопок

        self:Create()

        return self
    end

    function CircularMenu:Create()
        if IsValid(self.panel) then
            self.panel:Remove()
            self.panel = nil
        end

        local screenW, screenH = ScrW(), ScrH()
        self.panel = vgui.Create("DPanel")
        self.panel:SetSize(screenW, screenH)
        self.panel:SetPos(0, 0)
        self.panel:SetPaintBackground(false)
        self.panel.m_fCreateTime = SysTime()

        self.panel.Paint = function(s, w, h)
            Derma_DrawBackgroundBlur(s, s.m_fCreateTime or SysTime())
        end

        self.panel:SetMouseInputEnabled(true)
        self.panel:SetKeyboardInputEnabled(true)
        gui.EnableScreenClicker(true)

        local center = { x = screenW / 2, y = screenH / 2 }
        local buttonsCount = #self.buttonData
        local angleStep = 2 * math.pi / buttonsCount
        local startY = screenH + 100

        self.animations = {}
        self.buttons = {}

        for i = 1, buttonsCount do
            local angle = (i - 1) * angleStep - math.pi / 2
            local btnX = center.x + math.cos(angle) * self.radius - 32 -- центрируем по размеру кнопки 64x64
            local btnY = center.y + math.sin(angle) * self.radius - 32

            -- Создаем кнопку
            local btnData = self.buttonData[i]
            btnData.text = btnData.text or tostring(i)

            local btn = CircleButton:New(self.panel, i, { x = btnX, y = startY }, 64, btnData)

            -- Подписываемся на события кнопки
            btn.OnClick = function()
                self:Remove()
                if btnData.action then
                    btnData.action()
                end
            end

            btn.OnHover = function()
                local animData = self.animations[i]
                if animData and animData.progress < 1 then
                    return -- не показываем пока не завершится анимация
                end

                if btnData.desc then
                    self.descriptionPanel:Show(btnData.desc)
                else
                    self.descriptionPanel:Hide()
                end

                self:ShowSubButtons(btn)
            end

            btn.OnShowDescription = function(desc)
                self.descriptionPanel:Show(desc)
            end

            btn.OnHideDescription = function()
                self.descriptionPanel:Hide()
                -- Удаляем подкнопки после небольшого задержки
                timer.Simple(0.01, function()
                    self:RemoveSubButtons(i)
                end)
            end

            btn:SetAlpha(0) -- постановка в невидимость перед анимацией

            self.buttons[i] = btn

            self.animations[i] = {
                start = CurTime() + (i - 1) * self.animDelay,
                progress = 0,
                btn = btn,
                fromY = startY,
                toY = btnY,
                toX = btnX,
            }
        end

        -- Think для анимации кнопок
        self.panel.Think = function(pnl)
            local allDone = true
            for i, anim in ipairs(self.animations) do
                local a = anim
                local btn = a.btn
                if not btn or not btn:IsValid() then continue end

                local t = CurTime() - a.start

                if t < 0 then
                    btn:SetPos(a.toX, a.fromY)
                    btn.animProgress = 0
                    btn:SetAlpha(0)
                    allDone = false
                elseif t < self.animDuration then
                    local p = math.min(1, t / self.animDuration)
                    local easedP = 1 - (1 - p) ^ 2 -- easing

                    local y = Lerp(easedP, a.fromY, a.toY)
                    btn:SetPos(a.toX, y)

                    btn.animProgress = easedP
                    btn:SetAlpha(255 * easedP)
                    a.progress = easedP

                    allDone = false
                else
                    btn:SetPos(a.toX, a.toY)
                    btn.animProgress = 1
                    btn:SetAlpha(255)
                    a.progress = 1
                end
            end

            if allDone then
                pnl.Think = nil
            end
        end

        self.panel.OnRemove = function()
            gui.EnableScreenClicker(false)
            self.descriptionPanel:Hide()
            self:RemoveAllSubButtons()
        end

        self.panel.OnMousePressed = function(s, code)
            -- Закрываем меню по клику вне кнопок
            if code == MOUSE_LEFT and not s:IsHovered() then
                self:Remove()
            end
        end
    end

    -- Создает подкнопки для главной кнопки mainButton
    function CircularMenu:ShowSubButtons(mainButton)
        if not mainButton or not mainButton:IsValid() then return end

        local idx = mainButton.idx

        -- Если уже есть валидные подкнопки для этого idx, не пересоздаём
        if self.subButtons[idx] then
            local allValid = true
            for _, btn in ipairs(self.subButtons[idx]) do
                if not btn:IsValid() then
                    allValid = false
                    break
                end
            end
            if allValid then return end
        end

        self:RemoveSubButtons(idx)

        local dataList = self.subButtonData[idx]
        if not dataList or #dataList == 0 then return end

        self.subButtons[idx] = {}

        local subBtnCount = #dataList
        local subBtnRadius = 48
        local subBtnSize = 36

        local x, y = mainButton:GetPos()
        local w, h = mainButton:GetSize()
        local cx, cy = x + w / 2, y + h / 2

        for j = 1, subBtnCount do
            local angle = (j - 1) * (2 * math.pi / subBtnCount) - math.pi / 2
            local targetX = cx + math.cos(angle) * subBtnRadius - subBtnSize / 2
            local targetY = cy + math.sin(angle) * subBtnRadius - subBtnSize / 2

            local data = dataList[j]
            data.text = data.text or tostring(j)

            local btn = CircleButton:New(self.panel, 100 + j, { x = cx - subBtnSize / 2, y = cy - subBtnSize / 2 }, subBtnSize, data)

            btn:SetAlpha(0)
            btn.animProgress = 0

            -- Передаем _targetPos для анимации кнопки при Think
            btn._targetPos = { x = targetX, y = targetY }
            btn._centerPos = { x = cx - subBtnSize / 2, y = cy - subBtnSize / 2 }
            btn._mainButton = mainButton
            btn._parentMenu = self

            -- Подписываемся на события кнопки
            btn.OnClick = function()
                if data.action then
                    data.action()
                end
            end

            btn.OnShowDescription = function(desc)
                self.descriptionPanel:Show(desc)
            end

            btn.OnHideDescription = function()
                self.descriptionPanel:Hide()
                timer.Simple(0.01, function()
                    self:RemoveSubButtons(idx)
                end)
            end

            btn.btn.Think = function(s)
                if not IsValid(btn) or not IsValid(mainButton) then return end

                local hovered = mainButton:IsHovered() or btn:IsHovered()

                -- Проверяем попадание курсора рядом с главной кнопкой радиусом 80
                local mx, my = input.GetCursorPos()
                local bx, by = mainButton:GetPos()
                local bw, bh = mainButton:GetSize()
                local bcx, bcy = bx + bw / 2, by + bh / 2
                local hoverRadius = 80

                if CircularMenu.PointInCircle(mx, my, bcx, bcy, hoverRadius) then
                    hovered = true
                end

                if not hovered then
                    btn.animProgress = math.max(0, btn.animProgress - FrameTime() * 10)
                else
                    btn.animProgress = math.min(1, btn.animProgress + FrameTime() * 10)
                end

                local tx = Lerp(btn.animProgress, btn._centerPos.x, btn._targetPos.x)
                local ty = Lerp(btn.animProgress, btn._centerPos.y, btn._targetPos.y)

                btn:SetPos(tx, ty)
                btn:SetAlpha(255 * btn.animProgress)

                if btn.animProgress < 0.05 and not hovered then
                    btn:Remove()
                end
            end

            btn.btn.OnCursorEntered = function()
                if data.desc then
                    self.descriptionPanel:Show(data.desc)
                end
            end

            btn.btn.OnCursorExited = function()
                self.descriptionPanel:Hide()
                timer.Simple(0.01, function()
                    self:RemoveSubButtons(idx)
                end)
            end

            table.insert(self.subButtons[idx], btn)
        end
    end

    -- Удаляет подкнопки для основного индекса idx, если не находится в зоне ховера
    function CircularMenu:RemoveSubButtons(idx)
        if not self.subButtons[idx] then return end

        local mainBtn = self.buttons[idx]
        if not mainBtn or not mainBtn:IsValid() then return end

        local mx, my = input.GetCursorPos()
        local bx, by = mainBtn:GetPos()
        local bw, bh = mainBtn:GetSize()
        local bcx, bcy = bx + bw / 2, by + bh / 2
        local hoverRadius = 80

        -- Если курсор над главной кнопкой — не удаляем
        if CircularMenu.PointInCircle(mx, my, bcx, bcy, hoverRadius) then return end

        -- Если курсор над одной из подкнопок — не удаляем
        for _, btn in ipairs(self.subButtons[idx]) do
            if btn:IsValid() and btn:IsHovered() then
                return
            end
        end

        -- Удаляем все подкнопки
        for _, btn in ipairs(self.subButtons[idx]) do
            if btn:IsValid() then
                btn:Remove()
            end
        end
        self.subButtons[idx] = nil
    end

    function CircularMenu:RemoveAllSubButtons()
        for idx, _ in pairs(self.subButtons) do
            self:RemoveSubButtons(idx)
        end
    end

    -- Полное удаление меню и очистка ресурсов
    function CircularMenu:Remove()
        if IsValid(self.panel) then
            self.panel:Remove()
            self.panel = nil
        end

        -- Не удаляем descriptionPanel здесь, чтобы не было попытки обращения к nil
        self.buttons = {}
        self.subButtons = {}
        gui.EnableScreenClicker(false)
    end


    -- =========================
    -- ДАННЫЕ КНОПОК
    -- =========================

    local buttonData = {
        [1] = { desc = "Отыгровка действий [По умолчанию исполнить партийное приветствие.]", action = function() print("Открыт инвентарь!") end, text = "1" },
        [2] = { desc = "Открыть карту", action = function() print("Открыта карта!") end, text = "2" },
        [3] = { desc = "Профиль игрока", action = function() print("Профиль!") end, text = "3" },
        [4] = { desc = "Настройки", action = function() print("Настройки!") end, text = "4" },
        [5] = { desc = "Помощь", action = function() print("Помощь!") end, text = "5" },
        [6] = { desc = "Магазин", action = function() print("Магазин!") end, text = "6" },
        [7] = { desc = "Достижения", action = function() print("Достижения!") end, text = "7" },
        [8] = { desc = "Выход", action = function() print("Выход!") end, text = "8" },
    }

    local subButtonData = {}
    for i = 1, 8 do
        subButtonData[i] = {}
        for j = 1, 4 do
            subButtonData[i][j] = {
                desc = string.format("Подкнопка %d-%d", i, j),
                action = function() print(string.format("Подкнопка %d-%d", i, j)) end,
                text = tostring(j)
            }
        end
    end


    -- =========================
    -- ХУКИ ОТКРЫТИЯ/ЗАКРЫТИЯ МЕНЮ
    -- =========================

    local menuInstance = nil

    hook.Add("OnContextMenuOpen", "OpenCircularMenu", function()
        if not menuInstance or not menuInstance.panel or not IsValid(menuInstance.panel) then
            menuInstance = CircularMenu:New(buttonData, subButtonData)
        end
        return true -- блокируем стандартное меню
    end)

    hook.Add("OnContextMenuClose", "CloseCircularMenu", function()
        if menuInstance then
            menuInstance:Remove()
            menuInstance = nil
            gui.EnableScreenClicker(false)
        end
    end)

end
```