AI/Rat.cs
/// <summary>
/// A rat NPC
/// </summary>
public sealed class Rat : Component, Component.IDamageable, IKillIcon, Component.ICollisionListener, IInstigator
{
	/// <summary>
	/// The <see cref="NavMeshAgent"/> which lets the rat traverse the navmesh
	/// </summary>
	[RequireComponent] public NavMeshAgent Agent { get; set; }

	/// <summary>
	/// The <see cref="Rigidbody"/> which we switch out to when jumping off of the navmesh
	/// </summary>
	[RequireComponent] public Rigidbody Rigidbody { get; set; }

	/// <summary>
	/// What should we spawn when the Rat dies?
	/// </summary>
	[Property] public GameObject DeathEffects { get; set; }

	/// <summary>
	/// What sound should we play when the Rat dies?
	/// </summary>
	[Property] public SoundEvent AttackSound { get; set; }

	/// <summary>
	/// The mouth of the rat, used for attack range
	/// </summary>
	[Property] public GameObject Mouth { get; set; }

	/// <summary>
	/// An icon to show on the kill feed when the rat kills someone
	/// </summary>
	[Property] Texture IKillIcon.DisplayIcon { get; set; }

	/// <summary>
	/// How far (sq units) away should the rat be before it starts attacking people?
	/// </summary>
	[Property, Feature( "Balance" )] public float AttackDistance { get; set; } = 50000f;

	/// <summary>
	/// How long until the rat implodes
	/// </summary>
	[Property, Feature( "Balance" )] public float Lifetime { get; set; } = 30f;

	/// <summary>
	/// How frequently should the rat choose a new roam location
	/// </summary>
	[Property, Feature( "Balance" )] public float RoamFrequency { get; set; } = 5f;

	/// <summary>
	/// How far away can the rat roam
	/// </summary>
	[Property, Feature( "Balance" )] public float RoamRadius { get; set; } = 1024f;

	/// <summary>
	/// Damage dealt when the rat bites a target
	/// </summary>
	[Property, Feature( "Balance" )] public float BiteDamage { get; set; } = 10f;

	/// <summary>
	/// Time (in seconds) between bite attacks
	/// </summary>
	[Property, Feature( "Balance" )] public float BiteCooldown { get; set; } = 1f;

	/// <summary>
	/// Time (in seconds) between lunge attacks
	/// </summary>
	[Property, Feature( "Balance" )] public float LungeCooldown { get; set; } = 3f;

	/// <summary>
	/// How often the rat thinks/updates its behavior (in seconds)
	/// </summary>
	[Property, Feature( "Balance" )] public float ThinkInterval { get; set; } = 0.1f;

	/// <summary>
	/// Distance squared for bite attack range
	/// </summary>
	[Property, Feature( "Balance" )] public float BiteRangeSquared { get; set; } = 5000f;

	/// <summary>
	/// Distance squared for target detection range
	/// </summary>
	[Property, Feature( "Balance" )] public float TargetDetectionRangeSquared { get; set; } = 262144f;

	/// <summary>
	/// Distance threshold for reaching roam target
	/// </summary>
	[Property, Feature( "Balance" )] public float RoamTargetThreshold { get; set; } = 32f;

	/// <summary>
	/// Velocity when lunging at target (X = horizontal, Y = vertical)
	/// </summary>
	[Property, Feature( "Balance" )] public Vector2 LungeVelocity { get; set; } = new Vector2( 512f, 256f );

	/// <summary>
	/// Velocity when thrown (X = horizontal, Y = vertical)
	/// </summary>
	[Property, Feature( "Balance" )] public Vector2 ThrowVelocity { get; set; } = new Vector2( 1024f, 50f );

	/// <summary>
	/// Initial think timer delay on start
	/// </summary>
	[Property, Feature( "Balance" )] public float InitialThinkDelay { get; set; } = 5f;

	/// <summary>
	/// Lunge timer when a new target is acquired
	/// </summary>
	[Property, Feature( "Balance" )] public float NewTargetLungeDelay { get; set; } = 1.5f;

