]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Add tether gun (#16430)
authormetalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Thu, 18 May 2023 01:36:06 +0000 (11:36 +1000)
committerGitHub <noreply@github.com>
Thu, 18 May 2023 01:36:06 +0000 (11:36 +1000)
28 files changed:
Content.Client/Weapons/Misc/TetherGunOverlay.cs [new file with mode: 0644]
Content.Client/Weapons/Misc/TetherGunSystem.cs [new file with mode: 0644]
Content.Client/Weapons/Ranged/Systems/TetherGunSystem.cs [deleted file]
Content.Server/PowerCell/PowerCellSystem.cs
Content.Server/Weapons/Misc/TetherGunSystem.cs [new file with mode: 0644]
Content.Server/Weapons/Ranged/Systems/TetherGunSystem.cs [deleted file]
Content.Server/Weapons/TetherGunCommand.cs [deleted file]
Content.Shared/Buckle/Components/BuckleComponent.cs
Content.Shared/Weapons/Misc/SharedTetherGunSystem.cs [new file with mode: 0644]
Content.Shared/Weapons/Misc/TetherGunComponent.cs [new file with mode: 0644]
Content.Shared/Weapons/Misc/TetheredComponent.cs [new file with mode: 0644]
Content.Shared/Weapons/Ranged/Systems/SharedTetherGunSystem.cs [deleted file]
Resources/Audio/Weapons/attributions.yml
Resources/Audio/Weapons/licenses.txt
Resources/Audio/Weapons/weoweo.ogg [new file with mode: 0644]
Resources/Locale/en-US/research/technologies.ftl
Resources/Prototypes/Entities/Objects/Fun/pai.yml
Resources/Prototypes/Entities/Objects/Weapons/Guns/Launchers/launchers.yml
Resources/Prototypes/Entities/Objects/base_item.yml
Resources/Prototypes/Entities/Virtual/tether.yml
Resources/Prototypes/Research/experimental.yml
Resources/Textures/Objects/Weapons/Guns/Launchers/tether_gun.rsi/base-unshaded.png [new file with mode: 0644]
Resources/Textures/Objects/Weapons/Guns/Launchers/tether_gun.rsi/base.png [new file with mode: 0644]
Resources/Textures/Objects/Weapons/Guns/Launchers/tether_gun.rsi/inhand-left-unshaded.png [new file with mode: 0644]
Resources/Textures/Objects/Weapons/Guns/Launchers/tether_gun.rsi/inhand-left.png [new file with mode: 0644]
Resources/Textures/Objects/Weapons/Guns/Launchers/tether_gun.rsi/inhand-right-unshaded.png [new file with mode: 0644]
Resources/Textures/Objects/Weapons/Guns/Launchers/tether_gun.rsi/inhand-right.png [new file with mode: 0644]
Resources/Textures/Objects/Weapons/Guns/Launchers/tether_gun.rsi/meta.json [new file with mode: 0644]

diff --git a/Content.Client/Weapons/Misc/TetherGunOverlay.cs b/Content.Client/Weapons/Misc/TetherGunOverlay.cs
new file mode 100644 (file)
index 0000000..215589c
--- /dev/null
@@ -0,0 +1,52 @@
+using Content.Shared.Weapons.Misc;
+using Robust.Client.Graphics;
+using Robust.Shared.Enums;
+
+namespace Content.Client.Weapons.Misc;
+
+public sealed class TetherGunOverlay : Overlay
+{
+    public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV;
+
+    private IEntityManager _entManager;
+
+    public TetherGunOverlay(IEntityManager entManager)
+    {
+        _entManager = entManager;
+    }
+
+    protected override void Draw(in OverlayDrawArgs args)
+    {
+        var query = _entManager.EntityQueryEnumerator<TetheredComponent>();
+        var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
+        var worldHandle = args.WorldHandle;
+        var xformSystem = _entManager.System<SharedTransformSystem>();
+
+        while (query.MoveNext(out var uid, out var tethered))
+        {
+            var gun = tethered.Tetherer;
+
+            if (!xformQuery.TryGetComponent(gun, out var gunXform) ||
+                !xformQuery.TryGetComponent(uid, out var xform))
+            {
+                continue;
+            }
+
+            if (xform.MapID != gunXform.MapID)
+                continue;
+
+            var worldPos = xformSystem.GetWorldPosition(xform, xformQuery);
+            var gunWorldPos = xformSystem.GetWorldPosition(gunXform, xformQuery);
+            var diff = worldPos - gunWorldPos;
+            var angle = diff.ToWorldAngle();
+            var length = diff.Length / 2f;
+            var midPoint = gunWorldPos + diff / 2;
+            const float Width = 0.05f;
+
+            var box = new Box2(-Width, -length, Width, length);
+            var rotated = new Box2Rotated(box.Translated(midPoint), angle, midPoint);
+
+            worldHandle.DrawRect(rotated, Color.Orange.WithAlpha(0.3f));
+        }
+    }
+}
diff --git a/Content.Client/Weapons/Misc/TetherGunSystem.cs b/Content.Client/Weapons/Misc/TetherGunSystem.cs
new file mode 100644 (file)
index 0000000..1219fe1
--- /dev/null
@@ -0,0 +1,103 @@
+using Content.Shared.Weapons.Misc;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.Input;
+using Robust.Client.Player;
+using Robust.Shared.Map;
+using Robust.Shared.Timing;
+
+namespace Content.Client.Weapons.Misc;
+
+public sealed class TetherGunSystem : SharedTetherGunSystem
+{
+    [Dependency] private readonly IEyeManager _eyeManager = default!;
+    [Dependency] private readonly IGameTiming _timing = default!;
+    [Dependency] private readonly IInputManager _input = default!;
+    [Dependency] private readonly IMapManager _mapManager = default!;
+    [Dependency] private readonly IOverlayManager _overlay = default!;
+    [Dependency] private readonly IPlayerManager _player = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+        SubscribeLocalEvent<TetheredComponent, ComponentStartup>(OnTetheredStartup);
+        SubscribeLocalEvent<TetheredComponent, ComponentShutdown>(OnTetheredShutdown);
+        _overlay.AddOverlay(new TetherGunOverlay(EntityManager));
+    }
+
+    public override void Shutdown()
+    {
+        base.Shutdown();
+        _overlay.RemoveOverlay<TetherGunOverlay>();
+    }
+
+    protected override bool CanTether(EntityUid uid, TetherGunComponent component, EntityUid target, EntityUid? user)
+    {
+        // Need powercells predicted sadly :<
+        return false;
+    }
+
+    public override void Update(float frameTime)
+    {
+        base.Update(frameTime);
+
+        if (!_timing.IsFirstTimePredicted)
+            return;
+
+        var player = _player.LocalPlayer?.ControlledEntity;
+
+        if (player == null ||
+            !TryGetTetherGun(player.Value, out var gunUid, out var gun) ||
+            gun.TetherEntity == null)
+        {
+            return;
+        }
+
+        var mousePos = _input.MouseScreenPosition;
+        var mouseWorldPos = _eyeManager.ScreenToMap(mousePos);
+
+        if (mouseWorldPos.MapId == MapId.Nullspace)
+            return;
+
+        EntityCoordinates coords;
+
+        if (_mapManager.TryFindGridAt(mouseWorldPos, out var grid))
+        {
+            coords = EntityCoordinates.FromMap(grid.Owner, mouseWorldPos, TransformSystem);
+        }
+        else
+        {
+            coords = EntityCoordinates.FromMap(_mapManager.GetMapEntityId(mouseWorldPos.MapId), mouseWorldPos, TransformSystem);
+        }
+
+        const float BufferDistance = 0.1f;
+
+        if (TryComp<TransformComponent>(gun.TetherEntity, out var tetherXform) &&
+            tetherXform.Coordinates.TryDistance(EntityManager, TransformSystem, coords, out var distance) &&
+            distance < BufferDistance)
+        {
+            return;
+        }
+
+        RaisePredictiveEvent(new RequestTetherMoveEvent()
+        {
+            Coordinates = coords
+        });
+    }
+
+    private void OnTetheredStartup(EntityUid uid, TetheredComponent component, ComponentStartup args)
+    {
+        if (!TryComp<SpriteComponent>(uid, out var sprite))
+            return;
+
+        sprite.Color = Color.Orange;
+    }
+
+    private void OnTetheredShutdown(EntityUid uid, TetheredComponent component, ComponentShutdown args)
+    {
+        if (!TryComp<SpriteComponent>(uid, out var sprite))
+            return;
+
+        sprite.Color = Color.White;
+    }
+}
diff --git a/Content.Client/Weapons/Ranged/Systems/TetherGunSystem.cs b/Content.Client/Weapons/Ranged/Systems/TetherGunSystem.cs
deleted file mode 100644 (file)
index 3a66f07..0000000
+++ /dev/null
@@ -1,142 +0,0 @@
-using Content.Client.Gameplay;
-using Content.Shared.Weapons.Ranged.Systems;
-using Robust.Client.GameObjects;
-using Robust.Client.Graphics;
-using Robust.Client.Input;
-using Robust.Client.Physics;
-using Robust.Client.State;
-using Robust.Shared.Input;
-using Robust.Shared.Map;
-using Robust.Shared.Physics.Components;
-using Robust.Shared.Timing;
-
-namespace Content.Client.Weapons.Ranged.Systems;
-
-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 InputSystem _inputSystem = default!;
-    [Dependency] private readonly PhysicsSystem _physics = default!;
-
-    public bool Enabled { get; set; }
-
-    /// <summary>
-    /// The entity being dragged around.
-    /// </summary>
-    private EntityUid? _dragging;
-    private EntityUid? _tether;
-
-    private MapCoordinates? _lastMousePosition;
-
-    public override void Initialize()
-    {
-        base.Initialize();
-        SubscribeNetworkEvent<PredictTetherEvent>(OnPredictTether);
-        SubscribeNetworkEvent<TetherGunToggleMessage>(OnTetherGun);
-        SubscribeLocalEvent<UpdateIsPredictedEvent>(OnUpdatePrediction);
-    }
-
-    private void OnUpdatePrediction(ref UpdateIsPredictedEvent ev)
-    {
-        if (ev.Uid == _dragging || ev.Uid == _tether)
-            ev.IsPredicted = true;
-    }
-
-    private void OnTetherGun(TetherGunToggleMessage ev)
-    {
-        Enabled = ev.Enabled;
-    }
-
-    private void OnPredictTether(PredictTetherEvent ev)
-    {
-        if (_dragging != ev.Entity || _tether == ev.Entity)
-            return;
-
-        var oldTether = _tether;
-        _tether = ev.Entity;
-        _physics.UpdateIsPredicted(oldTether);
-        _physics.UpdateIsPredicted(_tether);
-    }
-
-    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 gameState = IoCManager.Resolve<IStateManager>().CurrentState;
-
-            if (gameState is GameplayState game)
-            {
-                var uid = game.GetClickedEntity(mousePos);
-
-                if (uid != null)
-                    StartDragging(uid.Value, mousePos);
-            }
-
-            if (_dragging == null)
-                return;
-        }
-
-        if (!TryComp<TransformComponent>(_dragging!.Value, out var xform) ||
-            _lastMousePosition!.Value.MapId != xform.MapID ||
-            !TryComp<PhysicsComponent>(_dragging, out var body))
-        {
-            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;
-
-        var oldDrag = _dragging;
-        var oldTether = _tether;
-        RaiseNetworkEvent(new StopTetherEvent());
-        _dragging = null;
-        _lastMousePosition = null;
-        _tether = null;
-
-        _physics.UpdateIsPredicted(oldDrag);
-        _physics.UpdateIsPredicted(oldTether);
-    }
-
-    private void StartDragging(EntityUid uid, MapCoordinates coordinates)
-    {
-        _dragging = uid;
-        _lastMousePosition = coordinates;
-        RaiseNetworkEvent(new StartTetherEvent()
-        {
-            Entity = _dragging!.Value,
-            Coordinates = coordinates,
-        });
-
-        _physics.UpdateIsPredicted(uid);
-
-    }
-}
index 7719773d9d4518ae7ce3fac2d444305004f7a4d2..2ba103a05bd792c3a040fec6f01b64f07c7bec2d 100644 (file)
@@ -157,6 +157,18 @@ public sealed partial class PowerCellSystem : SharedPowerCellSystem
         return false;
     }
 
