WorkInProgress/PlayerGrabber.cs
using Sandbox;
using System;

public class PlayerGrabber : Component
{
	[Property] public GameObject ImpactEffect { get; set; }
	[Property] public GameObject DecalEffect { get; set; }
	[Property] public float ShootDamage { get; set; } = 9.0f;

	/// <summary>
	/// The higher this is, the "looser" the grip is when dragging objects
	/// </summary>
	[Property, Range( 1, 16 )] public float MovementSmoothness { get; set; } = 3.0f;

	PhysicsBody grabbedBody;
	Transform grabbedOffset;
	Vector3 localOffset;

	bool waitForUp = false;

	protected override void OnUpdate()
	{
		if ( IsProxy )
			return;

		Transform aimTransform = Scene.Camera.WorldTransform;

		if ( waitForUp )
		{
			return;
		}

		if ( grabbedBody.IsValid() )
		{
			if ( Input.Down( "attack2" ) )
			{
				grabbedBody.MotionEnabled = false;
				grabbedBody.Velocity = 0;
				grabbedBody.AngularVelocity = 0;

				grabbedOffset = default;
				grabbedBody = default;
				waitForUp = true;
				return;
			}

			var targetTx = aimTransform.ToWorld( grabbedOffset );

			var worldStart = grabbedBody.GetLerpedTransform( Time.Now ).PointToWorld( localOffset );
			var worldEnd = targetTx.PointToWorld( localOffset );

			//var delta = Scene.Camera.WorldTransform.PointToWorld( new Vector3( 0, -10, -5 ) ) - worldStart;
			var delta = worldEnd - worldStart;
			for ( var f = 0.0f; f < delta.Length; f += 2.0f )
			{
				var size = 1 - f * 0.01f;
				if ( size < 0 ) break;

				Gizmo.Draw.Color = Color.Cyan;
				Gizmo.Draw.SolidSphere( worldStart + delta.Normal * f, size );
			}

			if ( !Input.Down( "attack1" ) )
			{
				grabbedOffset = default;
				grabbedBody = default;
			}
			else
			{
				return;
			}
		}

		if ( Input.Down( "attack2" ) )
		{
			Shoot();
			return;
		}

		var tr = Scene.Trace.Ray( Scene.Camera.WorldPosition, Scene.Camera.WorldPosition + Scene.Camera.WorldRotation.Forward * 1000 )
			.IgnoreGameObjectHierarchy( GameObject.Root )
			.Run();

		if ( !tr.Hit || tr.Body is null )
			return;

		if ( tr.Body.BodyType == PhysicsBodyType.Static )
			return;

		if ( Input.Down( "attack1" ) )
		{
			grabbedBody = tr.Body;
			localOffset = tr.Body.Transform.PointToLocal( tr.HitPosition );
			grabbedOffset = aimTransform.ToLocal( tr.Body.Transform );
			grabbedBody.MotionEnabled = true;
		}
	}

	protected override void OnFixedUpdate()
	{
		if ( IsProxy )
			return;

		Transform aimTransform = Scene.Camera.WorldTransform;

		if ( waitForUp )
		{
			if ( Input.Down( "attack1" ) || Input.Down( "attack2" ) )
			{
				return;
			}
		}

		waitForUp = false;

		if ( grabbedBody.IsValid() )
		{
			if ( Input.Down( "attack1" ) )
			{
				var targetTx = aimTransform.ToWorld( grabbedOffset );
				grabbedBody.SmoothMove( targetTx, 0.02f * MovementSmoothness, Time.Delta );
				return;
			}
		}
	}

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


		if ( grabbedBody is null )
		{
			var tr = Scene.Trace.Ray( Scene.Camera.ScreenNormalToRay( 0.5f ), 1000.0f )
							.IgnoreGameObjectHierarchy( GameObject.Root )
							.Run();

			if ( tr.Hit )
			{
				Gizmo.Draw.Color = Color.Cyan;
				Gizmo.Draw.SolidSphere( tr.HitPosition, 1 );
			}
		}
	}

	SoundEvent shootSound = Cloud.SoundEvent( "mdlresrc.toolgunshoot" );

	TimeSince timeSinceShoot;

	public void Shoot()
	{
		if ( timeSinceShoot < 0.1f )
			return;

		timeSinceShoot = 0;

		Sound.Play( shootSound, WorldPosition );

		var ray = Scene.Camera.ScreenNormalToRay( 0.5f );
		ray.Forward += Vector3.Random * 0.03f;

		var tr = Scene.Trace.Ray( ray, 3000.0f )
				.IgnoreGameObjectHierarchy( GameObject.Root )
				.Run();

		if ( !tr.Hit || tr.GameObject is null )
			return;

		if ( ImpactEffect.IsValid() )
		{
			ImpactEffect.Clone( new Transform( tr.HitPosition + tr.Normal * 2.0f, Rotation.LookAt( tr.Normal ) ) );
		}

		if ( DecalEffect.IsValid() )
		{
			var decal = DecalEffect.Clone( new Transform( tr.HitPosition + tr.Normal * 2.0f, Rotation.LookAt( -tr.Normal, Vector3.Random ), Random.Shared.Float( 0.8f, 1.2f ) ) );
			decal.SetParent( tr.GameObject );
		}

		if ( tr.Body.IsValid() )
		{
			tr.Body.ApplyImpulseAt( tr.HitPosition, tr.Direction * 200.0f * tr.Body.Mass.Clamp( 0, 200 ) );
		}

		var damage = new DamageInfo( ShootDamage, GameObject, GameObject, tr.Hitbox );
		damage.Position = tr.HitPosition;
		damage.Shape = tr.Shape;

		foreach ( var damageable in tr.GameObject.Components.GetAll<IDamageable>() )
		{
			damageable.OnDamage( damage );
		}
	}
}