2294 results

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Sandbox;

namespace SFXR;


[Title( "SFXR Component" )]
[Category( "SFXR" )]
[Icon( "volume_up" )]
public sealed class SFXRComponent : Component
{

	/// <summary>
	/// The base Waveform
	/// (Default: Square)
	/// </summary>
	[Property, Group( "Sound" )]
	public Waveform Waveform { get; set; } = Waveform.Square;

	/// <summary>
	/// The sample rate of the sound
	/// </summary>
	[Property, Group( "Sound" )]
	public SampleRate SampleRate { get; set; } = SampleRate.Hz44100;

	/// <summary>
	/// The bit depth of the sound
	/// </summary>
	// [Property, Group( "Sound" )]
	public BitDepth BitDepth { get; set; } = BitDepth.Bit16;

	/// <summary>
	/// The length of the sound in seconds
	/// </summary>
	[Property, Group( "Sound" ), Range( 0f, 20f, 0.01f )]
	public float Length { get; set; } = 0.5f;

	/// <summary>
	/// The volume of the sound
	/// (Default: 0.5)
	/// </summary>
	[Property, Group( "Sound" ), Range( 0f, 1f, 0.01f )]
	public float MasterVolume { get; set; } = 0.5f;

	[Property, Group( "Frequency" ), Range( 0, 3000f, 1f )]
	float StartFrequency
	{
		get => Frequency.Start;
		set => Frequency.Start = value;
	}

	[Property, Group( "Frequency" ), Range( -3000f, 3000f, 1f )]
	float Slide
	{
		get => Frequency.Slide;
		set => Frequency.Slide = value;
	}

	[Property, Group( "Frequency" ), Range( -3000f, 3000f, 1f )]
	float SlideDelta
	{
		get => Frequency.DeltaSlide;
		set => Frequency.DeltaSlide = value;
	}

	/// <summary>
	/// The random seed
	/// </summary>
	[Property, Group( "Controls" )]
	public long Seed { get; set; } = 0;

	[Property, Group( "Controls" )]
	public SFXRControls Controls { get; set; } = new SFXRControls();

	public SFXRFrequency Frequency { get; set; } = new SFXRFrequency();
	Random _random = new Random();

	List<SFXRNote> NotesPlaying = new();

	/// <summary>
	/// Plays the sound defined by the component
	/// </summary>
	/// <returns>The sound handle of the sound. This can be used to change position, pitch, ect</returns>
	public SoundHandle PlaySound()
	{
		var sfx = Generate( (int)(Length * (int)SampleRate) );
		var handle = sfx.Play();
		// DestroyStream(sfx, Length);
		return handle;
	}

	/// <summary>
	/// Plays the sound defined by the component (Via a frequency trigger. This will play indefinitely until released)
	/// </summary>
	/// <param name="frequency">The frequency of the sound</param>
	/// <param name="volume">The volume of the trigger </param>
	public void TriggerNotePress( float frequency, float volume = 1f )
	{
		foreach ( var note in NotesPlaying.Where( x => x.Frequency == frequency ) )
		{
			note.Release();
		}

		var newNote = new SFXRNote( this, frequency, volume );
		newNote.Trigger();
		NotesPlaying.Add( newNote );
	}

	/// <summary>
	/// Releases a note playing at the given frequency
	/// </summary>
	/// <param name="frequency">The frequency of the sound</param>
	public void TriggerNoteRelease( float frequency )
	{
		foreach ( var note in NotesPlaying.Where( x => x.Frequency == frequency ) )
		{
			note.Release();
		}
	}

	/// <summary>
	/// Releases all notes playing
	/// </summary>
	public void TriggerReleaseAll()
	{
		foreach ( var note in NotesPlaying )
		{
			note.Release();
		}
	}

	/// <summary>
	/// Generates a sound stream from the component
	/// </summary>
	/// <param name="sampleCount">How many samples the stream should be filled with</param>
	/// <returns></returns>
	public SoundStream Generate( int sampleCount )
	{
		List<SFXREffect> effects = new();

		foreach ( var component in GameObject.Components.GetAll() )
		{
			if ( component is not SFXREffect effect || !effect.Enabled ) continue;
			effects.Add( effect );
		}

		return Generate( sampleCount, effects );
	}

	/// <summary>
	/// Generates a sound stream from the component with the given effects
	/// </summary>
	/// <param name="sampleCount">The number of samples</param>
	/// <param name="effects">A list of the effects to apply</param>
	/// <returns></returns>
	public SoundStream Generate( int sampleCount, List<SFXREffect> effects )
	{
		short[] samples = new short[sampleCount];

		float t = 0;
		for ( int i = 0; i < sampleCount; i++ )
		{
			t += 1f / (int)SampleRate;
			short sampleValue = SFXR.GetWaveformSample( Waveform, t, Frequency.GetFrequency( t ) );

			sampleValue = (short)((float)sampleValue * MasterVolume);

			samples[i] = sampleValue;
		}

		foreach ( var effect in effects )
		{
			if ( !effect.Enabled ) continue;
			samples = effect.Apply( samples, this );
		}

		var stream = new SoundStream( (int)SampleRate );
		stream.WriteData( samples );

		return stream;
	}

	/// <summary>
	/// Randomizes the component's parameters
	/// </summary>
	public void Randomize()
	{
		if ( Seed != 0 ) _random = new Random( (int)Seed );

		var waveform = Waveform;
		ResetParameters();
		Waveform = waveform;

		Frequency.Start = _random.Next( 10, 3000 );
		if ( _random.Next( 2 ) == 0 ) Frequency.Slide = _random.Next( -3000, 3000 );
		if ( Frequency.Start > 2000 && Frequency.Slide > 200 ) Frequency.Slide = -Frequency.Slide;
		else if ( Frequency.Start < 400 && Frequency.Slide < -50 ) Frequency.Slide = -Frequency.Slide;
		if ( _random.Next( 2 ) == 0 ) Frequency.DeltaSlide = _random.Next( -3000, 3000 );

		SanitizeParameters();
	}

	/// <summary>
	/// Mutates the component's parameters slightly
	/// </summary>
	public void Mutate( float mutation = 0.05f )
	{
		if ( Seed != 0 ) _random = new Random( (int)Seed );

		Frequency.Start += _random.Float( -mutation, mutation ) * 1000;
		if ( Frequency.Start > 2000 && Frequency.Slide > 200 ) Frequency.Slide = -Frequency.Slide;
		else if ( Frequency.Start < 400 && Frequency.Slide < -50 ) Frequency.Slide = -Frequency.Slide;
		Frequency.Slide += _random.Float( -mutation, mutation ) * 1000;
		Frequency.DeltaSlide += _random.Float( -mutation, mutation ) * 1000;
		if ( Frequency.Slide < -3000 ) Frequency.Slide = -3000;
		if ( Frequency.Slide > 3000 ) Frequency.Slide = 3000;

		SanitizeParameters();
	}


	public void RandomizePickup()
	{
		if ( Seed != 0 ) _random = new Random( (int)Seed );
		ResetParameters();
		foreach ( var component in GameObject.Components.GetAll() )
		{
			if ( component is not SFXREffect effect ) continue;
			effect.Enabled = false;
		}
		var envelope = Components.GetOrCreate<SFXREnvelope>();

		Waveform = (Waveform)_random.Int( 0, 2 );

		Frequency.Start = _random.Float( 0.4f, 0.9f ) * 3000;

		envelope.Enabled = true;
		envelope.Attack = 0;
		envelope.Decay = _random.Float( 0.1f, 0.3f );
		envelope.Sustain = _random.Float( 0f, 0.1f );
		envelope.Release = _random.Float( 0.1f, 0.3f );

		Length = envelope.Attack + envelope.Sustain + envelope.Decay + envelope.Release;

	}

	public void RandomizeLaser()
	{
		if ( Seed != 0 ) _random = new Random( (int)Seed );
		ResetParameters();
		foreach ( var component in GameObject.Components.GetAll() )
		{
			if ( component is not SFXREffect effect ) continue;
			effect.Enabled = false;
		}
		var envelope = Components.GetOrCreate<SFXREnvelope>();
		var highpass = Components.GetOrCreate<SFXRHighPass>();

		Waveform = (Waveform)_random.Int( 0, 2 );
		if ( Waveform == Waveform.Sine && _random.Next( 2 ) == 0 ) Waveform = (Waveform)_random.Int( 0, 1 );

		Frequency.Start = _random.Float( 0.6f, 0.75f ) * 3000;
		Frequency.Slide = _random.Float( -0.25f, -0.15f ) * 3000;
		envelope.Enabled = true;
		envelope.Attack = 0;
		envelope.Decay = _random.Float( 0f, 0.4f );
		envelope.Sustain = _random.Float( 0.1f, 0.3f );
		envelope.Release = _random.Float( 0.25f, 0.3f );
		Length = envelope.Attack + envelope.Sustain + envelope.Decay + envelope.Release;

		if ( _random.Next( 2 ) == 0 )
		{
			highpass.Enabled = true;
			highpass.Cutoff = _random.Float( 0f, 0.3f );
		}
	}

	public void RandomizeExplosion()
	{

		if ( Seed != 0 ) _random = new Random( (int)Seed );
		ResetParameters();
		foreach ( var component in GameObject.Components.GetAll() )
		{
			if ( component is not SFXREffect effect ) continue;
			effect.Enabled = false;
		}
		var envelope = Components.GetOrCreate<SFXREnvelope>();
		var vibrato = Components.GetOrCreate<SFXRVibrato>();

		Waveform = Waveform.Noise;

		if ( _random.Next( 2 ) == 0 )
		{
			Frequency.Start = _random.Float( 0.025f, 0.15f ) * 3000;
			Frequency.Slide = _random.Float( -0.1f, -0.01f ) * 3000;
		}
		else
		{
			Frequency.Start = _random.Float( 0.1f, 0.2f ) * 3000;
			Frequency.Slide = _random.Float( -0.6f, 0.6f ) * 3000;
		}

		if ( _random.Next( 4 ) == 0 ) Frequency.Slide = 0;

		envelope.Enabled = true;
		envelope.Attack = 0;
		envelope.Sustain = _random.Float( 0.1f, 0.4f );
		envelope.Release = _random.Float( 0.1f, 0.3f );
		Length = envelope.Attack + envelope.Sustain + envelope.Decay + envelope.Release;


		if ( _random.Next( 2 ) == 0 )
		{
			vibrato.Enabled = true;
			vibrato.Depth = _random.Float( 0f, 0.7f );
			vibrato.Speed = _random.Float( 0f, 60f );
		}
		else
		{
			vibrato.Enabled = false;
		}

		if ( -Frequency.Slide > Frequency.Start )
		{
			Frequency.Slide = -Frequency.Start;
		}
	}

	public void RandomizePowerup()
	{
		if ( Seed != 0 ) _random = new Random( (int)Seed );
		ResetParameters();
		foreach ( var component in GameObject.Components.GetAll() )
		{
			if ( component is not SFXREffect effect ) continue;
			effect.Enabled = false;
		}
		var envelope = Components.GetOrCreate<SFXREnvelope>();
		var vibrato = Components.GetOrCreate<SFXRVibrato>();

		if ( _random.Next( 2 ) == 0 )
		{
			Waveform = Waveform.Sawtooth;
		}

		if ( _random.Next( 2 ) == 0 )
		{
			Frequency.Start = _random.Float( 0.2f, 0.5f ) * 3000;
			Frequency.Slide = _random.Float( 0.1f, 0.5f ) * 3000;
		}
		else
		{
			Frequency.Start = _random.Float( 0.25f, 0.5f ) * 3000;
			Frequency.Slide = _random.Float( 0.05f, 0.25f ) * 3000;
			if ( _random.Next( 2 ) == 0 )
			{
				vibrato.Enabled = true;
				vibrato.Depth = _random.Float( 0, 0.7f );
				vibrato.Speed = _random.Float( 0, 60f );
			}
			else
			{
				vibrato.Enabled = false;
			}
		}

		if ( -Frequency.Slide > Frequency.Start )
		{
			Frequency.Slide = -Frequency.Start;
		}

		envelope.Enabled = true;
		envelope.Attack = 0;
		envelope.Sustain = _random.Float( 0f, 0.4f );
		envelope.Release = _random.Float( 0.1f, 0.5f );
		Length = envelope.Attack + envelope.Sustain + envelope.Decay + envelope.Release;
	}

	public void RandomizeHit()
	{
		if ( Seed != 0 ) _random = new Random( (int)Seed );
		ResetParameters();
		foreach ( var component in GameObject.Components.GetAll() )
		{
			if ( component is not SFXREffect effect ) continue;
			effect.Enabled = false;
		}
		var envelope = Components.GetOrCreate<SFXREnvelope>();
		var highpass = Components.GetOrCreate<SFXRHighPass>();

		Waveform = (Waveform)_random.Int( 0, 3 );
		if ( Waveform == Waveform.Sine )
		{
			Waveform = Waveform.Noise;
		}

		Frequency.Start = _random.Float( 0.1f, 0.5f ) * 3000;
		Frequency.Slide = _random.Float( -0.7f, -0.3f ) * 3000;

		if ( -Frequency.Slide > Frequency.Start )
		{
			Frequency.Slide = -Frequency.Start;
		}

		envelope.Enabled = true;
		envelope.Attack = 0;
		envelope.Decay = 0;
		envelope.Sustain = _random.Float( 0.025f, 0.1f );
		envelope.Release = _random.Float( 0.1f, 0.3f );
		Length = envelope.Attack + envelope.Sustain + envelope.Decay + envelope.Release;

		if ( _random.Next( 2 ) == 0 )
		{
			highpass.Enabled = true;
			highpass.Cutoff = _random.Float( 0f, 0.3f );
		}
		else
		{
			highpass.Enabled = false;
		}
	}

	public void RandomizeJump()
	{
		if ( Seed != 0 ) _random = new Random( (int)Seed );
		ResetParameters();
		foreach ( var component in GameObject.Components.GetAll() )
		{
			if ( component is not SFXREffect effect ) continue;
			effect.Enabled = false;
		}
		var envelope = Components.GetOrCreate<SFXREnvelope>();

		Waveform = Waveform.Square;

		Frequency.Start = _random.Float( 0.3f, 0.6f ) * 3000;
		Frequency.Slide = _random.Float( 0.1f, 0.3f ) * 3000;

		if ( -Frequency.Slide > Frequency.Start )
		{
			Frequency.Slide = -Frequency.Start;
		}

		envelope.Enabled = true;
		envelope.Attack = 0;
		envelope.Sustain = _random.Float( 0.1f, 0.4f );
		envelope.Release = _random.Float( 0.1f, 0.3f );
		Length = envelope.Attack + envelope.Sustain + envelope.Decay + envelope.Release;
	}

	public void RandomizeBlip()
	{
		if ( Seed != 0 ) _random = new Random( (int)Seed );
		ResetParameters();
		foreach ( var component in GameObject.Components.GetAll() )
		{
			if ( component is not SFXREffect effect ) continue;
			effect.Enabled = false;
		}
		var envelope = Components.GetOrCreate<SFXREnvelope>();

		Waveform = Waveform.Square;

		Frequency.Start = _random.Float( 0.2f, 0.6f ) * 3000;

		envelope.Enabled = true;
		envelope.Attack = 0;
		envelope.Decay = _random.Float( 0.1f, 0.2f );
		envelope.Sustain = _random.Float( 0.025f, 0.1f );
		envelope.Release = _random.Float( 0.1f, 0.3f );
		Length = envelope.Attack + envelope.Sustain + envelope.Decay + envelope.Release;
	}


	public void ResetParameters()
	{
		Waveform = Waveform.Square;
		SampleRate = SampleRate.Hz44100;
		BitDepth = BitDepth.Bit16;
		Length = 0.5f;
		MasterVolume = 0.5f;
		Frequency = new SFXRFrequency();
		Controls = new SFXRControls();
	}

	void SanitizeParameters()
	{

	}

	protected override void OnUpdate()
	{
		foreach ( var note in NotesPlaying )
		{
			note.Update();

			// if (!note.IsPlaying)
			// {
			// 	note.DestroyStreams();
			// }
		}

		NotesPlaying.RemoveAll( x => !x.IsPlaying );
	}
}
using System;
using System.Collections.Generic;
using Sandbox;

namespace SFXR;

[Title( "ADSR Envelope" )]
[Category( "SFXR Effects" )]
[Icon( "mail_outline" )]
public class SFXREnvelope : SFXREffect
{
    /// <summary>
    /// Time the sound takes to reach its peak amplitude
    /// (Default: 0)
    /// </summary>
    [Property, Range( 0, 10 )]
    public float Attack { get; set; } = 0;

    /// <summary>
    /// The time taken for the sound to fade to the sustain level
    /// </summary>
    [Property, Range( 0, 10 )]
    public float Decay { get; set; } = 0;

    /// <summary>
    /// The level maintained until release is triggered
    /// (Default: 1)
    /// </summary>
    [Property, Range( 0, 1 )]
    public float Sustain { get; set; } = 1f;

    /// <summary>
    /// The time taken for the sound to fade to zero after the sustain
    /// (Default: 0.3)
    /// </summary>
    [Property, Range( 0, 10 )]
    public float SustainTime { get; set; } = 0.3f;

    /// <summary>
    /// The time taken for the sound to fade to zero after the release
    /// (Default: 0.4)
    /// </summary>
    [Property, Range( 0, 10 )]
    public float Release { get; set; } = 0.4f;

    /// <summary>
    /// Returns the amplitude of the envelope at a given time
    /// </summary>
    /// <param name="time">Time in seconds</param>
    /// <returns>Amplitude of the envelope at the given time</returns>
    public float GetAmplitude( float time )
    {
        return GetCurve().Evaluate( time / GetLength() );
    }

    public override short[] Apply( short[] samples, SFXRComponent sound )
    {
        // Calculate the envelope amplitude for each sample
        for ( int i = 0; i < samples.Length; i++ )
        {
            float t = i / (float)sound.SampleRate;
            float amplitude = GetAmplitude( t );
            samples[i] = (short)(samples[i] * amplitude);
        }

        return samples;
    }

    public float GetLength()
    {
        return Attack + Decay + SustainTime + Release;
    }

    public Curve GetCurve()
    {
        Curve curve = new();

        List<Vector2> points = new();

        // Add the attack curve
        points.Add( new Vector2( 0, 0 ) );
        points.Add( new Vector2( Attack, 1 ) );

        // Add the decay curve
        points.Add( new Vector2( Attack + Decay, Sustain ) );

        // Add the sustain curve
        points.Add( new Vector2( Attack + Decay + SustainTime, Sustain ) );

        // Add the release curve
        points.Add( new Vector2( Attack + Decay + SustainTime + Release, 0 ) );

        // Normalize the curve to 0-1 in the x
        for ( int i = 0; i < points.Count; i++ )
        {
            points[i] = new Vector2( points[i].x / (Attack + Decay + SustainTime + Release), points[i].y );
        }

        // Add the points to the curve
        foreach ( var point in points )
        {
            curve.AddPoint( point.x, point.y );
        }

        return curve;
    }
}
using Sandbox;

public sealed class SceneTrigger : Component, Component.ITriggerListener
{
	[Property] public SceneFile SceneFile { get; set; }
	protected override void OnUpdate()
	{

	}

	void ITriggerListener.OnTriggerEnter(Sandbox.Collider other)
	{
		if (other.GameObject.Parent.Tags.Has("player") || other.GameObject.Tags.Has("boat"))
		{
			Game.ActiveScene.Load(SceneFile);
		}
	}

	void ITriggerListener.OnTriggerExit(Sandbox.Collider other)
	{

	}
}
using System.Collections.Generic;

namespace Sandbox;

/// <summary>
/// How to use the system: 
/// <code>
/// public sealed class ExampleComponent : Component
/// {
///		// Reference to the system.
///		private FixedUpdateInputSystem _fixedInput;
///		
///		protected override void Start()
///		{
///			// Get the reference like this:
///			_fixedInput = Scene.GetSystem&lt;FixedUpdateInputSystem&gt;();
///			
///			base.OnStart();
///		}
///		
///		protected override void OnFixedUpdate()
///		{
///			// Query for input like usual.
///			if( _fixedInput.Pressed("jump") )
///			{
///				Log.Info("Jumped");
///			}
///			
///			base.OnFixedUpdate();
///		}
/// }
/// </code>
/// </summary>
public sealed class FixedUpdateInputSystem : GameObjectSystem
{
	private struct FixedUpdateInputBuffer
	{
		private class State
		{
			public bool Held;
			public bool Pressed;
			public bool Released;
		}

		private Dictionary<string, State> _actionStates;

		public FixedUpdateInputBuffer()
		{
			_actionStates = new Dictionary<string, State>();

			foreach ( var b in Input.GetActions() )
			{
				_actionStates[b.Name.ToLowerInvariant()] = new State();
			}
		}

		/// <summary>
		/// Call from a <see cref="Component.OnUpdate"/> method
		/// to update the states of the actions.
		/// </summary>
		public void OnUpdate()
		{
			foreach ( var (name, state) in _actionStates )
			{
				if ( Input.Down( name ) )
					_actionStates[name].Held = true;

				if ( Input.Pressed( name ) )
					_actionStates[name].Pressed = true;

				if ( Input.Released( name ) )
					_actionStates[name].Released = true;
			}
		}

		/// <summary>
		/// Call from a <see cref="Component.OnFixedUpdate"/>
		/// method to get the <see cref="State.Held"/> state of this action.
		/// </summary>
		/// <param name="action">The action name (case insensitive).</param>
		/// <returns></returns>
		/// 
		public bool Held( string action )
		{
			return _actionStates[action.ToLowerInvariant()].Held;
		}

		/// <summary>
		/// Call from a <see cref="Component.OnFixedUpdate"/>
		/// method to get the <see cref="State.Pressed"/> state of this action.
		/// </summary>
		/// <param name="action">The action name (case insensitive).</param>
		/// <returns></returns>
		public bool Pressed( string action )
		{
			return _actionStates[action.ToLowerInvariant()].Pressed;
		}

		/// <summary>
		/// Call from a <see cref="Component.OnFixedUpdate"/>
		/// method to get the <see cref="State.Pressed"/> state of this action.
		/// </summary>
		/// <param name="action">The action name (case insensitive).</param>
		/// <returns></returns>
		public bool Released( string action )
		{
			return _actionStates[action.ToLowerInvariant()].Released;
		}

		/// <summary>
		/// Call at the end of your <see cref="Component.OnFixedUpdate"/> method
		/// to clear the state of the struct and reset.
		/// </summary>
		public void Clear()
		{
			foreach ( var actionName in _actionStates.Keys )
			{
				_actionStates[actionName].Held = false;
				_actionStates[actionName].Pressed = false;
			}
		}
	}

	private FixedUpdateInputBuffer _buffer;

	public FixedUpdateInputSystem( Scene scene ) : base( scene )
	{
		_buffer = new();
		Listen( Stage.StartUpdate, int.MinValue, OnStartUpdate, "FUIB.OnStartUpdate" );
		Listen( Stage.FinishFixedUpdate, int.MaxValue, OnFinishFixedUpdate, "FUIB.OnFinishFixedUpdate" );
	}

	private void OnStartUpdate()
	{
		_buffer.OnUpdate();
	}

	private void OnFinishFixedUpdate()
	{
		_buffer.Clear();
	}

	/// <summary>
	/// Is the action currently held down?
	/// </summary>
	/// <param name="action">The action name (case insensitive).</param>
	/// <returns></returns>
	/// 
	public bool Held( string action ) => _buffer.Held( action );

	/// <summary>
	/// Was the action pressed?
	/// </summary>
	/// <param name="action">The action name (case insensitive).</param>
	/// <returns></returns>
	public bool Pressed( string action ) => _buffer.Pressed( action );

	/// <summary>
	/// Was the action released?
	/// </summary>
	/// <param name="action">The action name (case insensitive).</param>
	/// <returns></returns>
	public bool Released( string action ) => _buffer.Released( action );
}
using System.Collections.Generic;
using System.Linq;

namespace Sandbox.Events;

/// <summary>
/// Generate an ordering based on a set of first-most and last-most items, and
/// individual constraints between pairs of items. All first-most items will be
/// ordered before all last-most items, and any other items will be put in the
/// middle unless forced to be elsewhere by a constraint.
/// </summary>
internal class SortingHelper
{
	public record struct SortConstraint( int EarlierIndex, int LaterIndex )
	{
		public SortConstraint Complement => new ( LaterIndex, EarlierIndex );
	}

	private readonly int _itemCount;

	private readonly HashSet<SortConstraint> _initialConstraints = new HashSet<SortConstraint>();

	private readonly HashSet<int> _first = new HashSet<int>();
	private readonly HashSet<int> _last = new HashSet<int>();

	public SortingHelper( int itemCount )
	{
		_itemCount = itemCount;
	}

	public void AddConstraint( int earlierIndex, int laterIndex )
	{
		_initialConstraints.Add( new SortConstraint( earlierIndex, laterIndex ) );
	}

	public void AddFirst( int earlierIndex )
	{
		_first.Add( earlierIndex );
	}

	public void AddLast( int laterIndex )
	{
		_last.Add( laterIndex );
	}

