BulletEnemy.cs
using Sandbox;
using Sandbox.Diagnostics;

public interface IBulletEnemyEvents : ISceneEvent<IBulletEnemyEvents>
{
	void OnDeath(GameObject enemy) { }
}

public class ThresholdObject
{
	[Property]
	[Range(0.0f, 1.0f)]
	public float HealthPercentage { get; set; }

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

public sealed class BulletEnemy : Component, Component.IDamageable, IRareEnemy
{
	[Property]
	public GameObject BulletSpawnPoint { get; set; }

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

	[Property]
	public float FireDelay { get; set; }

	[Property]
	public int StartingHealth { get; set; } = 100;

	[Property]
	public SoundEvent ImpactSound { get; set; }

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

	[Property]
	public SoundEvent DeathSoundEffect { get; set; }

	/// <summary>
	/// The part that turns left and right.
	/// </summary>
	[Property]
	public GameObject TurretBody { get; set; }

	/// <summary>
	/// The part that turns up and down.
	/// </summary>
	[Property]
	public GameObject TurretGun { get; set; }

	/// <summary>
	/// The thing that spawns on death
	/// </summary>
	[Property]
	public GameObject Goodie { get; set; }

	[Property]
	public int MinGoodieCount { get; set; }

	[Property]
	public int MaxGoodieCount { get; set; }

	/// <summary>
	/// Objects that should be activated at certain HP thresholds.
	/// </summary>
	[Property]
	[InlineEditor]
	[WideMode]
	public List<ThresholdObject> ThresholdObjects { get; set; }

	[Property]
	public SoundEvent ChargingBullet { get; set; }

	[Property]
	public SoundEvent Shot { get; set; }

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

	[Property]
	public SoundEvent Spawned { get; set; }

	bool canFire = true;
	float currentHealth;

	bool IsAlive => currentHealth > 0;

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

		Assert.IsValid( BulletSpawnPoint );
		Assert.IsValid( DeathExplosion );
		Assert.True( FireDelay > 0 );

		currentHealth = StartingHealth;

		FireDelay *= Game.Random.Float( 0.95f, 1.05f );
		GetComponent<NavMeshAgent>().MaxSpeed *= Game.Random.Float( 0.9f, 1.1f );

		Sound.Play( Spawned, WorldPosition );
	}

	protected override void OnUpdate()
	{
		if ( IsAlive && canFire ) FireAtPlayer();
	}

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

