Physical bullet mover component for SWB weapons. Advances the bullet each fixed update with drag and gravity, traces the path, applies damage and decals, and continues through penetrated players up to a cap.
using SWB.Shared;
using System;
using System.Collections.Generic;
using System.Linq;
namespace SWB.Base;
[Group( "SWB" )]
[Title( "Physical Bullet Mover" )]
public class PhysicalBulletMover : Component
{
private const int MaxPenetrations = 10; // safety valve, also acts as a practical per-bullet pierce cap
private readonly List<GameObject> ignoreGameObjects = new();
private readonly Guid shotId = Guid.NewGuid(); // correlates every hit from this single physical bullet (penetration)
public IPlayerBase Owner { get; set; }
public string ClassName { get; set; }
public Vector3 BulletVelocity { get; set; }
public Weapon Weapon { get; set; }
public ShootInfo ShootInfo { get; set; }
public PhysicalBulletInfo BulletInfo { get; set; }
public float BulletGravity => BulletInfo.Gravity;
public float BulletDrag => BulletInfo.Drag;
public bool HasImpacted { get; private set; } = false;
public void Initialize( PhysicalBulletInfo bulletInfo, Weapon weapon, ShootInfo shootInfo, Vector3 bulletVelocity )
{
BulletInfo = bulletInfo;
Owner = weapon.Owner;
ClassName = weapon.ClassName;
Weapon = weapon;
ShootInfo = shootInfo;
BulletVelocity = bulletVelocity;
// If a bullet is flying this long it probably got bugged
GetOrAddComponent<TemporaryEffect>().DestroyAfterSeconds = 20f;
}
protected override void OnFixedUpdate()
{
if ( IsProxy || HasImpacted || Owner is null ) return;
// Apply drag (before gravity so we aren't immediately dragging on gravity)
BulletVelocity *= 1 - BulletDrag;
// Apply gravity
BulletVelocity += Vector3.Down * BulletGravity * Time.Delta;
var bulletMovement = BulletVelocity * Time.Delta;
// Trace along path to see if we hit anything
var traceIgnoreTags = ShootInfo.Penetration ? Weapon.PenetrationBulletTraceIgnoreTags : null;
var bulletTrace = Weapon.TraceBullet( Owner.GameObject, WorldPosition, WorldPosition + bulletMovement, ignoreTags: traceIgnoreTags, extraIgnoreGOs: ignoreGameObjects );
if ( bulletTrace.Hit )
{
var penetratedPlayer = HandleImpact( bulletTrace );
if ( penetratedPlayer is not null && ignoreGameObjects.Count < MaxPenetrations )
{
ignoreGameObjects.Add( penetratedPlayer.GameObject );
WorldPosition = bulletTrace.HitPosition + BulletVelocity.Normal * 1.0f;
return;
}
WorldPosition = bulletTrace.HitPosition;
// Allows for graceful ending of effects
HasImpacted = true;
GetOrAddComponent<TemporaryEffect>().DestroyAfterSeconds = 0.5f;
ITemporaryEffect.DisableLoopingEffects( GameObject );
return;
}
// If we didn't hit anything, we can move the bullet
WorldPosition += bulletMovement;
}
protected IPlayerBase HandleImpact( SceneTraceResult traceResult )
{
var hitObject = traceResult.GameObject;
if ( SurfaceUtil.IsSkybox( traceResult.Surface ) || traceResult.HitPosition == Vector3.Zero ) return null;
// Impact
var decal = Weapon.CreateBulletImpact( traceResult );
decal?.NetworkSpawn();
// Damage
IPlayerBase penetratedPlayer = null;
if ( hitObject is not null )
{
var target = hitObject.Components.GetInAncestorsOrSelf<IDamageable>();
var hitTags = Array.Empty<string>();
if ( traceResult.Hitbox is not null )
hitTags = traceResult.Hitbox.Tags.TryGetAll().ToArray();
var forward = BulletVelocity.Normal;
// Assume force from shoot info is the force at firing
var force = BulletVelocity.Length.Remap( 0, BulletInfo.Velocity, 0, ShootInfo.Force );
var dmgInfo = Shared.DamageInfo.FromBullet(
Owner.GameObject,
Weapon.GameObject,
traceResult.Hitbox,
traceResult.EndPosition,
traceResult.Shape,
ClassName,
ShootInfo.Damage,
traceResult.HitPosition,
forward * 100 * force,
ShootInfo.HitFlinch,
Weapon.GetMovementImpactFromForce( ShootInfo.Force ),
hitTags,
Weapon.GetKillDetails(),
shotId
);
target?.OnDamage( dmgInfo );
if ( ShootInfo.Penetration && target is IPlayerBase playerTarget )
penetratedPlayer = playerTarget;
}
return penetratedPlayer;
}
}