	/// <summary>
	/// Height for roam area bounding box
	/// </summary>
	[Property, Feature( "Balance" )] public float RoamAreaHeight { get; set; } = 128f;

	/// <summary>
	/// Ground check distance (up and down)
	/// </summary>
	[Property, Feature( "Balance" )] public float GroundCheckDistance { get; set; } = 8f;

	/// <summary>
	/// Who's the rat's friend (the person who threw them.. not much of a friend are they)
	/// </summary>
	[Sync]
	public PlayerData Instigator { get; set; }

	/// <summary>
	/// Are we on the ground?
	/// </summary>
	bool IsOnGround = true;

	/// <summary>
	/// The current target of the rat
	/// </summary>
	Player target;

	/// <summary>
	/// How many seconds has it been since the rat bit someone?
	/// </summary>
	TimeSince TimeSinceBitten = 0;

	/// <summary>
	/// How many seconds has it been since we last LUNGED
	/// </summary>
	TimeSince LungeTimer = 0;

	/// <summary>
	/// How long has the rat been alive?
	/// </summary>
	TimeSince TimeSinceCreated = 0;

	/// <summary>
	/// Time since we picked a new target
	/// </summary>
	TimeSince ThinkTimer = 0;

	/// <summary>
	/// The roam target
	/// </summary>
	Vector3 RoamTarget;

	protected override void OnStart()
	{
		TimeSinceCreated = 0;
		ThinkTimer = InitialThinkDelay;
	}

	/// <summary>
	/// Trace down, see if we're hitting the ground
	/// </summary>
	/// <returns></returns>
	private void UpdateGrounded()
	{
		var tr = Scene.Trace.Ray( WorldPosition + Vector3.Up * GroundCheckDistance, WorldPosition + Vector3.Down * GroundCheckDistance )
			.IgnoreGameObjectHierarchy( GameObject.Root )
			.Run();

		IsOnGround = tr.Hit;
	}

	/// <summary>
	/// Throws a rat in a set direction
	/// </summary>
	/// <param name="rotation"></param>
	public void Throw( Rotation rotation )
	{
		var direction = rotation.Forward;
		Agent.UpdatePosition = false;
		WorldRotation = Rotation.LookAt( direction, Vector3.Up );
		Rigidbody.Velocity = WorldRotation.Forward * ThrowVelocity.x + Vector3.Up * ThrowVelocity.y;
	}

	/// <summary>
	/// Called when the rat either lifts off the ground, or lands
	/// </summary>
	void OnGroundedChanged( bool before, bool after )
	{
		Agent.SetAgentPosition( WorldPosition );
	}


	private bool hasLanded = false;
	void ICollisionListener.OnCollisionStart( Collision collision )
	{
		// if we're thrown somewhere, hit the ground running
		if ( !hasLanded )
		{
			Rigidbody.Velocity = Vector3.Zero;
			Agent.SetAgentPosition( WorldPosition );
			hasLanded = true;
		}
	}

	void Bite( Player target )
	{
		TimeSinceBitten = 0;

		DoAttackEffects();

		var dmg = new DamageInfo( BiteDamage, Instigator?.Player?.GameObject, GameObject );

		target.OnDamage( dmg );
	}