+    /// <summary>
+    /// Whether the power cell has any power at all for the draw rate.
+    /// </summary>
+    public bool HasDrawCharge(EntityUid uid, PowerCellDrawComponent? battery = null,
+        PowerCellSlotComponent? cell = null, EntityUid? user = null)
+    {
+        if (!Resolve(uid, ref battery, ref cell, false))
+            return true;
+
+        return HasCharge(uid, float.MinValue, cell, user);
+    }
+
     #endregion
 
     public void SetPowerCellDrawEnabled(EntityUid uid, bool enabled, PowerCellDrawComponent? component = null)
diff --git a/Content.Server/Weapons/Misc/TetherGunSystem.cs b/Content.Server/Weapons/Misc/TetherGunSystem.cs
new file mode 100644 (file)
index 0000000..3b62e3f
--- /dev/null
@@ -0,0 +1,46 @@
+using Content.Server.PowerCell;
+using Content.Shared.PowerCell.Components;
+using Content.Shared.Weapons.Misc;
+using Robust.Shared.Physics.Components;
+
+namespace Content.Server.Weapons.Misc;
+
+public sealed class TetherGunSystem : SharedTetherGunSystem
+{
+    [Dependency] private readonly PowerCellSystem _cell = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+        SubscribeLocalEvent<TetherGunComponent, PowerCellSlotEmptyEvent>(OnGunEmpty);
+    }
+
+    private void OnGunEmpty(EntityUid uid, TetherGunComponent component, ref PowerCellSlotEmptyEvent args)
+    {
+        StopTether(uid, component);
+    }
+
+    protected override bool CanTether(EntityUid uid, TetherGunComponent component, EntityUid target, EntityUid? user)
+    {
+        if (!base.CanTether(uid, component, target, user))
+            return false;
+
+        if (!_cell.HasDrawCharge(uid, user: user))
+            return false;
+
+        return true;
+    }
+
+    protected override void StartTether(EntityUid gunUid, TetherGunComponent component, EntityUid target, EntityUid? user,
+        PhysicsComponent? targetPhysics = null, TransformComponent? targetXform = null)
+    {
+        base.StartTether(gunUid, component, target, user, targetPhysics, targetXform);
+        _cell.SetPowerCellDrawEnabled(gunUid, true);
+    }
+
+    protected override void StopTether(EntityUid gunUid, TetherGunComponent component, bool transfer = false)
+    {
+        base.StopTether(gunUid, component, transfer);
+        _cell.SetPowerCellDrawEnabled(gunUid, false);
+    }
+}
diff --git a/Content.Server/Weapons/Ranged/Systems/TetherGunSystem.cs b/Content.Server/Weapons/Ranged/Systems/TetherGunSystem.cs
deleted file mode 100644 (file)
index d81f571..0000000
+++ /dev/null
@@ -1,199 +0,0 @@
-using Content.Server.Ghost.Components;
-using Content.Shared.Administration;
-using Content.Shared.Weapons.Ranged.Systems;
-using Robust.Server.Console;
-using Robust.Server.Player;
-using Robust.Shared.Containers;
-using Robust.Shared.Map;
-using Robust.Shared.Physics;
-using Robust.Shared.Physics.Components;
-using Robust.Shared.Physics.Dynamics.Joints;
-using Robust.Shared.Physics.Systems;
-using Robust.Shared.Players;
-using Robust.Shared.Timing;
-using Robust.Shared.Utility;
-
-namespace Content.Server.Weapons.Ranged.Systems;
-
-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!;
-    [Dependency] private readonly SharedPhysicsSystem _physics = default!;
-
-    private readonly Dictionary<ICommonSession, (EntityUid Entity, EntityUid Tether, Joint Joint)> _tethered = new();
-    private readonly HashSet<ICommonSession> _draggers = new();
-
-    private const string JointId = "tether-joint";
-
-    public override void Initialize()
-    {
-        base.Initialize();
-        SubscribeNetworkEvent<StartTetherEvent>(OnStartTether);
-        SubscribeNetworkEvent<StopTetherEvent>(OnStopTether);
-        SubscribeNetworkEvent<TetherMoveEvent>(OnMoveTether);
-
-        _playerManager.PlayerStatusChanged += OnStatusChange;
-    }
-
-    private void OnStatusChange(object? sender, SessionStatusEventArgs e)
-    {
-        StopTether(e.Session);
-    }
-
-    public override void Shutdown()
-    {
-        base.Shutdown();
-
-        _playerManager.PlayerStatusChanged -= OnStatusChange;
-    }
-
-    public void Toggle(ICommonSession? session)
-    {
-        if (session == null)
-            return;
-
-        if (_draggers.Add(session))
-        {
-            RaiseNetworkEvent(new TetherGunToggleMessage()
-            {
-                Enabled = true,
-            }, session.ConnectedClient);
-            return;
-        }
-
-        _draggers.Remove(session);
-        RaiseNetworkEvent(new TetherGunToggleMessage()
-        {
-            Enabled = false,
-        }, session.ConnectedClient);
-    }
-
-    public bool IsEnabled(ICommonSession? session)
-    {
-        if (session == null)
-            return false;
-
-        return _draggers.Contains(session);
-    }
-
-    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)) return;
-
-        var tether = Spawn("TetherEntity", msg.Coordinates);
-
-        if (!TryComp<PhysicsComponent>(tether, out var bodyA) ||
-            !TryComp<PhysicsComponent>(msg.Entity, out var bodyB))
-        {
-            Del(tether);
-            return;
-        }
-
-        EnsureComp<AdminFrozenComponent>(msg.Entity);
-
-        if (TryComp<TransformComponent>(msg.Entity, out var xform))
-        {
-            xform.Anchored = false;
-        }
-
-        if (_container.IsEntityInContainer(msg.Entity))
-        {
-            xform?.AttachToGridOrMap();
-        }
-
-        if (TryComp<PhysicsComponent>(msg.Entity, out var body))
-        {
-            _physics.SetBodyStatus(body, BodyStatus.InAir);
-        }
-
-        _physics.WakeBody(tether, body: bodyA);
-        _physics.WakeBody(msg.Entity, body: bodyB);
-        var joint = _joints.CreateMouseJoint(tether, msg.Entity, 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 = 10000f * bodyB.Mass;
-
-        _tethered.Add(playerSession, (msg.Entity, tether, joint));
-        RaiseNetworkEvent(new PredictTetherEvent()
-        {
-            Entity = msg.Entity
-        }, args.SenderSession.ConnectedClient);
-    }
-
-    private void OnStopTether(StopTetherEvent msg, EntitySessionEventArgs args)
-    {
-        StopTether(args.SenderSession);
-    }
-
-    private void StopTether(ICommonSession session)
-    {
-        if (!_tethered.TryGetValue(session, out var weh))
-            return;
-
-        RemComp<AdminFrozenComponent>(weh.Entity);
-
-        if (TryComp<PhysicsComponent>(weh.Entity, out var body) &&
-            !HasComp<GhostComponent>(weh.Entity))
-        {
-            Timer.Spawn(1000, () =>
-            {
-                if (Deleted(weh.Entity)) return;
-
-                _physics.SetBodyStatus(body, 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<TransformComponent>(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<ICommonSession>();
-        var bodyQuery = GetEntityQuery<PhysicsComponent>();
-
-        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))
-            {
-                _physics.WakeBody(entity.Entity, body: body);
-            }
-        }
-
-        foreach (var session in toRemove)
-        {
-            StopTether(session);
-        }
-    }
-}
diff --git a/Content.Server/Weapons/TetherGunCommand.cs b/Content.Server/Weapons/TetherGunCommand.cs
deleted file mode 100644 (file)
index 38136ce..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-using Content.Server.Administration;
-using Content.Server.Weapons.Ranged.Systems;
-using Content.Shared.Administration;
-using Content.Shared.Weapons.Ranged.Systems;
-using Robust.Shared.Console;
-
-namespace Content.Server.Weapons;
-
-[AdminCommand(AdminFlags.Fun)]
-public sealed class TetherGunCommand : IConsoleCommand
-{
-    public string Command => SharedTetherGunSystem.CommandName;
-    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)
-    {
-        var system = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<TetherGunSystem>();
-        system.Toggle(shell.Player);
-
-        if (system.IsEnabled(shell.Player))
-            shell.WriteLine("Tether gun toggled on");
-        else
-            shell.WriteLine("Tether gun toggled off");
-    }
-}
index 1e4cc3c19628373d1c8e78982d15a124194a5943..8b5d092e29599779ca40bbddb2c1f3bf78aa5afd 100644 (file)
@@ -94,7 +94,7 @@ public sealed class BuckleComponentState : ComponentState
 }
 
 [ByRefEvent]
