Code/Ultimate_Light_Manager.cs
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;

[Title( "Ultimate Light Manager" )]
[Category( "Light" )]
[Icon( "tungsten" )]
public class UltimateLightManager : Component, Component.ExecuteInEditor
{
    // =========================================================
    // GLOBAL MANAGEMENT (STATIC)
    // =========================================================
    public static List<UltimateLightManager> AllLights = new();

    public static void SetGroupState( string groupName, bool isEnabled )
    {
        foreach ( var light in AllLights.Where( l => l.LightGroup == groupName ) )
        {
            light.IsEnabled = isEnabled;
        }
    }

    // =========================================================
    // 0. MANAGEMENT & DEBUG
    // =========================================================
    [Property, Order( -1 ), Group( "Management" )] 
    [Description("EN: Tag for global script control | FR: Tag pour contrôle global | ES: Etiqueta para control global | RU: Тег для глобального контроля")]
    public string LightGroup { get; set; } = "Default";

    [Property, Group( "Management" )] 
    [Description("EN: Turn-on delay (sec) | FR: Délai d'allumage (sec) | ES: Retraso de encendido (seg) | RU: Задержка включения (сек)")]
    public float StartDelay { get; set; } = 0.0f;

    [Property, Group( "Management" )] 
    [Description("EN: Desyncs duplicated lights | FR: Désynchronise les lumières dupliquées | ES: Desincroniza luces duplicadas | RU: Рассинхронизация дубликатов")]
    public bool AutoDesync { get; set; } = true;

    [Property, Group( "Management" )] 
    [Description("EN: Show visual debug ranges | FR: Afficher les portées visuelles | ES: Mostrar rangos visuales | RU: Показать визуальные границы")]
    public bool ShowDebugGizmos { get; set; } = false;

    // =========================================================
    // 1. SETUP & GENERAL
    // =========================================================
    public enum LightTypeEnum { Point, Spot }

    [Property, Group( "General" )] 
    [Description("EN: Point or Spot | FR: Ampoule ou Projecteur | ES: Bombilla o Foco | RU: Лампа или Прожектор")]
    public LightTypeEnum TargetLightType { get; set; } = LightTypeEnum.Point;

    [Property, Group( "General" ), Sync] 
    [Description("EN: Toggle state (Synced) | FR: État (Synchronisé) | ES: Estado (Sincronizado) | RU: Состояние (Синхронизировано)")]
    public bool IsEnabled { get; set; } = true;

    [Property, Group( "General" ), Sync] 
    [Description("EN: Base light color | FR: Couleur de base | ES: Color base | RU: Базовый цвет")]
    public Color LightColor { get; set; } = Color.White;

    [Property, Group( "General" ), Range( 0, 100 ), Sync] 
    [Description("EN: Light intensity | FR: Intensité lumineuse | ES: Intensidad de luz | RU: Интенсивность света")]
    public float Brightness { get; set; } = 1.0f;

    [Property, Group( "General" ), Range( 0, 10 )] 
    [Description("EN: Fog/God Rays multiplier | FR: Multiplicateur de God Rays | ES: Multiplicador de God Rays | RU: Множитель объемных лучей")]
    public float VolumetricBoost { get; set; } = 1.0f;

    [Property, Group( "General" )] 
    [Description("EN: Enable dynamic shadows | FR: Activer les ombres dynamiques | ES: Activar sombras dinámicas | RU: Включить тени")]
    public bool CastShadows { get; set; } = true;

    // =========================================================
    // 2. AUDIO SYSTEM
    // =========================================================
    [Property, Group( "Audio" )] 
    [Description("EN: Ambient loop sound | FR: Son d'ambiance en boucle | ES: Sonido ambiental en bucle | RU: Фоновый звук")]
    public SoundEvent AmbientSound { get; set; }
    
