Enemy subclass for the Miniboss Slammer. Defines stats, movement and a slam attack state machine, handles spawning, timers, animations and RPCs to broadcast prepare and slam effects to clients.
using System;
using Sandbox;
public class MinibossSlammer : Enemy
{
public override EnemyType EnemyType => EnemyType.MinibossSlammer;
public override string GibFolder => "miniboss_slammer";
public override float OverrideGibChance => 1f;
public override int ExtraDeathBloodSprayAmount => 25;
protected override float MinibossHealthScale => 1.25f;
public override float GetMaxHealth() => MinibossBaseHealth * MinibossHealthScale;
public override Vector3 SpawnScale => new Vector3( 1.7f );
public override bool ShowHealthbar => true;
public override float HealthbarOffset => 100f;
public override float HealthbarOpacity => Utils.EasePercent( SpawnProgress, EasingType.QuadIn );
public override float HealthbarArmorOpacity => Utils.EasePercent( SpawnProgress, EasingType.QuadIn );
public override bool IsBoss => true;
public override bool IsMiniboss => true;
// todo: should targer player when boss has spawned?
public override bool CanHaveTarget => false;
public override bool CanTurn => base.CanTurn && !IsDying && State == MinibossSlammerState.Default;
public override bool CanMove => base.CanMove && State == MinibossSlammerState.Default;
//public override bool CanAttack => base.CanAttack && State == MinibossSlammerState.Default;
public override bool CanAttack => false;
//public override bool CanDamageByTouch => !IsDying && !IsStunned && !IsInTheAir && State == MinibossSlammerState.Default;
public override bool CanDamageByTouch => false;
public bool IsReadyForAction => State == MinibossSlammerState.Default && !IsInTheAir && !IsSpawning && !IsStunned;
private Vector2 _moveDir;
private float _slamDelayTimer;
private const float SLAM_DELAY_MIN = 5f;
private const float SLAM_DELAY_MAX = 11f;
private const float ACCELERATION_MIN = 240f;
private const float ACCELERATION_MAX = 320f;
protected enum MinibossSlammerState
{
Default,
SlamPrepare,
Slam,
}
[Sync] protected MinibossSlammerState State { get; private set; } = MinibossSlammerState.Default;
protected override void OnStart()
{
base.OnStart();
CoinValueMin = 10;
CoinValueMax = 19;
CoinChance = 1f;
PushStrength = 9000f;
Weight = 1.5f;
_personalSpeedScale = 1f;
_personalSpeedFreq = 12f;
if ( IsProxy )
return;
AggroRange = 50f;
DetectTargetRange = 850f;
LoseTargetRange = 1300f;
LoseTargetTime = 5f;
MeleeDamage = Utils.Select( Manager.Instance.Difficulty, 12f, 15f, 16f );
DamageTargetDelay = 0.8f;
Acceleration = ACCELERATION_MIN * Utils.Select( Manager.Instance.Difficulty, 0.9f, 1f, 1.05f );
AccelerationAttacking = ACCELERATION_MAX * Utils.Select( Manager.Instance.Difficulty, 0.9f, 1f, 1.05f );
Deceleration = 1.75f;
DecelerationAttacking = 1.6f;
_personalTurnSpeed = 4.5f;
_slamDelayTimer = Game.Random.Float( SLAM_DELAY_MIN, SLAM_DELAY_MAX );
_moveDir = Utils.GetRandomVector();
}
protected override void OnUpdate()
{
base.OnUpdate();
//Gizmo.Draw.Color = Color.White;
//Gizmo.Draw.Text( $"{State}", new global::Transform( WorldPosition ) );
if ( Manager.Instance.IsGameOver )
return;
_personalSpeedScale = Utils.Map( Health, MaxHealth, 0f, 1f, 1.5f, EasingType.Linear );
if ( IsProxy )
return;
TargetPos = Position2D + _moveDir * 100f;
Acceleration = AccelerationAttacking = Utils.Map( Health, MaxHealth, 0f, ACCELERATION_MIN, ACCELERATION_MAX, EasingType.Linear ) * Utils.Select( Manager.Instance.Difficulty, 0.9f, 1f, 1.05f );
if ( !IsStunned && !IsDying )
HandleState();
// todo: if walking too long without touching fence, probably got stuck on trees or something, so walk toward the nearest player
}
protected void HandleState()
{
switch ( State )
{
case MinibossSlammerState.Default:
if( IsReadyForAction )
{
_slamDelayTimer -= Time.Delta;
if ( _slamDelayTimer < 0f )
SetState( MinibossSlammerState.SlamPrepare );
}
break;
case MinibossSlammerState.SlamPrepare:
Velocity *= (1f - Time.Delta * 6f * Manager.Instance.GlobalFrictionModifier);
if ( _timeSinceChangeState > 0.85f )
SetState( MinibossSlammerState.Slam );
break;
case MinibossSlammerState.Slam:
Velocity *= (1f - Time.Delta * 6f * Manager.Instance.GlobalFrictionModifier);
if ( _timeSinceChangeState > 0.6f )
SetState( MinibossSlammerState.Default );
break;
}
}
protected void SetState( MinibossSlammerState state )
{
State = state;
_timeSinceChangeState = 0f;
switch ( state )
{
case MinibossSlammerState.Default:
EnterDefaultStateRpc();
break;
case MinibossSlammerState.SlamPrepare:
StartSlammingRpc();
Velocity *= 0.5f;
_slamDelayTimer = Game.Random.Float( SLAM_DELAY_MIN, SLAM_DELAY_MAX ) * Utils.Map( HpPercent, 1f, 0f, 1f, 0.7f ) * Utils.Select( Manager.Instance.Difficulty, 1.35f, 1f, 0.95f );
//SetPlaybackRate( Utils.MapReturn( _prepareSlamTime, 0f, 1f, 0.8f, 0f, EasingType.QuadIn ) );
break;
case MinibossSlammerState.Slam:
SlamRpc();
break;
}
}
protected override float GetMoveSpeedFactor()
{
var progress = ModelRenderer.SceneModel.CurrentSequence.TimeNormalized;
return Exploder.GetWalkAnimSpeed( progress, IsAttacking, EasingType.Linear );
}
[Rpc.Broadcast]
void StartSlammingRpc()
{
CanAnimate = false;
SetAnim( "Attack" );
SetPlaybackRate( 0.85f );
//SS2Game.PlaySfx( "spitter.prepare", Position, pitch: Game.Random.Float( 1f, 1.1f ), volume: 0.6f );
}
[Rpc.Broadcast(NetFlags.Reliable)]
void SlamRpc()
{
Manager.Instance.ShakeCamsNearby( Position2D, radius: 320f, maxStrength: 5.5f, time: 0.35f );
Manager.Instance.PlaySfxNearby( "slam", Position2D, pitch: Game.Random.Float( 0.85f, 0.95f ), volume: 1f, maxDist: 500f );
float radius = 750f * Utils.Select(Manager.Instance.Difficulty, 0.9f, 1.25f, 1.4f);
float lifetime = Utils.Map( Health, MaxHealth, 0f, 2.5f, Utils.Select( Manager.Instance.Difficulty, 2.5f, 1.85f, 1.6f ), EasingType.SineIn );
float force = Utils.Map( Health, MaxHealth, 0f, 400f, 600f, EasingType.SineIn );
var shockwaveGradient = new Gradient();
shockwaveGradient.AddColor( 0.0f, new Color( 1f, 0.65f, 0f ) );
shockwaveGradient.AddColor( 0.4f, new Color( 0.7f, 0.3f, 0f ).WithAlpha( 0.5f ) );
shockwaveGradient.AddColor( 0.5f, new Color( 1f, 1f, 0.3f ) );
shockwaveGradient.AddColor( 0.6f, new Color( 0.7f, 0.3f, 0f ).WithAlpha( 0.5f ) );
shockwaveGradient.AddColor( 1.0f, new Color( 1f, 0.65f, 0f ) );
Manager.Instance.SpawnShockwave( Position2D, damage: Utils.Select( Manager.Instance.Difficulty, 9f, 13f, 15f ), radius, lifetime, force, gradient: shockwaveGradient, enemySource: this, enemyType: this.EnemyType );
}
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 ) );
// todo: chance to head toward a player (chance increases with time)
}
public override void OnStun()
{
base.OnStun();
PlayFlinchAnim();
SetState( MinibossSlammerState.Default );
}
[Rpc.Broadcast]
public void EnterDefaultStateRpc()
{
CanAnimate = true;
PlayWalkAnim();
}
protected override void Jump( Vector2 targetPos, float height, float lifetime )
{
SetState( MinibossSlammerState.Default );
base.Jump( targetPos, height, lifetime );
}
}