|
@@ -1,18 +1,30 @@
|
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
|
|
|
+using System.Linq;
|
|
|
using System.Numerics;
|
|
using System.Numerics;
|
|
|
|
|
+using Content.Shared._RMC14.CCVar;
|
|
|
|
|
+using Content.Shared._RMC14.Random;
|
|
|
|
|
+using Content.Shared._RMC14.Weapons.Ranged.Prediction;
|
|
|
using Content.Shared.ActionBlocker;
|
|
using Content.Shared.ActionBlocker;
|
|
|
using Content.Shared.Actions;
|
|
using Content.Shared.Actions;
|
|
|
using Content.Shared.Administration.Logs;
|
|
using Content.Shared.Administration.Logs;
|
|
|
using Content.Shared.Audio;
|
|
using Content.Shared.Audio;
|
|
|
|
|
+using Content.Shared.Camera;
|
|
|
using Content.Shared.CombatMode;
|
|
using Content.Shared.CombatMode;
|
|
|
using Content.Shared.Containers.ItemSlots;
|
|
using Content.Shared.Containers.ItemSlots;
|
|
|
using Content.Shared.Damage;
|
|
using Content.Shared.Damage;
|
|
|
|
|
+using Content.Shared.Damage.Components;
|
|
|
|
|
+using Content.Shared.Damage.Systems;
|
|
|
|
|
+using Content.Shared.Database;
|
|
|
|
|
+using Content.Shared.Effects;
|
|
|
using Content.Shared.Examine;
|
|
using Content.Shared.Examine;
|
|
|
using Content.Shared.Gravity;
|
|
using Content.Shared.Gravity;
|
|
|
using Content.Shared.Hands;
|
|
using Content.Shared.Hands;
|
|
|
using Content.Shared.Hands.Components;
|
|
using Content.Shared.Hands.Components;
|
|
|
|
|
+using Content.Shared.Interaction;
|
|
|
|
|
+using Content.Shared.Interaction.Components;
|
|
|
using Content.Shared.Popups;
|
|
using Content.Shared.Popups;
|
|
|
using Content.Shared.Projectiles;
|
|
using Content.Shared.Projectiles;
|
|
|
|
|
+using Content.Shared.Stunnable;
|
|
|
using Content.Shared.Tag;
|
|
using Content.Shared.Tag;
|
|
|
using Content.Shared.Throwing;
|
|
using Content.Shared.Throwing;
|
|
|
using Content.Shared.Timing;
|
|
using Content.Shared.Timing;
|
|
@@ -21,14 +33,18 @@
|
|
|
using Content.Shared.Weapons.Melee.Events;
|
|
using Content.Shared.Weapons.Melee.Events;
|
|
|
using Content.Shared.Weapons.Ranged.Components;
|
|
using Content.Shared.Weapons.Ranged.Components;
|
|
|
using Content.Shared.Weapons.Ranged.Events;
|
|
using Content.Shared.Weapons.Ranged.Events;
|
|
|
|
|
+using Content.Shared.Weapons.Reflect;
|
|
|
using Content.Shared.Whitelist;
|
|
using Content.Shared.Whitelist;
|
|
|
using Robust.Shared.Audio;
|
|
using Robust.Shared.Audio;
|
|
|
using Robust.Shared.Audio.Systems;
|
|
using Robust.Shared.Audio.Systems;
|
|
|
|
|
+using Robust.Shared.Configuration;
|
|
|
using Robust.Shared.Containers;
|
|
using Robust.Shared.Containers;
|
|
|
using Robust.Shared.Map;
|
|
using Robust.Shared.Map;
|
|
|
using Robust.Shared.Network;
|
|
using Robust.Shared.Network;
|
|
|
|
|
+using Robust.Shared.Physics;
|
|
|
using Robust.Shared.Physics.Components;
|
|
using Robust.Shared.Physics.Components;
|
|
|
using Robust.Shared.Physics.Systems;
|
|
using Robust.Shared.Physics.Systems;
|
|
|
|
|
+using Robust.Shared.Player;
|
|
|
using Robust.Shared.Prototypes;
|
|
using Robust.Shared.Prototypes;
|
|
|
using Robust.Shared.Random;
|
|
using Robust.Shared.Random;
|
|
|
using Robust.Shared.Serialization;
|
|
using Robust.Shared.Serialization;
|
|
@@ -39,23 +55,23 @@ namespace Content.Shared.Weapons.Ranged.Systems;
|
|
|
|
|
|
|
|
public abstract partial class SharedGunSystem : EntitySystem
|
|
public abstract partial class SharedGunSystem : EntitySystem
|
|
|
{
|
|
{
|
|
|
- [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
|
|
|
|
|
|
|
+ [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
|
|
|
[Dependency] protected readonly IGameTiming Timing = default!;
|
|
[Dependency] protected readonly IGameTiming Timing = default!;
|
|
|
[Dependency] protected readonly IMapManager MapManager = default!;
|
|
[Dependency] protected readonly IMapManager MapManager = default!;
|
|
|
- [Dependency] private readonly INetManager _netManager = default!;
|
|
|
|
|
|
|
+ [Dependency] private readonly INetManager _netManager = default!;
|
|
|
[Dependency] protected readonly IPrototypeManager ProtoManager = default!;
|
|
[Dependency] protected readonly IPrototypeManager ProtoManager = default!;
|
|
|
[Dependency] protected readonly IRobustRandom Random = default!;
|
|
[Dependency] protected readonly IRobustRandom Random = default!;
|
|
|
[Dependency] protected readonly ISharedAdminLogManager Logs = default!;
|
|
[Dependency] protected readonly ISharedAdminLogManager Logs = default!;
|
|
|
[Dependency] protected readonly DamageableSystem Damageable = default!;
|
|
[Dependency] protected readonly DamageableSystem Damageable = default!;
|
|
|
[Dependency] protected readonly ExamineSystemShared Examine = default!;
|
|
[Dependency] protected readonly ExamineSystemShared Examine = default!;
|
|
|
- [Dependency] private readonly ItemSlotsSystem _slots = default!;
|
|
|
|
|
- [Dependency] private readonly RechargeBasicEntityAmmoSystem _recharge = default!;
|
|
|
|
|
|
|
+ [Dependency] private readonly ItemSlotsSystem _slots = default!;
|
|
|
|
|
+ [Dependency] private readonly RechargeBasicEntityAmmoSystem _recharge = default!;
|
|
|
[Dependency] protected readonly SharedActionsSystem Actions = default!;
|
|
[Dependency] protected readonly SharedActionsSystem Actions = default!;
|
|
|
[Dependency] protected readonly SharedAppearanceSystem Appearance = default!;
|
|
[Dependency] protected readonly SharedAppearanceSystem Appearance = default!;
|
|
|
[Dependency] protected readonly SharedAudioSystem Audio = default!;
|
|
[Dependency] protected readonly SharedAudioSystem Audio = default!;
|
|
|
- [Dependency] private readonly SharedCombatModeSystem _combatMode = default!;
|
|
|
|
|
|
|
+ [Dependency] private readonly SharedCombatModeSystem _combatMode = default!;
|
|
|
[Dependency] protected readonly SharedContainerSystem Containers = default!;
|
|
[Dependency] protected readonly SharedContainerSystem Containers = default!;
|
|
|
- [Dependency] private readonly SharedGravitySystem _gravity = default!;
|
|
|
|
|
|
|
+ [Dependency] private readonly SharedGravitySystem _gravity = default!;
|
|
|
[Dependency] protected readonly SharedPointLightSystem Lights = default!;
|
|
[Dependency] protected readonly SharedPointLightSystem Lights = default!;
|
|
|
[Dependency] protected readonly SharedPopupSystem PopupSystem = default!;
|
|
[Dependency] protected readonly SharedPopupSystem PopupSystem = default!;
|
|
|
[Dependency] protected readonly SharedPhysicsSystem Physics = default!;
|
|
[Dependency] protected readonly SharedPhysicsSystem Physics = default!;
|
|
@@ -63,8 +79,13 @@ public abstract partial class SharedGunSystem : EntitySystem
|
|
|
[Dependency] protected readonly SharedTransformSystem TransformSystem = default!;
|
|
[Dependency] protected readonly SharedTransformSystem TransformSystem = default!;
|
|
|
[Dependency] protected readonly TagSystem TagSystem = default!;
|
|
[Dependency] protected readonly TagSystem TagSystem = default!;
|
|
|
[Dependency] protected readonly ThrowingSystem ThrowingSystem = default!;
|
|
[Dependency] protected readonly ThrowingSystem ThrowingSystem = default!;
|
|
|
- [Dependency] private readonly UseDelaySystem _useDelay = default!;
|
|
|
|
|
|
|
+ [Dependency] private readonly UseDelaySystem _useDelay = default!;
|
|
|
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
|
|
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
|
|
|
|
|
+ [Dependency] private readonly StaminaSystem _stamina = default!;
|
|
|
|
|
+ [Dependency] private readonly SharedStunSystem _stun = default!;
|
|
|
|
|
+ [Dependency] private readonly SharedColorFlashEffectSystem _color = default!;
|
|
|
|
|
+ [Dependency] private readonly SharedCameraRecoilSystem _recoil = default!;
|
|
|
|
|
+ [Dependency] private readonly IConfigurationManager _config = default!;
|
|
|
|
|
|
|
|
private const float InteractNextFire = 0.3f;
|
|
private const float InteractNextFire = 0.3f;
|
|
|
private const double SafetyNextFire = 0.5;
|
|
private const double SafetyNextFire = 0.5;
|
|
@@ -72,10 +93,12 @@ public abstract partial class SharedGunSystem : EntitySystem
|
|
|
protected const string AmmoExamineColor = "yellow";
|
|
protected const string AmmoExamineColor = "yellow";
|
|
|
protected const string FireRateExamineColor = "yellow";
|
|
protected const string FireRateExamineColor = "yellow";
|
|
|
public const string ModeExamineColor = "cyan";
|
|
public const string ModeExamineColor = "cyan";
|
|
|
|
|
+ public const float GunClumsyChance = 0.5f;
|
|
|
|
|
+ private const float DamagePitchVariation = 0.05f;
|
|
|
|
|
+ public bool GunPrediction { get; private set; }
|
|
|
|
|
|
|
|
public override void Initialize()
|
|
public override void Initialize()
|
|
|
{
|
|
{
|
|
|
- SubscribeAllEvent<RequestShootEvent>(OnShootRequest);
|
|
|
|
|
SubscribeAllEvent<RequestStopShootEvent>(OnStopShootRequest);
|
|
SubscribeAllEvent<RequestStopShootEvent>(OnStopShootRequest);
|
|
|
SubscribeLocalEvent<GunComponent, MeleeHitEvent>(OnGunMelee);
|
|
SubscribeLocalEvent<GunComponent, MeleeHitEvent>(OnGunMelee);
|
|
|
|
|
|
|
@@ -97,6 +120,7 @@ public override void Initialize()
|
|
|
SubscribeLocalEvent<GunComponent, CycleModeEvent>(OnCycleMode);
|
|
SubscribeLocalEvent<GunComponent, CycleModeEvent>(OnCycleMode);
|
|
|
SubscribeLocalEvent<GunComponent, HandSelectedEvent>(OnGunSelected);
|
|
SubscribeLocalEvent<GunComponent, HandSelectedEvent>(OnGunSelected);
|
|
|
SubscribeLocalEvent<GunComponent, MapInitEvent>(OnMapInit);
|
|
SubscribeLocalEvent<GunComponent, MapInitEvent>(OnMapInit);
|
|
|
|
|
+ Subs.CVar(_config, RMCCVars.RMCGunPrediction, v => GunPrediction = v, true);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private void OnMapInit(Entity<GunComponent> gun, ref MapInitEvent args)
|
|
private void OnMapInit(Entity<GunComponent> gun, ref MapInitEvent args)
|
|
@@ -119,29 +143,10 @@ private void OnGunMelee(EntityUid uid, GunComponent component, MeleeHitEvent arg
|
|
|
if (melee.NextAttack > component.NextFire)
|
|
if (melee.NextAttack > component.NextFire)
|
|
|
{
|
|
{
|
|
|
component.NextFire = melee.NextAttack;
|
|
component.NextFire = melee.NextAttack;
|
|
|
- EntityManager.DirtyField(uid, component, nameof(GunComponent.NextFire));
|
|
|
|
|
|
|
+ Dirty(uid, component);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private void OnShootRequest(RequestShootEvent msg, EntitySessionEventArgs args)
|
|
|
|
|
- {
|
|
|
|
|
- var user = args.SenderSession.AttachedEntity;
|
|
|
|
|
-
|
|
|
|
|
- if (user == null ||
|
|
|
|
|
- !_combatMode.IsInCombatMode(user) ||
|
|
|
|
|
- !TryGetGun(user.Value, out var ent, out var gun))
|
|
|
|
|
- {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (ent != GetEntity(msg.Gun))
|
|
|
|
|
- return;
|
|
|
|
|
-
|
|
|
|
|
- gun.ShootCoordinates = GetCoordinates(msg.Coordinates);
|
|
|
|
|
- gun.Target = GetEntity(msg.Target);
|
|
|
|
|
- AttemptShoot(user.Value, ent, gun);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
private void OnStopShootRequest(RequestStopShootEvent ev, EntitySessionEventArgs args)
|
|
private void OnStopShootRequest(RequestStopShootEvent ev, EntitySessionEventArgs args)
|
|
|
{
|
|
{
|
|
|
var gunUid = GetEntity(ev.Gun);
|
|
var gunUid = GetEntity(ev.Gun);
|
|
@@ -200,7 +205,7 @@ private void StopShooting(EntityUid uid, GunComponent gun)
|
|
|
gun.ShotCounter = 0;
|
|
gun.ShotCounter = 0;
|
|
|
gun.ShootCoordinates = null;
|
|
gun.ShootCoordinates = null;
|
|
|
gun.Target = null;
|
|
gun.Target = null;
|
|
|
- EntityManager.DirtyField(uid, gun, nameof(GunComponent.ShotCounter));
|
|
|
|
|
|
|
+ Dirty(uid, gun);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
@@ -211,7 +216,6 @@ public void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun, Ent
|
|
|
gun.ShootCoordinates = toCoordinates;
|
|
gun.ShootCoordinates = toCoordinates;
|
|
|
AttemptShoot(user, gunUid, gun);
|
|
AttemptShoot(user, gunUid, gun);
|
|
|
gun.ShotCounter = 0;
|
|
gun.ShotCounter = 0;
|
|
|
- EntityManager.DirtyField(gunUid, gun, nameof(GunComponent.ShotCounter));
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
@@ -219,24 +223,22 @@ public void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun, Ent
|
|
|
/// </summary>
|
|
/// </summary>
|
|
|
public void AttemptShoot(EntityUid gunUid, GunComponent gun)
|
|
public void AttemptShoot(EntityUid gunUid, GunComponent gun)
|
|
|
{
|
|
{
|
|
|
- var coordinates = new EntityCoordinates(gunUid, gun.DefaultDirection);
|
|
|
|
|
|
|
+ var coordinates = new EntityCoordinates(gunUid, new Vector2(0, -1));
|
|
|
gun.ShootCoordinates = coordinates;
|
|
gun.ShootCoordinates = coordinates;
|
|
|
AttemptShoot(gunUid, gunUid, gun);
|
|
AttemptShoot(gunUid, gunUid, gun);
|
|
|
gun.ShotCounter = 0;
|
|
gun.ShotCounter = 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun)
|
|
|
|
|
|
|
+ private List<EntityUid>? AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun, List<int>? predictedProjectiles = null, ICommonSession? userSession = null)
|
|
|
{
|
|
{
|
|
|
if (gun.FireRateModified <= 0f ||
|
|
if (gun.FireRateModified <= 0f ||
|
|
|
!_actionBlockerSystem.CanAttack(user))
|
|
!_actionBlockerSystem.CanAttack(user))
|
|
|
- {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ return null;
|
|
|
|
|
|
|
|
var toCoordinates = gun.ShootCoordinates;
|
|
var toCoordinates = gun.ShootCoordinates;
|
|
|
|
|
|
|
|
if (toCoordinates == null)
|
|
if (toCoordinates == null)
|
|
|
- return;
|
|
|
|
|
|
|
+ return null;
|
|
|
|
|
|
|
|
var curTime = Timing.CurTime;
|
|
var curTime = Timing.CurTime;
|
|
|
|
|
|
|
@@ -248,22 +250,19 @@ private void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun)
|
|
|
};
|
|
};
|
|
|
RaiseLocalEvent(gunUid, ref prevention);
|
|
RaiseLocalEvent(gunUid, ref prevention);
|
|
|
if (prevention.Cancelled)
|
|
if (prevention.Cancelled)
|
|
|
- return;
|
|
|
|
|
|
|
+ return null;
|
|
|
|
|
|
|
|
RaiseLocalEvent(user, ref prevention);
|
|
RaiseLocalEvent(user, ref prevention);
|
|
|
if (prevention.Cancelled)
|
|
if (prevention.Cancelled)
|
|
|
- return;
|
|
|
|
|
|
|
+ return null;
|
|
|
|
|
|
|
|
// Need to do this to play the clicking sound for empty automatic weapons
|
|
// Need to do this to play the clicking sound for empty automatic weapons
|
|
|
// but not play anything for burst fire.
|
|
// but not play anything for burst fire.
|
|
|
if (gun.NextFire > curTime)
|
|
if (gun.NextFire > curTime)
|
|
|
- return;
|
|
|
|
|
|
|
+ return null;
|
|
|
|
|
|
|
|
var fireRate = TimeSpan.FromSeconds(1f / gun.FireRateModified);
|
|
var fireRate = TimeSpan.FromSeconds(1f / gun.FireRateModified);
|
|
|
|
|
|
|
|
- if (gun.SelectedMode == SelectiveFire.Burst || gun.BurstActivated)
|
|
|
|
|
- fireRate = TimeSpan.FromSeconds(1f / gun.BurstFireRate);
|
|
|
|
|
-
|
|
|
|
|
// First shot
|
|
// First shot
|
|
|
// Previously we checked shotcounter but in some cases all the bullets got dumped at once
|
|
// Previously we checked shotcounter but in some cases all the bullets got dumped at once
|
|
|
// curTime - fireRate is insufficient because if you time it just right you can get a 3rd shot out slightly quicker.
|
|
// curTime - fireRate is insufficient because if you time it just right you can get a 3rd shot out slightly quicker.
|
|
@@ -280,28 +279,22 @@ private void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// NextFire has been touched regardless so need to dirty the gun.
|
|
// NextFire has been touched regardless so need to dirty the gun.
|
|
|
- EntityManager.DirtyField(gunUid, gun, nameof(GunComponent.NextFire));
|
|
|
|
|
|
|
+ Dirty(gunUid, gun);
|
|
|
|
|
|
|
|
// Get how many shots we're actually allowed to make, due to clip size or otherwise.
|
|
// Get how many shots we're actually allowed to make, due to clip size or otherwise.
|
|
|
// Don't do this in the loop so we still reset NextFire.
|
|
// Don't do this in the loop so we still reset NextFire.
|
|
|
- if (!gun.BurstActivated)
|
|
|
|
|
- {
|
|
|
|
|
- switch (gun.SelectedMode)
|
|
|
|
|
- {
|
|
|
|
|
- case SelectiveFire.SemiAuto:
|
|
|
|
|
- shots = Math.Min(shots, 1 - gun.ShotCounter);
|
|
|
|
|
- break;
|
|
|
|
|
- case SelectiveFire.Burst:
|
|
|
|
|
- shots = Math.Min(shots, gun.ShotsPerBurstModified - gun.ShotCounter);
|
|
|
|
|
- break;
|
|
|
|
|
- case SelectiveFire.FullAuto:
|
|
|
|
|
- break;
|
|
|
|
|
- default:
|
|
|
|
|
- throw new ArgumentOutOfRangeException($"No implemented shooting behavior for {gun.SelectedMode}!");
|
|
|
|
|
- }
|
|
|
|
|
- } else
|
|
|
|
|
|
|
+ switch (gun.SelectedMode)
|
|
|
{
|
|
{
|
|
|
- shots = Math.Min(shots, gun.ShotsPerBurstModified - gun.ShotCounter);
|
|
|
|
|
|
|
+ case SelectiveFire.SemiAuto:
|
|
|
|
|
+ shots = Math.Min(shots, 1 - gun.ShotCounter);
|
|
|
|
|
+ break;
|
|
|
|
|
+ case SelectiveFire.Burst:
|
|
|
|
|
+ shots = Math.Min(shots, gun.ShotsPerBurstModified - gun.ShotCounter);
|
|
|
|
|
+ break;
|
|
|
|
|
+ case SelectiveFire.FullAuto:
|
|
|
|
|
+ break;
|
|
|
|
|
+ default:
|
|
|
|
|
+ throw new ArgumentOutOfRangeException($"No implemented shooting behavior for {gun.SelectedMode}!");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
var attemptEv = new AttemptShootEvent(user, null);
|
|
var attemptEv = new AttemptShootEvent(user, null);
|
|
@@ -313,12 +306,14 @@ private void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun)
|
|
|
{
|
|
{
|
|
|
PopupSystem.PopupClient(attemptEv.Message, gunUid, user);
|
|
PopupSystem.PopupClient(attemptEv.Message, gunUid, user);
|
|
|
}
|
|
}
|
|
|
- gun.BurstActivated = false;
|
|
|
|
|
- gun.BurstShotsCount = 0;
|
|
|
|
|
|
|
+
|
|
|
gun.NextFire = TimeSpan.FromSeconds(Math.Max(lastFire.TotalSeconds + SafetyNextFire, gun.NextFire.TotalSeconds));
|
|
gun.NextFire = TimeSpan.FromSeconds(Math.Max(lastFire.TotalSeconds + SafetyNextFire, gun.NextFire.TotalSeconds));
|
|
|
- return;
|
|
|
|
|
|
|
+ return null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ if (!Timing.IsFirstTimePredicted)
|
|
|
|
|
+ return null;
|
|
|
|
|
+
|
|
|
var fromCoordinates = Transform(user).Coordinates;
|
|
var fromCoordinates = Transform(user).Coordinates;
|
|
|
// Remove ammo
|
|
// Remove ammo
|
|
|
var ev = new TakeAmmoEvent(shots, new List<(EntityUid? Entity, IShootable Shootable)>(), fromCoordinates, user);
|
|
var ev = new TakeAmmoEvent(shots, new List<(EntityUid? Entity, IShootable Shootable)>(), fromCoordinates, user);
|
|
@@ -334,7 +329,6 @@ private void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun)
|
|
|
// Even if we don't actually shoot update the ShotCounter. This is to avoid spamming empty sounds
|
|
// Even if we don't actually shoot update the ShotCounter. This is to avoid spamming empty sounds
|
|
|
// where the gun may be SemiAuto or Burst.
|
|
// where the gun may be SemiAuto or Burst.
|
|
|
gun.ShotCounter += shots;
|
|
gun.ShotCounter += shots;
|
|
|
- EntityManager.DirtyField(gunUid, gun, nameof(GunComponent.ShotCounter));
|
|
|
|
|
|
|
|
|
|
if (ev.Ammo.Count <= 0)
|
|
if (ev.Ammo.Count <= 0)
|
|
|
{
|
|
{
|
|
@@ -342,44 +336,27 @@ private void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun)
|
|
|
var emptyGunShotEvent = new OnEmptyGunShotEvent();
|
|
var emptyGunShotEvent = new OnEmptyGunShotEvent();
|
|
|
RaiseLocalEvent(gunUid, ref emptyGunShotEvent);
|
|
RaiseLocalEvent(gunUid, ref emptyGunShotEvent);
|
|
|
|
|
|
|
|
- gun.BurstActivated = false;
|
|
|
|
|
- gun.BurstShotsCount = 0;
|
|
|
|
|
- gun.NextFire += TimeSpan.FromSeconds(gun.BurstCooldown);
|
|
|
|
|
-
|
|
|
|
|
// Play empty gun sounds if relevant
|
|
// Play empty gun sounds if relevant
|
|
|
// If they're firing an existing clip then don't play anything.
|
|
// If they're firing an existing clip then don't play anything.
|
|
|
if (shots > 0)
|
|
if (shots > 0)
|
|
|
{
|
|
{
|
|
|
- PopupSystem.PopupCursor(ev.Reason ?? Loc.GetString("gun-magazine-fired-empty"));
|
|
|
|
|
|
|
+ if (ev.Reason != null && Timing.IsFirstTimePredicted)
|
|
|
|
|
+ {
|
|
|
|
|
+ PopupSystem.PopupCursor(ev.Reason);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
// Don't spam safety sounds at gun fire rate, play it at a reduced rate.
|
|
// Don't spam safety sounds at gun fire rate, play it at a reduced rate.
|
|
|
// May cause prediction issues? Needs more tweaking
|
|
// May cause prediction issues? Needs more tweaking
|
|
|
gun.NextFire = TimeSpan.FromSeconds(Math.Max(lastFire.TotalSeconds + SafetyNextFire, gun.NextFire.TotalSeconds));
|
|
gun.NextFire = TimeSpan.FromSeconds(Math.Max(lastFire.TotalSeconds + SafetyNextFire, gun.NextFire.TotalSeconds));
|
|
|
Audio.PlayPredicted(gun.SoundEmpty, gunUid, user);
|
|
Audio.PlayPredicted(gun.SoundEmpty, gunUid, user);
|
|
|
- return;
|
|
|
|
|
|
|
+ return null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Handle burstfire
|
|
|
|
|
- if (gun.SelectedMode == SelectiveFire.Burst)
|
|
|
|
|
- {
|
|
|
|
|
- gun.BurstActivated = true;
|
|
|
|
|
- }
|
|
|
|
|
- if (gun.BurstActivated)
|
|
|
|
|
- {
|
|
|
|
|
- gun.BurstShotsCount += shots;
|
|
|
|
|
- if (gun.BurstShotsCount >= gun.ShotsPerBurstModified)
|
|
|
|
|
- {
|
|
|
|
|
- gun.NextFire += TimeSpan.FromSeconds(gun.BurstCooldown);
|
|
|
|
|
- gun.BurstActivated = false;
|
|
|
|
|
- gun.BurstShotsCount = 0;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ return null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Shoot confirmed - sounds also played here in case it's invalid (e.g. cartridge already spent).
|
|
// Shoot confirmed - sounds also played here in case it's invalid (e.g. cartridge already spent).
|
|
|
- Shoot(gunUid, gun, ev.Ammo, fromCoordinates, toCoordinates.Value, out var userImpulse, user, throwItems: attemptEv.ThrowItems);
|
|
|
|
|
|
|
+ var projectiles = Shoot(gunUid, gun, ev.Ammo, fromCoordinates, toCoordinates.Value, out var userImpulse, user, throwItems: attemptEv.ThrowItems, predictedProjectiles, userSession);
|
|
|
var shotEv = new GunShotEvent(user, ev.Ammo);
|
|
var shotEv = new GunShotEvent(user, ev.Ammo);
|
|
|
RaiseLocalEvent(gunUid, ref shotEv);
|
|
RaiseLocalEvent(gunUid, ref shotEv);
|
|
|
|
|
|
|
@@ -388,6 +365,9 @@ private void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun)
|
|
|
if (_gravity.IsWeightless(user, userPhysics))
|
|
if (_gravity.IsWeightless(user, userPhysics))
|
|
|
CauseImpulse(fromCoordinates, toCoordinates.Value, user, userPhysics);
|
|
CauseImpulse(fromCoordinates, toCoordinates.Value, user, userPhysics);
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ Dirty(gunUid, gun);
|
|
|
|
|
+ return projectiles;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public void Shoot(
|
|
public void Shoot(
|
|
@@ -404,7 +384,7 @@ private void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun)
|
|
|
Shoot(gunUid, gun, new List<(EntityUid? Entity, IShootable Shootable)>(1) { (ammo, shootable) }, fromCoordinates, toCoordinates, out userImpulse, user, throwItems);
|
|
Shoot(gunUid, gun, new List<(EntityUid? Entity, IShootable Shootable)>(1) { (ammo, shootable) }, fromCoordinates, toCoordinates, out userImpulse, user, throwItems);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- public abstract void Shoot(
|
|
|
|
|
|
|
+ public List<EntityUid>? Shoot(
|
|
|
EntityUid gunUid,
|
|
EntityUid gunUid,
|
|
|
GunComponent gun,
|
|
GunComponent gun,
|
|
|
List<(EntityUid? Entity, IShootable Shootable)> ammo,
|
|
List<(EntityUid? Entity, IShootable Shootable)> ammo,
|
|
@@ -412,9 +392,458 @@ private void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun)
|
|
|
EntityCoordinates toCoordinates,
|
|
EntityCoordinates toCoordinates,
|
|
|
out bool userImpulse,
|
|
out bool userImpulse,
|
|
|
EntityUid? user = null,
|
|
EntityUid? user = null,
|
|
|
- bool throwItems = false);
|
|
|
|
|
|
|
+ bool throwItems = false,
|
|
|
|
|
+ List<int>? predictedProjectiles = null,
|
|
|
|
|
+ ICommonSession? userSession = null)
|
|
|
|
|
+ {
|
|
|
|
|
+ userImpulse = true;
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ var fromMap = fromCoordinates.ToMap(EntityManager, TransformSystem);
|
|
|
|
|
+ var toMap = toCoordinates.ToMapPos(EntityManager, TransformSystem);
|
|
|
|
|
+ var mapDirection = toMap - fromMap.Position;
|
|
|
|
|
+ var mapAngle = mapDirection.ToAngle();
|
|
|
|
|
+ var angle = GetRecoilAngle(Timing.CurTime, gun, mapDirection.ToAngle());
|
|
|
|
|
+
|
|
|
|
|
+ // If applicable, this ensures the projectile is parented to grid on spawn, instead of the map.
|
|
|
|
|
+ var fromEnt = MapManager.TryFindGridAt(fromMap, out var gridUid, out var grid)
|
|
|
|
|
+ ? fromCoordinates.WithEntityId(gridUid, EntityManager)
|
|
|
|
|
+ : new EntityCoordinates(MapManager.GetMapEntityId(fromMap.MapId), fromMap.Position);
|
|
|
|
|
+
|
|
|
|
|
+ // Update shot based on the recoil
|
|
|
|
|
+ toMap = fromMap.Position + angle.ToVec() * mapDirection.Length();
|
|
|
|
|
+ mapDirection = toMap - fromMap.Position;
|
|
|
|
|
+ var gunVelocity = Physics.GetMapLinearVelocity(fromEnt);
|
|
|
|
|
+
|
|
|
|
|
+ // I must be high because this was getting tripped even when true.
|
|
|
|
|
+ // DebugTools.Assert(direction != Vector2.Zero);
|
|
|
|
|
+ var shotProjectiles = new List<EntityUid>(ammo.Count);
|
|
|
|
|
+
|
|
|
|
|
+ void MarkPredicted(EntityUid projectile, int index)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (!GunPrediction)
|
|
|
|
|
+ return;
|
|
|
|
|
+
|
|
|
|
|
+ if (predictedProjectiles == null || userSession == null)
|
|
|
|
|
+ return;
|
|
|
|
|
+
|
|
|
|
|
+ if (predictedProjectiles.TryGetValue(index, out var predicted))
|
|
|
|
|
+ {
|
|
|
|
|
+ var comp = new PredictedProjectileServerComponent
|
|
|
|
|
+ {
|
|
|
|
|
+ Shooter = userSession,
|
|
|
|
|
+ ClientId = predicted,
|
|
|
|
|
+ ClientEnt = user,
|
|
|
|
|
+ };
|
|
|
|
|
+ AddComp(projectile, comp, true);
|
|
|
|
|
+ Dirty(projectile, comp);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ foreach (var (ent, shootable) in ammo)
|
|
|
|
|
+ {
|
|
|
|
|
+ // pneumatic cannon doesn't shoot bullets it just throws them, ignore ammo handling
|
|
|
|
|
+ if (throwItems && ent != null)
|
|
|
|
|
+ {
|
|
|
|
|
+ Recoil(user, mapDirection, gun.CameraRecoilScalarModified);
|
|
|
|
|
+ ShootOrThrow(ent.Value, mapDirection, gunVelocity, gun, gunUid, user);
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ switch (shootable)
|
|
|
|
|
+ {
|
|
|
|
|
+ // Cartridge shoots something else
|
|
|
|
|
+ case CartridgeAmmoComponent cartridge:
|
|
|
|
|
+ if (!cartridge.Spent)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (_netManager.IsServer || GunPrediction)
|
|
|
|
|
+ {
|
|
|
|
|
+ var uid = Spawn(cartridge.Prototype, fromEnt);
|
|
|
|
|
+ shotProjectiles.Add(uid);
|
|
|
|
|
+ CreateAndFireProjectiles(uid, cartridge);
|
|
|
|
|
+
|
|
|
|
|
+ RaiseLocalEvent(ent!.Value, new AmmoShotEvent()
|
|
|
|
|
+ {
|
|
|
|
|
+ FiredProjectiles = shotProjectiles,
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ SetCartridgeSpent(ent.Value, cartridge, true);
|
|
|
|
|
+
|
|
|
|
|
+ if (cartridge.DeleteOnSpawn &&
|
|
|
|
|
+ (_netManager.IsServer || IsClientSide(ent.Value)))
|
|
|
|
|
+ {
|
|
|
|
|
+ Del(ent.Value);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ MuzzleFlash(gunUid, cartridge, mapDirection.ToAngle(), user);
|
|
|
|
|
+ Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ userImpulse = false;
|
|
|
|
|
+ Audio.PlayPredicted(gun.SoundEmpty, gunUid, user);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Recoil(user, mapDirection, gun.CameraRecoilScalarModified);
|
|
|
|
|
+
|
|
|
|
|
+ // Something like ballistic might want to leave it in the container still
|
|
|
|
|
+ if (!cartridge.DeleteOnSpawn && !Containers.IsEntityInContainer(ent!.Value))
|
|
|
|
|
+ EjectCartridge(ent.Value, angle);
|
|
|
|
|
+
|
|
|
|
|
+ if (IsClientSide(ent!.Value))
|
|
|
|
|
+ Del(ent.Value);
|
|
|
|
|
+ else
|
|
|
|
|
+ Dirty(ent!.Value, cartridge);
|
|
|
|
|
+ break;
|
|
|
|
|
+ // Ammo shoots itself
|
|
|
|
|
+ case AmmoComponent newAmmo:
|
|
|
|
|
+ if (_netManager.IsServer || GunPrediction)
|
|
|
|
|
+ {
|
|
|
|
|
+ shotProjectiles.Add(ent!.Value);
|
|
|
|
|
+ CreateAndFireProjectiles(ent.Value, newAmmo);
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ MuzzleFlash(gunUid, newAmmo, mapDirection.ToAngle(), user);
|
|
|
|
|
+ Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Recoil(user, mapDirection, gun.CameraRecoilScalarModified);
|
|
|
|
|
+
|
|
|
|
|
+ if (IsClientSide(ent!.Value))
|
|
|
|
|
+ Del(ent.Value);
|
|
|
|
|
+ else if (_netManager.IsClient)
|
|
|
|
|
+ RemoveShootable(ent.Value);
|
|
|
|
|
+ MarkPredicted(ent!.Value, 0);
|
|
|
|
|
+ break;
|
|
|
|
|
+ case HitscanPrototype hitscan:
|
|
|
|
|
+ EntityUid? lastHit = null;
|
|
|
|
|
+
|
|
|
|
|
+ var from = fromMap;
|
|
|
|
|
+ // can't use map coords above because funny FireEffects
|
|
|
|
|
+ var fromEffect = fromCoordinates;
|
|
|
|
|
+ var dir = mapDirection.Normalized();
|
|
|
|
|
+
|
|
|
|
|
+ //in the situation when user == null, means that the cannon fires on its own (via signals). And we need the gun to not fire by itself in this case
|
|
|
|
|
+ var lastUser = user ?? gunUid;
|
|
|
|
|
+
|
|
|
|
|
+ if (hitscan.Reflective != ReflectType.None)
|
|
|
|
|
+ {
|
|
|
|
|
+ for (var reflectAttempt = 0; reflectAttempt < 3; reflectAttempt++)
|
|
|
|
|
+ {
|
|
|
|
|
+ var ray = new CollisionRay(from.Position, dir, hitscan.CollisionMask);
|
|
|
|
|
+ var rayCastResults =
|
|
|
|
|
+ Physics.IntersectRay(from.MapId, ray, hitscan.MaxLength, lastUser, false).ToList();
|
|
|
|
|
+ if (!rayCastResults.Any())
|
|
|
|
|
+ break;
|
|
|
|
|
+
|
|
|
|
|
+ var result = rayCastResults[0];
|
|
|
|
|
+
|
|
|
|
|
+ // Check if laser is shot from in a container
|
|
|
|
|
+ if (!Containers.IsEntityOrParentInContainer(lastUser))
|
|
|
|
|
+ {
|
|
|
|
|
+ // Checks if the laser should pass over unless targeted by its user
|
|
|
|
|
+ foreach (var collide in rayCastResults)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (collide.HitEntity != gun.Target &&
|
|
|
|
|
+ CompOrNull<RequireProjectileTargetComponent>(collide.HitEntity)?.Active == true)
|
|
|
|
|
+ {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ result = collide;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var hit = result.HitEntity;
|
|
|
|
|
+ lastHit = hit;
|
|
|
|
|
+
|
|
|
|
|
+ FireEffects(fromEffect, result.Distance, dir.Normalized().ToAngle(), hitscan, hit);
|
|
|
|
|
+
|
|
|
|
|
+ var ev = new HitScanReflectAttemptEvent(user, gunUid, hitscan.Reflective, dir, false);
|
|
|
|
|
+ RaiseLocalEvent(hit, ref ev);
|
|
|
|
|
+
|
|
|
|
|
+ if (!ev.Reflected)
|
|
|
|
|
+ break;
|
|
|
|
|
+
|
|
|
|
|
+ fromEffect = Transform(hit).Coordinates;
|
|
|
|
|
+ from = fromEffect.ToMap(EntityManager, TransformSystem);
|
|
|
|
|
+ dir = ev.Direction;
|
|
|
|
|
+ lastUser = hit;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (lastHit != null)
|
|
|
|
|
+ {
|
|
|
|
|
+ var hitEntity = lastHit.Value;
|
|
|
|
|
+ if (hitscan.StaminaDamage > 0f)
|
|
|
|
|
+ _stamina.TakeStaminaDamage(hitEntity, hitscan.StaminaDamage, source: user);
|
|
|
|
|
+
|
|
|
|
|
+ var dmg = hitscan.Damage;
|
|
|
|
|
+
|
|
|
|
|
+ var hitName = ToPrettyString(hitEntity);
|
|
|
|
|
+ if (dmg != null)
|
|
|
|
|
+ dmg = Damageable.TryChangeDamage(hitEntity, dmg, origin: user);
|
|
|
|
|
+
|
|
|
|
|
+ // check null again, as TryChangeDamage returns modified damage values
|
|
|
|
|
+ if (dmg != null)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (!Deleted(hitEntity))
|
|
|
|
|
+ {
|
|
|
|
|
+ if (dmg.AnyPositive())
|
|
|
|
|
+ {
|
|
|
|
|
+ _color.RaiseEffect(Color.Red, new List<EntityUid>() { hitEntity }, Filter.Pvs(hitEntity, entityManager: EntityManager));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // TODO get fallback position for playing hit sound.
|
|
|
|
|
+ PlayImpactSound(hitEntity, dmg, hitscan.Sound, hitscan.ForceSound);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (user != null)
|
|
|
|
|
+ {
|
|
|
|
|
+ Logs.Add(LogType.HitScanHit,
|
|
|
|
|
+ $"{ToPrettyString(user.Value):user} hit {hitName:target} using hitscan and dealt {dmg.GetTotal():damage} damage");
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ Logs.Add(LogType.HitScanHit,
|
|
|
|
|
+ $"{hitName:target} hit by hitscan dealing {dmg.GetTotal():damage} damage");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ FireEffects(fromEffect, hitscan.MaxLength, dir.ToAngle(), hitscan);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
|
|
|
|
|
+ Recoil(user, mapDirection, gun.CameraRecoilScalarModified);
|
|
|
|
|
+ break;
|
|
|
|
|
+ default:
|
|
|
|
|
+ throw new ArgumentOutOfRangeException();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ RaiseLocalEvent(gunUid, new AmmoShotEvent()
|
|
|
|
|
+ {
|
|
|
|
|
+ FiredProjectiles = shotProjectiles,
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ void CreateAndFireProjectiles(EntityUid ammoEnt, AmmoComponent ammoComp)
|
|
|
|
|
+ {
|
|
|
|
|
+ predictedProjectiles ??= new List<int>();
|
|
|
|
|
+ MarkPredicted(ammoEnt, 0);
|
|
|
|
|
+ if (TryComp<ProjectileSpreadComponent>(ammoEnt, out var ammoSpreadComp))
|
|
|
|
|
+ {
|
|
|
|
|
+ var spreadEvent = new GunGetAmmoSpreadEvent(ammoSpreadComp.Spread);
|
|
|
|
|
+ RaiseLocalEvent(gunUid, ref spreadEvent);
|
|
|
|
|
+
|
|
|
|
|
+ var angles = LinearSpread(mapAngle - spreadEvent.Spread / 2,
|
|
|
|
|
+ mapAngle + spreadEvent.Spread / 2, ammoSpreadComp.Count);
|
|
|
|
|
+
|
|
|
|
|
+ ShootOrThrow(ammoEnt, angles[0].ToVec(), gunVelocity, gun, gunUid, user);
|
|
|
|
|
+ shotProjectiles.Add(ammoEnt);
|
|
|
|
|
+
|
|
|
|
|
+ for (var i = 1; i < ammoSpreadComp.Count; i++)
|
|
|
|
|
+ {
|
|
|
|
|
+ var newuid = Spawn(ammoSpreadComp.Proto, fromEnt);
|
|
|
|
|
+ ShootOrThrow(newuid, angles[i].ToVec(), gunVelocity, gun, gunUid, user);
|
|
|
|
|
+ shotProjectiles.Add(newuid);
|
|
|
|
|
+ MarkPredicted(newuid, i + 1);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ else
|
|
|
|
|
+ {
|
|
|
|
|
+ ShootOrThrow(ammoEnt, mapDirection, gunVelocity, gun, gunUid, user);
|
|
|
|
|
+ shotProjectiles.Add(ammoEnt);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ MuzzleFlash(gunUid, ammoComp, mapDirection.ToAngle(), user);
|
|
|
|
|
+ Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return shotProjectiles;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Angle GetRecoilAngle(TimeSpan curTime, GunComponent component, Angle direction)
|
|
|
|
|
+ {
|
|
|
|
|
+ var timeSinceLastFire = (curTime - component.LastFire).TotalSeconds;
|
|
|
|
|
+ var newTheta = MathHelper.Clamp(component.CurrentAngle.Theta + component.AngleIncreaseModified.Theta - component.AngleDecayModified.Theta * timeSinceLastFire, component.MinAngleModified.Theta, component.MaxAngleModified.Theta);
|
|
|
|
|
+ component.CurrentAngle = new Angle(newTheta);
|
|
|
|
|
+ component.LastFire = component.NextFire;
|
|
|
|
|
+
|
|
|
|
|
+ // Convert it so angle can go either side.
|
|
|
|
|
+ long tick = Timing.CurTick.Value;
|
|
|
|
|
+ tick = tick << 32;
|
|
|
|
|
+ tick = tick | (uint)GetNetEntity(component.Owner).Id;
|
|
|
|
|
+ Logger.Info(Timing.CurTick.ToString());
|
|
|
|
|
+ var random = new Xoroshiro64S(tick).NextFloat(-0.5f, 0.5f);
|
|
|
|
|
+ var spread = component.CurrentAngle.Theta * random;
|
|
|
|
|
+ var angle = new Angle(direction.Theta + component.CurrentAngle.Theta * random);
|
|
|
|
|
+ DebugTools.Assert(spread <= component.MaxAngleModified.Theta);
|
|
|
|
|
+ return angle;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void ShootOrThrow(EntityUid uid, Vector2 mapDirection, Vector2 gunVelocity, GunComponent gun, EntityUid gunUid, EntityUid? user)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (gun.Target is { } target && !TerminatingOrDeleted(target))
|
|
|
|
|
+ {
|
|
|
|
|
+ var targeted = EnsureComp<TargetedProjectileComponent>(uid);
|
|
|
|
|
+ targeted.Target = target;
|
|
|
|
|
+ Dirty(uid, targeted);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Do a throw
|
|
|
|
|
+ if (!HasComp<ProjectileComponent>(uid))
|
|
|
|
|
+ {
|
|
|
|
|
+ RemoveShootable(uid);
|
|
|
|
|
+ // TODO: Someone can probably yeet this a billion miles so need to pre-validate input somewhere up the call stack.
|
|
|
|
|
+ ThrowingSystem.TryThrow(uid, mapDirection, gun.ProjectileSpeedModified, user);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ ShootProjectile(uid, mapDirection, gunVelocity, gunUid, user, gun.ProjectileSpeedModified);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ #region Hitscan effects
|
|
|
|
|
+
|
|
|
|
|
+ private void FireEffects(EntityCoordinates fromCoordinates, float distance, Angle mapDirection, HitscanPrototype hitscan, EntityUid? hitEntity = null)
|
|
|
|
|
+ {
|
|
|
|
|
+ // Lord
|
|
|
|
|
+ // Forgive me for the shitcode I am about to do
|
|
|
|
|
+ // Effects tempt me not
|
|
|
|
|
+ var sprites = new List<(NetCoordinates coordinates, Angle angle, SpriteSpecifier sprite, float scale)>();
|
|
|
|
|
+ var gridUid = fromCoordinates.GetGridUid(EntityManager);
|
|
|
|
|
+ var angle = mapDirection;
|
|
|
|
|
+
|
|
|
|
|
+ // We'll get the effects relative to the grid / map of the firer
|
|
|
|
|
+ // Look you could probably optimise this a bit with redundant transforms at this point.
|
|
|
|
|
+ var xformQuery = GetEntityQuery<TransformComponent>();
|
|
|
|
|
+
|
|
|
|
|
+ if (xformQuery.TryGetComponent(gridUid, out var gridXform))
|
|
|
|
|
+ {
|
|
|
|
|
+ var (_, gridRot, gridInvMatrix) = TransformSystem.GetWorldPositionRotationInvMatrix(gridXform, xformQuery);
|
|
|
|
|
+
|
|
|
|
|
+ fromCoordinates = new EntityCoordinates(gridUid.Value,
|
|
|
|
|
+ Vector2.Transform(fromCoordinates.ToMapPos(EntityManager, TransformSystem), gridInvMatrix));
|
|
|
|
|
+
|
|
|
|
|
+ // Use the fallback angle I guess?
|
|
|
|
|
+ angle -= gridRot;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (distance >= 1f)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (hitscan.MuzzleFlash != null)
|
|
|
|
|
+ {
|
|
|
|
|
+ var coords = fromCoordinates.Offset(angle.ToVec().Normalized() / 2);
|
|
|
|
|
+ var netCoords = GetNetCoordinates(coords);
|
|
|
|
|
+
|
|
|
|
|
+ sprites.Add((netCoords, angle, hitscan.MuzzleFlash, 1f));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (hitscan.TravelFlash != null)
|
|
|
|
|
+ {
|
|
|
|
|
+ var coords = fromCoordinates.Offset(angle.ToVec() * (distance + 0.5f) / 2);
|
|
|
|
|
+ var netCoords = GetNetCoordinates(coords);
|
|
|
|
|
+
|
|
|
|
|
+ sprites.Add((netCoords, angle, hitscan.TravelFlash, distance - 1.5f));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (hitscan.ImpactFlash != null)
|
|
|
|
|
+ {
|
|
|
|
|
+ var coords = fromCoordinates.Offset(angle.ToVec() * distance);
|
|
|
|
|
+ var netCoords = GetNetCoordinates(coords);
|
|
|
|
|
+
|
|
|
|
|
+ sprites.Add((netCoords, angle.FlipPositive(), hitscan.ImpactFlash, 1f));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (_netManager.IsServer && sprites.Count > 0)
|
|
|
|
|
+ {
|
|
|
|
|
+ RaiseNetworkEvent(new HitscanEvent
|
|
|
|
|
+ {
|
|
|
|
|
+ Sprites = sprites,
|
|
|
|
|
+ }, Filter.Pvs(fromCoordinates, entityMan: EntityManager));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ #endregion
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// Gets a linear spread of angles between start and end.
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ /// <param name="start">Start angle in degrees</param>
|
|
|
|
|
+ /// <param name="end">End angle in degrees</param>
|
|
|
|
|
+ /// <param name="intervals">How many shots there are</param>
|
|
|
|
|
+ private Angle[] LinearSpread(Angle start, Angle end, int intervals)
|
|
|
|
|
+ {
|
|
|
|
|
+ var angles = new Angle[intervals];
|
|
|
|
|
+ DebugTools.Assert(intervals > 1);
|
|
|
|
|
+
|
|
|
|
|
+ for (var i = 0; i <= intervals - 1; i++)
|
|
|
|
|
+ {
|
|
|
|
|
+ angles[i] = new Angle(start + (end - start) * i / (intervals - 1));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return angles;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public void PlayImpactSound(EntityUid otherEntity, DamageSpecifier? modifiedDamage, SoundSpecifier? weaponSound, bool forceWeaponSound, Filter? filter = null, EntityUid? projectile = null)
|
|
|
|
|
+ {
|
|
|
|
|
+ DebugTools.Assert(!Deleted(otherEntity), "Impact sound entity was deleted");
|
|
|
|
|
+
|
|
|
|
|
+ // Like projectiles and melee,
|
|
|
|
|
+ // 1. Entity specific sound
|
|
|
|
|
+ // 2. Ammo's sound
|
|
|
|
|
+ // 3. Nothing
|
|
|
|
|
+ if (_netManager.IsClient && HasComp<PredictedProjectileServerComponent>(projectile))
|
|
|
|
|
+ return;
|
|
|
|
|
+
|
|
|
|
|
+ filter ??= Filter.Pvs(otherEntity);
|
|
|
|
|
+ var playedSound = false;
|
|
|
|
|
+
|
|
|
|
|
+ if (!forceWeaponSound && modifiedDamage != null && modifiedDamage.GetTotal() > 0 && TryComp<RangedDamageSoundComponent>(otherEntity, out var rangedSound))
|
|
|
|
|
+ {
|
|
|
|
|
+ var type = SharedMeleeWeaponSystem.GetHighestDamageSound(modifiedDamage, ProtoManager);
|
|
|
|
|
+
|
|
|
|
|
+ if (type != null &&
|
|
|
|
|
+ rangedSound.SoundTypes?.TryGetValue(type, out var damageSoundType) == true &&
|
|
|
|
|
+ filter.Count > 0)
|
|
|
|
|
+ {
|
|
|
|
|
+ Audio.PlayEntity(damageSoundType, filter, otherEntity, true, AudioParams.Default.WithVariation(DamagePitchVariation));
|
|
|
|
|
+ playedSound = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ else if (type != null &&
|
|
|
|
|
+ rangedSound.SoundGroups?.TryGetValue(type, out var damageSoundGroup) == true &&
|
|
|
|
|
+ filter.Count > 0)
|
|
|
|
|
+ {
|
|
|
|
|
+ Audio.PlayEntity(damageSoundGroup, filter, otherEntity, true, AudioParams.Default.WithVariation(DamagePitchVariation));
|
|
|
|
|
+ playedSound = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- public void ShootProjectile(EntityUid uid, Vector2 direction, Vector2 gunVelocity, EntityUid gunUid, EntityUid? user = null, float speed = 20f)
|
|
|
|
|
|
|
+ if (!playedSound && weaponSound != null && filter.Count > 0)
|
|
|
|
|
+ {
|
|
|
|
|
+ Audio.PlayEntity(weaponSound, filter, otherEntity, true);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void Recoil(EntityUid? user, Vector2 recoil, float recoilScalar)
|
|
|
|
|
+ {
|
|
|
|
|
+ if (_netManager.IsServer)
|
|
|
|
|
+ return;
|
|
|
|
|
+
|
|
|
|
|
+ if (!Timing.IsFirstTimePredicted || user == null || recoil == Vector2.Zero || recoilScalar == 0)
|
|
|
|
|
+ return;
|
|
|
|
|
+
|
|
|
|
|
+ _recoil.KickCamera(user.Value, recoil.Normalized() * 0.5f * recoilScalar);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public virtual void ShootProjectile(EntityUid uid, Vector2 direction, Vector2 gunVelocity, EntityUid gunUid, EntityUid? user = null, float speed = 20f)
|
|
|
{
|
|
{
|
|
|
var physics = EnsureComp<PhysicsComponent>(uid);
|
|
var physics = EnsureComp<PhysicsComponent>(uid);
|
|
|
Physics.SetBodyStatus(uid, physics, BodyStatus.InAir);
|
|
Physics.SetBodyStatus(uid, physics, BodyStatus.InAir);
|
|
@@ -428,7 +857,26 @@ public void ShootProjectile(EntityUid uid, Vector2 direction, Vector2 gunVelocit
|
|
|
Projectiles.SetShooter(uid, projectile, user ?? gunUid);
|
|
Projectiles.SetShooter(uid, projectile, user ?? gunUid);
|
|
|
projectile.Weapon = gunUid;
|
|
projectile.Weapon = gunUid;
|
|
|
|
|
|
|
|
- TransformSystem.SetWorldRotation(uid, direction.ToWorldAngle() + projectile.Angle);
|
|
|
|
|
|
|
+ TransformSystem.SetWorldRotationNoLerp(uid, direction.ToWorldAngle());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public List<EntityUid>? ShootRequested(NetEntity netGun, NetCoordinates coordinates, NetEntity? target, List<int>? projectiles, ICommonSession session)
|
|
|
|
|
+ {
|
|
|
|
|
+ var user = session.AttachedEntity;
|
|
|
|
|
+
|
|
|
|
|
+ if (user == null ||
|
|
|
|
|
+ !_combatMode.IsInCombatMode(user) ||
|
|
|
|
|
+ !TryGetGun(user.Value, out var ent, out var gun))
|
|
|
|
|
+ {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (ent != GetEntity(netGun))
|
|
|
|
|
+ return null;
|
|
|
|
|
+
|
|
|
|
|
+ gun.ShootCoordinates = GetCoordinates(coordinates);
|
|
|
|
|
+ gun.Target = GetEntity(target);
|
|
|
|
|
+ return AttemptShoot(user.Value, ent, gun, projectiles, session);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
protected abstract void Popup(string message, EntityUid? uid, EntityUid? user);
|
|
protected abstract void Popup(string message, EntityUid? uid, EntityUid? user);
|
|
@@ -436,12 +884,12 @@ public void ShootProjectile(EntityUid uid, Vector2 direction, Vector2 gunVelocit
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// Call this whenever the ammo count for a gun changes.
|
|
/// Call this whenever the ammo count for a gun changes.
|
|
|
/// </summary>
|
|
/// </summary>
|
|
|
- protected virtual void UpdateAmmoCount(EntityUid uid, bool prediction = true) {}
|
|
|
|
|
|
|
+ protected virtual void UpdateAmmoCount(EntityUid uid, bool prediction = true) { }
|
|
|
|
|
|
|
|
protected void SetCartridgeSpent(EntityUid uid, CartridgeAmmoComponent cartridge, bool spent)
|
|
protected void SetCartridgeSpent(EntityUid uid, CartridgeAmmoComponent cartridge, bool spent)
|
|
|
{
|
|
{
|
|
|
if (cartridge.Spent != spent)
|
|
if (cartridge.Spent != spent)
|
|
|
- DirtyField(uid, cartridge, nameof(CartridgeAmmoComponent.Spent));
|
|
|
|
|
|
|
+ Dirty(uid, cartridge);
|
|
|
|
|
|
|
|
cartridge.Spent = spent;
|
|
cartridge.Spent = spent;
|
|
|
Appearance.SetData(uid, AmmoVisuals.Spent, spent);
|
|
Appearance.SetData(uid, AmmoVisuals.Spent, spent);
|
|
@@ -505,7 +953,7 @@ protected void MuzzleFlash(EntityUid gun, AmmoComponent component, Angle worldAn
|
|
|
return;
|
|
return;
|
|
|
|
|
|
|
|
var ev = new MuzzleFlashEvent(GetNetEntity(gun), sprite, worldAngle);
|
|
var ev = new MuzzleFlashEvent(GetNetEntity(gun), sprite, worldAngle);
|
|
|
- CreateEffect(gun, ev, user);
|
|
|
|
|
|
|
+ CreateEffect(gun, ev, gun, user);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public void CauseImpulse(EntityCoordinates fromCoordinates, EntityCoordinates toCoordinates, EntityUid user, PhysicsComponent userPhysics)
|
|
public void CauseImpulse(EntityCoordinates fromCoordinates, EntityCoordinates toCoordinates, EntityUid user, PhysicsComponent userPhysics)
|
|
@@ -515,7 +963,7 @@ public void CauseImpulse(EntityCoordinates fromCoordinates, EntityCoordinates to
|
|
|
var shotDirection = (toMap - fromMap).Normalized();
|
|
var shotDirection = (toMap - fromMap).Normalized();
|
|
|
|
|
|
|
|
const float impulseStrength = 25.0f;
|
|
const float impulseStrength = 25.0f;
|
|
|
- var impulseVector = shotDirection * impulseStrength;
|
|
|
|
|
|
|
+ var impulseVector = shotDirection * impulseStrength;
|
|
|
Physics.ApplyLinearImpulse(user, -impulseVector, body: userPhysics);
|
|
Physics.ApplyLinearImpulse(user, -impulseVector, body: userPhysics);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -540,62 +988,20 @@ public void RefreshModifiers(Entity<GunComponent?> gun)
|
|
|
|
|
|
|
|
RaiseLocalEvent(gun, ref ev);
|
|
RaiseLocalEvent(gun, ref ev);
|
|
|
|
|
|
|
|
- if (comp.SoundGunshotModified != ev.SoundGunshot)
|
|
|
|
|
- {
|
|
|
|
|
- comp.SoundGunshotModified = ev.SoundGunshot;
|
|
|
|
|
- DirtyField(gun, nameof(GunComponent.SoundGunshotModified));
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (!MathHelper.CloseTo(comp.CameraRecoilScalarModified, ev.CameraRecoilScalar))
|
|
|
|
|
- {
|
|
|
|
|
- comp.CameraRecoilScalarModified = ev.CameraRecoilScalar;
|
|
|
|
|
- DirtyField(gun, nameof(GunComponent.CameraRecoilScalarModified));
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (!comp.AngleIncreaseModified.EqualsApprox(ev.AngleIncrease))
|
|
|
|
|
- {
|
|
|
|
|
- comp.AngleIncreaseModified = ev.AngleIncrease;
|
|
|
|
|
- DirtyField(gun, nameof(GunComponent.AngleIncreaseModified));
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (!comp.AngleDecayModified.EqualsApprox(ev.AngleDecay))
|
|
|
|
|
- {
|
|
|
|
|
- comp.AngleDecayModified = ev.AngleDecay;
|
|
|
|
|
- DirtyField(gun, nameof(GunComponent.AngleDecayModified));
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (!comp.MaxAngleModified.EqualsApprox(ev.MinAngle))
|
|
|
|
|
- {
|
|
|
|
|
- comp.MaxAngleModified = ev.MaxAngle;
|
|
|
|
|
- DirtyField(gun, nameof(GunComponent.MaxAngleModified));
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (!comp.MinAngleModified.EqualsApprox(ev.MinAngle))
|
|
|
|
|
- {
|
|
|
|
|
- comp.MinAngleModified = ev.MinAngle;
|
|
|
|
|
- DirtyField(gun, nameof(GunComponent.MinAngleModified));
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (comp.ShotsPerBurstModified != ev.ShotsPerBurst)
|
|
|
|
|
- {
|
|
|
|
|
- comp.ShotsPerBurstModified = ev.ShotsPerBurst;
|
|
|
|
|
- DirtyField(gun, nameof(GunComponent.ShotsPerBurstModified));
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (!MathHelper.CloseTo(comp.FireRateModified, ev.FireRate))
|
|
|
|
|
- {
|
|
|
|
|
- comp.FireRateModified = ev.FireRate;
|
|
|
|
|
- DirtyField(gun, nameof(GunComponent.FireRateModified));
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (!MathHelper.CloseTo(comp.ProjectileSpeedModified, ev.ProjectileSpeed))
|
|
|
|
|
- {
|
|
|
|
|
- comp.ProjectileSpeedModified = ev.ProjectileSpeed;
|
|
|
|
|
- DirtyField(gun, nameof(GunComponent.ProjectileSpeedModified));
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ comp.SoundGunshotModified = ev.SoundGunshot;
|
|
|
|
|
+ comp.CameraRecoilScalarModified = ev.CameraRecoilScalar;
|
|
|
|
|
+ comp.AngleIncreaseModified = ev.AngleIncrease;
|
|
|
|
|
+ comp.AngleDecayModified = ev.AngleDecay;
|
|
|
|
|
+ comp.MaxAngleModified = ev.MaxAngle;
|
|
|
|
|
+ comp.MinAngleModified = ev.MinAngle;
|
|
|
|
|
+ comp.ShotsPerBurstModified = ev.ShotsPerBurst;
|
|
|
|
|
+ comp.FireRateModified = ev.FireRate;
|
|
|
|
|
+ comp.ProjectileSpeedModified = ev.ProjectileSpeed;
|
|
|
|
|
+
|
|
|
|
|
+ Dirty(gun);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- protected abstract void CreateEffect(EntityUid gunUid, MuzzleFlashEvent message, EntityUid? user = null);
|
|
|
|
|
|
|
+ protected abstract void CreateEffect(EntityUid gunUid, MuzzleFlashEvent message, EntityUid? user = null, EntityUid? player = null);
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
|
/// Used for animated effects on the client.
|
|
/// Used for animated effects on the client.
|