using System.Numerics; using Content.Shared.CombatMode; using Content.Shared.Hands; using Content.Shared.Hands.EntitySystems; using Content.Shared.Interaction; using Content.Shared.Movement.Events; using Content.Shared.Physics; using Content.Shared.Projectiles; using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Weapons.Ranged.Systems; using Robust.Shared.Audio.Systems; using Robust.Shared.Network; using Robust.Shared.Physics; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Dynamics.Joints; using Robust.Shared.Physics.Systems; using Robust.Shared.Serialization; using Robust.Shared.Timing; namespace Content.Shared.Weapons.Misc; public abstract class SharedGrapplingGunSystem : EntitySystem { [Dependency] protected readonly IGameTiming Timing = default!; [Dependency] private readonly INetManager _netManager = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedHandsSystem _hands = default!; [Dependency] private readonly SharedJointSystem _joints = default!; [Dependency] private readonly SharedGunSystem _gun = default!; [Dependency] private readonly SharedPhysicsSystem _physics = default!; public const string GrapplingJoint = "grappling"; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnGrappleCollide); SubscribeLocalEvent(OnGrappleJointRemoved); SubscribeLocalEvent(OnWeightlessMove); SubscribeAllEvent(OnGrapplingReel); // TODO: After step trigger refactor, dropping a grappling gun should manually try and activate step triggers it's suppressing. SubscribeLocalEvent(OnGrapplingShot); SubscribeLocalEvent(OnGunActivate); SubscribeLocalEvent(OnGrapplingDeselected); } private void OnGrappleJointRemoved(EntityUid uid, GrapplingProjectileComponent component, JointRemovedEvent args) { if (_netManager.IsServer) QueueDel(uid); } private void OnGrapplingShot(EntityUid uid, GrapplingGunComponent component, ref GunShotEvent args) { foreach (var (shotUid, _) in args.Ammo) { if (!HasComp(shotUid)) continue; //todo: this doesn't actually support multigrapple // At least show the visuals. component.Projectile = shotUid.Value; Dirty(uid, component); var visuals = EnsureComp(shotUid.Value); visuals.Sprite = component.RopeSprite; visuals.OffsetA = new Vector2(0f, 0.5f); visuals.Target = uid; Dirty(shotUid.Value, visuals); } TryComp(uid, out var appearance); _appearance.SetData(uid, SharedTetherGunSystem.TetherVisualsStatus.Key, false, appearance); Dirty(uid, component); } private void OnGrapplingDeselected(EntityUid uid, GrapplingGunComponent component, HandDeselectedEvent args) { SetReeling(uid, component, false, args.User); } private void OnGrapplingReel(RequestGrapplingReelMessage msg, EntitySessionEventArgs args) { if (args.SenderSession.AttachedEntity is not { } player) return; if (!_hands.TryGetActiveItem(player, out var activeItem) || !TryComp(activeItem, out var grappling)) { return; } if (msg.Reeling && (!TryComp(player, out var combatMode) || !combatMode.IsInCombatMode)) { return; } SetReeling(activeItem.Value, grappling, msg.Reeling, player); } private void OnWeightlessMove(ref CanWeightlessMoveEvent ev) { if (ev.CanMove || !TryComp(ev.Uid, out var relayComp)) return; foreach (var relay in relayComp.Relayed) { if (TryComp(relay, out var jointRelay) && jointRelay.GetJoints.ContainsKey(GrapplingJoint)) { ev.CanMove = true; return; } } } private void OnGunActivate(EntityUid uid, GrapplingGunComponent component, ActivateInWorldEvent args) { if (!Timing.IsFirstTimePredicted || args.Handled || !args.Complex || component.Projectile is not { } projectile) return; _audio.PlayPredicted(component.CycleSound, uid, args.User); _appearance.SetData(uid, SharedTetherGunSystem.TetherVisualsStatus.Key, true); if (_netManager.IsServer) QueueDel(projectile); component.Projectile = null; SetReeling(uid, component, false, args.User); _gun.ChangeBasicEntityAmmoCount(uid, 1); args.Handled = true; } private void SetReeling(EntityUid uid, GrapplingGunComponent component, bool value, EntityUid? user) { if (component.Reeling == value) return; if (value) { if (Timing.IsFirstTimePredicted) component.Stream = _audio.PlayPredicted(component.ReelSound, uid, user)?.Entity; } else { if (Timing.IsFirstTimePredicted) { component.Stream = _audio.Stop(component.Stream); } } component.Reeling = value; Dirty(uid, component); } public override void Update(float frameTime) { base.Update(frameTime); var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var grappling)) { if (!grappling.Reeling) { if (Timing.IsFirstTimePredicted) { // Just in case. grappling.Stream = _audio.Stop(grappling.Stream); } continue; } if (!TryComp(uid, out var jointComp) || !jointComp.GetJoints.TryGetValue(GrapplingJoint, out var joint) || joint is not DistanceJoint distance) { SetReeling(uid, grappling, false, null); continue; } // TODO: This should be on engine. distance.MaxLength = MathF.Max(distance.MinLength, distance.MaxLength - grappling.ReelRate * frameTime); distance.Length = MathF.Min(distance.MaxLength, distance.Length); _physics.WakeBody(joint.BodyAUid); _physics.WakeBody(joint.BodyBUid); if (jointComp.Relay != null) { _physics.WakeBody(jointComp.Relay.Value); } Dirty(uid, jointComp); if (distance.MaxLength.Equals(distance.MinLength)) { SetReeling(uid, grappling, false, null); } } } /// /// Checks whether the entity is hooked to something via grappling gun. /// /// Entity to check. /// True if hooked, false otherwise. public bool IsEntityHooked(Entity entity) { if (!Resolve(entity, ref entity.Comp, false)) return false; foreach (var uid in entity.Comp.Relayed) { if (HasComp(uid)) return true; } return false; } private void OnGrappleCollide(EntityUid uid, GrapplingProjectileComponent component, ref ProjectileEmbedEvent args) { if (!Timing.IsFirstTimePredicted) return; var jointComp = EnsureComp(uid); var joint = _joints.CreateDistanceJoint(uid, args.Weapon, anchorA: new Vector2(0f, 0.5f), id: GrapplingJoint); joint.MaxLength = joint.Length + 0.2f; joint.Stiffness = 1f; joint.MinLength = 0.35f; // Setting velocity directly for mob movement fucks this so need to make them aware of it. // joint.Breakpoint = 4000f; Dirty(uid, jointComp); } [Serializable, NetSerializable] protected sealed class RequestGrapplingReelMessage : EntityEventArgs { public bool Reeling; public RequestGrapplingReelMessage(bool reeling) { Reeling = reeling; } } }