Enemy subclass 'Spitter' that controls a spitting enemy behavior, movement, shooting projectiles, collision handling and animations. It handles timing for preparing, shooting and finishing attacks, spawns enemy bullets via the Manager, and reacts to collisions with players and other enemies.
using Sandbox;
public class Spitter : Enemy
{
private TimeSince _damageTime;
private const float DAMAGE_TIME = 0.75f;
private float _shootDelayTimer;
protected float SHOOT_DELAY_MIN = 2f;
protected float SHOOT_DELAY_MAX = 4f;
public bool IsShooting { get; private set; }
protected bool _hasShot;
private TimeSince _prepareShootTime;
public override float HeightVariance => 0.04f;
public override float WidthVariance => 0.02f;
public float ShootRange { get; protected set; }
protected override void OnAwake()
{
//OffsetY = -0.45f;
ShadowScale = 1.1f;
ShadowFullOpacity = 0.8f;
ShadowOpacity = 0f;
Scale = 1f;
base.OnAwake();
//Sprite.Texture = Texture.Load("textures/sprites/spitter.vtex");
//Sprite.Size = new Vector2( 1f, 1f ) * Scale;
PushStrength = 8f;
Radius = 0.25f;
Health = 50f;
if ( Manager.Instance.Difficulty < 0 )
Health = 40f;
MaxHealth = Health;
DamageToPlayer = 10f;
CoinValueMin = 2;
CoinValueMax = 3;
CoinChance = 0.65f;
Sprite.PlayAnimation( AnimSpawnPath );
if ( IsProxy )
return;
CollideWith.Add( typeof( Enemy ) );
CollideWith.Add( typeof( Player ) );
_damageTime = DAMAGE_TIME;
_shootDelayTimer = Game.Random.Float( SHOOT_DELAY_MIN, SHOOT_DELAY_MAX );
ShootRange = 5f;
}
protected override void UpdatePosition( float dt )
{
//Gizmo.Draw.Color = Color.White;
//Gizmo.Draw.Text( $"Sprite.PlaybackSpeed: {Sprite.PlaybackSpeed}", new global::Transform( (Vector3)Position2D + new Vector3( 0f, -0.7f, 0f ) ) );
base.UpdatePosition( dt );
if ( IsProxy )
return;
var targetPos = GetTargetPos();
if ( IsShooting )
{
if ( !_hasShot && _prepareShootTime > 1.0f )
Shoot();
if ( _prepareShootTime > 1.6f )
FinishShooting();
return;
}
else
{
Velocity += (targetPos - Position2D).Normal * 1.0f * dt * (IsFeared ? -1f : 1f);
}
float speed = 0.9f * (IsAttacking ? 1.3f : 0.7f) + Utils.FastSin( MoveTimeOffset + Time.Now * (IsAttacking ? 15f : 7.5f) ) * (IsAttacking ? 0.66f : 0.35f);
if ( Manager.Instance.Difficulty < 0 )
speed *= 0.85f;
WorldPosition += (Vector3)Velocity * speed * dt;
if( Target.IsValid() )
{
var target_dist_sqr = (Target.Position2D - Position2D).LengthSquared;
var rangeSqr = ShootRange * ShootRange;
if ( Manager.Instance.Difficulty < 0 )
rangeSqr *= 0.75f;
//Gizmo.Draw.Color = Color.White;
//Gizmo.Draw.Text( $"rangeSqr: {rangeSqr} ShootRange: {ShootRange} target_dist_sqr: {target_dist_sqr}", new global::Transform( (Vector3)Position2D + new Vector3( 0f, -0.7f, 0f ) ) );
if ( !IsShooting && (!IsAttacking || IsCharmed) && target_dist_sqr < rangeSqr && !Manager.Instance.IsGameOver )
{
_shootDelayTimer -= dt;
if ( _shootDelayTimer < 0f )
{
PrepareToShoot();
}
}
}
}
protected virtual Vector2 GetTargetPos()
{
return Target.IsValid() ? Target.Position2D : (IsCharmed ? Manager.Instance.Player.Position2D : Position2D);
}
protected override void UpdateSprite( Thing target )
{
if ( Sprite.CurrentAnimation.Name.Contains( "shoot" ) ) return;
base.UpdateSprite( target );
}
public void PrepareToShoot()
{
if ( Manager.Instance.IsGameOver )
return;
_prepareShootTime = 0f;
IsShooting = true;
_hasShot = false;
DontChangeAnimSpeed = true;
AnimSpeed = 1f;
Sprite.PlayAnimation( "shoot" );
Manager.Instance.PlaySfxNearby( "spitter.prepare", Position2D, pitch: Game.Random.Float( 1f, 1.1f ), volume: 0.6f, maxDist: 2.75f );
CanAttack = false;
CanAttackAnim = false;
}
public virtual void Shoot()
{
if ( IsProxy ) return;
var target_pos = Target.IsValid() ? Target.Position2D + Target.Velocity * Game.Random.Float( 0.5f, 1.5f ) : Position2D + Utils.GetRandomVector() * 5f;
var dir = Utils.RotateVector( (target_pos - Position2D).Normal, Game.Random.Float( -10f, 10f ) );
//Manager.Instance.SpawnEnemyBullet( Position2D + new Vector2( 0f, 0.45f ) + dir * 0.05f, dir, speed: 2f );
var bullet = Manager.Instance.SpawnEnemyBullet( Position2D + dir * 0.05f, dir, speed: 1.8f );
if(IsCharmed)
{
bullet.Creator = this;
bullet.BecomeCharmed();
}
Velocity *= 0.25f;
_hasShot = true;
Manager.Instance.PlaySfxNearby( "spitter.shoot", Position2D, pitch: Game.Random.Float( 0.8f, 0.9f ), volume: 0.9f, maxDist: 5f );
Sprite.PlayAnimation( "shoot_reverse" );
}
public void FinishShooting()
{
Sprite.PlayAnimation( AnimIdlePath );
CanAttack = true;
CanAttackAnim = true;
_shootDelayTimer = Game.Random.Float( SHOOT_DELAY_MIN, SHOOT_DELAY_MAX ) * (Manager.Instance.Difficulty < 0 ? 1.5f : 1f);
IsShooting = false;
DontChangeAnimSpeed = false;
}
public override void Colliding( Thing other, float percent, float dt )
{
base.Colliding( other, percent, dt );
if ( other is Enemy enemy && !enemy.IsDying )
{
var spawnFactor = Utils.Map( enemy.TimeSinceSpawn, 0f, enemy.SpawnTime, 0f, 1f, EasingType.QuadIn );
Velocity += (Position2D - enemy.Position2D).Normal * Utils.Map( percent, 0f, 1f, 0f, 1f ) * enemy.PushStrength * (1f + enemy.TempWeight) * spawnFactor * dt;
if ( IsAttacking && IsCharmed != enemy.IsCharmed && _damageTime > (DAMAGE_TIME / TimeScale) )
{
var dmg = DamageToPlayer;
if ( IsCharmed )
dmg *= CharmDamageDealtMultiplier;
enemy.Damage( dmg, null, addVel: Vector2.Zero, addTempWeight: 0f, isCrit: false, DamageType.Melee );
enemy.Target = this;
_damageTime = 0f;
Manager.Instance.PlaySfxNearby( "zombie.attack.player", Position2D, pitch: Utils.Map( enemy.Health, enemy.MaxHealth, 0f, 0.95f, 1.15f, EasingType.QuadIn ), volume: 0.6f, maxDist: 4.5f );
}
}
else if ( other is Player player )
{
if ( !player.IsDead )
{
Velocity += (Position2D - player.Position2D).Normal * Utils.Map( percent, 0f, 1f, 0f, 1f ) * player.Stats[PlayerStat.PushStrength] * (1f + player.TempWeight) * dt;
if ( IsAttacking && _damageTime > (DAMAGE_TIME / TimeScale) && !IsCharmed )
{
float dmg = player.CheckDamageAmount( DamageToPlayer, DamageType.Melee );
if ( !player.IsInvulnerable && !player.IsTimePausedForChoosing )
{
Manager.Instance.PlaySfxNearby( "zombie.attack.player", Position2D, pitch: Utils.Map( player.Health, player.Stats[PlayerStat.MaxHp], 0f, 0.95f, 1.15f, EasingType.QuadIn ), volume: 1f, maxDist: 5.5f );
player.Damage( dmg );
if ( dmg > 0f )
OnDamagePlayer( player, dmg );
}
_damageTime = 0f;
}
}
}
}
public override void Celebrate()
{
base.Celebrate();
CelebrateAsync();
}
async void CelebrateAsync()
{
await Task.Delay( Game.Random.Int( 0, 500 ) );
Sprite.PlaybackSpeed = Game.Random.Float( 1f, 3f );
Sprite.PlayAnimation( "cheer_start" );
await Task.Delay( Game.Random.Int( 200, 400 ) );
Sprite.PlayAnimation( "cheer" );
}
}