GhostBrain.cs
using Sandbox;
using System;
using System.Threading.Tasks;
using static Ballistics;
using static HealthSystem;

public enum GhostState { Culled, Idle, Moving, Attack, Staggered }


public sealed class GhostBrain : Component, HealthSystem.IHealthEvent
{
	GameObject Player;
	Rigidbody PlayerBody;
	[Property] GameObject GhostBall;
	[Property] GameObject DeadGhost;
	[Property] TextRenderer StateDebugText;
	[Property] bool DebugMode;
	[Property] GameObject BloodDecal;
	[Property] SoundEvent AttackSound;
	[Property] SoundEvent IdleSound;
	[Property] SoundEvent DeathSound;

	public GhostState CurrentState;

	SceneTraceResult GroundTrace;

	Vector3 HorizontalOffset;
	public Vector3 TargetPosition;
	Vector3 SchwebeMittelPunkt;
	float SchwebeDistance = 100;
	float SchwebeFrequenz = 2f;

	Vector3 IdleKreisPunkt;
	float IdleKreisRadius = 1000f;
	float IdleKreisAngle = 0f;

	TimeSince timeSinceLastAttack;
	float AttackCharge = 0;

	TimeSince SinceHitAnimationStart;
	Vector3 originalScale;

	TimeSince SinceGroundCheck = 0;
	public TimeUntil UntilKnockBack;
	TimeUntil NextOffset;
	Random random;

	void IHealthEvent.OnDeath()
	{
		GroundTrace = Scene.Trace.Ray( WorldPosition, WorldPosition + Vector3.Down * 7000 )
			.IgnoreGameObjectHierarchy( GameObject )
			.WithoutTags( "enemy", "dead", "player", "playerclip" )
			.Run();
		GameObject newDecal = BloodDecal.Clone( GroundTrace.EndPosition, Rotation.LookAt( GroundTrace.Normal, Vector3.Up ).Angles().WithYaw( random.Int( -180, 180 ) ) );


		Sound.Play(DeathSound, WorldPosition);
		DeadGhost.Clone( WorldPosition + Vector3.Up * 100, GameObject.WorldRotation, GameObject.WorldScale );
		GameObject.Parent.Destroy();
	}

	protected override void OnStart()
	{
		random = new Random();

		Player = Scene.FindAllWithTag( "carbody" ).First<GameObject>();
		PlayerBody = Player.GetComponent<Rigidbody>();

		originalScale = WorldScale;

		if ( !DebugMode ) { StateDebugText.Enabled = false; }

		CurrentState = GhostState.Moving;

		GroundTrace = Scene.Trace
			.Ray( WorldPosition, WorldPosition + Vector3.Down * 3000 )
			.Radius( 1 )
			.IgnoreGameObjectHierarchy( GameObject )
			.WithoutTags( "enemy", "player", "dead" )
			.Run();

		HorizontalOffset = (Vector3)random.VectorInCircle(1000);
		SchwebeMittelPunkt = GroundTrace.EndPosition + Vector3.Up * 80;

		SchwebeFrequenz += random.Float( -0.1f, 0.1f );
		GameObject.WorldPosition = SchwebeMittelPunkt;

		IdleKreisPunkt = WorldPosition.WithZ( GroundTrace.EndPosition.z ) + (Vector3)random.VectorInCircle( 1 ).Normal * random.Int( 300, 500 );
	}

