swb_base/bullets/BulletInfo.HitScan.cs
using SWB.Shared;
using System;
using System.Linq;

namespace SWB.Base;

[Group( "SWB" )]
[Title( "HitScan Bullet Info" )]
public class HitScanBulletInfo : BulletInfo
{
	public override void Shoot( Weapon weapon, bool isPrimary, Vector3 spreadOffset )
	{
		if ( !weapon.IsValid() ) return;

		var player = weapon.Owner;
		if ( !player.IsValid() ) return;

		var forward = player.EyeAngles.Forward + spreadOffset;
		forward = forward.Normal;
		var endPos = player.EyePos + forward * 999999;
		var bulletTr = weapon.TraceBullet( player.EyePos, endPos );
		var hitObj = bulletTr.GameObject;
		var shootInfo = weapon.GetShootInfo( isPrimary );
		var hasTracer = ShouldSpawnTracer( shootInfo );
		var hasImpact = !SurfaceUtil.IsSkybox( bulletTr.Surface ) && bulletTr.HitPosition != Vector3.Zero;

		// Damage
		if ( hitObj is not null )
		{
			var target = hitObj.Components.GetInAncestorsOrSelf<IDamageable>();
			var hitTags = Array.Empty<string>();

			if ( bulletTr.Hitbox is not null )
				hitTags = bulletTr.Hitbox.Tags.TryGetAll().ToArray();

			var force = forward * 100 * shootInfo.Force;
			var dmgInfo = Shared.DamageInfo.FromBullet(
				weapon.Owner.GameObject,
				weapon.GameObject,
				bulletTr.Hitbox,
				bulletTr.EndPosition,
				bulletTr.Shape,
				weapon.ClassName,
				shootInfo.Damage,
				bulletTr.HitPosition,
				force,
				shootInfo.HitFlinch,
				Weapon.GetMovementImpactFromForce( shootInfo.Force ),
				hitTags
			);
			target?.OnDamage( dmgInfo );
		}

		// Effects
		if ( hasTracer || hasImpact )
			SpawnEffects( weapon, isPrimary, hasImpact, hasTracer, bulletTr.EndPosition, bulletTr.Normal, bulletTr.Surface?.SoundCollection.Bullet, bulletTr.Surface?.PrefabCollection.BulletImpact );
	}

	[Rpc.Broadcast( NetFlags.Unreliable )]
	public void SpawnEffects( Weapon weapon, bool isPrimary, bool hasImpact, bool hasTracer, Vector3 hitPos, Vector3 hitNormal, SoundEvent hitSound, GameObject hitParticles )
	{
		if ( !weapon.IsValid() || Application.IsDedicatedServer ) return;

		// Impact
		if ( hasImpact )
			Weapon.CreateBulletImpact( hitPos, hitNormal, hitSound, hitParticles );

		// Tracer
		if ( hasTracer )
			TracerEffects( weapon, isPrimary, hitPos );
	}

	public virtual void TracerEffects( Weapon weapon, bool isPrimary, Vector3 hitPos )
	{
		var muzzleTransform = weapon.GetMuzzleTransform();
		if ( !muzzleTransform.HasValue ) return;

		var shootInfo = weapon.GetShootInfo( isPrimary );
		var scale = weapon.CanSeeViewModel ? shootInfo.VMParticleScale : shootInfo.WMParticleScale;
		var direction = (hitPos - muzzleTransform.Value.Position).Normal;
		var rotation = Rotation.LookAt( direction );
		var particleTransform = muzzleTransform.Value.WithRotation( rotation );
		weapon.CreateParticle( shootInfo.BulletTracerParticle, particleTransform, scale );
	}
}