things/enemies/Boss.cs
using Sandbox;
using SpriteTools;
using static Sandbox.VertexLayout;

public class Boss : Enemy
{
	private TimeSince _damageTime;
	private const float DAMAGE_TIME = 0.5f;

	private float _shootDelayTimer;
	private const float SHOOT_DELAY_MIN = 1.25f;
	private const float SHOOT_DELAY_MAX = 3f;

	public bool IsShooting { get; private set; }
	private bool _hasShot;

	private TimeSince _prepareShootTime;

	public bool IsCharging { get; private set; }
	private float _chargeTimer;
	private const float CHARGE_TIME_MIN = 1f;
	private const float CHARGE_TIME_MAX = 2.4f;
	private float _chargeTime;
	private Vector2 _chargeDir;
	private Vector2 _chargeVel;
	private TimeSince _chargeCloudTimer;
	private float _nextRedirectTime;
	private TimeSince _timeSinceRedirect;

	public bool IsPreparingToCharge { get; private set; }
	private float _prepareTimer;
	private const float PREPARE_TIME = 0.8f;
	private bool _hasLandedCharge;

	private float _chargeDelayTimer;
	private const float CHARGE_DELAY_MIN = 3f;
	private const float CHARGE_DELAY_MAX = 6f;

	// todo: charge reflects off arena bounds

	private bool _isJumping;
	private Vector2 _jumpStartPos;
	private Vector2 _jumpTargetPos;
	private TimeSince _timeSinceBeginJump;
	private float _jumpTime;
	private float _jumpHeight;

	private bool _isPreparingJump;
	private float _prepareJumpTime;
	private TimeSince _timeSincePrepareJump;
	private TimeSince _timeSinceJumping;
	private float _delayUntilNextJump;

	private const float SHOCKWAVE_TOTAL_TIME = 4f;

	public int BossNum { get; set; }

	[Property] public Sprite OtherBossSprite { get; set; }

	protected override void OnAwake()
	{
		//OffsetY = -0.95f;
		ShadowScale = 2.15f;
		ShadowFullOpacity = 0.8f;
		ShadowOpacity = 0f;

		Scale = 2.5f;

		base.OnAwake();

		//AnimSpeed = 3f;
		//BasePivotY = 0.05f;

		//Sprite.Texture = Texture.Load("textures/sprites/boss.vtex");

		//ScaleFactor = 0.85f;
		//Sprite.Size = new Vector2( 1f, 1f ) * Scale;

		PushStrength = 75f;
		DeathTime = 0.4f;

		Radius = 0.42f;

		Deceleration = 1.1f;
		DecelerationAttacking = Manager.Instance.Difficulty >= 1 ? 0.9f : 1.1f;

		if ( Manager.Instance.Difficulty < 0 )
			Health = 5000f;
		else if ( Manager.Instance.Difficulty == 0 )
			Health = 10000f;
		else if ( Manager.Instance.Difficulty == 1 )
			Health = 11000f;
		else if ( Manager.Instance.Difficulty == 2 )
			Health = 12000f;
		else if ( Manager.Instance.Difficulty == 3 )
			Health = 12500f;
		else if ( Manager.Instance.Difficulty == 4 )
			Health = 13000f;
		else if ( Manager.Instance.Difficulty >= 5 )
			Health = 7000f;

		MaxHealth = Health;
		DamageToPlayer = Manager.Instance.Difficulty < 0 ? 20f : 25f;

		Sprite.PlayAnimation( AnimSpawnPath );

		if ( IsProxy )
			return;

		CollideWith.Add( typeof( Enemy ) );
		CollideWith.Add( typeof( Player ) );

		_damageTime = DAMAGE_TIME;
		_shootDelayTimer = Game.Random.Float( SHOOT_DELAY_MIN, SHOOT_DELAY_MAX );
		_chargeDelayTimer = Game.Random.Float( CHARGE_DELAY_MIN, CHARGE_DELAY_MAX );
		_delayUntilNextJump = Game.Random.Float( 4f, 8f );
	}

