Shockwave.cs

A game Component that renders and manages a circular shockwave effect. It updates radius over time, applies damage/force to nearby non-proxy players once when the wave passes, and builds a SceneLineObject for the visual ring with wobble and scrolling texture.

NetworkingFile Access
using System;
using Sandbox;

/// <summary>
/// Shockwaves are created on client and can't hurt proxy players.
/// </summary>
public class Shockwave : Component
{
	private SceneLineObject _so;

	private float _shockwaveRadius;
	private float _shockwaveTimer;

	private Dictionary<Player, bool> _playerShockwaveChecks = new();
	private List<Player> _shockwaveDamagedPlayers = new();

	private float _wobbleRate;
	private float _wobblePhase;
	private float _scrollRate;

	private int _numSegments;

	public Enemy EnemySource { get; set; }
	public EnemyType EnemyType { get; set; }

	public float Damage { get; set; }
	public float Radius { get; set; }
	public float Lifetime { get; set; }
	public float Force { get; set; }
	public Gradient Gradient { get; set; }
	public float ScrollSpeed { get; set; } = 1f;
	public float SlowdownDuration { get; set; } = 0.5f;
	public float WobbleIntensity { get; set; }
	public float WobbleSpeed { get; set; } = 15f;
	public float EmissiveScale { get; set; } = 4f;
	[Property] public Texture LineTexture { get; set; }

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

		if ( !Application.IsDedicatedServer )
		{
			_so = new SceneLineObject( Scene.SceneWorld );
			_so.RenderingEnabled = false;
			_so.Flags.IsOpaque = false;
			_so.Flags.IsTranslucent = true;
			_so.Flags.CastShadows = true;
			_so.Material = Material.Load( "materials/shockwave.vmat" );
			_so.Attributes.Set( "g_flEmissiveScale", EmissiveScale );
			_so.Attributes.Set( "BaseTexture", LineTexture ?? Texture.White );
		}

		WobbleIntensity = Game.Random.Float( 6f, 10f );

		float direction = Game.Random.Int( 0, 1 ) == 0 ? 1f : -1f;
		_wobbleRate = Game.Random.Float( 0.15f, 1f ) * WobbleSpeed * direction;
		_wobblePhase = Game.Random.Float( 0f, MathF.PI * 2f );

		float scrollDirection = Game.Random.Int( 0, 1 ) == 0 ? 1f : -1f;
		_scrollRate = Game.Random.Float( 0.6f, 1.4f ) * ScrollSpeed * scrollDirection;

		_numSegments = Game.Random.Int( 70, 80 );
	}

	protected override void OnDisabled()
	{
		_so?.Delete();
		_so = null;
	}

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

		var pos = (Vector2)WorldPosition;

		_shockwaveTimer += Time.Delta;
		float slowdownStart = Lifetime - SlowdownDuration;
		// midRadius chosen so that constant decel from v0→0 over SlowdownDuration lands exactly at Radius
		float midRadius = Radius * 2f * slowdownStart / (2f * slowdownStart + SlowdownDuration);
		float v0 = midRadius / slowdownStart;
		float naturalRadius;
		if ( _shockwaveTimer <= slowdownStart )
		{
			naturalRadius = v0 * _shockwaveTimer;
		}
		else
		{
			float tau = _shockwaveTimer - slowdownStart;
			naturalRadius = midRadius + v0 * tau * (1f - tau / (2f * SlowdownDuration));
		}
		_shockwaveRadius = naturalRadius * 0.92f;

		if ( _shockwaveTimer > Lifetime )
		{
			GameObject.Destroy();
			return;
		}
		else
		{
			foreach ( Player player in Manager.Instance.AlivePlayers )
			{
				if ( player.IsProxy )
					continue;

				bool withinShockwave = (player.Position2D - pos).LengthSquared < MathF.Pow( _shockwaveRadius, 2f );

				if ( _playerShockwaveChecks.ContainsKey( player ) )
				{
					if ( withinShockwave != _playerShockwaveChecks[player] && !_shockwaveDamagedPlayers.Contains( player ) && player.TimeSinceTeleport > 0.1f && !player.IsInTheAir && !player.IsDead &&
						_shockwaveTimer < Lifetime - SlowdownDuration * 0.5f )
					{
						var dir = (player.Position2D - pos).Normal;
						var hitPos = player.Position2D - dir * player.Radius;
						player.Damage( Damage, DamageType.Shockwave, hitPos, dir, upwardAmount: Game.Random.Float( 0f, 0.5f ), force: Force, ragdollForce: Force * 0.005f, EnemySource, EnemyType );
						_shockwaveDamagedPlayers.Add( player );
					}

					_playerShockwaveChecks[player] = withinShockwave;
				}
				else
				{
					_playerShockwaveChecks.Add( player, withinShockwave );
				}
			}
		}

		if ( _so is null )
			return;

		_so.Transform = WorldTransform;
		_so.StartCap = SceneLineObject.CapStyle.None;
		_so.EndCap = SceneLineObject.CapStyle.None;
		_so.Face = SceneLineObject.FaceMode.Camera;
		_so.RenderingEnabled = true;
		//_so.Attributes.Set( "BaseTexture", Texture.White );

		float widthDecay = Utils.Map( _shockwaveTimer, 0f, Lifetime, 1.4f, 0.55f );
		float baseWidth = (8f + Utils.FastSin( Time.Now * 16f ) * 1.5f) * widthDecay;
		float alpha = MathF.Min( (0.8f + Utils.FastSin( Time.Now * 12f ) * 0.2f)
		                         * Utils.Map( _shockwaveTimer, Lifetime - SlowdownDuration, Lifetime, 1f, 0f ), 1f );
		float scrollOffset = (Time.Now * _scrollRate) % 1.0f;
		float wobbleScale = _shockwaveRadius / Radius;
		var center = WorldPosition;

		_so.StartLine();
		for ( int i = 0; i <= _numSegments; i++ )
		{
			float t = (float)i / _numSegments;
			float angle = (1f - MathF.Cos( t * MathF.PI )) / 2f * MathF.PI * 2f;
			float wobble = (MathF.Sin( angle * 5f + Time.Now * _wobbleRate + _wobblePhase ) * WobbleIntensity
			             + MathF.Sin( angle * 11f + Time.Now * _wobbleRate * 1.7f + _wobblePhase * 1.37f ) * WobbleIntensity * 0.4f
			             + MathF.Sin( angle * 7f + Time.Now * _wobbleRate * 0.9f + _wobblePhase * 2.1f ) * WobbleIntensity * 0.25f) * wobbleScale;
			float r = _shockwaveRadius + wobble;
			Vector3 pointPos = center + new Vector3( MathF.Cos( angle ) * r, MathF.Sin( angle ) * r, 15f );

			float colorT = angle / (MathF.PI * 2f);
			float sampledT = ((colorT + scrollOffset) % 1.0f + 1.0f) % 1.0f;
			Color color = Gradient.Evaluate( sampledT ).WithAlpha( alpha );

			float widthWobble = MathF.Sin( angle * 6f + Time.Now * _wobbleRate * 2f + _wobblePhase * 1.5f ) * WobbleIntensity * 0.4f * wobbleScale;
			float width = MathF.Max( baseWidth + widthWobble, 1f );

			_so.AddLinePoint( pointPos, WorldTransform.Up, color, width, colorT + scrollOffset );
		}
		_so.EndLine();

		if ( IsProxy )
			return;

	}
}