-public readonly record struct BuckleAttemptEvent(EntityUid StrapEntity, EntityUid BuckledEntity, bool Buckling, bool Cancelled = false);
+public record struct BuckleAttemptEvent(EntityUid StrapEntity, EntityUid BuckledEntity, bool Buckling, bool Cancelled = false);
 
 [ByRefEvent]
 public readonly record struct BuckleChangeEvent(EntityUid StrapEntity, EntityUid BuckledEntity, bool Buckling);
diff --git a/Content.Shared/Weapons/Misc/SharedTetherGunSystem.cs b/Content.Shared/Weapons/Misc/SharedTetherGunSystem.cs
new file mode 100644 (file)
index 0000000..67fb990
--- /dev/null
@@ -0,0 +1,267 @@
+using System.Diagnostics.CodeAnalysis;
+using Content.Shared.ActionBlocker;
+using Content.Shared.Buckle.Components;
+using Content.Shared.Hands.Components;
+using Content.Shared.Interaction;
+using Content.Shared.Mobs.Systems;
+using Content.Shared.Movement.Events;
+using Content.Shared.Throwing;
+using Content.Shared.Toggleable;
+using Robust.Shared.Map;
+using Robust.Shared.Network;
+using Robust.Shared.Physics;
+using Robust.Shared.Physics.Components;
+using Robust.Shared.Physics.Systems;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Weapons.Misc;
+
+public abstract class SharedTetherGunSystem : EntitySystem
+{
+    [Dependency] private   readonly INetManager _netManager = default!;
+    [Dependency] private   readonly ActionBlockerSystem _blocker = default!;
+    [Dependency] private   readonly MobStateSystem _mob = default!;
+    [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+    [Dependency] private   readonly SharedAudioSystem _audio = default!;
+    [Dependency] private   readonly SharedJointSystem _joints = default!;
+    [Dependency] private   readonly SharedPhysicsSystem _physics = default!;
+    [Dependency] protected readonly SharedTransformSystem TransformSystem = default!;
+    [Dependency] private   readonly ThrownItemSystem _thrown = default!;
+
+    private const string TetherJoint = "tether";
+
+    private const float SpinVelocity = MathF.PI;
+    private const float AngularChange = 1f;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+        SubscribeLocalEvent<TetherGunComponent, ActivateInWorldEvent>(OnTetherActivate);
+        SubscribeLocalEvent<TetherGunComponent, AfterInteractEvent>(OnTetherRanged);
+        SubscribeAllEvent<RequestTetherMoveEvent>(OnTetherMove);
+
+        SubscribeLocalEvent<TetheredComponent, BuckleAttemptEvent>(OnTetheredBuckleAttempt);
+        SubscribeLocalEvent<TetheredComponent, UpdateCanMoveEvent>(OnTetheredUpdateCanMove);
+    }
+
+    private void OnTetheredBuckleAttempt(EntityUid uid, TetheredComponent component, ref BuckleAttemptEvent args)
+    {
+        args.Cancelled = true;
+    }
+
+    private void OnTetheredUpdateCanMove(EntityUid uid, TetheredComponent component, UpdateCanMoveEvent args)
+    {
+        args.Cancel();
+    }
+
+    public override void Update(float frameTime)
+    {
+        base.Update(frameTime);
+
+        // Just to set the angular velocity due to joint funnies
+        var tetheredQuery = EntityQueryEnumerator<TetheredComponent, PhysicsComponent>();
+
+        while (tetheredQuery.MoveNext(out var uid, out _, out var physics))
+        {
+            var sign = Math.Sign(physics.AngularVelocity);
+
+            if (sign == 0)
+            {
+                sign = 1;
+            }
+
+            var targetVelocity = MathF.PI * sign;
+
+            var shortFall = Math.Clamp(targetVelocity - physics.AngularVelocity, -SpinVelocity, SpinVelocity);
+            shortFall *= frameTime * AngularChange;
+
+            _physics.ApplyAngularImpulse(uid, shortFall, body: physics);
+        }
+    }
+
+    private void OnTetherMove(RequestTetherMoveEvent msg, EntitySessionEventArgs args)
+    {
+        var user = args.SenderSession.AttachedEntity;
+
+        if (user == null)
+            return;
+
+        if (!TryGetTetherGun(user.Value, out var gunUid, out var gun) || gun.TetherEntity == null)
+        {
+            return;
+        }
+
+        if (!msg.Coordinates.TryDistance(EntityManager, TransformSystem, Transform(gunUid.Value).Coordinates,
+                out var distance) ||
+            distance > gun.MaxDistance)
+        {
+            return;
+        }
+
+        TransformSystem.SetCoordinates(gun.TetherEntity.Value, msg.Coordinates);
+    }
+
+    private void OnTetherRanged(EntityUid uid, TetherGunComponent component, AfterInteractEvent args)
+    {
+        if (args.Target == null || args.Handled)
+            return;
+
+        TryTether(uid, args.Target.Value, args.User, component);
+    }
+
+    protected bool TryGetTetherGun(EntityUid user, [NotNullWhen(true)] out EntityUid? gunUid, [NotNullWhen(true)] out TetherGunComponent? gun)
+    {
+        gunUid = null;
+        gun = null;
+
+        if (!TryComp<HandsComponent>(user, out var hands) ||
+            !TryComp(hands.ActiveHandEntity, out gun))
+        {
+            return false;
+        }
+
+        gunUid = hands.ActiveHandEntity.Value;
+        return true;
+    }
+
+    private void OnTetherActivate(EntityUid uid, TetherGunComponent component, ActivateInWorldEvent args)
+    {
+        StopTether(uid, component);
+    }
+
+    public void TryTether(EntityUid gun, EntityUid target, EntityUid? user, TetherGunComponent? component = null)
+    {
+        if (!Resolve(gun, ref component))
+            return;
+
+        if (!CanTether(gun, component, target, user))
+            return;
+
+        StartTether(gun, component, target, user);
+    }
+
+    protected virtual bool CanTether(EntityUid uid, TetherGunComponent component, EntityUid target, EntityUid? user)
+    {
+        if (HasComp<TetheredComponent>(target) || !TryComp<PhysicsComponent>(target, out var physics))
+            return false;
+
+        if (physics.BodyType == BodyType.Static && !component.CanUnanchor)
+            return false;
+
+        if (physics.Mass > component.MassLimit)
+            return false;
+
+        if (!component.CanTetherAlive && _mob.IsAlive(target))
+            return false;
+
+        if (TryComp<StrapComponent>(target, out var strap) && strap.BuckledEntities.Count > 0)
+            return false;
+
+        return true;
+    }
+
+    protected virtual void StartTether(EntityUid gunUid, TetherGunComponent component, EntityUid target, EntityUid? user,
+        PhysicsComponent? targetPhysics = null, TransformComponent? targetXform = null)
+    {
+        if (!Resolve(target, ref targetPhysics, ref targetXform))
+            return;
+
+        if (component.Tethered != null)
+        {
+            StopTether(gunUid, component, true);
+        }
+
+        TryComp<AppearanceComponent>(gunUid, out var appearance);
+        _appearance.SetData(gunUid, TetherVisualsStatus.Key, true, appearance);
+        _appearance.SetData(gunUid, ToggleableLightVisuals.Enabled, true, appearance);
+
+        // Target updates
+        TransformSystem.Unanchor(target, targetXform);
+        component.Tethered = target;
+        var tethered = EnsureComp<TetheredComponent>(target);
+        _physics.SetBodyStatus(targetPhysics, BodyStatus.InAir, false);
+        _physics.SetSleepingAllowed(target, targetPhysics, false);
+        tethered.Tetherer = gunUid;
+        tethered.OriginalAngularDamping = targetPhysics.AngularDamping;
+        _physics.SetAngularDamping(targetPhysics, 0f);
+        _physics.SetLinearDamping(targetPhysics, 0f);
+        _physics.SetAngularVelocity(target, SpinVelocity, body: targetPhysics);
+        _physics.WakeBody(target, body: targetPhysics);
+        var thrown = EnsureComp<ThrownItemComponent>(component.Tethered.Value);
+        thrown.Thrower = gunUid;
+        _blocker.UpdateCanMove(target);
+
+        // Invisible tether entity
+        var tether = Spawn("TetherEntity", Transform(target).MapPosition);
+        var tetherPhysics = Comp<PhysicsComponent>(tether);
+        component.TetherEntity = tether;
+        _physics.WakeBody(tether);
+
+        var joint = _joints.CreateMouseJoint(tether, target, id: TetherJoint);
+
+        SharedJointSystem.LinearStiffness(component.Frequency, component.DampingRatio, tetherPhysics.Mass, targetPhysics.Mass, out var stiffness, out var damping);
+        joint.Stiffness = stiffness;
+        joint.Damping = damping;
+        joint.MaxForce = component.MaxForce;
+
+        // Sad...
+        if (_netManager.IsServer && component.Stream == null)
+            component.Stream = _audio.PlayPredicted(component.Sound, gunUid, null);
+
+        Dirty(tethered);
+        Dirty(component);
+    }
+
+    protected virtual void StopTether(EntityUid gunUid, TetherGunComponent component, bool transfer = false)
+    {
+        if (component.Tethered == null)
+            return;
+
+        if (component.TetherEntity != null)
+        {
+            _joints.RemoveJoint(component.TetherEntity.Value, TetherJoint);
+
+            if (_netManager.IsServer)
+                QueueDel(component.TetherEntity.Value);
+
+            component.TetherEntity = null;
+        }
+
+        if (TryComp<PhysicsComponent>(component.Tethered, out var targetPhysics))
+        {
+            var thrown = EnsureComp<ThrownItemComponent>(component.Tethered.Value);
+            _thrown.LandComponent(component.Tethered.Value, thrown, targetPhysics);
+
+            _physics.SetBodyStatus(targetPhysics, BodyStatus.OnGround);
+            _physics.SetSleepingAllowed(component.Tethered.Value, targetPhysics, true);
+            _physics.SetAngularDamping(targetPhysics, Comp<TetheredComponent>(component.Tethered.Value).OriginalAngularDamping);
+        }
+
+        if (!transfer)
+        {
+            component.Stream?.Stop();
+            component.Stream = null;
+        }
+
+        TryComp<AppearanceComponent>(gunUid, out var appearance);
+        _appearance.SetData(gunUid, TetherVisualsStatus.Key, false, appearance);
+        _appearance.SetData(gunUid, ToggleableLightVisuals.Enabled, false, appearance);
+
+        RemCompDeferred<TetheredComponent>(component.Tethered.Value);
+        _blocker.UpdateCanMove(component.Tethered.Value);
+        component.Tethered = null;
+        Dirty(component);
+    }
+
+    [Serializable, NetSerializable]
+    protected sealed class RequestTetherMoveEvent : EntityEventArgs
+    {
+        public EntityCoordinates Coordinates;
+    }
+
+    [Serializable, NetSerializable]
+    public enum TetherVisualsStatus : byte
+    {
+        Key,
+    }
+}
diff --git a/Content.Shared/Weapons/Misc/TetherGunComponent.cs b/Content.Shared/Weapons/Misc/TetherGunComponent.cs
new file mode 100644 (file)
index 0000000..7feaf7f
--- /dev/null
@@ -0,0 +1,58 @@
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Weapons.Misc;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class TetherGunComponent : Component
+{
+    [ViewVariables(VVAccess.ReadWrite), DataField("maxDistance"), AutoNetworkedField]
+    public float MaxDistance = 10f;
+
+    /// <summary>
+    /// The entity the tethered target has a joint to.
+    /// </summary>
+    [DataField("tetherEntity"), AutoNetworkedField]
+    public EntityUid? TetherEntity;
+
+    /// <summary>
+    /// The entity currently tethered.
+    /// </summary>
+    [ViewVariables(VVAccess.ReadWrite), DataField("tethered"), AutoNetworkedField]
+    public EntityUid? Tethered;
+
+    /// <summary>
+    /// Can the tethergun unanchor entities.
+    /// </summary>
+    [ViewVariables(VVAccess.ReadWrite), DataField("canUnanchor"), AutoNetworkedField]
+    public bool CanUnanchor = false;
+
+    [ViewVariables(VVAccess.ReadWrite), DataField("canTetherAlive"), AutoNetworkedField]
+    public bool CanTetherAlive = false;
+
+    /// <summary>
+    /// Max force between the tether entity and the tethered target.
+    /// </summary>
+    [ViewVariables(VVAccess.ReadWrite), DataField("maxForce"), AutoNetworkedField]
+    public float MaxForce = 200f;
+
+    [ViewVariables(VVAccess.ReadWrite), DataField("frequency"), AutoNetworkedField]
+    public float Frequency = 10f;
+
+    [ViewVariables(VVAccess.ReadWrite), DataField("dampingRatio"), AutoNetworkedField]
+    public float DampingRatio = 2f;
+
+    /// <summary>
+    /// Maximum amount of mass a tethered entity can have.
+    /// </summary>
+    [ViewVariables(VVAccess.ReadWrite), DataField("massLimit"), AutoNetworkedField]
+    public float MassLimit = 100f;
+
+    [ViewVariables(VVAccess.ReadWrite), DataField("sound"), AutoNetworkedField]
+    public SoundSpecifier? Sound = new SoundPathSpecifier("/Audio/Weapons/weoweo.ogg")
+    {
+        Params = AudioParams.Default.WithLoop(true).WithVolume(-8f),
+    };
+
+    public IPlayingAudioStream? Stream;
+}
diff --git a/Content.Shared/Weapons/Misc/TetheredComponent.cs b/Content.Shared/Weapons/Misc/TetheredComponent.cs
new file mode 100644 (file)
index 0000000..3a92c69
--- /dev/null
@@ -0,0 +1,16 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Weapons.Misc;
+
+/// <summary>
+/// Added to entities tethered by a tethergun.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class TetheredComponent : Component
+{
+    [DataField("tetherer"), AutoNetworkedField]
+    public EntityUid Tetherer;
+
+    [ViewVariables(VVAccess.ReadWrite), DataField("originalAngularDamping"), AutoNetworkedField]
+    public float OriginalAngularDamping;
+}
diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedTetherGunSystem.cs b/Content.Shared/Weapons/Ranged/Systems/SharedTetherGunSystem.cs
deleted file mode 100644 (file)
index 8b0ed07..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-using Robust.Shared.Map;
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.Weapons.Ranged.Systems;
-
-public abstract class SharedTetherGunSystem : EntitySystem
-{
-    public const string CommandName = "tethergun";
-}
-
-/// <summary>
-/// Sent from server to client if tether gun is toggled on.
-/// </summary>
-[Serializable, NetSerializable]
-public sealed class TetherGunToggleMessage : EntityEventArgs
-{
-    public bool Enabled;
-}
-
-[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;
-}
-
-/// <summary>
-/// Client can't know the tether's <see cref="EntityUid"/> in advance so needs to be told about it for prediction.
-/// </summary>
-[Serializable, NetSerializable]
-public sealed class PredictTetherEvent : EntityEventArgs
-{
-    public EntityUid Entity;
-}
index c7be93af11a264587dc60087d94e38f303918c49..483a0e1d2a6ebc7c5d7941eb3d56a6318eaf76fc 100644 (file)
@@ -1,4 +1,4 @@
-- files: ["plasm_cutter.ogg"]
+- files: ["plasma_cutter.ogg"]
   license: "CC-BY-SA-3.0"
   copyright: "Taken from Citadel station."
   source: "https://github.com/Citadel-Station-13/Citadel-Station-13-RP/blob/5b43cb2545a19957ec6ce3352dceac5e347e77df/sound/weapons/plasma_cutter.ogg"