	protected override void OnFixedUpdate()
	{
		if (SinceGroundCheck > 0.3f && CurrentState != GhostState.Idle ) 
		{
			GroundTrace = Scene.Trace
				.Ray( TargetPosition, TargetPosition + Vector3.Down * 3000 )
				.Radius( 1 )
				.IgnoreGameObjectHierarchy( GameObject )
				.WithoutTags( "enemy", "player", "dead" )
				.Run();

			SinceGroundCheck = 0;
		}

		Vector3 playerPosition = Player.WorldPosition;
		Vector3 hoverHeight = (GroundTrace.EndPosition + Vector3.Up * 1000).WithY( 0 ).WithX( 0 );
		if ( !GroundTrace.Hit ) { hoverHeight = Vector3.Up * 1000; }

		switch ( CurrentState ) 
		{
			default: 
				StateDebugText.Text = "DEFAULT";
				break;

			case GhostState.Idle:
				StateDebugText.Text = "IDLE";

				// Wenn Ghost weiter als der Radius * 1.5f vom Punkt entfernt ist soll er einen neuen zufälligen finden
				if ((WorldPosition.WithZ(0) - IdleKreisPunkt.WithZ(0)).Length > IdleKreisRadius * 1.5f)
				{
					IdleKreisPunkt = WorldPosition.WithZ(GroundTrace.EndPosition.z) + (Vector3)random.VectorInCircle(1).Normal * random.Int(300, 500);
				}

				// Winkel un damit Koordinaten vom aufm Kreis berechnen
				IdleKreisAngle += Time.Delta * 0.2f; // * Speed
				float x = MathF.Cos(IdleKreisAngle);
				float y = MathF.Sin(IdleKreisAngle);

				// TargetPosition aus IdleKreisPunkt, Koordinaten und height berechnen, SchwebeMittelPunkt zu TargetPos LERPen
				TargetPosition = IdleKreisPunkt + new Vector3(x, y, 0).Normal * IdleKreisRadius + hoverHeight;
				SchwebeMittelPunkt = SchwebeMittelPunkt.LerpTo(TargetPosition, Time.Delta * (TargetPosition - SchwebeMittelPunkt).Length.Remap(0, 5000, 0.5f, 1));

				if (DebugMode)
				{
					DebugOverlay.Sphere( new Sphere( TargetPosition, 16 ), Color.Red );
					DebugOverlay.Sphere( new Sphere( IdleKreisPunkt.WithZ( GroundTrace.EndPosition.z ), 16 ), Color.Blue );
				}
				
				// Zeit schiebt Sinusfunktion(Welle) voran, multipliziert mit Frequenz für enge oder weite Wellen, 
				// dann mit Distanz multiplizieren und auf MittelPunkt addieren
				Transform transform = new Transform(
					SchwebeMittelPunkt + Vector3.Up * (MathF.Sin( Time.Now * (SchwebeFrequenz) ) * SchwebeDistance),
					Rotation.LookAt( TargetPosition - SchwebeMittelPunkt, Vector3.Up ),
					WorldScale
				);

				WorldTransform = transform;

				if ( (playerPosition - WorldPosition).Length < 10000 ) { CurrentState = GhostState.Moving; }	

				break;

			// MOVING&SHOOTING
			case GhostState.Moving: 
				StateDebugText.Text = "WANDER";
				
				TargetPosition = playerPosition.WithZ(0) + (WorldPosition.WithZ( 0 ) - playerPosition.WithZ(0) ).Normal * 5500 + hoverHeight + HorizontalOffset;
				SchwebeMittelPunkt = SchwebeMittelPunkt.LerpTo(TargetPosition, Time.Delta * (TargetPosition - SchwebeMittelPunkt).Length.Remap(0, 5000, 0.2f, 0.5f));
				// SchwebeMittelPunkt = SchwebeMittelPunkt + ( TargetPosition - SchwebeMittelPunkt ) * 0.5f + Vector3.Left * (MathF.Sin( (TargetPosition - SchwebeMittelPunkt).Length * SchwebeFrequenz ) * SchwebeDistance);
				
				// Zeit schiebt Sinusfunktion(Welle) voran, multipliziert mit Frequenz für enge oder weite Wellen, dann mit Distanz multiplizieren und auf MittelPunkt addieren
				GameObject.WorldPosition = SchwebeMittelPunkt + Vector3.Up * (MathF.Sin( Time.Now * (SchwebeFrequenz) ) * SchwebeDistance);
				
				LookAtPlayer();
				
				if ( timeSinceLastAttack > 4 && playerPosition.Distance(WorldPosition) < 7000 && playerPosition.Distance( WorldPosition ) > 2000 ) 
				{ timeSinceLastAttack = random.Float(0f, 0.2f); Attack(); }
				else {  }
				if ( timeSinceLastAttack < 0.01f )
				{
					// HorizontalOffset = Player.WorldRotation.Left * random.Int( 1, 3 ) * random.Int( -1, 1 );
				}
				// if ( (TargetPosition - MittelPunkt).IsNearlyZero(500) ) CurrentState = GhostState.Attack;
				
				if ( (playerPosition - WorldPosition).Length > 10000 ) { CurrentState = GhostState.Idle; }	
				break;

			// ATTACK
			case GhostState.Attack:
				StateDebugText.Text = "ATTACK";


				break;

			// STAGGERED
			case GhostState.Staggered:
				StateDebugText.Text = "STAGGERED";

				SchwebeMittelPunkt = SchwebeMittelPunkt.LerpTo( TargetPosition, Time.Delta * (TargetPosition - SchwebeMittelPunkt).Length.Remap( 0, 5000, 1, 3 ) );
				GameObject.WorldPosition = SchwebeMittelPunkt + Vector3.Up * (MathF.Sin( Time.Now * (SchwebeFrequenz) ) * SchwebeDistance);

				if ( UntilKnockBack ) { CurrentState = GhostState.Moving; }

				break;
		}


	}

