Gun stuff (#132)

* Guns can now be fully automatic.

Take that BYOND.

* Improve delay handling

* Bullet spread

* Fix firing guns on pickup
This commit is contained in:
Pieter-Jan Briers
2018-12-13 14:49:57 +01:00
committed by GitHub
parent 69946c79d8
commit 7ca90d11b3
14 changed files with 309 additions and 29 deletions

View File

@@ -82,7 +82,9 @@
<Compile Include="GameObjects\Components\Power\ApcBoundUserInterface.cs" /> <Compile Include="GameObjects\Components\Power\ApcBoundUserInterface.cs" />
<Compile Include="GameObjects\Components\Power\PowerCellVisualizer2D.cs" /> <Compile Include="GameObjects\Components\Power\PowerCellVisualizer2D.cs" />
<Compile Include="GameObjects\Components\Storage\ClientStorageComponent.cs" /> <Compile Include="GameObjects\Components\Storage\ClientStorageComponent.cs" />
<Compile Include="GameObjects\Components\Weapons\Ranged\ClientRangedWeaponComponent.cs" />
<Compile Include="GameObjects\EntitySystems\ClientNotifySystem.cs" /> <Compile Include="GameObjects\EntitySystems\ClientNotifySystem.cs" />
<Compile Include="GameObjects\EntitySystems\RangedWeaponSystem.cs" />
<Compile Include="GameObjects\EntitySystems\VerbSystem.cs" /> <Compile Include="GameObjects\EntitySystems\VerbSystem.cs" />
<Compile Include="GameTicking\ClientGameTicker.cs" /> <Compile Include="GameTicking\ClientGameTicker.cs" />
<Compile Include="Graphics\Overlays\CircleMaskOverlay.cs" /> <Compile Include="Graphics\Overlays\CircleMaskOverlay.cs" />
@@ -145,4 +147,4 @@
<Compile Include="Construction\ConstructionPlacementHijack.cs" /> <Compile Include="Construction\ConstructionPlacementHijack.cs" />
<Compile Include="GameObjects\Components\IconSmoothing\IconSmoothComponent.cs" /> <Compile Include="GameObjects\Components\IconSmoothing\IconSmoothComponent.cs" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -5,12 +5,14 @@ using Content.Client.GameObjects.Components.Construction;
using Content.Client.GameObjects.Components.Power; using Content.Client.GameObjects.Components.Power;
using Content.Client.GameObjects.Components.SmoothWalling; using Content.Client.GameObjects.Components.SmoothWalling;
using Content.Client.GameObjects.Components.Storage; using Content.Client.GameObjects.Components.Storage;
using Content.Client.GameObjects.Components.Weapons.Ranged;
using Content.Client.GameTicking; using Content.Client.GameTicking;
using Content.Client.Input; using Content.Client.Input;
using Content.Client.Interfaces; using Content.Client.Interfaces;
using Content.Client.Interfaces.GameObjects; using Content.Client.Interfaces.GameObjects;
using Content.Client.Interfaces.Parallax; using Content.Client.Interfaces.Parallax;
using Content.Client.Parallax; using Content.Client.Parallax;
using Content.Shared.GameObjects.Components.Weapons.Ranged;
using Content.Shared.Interfaces; using Content.Shared.Interfaces;
using SS14.Client; using SS14.Client;
using SS14.Client.Interfaces; using SS14.Client.Interfaces;
@@ -52,6 +54,7 @@ namespace Content.Client
factory.RegisterIgnore("Welder"); factory.RegisterIgnore("Welder");
factory.RegisterIgnore("Wrench"); factory.RegisterIgnore("Wrench");
factory.RegisterIgnore("Crowbar"); factory.RegisterIgnore("Crowbar");
factory.Register<ClientRangedWeaponComponent>();
factory.RegisterIgnore("HitscanWeapon"); factory.RegisterIgnore("HitscanWeapon");
factory.RegisterIgnore("ProjectileWeapon"); factory.RegisterIgnore("ProjectileWeapon");
factory.RegisterIgnore("Projectile"); factory.RegisterIgnore("Projectile");

View File

@@ -27,6 +27,8 @@ namespace Content.Client.GameObjects
[ViewVariables] private ISpriteComponent _sprite; [ViewVariables] private ISpriteComponent _sprite;
[ViewVariables] public IEntity ActiveHand => GetEntity(ActiveIndex);
public override void OnAdd() public override void OnAdd()
{ {
base.OnAdd(); base.OnAdd();

View File

@@ -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<IGameTiming>().CurTime;
var span = curTime - _lastFireTime;
if (span.TotalSeconds < 1 / FireRate)
{
return;
}
Logger.Debug("Delay: {0}", span.TotalSeconds);
_lastFireTime = curTime;
SendNetworkMessage(new FireMessage(worldPos, _tick++));
}
}
}