	public bool Sort( List<int> result, out SortConstraint invalidConstraint )
	{
		var middle = new HashSet<int>();

		for ( var index = 0; index < _itemCount; ++index )
		{
			if ( !_first.Contains( index ) && !_last.Contains( index ) )
				middle.Add( index );
		}

		var allConstraints = new HashSet<SortConstraint>();
		var newConstraints = new Queue<SortConstraint>();
		var beforeDict = new Dictionary<int, HashSet<int>>();
		var afterDict = new Dictionary<int, HashSet<int>>();

		bool AddWorkingConstraint( int earlierIndex, int laterIndex, out SortConstraint constraint )
		{
			constraint = new SortConstraint( earlierIndex, laterIndex );

			if ( allConstraints.Contains( constraint.Complement ) )
				return false;

			if ( !allConstraints.Add( constraint ) )
				return true;

			newConstraints.Enqueue( constraint );

			if ( !beforeDict.TryGetValue( earlierIndex, out var before ) )
				beforeDict.Add( earlierIndex, before = new HashSet<int>() );

			if ( !afterDict.TryGetValue( laterIndex, out var after ) )
				afterDict.Add( laterIndex, after = new HashSet<int>() );

			before.Add( laterIndex );
			after.Add( earlierIndex );

			return true;
		}

		// Add initial constraints

		foreach ( var initialConstraint in _initialConstraints )
		{
			if ( !AddWorkingConstraint( initialConstraint.EarlierIndex, initialConstraint.LaterIndex, out invalidConstraint ) )
				return false;
		}

		// Everything in _first should be before everything in _last

		foreach ( var earlierIndex in _first )
		{
			foreach ( var laterIndex in _last )
			{
				if ( !AddWorkingConstraint( earlierIndex, laterIndex, out invalidConstraint ) )
					return false;
			}
		}

		// Keep propagating constraints until nothing changes

		while ( newConstraints.TryDequeue( out var nextConstraint ) )
		{
			// if a < b, and b < c, then a < c etc

			if ( beforeDict.TryGetValue( nextConstraint.LaterIndex, out var before ) )
			{
				foreach ( var laterIndex in before )
				{
					if ( !AddWorkingConstraint( nextConstraint.EarlierIndex, laterIndex, out invalidConstraint ) )
						return false;
				}
			}

			if ( afterDict.TryGetValue( nextConstraint.EarlierIndex, out var after ) )
			{
				foreach ( var earlierIndex in after )
				{
					if ( !AddWorkingConstraint( earlierIndex, nextConstraint.LaterIndex, out invalidConstraint ) )
					{
						return false;
					}
				}
			}
		}

		// Now if we have any items that aren't using GroupOrder.First, and haven't
		// determined that they are ordered before another item with GroupOrder.First,
		// we can safely order them after all GroupOrder.First items. And vice versa.

		foreach ( var middleIndex in middle )
		{
			var isBeforeAnyFirst = beforeDict.TryGetValue( middleIndex, out var before )
				&& before.Any( x => _first.Contains( x ) );

			var isAfterAnyLast = afterDict.TryGetValue( middleIndex, out var after )
				&& after.Any( x => _last.Contains( x ) );

			if ( !isBeforeAnyFirst )
			{
				foreach ( var earlierIndex in _first )
					AddWorkingConstraint( earlierIndex, middleIndex, out invalidConstraint );
			}

			if ( !isAfterAnyLast )
			{
				foreach ( var laterIndex in _last )
					AddWorkingConstraint( middleIndex, laterIndex, out invalidConstraint );
			}
		}

		// Now lets add items to the final ordering if all items that should be sorted
		// before them are already added to that ordering. We'll implement this by choosing
		// items that have an empty list / don't appear in afterDict, and update that
		// dictionary as we go.

		var earliestRemaining = new Queue<int>();

		// First, seed the queue with everything that's already not ordered after anything

		for ( var index = 0; index < _itemCount; ++index )
		{
			if ( !afterDict.ContainsKey( index ) )
			{
				earliestRemaining.Enqueue( index );
			}
		}

		result.Clear();

		while ( earliestRemaining.TryDequeue( out var nextIndex ) )
		{
			result.Add( nextIndex );

			foreach ( var laterIndex in beforeDict.TryGetValue( nextIndex, out var laterIndices )
				? laterIndices : Enumerable.Empty<int>() )
			{
				var beforeLater = afterDict[laterIndex];
				beforeLater.Remove( nextIndex );

				if ( beforeLater.Count == 0 )
					earliestRemaining.Enqueue( laterIndex );
			}
		}

		invalidConstraint = default;
		return result.Count == _itemCount;
	}
}
using Sandbox;
using System.Collections.Generic;

namespace EZCameraShake
{
    public class CameraShaker : Component
    {
        /// <summary>
        /// The single instance of the CameraShaker in the current scene. Do not use if you have multiple instances.
        /// </summary>
        public static CameraShaker Instance;
        static Dictionary<string, CameraShaker> instanceList = new Dictionary<string, CameraShaker>();

        /// <summary>
        /// The default position influcence of all shakes created by this shaker.
        /// </summary>
        [Property] public Vector3 DefaultPosInfluence = new Vector3(0.15f, 0.15f, 0.15f);
		/// <summary>
		/// The default rotation influcence of all shakes created by this shaker.
		/// </summary>
		[Property]  public Vector3 DefaultRotInfluence = new Vector3(1, 1, 1);
		/// <summary>
		/// Offset that will be applied to the camera's default (0,0,0) rest position
		/// </summary>
		[Property] public Vector3 RestPositionOffset = new Vector3(0, 0, 0);
		/// <summary>
		/// Offset that will be applied to the camera's default (0,0,0) rest rotation
		/// </summary>
		[Property] public Vector3 RestRotationOffset = new Vector3(0, 0, 0);

        Vector3 posAddShake, rotAddShake;

        List<CameraShakeInstance> cameraShakeInstances = new List<CameraShakeInstance>();

        protected override void OnAwake()
        {
            Instance = this;
            instanceList.Add(GameObject.Name, this);
        }

		protected override void OnUpdate()
        {
            posAddShake = Vector3.Zero;
            rotAddShake = Vector3.Zero;

            for (int i = 0; i < cameraShakeInstances.Count; i++)
            {
                if (i >= cameraShakeInstances.Count)
                    break;

                CameraShakeInstance c = cameraShakeInstances[i];

                if (c.CurrentState == CameraShakeState.Inactive && c.DeleteOnInactive)
                {
                    cameraShakeInstances.RemoveAt(i);
                    i--;
                }
                else if (c.CurrentState != CameraShakeState.Inactive)
                {
                    posAddShake += CameraUtilities.MultiplyVectors(c.UpdateShake(), c.PositionInfluence);
                    rotAddShake += CameraUtilities.MultiplyVectors(c.UpdateShake(), c.RotationInfluence);
                }
            }

            Transform.LocalPosition = (posAddShake) + RestPositionOffset;
			Vector3 thing = (rotAddShake / 100) + RestRotationOffset;

			Transform.LocalRotation = new Angles(thing.x, thing.y, thing.z);
        }

        /// <summary>
        /// Gets the CameraShaker with the given name, if it exists.
        /// </summary>
        /// <param name="name">The name of the camera shaker instance.</param>
        /// <returns></returns>
        public static CameraShaker GetInstance(string name)
        {
            CameraShaker c;

            if (instanceList.TryGetValue(name, out c))
                return c;

            Log.Error("CameraShake " + name + " not found!");

            return null;
        }

        /// <summary>
        /// Starts a shake using the given preset.
        /// </summary>
        /// <param name="shake">The preset to use.</param>
        /// <returns>A CameraShakeInstance that can be used to alter the shake's properties.</returns>
        public CameraShakeInstance Shake(CameraShakeInstance shake)
        {
            cameraShakeInstances.Add(shake);
            return shake;
        }

        /// <summary>
        /// Shake the camera once, fading in and out  over a specified durations.
        /// </summary>
        /// <param name="magnitude">The intensity of the shake.</param>
        /// <param name="roughness">Roughness of the shake. Lower values are smoother, higher values are more jarring.</param>
        /// <param name="fadeInTime">How long to fade in the shake, in seconds.</param>
        /// <param name="fadeOutTime">How long to fade out the shake, in seconds.</param>
        /// <returns>A CameraShakeInstance that can be used to alter the shake's properties.</returns>
        public CameraShakeInstance ShakeOnce(float magnitude, float roughness, float fadeInTime, float fadeOutTime)
        {
            CameraShakeInstance shake = new CameraShakeInstance(magnitude, roughness, fadeInTime, fadeOutTime);
            shake.PositionInfluence = DefaultPosInfluence;
            shake.RotationInfluence = DefaultRotInfluence;
            cameraShakeInstances.Add(shake);

            return shake;
        }

        /// <summary>
        /// Shake the camera once, fading in and out over a specified durations.
        /// </summary>
        /// <param name="magnitude">The intensity of the shake.</param>
        /// <param name="roughness">Roughness of the shake. Lower values are smoother, higher values are more jarring.</param>
        /// <param name="fadeInTime">How long to fade in the shake, in seconds.</param>
        /// <param name="fadeOutTime">How long to fade out the shake, in seconds.</param>
        /// <param name="posInfluence">How much this shake influences position.</param>
        /// <param name="rotInfluence">How much this shake influences rotation.</param>
        /// <returns>A CameraShakeInstance that can be used to alter the shake's properties.</returns>
        public CameraShakeInstance ShakeOnce(float magnitude, float roughness, float fadeInTime, float fadeOutTime, Vector3 posInfluence, Vector3 rotInfluence)
        {
            CameraShakeInstance shake = new CameraShakeInstance(magnitude, roughness, fadeInTime, fadeOutTime);
            shake.PositionInfluence = posInfluence;
            shake.RotationInfluence = rotInfluence;
            cameraShakeInstances.Add(shake);

            return shake;
        }

        /// <summary>
        /// Start shaking the camera.
        /// </summary>
        /// <param name="magnitude">The intensity of the shake.</param>
        /// <param name="roughness">Roughness of the shake. Lower values are smoother, higher values are more jarring.</param>
        /// <param name="fadeInTime">How long to fade in the shake, in seconds.</param>
        /// <returns>A CameraShakeInstance that can be used to alter the shake's properties.</returns>
        public CameraShakeInstance StartShake(float magnitude, float roughness, float fadeInTime)
        {
            CameraShakeInstance shake = new CameraShakeInstance(magnitude, roughness);
            shake.PositionInfluence = DefaultPosInfluence;
            shake.RotationInfluence = DefaultRotInfluence;
            shake.StartFadeIn(fadeInTime);
            cameraShakeInstances.Add(shake);
            return shake;
        }

        /// <summary>
        /// Start shaking the camera.
        /// </summary>
        /// <param name="magnitude">The intensity of the shake.</param>
        /// <param name="roughness">Roughness of the shake. Lower values are smoother, higher values are more jarring.</param>
        /// <param name="fadeInTime">How long to fade in the shake, in seconds.</param>
        /// <param name="posInfluence">How much this shake influences position.</param>
        /// <param name="rotInfluence">How much this shake influences rotation.</param>
        /// <returns>A CameraShakeInstance that can be used to alter the shake's properties.</returns>
        public CameraShakeInstance StartShake(float magnitude, float roughness, float fadeInTime, Vector3 posInfluence, Vector3 rotInfluence)
        {
            CameraShakeInstance shake = new CameraShakeInstance(magnitude, roughness);
            shake.PositionInfluence = posInfluence;
            shake.RotationInfluence = rotInfluence;
            shake.StartFadeIn(fadeInTime);
            cameraShakeInstances.Add(shake);
            return shake;
        }

        /// <summary>
        /// Gets a copy of the list of current camera shake instances.
        /// </summary>
        public List<CameraShakeInstance> ShakeInstances
        { get { return new List<CameraShakeInstance>(cameraShakeInstances); } }

		protected override void OnDestroy()
        {
            instanceList.Remove(GameObject.Name);
        }
    }
}
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;

namespace Sandbox.Events;

/// <summary>
/// Interface for event payloads that can be listened for by <see cref="IGameEventHandler{T}"/>s.
/// </summary>
public interface IGameEvent { }

/// <summary>
/// Interface for components that handle game events with a payload of type <see cref="T"/>.
/// </summary>
/// <typeparam name="T">Event payload type.</typeparam>
public interface IGameEventHandler<in T>
	where T : IGameEvent
{
	/// <summary>
	/// Called when an event with payload of type <see cref="T"/> is dispatched on a <see cref="GameObject"/>
	/// that contains this component, including on a descendant.
	/// </summary>
	/// <param name="eventArgs">Event payload.</param>
	void OnGameEvent( T eventArgs );
}

/// <summary>
/// Helper for dispatching game events in a scene.
/// </summary>
public static class GameEvent
{
	private static Dictionary<Type, IReadOnlyDictionary<Type, int>> HandlerOrderingCache { get; } = new();

	/// <summary>
	/// Notifies all <see cref="IGameEventHandler{T}"/> components that are within <paramref name="root"/>,
	/// with a payload of type <typeparamref name="T"/>.
	/// </summary>
	public static void Dispatch<T>( this GameObject root, T eventArgs )
		where T : IGameEvent
	{
		var handlers = (root is Scene scene
			? scene.GetAllComponents<IGameEventHandler<T>>() // I think this is more efficient?
			: root.Components.GetAll<IGameEventHandler<T>>())
			.ToArray();

		if ( !HandlerOrderingCache.TryGetValue( typeof(T), out var ordering ) || handlers.Any( x => !ordering.ContainsKey( x.GetType() ) ) )
		{
			ordering = HandlerOrderingCache[typeof(T)] = GetHandlerOrdering<T>();
		}

		List<Exception>? exceptions = null;

		foreach ( var handler in handlers.OrderBy( x => ordering[x.GetType()] ) )
		{
			try
			{
				handler.OnGameEvent( eventArgs );
			}
			catch ( Exception e )
			{
				exceptions ??= new();
				exceptions.Add( e );
			}
		}

		switch ( exceptions?.Count )
		{
			case 1:
				Log.Error( exceptions[0] );
				break;

			case > 1:
				Log.Error( new AggregateException( exceptions ) );
				break;
		}
	}

	private static bool IsImplementingMethodName( string methodName )
	{
		if ( methodName == nameof(IGameEventHandler<IGameEvent>.OnGameEvent) )
		{
			return true;
		}

		return methodName.StartsWith( "Sandbox.Events.IGameEventHandler<" ) && methodName.EndsWith( ">.OnGameEvent" );
	}

	private static MethodDescription? GetImplementation<T>( TypeDescription type )
	{
		foreach ( var method in type.Methods )
		{
			if ( method.IsStatic ) continue;
			if ( method.Parameters.Length != 1 ) continue;
			if ( method.Parameters[0].ParameterType != typeof( T ) ) continue;

			if ( !IsImplementingMethodName( method.Name ) ) continue;

			return method;
		}

		return null;
	}

	private static IReadOnlyDictionary<Type, int> GetHandlerOrdering<T>()
		where T : IGameEvent
	{
		var types = TypeLibrary.GetTypes<IGameEventHandler<T>>().ToArray();
		var helper = new SortingHelper( types.Length );

		for ( var i = 0; i < types.Length; ++i )
		{
			var type = types[i];
			var method = GetImplementation<T>( type );

			if ( method is null )
			{
				Log.Warning( $"Can't find {nameof( IGameEventHandler<T> )}<{typeof( T ).Name}> implementation in {type.Name}!" );
				continue;
			}

			foreach ( var attrib in method.Attributes )
			{
				switch ( attrib )
				{
					case EarlyAttribute:
						helper.AddFirst( i );
						break;

					case LateAttribute:
						helper.AddLast( i );
						break;

					case IBeforeAttribute before:
						for ( var j = 0; j < types.Length; ++j )
						{
							if ( i == j ) continue;

							var other = types[j];

							if ( before.Type.IsAssignableFrom( other.TargetType ) )
							{
								helper.AddConstraint( i, j );
							}
						}

						break;

					case IAfterAttribute after:
						for ( var j = 0; j < types.Length; ++j )
						{
							if ( i == j ) continue;

							var other = types[j];

							if ( after.Type.IsAssignableFrom( other.TargetType ) )
							{
								helper.AddConstraint( j, i );
							}
						}

						break;
				}
			}
		}

		var ordering = new List<int>();

		if ( !helper.Sort( ordering, out var invalid ) )
		{
			Log.Error( $"Invalid event ordering constraint between {types[invalid.EarlierIndex].Name} and {types[invalid.LaterIndex].Name}!" );
			return ImmutableDictionary<Type, int>.Empty;
		}

		return Enumerable.Range( 0, ordering.Count )
			.ToImmutableDictionary( i => types[ordering[i]].TargetType, i => i );
	}
}

public delegate void GameEventAction<in T>( T eventArgs )
	where T : IGameEvent;

/// <summary>
/// Base class for components that expose game events to Action Graph.
/// </summary>
public abstract class GameEventComponent<T> : Component, IGameEventHandler<T>
	where T : IGameEvent
{
	/// <summary>
	/// Action invoked when the <typeparamref name="T"/> event is dispatched.
	/// </summary>
	[Property]
	public GameEventAction<T>? OnEvent { get; set; }

	/// <summary>
	/// If this component is within a state machine, optional state to transition
	/// to when this event is dispatched.
	/// </summary>
	[Property]
	public StateComponent? NextState { get; set; }

	void IGameEventHandler<T>.OnGameEvent( T eventArgs )
	{
		OnEvent?.Invoke( eventArgs );

		if ( NextState is not null )
		{
			Components.GetInAncestorsOrSelf<StateMachineComponent>()?.Transition( NextState );
		}
	}
}
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using Sandbox;
using System.Threading.Tasks;

public sealed class WebSocketUtility : Component
{
	[Property] public List<WebsocketTools> websocketToolsList { get; set; }
	protected override void OnAwake()
	{
		foreach ( var websocketTools in websocketToolsList )
		{
			if ( websocketTools.url is null )
			{
				Log.Error( "WebsocketTools URL is null" );
				return;
			}
			websocketTools.webSocket = new WebSocket();
			ConnectToSocket( websocketTools.webSocket, websocketTools.url );
			websocketTools.isConnected = true;
			websocketTools.webSocket.OnMessageReceived += websocketTools.OnMessageReceivedMethod;
			websocketTools.isSubscribed = true;
		}
	}

	protected override void OnUpdate()
	{
		SendMessageFromList( WebsocketTools.Fetch.OnUpdate );
	}
	protected override void OnFixedUpdate()
	{
		SendMessageFromList( WebsocketTools.Fetch.OnFixedUpdate );
	}
	protected override void OnStart()
	{
		SendMessageFromList( WebsocketTools.Fetch.OnStart );
	}

	private async void SendMessageFromList( WebsocketTools.Fetch fetch )
	{
		foreach ( var websocketTools in websocketToolsList )
		{
			if ( websocketTools.fetch == fetch )
			{
				if ( websocketTools.message.UseJsonTags )
				{
					var jsonStrings = websocketTools.message.jsonTags.Select( tag => Json.Serialize( tag.ToString() ) );

					var bigString = string.Join( "", jsonStrings );

					var finalJsonString = Json.Serialize( bigString );

					await websocketTools.webSocket.Send( finalJsonString );
				}
				else
				{
					var messageBytes = Encoding.UTF8.GetBytes( websocketTools.message.message );
					await websocketTools.webSocket.Send( messageBytes );
				}
			}
		}
	}

	[Description( "Sends a message over a websocket connection" )]
	public static async Task SendAsync( WebsocketTools websocketTools )
	{
		if ( websocketTools.webSocket is null )
		{
			websocketTools.webSocket = new WebSocket();
		}
		if ( !websocketTools.isConnected )
		{
			await websocketTools.webSocket.Connect( websocketTools.url );
			websocketTools.isConnected = true;
		}

        if ( websocketTools.message.UseJsonTags )
            await websocketTools.webSocket.Send( Json.Serialize( websocketTools.message.jsonTags ) );
        else
		    await websocketTools.webSocket.Send( websocketTools.message.message );

		if ( !websocketTools.isSubscribed )
		{
			websocketTools.webSocket.OnMessageReceived += websocketTools.OnMessageReceivedMethod;
			websocketTools.isSubscribed = true;
		}
	}

	public static async Task SendStringAsync( string url, string message )
	{
		var webSocket = new WebSocket();
		await webSocket.Connect( url );
		await webSocket.Send( message );
	}

	public static void ChangeJsonTagValue( WebsocketMessage message, string tag, string value )
	{
         if ( message is null )
            message = new WebsocketMessage();

		if ( message.jsonTags is null )
			message.jsonTags = new List<JsonTags>();

		var jsonTag = message.jsonTags.Find( x => x.tag == tag );
		if ( jsonTag is null )
		{
			Log.Warning( $"Tag {tag} not found in message" );
		}
		else
		{
			jsonTag.value = value;
		}
	}

	public static void AddJsonTag( WebsocketMessage message, string tag, string value )
	{
        if ( message is null )
            message = new WebsocketMessage();
		
		if ( message.jsonTags is null )
			message.jsonTags = new List<JsonTags>();

		var jsonTag = new JsonTags
		{
			tag = tag,
			value = value
		};
		message.jsonTags.Add( jsonTag );
	}

	private async void ConnectToSocket( WebSocket webSocket, string url )
	{
		await webSocket.Connect( url );
	}

	[ActionGraphNode( "new websocket tools" ), Pure]
	public static WebsocketTools NewWebsocketTools()
	{
		return new WebsocketTools();
	}
}

public class WebsocketTools
{
	public delegate void OnMessageReceived( string message );
	public OnMessageReceived onMessageReceived { get; set; }
	public WebSocket webSocket { get; set; }
	public string url { get; set; }
	public WebsocketMessage message { get; set; } = new();
	public bool isConnected { get; set; }
	public bool isSubscribed { get; set; }
	public string returnMessage { get; set; }
	public enum Fetch
	{
		OnUpdate,
		OnFixedUpdate,
		OnStart,
	}
	public Fetch fetch { get; set; }

	public void OnMessageReceivedMethod( string message )
	{
		onMessageReceived?.Invoke( message );
		returnMessage = message;
	}

	public WebsocketTools()
	{
		url = "ws://localhost:8080";
		fetch = Fetch.OnUpdate;
		onMessageReceived = null;
		message = null;
	}

	public WebsocketTools( string url, OnMessageReceived onMessageReceived, WebsocketMessage message, Fetch fetch = Fetch.OnUpdate )
	{
		this.url = url;
		this.fetch = fetch;
		this.onMessageReceived = onMessageReceived;
		this.message = message;
	}

}
[GameResource( "Message", "message", "A message to be sent over a websocket connection", Icon = "chat_bubble" )]
public class WebsocketMessage : GameResource
{
	public bool UseJsonTags { get; set; }
	[ShowIf( "UseJsonTags", false )] public string message { get; set; } = "";
	[ShowIf( "UseJsonTags", true )] public List<JsonTags> jsonTags { get; set; } = new();
}

public class JsonTags
{
	public string tag { get; set; }
	public string value { get; set; }

}

public sealed class PlayerPusher : Component
{
	[Property] public float Radius { get; set; } = 100;

	protected override void DrawGizmos()
	{
		base.DrawGizmos();

		Gizmo.Draw.LineSphere( Vector3.Zero, Radius );
	}

	public static Vector3 GetPushVector( in Vector3 position, Scene scene, GameObject ignore )
	{
		Vector3 vec = default;

		foreach ( var pusher in scene.GetAllComponents<PlayerPusher>() )
		{
			if ( pusher.GameObject.IsAncestor( ignore ) )
				continue;

			pusher.Collect( position, ref vec );
		}

		return vec;
	}

	private void Collect( Vector3 position, ref Vector3 output )
	{
		var delta = (position - Transform.Position);
		if ( delta.Length > Radius ) return;

		delta.z = 0; // ignore z

		var distanceDelta = (delta.Length / Radius);

		output += delta.Normal * (1.0f - distanceDelta);
	}
}

using System;

namespace Sandbox.Events;

/// <summary>
/// Only valid on <see cref="IGameEventHandler{T}.OnGameEvent"/> implementations. Forces this
/// event handler to be invoked before any handlers not marked as early, except if more specific
/// constraints are given (i.e., <see cref="BeforeAttribute{T}"/>, <see cref="AfterAttribute{T}"/>).
/// </summary>
[AttributeUsage( AttributeTargets.Method )]
public sealed class EarlyAttribute : Attribute
{

}

/// <summary>
/// Only valid on <see cref="IGameEventHandler{T}.OnGameEvent"/> implementations. Forces this
/// event handler to be invoked after any handlers not marked as late, except if more specific
/// constraints are given (i.e., <see cref="BeforeAttribute{T}"/>, <see cref="AfterAttribute{T}"/>).
/// </summary>
[AttributeUsage( AttributeTargets.Method )]
public sealed class LateAttribute : Attribute
{

}

internal interface IBeforeAttribute
{
	Type Type { get; }
}

internal interface IAfterAttribute
{
	Type Type { get; }
}

/// <summary>
/// Only valid on <see cref="IGameEventHandler{T}.OnGameEvent"/> implementations. Forces this
/// event handler to be invoked before any handlers in the specified type.
/// </summary>
[AttributeUsage( AttributeTargets.Method, AllowMultiple = true )]
public sealed class BeforeAttribute<T> : Attribute, IBeforeAttribute
{
	Type IBeforeAttribute.Type => typeof(T);
}

/// <summary>
/// Only valid on <see cref="IGameEventHandler{T}.OnGameEvent"/> implementations. Forces this
/// event handler to be invoked after any handlers in the specified type.
/// </summary>
[AttributeUsage( AttributeTargets.Method, AllowMultiple = true )]
public sealed class AfterAttribute<T> : Attribute, IAfterAttribute
{
	Type IAfterAttribute.Type => typeof( T );
}
using System.Collections.Generic;
using System.Linq;

namespace Sandbox.Events;

/// <summary>
/// Generate an ordering based on a set of first-most and last-most items, and
/// individual constraints between pairs of items. All first-most items will be
/// ordered before all last-most items, and any other items will be put in the
/// middle unless forced to be elsewhere by a constraint.
/// </summary>
internal class SortingHelper
{
	public record struct SortConstraint( int EarlierIndex, int LaterIndex )
	{
		public SortConstraint Complement => new ( LaterIndex, EarlierIndex );
	}

	private readonly int _itemCount;

	private readonly HashSet<SortConstraint> _initialConstraints = new HashSet<SortConstraint>();

	private readonly HashSet<int> _first = new HashSet<int>();
	private readonly HashSet<int> _last = new HashSet<int>();

	public SortingHelper( int itemCount )
	{
		_itemCount = itemCount;
	}

	public void AddConstraint( int earlierIndex, int laterIndex )
	{
		_initialConstraints.Add( new SortConstraint( earlierIndex, laterIndex ) );
	}

	public void AddFirst( int earlierIndex )
	{
		_first.Add( earlierIndex );
	}

	public void AddLast( int laterIndex )
	{
		_last.Add( laterIndex );
	}

