TeleportTrigger.cs
namespace TeleportSystem;

[Title( "Teleport Trigger" )]
[Category( "Teleport" )]
public sealed class TeleportTrigger : Component, Component.ExecuteInEditor, Component.ITriggerListener
{
	private static readonly Dictionary<GameObject, float> NextAllowedTeleportTime = new();
	private static readonly Dictionary<GameObject, PendingArrivalRotation> PendingArrivalRotations = new();

	private readonly record struct PendingArrivalRotation( Rotation Rotation, Vector3 Position, float ExpiresAt );
	private readonly HashSet<GameObject> _overlappingActors = new();
	private readonly HashSet<GameObject> _actorsWaitingForExit = new();

	[Property] public TeleportTrigger Destination { get; set; }

	[Property, Group( "Filter" )] public bool PlayersOnly { get; set; } = true;
	[Property, Group( "Filter" )] public bool RequireEnabledCharacterController { get; set; } = true;

	[Property, Group( "Teleport" )] public GameObject DestinationPoint { get; set; }
	[Property, Group( "Teleport" )] public bool MatchDestinationRotation { get; set; } = true;
	[Property, Group( "Teleport" )] public float CooldownSeconds { get; set; } = 0.35f;

	private Collider _zoneCollider;

	protected override void OnStart()
	{
		ResolveZoneCollider();
	}

	protected override void OnValidate()
	{
		ResolveZoneCollider();
	}

	protected override void OnUpdate()
	{
		if ( !Game.IsPlaying )
			return;

		ApplyPendingArrivalRotations();

		if ( Destination is null || !Destination.IsValid() )
			return;

		if ( _zoneCollider is null || !_zoneCollider.IsValid() )
			return;

		foreach ( var actor in _overlappingActors.ToArray() )
		{
			if ( actor is null || !actor.IsValid() )
			{
				_overlappingActors.Remove( actor );
				continue;
			}

			if ( !CanTeleport( actor ) )
				continue;

			if ( _actorsWaitingForExit.Contains( actor ) )
				continue;

			TeleportActorToDestination( actor );
		}
	}

	public void OnTriggerEnter( Collider other ) => TryTrackActorFromCollider( other );

	public void OnTriggerEnter( Collider self, Collider other )
	{
		if ( _zoneCollider is not null && self != _zoneCollider )
			return;

		TryTrackActorFromCollider( other );
	}

	public void OnTriggerEnter( GameObject other )
	{
		if ( other is null || !other.IsValid() )
			return;

		TryTrackActorFromGameObject( other );
	}

	public void OnTriggerEnter( Collider self, GameObject other )
	{
		if ( _zoneCollider is not null && self != _zoneCollider )
			return;

		if ( other is null || !other.IsValid() )
			return;

		TryTrackActorFromGameObject( other );
	}

	public void OnTriggerExit( Collider other ) => StopTrackingActorFromCollider( other );

	public void OnTriggerExit( Collider self, Collider other )
	{
		if ( _zoneCollider is not null && self != _zoneCollider )
			return;

		StopTrackingActorFromCollider( other );
	}

	public void OnTriggerExit( GameObject other ) => StopTrackingActorFromGameObject( other );

	public void OnTriggerExit( Collider self, GameObject other )
	{
		if ( _zoneCollider is not null && self != _zoneCollider )
			return;

		if ( other is null || !other.IsValid() )
			return;

		StopTrackingActorFromGameObject( other );
	}

	private void TryTrackActorFromCollider( Collider touchedCollider )
	{
		if ( touchedCollider is null || !touchedCollider.IsValid() )
			return;

		TryTrackActorFromGameObject( touchedCollider.GameObject );
	}

	private void TryTrackActorFromGameObject( GameObject touchedObject )
	{
		var actor = ResolveActorForTeleport( touchedObject );
		if ( actor is null || !actor.IsValid() )
			return;

		_overlappingActors.Add( actor );

		if ( !CanTeleport( actor ) )
			return;

		if ( Destination is null || !Destination.IsValid() )
			return;

		TeleportActorToDestination( actor );
	}