    [Property, Group( "Audio" )] 
    [Description("EN: Modulate volume with light | FR: Moduler volume selon l'intensité | ES: Modular volumen con intensidad | RU: Изменение громкости от света")]
    public bool ModulateVolumeWithLight { get; set; } = true;

    [Property, Group( "Audio" ), Range( 0, 5 )] 
    [Description("EN: Base sound volume | FR: Volume de base | ES: Volumen base | RU: Базовая громкость")]
    public float BaseVolume { get; set; } = 1.0f;

    // =========================================================
    // 3. OPTIMIZATIONS
    // =========================================================
    [Property, Group( "Optimization" )] 
    [Description("EN: Max distance before culling | FR: Distance max avant désactivation | ES: Distancia máx antes de desactivar | RU: Макс. дистанция до отключения")]
    public float MaxDistance { get; set; } = 2500.0f;

    [Property, Group( "Optimization" )] 
    [Description("EN: Max shadow casting distance | FR: Distance max des ombres | ES: Distancia máx de sombras | RU: Макс. дистанция теней")]
    public float ShadowMaxDistance { get; set; } = 800.0f;

    [Property, Group( "Optimization" )] 
    [Description("EN: Enable distance culling | FR: Activer la désactivation par distance | ES: Activar desactivación por distancia | RU: Включить отключение по дистанции")]
    public bool EnableCulling { get; set; } = true;

    // =========================================================
    // 4. ADVANCED FEATURES
    // =========================================================

    // --- FIRE & CANDLE ---
    [Property, FeatureEnabled( "Fire & Candle" )] public bool EnableFire { get; set; } = false;
    
    [Property, Feature( "Fire & Candle" )] 
    [Description("EN: Flicker speed | FR: Vitesse d'agitation | ES: Velocidad de agitación | RU: Скорость мерцания")]
    public float FireSpeed { get; set; } = 12.0f;
    
    [Property, Feature( "Fire & Candle" ), Range( 0, 1 )] 
    [Description("EN: Flicker depth | FR: Profondeur des creux | ES: Profundidad del parpadeo | RU: Глубина мерцания")]
    public float FireIntensity { get; set; } = 0.3f;
    
    [Property, Feature( "Fire & Candle" ), Range( 0, 2 )] 
    [Description("EN: Randomness factor | FR: Facteur de chaos/aléatoire | ES: Factor aleatorio | RU: Фактор хаоса")]
    public float FireChaos { get; set; } = 1.0f;

    // --- HORROR (BROKEN BULB) ---
    [Property, FeatureEnabled( "Horror Mode" )] public bool EnableHorror { get; set; } = false;
    
    [Property, Feature( "Horror Mode" )] 
    [Description("EN: Min time between flickers | FR: Délai min entre clignotements | ES: Tiempo mín entre parpadeos | RU: Мин. время между мерцаниями")]
    public float MinFlickerDelay { get; set; } = 0.05f;
    
    [Property, Feature( "Horror Mode" )] 
    [Description("EN: Max time between flickers | FR: Délai max entre clignotements | ES: Tiempo máx entre parpadeos | RU: Макс. время между мерцаниями")]
    public float MaxFlickerDelay { get; set; } = 0.4f;
    
    [Property, Feature( "Horror Mode" ), Range( 0, 1 )] 
    [Description("EN: Voltage drop probability | FR: Probabilité de chute de tension | ES: Probabilidad de caída de voltaje | RU: Вероятность падения напряжения")]
    public float DamageSeverity { get; set; } = 0.8f;
    
    [Property, Feature( "Horror Mode" )] 
    [Description("EN: Spark sound effect | FR: Son d'étincelle | ES: Sonido de chispa | RU: Звук искры")]
    public SoundEvent SparkSound { get; set; }

    // --- DISCO (RAINBOW) ---
    [Property, FeatureEnabled( "Disco Mode" )] public bool EnableDisco { get; set; } = false;
    