	public bool Sort( List<int> result, out SortConstraint invalidConstraint )
	{
		var middle = new HashSet<int>();

		for ( var index = 0; index < _itemCount; ++index )
		{
			if ( !_first.Contains( index ) && !_last.Contains( index ) )
				middle.Add( index );
		}

		var allConstraints = new HashSet<SortConstraint>();
		var newConstraints = new Queue<SortConstraint>();
		var beforeDict = new Dictionary<int, HashSet<int>>();
		var afterDict = new Dictionary<int, HashSet<int>>();

		bool AddWorkingConstraint( int earlierIndex, int laterIndex, out SortConstraint constraint )
		{
			constraint = new SortConstraint( earlierIndex, laterIndex );

			if ( allConstraints.Contains( constraint.Complement ) )
				return false;

			if ( !allConstraints.Add( constraint ) )
				return true;

			newConstraints.Enqueue( constraint );

			if ( !beforeDict.TryGetValue( earlierIndex, out var before ) )
				beforeDict.Add( earlierIndex, before = new HashSet<int>() );

			if ( !afterDict.TryGetValue( laterIndex, out var after ) )
				afterDict.Add( laterIndex, after = new HashSet<int>() );

			before.Add( laterIndex );
			after.Add( earlierIndex );

			return true;
		}

		// Add initial constraints

		foreach ( var initialConstraint in _initialConstraints )
		{
			if ( !AddWorkingConstraint( initialConstraint.EarlierIndex, initialConstraint.LaterIndex, out invalidConstraint ) )
				return false;
		}

		// Everything in _first should be before everything in _last

		foreach ( var earlierIndex in _first )
		{
			foreach ( var laterIndex in _last )
			{
				if ( !AddWorkingConstraint( earlierIndex, laterIndex, out invalidConstraint ) )
					return false;
			}
		}

		// Keep propagating constraints until nothing changes

		while ( newConstraints.TryDequeue( out var nextConstraint ) )
		{
			// if a < b, and b < c, then a < c etc

			if ( beforeDict.TryGetValue( nextConstraint.LaterIndex, out var before ) )
			{
				foreach ( var laterIndex in before )
				{
					if ( !AddWorkingConstraint( nextConstraint.EarlierIndex, laterIndex, out invalidConstraint ) )
						return false;
				}
			}

			if ( afterDict.TryGetValue( nextConstraint.EarlierIndex, out var after ) )
			{
				foreach ( var earlierIndex in after )
				{
					if ( !AddWorkingConstraint( earlierIndex, nextConstraint.LaterIndex, out invalidConstraint ) )
					{
						return false;
					}
				}
			}
		}

		// Now if we have any items that aren't using GroupOrder.First, and haven't
		// determined that they are ordered before another item with GroupOrder.First,
		// we can safely order them after all GroupOrder.First items. And vice versa.

		foreach ( var middleIndex in middle )
		{
			var isBeforeAnyFirst = beforeDict.TryGetValue( middleIndex, out var before )
				&& before.Any( x => _first.Contains( x ) );

			var isAfterAnyLast = afterDict.TryGetValue( middleIndex, out var after )
				&& after.Any( x => _last.Contains( x ) );

			if ( !isBeforeAnyFirst )
			{
				foreach ( var earlierIndex in _first )
					AddWorkingConstraint( earlierIndex, middleIndex, out invalidConstraint );
			}

			if ( !isAfterAnyLast )
			{
				foreach ( var laterIndex in _last )
					AddWorkingConstraint( middleIndex, laterIndex, out invalidConstraint );
			}
		}

		// Now lets add items to the final ordering if all items that should be sorted
		// before them are already added to that ordering. We'll implement this by choosing
		// items that have an empty list / don't appear in afterDict, and update that
		// dictionary as we go.

		var earliestRemaining = new Queue<int>();

		// First, seed the queue with everything that's already not ordered after anything

		for ( var index = 0; index < _itemCount; ++index )
		{
			if ( !afterDict.ContainsKey( index ) )
			{
				earliestRemaining.Enqueue( index );
			}
		}

		result.Clear();

		while ( earliestRemaining.TryDequeue( out var nextIndex ) )
		{
			result.Add( nextIndex );

			foreach ( var laterIndex in beforeDict.TryGetValue( nextIndex, out var laterIndices )
				? laterIndices : Enumerable.Empty<int>() )
			{
				var beforeLater = afterDict[laterIndex];
				beforeLater.Remove( nextIndex );

				if ( beforeLater.Count == 0 )
					earliestRemaining.Enqueue( laterIndex );
			}
		}

		invalidConstraint = default;
		return result.Count == _itemCount;
	}
}
public sealed class JiggleBone : TransformProxyComponent
{
	JiggleBoneState state = new JiggleBoneState();

	[Property]
	public Vector3 StartPoint = new Vector3( 0, 0, 0 );

	[Property]
	public Vector3 EndPoint = new Vector3( 32, 0, 0 );

	[Property, Range( 0, 2 )]
	public float Speed { get; set; } = 1.0f;

	[Property, Range( 0, 2 )]
	public float Stiffness { get; set; } = 1.0f;

	[Property, Range( 0, 2 )]
	public float Damping { get; set; } = 1.0f;

	[Property, Range( 0, 100 )]
	public float Radius { get; set; } = 40.0f;

	[Property, Range( 0, 100 )]
	public float Mass { get; set; } = 1.0f;

	Transform LocalJigglePosition;

	protected override void OnEnabled()
	{
		LocalJigglePosition = Transform.Local;

		base.OnEnabled();

		state = new JiggleBoneState();
	}

	protected override void OnUpdate()
	{
		var oldPos = LocalJigglePosition;



		using ( Transform.DisableProxy() )
		{
			var worldTx = Transform.World;

			var startPoint = worldTx.PointToWorld( StartPoint );
			var endPoint = worldTx.PointToWorld( EndPoint );

			//Gizmo.Draw.LineSphere( startPoint, 1 );
			//Gizmo.Draw.LineSphere( endPoint, 1 );

			state.Extent = (endPoint - startPoint);
			state.Stiffness = Stiffness;
			state.Damping = Damping;
			state.Radius = Radius;
			state.Mass = Mass;

			state.Update( startPoint, Time.Delta * Speed * 16.0f );

			var tx = worldTx.RotateAround( startPoint, state.Rotation );
			LocalJigglePosition = GameObject.Parent.Transform.World.ToLocal( tx );
		}

		if ( oldPos != LocalJigglePosition )
		{
			MarkTransformChanged();
		}
	}

	protected override void DrawGizmos()
	{
		base.DrawGizmos();

		if ( !Gizmo.IsSelected )
			return;

		using ( Transform.DisableProxy() )
		{
			Gizmo.Transform = Transform.World;
			Gizmo.Draw.IgnoreDepth = false;
			Gizmo.Draw.Color = Gizmo.Colors.Yaw.WithAlpha( 0.5f );
			Gizmo.Draw.Line( StartPoint, EndPoint );
			Gizmo.Draw.LineBBox( BBox.FromPositionAndSize( StartPoint, 5 ) );
			Gizmo.Draw.LineBBox( BBox.FromPositionAndSize( EndPoint, 5 ) );
			Gizmo.Draw.LineSphere( EndPoint, Radius * 2.0f, 4 );
		}
	}

	public override Transform GetLocalTransform()
	{
		return LocalJigglePosition;
	}
}

class JiggleBoneState
{
	public Vector3 Extent = new Vector3( 32, 0, 0 );

	public Vector3 Position { get; set; }
	public Rotation Rotation { get; set; }
	public float Stiffness { get; set; } = 1.0f;
	public float Damping { get; set; } = 1.0f;
	public float Radius { get; set; } = 10.0f;
	public float Gravity { get; set; } = 1.0f;
	public float Mass { get; set; } = 1.0f;


	Vector3 basePosition;
	Vector3 velocity;

	public JiggleBoneState()
	{

	}

	internal void Update( Vector3 position, float timeDelta )
	{
		basePosition = position + Extent;

		// initialization
		if ( Position == default )
		{
			Position = basePosition;
		}

		// Calculate spring force based on displacement from the cube
		Vector3 displacement = Position - basePosition;
		Vector3 springForce = -Stiffness * displacement;

		// Calculate acceleration (Newton's second law)
		Vector3 acceleration = springForce / Mass;

		// Update velocity (integrate acceleration)
		velocity += acceleration * timeDelta;

		// Apply exponential damping
		velocity *= (float)Math.Exp( -Damping * timeDelta );

		// Update position (integrate velocity)
		Position += velocity * timeDelta;

		{
			var diff = Position - basePosition;
			var diffLen = diff.Length;
			if ( diffLen > Radius )
			{
				Position = basePosition + diff.Normal * Radius;
				//velocity = velocity.AddClamped( -diff * 2.0f, diff.Length );
			}
		}

		// Store the rotation offset result
		Rotation = Rotation.FromToRotation( basePosition - position, Position - position );

		//Gizmo.Draw.IgnoreDepth = true;
		//Gizmo.Draw.Line( position, Position );
		//Gizmo.Draw.Line( basePosition, Position );
	}
}
using Sandbox;

/// <summary>
/// This is a component - in your library!
/// </summary>
[Title( "LibraryImporter - My Component" )]
public class MyLibraryComponent : Component
{

}
global using Microsoft.AspNetCore.Components; 
global using Microsoft.AspNetCore.Components.Rendering;
using Sandbox;

public sealed class CameraMovement : Component
{
	[Property] public CharacterController1 Player { get; set; }
	[Property] public GameObject Body { get; set; }
	[Property] public GameObject Head { get; set; }
	[Property] public float Distance { get; set; } = 0f;
	[Property] public float Sensitivity { get; set; } = 0.1f;
	public bool IsFirstPerson => Distance == 0f;
	private CameraComponent Camera;
	private ModelRenderer BodyRenderer;
	private Vector3 CurrentOffset = Vector3.Zero;
	protected override void OnAwake()
	{
		base.OnAwake();
		Camera = Components.Get<CameraComponent>();
		BodyRenderer = Body.Components.Get<ModelRenderer>();
	}
	protected override void OnUpdate()
	{
		var eyeAngles = Head.Transform.Rotation.Angles();
		eyeAngles.pitch += Input.MouseDelta.y * Sensitivity;
		eyeAngles.yaw -= Input.MouseDelta.x * Sensitivity;
		eyeAngles.roll = 0f;
		eyeAngles.pitch = eyeAngles.pitch.Clamp( -89.9f, 89.9f );
		Head.Transform.Rotation = eyeAngles.ToRotation();
		var targetOffset = Vector3.Zero;
		if ( Player.IsCrouching ) targetOffset += Vector3.Down * 35f;
		CurrentOffset = Vector3.Lerp( CurrentOffset, targetOffset, Time.Delta * 10f );
		if ( Camera is not null )
		{
			var camPos = Head.Transform.Position + CurrentOffset;
			if ( !IsFirstPerson )
			{
				var camForward = eyeAngles.ToRotation().Forward;
				var camTrace = Scene.Trace.Ray( camPos, camPos - (camForward * Distance) )
					.WithoutTags( "player", "trigger" )
					.Run();
				if ( camTrace.Hit )
				{
					camPos = camTrace.HitPosition + camTrace.Normal;
				}
				else
				{
					camPos = camTrace.EndPosition;
				}
				BodyRenderer.RenderType = ModelRenderer.ShadowRenderType.On;
			}
			else
			{
				BodyRenderer.RenderType = ModelRenderer.ShadowRenderType.ShadowsOnly;
			}


			Log.Info( CurrentOffset );
			Camera.Transform.Position = camPos;
			Camera.Transform.Rotation = eyeAngles.ToRotation();
		}
	}
}
using System.Collections.Generic;
using Sandbox.Diagnostics;

namespace NPBehave
{
    public class Parallel : Composite
    {
        public enum Policy
        {
            One,
            All,
        }

        // public enum Wait
        // {
        //     NEVER,
        //     ON_FAILURE,
        //     ON_SUCCESS,
        //     BOTH
        // }

        // private Wait waitForPendingChildrenRule;
        private Policy _failurePolicy;
        private Policy _successPolicy;
        private int _childrenCount = 0;
        private int _runningCount = 0;
        private int _succeededCount = 0;
        private int _failedCount = 0;
        private Dictionary<Node, bool> _childrenResults;
        private bool _successState;
        private bool _childrenAborted;

        public Parallel(Policy successPolicy, Policy failurePolicy, /*Wait waitForPendingChildrenRule,*/ params Node[] children) : base("Parallel", children)
        {
            _successPolicy = successPolicy;
            _failurePolicy = failurePolicy;
            // this.waitForPendingChildrenRule = waitForPendingChildrenRule;
            _childrenCount = children.Length;
            _childrenResults = new Dictionary<Node, bool>();
        }

        protected override void DoStart()
        {
            foreach (Node child in Children)
            {
                Assert.AreEqual(child.CurrentState, State.Inactive);
            }

            _childrenAborted = false;
            _runningCount = 0;
            _succeededCount = 0;
            _failedCount = 0;
            foreach (Node child in Children)
            {
                _runningCount++;
                child.Start();
            }
        }

        protected override void DoStop()
        {
            Assert.True(_runningCount + _succeededCount + _failedCount == _childrenCount);

            foreach (Node child in Children)
            {
                if (child.IsActive)
                {
                    child.Stop();
                }
            }
        }

        protected override void DoChildStopped(Node child, bool result)
        {
            _runningCount--;
            if (result)
            {
                _succeededCount++;
            }
            else
            {
                _failedCount++;
            }
            _childrenResults[child] = result;

            bool allChildrenStarted = _runningCount + _succeededCount + _failedCount == _childrenCount;
            if (allChildrenStarted)
            {
                if (_runningCount == 0)
                {
                    if (!_childrenAborted) // if children got aborted because rule was evaluated previously, we don't want to override the successState 
                    {
                        if (_failurePolicy == Policy.One && _failedCount > 0)
                        {
                            _successState = false;
                        }
                        else if (_successPolicy == Policy.One && _succeededCount > 0)
                        {
                            _successState = true;
                        }
                        else if (_successPolicy == Policy.All && _succeededCount == _childrenCount)
                        {
                            _successState = true;
                        }
                        else
                        {
                            _successState = false;
                        }
                    }
                    Stopped(_successState);
                }
                else if (!_childrenAborted)
                {
                    Assert.False(_succeededCount == _childrenCount);
                    Assert.False(_failedCount == _childrenCount);

                    if (_failurePolicy == Policy.One && _failedCount > 0/* && waitForPendingChildrenRule != Wait.ON_FAILURE && waitForPendingChildrenRule != Wait.BOTH*/)
                    {
                        _successState = false;
                        _childrenAborted = true;
                    }
                    else if (_successPolicy == Policy.One && _succeededCount > 0/* && waitForPendingChildrenRule != Wait.ON_SUCCESS && waitForPendingChildrenRule != Wait.BOTH*/)
                    {
                        _successState = true;
                        _childrenAborted = true;
                    }

                    if (_childrenAborted)
                    {
                        foreach (Node currentChild in Children)
                        {
                            if (currentChild.IsActive)
                            {
                                currentChild.Stop();
                            }
                        }
                    }
                }
            }
        }

        public override void StopLowerPriorityChildrenForChild(Node abortForChild, bool immediateRestart)
        {
            if (immediateRestart)
            {
                Assert.False(abortForChild.IsActive);
                if (_childrenResults[abortForChild])
                {
                    _succeededCount--;
                }
                else
                {
                    _failedCount--;
                }
                _runningCount++;
                abortForChild.Start();
            }
            else
            {
                throw new Exception("On Parallel Nodes all children have the same priority, thus the method does nothing if you pass false to 'immediateRestart'!");
            }
        }
    }
}
using System.Collections;
using Sandbox.Diagnostics;

namespace NPBehave
{
    public class RandomSequence : Composite
    {
        static System.Random _rng = new System.Random();


#if DEBUG
        static public void DebugSetSeed( int seed )
        {
            _rng = new System.Random( seed );
        }
#endif

        private int _currentIndex = -1;
        private int[] _randomizedOrder;

        public RandomSequence(params Node[] children) : base("Random Sequence", children)
        {
            _randomizedOrder = new int[children.Length];
            for (int i = 0; i < Children.Length; i++)
            {
                _randomizedOrder[i] = i;
            }
        }

        protected override void DoStart()
        {
            foreach (Node child in Children)
            {
                Assert.AreEqual(child.CurrentState, State.Inactive);
            }

            _currentIndex = -1;

            // Shuffling
            int n = _randomizedOrder.Length;
            while (n > 1)
            {
                int k = _rng.Next(n--);
                (_randomizedOrder[n], _randomizedOrder[k]) = (_randomizedOrder[k], _randomizedOrder[n]);
            }

            ProcessChildren();
        }

        protected override void DoStop()
        {
            Children[_randomizedOrder[_currentIndex]].Stop();
        }


        protected override void DoChildStopped(Node child, bool result)
        {
            if (result)
            {
                ProcessChildren();
            }
            else
            {
                Stopped(false);
            }
        }

        private void ProcessChildren()
        {
            if (++_currentIndex < Children.Length)
            {
                if (IsStopRequested)
                {
                    Stopped(false);
                }
                else
                {
                    Children[_randomizedOrder[_currentIndex]].Start();
                }
            }
            else
            {
                Stopped(true);
            }
        }

        public override void StopLowerPriorityChildrenForChild(Node abortForChild, bool immediateRestart)
        {
            int indexForChild = 0;
            bool found = false;
            foreach (Node currentChild in Children)
            {
                if (currentChild == abortForChild)
                {
                    found = true;
                }
                else if (!found)
                {
                    indexForChild++;
                }
                else if (found && currentChild.IsActive)
                {
                    if (immediateRestart)
                    {
                        _currentIndex = indexForChild - 1;
                    }
                    else
                    {
                        _currentIndex = Children.Length;
                    }
                    currentChild.Stop();
                    break;
                }
            }
        }

        public override string ToString()
        {
            return $"{base.ToString()}[{_currentIndex}]";
        }
    }
}
namespace NPBehave
{
    public class Succeeder : Decorator
    {
        public Succeeder(Node decoratee) : base("Succeeder", decoratee)
        {
        }

        protected override void DoStart()
        {
            Decoratee.Start();
        }

        protected override void DoStop()
        {
            Decoratee.Stop();
        }

        protected override void DoChildStopped(Node child, bool result)
        {
            Stopped(true);
        }
    }
}
using System;

namespace NPBehave
{
    public class Exception : System.Exception
    {
        public Exception(string message) : base(message)
        {
        }
    }
}
namespace NPBehave
{
    public class Repeater : Decorator
    {
        private int _loopCount = -1;
        private int _currentLoop;

        /// <param name="loopCount">number of times to execute the decoratee. Set to -1 to repeat forever, be careful with endless loops!</param>
        /// <param name="decoratee">Decorated Node</param>
        public Repeater(int loopCount, Node decoratee) : base("Repeater", decoratee)
        {
            _loopCount = loopCount;
        }

        /// <param name="decoratee">Decorated Node, repeated forever</param>
        public Repeater(Node decoratee) : base("Repeater", decoratee)
        {
        }

        protected override void DoStart()
        {
            if (_loopCount != 0)
            {
                _currentLoop = 0;
                Decoratee.Start();
            }
            else
            {
                Stopped(true);
            }
        }

        protected override void DoStop()
        {
            Clock.RemoveTimer(RestartDecoratee);
            
            if (Decoratee.IsActive)
            {
                Decoratee.Stop();
            }
            else
            {
                Stopped(false);
            }
        }

        protected override void DoChildStopped(Node child, bool result)
        {
            if (result)
            {
                if (IsStopRequested || (_loopCount > 0 && ++_currentLoop >= _loopCount))
                {
                    Stopped(true);
                }
                else
                {
                    Clock.AddTimer(0, 0, RestartDecoratee);
                }
            }
            else
            {
                Stopped(false);
            }
        }

        protected void RestartDecoratee()
        {
            Decoratee.Start();
        }
    }
}
OptionsScreen {
	position: absolute;
	top: 0;
	left: 0;
	right: 0;
	bottom: 0;
	justify-content: flex-start;
	align-items: flex-start;
	flex-direction: column;
	font-weight: bold;
	font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
	transform: scale( 1 );
	transition: none;
	pointer-events: all;
	background-color: #111111;

	.title {
		color: white;
		text-shadow: none;
		box-shadow: none;
		align-self: flex-end;
		font-size: 75px;
		text-shadow: 0px 0px 2px black;
		flex-shrink: 0;
		font-family: Consolas;
		margin: 20px 0px;
	}

	.button {
		//flex-direction: row;
		justify-content: flex-start;
		align-items: center;
		flex-shrink: 0;
		flex-grow: 0;
		color: rgba(255,255,255, 0.9);
		width: auto;
		padding: 15px 32px;
		text-align: center;
		align-self: center;
		font-size: 24px;
		font-weight: 600;
		margin: 20px;
		gap: 0;
		transition: all 50ms linear;
		cursor: pointer;
	}


	> .container {
		position: relative;
		flex-direction: column;
		justify-content: space-between;
		width: 100%;
		height: 100%;

		.title {
			align-self: center;
			font-size: 60px;
			flex-shrink: 0;
		}

		> .buttons {
			flex-direction: row;
			justify-content: flex-start;
			align-items: flex-start;
			flex-shrink: 0;
			// gap: 8px;
		}

		.content {
			padding: 5px;
			flex-grow: 1;
			flex-direction: column;
			align-self: center;
			//border-radius: 20px;

			&.bg {
				//background-color: rgba(220,220,220,0.5);
			}
		}
	}
	// Buttons on the left side
	.container .buttons {
		.button-bg .button {
			width: 100%;
			min-width: 400px;
			max-width: 600px;
		}
	}

	.content {
		height: auto;
		// Buttons inside the content box
		.buttons {
			.button {
				padding: 5px;
				font-size: 30px;
			}
		}

		&.tabs-container {
			flex-grow: 0;
			flex-shrink: 0;
			overflow: hidden;
		}

		.tabs-group {
			flex-direction: row;
			align-items: center;
			flex-grow: 0;

			.button {
				margin: 0 15px;
				background-color: #242424;
				// Can't remove this sound once it's set
				// Use .inactive instead
				&:hover {
					//sound-in: ui.button.over;
				}

				&:hover {
					background-color: #3B3B3B; //#EC594F;
				}

				&:active {
					background-color: #6E6E6E33; //#D32F2F;
				}

				&.active {
					background-color: #F44336;
				}

				&.inactive:hover {
					sound-in: ui.button.over;
				}
			}
		}
	}

	.content.tab-content {
		// CONTENT ADD HERE

		.button {
			margin: 0;
		}
	}

	.content.tab-content .button, .menu.buttons .button {

		&:hover {
			sound-in: ui.button.over;
		}

		&.green {
			background-color: #4CAF50;

			&:hover {
				background-color: #57BB5A;
			}

			&:active {
				background-color: #45A049;
			}
		}

		&.red {
			background-color: #F44336;

			&:hover {
				background-color: #EC594F;
			}

			&:active {
				background-color: #D32F2F;
			}
		}

		&.gray, &.grey {
			background-color: #666;
			color: white;

			&:hover {
				background-color: #777;
			}

			&:active {
				background-color: #444;
			}
		}
	}

	.table {
		position: relative;
		flex-shrink: 1;
		flex-grow: 0;
		justify-content: center;
		flex-wrap: wrap;
		color: #9b9fa8;
		align-self: center;
		max-width: 1500px;
		width: 85%;
		font-size: 30px;
		// TABLE ADD HERE
		overflow-y: scroll;

		.row {
			flex-direction: row;
			width: 100%;
			align-items: center;
			justify-content: center;
			flex-grow: 0;
			flex-shrink: 0;
			font-weight: 600;
			margin: 10px 0;
		}

		.row.title {
			color: white;
			margin-top: 20px;
			padding: 5px 0;
			justify-content: flex-start;
			font-size: 32px;
			font-weight: 800;
			border-bottom: 0.2em solid rgb(30, 30, 30, 0.25);

			&:first-child {
				margin-top: 0;
			}
		}

		.column {
			flex-direction: column;
			align-items: flex-start;
			width: 100%;
		}

		.column:last-child {
			align-items: flex-end;
			justify-content: center;
			//height: 100%;
		}

		.column.value {
			.button {
				width: 100px;
				justify-content: center;
			}
		}

		SliderControl {
			width: 100%;
		}
	}

	.cycling-selector {
		display: flex;
		flex-direction: column;
		align-items: center;
		width: 100%; /* Adjust width as needed */
		max-width: 500px; /* Set max width */
		background-color: #111; /* Dark background similar to your image */
		padding: 10px;
		border-radius: 5px;
		color: white;
		font-family: Arial, sans-serif;
	}

	.cycling-label {
		text-align: center;
		margin-bottom: 10px; /* Spacing between label and controls */
		font-size: 14px;
		width: 100%;
	}

	.cycling-controls {
		display: flex;
		align-items: center;
		justify-content: space-between;
		width: 100%;

		.button {
			width: auto;
			padding: 5px;
		}
	}

	.cycling-controls .value {
		align-items: center;
		font-weight: bold;
		font-size: 22px;
		padding: 5px;
	}

	.cycling-controls .arrow {
		border: none;
		color: white;
		font-size: 40px;
		line-height: 10px;
		cursor: pointer;
		padding: 5px;

		&.left {
			padding-left: 0;
		}

		&.right {
			padding-right: 0;
		}
	}

	.cycling-controls .arrow:hover {
		color: #ccc;
	}
}
global using Sandbox;
global using System.Collections.Generic;
global using System.Linq;
global using Microsoft.AspNetCore.Components; 
global using Microsoft.AspNetCore.Components.Rendering;
global using Sandbox;
global using System.Collections.Generic;
global using System.Linq;
using System.Threading.Tasks;
using System.Threading;
using System;

namespace Duccsoft;

/// <summary>
/// Provides a handy asynchronous wrapper for loading a VideoPlayer and waiting
/// until its video and audio are both loaded.
/// </summary>
public class AsyncVideoLoader
{
	public AsyncVideoLoader() 
	{
		_videoPlayer = new VideoPlayer();
	}

	public AsyncVideoLoader( VideoPlayer player )
	{
		_videoPlayer = player ?? new VideoPlayer();
	}

	public bool IsLoading { get; private set; }

	private VideoPlayer _videoPlayer;
	private Action _onLoaded;
	private Action _onAudioReady;

	public async Task<VideoPlayer> LoadFromUrl( string url, CancellationToken cancelToken = default )
	{
		void Play( VideoPlayer player ) => player.Play( url );

		await Load( Play, cancelToken );
		return _videoPlayer;
	}

	public async Task<VideoPlayer> LoadFromFile( BaseFileSystem fileSystem, string path, CancellationToken cancelToken )
	{
		void Play( VideoPlayer player ) => player.Play( fileSystem, path );

		await Load( Play, cancelToken );
		return _videoPlayer;
	}

