things/enemies/MinibossSlammer.cs

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.

Networking
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 );
	}
}