Weapons/PhysGun/Physgun.Effects.cs
using Sandbox.Rendering;
using Sandbox.Utility;

public partial class Physgun : ScreenWeapon, IPlayerControllable
{
	[Property] public LineRenderer BeamRenderer { get; set; }
	[Property] public GameObject EndPointEffectPrefab { get; set; }
	[Property] public GameObject FreezeEffectPrefab { get; set; }
	[Property] public GameObject UnFreezeEffectPrefab { get; set; }
	[Property] public GameObject GrabEffectPrefab { get; set; }

	[Property, Sync, ClientEditable, Group( "Inputs" )] public ClientInput ShootInput { get; set; }
	[Property, Sync, ClientEditable, Group( "Inputs" )] public ClientInput SecondaryInput { get; set; }
	[Property, Sync, ClientEditable, Group( "Inputs" )] public ClientInput ExtendInput { get; set; }
	[Property, Sync, ClientEditable, Group( "Inputs" )] public ClientInput RetractInput { get; set; }

	public void OnStartControl() { }
	public void OnEndControl() { }

	[Property, Group( "Screen" )] public float PowerMinDistance { get; set; } = 64f;
	[Property, Group( "Screen" )] public float PowerMaxDistance { get; set; } = 512f;
	[Property, Group( "Screen" )] public float PowerMinFraction { get; set; } = 0.5f;
	[Property, Group( "Screen" )] public float PowerMaxFraction { get; set; } = 1f;

	protected override string ScreenMaterialName => "v_physgun_display";
	protected override string ScreenMaterialPath => "weapons/physgun/physgun-screen.vmat";
	protected override float ScreenRefreshInterval => 0.1f;
	protected override Vector2Int ScreenTextureSize => new Vector2Int( 80, 80 );

	Vector3.SpringDamped middleSpring = new Vector3.SpringDamped( 0, 0 );

	float _prevBeamDistance;
	GameObject _endPointEffect;
	GameObject _grabEffect;

	public bool BeamActive => BeamRenderer?.Active == true || _state.Pulling || _stateHovered.Pulling;
	public bool PullActive => _state.Pulling || _stateHovered.Pulling;