	private async Task Load( Action<VideoPlayer> playAction, CancellationToken cancelToken = default )
	{
		// Attempting to play a video from a thread would throw an exception.
		await GameTask.MainThread( cancelToken );

		if ( IsLoading )
		{
			throw new InvalidOperationException( "Another video was already being loaded. Check IsLoading or create a new instance of AsyncVideoLoader." );
		}

		IsLoading = true;

		bool videoLoaded = false;
		bool audioLoaded = false;

		// Assign private members instead of named methods to the invocation lists of the
		// VideoPlayer delegates to break reference equality between runs.
		_onLoaded = () => videoLoaded = true;
		_onAudioReady = () => audioLoaded = true;

		_videoPlayer.OnLoaded = _onLoaded;
		_videoPlayer.OnAudioReady = _onAudioReady;

		playAction?.Invoke( _videoPlayer );

		// Non-blocking spin until video and audio are loaded.
		while ( !videoLoaded || !audioLoaded )
		{
			// If OnLoaded or OnAudioReady are changed externally before we're finished
			// loading, the video will likely never load. Abort to avoid spinning forever.
			var callbacksChanged = _onLoaded != _videoPlayer.OnLoaded || _onAudioReady != _videoPlayer.OnAudioReady;
			if ( callbacksChanged || cancelToken.IsCancellationRequested )
			{
				IsLoading = false;
				return;
			}

			await GameTask.Yield();
		}

		IsLoading = false;
	}
}
global using Microsoft.AspNetCore.Components; 
global using Microsoft.AspNetCore.Components.Rendering;
namespace LobbySystem;

/// <summary>
/// Auto-hosts a lobby so Steam friends can join, and keeps one networked pawn per connection plus optional
/// bots by cloning <see cref="PlayerPrefab"/>. The pawn only has to implement <see cref="ILobbyAgent"/>.
/// Spawning is de-duped and runs in OnUpdate so a join can't fire mid-enumeration.
/// </summary>
public sealed class LobbyNetworkManager : Component, Component.INetworkListener
{
	[Property] public GameObject PlayerPrefab { get; set; }
	[Property] public int BotCount { get; set; } = 1;

	/// <summary>When true, bots only exist during an active round.</summary>
	[Property] public bool BotsOnlyDuringRound { get; set; } = true;

	[Property] public Color BotTint { get; set; } = new Color( 1f, 0.35f, 0.3f );

	// Lobby spawn ring, used before a round map loads.
	readonly Vector3[] _spawns =
	{
		new Vector3( 0f, -300f, 40f ), new Vector3( 300f, 0f, 40f ),
		new Vector3( 0f, 300f, 40f ),  new Vector3( -300f, 0f, 40f ),
		new Vector3( 250f, 250f, 40f ), new Vector3( -250f, -250f, 40f ),
	};
	int _spawnIndex;
	readonly Dictionary<Guid, GameObject> _pawns = new();
	readonly List<GameObject> _bots = new();

	bool _reconcileNow;
	TimeUntil _nextReconcile;
	TimeUntil _nextSweep;

	protected override async Task OnLoad()
	{
		// When joining a friend the engine is mid-connect and IsActive is briefly false, so poll for a
		// moment before hosting. Otherwise a joiner would spin up its own solo lobby.
		if ( Networking.IsActive ) return;
		for ( int i = 0; i < 6 && !Networking.IsActive; i++ )
			await Task.DelayRealtimeSeconds( 0.1f );
		if ( !Networking.IsActive )
			Networking.CreateLobby( new() );
	}

	void INetworkListener.OnActive( Connection channel ) => _reconcileNow = true;

	protected override void OnUpdate()
	{
		if ( !Networking.IsHost || PlayerPrefab is null ) return;

		if ( !_reconcileNow && _nextReconcile > 0f ) return;
		_reconcileNow = false;
		_nextReconcile = 0.25f;

		try
		{
			bool wantBots = !BotsOnlyDuringRound || (LobbyDirector.Current?.State == LobbyState.Active);
			ReconcileBots( wantBots ? Math.Max( 0, BotCount ) : 0 );

			foreach ( var conn in Connection.All.ToList() )
			{
				if ( conn is null || !conn.IsActive ) continue;
				if ( _pawns.TryGetValue( conn.Id, out var pawn ) && pawn.IsValid() ) continue;
				var id = conn.Id;
				_pawns[id] = FindConnectionPawn( id ) ?? SpawnPawn( false, conn.DisplayName, conn );
			}

			Sweep();
		}
		catch
		{
			// Connection or scene list changed during the pass; retry next frame.
		}
	}

	void ReconcileBots( int target )
	{
		_bots.RemoveAll( b => !b.IsValid() );
		while ( _bots.Count > target )
		{
			var b = _bots[_bots.Count - 1];
			_bots.RemoveAt( _bots.Count - 1 );
			if ( b.IsValid() ) b.Destroy();
		}
		while ( _bots.Count < target )
			_bots.Add( SpawnPawn( true, "Bot", null ) );
	}

	GameObject FindConnectionPawn( Guid id )
	{
		foreach ( var a in Scene.GetAllComponents<ILobbyAgent>() )
		{
			if ( !a.IsValid() || a.IsBot ) continue;
			if ( a is Component c && c.Network.OwnerId == id ) return c.GameObject;
		}
		return null;
	}

	void Sweep()
	{
		if ( _nextSweep > 0f ) return;
		_nextSweep = 1f;
		foreach ( var key in _pawns.Where( kv => !kv.Value.IsValid() ).Select( kv => kv.Key ).ToList() )
			_pawns.Remove( key );
	}

	GameObject SpawnPawn( bool isBot, string displayName, Connection owner )
	{
		var go = PlayerPrefab.Clone( NextSpawn() );
		go.Name = isBot ? "Bot" : $"Player - {displayName}";
		go.Enabled = true;

		var agent = go.Components.Get<ILobbyAgent>() ?? go.Components.GetInChildren<ILobbyAgent>();
		agent?.InitAgent( isBot, displayName );

		if ( isBot )
		{
			var rend = go.Components.GetInChildren<SkinnedModelRenderer>();
			if ( rend is not null ) rend.Tint = BotTint;
		}

		if ( owner is not null ) go.NetworkSpawn( owner );
		else go.NetworkSpawn();
		return go;
	}

	Vector3 NextSpawn()
	{
		int idx = _spawnIndex++;
		var dir = LobbyDirector.Current;
		if ( dir is not null && dir.UseRoundMap && dir.MapReady )
			return dir.RoundSpawnPoint( idx );
		return _spawns[idx % _spawns.Length];
	}
}
namespace LobbySystem;

/// <summary>Lifecycle of the lobby: Lobby, then Active, then Ended before looping back.</summary>
public enum LobbyState
{
	/// <summary>Free roam before and after a round. The mode button works here.</summary>
	Lobby,
	/// <summary>A round is running.</summary>
	Active,
	/// <summary>Round finished; results show before returning to the lobby.</summary>
	Ended
}
namespace LobbySystem;

/// <summary>
/// In-world button that opens the mode menu for the host, or a local suggestion menu for a client. It needs
/// a ModelRenderer to be visible and is hidden while a round is live. When the local player is within
/// <see cref="UseRange"/> and presses Use, the menu opens.
/// </summary>
public sealed class LobbyModeButton : Component
{
	[Property] public float UseRange { get; set; } = 130f;
	[Property] public bool GlowWhenInRange { get; set; } = true;
	[Property] public Color IdleTint { get; set; } = new Color( 0.85f, 0.4f, 0.15f );
	[Property] public Color ActiveTint { get; set; } = new Color( 1f, 0.85f, 0.3f );

	ModelRenderer _renderer;
	ILobbyAgent _me;

	protected override void OnStart()
	{
		_renderer = Components.Get<ModelRenderer>() ?? Components.GetInChildren<ModelRenderer>();
		if ( _renderer is not null ) _renderer.Tint = IdleTint;
	}

	protected override void OnUpdate()
	{
		var dir = LobbyDirector.Current;
		bool inLobby = dir is null || !dir.RoundLive;
		if ( _renderer is not null && _renderer.Enabled != inLobby )
			_renderer.Enabled = inLobby;
		if ( !inLobby ) return;

		var me = LocalPlayer();
		bool inRange = me is not null && WorldPosition.Distance( me.WorldPosition ) <= UseRange;

		if ( GlowWhenInRange && _renderer is not null )
			_renderer.Tint = inRange ? ActiveTint : IdleTint;

		if ( inRange && Input.Pressed( "Use" ) )
			dir?.RequestModeMenu();
	}

	ILobbyAgent LocalPlayer()
	{
		if ( _me is not null && _me.IsValid() && !_me.IsBot && !_me.IsProxy ) return _me;
		try { _me = Scene.GetAllComponents<ILobbyAgent>().FirstOrDefault( c => c.IsValid() && !c.IsBot && !c.IsProxy ); }
		catch { _me = null; }
		return _me;
	}
}
global using static Sandbox.Internal.GlobalGameNamespace;
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Rendering;
[assembly: global::System.Reflection.AssemblyMetadata( "AddonTitle", "MC Clouds" )]
[assembly: global::System.Reflection.AssemblyMetadata( "AddonIdent", "mcclouds" )]
[assembly: global::System.Reflection.AssemblyMetadata( "OrgIdent", "trend" )]
[assembly: global::System.Reflection.AssemblyMetadata( "Ident", "trend.mcclouds" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineVersion", "26" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineMinorVersion", "1" )]

[assembly: System.Runtime.Versioning.TargetFramework( ".NETCoreApp,Version=v9.0", FrameworkDisplayName = ".NET 9.0" )]
[assembly: global::System.Reflection.AssemblyMetadata( "CompileTime", "2026-06-16T17:04:05.4666731Z" )]
[assembly: global::System.Reflection.AssemblyVersion("0.0.124.0")]
[assembly: global::System.Reflection.AssemblyFileVersion("0.0.124.0")]
using Sandbox;
using Sandbox.UI;
using System;

namespace SbTween;

public static class LightExtensions
{

	public static BaseTween TweenLightColor( this Light Light, Color target, float duration )
	{
		Color start = Light.LightColor;
		var tween = new BaseTween( duration );
		tween.Target = Light.GameObject;
		return TweenManager.Instance.AddTween( tween
			.OnStart( () => start = Light.LightColor )
			.OnUpdate( p => Light.LightColor = Color.Lerp( start, target, p ) ) );
	}

	public static BaseTween TweenRadius( this SpotLight light, float target, float duration )
	{
		if ( !light.IsValid() ) return null;

		float start = light.Radius;
		return TweenManager.Instance.AddTween( new BaseTween( duration )
			.OnStart( () => start = light.Radius )
			.OnUpdate( p => light.Radius = MathX.Lerp( start, target, p ) ) );
	}

	public static BaseTween TweenConeOuter( this SpotLight light, float target, float duration )
	{
		if ( !light.IsValid() ) return null;

		float start = light.ConeOuter;
		return TweenManager.Instance.AddTween( new BaseTween( duration )
			.OnStart( () => start = light.ConeOuter )
			.OnUpdate( p => light.ConeOuter = MathX.Lerp( start, target, p ) ) );
	}

	public static BaseTween TweenInnerCone( this SpotLight light, float target, float duration )
	{
		if ( !light.IsValid() ) return null;

		float start = light.ConeInner;
		return TweenManager.Instance.AddTween( new BaseTween( duration )
			.OnStart( () => start = light.ConeInner )
			.OnUpdate( p => light.ConeInner = MathX.Lerp( start, target, p ) ) );
	}

	public static BaseTween TweenAttenuation( this PointLight light, float target, float duration )
	{
		if ( !light.IsValid() ) return null;

		float start = light.Attenuation;
		return TweenManager.Instance.AddTween( new BaseTween( duration )
			.OnStart( () => start = light.Attenuation )
			.OnUpdate( p => light.Attenuation = MathX.Lerp( start, target, p ) ) );
	}

	public static BaseTween TweenRadius( this PointLight light, float target, float duration )
	{
		if ( !light.IsValid() ) return null;

		float start = light.Radius;
		return TweenManager.Instance.AddTween( new BaseTween( duration )
			.OnStart( () => start = light.Radius )
			.OnUpdate( p => light.Radius = MathX.Lerp( start, target, p ) ) );
	}

	public static BaseTween TweenAttenuation( this SpotLight light, float target, float duration )
	{
		if ( !light.IsValid() ) return null;

		float start = light.Attenuation;
		return TweenManager.Instance.AddTween( new BaseTween( duration )
			.OnStart( () => start = light.Attenuation )
			.OnUpdate( p => light.Attenuation = MathX.Lerp( start, target, p ) ) );
	}

	//FLICKERING LIGHT
	public static BaseTween TweenFlickerLight( this PointLight light, float minBrightness, float maxBrightness, float duration, float speed = 10f )
	{
		if ( !light.IsValid() ) return null;

		float time = 0f;
		return TweenManager.Instance.AddTween( new BaseTween( duration )
			.OnUpdate( _ =>
			{
				time += Time.Delta * speed;
				float noise = MathF.Sin( time * 1.3f ) * MathF.Sin( time * 2.7f ) * MathF.Sin( time * 0.9f );
				float t = (noise + 1f) * 0.5f;
				light.Attenuation = MathX.Lerp( minBrightness, maxBrightness, t );
			} ) );
	}

	public static BaseTween TweenFlickerLight( this SpotLight light, float minBrightness, float maxBrightness, float duration, float speed = 10f )
	{
		if ( !light.IsValid() ) return null;

		float time = 0f;
		return TweenManager.Instance.AddTween( new BaseTween( duration )
			.OnUpdate( _ =>
			{
				time += Time.Delta * speed;
				float noise = MathF.Sin( time * 1.3f ) * MathF.Sin( time * 2.7f ) * MathF.Sin( time * 0.9f );
				float t = (noise + 1f) * 0.5f;
				light.Attenuation = MathX.Lerp( minBrightness, maxBrightness, t );
			} ) );
	}

	//FLICKERING Color

	public static BaseTween TweenFlickerColor( this PointLight light, Color colorA, Color colorB, float duration, float speed = 10f )
	{
		if ( !light.IsValid() ) return null;

		float time = 0f;
		return TweenManager.Instance.AddTween( new BaseTween( duration )
			.OnUpdate( _ =>
			{
				time += Time.Delta * speed;
				float noise = MathF.Sin( time * 1.3f ) * MathF.Sin( time * 2.7f ) * MathF.Sin( time * 0.9f );
				float t = (noise + 1f) * 0.5f;
				light.LightColor = Color.Lerp( colorA, colorB, t );
			} ) );
	}

	public static BaseTween TweenFlickerColor( this SpotLight light, Color colorA, Color colorB, float duration, float speed = 10f )
	{
		if ( !light.IsValid() ) return null;

		float time = 0f;
		return TweenManager.Instance.AddTween( new BaseTween( duration )
			.OnUpdate( _ =>
			{
				time += Time.Delta * speed;
				float noise = MathF.Sin( time * 1.3f ) * MathF.Sin( time * 2.7f ) * MathF.Sin( time * 0.9f );
				float t = (noise + 1f) * 0.5f;
				light.LightColor = Color.Lerp( colorA, colorB, t );
			} ) );
	}

}
using Sandbox;
using System;

namespace SbTween;

public static class AudioExtensions
{
	public static BaseTween TweenVolume( this SoundPointComponent sound, float targetVolume, float duration )
	{
		float startVolume = sound.Volume;
		var tween = new BaseTween( duration );
		tween.Target = sound.GameObject;

		return TweenManager.Instance.AddTween( tween
			.OnStart( () => startVolume = sound.Volume )
			.OnUpdate( p =>
			{
				if ( !sound.IsValid() ) return;
				sound.Volume = MathX.Lerp( startVolume, targetVolume, p );
			} )
		);
	}
	
	public static BaseTween TweenPitch( this SoundPointComponent sound, float targetPitch, float duration )
	{
		float startPitch = sound.Pitch;
		var tween = new BaseTween( duration );
		tween.Target = sound.GameObject;

		return TweenManager.Instance.AddTween( tween
			.OnStart( () => startPitch = sound.Pitch )
			.OnUpdate( p =>
			{
				if ( !sound.IsValid() ) return;
				sound.Pitch = MathX.Lerp( startPitch, targetPitch, p );
			} )
		);
	}
}
using Sandbox;
using System;

namespace SbTween;

public static class MathTweenExtensions
{
	public static BaseTween TweenInCircle( this GameObject obj, float duration, Vector3 axis, float range, float speed, bool snapping = false )
	{
		Vector3 centerPos = obj.WorldPosition;
		var tween = new BaseTween( duration );
		tween.Target = obj;

		Vector3 normal = axis.Normal;
		Vector3 v1 = Vector3.Cross( normal, MathF.Abs( normal.z ) < 0.9f ? Vector3.Up : Vector3.Forward ).Normal;
		Vector3 v2 = Vector3.Cross( normal, v1 ).Normal;

		return TweenManager.Instance.AddTween( tween
			.OnUpdate( p =>
			{
				if ( !obj.IsValid() ) return;

				float angleDegrees = p * 360f * speed;
				float angleRadians = angleDegrees * (MathF.PI / 180f);

				float cos = MathF.Cos( angleRadians ) * range;
				float sin = MathF.Sin( angleRadians ) * range;

				Vector3 rotatedOffset = (v1 * cos) + (v2 * sin);
				obj.WorldPosition = centerPos + rotatedOffset;
			} )
		);
	}

	public static BaseTween TweenSpiral( this GameObject obj, float duration, Vector3 axis, float speed, float frequency )
	{
		Vector3 startPos = obj.WorldPosition;
		var tween = new BaseTween( duration );
		tween.Target = obj;

		return TweenManager.Instance.AddTween( tween
			.OnStart( () => startPos = obj.WorldPosition )
			.OnUpdate( p =>
			{
				if ( !obj.IsValid() ) return;

				float angle = p * MathF.PI * 2f * frequency;

				float currentRadius = p * speed;

				float x = MathF.Cos( angle ) * currentRadius;
				float y = MathF.Sin( angle ) * currentRadius;

				Vector3 axisOffset = axis * p;

				Vector3 circleOffset = new Vector3( x, y, 0 );

				obj.WorldPosition = startPos + axisOffset + circleOffset;
			} )
		);
	}
	
	public static BaseTween TweenPunchFloat( this GameObject obj, float v, float amplitude, float duration, int vibrations = 5, float elasticity = 1f, Action<float> setter = null )
	{
		var tween = new BaseTween( duration );
		tween.Target = obj;

		return TweenManager.Instance.AddTween( tween
			.OnUpdate( p =>
			{
				if ( !obj.IsValid() ) return;
				if ( p >= 1.0f )
				{
					setter?.Invoke( v );
					return;
				}
				
				float decay = MathF.Pow( 1f - p, elasticity * 3f );

				float omega = vibrations * MathF.PI * 2f;
				float oscillation = MathF.Sin( p * omega );

				float currentOffset = amplitude * oscillation * decay;

				setter?.Invoke( v + currentOffset );
			} )
			.OnComplete( () => setter?.Invoke( v ) )
		);
	}
	
	public static BaseTween TweenShakeFloat( this GameObject obj, float baseline, float strength, float duration, Action<float> setter = null )
	{
		var tween = new BaseTween( duration );
		tween.Target = obj;

		return TweenManager.Instance.AddTween( tween
			.OnUpdate( p =>
			{
				if ( !obj.IsValid() ) return;
				if ( p >= 1.0f )
				{
					setter?.Invoke( baseline );
					return;
				}

				float currentStrength = strength * (1.0f - p);
             
				float randomOffset = Game.Random.Float( -currentStrength, currentStrength );

				setter?.Invoke( baseline + randomOffset );
			} )
			.OnComplete( () => setter?.Invoke( baseline ) )
		);
	}
}
namespace Sandbox.UiPro;

public enum HorizontalAlignment
{
	Left,
	Center,
	Right
}

public enum VerticalAlignment
{
	Top,
	Center,
	Bottom
}

[Title( "Text Node - UI Pro" ), Category( "UI Pro" ), Icon( "text_fields" )]
public class TextNode : NodeComponent
{
	[Property, InlineEditor, Group( "Layout Settings" ), Order( -999 )] public override NodeStyle Style { get; set; } = GetDefaultStyle();

	[Property, Group("Text Settings")] public HorizontalAlignment HorizontalAlignment { get; set; } = HorizontalAlignment.Center;
	[Property, Group( "Text Settings" )] public VerticalAlignment VerticalAlignment { get; set; } = VerticalAlignment.Center;
	[Property, InlineEditor, Group( "Text Settings" )] public TextRendering.Scope TextScope { get; set; } = TextRendering.Scope.Default;

	private static NodeStyle GetDefaultStyle()
	{
		return new NodeStyle()
		{
			Anchor = NodePoint.CenterMiddle,
			Pivot = NodePoint.CenterMiddle,
			Offset = Vector2.Zero,
			Size = new Vector2( 100, 100 ),
			ChildPadding = Vector2.Zero,
			ClipChildren = false,
			StretchHorizontal = false,
			StretchVertical = false,
			CornerRadius = 0,
			BorderWidth = 0,
			BorderColor = Color.Black,
			Texture = Texture.White,
			Tint = Color.White,
			UvScale = Vector2.One,
			UvOffset = Vector2.Zero
		};
	}

	protected override void UpdateStyle(float scaleFactor)
	{
		Style.BorderWidth = 0; // for debugging
		Style.BorderColor = Color.White;

		TextRendering.Scope scope = TextScope;
		scope.FontSize *= scaleFactor;

		Texture texture = TextRendering.GetOrCreateTexture( scope );
		Style.Texture = texture;

		Vector2 scale = (Layout.Outer.Size * scaleFactor) / texture.Size;
		Style.UvScale = scale;

		float alignX = HorizontalAlignment switch
		{
			HorizontalAlignment.Left => 0f,
			HorizontalAlignment.Center => 0.5f,
			HorizontalAlignment.Right => 1f,
			_ => 0.5f,
		};

		float alignY = VerticalAlignment switch
		{
			VerticalAlignment.Top => 0f,
			VerticalAlignment.Center => 0.5f,
			VerticalAlignment.Bottom => 1f,
			_ => 0.5f,
		};

		float offsetX = alignX * (1f / scale.x - 1f);
		float offsetY = alignY * (1f / scale.y - 1f);

		Style.UvOffset = new Vector2( offsetX, offsetY );
	}
}
using Sandbox.UiPro;

namespace Sandbox;

// Hooks up the Button's OnClick event and responds
// by updating the TextNode
public class ExampleButtonController : Component
{
	[Property] public Button MyButton { get; set; }
	[Property] public TextNode MyText { get; set; }
	[Property, ReadOnly] public int ClickCount { get; set; } = 0;

	protected override void OnStart()
	{
		if ( !MyButton.IsValid() ) return;

		MyButton.OnClick = OnButtonClicked;
	}

	private void OnButtonClicked()
	{
		ClickCount++;

		if ( !MyText.IsValid() ) return;

		TextRendering.Scope scope = MyText.TextScope;
		scope.Text = $"Clicked {ClickCount} Times";
		MyText.TextScope = scope;
	}
}
using System;

namespace Sandbox.UiPro;

public enum NodePoint
{
	TopLeft, TopMiddle, TopRight,
	CenterLeft, CenterMiddle, CenterRight,
	BottomLeft, BottomMiddle, BottomRight,
}

public class NodeStyle
{
	[Property] public NodePoint Anchor { get; set; }
	[Property] public NodePoint Pivot { get; set; }
	[Property] public Vector2 Offset { get; set; }
	[Property] public Vector2 Size { get; set; }
	[Property] public Vector4 ChildPadding { get; set; }
	[Property] public bool ClipChildren { get; set; }
	[Property] public bool StretchHorizontal { get; set; }
	[Property] public bool StretchVertical { get; set; }
	
	[Property, Hide] public float CornerRadius { get; set; }
	[Property, Hide] public float BorderWidth { get; set; }
	[Property, Hide] public Color BorderColor { get; set; }
	[Property, Hide] public Texture Texture { get; set; }
	[Property, Hide] public Color Tint { get; set; }
	[Property, Hide] public Vector2 UvScale { get; set; }
	[Property, Hide] public Vector2 UvOffset { get; set; }
}

public class NodeLayout
{
	public Rect Outer { get; private set; }
	public Rect Inner { get; private set; }
	public Rect ClipRect { get; private set; }
	public float ClipRadius { get; private set; }
	public Rect ChildClipRect { get; private set; }
	public float ChildClipRadius { get; private set; }

	public static NodeLayout GetRootLayout( Vector2 size )
	{
		Rect rootRect = new Rect( Vector2.Zero, size );

		NodeLayout layout = new NodeLayout()
		{
			Outer = rootRect,
			Inner = rootRect,
			ClipRect = rootRect,
			ClipRadius = 0,
			ChildClipRect = rootRect,
			ChildClipRadius = 0
		};

		return layout;
	}

	public void Compute( NodeLayout parentLayout, NodeStyle style )
	{
		Vector2 anchor = AnchorFraction( style.Anchor );
		Vector2 pivot = AnchorFraction( style.Pivot );

		float width = style.StretchHorizontal ? parentLayout.Inner.Size.x : style.Size.x;
		float height = style.StretchVertical ? parentLayout.Inner.Size.y : style.Size.y;

		float x = style.StretchHorizontal
			? parentLayout.Inner.Position.x + style.Offset.x
			: parentLayout.Inner.Position.x + anchor.x * parentLayout.Inner.Size.x - pivot.x * width + style.Offset.x;

		float y = style.StretchVertical
			? parentLayout.Inner.Position.y + style.Offset.y
			: parentLayout.Inner.Position.y + anchor.y * parentLayout.Inner.Size.y - pivot.y * height + style.Offset.y;

		Outer = new Rect( new Vector2( x, y ), new Vector2( width, height ) );

		float innerW = Math.Max( 0f, width - style.ChildPadding.x - style.ChildPadding.z );
		float innerH = Math.Max( 0f, height - style.ChildPadding.y - style.ChildPadding.w );
		Inner = new Rect( new Vector2( x + style.ChildPadding.x, y + style.ChildPadding.y ), new Vector2( innerW, innerH ) );

		ClipRect = parentLayout.ChildClipRect;
		ClipRadius = parentLayout.ChildClipRadius;

		if ( style.ClipChildren )
		{
			float bw = Math.Max( 0f, style.BorderWidth );
			float clipW = Math.Max( 0f, Outer.Size.x - bw * 2f );
			float clipH = Math.Max( 0f, Outer.Size.y - bw * 2f );

			ChildClipRect = new Rect( new Vector2( Outer.Position.x + bw, Outer.Position.y + bw ), new Vector2( clipW, clipH ) );
			ChildClipRadius = Math.Max( 0f, style.CornerRadius - bw );
		}
		else
		{
			ChildClipRect = parentLayout.ChildClipRect;
			ChildClipRadius = parentLayout.ChildClipRadius;
		}
	}

