--- /dev/null
+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));
+ }
+ }
+}
--- /dev/null
+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;
+ }
+}
+++ /dev/null
-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);
-
- }
-}
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)
--- /dev/null
+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);
+ }
+}
+++ /dev/null
-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);
- }
- }
-}
+++ /dev/null
-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");
- }
-}
}
[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);
--- /dev/null
+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,
+ }
+}
--- /dev/null
+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;
+}
--- /dev/null
+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;
+}
+++ /dev/null
-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;
-}
-- 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"
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
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
- key: enum.InstrumentUiKey.Key
type: InstrumentBoundUserInterface
- type: Sprite
- netsync: false
sprite: Objects/Fun/pai.rsi
layers:
- state: pai-base
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
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
components:
- type: Physics
bodyType: Dynamic
+ sleepingAllowed: false
- type: Fixtures
fixtures:
tether:
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
--- /dev/null
+{
+ "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