    [Property, Feature( "Disco Mode" )] 
    [Description("EN: Color cycle speed | FR: Vitesse du cycle | ES: Velocidad del ciclo | RU: Скорость цикла")]
    public float DiscoSpeed { get; set; } = 20.0f;
    
    [Property, Feature( "Disco Mode" ), Range( 0, 1 )] 
    [Description("EN: Color purity (0-1) | FR: Pureté de la couleur (0-1) | ES: Pureza del color (0-1) | RU: Насыщенность цвета (0-1)")]
    public float DiscoSaturation { get; set; } = 1.0f;
    
    [Property, Feature( "Disco Mode" ), Range( 0, 1 )] 
    [Description("EN: Max brightness (0-1) | FR: Luminosité max (0-1) | ES: Brillo máx (0-1) | RU: Макс. яркость (0-1)")]
    public float DiscoValue { get; set; } = 1.0f;

    // --- PROXIMITY SENSOR ---
    [Property, FeatureEnabled( "Proximity Sensor" )] public bool EnableSensor { get; set; } = false;
    
    [Property, Feature( "Proximity Sensor" )] 
    [Description("EN: Detection radius | FR: Rayon de détection | ES: Radio de detección | RU: Радиус обнаружения")]
    public float SensorRange { get; set; } = 300.0f;
    
    [Property, Feature( "Proximity Sensor" ), Range( 0, 1 )] 
    [Description("EN: Brightness when far | FR: Luminosité si éloigné | ES: Brillo cuando estás lejos | RU: Яркость вдали")]
    public float SensorMinBrightness { get; set; } = 0.0f;
    
    [Property, Feature( "Proximity Sensor" ), Range( 0, 1 )] 
    [Description("EN: Brightness when near | FR: Luminosité si proche | ES: Brillo cuando estás cerca | RU: Яркость вблизи")]
    public float SensorMaxBrightness { get; set; } = 1.0f;
    
    [Property, Feature( "Proximity Sensor" ), Range( 1, 20 )] 
    [Description("EN: Fade smoothness | FR: Douceur de transition | ES: Suavidad de transición | RU: Плавность перехода")]
    public float SensorSmoothness { get; set; } = 5.0f;
    
    [Property, Feature( "Proximity Sensor" )] 
    [Description("EN: Off when near | FR: S'éteint si proche | ES: Apagar al acercarse | RU: Выключать при приближении")]
    public bool InvertSensor { get; set; } = false;

    // --- MOTION SWAY ---
    [Property, FeatureEnabled( "Motion Sway" )] public bool EnableSway { get; set; } = false;
    
    [Property, Feature( "Motion Sway" )] 
    [Description("EN: X-Axis swing speed | FR: Vitesse balancement X | ES: Velocidad balanceo X | RU: Скорость раскачивания X")]
    public float SwaySpeedPitch { get; set; } = 1.0f;
    
    [Property, Feature( "Motion Sway" )] 
    [Description("EN: X-Axis swing amount | FR: Amplitude balancement X | ES: Amplitud balanceo X | RU: Амплитуда раскачивания X")]
    public float SwayAmountPitch { get; set; } = 5.0f;
    
    [Property, Feature( "Motion Sway" )] 
    [Description("EN: Y-Axis swing speed | FR: Vitesse balancement Y | ES: Velocidad balanceo Y | RU: Скорость раскачивания Y")]
    public float SwaySpeedRoll { get; set; } = 0.7f;
    
    [Property, Feature( "Motion Sway" )] 
    [Description("EN: Y-Axis swing amount | FR: Amplitude balancement Y | ES: Amplitud balanceo Y | RU: Амплитуда раскачивания Y")]
    public float SwayAmountRoll { get; set; } = 3.0f;

    // --- PATTERN (QUAKE STYLE) ---
    [Property, FeatureEnabled( "Flicker Pattern" )] public bool EnablePattern { get; set; } = false;
    