	protected override void OnUpdate()
	{
		if ( IsProxy )
			return;

		if ( TimeSinceCreated > Lifetime )
		{
			Die();
			return;
		}

		// We're lunging, and we have a target
		if ( !IsOnGround && target.IsValid() && TimeSinceBitten > BiteCooldown )
		{
			// Bite the target if they're close enough
			if ( target.WorldPosition.DistanceSquared( Mouth.WorldPosition ) < BiteRangeSquared )
				Bite( target );
		}

		var prevOnGround = IsOnGround;
		UpdateGrounded();

		if ( prevOnGround != IsOnGround )
			OnGroundedChanged( prevOnGround, IsOnGround );

		if ( !IsOnGround )
			return;

		if ( !Agent.Enabled )
			return;

		if ( target.IsValid() )
		{
			// If we haven't attacked in a while, move towards the enemy and try to jump them
			if ( ThinkTimer > ThinkInterval )
			{
				ThinkTimer = 0;
				if ( WorldPosition.DistanceSquared( target.WorldPosition ) < AttackDistance && LungeTimer > LungeCooldown )
				{
					Attack();
					return;
				}

				Agent.UpdatePosition = true;
				Agent.UpdateRotation = true;
				Agent.MoveTo( target.WorldPosition );
			}
		}
		else // roaming
		{
			if ( ThinkTimer > RoamFrequency || Vector3.DistanceBetween( WorldPosition, RoamTarget ) <= RoamTargetThreshold )
			{
				ThinkTimer = 0;

				var target = GetRoamPoint();
				if ( target.HasValue )
				{
					RoamTarget = target.Value;

					Agent.UpdatePosition = true;
					Agent.UpdateRotation = true;
					Agent.MoveTo( RoamTarget );
				}
			}

			LookForTarget();

			if ( target.IsValid() )
			{
				// attack soon if we've just got a target
				LungeTimer = NewTargetLungeDelay;
			}
		}
	}

	private Vector3? GetRoamPoint()
	{
		var roamSize = new Vector3( RoamRadius, RoamRadius, RoamAreaHeight );
		var bbox = BBox.FromPositionAndSize( WorldPosition, new Vector3( RoamRadius, RoamRadius, RoamAreaHeight ) );

		// vaguely prefer moving in the direction we're looking
		bbox += ((roamSize.WithZ( 0 ) * WorldRotation.Forward) * 0.4f);

		for ( int i = 0; i < 10; i++ )
		{
			var p = Scene.NavMesh.GetClosestPoint( bbox.RandomPointInside );
			if ( !p.HasValue || p.Value.Distance( WorldPosition ) > RoamRadius )
				continue;

			// check if navable
			var path = Scene.NavMesh.CalculatePath( new Sandbox.Navigation.CalculatePathRequest()
			{
				Start = WorldPosition,
				Target = p.Value
			} );

			if ( path.Status != Sandbox.Navigation.NavMeshPathStatus.Partial && path.Status != Sandbox.Navigation.NavMeshPathStatus.Complete )
				continue;

			return p.Value;
		}

		return default;
	}

	/// <summary>
	/// Looks for a target, closest non-friend player is the target
	/// </summary>
	private void LookForTarget( bool withRange = true )
	{
		var allPlayers = Scene.GetAllComponents<Player>()
			.Where( x => Instigator.IsValid() ? x.PlayerData != Instigator : true );

		if ( withRange )
			allPlayers = allPlayers.Where( x => x.WorldPosition.DistanceSquared( WorldPosition ) < TargetDetectionRangeSquared );

		allPlayers = allPlayers.OrderBy( x => x.WorldPosition.DistanceSquared( WorldPosition ) );

		if ( allPlayers.Any() ) target = allPlayers.First();
	}

	void Attack()
	{
		// wait the full attack length
		LungeTimer = 0;

		DoAttackEffects();

		// We want to take over the rat movement by enabling a rigidbody and just flinging them at a player
		Agent.UpdatePosition = false;
		Agent.UpdateRotation = false;

		var dir = (target.WorldPosition - WorldPosition).WithZ( 0 ).Normal;

		IsOnGround = false;
		Rigidbody.Velocity = (dir * LungeVelocity.x) + (Vector3.Up * LungeVelocity.y);
		WorldRotation = Rotation.LookAt( dir );

		target = null;
	}

	[Rpc.Broadcast]
	void DoDeathEffects()
	{
		if ( Application.IsDedicatedServer ) return;

		DeathEffects?.Clone( WorldPosition );
	}

	[Rpc.Broadcast]
	void DoAttackEffects()
	{
		if ( Application.IsDedicatedServer ) return;

		Sound.Play( AttackSound, WorldPosition );
	}

	void IDamageable.OnDamage( in DamageInfo damage )
	{
		Die();
	}

	void Die()
	{
		DoDeathEffects();
		GameObject.Destroy();
	}
}