TestWeapon.cs
using Sandbox;
using Sandbox.Rendering;
using System;
using System.Threading.Tasks;


public enum GrabAction
{
	None,
	SweepDown,
	SweepRight,
	SweepLeft,
	PushButton
}

public interface IGrabAction
{
	/// <summary>
	/// Returns the grab action for this component
	/// </summary>
	GrabAction GrabAction { get; }
}

public sealed class TestWeapon : Component, PlayerController.IEvents, ICameraSetup
{
	[Property, Group( "View Model" )]
	public Model ViewModel { get; set; }

	[Property, Group( "View Model" ), Model.BodyGroupMask( ModelParameter = "ViewModel" )]
	public ulong Bodygroups { get; set; }

	[Property, Group( "View Model" )]
	public Vector3 ViewModelLocalOffset { get; set; }

	[Property, Group( "View Model" )]
	public Angles ViewModelLocalRotation { get; set; }

	[Property, Group( "Primary" )]
	public bool PrimaryAutomatic { get; set; } = true;

	[Property, Group( "Primary" )]
	public float PrimaryDelay { get; set; } = 0.05f;

	[Property, Group( "Primary" )]
	public SoundEvent PrimaryAttackSound { get; set; }

	[Property, Group( "Config" )]
	public AnimationGraph GraphOverride { get; set; }

	[Property, Group( "Config" )]
	public float RayDistance { get; set; } = 4096;

	[Property, Group( "Ammo" )]
	public int Ammo { get; set; } = 30;

	[Property, Group( "Ammo" )]
	public int MaxAmmo { get; set; } = 30;

	[Property, Group( "Ammo" )]
	public float ReloadTime { get; set; } = 1.5f;

	[Property, Group( "View Model" )]
	public GrabAction UseGrabAction { get; set; }

	[Property, Group( "Weapon-Specific" )]
	public bool IsBoltAction { get; set; } = false;

	[Property, Group( "Weapon-Specific" )]
	public bool UseMuzzleFlash { get; set; } = true;

	[Property, Group( "Weapon-Specific" )]
	public bool SingularReload { get; set; } = false;

	[Property, Group( "Weapon-Specific" )]
	public bool TwoHanded { get; set; } = false;

	[Property, Group( "Weapon-Specific" )]
	public bool UseJoystick { get; set; } = false;

	[Property, Group( "Weapon-Specific" ), Range( 0, 2 ), Step( 0.05f )]
	public float IronsightsFireScale { get; set; } = 0.5f;

	[Property, Group( "Recoil" )]
	public RangedFloat PitchRecoil { get; set; } = new RangedFloat( -0.25f, -0.5f );

	[Property, Group( "Recoil" )]
	public RangedFloat YawRecoil { get; set; } = new RangedFloat( -0.4f, 0.4f );

	[Property, Group( "Recoil" )]
	public float RecoilNoiseScale { get; set; } = 1.0f;

	[Property, Group( "Recoil" )]
	public float RecoilNoiseSpeed { get; set; } = 0.3f;

	/// <summary>
	/// Will copy some parameters from the body renderer
	/// </summary>
	[Property, Group( "Config" )]
	public SkinnedModelRenderer BodyRenderer { get; set; }

	private enum FireMode
	{
		Off,
		Single,
		Burst,
		FullAuto
	}

	FireMode fireMode = FireMode.Off;
	bool isReloading = false;
	TimeSince timeSinceReload = 0.0f;

	public bool IsAiming => Input.Down( "attack2" ) && !UseJoystick;

	void PlayerController.IEvents.StartPressing( Component target )
	{
		if ( !(viewmodel?.Components.TryGet<SkinnedModelRenderer>( out var vm ) ?? false) )
			return;

		if ( target is not IGrabAction grabber ) return;

		vm.Set( "grab_action", (int)grabber.GrabAction );
	}

	protected override void OnUpdate()
	{
		if ( Input.Pressed( "Drop" ) )
		{
			ToggleLower();
		}

		var aimPos = Screen.Size * 0.5f;
		DrawHud( Scene.Camera.Hud, aimPos );
		AnimationThink();
	}

	void ToggleLower()
	{
		lower = !lower;
		timeSinceLowered = 0;
	}

	bool lower = false;
	TimeSince timeSinceLowered = 0.0f;
	Rotation lastRot;
	TimeSince timeSincePrimaryAttackStarted = 0.0f;
	Vector3.SmoothDamped smoothedWish = new Vector3.SmoothDamped( 0, 0, 0.5f );
	Vector3.SmoothDamped smoothedJoystick = new Vector3.SmoothDamped( 0, 0, 0.5f );

