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();
}
}