index b16fcd263a14d5e2ec80f0acc792f10ce07987d1..7b077ffc7114c5f8b27c33c653e279742454709d 100644 (file)
@@ -12,4 +12,8 @@ boxingbell.ogg taken from Herkules92 at https://freesound.org/people/Herkules92/
 
 block_metal1.ogg taken from https://github.com/Citadel-Station-13/Citadel-Station-13/commit/31c5996a5db8cce0cb431cb1dc20d99cac83f268 under CC BY-SA 3.0
 
-pierce.ogg taken from: https://github.com/tgstation/tgstation/commit/106cd26fc00851a51dd362f3131120318d848a53
\ No newline at end of file
+pierce.ogg taken from: https://github.com/tgstation/tgstation/commit/106cd26fc00851a51dd362f3131120318d848a53
+
+- files: ["weoweo.ogg"]
+  license: "SONNISS #GAMEAUDIOGDC BUNDLE LICENSING"
+  copyright: "Taken from Sonniss.com - GDC 2023 - Systematic Sound - TonalElements Obscurum - Dark Drones"
\ No newline at end of file
diff --git a/Resources/Audio/Weapons/weoweo.ogg b/Resources/Audio/Weapons/weoweo.ogg
new file mode 100644 (file)
index 0000000..e7d6cea
Binary files /dev/null and b/Resources/Audio/Weapons/weoweo.ogg differ
index e6c9ae310210f2dad457f7e9d129b4d2de1535a7..c35e97a2057d6bc6ca78235a33767d3bbb312e80 100644 (file)
@@ -34,6 +34,7 @@ research-technology-alternative-research = Alternative Research
 research-technology-magnets-tech = Localized Magnetism
 research-technology-advanced-parts = Advanced Parts
 research-technology-abnormal-artifact-manipulation = Abnormal Artifact Manipulation
