using System.Numerics; using Content.Shared._RMC14.Weapons.Ranged.Prediction; using Content.Shared.Administration.Logs; using Content.Shared.Camera; using Content.Shared.CombatMode.Pacification; using Content.Shared.Damage; using Content.Shared.Database; using Content.Shared.DoAfter; using Content.Shared.Effects; using Content.Shared.Hands.EntitySystems; using Content.Shared.Interaction; using Content.Shared.Throwing; using Content.Shared.Weapons.Ranged.Systems; using Robust.Shared.Audio.Systems; using Robust.Shared.Map; using Robust.Shared.Network; using Robust.Shared.Physics; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Events; using Robust.Shared.Physics.Systems; using Robust.Shared.Player; using Robust.Shared.Serialization; using Content.Shared.Barricade; using Robust.Shared.Random; namespace Content.Shared.Projectiles; public abstract partial class SharedProjectileSystem : EntitySystem { public const string ProjectileFixture = "projectile"; [Dependency] private readonly INetManager _netManager = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly SharedHandsSystem _hands = default!; [Dependency] private readonly SharedPhysicsSystem _physics = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; [Dependency] private readonly SharedColorFlashEffectSystem _color = default!; [Dependency] private readonly DamageableSystem _damageableSystem = default!; [Dependency] private readonly SharedGunSystem _guns = default!; [Dependency] private readonly SharedCameraRecoilSystem _sharedCameraRecoil = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly ILogManager _log = default!; private ISawmill _sawmill = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnStartCollide); SubscribeLocalEvent(PreventCollision); SubscribeLocalEvent(OnEmbedProjectileHit); SubscribeLocalEvent(OnEmbedThrowDoHit); SubscribeLocalEvent(OnEmbedActivate); SubscribeLocalEvent(OnEmbedRemove); _sawmill = _log.GetSawmill("projectile"); } private void OnStartCollide(EntityUid uid, ProjectileComponent component, ref StartCollideEvent args) { // This is so entities that shouldn't get a collision are ignored. if (args.OurFixtureId != ProjectileFixture || !args.OtherFixture.Hard || component.DamagedEntity || component is { Weapon: null, OnlyCollideWhenShot: true }) return; ProjectileCollide((uid, component, args.OurBody), args.OtherEntity); } public void ProjectileCollide(Entity projectile, EntityUid target, bool predicted = false) { var (uid, component, ourBody) = projectile; if (projectile.Comp1.DamagedEntity) { if (_netManager.IsServer && component.DeleteOnCollide) QueueDel(uid); return; } // it's here so this check is only done once before possible hit var attemptEv = new ProjectileReflectAttemptEvent(uid, component, false); RaiseLocalEvent(target, ref attemptEv); if (attemptEv.Cancelled) { SetShooter(uid, component, target); return; } var ev = new ProjectileHitEvent(component.Damage, target, component.Shooter); RaiseLocalEvent(uid, ref ev); if (ev.Handled) return; var coordinates = Transform(projectile).Coordinates; var otherName = ToPrettyString(target); var direction = ourBody.LinearVelocity.Normalized(); var modifiedDamage = _netManager.IsServer ? _damageableSystem.TryChangeDamage(target, ev.Damage, component.IgnoreResistances, origin: component.Shooter) : new DamageSpecifier(ev.Damage); var deleted = Deleted(target); var filter = Filter.Pvs(coordinates, entityMan: EntityManager); if (_guns.GunPrediction && TryComp(projectile, out PredictedProjectileServerComponent? serverProjectile)) filter = filter.RemovePlayer(serverProjectile.Shooter); if (modifiedDamage is not null && (EntityManager.EntityExists(component.Shooter) || EntityManager.EntityExists(component.Weapon))) { if (modifiedDamage.AnyPositive() && !deleted) { _color.RaiseEffect(Color.Red, new List { target }, filter); } var shooterOrWeapon = EntityManager.EntityExists(component.Shooter) ? component.Shooter!.Value : component.Weapon!.Value; _adminLogger.Add(LogType.BulletHit, HasComp(target) ? LogImpact.Extreme : LogImpact.High, $"Projectile {ToPrettyString(uid):projectile} shot by {ToPrettyString(shooterOrWeapon):source} hit {otherName:target} and dealt {modifiedDamage.GetTotal():damage} damage"); } if (!deleted) { _guns.PlayImpactSound(target, modifiedDamage, component.SoundHit, component.ForceSound, filter, projectile); //_sharedCameraRecoil.KickCamera(target, direction); # this makes people blind or something } component.DamagedEntity = true; Dirty(uid, component); if (!predicted && component.DeleteOnCollide && (_netManager.IsServer || IsClientSide(uid))) QueueDel(uid); else if (_netManager.IsServer && component.DeleteOnCollide) { var predictedComp = EnsureComp(uid); predictedComp.Origin = _transform.GetMoverCoordinates(coordinates); var targetCoords = _transform.GetMoverCoordinates(target); if (predictedComp.Origin.TryDistance(EntityManager, _transform, targetCoords, out var distance)) predictedComp.Distance = distance; Dirty(uid, predictedComp); } if ((_netManager.IsServer || IsClientSide(uid)) && component.ImpactEffect != null) { var impactEffectEv = new ImpactEffectEvent(component.ImpactEffect, GetNetCoordinates(coordinates)); if (_netManager.IsServer) RaiseNetworkEvent(impactEffectEv, filter); else RaiseLocalEvent(impactEffectEv); } } private void OnEmbedActivate(EntityUid uid, EmbeddableProjectileComponent component, ActivateInWorldEvent args) { // Nuh uh if (component.RemovalTime == null) return; if (args.Handled || !args.Complex || !TryComp(uid, out var physics) || physics.BodyType != BodyType.Static) return; args.Handled = true; _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, component.RemovalTime.Value, new RemoveEmbeddedProjectileEvent(), eventTarget: uid, target: uid)); } private void OnEmbedRemove(EntityUid uid, EmbeddableProjectileComponent component, RemoveEmbeddedProjectileEvent args) { // Whacky prediction issues. if (args.Cancelled || _netManager.IsClient) return; if (component.DeleteOnRemove) { QueueDel(uid); return; } var xform = Transform(uid); TryComp(uid, out var physics); _physics.SetBodyType(uid, BodyType.Dynamic, body: physics, xform: xform); _transform.AttachToGridOrMap(uid, xform); // Reset whether the projectile has damaged anything if it successfully was removed if (TryComp(uid, out var projectile)) { projectile.Shooter = null; projectile.Weapon = null; projectile.DamagedEntity = false; } // Land it just coz uhhh yeah var landEv = new LandEvent(args.User, true); RaiseLocalEvent(uid, ref landEv); _physics.WakeBody(uid, body: physics); // try place it in the user's hand _hands.TryPickupAnyHand(args.User, uid); } private void OnEmbedThrowDoHit(EntityUid uid, EmbeddableProjectileComponent component, ThrowDoHitEvent args) { if (!component.EmbedOnThrow) return; Embed(uid, args.Target, null, component); } private void OnEmbedProjectileHit(EntityUid uid, EmbeddableProjectileComponent component, ref ProjectileHitEvent args) { Embed(uid, args.Target, args.Shooter, component); // Raise a specific event for projectiles. if (TryComp(uid, out ProjectileComponent? projectile)) { var ev = new ProjectileEmbedEvent(projectile.Shooter!.Value, projectile.Weapon!.Value, args.Target); RaiseLocalEvent(uid, ref ev); } } private void Embed(EntityUid uid, EntityUid target, EntityUid? user, EmbeddableProjectileComponent component) { TryComp(uid, out var physics); _physics.SetLinearVelocity(uid, Vector2.Zero, body: physics); _physics.SetBodyType(uid, BodyType.Static, body: physics); var xform = Transform(uid); _transform.SetParent(uid, xform, target); if (component.Offset != Vector2.Zero) { var rotation = xform.LocalRotation; if (TryComp(uid, out var throwingAngleComp)) rotation += throwingAngleComp.Angle; _transform.SetLocalPosition(uid, xform.LocalPosition + rotation.RotateVec(component.Offset), xform); } _audio.PlayPredicted(component.Sound, uid, null); var ev = new EmbedEvent(user, target); RaiseLocalEvent(uid, ref ev); } private void PreventCollision(EntityUid uid, ProjectileComponent component, ref PreventCollideEvent args) { if (component.IgnoreShooter && (args.OtherEntity == component.Shooter || args.OtherEntity == component.Weapon)) { args.Cancelled = true; } //check for barricade component (percentage of chance to hit/pass over) if (TryComp(args.OtherEntity, out BarricadeComponent? barricade)) { var alwaysPassThrough = false; //_sawmill.Info("Checking barricade..."); if (component.Shooter is { } shooterUid && Exists(shooterUid)) { // Condition 1: Directions are the same (using cardinal directions). // Or, if bidirectional, directions can be opposite. var shooterWorldRotation = _transform.GetWorldRotation(shooterUid); var barricadeWorldRotation = _transform.GetWorldRotation(args.OtherEntity); var shooterDir = shooterWorldRotation.GetCardinalDir(); var barricadeDir = barricadeWorldRotation.GetCardinalDir(); bool directionallyAllowed = false; if (shooterDir == barricadeDir) { directionallyAllowed = true; //_sawmill.Debug("Shooter and barricade facing same cardinal direction."); } else if (barricade.Bidirectional) { var oppositeBarricadeDir = (Direction)(((int)barricadeDir + 4) % 8); if (shooterDir == oppositeBarricadeDir) { directionallyAllowed = true; //_sawmill.Debug("Shooter and barricade facing opposite cardinal directions (bidirectional pass)."); } } if (directionallyAllowed) { // Condition 2: Firer is within 1 tile of the barricade. var shooterCoords = Transform(shooterUid).Coordinates; var barricadeCoords = Transform(args.OtherEntity).Coordinates; if (shooterCoords.TryDistance(EntityManager, barricadeCoords, out var distance) && distance <= 1.5f) { alwaysPassThrough = true; } } } if (alwaysPassThrough) { args.Cancelled = true; } else { //_sawmill.Debug("Barricade direction/distance check failed or shooter not valid."); // Standard barricade blocking logic if the special conditions are not met. var rando = _random.NextFloat(0.0f, 100.0f); if (rando >= barricade.Blocking) { args.Cancelled = true; } else { return; } } } } public void SetShooter(EntityUid id, ProjectileComponent component, EntityUid? shooterId = null) { if (component.Shooter == shooterId || shooterId == null) return; component.Shooter = shooterId; Dirty(id, component); } [Serializable, NetSerializable] private sealed partial class RemoveEmbeddedProjectileEvent : DoAfterEvent { public override DoAfterEvent Clone() => this; } } [Serializable, NetSerializable] public sealed class ImpactEffectEvent : EntityEventArgs { public string Prototype; public NetCoordinates Coordinates; public ImpactEffectEvent(string prototype, NetCoordinates coordinates) { Prototype = prototype; Coordinates = coordinates; } } /// /// Raised when an entity is just about to be hit with a projectile but can reflect it /// [ByRefEvent] public record struct ProjectileReflectAttemptEvent(EntityUid ProjUid, ProjectileComponent Component, bool Cancelled); /// /// Raised when a projectile hits an entity /// [ByRefEvent] public record struct ProjectileHitEvent(DamageSpecifier Damage, EntityUid Target, EntityUid? Shooter = null, bool Handled = false);