Projectile.cs
using Sandbox;
using System;
using System.Numerics;

public sealed class Projectile : Component
{
	public Vector3 Velocity { get; set; }

	/// <summary>
	/// Reference to the weapon to ignore
	/// </summary>
	public GameObject WeaponObject { get; set; }

	[Property] public float Damage { get; set; } = 25f;

	/// <summary>Seconds until the projectile is destroyed, including after it sticks when Stay In World is enabled.</summary>
	[Property, Group( "Lifetime" )]
	public float Lifetime { get; set; } = 5f;

	/// <summary>Keeps the projectile mesh in the world until Lifetime expires instead of destroying it on impact. Enable for crossbow bolts.</summary>
	[Property, Group( "Lifetime" )]
	public bool StayInWorld { get; set; }

	/// <summary>Impulse applied at the hit point of dynamic props and existing ragdolls; live players are excluded.</summary>
	[Property] public float ImpactImpulse { get; set; } = 50f;

	/// <summary>Used when the hit surface has no bullet impact effect assigned.</summary>
	[Property] public GameObject FallbackImpactEffect { get; set; }
	/// <summary>Used when the hit surface has no bullet impact sound assigned.</summary>
	[Property] public SoundEvent FallbackImpactSound { get; set; }
	/// <summary>Plays instead of the surface sound when the bullet hits a player.</summary>
	[Property] public SoundEvent FleshImpactSound { get; set; }

	private Dictionary<string, float> HitboxDamageMultipliers { get; set; } = new()
	{
		["ankle_L"] = 0.6f,
		["ankle_R"] = 0.6f,
		["leg_lower_L"] = 0.6f,
		["leg_lower_R"] = 0.6f,
		["leg_upper_L"] = 0.6f,
		["leg_upper_R"] = 0.6f,
		["pelvis"] = 0.75f,
		["spine_0"] = 1f,
		["spine_1"] = 1f,
		["spine_2"] = 1f,
		["clavicle_L"] = 1f,
		["clavicle_R"] = 1f,
		["neck_0"] = 1f,
		["head"] = 2f,
		["arm_upper_L"] = 0.75f,
		["arm_upper_R"] = 0.75f,
		["arm_lower_L"] = 0.75f,
		["arm_lower_R"] = 0.75f,
		["hand_L"] = 0.6f,
		["hand_R"] = 0.6f,
	};

	private float _deathTime;
	private bool _stuck;

	protected override void OnStart()
	{
		_deathTime = Time.Now + Lifetime;
	}

	protected override void OnUpdate()
	{
		if ( IsProxy )
			return;

		if ( Time.Now >= _deathTime )
		{
			GameObject.Destroy();
			return;
		}

		if ( _stuck )
			return;

		Vector3 frameMovement = Velocity * Time.Delta;
		Vector3 nextPosition = WorldPosition + frameMovement;

		// Configure the ray trace path
		var trace = Scene.Trace.Ray( WorldPosition, nextPosition )
			.Size( 1f )
			.UseHitboxes()
			.WithoutTags("dead")
			.IgnoreGameObject( GameObject ) // Ignore the bullet mesh itself
			.IgnoreGameObject( WeaponObject )
			.IgnoreGameObjectHierarchy( Player.CurrentPlayer.GameObject ) // Ignore the shooter's entire gun/avatar tree
			.Run();

		if ( trace.Hit )
		{
			HandleImpact( trace );
			return;
		}

		WorldPosition = nextPosition;
	}

	private void HandleImpact( SceneTraceResult trace )
	{
		var health = trace.GameObject?.Components.Get<HealthComponent>( FindMode.InSelf );
		var hitFlesh = health.IsValid();

		if ( hitFlesh )
		{
			var damageMultiplier = HitboxDamageMultipliers[trace.Hitbox.Bone.Name];
			var damage = Damage * damageMultiplier;
			var headshot = trace.Hitbox.Bone.Name == "head";
			var weaponIcon = WeaponObject?.Components.Get<InventoryItem>()?.ItemIconPath ?? "";
			health.TakeDamage( damage, Vector3.Zero, headshot, weaponIcon );
		}
		else
		{
			ApplyPhysicsImpact( trace );
		}

		PlayImpactSound( trace, hitFlesh );
		SpawnImpactEffect( trace );

		if ( StayInWorld )
		{
			StickInWorld( trace );
			return;
		}

		GameObject.Destroy();
	}

