```lua
-- cl_firstperson.lua
if CLIENT then
    -- Локальная таблица для инкапсуляции функционала FirstPersonView
    local FirstPersonView = {}
    -- Конфигурация
    FirstPersonView.config = {
        headBoneName = "ValveBiped.Bip01_Head1", -- Имя кости головы. Измените при необходимости.
        cameraOffset = Vector(2, 0, 0), -- Смещение камеры относительно головы. Настройте по своему усмотрению.
        drawLocalPlayer = true, -- Определяет, должна ли модель игрока отображаться в первом лице.
        spectatorTeam = TEAM_SPECTATOR or 8, -- Команда зрителей. Замените 8 на актуальный номер команды зрителей, если необходимо.
        notifyOnError = true, -- Показывать уведомления игроку при ошибках.
    }

    -- Таблица для кэширования индексов костей головы по игрокам
    FirstPersonView.headBoneIndices = {}
    -- Инициализация FirstPersonView
    function FirstPersonView:Initialize()
        self:CacheHeadBone(LocalPlayer())
        -- Добавление необходимых хуков
        hook.Add("CalcView", "FirstPersonView_CalcView", function(ply, pos, angles, fov) return self:CalcView(ply, pos, angles, fov) end)
        hook.Add("ShouldDrawLocalPlayer", "FirstPersonView_ShouldDrawLocalPlayer", function() return self:ShouldDrawLocalPlayer() end)
        hook.Add("PlayerSetModel", "FirstPersonView_PlayerSetModel", function(ply) self:OnPlayerSetModel(ply) end)
        hook.Add("Shutdown", "FirstPersonView_Shutdown", function() self:RemoveHooks() end)
        hook.Add("InitPostEntity", "FirstPersonView_InitPostEntity", function() self:CacheHeadBone(LocalPlayer()) end)
        -- Обновление кэша костей при изменении команды игрока
        hook.Add("OnPlayerChangedTeam", "FirstPersonView_OnPlayerChangedTeam", function(ply, oldTeam, newTeam) if ply == LocalPlayer() then self:CacheHeadBone(ply) end end)
        -- Обработка загрузки ассетов для игрока
        hook.Add("OnEntityCreated", "FirstPersonView_OnEntityCreated", function(ent) if ent:IsPlayer() and ent == LocalPlayer() then ent:CallOnClient("FirstPersonView_Init", function() self:CacheHeadBone(ent) end) end end)
        -- Добавление хуков для скрытия головы
        hook.Add("PrePlayerDraw", "FirstPersonView_PrePlayerDraw", function(ply)
            if ply == LocalPlayer() then
                local boneIndex = ply:LookupBone(self.config.headBoneName)
                if boneIndex then
                    ply:ManipulateBoneScale(boneIndex, Vector(0, 0, 0)) -- Скрыть голову
                end
            end
        end)

        hook.Add("PostPlayerDraw", "FirstPersonView_PostPlayerDraw", function(ply)
            if ply == LocalPlayer() then
                local boneIndex = ply:LookupBone(self.config.headBoneName)
                if boneIndex then
                    ply:ManipulateBoneScale(boneIndex, Vector(1, 1, 1)) -- Восстановить масштаб головы
                end
            end
        end)
    end

    -- Кэширование индекса кости головы для конкретного игрока
    function FirstPersonView:CacheHeadBone(ply)
        if not IsValid(ply) then return end
        local boneIndex = ply:LookupBone(self.config.headBoneName)
        if boneIndex then
            self.headBoneIndices[ply] = boneIndex
        else
            self.headBoneIndices[ply] = nil
            if self.config.notifyOnError then
                chat.AddText(Color(255, 0, 0), "[FirstPersonView] Предупреждение: Не удалось найти кость '" .. self.config.headBoneName .. "' для модели игрока.")
            else
                print("[FirstPersonView] Предупреждение: Не удалось найти кость '" .. self.config.headBoneName .. "' для модели игрока.")
            end
        end
    end

    -- Обработка изменения модели игрока
    function FirstPersonView:OnPlayerSetModel(ply)
        if ply ~= LocalPlayer() then return end
        -- Пере-кэширование индекса кости головы при смене модели
        self:CacheHeadBone(ply)
    end

    -- Получение позиции и угла кости головы для конкретного игрока
    function FirstPersonView:GetHeadPos(ply)
        if not IsValid(ply) then return nil, nil end
        -- Проверка и кэширование индекса кости головы
        if not self.headBoneIndices[ply] then
            self:CacheHeadBone(ply)
            if not self.headBoneIndices[ply] then return nil, nil end
        end

        local boneIndex = self.headBoneIndices[ply]
        if not boneIndex then return nil, nil end
        local bonePos, boneAng = ply:GetBonePosition(boneIndex)
        if not bonePos or not boneAng then
            if self.config.notifyOnError then
                chat.AddText(Color(255, 0, 0), "[FirstPersonView] Предупреждение: Не удалось получить позицию или угол кости головы.")
            else
                print("[FirstPersonView] Предупреждение: Не удалось получить позицию или угол кости головы.")
            end
            return nil, nil
        end
        return bonePos, boneAng
    end

    -- Хук CalcView для изменения вида камеры
    function FirstPersonView:CalcView(ply, pos, angles, fov)
        if not IsValid(ply) or ply:Team() == self.config.spectatorTeam then return end
        local headPos, headAng = self:GetHeadPos(ply)
        if not headPos or not headAng then return end
        -- Расчёт новой позиции камеры
        local cameraPos = headPos + headAng:Forward() * self.config.cameraOffset.x + headAng:Right() * self.config.cameraOffset.y + headAng:Up() * self.config.cameraOffset.z
        -- Настройка параметров вида камеры
        local view = {
            origin = cameraPos,
            angles = angles, -- Используем реальные углы обзора игрока
            fov = fov,
            drawviewer = self.config.drawLocalPlayer, -- Рисует камеру от первого лица, если отображается модель игрока
        }
        return view
    end

    -- Хук ShouldDrawLocalPlayer для управления отрисовкой модели игрока
    function FirstPersonView:ShouldDrawLocalPlayer()
        return self.config.drawLocalPlayer
    end

    -- Функция очистки ресурсов и удаления хуков
    function FirstPersonView:RemoveHooks()
        hook.Remove("CalcView", "FirstPersonView_CalcView")
        hook.Remove("ShouldDrawLocalPlayer", "FirstPersonView_ShouldDrawLocalPlayer")
        hook.Remove("PlayerSetModel", "FirstPersonView_PlayerSetModel")
        hook.Remove("Shutdown", "FirstPersonView_Shutdown")
        hook.Remove("InitPostEntity", "FirstPersonView_InitPostEntity")
        hook.Remove("OnPlayerChangedTeam", "FirstPersonView_OnPlayerChangedTeam")
        hook.Remove("OnEntityCreated", "FirstPersonView_OnEntityCreated")
        hook.Remove("OnPlayerSpawn", "FirstPersonView_OnPlayerSpawn")
        hook.Remove("PrePlayerDraw", "FirstPersonView_PrePlayerDraw")
        hook.Remove("PostPlayerDraw", "FirstPersonView_PostPlayerDraw")
    end

    -- Функция для получения конфигурации (опционально используется, если требуется доступ из других частей скрипта)
    function FirstPersonView:GetConfig()
        return self.config
    end

    -- Запуск инициализации при загрузке скрипта
    hook.Add("InitPostEntity", "FirstPersonView_Initialize", function() FirstPersonView:Initialize() end)
    -- Обновление кэша костей при появлении игрока в игре
    hook.Add("OnPlayerSpawn", "FirstPersonView_OnPlayerSpawn", function(ply)
        if ply == LocalPlayer() then
            timer.Simple(0.1, function()
                -- Немного отложим кэширование, чтобы модель успела загрузиться
                FirstPersonView:CacheHeadBone(ply)
            end)
        end
    end)
end
```
Перечисли все возможные баги, ошибки, недочеты, утечки и другие проблемы.