Enemy subclass for a snake-like enemy called Snaker. It handles movement, leader/follower chaining (segments), shooting state machine (prepare, shoot, finish), projectile spawning, animations and reactions to death/stun and out-of-bounds events.
using System;
using Sandbox;
public class Snaker : Enemy
{
public override EnemyType EnemyType => EnemyType.Snaker;
public override float GetMaxHealth()
{
return 65f;
}
public override Vector3 SpawnScale => new Vector3( 1.2f );
protected float _shootDelayTimer;
protected float _shootDelayMin;
protected float _shootDelayMax;
protected float _shootRange;
public override bool CanMove => base.CanMove && !HasLeader;
protected override bool ShouldRetreatFromTarget => IsFearful && !HasLeader && !IsAttacking;
public Snaker Leader { get; set; }
public Snaker Follower { get; set; }
private Vector2 _moveDir;
public bool HasLeader => Leader.IsValid();
private TimeSince _timeSinceLeaderTargetChanged;
private float _targetDelay;
public int NumSegments { get; set; }
public int NumSegmentsStart { get; set; }
protected enum SnakerState
{
Default,
ShootPrepare,
Shoot,
ShootFinish,
}
protected SnakerState State { get; private set; } = SnakerState.Default;
protected override void OnStart()
{
base.OnStart();
CoinValueMin = 2;
CoinValueMax = 4;
CoinChance = 0.55f;
PushStrength = 7000f;
Weight = 1.4f;
_personalSpeedScale = Game.Random.Float( 1.2f, 1.4f );
_personalSpeedFreq = Game.Random.Float( 9f, 11f );
if ( IsProxy )
return;
AggroRange = 50f;
DetectTargetRange = 800f;
LoseTargetRange = 1300f;
LoseTargetTime = 5f;
MeleeDamage = 11f;
DamageTargetDelay = 0.6f;
_personalTurnSpeed = Game.Random.Float( 7f, 8f );
Acceleration = AccelerationAttacking = 160f;
Deceleration = DecelerationAttacking = 1.9f;
_shootDelayMin = 6.5f;
_shootDelayMax = 8f;
_shootRange = 500f;
_shootDelayTimer = Game.Random.Float( 1f, 10f );
_moveDir = Utils.GetRandomVector();
_timeSinceLeaderTargetChanged = 0f;
_targetDelay = Game.Random.Float( 2f, 5f );
}
protected override void OnUpdate()
{
base.OnUpdate();
//Gizmo.Draw.Color = Color.White;
//Gizmo.Draw.Text( $"{NumSegments}", new global::Transform( WorldPosition ) );
//if ( IsBlinking )
//{
// Gizmo.Draw.Color = Color.Cyan;
// Gizmo.Draw.Line( WorldPosition, _blinkPos );
//}
if ( Manager.Instance.IsGameOver )
return;
if ( IsProxy )
return;
if ( !IsStunned )
HandleState();
if ( IsInTheAir )
return;
bool hasLeader = Leader.IsValid();
if ( !hasLeader && _timeSinceLeaderTargetChanged > _targetDelay && TargetUnit.IsValid() )
{
var targetPos = TargetUnit.Position2D + TargetUnit.Velocity * Game.Random.Float( 0f, 2f ) + Utils.GetRandomVector() * Game.Random.Float(0f, 50f);
_moveDir = (targetPos - Position2D).Normal;
_timeSinceLeaderTargetChanged = 0f;
_targetDelay = Game.Random.Float( 1f, 10f ) * Utils.Map( NumSegments, NumSegmentsStart, 1, 1f, 0.1f, EasingType.QuadIn );
}
//Gizmo.Draw.Color = Color.White;
//Gizmo.Draw.Line( WorldPosition, TargetPos );
if( hasLeader )
Position2D = Utils.DynamicEaseTo( Position2D, Leader.Position2D, Utils.Map( (Leader.Position2D - Position2D).LengthSquared, MathF.Pow( 250f, 2f ), 0f, 0.5f, 0f, EasingType.QuadIn ), Time.Delta );
AnimSpeedModifier = HasLeader
? Utils.Map( (Leader.Position2D - Position2D).LengthSquared, MathF.Pow( 250f, 2f ), Radius * Radius, 15f, 0.1f, EasingType.QuadIn )
: 1.5f;
AnimSpeedModifier *= Utils.Map( NumSegments, NumSegmentsStart, 1, 0.8f, 1.5f );
}
protected void HandleState()
{
switch ( State )
{
case SnakerState.Default:
if ( TargetUnit.IsValid() && !IsInTheAir )
{
var targetDistSqr = (TargetUnit.Position2D - Position2D).LengthSquared;
if ( targetDistSqr < MathF.Pow( _shootRange, 2f ) )
{
_shootDelayTimer -= Time.Delta * TimeScale;
if ( _shootDelayTimer < 0f && targetDistSqr < MathF.Pow( _shootRange * 0.85f, 2f ) )
SetState( SnakerState.ShootPrepare );
}
}
break;
case SnakerState.ShootPrepare:
if ( _timeSinceChangeState > 1f )
SetState( SnakerState.Shoot );
break;
case SnakerState.Shoot:
if ( _timeSinceChangeState > 0.6f )
SetState( SnakerState.ShootFinish );
break;
}
}
protected void SetState( SnakerState state )
{
State = state;
_timeSinceChangeState = 0f;
switch ( state )
{
case SnakerState.Default:
EnterDefaultStateRpc();
break;
case SnakerState.ShootPrepare:
StartShootingRpc();
_shootDelayTimer = Game.Random.Float( _shootDelayMin, _shootDelayMax ) * Utils.Map( NumSegments, NumSegmentsStart, 1, 2f, 0.25f, EasingType.SineIn );
break;
case SnakerState.Shoot:
if( TargetUnit.IsValid() )
{
ShootRpc();
var targetPos = TargetUnit.Position2D + TargetUnit.Velocity * Game.Random.Float( 0f, 2f ) + Utils.GetRandomVector() * Game.Random.Float( 0f, 50f );
var dir = (targetPos - Position2D).Normal;
var pos = Position2D + dir * 20f;
Manager.Instance.SpawnEnemyProjectile( pos, dir, shooter: this, enemyType: this.EnemyType, startVel: 150f );
}
break;
case SnakerState.ShootFinish:
SetState( SnakerState.Default );
break;
}
}
protected override void HandleRotation()
{
var facingPos = HasLeader
? Leader.Position2D
: Position2D + _moveDir * 100f;
var targetFacingDir = ((Vector3)facingPos - WorldPosition).Normal.WithZ( 0f ); // todo: optimize?
if ( ShouldRetreatFromTarget )
targetFacingDir *= -1f;
WorldRotation = Rotation.Lerp( WorldRotation, Rotation.LookAt( targetFacingDir ), _personalTurnSpeed * Time.Delta * TimeScale );
}
protected override float GetMoveSpeedFactor()
{
return (0.8f + Utils.FastSin( MoveTimeOffset + Time.Now * _personalSpeedFreq ) * 0.2f) * _personalSpeedScale;
}
//protected override Vector2 GetTargetOffset()
//{
// var offset = TargetUnit.Velocity * (0.5f + Utils.FastSin( TimeSinceSpawn * 3f ) * 0.5f) * (TargetUnit.Position2D - Position2D).Length * 0.012f;
// if ( !IsShooting && !_isRetreating )
// offset += _personalTargetOffset + new Vector2( Utils.FastSin( TimeSinceSpawn * 0.7f ), Utils.FastSin( TimeSinceSpawn * 1.1f ) ) * 50f;
// return offset;
//}
[Rpc.Broadcast]
protected void StartShootingRpc()
{
//CanAnimate = false;
//PlayShootAnim();
Manager.Instance.PlaySfxNearby( "spitter.prepare", Position2D, pitch: Game.Random.Float( 1.4f, 1.5f ), volume: 0.2f, maxDist: 200f );
}
//protected virtual void PlayShootAnim()
//{
// SetAnim( Game.Random.Float( 0f, 1f ) < 0.5f ? "HoldItem_RH_Throw_Normal" : "HoldItem_LH_Throw_Normal" );
// SetPlaybackRate( 0.5f );
//}
[Rpc.Broadcast]
protected void ShootRpc()
{
Manager.Instance.PlaySfxNearby( "spitter.shoot", Position2D, pitch: Game.Random.Float( 1.4f, 1.5f ), volume: 0.6f, maxDist: 350f );
}
public override void Die( Vector2 dir, float force, Player player, DamageType damageType )
{
base.Die( dir, force, player, damageType );
if ( IsProxy )
return;
if ( Follower.IsValid() && Leader.IsValid() )
Follower.AssignLeader( Leader );
if ( Leader.IsValid() )
Leader.NotifyDeathAhead();
if ( Follower.IsValid() )
Follower.NotifyDeathBehind();
}
public void AssignLeader( Snaker other )
{
Leader = other;
other.AssignFollower( this );
}
public void AssignFollower( Snaker other )
{
Follower = other;
}
public void NotifyDeathAhead()
{
NotifyDeath();
if ( Leader.IsValid() )
Leader.NotifyDeathAhead();
}
public void NotifyDeathBehind()
{
NotifyDeath();
if ( Follower.IsValid() )
Follower.NotifyDeathBehind();
}
public void NotifyDeath()
{
NumSegments--;
Acceleration = AccelerationAttacking = Utils.Map( NumSegments, NumSegmentsStart, 1, 160f, 240f, EasingType.SineIn );
Deceleration = Utils.Map( NumSegments, NumSegmentsStart, 1, 1.9f, 1.5f, EasingType.QuadIn );
DecelerationAttacking = DecelerationAttacking * 0.9f;
}
protected override void OnOutOfBounds( Direction direction )
{
base.OnOutOfBounds( direction );
if ( direction == Direction.Left )
_moveDir = new Vector2( Math.Abs( _moveDir.x ), _moveDir.y );
else if ( direction == Direction.Right )
_moveDir = new Vector2( -Math.Abs( _moveDir.x ), _moveDir.y );
else if ( direction == Direction.Down )
_moveDir = new Vector2( _moveDir.x, Math.Abs( _moveDir.y ) );
else if ( direction == Direction.Up )
_moveDir = new Vector2( _moveDir.x, -Math.Abs( _moveDir.y ) );
}
public override void OnStun()
{
base.OnStun();
PlayFlinchAnim();
SetState( SnakerState.Default );
}
[Rpc.Broadcast]
public void EnterDefaultStateRpc()
{
CanAnimate = true;
PlayWalkAnim();
}
}