2023-04-02 16:48:32 +03:00
using Content.Server.Administration.Logs ;
2025-02-04 22:55:09 +01:00
using Content.Server.Destructible ;
2023-08-11 03:44:52 +10:00
using Content.Server.Effects ;
2023-04-02 16:48:32 +03:00
using Content.Server.Weapons.Ranged.Systems ;
using Content.Shared.Camera ;
using Content.Shared.Damage ;
using Content.Shared.Database ;
2025-02-04 22:55:09 +01:00
using Content.Shared.FixedPoint ;
2023-04-02 16:48:32 +03:00
using Content.Shared.Projectiles ;
using Robust.Shared.Physics.Events ;
2023-10-17 21:45:35 -07:00
using Robust.Shared.Player ;
2023-04-02 16:48:32 +03:00
namespace Content.Server.Projectiles ;
public sealed class ProjectileSystem : SharedProjectileSystem
{
[Dependency] private readonly IAdminLogManager _adminLogger = default ! ;
2023-08-11 03:44:52 +10:00
[Dependency] private readonly ColorFlashEffectSystem _color = default ! ;
2023-04-02 16:48:32 +03:00
[Dependency] private readonly DamageableSystem _damageableSystem = default ! ;
2025-02-04 22:55:09 +01:00
[Dependency] private readonly DestructibleSystem _destructibleSystem = default ! ;
2023-04-02 16:48:32 +03:00
[Dependency] private readonly GunSystem _guns = default ! ;
[Dependency] private readonly SharedCameraRecoilSystem _sharedCameraRecoil = default ! ;
public override void Initialize ( )
{
base . Initialize ( ) ;
SubscribeLocalEvent < ProjectileComponent , StartCollideEvent > ( OnStartCollide ) ;
}
private void OnStartCollide ( EntityUid uid , ProjectileComponent component , ref StartCollideEvent args )
{
// This is so entities that shouldn't get a collision are ignored.
2023-09-22 02:45:21 -07:00
if ( args . OurFixtureId ! = ProjectileFixture | | ! args . OtherFixture . Hard
2025-02-04 22:55:09 +01:00
| | component . ProjectileSpent | | component is { Weapon : null , OnlyCollideWhenShot : true } )
2023-04-02 16:48:32 +03:00
return ;
2023-08-06 16:44:41 +03:00
var target = args . OtherEntity ;
2023-04-02 16:48:32 +03:00
// it's here so this check is only done once before possible hit
var attemptEv = new ProjectileReflectAttemptEvent ( uid , component , false ) ;
2023-08-06 16:44:41 +03:00
RaiseLocalEvent ( target , ref attemptEv ) ;
2023-04-02 16:48:32 +03:00
if ( attemptEv . Cancelled )
{
2023-10-17 21:45:35 -07:00
SetShooter ( uid , component , target ) ;
2023-04-02 16:48:32 +03:00
return ;
}
2025-02-18 08:28:42 +01:00
var ev = new ProjectileHitEvent ( component . Damage * _damageableSystem . UniversalProjectileDamageModifier , target , component . Shooter ) ;
2023-08-06 16:44:41 +03:00
RaiseLocalEvent ( uid , ref ev ) ;
var otherName = ToPrettyString ( target ) ;
2025-02-04 22:55:09 +01:00
var damageRequired = _destructibleSystem . DestroyedAt ( target ) ;
if ( TryComp < DamageableComponent > ( target , out var damageableComponent ) )
{
damageRequired - = damageableComponent . TotalDamage ;
damageRequired = FixedPoint2 . Max ( damageRequired , FixedPoint2 . Zero ) ;
}
var modifiedDamage = _damageableSystem . TryChangeDamage ( target , ev . Damage , component . IgnoreResistances , damageable : damageableComponent , origin : component . Shooter ) ;
2023-08-06 16:44:41 +03:00
var deleted = Deleted ( target ) ;
2023-04-02 16:48:32 +03:00
2025-06-26 19:50:49 -04:00
if ( modifiedDamage is not null & & Exists ( component . Shooter ) )
2023-04-02 16:48:32 +03:00
{
2024-05-31 14:28:11 +12:00
if ( modifiedDamage . AnyPositive ( ) & & ! deleted )
2023-04-02 16:48:32 +03:00
{
2023-08-11 03:44:52 +10:00
_color . RaiseEffect ( Color . Red , new List < EntityUid > { target } , Filter . Pvs ( target , entityManager : EntityManager ) ) ;
2023-04-02 16:48:32 +03:00
}
_adminLogger . Add ( LogType . BulletHit ,
2025-03-20 20:56:51 +01:00
LogImpact . Medium ,
2024-01-22 02:59:14 +01:00
$"Projectile {ToPrettyString(uid):projectile} shot by {ToPrettyString(component.Shooter!.Value):user} hit {otherName:target} and dealt {modifiedDamage.GetTotal():damage} damage" ) ;
2023-04-02 16:48:32 +03:00
}
2025-02-04 22:55:09 +01:00
// If penetration is to be considered, we need to do some checks to see if the projectile should stop.
if ( modifiedDamage is not null & & component . PenetrationThreshold ! = 0 )
{
// If a damage type is required, stop the bullet if the hit entity doesn't have that type.
if ( component . PenetrationDamageTypeRequirement ! = null )
{
var stopPenetration = false ;
foreach ( var requiredDamageType in component . PenetrationDamageTypeRequirement )
{
if ( ! modifiedDamage . DamageDict . Keys . Contains ( requiredDamageType ) )
{
stopPenetration = true ;
break ;
}
}
if ( stopPenetration )
component . ProjectileSpent = true ;
}
// If the object won't be destroyed, it "tanks" the penetration hit.
if ( modifiedDamage . GetTotal ( ) < damageRequired )
{
component . ProjectileSpent = true ;
}
if ( ! component . ProjectileSpent )
{
component . PenetrationAmount + = damageRequired ;
// The projectile has dealt enough damage to be spent.
if ( component . PenetrationAmount > = component . PenetrationThreshold )
{
component . ProjectileSpent = true ;
}
}
}
else
{
component . ProjectileSpent = true ;
}
2023-04-02 16:48:32 +03:00
if ( ! deleted )
{
2023-08-06 16:44:41 +03:00
_guns . PlayImpactSound ( target , modifiedDamage , component . SoundHit , component . ForceSound ) ;
2024-12-05 15:20:27 +03:00
if ( ! args . OurBody . LinearVelocity . IsLengthZero ( ) )
_sharedCameraRecoil . KickCamera ( target , args . OurBody . LinearVelocity . Normalized ( ) ) ;
2023-04-02 16:48:32 +03:00
}
2025-02-04 22:55:09 +01:00
if ( component . DeleteOnCollide & & component . ProjectileSpent )
2023-08-06 16:44:41 +03:00
QueueDel ( uid ) ;
2023-04-02 16:48:32 +03:00
2024-05-21 17:40:35 +12:00
if ( component . ImpactEffect ! = null & & TryComp ( uid , out TransformComponent ? xform ) )
2023-08-06 16:44:41 +03:00
{
2023-09-11 09:42:41 +10:00
RaiseNetworkEvent ( new ImpactEffectEvent ( component . ImpactEffect , GetNetCoordinates ( xform . Coordinates ) ) , Filter . Pvs ( xform . Coordinates , entityMan : EntityManager ) ) ;
2023-04-02 16:48:32 +03:00
}
}
}