RingEffect.cs
using Sandbox;
using SpriteTools;

public sealed class RingEffect : Component
{
	[Property] public SpriteRendererLayer Sprite { get; set; }

	private TimeSince _spawnTime;
	public float Lifetime { get; set; }
	public float ScaleLifetime { get; set; }

	public Color ColorA { get; private set; }
	public Color ColorB { get; private set; }
	public float Opacity { get; set; }

	private float _radius;
	public float ScaleMin { get; set; }
	public float ScaleMax { get; set; }

	public float ScaleOffset { get; set; }

	public float Damage { get; set; }

	private bool _playerShockwaveCheck;
	private bool _hasShockwaveHitPlayer;

	public Vector2 Position2D { get; set; }

	protected override void OnAwake()
	{
		base.OnAwake();

		//LocalScale = new Vector3( 1f, 0.925f, 1f ) * 2.85f * Globals.SPRITE_SCALE;
		//Sprite.Size = new Vector2( 1f, 0.95f ) * 2.85f;
		_spawnTime = 0f;
		Sprite.PlaybackSpeed = 1.5f;
		_radius = 0f;
	}

	public void Init( Color colorA, Color colorB, float opacity )
	{
		ColorA = colorA;
		ColorB = colorB;
		Opacity = opacity;

		Sprite.Tint = colorA.WithAlpha( opacity );
	}

	protected override void OnUpdate()
	{
		Sprite.Tint = Color.Lerp(ColorA, ColorB, 0.5f + Utils.FastSin(Time.Now * 42f) * 0.5f ).WithAlpha( Utils.Map( _spawnTime, 0f, Lifetime, Opacity, 0f, EasingType.SineIn ) );

		_radius = Math.Max( Utils.Map( _spawnTime, 0f, ScaleLifetime, ScaleMin, ScaleMax, EasingType.Linear ) + ScaleOffset, 0f );
		LocalScale = new Vector3( 1f, 1f, 1f ) * _radius * Globals.SPRITE_SCALE;

		if ( _spawnTime > Lifetime )
		{
			Manager.Instance.RemoveRingEffect( this );
			GameObject.Destroy();
			return;
		}

		if ( Damage > 0f )
		{
			var player = Manager.Instance.Player;
			var shockwaveRadius = _radius * 0.49f;
			bool withinShockwave = (player.Position2D - Position2D).LengthSquared < MathF.Pow( shockwaveRadius, 2f );

			if ( withinShockwave != _playerShockwaveCheck && !_hasShockwaveHitPlayer && !player.IsDead && _spawnTime < Lifetime - 0.25f )
			{
				float dmg = player.CheckDamageAmount( Damage, DamageType.Ranged );

				if ( !player.IsInvulnerable && !player.IsTimePausedForChoosing )
				{
					player.Damage( dmg );
					Manager.Instance.PlaySfxNearby( "zombie.attack.player", player.Position2D, pitch: Game.Random.Float( 1.15f, 1.2f ), volume: 0.9f, maxDist: 3f );
				}

				_hasShockwaveHitPlayer = true;
				player.Velocity += (player.Position2D - Position2D).Normal * (player.IsInvulnerable ? 1f : 3f);
			}

			_playerShockwaveCheck = withinShockwave;

			//Gizmo.Draw.Color = Color.White.WithAlpha( 1f );
			//Gizmo.Draw.LineSphere( ((Vector3)Position2D).WithZ(120f), shockwaveRadius, 30 );
		}
	}
}