Game/Weapon/MeleeWeapon/MeleeWeapon.cs
using Sandbox.Rendering;

public class MeleeWeapon : BaseCarryable
{
	/// <summary>
	/// Cooldown after a hit connects.
	/// </summary>
	[Property] public float SwingDelay { get; set; } = 0.5f;

	/// <summary>
	/// Cooldown after a swing misses.
	/// </summary>
	[Property] public float MissSwingDelay { get; set; } = 0.75f;

	/// <summary>
	/// Damage dealt per hit.
	/// </summary>
	[Property] public float Damage { get; set; } = 12f;

	/// <summary>
	/// Reach of the swing trace.
	/// </summary>
	[Property] public float Range { get; set; } = 128f;

	/// <summary>
	/// Radius of the swing trace sphere.
	/// </summary>
	[Property] public float SwingRadius { get; set; } = 10f;

	/// <summary>
	/// Physics impulse magnitude applied to hit objects.
	/// </summary>
	[Property] public float SwingForce { get; set; } = 1000f;

	[Property] public SoundEvent SwingSound { get; set; }
	[Property] public SoundEvent HitSound { get; set; }

	TimeUntil timeUntilSwing = 0;

	public bool CanAttack() => timeUntilSwing <= 0;

	protected virtual bool WantsPrimaryAttack() => Input.Down( "attack1" );

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

		if ( WantsPrimaryAttack() )
			Swing( player );
	}

	public void Swing( Player player )
	{
		if ( !CanAttack() )
			return;

		var forward = AimRay.Forward;

		var trace = Scene.Trace.Ray( AimRay with { Forward = forward }, Range )
							.IgnoreGameObjectHierarchy( AimIgnoreRoot )
							.WithoutTags( "playercontroller" )
							.UseHitboxes();

		var tr = trace.Run();
		if ( !tr.Hit )
		{
			tr = trace.Radius( SwingRadius ).Run();
		}

		timeUntilSwing = tr.GameObject.IsValid() ? SwingDelay : MissSwingDelay;

		SwingEffects( tr.HitPosition, tr.Hit, tr.Normal, tr.GameObject, tr.Surface );
		TraceAttack( TraceAttackInfo.From( tr, Damage, localise: false ) );

		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 )
		{
			new Sandbox.CameraNoise.Punch( new Vector3( Random.Shared.Float( -10, -15 ), Random.Shared.Float( -10, 0 ), 0 ), 1.0f, 3, 0.5f );
			new Sandbox.CameraNoise.Shake( 0.3f, 1.2f );
		}
	}

	[Rpc.Broadcast]
	public void SwingEffects( Vector3 hitpoint, bool hit, Vector3 normal, GameObject hitObject, Surface hitSurface )
	{
		if ( Application.IsDedicatedServer ) return;

		var player = Owner;
		if ( player.IsValid() )
			player.Controller.Renderer.Set( "b_attack", true );

		if ( ViewModel.IsValid() )
			ViewModel.RunEvent<ViewModel>( x => x.OnAttack() );
		else if ( WorldModel.IsValid() )
			WorldModel.RunEvent<WorldModel>( x => x.OnAttack() );

		GameObject.PlaySound( SwingSound );

		if ( !hit || !hitObject.IsValid() )
			return;

		if ( ViewModel.IsValid() )
			ViewModel.RunEvent<ViewModel>( x => x.Renderer.Set( "b_attack_has_hit", true ) );

		hitObject.PlaySound(
			hitSurface.SoundCollection.ImpactHard ?? hitSurface.GetBaseSurface()?.SoundCollection.ImpactHard ?? HitSound,
			hitObject.WorldTransform.PointToLocal( hitpoint ) );

		var prefab = hitSurface.PrefabCollection.BulletImpact ?? hitSurface.GetBaseSurface()?.PrefabCollection.BulletImpact;
		if ( prefab is null )
			return;

		var fwd = Rotation.LookAt( normal * -1.0f, Vector3.Random );

		var impact = prefab.Clone();
		impact.WorldPosition = hitpoint;
		impact.WorldRotation = fwd;
		impact.SetParent( hitObject, true );

		if ( hitObject.GetComponentInChildren<SkinnedModelRenderer>() is not { CreateBoneObjects: true } skinned )
			return;

		// find closest bone
		var bones = skinned.GetBoneTransforms( true );

		var closestDist = float.MaxValue;

		for ( var i = 0; i < bones.Length; i++ )
		{
			var bone = bones[i];
			var dist = bone.Position.Distance( hitpoint );
			if ( dist < closestDist )
			{
				closestDist = dist;
				impact.SetParent( skinned.GetBoneObject( i ), true );
			}
		}
	}

	public override void DrawHud( HudPainter painter, Vector2 crosshair )
	{
		DrawCrosshair( painter, crosshair );
	}

	public virtual void DrawCrosshair( HudPainter hud, Vector2 center )
	{
		var len = 6;
		Color color = CanAttack() ? Color.White : Color.Red;

		hud.SetBlendMode( BlendMode.Lighten );
		hud.DrawCircle( center, len, color );
	}
}