From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Thu, 18 May 2023 01:36:06 +0000 (+1000) Subject: Add tether gun (#16430) X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=775258be5226e879321be60458f6f94125e252d1;p=space-station-14.git Add tether gun (#16430) --- diff --git a/Content.Client/Weapons/Misc/TetherGunOverlay.cs b/Content.Client/Weapons/Misc/TetherGunOverlay.cs new file mode 100644 index 0000000000..215589c38d --- /dev/null +++ b/Content.Client/Weapons/Misc/TetherGunOverlay.cs @@ -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(); + var xformQuery = _entManager.GetEntityQuery(); + var worldHandle = args.WorldHandle; + var xformSystem = _entManager.System(); + + 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 index 0000000000..1219fe1357 --- /dev/null +++ b/Content.Client/Weapons/Misc/TetherGunSystem.cs @@ -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(OnTetheredStartup); + SubscribeLocalEvent(OnTetheredShutdown); + _overlay.AddOverlay(new TetherGunOverlay(EntityManager)); + } + + public override void Shutdown() + { + base.Shutdown(); + _overlay.RemoveOverlay(); + } + + 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(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(uid, out var sprite)) + return; + + sprite.Color = Color.Orange; + } + + private void OnTetheredShutdown(EntityUid uid, TetheredComponent component, ComponentShutdown args) + { + if (!TryComp(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 index 3a66f07bc9..0000000000 --- a/Content.Client/Weapons/Ranged/Systems/TetherGunSystem.cs +++ /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; } - - /// - /// The entity being dragged around. - /// - private EntityUid? _dragging; - private EntityUid? _tether; - - private MapCoordinates? _lastMousePosition; - - public override void Initialize() - { - base.Initialize(); - SubscribeNetworkEvent(OnPredictTether); - SubscribeNetworkEvent(OnTetherGun); - SubscribeLocalEvent(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().CurrentState; - - if (gameState is GameplayState game) - { - var uid = game.GetClickedEntity(mousePos); - - if (uid != null) - StartDragging(uid.Value, mousePos); - } - - if (_dragging == null) - return; - } - - if (!TryComp(_dragging!.Value, out var xform) || - _lastMousePosition!.Value.MapId != xform.MapID || - !TryComp(_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); - - } -} diff --git a/Content.Server/PowerCell/PowerCellSystem.cs b/Content.Server/PowerCell/PowerCellSystem.cs index 7719773d9d..2ba103a05b 100644 --- a/Content.Server/PowerCell/PowerCellSystem.cs +++ b/Content.Server/PowerCell/PowerCellSystem.cs @@ -157,6 +157,18 @@ public sealed partial class PowerCellSystem : SharedPowerCellSystem return false; } + /// + /// Whether the power cell has any power at all for the draw rate. + /// + 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 index 0000000000..3b62e3f8ca --- /dev/null +++ b/Content.Server/Weapons/Misc/TetherGunSystem.cs @@ -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(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 index d81f571a32..0000000000 --- a/Content.Server/Weapons/Ranged/Systems/TetherGunSystem.cs +++ /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 _tethered = new(); - private readonly HashSet _draggers = new(); - - private const string JointId = "tether-joint"; - - public override void Initialize() - { - base.Initialize(); - SubscribeNetworkEvent(OnStartTether); - SubscribeNetworkEvent(OnStopTether); - SubscribeNetworkEvent(OnMoveTether); - - _playerManager.PlayerStatusChanged += OnStatusChange; - } - - private void OnStatusChange(object? sender, SessionStatusEventArgs e) - { - StopTether(e.Session); - } - - public override void Shutdown() - { - base.Shutdown(); - - _playerManager.PlayerStatusChanged -= OnStatusChange; - } - - 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(tether, out var bodyA) || - !TryComp(msg.Entity, out var bodyB)) - { - Del(tether); - return; - } - - EnsureComp(msg.Entity); - - if (TryComp(msg.Entity, out var xform)) - { - xform.Anchored = false; - } - - if (_container.IsEntityInContainer(msg.Entity)) - { - xform?.AttachToGridOrMap(); - } - - if (TryComp(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(weh.Entity); - - if (TryComp(weh.Entity, out var body) && - !HasComp(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(tether.Tether, out var xform) || - xform.MapID != msg.Coordinates.MapId) return; - - xform.WorldPosition = msg.Coordinates.Position; - } - - public override void Update(float frameTime) - { - base.Update(frameTime); - - var toRemove = new RemQueue(); - var bodyQuery = GetEntityQuery(); - - foreach (var (session, entity) in _tethered) - { - if (Deleted(entity.Entity) || - Deleted(entity.Tether) || - !entity.Joint.Enabled) - { - toRemove.Add(session); - continue; - } - - // Force it awake, always - if (bodyQuery.TryGetComponent(entity.Entity, out var body)) - { - _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 index 38136ce220..0000000000 --- a/Content.Server/Weapons/TetherGunCommand.cs +++ /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().GetEntitySystem(); - system.Toggle(shell.Player); - - if (system.IsEnabled(shell.Player)) - shell.WriteLine("Tether gun toggled on"); - else - shell.WriteLine("Tether gun toggled off"); - } -} diff --git a/Content.Shared/Buckle/Components/BuckleComponent.cs b/Content.Shared/Buckle/Components/BuckleComponent.cs index 1e4cc3c196..8b5d092e29 100644 --- a/Content.Shared/Buckle/Components/BuckleComponent.cs +++ b/Content.Shared/Buckle/Components/BuckleComponent.cs @@ -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 index 0000000000..67fb9904a7 --- /dev/null +++ b/Content.Shared/Weapons/Misc/SharedTetherGunSystem.cs @@ -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(OnTetherActivate); + SubscribeLocalEvent(OnTetherRanged); + SubscribeAllEvent(OnTetherMove); + + SubscribeLocalEvent(OnTetheredBuckleAttempt); + SubscribeLocalEvent(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(); + + 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(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(target) || !TryComp(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(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(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(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(component.Tethered.Value); + thrown.Thrower = gunUid; + _blocker.UpdateCanMove(target); + + // Invisible tether entity + var tether = Spawn("TetherEntity", Transform(target).MapPosition); + var tetherPhysics = Comp(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(component.Tethered, out var targetPhysics)) + { + var thrown = EnsureComp(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(component.Tethered.Value).OriginalAngularDamping); + } + + if (!transfer) + { + component.Stream?.Stop(); + component.Stream = null; + } + + TryComp(gunUid, out var appearance); + _appearance.SetData(gunUid, TetherVisualsStatus.Key, false, appearance); + _appearance.SetData(gunUid, ToggleableLightVisuals.Enabled, false, appearance); + + RemCompDeferred(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 index 0000000000..7feaf7f518 --- /dev/null +++ b/Content.Shared/Weapons/Misc/TetherGunComponent.cs @@ -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; + + /// + /// The entity the tethered target has a joint to. + /// + [DataField("tetherEntity"), AutoNetworkedField] + public EntityUid? TetherEntity; + + /// + /// The entity currently tethered. + /// + [ViewVariables(VVAccess.ReadWrite), DataField("tethered"), AutoNetworkedField] + public EntityUid? Tethered; + + /// + /// Can the tethergun unanchor entities. + /// + [ViewVariables(VVAccess.ReadWrite), DataField("canUnanchor"), AutoNetworkedField] + public bool CanUnanchor = false; + + [ViewVariables(VVAccess.ReadWrite), DataField("canTetherAlive"), AutoNetworkedField] + public bool CanTetherAlive = false; + + /// + /// Max force between the tether entity and the tethered target. + /// + [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; + + /// + /// Maximum amount of mass a tethered entity can have. + /// + [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 index 0000000000..3a92c69fbf --- /dev/null +++ b/Content.Shared/Weapons/Misc/TetheredComponent.cs @@ -0,0 +1,16 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Weapons.Misc; + +/// +/// Added to entities tethered by a tethergun. +/// +[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 index 8b0ed07162..0000000000 --- a/Content.Shared/Weapons/Ranged/Systems/SharedTetherGunSystem.cs +++ /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"; -} - -/// -/// Sent from server to client if tether gun is toggled on. -/// -[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; -} - -/// -/// Client can't know the tether's in advance so needs to be told about it for prediction. -/// -[Serializable, NetSerializable] -public sealed class PredictTetherEvent : EntityEventArgs -{ - public EntityUid Entity; -} diff --git a/Resources/Audio/Weapons/attributions.yml b/Resources/Audio/Weapons/attributions.yml index c7be93af11..483a0e1d2a 100644 --- a/Resources/Audio/Weapons/attributions.yml +++ b/Resources/Audio/Weapons/attributions.yml @@ -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" diff --git a/Resources/Audio/Weapons/licenses.txt b/Resources/Audio/Weapons/licenses.txt index b16fcd263a..7b077ffc71 100644 --- a/Resources/Audio/Weapons/licenses.txt +++ b/Resources/Audio/Weapons/licenses.txt @@ -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 index 0000000000..e7d6ceac39 Binary files /dev/null and b/Resources/Audio/Weapons/weoweo.ogg differ diff --git a/Resources/Locale/en-US/research/technologies.ftl b/Resources/Locale/en-US/research/technologies.ftl index e6c9ae3102..c35e97a205 100644 --- a/Resources/Locale/en-US/research/technologies.ftl +++ b/Resources/Locale/en-US/research/technologies.ftl @@ -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 diff --git a/Resources/Prototypes/Entities/Objects/Fun/pai.yml b/Resources/Prototypes/Entities/Objects/Fun/pai.yml index 6f34e113fa..29d25a621e 100644 --- a/Resources/Prototypes/Entities/Objects/Fun/pai.yml +++ b/Resources/Prototypes/Entities/Objects/Fun/pai.yml @@ -16,7 +16,6 @@ - key: enum.InstrumentUiKey.Key type: InstrumentBoundUserInterface - type: Sprite - netsync: false sprite: Objects/Fun/pai.rsi layers: - state: pai-base diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Launchers/launchers.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Launchers/launchers.yml index ade1337c79..4356efa474 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Launchers/launchers.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Launchers/launchers.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Objects/base_item.yml b/Resources/Prototypes/Entities/Objects/base_item.yml index ca0c34f72e..b8f71ee236 100644 --- a/Resources/Prototypes/Entities/Objects/base_item.yml +++ b/Resources/Prototypes/Entities/Objects/base_item.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Virtual/tether.yml b/Resources/Prototypes/Entities/Virtual/tether.yml index 4c6bd86864..ce953854d6 100644 --- a/Resources/Prototypes/Entities/Virtual/tether.yml +++ b/Resources/Prototypes/Entities/Virtual/tether.yml @@ -4,6 +4,7 @@ components: - type: Physics bodyType: Dynamic + sleepingAllowed: false - type: Fixtures fixtures: tether: diff --git a/Resources/Prototypes/Research/experimental.yml b/Resources/Prototypes/Research/experimental.yml index edac88d2a4..f6f940a997 100644 --- a/Resources/Prototypes/Research/experimental.yml +++ b/Resources/Prototypes/Research/experimental.yml @@ -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 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 index 0000000000..84cabefc32 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 index 0000000000..dac93929b3 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 index 0000000000..97c360c59a 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 index 0000000000..5d314fb76d 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 index 0000000000..1b97553546 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 index 0000000000..913d556380 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 index 0000000000..4d8baad4cf --- /dev/null +++ b/Resources/Textures/Objects/Weapons/Guns/Launchers/tether_gun.rsi/meta.json @@ -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