    [Property, Feature( "Flicker Pattern" )] 
    [Description("EN: Sequence (a=0%, m=100%) | FR: Séquence (a=0%, m=100%) | ES: Secuencia (a=0%, m=100%) | RU: Последовательность (a=0%, m=100%)")]
    public string Pattern { get; set; } = "mmnmmommommnonmmonqnmmo";
    
    [Property, Feature( "Flicker Pattern" )] 
    [Description("EN: Sequence reading speed | FR: Vitesse de lecture | ES: Velocidad de lectura | RU: Скорость чтения")]
    public float PatternSpeed { get; set; } = 10.0f;

    // --- PULSE & STROBE ---
    [Property, FeatureEnabled( "Pulse" )] public bool EnablePulse { get; set; } = false;
    
    [Property, Feature( "Pulse" )] 
    [Description("EN: Breathing speed | FR: Vitesse de respiration | ES: Velocidad de respiración | RU: Скорость пульсации")]
    public float PulseSpeed { get; set; } = 1.0f;
    
    [Property, Feature( "Pulse" ), Range( 0, 1 )] 
    [Description("EN: Lowest brightness in pulse | FR: Luminosité minimale | ES: Brillo mínimo | RU: Мин. яркость пульсации")]
    public float PulseMin { get; set; } = 0.2f;
    
    [Property, FeatureEnabled( "Strobe" )] public bool EnableStrobe { get; set; } = false;
    
    [Property, Feature( "Strobe" )] 
    [Description("EN: Flash frequency | FR: Fréquence des flashs | ES: Frecuencia de parpadeo | RU: Частота вспышек")]
    public float StrobeSpeed { get; set; } = 10.0f;
    
    [Property, Feature( "Strobe" ), Range( 0.1f, 0.9f )] 
    [Description("EN: Time ON vs OFF | FR: Ratio Allumé/Éteint | ES: Relación Encendido/Apagado | RU: Отношение Вкл/Выкл")]
    public float StrobeDutyCycle { get; set; } = 0.5f;

    // --- KELVIN ---
    [Property, FeatureEnabled( "Kelvin" )] public bool EnableKelvin { get; set; } = false;
    
    [Property, Feature( "Kelvin" ), Range( 1000, 12000 )] 
    [Description("EN: Color temp (K) | FR: Température de couleur (K) | ES: Temperatura de color (K) | RU: Цветовая температура (K)")]
    public int KelvinTemperature { get; set; } = 4500;

    private PointLight _pointLight;
    private SpotLight _spotLight;
    private Light ActiveLight => (TargetLightType == LightTypeEnum.Point) ? (Light)_pointLight : (Light)_spotLight;

    // Variables internes
    private int _lastKelvin = -1;
    private Color _cachedKelvinColor;
    private Rotation _baseRotation;
    private bool _isInitialized = false;
    private float _brokenMultiplier = 1.0f;
    private float _nextFlicker;
    private float _sensorWeightTarget = 1.0f;
    private float _sensorWeightCurrent = 1.0f;
    private float _randomTimeOffset = 0f;
    private float _creationTime = 0f;
    
    private SoundHandle _ambientSoundHandle;

    protected override void OnStart()
    {
        if ( !AllLights.Contains( this ) ) AllLights.Add( this );
        
        _baseRotation = LocalRotation;
        _creationTime = RealTime.Now;
        
        if ( AutoDesync ) _randomTimeOffset = Game.Random.Float( 0f, 100f );

        _isInitialized = true;
        SyncComponents();
    }

