using System.Linq; using System.Numerics; using Content.Shared._Shitmed.Targeting; using Content.Shared._White.Penetrated; using Content.Shared._White.Projectile; using Content.Shared.Damage; using Content.Shared.DoAfter; using Content.Shared.Examine; using Content.Shared.Hands.EntitySystems; using Content.Shared.IdentityManagement; using Content.Shared.Interaction; using Content.Shared.Item.ItemToggle; using Content.Shared.Physics; using Content.Shared.Popups; using Content.Shared.Throwing; using Content.Shared.UserInterface; 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.Serialization; using Robust.Shared.Timing; using Robust.Shared.Utility; namespace Content.Shared.Projectiles; public abstract partial class SharedProjectileSystem : EntitySystem { public const string ProjectileFixture = "projectile"; [Dependency] private readonly INetManager _net = 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 IGameTiming _timing = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; // WD EDIT START [Dependency] private readonly PenetratedSystem _penetrated = default!; // WD EDIT END public override void Initialize() { base.Initialize(); SubscribeLocalEvent(PreventCollision); SubscribeLocalEvent(OnEmbedProjectileHit); SubscribeLocalEvent(OnEmbedThrowDoHit); SubscribeLocalEvent(OnEmbedActivate, before: new[] { typeof(ActivatableUISystem), typeof(ItemToggleSystem), }); SubscribeLocalEvent(OnEmbedRemove); SubscribeLocalEvent(OnExamined); SubscribeLocalEvent(OnPreventCollision); // WD EDIT SubscribeLocalEvent(OnEmbeddableTermination); } // TODO: rename Embedded to Target in every context public override void Update(float frameTime) { base.Update(frameTime); var query = EntityQueryEnumerator(); var curTime = _timing.CurTime; while (query.MoveNext(out var uid, out var _)) { if (!TryComp(uid, out EmbeddableProjectileComponent? comp)) { RemCompDeferred(uid); continue; } if (comp.AutoRemoveTime == null || comp.AutoRemoveTime > curTime) continue; if (comp.EmbeddedIntoUid is { } targetUid) _popup.PopupClient(Loc.GetString("throwing-embed-falloff", ("item", uid)), targetUid, targetUid); EmbedDetach(uid, comp); } } private void OnEmbedActivate(Entity embeddable, ref ActivateInWorldEvent args) { // Unremovable embeddables moment if (embeddable.Comp.RemovalTime == null) return; if (args.Handled || !args.Complex || !TryComp(embeddable, out var physics) || physics.BodyType != BodyType.Static) return; args.Handled = true; if (embeddable.Comp.EmbeddedIntoUid is { } targetUid) _popup.PopupClient(Loc.GetString("throwing-embed-remove-alert-owner", ("item", embeddable), ("other", args.User)), args.User, targetUid); _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, embeddable.Comp.RemovalTime.Value, new RemoveEmbeddedProjectileEvent(), eventTarget: embeddable, target: embeddable) { DistanceThreshold = SharedInteractionSystem.InteractionRange, BreakOnMove = true, NeedHand = true, }); } private void OnEmbedRemove(Entity embeddable, ref RemoveEmbeddedProjectileEvent args) { if (args.Cancelled) return; EmbedDetach(embeddable, embeddable.Comp, args.User); // try place it in the user's hand _hands.TryPickupAnyHand(args.User, embeddable); } private void OnEmbedThrowDoHit(Entity embeddable, ref ThrowDoHitEvent args) { if (!embeddable.Comp.EmbedOnThrow || HasComp(args.Target)) // I hate it. TODO: Use before event unstead HasComp. By Spatison return; EmbedAttach(embeddable, args.Target, null, embeddable.Comp); } private void OnEmbedProjectileHit(Entity embeddable, ref ProjectileHitEvent args) { EmbedAttach(embeddable, args.Target, args.Shooter, embeddable.Comp); // Raise a specific event for projectiles. if (TryComp(embeddable, out ProjectileComponent? projectile)) { var ev = new ProjectileEmbedEvent(projectile.Shooter, projectile.Weapon, args.Target); // WD EDIT RaiseLocalEvent(embeddable, ref ev); } } public void EmbedAttach(EntityUid uid, EntityUid target, EntityUid? user, EmbeddableProjectileComponent component, TargetBodyPart? targetPart = null) { // WD EDIT START if (!TryComp(uid, out var physics) || TerminatingOrDeleted(target) || physics.LinearVelocity.Length() < component.MinimumSpeed || _net.IsClient) return; var attemptEmbedEvent = new AttemptEmbedEvent(user, target); RaiseLocalEvent(uid, ref attemptEmbedEvent); var xform = Transform(uid); if (!TryComp(uid, out var penetratedProjectile) || !penetratedProjectile.PenetratedUid.HasValue || (penetratedProjectile.PenetratedUid != target && !HasComp(target))) { EnsureComp(uid); _physics.SetLinearVelocity(uid, Vector2.Zero, body: physics); _physics.SetBodyType(uid, BodyType.Static, body: physics); _transform.SetParent(uid, xform, target); } // WD EDIT END 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); component.TargetBodyPart = targetPart; component.EmbeddedIntoUid = target; var ev = new EmbedEvent(user, target, targetPart); RaiseLocalEvent(uid, ref ev); if (component.AutoRemoveDuration != 0) component.AutoRemoveTime = _timing.CurTime + TimeSpan.FromSeconds(component.AutoRemoveDuration); Dirty(uid, component); EnsureComp(target, out var embeddedContainer); //Assert that this entity not embed DebugTools.AssertEqual(embeddedContainer.EmbeddedObjects.Contains(uid), false); embeddedContainer.EmbeddedObjects.Add(uid); } public void EmbedDetach(EntityUid uid, EmbeddableProjectileComponent? component, EntityUid? remover = null) { if (!Resolve(uid, ref component)) return; if (component.DeleteOnRemove) { QueueDel(uid); FreePenetrated(uid); // WD EDIT return; } if (component.EmbeddedIntoUid is not null) { if (TryComp(component.EmbeddedIntoUid.Value, out var embeddedContainer)) { embeddedContainer.EmbeddedObjects.Remove(uid); } } component.AutoRemoveTime = null; component.EmbeddedIntoUid = null; component.TargetBodyPart = null; RemCompDeferred(uid); var ev = new RemoveEmbedEvent(remover); RaiseLocalEvent(uid, ref ev); if (!TryComp(uid, out PhysicsComponent? physics)) return; var xform = Transform(uid); _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.ProjectileSpent = false; } FreePenetrated(uid); // WD EDIT // Land it just coz uhhh yeah var landEv = new LandEvent(remover, true); RaiseLocalEvent(uid, ref landEv); _physics.WakeBody(uid, body: physics); // try place it in the user's hand if (remover is { } removerUid) _hands.TryPickupAnyHand(removerUid, uid); Dirty(uid, component); } private void OnEmbeddableTermination(Entity container, ref EntityTerminatingEvent args) { DetachAllEmbedded(container); } public void DetachAllEmbedded(Entity container) { foreach (var embedded in container.Comp.EmbeddedObjects) { if (!TryComp(embedded, out var embeddedComp)) continue; EmbedDetach(embedded, embeddedComp); } } 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); } private void OnExamined(EntityUid uid, EmbeddableProjectileComponent component, ExaminedEvent args) { if (!(component.EmbeddedIntoUid is { } target)) return; var targetIdentity = Identity.Entity(target, EntityManager); var loc = component.TargetBodyPart == null ? Loc.GetString("throwing-examine-embedded", ("embedded", uid), ("target", targetIdentity)) : Loc.GetString("throwing-examine-embedded-part", ("embedded", uid), ("target", targetIdentity), ("targetName", Name(targetIdentity)), // WWDP ("targetPart", Loc.GetString($"body-part-{component.TargetBodyPart.ToString()}"))); args.PushMarkup(loc); } // WD EDIT START private void OnPreventCollision(EntityUid uid, EmbeddableProjectileComponent component, ref PreventCollideEvent args) { // Opaque collision mask doesn't work for EmbeddableProjectileComponent if (component.PreventCollide && TryComp(args.OtherEntity, out FixturesComponent? fixtures) && fixtures.Fixtures.All(fix => (fix.Value.CollisionLayer & (int) CollisionGroup.Opaque) == 0)) args.Cancelled = true; } private void FreePenetrated(EntityUid uid, PenetratedProjectileComponent? penetratedProjectile = null) { if (!Resolve(uid, ref penetratedProjectile, false) || !penetratedProjectile.PenetratedUid.HasValue) return; _penetrated.FreePenetrated(penetratedProjectile.PenetratedUid.Value); } // WD EDIT END [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); /// /// Raised after a projectile has dealt it's damage. /// [ByRefEvent] public record struct AfterProjectileHitEvent(DamageSpecifier Damage, EntityUid Target);