Weapons/GluonGun/GluonWeapon.cs
using Sandbox.Rendering;
using Sandbox.Utility;

public sealed class GluonWeapon : BaseBulletWeapon, BaseWeapon.IWeaponEvent
{
	[Property] public float BeamDamage { get; set; } = 10;
	[Property] public float BeamRange { get; set; } = 1000;
	[Property] public float BeamPulseInterval { get; set; } = 0.2f;
	[Property] public float BeamAmmoDrainFrequency { get; set; } = 0.2f;
	[Property] public SoundEvent StartSound { get; set; }
	[Property] public SoundEvent StopSound { get; set; }
	[Property] public SoundEvent LoopSound { get; set; }
	[Property] public GameObject BeamEffect { get; set; }

	[Sync]
	private GameObject LockTargetGameObject { get; set; }

	[Sync]
	private Vector3 beamTarget { get; set; }

	IDamageable lockTarget;
	BeamComponent beam;
	SoundHandle loopSound;
	Vector3 hitPosition;
	TimeSince timeSinceAmmoTick;
	TimeSince timeSinceFireTick;

	public override void OnHolstered( Player player )
	{
		base.OnHolstered( player );
		StopAttack();
	}

	public override void OnPlayerUpdate( Player player )
	{
		base.OnPlayerUpdate( player );

		if ( player.IsLocalPlayer )
		{
			UpdateShootEffects( player );
		}
	}

	private Player FindLockTargetWithRadius( Player player, float radius )
	{
		var tr = GetBulletTrace( player, radius ).Run();

		if ( !tr.Hit || tr.GameObject.GetComponentInParent<Player>() is null )
		{
			return null;
		}

		return tr.GameObject.GetComponentInParent<Player>();
	}

	private Player FindLockTarget( Player player )
	{
		return FindLockTargetWithRadius( player, 1f ) ?? FindLockTargetWithRadius( player, 15f ) ?? FindLockTargetWithRadius( player, 50f ) ?? FindLockTargetWithRadius( player, 100 );
	}

	private void UpdateBeamTarget( Vector3 target )
	{
		if ( !Owner.IsLocalPlayer )
		{
			// No interpolation 
			beamTarget = target;
			return;
		}

		if ( beam.IsValid() )
		{
			// Don't lerp if the distance is too far away
			if ( beamTarget.DistanceSquared( target ) > 62500f )
			{
				beamTarget = target;
			}
			else
			{
				beamTarget = beamTarget.LerpTo( target, Time.Delta * 15f );
			}
		}
	}

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

		if ( !beam.IsValid() )
		{
			return;
		}

		if ( LockTargetGameObject.IsValid() )
		{
			var target = LockTargetGameObject.GetBounds().Center;
			UpdateBeamTarget( target );
			beam.SetMiddlePoint( Owner.EyeTransform.Forward, 2.5f );
		}