	private static Vector3 GetLocalVelocity( Rotation rotation, Vector3 worldVelocity )
	{
		// TODO: this could be rotation.Inverse * worldVelocity

		var forward = rotation.Forward.Dot( worldVelocity );
		var sideward = rotation.Right.Dot( worldVelocity );

		return new Vector3( forward, sideward, worldVelocity.z );
	}

	private static float GetAngle( Vector3 localVelocity )
	{
		return MathF.Atan2( localVelocity.y, localVelocity.x ).RadianToDegree().NormalizeDegrees();
	}


	void AnimationThink()
	{
		if ( !(viewmodel?.Components.TryGet<SkinnedModelRenderer>( out var vm ) ?? false) )
			return;

		var controller = Components.Get<PlayerController>( FindMode.InAncestors );

		var vel = controller.Velocity;
		var wishVel = controller.WishVelocity;
		var rot = vm.WorldRotation;
		var isAiming = IsAiming;
		var wantsSprint = Input.Down( controller.AltMoveButton );

		var isHovering = controller.Hovered.IsValid() && controller.Hovered.WorldPosition.Distance( WorldPosition ) < 128;

		vm.Set( "b_grab", isHovering );

		//
		// General states
		//
		vm.Set( "ironsights", isAiming && !wantsSprint ? 1 : 0 );
		vm.Set( "b_sprint", wantsSprint && vel.Length > 0f );
		vm.Set( "b_lower_weapon", lower );
		vm.Set( "firing_mode", (int)fireMode );
		vm.Set( "b_empty", Ammo < 1 );
		vm.Set( "b_twohanded", TwoHanded );
		vm.Set( "ironsights_fire_scale", IronsightsFireScale );

		controller.UseLookControls = !(UseJoystick && Input.Down( "Attack2" ));

		//
		// Joystick
		//
		{
			vm.Set( "b_joystick", UseJoystick && Input.Down( "Attack2" ) );

			var smoothed = Mouse.Delta * 0.1f;
			{
				smoothedJoystick.Target = smoothed;
				smoothedJoystick.SmoothTime = 0.1f;
				smoothedJoystick.Update( Time.Delta );
				smoothed = smoothedJoystick.Current;
			}

			vm.Set( "joystick_x", UseJoystick ? smoothed.y : 0f );
			vm.Set( "joystick_y", UseJoystick ? -smoothed.x : 0f );
		}

		//
		// Bolt action
		//
		if ( Input.Released( "Attack1" ) && IsBoltAction )
		{
			vm.Set( "b_reload_bolt", true );
		}

		//
		// Movement
		//
		{
			var smoothed = wishVel;
			{
				smoothedWish.Target = smoothed;
				smoothedWish.SmoothTime = 0.1f;
				smoothedWish.Update( Time.Delta );
				smoothed = smoothedWish.Current;
			}

			smoothed = GetLocalVelocity( rot, smoothed );

			vm.Set( "move_direction", GetAngle( smoothed ) );
			vm.Set( "move_bob", smoothed.Length.Remap( 0, 150, 0, 1 ) );
			vm.Set( "move_groundspeed", smoothed.WithZ( 0f ).Length );
			vm.Set( "move_x", smoothed.x );
			vm.Set( "move_y", smoothed.y );
			vm.Set( "move_z", smoothed.z );
		}

		vm.Set( "attack_hold", CanShoot() && Input.Down( "Attack1" ) ? timeSincePrimaryAttackStarted : 0 );

		var rotationDelta = Rotation.Difference( lastRot, Scene.Camera.WorldRotation );
		lastRot = Scene.Camera.WorldRotation;

		var angles = rotationDelta.Angles();

		vm.Set( "aim_pitch", angles.pitch );
		vm.Set( "aim_yaw", angles.yaw );

		vm.Set( "aim_pitch_inertia", angles.pitch );
		vm.Set( "aim_yaw_inertia", angles.yaw );

		if ( BodyRenderer.IsValid() )
		{
			vm.Set( "b_jump", BodyRenderer.GetBool( "b_jump" ) );
			vm.Set( "move_groundspeed", BodyRenderer.GetFloat( "move_groundspeed" ) );
			vm.Set( "b_grounded", controller.IsOnGround );
			vm.Set( "move_x", BodyRenderer.GetFloat( "move_x" ) );
			vm.Set( "move_y", BodyRenderer.GetFloat( "move_y" ) );
			vm.Set( "move_z", BodyRenderer.GetFloat( "move_z" ) );
		}
	}

