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