Add tether gun (#16430)
This commit is contained in:
52
Content.Client/Weapons/Misc/TetherGunOverlay.cs
Normal file
52
Content.Client/Weapons/Misc/TetherGunOverlay.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
103
Content.Client/Weapons/Misc/TetherGunSystem.cs
Normal file
103
Content.Client/Weapons/Misc/TetherGunSystem.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
46
Content.Server/Weapons/Misc/TetherGunSystem.cs
Normal file
46
Content.Server/Weapons/Misc/TetherGunSystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
267
Content.Shared/Weapons/Misc/SharedTetherGunSystem.cs
Normal file
267
Content.Shared/Weapons/Misc/SharedTetherGunSystem.cs
Normal 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,
|
||||
}
|
||||
}
|
||||
58
Content.Shared/Weapons/Misc/TetherGunComponent.cs
Normal file
58
Content.Shared/Weapons/Misc/TetherGunComponent.cs
Normal 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;
|
||||
}
|
||||
16
Content.Shared/Weapons/Misc/TetheredComponent.cs
Normal file
16
Content.Shared/Weapons/Misc/TetheredComponent.cs
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
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"
|
||||
BIN
Resources/Audio/Weapons/weoweo.ogg
Normal file
BIN
Resources/Audio/Weapons/weoweo.ogg
Normal file
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
- key: enum.InstrumentUiKey.Key
|
||||
type: InstrumentBoundUserInterface
|
||||
- type: Sprite
|
||||
netsync: false
|
||||
sprite: Objects/Fun/pai.rsi
|
||||
layers:
|
||||
- state: pai-base
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
components:
|
||||
- type: Physics
|
||||
bodyType: Dynamic
|
||||
sleepingAllowed: false
|
||||
- type: Fixtures
|
||||
fixtures:
|
||||
tether:
|
||||
|
||||
@@ -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 |
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user