```lua
if CLIENT then
    -------------------------------------------------------------------
    -- FirstPersonView Module
    -- Предоставляет точную первую личную камеру, расположенную на голове игрока.
    -------------------------------------------------------------------
    -- Определение таблицы FirstPersonView
    local FirstPersonView = {
        -- Конфигурационная таблица с значениями по умолчанию. Эти значения могут быть переопределены через
        -- консольные переменные (ConVars) для динамической настройки.
        config = {
            headBoneNames = {
                "ValveBiped.Bip01_Head1", -- Приоритетный список названий костей головы
                "ValveBiped.Bip01_Neck1",
                "ValveBiped.Bip01_EyeLeft",
                "ValveBiped.Bip01_EyeRight",
                "ValveBiped.Bip01_Mouth"
            },
            cameraOffset = Vector(0, 0, 0), -- Смещение камеры по умолчанию (изменяется через ConVar)
            drawLocalPlayer = true, -- Отображать модель локального игрока в первом лице
            spectatorTeam = nil, -- Идентификатор команды наблюдателей (устанавливается при инициализации)
            notifyOnError = true, -- Уведомлять игрока об ошибках
            cameraCollision = true, -- Включить детекцию коллизий камеры
            maxCameraDistance = 60, -- Максимальное расстояние камеры для предотвращения клиппинга
            hullSize = 2, -- Размер для TraceHull (определяет полусзмер коробки)
        },
        -- Таблица для кеширования индексов костей головы для локального игрока
        headBoneIndices = {},
        -- Флаг для однократной инициализации
        initialized = false,
        -- Ограничитель частоты уведомлений об ошибках для предотвращения спама
        errorRateLimiter = {
            lastErrorTime = 0,
            cooldown = 5, -- секунды
        },
        -- Таблица для хранения оригинальных масштабов костей
        originalBoneScales = {},
        -- Таблица для хранения идентификаторов callback для ConVar
        convarCallbacks = {},
    }

    ---------------------------------------------------
    -- Утилиты
    ---------------------------------------------------
    -- Функция для логирования сообщений с префиксом
    local function Log(message, isError)
        if isError then
            if FirstPersonView.config.notifyOnError then chat.AddText(Color(255, 0, 0), "[FirstPersonView] " .. message) end
            print("[FirstPersonView] " .. message)
        else
            print("[FirstPersonView] " .. message)
        end
    end

    -- Функция для проверки и обновления ограничителя скорости уведомлений об ошибках
    local function CanNotifyError()
        local currentTime = CurTime()
        if currentTime - FirstPersonView.errorRateLimiter.lastErrorTime >= FirstPersonView.errorRateLimiter.cooldown then
            FirstPersonView.errorRateLimiter.lastErrorTime = currentTime
            return true
        end
        return false
    end

    ---------------------------------------------------
    -- Загрузка и управление конфигурацией
    ---------------------------------------------------
    -- Функция для загрузки и применения смещения камеры из ConVar
    local function LoadCameraOffset()
        local offsetX = GetConVar("fvp_camera_offset_x"):GetFloat()
        local offsetY = GetConVar("fvp_camera_offset_y"):GetFloat()
        local offsetZ = GetConVar("fvp_camera_offset_z"):GetFloat()
        FirstPersonView.config.cameraOffset = Vector(offsetX, offsetY, offsetZ)
    end

    -- Функция для загрузки названий костей головы из ConVar (разделенные запятыми)
    local function LoadHeadBoneNames()
        local boneNamesString = GetConVar("fvp_head_bone_names"):GetString()
        local boneNamesRaw = string.Split(boneNamesString, ",")
        -- Обрезка пробелов и обеспечение уникальности с использованием временной таблицы
        local uniqueBoneSet = {}
        local uniqueBoneList = {}
        for _, name in ipairs(boneNamesRaw) do
            local trimmed = string.Trim(name)
            if trimmed ~= "" and not uniqueBoneSet[trimmed] then
                table.insert(uniqueBoneList, trimmed)
                uniqueBoneSet[trimmed] = true
            end
        end

        FirstPersonView.config.headBoneNames = uniqueBoneList
    end

    -- Функция для загрузки команды наблюдателей из ConVar
    local function LoadSpectatorTeam()
        -- Попытка безопасно получить TEAM_SPECTATOR
        local spectatorTeam = GetConVar("fvp_spectator_team"):GetInt()
        if spectatorTeam <= 0 then
            -- Обратный план на случай некорректного значения
            spectatorTeam = TEAM_SPECTATOR or 8
        end

        FirstPersonView.config.spectatorTeam = spectatorTeam
    end

    -- Функция для инициализации ConVar для динамической конфигурации
    local function InitializeConVars()
        -- Создание ConVar с уникальными идентификаторами для последующего удаления
        FirstPersonView.convarCallbacks.fvp_camera_offset_x = CreateClientConVar("fvp_camera_offset_x", "0", true, false, "FirstPersonView: Смещение камеры X (вперед/назад)")
        FirstPersonView.convarCallbacks.fvp_camera_offset_y = CreateClientConVar("fvp_camera_offset_y", "0", true, false, "FirstPersonView: Смещение камеры Y (влево/вправо)")
        FirstPersonView.convarCallbacks.fvp_camera_offset_z = CreateClientConVar("fvp_camera_offset_z", "0", true, false, "FirstPersonView: Смещение камеры Z (вверх/вниз)")
        FirstPersonView.convarCallbacks.fvp_head_bone_names = CreateClientConVar("fvp_head_bone_names", "ValveBiped.Bip01_Head1,ValveBiped.Bip01_Neck1,ValveBiped.Bip01_EyeLeft,ValveBiped.Bip01_EyeRight,ValveBiped.Bip01_Mouth", true, false, "FirstPersonView: Список названий костей головы, разделенных запятыми")
        FirstPersonView.convarCallbacks.fvp_spectator_team = CreateClientConVar("fvp_spectator_team", tostring(FirstPersonView.config.spectatorTeam or (TEAM_SPECTATOR or 8)), true, false, "FirstPersonView: Идентификатор команды наблюдателей")
    end

    -- Функция для обновления конфигурации из ConVar
    local function RefreshConfig()
        LoadCameraOffset()
        LoadHeadBoneNames()
        LoadSpectatorTeam()
    end

    -- Функция для установки хуков на изменение ConVar
    local function SetupConVarChangeHooks()
        -- Функция для добавления callback и сохранения его идентификатора
        local function AddConVarCallback(cvarName, callbackFunc, identifier)
            local callback = function(convar, oldValue, newValue) callbackFunc() end
            cvars.AddChangeCallback(cvarName, callback, identifier)
            -- Сохранение идентификатора для последующего удаления
            FirstPersonView.convarCallbacks[identifier] = callback
        end

        -- Добавление хуков на изменения ConVar
        AddConVarCallback("fvp_camera_offset_x", LoadCameraOffset, "FirstPersonView_CameraOffsetX")
        AddConVarCallback("fvp_camera_offset_y", LoadCameraOffset, "FirstPersonView_CameraOffsetY")
        AddConVarCallback("fvp_camera_offset_z", LoadCameraOffset, "FirstPersonView_CameraOffsetZ")
        AddConVarCallback("fvp_head_bone_names", LoadHeadBoneNames, "FirstPersonView_HeadBoneNames")
        AddConVarCallback("fvp_spectator_team", LoadSpectatorTeam, "FirstPersonView_SpectatorTeam")
    end

    ---------------------------------------------------
    -- Кеширование костей
    ---------------------------------------------------
    -- Функция для кеширования индексов костей головы для локального игрока
    function FirstPersonView:CacheHeadBones()
        local ply = LocalPlayer()
        if not IsValid(ply) then
            if CanNotifyError() then Log("Локальный игрок не валиден. Невозможно кешировать кости головы.", true) end
            return
        end

        local model = ply:GetModel()
        if not model or model == "" then
            if CanNotifyError() then Log("Модель локального игрока не валидна. Невозможно кешировать кости головы.", true) end
            return
        end

        -- Очистка существующих кешированных костей
        table.Empty(self.headBoneIndices)
        -- Проход по списку возможных названий костей головы и кеширование найденных костей
        for _, boneName in ipairs(self.config.headBoneNames) do
            local boneIndex = ply:LookupBone(boneName)
            if boneIndex then
                table.insert(self.headBoneIndices, boneIndex)
                -- Логирование кеширования костей (раскомментировать для уменьшения спама в консоли)
                -- Log("Кеширована кость '" .. boneName .. "' (Индекс: " .. tostring(boneIndex) .. ") для модели '" .. model .. "'.", false)
            end
        end

        -- Если не найдены кости головы, вывод предупреждения с ограничением частоты уведомлений
        if #self.headBoneIndices == 0 and CanNotifyError() then Log("Предупреждение: Не найдены валидные кости головы для модели '" .. model .. "'. Ожидаемые кости: " .. table.concat(self.config.headBoneNames, ", "), true) end
    end

    ---------------------------------------------------
    -- Позиционирование камеры
    ---------------------------------------------------
    -- Функция для получения позиции головы локального игрока
    function FirstPersonView:GetHeadPosition()
        local ply = LocalPlayer()
        if not IsValid(ply) then return nil end
        if #self.headBoneIndices == 0 then
            -- Попытка кешировать кости головы, если они ещё не кешированы
            self:CacheHeadBones()
            if #self.headBoneIndices == 0 then
                -- Обратный план на позицию глаз, если кости головы не найдены
                Log("Используется позиция глаз в качестве обратного плана для камеры.", true)
                return ply:EyePos()
            end
        end

        -- Использование позиции первой найденной валидной кости головы
        local boneIndex = self.headBoneIndices[1]
        local bonePos = ply:GetBonePosition(boneIndex) -- Удалили неиспользуемую переменную boneAng
        if bonePos then
            return bonePos
        else
            Log("Не удалось получить позицию для кости головы с индексом " .. tostring(boneIndex) .. ". Используется позиция глаз.", true)
            return ply:EyePos()
        end
    end

    -- Переменная для хранения текущего вида камеры
    local currentView = {
        origin = Vector(0, 0, 0),
        angles = Angle(0, 0, 0),
        fov = 0,
        drawviewer = false,
    }

    -- Хук для вычисления и установки вида камеры
    function FirstPersonView:CalcView(ply, pos, angles, fov)
        if not IsValid(ply) then return end
        if ply:Team() == self.config.spectatorTeam then return end
        local headPos = self:GetHeadPosition()
        if not headPos then return end
        -- Использование реальных углов обзора игрока для выравнивания камеры с его видом
        local playerAngles = ply:EyeAngles()
        -- Вычисление желаемой позиции камеры на основе позиции головы и смещения камеры
        local desiredCameraPos = headPos + playerAngles:Forward() * self.config.cameraOffset.x + playerAngles:Right() * self.config.cameraOffset.y + playerAngles:Up() * self.config.cameraOffset.z
        -- Реализация обнаружения коллизий камеры для предотвращения клиппинга
        if self.config.cameraCollision then
            local trace = {}
            trace.start = headPos
            trace.endpos = desiredCameraPos
            trace.filter = ply
            trace.mask = MASK_SOLID
            trace.min = Vector(-self.config.hullSize, -self.config.hullSize, -self.config.hullSize)
            trace.max = Vector(self.config.hullSize, self.config.hullSize, self.config.hullSize)
            local tr = util.TraceHull(trace)
            if tr.Hit then
                local distance = tr.HitPos:Distance(headPos)
                if distance < self.config.maxCameraDistance then
                    desiredCameraPos = tr.HitPos
                else
                    -- Ограничение дистанции камеры
                    desiredCameraPos = headPos + playerAngles:Forward() * self.config.maxCameraDistance + playerAngles:Right() * self.config.cameraOffset.y + playerAngles:Up() * self.config.cameraOffset.z
                end
            end
        end

        -- Установка позиции камеры и углов без сглаживания
        currentView.origin = desiredCameraPos
        currentView.angles = playerAngles
        currentView.fov = fov
        currentView.drawviewer = self.config.drawLocalPlayer
        return currentView
    end

    ---------------------------------------------------
    -- Отображение модели локального игрока
    ---------------------------------------------------
    -- Функция для определения, следует ли рисовать локального игрока
    function FirstPersonView:ShouldDrawLocalPlayer()
        local ply = LocalPlayer()
        if not IsValid(ply) then return true end
        -- Не отображать модель игрока, если он находится в режиме наблюдателя
        if ply:Team() == self.config.spectatorTeam then return false end
        return self.config.drawLocalPlayer
    end

    ---------------------------------------------------
    -- Манипуляция костями для скрытия головы
    ---------------------------------------------------
    -- Функция для скрытия костей головы игрока
    function FirstPersonView:HideHeadBones()
        local ply = LocalPlayer()
        if not IsValid(ply) then return end
        if #self.headBoneIndices == 0 then return end
        for _, boneIndex in ipairs(self.headBoneIndices) do
            -- Сохранение оригинального масштаба, если он ещё не сохранён
            if not self.originalBoneScales[boneIndex] then
                local originalScale = ply:GetManipulateBoneScale(boneIndex) or Vector(1, 1, 1)
                self.originalBoneScales[boneIndex] = originalScale
            end

            -- Скрытие кости путем установки её масштаба в ноль
            ply:ManipulateBoneScale(boneIndex, Vector(0, 0, 0))
        end
    end

    -- Функция для восстановления оригинальных масштабов костей игрока
    function FirstPersonView:RestoreHeadBones()
        local ply = LocalPlayer()
        if not IsValid(ply) then return end
        if #self.headBoneIndices == 0 then return end
        for _, boneIndex in ipairs(self.headBoneIndices) do
            if self.originalBoneScales[boneIndex] then
                ply:ManipulateBoneScale(boneIndex, self.originalBoneScales[boneIndex])
                self.originalBoneScales[boneIndex] = nil
            else
                -- Если оригинальный масштаб не сохранён, устанавливаем стандартный
                ply:ManipulateBoneScale(boneIndex, Vector(1, 1, 1))
            end
        end
    end

    -- Хук для обработки предварительного рисования модели игрока
    function FirstPersonView:PrePlayerDraw(ply)
        if ply ~= LocalPlayer() then return end
        self:HideHeadBones()
    end

    -- Хук для обработки пост-рисования модели игрока
    function FirstPersonView:PostPlayerDraw(ply)
        if ply ~= LocalPlayer() then return end
        self:RestoreHeadBones()
    end

    ---------------------------------------------------
    -- Обработчики событий
    ---------------------------------------------------
    -- Хук для обработки смены модели игрока
    function FirstPersonView:OnPlayerSetModel(ply)
        if ply ~= LocalPlayer() then return end
        self:CacheHeadBones()
    end

    -- Хук для обработки событий спавна игрока
    function FirstPersonView:OnPlayerSpawn(ply)
        if ply ~= LocalPlayer() then return end
        -- Использование безопасного таймера для задержки кеширования до полной загрузки модели
        timer.Simple(0.5, function() if IsValid(ply) then self:CacheHeadBones() end end)
    end

    ---------------------------------------------------
    -- Инициализация и очистка
    ---------------------------------------------------
    -- Функция для очистки всех хуков и кешированных данных
    function FirstPersonView:RemoveHooks()
        hook.Remove("CalcView", "FirstPersonView_CalcView")
        hook.Remove("ShouldDrawLocalPlayer", "FirstPersonView_ShouldDrawLocalPlayer")
        hook.Remove("SetModel", "FirstPersonView_PlayerSetModel")
        hook.Remove("PlayerSpawn", "FirstPersonView_PlayerSpawn")
        hook.Remove("PrePlayerDraw", "FirstPersonView_PrePlayerDraw")
        hook.Remove("PostPlayerDraw", "FirstPersonView_PostPlayerDraw")
        -- Удаление хуков на изменения ConVar
        for identifier, _ in pairs(self.convarCallbacks) do
            cvars.RemoveChangeCallback(identifier)
        end

        -- Восстановление костей головы, если они скрыты
        self:RestoreHeadBones()
        -- Очистка кешированных индексов костей
        table.Empty(self.headBoneIndices)
        self.initialized = false
        Log("Все хуки удалены и кеш очищен.", false)
    end

    -- Функция инициализации модуля
    function FirstPersonView:Initialize()
        if self.initialized then
            Log("Инициализация пропущена: уже инициализировано.", false)
            return
        end

        self.initialized = true
        InitializeConVars()
        RefreshConfig()
        SetupConVarChangeHooks()
        -- Кеширование костей головы для локального игрока
        self:CacheHeadBones()
        -- Регистрация хуков
        hook.Add("CalcView", "FirstPersonView_CalcView", function(ply, pos, angles, fov) return FirstPersonView:CalcView(ply, pos, angles, fov) end)
        hook.Add("ShouldDrawLocalPlayer", "FirstPersonView_ShouldDrawLocalPlayer", function() return FirstPersonView:ShouldDrawLocalPlayer() end)
        hook.Add("SetModel", "FirstPersonView_PlayerSetModel", function(ply) FirstPersonView:OnPlayerSetModel(ply) end)
        hook.Add("PlayerSpawn", "FirstPersonView_PlayerSpawn", function(ply) FirstPersonView:OnPlayerSpawn(ply) end)
        hook.Add("PrePlayerDraw", "FirstPersonView_PrePlayerDraw", function(ply) FirstPersonView:PrePlayerDraw(ply) end)
        hook.Add("PostPlayerDraw", "FirstPersonView_PostPlayerDraw", function(ply) FirstPersonView:PostPlayerDraw(ply) end)
        hook.Add("ShutDown", "FirstPersonView_ShutDown", function() FirstPersonView:RemoveHooks() end)
        Log("FirstPersonView успешно инициализирован.", false)
    end

    ---------------------------------------------------
    -- Запуск инициализации, если модуль ещё не инициализирован
    ---------------------------------------------------
    if not FirstPersonView.initialized then
        -- Задержка инициализации до полной загрузки сущностей
        hook.Add("InitPostEntity", "FirstPersonView_Initialize", function() FirstPersonView:Initialize() end)
    end
    -------------------------------------------------------------------
    -- Конец модуля FirstPersonView
    -------------------------------------------------------------------
end
```
```lua
-- Файл: lua/weapons/custom_weapon.lua
SWEP.PrintName = "Кастомное Оружие"
SWEP.Author = "ВашеИмя"
SWEP.Instructions = "Левый клик для стрельбы."
SWEP.Spawnable = true
SWEP.AdminOnly = false
-- Настройка боеприпасов
SWEP.Primary.ClipSize = 7 -- Размер магазина
SWEP.Primary.DefaultClip = 8 -- 7 в магазине + 1 в патронник
SWEP.Primary.Automatic = false
SWEP.Primary.Ammo = "Pistol" -- Тип боеприпасов
SWEP.Secondary.ClipSize = -1
SWEP.Secondary.DefaultClip = -1
SWEP.Secondary.Automatic = false
SWEP.Secondary.Ammo = "none"
-- Отключаем стандартные руки и ViewModel
SWEP.UseHands = false
SWEP.ViewModel = "" -- Оставьте пустым для отключения
SWEP.ViewModelFOV = 54
-- Указываем модель для мира (что видят другие игроки)
SWEP.WorldModel = "models/deadfall/weapons/fb wz,35 vis.mdl" -- Укажите правильный путь к модели
-- Установка типа хватки
function SWEP:Initialize()
    self:SetHoldType("pistol") -- Устанавливаем начальный тип хватки
    if CLIENT then
        self.ShowAmmoStartTime = 0 -- Время начала отображения текста
        self.ShowAmmoDuration = 3 -- Время, в течение которого текст полностью видим (без учета фейдов)
        self.FadeTime = 1 -- Время фейда (плавного появления и исчезновения)
        self.CurrentHoldType = "pistol" -- Переменная для отслеживания текущего типа хватки
    end
end

-- Создание клиентской модели оружия
if CLIENT then
    function SWEP:CreateWeaponModel()
        if IsValid(self.WeaponModel) then return end
        self.WeaponModel = ClientsideModel(self.WorldModel)
        if IsValid(self.WeaponModel) then
            self.WeaponModel:SetNoDraw(true)
        else
            print("Ошибка: Не удалось создать клиентскую модель оружия по пути " .. self.WorldModel)
        end
    end

    -- Обновление позиции и углов модели оружия
    function SWEP:UpdateWeaponModel()
        if not IsValid(self.WeaponModel) or not IsValid(self:GetOwner()) then return end
        local owner = self:GetOwner()
        local boneIndex = owner:LookupBone("ValveBiped.Bip01_R_Hand") -- Кость правой руки
        if not boneIndex then return end
        local boneMatrix = owner:GetBoneMatrix(boneIndex)
        if not boneMatrix then return end
        local pos, ang = boneMatrix:GetTranslation(), boneMatrix:GetAngles()
        -- Настройка углов поворота для правильного отображения модели
        ang:RotateAroundAxis(ang:Forward(), 10)
        ang:RotateAroundAxis(ang:Right(), 180)
        ang:RotateAroundAxis(ang:Up(), 180)
        -- Настройка позиции относительно кости руки
        pos = pos + ang:Forward() * -1.5 + ang:Right() * -2 + ang:Up() * 2.3
        self.WeaponModel:SetRenderOrigin(pos)
        self.WeaponModel:SetRenderAngles(ang)
    end

    -- Функция Think для отслеживания нажатия клавиши R (перезарядки) и изменения типа хватки
    function SWEP:Think()
        -- Существующая логика отображения патронов при перезарядке
        if self.ShowAmmoStartTime == nil then self.ShowAmmoStartTime = 0 end
        if self:GetOwner():KeyPressed(IN_RELOAD) then
            self.ShowAmmoStartTime = CurTime() -- Начало отображения текста
        end

        -- Новая логика для изменения типа хватки при нажатии правой кнопки мыши
        local currentOwner = self:GetOwner() -- Renamed from 'owner' to 'currentOwner'
        if IsValid(currentOwner) then
            if currentOwner:KeyDown(IN_ATTACK2) then -- Проверяем, удерживается ли правая кнопка мыши
                if self.CurrentHoldType ~= "revolver" then
                    self:SetHoldType("revolver") -- Меняем тип хватки на "revolver"
                    self.CurrentHoldType = "revolver" -- Обновляем текущий тип хватки
                end
            else
                if self.CurrentHoldType ~= "pistol" then
                    self:SetHoldType("pistol") -- Возвращаем тип хватки на "pistol"
                    self.CurrentHoldType = "pistol" -- Обновляем текущий тип хватки
                end
            end
        end

        -- Существующая логика функции Think
        local currentTime = CurTime()
        local elapsedTime = currentTime - self.ShowAmmoStartTime
        local totalDisplayTime = self.ShowAmmoDuration + 2 * self.FadeTime -- Общее время отображения текста
        if elapsedTime < 0 or elapsedTime > totalDisplayTime then
            return -- Не отображаем текст
        end

        -- Вычисляем альфа-канал для плавного появления и исчезновения
        local alpha = 255
        if elapsedTime < self.FadeTime then
            -- Фаза плавного появления
            alpha = Lerp(elapsedTime / self.FadeTime, 0, 255)
        elseif elapsedTime > (self.ShowAmmoDuration + self.FadeTime) then
            -- Фаза плавного исчезновения
            local fadeOutTime = elapsedTime - (self.ShowAmmoDuration + self.FadeTime)
            alpha = Lerp(fadeOutTime / self.FadeTime, 255, 0)
        else
            alpha = 255 -- Полностью видимый текст
        end

        if not IsValid(self:GetOwner()) then return end
        local owner = self:GetOwner()
        local boneIndex = owner:LookupBone("ValveBiped.Bip01_R_Hand")
        if not boneIndex then return end
        local bonePos, boneAng = owner:GetBonePosition(boneIndex)
        -- Настройка позиции текста относительно руки
        local offset = Vector(0, 10, -5)
        local textPos = bonePos + boneAng:Right() * offset.x + boneAng:Forward() * offset.y + boneAng:Up() * offset.z
        -- Настройка угла поворота текста
        local textAng = boneAng
        textAng:RotateAroundAxis(boneAng:Right(), 90)
        textAng:RotateAroundAxis(boneAng:Up(), 90)
        textAng:RotateAroundAxis(boneAng:Forward(), 0)
        cam.Start3D2D(textPos, textAng, 0.1)
        draw.SimpleText("Патроны: " .. self:Clip1() .. "/" .. currentOwner:GetAmmoCount(self.Primary.Ammo), "DermaLarge", 0, 0, Color(255, 255, 255, alpha), TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
        cam.End3D2D()
    end

    -- Отрисовка модели оружия в мире и 3D текста с количеством патронов
    function SWEP:DrawWorldModel()
        -- Сначала отрисовываем модель оружия
        if not IsValid(self.WeaponModel) then
            self:CreateWeaponModel()
            if not IsValid(self.WeaponModel) then return end
        end

        if IsValid(self:GetOwner()) then
            self:UpdateWeaponModel()
            self.WeaponModel:DrawModel()
        else
            self:DrawModel()
        end

        -- Затем отрисовываем 3D текст с количеством патронов, если время отображения не истекло
        local currentTime = CurTime()
        local elapsedTime = currentTime - self.ShowAmmoStartTime
        local totalDisplayTime = self.ShowAmmoDuration + 2 * self.FadeTime -- Общее время отображения текста
        if elapsedTime < 0 or elapsedTime > totalDisplayTime then
            return -- Не отображаем текст
        end

        -- Вычисляем альфа-канал для плавного появления и исчезновения
        local alpha = 255
        if elapsedTime < self.FadeTime then
            -- Фаза плавного появления
            alpha = Lerp(elapsedTime / self.FadeTime, 0, 255)
        elseif elapsedTime > (self.ShowAmmoDuration + self.FadeTime) then
            -- Фаза плавного исчезновения
            local fadeOutTime = elapsedTime - (self.ShowAmmoDuration + self.FadeTime)
            alpha = Lerp(fadeOutTime / self.FadeTime, 255, 0)
        else
            alpha = 255 -- Полностью видимый текст
        end

        local currentOwner = self:GetOwner() -- Renamed from 'owner' to 'currentOwner'
        if not IsValid(currentOwner) then return end
        local boneIndex = currentOwner:LookupBone("ValveBiped.Bip01_R_Hand")
        if not boneIndex then return end
        local bonePos, boneAng = currentOwner:GetBonePosition(boneIndex)
        -- Настройка позиции текста относительно руки
        local offset = Vector(0, 10, -5)
        local textPos = bonePos + boneAng:Right() * offset.x + boneAng:Forward() * offset.y + boneAng:Up() * offset.z
        -- Настройка угла поворота текста
        local textAng = boneAng
        textAng:RotateAroundAxis(boneAng:Right(), 90)
        textAng:RotateAroundAxis(boneAng:Up(), 90)
        textAng:RotateAroundAxis(boneAng:Forward(), 0)
        cam.Start3D2D(textPos, textAng, 0.1)
        draw.SimpleText("Патроны: " .. self:Clip1() .. "/" .. currentOwner:GetAmmoCount(self.Primary.Ammo), "DermaLarge", 0, 0, Color(255, 255, 255, alpha), TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER)
        cam.End3D2D()
    end

    -- Удаление клиентской модели оружия
    function SWEP:OnRemove()
        if IsValid(self.WeaponModel) then
            self.WeaponModel:Remove()
            self.WeaponModel = nil
        end
    end

    -- Очистка модели при холстере
    function SWEP:Holster()
        if IsValid(self.WeaponModel) then
            self.WeaponModel:Remove()
            self.WeaponModel = nil
        end
        return true
    end
end

-- Деплой оружия
function SWEP:Deploy()
    if CLIENT then self:CreateWeaponModel() end
    self:SendWeaponAnim(ACT_VM_DRAW)
    return true
end

-- Основная логика при первичной атаке (стрельба)
function SWEP:PrimaryAttack()
    if not self:CanPrimaryAttack() then return end
    self:SetNextPrimaryFire(CurTime() + self:GetPrimaryFireDelay())
    local owner = self:GetOwner()
    if not IsValid(owner) then return end
    -- Воспроизвести звуковой эффект
    self:EmitSound("Weapon_Pistol.Single")
    -- Воспроизвести анимацию выстрела
    self:SendWeaponAnim(ACT_VM_PRIMARYATTACK)
    owner:SetAnimation(PLAYER_ATTACK1) -- Анимация игрока при выстреле
    -- Уменьшаем количество патронов в магазине
    self:TakePrimaryAmmo(1)
    -- Создать эффект выстрела (Muzzle Flash)
    if CLIENT then
        local vm = owner:GetViewModel()
        if IsValid(vm) then
            local attachmentID = vm:LookupAttachment("1") -- Обычно "1" это muzzle
            local attachment = vm:GetAttachment(attachmentID)
            if attachment then
                local effect = EffectData()
                effect:SetOrigin(attachment.Pos)
                effect:SetAngles(attachment.Ang)
                effect:SetScale(1)
                util.Effect("MuzzleFlash", effect)
            end
        end
    end

    -- Логика выстрела пули со всеми эффектами
    if SERVER then
        local bullet = {}
        bullet.Num = 1
        bullet.Src = owner:GetShootPos() -- Позиция вылета пули
        bullet.Dir = owner:GetAimVector() -- Направление вылета пули
        bullet.Spread = Vector(0, 0, 0) -- Разброс (измените при необходимости)
        bullet.Tracer = 1 -- Частота появления трассеров (1 = каждый выстрел)
        bullet.TracerName = "Tracer" -- Эффект трассера ("Tracer" - стандартный)
        bullet.Force = 5 -- Сила попадания
        bullet.Damage = 10 -- Урон
        bullet.AmmoType = self.Primary.Ammo
        owner:FireBullets(bullet)
    end
end

-- Задержка между выстрелами
function SWEP:GetPrimaryFireDelay()
    return 0.5 -- Задержка между выстрелами в секундах
end

-- Проверка возможности атаки
function SWEP:CanPrimaryAttack()
    if self:Clip1() <= 0 then
        self:EmitSound("Weapon_Pistol.Empty") -- Звук пустого магазина
        self:SetNextPrimaryFire(CurTime() + 0.2) -- Небольшая задержка перед следующим действием
        return false
    end
    return true
end

-- Действие при вторичной атаке
function SWEP:SecondaryAttack()
    -- Если вторичная атака не используется, оставьте пустым
    -- Hold type изменяется динамически в функции Think()
end

-- Функция перезарядки оружия
function SWEP:Reload()
    if self:GetOwner():KeyDown(IN_ATTACK) then -- Не перезаряжаться при атаке
        return
    end

    if self:Clip1() >= self.Primary.ClipSize then -- Магазин полон
        return
    end

    if self:GetOwner():GetAmmoCount(self.Primary.Ammo) <= 0 then -- Нет боеприпасов
        return
    end

    self:DefaultReload(ACT_VM_RELOAD)
    self:EmitSound("Weapon_Pistol.Reload") -- Звук перезарядки
    self:SetNextPrimaryFire(CurTime() + 2) -- Устанавливаем задержку на время перезарядки
    self:GetOwner():SetAnimation(PLAYER_RELOAD) -- Анимация перезарядки для игрока
end
```
Сделай framework, который совмещает эти файлы в одном аддоне и делает удобную модульность, ООП и SOLID.