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

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


public sealed class VampireBrain : Component, HealthSystem.IHealthEvent
{
	GameObject Player;
	Rigidbody PlayerBody;
	[Property] TextRenderer StateDebugText;
	[Property] bool DebugMode;
	[Property] GameObject ParticleObject;
	[Property] GameObject BloodDecal;
	ParticleEmitter BloodEmitter;
	[Property] GameObject DeathParticle;

	public VampireState CurrentState;

	SceneTraceResult GroundTrace;

	Vector3 CircleOffset;
	public Vector3 TargetPosition;
	Vector3 SchwebeMittelPunkt;
	float SchwebeDistance = 25;
	float SchwebeFrequenz = 4f;

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

	float AttackCharge = 0;

	TimeSince SinceHitAnimationStart;
	Vector3 originalScale;

	TimeSince SinceGroundCheck = 0;
	public TimeUntil UntilKnockBack;
	TimeUntil NextOffset;
	TimeUntil UntilNextIdleSound;
	TimeSince SinceAttack;
	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( "sounds/vampire/vampire-death.sound", GameObject.WorldPosition );
		DeathParticle.Clone( GameObject.WorldPosition + Vector3.Up * 150, WorldRotation, WorldScale * 1.5f );
		GameObject.Parent.Destroy();
	}

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

		originalScale = WorldScale;

		if ( !DebugMode ) { StateDebugText.Enabled = false; }
		
		Player = Scene.FindAllWithTag( "carbody" ).First<GameObject>();
		PlayerBody = Player.GetComponent<Rigidbody>();
		BloodEmitter = ParticleObject.GetComponent<ParticleEmitter>();
		// Log.Info(Player.Name);

		CurrentState = VampireState.Moving;

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

		// HorizontalOffset = Player.WorldRotation.Left * random.Int( 1000, 3000 ) * random.Int( -1, 1 );
		CircleOffset = new Vector3(  random.Float( -0.5f, 0.5f ), random.Float( -1, 1 ), 0 ).Normal;
		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 != VampireState.Idle )
		{
			SinceGroundCheck = 0;
			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 * 50).WithY( 0 ).WithX( 0 );
		if ( !GroundTrace.Hit ) { hoverHeight = Vector3.Up * 100; }

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

			// IDLE
			case VampireState.Idle:
				StateDebugText.Text = "IDLE";
				
				if (UntilNextIdleSound) 
				{
					Sound.Play("sounds/vampire/vampire-idle.sound", WorldPosition);
					UntilNextIdleSound = random.Float(6, 10);
				}

				// Wenn Vampir 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.5f; // * 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, 1, 3));
				
				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 - GameObject.WorldPosition, Vector3.Up ),
					WorldScale);

				if ( (playerPosition - WorldPosition).Length < 8000 ) { CurrentState = VampireState.Moving; }				
				break;


			// WANDER
			case VampireState.Moving: 
				StateDebugText.Text = "MOVING";
				
				if (UntilNextIdleSound) 
				{
					Sound.Play("sounds/vampire/vampire-idle.sound", WorldPosition);
					UntilNextIdleSound = random.Float(6, 10);
				}

				// TargetPosition bestimmen und MittePunkt hinLERPen
				TargetPosition = playerPosition + CircleOffset * 1000 + hoverHeight;
				SchwebeMittelPunkt = SchwebeMittelPunkt.LerpTo(TargetPosition, Time.Delta * (TargetPosition - SchwebeMittelPunkt).Length.Remap(0, 5000, 1, 3));
				// 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 ( (playerPosition - WorldPosition).Length < 1300 && SinceAttack > 1f ) { Attack(); SinceAttack = 0 + random.Float(0.05f, 0.1f); }
				// else { // BloodEmitter.Enabled = false; }
				
				if ( (playerPosition - WorldPosition).Length > 8000 ) { CurrentState = VampireState.Idle; }
				break;

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


				break;

			// STAGGERED
			case VampireState.Staggered:
				StateDebugText.Text = "STAGGERED";
				
				BloodEmitter.Enabled = false;

				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 = VampireState.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()
	{
		Player.GetComponentInParent<HealthSystem>().Damage(80, false);
		// Particle & Sound

		ParticleObject.WorldPosition = PlayerBody.WorldPosition + Vector3.Up * 50;
		BloodEmitter.ResetEmitter();
	}

	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;
	}


}