diff --git a/Content.Client/Weapons/Ranged/TetherGunCommand.cs b/Content.Client/Weapons/Ranged/TetherGunCommand.cs new file mode 100644 index 0000000000..7c030304d5 --- /dev/null +++ b/Content.Client/Weapons/Ranged/TetherGunCommand.cs @@ -0,0 +1,14 @@ +using Robust.Shared.Console; + +namespace Content.Client.Weapons.Ranged; + +public sealed class TetherGunCommand : IConsoleCommand +{ + public string Command => "tethergun"; + public string Description => "Allows you to drag mobs around with your mouse."; + public string Help => $"{Command}"; + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + IoCManager.Resolve().GetEntitySystem().Enabled ^= true; + } +} diff --git a/Content.Client/Weapons/Ranged/TetherGunSystem.cs b/Content.Client/Weapons/Ranged/TetherGunSystem.cs new file mode 100644 index 0000000000..0736ecec0b --- /dev/null +++ b/Content.Client/Weapons/Ranged/TetherGunSystem.cs @@ -0,0 +1,107 @@ +using Content.Client.Clickable; +using Content.Shared.Weapons.Ranged; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.Input; +using Robust.Shared.Input; +using Robust.Shared.Map; +using Robust.Shared.Timing; + +namespace Content.Client.Weapons.Ranged; + +public sealed class TetherGunSystem : SharedTetherGunSystem +{ + [Dependency] private readonly IEyeManager _eyeManager = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly IInputManager _inputManager = default!; + [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly InputSystem _inputSystem = default!; + + public bool Enabled { get; set; } + + /// + /// The entity being dragged around. + /// + private EntityUid? _dragging; + + private MapCoordinates? _lastMousePosition; + + public override void Update(float frameTime) + { + base.Update(frameTime); + + if (!Enabled || !_gameTiming.IsFirstTimePredicted) return; + + var state = _inputSystem.CmdStates.GetState(EngineKeyFunctions.Use); + + if (state != BoundKeyState.Down) + { + StopDragging(); + return; + } + + var mouseScreenPos = _inputManager.MouseScreenPosition; + var mousePos = _eyeManager.ScreenToMap(mouseScreenPos); + + if (_dragging == null) + { + var bodyQuery = GetEntityQuery(); + var lowest = new List<(int DrawDepth, EntityUid Entity)>(); + + foreach (var ent in _lookup.GetEntitiesIntersecting(mousePos)) + { + if (!bodyQuery.HasComponent(ent) || + !TryComp(ent, out var clickable) || + !clickable.CheckClick(mousePos.Position, out var drawDepth, out _)) continue; + + lowest.Add((drawDepth, ent)); + } + + lowest.Sort((x, y) => y.DrawDepth.CompareTo(x.DrawDepth)); + + foreach (var ent in lowest) + { + StartDragging(ent.Entity, mousePos); + break; + } + + if (_dragging == null) return; + } + + if (!TryComp(_dragging!.Value, out var xform) || + _lastMousePosition!.Value.MapId != xform.MapID) + { + StopDragging(); + return; + } + + if (_lastMousePosition.Value.Position.EqualsApprox(mousePos.Position)) return; + + _lastMousePosition = mousePos; + + RaiseNetworkEvent(new TetherMoveEvent() + { + Coordinates = _lastMousePosition!.Value, + }); + } + + private void StopDragging() + { + if (_dragging == null) return; + + RaiseNetworkEvent(new StopTetherEvent()); + _dragging = null; + _lastMousePosition = null; + } + + private void StartDragging(EntityUid uid, MapCoordinates coordinates) + { + _dragging = uid; + _lastMousePosition = coordinates; + RaiseNetworkEvent(new StartTetherEvent() + { + Entity = _dragging!.Value, + Coordinates = coordinates, + }); + } +} diff --git a/Content.Server/Weapon/Ranged/TetherGunSystem.cs b/Content.Server/Weapon/Ranged/TetherGunSystem.cs new file mode 100644 index 0000000000..d93d132ced --- /dev/null +++ b/Content.Server/Weapon/Ranged/TetherGunSystem.cs @@ -0,0 +1,153 @@ +using Content.Shared.Administration; +using Content.Shared.Weapons.Ranged; +using Robust.Server.Console; +using Robust.Server.Player; +using Robust.Shared.Containers; +using Robust.Shared.Map; +using Robust.Shared.Physics; +using Robust.Shared.Physics.Dynamics.Joints; +using Robust.Shared.Players; +using Robust.Shared.Timing; +using Robust.Shared.Utility; + +namespace Content.Server.Weapon.Ranged; + +public sealed class TetherGunSystem : SharedTetherGunSystem +{ + [Dependency] private readonly IConGroupController _admin = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; + [Dependency] private readonly SharedJointSystem _joints = default!; + + private Dictionary _tethered = new(); + + private const string JointId = "tether-joint"; + + public override void Initialize() + { + base.Initialize(); + SubscribeNetworkEvent(OnStartTether); + SubscribeNetworkEvent(OnStopTether); + SubscribeNetworkEvent(OnMoveTether); + + _playerManager.PlayerStatusChanged += OnStatusChange; + } + + private void OnStatusChange(object? sender, SessionStatusEventArgs e) + { + StopTether(e.Session); + } + + public override void Shutdown() + { + base.Shutdown(); + + _playerManager.PlayerStatusChanged -= OnStatusChange; + } + + private void OnStartTether(StartTetherEvent msg, EntitySessionEventArgs args) + { + if (args.SenderSession is not IPlayerSession playerSession || + !_admin.CanCommand(playerSession, CommandName) || + !Exists(msg.Entity) || + Deleted(msg.Entity) || + msg.Coordinates == MapCoordinates.Nullspace || + _tethered.ContainsKey(args.SenderSession) || + _container.IsEntityInContainer(msg.Entity)) return; + + var tether = Spawn("TetherEntity", msg.Coordinates); + + if (!TryComp(tether, out var bodyA) || + !TryComp(msg.Entity, out var bodyB)) + { + Del(tether); + return; + } + + EnsureComp(msg.Entity); + + if (TryComp(msg.Entity, out var xform)) + { + xform.Anchored = false; + } + + if (TryComp(msg.Entity, out var body)) + { + body.BodyStatus = BodyStatus.InAir; + } + + var joint = _joints.CreateMouseJoint(bodyA.Owner, bodyB.Owner, id: JointId); + + SharedJointSystem.LinearStiffness(5f, 0.7f, bodyA.Mass, bodyB.Mass, out var stiffness, out var damping); + joint.Stiffness = stiffness; + joint.Damping = damping; + joint.MaxForce = 5000f * bodyB.Mass; + + _tethered.Add(playerSession, (msg.Entity, tether, joint)); + } + + private void OnStopTether(StopTetherEvent msg, EntitySessionEventArgs args) + { + StopTether(args.SenderSession); + } + + private void StopTether(ICommonSession session) + { + if (!_tethered.TryGetValue(session, out var weh)) + return; + + RemComp(weh.Entity); + + if (TryComp(weh.Entity, out var body)) + { + Timer.Spawn(1000, () => + { + if (Deleted(body.Owner)) return; + body.BodyStatus = BodyStatus.OnGround; + }); + } + + _joints.RemoveJoint(weh.Joint); + Del(weh.Tether); + _tethered.Remove(session); + } + + private void OnMoveTether(TetherMoveEvent msg, EntitySessionEventArgs args) + { + if (!_tethered.TryGetValue(args.SenderSession, out var tether) || + !TryComp(tether.Tether, out var xform) || + xform.MapID != msg.Coordinates.MapId) return; + + xform.WorldPosition = msg.Coordinates.Position; + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var toRemove = new RemQueue(); + var bodyQuery = GetEntityQuery(); + + foreach (var (session, entity) in _tethered) + { + if (Deleted(entity.Entity) || + Deleted(entity.Tether) || + !entity.Joint.Enabled) + { + toRemove.Add(session); + continue; + } + + // Force it awake, always + if (bodyQuery.TryGetComponent(entity.Entity, out var body)) + { + body.WakeBody(); + } + } + + foreach (var session in toRemove) + { + StopTether(session); + } + } +} diff --git a/Content.Shared/Weapons/Ranged/SharedTetherGunSystem.cs b/Content.Shared/Weapons/Ranged/SharedTetherGunSystem.cs new file mode 100644 index 0000000000..c871298c6a --- /dev/null +++ b/Content.Shared/Weapons/Ranged/SharedTetherGunSystem.cs @@ -0,0 +1,25 @@ +using Robust.Shared.Map; +using Robust.Shared.Serialization; + +namespace Content.Shared.Weapons.Ranged; + +public abstract class SharedTetherGunSystem : EntitySystem +{ + public const string CommandName = "tethergun"; +} + +[Serializable, NetSerializable] +public sealed class StartTetherEvent : EntityEventArgs +{ + public EntityUid Entity; + public MapCoordinates Coordinates; +} + +[Serializable, NetSerializable] +public sealed class StopTetherEvent : EntityEventArgs {} + +[Serializable, NetSerializable] +public sealed class TetherMoveEvent : EntityEventArgs +{ + public MapCoordinates Coordinates; +} diff --git a/Resources/Prototypes/Entities/Virtual/tether.yml b/Resources/Prototypes/Entities/Virtual/tether.yml new file mode 100644 index 0000000000..31695f220e --- /dev/null +++ b/Resources/Prototypes/Entities/Virtual/tether.yml @@ -0,0 +1,6 @@ +- type: entity + id: TetherEntity + noSpawn: true + components: + - type: Physics + - type: Fixtures diff --git a/Resources/clientCommandPerms.yml b/Resources/clientCommandPerms.yml index 9f7ff9b6bd..c3ef8280d8 100644 --- a/Resources/clientCommandPerms.yml +++ b/Resources/clientCommandPerms.yml @@ -17,6 +17,7 @@ - Flags: MAPPING Commands: + - tethergun - griddrag - showmarkers - showsubfloor