	void UpdateBeam( Transform source, Vector3 end, Vector3 endNormal, bool grabbed )
	{
		if ( !BeamRenderer.IsValid() ) return;

		var endTx = new Transform( end, Rotation.LookAt( endNormal ) );

		if ( grabbed )
		{
			if ( _endPointEffect != null )
			{
				ITemporaryEffect.DisableLoopingEffects( _endPointEffect );
				_endPointEffect = null;
			}


			if ( !_grabEffect.IsValid() )
			{
				_grabEffect = GrabEffectPrefab.Clone( endTx );
			}

			if ( _grabEffect.IsValid() )
			{
				_grabEffect.WorldTransform = endTx;
			}

		}
		else
		{
			if ( _grabEffect != null )
			{
				_grabEffect.Destroy();
				_grabEffect = null;
			}

			if ( !_endPointEffect.IsValid() )
			{
				_endPointEffect = EndPointEffectPrefab.Clone( endTx );
			}

			if ( _endPointEffect.IsValid() )
			{
				_endPointEffect.WorldTransform = endTx;
			}
		}

		// obj
		if ( _state.GameObject.IsValid() )
		{
			//	BeamHighlight.Enabled = true;
			//	BeamHighlight.OverrideTargets = true;
			//	BeamHighlight.Targets.Clear();
			//	BeamHighlight.Targets.AddRange( _state.GameObject.GetComponents<Renderer>() );
			//	BeamHighlight.Width = 0.1f + Noise.Fbm( 3, Time.Now * 100.0f ) * 0.1f;
			//	BeamHighlight.Color = Color.Lerp( Color.Cyan, Color.White, Noise.Fbm( 3, Time.Now * 40.0f ) * 0.5f ) * 200.0f;
		}

		bool justEnabled = !BeamRenderer.GameObject.Enabled;

			if ( BeamRenderer.VectorPoints == null || BeamRenderer.VectorPoints.Count != 4 )
			BeamRenderer.VectorPoints = new List<Vector3>( [0, 0, 0, 0] );

		var distance = source.Position.Distance( end );
		var targetMiddle = source.Position + source.Forward * distance * 0.33f;
		targetMiddle = targetMiddle + Noise.FbmVector( 2, Time.Now * 400.0f, Time.Now * 100.0f ) * 1.0f;

		if ( !justEnabled )
		{
			// If the beam halved or more in a single frame, snap the spring to the new position to avoid shakiness
			if ( _prevBeamDistance > 1f && distance / _prevBeamDistance < 0.5f )
			{
				middleSpring = new Vector3.SpringDamped( targetMiddle, targetMiddle, 4, 0.2f );
			}

			// Ensure the middle point is never behind the first one
			var alongFwd = Vector3.Dot( middleSpring.Current - source.Position, source.Forward );
			if ( alongFwd < 0 )
			{
				var clamped = middleSpring.Current - source.Forward * alongFwd;
				middleSpring = new Vector3.SpringDamped( clamped, targetMiddle, 4, 0.2f );
			}
		}
		_prevBeamDistance = distance;

		BeamRenderer.VectorPoints[0] = source.Position;

		BeamRenderer.VectorPoints[1] = middleSpring.Current;
		middleSpring.Target = targetMiddle;
		middleSpring.Update( Time.Delta );

		BeamRenderer.VectorPoints[2] = Vector3.Lerp( (end + endNormal * 10), BeamRenderer.VectorPoints[1], 0.3f + MathF.Sin( Time.Now * 10.0f ) * 0.2f );
		BeamRenderer.VectorPoints[3] = end;

		if ( justEnabled )
		{
			BeamRenderer.GameObject.Enabled = true;
			_prevBeamDistance = distance;
			BeamRenderer.VectorPoints[1] = targetMiddle;
			middleSpring = new Vector3.SpringDamped( targetMiddle, targetMiddle, 4, 0.2f );
		}


	}

	void CloseBeam()
	{
		if ( _stateHovered.GameObject.IsValid() )
		{
			//	BeamHighlight.Enabled = true;
			//	BeamHighlight.OverrideTargets = true;
			//	BeamHighlight.Targets.Clear();
			//	BeamHighlight.Targets.AddRange( _stateHovered.GameObject.GetComponents<Renderer>() );
			//	BeamHighlight.Width = 0.2f;
			//	BeamHighlight.Color = new Color( 0.5f, 1, 1, 0.3f );
		}
		else
		{
			BeamHighlight.Enabled = false;
		}

		if ( !BeamRenderer.IsValid() ) return;

		BeamRenderer.GameObject.Enabled = false;

		if ( _endPointEffect.IsValid() )
		{
			ITemporaryEffect.DisableLoopingEffects( _endPointEffect );
			_endPointEffect = null;
		}

		if ( _grabEffect.IsValid() )
		{
			_grabEffect.Destroy();
			_grabEffect = null;
		}
	}

	private const int GraphSamples = 128;
	private float[] _graph1 = new float[GraphSamples];
	private float[] _graph2 = new float[GraphSamples];
	private float[] _graph3 = new float[GraphSamples];
	private int _graphCursor;
	private float _graphTimer;
	private const float GraphInterval = 0.02f;

	private float _plotValue1;
	private float _plotValue2;
	private float _plotValue3;

	private Texture _graphTexture;
	private byte[] _graphPixels = new byte[GraphSamples * 4]; // RGBA8