	GameObject viewmodel;

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

		CreateViewmodel();
	}

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

		viewmodel?.Destroy();
	}

	bool CanShoot()
	{
		if ( isReloading ) return false;

		if ( !PrimaryAutomatic && !Input.Pressed( "attack1" ) )
			return false;

		if ( timeSinceLastShoot < PrimaryDelay )
			return false;

		if ( MaxAmmo > 0 && Ammo < 1 ) return false;

		if ( lower && Input.Down( "Attack1" ) )
		{
			ToggleLower();
		}

		if ( timeSinceLowered < 0.35f ) return false;

		return true;
	}

	protected override void OnFixedUpdate()
	{
		if ( Input.Pressed( "Flashlight" ) )
		{
			// Cycle through to the next fire mode, wrapping around using modulo
			fireMode = (FireMode)(((int)fireMode + 1) % Enum.GetValues<FireMode>().Length);
		}

		var pressedFire = Input.Pressed( "attack1" );

		if ( Input.Down( "attack1" ) )
		{
			if ( CanShoot() )
			{
				RunAttack();

				if ( pressedFire )
				{
					timeSincePrimaryAttackStarted = 0.0f;
				}
			}
			else
			{
				if ( pressedFire && Ammo < 1 )
				{
					if ( viewmodel.Components.TryGet<SkinnedModelRenderer>( out var vm ) )
					{
						vm.Set( "b_attack_dry", true );
					}
				}
			}
		}

		if ( Input.Pressed( "reload" ) )
		{
			if ( MaxAmmo > 0 && Ammo >= MaxAmmo ) return; // Don't reload if we're full

			// Start the reload sequence
			if ( viewmodel.Components.TryGet<SkinnedModelRenderer>( out var vm ) )
			{
				isReloading = true;

				if ( !SingularReload )
				{
					vm.Set( "b_reload", true );

					Invoke( ReloadTime, () =>
					{
						Ammo = MaxAmmo;
						isReloading = false;
					} );
				}
				else
				{
					vm.Set( "b_reloading", true );
				}

				timeSinceReload = 0;
			}
		}

		if ( isReloading && SingularReload )
		{
			if ( timeSinceReload >= ReloadTime )
			{
				if ( viewmodel.Components.TryGet<SkinnedModelRenderer>( out var vm ) )
				{
					vm.Set( "b_reloading_shell", true );
					timeSinceReload = 0;
					Ammo = Math.Min( Ammo + 1, MaxAmmo );

					if ( Ammo >= MaxAmmo )
					{
						isReloading = false;
						vm.Set( "b_reloading", false );
					}
				}
			}
		}

		if ( Input.Down( "attack2" ) )
		{
			RunAltAttack();
		}
	}

	TimeSince timeSinceLastShoot = 0.0f;

	void RunAttack()
	{
		timeSinceLastShoot = 0;

		Ammo--;

		Vector3 shootPosition = WorldPosition;

		if ( viewmodel.Components.TryGet<SkinnedModelRenderer>( out var vm ) )
		{
			vm.Set( "b_attack", true );

			var controller = Components.Get<PlayerController>( FindMode.InAncestors );
			controller.EyeAngles += new Angles( PitchRecoil.GetValue(), YawRecoil.GetValue(), 0 );

			_ = new Sandbox.CameraNoise.Recoil( RecoilNoiseScale, RecoilNoiseSpeed );

			if ( UseMuzzleFlash )
			{
				var muzzle = vm.GetBoneObject( vm.Model.Bones.GetBone( "muzzle" ) ) ?? vm.GameObject;
				shootPosition = muzzle.WorldPosition;
				GameObject.Clone( "/effects/muzzle.prefab", global::Transform.Zero, muzzle );
			}
		}

		Sound.Play( PrimaryAttackSound, shootPosition );

		ShootBullet();

	}

	void RunAltAttack()
	{
	}

	void ShootBullet()
	{
		var ray = Scene.Camera.WorldTransform.ForwardRay;
		ray.Forward += Vector3.Random * 0.01f;

		var tr = Scene.Trace.Ray( ray, RayDistance )
					.IgnoreGameObjectHierarchy( GameObject.Parent )
					.Run();

		//Sound.Play( shootSound, Transform.Position );

		if ( !tr.Hit || tr.GameObject is null )
			return;

		if ( tr.Surface is not null )
		{
			var prefab = tr.Surface.PrefabCollection.BulletImpact ?? tr.Surface.GetBaseSurface()?.PrefabCollection.BulletImpact;
			if ( prefab is not null )
			{
				prefab?.Clone( new Transform( tr.HitPosition + tr.Normal, Rotation.LookAt( -tr.Normal ) ) );
			}
		}
		else
		{
			GameObject.Clone( "/effects/impact_default.prefab", new Transform( tr.HitPosition + tr.Normal * 2.0f, Rotation.LookAt( tr.Normal ) ) );

			{
				var go = GameObject.Clone( "/effects/decal_bullet_default.prefab" );
				go.WorldTransform = new Transform( tr.HitPosition + tr.Normal * 2.0f, Rotation.LookAt( -tr.Normal, Vector3.Random ), System.Random.Shared.Float( 0.8f, 1.2f ) );
				go.SetParent( tr.GameObject );
			}
		}


		if ( tr.Body.IsValid() )
		{
			tr.Body.ApplyImpulseAt( tr.HitPosition, tr.Direction * 200.0f * tr.Body.Mass.Clamp( 0, 200 ) );
		}

		var damage = new DamageInfo( 10, GameObject, GameObject, tr.Hitbox );
		damage.Position = tr.HitPosition;
		damage.Shape = tr.Shape;

		foreach ( var damageable in tr.GameObject.Components.GetAll<IDamageable>() )
		{
			damageable.OnDamage( damage );
		}
	}

	void CreateViewmodel()
	{
		viewmodel = new GameObject( true, "viewmodel" );

		var modelRender = viewmodel.Components.Create<SkinnedModelRenderer>();
		modelRender.Model = ViewModel;
		modelRender.BodyGroups |= Bodygroups;
		modelRender.RenderOptions.Overlay = true;
		modelRender.RenderOptions.Game = false;

		modelRender.CreateBoneObjects = true;
		modelRender.RenderType = ModelRenderer.ShadowRenderType.Off;

		if ( GraphOverride is not null )
			modelRender.AnimationGraph = GraphOverride;

		{
			var arms = new GameObject();
			arms.Parent = viewmodel;

			var model = arms.Components.Create<SkinnedModelRenderer>();
			model.Model = Model.Load( "models/first_person/first_person_arms.vmdl" );
			model.BoneMergeTarget = modelRender;
			model.RenderOptions.Overlay = true;
			model.RenderOptions.Game = false;
			model.RenderType = ModelRenderer.ShadowRenderType.Off;
		}
	}

	void ICameraSetup.Setup( CameraComponent cc )
	{
		if ( viewmodel is null ) return;

		viewmodel.Tags.Set( "viewer", !BodyRenderer.Tags.Has( "viewer" ) );

		viewmodel.WorldPosition = cc.WorldPosition;
		viewmodel.WorldRotation = cc.WorldRotation;

		viewmodel.LocalRotation *= ViewModelLocalRotation.ToRotation();

		viewmodel.LocalPosition += viewmodel.WorldRotation * ViewModelLocalOffset;

		if ( viewmodel.Components.TryGet<SkinnedModelRenderer>( out var vm ) )
		{
			var bone = vm.GetBoneObject( "camera" );
			var scale = 1;

			cc.LocalPosition += bone.LocalPosition * scale;
			cc.LocalRotation *= bone.LocalRotation * scale;
		}
	}

	public float HolsterTime => 1.2f;

	private TaskCompletionSource<bool> holsterTask;

	public async Task Holster()
	{
		if ( !GameObject.Active )
			return;

		// Already holstering
		if ( holsterTask is not null )
			await holsterTask.Task;

		holsterTask = new TaskCompletionSource<bool>();

		if ( viewmodel?.Components.TryGet<SkinnedModelRenderer>( out var vm ) ?? false )
		{
			vm.Set( "b_holster", true );
		}

		// Wait for holster time
		await GameTask.DelaySeconds( HolsterTime );

		holsterTask.SetResult( true );
		holsterTask = null;
	}

	void DrawHud( HudPainter hud, Vector2 center )
	{
		if ( IsAiming )
			return;

		var gap = 8;
		var len = 12;
		var w = 2f;
		var color = Color.White.WithAlpha( 0.5f );

		hud.SetBlendMode( BlendMode.Lighten );
		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 );
	}
}