Code/SimpleInteraction.cs
using Sandbox;
using Sandbox.Utility;
using Sandbox.Diagnostics;
using System.Linq;
using System.Threading.Tasks;

namespace SimpleInteractions {

	/// <summary>
	/// Simple interaction component
	/// </summary>
	[Icon( "touch_app" )]
	[Title( "Simple Interaction" )]
	public class SimpleInteraction : Component
	{
		[Property]
		public bool InteractionEnabled {get; set;} = true;

		[Property, Title("Interaction Name")]
		public string InteractionString {get; set;} = "Interact";

		[Property]
		public float InteractionDistance {get; set;} = 120f;

		[Property, ToggleGroup("InteractionHold")]
		public bool InteractionHold {get; set;} = false;

		[Property, Group("InteractionHold")]
		public float InteractionHoldDuration {get; set;} = 0.5f;


		/// <summary>
		/// If not set, will try to find a collider on the same GameObject.
		/// </summary>
		[Property, Title("Override collider")]
		public Collider Collider { get; set; }

		private GameObject CurrentPanel = null;

		private TimeSince HoldTime = 0;
		private bool Holding = false;
		private bool HoldingInteractionHappened = false;

		static protected GameObject InteractionPanelPrefab ;

		protected override void OnStart()
		{
			InteractionPanelPrefab = GameObject.GetPrefab("InteractionsPanel.prefab");

			Assert.True(InteractionPanelPrefab.IsValid(), $"No InteractionPanel prefab found for {this.GameObject.Name}!");

			if (!Collider.IsValid()) {
				
				Collider = this.GameObject.GetComponent<Collider>();

				Assert.True(Collider.IsValid(), $"No collider found for {this.GameObject.Name}!");
			}
			this.GameObject.Tags.Add("Interact");
		}

		protected override void OnUpdate()
		{
			if (!InteractionEnabled) 
			{
				// Reset everything just in case
				Holding = false;
				HoldingInteractionHappened = false;

				// Delete the Interaction panel otherwise it would just float there...
				if (CurrentPanel.IsValid())
				{
					InteractionPanel panel = CurrentPanel.GetComponent<InteractionPanel>();
					if (panel.IsValid()) {
						_ = DeletePanel();
					}
				}
				return;
			}

			Ray ray = Scene.Camera.GameObject.Transform.World.ForwardRay;

			var traces = Scene.Trace.Ray(ray, InteractionDistance)
			.WithoutTags("IgnoreInteract")
			.HitTriggers()
			.RunAll();
			
			// Gizmo.Draw.Line(tr.StartPosition, tr.EndPosition);

			if (traces.Count() <= 0)
			{
				_ = DeletePanel();
				
				// Force repressing use in case you looked away while holding down.
				HoldingInteractionHappened = true;
				return;
			}

			foreach (var tr in traces)
			{

				Collider HitCollider = tr.Shape.Collider as Collider;

				// If it's a trigger and it doesn't have the interact tag, skip it.
				// We can see through it.
				if (HitCollider.IsTrigger && !HitCollider.GameObject.Tags.Has("Interact"))
				{
					continue;
				} else if (!HitCollider.IsTrigger && !HitCollider.GameObject.Tags.Has("Interact"))
				{
					// Something is blocking the interaction.
					_ = DeletePanel();
					
					// Force repressing use in case you looked away while holding down.
					HoldingInteractionHappened = true;
					break;
				}

				Vector3 offset = Vector3.Zero;

				if (HitCollider is BoxCollider)
				{
					offset = (HitCollider as BoxCollider).Center;
				} else if (HitCollider is SphereCollider)
				{
					offset = new Vector3((HitCollider as SphereCollider).Center);
				}


				if (HitCollider == Collider)
				{
					Vector3 pos = new Vector3(offset.x, offset.y, - offset.z);
					OnHover(HitCollider.GameObject.WorldPosition - pos);
					break;
				} else
				{
					_ = DeletePanel();
					
					// Force repressing use in case you looked away while holding down.
					HoldingInteractionHappened = true;
				}
			}
		}

		private void OnHover(Vector3 pos)
		{
			if (!CurrentPanel.IsValid())
			{
				CurrentPanel = InteractionPanelPrefab.Clone();
			}

			CurrentPanel.WorldPosition = pos;

			// Flip the panel to face the camera
			Rotation camRotation = Scene.Camera.WorldRotation;

			Angles ang = camRotation.Angles();
			ang.roll += 180;
			ang.pitch += 180;
			Rotation rot = ang.ToRotation();
			CurrentPanel.WorldRotation = rot;

			InteractionPanel panel = CurrentPanel.GetComponent<InteractionPanel>();
			panel.InteractionString = InteractionString;
			panel.IsHoldInteraction = InteractionHold;
			panel.ProgressionHold = 0;



			if (!InteractionHold)
			{
				if (Input.Pressed("use"))
				{
					_ = panel.TriggerInteractAnimation();
					OnInteract();
				}
				return;
			}

			
			if (!Input.Down("use"))
			{
				Holding = false;
				HoldingInteractionHappened = false;
				return;
			}

			// Interaction already happened. Player needs to release and press again.
			if (HoldingInteractionHappened)
			{
				return;
			}

			if (Holding)
			{
				panel.ProgressionHold = Easing.QuadraticInOut(HoldTime / InteractionHoldDuration);
				if (HoldTime >= InteractionHoldDuration)
				{
					HoldingInteractionHappened = true;
					OnInteract();
				}
			} else
			{
				// Started holding.
				Holding = true;
				HoldTime = 0;
				_ = panel.TriggerInteractAnimation();
			}
		}

		async private Task DeletePanel()
		{
			if(!CurrentPanel.IsValid()) return;

			CurrentPanel.GetComponent<PanelComponent>().Panel.Delete();
			await Task.DelaySeconds( 0.1f );
			CurrentPanel.Destroy();
		}

		[Rpc.Broadcast]
		protected virtual void OnInteract()
		{
			Log.Error($"Interaction not implemented for {this.GameObject.Name}!");
		}

	}

}