	protected override void DrawScreenContent( Rect rect, HudPainter paint )
	{
		paint.SetBlendMode( BlendMode.Lighten );

		var w = rect.Width;
		var h = rect.Height;
		var padX = w * 0.05f;
		var padY = h * 0.15f;

		var barWidthFraction = 0.55f;
		var barHeightFraction = 0.1f;

		var barW = w * barWidthFraction;
		var barH = h * barHeightFraction;
		var barX = rect.Left + padX;
		var barY = rect.Top + padY;

		var borderColor = new Color( 0.5f, 0.5f, 0.5f );

		// Fill bar
		var fillFraction = MathF.Max( _plotValue1, _plotValue2 );
		var normalized = ((fillFraction - 0.1f) / (0.8f - 0.1f)).Clamp( 0f, 1f );
		var fillWidth = barW * normalized;
		if ( fillWidth > 0f )
		{
			paint.DrawRect( new Rect( barX, barY, fillWidth, barH ), new Color( 1, 1, 1, 0.8f ) );
		}

		// Bar outline
		paint.DrawLine( new Vector2( barX, barY ), new Vector2( barX + barW, barY ), 1f, borderColor );
		paint.DrawLine( new Vector2( barX, barY + barH ), new Vector2( barX + barW, barY + barH ), 1f, borderColor );
		paint.DrawLine( new Vector2( barX, barY ), new Vector2( barX, barY + barH ), 1f, borderColor );
		paint.DrawLine( new Vector2( barX + barW, barY ), new Vector2( barX + barW, barY + barH ), 1f, borderColor );

		// Percentage label
		var percent = (int)(normalized * 100f);
		var percentLabel = new TextRendering.Scope( $"{percent}", Color.White, h * 0.135f );
		percentLabel.FontName = "Consolas";
		percentLabel.TextColor = Color.White;
		percentLabel.FontWeight = 100;
		percentLabel.FilterMode = FilterMode.Point;
		paint.DrawText( percentLabel, new Rect( barX + barW + padX, barY, w - barW - padX * 3f, barH ), TextFlag.LeftCenter );

		// Channel / voltage row
		var rowY = barY + barH + padY;

		var ch2 = new TextRendering.Scope( "Ch2", Color.White, h * 0.14f );
		ch2.FontName = "Consolas";
		ch2.TextColor = new Color( 0f, 1f, 0f );
		ch2.FontWeight = 400;
		ch2.FilterMode = FilterMode.Point;
		paint.DrawText( ch2, new Rect( barX, rowY, w * 0.45f, 0 ), TextFlag.LeftCenter );

		var voltage = new TextRendering.Scope( "731v", Color.White, h * 0.14f );
		voltage.FontName = "Consolas";
		voltage.TextColor = new Color( 0f, 1f, 0f );
		voltage.FontWeight = 400;
		voltage.FilterMode = FilterMode.Point;
		paint.DrawText( voltage, new Rect( barX + w * 0.45f, rowY, w * 0.45f, 0 ), TextFlag.LeftCenter );
	}

	private float _spinIntensity;

	private TimeSince _lastGraphUpdate;