	private static Vector2 AnchorFraction( NodePoint point )
	{
		float fx = point switch
		{
			NodePoint.TopLeft or NodePoint.CenterLeft or NodePoint.BottomLeft => 0f,
			NodePoint.TopMiddle or NodePoint.CenterMiddle or NodePoint.BottomMiddle => 0.5f,
			_ => 1f,
		};
		float fy = point switch
		{
			NodePoint.TopLeft or NodePoint.TopMiddle or NodePoint.TopRight => 0f,
			NodePoint.CenterLeft or NodePoint.CenterMiddle or NodePoint.CenterRight => 0.5f,
			_ => 1f,
		};
		return new Vector2( fx, fy );
	}
}
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Dreams.UltimateLightManager;

[Library( "UltimateLightManager" )]
[Title( "Ultimate Light Manager" )]
[Description( "Advanced light component with presets, runtime controls, grouping, and an integrated S&box editor workflow." )]
[Category( "Light" )]
[Icon( "tungsten" )]
public class UltimateLightManager : Component, Component.ExecuteInEditor
{
    public enum LightTypeEnum
    {
        Point,
        Spot
    }

    public enum LightPreset
    {
        Custom,
        Candle,
        Torch,
        Neon,
        Alarm,
        BrokenLamp,
        SciFi,
        StreetLight
    }

    public static void SetGroupState( string groupName, bool isEnabled )
    {
        foreach ( var light in GetLightsInGroup( groupName ) )
        {
            light.SetEnabledState( isEnabled );
        }
    }

    public static void SetGroupBrightness( string groupName, float brightness )
    {
        brightness = Math.Max( brightness, 0f );

        foreach ( var light in GetLightsInGroup( groupName ) )
        {
            light.SetBrightnessLevel( brightness );
        }
    }

    public static void SetGroupColor( string groupName, Color color )
    {
        foreach ( var light in GetLightsInGroup( groupName ) )
        {
            light.SetLightColorValue( color );
        }
    }

    public static void ApplyPresetToGroup( string groupName, LightPreset preset )
    {
        foreach ( var light in GetLightsInGroup( groupName ) )
        {
            light.ApplyPreset( preset );
        }
    }

    public static void TriggerGroupFlash( string groupName, float duration = 0.15f, float brightnessMultiplier = 2f )
    {
        foreach ( var light in GetLightsInGroup( groupName ) )
        {
            light.TriggerFlash( duration, brightnessMultiplier );
        }
    }

    public static void TriggerGroupAlarm( string groupName, float duration = 2f )
    {
        foreach ( var light in GetLightsInGroup( groupName ) )
        {
            light.TriggerAlarm( duration );
        }
    }

    public static void SetPowerGridState( string powerGridTag, bool isPowered )
    {
        foreach ( var light in GetLightsInPowerGrid( powerGridTag ) )
        {
            light.SetPowered( isPowered );
        }
    }

    private static IEnumerable<UltimateLightManager> GetLightsInGroup( string groupName )
    {
        return EnumerateAllLights().Where( light => string.Equals( light.LightGroup, groupName, StringComparison.OrdinalIgnoreCase ) );
    }

    private static IEnumerable<UltimateLightManager> GetLightsInPowerGrid( string powerGridTag )
    {
        return EnumerateAllLights().Where( light => string.Equals( light.PowerGridTag, powerGridTag, StringComparison.OrdinalIgnoreCase ) );
    }

    private static IEnumerable<UltimateLightManager> EnumerateAllLights()
    {
        var visitedScenes = new HashSet<Scene>();
        var visitedLights = new HashSet<UltimateLightManager>();

        foreach ( var scene in EnumerateCandidateScenes() )
        {
            if ( scene == null || !visitedScenes.Add( scene ) )
            {
                continue;
            }

            foreach ( var light in scene.GetAllComponents<UltimateLightManager>() )
            {
                if ( light != null && visitedLights.Add( light ) )
                {
                    yield return light;
                }
            }
        }
    }

    private static IEnumerable<Scene> EnumerateCandidateScenes()
    {
        if ( Game.ActiveScene != null )
        {
            yield return Game.ActiveScene;
        }

        foreach ( var scene in Scene.All )
        {
            if ( scene != null )
            {
                yield return scene;
            }
        }
    }

    [Property, Order( -1 ), Group( "Management" )]
    public string LightGroup { get; set; } = "Default";

    [Property, Group( "Management" )]
    public string PowerGridTag { get; set; } = string.Empty;

    [Property, Group( "Management" )]
    public float StartDelay { get; set; } = 0.0f;

    [Property, Group( "Management" )]
    public bool AutoDesync { get; set; } = true;

    [Property, Group( "Management" )]
    public bool ShowDebugGizmos { get; set; } = false;

    [Property, Group( "Management" )]
    public bool ForceNetworkObjectMode { get; set; } = true;

    [Property, Group( "General" ), Sync( SyncFlags.FromHost )]
    public LightTypeEnum TargetLightType { get; set; } = LightTypeEnum.Point;

    [Property, Group( "General" ), Sync( SyncFlags.FromHost )]
    public bool IsEnabled { get; set; } = true;

    [Property, Group( "General" ), Sync( SyncFlags.FromHost )]
    public Color LightColor { get; set; } = Color.White;

    [Property, Group( "General" ), Range( 0, 100 ), Sync( SyncFlags.FromHost | SyncFlags.Interpolate )]
    public float Brightness { get; set; } = 1.0f;

    [Property, Group( "General" ), Range( 0, 10 )]
    public float VolumetricBoost { get; set; } = 1.0f;

    [Property, Group( "General" )]
    public bool CastShadows { get; set; } = true;

    [Property, Group( "Presets" ), Sync( SyncFlags.FromHost )]
    public LightPreset SelectedPreset { get; set; } = LightPreset.Custom;

    [Property, Group( "Presets" ), Sync( SyncFlags.FromHost )]
    public bool AutoApplyPreset { get; set; } = true;

    [Property, Group( "Transitions" )]
    public bool EnableFade { get; set; } = false;

    [Property, Group( "Transitions" )]
    public float FadeInDuration { get; set; } = 0.2f;

    [Property, Group( "Transitions" )]
    public float FadeOutDuration { get; set; } = 0.2f;

    [Property, Group( "Audio" )]
    public SoundEvent AmbientSound { get; set; }

    [Property, Group( "Audio" )]
    public SoundEvent ToggleOnSound { get; set; }

    [Property, Group( "Audio" )]
    public SoundEvent ToggleOffSound { get; set; }

    [Property, Group( "Audio" )]
    public bool ModulateVolumeWithLight { get; set; } = true;

    [Property, Group( "Audio" )]
    public bool ModulatePitchWithLight { get; set; } = false;

    [Property, Group( "Audio" ), Range( 0, 5 )]
    public float BaseVolume { get; set; } = 1.0f;

    [Property, Group( "Audio" ), Range( 0.5f, 2f )]
    public float MinPitch { get; set; } = 0.9f;

    [Property, Group( "Audio" ), Range( 0.5f, 2f )]
    public float MaxPitch { get; set; } = 1.1f;

    [Property, Group( "Optimization" )]
    public float MaxDistance { get; set; } = 2500.0f;

    [Property, Group( "Optimization" )]
    public float ShadowMaxDistance { get; set; } = 800.0f;

    [Property, Group( "Optimization" )]
    public bool EnableCulling { get; set; } = true;

    [Property, Group( "Optimization" )]
    public bool EnableAdaptiveUpdates { get; set; } = false;

    [Property, Group( "Optimization" ), Range( 1, 120 )]
    public float NearUpdateRate { get; set; } = 60.0f;

    [Property, Group( "Optimization" ), Range( 1, 120 )]
    public float FarUpdateRate { get; set; } = 12.0f;

    [Property, Group( "Gameplay" )]
    public float DefaultAlarmDuration { get; set; } = 2.0f;

    [Property, Group( "Gameplay" ), Sync( SyncFlags.FromHost )]
    public Color AlarmColor { get; set; } = new Color( 1.0f, 0.15f, 0.1f );

    [Property, Group( "Gameplay" )]
    public float AlarmStrobeSpeed { get; set; } = 8.0f;

    [Property, Group( "Gameplay" )]
    public float AlarmBrightnessMultiplier { get; set; } = 1.5f;

    [Property, FeatureEnabled( "Fire & Candle" )]
    public bool EnableFire { get; set; } = false;

    [Property, Feature( "Fire & Candle" )]
    public float FireSpeed { get; set; } = 12.0f;

    [Property, Feature( "Fire & Candle" ), Range( 0, 1 )]
    public float FireIntensity { get; set; } = 0.3f;

    [Property, Feature( "Fire & Candle" ), Range( 0, 2 )]
    public float FireChaos { get; set; } = 1.0f;

    [Property, FeatureEnabled( "Horror Mode" )]
    public bool EnableHorror { get; set; } = false;

    [Property, Feature( "Horror Mode" )]
    public float MinFlickerDelay { get; set; } = 0.05f;

    [Property, Feature( "Horror Mode" )]
    public float MaxFlickerDelay { get; set; } = 0.4f;

    [Property, Feature( "Horror Mode" ), Range( 0, 1 )]
    public float DamageSeverity { get; set; } = 0.8f;

    [Property, Feature( "Horror Mode" )]
    public SoundEvent SparkSound { get; set; }

    [Property, FeatureEnabled( "Disco Mode" )]
    public bool EnableDisco { get; set; } = false;

    [Property, Feature( "Disco Mode" )]
    public float DiscoSpeed { get; set; } = 20.0f;

    [Property, Feature( "Disco Mode" ), Range( 0, 1 )]
    public float DiscoSaturation { get; set; } = 1.0f;

    [Property, Feature( "Disco Mode" ), Range( 0, 1 )]
    public float DiscoValue { get; set; } = 1.0f;

    [Property, FeatureEnabled( "Color Transition" )]
    public bool EnableColorTransition { get; set; } = false;

    [Property, Feature( "Color Transition" ), Sync( SyncFlags.FromHost )]
    public Color SecondaryColor { get; set; } = new Color( 0.2f, 0.85f, 1.0f );

    [Property, Feature( "Color Transition" )]
    public float ColorTransitionSpeed { get; set; } = 1.0f;

    [Property, FeatureEnabled( "Proximity Sensor" )]
    public bool EnableSensor { get; set; } = false;

    [Property, Feature( "Proximity Sensor" )]
    public float SensorRange { get; set; } = 300.0f;

    [Property, Feature( "Proximity Sensor" ), Range( 0, 1 )]
    public float SensorMinBrightness { get; set; } = 0.0f;

    [Property, Feature( "Proximity Sensor" ), Range( 0, 1 )]
    public float SensorMaxBrightness { get; set; } = 1.0f;

    [Property, Feature( "Proximity Sensor" ), Range( 1, 20 )]
    public float SensorSmoothness { get; set; } = 5.0f;

    [Property, Feature( "Proximity Sensor" )]
    public bool InvertSensor { get; set; } = false;

    [Property, FeatureEnabled( "Motion Sway" )]
    public bool EnableSway { get; set; } = false;

    [Property, Feature( "Motion Sway" )]
    public float SwaySpeedPitch { get; set; } = 1.0f;

    [Property, Feature( "Motion Sway" )]
    public float SwayAmountPitch { get; set; } = 5.0f;

    [Property, Feature( "Motion Sway" )]
    public float SwaySpeedRoll { get; set; } = 0.7f;

    [Property, Feature( "Motion Sway" )]
    public float SwayAmountRoll { get; set; } = 3.0f;

    [Property, FeatureEnabled( "Flicker Pattern" )]
    public bool EnablePattern { get; set; } = false;

    [Property, Feature( "Flicker Pattern" )]
    public string Pattern { get; set; } = "mmnmmommommnonmmonqnmmo";

    [Property, Feature( "Flicker Pattern" )]
    public float PatternSpeed { get; set; } = 10.0f;

    [Property, FeatureEnabled( "Pulse" )]
    public bool EnablePulse { get; set; } = false;

    [Property, Feature( "Pulse" )]
    public float PulseSpeed { get; set; } = 1.0f;

    [Property, Feature( "Pulse" ), Range( 0, 1 )]
    public float PulseMin { get; set; } = 0.2f;

    [Property, FeatureEnabled( "Strobe" )]
    public bool EnableStrobe { get; set; } = false;

    [Property, Feature( "Strobe" )]
    public float StrobeSpeed { get; set; } = 10.0f;

    [Property, Feature( "Strobe" ), Range( 0.1f, 0.9f )]
    public float StrobeDutyCycle { get; set; } = 0.5f;

    [Property, FeatureEnabled( "Kelvin" )]
    public bool EnableKelvin { get; set; } = false;

    [Property, Feature( "Kelvin" ), Range( 1000, 12000 )]
    public int KelvinTemperature { get; set; } = 4500;

    [Property, FeatureEnabled( "Power Surge" )]
    public bool EnablePowerSurge { get; set; } = false;

    [Property, Feature( "Power Surge" )]
    public float SurgeMinInterval { get; set; } = 4.0f;

    [Property, Feature( "Power Surge" )]
    public float SurgeMaxInterval { get; set; } = 10.0f;

    [Property, Feature( "Power Surge" )]
    public float SurgeDuration { get; set; } = 0.15f;

    [Property, Feature( "Power Surge" )]
    public float SurgeBrightnessMultiplier { get; set; } = 1.8f;

    public bool Powered => PoweredState;
    public float ExternalBrightnessMultiplier => ExternalBrightnessMultiplierState;
    public bool HasColorOverride => HasExternalColorOverrideState;
    public bool AlarmActive => AlarmEndTimeState > RealTime.Now;

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

    private int _lastKelvin = -1;
    private Color _cachedKelvinColor = Color.White;
    private Rotation _baseRotation;
    private bool _isInitialized;
    private bool _hasAppliedPreset;
    private LightPreset _lastAppliedPreset = LightPreset.Custom;
    private LightPreset _lastObservedPreset = LightPreset.Custom;
    private bool _lastObservedAutoApplyPreset = true;
    private LightTypeEnum _lastSyncedLightType = LightTypeEnum.Point;
    private float _brokenMultiplier = 1.0f;
    private float _nextFlicker;
    private float _sensorWeightTarget = 1.0f;
    private float _sensorWeightCurrent = 1.0f;
    private float _randomTimeOffset;
    private float _creationTime;
    private float _lastUpdateTimestamp;
    private float _enabledBlend = 1.0f;
    private float _nextAdaptiveUpdateTime;
    private float _nextViewerCameraRefreshTime;
    private float _nextSurgeTime;
    private float _surgeEndTime;
    private bool _hasOutputState;
    private bool _lastOutputEnabled;
    private CameraComponent _cachedViewerCamera;
    private SoundHandle _ambientSoundHandle;

    [Sync( SyncFlags.FromHost )]
    private bool PoweredState { get; set; } = true;

    [Sync( SyncFlags.FromHost | SyncFlags.Interpolate )]
    private float ExternalBrightnessMultiplierState { get; set; } = 1.0f;

    [Sync( SyncFlags.FromHost )]
    private bool HasExternalColorOverrideState { get; set; }

    [Sync( SyncFlags.FromHost )]
    private Color ExternalColorOverrideState { get; set; } = Color.White;

    [Sync( SyncFlags.FromHost )]
    private float FlashEndTimeState { get; set; }

    [Sync( SyncFlags.FromHost )]
    private float FlashBrightnessMultiplierState { get; set; } = 1.0f;

    [Sync( SyncFlags.FromHost )]
    private float AlarmEndTimeState { get; set; }

    protected override void OnAwake()
    {
        EnsureNetworkMode();
    }

    protected override void OnStart()
    {
        EnsureNetworkMode();

        _baseRotation = LocalRotation;
        _creationTime = RealTime.Now;
        _lastUpdateTimestamp = _creationTime;
        _enabledBlend = IsEnabled ? 1.0f : 0.0f;

        if ( AutoDesync )
        {
            _randomTimeOffset = Game.Random.Float( 0f, 100f );
        }

        SyncComponentsIfNeeded( force: true );

        if ( AutoApplyPreset )
        {
            ApplyPresetInternal( SelectedPreset );
        }

        ScheduleNextSurge( _creationTime );

        _lastObservedPreset = SelectedPreset;
        _lastObservedAutoApplyPreset = AutoApplyPreset;
        _isInitialized = true;
    }

    protected override void OnUpdate()
    {
        if ( !_isInitialized )
        {
            return;
        }

        SyncComponentsIfNeeded();
        SyncPresetSelectionIfNeeded();

        var light = ActiveLight;
        if ( light == null )
        {
            return;
        }

        bool isPlaying = Game.IsPlaying;
        float absoluteTime = RealTime.Now;
        float effectTime = (isPlaying ? Time.Now : absoluteTime) + _randomTimeOffset;
        float deltaTime = GetFrameDelta( absoluteTime );

        if ( isPlaying && StartDelay > 0f && (absoluteTime - _creationTime) < StartDelay )
        {
            DisableOutput( light );
            return;
        }

        var viewerCam = GetViewerCamera( absoluteTime );
        float distSq = viewerCam != null ? WorldPosition.DistanceSquared( viewerCam.WorldPosition ) : 0f;

        if ( ShouldSkipAdaptiveUpdate( isPlaying, viewerCam, distSq, absoluteTime ) )
        {
            UpdateAmbientSoundPosition();
            return;
        }

        UpdateSensorWeight( viewerCam, distSq, deltaTime );
        UpdateEnabledBlend( deltaTime );
        UpdateSway( effectTime );

        if ( isPlaying && EnableCulling && viewerCam != null && distSq > MaxDistance * MaxDistance )
        {
            DisableOutput( light );
            return;
        }

        float fx = 1.0f;

        fx *= EvaluatePulseAndStrobe( effectTime );
        fx *= EvaluatePattern( effectTime );
        fx *= EvaluateFire( effectTime );
        fx *= EvaluateHorror( effectTime, isPlaying );
        fx *= EvaluatePowerSurge( absoluteTime );
        fx *= EvaluateAlarm( effectTime, absoluteTime );

        if ( absoluteTime < FlashEndTimeState )
        {
            fx *= FlashBrightnessMultiplierState;
        }

        float finalBrightness = Brightness * fx * _sensorWeightCurrent * _enabledBlend * ExternalBrightnessMultiplierState;
        finalBrightness = Math.Max( finalBrightness, 0f );

        bool shouldBeEnabled = finalBrightness > 0.001f;
        light.Enabled = shouldBeEnabled;

        Color resolvedColor = ResolveLightColor( effectTime, absoluteTime );
        light.LightColor = resolvedColor * finalBrightness;
        light.Shadows = CastShadows && ( !isPlaying || distSq < ShadowMaxDistance * ShadowMaxDistance );
        light.FogStrength = VolumetricBoost;

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

    public void TurnOn()
    {
        SetEnabledState( true );
    }

    [Button]
    public void ApplySelectedPreset()
    {
        ApplyPreset( SelectedPreset );
    }

    public void TurnOff()
    {
        SetEnabledState( false );
    }

    public void Toggle()
    {
        if ( ShouldIgnoreNetworkMutation() )
        {
            return;
        }

        IsEnabled = !IsEnabled;
    }

    public void SetPowered( bool powered )
    {
        if ( ShouldIgnoreNetworkMutation() )
        {
            return;
        }

        PoweredState = powered;
    }

    public void SetExternalBrightness( float multiplier )
    {
        if ( ShouldIgnoreNetworkMutation() )
        {
            return;
        }

        ExternalBrightnessMultiplierState = Math.Max( multiplier, 0f );
    }

    public void ResetExternalBrightness()
    {
        if ( ShouldIgnoreNetworkMutation() )
        {
            return;
        }

        ExternalBrightnessMultiplierState = 1.0f;
    }

    public void SetColorOverride( Color color )
    {
        if ( ShouldIgnoreNetworkMutation() )
        {
            return;
        }

        HasExternalColorOverrideState = true;
        ExternalColorOverrideState = color;
    }

    public void ClearColorOverride()
    {
        if ( ShouldIgnoreNetworkMutation() )
        {
            return;
        }

        HasExternalColorOverrideState = false;
        ExternalColorOverrideState = Color.White;
    }

    public void TriggerFlash( float duration = 0.15f, float brightnessMultiplier = 2f )
    {
        if ( ShouldIgnoreNetworkMutation() )
        {
            return;
        }

        FlashEndTimeState = RealTime.Now + Math.Max( duration, 0.01f );
        FlashBrightnessMultiplierState = Math.Max( brightnessMultiplier, 1.0f );
    }

    public void TriggerAlarm( float duration = -1f )
    {
        if ( ShouldIgnoreNetworkMutation() )
        {
            return;
        }

        if ( duration <= 0f )
        {
            duration = DefaultAlarmDuration;
        }

        AlarmEndTimeState = Math.Max( AlarmEndTimeState, RealTime.Now + duration );
    }

    public void ClearAlarm()
    {
        if ( ShouldIgnoreNetworkMutation() )
        {
            return;
        }

        AlarmEndTimeState = 0f;
    }

    [Button]
    public void PreviewFlash()
    {
        TriggerFlash();
    }

    [Button]
    public void PreviewAlarm()
    {
        TriggerAlarm();
    }

    [Button]
    public void ResetRuntimeOverrides()
    {
        if ( ShouldIgnoreNetworkMutation() )
        {
            return;
        }

        ClearAlarm();
        ClearColorOverride();
        ResetExternalBrightness();
        SetPowered( true );
    }

    public void ApplyPreset( LightPreset preset )
    {
        if ( ShouldIgnoreNetworkMutation() )
        {
            return;
        }

        ApplyPresetInternal( preset );
    }

    public void SetEnabledState( bool isEnabled )
    {
        if ( ShouldIgnoreNetworkMutation() )
        {
            return;
        }

        IsEnabled = isEnabled;
    }

    public void SetBrightnessLevel( float brightness )
    {
        if ( ShouldIgnoreNetworkMutation() )
        {
            return;
        }

        Brightness = Math.Max( brightness, 0f );
    }

    public void SetLightColorValue( Color color )
    {
        if ( ShouldIgnoreNetworkMutation() )
        {
            return;
        }

        LightColor = color;
    }

    private void ApplyPresetInternal( LightPreset preset )
    {
        SelectedPreset = preset;
        ResetPresetControlledFeatures();

        switch ( preset )
        {
            case LightPreset.Candle:
                Brightness = 0.75f;
                LightColor = new Color( 1.0f, 0.76f, 0.5f );
                SecondaryColor = new Color( 1.0f, 0.66f, 0.35f );
                EnableKelvin = true;
                KelvinTemperature = 1800;
                EnableFire = true;
                FireSpeed = 10.0f;
                FireIntensity = 0.18f;
                FireChaos = 0.6f;
                VolumetricBoost = 0.6f;
                break;

            case LightPreset.Torch:
                Brightness = 1.35f;
                LightColor = new Color( 1.0f, 0.72f, 0.38f );
                SecondaryColor = new Color( 1.0f, 0.45f, 0.2f );
                EnableKelvin = true;
                KelvinTemperature = 2200;
                EnableFire = true;
                FireSpeed = 13.0f;
                FireIntensity = 0.28f;
                FireChaos = 1.0f;
                VolumetricBoost = 1.4f;
                break;

            case LightPreset.Neon:
                Brightness = 1.15f;
                LightColor = new Color( 0.2f, 0.95f, 1.0f );
                SecondaryColor = new Color( 1.0f, 0.2f, 0.85f );
                EnableColorTransition = true;
                ColorTransitionSpeed = 0.65f;
                EnablePulse = true;
                PulseSpeed = 1.2f;
                PulseMin = 0.75f;
                CastShadows = false;
                VolumetricBoost = 0.25f;
                break;

            case LightPreset.Alarm:
                Brightness = 2.0f;
                LightColor = new Color( 1.0f, 0.18f, 0.12f );
                AlarmColor = LightColor;
                EnableStrobe = true;
                StrobeSpeed = 7.0f;
                StrobeDutyCycle = 0.45f;
                CastShadows = false;
                VolumetricBoost = 1.1f;
                break;

            case LightPreset.BrokenLamp:
                Brightness = 1.0f;
                LightColor = new Color( 1.0f, 0.93f, 0.82f );
                EnableHorror = true;
                MinFlickerDelay = 0.04f;
                MaxFlickerDelay = 0.25f;
                DamageSeverity = 0.85f;
                EnableKelvin = true;
                KelvinTemperature = 3400;
                break;

            case LightPreset.SciFi:
                Brightness = 1.6f;
                LightColor = new Color( 0.35f, 0.78f, 1.0f );
                SecondaryColor = new Color( 0.1f, 1.0f, 0.8f );
                EnableColorTransition = true;
                ColorTransitionSpeed = 1.15f;
                EnablePulse = true;
                PulseSpeed = 0.85f;
                PulseMin = 0.55f;
                VolumetricBoost = 2.0f;
                break;

            case LightPreset.StreetLight:
                Brightness = 1.4f;
                LightColor = new Color( 1.0f, 0.84f, 0.68f );
                EnableKelvin = true;
                KelvinTemperature = 3500;
                MaxDistance = 4500.0f;
                ShadowMaxDistance = 1200.0f;
                VolumetricBoost = 0.55f;
                break;

            case LightPreset.Custom:
            default:
                break;
        }

        _lastAppliedPreset = preset;
        _hasAppliedPreset = true;
    }

    private void ResetPresetControlledFeatures()
    {
        EnableFire = false;
        EnableHorror = false;
        EnableDisco = false;
        EnableColorTransition = false;
        EnablePulse = false;
        EnableStrobe = false;
        EnableKelvin = false;
    }

    private void SyncPresetSelectionIfNeeded()
    {
        bool autoApplyChanged = AutoApplyPreset != _lastObservedAutoApplyPreset;
        bool presetChanged = SelectedPreset != _lastObservedPreset;

        _lastObservedAutoApplyPreset = AutoApplyPreset;
        _lastObservedPreset = SelectedPreset;

        if ( !AutoApplyPreset )
        {
            return;
        }

        if ( autoApplyChanged || presetChanged || !_hasAppliedPreset || SelectedPreset != _lastAppliedPreset )
        {
            ApplyPresetInternal( SelectedPreset );
        }
    }

    private void SyncComponentsIfNeeded( bool force = false )
    {
        bool missingActiveLight = TargetLightType == LightTypeEnum.Point ? _pointLight == null : _spotLight == null;
        if ( !force && !missingActiveLight && TargetLightType == _lastSyncedLightType )
        {
            return;
        }

        Component createdComponent = null;

        if ( TargetLightType == LightTypeEnum.Point )
        {
            if ( _pointLight == null )
            {
                _pointLight = Components.GetOrCreate<PointLight>();
                createdComponent = _pointLight;
            }

            if ( _spotLight != null && _spotLight.Enabled )
            {
                _spotLight.Enabled = false;
            }
        }
        else
        {
            if ( _spotLight == null )
            {
                _spotLight = Components.GetOrCreate<SpotLight>();
                createdComponent = _spotLight;
            }

            if ( _pointLight != null && _pointLight.Enabled )
            {
                _pointLight.Enabled = false;
            }
        }

        _lastSyncedLightType = TargetLightType;

        if ( createdComponent != null && Game.IsPlaying && GameObject.NetworkMode == NetworkMode.Object )
        {
            GameObject.Network.Refresh( createdComponent );
        }
    }

    private CameraComponent GetViewerCamera( float absoluteTime )
    {
        var sceneCamera = Scene.Camera;
        if ( sceneCamera != null )
        {
            _cachedViewerCamera = sceneCamera;
            _nextViewerCameraRefreshTime = absoluteTime + 0.25f;
            return sceneCamera;
        }

        if ( _cachedViewerCamera != null && absoluteTime < _nextViewerCameraRefreshTime )
        {
            return _cachedViewerCamera;
        }

        _nextViewerCameraRefreshTime = absoluteTime + 0.25f;
        _cachedViewerCamera = Scene.GetAllComponents<CameraComponent>().FirstOrDefault( camera => camera != null && camera.Enabled );
        return _cachedViewerCamera;
    }

    private void EnsureNetworkMode()
    {
        if ( !ForceNetworkObjectMode || !Game.IsPlaying || GameObject.NetworkMode == NetworkMode.Object )
        {
            return;
        }

        GameObject.NetworkMode = NetworkMode.Object;
    }

    private bool ShouldIgnoreNetworkMutation()
    {
        return Game.IsPlaying && IsProxy;
    }

    private float GetFrameDelta( float absoluteTime )
    {
        float delta = Math.Clamp( absoluteTime - _lastUpdateTimestamp, 0.0001f, 0.25f );
        _lastUpdateTimestamp = absoluteTime;
        return delta;
    }

    private bool ShouldSkipAdaptiveUpdate( bool isPlaying, CameraComponent viewerCam, float distSq, float absoluteTime )
    {
        if ( !EnableAdaptiveUpdates || !isPlaying || viewerCam == null )
        {
            return false;
        }

        if ( absoluteTime < _nextAdaptiveUpdateTime )
        {
            return true;
        }

        float maxDistanceSq = Math.Max( MaxDistance * MaxDistance, 1f );
        float distRatio = Math.Clamp( distSq / maxDistanceSq, 0f, 1f );
        float nearInterval = 1f / Math.Max( NearUpdateRate, 1f );
        float farInterval = 1f / Math.Max( FarUpdateRate, 1f );
        _nextAdaptiveUpdateTime = absoluteTime + MathX.Lerp( nearInterval, farInterval, distRatio );
        return false;
    }

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

        _sensorWeightCurrent = MathX.Lerp( _sensorWeightCurrent, _sensorWeightTarget, Math.Clamp( deltaTime * SensorSmoothness, 0f, 1f ) );
    }