    protected override void OnUpdate()
    {
        if ( !_isInitialized ) return;
        SyncComponents();
        
        var light = ActiveLight;
        if ( light == null ) return;

        bool isPlaying = Game.IsPlaying;
        
        if ( isPlaying && StartDelay > 0 && (RealTime.Now - _creationTime) < StartDelay )
        {
            light.Enabled = false;
            ManageAudio(false, 0);
            return;
        }

        float currentTime = (isPlaying ? Time.Now : RealTime.Now) + _randomTimeOffset;

        var viewerCam = Scene.Camera ?? Scene.GetAllComponents<CameraComponent>().FirstOrDefault();
        float distSq = viewerCam != null ? WorldPosition.DistanceSquared( viewerCam.WorldPosition ) : 0;

        if ( EnableSensor && viewerCam != null )
        {
            float distRatio = Math.Clamp( 1.0f - (MathF.Sqrt(distSq) / SensorRange), 0, 1 );
            float rawWeight = InvertSensor ? 1.0f - distRatio : distRatio;
            _sensorWeightTarget = MathX.Lerp( SensorMinBrightness, SensorMaxBrightness, rawWeight );
        }
        else 
        {
            _sensorWeightTarget = 1.0f;
        }

        _sensorWeightCurrent = MathX.Lerp( _sensorWeightCurrent, _sensorWeightTarget, Time.Delta * SensorSmoothness );

        if ( isPlaying && EnableCulling && viewerCam != null && distSq > MaxDistance * MaxDistance ) 
        { 
            light.Enabled = false; 
            ManageAudio(false, 0);
            return; 
        }

        if ( EnableSway )
        {
            float p = MathF.Sin( currentTime * SwaySpeedPitch ) * SwayAmountPitch;
            float r = MathF.Cos( currentTime * SwaySpeedRoll ) * SwayAmountRoll;
            LocalRotation = _baseRotation * Rotation.From( p, 0, r );
        }

        float fx = 1.0f;
        
        if ( EnableStrobe ) 
        {
            float cycle = (currentTime * StrobeSpeed) % 1.0f;
            fx = (cycle < StrobeDutyCycle) ? 1.0f : 0.0f;
        }
        else if ( EnablePulse ) 
        {
            float sine = (MathF.Sin( currentTime * PulseSpeed * 2.0f ) + 1.0f) / 2.0f;
            fx = MathX.Lerp( PulseMin, 1.0f, sine );
        }

        if ( EnablePattern && !string.IsNullOrEmpty( Pattern ) )
        {
            int index = (int)(currentTime * PatternSpeed) % Pattern.Length;
            float val = Math.Max(0, (char.ToLower(Pattern[index]) - 'a') / 12.0f);
            fx *= val;
        }

        if ( EnableFire ) 
        {
            float noise = MathF.Sin(currentTime * FireSpeed) + MathF.Sin(currentTime * FireSpeed * 0.5f);
            if ( FireChaos > 0 ) noise += MathF.Sin(currentTime * FireSpeed * 1.5f) * FireChaos;
            fx -= (noise * 0.15f * FireIntensity);
        }

        if ( EnableHorror )
        {
            if ( currentTime > _nextFlicker )
            {
                bool isDamaged = Game.Random.Float( 0, 1 ) < DamageSeverity;
                _brokenMultiplier = isDamaged ? Game.Random.Float( 0.0f, 0.4f ) : 1.0f;
                _nextFlicker = currentTime + Game.Random.Float( MinFlickerDelay, MaxFlickerDelay );

                if ( isPlaying && isDamaged && SparkSound != null && _brokenMultiplier < 0.2f )
                {
                    Sound.Play( SparkSound, WorldPosition ); 
                }
            }
            fx *= _brokenMultiplier;
        }

        float finalBrightness = Brightness * fx * _sensorWeightCurrent * (IsEnabled ? 1 : 0);
        bool shouldBeEnabled = finalBrightness > 0.001f;
        
        if ( light.Enabled != shouldBeEnabled ) light.Enabled = shouldBeEnabled;

        Color col = LightColor;
        if ( EnableDisco ) 
        {
            col = new ColorHsv( (currentTime * DiscoSpeed) % 360f, DiscoSaturation, DiscoValue ).ToColor();
        }
        else if ( EnableKelvin )
        {
            if ( KelvinTemperature != _lastKelvin ) { _cachedKelvinColor = KelvinToColor( KelvinTemperature ); _lastKelvin = KelvinTemperature; }
            col = _cachedKelvinColor;
        }

        light.LightColor = col * finalBrightness;
        light.Shadows = CastShadows && (!isPlaying || distSq < ShadowMaxDistance * ShadowMaxDistance);
        light.FogStrength = VolumetricBoost;

        ManageAudio( shouldBeEnabled, finalBrightness / Math.Max(Brightness, 0.01f) );
    }