	protected override void UpdatePosition( float dt )
	{
		//Gizmo.Draw.Color = Color.White.WithAlpha( 0.05f );
		//Gizmo.Draw.LineSphere( (Vector3)Position2D, Radius );

		//Gizmo.Draw.Color = Color.White;
		//Gizmo.Draw.Text( $"Sprite.PlaybackSpeed: {Sprite.PlaybackSpeed}", new global::Transform( (Vector3)Position2D + new Vector3( 0f, -0.7f, 0f ) ) );

		base.UpdatePosition( dt );

		if ( Manager.Instance.Difficulty >= 1 && !_isPreparingJump && !_isJumping && _timeSinceJumping > _delayUntilNextJump * (IsAttacking ? 2f : 1f) && !IsShooting && !IsCharging && !IsPreparingToCharge && !Manager.Instance.IsGameOver )
		{
			PrepareJump( Target.Position2D + Target.Velocity * Game.Random.Float( 0f, 4f ) + Utils.GetRandomVector() * Game.Random.Float( 2f, 4f ) );
		}

		if ( _isPreparingJump )
		{
			if ( Manager.Instance.IsGameOver )
				_isPreparingJump = false;

			if ( _timeSincePrepareJump > _prepareJumpTime )
			{
				Jump();
			}

			Velocity *= (1f - dt * 1.75f);
			WorldPosition += (Vector3)Velocity * dt;

			return;
		}

		if ( _isJumping )
		{
			if ( _timeSinceBeginJump > _jumpTime )
			{
				FinishJump();
			}
			else
			{
				float progress = Utils.Map( _timeSinceBeginJump, 0f, _jumpTime, 0f, 1f, EasingType.Linear );
				Position2D = Vector2.Lerp( _jumpStartPos, _jumpTargetPos, progress );
				Sprite.LocalPosition = new Vector3( 0f, Utils.MapReturn( progress, 0f, 1f, 0f, _jumpHeight, EasingType.SineOut ), 0f );

				float shadowScale = Utils.MapReturn( progress, 0f, 1f, ShadowScale, ShadowScale * 0.5f, EasingType.SineOut );
				ShadowSprite.LocalScale = new Vector3( shadowScale * Globals.SPRITE_SCALE, shadowScale * Globals.SPRITE_SCALE, 1f );

				float shadowOpacity = Utils.MapReturn( progress, 0f, 1f, ShadowOpacity, ShadowOpacity * 0.5f, EasingType.SineOut );
				ShadowSprite.Tint = Color.Black.WithAlpha( shadowOpacity );
			}

			return;
		}

		if ( IsPreparingToCharge )
		{
			_prepareTimer -= dt;
			if ( _prepareTimer < 0f )
			{
				Charge();
				return;
			}
		}
		else if ( IsCharging )
		{
			_chargeTimer -= dt;

			if ( !_hasLandedCharge && _chargeTimer < 0.5f )
			{
				Sprite.PlayAnimation( "charge_reverse" );
				_hasLandedCharge = true;
			}

			if ( _chargeTimer < 0f )
			{
				IsCharging = false;
				Sprite.PlayAnimation( AnimIdlePath );
				CanTurn = true;
				CanAttackAnim = true;
				DontChangeAnimSpeed = false;
			}
			else
			{
				float chargeSpeed = Manager.Instance.Difficulty >= 1 ? 12f : 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.1f );
					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.1f );
					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.1f );
				}
				else if ( Position2D.y > y_max && _chargeDir.y > 0f )
				{
					_chargeDir = _chargeDir.WithY( -Math.Abs( _chargeDir.y ) );
					_chargeVel = _chargeDir.WithY( -Math.Abs( _chargeVel.y ) * 0.1f );
				}
			}

			if ( _chargeTimer < 0.1f )
				_chargeVel *= 0f;

			WorldPosition += (Vector3)(_chargeVel + Velocity) * dt;

			if ( Manager.Instance.Difficulty >= 1 && _timeSinceRedirect > _nextRedirectTime )
			{
				_chargeDir = (Target.Position2D - Position2D).Normal;

				if ( Math.Abs( Target.Position2D.x - Position2D.x ) > 0.15f )
					FlipX = Target.Position2D.x > Position2D.x;

				_nextRedirectTime = Game.Random.Float( 0.5f, 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 );

				if ( Health < MaxHealth / 2f )
				{
					var dir = (new Vector2( Game.Random.Float( -1f, 1f ), Game.Random.Float( -1f, 1f ) )).Normal;
					var enemyBullet = Manager.Instance.SpawnEnemyBullet( Position2D + new Vector2( 0f, 0.55f ) + dir * 0.03f, dir, speed: 3f );
					enemyBullet.SetColor( new Color( 1f, 1f, 0f ) );
					//enemyBullet.BecomeHoming( dir * 3f, Target );
				}
			}
		}
		else
		{
			if ( IsShooting )
			{
				if ( !_hasShot && _prepareShootTime > 1.0f )
					Shoot();

				if ( _prepareShootTime > 1.6f )
					FinishShooting();

				return;
			}
			else
			{
				if ( !Target.Position2D.Equals( Position2D ) )
					Velocity += (Target.Position2D - Position2D).Normal * 1.0f * dt * (IsFeared ? -1f : 1f);
			}

			float speed = 1.66f * (IsAttacking ? 1.3f : 0.7f) + Utils.FastSin( MoveTimeOffset + Time.Now * (IsAttacking ? 15f : 7.5f) );

			if ( Manager.Instance.Difficulty < 0 )
				speed *= 0.85f;
			else if ( Manager.Instance.Difficulty >= 1 )
				speed *= 1.1f;

			WorldPosition += (Vector3)Velocity * speed * dt;

			var player_dist_sqr = (Target.Position2D - Position2D).LengthSquared;

			if ( !IsPreparingToCharge && !IsCharging && !IsShooting && !IsAttacking && !_isPreparingJump && !_isJumping && player_dist_sqr < MathF.Pow( 9f, 2f ) && !Manager.Instance.IsGameOver)
			{
				_shootDelayTimer -= dt;
				if ( _shootDelayTimer < 0f )
				{
					PrepareToShoot();
				}
			}

			if ( !IsPreparingToCharge && !IsCharging && !IsShooting && !IsAttacking && !_isPreparingJump && !_isJumping && player_dist_sqr < 15f * 15f && !Manager.Instance.IsGameOver )
			{
				_chargeDelayTimer -= dt;
				if ( _chargeDelayTimer < 0f )
				{
					PrepareToCharge();
				}
			}
		}
	}

	protected override void HandleDeceleration( float dt )
	{
		base.HandleDeceleration( dt );

		if ( IsCharging && Manager.Instance.Difficulty >= 1)
			_chargeVel *= (1f - dt * 3f);
	}

	protected override void UpdateSprite( Thing target )
	{
		if ( Sprite.CurrentAnimation.Name.Contains( "shoot" ) || Sprite.CurrentAnimation.Name.Contains( "charge" ) || _isPreparingJump || _isJumping ) 
			return;

		base.UpdateSprite( target );
	}

	public void PrepareToShoot()
	{
		_prepareShootTime = 0f;
		IsShooting = true;
		_hasShot = false;
		BroadcastShootAnim();
		Manager.Instance.PlaySfxNearby( "boss.prepare", Position2D, pitch: Game.Random.Float( 0.75f, 0.85f ), volume: 1.7f, maxDist: 16f );
		CanAttack = false;
		CanAttackAnim = false;
		AnimSpeed = 1f;
		DontChangeAnimSpeed = true;
	}

	void BroadcastShootAnim()
	{
		Sprite.PlayAnimation( "shoot" );
	}

	public void Shoot()
	{
		float homingBulletChance = 0f;

		//if(Manager.Instance.Difficulty > 0)
		//	homingBulletChance = 0.3f;

		bool shootHoming = Game.Random.Float( 0f, 1f ) < homingBulletChance;

		var numBullets = MathX.FloorToInt( Utils.Map( Health, MaxHealth, 0f, 3f, 8f, EasingType.SineIn ) ) + Game.Random.Int( 0, 1 );
		var spread = Game.Random.Float( 30f, 60f );

		float currAngleOffset = -spread * 0.5f;
		float increment = spread / (float)(numBullets - 1);

		var target_pos = Target.Position2D + Target.Velocity * Game.Random.Float( 0.25f, 1.5f );
		Vector2 aim_dir = Utils.RotateVector( (target_pos - Position2D).Normal, Game.Random.Float( -15f, 15f ) );

		if(shootHoming)
		{
			var dir = (Target.Position2D - Position2D).Normal;
			var enemyBullet = Manager.Instance.SpawnEnemyBullet( Position2D + dir * 0.03f, dir, speed: 3f );
			enemyBullet.SetColor( new Color( 1f, 1f, 0f ) );
			enemyBullet.BecomeHoming( dir * 3f, Target );
		}
		else
		{
			for ( int i = 0; i < numBullets; i++ )
			{
				var dir = Utils.RotateVector( aim_dir, currAngleOffset + increment * i );
				var enemyBullet = Manager.Instance.SpawnEnemyBullet( Position2D + dir * 0.03f, dir, speed: 3f );
				enemyBullet.SetColor( new Color( 1f, 1f, 0f ) );
			}
		}

		Velocity *= 0.25f;
		_hasShot = true;

		BroadcastReverseShootAnim();
		Manager.Instance.PlaySfxNearby( "boss.shoot", Position2D, pitch: Game.Random.Float( 0.65f, 0.75f ), volume: 0.4f, maxDist: 10f );
		Manager.Instance.PlaySfxNearby( "shoot_squelch", Position2D, pitch: Game.Random.Float( 0.85f, 1.15f ), volume: 1.3f, maxDist: 10f );

		// todo: different homing sfx
	}

	public void FinishShooting()
	{
		Sprite.PlayAnimation( AnimIdlePath );
		CanAttack = true;
		CanAttackAnim = true;
		DontChangeAnimSpeed = false;
		_shootDelayTimer = Game.Random.Float( SHOOT_DELAY_MIN, SHOOT_DELAY_MAX ) * Utils.Map( Health, MaxHealth, 0f, 1f, 0.5f, EasingType.QuadIn ) * (Manager.Instance.Difficulty < 0 ? 1.7f : 1f);
		IsShooting = false;
	}

	public void PrepareToCharge()
	{
		_prepareTimer = PREPARE_TIME;
		IsPreparingToCharge = true;
		Manager.Instance.PlaySfxNearby( "boss.prepare", Position2D, pitch: Game.Random.Float( 1.05f, 1.1f ), volume: 1.75f, maxDist: 10f );
		BroadcastChargeAnim();
		CanTurn = false;
		CanAttack = false;
		CanAttackAnim = false;
		AnimSpeed = 1f;
		DontChangeAnimSpeed = true;
	}

	void BroadcastChargeAnim()
	{
		Sprite.PlayAnimation( "charge" );
	}

	void BroadcastReverseShootAnim()
	{
		Sprite.PlayAnimation( "shoot_reverse" );
	}

	public void Charge()
	{
		var target_pos = Target.Position2D + Target.Velocity * Game.Random.Float( 0f, 1.66f );
		_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 );
		if ( Manager.Instance.Difficulty >= 1 )
			_chargeTime *= Game.Random.Float( 1f, 3f );

		_chargeTimer = _chargeTime;
		CanAttack = true;
		CanAttackAnim = true;
		_hasLandedCharge = false;

		_nextRedirectTime = Game.Random.Float( 0.5f, 3f );

		_chargeDelayTimer = Game.Random.Float( CHARGE_DELAY_MIN, CHARGE_DELAY_MAX ) * Utils.Map( Health, MaxHealth, 0f, 1f, 0.5f, EasingType.SineIn ) * (Manager.Instance.Difficulty < 0 ? 1.5f : 1f);
		_chargeVel = Vector2.Zero;

		FlipX = _chargeDir.x > 0f;
		Sprite.SpriteFlags = FlipX ? SpriteFlags.HorizontalFlip : SpriteFlags.None;
		//Sprite.FlipHorizontal = target_pos.x > Position2D.x;

		Manager.Instance.PlaySfxNearby( "boss.charge", Position2D, pitch: Game.Random.Float( 0.9f, 1.05f ), volume: 1.6f, maxDist: 9f );
	}

	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 * (IsCharging ? 0.25f : 1f) * dt;

			if ( IsAttacking && IsCharmed != enemy.IsCharmed && _damageTime > (DAMAGE_TIME / TimeScale) )
			{
				enemy.Damage( DamageToPlayer, 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 * 0.25f;

				if ( IsAttacking && _damageTime > (DAMAGE_TIME / TimeScale) )
				{
					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 StartDying( Player player )
	{
		base.StartDying( player );

		//ColorFill = new ColorHsv( 0f, 0f, 0f, 0f );

		Manager.Instance.PlaySfxNearby( "boss.die", Position2D, pitch: Game.Random.Float( 0.75f, 0.8f ), volume: 1.5f, maxDist: 15f );
		Manager.Instance.BossDied( BossNum );
	}

	public override void FinishDying()
	{
		//base.FinishDying();

		if(Manager.Instance.Difficulty >= 5)
		{
			if ( Manager.Instance.NumBossesKilled >= 2 )
				Manager.Instance.Victory();
			else
				Remove();
		}
		else
		{
			Manager.Instance.Victory();
		}

		//Manager.Instance.SpawnCrown( Position2D );
	}

	public void PrepareJump( Vector2 targetPos )
	{
		var BUFFER = 0.3f;
		targetPos = new Vector2( MathX.Clamp( targetPos.x, Manager.Instance.BOUNDS_MIN.x + BUFFER, Manager.Instance.BOUNDS_MAX.x - BUFFER ), MathX.Clamp( targetPos.y, Manager.Instance.BOUNDS_MIN.y + BUFFER, Manager.Instance.BOUNDS_MAX.y - BUFFER ) );

		float MAX_DIST = Game.Random.Float( 3.7f, 4.1f );

		if ( (targetPos - Position2D).Length > MAX_DIST )
			targetPos = Position2D + (targetPos - Position2D).Normal * MAX_DIST;

		_jumpTargetPos = targetPos;
		_jumpTime = Utils.Map( (targetPos - Position2D).Length, 1.5f, MAX_DIST, 1.4f, 1.7f, EasingType.SineIn ) * Game.Random.Float( 0.95f, 1.1f );
		_jumpHeight = Utils.Map( _jumpTime, 1f, 1.25f, 2.7f, 3.2f ) * Game.Random.Float( 0.95f, 1.1f );

		_isPreparingJump = true;
		_timeSincePrepareJump = 0f;
		_prepareJumpTime = Game.Random.Float( 0.8f, 1.2f );
		IsAttacking = false;
		CanTurn = false;
		CanAttack = false;
		CanAttackAnim = false;

		FlipX = targetPos.x > Position2D.x;

		Sprite.PlayAnimation( "jump_prepare" );

		// todo: sfx
	}

	public void Jump()
	{
		if ( Manager.Instance.IsGameOver )
			return;

		_isPreparingJump = false;
		_isJumping = true;
		_jumpStartPos = Position2D;
		_timeSinceBeginJump = 0f;
		IgnoreCollision = true;

		ShouldUpdateAfterGameOver = true;

		Sprite.PlayAnimation( "jump_up" );

		Manager.Instance.PlaySfxNearby( "jump_whoosh", Position2D, pitch: Game.Random.Float( 1.35f, 1.4f ), volume: 1f, maxDist: 12f );
	}

	void FinishJump()
	{
		_isJumping = false;
		Position2D = _jumpTargetPos;
		Sprite.LocalPosition = Vector3.Zero;
		IgnoreCollision = false;
		CanTurn = true;
		CanAttack = true;
		CanAttackAnim = true;

		_timeSinceJumping = 0f;
		_delayUntilNextJump = Game.Random.Float( 6.5f, 18.5f ) * Utils.Map(Health, MaxHealth, 0f, 1f, 0.65f, EasingType.QuadIn);

		ShadowSprite.LocalScale = new Vector3( ShadowScale * Globals.SPRITE_SCALE, ShadowScale * Globals.SPRITE_SCALE, 1f );
		ShadowSprite.Tint = Color.Black.WithAlpha( ShadowOpacity );

		for ( int i = 0; i < Game.Random.Int( 2, 4 ); i++ )
		{
			SpawnCloudClient( Position2D, Utils.GetRandomVector() * Game.Random.Float( 0.3f, 1.5f ) );
		}

		if ( Target.IsValid() )
			Velocity += (Target.Position2D - Position2D).Normal * Game.Random.Float( 0f, 0.5f );

		Slam();

		ShouldUpdateAfterGameOver = false;
		if ( Manager.Instance.IsGameOver && !Manager.Instance.ShouldUpdateThings )
			Celebrate();
	}

	void Slam()
	{
		SpawnShockwaveEffect( SHOCKWAVE_TOTAL_TIME, 100f, 0f, damage: 16f );
		SpawnShockwaveEffect( SHOCKWAVE_TOTAL_TIME * 0.45f, 99f, -0.1f );
		SpawnShockwaveEffect( SHOCKWAVE_TOTAL_TIME * 0.35f, 98f, -0.2f );
		SpawnShockwaveEffect( SHOCKWAVE_TOTAL_TIME * 0.25f, 97f, -0.3f );
		SpawnShockwaveEffect( SHOCKWAVE_TOTAL_TIME * 0.15f, 96f, -0.4f );

		//SS2Game.Current.ShakePlayerCams( Position2D, 400f, 8f, 0.5f );

		Manager.Instance.PlaySfxNearby( "shockwave", Position2D, pitch: Game.Random.Float( 1.6f, 1.7f ), volume: 0.75f, maxDist: 12f );

		Manager.Instance.ShakePlayerCam( Position2D, 8f, 0.125f, 0.4f );
	}

	void SpawnShockwaveEffect(float lifetime, float depth, float scaleOffset, float damage = 0f)
	{
		var ring = Manager.Instance.SpawnRingEffect( Position2D, new Color( 1f, 0f, 0f ), new Color( 1f, 1f, 0f ), damage, opacity: 1f, scaleMin: 0f, scaleMax: 37.5f, depth );
		ring.Lifetime = lifetime;
		ring.ScaleLifetime = SHOCKWAVE_TOTAL_TIME;
		ring.ScaleOffset = scaleOffset;
	}

	public override void Celebrate()
	{
		base.Celebrate();

		if ( _isJumping )
			return;

		CelebrateAsync();
	}

	async void CelebrateAsync()
	{
		Sprite.PlayAnimation( AnimIdlePath );

		await Task.Delay( Game.Random.Int( 50, 100 ) );

		Sprite.PlaybackSpeed = Game.Random.Float( 2.5f, 3f );

		Sprite.PlayAnimation( "cheer_start" );

		await Task.Delay( Game.Random.Int( 350, 400 ) );

		Sprite.PlaybackSpeed = Game.Random.Float( 2f, 3f );

		Sprite.PlayAnimation( "cheer" );
	}
}