World/TimerZones.cs
using Sandbox;
using Sandbox.Diagnostics;
using System.Runtime.InteropServices.Marshalling;

namespace KOTH;

public sealed class TimerZones : Component
{
	[Property] Zone EnterZone { get; set; }
	[Property] Zone EndZone { get; set; }
	[Property] LeaderboardText LeaderboardText { get; set; }
	[Property] Vector3 StartPoint { get; set; }
	[Property] string Detials { get => LeaderboardText == null ? "" : LeaderboardText.StatName; }

	private bool IsTimerGoing = false;
	private TimeSince TimeSinceEnter;

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

		Assert.IsValid(EnterZone);
		Assert.IsValid(EndZone);
		Assert.IsValid(LeaderboardText);

		EnterZone.OnZoneEnter += StartTimer;
		EndZone.OnZoneEnter += StopTimer;
	}

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

		Gizmo.Draw.Color = Color.Green;
		Gizmo.Draw.SolidBox(BBox.FromPositionAndSize(StartPoint, 20));
	}

	private void StartTimer(Collider Collider)
	{
		if (IsTimerGoing)
		{
			return;
		}

		if (Collider.GameObject.Root.GetComponent<PlayerPawn>() is { } PlayerPawn)
		{
			Log.Info($"starting timer for {PlayerPawn}");
			
			TimeSinceEnter = 0;
			IsTimerGoing = true;
		}
	}

	private void StopTimer(Collider Collider)
	{
		if (!IsTimerGoing)
		{
			return;
		}

		if (Collider.GameObject.Root.GetComponent<PlayerPawn>() is { } PlayerPawn)
		{
			double RunTime = TimeSinceEnter;
			Log.Info($"stopping timer for {PlayerPawn} @ {RunTime}");

			IsTimerGoing = false;
			LocalSetJumpTimer(RunTime);

			LeaderboardText.HeaderText = $"(leaderboard will take time to update)\nYour last attempt {RunTime:0.00}s";
			LeaderboardText.RefreshLeaderboardText();

			TeleportPlayerToStart(PlayerPawn);
		}
	}

	private void TeleportPlayerToStart(PlayerPawn PlayerPawn)
	{
		if (!PlayerPawn.IsValid())
		{
			return;
		}

		Transform TeleportTransform = GameObject.Transform.Local;
		TeleportTransform.Position += StartPoint;
		PlayerPawn.Teleport(TeleportTransform);
	}

	[Rpc.Broadcast]
	private void LocalSetJumpTimer(double Time)
	{
		Assert.True(Time > 0);

		if (Sandbox.Services.Stats.LocalPlayer.TryGet(LeaderboardText.StatName, out var BestTime))
		{
			if (Time > BestTime.LastValue)
			{
				Log.Info("2slow");
				return;
			}
		}

		Sandbox.Services.Stats.SetValue(LeaderboardText.StatName, Time);
	}
}