From 7ca90d11b331381bedfb3559396979b1c65467dc Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Thu, 13 Dec 2018 14:49:57 +0100 Subject: [PATCH] Gun stuff (#132) * Guns can now be fully automatic. Take that BYOND. * Improve delay handling * Bullet spread * Fix firing guns on pickup --- Content.Client/Content.Client.csproj | 4 +- Content.Client/EntryPoint.cs | 3 + .../Components/Items/ClientHandsComponent.cs | 2 + .../Ranged/ClientRangedWeaponComponent.cs | 29 ++++++ .../EntitySystems/RangedWeaponSystem.cs | 76 +++++++++++++++ .../Components/Items/IHandsComponent.cs | 1 + Content.Server/EntryPoint.cs | 2 + .../Ranged/Hitscan/HitscanWeaponComponent.cs | 15 ++- .../Ranged/Projectile/ProjectileWeapon.cs | 61 ++++++++++-- .../Components/Weapon/Ranged/RangedWeapon.cs | 93 +++++++++++++++---- Content.Shared/Content.Shared.csproj | 1 + .../Ranged/SharedRangedWeaponComponent.cs | 46 +++++++++ Content.Shared/GameObjects/ContentNetIDs.cs | 1 + Resources/Prototypes/Entities/Weapons.yml | 4 + 14 files changed, 309 insertions(+), 29 deletions(-) create mode 100644 Content.Client/GameObjects/Components/Weapons/Ranged/ClientRangedWeaponComponent.cs create mode 100644 Content.Client/GameObjects/EntitySystems/RangedWeaponSystem.cs create mode 100644 Content.Shared/GameObjects/Components/Weapons/Ranged/SharedRangedWeaponComponent.cs 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