using System.Numerics; using Content.Shared.CombatMode.Pacification; using Content.Shared.Damage; using Content.Shared.DoAfter; using Content.Shared.Hands.EntitySystems; using Content.Shared.Interaction; using Content.Shared.Mobs.Components; using Content.Shared.Throwing; 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.Dynamics; using Robust.Shared.Physics.Events; using Robust.Shared.Physics.Systems; using Robust.Shared.Serialization; 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!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(PreventCollision); SubscribeLocalEvent(OnEmbedProjectileHit); SubscribeLocalEvent(OnEmbedThrowDoHit); SubscribeLocalEvent(OnEmbedActivate); SubscribeLocalEvent(OnEmbedRemove); SubscribeLocalEvent(OnAttemptPacifiedThrow); } private void OnEmbedActivate(EntityUid uid, EmbeddableProjectileComponent component, ActivateInWorldEvent args) { // Nuh uh if (component.RemovalTime == null) return; if (args.Handled || !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) { _transform.SetLocalPosition(uid, xform.LocalPosition + xform.LocalRotation.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; } } public void SetShooter(EntityUid id, ProjectileComponent component, EntityUid shooterId) { if (component.Shooter == shooterId) return; component.Shooter = shooterId; Dirty(id, component); } [Serializable, NetSerializable] private sealed partial class RemoveEmbeddedProjectileEvent : DoAfterEvent { public override DoAfterEvent Clone() => this; } /// /// Prevent players with the Pacified status effect from throwing embeddable projectiles. /// private void OnAttemptPacifiedThrow(Entity ent, ref AttemptPacifiedThrowEvent args) { args.Cancel("pacified-cannot-throw-embed"); } } [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);