		MoveForwards();
		AimTowardsTarget();
	}

	void MoveForwards()
	{
		LocalPosition = GetComponent<NavMeshAgent>().AgentPosition;
		// var body = GetComponent<Rigidbody>();

		// body.SmoothMove(GetComponent<NavMeshAgent>().AgentPosition, 0.1f, Time.Delta);
		// body.SmoothRotate(GetComponent<NavMeshAgent>().WorldRotation, 0.1f, Time.Delta);
	}


	void AimTowardsTarget()
	{
		if ( Target is null ) return;

		var rot = Rotation.LookAt( Target.WorldPosition.WithZ( 10 ) - WorldPosition );

		TurretBody.WorldRotation = Rotation.FromYaw( rot.Yaw() );
		TurretGun.LocalRotation = Rotation.FromPitch( rot.Pitch() );
	}

	public GameObject Target;

	GameObject ClosestPlayer()
	{
		try
		{
			return Scene.GetSystem<PlayerWatcher>()
				.AlivePlayers
				.Select( player => (player.WorldPosition.DistanceSquared( WorldPosition ), player) )
				.Aggregate( ( best, next ) => best.Item1 < next.Item1 ? best : next )
				.player;
		}
		catch ( System.InvalidOperationException )
		{
			return null;
		}
	}

	async void FireAtPlayer()
	{
		if ( BulletPrefab is null ) return;

		canFire = false;

		Target = ClosestPlayer();
		if ( Target is null )
		{
			await Task.DelaySeconds( 0.05f );
			canFire = true;
			return; // TODO: react to no players
		}

		var trace = Scene.Trace.Ray( BulletSpawnPoint.WorldTransform.ForwardRay, 5000f )
			.Run();

		if ( !trace.Hit || !trace.GameObject.Tags.Has("player") )
		{
			await Task.DelaySeconds( 0.05f );
			canFire = true;
			return;
		}

		Sound.Play( ChargingBullet, WorldPosition ).Parent = GameObject;

		await Task.DelaySeconds( 0.8f );

		// in case we died
		if ( !Enabled ) return;

		var bullet = BulletPrefab.Clone( BulletSpawnPoint.WorldTransform, null, false );
		bullet.Tags.Add( "enemy" );

		var bulletInfo = bullet.GetComponent<Bullet>( true );
		bulletInfo.Owner = GameObject;
		bulletInfo.Weapon = TurretGun;
		bullet.Enabled = true;

		Sound.Play( Shot, WorldPosition ).Parent = GameObject;

		var tr = BulletSpawnPoint.WorldTransform;
		tr.Rotation *= Rotation.FromRoll( 180 );

		tr.Position += tr.Forward * 5;

		ShootEffect.Clone( tr );

		await Task.DelaySeconds( FireDelay - 0.8f );
		canFire = true;
	}

	void IDamageable.OnDamage( in DamageInfo damage )
	{
		//Sound.Play( ImpactSound, damage.Position );
		currentHealth -= damage.Damage;

		var healthPercentage = currentHealth / StartingHealth;
		var objectsToActivate = ThresholdObjects.Where( thr => healthPercentage < thr.HealthPercentage ).ToList();

		foreach (var obj in objectsToActivate)
		{
			obj.GameObject.Enabled = true;
			ThresholdObjects.Remove( obj );
		}

		if ( currentHealth <= 0 )
		{
			// spawn goodies
			var goodiesToSpawn = Game.Random.Int( MinGoodieCount, MaxGoodieCount );
			for ( var i = 0; i < goodiesToSpawn; i++ )
			{
				var pos = Game.Random.VectorInCube( 30 );
				var goodie = Goodie.Clone( WorldPosition + pos.WithZ( 5 ) );
			}

			Die();
		}
	}

	public void Die()
	{
		// play death effects
		Sound.Play( DeathSoundEffect, WorldPosition );
		DeathExplosion.Clone( WorldPosition ).AddComponent<TemporaryEffect>();

		// turn off everything
		foreach (var child in GameObject.Children)
		{
			if ( child.GetComponent<ParticleEffect>() is null ) child.Enabled = false;
		}

		// turn off all our components
		foreach (var component in GameObject.Components.GetAll())
		{
			component.Enabled = false;
		}

		// disable particle emitters
		foreach (var emitter in GameObject.Components.GetAll<ParticleEmitter>())
		{
			emitter.Enabled = false;
		}

		// notify
		IBulletEnemyEvents.Post( x => x.OnDeath( GameObject ) );

		// disappear
		// DestroyGameObject();
		GameObject.AddComponent<TemporaryEffect>().DestroyAfterSeconds = 0;
	}

	void IRareEnemy.MakeRare()
	{
		// red paint = danger
		foreach ( var model in Components.GetAll<ModelRenderer>() )
		{
			model.Tint = "#AD0000";
		}

		// increase our stats
		GetComponent<NavMeshAgent>().MaxSpeed *= 2;
		FireDelay /= 2;
		StartingHealth *= 2; // so the health threshold things work properly
		currentHealth *= 2;
		MinGoodieCount *= 2;
		MaxGoodieCount *= 2;

		// increase engine noise pitch so we can be idenfitied at range
		var sound = GetComponent<BaseSoundComponent>();
		sound.Pitch = 1.7f;
		sound.SoundOverride = true;

		// debugging
		GameObject.Name = "RARE " + GameObject.Name;
	}
}