```lua
-- cl_realistic_smoke.lua

if CLIENT then
    -- Таблица для хранения всех активных частиц дыма
    local SmokeParticles = {}

    -- Настройки дыма
    local SmokeSettings = {
        EmitRate = 10,            -- Количество частиц, создаваемых за кадр
        MaxParticles = 1000,      -- Максимальное количество частиц
        SpawnPosition = Vector(0, 0, 0), -- Позиция эмиттера (можно изменить динамически)
        Spread = 50,              -- Разброс начальной позиции частиц
        InitialVelocity = Vector(0, 0, 50), -- Начальная скорость частиц (подъем)
        VelocityRandomness = 20,  -- Разброс скорости
        ParticleLifetime = {4, 8},-- Диапазон времени жизни частиц (секунды)
        ParticleSize = {15, 40},  -- Диапазон начального размера частиц
        FadeOutTime = 2,          -- Время плавного исчезновения
        ColorStart = Color(120, 120, 120, 255), -- Начальный цвет дыма
        ColorEnd = Color(80, 80, 80, 0),         -- Конечный цвет дыма
        Texture = "particle/particle_smokegrenade", -- Текстура частицы
        Buoyancy = 20,            -- Ускорение вверх (бууйанс)
        Turbulence = 10,          -- Сила турбулентности
        Wind = Vector(10, 0, 0),  -- Направление и сила ветра
    }

    -- Класс SmokeParticle
    local SmokeParticle = {}
    SmokeParticle.__index = SmokeParticle

    function SmokeParticle:New(pos)
        local self = setmetatable({}, SmokeParticle)
        -- Начальная позиция с разбросом
        self.pos = pos + Vector(
            math.Rand(-SmokeSettings.Spread, SmokeSettings.Spread),
            math.Rand(-SmokeSettings.Spread, SmokeSettings.Spread),
            math.Rand(0, 10)
        )
        -- Начальная скорость с разбросом и турбулентностью
        self.velocity = SmokeSettings.InitialVelocity +
                        Vector(
                            math.Rand(-SmokeSettings.VelocityRandomness, SmokeSettings.VelocityRandomness),
                            math.Rand(-SmokeSettings.VelocityRandomness, SmokeSettings.VelocityRandomness),
                            math.Rand(-10, 10)
                        )
        self.size = math.Rand(SmokeSettings.ParticleSize[1], SmokeSettings.ParticleSize[2])
        self.maxSize = self.size * 3
        self.colorStart = SmokeSettings.ColorStart
        self.colorEnd = SmokeSettings.ColorEnd
        self.alpha = 255
        self.spawnTime = CurTime()
        self.lifetime = math.Rand(SmokeSettings.ParticleLifetime[1], SmokeSettings.ParticleLifetime[2])
        self.texture = SmokeSettings.Texture
        self.collided = false
        return self
    end

    -- Обновление частицы
    function SmokeParticle:Update(dt)
        if self.collided then
            -- Если частица столкнулась, можно добавить эффект дыма на месте столкновения
            return
        end

        -- Добавление бууйанса (постепенное ускорение вверх)
        self.velocity = self.velocity + Vector(0, 0, SmokeSettings.Buoyancy * dt)

        -- Добавление турбулентности (случайные изменения скорости)
        local turbulence = Vector(
            math.Rand(-SmokeSettings.Turbulence, SmokeSettings.Turbulence),
            math.Rand(-SmokeSettings.Turbulence, SmokeSettings.Turbulence),
            math.Rand(-5, 5)
        )
        self.velocity = self.velocity + turbulence * dt

        -- Добавление ветра
        self.velocity = self.velocity + SmokeSettings.Wind * dt

        -- Обновление позиции
        self.pos = self.pos + self.velocity * dt

        -- Проверка коллизий с миром
        local tr = util.TraceLine({
            start = self.pos - self.velocity * dt, -- Предыдущая позиция
            endpos = self.pos,
            mask = MASK_SOLID_BRUSHONLY -- Проверка только на статические объекты
        })

        if tr.Hit then
            self.pos = tr.HitPos
            self.velocity = Vector(0, 0, 0)
            self.collided = true
        end

        -- Расширение частицы для эффекта диффузии
        self.size = math.min(self.size + 15 * dt, self.maxSize)

        -- Плавное уменьшение альфа для исчезновения
        local age = CurTime() - self.spawnTime
        if age > (self.lifetime - SmokeSettings.FadeOutTime) then
            local fadeAlpha = 255 * (1 - (age - (self.lifetime - SmokeSettings.FadeOutTime)) / SmokeSettings.FadeOutTime)
            self.alpha = math.Clamp(fadeAlpha, 0, 255)
        end
    end

    -- Отрисовка частицы
    function SmokeParticle:Draw()
        if self.alpha <= 0 then return end

        render.SetMaterial(Material(self.texture))
        -- Линейная интерполяция цвета от начала до конца жизни частицы
        local age = (CurTime() - self.spawnTime) / self.lifetime
        age = math.Clamp(age, 0, 1)
        local color = Color(
            Lerp(age, self.colorStart.r, self.colorEnd.r),
            Lerp(age, self.colorStart.g, self.colorEnd.g),
            Lerp(age, self.colorStart.b, self.colorEnd.b),
            self.alpha
        )
        render.DrawSprite(self.pos, self.size, self.size, color)
    end

    -- Функция для эмитирования новых частиц
    local function EmitSmokeParticles()
        -- Определение позиции эмиттера (можно заменить на динамическую, например, позицию игрока)
        local emitterPos = SmokeSettings.SpawnPosition

        for i = 1, SmokeSettings.EmitRate do
            if #SmokeParticles >= SmokeSettings.MaxParticles then break end
            table.insert(SmokeParticles, SmokeParticle:New(emitterPos))
        end
    end

    -- Функция для обновления и отрисовки частиц
    local function UpdateAndDrawParticles()
        local dt = FrameTime()
        for i = #SmokeParticles, 1, -1 do
            local particle = SmokeParticles[i]
            local age = CurTime() - particle.spawnTime

            if age > particle.lifetime then
                table.remove(SmokeParticles, i)
            else
                particle:Update(dt)
                particle:Draw()
            end
        end
    end

    -- Добавляем хуки
    hook.Add("Think", "RealisticSmokeThink", function()
        EmitSmokeParticles()
    end)

    hook.Add("PostDrawTranslucentRenderables", "RealisticSmokeDraw", function()
        UpdateAndDrawParticles()
    end)

    -- Обработка команды для изменения позиции эмиттера
    concommand.Add("set_smoke_position", function(ply, cmd, args)
        if args[1] and args[2] and args[3] then
            local x = tonumber(args[1]) or 0
            local y = tonumber(args[2]) or 0
            local z = tonumber(args[3]) or 0
            SmokeSettings.SpawnPosition = Vector(x, y, z)
            print("Smoke emitter position set to:", SmokeSettings.SpawnPosition)
        else
            print("Usage: set_smoke_position <x> <y> <z>")
        end
    end)

    -- Инициализация: Установим позицию эмиттера на позицию игрока при подключении
    hook.Add("InitPostEntity", "InitializeSmokeEmitter", function()
        local ply = LocalPlayer()
        if ply:IsValid() then
            SmokeSettings.SpawnPosition = ply:GetPos() + Vector(0, 0, 50) -- Например, над головой игрока
        end
    end)

    -- Опционально: Обновление позиции эмиттера каждый кадр (например, за игроком)
    hook.Add("Think", "UpdateSmokeEmitterPosition", function()
        local ply = LocalPlayer()
        if ply:IsValid() then
            SmokeSettings.SpawnPosition = ply:GetPos() + Vector(0, 0, 50) -- Над головой игрока
        end
    end)
end
```
Сделай чтобы дым заполнял комнату, чтобы он был не над игроком, чтобы его можно было заспавнить на определнном месте.  И сделай debug функции