Enemy subclass ChargerElite that implements a charging elite enemy behavior. It controls movement, charge preparation and execution, collision damage to players and enemies, sprite animation changes, sound effects, and AI timing (delay, redirect, cloud spawn).
using Sandbox;
public class ChargerElite : Enemy
{
private TimeSince _damageTime;
private const float DAMAGE_TIME = 1f;
private float _chargeDelayTimer;
private const float CHARGE_DELAY_MIN = 2f;
private const float CHARGE_DELAY_MAX = 6f;
public bool IsPreparingToCharge { get; private set; }
public bool IsCharging { get; private set; }
private float _prepareTimer;
private const float PREPARE_TIME = 1f;
private float _chargeTimer;
private const float CHARGE_TIME_MIN = 2.5f;
private const float CHARGE_TIME_MAX = 3.5f;
private float _chargeTime;
private float _nextRedirectTime;
private TimeSince _timeSinceRedirect;
private Vector2 _chargeDir;
private Vector2 _chargeVel;
private TimeSince _chargeCloudTimer;
public override float HeightVariance => 0.03f;
public override float WidthVariance => 0.015f;
protected override void OnAwake()
{
//OffsetY = -0.66f;
ShadowScale = 1.45f;
ShadowFullOpacity = 0.8f;
ShadowOpacity = 0f;
Scale = 1.45f;
base.OnAwake();
//Sprite.Texture = Texture.Load("textures/sprites/charger_elite.vtex");
//Sprite.Size = new Vector2( 1f, 1f ) * Scale;
PushStrength = 30f;
Radius = 0.39f;
Health = 335f;
if ( Manager.Instance.Difficulty < 0 )
Health = 280f;
MaxHealth = Health;
DamageToPlayer = 14f;
CoinValueMin = 3;
CoinValueMax = 6;
CoinChance = 0.8f;
Sprite.PlayAnimation( AnimSpawnPath );
if ( IsProxy )
return;
CollideWith.Add( typeof( Enemy ) );
CollideWith.Add( typeof( Player ) );
_damageTime = DAMAGE_TIME;
_chargeDelayTimer = Game.Random.Float( CHARGE_DELAY_MIN, CHARGE_DELAY_MAX );
}
protected override void UpdatePosition( float dt )
{
base.UpdatePosition( dt );
//Gizmo.Draw.Color = Color.White.WithAlpha( 0.5f );
//Gizmo.Draw.Text( $"{(closestPlayer.Position2D - Position2D).Length}", new global::Transform( (Vector3)Position2D + new Vector3( 0f, -0.7f, 0f ) ) );
var targetPos = Target.IsValid() ? Target.Position2D : (IsCharmed ? Manager.Instance.Player.Position2D : Position2D);
if ( IsPreparingToCharge )
{
_prepareTimer -= dt;
if ( _prepareTimer < 0f )
{
Charge();
return;
}
}
else if ( IsCharging )
{
_chargeTimer -= dt;
if ( _chargeTimer < 0f )
{
IsCharging = false;
Sprite.PlayAnimation( AnimIdlePath );
CanTurn = true;
DontChangeAnimSpeed = false;
}
else
{
float chargeSpeed = Manager.Instance.Difficulty >= 1 ? 20f : 4f;
_chargeVel += _chargeDir * chargeSpeed * Utils.MapReturn( _chargeTimer, _chargeTime, 0f, 0f, 1f, EasingType.Linear ) * dt;
TempWeight += Utils.MapReturn( _chargeTimer, _chargeTime, 0f, 1f, 6f, EasingType.Linear ) * dt;
float BUFFER = 0.01f;
var x_min = Manager.Instance.BOUNDS_MIN.x + Radius + BUFFER;
var x_max = Manager.Instance.BOUNDS_MAX.x - Radius - BUFFER;
var y_min = Manager.Instance.BOUNDS_MIN.y + BUFFER;
var y_max = Manager.Instance.BOUNDS_MAX.y - Radius - BUFFER;
if ( Position2D.x < x_min && _chargeDir.x < 0f )
{
_chargeDir = _chargeDir.WithX( Math.Abs( _chargeDir.x ) );
_chargeVel = _chargeVel.WithX( Math.Abs(_chargeVel.x) * 0.25f );
FlipX = true;
Sprite.SpriteFlags = SpriteFlags.HorizontalFlip;
}
else if ( Position2D.x > x_max && _chargeDir.x > 0f )
{
_chargeDir = _chargeDir.WithX( -Math.Abs( _chargeDir.x ) );
_chargeVel = _chargeVel.WithX( -Math.Abs( _chargeVel.x ) * 0.25f );
FlipX = false;
Sprite.SpriteFlags = SpriteFlags.None;
}
if ( Position2D.y < y_min && _chargeDir.y < 0f )
{
_chargeDir = _chargeDir.WithY( Math.Abs( _chargeDir.y ) );
_chargeVel = _chargeDir.WithY( Math.Abs( _chargeVel.y ) * 0.25f );
}
else if ( Position2D.y > y_max && _chargeDir.y > 0f )
{
_chargeDir = _chargeDir.WithY( -Math.Abs( _chargeDir.y ) );
_chargeVel = _chargeDir.WithY( -Math.Abs( _chargeVel.y ) * 0.25f );
}
}
WorldPosition += (Vector3)(_chargeVel + Velocity) * dt;
if ( Manager.Instance.Difficulty >= 1 && _timeSinceRedirect > _nextRedirectTime )
{
_chargeDir = (targetPos - Position2D).Normal;
if ( Math.Abs( targetPos.x - Position2D.x ) > 0.15f )
FlipX = targetPos.x > Position2D.x;
_nextRedirectTime = Game.Random.Float( 0.6f, 3f );
_timeSinceRedirect = 0f;
}
if ( _chargeCloudTimer > 0.25f )
{
SpawnCloudClient( Position2D + new Vector2( 0f, 0.25f ), -_chargeDir * Game.Random.Float( 0.2f, 0.8f ) );
_chargeCloudTimer = Game.Random.Float( 0f, 0.075f );
}
}
else
{
Velocity += (targetPos - Position2D).Normal * dt * (IsFeared ? -1f : 1f);
float speed = 0.75f * (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;
var newPos = Position2D + Velocity * dt * speed;
if ( float.IsNaN( newPos.x ) || float.IsNaN( newPos.y ) )
{
StartDying( null );
Flash( 0.05f );
return;
}
WorldPosition += (Vector3)Velocity * speed * dt;
}
var player_dist_sqr = (targetPos - Position2D).LengthSquared;
if ( !IsPreparingToCharge && !IsCharging && !IsAttacking && player_dist_sqr < 8.5f * 8.5f && Target.IsValid() )
{
_chargeDelayTimer -= dt;
if ( _chargeDelayTimer < 0f )
{
PrepareToCharge();
}
}
}
protected override void HandleDeceleration( float dt )
{
if ( IsCharging )
{
Velocity *= (1f - dt * 1.75f);
float decel = Manager.Instance.Difficulty >= 1 ? 3.5f : 0.5f;
_chargeVel *= (1f - dt * decel);
}
else
{
base.HandleDeceleration( dt );
}
}
protected override void UpdateSprite( Thing target )
{
if ( !IsCharging )
base.UpdateSprite( target );
}
protected override void HandleAttacking( Thing target, float dt )
{
if ( !IsPreparingToCharge && !IsCharging )
base.HandleAttacking( target, dt );
}
public void PrepareToCharge()
{
_prepareTimer = PREPARE_TIME;
IsPreparingToCharge = true;
Manager.Instance.PlaySfxNearby( "enemy.roar.prepare", Position2D, pitch: Game.Random.Float( 0.95f, 1.05f ), volume: 1f, maxDist: 5f );
Sprite.PlayAnimation( "charge_start" );
CanTurn = false;
CanAttack = false;
CanAttackAnim = false;
DontChangeAnimSpeed = true;
if ( Manager.Instance.Difficulty >= 1 )
{
_nextRedirectTime = Game.Random.Float( 0.6f, 2.7f );
_timeSinceRedirect = 0f;
}
}
public void Charge()
{
var target_pos = Target.IsValid() && !IsFeared
? Target.Position2D + Target.Velocity * Game.Random.Float( 0.5f, 2.45f )
: Position2D + Utils.GetRandomVector() * 0.5f;
_chargeDir = Utils.RotateVector( (target_pos - Position2D).Normal, Game.Random.Float( -10f, 10f ) );
IsPreparingToCharge = false;
IsCharging = true;
_chargeTime = Game.Random.Float( CHARGE_TIME_MIN, CHARGE_TIME_MAX ) * (Manager.Instance.Difficulty >= 1 ? Game.Random.Float( 1.25f, Utils.Map( Manager.Instance.Difficulty, 1, 10, 1.75f, 3f, EasingType.SineIn ) ) : 1f);
_chargeTimer = _chargeTime;
CanAttack = true;
CanAttackAnim = true;
_chargeDelayTimer = Game.Random.Float( CHARGE_DELAY_MIN, CHARGE_DELAY_MAX ) * (Manager.Instance.Difficulty < 0 ? 1.5f : 1f);
_chargeVel = Vector2.Zero;
Sprite.PlayAnimation( "charge_loop" );
AnimSpeed = 3f;
FlipX = _chargeDir.x > 0f;
Sprite.SpriteFlags = FlipX ? SpriteFlags.HorizontalFlip : SpriteFlags.None;
//Sprite.SpriteFlags = target_pos.x > Position2D.x ? SpriteFlags.HorizontalFlip : SpriteFlags.None;
Manager.Instance.PlaySfxNearby( "enemy.roar", Position2D, pitch: Game.Random.Float( 0.825f, 0.875f ), volume: 1f, maxDist: 8f );
}
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 || IsCharging) && 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 );
}
}
// todo: move collision check to player instead to prevent laggy hits?
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 || IsCharging) && _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, 2.5f );
Sprite.PlayAnimation( "cheer" );
}
}