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

    -- Кэшированный индекс кости головы для LocalPlayer
    FirstPersonView.headBoneIndex = nil
    -- Инициализация FirstPersonView
    function FirstPersonView:Initialize()
        self:CacheHeadBone()
        -- Добавление необходимых хуков
        hook.Add("CalcView", "FirstPersonView_CalcView", function(...) self:CalcView(...) 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)
    end

    -- Кэширование индекса кости головы
    function FirstPersonView:CacheHeadBone(ply)
        ply = ply or LocalPlayer()
        if not IsValid(ply) then return end
        local boneIndex = ply:LookupBone(self.config.headBoneName)
        if boneIndex then
            self.headBoneIndex = boneIndex
        else
            print("[FirstPersonView] Предупреждение: Не удалось найти кость '" .. self.config.headBoneName .. "' для модели игрока.")
            self.headBoneIndex = nil
        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.headBoneIndex then
            self:CacheHeadBone(ply)
            if not self.headBoneIndex then return nil, nil end
        end

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

    -- Хук CalcView для изменения вида камеры
    function FirstPersonView:CalcView(ply, pos, angles, fov)
        if not IsValid(ply) or ply:Team() == TEAM_SPECTATOR 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 = true, -- Рисует камеру от первого лица
        }
        return view
    end

    -- Хук ShouldDrawLocalPlayer для управления отрисовкой модели игрока
    function FirstPersonView:ShouldDrawLocalPlayer()
        return true -- Всегда возвращаем true, чтобы модель игрока отображалась в первом лице
    end

    -- Функция очистки (при необходимости расширить)
    function FirstPersonView:Cleanup()
        -- В текущей реализации дополнительных действий не требуется
        -- Если будут использоваться клиент-сайд модели или другие ресурсы, их очистку сюда
    end

    -- Удаление хуков при отключении скрипта
    function FirstPersonView:RemoveHooks()
        hook.Remove("CalcView", "FirstPersonView_CalcView")
        hook.Remove("ShouldDrawLocalPlayer", "FirstPersonView_ShouldDrawLocalPlayer")
        hook.Remove("PlayerSetModel", "FirstPersonView_PlayerSetModel")
        hook.Remove("ShutDown", "FirstPersonView_ShutDown")
    end

    -- Запуск инициализации при загрузке скрипта
    FirstPersonView:Initialize()
end
```
Как сделать привязку камеры к голове персонажа