Helper/RespawnHelper.cs
using Sandbox.Internal;
using System;

namespace Skateboard;

[ClassFileLocation( "RespawnHelper.cs" )]
public static class RespawnHelper
{
	private const float PawnHeight = 50f;
	private const float TraceHeight = 5000f;

	private const float RespawnAreaSize = 5f;
	private const int Steps = 5;
	private const float StepDistance = 30f;

	private const float MinimumUpwardsNormal = 0.7f;
	private const float MaximumHeightDifference = 5f;

	public static RespawnHelperResult FindRespawnPosition( Vector3 bailPosition )
	{
		var scene = Game.ActiveScene;
		if ( scene is null )
			return RespawnHelperResult.NotFound;

		var origin = bailPosition + Vector3.Up * PawnHeight
				   - Vector3.Right * 75f
				   - Vector3.Forward * 75f;

		var candidates = new List<Vector3>();
		var traces = new SceneTraceResult[5];

		for ( int y = 0; y < Steps; y++ )
		{
			for ( int x = 0; x < Steps; x++ )
			{
				var samplePos = origin + Vector3.Right * (x * StepDistance) + Vector3.Forward * (y * StepDistance);

				DoSampleTraces( scene, samplePos, traces );

				if ( AreAllTracesValid( traces ) && HasLineOfSightFromBail( scene, bailPosition, traces[0].EndPosition ) )
				{
					candidates.Add( traces[0].EndPosition );
				}
			}
		}

		if ( candidates.Count == 0 )
			return RespawnHelperResult.NotFound;

		var best = candidates[0];
		var bestDist = Vector3.DistanceBetween( bailPosition, best );

		for ( int i = 1; i < candidates.Count; i++ )
		{
			var d = Vector3.DistanceBetween( bailPosition, candidates[i] );
			if ( d < bestDist )
			{
				bestDist = d;
				best = candidates[i];
			}
		}

		return new RespawnHelperResult( best );
	}

	private static void DoSampleTraces( Scene scene, Vector3 samplePos, SceneTraceResult[] results )
	{
		var offsets = new[]
		{
			Vector3.Zero,
			Vector3.Right   * RespawnAreaSize + Vector3.Forward * RespawnAreaSize,
			Vector3.Right   * RespawnAreaSize - Vector3.Forward * RespawnAreaSize,
			-Vector3.Right  * RespawnAreaSize - Vector3.Forward * RespawnAreaSize,
			-Vector3.Right  * RespawnAreaSize + Vector3.Forward * RespawnAreaSize
		};

		for ( int i = 0; i < results.Length; i++ )
		{
			var from = samplePos + offsets[i];
			var to = from + Vector3.Down * TraceHeight;

			results[i] = scene.Trace.Ray( from, to )
				.WithAnyTags( "solid", "skateable" )
				.WithoutTags( "unskateable" )
				.Run();
		}
	}

	private static bool HasLineOfSightFromBail( Scene scene, Vector3 bailPosition, Vector3 candidateGroundPos )
	{
		var from = bailPosition + Vector3.Up * PawnHeight;
		var to = candidateGroundPos + Vector3.Up * PawnHeight;

		return !scene.Trace.Ray( from, to )
			.WithAnyTags( "solid", "playerclip", "passbullets", "unskateable" )
			.Run()
			.Hit;
	}

	private static bool AreAllTracesValid( SceneTraceResult[] traces )
	{
		var first = traces[0];
		if ( !first.Hit ) return false;

		if ( Vector3.Dot( first.Normal, Vector3.Up ) < MinimumUpwardsNormal )
			return false;

		var refNormal = first.Normal;
		var refHeight = Vector3.Dot( first.EndPosition, refNormal );

		for ( int i = 1; i < traces.Length; i++ )
		{
			var tr = traces[i];

			if ( !tr.Hit )
				return false;

			if ( Vector3.Dot( tr.Normal, Vector3.Up ) < MinimumUpwardsNormal )
				return false;

			var h = Vector3.Dot( tr.EndPosition, refNormal );
			if ( Math.Abs( refHeight - h ) > MaximumHeightDifference )
				return false;
		}

		return true;
	}
}

public struct RespawnHelperResult
{
	public static readonly RespawnHelperResult NotFound = new RespawnHelperResult
	{
		Found = false
	};

	public bool Found;
	public Vector3 Location;

	public RespawnHelperResult( Vector3 location )
	{
		Found = false;
		Location = Vector3.Zero;
		Found = true;
		Location = location;
	}
}