+research-technology-gravity-manipulation = Gravity Manipulation
 research-technology-mobile-anomaly-tech = Mobile Anomaly Tech
 research-technology-rped = Rapid Part Exchange
 research-technology-super-parts = Super Parts
index 6f34e113fafb25657f762e9590e129bf2a50b1f6..29d25a621e2eac18b7f926121bf272f053ec0184 100644 (file)
@@ -16,7 +16,6 @@
     - key: enum.InstrumentUiKey.Key
       type: InstrumentBoundUserInterface
   - type: Sprite
-    netsync: false
     sprite: Objects/Fun/pai.rsi
     layers:
     - state: pai-base
index ade1337c794d516494abd6f5f5408378367ddcda..4356efa474f2d04191b9ac8291b355629bc424c0 100644 (file)
       soundInsert:
         path: /Audio/Weapons/Guns/Gunshots/grenade_launcher.ogg
 
+- type: entity
+  name: tether gun
+  parent:
+  - BaseItem
+  - PowerCellSlotMediumItem
+  id: WeaponTetherGun
+  description: Manipulates gravity around objects to fling them at high velocities.
+  components:
+    - type: TetherGun
+    - type: PowerCellDraw
+    - type: Sprite
+      sprite: Objects/Weapons/Guns/Launchers/tether_gun.rsi
+      layers:
+        - state: base
+        - state: base-unshaded
+          map: [ "unshaded" ]
+          shader: unshaded
+          visible: false
+    - type: ToggleableLightVisuals
+      spriteLayer: unshaded
+      inhandVisuals:
+        left:
+          - state: inhand-left-unshaded
+            shader: unshaded
+        right:
+          - state: inhand-right-unshaded
+            shader: unshaded
+    - type: Appearance
+    - type: GenericVisualizer
+      visuals:
+        enum.TetherVisualsStatus.Key:
+          unshaded:
+            True: { visible: true }
+            False: { visible: false }
+
 # Admeme