View File

@@ -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<InputSystem>();
}
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);
}
}
}
}

View File

@@ -8,6 +8,7 @@ namespace Content.Client.Interfaces.GameObjects
{ {
IEntity GetEntity(string index); IEntity GetEntity(string index);
string ActiveIndex { get; } string ActiveIndex { get; }
IEntity ActiveHand { get; }
void SendChangeHand(string index); void SendChangeHand(string index);
void AttackByInHand(string index); void AttackByInHand(string index);

View File

@@ -34,6 +34,7 @@ using Content.Server.GameObjects.EntitySystems;
using Content.Server.Mobs; using Content.Server.Mobs;
using Content.Server.Players; using Content.Server.Players;
using Content.Server.GameObjects.Components.Interactable; using Content.Server.GameObjects.Components.Interactable;
using Content.Server.GameObjects.Components.Weapon.Ranged;
using Content.Server.GameTicking; using Content.Server.GameTicking;
using Content.Server.Interfaces; using Content.Server.Interfaces;
using Content.Server.Interfaces.GameTicking; using Content.Server.Interfaces.GameTicking;
@@ -93,6 +94,7 @@ namespace Content.Server
factory.Register<CrowbarComponent>(); factory.Register<CrowbarComponent>();
factory.Register<HitscanWeaponComponent>(); factory.Register<HitscanWeaponComponent>();
factory.Register<RangedWeaponComponent>();
factory.Register<ProjectileWeaponComponent>(); factory.Register<ProjectileWeaponComponent>();
factory.Register<ProjectileComponent>(); factory.Register<ProjectileComponent>();
factory.Register<ThrownItemComponent>(); factory.Register<ThrownItemComponent>();

View File

@@ -12,10 +12,11 @@ using SS14.Shared.Maths;
using SS14.Shared.Physics; using SS14.Shared.Physics;
using SS14.Shared.Serialization; using SS14.Shared.Serialization;
using System; using System;
using SS14.Shared.GameObjects;
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Hitscan namespace Content.Server.GameObjects.Components.Weapon.Ranged.Hitscan
{ {
public class HitscanWeaponComponent : RangedWeaponComponent public class HitscanWeaponComponent : Component
{ {
private const float MaxLength = 20; private const float MaxLength = 20;
public override string Name => "HitscanWeapon"; public override string Name => "HitscanWeapon";
@@ -31,10 +32,18 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Hitscan
serializer.DataField(ref Damage, "damage", 10); serializer.DataField(ref Damage, "damage", 10);
} }
protected override void Fire(IEntity user, GridLocalCoordinates clicklocation) public override void Initialize()
{
base.Initialize();
var rangedWeapon = Owner.GetComponent<RangedWeaponComponent>();
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 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 ray = new Ray(userPosition, angle.ToVec());
var rayCastResults = IoCManager.Resolve<IPhysicsManager>().IntersectRay(ray, MaxLength, var rayCastResults = IoCManager.Resolve<IPhysicsManager>().IntersectRay(ray, MaxLength,

View File

@@ -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;
using SS14.Server.GameObjects.EntitySystems; using SS14.Server.GameObjects.EntitySystems;
using SS14.Server.Interfaces.GameObjects; using SS14.Server.Interfaces.GameObjects;
using SS14.Shared.GameObjects;
using SS14.Shared.Interfaces.GameObjects; using SS14.Shared.Interfaces.GameObjects;
using SS14.Shared.Interfaces.GameObjects.Components; using SS14.Shared.Interfaces.GameObjects.Components;
using SS14.Shared.IoC; using SS14.Shared.IoC;
using SS14.Shared.Log; using SS14.Shared.Log;
using SS14.Shared.Map; using SS14.Shared.Map;
using SS14.Shared.Maths; using SS14.Shared.Maths;
using SS14.Shared.Serialization;
using SS14.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
{ {
public class ProjectileWeaponComponent : RangedWeaponComponent public class ProjectileWeaponComponent : Component
{ {
public override string Name => "ProjectileWeapon"; public override string Name => "ProjectileWeapon";
private string _ProjectilePrototype = "ProjectileBullet"; private string _ProjectilePrototype = "ProjectileBullet";
private float _velocity = 20f; 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<ITransformComponent>().LocalPosition; //Remember world positions are ephemeral and can only be used instantaneously get => _spread;
var angle = new Angle(clicklocation.Position - userposition.Position); set => _spread = value;
}
var theta = angle.Theta; [ViewVariables(VVAccess.ReadWrite)]
public float SpreadStdDev
{
get => _spreadStdDev;
set => _spreadStdDev = value;
}
//Spawn the projectileprototype public override void Initialize()
IEntity projectile = IoCManager.Resolve<IServerEntityManager>().ForceSpawnEntityAt(_ProjectilePrototype, userposition); {
base.Initialize();
var rangedWeapon = Owner.GetComponent<RangedWeaponComponent>();
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<IServerEntityManager>().ForceSpawnEntityAt(_ProjectilePrototype, userPosition);
//Give it the velocity we fire from this weapon, and make sure it doesn't shoot our character //Give it the velocity we fire from this weapon, and make sure it doesn't shoot our character
projectile.GetComponent<ProjectileComponent>().IgnoreEntity(user); projectile.GetComponent<ProjectileComponent>().IgnoreEntity(user);
@@ -36,7 +79,7 @@ namespace Content.Server.GameObjects.Components.Weapon.Ranged.Projectile
projectile.GetComponent<PhysicsComponent>().LinearVelocity = angle.ToVec() * _velocity; projectile.GetComponent<PhysicsComponent>().LinearVelocity = angle.ToVec() * _velocity;
//Rotate the bullets sprite to the correct direction, from north facing I guess //Rotate the bullets sprite to the correct direction, from north facing I guess
projectile.GetComponent<ITransformComponent>().LocalRotation = angle.Theta; projectile.Transform.LocalRotation = angle.Theta;
// Sound! // Sound!
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AudioSystem>().Play("/Audio/gunshot_c20.ogg"); IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AudioSystem>().Play("/Audio/gunshot_c20.ogg");

View File

@@ -1,35 +1,96 @@
using SS14.Shared.GameObjects; using System;
using SS14.Shared.GameObjects;
using Content.Server.GameObjects.EntitySystems; 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.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.Map;
using SS14.Shared.Timers;
namespace Content.Server.GameObjects.Components.Weapon.Ranged 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<bool> WeaponCanFireHandler;
public Func<IEntity, bool> UserCanFireHandler;
public Action<IEntity, GridLocalCoordinates> 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<IGameTiming>().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<IPlayerManager>();
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) if (!UserCanFire(user) || !WeaponCanFire())
{ {
return true; return;
} }
protected virtual void Fire(IEntity user, GridLocalCoordinates clicklocation) // Firing delays are quite complicated.
{ // Sometimes the client's fire messages come in just too early.
return; // 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<IGameTiming>().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);
} }
} }
} }

View File

@@ -70,6 +70,7 @@
<Compile Include="GameObjects\Components\Power\PowerShared.cs" /> <Compile Include="GameObjects\Components\Power\PowerShared.cs" />
<Compile Include="GameObjects\Components\Power\SharedPowerCellComponent.cs" /> <Compile Include="GameObjects\Components\Power\SharedPowerCellComponent.cs" />
<Compile Include="GameObjects\Components\Storage\SharedStorageComponent.cs" /> <Compile Include="GameObjects\Components\Storage\SharedStorageComponent.cs" />
<Compile Include="GameObjects\Components\Weapons\Ranged\SharedRangedWeaponComponent.cs" />
<Compile Include="GameObjects\ContentNetIDs.cs" /> <Compile Include="GameObjects\ContentNetIDs.cs" />
<Compile Include="GameObjects\EntitySystemMessages\VerbSystemMessages.cs" /> <Compile Include="GameObjects\EntitySystemMessages\VerbSystemMessages.cs" />
<Compile Include="GameObjects\Messages\Mob\HealthHud.cs" /> <Compile Include="GameObjects\Messages\Mob\HealthHud.cs" />

View File

@@ -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;
/// <summary>
/// If true, this weapon is fully automatic, holding down left mouse button will keep firing it.
/// </summary>
public bool Automatic => _automatic;
/// <summary>
/// If the weapon is automatic, controls how many shots can be fired per second.
/// </summary>
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;
}
}
}
}

View File

@@ -11,6 +11,7 @@
public const uint INVENTORY = 1006; public const uint INVENTORY = 1006;
public const uint POWER_DEBUG_TOOL = 1007; public const uint POWER_DEBUG_TOOL = 1007;
public const uint CONSTRUCTOR = 1008; public const uint CONSTRUCTOR = 1008;
public const uint RANGED_WEAPON = 1010;
public const uint SPECIES = 1009; public const uint SPECIES = 1009;
} }
} }

View File

@@ -10,6 +10,7 @@
- type: Icon - type: Icon
sprite: Objects/laser_retro.rsi sprite: Objects/laser_retro.rsi
state: 100 state: 100
- type: RangedWeapon
- type: HitscanWeapon - type: HitscanWeapon
damage: 30 damage: 30
sprite: "Objects/laser.png" sprite: "Objects/laser.png"
@@ -31,6 +32,9 @@
- type: Icon - type: Icon
sprite: Objects/c20r.rsi sprite: Objects/c20r.rsi
state: c20r-20 state: c20r-20
- type: RangedWeapon
automatic: true
firerate: 8
- type: ProjectileWeapon - type: ProjectileWeapon
- type: Item - type: Item
Size: 24 Size: 24