    private void UpdateEnabledBlend( float deltaTime )
    {
        float target = IsEnabled && PoweredState ? 1.0f : 0.0f;

        if ( !EnableFade )
        {
            _enabledBlend = target;
            return;
        }

        float duration = target > _enabledBlend ? Math.Max( FadeInDuration, 0.0001f ) : Math.Max( FadeOutDuration, 0.0001f );
        float lerp = Math.Clamp( deltaTime / duration, 0f, 1f );
        _enabledBlend = MathX.Lerp( _enabledBlend, target, lerp );

        if ( Math.Abs( _enabledBlend - target ) < 0.001f )
        {
            _enabledBlend = target;
        }
    }

    private void UpdateSway( float effectTime )
    {
        if ( EnableSway )
        {
            float pitch = MathF.Sin( effectTime * SwaySpeedPitch ) * SwayAmountPitch;
            float roll = MathF.Cos( effectTime * SwaySpeedRoll ) * SwayAmountRoll;
            LocalRotation = _baseRotation * Rotation.From( pitch, 0f, roll );
            return;
        }

        LocalRotation = _baseRotation;
    }

    private float EvaluatePulseAndStrobe( float effectTime )
    {
        if ( EnableStrobe )
        {
            float cycle = (effectTime * StrobeSpeed) % 1.0f;
            return cycle < StrobeDutyCycle ? 1.0f : 0.0f;
        }

        if ( EnablePulse )
        {
            float sine = (MathF.Sin( effectTime * PulseSpeed * 2.0f ) + 1.0f) * 0.5f;
            return MathX.Lerp( PulseMin, 1.0f, sine );
        }

        return 1.0f;
    }

    private float EvaluatePattern( float effectTime )
    {
        if ( !EnablePattern || string.IsNullOrWhiteSpace( Pattern ) )
        {
            return 1.0f;
        }

        int index = (int)(effectTime * PatternSpeed) % Pattern.Length;
        float value = Math.Max( 0, (char.ToLower( Pattern[index] ) - 'a') / 12.0f );
        return value;
    }

    private float EvaluateFire( float effectTime )
    {
        if ( !EnableFire )
        {
            return 1.0f;
        }

        float noise = MathF.Sin( effectTime * FireSpeed ) + MathF.Sin( effectTime * FireSpeed * 0.5f );

        if ( FireChaos > 0f )
        {
            noise += MathF.Sin( effectTime * FireSpeed * 1.5f ) * FireChaos;
        }

        return 1.0f - (noise * 0.15f * FireIntensity);
    }

    private float EvaluateHorror( float effectTime, bool isPlaying )
    {
        if ( !EnableHorror )
        {
            return 1.0f;
        }

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

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

        return _brokenMultiplier;
    }

    private float EvaluatePowerSurge( float absoluteTime )
    {
        if ( !EnablePowerSurge )
        {
            return 1.0f;
        }

        if ( _nextSurgeTime <= 0f )
        {
            ScheduleNextSurge( absoluteTime );
        }

        if ( absoluteTime >= _nextSurgeTime )
        {
            _surgeEndTime = absoluteTime + Math.Max( SurgeDuration, 0.01f );
            ScheduleNextSurge( _surgeEndTime );
        }

        return absoluteTime < _surgeEndTime ? Math.Max( SurgeBrightnessMultiplier, 1.0f ) : 1.0f;
    }

    private float EvaluateAlarm( float effectTime, float absoluteTime )
    {
        if ( absoluteTime >= AlarmEndTimeState )
        {
            return 1.0f;
        }

        float cycle = (effectTime * AlarmStrobeSpeed) % 1.0f;
        float gate = cycle < 0.5f ? 1.0f : 0.15f;
        return gate * Math.Max( AlarmBrightnessMultiplier, 0f );
    }

    private void ScheduleNextSurge( float absoluteTime )
    {
        float minInterval = Math.Min( SurgeMinInterval, SurgeMaxInterval );
        float maxInterval = Math.Max( SurgeMinInterval, SurgeMaxInterval );
        _nextSurgeTime = absoluteTime + Game.Random.Float( Math.Max( minInterval, 0.01f ), Math.Max( maxInterval, 0.01f ) );
    }

    private Color ResolveLightColor( float effectTime, float absoluteTime )
    {
        Color color = LightColor;

        if ( EnableKelvin )
        {
            if ( KelvinTemperature != _lastKelvin )
            {
                _cachedKelvinColor = KelvinToColor( KelvinTemperature );
                _lastKelvin = KelvinTemperature;
            }

            color = _cachedKelvinColor;
        }

        if ( EnableColorTransition )
        {
            float lerp = (MathF.Sin( effectTime * ColorTransitionSpeed ) + 1.0f) * 0.5f;
            color = Color.Lerp( color, SecondaryColor, lerp, true );
        }

        if ( EnableDisco )
        {
            color = new ColorHsv( (effectTime * DiscoSpeed) % 360f, DiscoSaturation, DiscoValue ).ToColor();
        }

        if ( absoluteTime < AlarmEndTimeState )
        {
            color = Color.Lerp( color, AlarmColor, 0.85f, true );
        }

        if ( HasExternalColorOverrideState )
        {
            color = ExternalColorOverrideState;
        }

        return color;
    }

    private void UpdateOutputState( bool shouldBeEnabled, bool playOneShot )
    {
        if ( !_hasOutputState )
        {
            _hasOutputState = true;
            _lastOutputEnabled = shouldBeEnabled;
            return;
        }

        if ( _lastOutputEnabled == shouldBeEnabled )
        {
            return;
        }

        if ( playOneShot && Game.IsPlaying )
        {
            if ( shouldBeEnabled && ToggleOnSound != null )
            {
                Sound.Play( ToggleOnSound, WorldPosition );
            }
            else if ( !shouldBeEnabled && ToggleOffSound != null )
            {
                Sound.Play( ToggleOffSound, WorldPosition );
            }
        }

        _lastOutputEnabled = shouldBeEnabled;
    }

    private void DisableOutput( Light light )
    {
        light.Enabled = false;
        UpdateOutputState( false, false );
        ManageAudio( false, 0f );
    }

    private void UpdateAmbientSoundPosition()
    {
        if ( _ambientSoundHandle != null && !_ambientSoundHandle.IsStopped )
        {
            _ambientSoundHandle.Position = WorldPosition;
        }
    }

    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);
                _ambientSoundHandle.Pitch = ModulatePitchWithLight ? MathX.Lerp( MinPitch, MaxPitch, intensityRatio ) : 1.0f;
            }
        }
        else if ( _ambientSoundHandle != null )
        {
            _ambientSoundHandle.Stop();
            _ambientSoundHandle = null;
        }
    }

    private Color KelvinToColor( int kelvin )
    {
        float temperature = kelvin / 100.0f;
        float red;
        float green;
        float blue;

        if ( temperature <= 66f )
        {
            red = 255f;
            green = Math.Clamp( 99.47f * MathF.Log( temperature ) - 161.11f, 0f, 255f );
        }
        else
        {
            red = Math.Clamp( 329.698f * MathF.Pow( temperature - 60f, -0.133f ), 0f, 255f );
            green = Math.Clamp( 288.12f * MathF.Pow( temperature - 60f, -0.075f ), 0f, 255f );
        }

        if ( temperature >= 66f )
        {
            blue = 255f;
        }
        else if ( temperature <= 19f )
        {
            blue = 0f;
        }
        else
        {
            blue = Math.Clamp( 138.51f * MathF.Log( temperature - 10f ) - 305.04f, 0f, 255f );
        }

        return new Color( red / 255f, green / 255f, blue / 255f );
    }

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

        Gizmo.Draw.Text( $"Group: {LightGroup}", new Transform( Vector3.Up * 20f ), size: 12 );

        if ( !string.IsNullOrWhiteSpace( PowerGridTag ) )
        {
            Gizmo.Draw.Text( $"Grid: {PowerGridTag}", new Transform( Vector3.Up * 34f ), size: 12 );
        }

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

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

    protected override void OnDestroy()
    {
        if ( _ambientSoundHandle != null )
        {
            _ambientSoundHandle.Stop();
        }
    }
}
using System;
using Sandbox;
using Sandbox.ui;

public sealed class SceneGrassComponent : Component
{
    public static bool EditorPainterActive { get; set; }

	public GrassRenderObject Renderer { get; private set; }

	[Property] public GrassDensityMapResource DensityMapResource { get; set; }

	[Property] public float ChunkSize { get; set; } = 256.0f;

	[Property] public int ChunkResolution { get; set; } = 64;

	[Property] public int RenderRadius { get; set; } = 6;

	[Property] public float StreamingRadius { get; set; } = 1536.0f;

	[Property] public float LodCutoff { get; set; } = 2048.0f;

	[Property] public float DistanceCutoff { get; set; } = 4096.0f;

	[Property] public float LodTransitionRange { get; set; } = 200.0f;

	[Property] public float DistanceTransitionRange { get; set; } = 200.0f;

	[Property] public float DisplacementStrength { get; set; } = 200.0f;

	[Property] public float TerrainProbeTop { get; set; } = 4096.0f;

	[Property] public float TerrainProbeBottom { get; set; } = -4096.0f;

	[Property] public float TerrainHeightOffset { get; set; } = 0.0f;

	[Property] public float GrassHeightPadding { get; set; } = 128.0f;

	[Property] public float FallbackHeight { get; set; } = 0.0f;

	[Property] public float InteractionStrength { get; set; } = 8.0f;

	[Property] public float InteractionStampRate { get; set; } = 36.0f;

	[Property] public float InteractionBendHoldDuration { get; set; } = 0.5f;

	[Property] public float InteractionDecayUpdateInterval { get; set; } = 0.05f;

	[Property] public float CutDuration { get; set; } = 8.0f;

	protected override void OnAwake()
	{
		base.OnAwake();

		using ( Scene.Push() )
		{
			Renderer = new GrassRenderObject( Scene.SceneWorld );

		}

		SyncRendererSettings();
	}

	private void SyncRendererSettings()
	{
		if ( Renderer == null )
			return;

		float streamingRadius = StreamingRadius;
		if ( EditorPainterActive )
			streamingRadius *= 10.0f;

		Renderer.ChunkSize = ChunkSize;
		Renderer.ChunkResolution = ChunkResolution;
		float requiredStreamingRadius = Math.Max( streamingRadius, Math.Max( LodCutoff, DistanceCutoff ) + ChunkSize );
		Renderer.RenderRadius = Math.Max( 1, MathX.CeilToInt( requiredStreamingRadius / Math.Max( ChunkSize, 0.001f ) ) );
		Renderer.LodCutoff = LodCutoff;
		Renderer.LodTransitionRange = LodTransitionRange;
		Renderer.DistanceTransitionRange = DistanceTransitionRange;
		Renderer.DistanceCutoff = DistanceCutoff;
		Renderer.DisplacementStrength = DisplacementStrength;
		Renderer.TerrainProbeTop = TerrainProbeTop;
		Renderer.TerrainProbeBottom = TerrainProbeBottom;
		Renderer.TerrainHeightOffset = TerrainHeightOffset;
		Renderer.GrassHeightPadding = GrassHeightPadding;
		Renderer.FallbackHeight = FallbackHeight;
		Renderer.InteractionStrength = InteractionStrength;
		Renderer.InteractionStampRate = InteractionStampRate;
		Renderer.InteractionBendHoldDuration = InteractionBendHoldDuration;
		Renderer.CutDuration = CutDuration;
		Renderer.InteractionDecayUpdateInterval = InteractionDecayUpdateInterval;
		Renderer.SetDensityResource( DensityMapResource );
		Renderer.CullingCamera = Scene.Camera;
}

	protected override void OnUpdate()
	{
		base.OnUpdate();

		UpdateRenderer();

	}

	private void UpdateRenderer()
	{
		if ( Renderer == null )
			return;

		if ( Scene.Camera == null )
			return;

		SyncRendererSettings();
		Renderer.SetDensityResource( DensityMapResource );
		Renderer.CullingCamera = Scene.Camera;
		Renderer.UpdateInteractionField( Scene.GetAllComponents<GrassInteractionSourceComponent>(), Time.Delta );

		Vector3 camPos = Scene.Camera.WorldPosition;

		Renderer.UpdateStreaming( camPos );

		Renderer.ProcessPendingDestroy();
	}

	protected override void OnDisabled()
	{
		base.OnDisabled();

		Renderer?.Disable();

		Renderer = null;
	}

	protected override void DrawGizmos()
	{
		base.DrawGizmos();

		UpdateRenderer();

		if ( !EditorPainterActive )
			return;

		if ( Renderer == null )
			return;

		Gizmo.Draw.Color = Color.Yellow;

		foreach ( var pair in Renderer.ActiveChunks )
		{
			BBox bounds = BBox.FromPositionAndSize( pair.Value.Bounds.Center, pair.Value.Bounds.Size.WithZ( 0 ) );
			Gizmo.Draw.LineBBox( bounds );
		}
	}
}
using System;
using System.Collections.Generic;
using System.Text;

namespace Saandy.Tilemapper;

public interface ITilemapSceneEvent : ISceneEvent<ITilemapSceneEvent>
{

	/// <summary>
	/// Called when tilemap was changed this frame.
	/// </summary>
	void OnTilemapChanged() { }

	/// <summary>
	/// Called when the tilemap has been updated recently and stopped being updated.
	/// </summary>
	void OnTilemapStable() { }
}
using PanelRenderTarget;
using Sandbox;
using Sandbox.UI;
using System;
using System.Linq;

public class TargetScreen : Component, Component.DontExecuteOnServer, ITargetScreen
{
	[Property, Feature( "interaction" ), Description( "Enable verbose logs for screen mouse detection" )]
	public bool DebugMouseTrace { get; set; } = false;

	[Property] public string ScreenMaterialName { get; set; } = "screen-01";
	[Property] public Material ScreenMaterial { get; set; } = Material.Load( "materials/screen.vmat" );
	[Property] public Vector2Int ScreenTextureSize { get; set; } = new( 1280, 720 );
	[Property] public float TraceDistance { get; set; } = 200f;
	[Property] public bool ForceUpdate { get; set; } = false;
	[Property] public PanelTypeReference PanelType { get; set; } = new();
	[Property, Feature( "interaction" ), Description( "Small UV offset applied after triangle interpolation to correct slight drift" )]
	public Vector2 ScreenUvOffset { get; set; } = Vector2.Zero;

	[Property, Feature("interaction"), Description("interact with 2d mouse cursor")]
	public bool ScreenCursorInteraction { get; set; } = false;

	[Property,Feature("interaction"), Description("whether to show the virtual cursor") ]
	public bool ShowVirtualCursor { get; set; } = true;

	[Property, Feature( "Optimisation" ), Description("fps when the panel is focused") ]
	public int UpdateRateFocus { get; set; } = 60;

	[Property, Feature("Optimisation")]
	public bool UpdateWhenNotFocused { get; set; } = false;

	[Property, Feature( "Optimisation" ), Description( "fps update rate when the panel is visible in camera" ) ]
	public int UpdateRateNotFocused { get; set; } = 30;

	[Property, Feature( "Optimisation" ), Description("the distance of the update rate visible but not focus") ]
	public int UpdateDistanceMax { get; set; } = 500;


	private bool _firstUpdate = false;



	private readonly TargetPanelInput _input = new();

	public ModelRenderer Renderer { get; private set; }
	private Material _screenMaterialCopy;
	private Texture _screenTexture;
	private TargetRootPanel _rootPanel;
	private PanelSceneObject _panelObject;
	private Vertex[] _cachedVertices;
	private uint[] _cachedIndices;
	private Model _cachedMeshModel;
	private TriangleMaterialRange[] _cachedTriangleMaterialRanges = Array.Empty<TriangleMaterialRange>();
	private double _tracePerfAccumMs;
	private double _tracePerfMaxMs;
	private double _tracePerfCacheAccumMs;
	private double _tracePerfTrianglesAccumMs;
	private int _tracePerfSamples;
	private TimeSince _tracePerfLogTimer;

	private readonly struct TriangleMaterialRange
	{
		public int StartTriangle { get; init; }
		public int EndTriangleExclusive { get; init; }
		public Material Material { get; init; }
	}

	protected Panel Panel { get; private set; }
	public TargetRootPanel RootPanel => _rootPanel;
	public PanelSceneObject PanelObject => _panelObject;
	protected Texture ScreenTexture => _screenTexture;

	protected override void OnPreRender()
	{
		//ensure sceneobject have transform and correct bound for engine culling
		_panelObject.Transform = Renderer.Transform.World;
		_panelObject.Bounds = Renderer.Bounds;
	}

	protected override void OnEnabled()
	{
		base.OnEnabled();

		Renderer = Components.Get<ModelRenderer>();

		if ( !Renderer.IsValid() )
		{
			Log.Warning( "No ModelRenderer found." );
			Enabled = false;
			return;
		}

		RefreshMeshCache();
		CreateTexture();
		CreatePanel();
		SetupMaterial();
		CreatePanelObject();

		OnPanelCreated( Panel );

		var panelComponent = Components.Get<PanelComponent>();
		TargetPanelSystem.Current.RegisterScreen( this );
	}

	public Panel GetPanel()
	{
		return Panel;
	}

	protected virtual void OnPanelCreated( Panel panel )
	{
	}

	private void CreateTexture()
	{
		_screenTexture = Texture.CreateRenderTarget()
			.WithSize( ScreenTextureSize.x, ScreenTextureSize.y )
			.WithInitialColor( Color.Black )
			.WithMips()
			.Create();
	}

	private void CreatePanel()
	{
		var bounds = new Rect( 0, 0, ScreenTextureSize.x, ScreenTextureSize.y );

		_rootPanel = new TargetRootPanel
		{
			RenderedManually = true,
			FixedBounds = bounds,
			FixedScale = 1f,
			PanelBounds = bounds,
			MouseVisibility = ScreenCursorInteraction ? MouseVisibility.Visible : MouseVisibility.Hidden
		};

		_rootPanel.Style.Width = Length.Pixels( ScreenTextureSize.x );
		_rootPanel.Style.Height = Length.Pixels( ScreenTextureSize.y );

		var type = PanelType?.Resolve();

		if ( type?.TargetType is null || !typeof( Panel ).IsAssignableFrom( type.TargetType ) )
		{
			Log.Warning( $"Invalid panel type: {PanelType?.TypeName}" );
			return;
		}

		Panel = type.Create<Panel>();
		_rootPanel.AddChild( Panel );

		Panel.Style.Width = Length.Percent( 100 );
		Panel.Style.Height = Length.Percent( 100 );
	}

	private void CreatePanelObject()
	{
		_panelObject = new PanelSceneObject(
			GameObject.GetBounds(),
			Scene.SceneWorld,
			_rootPanel,
			_screenTexture,
			this
		);
	}

	public void Tick()
	{
		if ( !Renderer.IsValid() || Renderer.Model is null || !Panel.IsValid() )
			return;

		var camera = Scene.Camera;

		if ( camera is null )
			return;

		if ( !TryGetPanelPosition( camera, out var panelPos ) )
		{
			ClearInput();
			return;
		}

		_panelObject.CursorPosition = panelPos;
		
		_input.Tick(
			_rootPanel,
			panelPos,
			Input.Down( "attack1" ),
			Input.MouseWheel
		);

	}


	private bool TryGetPanelPosition( CameraComponent camera, out Vector2 panelPos )
	{
		panelPos = default;
		
		var screenCenter = new Vector2(
			camera.ScreenRect.Size.x * 0.5f,
			camera.ScreenRect.Size.y * 0.5f
		);

		var ray = camera.ScreenPixelToRay( screenCenter );

		if( ScreenCursorInteraction )
		{
			ray = camera.ScreenPixelToRay( Mouse.Position );
		}

		return TryGetPanelPositionFromMeshRaycast( ray, out panelPos );
	}

	private bool TryGetPanelPositionFromMeshRaycast( Ray ray, out Vector2 panelPos )
	{
		using var scope = Sandbox.Diagnostics.Performance.Scope( "TargetScreen.MouseTrace" );
		var totalTimer = Sandbox.Diagnostics.FastTimer.StartNew();
		panelPos = default;

		var cacheTimer = Sandbox.Diagnostics.FastTimer.StartNew();
		RefreshMeshCache();
		var cacheMs = cacheTimer.ElapsedMilliSeconds;

		if ( _cachedVertices is null || _cachedIndices is null || _cachedIndices.Length < 3 )
		{
			if ( DebugMouseTrace )
				Log.Info( $"[TargetScreen] Reject mesh cache: vertices={_cachedVertices?.Length ?? 0} indices={_cachedIndices?.Length ?? 0}" );
			UpdateDebugTracePerf( totalTimer.ElapsedMilliSeconds, cacheMs, 0f );
			return false;
		}

		var localOrigin = Renderer.Transform.World.PointToLocal( ray.Position );
		var localEnd = Renderer.Transform.World.PointToLocal( ray.Position + ray.Forward * TraceDistance );
		var localDirection = (localEnd - localOrigin).Normal;

		var bestDistance = float.MaxValue;
		var bestUv = Vector2.Zero;
		var bestTriangle = -1;
		var triangleTimer = Sandbox.Diagnostics.FastTimer.StartNew();

		for ( int triStart = 0; triStart + 2 < _cachedIndices.Length; triStart += 3 )
		{
			var triangleIndex = triStart / 3;
			if ( !IsTriangleMaterialMatch( triangleIndex ) )
				continue;

			var i0 = _cachedIndices[triStart + 0];
			var i1 = _cachedIndices[triStart + 1];
			var i2 = _cachedIndices[triStart + 2];
			if ( i0 >= _cachedVertices.Length || i1 >= _cachedVertices.Length || i2 >= _cachedVertices.Length )
				continue;

			var v0 = _cachedVertices[i0];
			var v1 = _cachedVertices[i1];
			var v2 = _cachedVertices[i2];

			if ( !TryRayTriangleIntersection( localOrigin, localDirection, v0.Position, v1.Position, v2.Position, out var distance, out var barycentric ) )
				continue;

			if ( distance > TraceDistance || distance >= bestDistance )
				continue;

			var triangleUv =
				v0.TexCoord0 * barycentric.x +
				v1.TexCoord0 * barycentric.y +
				v2.TexCoord0 * barycentric.z;

			bestDistance = distance;
			bestUv = triangleUv;
			bestTriangle = triangleIndex;
		}

		var triangleMs = triangleTimer.ElapsedMilliSeconds;

		if ( bestTriangle < 0 )
		{
			if ( DebugMouseTrace )
				Log.Info( $"[TargetScreen] No triangle hit. vertices={_cachedVertices.Length} indices={_cachedIndices.Length}" );
			UpdateDebugTracePerf( totalTimer.ElapsedMilliSeconds, cacheMs, triangleMs );
			return false;
		}

		if ( DebugMouseTrace )
			Log.Info( $"[TargetScreen] Mesh hit triangle={bestTriangle} distance={bestDistance} rawUv={bestUv}" );

		var uv = new Vector2(
			bestUv.x - MathF.Floor( bestUv.x ),
			bestUv.y - MathF.Floor( bestUv.y )
		);

		uv += ScreenUvOffset;

		panelPos = new Vector2(
			Math.Clamp( uv.x, 0f, 1f ) * ScreenTextureSize.x,
			Math.Clamp( uv.y, 0f, 1f ) * ScreenTextureSize.y
		);

		if ( DebugMouseTrace )
			Log.Info( $"[TargetScreen] Final UV={uv} panelPos={panelPos}" );

		UpdateDebugTracePerf( totalTimer.ElapsedMilliSeconds, cacheMs, triangleMs );
		return true;
	}

