diff --git a/Content.Client/Content.Client.csproj b/Content.Client/Content.Client.csproj
index 2228239cdd..06234e9529 100644
--- a/Content.Client/Content.Client.csproj
+++ b/Content.Client/Content.Client.csproj
@@ -82,7 +82,9 @@
+
+
@@ -145,4 +147,4 @@
-
+
\ No newline at end of file
diff --git a/Content.Client/EntryPoint.cs b/Content.Client/EntryPoint.cs
index d3df4a3c74..6f32bed99d 100644
--- a/Content.Client/EntryPoint.cs
+++ b/Content.Client/EntryPoint.cs
@@ -5,12 +5,14 @@ using Content.Client.GameObjects.Components.Construction;
using Content.Client.GameObjects.Components.Power;
using Content.Client.GameObjects.Components.SmoothWalling;
using Content.Client.GameObjects.Components.Storage;
+using Content.Client.GameObjects.Components.Weapons.Ranged;
using Content.Client.GameTicking;
using Content.Client.Input;
using Content.Client.Interfaces;
using Content.Client.Interfaces.GameObjects;
using Content.Client.Interfaces.Parallax;
using Content.Client.Parallax;
+using Content.Shared.GameObjects.Components.Weapons.Ranged;
using Content.Shared.Interfaces;
using SS14.Client;
using SS14.Client.Interfaces;
@@ -52,6 +54,7 @@ namespace Content.Client
factory.RegisterIgnore("Welder");
factory.RegisterIgnore("Wrench");
factory.RegisterIgnore("Crowbar");
+ factory.Register();
factory.RegisterIgnore("HitscanWeapon");
factory.RegisterIgnore("ProjectileWeapon");
factory.RegisterIgnore("Projectile");
diff --git a/Content.Client/GameObjects/Components/Items/ClientHandsComponent.cs b/Content.Client/GameObjects/Components/Items/ClientHandsComponent.cs
index 2411aefa22..17c8e81ec0 100644
--- a/Content.Client/GameObjects/Components/Items/ClientHandsComponent.cs
+++ b/Content.Client/GameObjects/Components/Items/ClientHandsComponent.cs
@@ -27,6 +27,8 @@ namespace Content.Client.GameObjects
[ViewVariables] private ISpriteComponent _sprite;
+ [ViewVariables] public IEntity ActiveHand => GetEntity(ActiveIndex);
+
public override void OnAdd()
{
base.OnAdd();
diff --git a/Content.Client/GameObjects/Components/Weapons/Ranged/ClientRangedWeaponComponent.cs b/Content.Client/GameObjects/Components/Weapons/Ranged/ClientRangedWeaponComponent.cs
new file mode 100644
index 0000000000..397a71cf8f
--- /dev/null
+++ b/Content.Client/GameObjects/Components/Weapons/Ranged/ClientRangedWeaponComponent.cs
@@ -0,0 +1,29 @@
+using System;
+using Content.Shared.GameObjects.Components.Weapons.Ranged;
+using SS14.Shared.Interfaces.Timing;
+using SS14.Shared.IoC;
+using SS14.Shared.Log;
+using SS14.Shared.Map;
+
+namespace Content.Client.GameObjects.Components.Weapons.Ranged
+{
+ public sealed class ClientRangedWeaponComponent : SharedRangedWeaponComponent
+ {
+ private TimeSpan _lastFireTime;
+ private int _tick;
+
+ public void TryFire(GridLocalCoordinates worldPos)
+ {
+ var curTime = IoCManager.Resolve().CurTime;
+ var span = curTime - _lastFireTime;
+ if (span.TotalSeconds < 1 / FireRate)
+ {
+ return;
+ }
+
+ Logger.Debug("Delay: {0}", span.TotalSeconds);
+ _lastFireTime = curTime;
+ SendNetworkMessage(new FireMessage(worldPos, _tick++));
+ }
+ }
+}
diff --git a/Content.Client/GameObjects/EntitySystems/RangedWeaponSystem.cs b/Content.Client/GameObjects/EntitySystems/RangedWeaponSystem.cs
new file mode 100644
index 0000000000..e4d8db6114
--- /dev/null
+++ b/Content.Client/GameObjects/EntitySystems/RangedWeaponSystem.cs
@@ -0,0 +1,76 @@
+using Content.Client.GameObjects.Components.Weapons.Ranged;
+using Content.Client.Interfaces.GameObjects;
+using Content.Shared.Input;
+using SS14.Client.GameObjects.EntitySystems;
+using SS14.Client.Interfaces.Graphics.ClientEye;
+using SS14.Client.Interfaces.Input;
+using SS14.Client.Player;
+using SS14.Shared.GameObjects.Systems;
+using SS14.Shared.Input;
+using SS14.Shared.IoC;
+
+namespace Content.Client.GameObjects.EntitySystems
+{
+ public class RangedWeaponSystem : EntitySystem
+ {
+
+#pragma warning disable 649
+ [Dependency] private readonly IPlayerManager _playerManager;
+ [Dependency] private readonly IEyeManager _eyeManager;
+ [Dependency] private readonly IInputManager _inputManager;
+#pragma warning restore 649
+
+ private InputSystem _inputSystem;
+ private bool _isFirstShot;
+ private bool _blocked;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ IoCManager.InjectDependencies(this);
+ _inputSystem = EntitySystemManager.GetEntitySystem();
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ var canFireSemi = _isFirstShot;
+ var state = _inputSystem.CmdStates.GetState(ContentKeyFunctions.UseItemInHand);
+ if (state != BoundKeyState.Down)
+ {
+ _isFirstShot = true;
+ _blocked = false;
+ return;
+ }
+
+ _isFirstShot = false;
+
+ var entity = _playerManager.LocalPlayer.ControlledEntity;
+ if (entity == null || !entity.TryGetComponent(out IHandsComponent hands))
+ {
+ return;
+ }
+
+ var held = hands.ActiveHand;
+ if (held == null || !held.TryGetComponent(out ClientRangedWeaponComponent weapon))
+ {
+ _blocked = true;
+ return;
+ }
+
+ if (_blocked)
+ {
+ return;
+ }
+
+ var worldPos = _eyeManager.ScreenToWorld(_inputManager.MouseScreenPosition);
+
+ if (weapon.Automatic || canFireSemi)
+ {
+ weapon.TryFire(worldPos);
+ }
+ }
+ }
+}
diff --git a/Content.Client/Interfaces/GameObjects/Components/Items/IHandsComponent.cs b/Content.Client/Interfaces/GameObjects/Components/Items/IHandsComponent.cs
index 91c23976e1..9ee28d856d 100644
--- a/Content.Client/Interfaces/GameObjects/Components/Items/IHandsComponent.cs
+++ b/Content.Client/Interfaces/GameObjects/Components/Items/IHandsComponent.cs
@@ -8,6 +8,7 @@ namespace Content.Client.Interfaces.GameObjects
{
IEntity GetEntity(string index);
string ActiveIndex { get; }
+ IEntity ActiveHand { get; }
void SendChangeHand(string index);
void AttackByInHand(string index);
diff --git a/Content.Server/EntryPoint.cs b/Content.Server/EntryPoint.cs
index 75ba452a70..9478b5a2e1 100644
--- a/Content.Server/EntryPoint.cs
+++ b/Content.Server/EntryPoint.cs
@@ -34,6 +34,7 @@ using Content.Server.GameObjects.EntitySystems;
using Content.Server.Mobs;
using Content.Server.Players;
using Content.Server.GameObjects.Components.Interactable;
+using Content.Server.GameObjects.Components.Weapon.Ranged;
using Content.Server.GameTicking;
using Content.Server.Interfaces;
using Content.Server.Interfaces.GameTicking;
@@ -93,6 +94,7 @@ namespace Content.Server
factory.Register();
factory.Register();
+ factory.Register();
factory.Register();
factory.Register();
factory.Register();
diff --git a/Content.Server/GameObjects/Components/Weapon/Ranged/Hitscan/HitscanWeaponComponent.cs b/Content.Server/GameObjects/Components/Weapon/Ranged/Hitscan/HitscanWeaponComponent.cs
index 1606f9eaa4..81fd651d1e 100644
--- a/Content.Server/GameObjects/Components/Weapon/Ranged/Hitscan/HitscanWeaponComponent.cs
+++ b/Content.Server/GameObjects/Components/Weapon/Ranged/Hitscan/HitscanWeaponComponent.cs
@@ -12,10 +12,11 @@ using SS14.Shared.Maths;
using SS14.Shared.Physics;
using SS14.Shared.Serialization;
using System;
+using SS14.Shared.GameObjects;
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Hitscan
{
- public class HitscanWeaponComponent : RangedWeaponComponent
+ public class HitscanWeaponComponent : Component
{
private const float MaxLength = 20;
public override string Name => "HitscanWeapon";
@@ -31,10 +32,18 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Hitscan
serializer.DataField(ref Damage, "damage", 10);
}
- protected override void Fire(IEntity user, GridLocalCoordinates clicklocation)
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ var rangedWeapon = Owner.GetComponent();
+ rangedWeapon.FireHandler = Fire;
+ }
+
+ private void Fire(IEntity user, GridLocalCoordinates clickLocation)
{
var userPosition = user.Transform.WorldPosition; //Remember world positions are ephemeral and can only be used instantaneously
- var angle = new Angle(clicklocation.Position - userPosition);
+ var angle = new Angle(clickLocation.Position - userPosition);
var ray = new Ray(userPosition, angle.ToVec());
var rayCastResults = IoCManager.Resolve().IntersectRay(ray, MaxLength,
diff --git a/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/ProjectileWeapon.cs b/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/ProjectileWeapon.cs
index 9d0e9a5439..6bfe77c204 100644
--- a/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/ProjectileWeapon.cs
+++ b/Content.Server/GameObjects/Components/Weapon/Ranged/Projectile/ProjectileWeapon.cs
@@ -1,33 +1,76 @@
-using Content.Server.GameObjects.Components.Projectiles;
+using System;
+using Content.Server.GameObjects.Components.Projectiles;
using SS14.Server.GameObjects;
using SS14.Server.GameObjects.EntitySystems;
using SS14.Server.Interfaces.GameObjects;
+using SS14.Shared.GameObjects;
using SS14.Shared.Interfaces.GameObjects;
using SS14.Shared.Interfaces.GameObjects.Components;
using SS14.Shared.IoC;
using SS14.Shared.Log;
using SS14.Shared.Map;
using SS14.Shared.Maths;
+using SS14.Shared.Serialization;
+using SS14.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
{
- public class ProjectileWeaponComponent : RangedWeaponComponent
+ public class ProjectileWeaponComponent : Component
{
public override string Name => "ProjectileWeapon";
private string _ProjectilePrototype = "ProjectileBullet";
private float _velocity = 20f;
+ private float _spreadStdDev = 3;
+ private bool _spread = true;
- protected override void Fire(IEntity user, GridLocalCoordinates clicklocation)
+ private Random _spreadRandom;
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ public bool Spread
{
- var userposition = user.GetComponent().LocalPosition; //Remember world positions are ephemeral and can only be used instantaneously
- var angle = new Angle(clicklocation.Position - userposition.Position);
+ get => _spread;
+ set => _spread = value;
+ }
- var theta = angle.Theta;
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float SpreadStdDev
+ {
+ get => _spreadStdDev;
+ set => _spreadStdDev = value;
+ }
- //Spawn the projectileprototype
- IEntity projectile = IoCManager.Resolve().ForceSpawnEntityAt(_ProjectilePrototype, userposition);
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ var rangedWeapon = Owner.GetComponent();
+ rangedWeapon.FireHandler = Fire;
+
+ _spreadRandom = new Random(Owner.Uid.GetHashCode() ^ DateTime.Now.GetHashCode());
+ }
+
+ public override void ExposeData(ObjectSerializer serializer)
+ {
+ base.ExposeData(serializer);
+
+ serializer.DataField(ref _spread, "spread", true);
+ serializer.DataField(ref _spreadStdDev, "spreadstddev", 3);
+ }
+
+ private void Fire(IEntity user, GridLocalCoordinates clickLocation)
+ {
+ var userPosition = user.Transform.LocalPosition; //Remember world positions are ephemeral and can only be used instantaneously
+ var angle = new Angle(clickLocation.Position - userPosition.Position);
+
+ if (Spread)
+ {
+ angle += Angle.FromDegrees(_spreadRandom.NextGaussian(0, SpreadStdDev));
+ }
+
+ //Spawn the projectilePrototype
+ var projectile = IoCManager.Resolve().ForceSpawnEntityAt(_ProjectilePrototype, userPosition);
//Give it the velocity we fire from this weapon, and make sure it doesn't shoot our character
projectile.GetComponent().IgnoreEntity(user);
@@ -36,7 +79,7 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
projectile.GetComponent().LinearVelocity = angle.ToVec() * _velocity;
//Rotate the bullets sprite to the correct direction, from north facing I guess
- projectile.GetComponent().LocalRotation = angle.Theta;
+ projectile.Transform.LocalRotation = angle.Theta;
// Sound!
IoCManager.Resolve().GetEntitySystem().Play("/Audio/gunshot_c20.ogg");
diff --git a/Content.Server/GameObjects/Components/Weapon/Ranged/RangedWeapon.cs b/Content.Server/GameObjects/Components/Weapon/Ranged/RangedWeapon.cs
index 869b3faf8c..f3a4a0d4ab 100644
--- a/Content.Server/GameObjects/Components/Weapon/Ranged/RangedWeapon.cs
+++ b/Content.Server/GameObjects/Components/Weapon/Ranged/RangedWeapon.cs
@@ -1,35 +1,96 @@
-using SS14.Shared.GameObjects;
+using System;
+using SS14.Shared.GameObjects;
using Content.Server.GameObjects.EntitySystems;
+using Content.Shared.GameObjects.Components.Weapons.Ranged;
+using SS14.Server.Interfaces.Player;
using SS14.Shared.Interfaces.GameObjects;
+using SS14.Shared.Interfaces.Network;
+using SS14.Shared.Interfaces.Timing;
+using SS14.Shared.IoC;
+using SS14.Shared.Log;
using SS14.Shared.Map;
+using SS14.Shared.Timers;
namespace Content.Server.GameObjects.Components.Weapon.Ranged
{
- public class RangedWeaponComponent : Component, IAfterAttack
+ public sealed class RangedWeaponComponent : SharedRangedWeaponComponent
{
- public override string Name => "RangedWeapon";
+ private TimeSpan _lastFireTime;
- void IAfterAttack.Afterattack(IEntity user, GridLocalCoordinates clicklocation, IEntity attacked)
+ public Func WeaponCanFireHandler;
+ public Func UserCanFireHandler;
+ public Action FireHandler;
+
+ private const int MaxFireDelayAttempts = 2;
+
+ private bool WeaponCanFire()
{
- if (UserCanFire(user) && WeaponCanFire())
+ return WeaponCanFireHandler == null || WeaponCanFireHandler();
+ }
+
+ private bool UserCanFire(IEntity user)
+ {
+ return UserCanFireHandler == null || UserCanFireHandler(user);
+ }
+
+ private void Fire(IEntity user, GridLocalCoordinates clickLocation)
+ {
+ _lastFireTime = IoCManager.Resolve().CurTime;
+ FireHandler?.Invoke(user, clickLocation);
+ }
+
+ public override void HandleMessage(ComponentMessage message, INetChannel netChannel = null,
+ IComponent component = null)
+ {
+ base.HandleMessage(message, netChannel, component);
+
+ switch (message)
{
- Fire(user, clicklocation);
+ case FireMessage msg:
+ var playerMgr = IoCManager.Resolve();
+ var session = playerMgr.GetSessionByChannel(netChannel);
+ var user = session.AttachedEntity;
+ if (user == null)
+ {
+ return;
+ }
+
+ _tryFire(user, msg.Target, 0);
+ break;
}
}
- protected virtual bool WeaponCanFire()
+ private void _tryFire(IEntity user, GridLocalCoordinates coordinates, int attemptCount)
{
- return true;
- }
+ if (!user.TryGetComponent(out HandsComponent hands) || hands.GetActiveHand.Owner != Owner)
+ {
+ return;
+ }
- protected virtual bool UserCanFire(IEntity user)
- {
- return true;
- }
+ if (!UserCanFire(user) || !WeaponCanFire())
+ {
+ return;
+ }
- protected virtual void Fire(IEntity user, GridLocalCoordinates clicklocation)
- {
- return;
+ // Firing delays are quite complicated.
+ // Sometimes the client's fire messages come in just too early.
+ // Generally this is a frame or two of being early.
+ // In that case we try them a few times the next frames to avoid having to drop them.
+ var curTime = IoCManager.Resolve().CurTime;
+ var span = curTime - _lastFireTime;
+ if (span.TotalSeconds < 1 / FireRate)
+ {
+ if (attemptCount >= MaxFireDelayAttempts)
+ {
+ return;
+ }
+
+ Timer.Spawn(TimeSpan.FromSeconds(1 / FireRate) - span,
+ () => _tryFire(user, coordinates, attemptCount + 1));
+ return;
+ }
+
+ Fire(user, coordinates);
}
}
}
diff --git a/Content.Shared/Content.Shared.csproj b/Content.Shared/Content.Shared.csproj
index 4f86c7a7fb..d2c544bb01 100644
--- a/Content.Shared/Content.Shared.csproj
+++ b/Content.Shared/Content.Shared.csproj
@@ -70,6 +70,7 @@
+
diff --git a/Content.Shared/GameObjects/Components/Weapons/Ranged/SharedRangedWeaponComponent.cs b/Content.Shared/GameObjects/Components/Weapons/Ranged/SharedRangedWeaponComponent.cs
new file mode 100644
index 0000000000..c0b9f5e383
--- /dev/null
+++ b/Content.Shared/GameObjects/Components/Weapons/Ranged/SharedRangedWeaponComponent.cs
@@ -0,0 +1,46 @@
+using System;
+using SS14.Shared.GameObjects;
+using SS14.Shared.Map;
+using SS14.Shared.Serialization;
+
+namespace Content.Shared.GameObjects.Components.Weapons.Ranged
+{
+ public class SharedRangedWeaponComponent : Component
+ {
+ private float _fireRate;
+ private bool _automatic;
+ public override string Name => "RangedWeapon";
+ public override uint? NetID => ContentNetIDs.RANGED_WEAPON;
+
+ ///
+ /// If true, this weapon is fully automatic, holding down left mouse button will keep firing it.
+ ///
+ public bool Automatic => _automatic;
+
+ ///
+ /// If the weapon is automatic, controls how many shots can be fired per second.
+ ///
+ public float FireRate => _fireRate;
+
+ public override void ExposeData(ObjectSerializer serializer)
+ {
+ base.ExposeData(serializer);
+
+ serializer.DataField(ref _fireRate, "firerate", 4);
+ serializer.DataField(ref _automatic, "automatic", false);
+ }
+
+ [Serializable, NetSerializable]
+ protected class FireMessage : ComponentMessage
+ {
+ public readonly GridLocalCoordinates Target;
+ public readonly int Tick;
+
+ public FireMessage(GridLocalCoordinates target, int tick)
+ {
+ Target = target;
+ Tick = tick;
+ }
+ }
+ }
+}
diff --git a/Content.Shared/GameObjects/ContentNetIDs.cs b/Content.Shared/GameObjects/ContentNetIDs.cs
index bd544e218b..b84b23dbd4 100644
--- a/Content.Shared/GameObjects/ContentNetIDs.cs
+++ b/Content.Shared/GameObjects/ContentNetIDs.cs
@@ -11,6 +11,7 @@
public const uint INVENTORY = 1006;
public const uint POWER_DEBUG_TOOL = 1007;
public const uint CONSTRUCTOR = 1008;
+ public const uint RANGED_WEAPON = 1010;
public const uint SPECIES = 1009;
}
}
diff --git a/Resources/Prototypes/Entities/Weapons.yml b/Resources/Prototypes/Entities/Weapons.yml
index 0321e95a6b..6af9612483 100644
--- a/Resources/Prototypes/Entities/Weapons.yml
+++ b/Resources/Prototypes/Entities/Weapons.yml
@@ -10,6 +10,7 @@
- type: Icon
sprite: Objects/laser_retro.rsi
state: 100
+ - type: RangedWeapon
- type: HitscanWeapon
damage: 30
sprite: "Objects/laser.png"
@@ -31,6 +32,9 @@
- type: Icon
sprite: Objects/c20r.rsi
state: c20r-20
+ - type: RangedWeapon
+ automatic: true
+ firerate: 8
- type: ProjectileWeapon
- type: Item
Size: 24