	private void LookAtPlayer()
	{
		if ( GameObject.WorldRotation.Forward.Angle( Player.WorldPosition - GameObject.WorldPosition ) > 1 )
		{
			GameObject.WorldRotation = GameObject.WorldRotation.LerpTo( Rotation.LookAt( Player.WorldPosition - GameObject.WorldPosition, Vector3.Up ), 0.5f );
		}
	}

	private void Attack()
	{
		SceneTraceResult sightlineCheck = Scene.Trace
			.Sphere( 16, WorldPosition, PlayerBody.WorldPosition + Vector3.Up * 100 )
			.IgnoreGameObjectHierarchy( GameObject )
			.WithoutTags( "enemy", "player", "world" )
			.Run();

		if ( sightlineCheck.Hit )
		{
			// Log.Info( "No line of sight to player, skipping attack" );
			HorizontalOffset = (Vector3)random.VectorInCircle( 1000 );
			return;
		}

		GameObject newBall = GhostBall.Clone( WorldPosition );
		Rigidbody newBody = newBall.GetComponent<Rigidbody>();
		// newBody.Velocity = Vector3.Down * 100;

		// Sound
		newBall.GetComponent<GhostBallLogic>().attackSoundHandle = Sound.Play( AttackSound, newBall.WorldPosition );

		// Unity Conversion
		Vector3 UnityProjPos = new Vector3( WorldPosition.x, WorldPosition.z, WorldPosition.y );
		Vector3 UnityTargetPos = new Vector3( Player.WorldPosition.x, Player.WorldPosition.z + 100, Player.WorldPosition.y ) + random.VectorInSphere(1);
		Vector3 UnityTargetVel = new Vector3( PlayerBody.Velocity.x, PlayerBody.Velocity.z, PlayerBody.Velocity.y );
		if ( UnityTargetVel.Length < 1 ) { UnityTargetVel = Vector3.Forward; }

		if ( Ballistics.solve_ballistic_arc( UnityProjPos, 
			5000, 
			UnityTargetPos, 
			UnityTargetVel, 
			Scene.PhysicsWorld.Gravity.Length * 0.001f,
			out Vector3 s0, out Vector3 s1 ) > 0 ) 
		{ 
			// Convert back to S&box
			Vector3 solutionVelocity = new Vector3(s0.x, s0.z, s0.y);
			
			//Apply Velocity
			newBody.Velocity = solutionVelocity;
		}
	}

	public async Task AnimateHit()
	{
		float duration = 0.1f;
		SinceHitAnimationStart = 0;

		WorldScale = WorldScale.WithZ( originalScale.z * (1f - 0.15f) );

		while ( SinceHitAnimationStart < duration )
		{
			WorldScale = WorldScale.WithZ( WorldScale.z.LerpTo( originalScale.z, 0.1f ) );
			await Task.FrameEnd();
		}

		if ( !this.IsValid() ) return;
		WorldScale = originalScale;
	}

}