	private void UpdateScreenGraph()
	{
		var active1 = _state.Active && !_state.Pulling;
		var active2 = Input.Down( "attack2" ) && !_preventReselect || _state.Pulling;
		var active3 = _isSpinning;

		var distancePower = 1f;
		if ( active1 )
		{
			var range = PowerMaxDistance - PowerMinDistance;
			var fraction = PowerMaxFraction - PowerMinFraction;
			distancePower = ((_state.GrabDistance - PowerMinDistance) / range * fraction + PowerMinFraction).Clamp( PowerMinFraction, PowerMaxFraction );
		}

		// Track rotation intensity from analog look input
		if ( active3 )
		{
			var look = Input.AnalogLook;
			var rotationMagnitude = MathF.Sqrt( look.pitch * look.pitch + look.yaw * look.yaw + look.roll * look.roll );
			var rotationPower = (rotationMagnitude / 5f).Clamp( 0f, 1f );
			_spinIntensity = _spinIntensity.LerpTo( 0.2f + rotationPower * 0.6f, Time.Delta * 15f );
		}
		else
		{
			_spinIntensity = _spinIntensity.LerpTo( 0f, Time.Delta * 10f );
		}

		var target1 = active1 ? (0.8f * distancePower) + Random.Shared.Float( -0.05f, 0.05f ) : 0.1f + Random.Shared.Float( -0.02f, 0.02f );

		// Held object velocity increases graph power on channel 1
		if ( active1 && _state.Body.IsValid() )
		{
			var velocityPower = (_state.Body.Velocity.Length / 500f).Clamp( 0f, 0.5f );
			target1 += velocityPower;
		}
		var target2 = active2 ? 0.8f + Random.Shared.Float( -0.05f, 0.05f ) : 0.1f + Random.Shared.Float( -0.02f, 0.02f );
		var target3 = active3 ? _spinIntensity + Random.Shared.Float( -0.03f, 0.03f ) : 0.1f + Random.Shared.Float( -0.02f, 0.02f );
		_plotValue1 = _plotValue1.LerpTo( target1, Time.Delta * 10f );
		_plotValue2 = _plotValue2.LerpTo( target2, Time.Delta * 10f );
		_plotValue3 = _plotValue3.LerpTo( target3, Time.Delta * 10f );

		_graphTimer += Time.Delta;
		while ( _graphTimer >= GraphInterval )
		{
			_graphTimer -= GraphInterval;
			_graph1[_graphCursor % GraphSamples] = _plotValue1;
			_graph2[_graphCursor % GraphSamples] = _plotValue2;
			_graph3[_graphCursor % GraphSamples] = _plotValue3;
			_graphCursor++;
		}

		if ( _lastGraphUpdate < ScreenRefreshInterval )
			return;

		_lastGraphUpdate = 0;

		var count = Math.Min( _graphCursor, GraphSamples );
		for ( var i = 0; i < GraphSamples; i++ )
		{
			float r, g, b;
			if ( i < count )
			{
				var idx = (_graphCursor - 1 - i + GraphSamples) % GraphSamples;
				r = _graph1[idx];
				g = _graph2[idx];
				b = _graph3[idx];
			}
			else
			{
				r = 0.1f;
				g = 0.1f;
				b = 0.1f;
			}

			var offset = i * 4;
			_graphPixels[offset + 0] = (byte)(r * 255f);
			_graphPixels[offset + 1] = (byte)(g * 255f);
			_graphPixels[offset + 2] = (byte)(b * 255f);
			_graphPixels[offset + 3] = 255;
		}

		_graphTexture ??= Texture.Create( GraphSamples, 1 ).WithDynamicUsage().Finish();
		_graphTexture.Update( _graphPixels );

		if ( !ViewModel.IsValid() ) return;

		var renderer = ViewModel.GetComponentInChildren<SkinnedModelRenderer>();
		if ( !renderer.IsValid() ) return;

		var so = renderer.SceneObject;
		so.Attributes.Set( "GraphData", _graphTexture );

		so.Attributes.Set( "Grid", new Vector4( 16f, 16f, 0.1f, 1.0f ) );
		so.Attributes.Set( "GraphInfo", new Vector4( GraphSamples, 0f, 0f, 0f ) );
		so.Attributes.Set( "Ch1Color", new Vector4( 0f, 1f, 1f, 1f ) );
		so.Attributes.Set( "Ch2Color", new Vector4( 1f, 1f, 0f, 1f ) );
		so.Attributes.Set( "Ch3Color", new Vector4( 1f, 0f, 0f, 0.5f ) );
		so.Attributes.Set( "Band1", new Vector4( 0.5f, 0.3f, 0f, 0f ) );
		so.Attributes.Set( "Band2", new Vector4( 0.48f, 0.28f, 0f, 0f ) );
		so.Attributes.Set( "Band3", new Vector4( 0.52f, 0.32f, 0f, 0f ) );
	}
}