diff --git a/Content.Client/Physics/JointVisualsOverlay.cs b/Content.Client/Physics/JointVisualsOverlay.cs new file mode 100644 index 0000000000..5362a848d5 --- /dev/null +++ b/Content.Client/Physics/JointVisualsOverlay.cs @@ -0,0 +1,74 @@ +using Content.Shared.Physics; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Shared.Enums; +using Robust.Shared.Physics; +using Robust.Shared.Physics.Dynamics.Joints; + +namespace Content.Client.Physics; + +/// +/// Draws a texture on top of a joint. +/// +public sealed class JointVisualsOverlay : Overlay +{ + public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV; + + private IEntityManager _entManager; + + private HashSet _drawn = new(); + + public JointVisualsOverlay(IEntityManager entManager) + { + _entManager = entManager; + } + + protected override void Draw(in OverlayDrawArgs args) + { + _drawn.Clear(); + var worldHandle = args.WorldHandle; + + var spriteSystem = _entManager.System(); + var xformSystem = _entManager.System(); + var joints = _entManager.EntityQueryEnumerator(); + var xformQuery = _entManager.GetEntityQuery(); + + while (joints.MoveNext(out var visuals, out var xform)) + { + if (xform.MapID != args.MapId) + continue; + + var other = visuals.Target; + + if (!xformQuery.TryGetComponent(other, out var otherXform)) + continue; + + if (xform.MapID != otherXform.MapID) + continue; + + var texture = spriteSystem.Frame0(visuals.Sprite); + var width = texture.Width / (float) EyeManager.PixelsPerMeter; + + var coordsA = xform.Coordinates; + var coordsB = otherXform.Coordinates; + + var rotA = xform.LocalRotation; + var rotB = otherXform.LocalRotation; + + coordsA = coordsA.Offset(rotA.RotateVec(visuals.OffsetA)); + coordsB = coordsB.Offset(rotB.RotateVec(visuals.OffsetB)); + + var posA = coordsA.ToMapPos(_entManager, xformSystem); + var posB = coordsB.ToMapPos(_entManager, xformSystem); + var diff = (posB - posA); + var length = diff.Length; + + var midPoint = diff / 2f + posA; + var angle = (posB - posA).ToWorldAngle(); + var box = new Box2(-width / 2f, -length / 2f, width / 2f, length / 2f); + var rotate = new Box2Rotated(box.Translated(midPoint), angle, midPoint); + + worldHandle.DrawTextureRect(texture, rotate); + } + } +} diff --git a/Content.Client/Physics/JointVisualsSystem.cs b/Content.Client/Physics/JointVisualsSystem.cs new file mode 100644 index 0000000000..2e4e2e4c61 --- /dev/null +++ b/Content.Client/Physics/JointVisualsSystem.cs @@ -0,0 +1,20 @@ +using Robust.Client.Graphics; + +namespace Content.Client.Physics; + +public sealed class JointVisualsSystem : EntitySystem +{ + [Dependency] private readonly IOverlayManager _overlay = default!; + + public override void Initialize() + { + base.Initialize(); + _overlay.AddOverlay(new JointVisualsOverlay(EntityManager)); + } + + public override void Shutdown() + { + base.Shutdown(); + _overlay.RemoveOverlay(); + } +} diff --git a/Content.Client/Weapons/Misc/GrapplingGunSystem.cs b/Content.Client/Weapons/Misc/GrapplingGunSystem.cs new file mode 100644 index 0000000000..b54b11ee09 --- /dev/null +++ b/Content.Client/Weapons/Misc/GrapplingGunSystem.cs @@ -0,0 +1,58 @@ +using System.Net; +using Content.Client.Hands.Systems; +using Content.Shared.CombatMode; +using Content.Shared.Weapons.Misc; +using Content.Shared.Weapons.Ranged.Components; +using Robust.Client.GameObjects; +using Robust.Client.Player; +using Robust.Shared.Input; +using Robust.Shared.Physics; +using Robust.Shared.Physics.Dynamics.Joints; + +namespace Content.Client.Weapons.Misc; + +public sealed class GrapplingGunSystem : SharedGrapplingGunSystem +{ + [Dependency] private readonly HandsSystem _hands = default!; + [Dependency] private readonly InputSystem _input = default!; + [Dependency] private readonly IPlayerManager _player = default!; + + public override void Update(float frameTime) + { + base.Update(frameTime); + + // Oh boy another input handler. + // If someone thinks of a better way to unify this please tell me. + if (!Timing.IsFirstTimePredicted) + return; + + var local = _player.LocalPlayer?.ControlledEntity; + var handUid = _hands.GetActiveHandEntity(); + + if (!TryComp(handUid, out var grappling)) + return; + + if (!TryComp(handUid, out var jointComp) || + !jointComp.GetJoints.TryGetValue(GrapplingJoint, out var joint) || + joint is not DistanceJoint distance) + { + return; + } + + if (distance.MaxLength <= distance.MinLength) + return; + + var reelKey = _input.CmdStates.GetState(EngineKeyFunctions.UseSecondary) == BoundKeyState.Down; + + if (!TryComp(local, out var combatMode) || + !combatMode.IsInCombatMode) + { + reelKey = false; + } + + if (grappling.Reeling == reelKey) + return; + + RaisePredictiveEvent(new RequestGrapplingReelMessage(reelKey)); + } +} diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.Ballistic.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.Ballistic.cs index 3c4f3a9ffd..bb1754f06c 100644 --- a/Content.Client/Weapons/Ranged/Systems/GunSystem.Ballistic.cs +++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.Ballistic.cs @@ -1,4 +1,5 @@ using Content.Shared.Weapons.Ranged.Components; +using Content.Shared.Weapons.Ranged.Events; using Robust.Shared.Map; namespace Content.Client.Weapons.Ranged.Systems; @@ -19,9 +20,10 @@ public sealed partial class GunSystem } } - protected override void Cycle(BallisticAmmoProviderComponent component, MapCoordinates coordinates) + protected override void Cycle(EntityUid uid, BallisticAmmoProviderComponent component, MapCoordinates coordinates) { - if (!Timing.IsFirstTimePredicted) return; + if (!Timing.IsFirstTimePredicted) + return; EntityUid? ent = null; @@ -43,5 +45,8 @@ public sealed partial class GunSystem if (ent != null && ent.Value.IsClientSide()) Del(ent.Value); + + var cycledEvent = new GunCycledEvent(); + RaiseLocalEvent(uid, ref cycledEvent); } } diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.cs index 191c2b1089..6132fdae67 100644 --- a/Content.Client/Weapons/Ranged/Systems/GunSystem.cs +++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.cs @@ -173,8 +173,10 @@ public sealed partial class GunSystem : SharedGunSystem } public override void Shoot(EntityUid gunUid, GunComponent gun, List<(EntityUid? Entity, IShootable Shootable)> ammo, - EntityCoordinates fromCoordinates, EntityCoordinates toCoordinates, EntityUid? user = null, bool throwItems = false) + EntityCoordinates fromCoordinates, EntityCoordinates toCoordinates, out bool userImpulse, EntityUid? user = null, bool throwItems = false) { + userImpulse = true; + // Rather than splitting client / server for every ammo provider it's easier // to just delete the spawned entities. This is for programmer sanity despite the wasted perf. // This also means any ammo specific stuff can be grabbed as necessary. @@ -207,6 +209,7 @@ public sealed partial class GunSystem : SharedGunSystem } else { + userImpulse = false; Audio.PlayPredicted(gun.SoundEmpty, gunUid, user); } diff --git a/Content.Server/Singularity/EntitySystems/EmitterSystem.cs b/Content.Server/Singularity/EntitySystems/EmitterSystem.cs index fb69f6b6b6..72c6be85df 100644 --- a/Content.Server/Singularity/EntitySystems/EmitterSystem.cs +++ b/Content.Server/Singularity/EntitySystems/EmitterSystem.cs @@ -293,7 +293,7 @@ namespace Content.Server.Singularity.EntitySystems _projectile.SetShooter(proj, uid); var targetPos = new EntityCoordinates(uid, (0, -1)); - _gun.Shoot(uid, guncomp, ent, xform.Coordinates, targetPos); + _gun.Shoot(uid, guncomp, ent, xform.Coordinates, targetPos, out _); } private void UpdateAppearance(EmitterComponent component) diff --git a/Content.Server/Weapons/Misc/GrapplingGunSystem.cs b/Content.Server/Weapons/Misc/GrapplingGunSystem.cs new file mode 100644 index 0000000000..3ece50af59 --- /dev/null +++ b/Content.Server/Weapons/Misc/GrapplingGunSystem.cs @@ -0,0 +1,8 @@ +using Content.Shared.Weapons.Misc; + +namespace Content.Server.Weapons.Misc; + +public sealed class GrapplingGunSystem : SharedGrapplingGunSystem +{ + +} diff --git a/Content.Server/Weapons/Ranged/Systems/GunSystem.Ballistic.cs b/Content.Server/Weapons/Ranged/Systems/GunSystem.Ballistic.cs index f6eaacd476..3a28c7cdb1 100644 --- a/Content.Server/Weapons/Ranged/Systems/GunSystem.Ballistic.cs +++ b/Content.Server/Weapons/Ranged/Systems/GunSystem.Ballistic.cs @@ -1,11 +1,12 @@ using Content.Shared.Weapons.Ranged.Components; +using Content.Shared.Weapons.Ranged.Events; using Robust.Shared.Map; namespace Content.Server.Weapons.Ranged.Systems; public sealed partial class GunSystem { - protected override void Cycle(BallisticAmmoProviderComponent component, MapCoordinates coordinates) + protected override void Cycle(EntityUid uid, BallisticAmmoProviderComponent component, MapCoordinates coordinates) { EntityUid? ent = null; @@ -27,5 +28,8 @@ public sealed partial class GunSystem if (ent != null) EjectCartridge(ent.Value); + + var cycledEvent = new GunCycledEvent(); + RaiseLocalEvent(uid, ref cycledEvent); } } diff --git a/Content.Server/Weapons/Ranged/Systems/GunSystem.cs b/Content.Server/Weapons/Ranged/Systems/GunSystem.cs index 9831b0643e..5c90d5ec32 100644 --- a/Content.Server/Weapons/Ranged/Systems/GunSystem.cs +++ b/Content.Server/Weapons/Ranged/Systems/GunSystem.cs @@ -67,8 +67,10 @@ public sealed partial class GunSystem : SharedGunSystem } public override void Shoot(EntityUid gunUid, GunComponent gun, List<(EntityUid? Entity, IShootable Shootable)> ammo, - EntityCoordinates fromCoordinates, EntityCoordinates toCoordinates, EntityUid? user = null, bool throwItems = false) + EntityCoordinates fromCoordinates, EntityCoordinates toCoordinates, out bool userImpulse, EntityUid? user = null, bool throwItems = false) { + userImpulse = true; + // Try a clumsy roll // TODO: Who put this here if (TryComp(user, out var clumsy)) @@ -88,6 +90,7 @@ public sealed partial class GunSystem : SharedGunSystem PopupSystem.PopupEntity(Loc.GetString("gun-clumsy"), user.Value); _adminLogger.Add(LogType.EntityDelete, LogImpact.Medium, $"Clumsy fire by {ToPrettyString(user.Value)} deleted {ToPrettyString(gunUid)}"); Del(gunUid); + userImpulse = false; return; } } @@ -161,6 +164,7 @@ public sealed partial class GunSystem : SharedGunSystem } else { + userImpulse = false; Audio.PlayPredicted(gun.SoundEmpty, gunUid, user); } diff --git a/Content.Shared/Movement/Events/WeightlessMoveEvent.cs b/Content.Shared/Movement/Events/WeightlessMoveEvent.cs index 73eac0d306..97e851d331 100644 --- a/Content.Shared/Movement/Events/WeightlessMoveEvent.cs +++ b/Content.Shared/Movement/Events/WeightlessMoveEvent.cs @@ -4,11 +4,7 @@ namespace Content.Shared.Movement.Events; /// Raised on an entity to check if it can move while weightless. /// [ByRefEvent] -public struct CanWeightlessMoveEvent +public record struct CanWeightlessMoveEvent(EntityUid Uid) { public bool CanMove = false; - - public CanWeightlessMoveEvent() - { - } } diff --git a/Content.Shared/Movement/Systems/SharedMoverController.cs b/Content.Shared/Movement/Systems/SharedMoverController.cs index ba52975105..b9e4bafb1a 100644 --- a/Content.Shared/Movement/Systems/SharedMoverController.cs +++ b/Content.Shared/Movement/Systems/SharedMoverController.cs @@ -185,8 +185,8 @@ namespace Content.Shared.Movement.Systems if (!touching) { - var ev = new CanWeightlessMoveEvent(); - RaiseLocalEvent(uid, ref ev); + var ev = new CanWeightlessMoveEvent(uid); + RaiseLocalEvent(uid, ref ev, true); // No gravity: is our entity touching anything? touching = ev.CanMove; diff --git a/Content.Shared/Physics/JointVisualsComponent.cs b/Content.Shared/Physics/JointVisualsComponent.cs new file mode 100644 index 0000000000..2394f2f6ff --- /dev/null +++ b/Content.Shared/Physics/JointVisualsComponent.cs @@ -0,0 +1,29 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Utility; + +namespace Content.Shared.Physics; + +/// +/// Just draws a generic line between this entity and the target. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class JointVisualsComponent : Component +{ + [ViewVariables(VVAccess.ReadWrite), DataField("sprite", required: true), AutoNetworkedField] + public SpriteSpecifier Sprite = default!; + + [ViewVariables(VVAccess.ReadWrite), DataField("target"), AutoNetworkedField] + public EntityUid? Target; + + /// + /// Offset from Body A. + /// + [ViewVariables(VVAccess.ReadWrite), DataField("offsetA"), AutoNetworkedField] + public Vector2 OffsetA; + + /// + /// Offset from Body B. + /// + [ViewVariables(VVAccess.ReadWrite), DataField("offsetB"), AutoNetworkedField] + public Vector2 OffsetB; +} diff --git a/Content.Shared/Projectiles/EmbeddableProjectileComponent.cs b/Content.Shared/Projectiles/EmbeddableProjectileComponent.cs new file mode 100644 index 0000000000..d5cd1ef76f --- /dev/null +++ b/Content.Shared/Projectiles/EmbeddableProjectileComponent.cs @@ -0,0 +1,12 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Weapons.Ranged.Components; + +/// +/// Embeds this entity inside of the hit target. +/// +[RegisterComponent, NetworkedComponent] +public sealed class EmbeddableProjectileComponent : Component +{ + +} diff --git a/Content.Shared/Projectiles/ProjectileEmbedEvent.cs b/Content.Shared/Projectiles/ProjectileEmbedEvent.cs new file mode 100644 index 0000000000..4dc9b9841c --- /dev/null +++ b/Content.Shared/Projectiles/ProjectileEmbedEvent.cs @@ -0,0 +1,7 @@ +namespace Content.Shared.Projectiles; + +/// +/// Raised directed on an entity when it embeds into something. +/// +[ByRefEvent] +public readonly record struct ProjectileEmbedEvent(EntityUid Shooter, EntityUid Weapon, EntityUid Embedded); diff --git a/Content.Shared/Projectiles/SharedProjectileSystem.cs b/Content.Shared/Projectiles/SharedProjectileSystem.cs index 08b974cfda..cd01b71cc0 100644 --- a/Content.Shared/Projectiles/SharedProjectileSystem.cs +++ b/Content.Shared/Projectiles/SharedProjectileSystem.cs @@ -1,6 +1,9 @@ using Content.Shared.Projectiles; +using Content.Shared.Weapons.Ranged.Components; using Robust.Shared.Map; +using Robust.Shared.Physics; using Robust.Shared.Physics.Events; +using Robust.Shared.Physics.Systems; using Robust.Shared.Serialization; namespace Content.Shared.Projectiles @@ -9,10 +12,26 @@ namespace Content.Shared.Projectiles { public const string ProjectileFixture = "projectile"; + [Dependency] private readonly SharedPhysicsSystem _physics = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + public override void Initialize() { base.Initialize(); SubscribeLocalEvent(PreventCollision); + SubscribeLocalEvent(OnEmbedCollide); + } + + private void OnEmbedCollide(EntityUid uid, EmbeddableProjectileComponent component, ref StartCollideEvent args) + { + if (!TryComp(uid, out var projectile)) + return; + + _physics.SetLinearVelocity(uid, Vector2.Zero, body: args.OurBody); + _physics.SetBodyType(uid, BodyType.Static, body: args.OurBody); + _transform.SetParent(uid, args.OtherEntity); + var ev = new ProjectileEmbedEvent(projectile.Shooter, projectile.Weapon, args.OtherEntity); + RaiseLocalEvent(uid, ref ev); } private void PreventCollision(EntityUid uid, ProjectileComponent component, ref PreventCollideEvent args) @@ -25,7 +44,8 @@ namespace Content.Shared.Projectiles public void SetShooter(ProjectileComponent component, EntityUid uid) { - if (component.Shooter == uid) return; + if (component.Shooter == uid) + return; component.Shooter = uid; Dirty(component); diff --git a/Content.Shared/Pulling/Systems/SharedPullingSystem.cs b/Content.Shared/Pulling/Systems/SharedPullingSystem.cs index c655687a7c..6bacc338ec 100644 --- a/Content.Shared/Pulling/Systems/SharedPullingSystem.cs +++ b/Content.Shared/Pulling/Systems/SharedPullingSystem.cs @@ -12,6 +12,8 @@ using Robust.Shared.Containers; using Robust.Shared.Input.Binding; using Robust.Shared.Map; using Robust.Shared.Physics; +using Robust.Shared.Physics.Events; +using Robust.Shared.Physics.Systems; using Robust.Shared.Players; namespace Content.Shared.Pulling @@ -22,6 +24,7 @@ namespace Content.Shared.Pulling [Dependency] private readonly SharedPullingStateManagementSystem _pullSm = default!; [Dependency] private readonly SharedGravitySystem _gravity = default!; [Dependency] private readonly AlertsSystem _alertsSystem = default!; + [Dependency] private readonly SharedJointSystem _joints = default!; /// /// A mapping of pullers to the entity that they are pulling. @@ -45,6 +48,7 @@ namespace Content.Shared.Pulling SubscribeLocalEvent(OnPullStopped); SubscribeLocalEvent(HandleContainerInsert); SubscribeLocalEvent(OnJointRemoved); + SubscribeLocalEvent(OnPullableCollisionChange); SubscribeLocalEvent(PullableHandlePullStarted); SubscribeLocalEvent(PullableHandlePullStopped); @@ -56,6 +60,14 @@ namespace Content.Shared.Pulling .Register(); } + private void OnPullableCollisionChange(EntityUid uid, SharedPullableComponent component, ref CollisionChangeEvent args) + { + if (component.PullJointId != null && !args.CanCollide) + { + _joints.RemoveJoint(uid, component.PullJointId); + } + } + private void OnJointRemoved(EntityUid uid, SharedPullableComponent component, JointRemovedEvent args) { if (component.Puller != args.OtherBody.Owner) diff --git a/Content.Shared/Weapons/Misc/GrapplingProjectileComponent.cs b/Content.Shared/Weapons/Misc/GrapplingProjectileComponent.cs new file mode 100644 index 0000000000..5dcc7702ee --- /dev/null +++ b/Content.Shared/Weapons/Misc/GrapplingProjectileComponent.cs @@ -0,0 +1,9 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Weapons.Misc; + +[RegisterComponent, NetworkedComponent] +public sealed class GrapplingProjectileComponent : Component +{ + +} diff --git a/Content.Shared/Weapons/Misc/SharedGrapplingGunSystem.cs b/Content.Shared/Weapons/Misc/SharedGrapplingGunSystem.cs new file mode 100644 index 0000000000..0787c4a28d --- /dev/null +++ b/Content.Shared/Weapons/Misc/SharedGrapplingGunSystem.cs @@ -0,0 +1,230 @@ +using Content.Shared.CombatMode; +using Content.Shared.Hands; +using Content.Shared.Hands.Components; +using Content.Shared.Interaction; +using Content.Shared.Movement.Events; +using Content.Shared.Physics; +using Content.Shared.Projectiles; +using Content.Shared.Timing; +using Content.Shared.Weapons.Ranged.Components; +using Content.Shared.Weapons.Ranged.Systems; +using Robust.Shared.Network; +using Robust.Shared.Physics; +using Robust.Shared.Physics.Components; +using Robust.Shared.Physics.Dynamics.Joints; +using Robust.Shared.Physics.Systems; +using Robust.Shared.Serialization; +using Robust.Shared.Timing; +using Robust.Shared.Utility; + +namespace Content.Shared.Weapons.Misc; + +public abstract class SharedGrapplingGunSystem : EntitySystem +{ + [Dependency] protected readonly IGameTiming Timing = default!; + [Dependency] private readonly INetManager _netManager = 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] private readonly UseDelaySystem _delay = default!; + + public const string GrapplingJoint = "grappling"; + + public const float ReelRate = 2.5f; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnGrappleCollide); + SubscribeLocalEvent(OnWeightlessMove); + SubscribeAllEvent(OnGrapplingReel); + + SubscribeLocalEvent(OnGrapplingShot); + SubscribeLocalEvent(OnGunActivate); + SubscribeLocalEvent(OnGrapplingDeselected); + } + + private void OnGrapplingShot(EntityUid uid, GrapplingGunComponent component, ref GunShotEvent args) + { + foreach (var (shotUid, _) in args.Ammo) + { + if (!HasComp(shotUid)) + continue; + + // At least show the visuals. + component.Projectile = shotUid.Value; + Dirty(component); + var visuals = EnsureComp(shotUid.Value); + visuals.Sprite = + new SpriteSpecifier.Rsi(new ResPath("Objects/Weapons/Guns/Launchers/grappling_gun.rsi"), "rope"); + visuals.OffsetA = new Vector2(0f, 0.5f); + visuals.Target = uid; + Dirty(visuals); + } + + TryComp(uid, out var appearance); + _appearance.SetData(uid, SharedTetherGunSystem.TetherVisualsStatus.Key, false, appearance); + } + + private void OnGrapplingDeselected(EntityUid uid, GrapplingGunComponent component, HandDeselectedEvent args) + { + SetReeling(uid, component, false, args.User); + } + + private void OnGrapplingReel(RequestGrapplingReelMessage msg, EntitySessionEventArgs args) + { + var player = args.SenderSession.AttachedEntity; + if (!TryComp(player, out var hands) || + !TryComp(hands.ActiveHandEntity, out var grappling)) + { + return; + } + + if (msg.Reeling && + (!TryComp(player, out var combatMode) || + !combatMode.IsInCombatMode)) + { + return; + } + + SetReeling(hands.ActiveHandEntity.Value, grappling, msg.Reeling, player.Value); + } + + private void OnWeightlessMove(ref CanWeightlessMoveEvent ev) + { + if (ev.CanMove || !TryComp(ev.Uid, out var relayComp)) + return; + + foreach (var relay in relayComp.Relayed) + { + if (TryComp(relay, out var jointRelay) && jointRelay.GetJoints.ContainsKey(GrapplingJoint)) + { + ev.CanMove = true; + return; + } + } + } + + private void OnGunActivate(EntityUid uid, GrapplingGunComponent component, ActivateInWorldEvent args) + { + if (!Timing.IsFirstTimePredicted || _delay.ActiveDelay(uid)) + return; + + _delay.BeginDelay(uid); + _audio.PlayPredicted(component.CycleSound, uid, args.User); + + TryComp(uid, out var appearance); + _appearance.SetData(uid, SharedTetherGunSystem.TetherVisualsStatus.Key, true, appearance); + SetReeling(uid, component, false, args.User); + + if (!Deleted(component.Projectile)) + { + if (_netManager.IsServer) + { + QueueDel(component.Projectile.Value); + } + + component.Projectile = null; + Dirty(component); + } + } + + private void SetReeling(EntityUid uid, GrapplingGunComponent component, bool value, EntityUid? user) + { + if (component.Reeling == value) + return; + + if (value) + { + if (Timing.IsFirstTimePredicted) + component.Stream = _audio.PlayPredicted(component.ReelSound, uid, user); + } + else + { + if (Timing.IsFirstTimePredicted) + { + component.Stream?.Stop(); + component.Stream = null; + } + } + + component.Reeling = value; + Dirty(component); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var grappling)) + { + if (!grappling.Reeling) + { + if (Timing.IsFirstTimePredicted) + { + // Just in case. + grappling.Stream?.Stop(); + grappling.Stream = null; + } + + continue; + } + + if (!TryComp(uid, out var jointComp) || + !jointComp.GetJoints.TryGetValue(GrapplingJoint, out var joint) || + joint is not DistanceJoint distance) + { + SetReeling(uid, grappling, false, null); + continue; + } + + // TODO: This should be on engine. + distance.MaxLength = MathF.Max(distance.MinLength, distance.MaxLength - ReelRate * frameTime); + distance.Length = MathF.Min(distance.MaxLength, distance.Length); + + _physics.WakeBody(joint.BodyAUid); + _physics.WakeBody(joint.BodyBUid); + + if (jointComp.Relay != null) + { + _physics.WakeBody(jointComp.Relay.Value); + } + + Dirty(jointComp); + + if (distance.MaxLength.Equals(distance.MinLength)) + { + SetReeling(uid, grappling, false, null); + } + } + } + + private void OnGrappleCollide(EntityUid uid, GrapplingProjectileComponent component, ref ProjectileEmbedEvent args) + { + if (!Timing.IsFirstTimePredicted) + return; + + var jointComp = EnsureComp(uid); + var joint = _joints.CreateDistanceJoint(uid, args.Weapon, anchorA: new Vector2(0f, 0.5f), id: GrapplingJoint); + joint.MaxLength = joint.Length + 0.2f; + joint.Stiffness = 1f; + joint.MinLength = 0.35f; + // Setting velocity directly for mob movement fucks this so need to make them aware of it. + // joint.Breakpoint = 4000f; + Dirty(jointComp); + } + + [Serializable, NetSerializable] + protected sealed class RequestGrapplingReelMessage : EntityEventArgs + { + public bool Reeling; + + public RequestGrapplingReelMessage(bool reeling) + { + Reeling = reeling; + } + } +} diff --git a/Content.Shared/Weapons/Misc/SharedTetherGunSystem.Force.cs b/Content.Shared/Weapons/Misc/SharedTetherGunSystem.Force.cs index 9b5665f1c4..c76d60888c 100644 --- a/Content.Shared/Weapons/Misc/SharedTetherGunSystem.Force.cs +++ b/Content.Shared/Weapons/Misc/SharedTetherGunSystem.Force.cs @@ -43,7 +43,7 @@ public abstract partial class SharedTetherGunSystem { // Pickup if (TryTether(uid, args.Target.Value, args.User, component)) - TransformSystem.SetCoordinates(component.TetherEntity!.Value, new EntityCoordinates(uid, new Vector2(0.0f, -0.8f))); + TransformSystem.SetCoordinates(component.TetherEntity!.Value, new EntityCoordinates(uid, new Vector2(0f, 0f))); } } diff --git a/Content.Shared/Weapons/Ranged/Components/BallisticAmmoProviderComponent.cs b/Content.Shared/Weapons/Ranged/Components/BallisticAmmoProviderComponent.cs index 9c4b9045cc..672d3da5f8 100644 --- a/Content.Shared/Weapons/Ranged/Components/BallisticAmmoProviderComponent.cs +++ b/Content.Shared/Weapons/Ranged/Components/BallisticAmmoProviderComponent.cs @@ -22,6 +22,8 @@ public sealed partial class BallisticAmmoProviderComponent : Component [ViewVariables(VVAccess.ReadWrite), DataField("capacity")] public int Capacity = 30; + public int Count => UnspawnedCount + Container.ContainedEntities.Count; + [ViewVariables(VVAccess.ReadWrite), DataField("unspawnedCount")] [AutoNetworkedField] public int UnspawnedCount; diff --git a/Content.Shared/Weapons/Ranged/Components/GrapplingGunComponent.cs b/Content.Shared/Weapons/Ranged/Components/GrapplingGunComponent.cs new file mode 100644 index 0000000000..51f3e835b7 --- /dev/null +++ b/Content.Shared/Weapons/Ranged/Components/GrapplingGunComponent.cs @@ -0,0 +1,28 @@ +using Robust.Shared.Audio; +using Robust.Shared.GameStates; + +namespace Content.Shared.Weapons.Ranged.Components; + +// I have tried to make this as generic as possible but "delete joint on cycle / right-click reels in" is very specific behavior. +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class GrapplingGunComponent : Component +{ + [DataField("jointId"), AutoNetworkedField] + public string Joint = string.Empty; + + [DataField("projectile")] public EntityUid? Projectile; + + [ViewVariables(VVAccess.ReadWrite), DataField("reeling"), AutoNetworkedField] + public bool Reeling; + + [ViewVariables(VVAccess.ReadWrite), DataField("reelSound"), AutoNetworkedField] + public SoundSpecifier? ReelSound = new SoundPathSpecifier("/Audio/Weapons/reel.ogg") + { + Params = AudioParams.Default.WithLoop(true) + }; + + [ViewVariables(VVAccess.ReadWrite), DataField("cycleSound"), AutoNetworkedField] + public SoundSpecifier? CycleSound = new SoundPathSpecifier("/Audio/Weapons/Guns/MagIn/kinetic_reload.ogg"); + + public IPlayingAudioStream? Stream; +} diff --git a/Content.Shared/Weapons/Ranged/Components/RechargeCycleAmmoComponent.cs b/Content.Shared/Weapons/Ranged/Components/RechargeCycleAmmoComponent.cs new file mode 100644 index 0000000000..70a053e292 --- /dev/null +++ b/Content.Shared/Weapons/Ranged/Components/RechargeCycleAmmoComponent.cs @@ -0,0 +1,12 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Weapons.Ranged.Components; + +/// +/// Recharges ammo upon the gun being cycled. +/// +[RegisterComponent, NetworkedComponent] +public sealed class RechargeCycleAmmoComponent : Component +{ + +} diff --git a/Content.Shared/Weapons/Ranged/Events/GunCycledEvent.cs b/Content.Shared/Weapons/Ranged/Events/GunCycledEvent.cs new file mode 100644 index 0000000000..537db0d6e2 --- /dev/null +++ b/Content.Shared/Weapons/Ranged/Events/GunCycledEvent.cs @@ -0,0 +1,7 @@ +namespace Content.Shared.Weapons.Ranged.Events; + +/// +/// Raised directed on a gun when it cycles. +/// +[ByRefEvent] +public readonly record struct GunCycledEvent; diff --git a/Content.Shared/Weapons/Ranged/Systems/RechargeCycleAmmoSystem.cs b/Content.Shared/Weapons/Ranged/Systems/RechargeCycleAmmoSystem.cs new file mode 100644 index 0000000000..136e9b59b2 --- /dev/null +++ b/Content.Shared/Weapons/Ranged/Systems/RechargeCycleAmmoSystem.cs @@ -0,0 +1,31 @@ +using Content.Shared.Interaction; +using Content.Shared.Weapons.Ranged.Components; + +namespace Content.Shared.Weapons.Ranged.Systems; + +/// +/// Recharges ammo whenever the gun is cycled. +/// +public sealed class RechargeCycleAmmoSystem : EntitySystem +{ + [Dependency] private readonly SharedGunSystem _gun = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnRechargeCycled); + } + + private void OnRechargeCycled(EntityUid uid, RechargeCycleAmmoComponent component, ActivateInWorldEvent args) + { + if (!TryComp(uid, out var basic) || args.Handled) + return; + + if (basic.Count >= basic.Capacity || basic.Count == null) + return; + + _gun.UpdateBasicEntityAmmoCount(uid, basic.Count.Value + 1, basic); + Dirty(basic); + args.Handled = true; + } +} diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs index b529a978fa..abafb4c95f 100644 --- a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs +++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs @@ -162,7 +162,7 @@ public abstract partial class SharedGunSystem var shots = GetBallisticShots(component); component.Cycled = true; - Cycle(component, coordinates); + Cycle(uid, component, coordinates); var text = Loc.GetString(shots == 0 ? "gun-ballistic-cycled-empty" : "gun-ballistic-cycled"); @@ -171,7 +171,7 @@ public abstract partial class SharedGunSystem UpdateAmmoCount(uid); } - protected abstract void Cycle(BallisticAmmoProviderComponent component, MapCoordinates coordinates); + protected abstract void Cycle(EntityUid uid, BallisticAmmoProviderComponent component, MapCoordinates coordinates); private void OnBallisticInit(EntityUid uid, BallisticAmmoProviderComponent component, ComponentInit args) { diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs index 89068db8ae..22243015f1 100644 --- a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs +++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs @@ -313,15 +313,16 @@ public abstract partial class SharedGunSystem : EntitySystem } // Shoot confirmed - sounds also played here in case it's invalid (e.g. cartridge already spent). - Shoot(gunUid, gun, ev.Ammo, fromCoordinates, toCoordinates.Value, user, throwItems: attemptEv.ThrowItems); - var shotEv = new GunShotEvent(user); + Shoot(gunUid, gun, ev.Ammo, fromCoordinates, toCoordinates.Value, out var userImpulse, user, throwItems: attemptEv.ThrowItems); + var shotEv = new GunShotEvent(user, ev.Ammo); RaiseLocalEvent(gunUid, ref shotEv); - // Projectiles cause impulses especially important in non gravity environments - if (TryComp(user, out var userPhysics)) + + if (userImpulse && TryComp(user, out var userPhysics)) { if (_gravity.IsWeightless(user, userPhysics)) CauseImpulse(fromCoordinates, toCoordinates.Value, user, userPhysics); } + Dirty(gun); } @@ -331,11 +332,12 @@ public abstract partial class SharedGunSystem : EntitySystem EntityUid ammo, EntityCoordinates fromCoordinates, EntityCoordinates toCoordinates, + out bool userImpulse, EntityUid? user = null, bool throwItems = false) { var shootable = EnsureComp(ammo); - Shoot(gunUid, gun, new List<(EntityUid? Entity, IShootable Shootable)>(1) { (ammo, shootable) }, fromCoordinates, toCoordinates, user, throwItems); + Shoot(gunUid, gun, new List<(EntityUid? Entity, IShootable Shootable)>(1) { (ammo, shootable) }, fromCoordinates, toCoordinates, out userImpulse, user, throwItems); } public abstract void Shoot( @@ -344,6 +346,7 @@ public abstract partial class SharedGunSystem : EntitySystem List<(EntityUid? Entity, IShootable Shootable)> ammo, EntityCoordinates fromCoordinates, EntityCoordinates toCoordinates, + out bool userImpulse, EntityUid? user = null, bool throwItems = false); @@ -436,7 +439,7 @@ public record struct AttemptShootEvent(EntityUid User, string? Message, bool Can /// /// The user that fired this gun. [ByRefEvent] -public record struct GunShotEvent(EntityUid User); +public record struct GunShotEvent(EntityUid User, List<(EntityUid? Uid, IShootable Shootable)> Ammo); public enum EffectLayers : byte { diff --git a/Content.Shared/Wieldable/WieldableSystem.cs b/Content.Shared/Wieldable/WieldableSystem.cs index ea1ac7e42a..61c167fda7 100644 --- a/Content.Shared/Wieldable/WieldableSystem.cs +++ b/Content.Shared/Wieldable/WieldableSystem.cs @@ -9,7 +9,6 @@ using Content.Shared.Popups; using Content.Shared.Verbs; using Content.Shared.Weapons.Melee.Events; using Content.Shared.Weapons.Melee.Components; -using Content.Shared.Weapons.Melee.Events; using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Weapons.Ranged.Systems; using Content.Shared.Wieldable.Components; diff --git a/Resources/Audio/Weapons/Guns/Gunshots/attributions.yml b/Resources/Audio/Weapons/Guns/Gunshots/attributions.yml index 4bbd672461..8bc504ec81 100644 --- a/Resources/Audio/Weapons/Guns/Gunshots/attributions.yml +++ b/Resources/Audio/Weapons/Guns/Gunshots/attributions.yml @@ -2,3 +2,8 @@ license: "CC0-1.0" copyright: "Watering by elittle13. Converted to .OGG and MONO by EmoGarbage404 (github)" source: "https://freesound.org/people/elittle13/sounds/568558" + +- files: ["harpoon.ogg"] + license: "CC0-1.0" + copyright: "grappling hook by 16bitstudios. Converted to .OGG and MONO by metalgearsloth" + source: "https://freesound.org/people/16bitstudios/sounds/541975/" diff --git a/Resources/Audio/Weapons/Guns/Gunshots/harpoon.ogg b/Resources/Audio/Weapons/Guns/Gunshots/harpoon.ogg new file mode 100644 index 0000000000..11187bac19 Binary files /dev/null and b/Resources/Audio/Weapons/Guns/Gunshots/harpoon.ogg differ diff --git a/Resources/Audio/Weapons/attributions.yml b/Resources/Audio/Weapons/attributions.yml index 483a0e1d2a..00ee4e7790 100644 --- a/Resources/Audio/Weapons/attributions.yml +++ b/Resources/Audio/Weapons/attributions.yml @@ -2,3 +2,8 @@ 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" + +- files: ["reel.ogg"] + license: "CC0-1.0" + copyright: "User tosha73 on freesound.org" + source: "https://freesound.org/people/tosha73/sounds/509902/" diff --git a/Resources/Audio/Weapons/reel.ogg b/Resources/Audio/Weapons/reel.ogg new file mode 100644 index 0000000000..31c00804e8 Binary files /dev/null and b/Resources/Audio/Weapons/reel.ogg differ diff --git a/Resources/Locale/en-US/research/technologies.ftl b/Resources/Locale/en-US/research/technologies.ftl index f64a1c2774..6d56273f60 100644 --- a/Resources/Locale/en-US/research/technologies.ftl +++ b/Resources/Locale/en-US/research/technologies.ftl @@ -34,6 +34,7 @@ research-technology-basic-xenoarcheology = Basic XenoArcheology research-technology-alternative-research = Alternative Research research-technology-magnets-tech = Localized Magnetism research-technology-advanced-parts = Advanced Parts +research-technology-grappling = Grappling research-technology-abnormal-artifact-manipulation = Abnormal Artifact Manipulation research-technology-gravity-manipulation = Gravity Manipulation research-technology-mobile-anomaly-tech = Mobile Anomaly Tech diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Launchers/launchers.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Launchers/launchers.yml index 3c7f670650..cbf7176bd8 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Launchers/launchers.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Launchers/launchers.yml @@ -214,22 +214,54 @@ 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: 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: grappling gun + parent: BaseItem + id: WeaponGrapplingGun + components: + - type: AmmoCounter + - type: GrapplingGun + - type: Gun + soundGunshot: /Audio/Weapons/Guns/Gunshots/harpoon.ogg + fireRate: 0.5 + - type: RechargeCycleAmmo + - type: BasicEntityAmmoProvider + proto: GrapplingHook + capacity: 1 + - type: Sprite + sprite: Objects/Weapons/Guns/Launchers/grappling_gun.rsi + layers: + - state: base + - state: base-unshaded + map: [ "unshaded" ] + shader: unshaded + visible: true + - type: UseDelay + delay: 1.5 - type: Appearance - type: GenericVisualizer visuals: enum.TetherVisualsStatus.Key: unshaded: - True: { visible: true } - False: { visible: false } + True: { state: base-unshaded } + False: { state: base-unshaded-off } # Admeme - type: entity diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml index f59153f320..705da6a081 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml @@ -670,3 +670,38 @@ radius: 1 color: orange energy: 0.5 + +- type: entity + id: GrapplingHook + name: grappling hook + noSpawn: true + components: + - type: EmbeddableProjectile + - type: Clickable + - type: Sprite + noRot: false + sprite: Objects/Weapons/Guns/Launchers/grappling_gun.rsi + layers: + - state: hook + - state: hook-unshaded + shader: unshaded + - type: Physics + bodyType: Dynamic + linearDamping: 0 + angularDamping: 0 + - type: Projectile + deleteOnCollide: false + damage: + types: + Blunt: 0 + - type: Fixtures + fixtures: + projectile: + shape: + !type:PhysShapeAabb + bounds: "-0.1,-0.1,0.1,0.1" + hard: false + mask: + - Impassable + - HighImpassable + - type: GrapplingProjectile diff --git a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml index 5641613d26..8b6e150e4c 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml @@ -243,6 +243,7 @@ - HolofanProjector - WeaponForceGun - WeaponTetherGun + - WeaponGrapplingGun - ClothingBackpackHolding - ClothingBackpackSatchelHolding - ClothingBackpackDuffelHolding diff --git a/Resources/Prototypes/Recipes/Lathes/devices.yml b/Resources/Prototypes/Recipes/Lathes/devices.yml index baa9e8e001..95d0e3086a 100644 --- a/Resources/Prototypes/Recipes/Lathes/devices.yml +++ b/Resources/Prototypes/Recipes/Lathes/devices.yml @@ -120,3 +120,12 @@ Steel: 500 Glass: 400 Silver: 100 + +- type: latheRecipe + id: WeaponGrapplingGun + result: WeaponGrapplingGun + completetime: 5 + materials: + Steel: 500 + Glass: 400 + Gold: 100 diff --git a/Resources/Prototypes/Research/experimental.yml b/Resources/Prototypes/Research/experimental.yml index c2e96fd0a7..2043bc2648 100644 --- a/Resources/Prototypes/Research/experimental.yml +++ b/Resources/Prototypes/Research/experimental.yml @@ -145,6 +145,18 @@ recipeUnlocks: - RPED +- type: technology + id: Grappling + name: research-technology-grappling + icon: + sprite: Objects/Weapons/Guns/Launchers/grappling_gun.rsi + state: base + discipline: Experimental + tier: 2 + cost: 5000 + recipeUnlocks: + - WeaponGrapplingGun + # Tier 3 - type: technology diff --git a/Resources/Textures/Objects/Weapons/Guns/Launchers/grappling_gun.rsi/base-unshaded-off.png b/Resources/Textures/Objects/Weapons/Guns/Launchers/grappling_gun.rsi/base-unshaded-off.png new file mode 100644 index 0000000000..ed7f70c53d Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Launchers/grappling_gun.rsi/base-unshaded-off.png differ diff --git a/Resources/Textures/Objects/Weapons/Guns/Launchers/grappling_gun.rsi/base-unshaded.png b/Resources/Textures/Objects/Weapons/Guns/Launchers/grappling_gun.rsi/base-unshaded.png new file mode 100644 index 0000000000..70be300e8f Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Launchers/grappling_gun.rsi/base-unshaded.png differ diff --git a/Resources/Textures/Objects/Weapons/Guns/Launchers/grappling_gun.rsi/base.png b/Resources/Textures/Objects/Weapons/Guns/Launchers/grappling_gun.rsi/base.png new file mode 100644 index 0000000000..f3019b187b Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Launchers/grappling_gun.rsi/base.png differ diff --git a/Resources/Textures/Objects/Weapons/Guns/Launchers/grappling_gun.rsi/hook-unshaded.png b/Resources/Textures/Objects/Weapons/Guns/Launchers/grappling_gun.rsi/hook-unshaded.png new file mode 100644 index 0000000000..91cdd1d075 Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Launchers/grappling_gun.rsi/hook-unshaded.png differ diff --git a/Resources/Textures/Objects/Weapons/Guns/Launchers/grappling_gun.rsi/hook.png b/Resources/Textures/Objects/Weapons/Guns/Launchers/grappling_gun.rsi/hook.png new file mode 100644 index 0000000000..d58cf6559c Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Launchers/grappling_gun.rsi/hook.png differ diff --git a/Resources/Textures/Objects/Weapons/Guns/Launchers/grappling_gun.rsi/inhand-left-unshaded.png b/Resources/Textures/Objects/Weapons/Guns/Launchers/grappling_gun.rsi/inhand-left-unshaded.png new file mode 100644 index 0000000000..c4bb8235fe Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Launchers/grappling_gun.rsi/inhand-left-unshaded.png differ diff --git a/Resources/Textures/Objects/Weapons/Guns/Launchers/grappling_gun.rsi/inhand-left.png b/Resources/Textures/Objects/Weapons/Guns/Launchers/grappling_gun.rsi/inhand-left.png new file mode 100644 index 0000000000..fe2a3a55c6 Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Launchers/grappling_gun.rsi/inhand-left.png differ diff --git a/Resources/Textures/Objects/Weapons/Guns/Launchers/grappling_gun.rsi/inhand-right-unshaded.png b/Resources/Textures/Objects/Weapons/Guns/Launchers/grappling_gun.rsi/inhand-right-unshaded.png new file mode 100644 index 0000000000..fd20f4db5a Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Launchers/grappling_gun.rsi/inhand-right-unshaded.png differ diff --git a/Resources/Textures/Objects/Weapons/Guns/Launchers/grappling_gun.rsi/inhand-right.png b/Resources/Textures/Objects/Weapons/Guns/Launchers/grappling_gun.rsi/inhand-right.png new file mode 100644 index 0000000000..af27ad1ecb Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Launchers/grappling_gun.rsi/inhand-right.png differ diff --git a/Resources/Textures/Objects/Weapons/Guns/Launchers/grappling_gun.rsi/meta.json b/Resources/Textures/Objects/Weapons/Guns/Launchers/grappling_gun.rsi/meta.json new file mode 100644 index 0000000000..a6d927a54e --- /dev/null +++ b/Resources/Textures/Objects/Weapons/Guns/Launchers/grappling_gun.rsi/meta.json @@ -0,0 +1,45 @@ +{ + "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": "base-unshaded-off" + }, + { + "name": "inhand-left", + "directions": 4 + }, + { + "name": "inhand-right", + "directions": 4 + }, + { + "name": "inhand-left-unshaded", + "directions": 4 + }, + { + "name": "inhand-right-unshaded", + "directions": 4 + }, + { + "name": "rope" + }, + { + "name": "hook" + }, + { + "name": "hook-unshaded" + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Objects/Weapons/Guns/Launchers/grappling_gun.rsi/rope.png b/Resources/Textures/Objects/Weapons/Guns/Launchers/grappling_gun.rsi/rope.png new file mode 100644 index 0000000000..dd1893fcd1 Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Launchers/grappling_gun.rsi/rope.png differ