Enemy AI component for a Charger enemy. Controls spawning stats, charge attack state machine, random walking, movement/rotation while charging, animations and networked RPCs for charge events.
using System;
using Sandbox;
public class Charger : Enemy
{
public override EnemyType EnemyType => EnemyType.Charger;
public override float MeleeForce => 40f;
public override float MeleeRagdollForce => 3f;
public override float MeleeUpwardForceAmount => State == ChargerState.Charge
? Game.Random.Float( 0.5f, 2f )
: Game.Random.Float( 0f, 1f );
public override float GetMaxHealth()
{
switch ( Manager.Instance.Difficulty )
{
case 0: default: return 82f;
case 1: return 87f;
case 2: return 90f;
}
}
public override Vector3 SpawnScale => new Vector3( 1.25f );
public override int ExtraDeathBloodSprayAmount => 15;
protected float _chargeDelayTimer;
protected float _chargeDelayMin;
protected float _chargeDelayMax;
protected float _prepareTimer;
protected float _chargeTimer;
protected float _chargeTime;
protected float _chargeTimeMin;
protected float _chargeTimeMax;
protected Vector2 _chargeDir;
protected Vector2 _chargeVel;
protected TimeSince _timeSinceChargeCloud;
protected float _personalChargeRange;
protected float _chargeSpeed;
public bool IsFinishingCharging { get; protected set; }
protected TimeSince _timeSinceCharging;
//protected bool _playingChargeEndAnim;
protected float _chargeRotateSpeed;
protected float _baseWeight;
protected float _chargeWeight;
protected float _chargeVelMax;
protected virtual bool UseRandomWalk => true;
// Random walk configuration
protected float _randomWalkIntervalMin = 7f;
protected float _randomWalkIntervalMax = 30f;
protected float _randomWalkDurationMin = 2f;
protected float _randomWalkDurationMax = 9f;
// Random walk state
protected float _randomWalkTimer;
protected float _randomWalkDuration;
protected bool _isRandomWalking;
protected Vector2 _randomWalkDirection;
public override bool CanTurn => base.CanTurn && !IsDying && !(State == ChargerState.ChargePrepare || State == ChargerState.ChargeFinish);
public override bool CanMove => base.CanMove && State == ChargerState.Default;// !(State == ChargerState.ChargePrepare || State == ChargerState.ChargeFinish);
public override bool CanAttack => base.CanAttack && !(State == ChargerState.ChargePrepare || State == ChargerState.Charge);
public override bool CanDamageByTouch => !IsDying && !IsStunned && !IsInTheAir && ( IsAttacking || State == ChargerState.Charge ) && State != ChargerState.ChargeFinish;
public override float ParticleYPosOverride => 0.75f;
public override float StunParticleYPosOverride => 1.05f;
protected enum ChargerState
{
Default,
ChargePrepare,
Charge,
ChargeFinish,
}
[Sync] protected ChargerState State { get; private set; } = ChargerState.Default;
protected override void OnStart()
{
base.OnStart();
CoinValueMin = 4;
CoinValueMax = 6;
CoinChance = 1f;
PushStrength = 12000f;
Weight = _baseWeight = 2.2f;
_chargeWeight = 3f;
_personalSpeedScale = Game.Random.Float( 0.9f, 1.1f );
_personalSpeedFreq = Game.Random.Float( 9f, 11f );
if ( IsProxy )
return;
AggroRange = 90f;
DetectTargetRange = 500f;
LoseTargetRange = 900f;
LoseTargetTime = 7f;
MeleeDamage = Utils.Select( Manager.Instance.Difficulty, 6f, 9f, 12f );
DamageTargetDelay = 1f;
Acceleration = Utils.Select( Manager.Instance.Difficulty, 200f, 220f, 225f );
AccelerationAttacking = Utils.Select( Manager.Instance.Difficulty, 230f, 270f, 275f );
Deceleration = 2.5f;
DecelerationAttacking = 2.3f;
_personalTurnSpeed = Game.Random.Float( 4f, 7f );
_personalChargeRange = Game.Random.Float( 280f, 420f );
_chargeTimeMin = 1.8f;
_chargeTimeMax = 2.5f;
_chargeDelayMin = 2f;
_chargeDelayMax = 6f;
_chargeDelayTimer = Game.Random.Float( _chargeDelayMin, _chargeDelayMax );
_chargeRotateSpeed = 7f;
_chargeSpeed = Utils.Select( Manager.Instance.Difficulty, 205f, 225f, 230f );
_chargeVelMax = Utils.Select( Manager.Instance.Difficulty, 300f, 400f, 400f );
_randomWalkTimer = Game.Random.Float( _randomWalkIntervalMin, _randomWalkIntervalMax );
}
protected override void OnUpdate()
{
base.OnUpdate();
if ( Manager.Instance.IsGameOver )
return;
if ( IsDying )
return;
//Gizmo.Draw.Color = Color.White;
//Gizmo.Draw.Text( $"_isRandomWalking: {_isRandomWalking}\n_randomWalkTimer: {_randomWalkTimer}\n_randomWalkDuration: {_randomWalkDuration}", new global::Transform( WorldPosition ) );
//Gizmo.Draw.Text( $"State: {State}\nCurrentSequence: {ModelRenderer.SceneModel.CurrentSequence.Name}\nprogress: {ModelRenderer.SceneModel.CurrentSequence.TimeNormalized.ToString( "N2" )}\nspeed: {GetMoveSpeedFactor().ToString( "N2" )}", new global::Transform( WorldPosition ) );
if ( State == ChargerState.Charge )
{
//if ( _timeSinceChargeCloud > 0.1f * (1f / TimeScale) )
if ( _timeSinceChargeCloud > 0.1f )
{
var pos = WorldPosition.WithZ( 10f );
GameObject.Clone( "prefabs/effects/cloud.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( pos ) } );
_timeSinceChargeCloud = 0f;
}
}
if ( IsProxy )
return;
if ( !IsStunned )
HandleState();
}
protected void HandleState()
{
switch ( State )
{
case ChargerState.Default:
if ( TargetUnit.IsValid() && !IsInTheAir && _timeSinceChangeState > 0.5f && !IsStunned )
{
var targetDistSqr = (TargetUnit.Position2D - Position2D).LengthSquared;
if (targetDistSqr < MathF.Pow( _personalChargeRange, 2f ) )
{
_chargeDelayTimer -= Time.Delta * TimeScale;
if ( _chargeDelayTimer < 0f )
SetState( ChargerState.ChargePrepare );
}
}
// Handle random walk behavior
if ( UseRandomWalk )
{
if ( _isRandomWalking )
{
_randomWalkDuration -= Time.Delta;
if ( _randomWalkDuration <= 0f )
{
_isRandomWalking = false;
_randomWalkTimer = Game.Random.Float( _randomWalkIntervalMin, _randomWalkIntervalMax );
}
}
else
{
_randomWalkTimer -= Time.Delta;
if ( _randomWalkTimer <= 0f )
StartRandomWalk();
}
}
break;
case ChargerState.ChargePrepare:
if ( _timeSinceChangeState > 0.75f )
SetState( ChargerState.Charge );
break;
case ChargerState.Charge:
// todo: charger gets too much speed now?
_chargeVel += _chargeDir * _chargeSpeed * Utils.MapReturn( _timeSinceChangeState, 0f, _chargeTime, 0.5f, 1f, EasingType.QuadOut ) * Time.Delta * Manager.Instance.GlobalMovespeedModifier;
if ( _chargeVel.LengthSquared > _chargeVelMax * _chargeVelMax )
_chargeVel = _chargeVel.Normal * _chargeVelMax;
WorldPosition += (Vector3)(_chargeVel + Velocity) * Time.Delta;
Velocity *= Math.Max( 1f - Time.Delta * 2.5f * Manager.Instance.GlobalFrictionModifier, 0f );
if ( Manager.Instance.IsWindActive )
Velocity += Manager.Instance.GlobalWindForce * Time.Delta;
//SetPlaybackRate( Utils.MapReturn( _chargeTimer, _chargeTime, 0f, 1f, 3f, EasingType.QuadOut ) );
if ( _timeSinceChangeState > _chargeTime )
SetState( ChargerState.ChargeFinish );
break;
case ChargerState.ChargeFinish:
//if ( _timeSinceChangeState > 0.5f )
SetState( ChargerState.Default );
break;
}
}
protected void StartRandomWalk()
{
_isRandomWalking = true;
_randomWalkDuration = Game.Random.Float( _randomWalkDurationMin, _randomWalkDurationMax );
_randomWalkDirection = Utils.GetRandomVector();
}
protected void SetState( ChargerState state )
{
State = state;
_timeSinceChangeState = 0f;
switch ( state )
{
case ChargerState.Default:
EnterDefaultStateRpc();
break;
case ChargerState.ChargePrepare:
_isRandomWalking = false; // Cancel random walk when preparing to charge
_chargeDelayTimer = Game.Random.Float( _chargeDelayMin, _chargeDelayMax );
PrepareToChargeRpc();
Velocity *= 0.7f;
break;
case ChargerState.Charge:
ChargeRpc();
break;
case ChargerState.ChargeFinish:
StopChargingRpc();
Velocity = _chargeVel;
break;
}
}
protected override void HandleRotation()
{
if ( State == ChargerState.Charge )
{
if( !HitstopActive )
WorldRotation = Rotation.Lerp( WorldRotation, Rotation.From( new Angles( 0f, -Utils.GetAngleDegreesFromVector( _chargeDir ), 0f ) ), _chargeRotateSpeed * Time.Delta * TimeScale );
}
else if ( State == ChargerState.Default && _isRandomWalking && !IsAttacking && !ShouldRetreatFromTarget )
{
var targetFacingDir = ((Vector3)_randomWalkDirection).WithZ( 0f );
WorldRotation = Rotation.Lerp( WorldRotation, Rotation.LookAt( targetFacingDir ), _personalTurnSpeed * Time.Delta * TimeScale );
}
else
{
base.HandleRotation();
}
}
//protected override void HandleMovement()
//{
// if ( State != ChargerState.Default )
// return;
// base.HandleMovement();
//}
protected override float GetMoveSpeedFactor()
{
var progress = ModelRenderer.SceneModel.CurrentSequence.TimeNormalized;
return Charger.GetChargerMoveSpeedFactor( progress, IsAttacking );
}
public static float GetChargerMoveSpeedFactor( float animProgress, bool isAttacking )
{
var leftFootStart = 0.70f;
var leftFootEnd = 0.2f;
var rightFootStart = isAttacking ? 0.3f : 0.22f;
var rightFootEnd = isAttacking ? 0.7f : 0.64f;
if ( animProgress > leftFootStart || animProgress < leftFootEnd )
{
var totalProgress = 1f + leftFootEnd;
var offsetProgress = animProgress < leftFootEnd
? 1f + animProgress
: animProgress;
return Utils.Map( offsetProgress, leftFootStart, totalProgress, 0f, 1f, EasingType.QuadOut );
}
else if ( animProgress > rightFootStart && animProgress < rightFootEnd )
{
return Utils.Map( animProgress, rightFootStart, rightFootEnd, 0f, 1f, EasingType.QuadOut );
}
return 0f;
}
[Rpc.Broadcast( NetFlags.Reliable )]
public void PrepareToChargeRpc()
{
Manager.Instance.PlaySfxNearby( "enemy.roar.prepare", Position2D, pitch: Game.Random.Float( 0.95f, 1.05f ), volume: 0.95f, maxDist: 550f );
SetAnim( "ChargePrepare" );
SetPlaybackRate( 1f );
CanAnimate = false;
}
[Rpc.Broadcast( NetFlags.Reliable )]
public void ChargeRpc()
{
Charge();
}
protected virtual void Charge()
{
Manager.Instance.PlaySfxNearby( "enemy.roar", Position2D, pitch: Game.Random.Float( 0.925f, 1.075f ), volume: 0.85f, maxDist: 550f );
SetAnim( "Charge" );
SetPlaybackRate( 1f );
_timeSinceChargeCloud = 0f;
if ( IsProxy )
return;
Player closestPlayer = Manager.Instance.GetClosestPlayer( Position2D );
if ( !closestPlayer.IsValid() )
return;
var targetPos = closestPlayer.Position2D + closestPlayer.Velocity * Game.Random.Float( 0.2f, 1f ) + new Vector2( Game.Random.Float( -1f, 1f ), Game.Random.Float( -1f, 1f ) ) * 100f;
Vector2 targetDir = (targetPos - Position2D).LengthSquared > Manager.TOUCH_DIST_REQUIRED_SQR
? (targetPos - Position2D).Normal
: Utils.GetRandomVector();
_chargeDir = Utils.RotateVector( targetDir, Game.Random.Float( -10f, 10f ) );
_chargeTime = Game.Random.Float( _chargeTimeMin, _chargeTimeMax );
_chargeVel = Vector2.Zero;
//PlaybackRate = 3f;
Weight = _chargeWeight;
}
[Rpc.Broadcast( NetFlags.Reliable )]
public void StopChargingRpc()
{
//SetAnim( "ChargePrepare" );
//SetPlaybackRate( 1.6f );
IsFinishingCharging = true;
_timeSinceCharging = 0f;
//_playingChargeEndAnim = true;
}
protected override void OnOutOfBounds( Direction direction )
{
base.OnOutOfBounds( direction );
if ( State == ChargerState.Charge )
{
if ( direction == Direction.Left )
{
_chargeVel = new Vector2( Math.Abs( _chargeVel.x ), _chargeVel.y );
_chargeDir = new Vector2( Math.Abs( _chargeDir.x ), _chargeDir.y );
}
else if ( direction == Direction.Right )
{
_chargeVel = new Vector2( -Math.Abs( _chargeVel.x ), _chargeVel.y );
_chargeDir = new Vector2( -Math.Abs( _chargeDir.x ), _chargeDir.y );
}
else if ( direction == Direction.Down )
{
_chargeVel = new Vector2( _chargeVel.x, Math.Abs( _chargeVel.y ) );
_chargeDir = new Vector2( _chargeDir.x, Math.Abs( _chargeDir.y ) );
}
else if ( direction == Direction.Up )
{
_chargeVel = new Vector2( _chargeVel.x, -Math.Abs( _chargeVel.y ) );
_chargeDir = new Vector2( _chargeDir.x, -Math.Abs( _chargeDir.y ) );
}
_chargeVel *= Utils.Select( Manager.Instance.Difficulty, 0.33f, 0.5f, 0.9f );
// todo: sfx
}
else if ( State == ChargerState.Default && _isRandomWalking )
{
if ( direction == Direction.Left )
_randomWalkDirection = new Vector2( Math.Abs( _randomWalkDirection.x ), _randomWalkDirection.y );
else if ( direction == Direction.Right )
_randomWalkDirection = new Vector2( -Math.Abs( _randomWalkDirection.x ), _randomWalkDirection.y );
else if ( direction == Direction.Down )
_randomWalkDirection = new Vector2( _randomWalkDirection.x, Math.Abs( _randomWalkDirection.y ) );
else if ( direction == Direction.Up )
_randomWalkDirection = new Vector2( _randomWalkDirection.x, -Math.Abs( _randomWalkDirection.y ) );
}
}
public override void OnStun()
{
base.OnStun();
PlayFlinchAnim();
SetState( ChargerState.Default );
}
protected override void Jump( Vector2 targetPos, float height, float lifetime )
{
SetState( ChargerState.Default );
base.Jump( targetPos, height, lifetime );
// todo: if jump while charging, weird anim speed
// todo: also can be charging way too slow, maybe related
}
public override void JumpFinish()
{
base.JumpFinish();
SetState( ChargerState.Default );
}
[Rpc.Broadcast( NetFlags.Reliable )]
public void EnterDefaultStateRpc()
{
CanAnimate = true;
PlayWalkAnim();
if ( IsProxy )
return;
Weight = _baseWeight;
}
public override void Celebrate( bool victory )
{
SetState( ChargerState.Default );
base.Celebrate( victory );
}
protected override void PlayFlinchAnim()
{
}
}