+- type: entity
+  name: tether gun
+  parent: BaseItem
+  id: WeaponTetherGunAdmin
+  suffix: admin
+  description: Manipulates gravity around objects to fling them at high velocities.
+  components:
+    - type: TetherGun
+      canTetherAlive: true
+      canUnanchor: true
+      maxForce: 10000
+      massLimit: 10000
+      dampingRatio: 4
+      frequency: 20
+    - type: Sprite
+      sprite: Objects/Weapons/Guns/Launchers/tether_gun.rsi
+      layers:
+        - state: base
+        - state: base-unshaded
+          map: [ "unshaded" ]
+          shader: unshaded
+          visible: false
+    - type: ToggleableLightVisuals
+      spriteLayer: unshaded
+      inhandVisuals:
+        left:
+          - state: inhand-left-unshaded
+            shader: unshaded
+        right:
+          - state: inhand-right-unshaded
+            shader: unshaded
+    - type: Appearance
+    - type: GenericVisualizer
+      visuals:
+        enum.TetherVisualsStatus.Key:
+          unshaded:
+            True: { visible: true }
+            False: { visible: false }
+
 - type: entity
   name: meteor launcher
   parent: WeaponLauncherMultipleRocket
index ca0c34f72e98f9a2d454446b29a6bbf59bae551e..b8f71ee236b8c7a03e9b7cf42ed48db05e497af6 100644 (file)
         cell_slot:
           name: power-cell-slot-component-slot-name-default
           startingItem: PowerCellMedium
