Weapons/CrossbowWeapon.cs
using Sandbox.Rendering;
using Sandbox.Utility;

public class CrossbowWeapon : BaseWeapon
{
	const float ZoomedFOV = 20.0f;

	[Property] public float TimeBetweenShots { get; set; } = 2f;
	[Property] public SoundEvent FireSound { get; set; }
	[Property] public GameObject ProjectilePrefab { get; set; }
	[Property] public GameObject StuckPrefab { get; set; }

	public bool IsZoomed { get; private set; }
	float fieldOfView;
	CommandList _commandList;

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

		fieldOfView = Scene.Camera.FieldOfView;

		_commandList = new CommandList( "Crossbow" ) { Flags = CommandList.Flag.Hud };
		Scene.Camera.AddCommandList( _commandList, Stage.AfterPostProcess, 2000 );
	}

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

		Scene.Camera.FieldOfView = Preferences.FieldOfView;
		Scene.Camera.RemoveCommandList( _commandList, Stage.AfterPostProcess );
		IsZoomed = false;
	}

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

		if ( Input.Down( "attack1" ) )
		{
			Shoot( player );
		}

		if ( Input.Pressed( "attack2" ) )
		{
			IsZoomed = !IsZoomed;
		}

		_commandList.Reset();
		if ( IsZoomed )
		{
			DrawCrossbowScope();
		}
	}

	[Rpc.Broadcast]
	private void ShootEffects( Vector3 hitpoint, bool hit, Vector3 normal, GameObject hitObject )
	{
		if ( !Owner.IsValid() ) return;
		if ( Application.IsDedicatedServer ) return;

		var ev = new IWeaponEvent.AttackEvent( ViewModel.IsValid() );
		IWeaponEvent.PostToGameObject( GameObject.Root, x => x.OnAttack( ev ) );

		if ( hit )
		{
			var stuck = StuckPrefab.Clone( hitpoint, Rotation.LookAt( hitpoint - Owner.EyeTransform.Position ), Vector3.One );
			stuck.SetParent( hitObject );
		}

		GameObject.PlaySound( FireSound );
	}

	[Rpc.Broadcast]
	private void ShootEffects()
	{
		if ( Application.IsDedicatedServer ) return;

		var ev = new IWeaponEvent.AttackEvent( ViewModel.IsValid() );
		IWeaponEvent.PostToGameObject( GameObject.Root, x => x.OnAttack( ev ) );

		GameObject.PlaySound( FireSound );
	}

	TimeSince TimeSinceShoot;

	public void Shoot( Player player )
	{
		if ( !CanShoot() || !TakeAmmo( 1 ) )
		{
			TryAutoReload();
			return;
		}

		TimeSinceShoot = 0;

		AddShootDelay( TimeBetweenShots );

		var forward = player.EyeTransform.Rotation.Forward;
		forward = forward.Normal;

		if ( IsZoomed )
		{
			var bulletRadius = GameSettings.CrossbowBulletRadius * GameSettings.BulletRadius;
			var tr = Scene.Trace.Ray( player.EyeTransform.ForwardRay with { Forward = forward }, 4096 )
							.IgnoreGameObjectHierarchy( player.GameObject )
							.WithCollisionRules( "bullet" )
							.Radius( bulletRadius )
							.UseHitboxes()
							.Run();

			ShootEffects( tr.EndPosition, tr.Hit, tr.Normal, tr.GameObject );
			TraceAttack( TraceAttackInfo.From( tr, 120 ) );
		}
		else
		{
			var tr = Scene.Trace.Ray( player.EyeTransform.ForwardRay with { Forward = forward }, 4096 )
								.IgnoreGameObjectHierarchy( player.GameObject )
								.WithCollisionRules( "bullet" )
								.UseHitboxes()
								.Run();

			ShootEffects();
			CreateProjectile( tr.StartPosition + tr.Direction * 64f, tr.Direction, 4096 );
		}

		player.Controller.EyeAngles += new Angles( Random.Shared.Float( -0.2f, -0.3f ), Random.Shared.Float( -0.1f, 0.1f ), 0 );

		if ( !player.Controller.ThirdPerson && player.IsLocalPlayer )
		{
			var punchPitch = Random.Shared.Float( 45, 35 );
			if ( IsZoomed ) punchPitch *= 0.2f;
			new Sandbox.CameraNoise.Punch( new Vector3( punchPitch, Random.Shared.Float( -10, -5 ), 0 ), 1.5f, 2, 0.5f );
			new Sandbox.CameraNoise.Shake( 1f, 0.6f );
		}
	}

	/// <summary>
	/// We want to control the camera fov
	/// </summary>
	public override void OnCameraSetup( Player player, Sandbox.CameraComponent camera )
	{
		if ( !player.Network.IsOwner || !Network.IsOwner ) return;

		var delta = 1f - MathF.Pow( 0.5f, Time.Delta * 50f );
		if ( IsZoomed )
		{
			fieldOfView = fieldOfView.LerpTo( ZoomedFOV, delta );
		}
		else
		{
			fieldOfView = fieldOfView.LerpTo( camera.FieldOfView, delta );
		}

		camera.FieldOfView = fieldOfView;
	}

	/// <summary>
	/// We want to control the camera sens
	/// </summary>
	public override void OnCameraMove( Player player, ref Angles angles )
	{
		if ( IsZoomed )
		{
			angles *= fieldOfView / Preferences.FieldOfView;
		}
	}

	/// <summary>
	/// Creates the projectile with the host's permission
	/// </summary>
	/// <param name="start"></param>
	/// <param name="direction"></param>
	/// <param name="speed"></param>
	[Rpc.Host]
	void CreateProjectile( Vector3 start, Vector3 direction, float speed )
	{
		if ( !Owner.IsValid() ) return;

		var go = ProjectilePrefab?.Clone( start );

		var projectile = go.GetComponent<CrossbowProjectile>();
		Assert.True( projectile.IsValid(), "ExplosiveProjectile not on projectile prefab" );

		projectile.Instigator = Owner.PlayerData;
		projectile.UpdateDirection( direction, speed );

		go.NetworkSpawn();
	}

	public override void DrawCrosshair( HudPainter hud, Vector2 center )
	{
		var tss = TimeSinceShoot.Relative.Remap( 0, 0.2f, 1, 0 );
		var gap = 32 + Easing.EaseOut( tss ) * 32;
		var w = 2;
		Color color = !CanShoot() ? UI.CrosshairInactive : UI.CrosshairActive;

		float lineLength = 12;
		float circleRadius = 4;

		hud.DrawLine(
			center + new Vector2( gap, 0 ),
			center + new Vector2( gap + lineLength, lineLength ),
			w, color );
		hud.DrawLine(
			center + new Vector2( gap, 0 ),
			center + new Vector2( gap + lineLength, -lineLength ),
			w, color );

		hud.DrawLine(
			center + new Vector2( -gap, 0 ),
			center + new Vector2( -gap - lineLength, lineLength ),
			w, color );
		hud.DrawLine(
			center + new Vector2( -gap, 0 ),
			center + new Vector2( -gap - lineLength, -lineLength ),
			w, color );

		hud.DrawCircle( center, circleRadius, color );
	}

	private void DrawCrossbowScope()
	{
		var mat = Material.FromShader( "shaders/weapons/ui_crossbow_zoom.shader" );

		var radius = MathX.Remap( fieldOfView, Preferences.FieldOfView, ZoomedFOV, 0f, 0.3f );
		if ( TimeSinceShoot < 0.5f )
		{
			radius += (0.5f - TimeSinceShoot) / 0.5f * 0.015f;
		}

		_commandList.Attributes.Set( "AspectRatio", Screen.Aspect );
		_commandList.Attributes.Set( "Radius", radius );
		_commandList.Blit( mat );
	}
}