    private void ManageAudio(bool isLightEnabled, float intensityRatio)
    {
        if ( !Game.IsPlaying || AmbientSound == null ) return;

        if ( isLightEnabled )
        {
            if ( _ambientSoundHandle == null || _ambientSoundHandle.IsStopped ) 
            {
                _ambientSoundHandle = Sound.Play( AmbientSound, WorldPosition );
            }
            
            if ( _ambientSoundHandle != null )
            {
                _ambientSoundHandle.Position = WorldPosition;
                _ambientSoundHandle.Volume = BaseVolume * (ModulateVolumeWithLight ? intensityRatio : 1.0f);
            }
        }
        else if ( _ambientSoundHandle != null )
        {
            _ambientSoundHandle.Stop();
            _ambientSoundHandle = null;
        }
    }

    private void SyncComponents()
    {
        if ( TargetLightType == LightTypeEnum.Point )
        {
            if ( _pointLight == null ) _pointLight = Components.GetOrCreate<PointLight>();
            if ( _spotLight != null && _spotLight.Enabled ) _spotLight.Enabled = false;
        }
        else
        {
            if ( _spotLight == null ) _spotLight = Components.GetOrCreate<SpotLight>();
            if ( _pointLight != null && _pointLight.Enabled ) _pointLight.Enabled = false;
        }
    }

    private Color KelvinToColor( int k )
    {
        float t = k / 100.0f;
        float r, g, b;
        if ( t <= 66 ) r = 255; else r = Math.Clamp( 329.698f * MathF.Pow( t - 60, -0.133f ), 0, 255 );
        if ( t <= 66 ) g = Math.Clamp( 99.47f * MathF.Log( t ) - 161.11f, 0, 255 ); else g = Math.Clamp( 288.12f * MathF.Pow( t - 60, -0.075f ), 0, 255 );
        if ( t >= 66 ) b = 255; else if ( t <= 19 ) b = 0; else b = Math.Clamp( 138.51f * MathF.Log( t - 10 ) - 305.04f, 0, 255 );
        return new Color( r / 255f, g / 255f, b / 255f );
    }

    protected override void DrawGizmos()
    {
        if ( !ShowDebugGizmos ) return;

        if ( EnableSensor )
        {
            Gizmo.Draw.Color = Color.Cyan.WithAlpha( 0.2f );
            Gizmo.Draw.SolidSphere( Vector3.Zero, SensorRange );
            Gizmo.Draw.Color = Color.Cyan;
            Gizmo.Draw.LineSphere( Vector3.Zero, SensorRange );
            Gizmo.Draw.Text( $"Sensor Range ({SensorRange})", new Transform(Vector3.Up * SensorRange), size: 14 );
        }

        if ( EnableCulling )
        {
            Gizmo.Draw.Color = Color.Red.WithAlpha( 0.05f );
            Gizmo.Draw.LineSphere( Vector3.Zero, MaxDistance );
            Gizmo.Draw.Color = Color.Red;
            Gizmo.Draw.Text( $"Max Distance Culling ({MaxDistance})", new Transform(Vector3.Up * (MaxDistance * 0.9f)), size: 14 );
        }
    }

    protected override void OnDestroy() 
    { 
        if ( AllLights.Contains( this ) ) AllLights.Remove( this ); 
        if ( _ambientSoundHandle != null ) _ambientSoundHandle.Stop();
    }
}