+
+- type: entity
+  id: PowerCellSlotHighItem
+  abstract: true
+  components:
+    - type: ContainerContainer
+      containers:
+        cell_slot: !type:ContainerSlot { }
+    - type: PowerCellSlot
+      cellSlotId: cell_slot
+    - type: ItemSlots
+      slots:
+        cell_slot:
+          name: power-cell-slot-component-slot-name-default
+          startingItem: PowerCellHigh
index 4c6bd868646e02c483c3e2d5bb0a1f5089c668cf..ce953854d6504fadeb85645551b024f01f878d79 100644 (file)
@@ -4,6 +4,7 @@
   components:
   - type: Physics
     bodyType: Dynamic
+    sleepingAllowed: false
   - type: Fixtures
     fixtures:
       tether:
index edac88d2a43924320a7353689470645699e8f628..f6f940a997ce97734266c43546c51db74f3c178f 100644 (file)
   recipeUnlocks:
   - TraversalDistorterMachineCircuitboard
 
+- type: technology
+  id: GravityManipulation
+  name: research-technology-gravity-manipulation
+  icon:
+    sprite: Objects/Weapons/Guns/Launchers/tether_gun.rsi
+    state: base
+  discipline: Experimental
+  tier: 2
+  cost: 7500
+  recipeUnlocks:
+    - WeaponTetherGun
+
 - type: technology
   id: MobileAnomalyTech
   name: research-technology-mobile-anomaly-tech
