Add tether gun (#16430)

This commit is contained in:
metalgearsloth
2023-05-18 11:36:06 +10:00
committed by GitHub
parent b386764639
commit 775258be52
28 changed files with 697 additions and 413 deletions

View File

@@ -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));
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View 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)

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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");
}
}

View 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);

View File

@@ -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,
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View 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"

View File

@@ -13,3 +13,7 @@ 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
- files: ["weoweo.ogg"]
license: "SONNISS #GAMEAUDIOGDC BUNDLE LICENSING"
copyright: "Taken from Sonniss.com - GDC 2023 - Systematic Sound - TonalElements Obscurum - Dark Drones"

Binary file not shown.

View 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

View File

@@ -16,7 +16,6 @@
- key: enum.InstrumentUiKey.Key
type: InstrumentBoundUserInterface
- type: Sprite
netsync: false
sprite: Objects/Fun/pai.rsi
layers:
- state: pai-base

View File

@@ -151,7 +151,81 @@
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

View File

@@ -105,3 +105,18 @@
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

View File

@@ -4,6 +4,7 @@
components:
- type: Physics
bodyType: Dynamic
sleepingAllowed: false
- type: Fixtures
fixtures:
tether:

View File

@@ -116,6 +116,18 @@
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@@ -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
}
]
}