	private void StopTrackingActorFromCollider( Collider touchedCollider )
	{
		if ( touchedCollider is null || !touchedCollider.IsValid() )
			return;

		StopTrackingActorFromGameObject( touchedCollider.GameObject );
	}

	private void StopTrackingActorFromGameObject( GameObject touchedObject )
	{
		var actor = ResolveActorForTeleport( touchedObject );
		if ( actor is null || !actor.IsValid() )
			return;

		_overlappingActors.Remove( actor );
		_actorsWaitingForExit.Remove( actor );
	}

	private GameObject ResolveActorForTeleport( GameObject touchedObject )
	{
		if ( touchedObject is null || !touchedObject.IsValid() )
			return null;

		if ( !PlayersOnly )
			return touchedObject;

		var walker = touchedObject;
		while ( walker is not null && walker.IsValid() )
		{
			if ( TryGetSupportedPlayerController( walker, out var isControllerEnabled ) )
			{
				if ( RequireEnabledCharacterController && !isControllerEnabled )
					return null;

				return walker;
			}

			walker = walker.Parent;
		}

		return null;
	}

	private static bool TryGetSupportedPlayerController( GameObject go, out bool isEnabled )
	{
		isEnabled = false;
		if ( go is null || !go.IsValid() )
			return false;

		var characterController = go.Components.Get<CharacterController>();
		if ( characterController is not null )
		{
			isEnabled = characterController.Enabled;
			return true;
		}

		var sandboxPlayerController = go.Components.Get<Sandbox.PlayerController>();
		if ( sandboxPlayerController is not null )
		{
			isEnabled = sandboxPlayerController.Enabled;
			return true;
		}

		return false;
	}

	private bool CanTeleport( GameObject actor )
	{
		if ( actor == GameObject )
			return false;

		if ( NextAllowedTeleportTime.TryGetValue( actor, out var nextAllowed ) && Time.Now < nextAllowed )
			return false;

		return true;
	}

	private void TeleportActorToDestination( GameObject actor )
	{
		var destinationTransform = Destination.GameObject.WorldTransform;
		if ( DestinationPoint is not null && DestinationPoint.IsValid() )
			destinationTransform = DestinationPoint.WorldTransform;

		var rb = actor.Components.Get<Rigidbody>();
		var characterController = actor.Components.Get<CharacterController>();
		var playerController = actor.Components.Get<Sandbox.PlayerController>();
		var velocity = rb?.Velocity
			?? playerController?.Velocity
			?? characterController?.Velocity
			?? Vector3.Zero;
		var previousRotation = actor.WorldRotation;
		var actorArrivalRotation = ResolveActorArrivalRotation( destinationTransform );

		actor.WorldPosition = destinationTransform.Position;
		if ( actorArrivalRotation is not null )
		{
			actor.WorldRotation = actorArrivalRotation.Value;
			velocity = RotateVelocityForArrival( velocity, previousRotation, actorArrivalRotation.Value );
			SyncArrivalPresentationRotation( actor, actorArrivalRotation.Value, actorArrivalRotation.Value );
			PendingArrivalRotations[actor] = new PendingArrivalRotation( actorArrivalRotation.Value, destinationTransform.Position, Time.Now + 0.15f );
		}

		if ( rb is not null )
		{
			rb.Velocity = Vector3.Zero;
			rb.Sleeping = false;
		}

		if ( characterController is not null )
		{
			characterController.MoveTo( destinationTransform.Position, false );
			characterController.Velocity = Vector3.Zero;
		}

		if ( playerController is not null )
		{
			playerController.WishVelocity = Vector3.Zero;
		}

		var lockUntil = Time.Now + Math.Max( CooldownSeconds, 0.01f );
		NextAllowedTeleportTime[actor] = lockUntil;
		MarkActorWaitingForExit( actor );
		Destination.BlockActorForCooldown( actor, lockUntil );
		Destination.MarkActorWaitingForExit( actor );
	}