	private void StickInWorld( SceneTraceResult trace )
	{
		Velocity = Vector3.Zero;
		WorldPosition = trace.HitPosition;
		_stuck = true;

		var player = trace.GameObject?.GetComponent<Player>();
		if ( player.IsValid() )
		{
			var bone = player.Avatar.GetBoneObject( trace.Hitbox.Bone );
			GameObject.SetParent( bone, true );
			return;
		}

		if ( trace.GameObject.IsValid() )
			GameObject.SetParent( trace.GameObject, true );
	}

	private void ApplyPhysicsImpact( SceneTraceResult trace )
	{
		if ( trace.Body is null )
			return;

		var impulse = Velocity.Normal * ImpactImpulse;
		var networkTarget = FindNetworkTarget( trace.Body.GameObject );

		if ( networkTarget.IsValid() && DeathmatchGame.Instance.IsValid() )
		{
			// One small targeted broadcast lets whichever peer controls this prop
			// apply the impulse. No component is required on the hit object.
			DeathmatchGame.Instance.ApplyProjectileImpulse( networkTarget, trace.HitPosition, impulse );
			return;
		}

		// Non-networked physics cannot be addressed across peers, so preserve the
		// useful local reaction without spending network bandwidth.
		trace.Body.ApplyImpulseAt( trace.HitPosition, impulse );
	}

	private static GameObject FindNetworkTarget( GameObject hitObject )
	{
		for ( var current = hitObject; current.IsValid(); current = current.Parent )
		{
			if ( current.Network.Active )
				return current;
		}

		return null;
	}

	private void SpawnImpactEffect( SceneTraceResult trace )
	{
		var prefab = GetImpactPrefab( trace );
		if ( !prefab.IsValid() )
			return;

		var decal = GetImpactDecalPrefab( trace );

		var effect = prefab.Clone( trace.HitPosition, Rotation.LookAt( -trace.Normal ) );
		decal?.Clone( parent: effect, position: Vector3.Zero, rotation: Rotation.Identity, scale: Vector3.One );

		var temporaryEffect = effect.GetComponent<TemporaryEffect>();
		if ( temporaryEffect.IsValid() )
		{
			temporaryEffect.WaitForChildEffects = false;
			temporaryEffect.DestroyAfterSeconds = 10f;
		}

		var player = trace.GameObject.GetComponent<Player>();
		if ( player.IsValid() )
		{
			var bone = player.Avatar.GetBoneObject( trace.Hitbox.Bone );
			effect.SetParent( bone, true );
		}
		else if ( trace.GameObject.IsValid() )
			effect.SetParent( trace.GameObject, true );

		effect.NetworkSpawn();
	}

	private GameObject GetImpactPrefab( SceneTraceResult trace )
	{
		var surface = trace.Surface;
		if ( surface is not null )
		{
			var impactPrefab = surface.PrefabCollection.BulletImpact;
			if ( impactPrefab.IsValid() )
				return impactPrefab;

			var baseSurface = surface.GetBaseSurface();
			impactPrefab = baseSurface?.PrefabCollection.BulletImpact;
			if ( impactPrefab.IsValid() )
				return impactPrefab;
		}

		return FallbackImpactEffect;
	}

	private GameObject GetImpactDecalPrefab( SceneTraceResult trace )
	{
		var surface = trace.Surface;
		if ( surface is not null )
		{
			var impactDecal = surface.PrefabCollection.BulletImpactDecal;
			if ( impactDecal.IsValid() )
				return impactDecal;
		}

		return null;
	}

	private void PlayImpactSound( SceneTraceResult trace, bool hitFlesh )
	{
		var sound = GetImpactSound( trace, hitFlesh );
		if ( sound is null )
			return;

		var position = trace.HitPosition + trace.Normal * 2f;

		if ( DeathmatchGame.Instance.IsValid() )
			DeathmatchGame.Instance.PlayBulletImpactSound( sound, position );
		else
			Sound.Play( sound, position );
	}

	private SoundEvent GetImpactSound( SceneTraceResult trace, bool hitFlesh )
	{
		if ( hitFlesh && FleshImpactSound is not null )
			return FleshImpactSound;

		var surface = trace.Surface;
		if ( surface is not null )
		{
			var bulletSound = surface.SoundCollection.Bullet;
			if ( bulletSound is not null )
				return bulletSound;

			var baseSurface = surface.GetBaseSurface();
			bulletSound = baseSurface?.SoundCollection.Bullet;
			if ( bulletSound is not null )
				return bulletSound;
		}

		return FallbackImpactSound;
	}
}