World/Buildings/TurretComponent.cs
using Sandbox;
using Sandbox.Diagnostics;
using Sandbox.Events;
using Sandbox.VR;
using System;
using System.Diagnostics;
using System.Numerics;

namespace KOTH;

public class BuildingComponent : Component
{
	[Sync(SyncFlags.FromHost)] public PlayerState OwnerState { get; set; }
	[RequireComponent] public DamageComponent DamageComponent { get; set; }

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

		Assert.NotNull(DamageComponent);
		DamageComponent.OnDeath += OnKill;

		if (Networking.IsHost)
		{
			Assert.NotNull(OwnerState);
			DamageComponent.Initalize(DamageComponent.MaxBaseHealth, OwnerState.Team);
		}
	}

	protected virtual void OnKill(FDamageTaken DamageTaken) 
	{
		GameObject.Root.Destroy();
	}
}

public sealed class TurretComponent : BuildingComponent
{
	[Property] public GameObject TurretMuzzleObject { get; set; }
	[Sync] public int Damage { get; private set; } = 1;
	[Sync] public float KnockbackStrength { get; private set; } = 1f;
	[Sync] public float Firerate { get; private set; } = 1f;
	[Sync] public float Range { get; private set; } = 720f;

	[Property] public GameObject TrailPrefab { get; set; }

	////////////////////////////////////////////////////////////////////////

	[Sync(SyncFlags.FromHost)] public PlayerPawn TargetPawn { get; private set; }

	////////////////////////////////////////////////////////////////////////

	public EEquipmentSlot EquippedSlot { get; private set; } = EEquipmentSlot.Undefined;

	public void SetFromWeaponGameObject(GameObject WeaponObject)
	{
		if (!WeaponObject.IsValid())
		{
			return;
		}

		var WeaponComponent = WeaponObject.GetComponent<InputWeaponComponent>();
		if (!WeaponComponent.IsValid())
		{
			return;
		}

		Model WeaponModel = null;
		Color WeaponTint = Color.Black;

		Assert.True(LoadDataFromInputWeaponComponent(WeaponComponent, WeaponModel, WeaponTint));
		EquippedSlot = WeaponObject.GetComponent<Equipment>().Slot;
	}

	private bool LoadDataFromInputWeaponComponent(InputWeaponComponent InputWeaponComponent, Model WeaponModel, Color WeaponTint)
	{
		if (!InputWeaponComponent.IsValid())
		{
			return false;
		}

		// TODO : check ownership!

		var WeaponStats = InputWeaponComponent.GetWeaponStats();
		Damage = (WeaponStats.BaseDamage * 0.66f).FloorToInt(); // !
		Firerate = WeaponStats.FireRate;
		KnockbackStrength = WeaponStats.KnockbackStrength;

		var TurretModelRenderer = TurretMuzzleObject.GetComponent<ModelRenderer>();
		if (TurretModelRenderer.IsValid() && WeaponModel.IsValid())
		{
			TurretModelRenderer.Model = WeaponModel;
			TurretModelRenderer.Tint = WeaponTint;
		}

		return true;
	}

	////////////////////////////////////////////////////////////////////////

	protected override void OnFixedUpdate()
	{
		if (Networking.IsHost)
		{
			ShootTargetIfExists();

			if (WorldRotation.Up.z < 0.5)
			{
				GameObject.Root.Destroy();
			}
		}
	}

	private void ShootTargetIfExists()
	{
		var TargetPlayerPawn = GetTargetPlayerPawn();
		if (TargetPlayerPawn == null)
		{
			return;
		}

		var TargetPosition = TargetPlayerPawn.CenterPosition;
		FireShot(TargetPosition);
	}

	////////////////////////////////////////////////////////////////////////

	[Rpc.Broadcast(NetFlags.OwnerOnly)]
	public void DoTurretFX(Vector3 TargetPosition)
	{
		if (!TurretMuzzleObject.IsValid())
		{
			Log.Warning($"no muzzle m8 {this}");
			return;
		}

		TurretMuzzleObject.WorldRotation = Rotation.LookAt(TurretMuzzleObject.WorldPosition - TargetPosition);

		//if (TrailPrefab.IsValid())
		//{
		//	var Lerp = 0f;
		//	while (Lerp < 1f)
		//	{
		//		var Position = Vector3.Lerp(TurretMuzzleObject.WorldPosition, TargetPosition, Lerp);
		//		TrailPrefab.Clone(Position);
		//		Lerp += 0.033f;
		//	}
		//}
	}

	private TimeSince TimeSinceShot = new();
	private bool FireShot(Vector3 TargetPosition)
	{
		if (!Networking.IsHost)
		{
			return false;
		}

		if (TimeSinceShot < Firerate)
		{
			return false;
		}

		var Shots = ShootHelper.GetShootTraceElements(Scene.Trace, GameObject, TurretMuzzleObject.WorldPosition, TargetPosition, DebugOverlay);
		foreach (var TraceElement in Shots)
		{
			if (!TraceElement.Hit)
			{
				continue;
			}

			if (TraceElement.GameObject.Root.Components.Get<PlayerPawn>(FindMode.EnabledInSelfAndDescendants) is { } HitPlayerPawn)
			{
				FDamageRequest DamageRequest = new()
				{
					TargetDamageComponent = HitPlayerPawn.DamageComponent,
					AttackerPlayerPawn = OwnerState.PlayerPawn,
					TargetPlayerPawn = HitPlayerPawn,
					DamageOrigin = TraceElement.HitPosition,
					TargetOrigin = HitPlayerPawn.CenterPosition,
					BaseDamage = Damage,
					BaseKnockbackStrength = KnockbackStrength,
					DamageType = EDamageType.HitScan,
					DamageFalloffType = EDamageFalloffType.Falloff,
				};

				Scene.Dispatch(new DamageRequestEvent(DamageRequest));

				DoTurretFX(TargetPosition);

				TimeSinceShot = 0;
				return true;
			}
		}

		return false;
	}

	private bool IsPlayerPawnValidTarget(PlayerPawn PlayerPawn, out float OutDistance)
	{
		OutDistance = float.PositiveInfinity;

		if (!PlayerPawn.IsValid() || !PlayerPawn.IsAlive)
		{
			return false;
		}

		var Distance = Math.Abs((PlayerPawn.WorldPosition - GameObject.WorldPosition).Length);
		OutDistance = Distance;

		if (Distance > Range)
		{
			return false;
		}

		return true;
	}

	private PlayerPawn GetTargetPlayerPawn()
	{
		if (IsPlayerPawnValidTarget(TargetPawn, out _))
		{
			return TargetPawn;
		}

		PlayerPawn BestTarget = null;
		float ShortestDistance = float.PositiveInfinity;

		// TODO : track playerpawns better, this is slow
		foreach (var GameObject in Scene.GetAllObjects(true))
		{
			if (GameObject.GetComponentInChildren<PlayerPawn>() is { } PlayerPawn)
			{
				if (PlayerPawn.Team != DamageComponent.Team)
				{
					if (IsPlayerPawnValidTarget(PlayerPawn, out var Distance))
					{
						if (Distance > ShortestDistance)
						{
							continue;
						}

						ShortestDistance = Distance;
						BestTarget = PlayerPawn;
					}
				}
			}
		}

		return BestTarget;
	}
}