		beam.SetBeam( beamTarget );
	}

	public override void OnControl( Player player )
	{
		base.OnControl( player );

		lockTarget = FindLockTarget( player );

		if ( Input.Pressed( "Attack1" ) )
		{
			if ( !TakeAmmo( 1 ) )
			{
				DryFire();
				AddShootDelay( 0.1f );

				SwitchAway();
			}
			else if ( CanShoot() )
			{
				timeSinceFireTick = BeamPulseInterval;
				beamTarget = hitPosition;
				StartAttack();
			}
		}

		if ( Input.Released( "Attack1" ) )
		{
			StopAttack();
		}

		if ( beam.IsValid() )
		{
			if ( timeSinceAmmoTick > BeamAmmoDrainFrequency )
			{
				timeSinceAmmoTick = 0;

				if ( !CanShoot() || !TakeAmmo( 2 ) )
				{
					AddShootDelay( 0.1f );
					StopAttack();
					return;
				}
			}

			Shoot( player );

			if ( !player.Controller.ThirdPerson && player.IsLocalPlayer )
			{
				new Sandbox.CameraNoise.Shake( 0.08f, 0.1f );
			}
		}
	}

	public override bool IsInUse() => beam.IsValid();

	public void Shoot( Player player )
	{
		float damageScale = timeSinceFireTick / BeamPulseInterval;

		if ( timeSinceFireTick > BeamPulseInterval )
		{
			SceneTraceResult tr = default;
			if ( lockTarget is not null )
			{
				var start = player.EyeTransform.Position;
				var rot = Rotation.LookAt( player.EyeTransform.Forward );
				var component = lockTarget as Component;
				var target = component.GameObject.GetBounds().Center;
				var direction = (target - start);

				tr = Scene.Trace.Ray( start, start + direction * BeamRange )
					.IgnoreGameObjectHierarchy( player.GameObject )
					.WithCollisionRules( "bullet" )
					.UseHitboxes()
					.Size( 1f )
					.Run();

				LockTargetGameObject = tr.GameObject;
			}
			else
			{
				LockTargetGameObject = null;
				tr = GetBulletTrace( player, 5 ).Run();
			}

			hitPosition = tr.EndPosition;

			TraceAttack( TraceAttackInfo.From( tr, BeamDamage * damageScale, [DamageTags.GibAlways, DamageTags.Shock] ) );
			SplashDamage( TraceAttackInfo.From( tr, BeamDamage / 4.0f * damageScale, [DamageTags.GibAlways, DamageTags.Shock] ) );

			if ( player.IsLocalPlayer )
			{
				HitMarker.CreateFromTrace( tr );
			}

			UpdateShootEffects( player );

			timeSinceFireTick = 0;
		}

		TimeSinceShoot = 0;
	}

	[Rpc.Host]
	void SplashDamage( TraceAttackInfo attack )
	{
		if ( !Owner.IsValid() ) return;

		Damage.Radius( attack.Position, 128, attack.Damage, attack.Tags, Owner.GameObject, GameObject, ignore: Owner.GameObject );
	}

	/// <summary>
	/// Constructs a trace and returns it, doesn't run it yet!
	/// </summary>
	SceneTrace GetBulletTrace( Player player, float radius )
	{
		var start = player.EyeTransform.Position;
		var rot = Rotation.LookAt( player.EyeTransform.Forward );

		return Scene.Trace.Ray( start, start + rot.Forward * BeamRange )
			.IgnoreGameObjectHierarchy( player.GameObject )
			.WithCollisionRules( "bullet" )
			.UseHitboxes()
			.Size( radius );
	}

	public override void DrawCrosshair( HudPainter hud, Vector2 center )
	{
		var tss = TimeSinceShoot.Relative.Remap( 0, 0.4f, 0.5f, 0 );

		var len = 40 + Easing.BounceIn( tss ) * 128;

		Color color = !CanShoot() ? UI.CrosshairInactive : UI.CrosshairActive;

		var rect = new Rect( center - len * 0.5f, len );

		hud.DrawRect( rect, Color.Transparent, cornerRadius: new Vector4( 512 ), borderWidth: new Vector4( 2 ), borderColor: color );

		if ( lockTarget is Component target && target.IsValid() )
		{
			var gap = 48f;
			var w = 2f;
			len = 16;

			hud.DrawLine( center + Vector2.Left * (len + gap), center + Vector2.Left * gap, w, color );
			hud.DrawLine( center - Vector2.Left * (len + gap), center - Vector2.Left * gap, w, color );
			hud.DrawLine( center + Vector2.Up * (len + gap), center + Vector2.Up * gap, w, color );
			hud.DrawLine( center - Vector2.Up * (len + gap), center - Vector2.Up * gap, w, color );

			// Define the size of the square
			var squareSize = 64f;
			var go = target.GameObject;
			center = Scene.Camera.PointToScreenPixels( go.GetBounds().Center );

			// Draw the four edges of the square
			hud.DrawLine( center + new Vector2( -squareSize / 2, -squareSize / 2 ), center + new Vector2( squareSize / 2, -squareSize / 2 ), w, color ); // Top edge
			hud.DrawLine( center + new Vector2( squareSize / 2, -squareSize / 2 ), center + new Vector2( squareSize / 2, squareSize / 2 ), w, color );   // Right edge
			hud.DrawLine( center + new Vector2( squareSize / 2, squareSize / 2 ), center + new Vector2( -squareSize / 2, squareSize / 2 ), w, color );  // Bottom edge
			hud.DrawLine( center + new Vector2( -squareSize / 2, squareSize / 2 ), center + new Vector2( -squareSize / 2, -squareSize / 2 ), w, color ); // Left edge
		}
	}

	void IWeaponEvent.OnAttackStart( IWeaponEvent.AttackEvent e )
	{
		StartShootEffects();
	}

	void IWeaponEvent.OnAttackStop()
	{
		StopShootEffects();
	}

	private void StartShootEffects()
	{
		if ( Application.IsDedicatedServer ) return;

		GameObject.PlaySound( StartSound );

		loopSound?.Stop();

		if ( LoopSound.IsValid() )
		{
			loopSound = GameObject.PlaySound( LoopSound );
		}

		// If player dies this can be invalid
		if ( WeaponModel.IsValid() )
		{
			var effect = BeamEffect.Clone( new CloneConfig { Parent = WeaponModel.MuzzleTransform, Transform = global::Transform.Zero, StartEnabled = true } );
			beam = effect.GetComponent<BeamComponent>( true );
		}
	}

	private void UpdateShootEffects( Player player )
	{
		if ( !beam.IsValid() )
			return;

		if ( !LockTargetGameObject.IsValid() )
		{
			var tr = GetBulletTrace( player, 5 ).Run();
			UpdateBeamTarget( tr.EndPosition );
			beam.SetMiddlePoint( Owner.EyeTransform.Forward, 0f );
		}
	}

	private void StopShootEffects()
	{
		if ( Application.IsDedicatedServer ) return;

		if ( loopSound.IsValid() )
		{
			loopSound.Stop();
			loopSound = null;

			if ( StopSound.IsValid() )
			{
				GameObject.PlaySound( StopSound );
			}
		}

		if ( beam.IsValid() )
		{
			beam.DestroyGameObject();
		}
	}

}