diff --git a/Resources/Textures/Objects/Weapons/Guns/Launchers/tether_gun.rsi/base-unshaded.png b/Resources/Textures/Objects/Weapons/Guns/Launchers/tether_gun.rsi/base-unshaded.png
new file mode 100644 (file)
index 0000000..84cabef
Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Launchers/tether_gun.rsi/base-unshaded.png differ
diff --git a/Resources/Textures/Objects/Weapons/Guns/Launchers/tether_gun.rsi/base.png b/Resources/Textures/Objects/Weapons/Guns/Launchers/tether_gun.rsi/base.png
new file mode 100644 (file)
index 0000000..dac9392
Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Launchers/tether_gun.rsi/base.png differ
diff --git a/Resources/Textures/Objects/Weapons/Guns/Launchers/tether_gun.rsi/inhand-left-unshaded.png b/Resources/Textures/Objects/Weapons/Guns/Launchers/tether_gun.rsi/inhand-left-unshaded.png
new file mode 100644 (file)
index 0000000..97c360c
Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Launchers/tether_gun.rsi/inhand-left-unshaded.png differ
diff --git a/Resources/Textures/Objects/Weapons/Guns/Launchers/tether_gun.rsi/inhand-left.png b/Resources/Textures/Objects/Weapons/Guns/Launchers/tether_gun.rsi/inhand-left.png
new file mode 100644 (file)
index 0000000..5d314fb
Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Launchers/tether_gun.rsi/inhand-left.png differ
diff --git a/Resources/Textures/Objects/Weapons/Guns/Launchers/tether_gun.rsi/inhand-right-unshaded.png b/Resources/Textures/Objects/Weapons/Guns/Launchers/tether_gun.rsi/inhand-right-unshaded.png
new file mode 100644 (file)
index 0000000..1b97553
Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Launchers/tether_gun.rsi/inhand-right-unshaded.png differ
diff --git a/Resources/Textures/Objects/Weapons/Guns/Launchers/tether_gun.rsi/inhand-right.png b/Resources/Textures/Objects/Weapons/Guns/Launchers/tether_gun.rsi/inhand-right.png
new file mode 100644 (file)
index 0000000..913d556
Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Launchers/tether_gun.rsi/inhand-right.png differ
diff --git a/Resources/Textures/Objects/Weapons/Guns/Launchers/tether_gun.rsi/meta.json b/Resources/Textures/Objects/Weapons/Guns/Launchers/tether_gun.rsi/meta.json
new file mode 100644 (file)
index 0000000..4d8baad
--- /dev/null
@@ -0,0 +1,33 @@
+{
+    "version": 1,
+    "license": "CC-BY-SA-3.0",
+    "copyright": "Sprited by discord Kheprep#7153",
+    "size": {
+        "x": 32,
+        "y": 32
+    },
+    "states": [
+        {
+            "name": "base"
+        },
+        {
+            "name": "base-unshaded"
+        },
+        {
+            "name": "inhand-left",
+            "directions": 4
+        },
+        {
+            "name": "inhand-right",
+            "directions": 4
+        },
+        {
+            "name": "inhand-left-unshaded",
+            "directions": 4
+        },
+        {
+            "name": "inhand-right-unshaded",
+            "directions": 4
+        }
+    ]
+}
\ No newline at end of file