	private void UpdateDebugTracePerf( double totalMs, double cacheMs, double triangleMs )
	{
		_tracePerfAccumMs += totalMs;
		_tracePerfCacheAccumMs += cacheMs;
		_tracePerfTrianglesAccumMs += triangleMs;
		_tracePerfMaxMs = Math.Max( _tracePerfMaxMs, totalMs );
		_tracePerfSamples++;

		if ( _tracePerfLogTimer < 5f )
			return;

		var avgTotalMs = _tracePerfSamples > 0 ? _tracePerfAccumMs / _tracePerfSamples : 0d;
		var avgCacheMs = _tracePerfSamples > 0 ? _tracePerfCacheAccumMs / _tracePerfSamples : 0d;
		var avgTrianglesMs = _tracePerfSamples > 0 ? _tracePerfTrianglesAccumMs / _tracePerfSamples : 0d;
		var callsPerSecond = _tracePerfLogTimer > 0f ? _tracePerfSamples / _tracePerfLogTimer : 0f;
		var triangleCount = _cachedIndices?.Length / 3 ?? 0;

		Log.Info( $"[TargetScreen] Trace perf avg={avgTotalMs:F3}ms max={_tracePerfMaxMs:F3}ms cache={avgCacheMs:F3}ms triangles={avgTrianglesMs:F3}ms calls={callsPerSecond:F1}/s tris={triangleCount}" );

		_tracePerfAccumMs = 0d;
		_tracePerfCacheAccumMs = 0d;
		_tracePerfTrianglesAccumMs = 0d;
		_tracePerfMaxMs = 0d;
		_tracePerfSamples = 0;
		_tracePerfLogTimer = 0f;
	}

	private void RefreshMeshCache()
	{
		var model = Renderer?.Model;
		if ( model is null || model == _cachedMeshModel )
			return;

		_cachedMeshModel = model;
		_cachedVertices = model.GetVertices();
		_cachedIndices = model.GetIndices();
		_cachedTriangleMaterialRanges = BuildTriangleMaterialRanges( model );

		if ( DebugMouseTrace )
			Log.Info( $"[TargetScreen] Refreshed mesh cache for {model.Name}: vertices={_cachedVertices?.Length ?? 0} indices={_cachedIndices?.Length ?? 0} materialRanges={_cachedTriangleMaterialRanges.Length}" );
	}

	private TriangleMaterialRange[] BuildTriangleMaterialRanges( Model model )
	{
		var meshInfo = model.MeshInfo;
		if ( meshInfo?.Meshes is null )
			return Array.Empty<TriangleMaterialRange>();

		var ranges = new System.Collections.Generic.List<TriangleMaterialRange>();
		var triangleCursor = 0;

		foreach ( var mesh in meshInfo.Meshes )
		{
			if ( mesh?.DrawCalls is null )
				continue;

			foreach ( var drawCall in mesh.DrawCalls )
			{
				var triangleCount = Math.Max( 0, drawCall.Indices / 3 );
				if ( triangleCount == 0 )
					continue;

				ranges.Add( new TriangleMaterialRange
				{
					StartTriangle = triangleCursor,
					EndTriangleExclusive = triangleCursor + triangleCount,
					Material = drawCall.Material
				} );

				if ( DebugMouseTrace )
					Log.Info( $"[TargetScreen] Material range {triangleCursor}->{triangleCursor + triangleCount} material={drawCall.Material?.Name}" );

				triangleCursor += triangleCount;
			}
		}

		return ranges.ToArray();
	}

	private bool IsTriangleMaterialMatch( int triangleIndex )
	{
		if ( string.IsNullOrWhiteSpace( ScreenMaterialName ) || _cachedTriangleMaterialRanges.Length == 0 )
			return true;

		foreach ( var range in _cachedTriangleMaterialRanges )
		{
			if ( triangleIndex < range.StartTriangle || triangleIndex >= range.EndTriangleExclusive )
				continue;

			var match = range.Material?.Name?.Contains( ScreenMaterialName, StringComparison.OrdinalIgnoreCase ) == true;
			if ( DebugMouseTrace && triangleIndex == range.StartTriangle )
				Log.Info( $"[TargetScreen] Triangle {triangleIndex} material={range.Material?.Name} match={match}" );
			return match;
		}

		return true;
	}

	private static bool TryRayTriangleIntersection( Vector3 origin, Vector3 direction, Vector3 a, Vector3 b, Vector3 c, out float distance, out Vector3 barycentric )
	{
		distance = 0f;
		barycentric = default;

		const float epsilon = 0.0001f;
		var edge1 = b - a;
		var edge2 = c - a;
		var pvec = Vector3.Cross( direction, edge2 );
		var det = Vector3.Dot( edge1, pvec );
		if ( MathF.Abs( det ) < epsilon )
			return false;

		var invDet = 1f / det;
		var tvec = origin - a;
		var v = Vector3.Dot( tvec, pvec ) * invDet;
		if ( v < 0f || v > 1f )
			return false;

		var qvec = Vector3.Cross( tvec, edge1 );
		var w = Vector3.Dot( direction, qvec ) * invDet;
		if ( w < 0f || v + w > 1f )
			return false;

		distance = Vector3.Dot( edge2, qvec ) * invDet;
		if ( distance < 0f )
			return false;

		barycentric = new Vector3( 1f - v - w, v, w );
		return true;
	}

	private void SetupMaterial()
	{
		var oldMaterial = Renderer.Model.Materials
			.FirstOrDefault( x => x.Name.Contains( ScreenMaterialName ) );

		var index = Renderer.Model.Materials.IndexOf( oldMaterial );

		if ( index < 0 )
		{
			Log.Warning( $"Screen material not found: {ScreenMaterialName}" );
			return;
		}

		_screenMaterialCopy = ScreenMaterial.CreateCopy();
		_screenMaterialCopy.Set( "g_tColor", _screenTexture );

		Renderer.Materials.SetOverride( index, _screenMaterialCopy );
	}

	private void ClearInput()
	{
		_input.Clear();
	}

	protected override void OnDisabled()
	{
		base.OnDisabled();

		ClearInput();

		_panelObject?.Delete();
		_panelObject = null;

		_rootPanel?.Delete( true );
		_rootPanel = null;

		_screenTexture?.Dispose();
		_screenTexture = null;

		_screenMaterialCopy = null;
		Panel = null;
		Renderer = null;

		TargetPanelSystem.Current.UnregisterScreen( this );
	}
}
@using Sandbox;
@using Sandbox.UI;
@using System.Threading.Tasks;
@using System.Collections.Generic;
@using System;
@inherits PanelComponent

<root class="@(IsFadingOut ? "fade-out" : "fade-in")" style="background-image: @(!string.IsNullOrEmpty(BackgroundImage) ? $"url({BackgroundImage})" : "none");">
    
    <div class="content">
        @* Logo Image (.png / .jpg) *@
        @if (!string.IsNullOrEmpty(LogoImage))
        {
            <img class="logo" src="@LogoImage" />
        }
        
        @* Text Lines *@
        @if (TextLines != null && TextLines.Count > 0)
        {
            <div class="text-container">
                @foreach (var line in TextLines)
                {
                    <label style="color: @line.TextColor.Hex; font-size: @(line.FontSize)px;">
                        @line.Text
                    </label>
                }
            </div>
        }
    </div>

</root>

@code {
    // === CUSTOM DATA CLASS FOR TEXT LINES ===
    
    public class SplashTextLine
    {
        [Property, Description("The text to display.")] 
        public string Text { get; set; } = "NEW LINE";

        [Property, Description("Text color for this specific line.")] 
        public Color TextColor { get; set; } = Color.White;

        [Property, Description("Font size for this specific line.")] 
        public float FontSize { get; set; } = 80f;
    }


    // === IMAGE SETTINGS ===
    
    [Property, ImageAssetPath, Group("Images"), Description("Supports .png and .jpg. If empty, the background will be black.")] 
    public string BackgroundImage { get; set; }

    [Property, ImageAssetPath, Group("Images"), Description("Main logo image (.png / .jpg). Appears above the text if both are set.")] 
    public string LogoImage { get; set; }


    // === TEXT SETTINGS ===

    [Property, Group("Text"), Description("Add text lines with individual settings (color, size).")]
    public List<SplashTextLine> TextLines { get; set; } = new();


    // === AUDIO & SCENE SETTINGS ===

    [Property, Group("Audio"), Description("Select a Sound Event (.sound) that contains your .mp3 or .ogg file.")] 
    public SoundEvent SplashSound { get; set; }

    [Property, Group("Scene"), Description("The scene to load after the splash screen finishes.")] 
    public SceneFile NextScene { get; set; }


    // === LOGIC ===
    
    public bool IsFadingOut { get; set; } = false;

    protected override void OnStart()
    {
        base.OnStart();
        
        // Start the asynchronous sequence
        _ = RunSplashSequence();
    }

    private async Task RunSplashSequence()
    {
        // 1. Wait half a second before starting to avoid stuttering during load
        await Task.Delay(500);

        // 2. Play the assigned sound (.mp3 / .ogg via Sound Event)
        if (SplashSound != null)
        {
            Sound.Play(SplashSound);
        }

        // 3. Wait while the logo/text is visible on the screen (3 seconds)
        await Task.Delay(3000);

        // 4. Trigger the fade-out animation
        IsFadingOut = true;
        StateHasChanged(); // Notify the UI to update CSS classes

        // 5. Wait for the fade-out animation to finish (matches the CSS transition time)
        await Task.Delay(2000);

        // 6. Load the next scene
        if (NextScene != null)
        {
            Scene.Load(NextScene);
        }
        else
        {
            Log.Warning("Next Scene is not assigned in the Splash Screen component!");
            GameObject.Destroy(); // Destroy the component if no scene is assigned
        }
    }
}
// <auto-generated />
// Generated by tools/StyleFacadeEmit. Do not edit by hand.
// Source of truth: tools/StyleFacadeEmit/style-manifest.json
using Sandbox;
using Sandbox.Rendering;
using Sandbox.UI;

namespace Goo;

public readonly partial record struct TextEntry
{
    public FlexDirection? FlexDirection { init => _style = StyleAccumulator.Add(_style, StyleField.FlexDirection, value, StyleValue.FromFlexDirection); }
    public Justify? JustifyContent { init => _style = StyleAccumulator.Add(_style, StyleField.JustifyContent, value, StyleValue.FromJustify); }
    public Align? AlignItems { init => _style = StyleAccumulator.Add(_style, StyleField.AlignItems, value, StyleValue.FromAlign); }
    public DisplayMode? Display { init => _style = StyleAccumulator.Add(_style, StyleField.Display, value, StyleValue.FromDisplay); }
    public Length? Width { init => _style = StyleAccumulator.Add(_style, StyleField.Width, value); }
    public Length? Height { init => _style = StyleAccumulator.Add(_style, StyleField.Height, value); }
    public Length? Padding { init => _style = StyleAccumulator.Add(_style, StyleField.Padding, value); }
    public Length? PaddingLeft { init => _style = StyleAccumulator.Add(_style, StyleField.PaddingLeft, value); }
    public Length? PaddingTop { init => _style = StyleAccumulator.Add(_style, StyleField.PaddingTop, value); }
    public Length? PaddingRight { init => _style = StyleAccumulator.Add(_style, StyleField.PaddingRight, value); }
    public Length? PaddingBottom { init => _style = StyleAccumulator.Add(_style, StyleField.PaddingBottom, value); }
    public Length? Margin { init => _style = StyleAccumulator.Add(_style, StyleField.Margin, value); }
    public Length? MarginLeft { init => _style = StyleAccumulator.Add(_style, StyleField.MarginLeft, value); }
    public Length? MarginTop { init => _style = StyleAccumulator.Add(_style, StyleField.MarginTop, value); }
    public Length? MarginRight { init => _style = StyleAccumulator.Add(_style, StyleField.MarginRight, value); }
    public Length? MarginBottom { init => _style = StyleAccumulator.Add(_style, StyleField.MarginBottom, value); }
    public Length? Gap { init => _style = StyleAccumulator.Add(_style, StyleField.Gap, value); }
    public Length? RowGap { init => _style = StyleAccumulator.Add(_style, StyleField.RowGap, value); }
    public Length? ColumnGap { init => _style = StyleAccumulator.Add(_style, StyleField.ColumnGap, value); }
    public Color? BackgroundColor { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundColor, value, StyleValue.FromColor); }
    public Length? BorderRadius { init => _style = StyleAccumulator.Add(_style, StyleField.BorderRadius, value); }
    public Length? BorderTopLeftRadius { init => _style = StyleAccumulator.Add(_style, StyleField.BorderTopLeftRadius, value); }
    public Length? BorderTopRightRadius { init => _style = StyleAccumulator.Add(_style, StyleField.BorderTopRightRadius, value); }
    public Length? BorderBottomRightRadius { init => _style = StyleAccumulator.Add(_style, StyleField.BorderBottomRightRadius, value); }
    public Length? BorderBottomLeftRadius { init => _style = StyleAccumulator.Add(_style, StyleField.BorderBottomLeftRadius, value); }
    public Align? AlignContent { init => _style = StyleAccumulator.Add(_style, StyleField.AlignContent, value, StyleValue.FromAlign); }
    public Align? AlignSelf { init => _style = StyleAccumulator.Add(_style, StyleField.AlignSelf, value, StyleValue.FromAlign); }
    public float? AspectRatio { init => _style = StyleAccumulator.Add(_style, StyleField.AspectRatio, value); }
    public Length? BackdropFilterBlur { init => _style = StyleAccumulator.Add(_style, StyleField.BackdropFilterBlur, value); }
    public Length? BackdropFilterBrightness { init => _style = StyleAccumulator.Add(_style, StyleField.BackdropFilterBrightness, value); }
    public Length? BackdropFilterContrast { init => _style = StyleAccumulator.Add(_style, StyleField.BackdropFilterContrast, value); }
    public Length? BackdropFilterHueRotate { init => _style = StyleAccumulator.Add(_style, StyleField.BackdropFilterHueRotate, value); }
    public Length? BackdropFilterInvert { init => _style = StyleAccumulator.Add(_style, StyleField.BackdropFilterInvert, value); }
    public Length? BackdropFilterSaturate { init => _style = StyleAccumulator.Add(_style, StyleField.BackdropFilterSaturate, value); }
    public Length? BackdropFilterSepia { init => _style = StyleAccumulator.Add(_style, StyleField.BackdropFilterSepia, value); }
    public Length? BackgroundAngle { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundAngle, value); }
    public string? BackgroundBlendMode { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundBlendMode, value); }
    public Texture? BackgroundImage { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundImage, value); }
    public bool? BackgroundPlaybackPaused { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundPlaybackPaused, value); }
    public Length? BackgroundPositionX { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundPositionX, value); }
    public Length? BackgroundPositionY { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundPositionY, value); }
    public BackgroundRepeat? BackgroundRepeat { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundRepeat, value, StyleValue.FromBackgroundRepeat); }
    public Length? BackgroundSizeX { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundSizeX, value); }
    public Length? BackgroundSizeY { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundSizeY, value); }
    public Color? BackgroundTint { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundTint, value, StyleValue.FromColor); }
    public Color? BorderBottomColor { init => _style = StyleAccumulator.Add(_style, StyleField.BorderBottomColor, value, StyleValue.FromColor); }
    public Color? BorderColor { init => _style = StyleAccumulator.Add(_style, StyleField.BorderColor, value, StyleValue.FromColor); }
    public Color? BorderLeftColor { init => _style = StyleAccumulator.Add(_style, StyleField.BorderLeftColor, value, StyleValue.FromColor); }
    public Color? BorderRightColor { init => _style = StyleAccumulator.Add(_style, StyleField.BorderRightColor, value, StyleValue.FromColor); }
    public Color? BorderTopColor { init => _style = StyleAccumulator.Add(_style, StyleField.BorderTopColor, value, StyleValue.FromColor); }
    public BorderImageFill? BorderImageFill { init => _style = StyleAccumulator.Add(_style, StyleField.BorderImageFill, value, StyleValue.FromBorderImageFill); }
    public BorderImageRepeat? BorderImageRepeat { init => _style = StyleAccumulator.Add(_style, StyleField.BorderImageRepeat, value, StyleValue.FromBorderImageRepeat); }
    public Texture? BorderImageSource { init => _style = StyleAccumulator.Add(_style, StyleField.BorderImageSource, value); }
    public Color? BorderImageTint { init => _style = StyleAccumulator.Add(_style, StyleField.BorderImageTint, value, StyleValue.FromColor); }
    public Length? BorderImageWidthBottom { init => _style = StyleAccumulator.Add(_style, StyleField.BorderImageWidthBottom, value); }
    public Length? BorderImageWidthLeft { init => _style = StyleAccumulator.Add(_style, StyleField.BorderImageWidthLeft, value); }
    public Length? BorderImageWidthRight { init => _style = StyleAccumulator.Add(_style, StyleField.BorderImageWidthRight, value); }
    public Length? BorderImageWidthTop { init => _style = StyleAccumulator.Add(_style, StyleField.BorderImageWidthTop, value); }
    public Length? BorderBottomWidth { init => _style = StyleAccumulator.Add(_style, StyleField.BorderBottomWidth, value); }
    public Length? BorderLeftWidth { init => _style = StyleAccumulator.Add(_style, StyleField.BorderLeftWidth, value); }
    public Length? BorderRightWidth { init => _style = StyleAccumulator.Add(_style, StyleField.BorderRightWidth, value); }
    public Length? BorderTopWidth { init => _style = StyleAccumulator.Add(_style, StyleField.BorderTopWidth, value); }
    public Length? BorderWidth { init => _style = StyleAccumulator.Add(_style, StyleField.BorderWidth, value); }
    public Length? Bottom { init => _style = StyleAccumulator.Add(_style, StyleField.Bottom, value); }
    public Length? Left { init => _style = StyleAccumulator.Add(_style, StyleField.Left, value); }
    public Length? Right { init => _style = StyleAccumulator.Add(_style, StyleField.Right, value); }
    public Length? Top { init => _style = StyleAccumulator.Add(_style, StyleField.Top, value); }
    public Color? CaretColor { init => _style = StyleAccumulator.Add(_style, StyleField.CaretColor, value, StyleValue.FromColor); }
    public string? Cursor { init => _style = StyleAccumulator.Add(_style, StyleField.Cursor, value); }
    public Length? FilterBlur { init => _style = StyleAccumulator.Add(_style, StyleField.FilterBlur, value); }
    public Color? FilterBorderColor { init => _style = StyleAccumulator.Add(_style, StyleField.FilterBorderColor, value, StyleValue.FromColor); }
    public Length? FilterBorderWidth { init => _style = StyleAccumulator.Add(_style, StyleField.FilterBorderWidth, value); }
    public Length? FilterBrightness { init => _style = StyleAccumulator.Add(_style, StyleField.FilterBrightness, value); }
    public Length? FilterContrast { init => _style = StyleAccumulator.Add(_style, StyleField.FilterContrast, value); }
    public Length? FilterHueRotate { init => _style = StyleAccumulator.Add(_style, StyleField.FilterHueRotate, value); }
    public Length? FilterInvert { init => _style = StyleAccumulator.Add(_style, StyleField.FilterInvert, value); }
    public Length? FilterSaturate { init => _style = StyleAccumulator.Add(_style, StyleField.FilterSaturate, value); }
    public Length? FilterSepia { init => _style = StyleAccumulator.Add(_style, StyleField.FilterSepia, value); }
    public Color? FilterTint { init => _style = StyleAccumulator.Add(_style, StyleField.FilterTint, value, StyleValue.FromColor); }
    public Length? FlexBasis { init => _style = StyleAccumulator.Add(_style, StyleField.FlexBasis, value); }
    public float? FlexGrow { init => _style = StyleAccumulator.Add(_style, StyleField.FlexGrow, value); }
    public float? FlexShrink { init => _style = StyleAccumulator.Add(_style, StyleField.FlexShrink, value); }
    public Wrap? FlexWrap { init => _style = StyleAccumulator.Add(_style, StyleField.FlexWrap, value, StyleValue.FromWrap); }
    public Color? FontColor { init => _style = StyleAccumulator.Add(_style, StyleField.FontColor, value, StyleValue.FromColor); }
    public string? FontFamily { init => _style = StyleAccumulator.Add(_style, StyleField.FontFamily, value); }
    public Length? FontSize { init => _style = StyleAccumulator.Add(_style, StyleField.FontSize, value); }
    public FontSmooth? FontSmooth { init => _style = StyleAccumulator.Add(_style, StyleField.FontSmooth, value, StyleValue.FromFontSmooth); }
    public FontStyle? FontStyle { init => _style = StyleAccumulator.Add(_style, StyleField.FontStyle, value, StyleValue.FromFontStyle); }
    public FontVariantNumeric? FontVariantNumeric { init => _style = StyleAccumulator.Add(_style, StyleField.FontVariantNumeric, value, StyleValue.FromFontVariantNumeric); }
    public int? FontWeight { init => _style = StyleAccumulator.Add(_style, StyleField.FontWeight, value); }
    public ImageRendering? ImageRendering { init => _style = StyleAccumulator.Add(_style, StyleField.ImageRendering, value, StyleValue.FromImageRendering); }
    public Length? LetterSpacing { init => _style = StyleAccumulator.Add(_style, StyleField.LetterSpacing, value); }
    public Length? LineHeight { init => _style = StyleAccumulator.Add(_style, StyleField.LineHeight, value); }
    public Length? MaskAngle { init => _style = StyleAccumulator.Add(_style, StyleField.MaskAngle, value); }
    public Texture? MaskImage { init => _style = StyleAccumulator.Add(_style, StyleField.MaskImage, value); }
    public MaskMode? MaskMode { init => _style = StyleAccumulator.Add(_style, StyleField.MaskMode, value, StyleValue.FromMaskMode); }
    public Length? MaskPositionX { init => _style = StyleAccumulator.Add(_style, StyleField.MaskPositionX, value); }
    public Length? MaskPositionY { init => _style = StyleAccumulator.Add(_style, StyleField.MaskPositionY, value); }
    public BackgroundRepeat? MaskRepeat { init => _style = StyleAccumulator.Add(_style, StyleField.MaskRepeat, value, StyleValue.FromBackgroundRepeat); }
    public MaskScope? MaskScope { init => _style = StyleAccumulator.Add(_style, StyleField.MaskScope, value, StyleValue.FromMaskScope); }
    public Length? MaskSizeX { init => _style = StyleAccumulator.Add(_style, StyleField.MaskSizeX, value); }
    public Length? MaskSizeY { init => _style = StyleAccumulator.Add(_style, StyleField.MaskSizeY, value); }
    public Length? MaxHeight { init => _style = StyleAccumulator.Add(_style, StyleField.MaxHeight, value); }
    public Length? MaxWidth { init => _style = StyleAccumulator.Add(_style, StyleField.MaxWidth, value); }
    public Length? MinHeight { init => _style = StyleAccumulator.Add(_style, StyleField.MinHeight, value); }
    public Length? MinWidth { init => _style = StyleAccumulator.Add(_style, StyleField.MinWidth, value); }
    public string? MixBlendMode { init => _style = StyleAccumulator.Add(_style, StyleField.MixBlendMode, value); }
    public float? Opacity { init => _style = StyleAccumulator.Add(_style, StyleField.Opacity, value); }
    public int? Order { init => _style = StyleAccumulator.Add(_style, StyleField.Order, value); }
    public Color? OutlineColor { init => _style = StyleAccumulator.Add(_style, StyleField.OutlineColor, value, StyleValue.FromColor); }
    public Length? OutlineOffset { init => _style = StyleAccumulator.Add(_style, StyleField.OutlineOffset, value); }
    public Length? OutlineWidth { init => _style = StyleAccumulator.Add(_style, StyleField.OutlineWidth, value); }
    public OverflowMode? Overflow { init => _style = StyleAccumulator.Add(_style, StyleField.Overflow, value, StyleValue.FromOverflowMode); }
    public OverflowMode? OverflowX { init => _style = StyleAccumulator.Add(_style, StyleField.OverflowX, value, StyleValue.FromOverflowMode); }
    public OverflowMode? OverflowY { init => _style = StyleAccumulator.Add(_style, StyleField.OverflowY, value, StyleValue.FromOverflowMode); }
    public Length? PerspectiveOriginX { init => _style = StyleAccumulator.Add(_style, StyleField.PerspectiveOriginX, value); }
    public Length? PerspectiveOriginY { init => _style = StyleAccumulator.Add(_style, StyleField.PerspectiveOriginY, value); }
    public PointerEvents? PointerEvents { init => _style = StyleAccumulator.Add(_style, StyleField.PointerEvents, value, StyleValue.FromPointerEvents); }
    public PositionMode? Position { init => _style = StyleAccumulator.Add(_style, StyleField.Position, value, StyleValue.FromPositionMode); }
    public string? SoundIn { init => _style = StyleAccumulator.Add(_style, StyleField.SoundIn, value); }
    public string? SoundOut { init => _style = StyleAccumulator.Add(_style, StyleField.SoundOut, value); }
    public TextAlign? TextAlign { init => _style = StyleAccumulator.Add(_style, StyleField.TextAlign, value, StyleValue.FromTextAlign); }
    public Length? TextBackgroundAngle { init => _style = StyleAccumulator.Add(_style, StyleField.TextBackgroundAngle, value); }
    public Color? TextDecorationColor { init => _style = StyleAccumulator.Add(_style, StyleField.TextDecorationColor, value, StyleValue.FromColor); }
    public TextDecoration? TextDecorationLine { init => _style = StyleAccumulator.Add(_style, StyleField.TextDecorationLine, value, StyleValue.FromTextDecoration); }
    public TextSkipInk? TextDecorationSkipInk { init => _style = StyleAccumulator.Add(_style, StyleField.TextDecorationSkipInk, value, StyleValue.FromTextSkipInk); }
    public TextDecorationStyle? TextDecorationStyle { init => _style = StyleAccumulator.Add(_style, StyleField.TextDecorationStyle, value, StyleValue.FromTextDecorationStyle); }
    public Length? TextDecorationThickness { init => _style = StyleAccumulator.Add(_style, StyleField.TextDecorationThickness, value); }
    public FilterMode? TextFilter { init => _style = StyleAccumulator.Add(_style, StyleField.TextFilter, value, StyleValue.FromFilterMode); }
    public Length? TextLineThroughOffset { init => _style = StyleAccumulator.Add(_style, StyleField.TextLineThroughOffset, value); }
    public TextOverflow? TextOverflow { init => _style = StyleAccumulator.Add(_style, StyleField.TextOverflow, value, StyleValue.FromTextOverflow); }
    public Length? TextOverlineOffset { init => _style = StyleAccumulator.Add(_style, StyleField.TextOverlineOffset, value); }
    public Color? TextStrokeColor { init => _style = StyleAccumulator.Add(_style, StyleField.TextStrokeColor, value, StyleValue.FromColor); }
    public Length? TextStrokeWidth { init => _style = StyleAccumulator.Add(_style, StyleField.TextStrokeWidth, value); }
    public TextTransform? TextTransform { init => _style = StyleAccumulator.Add(_style, StyleField.TextTransform, value, StyleValue.FromTextTransform); }
    public Length? TextUnderlineOffset { init => _style = StyleAccumulator.Add(_style, StyleField.TextUnderlineOffset, value); }
    public Goo.PanelTransform? Transform { init => _style = StyleAccumulator.Add(_style, StyleField.Transform, value, StyleValue.FromPanelTransform); }
    public Length? TransformOriginX { init => _style = StyleAccumulator.Add(_style, StyleField.TransformOriginX, value); }
    public Length? TransformOriginY { init => _style = StyleAccumulator.Add(_style, StyleField.TransformOriginY, value); }
    public WhiteSpace? WhiteSpace { init => _style = StyleAccumulator.Add(_style, StyleField.WhiteSpace, value, StyleValue.FromWhiteSpace); }
    public WordBreak? WordBreak { init => _style = StyleAccumulator.Add(_style, StyleField.WordBreak, value, StyleValue.FromWordBreak); }
    public Length? WordSpacing { init => _style = StyleAccumulator.Add(_style, StyleField.WordSpacing, value); }
    public int? ZIndex { init => _style = StyleAccumulator.Add(_style, StyleField.ZIndex, value); }
    public Color? HoverBackgroundColor { init => _style = StyleAccumulator.Add(_style, StyleField.HoverBackgroundColor, value, StyleValue.FromColor); }
    public Color? ActiveBackgroundColor { init => _style = StyleAccumulator.Add(_style, StyleField.ActiveBackgroundColor, value, StyleValue.FromColor); }
    public Color? FocusBackgroundColor { init => _style = StyleAccumulator.Add(_style, StyleField.FocusBackgroundColor, value, StyleValue.FromColor); }
    public Color? HoverFontColor { init => _style = StyleAccumulator.Add(_style, StyleField.HoverFontColor, value, StyleValue.FromColor); }
    public Color? ActiveFontColor { init => _style = StyleAccumulator.Add(_style, StyleField.ActiveFontColor, value, StyleValue.FromColor); }
    public Color? FocusFontColor { init => _style = StyleAccumulator.Add(_style, StyleField.FocusFontColor, value, StyleValue.FromColor); }
    public int? TransitionMs { init => _style = StyleAccumulator.Add(_style, StyleField.TransitionMs, value); }
}
using Goo;
using Sandbox.UI;