	private void BlockActorForCooldown( GameObject actor, float lockUntil )
	{
		if ( actor is null || !actor.IsValid() )
			return;

		NextAllowedTeleportTime[actor] = Math.Max( lockUntil, Time.Now + 0.01f );
	}

	private void MarkActorWaitingForExit( GameObject actor )
	{
		if ( actor is null || !actor.IsValid() )
			return;

		_actorsWaitingForExit.Add( actor );
	}

	private Rotation? ResolveActorArrivalRotation( Transform destinationTransform )
	{
		if ( MatchDestinationRotation )
		{
			return Rotation.FromYaw( destinationTransform.Rotation.Angles().yaw );
		}

		return null;
	}

	private static Vector3 RotateVelocityForArrival( Vector3 velocity, Rotation previousRotation, Rotation arrivalRotation )
	{
		if ( velocity.Length.AlmostEqual( 0f ) )
			return velocity;

		var yawDelta = arrivalRotation.Angles().yaw - previousRotation.Angles().yaw;
		return Rotation.FromYaw( yawDelta ) * velocity;
	}

	private static void SyncArrivalPresentationRotation( GameObject actor, Rotation presentationRotation, Rotation actorRotation )
	{
		var actorAngles = actorRotation.Angles();
		var walker = actor;
		while ( walker is not null && walker.IsValid() )
		{
			var playerController = walker.Components.Get<Sandbox.PlayerController>();
			if ( playerController is not null )
			{
				walker.WorldRotation = actorRotation;
				playerController.EyeAngles = actorAngles;
				if ( playerController.Renderer is not null && playerController.Renderer.IsValid() )
				{
					playerController.Renderer.GameObject.LocalRotation = Rotation.Identity;
					playerController.Renderer.GameObject.WorldRotation = presentationRotation;
					playerController.UpdateAnimation( playerController.Renderer );
				}

				AlignVisualBodyRotation( walker, presentationRotation );

				return;
			}

			walker = walker.Parent;
		}
	}

	private static void AlignVisualBodyRotation( GameObject actor, Rotation arrivalRotation )
	{
		var bodyObject = FindBodyObject( actor );
		if ( bodyObject is null || !bodyObject.IsValid() )
			return;

		bodyObject.WorldRotation = arrivalRotation;
	}

	private static void ApplyPendingArrivalRotations()
	{
		if ( PendingArrivalRotations.Count == 0 )
			return;

		foreach ( var (actor, pending) in PendingArrivalRotations.ToArray() )
		{
			if ( actor is null || !actor.IsValid() || Time.Now >= pending.ExpiresAt )
			{
				PendingArrivalRotations.Remove( actor );
				continue;
			}

			var characterController = actor.Components.Get<CharacterController>();
			if ( characterController is not null )
				characterController.MoveTo( pending.Position, false );

			actor.WorldPosition = pending.Position;
			SyncArrivalPresentationRotation( actor, pending.Rotation, actor.WorldRotation );
		}
	}

	private static GameObject FindBodyObject( GameObject actor )
	{
		foreach ( var child in actor.Children )
		{
			if ( child is null || !child.IsValid() )
				continue;

			if ( child.Name == "Body" )
				return child;

			if ( child.Components.Get<SkinnedModelRenderer>() is not null )
				return child;

			var nested = FindBodyObject( child );
			if ( nested is not null )
				return nested;
		}

		return null;
	}

	private void ResolveZoneCollider()
	{
		_zoneCollider ??= Components.Get<Collider>();
		if ( _zoneCollider is not null && _zoneCollider.IsValid() )
			return;

		_zoneCollider = Components.GetAll<Collider>().FirstOrDefault();

		if ( _zoneCollider is null || !_zoneCollider.IsValid() )
			Log.Warning( $"[TeleportTrigger:{GameObject.Name}] needs an existing Collider on the same object." );
	}
}