namespace Sandbox;

public class CounterUI : GooPanel<Container>
{
	private int _count;

	protected override Container Build() => new Container
	{
		Padding = 16,
		Width = 128,
		Height = 128,
		BackgroundColor = Color.White,
		BorderRadius = 12,
		FlexDirection = FlexDirection.Row,
		Gap = 12,
		AlignItems = Align.Center,
		Children =
		{
			new Text(_count.ToString()),
			new Container
			{
				Padding = 8,
				BackgroundColor = Color.Orange,
				HoverBackgroundColor = Color.Cyan,
				BorderRadius = 6,
				OnClick = e => { _count++; Rebuild(); },
				Children = { new Text("+") },
			},
		},
	};
}
using System;
using System.Collections.Generic;
using Sandbox;

namespace Goo.Input;

// Polls a curated key catalog once per frame for rising-edge presses + modifier state. ReemitHeldOnModifierRise re-emits a held key when a modifier rises (so "hold W, press Ctrl" reads as a chord).
public sealed class KeyTracker
{
    public bool ReemitHeldOnModifierRise { get; set; } = false;

    public ModifierState Modifiers { get; private set; }
    public IReadOnlyList<KeyDescriptor> JustPressed => _justPressed;

    readonly IReadOnlyList<KeyDescriptor> _catalog;
    readonly HashSet<string>     _downLastFrame = new();
    readonly List<KeyDescriptor> _justPressed   = new();

    public KeyTracker() : this( KnownKeys.All ) { }

    public KeyTracker( IReadOnlyList<KeyDescriptor> catalog )
    {
        _catalog = catalog;
    }

    public void Reset()
    {
        _downLastFrame.Clear();
        _justPressed.Clear();
        Modifiers = default;
    }

    static readonly Func<string, bool> s_engineDown = Sandbox.Input.Keyboard.Down;

    public void Poll() => Poll( s_engineDown );

    // isDown seam exists because engine Input statics throw outside a running engine process.
    public void Poll( Func<string, bool> isDown )
    {
        _justPressed.Clear();
        var prev = Modifiers;

        for ( int i = 0; i < _catalog.Count; i++ )
        {
            var  d    = _catalog[i];
            bool down = isDown( d.EngineName );
            if ( down && !_downLastFrame.Contains( d.EngineName ) )
                _justPressed.Add( d );
            if ( down ) _downLastFrame.Add( d.EngineName );
            else        _downLastFrame.Remove( d.EngineName );
        }

        Modifiers = new ModifierState(
            _downLastFrame.Contains( "ctrl"  ),
            _downLastFrame.Contains( "shift" ),
            _downLastFrame.Contains( "alt"   ),
            _downLastFrame.Contains( "win"   ) );

        if ( !ReemitHeldOnModifierRise ) return;

        bool modRose = ( Modifiers.Ctrl  && !prev.Ctrl  ) || ( Modifiers.Shift && !prev.Shift )
                    || ( Modifiers.Alt   && !prev.Alt   ) || ( Modifiers.Meta  && !prev.Meta  );
        if ( !modRose ) return;

        for ( int i = 0; i < _catalog.Count; i++ )
        {
            var d = _catalog[i];
            if ( d.Class == KeyClass.Modifier ) continue;
            if ( !_downLastFrame.Contains( d.EngineName ) ) continue;
            if ( _justPressed.Contains( d ) ) continue;
            _justPressed.Add( d );
        }
    }

    /// <summary>True if the named key had a rising edge (just-pressed, not held) this frame; call Poll first.</summary>
    public bool Pressed( string engineName )
    {
        for ( int i = 0; i < _justPressed.Count; i++ )
            if ( _justPressed[i].EngineName == engineName ) return true;
        return false;
    }
}
using System;
using Sandbox;
using Sandbox.UI;

namespace Goo.Internal;

internal sealed class StatefulLabel : Label, IStatefulHost, IStatefulEventHost
{
    StateController? _state;

    internal Action<MousePanelEvent>? _onClick;
    internal Action<MousePanelEvent>? _onRightClick;
    internal Action<MousePanelEvent>? _onMiddleClick;
    internal Action<MousePanelEvent>? _onMouseEnter;
    internal Action<MousePanelEvent>? _onMouseLeave;
    internal Action<MousePanelEvent>? _onMouseDown;
    internal Action<MousePanelEvent>? _onMouseUp;
    internal Action<MousePanelEvent>? _onMouseMove;
    internal bool    _userSetPointerEvents;
    internal Action? _requestRebuild;
    public Action? RequestRebuild { set => _requestRebuild = value; }

    public void ApplyStateVariants(
        Color? baseBg,  Color? baseFg,
        Color? hoverBg, Color? activeBg, Color? focusBg,
        Color? hoverFg, Color? activeFg, Color? focusFg,
        int? transitionMs)
    {
        _state ??= new StateController(this);
        _state.ApplyVariants(
            baseBg, baseFg,
            hoverBg, activeBg, focusBg,
            hoverFg, activeFg, focusFg,
            transitionMs);
    }

    public void ClearStateVariants() => _state?.ClearVariants();

    public bool HasActiveStateVariants => _state?.HasActiveVariants ?? false;

    public void ApplyEvents(in BlobEvents events)
    {
        _onClick      = events.OnClick;
        _onRightClick = events.OnRightClick;
        _onMiddleClick = events.OnMiddleClick;
        _onMouseEnter = events.OnMouseEnter;
        _onMouseLeave = events.OnMouseLeave;
        _onMouseDown  = events.OnMouseDown;
        _onMouseUp    = events.OnMouseUp;
        _onMouseMove  = events.OnMouseMove;
    }

    public bool HasEventHandlers =>
        _onClick != null || _onRightClick != null || _onMiddleClick != null || _onMouseEnter != null || _onMouseLeave != null ||
        _onMouseDown != null || _onMouseUp != null || _onMouseMove != null;

    public bool UserSetPointerEvents
    {
        get => _userSetPointerEvents;
        set => _userSetPointerEvents = value;
    }

    protected override void OnClick(MousePanelEvent e)
    {
        base.OnClick(e);
        EventDispatch.Fire(_onClick, e, _requestRebuild);
    }

    protected override void OnRightClick(MousePanelEvent e)
    {
        base.OnRightClick(e);
        EventDispatch.Fire(_onRightClick, e, _requestRebuild);
    }

    protected override void OnMiddleClick(MousePanelEvent e)
    {
        base.OnMiddleClick(e);
        EventDispatch.Fire(_onMiddleClick, e, _requestRebuild);
    }

    protected override void OnMouseOver(MousePanelEvent e)
    {
        base.OnMouseOver(e);
        EventDispatch.Fire(_onMouseEnter, e, _requestRebuild);
    }

    protected override void OnMouseOut(MousePanelEvent e)
    {
        base.OnMouseOut(e);
        EventDispatch.Fire(_onMouseLeave, e, _requestRebuild);
    }

    protected override void OnMouseDown(MousePanelEvent e)
    {
        base.OnMouseDown(e);
        EventDispatch.Fire(_onMouseDown, e, _requestRebuild);
    }

    protected override void OnMouseUp(MousePanelEvent e)
    {
        base.OnMouseUp(e);
        EventDispatch.Fire(_onMouseUp, e, _requestRebuild);
    }

    protected override void OnMouseMove(MousePanelEvent e)
    {
        base.OnMouseMove(e);
        EventDispatch.Fire(_onMouseMove, e, _requestRebuild);
    }
}
using System;
using System.Collections.Generic;
using Sandbox;
using Sandbox.Rendering;
using Sandbox.UI;

namespace Goo;

/// <summary>
/// A custom shader applied to a Blob. Point it at a compiled .shader; it parses the shader
/// source's Attribute() declarations to validate uniform names and to reset uniforms other
/// panels have set (panels share one CommandList attribute namespace), and pushes the uniform
/// bag every frame. Set uniforms with the collection initializer (<c>["Name"] = value</c>);
/// a value may be a literal or a per-frame Func. Subclass and override <see cref="Apply"/>
/// only for bespoke per-frame CPU logic.
/// </summary>
public record ShaderEffect
{
    static readonly Dictionary<object, Material> _materialCache = new();
    static readonly Dictionary<object, ShaderSchemaInfo?> _schemaCache = new();

    // Every uniform name any ShaderEffect has pushed this session, with the last value pushed
    // (its runtime type drives the reset conversion). Panels share one CommandList attribute
    // namespace, so a uniform set by one panel persists into every later panel's draw; an
    // effect that does not set a seen uniform must reset it to its shader's declared default
    // or it inherits the other panel's value (view-5u5m). Touched only from Apply (render thread).
    static readonly Dictionary<string, object> _seenUniforms = new();

    internal sealed class ShaderSchemaInfo
    {
        public required HashSet<string> Names;                       // declared attribute names
        public required Dictionary<string, (Vector4 Floats, Vector4 Ints)> Defaults;
    }

    readonly string? _path;
    readonly Shader? _shader;
    readonly GrabMode _grab;
    readonly Dictionary<string, UniformValue> _bag = new();
    bool _validated;

    /// <summary>For subclasses that supply their own <see cref="Material"/> and <see cref="Apply"/>.</summary>
    protected ShaderEffect() { }

    /// <summary>Apply the shader at <paramref name="shaderPath"/> (e.g. "shaders/ui_dither.shader").</summary>
    public ShaderEffect( string shaderPath, GrabMode grab = GrabMode.None )
    {
        _path = shaderPath;
        _grab = grab;
    }

    /// <summary>Apply a shader resource (drag-droppable in the inspector).</summary>
    public ShaderEffect( Shader shader, GrabMode grab = GrabMode.None )
    {
        _shader = shader;
        _grab = grab;
    }

    /// <summary>The shader asset path, or null when constructed from a <see cref="T:Sandbox.Shader"/> resource.</summary>
    public string? ShaderPath => _path;

    /// <summary>How this effect grabs the framebuffer behind its panel.</summary>
    public GrabMode Grab => _grab;

    /// <summary>Get or set a uniform by its shader attribute name.</summary>
    public UniformValue this[string name]
    {
        get => _bag[name];
        set => _bag[name] = value;
    }

    object CacheKey => (object?)_path ?? _shader!;

    /// <summary>The material this effect draws with, cached so it is excluded from record equality.</summary>
    public virtual Material Material
    {
        get
        {
            var key = CacheKey;
            if ( !_materialCache.TryGetValue( key, out var mat ) )
            {
                mat = _path is not null ? Material.FromShader( _path ) : Material.FromShader( _shader! );
                _materialCache[key] = mat;
            }
            return mat;
        }
    }

    /// <summary>Create the Material now, on the calling thread. Call on the main thread; Draw runs on the render thread and must only read the cache.</summary>
    public void Warm() => _ = Material;

    /// <summary>Set this effect's shader attributes for the frame, then grab the framebuffer per <see cref="Grab"/>.</summary>
    protected internal virtual void Apply( CommandList cl, Rect rect )
    {
        cl.Attributes.Set( "BoxSize", new Vector2( rect.Width, rect.Height ) );

        // Subclass escape hatch: a derived effect calling base.Apply gets only BoxSize.
        if ( _path is null && _shader is null ) return;

        Validate();

        // Reset seen-but-unset uniforms to this shader's declared defaults so another panel's
        // attribute writes do not bleed into this draw (view-5u5m).
        var schema = SchemaFor( CacheKey );
        if ( schema is not null )
        {
            foreach ( var (name, sample) in _seenUniforms )
            {
                if ( _bag.ContainsKey( name ) ) continue;
                if ( !schema.Defaults.TryGetValue( name, out var def ) ) continue;
                if ( ResetValue( sample, def.Floats, def.Ints ) is { } reset )
                    SetAttribute( cl, name, reset );
            }
        }

        foreach ( var (name, value) in _bag )
        {
            var resolved = value.Resolve();
            SetAttribute( cl, name, resolved );
            if ( resolved is not Texture )
                _seenUniforms[name] = resolved;
        }

        switch ( _grab )
        {
            case GrabMode.Sharp:
                cl.Attributes.GrabFrameTexture( "FrameBufferCopyTexture" );
                break;
            case GrabMode.Blurred:
                cl.Attributes.GrabFrameTexture( "FrameBufferCopyTexture", Graphics.DownsampleMethod.GaussianBlur );
                break;
        }
    }

    static void SetAttribute( CommandList cl, string name, object value )
    {
        switch ( value )
        {
            case float f:   cl.Attributes.Set( name, f ); break;
            case bool b:    cl.Attributes.Set( name, b ); break;
            case Vector2 v: cl.Attributes.Set( name, v ); break;
            case Vector3 v: cl.Attributes.Set( name, v ); break;
            case Vector4 v: cl.Attributes.Set( name, v ); break;
            case Color c:   cl.Attributes.Set( name, c ); break;
            case Texture t: cl.Attributes.Set( name, t ); break;
        }
    }

    // Reads the shader's declared attribute names once per instance and warns on uniform names
    // the shader does not declare. Skipped silently when the source is unavailable.
    void Validate()
    {
        if ( _validated ) return;
        _validated = true;

        var valid = SchemaFor( CacheKey )?.Names;
        if ( valid is null || valid.Count == 0 ) return;

        foreach ( var name in _bag.Keys )
            if ( !valid.Contains( name ) )
                Sandbox.Internal.GlobalSystemNamespace.Log.Warning(
                    $"ShaderEffect for \"{_path ?? _shader?.ResourcePath}\": uniform \"{name}\" is not declared by the shader " +
                    $"(valid: {string.Join( ", ", valid )}). Ignored." );
    }

    // Names + defaults come from parsing the .shader SOURCE, which ships with the project and
    // declares every Attribute() with its Default(). The engine's Shader.Schema is editor-only
    // plumbing: at game runtime it throws ("Load must be called on the main thread!" — Apply
    // runs on the render thread) so it is not consulted at all (view-5u5m probe, 2026-06-11).
    // Null when the source is unreadable (e.g. unit tests, published build without raw
    // .shader files); reset and validation both skip then.
    static ShaderSchemaInfo? SchemaFor( object key )
    {
        if ( _schemaCache.TryGetValue( key, out var info ) ) return info;

        info = null;
        if ( (key as string ?? (key as Shader)?.ResourcePath) is { } srcPath )
        {
            try
            {
                info = ParseShaderSource( FileSystem.Mounted.ReadAllText( srcPath ) );
            }
            catch
            {
                info = null;
            }
        }

        _schemaCache[key] = info;
        return info;
    }

    static readonly System.Text.RegularExpressions.Regex _declRegex = new(
        @"\b(?<type>float[234]?|bool|int|Texture2D)\s+\w+\s*<(?<block>[^>]*)>",
        System.Text.RegularExpressions.RegexOptions.Compiled );
    static readonly System.Text.RegularExpressions.Regex _attrRegex = new(
        @"Attribute\s*\(\s*""(?<name>[^""]+)""\s*\)",
        System.Text.RegularExpressions.RegexOptions.Compiled );
    static readonly System.Text.RegularExpressions.Regex _defaultRegex = new(
        @"Default[234]?\s*\(\s*(?<args>[^)]*)\)",
        System.Text.RegularExpressions.RegexOptions.Compiled );

    // Pure: extracts Attribute()-bound uniform declarations and their Default() values from
    // .shader source. Defaults are stored in both vector slots so ResetValue can convert by the
    // bled value's runtime type. Texture declarations contribute a name (for validation) but no
    // default. Only the main file is scanned; #include'd uniforms (BoxSize, DpiScale) are
    // framework-managed and excluded anyway. Missing Default() = zeros, matching the engine's
    // unset-attribute behavior.
    internal static ShaderSchemaInfo? ParseShaderSource( string source )
    {
        var names    = new HashSet<string>();
        var defaults = new Dictionary<string, (Vector4 Floats, Vector4 Ints)>();

        foreach ( System.Text.RegularExpressions.Match m in _declRegex.Matches( source ) )
        {
            var block = m.Groups["block"].Value;
            var attr  = _attrRegex.Match( block );
            if ( !attr.Success ) continue;

            var name = attr.Groups["name"].Value;
            names.Add( name );

            if ( name is "BoxSize" or "DpiScale" ) continue;     // framework-managed per draw
            if ( m.Groups["type"].Value == "Texture2D" ) continue; // no resettable default

            Vector4 v = default;
            var def = _defaultRegex.Match( block );
            if ( def.Success )
            {
                var parts = def.Groups["args"].Value.Split( ',' );
                for ( int i = 0; i < parts.Length && i < 4; i++ )
                {
                    if ( !float.TryParse( parts[i].Trim(), System.Globalization.NumberStyles.Float,
                             System.Globalization.CultureInfo.InvariantCulture, out var f ) ) continue;
                    switch ( i )
                    {
                        case 0: v.x = f; break;
                        case 1: v.y = f; break;
                        case 2: v.z = f; break;
                        case 3: v.w = f; break;
                    }
                }
            }
            defaults[name] = (v, v);
        }

        return names.Count > 0 ? new ShaderSchemaInfo { Names = names, Defaults = defaults } : null;
    }

    // Pure: converts a shader's declared default (schema FloatDefault/IntDefault vectors) to the
    // runtime type of the value that bled in, so the reset lands in the same attribute slot type.
    // Null = no safe reset (unsupported type; textures are never recorded so never reach this).
    internal static object? ResetValue( object sample, Vector4 floats, Vector4 ints ) => sample switch
    {
        float   => floats.x,
        bool    => ints.x != 0f,
        Vector2 => new Vector2( floats.x, floats.y ),
        Vector3 => new Vector3( floats.x, floats.y, floats.z ),
        Color   => new Color( floats.x, floats.y, floats.z, floats.w ),
        Vector4 => floats,
        _       => null,
    };

    public virtual bool Equals( ShaderEffect? other )
    {
        if ( other is null ) return false;
        if ( ReferenceEquals( this, other ) ) return true;
        if ( EqualityContract != other.EqualityContract ) return false;
        return _path == other._path
            && ReferenceEquals( _shader, other._shader )
            && _grab == other._grab
            && BagEquals( _bag, other._bag );
    }

    public override int GetHashCode()
    {
        var hc = new HashCode();
        hc.Add( EqualityContract );
        hc.Add( _path );
        hc.Add( _grab );
        hc.Add( _bag.Count );
        return hc.ToHashCode();
    }

    static bool BagEquals( Dictionary<string, UniformValue> a, Dictionary<string, UniformValue> b )
    {
        if ( a.Count != b.Count ) return false;
        foreach ( var (k, v) in a )
            if ( !b.TryGetValue( k, out var bv ) || !v.Equals( bv ) ) return false;
        return true;
    }
}
using System;
using Sandbox;

namespace Goo.Animation;

public record struct SmoothVector2
{
    public Vector2 Current;
    public Vector2 Target;
    public Vector2 Velocity;
    public float SmoothTime;

    public SmoothVector2(Vector2 initial, float smoothTime)
    {
        Current = initial;
        Target = initial;
        Velocity = default;
        SmoothTime = smoothTime;
    }

    public void Update(float dt)
    {
        float vx = Velocity.x, vy = Velocity.y;
        Current = new Vector2(
            MathX.SmoothDamp(Current.x, Target.x, ref vx, SmoothTime, dt),
            MathX.SmoothDamp(Current.y, Target.y, ref vy, SmoothTime, dt));
        Velocity = new Vector2(vx, vy);
    }

    public bool IsSettled =>
        MathF.Abs(Target.x - Current.x) < 0.0001f &&
        MathF.Abs(Target.y - Current.y) < 0.0001f &&
        MathF.Abs(Velocity.x) < 0.0001f &&
        MathF.Abs(Velocity.y) < 0.0001f;

    /// <summary>Advances by dt and returns true while still moving; chain calls with | (not ||) so every damper advances each frame.</summary>
    public bool Tick(float dt) { Update(dt); return !IsSettled; }
}
using System;

namespace Goo.Animation;

public readonly record struct Tween
{
    public Sandbox.Utility.Easing.Function Easing { get; init; }
    public float Duration   { get; init; }
    public float Delay      { get; init; }
    public float SpeedScale { get; init; }
    public int   Iterations { get; init; }
    public bool  PingPong   { get; init; }
    public bool  Reversed   { get; init; }

    public Tween(Sandbox.Utility.Easing.Function easing, float duration, float delay = 0f)
    {
        Easing     = easing;
        Duration   = duration;
        Delay      = delay;
        SpeedScale = 1f;
        Iterations = 1;
        PingPong   = false;
        Reversed   = false;
    }

    /// <summary>
    /// Bridge a designer-authored Sandbox.Curve (authored over [0, 1]) into a Tween.
    /// Allocates one delegate per call; cache the result in a static readonly field.
    /// </summary>
    public static Tween FromCurve(Sandbox.Curve curve, float duration, float delay = 0f)
        => new Tween(curve.Evaluate, duration, delay);

    public float Eval(float elapsedSec)
    {
        if (Duration <= 0f) return Reversed ? 0f : 1f;

        float t = (elapsedSec - Delay) * SpeedScale;
        if (t <= 0f) return Reversed ? 1f : 0f;

        float cycleDuration = PingPong ? 2f * Duration : Duration;

        if (Iterations > 0 && t >= cycleDuration * Iterations)
        {
            float endLocal = PingPong ? 0f : 1f;
            if (Reversed) endLocal = 1f - endLocal;
            return Easing(endLocal);
        }

        float cycleT = t % cycleDuration;
        float local = PingPong
            ? (cycleT < Duration ? cycleT / Duration : 1f - (cycleT - Duration) / Duration)
            : cycleT / Duration;

        if (Reversed) local = 1f - local;
        return Easing(local);
    }
}

public static class TweenExtensions
{
    public static Tween Loop(this Tween t)               => t with { Iterations = -1 };
    public static Tween Times(this Tween t, int n)       => t with { Iterations = n };
    public static Tween PingPong(this Tween t)           => t with { PingPong = true };
    public static Tween Scale(this Tween t, float speed) => t with { SpeedScale = speed };
    public static Tween WithDelay(this Tween t, float s) => t with { Delay = s };
    public static Tween Reverse(this Tween t)            => t with { Reversed = true };
}
using System;
using System.Collections;

namespace Goo;

public sealed class Children : IEnumerable
{
    internal FrameList? _list;
    internal int _buildId;
    internal Children() { }
    internal int Count { get { EnsureValid(); return _list!.Count; } }
    internal ref Frame this[int i] { get { EnsureValid(); return ref _list![i]; } }

    public void Add<T>(in T child) where T : struct, IBlob
    {
        EnsureValid();
        ref Frame slot = ref _list!.Reserve();
        child.WriteTo(ref slot);
    }

    void EnsureValid()
    {
        var ctx = BuildContext._current;
        if (ctx == null || _list == null || _buildId != ctx._currentBuildId)
            throw new InvalidOperationException(
                "Container reused across rebuilds. The same Container instance cannot survive " +
                "past the Build() it was created in. Build a new one inside Build(), or extract " +
                "a helper function that returns a fresh Container each call.");
    }

    // IEnumerable is required by C# collection-initializer syntax; iteration is not a
    // use case for Children, so this returns an empty enumerator.
    IEnumerator IEnumerable.GetEnumerator() => Array.Empty<object>().GetEnumerator();
}

namespace Goo;

/// <summary>Compile-time constraint for blob struct types. Never use as storage, return, or parameter type (boxes the struct, destroys per-Rebuild allocation profile); only valid in where T : struct, IBlob.</summary>
public interface IBlob
{
    static abstract BlobKind Kind { get; }
    string? Key { get; }
    internal void WriteTo(ref Frame frame);
}

/// <summary>Returns the single root Blob for a GooView build. A named delegate rather than
/// Func&lt;IBlob&gt; because IBlob has a static-abstract member (Kind) and C# bars such interfaces
/// as generic type arguments (CS8920). Consequence for Razor markup: a bare method group cannot
/// bind (its natural type is the illegal Func&lt;IBlob&gt;), so write
/// <c>Build=@(new BlobBuilder(MyBuild))</c>. See docs/site/docs/gotchas.md.</summary>
public delegate IBlob BlobBuilder();