* First commit * Lockers * Lockers electric boogaloo * Crates and Lockers * Almost finishing the Textures folder * Updating texture paths. Reminder to fix: * Lockers * Windows * Vending Machines * APC * Catwalks * Bedsheets and Cloaks * Status effects * dont know what happened here * Commit before merge * re-organizing * Lockers broken * Commit before merge * Submodule * renaming * Fixing most issues * forgot these ones * Updating submodule * typo * Fixing some paths * fixing some paths * updating submodule * (hopefully) fixing the submodule
414 lines
16 KiB
C#
414 lines
16 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Content.Server.GameObjects.Components.Mobs;
|
|
using Content.Server.GameObjects.Components.Projectiles;
|
|
using Content.Server.GameObjects.Components.Weapon.Ranged.Ammunition;
|
|
using Content.Server.GameObjects.EntitySystems;
|
|
using Content.Server.Interfaces.GameObjects.Components.Interaction;
|
|
using Content.Shared.Audio;
|
|
using Content.Shared.GameObjects.Components.Weapons.Ranged;
|
|
using Robust.Server.GameObjects.EntitySystems;
|
|
using Robust.Shared.Audio;
|
|
using Robust.Shared.GameObjects.Components;
|
|
using Robust.Shared.GameObjects.Systems;
|
|
using Robust.Shared.Interfaces.GameObjects;
|
|
using Robust.Shared.Interfaces.Map;
|
|
using Robust.Shared.Interfaces.Physics;
|
|
using Robust.Shared.Interfaces.Random;
|
|
using Robust.Shared.Interfaces.Timing;
|
|
using Robust.Shared.IoC;
|
|
using Robust.Shared.Log;
|
|
using Robust.Shared.Map;
|
|
using Robust.Shared.Maths;
|
|
using Robust.Shared.Prototypes;
|
|
using Robust.Shared.Random;
|
|
using Robust.Shared.Serialization;
|
|
using Robust.Shared.Utility;
|
|
|
|
namespace Content.Server.GameObjects.Components.Weapon.Ranged.Barrels
|
|
{
|
|
/// <summary>
|
|
/// All of the ranged weapon components inherit from this to share mechanics like shooting etc.
|
|
/// Only difference between them is how they retrieve a projectile to shoot (battery, magazine, etc.)
|
|
/// </summary>
|
|
public abstract class ServerRangedBarrelComponent : SharedRangedBarrelComponent, IUse, IInteractUsing
|
|
{
|
|
// There's still some of py01 and PJB's work left over, especially in underlying shooting logic,
|
|
// it's just when I re-organised it changed me as the contributor
|
|
#pragma warning disable 649
|
|
[Dependency] private IGameTiming _gameTiming;
|
|
[Dependency] private IRobustRandom _robustRandom;
|
|
#pragma warning restore 649
|
|
|
|
public override FireRateSelector FireRateSelector => _fireRateSelector;
|
|
private FireRateSelector _fireRateSelector;
|
|
public override FireRateSelector AllRateSelectors => _fireRateSelector;
|
|
private FireRateSelector _allRateSelectors;
|
|
public override float FireRate => _fireRate;
|
|
private float _fireRate;
|
|
|
|
// _lastFire is when we actually fired (so if we hold the button then recoil doesn't build up if we're not firing)
|
|
private TimeSpan _lastFire;
|
|
|
|
public abstract IEntity PeekAmmo();
|
|
public abstract IEntity TakeProjectile(GridCoordinates spawnAt);
|
|
|
|
// Recoil / spray control
|
|
private Angle _minAngle;
|
|
private Angle _maxAngle;
|
|
private Angle _currentAngle = Angle.Zero;
|
|
/// <summary>
|
|
/// How slowly the angle's theta decays per second in radians
|
|
/// </summary>
|
|
private float _angleDecay;
|
|
/// <summary>
|
|
/// How quickly the angle's theta builds for every shot fired in radians
|
|
/// </summary>
|
|
private float _angleIncrease;
|
|
// Multiplies the ammo spread to get the final spread of each pellet
|
|
private float _spreadRatio;
|
|
|
|
public bool CanMuzzleFlash => _canMuzzleFlash;
|
|
private bool _canMuzzleFlash = true;
|
|
|
|
// Sounds
|
|
public string SoundGunshot
|
|
{
|
|
get => _soundGunshot;
|
|
set => _soundGunshot = value;
|
|
}
|
|
private string _soundGunshot;
|
|
public string SoundEmpty => _soundEmpty;
|
|
private string _soundEmpty;
|
|
|
|
public override void ExposeData(ObjectSerializer serializer)
|
|
{
|
|
base.ExposeData(serializer);
|
|
serializer.DataField(ref _fireRateSelector, "currentSelector", FireRateSelector.Safety);
|
|
serializer.DataField(ref _fireRate, "fireRate", 2.0f);
|
|
|
|
// This hard-to-read area's dealing with recoil
|
|
// Use degrees in yaml as it's easier to read compared to "0.0125f"
|
|
if (serializer.Reading)
|
|
{
|
|
var minAngle = serializer.ReadDataField("minAngle", 0) / 2;
|
|
_minAngle = Angle.FromDegrees(minAngle);
|
|
// Random doubles it as it's +/- so uhh we'll just half it here for readability
|
|
var maxAngle = serializer.ReadDataField("maxAngle", 45) / 2;
|
|
_maxAngle = Angle.FromDegrees(maxAngle);
|
|
var angleIncrease = serializer.ReadDataField("angleIncrease", (40 / _fireRate));
|
|
_angleIncrease = angleIncrease * (float) Math.PI / 180;
|
|
var angleDecay = serializer.ReadDataField("angleDecay", (float) 20);
|
|
_angleDecay = angleDecay * (float) Math.PI / 180;
|
|
serializer.DataField(ref _spreadRatio, "ammoSpreadRatio", 1.0f);
|
|
|
|
// FireRate options
|
|
var allFireRates = serializer.ReadDataField("allSelectors", new List<FireRateSelector>());
|
|
foreach (var fireRate in allFireRates)
|
|
{
|
|
_allRateSelectors |= fireRate;
|
|
}
|
|
}
|
|
|
|
// For simplicity we'll enforce it this way; ammo determines max spread
|
|
if (_spreadRatio > 1.0f)
|
|
{
|
|
Logger.Error("SpreadRatio must be <= 1.0f for guns");
|
|
throw new InvalidOperationException();
|
|
}
|
|
|
|
serializer.DataField(ref _canMuzzleFlash, "canMuzzleFlash", true);
|
|
// Sounds
|
|
serializer.DataField(ref _soundGunshot, "soundGunshot", null);
|
|
serializer.DataField(ref _soundEmpty, "soundEmpty", "/Audio/Weapons/Guns/Empty/empty.ogg");
|
|
}
|
|
|
|
public override void OnAdd()
|
|
{
|
|
base.OnAdd();
|
|
var rangedWeapon = Owner.GetComponent<ServerRangedWeaponComponent>();
|
|
rangedWeapon.Barrel = this;
|
|
rangedWeapon.FireHandler += Fire;
|
|
rangedWeapon.WeaponCanFireHandler += WeaponCanFire;
|
|
}
|
|
|
|
public override void OnRemove()
|
|
{
|
|
base.OnRemove();
|
|
if (Owner.TryGetComponent(out ServerRangedWeaponComponent rangedWeaponComponent))
|
|
{
|
|
rangedWeaponComponent.Barrel = null;
|
|
rangedWeaponComponent.FireHandler -= Fire;
|
|
rangedWeaponComponent.WeaponCanFireHandler -= WeaponCanFire;
|
|
}
|
|
}
|
|
|
|
private Angle GetRecoilAngle(Angle direction)
|
|
{
|
|
var currentTime = _gameTiming.CurTime;
|
|
var timeSinceLastFire = (currentTime - _lastFire).TotalSeconds;
|
|
var newTheta = Math.Clamp(_currentAngle.Theta + _angleIncrease - _angleDecay * timeSinceLastFire, _minAngle.Theta, _maxAngle.Theta);
|
|
_currentAngle = new Angle(newTheta);
|
|
|
|
var random = (_robustRandom.NextDouble() - 0.5) * 2;
|
|
var angle = Angle.FromDegrees(direction.Degrees + _currentAngle.Degrees * random);
|
|
return angle;
|
|
}
|
|
|
|
public abstract bool UseEntity(UseEntityEventArgs eventArgs);
|
|
public abstract bool InteractUsing(InteractUsingEventArgs eventArgs);
|
|
|
|
public void ChangeFireSelector(FireRateSelector rateSelector)
|
|
{
|
|
if ((rateSelector & AllRateSelectors) != 0)
|
|
{
|
|
_fireRateSelector = rateSelector;
|
|
return;
|
|
}
|
|
|
|
throw new InvalidOperationException();
|
|
}
|
|
|
|
protected virtual bool WeaponCanFire()
|
|
{
|
|
// If the ServerRangedWeaponComponent gets re-done probably need to add the checks here
|
|
return true;
|
|
}
|
|
|
|
private void Fire(IEntity shooter, GridCoordinates target)
|
|
{
|
|
var soundSystem = EntitySystem.Get<AudioSystem>();
|
|
if (ShotsLeft == 0)
|
|
{
|
|
if (_soundEmpty != null)
|
|
{
|
|
soundSystem.PlayAtCoords(_soundEmpty, Owner.Transform.GridPosition);
|
|
}
|
|
return;
|
|
}
|
|
|
|
var ammo = PeekAmmo();
|
|
var projectile = TakeProjectile(shooter.Transform.GridPosition);
|
|
if (projectile == null)
|
|
{
|
|
soundSystem.PlayAtCoords(_soundEmpty, Owner.Transform.GridPosition);
|
|
return;
|
|
}
|
|
|
|
// At this point firing is confirmed
|
|
var worldPosition = IoCManager.Resolve<IMapManager>().GetGrid(target.GridID).LocalToWorld(target).Position;
|
|
var direction = (worldPosition - shooter.Transform.WorldPosition).ToAngle();
|
|
var angle = GetRecoilAngle(direction);
|
|
// This should really be client-side but for now we'll just leave it here
|
|
if (shooter.TryGetComponent(out CameraRecoilComponent recoilComponent))
|
|
{
|
|
recoilComponent.Kick(-angle.ToVec() * 0.15f);
|
|
}
|
|
|
|
// This section probably needs tweaking so there can be caseless hitscan etc.
|
|
if (projectile.TryGetComponent(out HitscanComponent hitscan))
|
|
{
|
|
FireHitscan(shooter, hitscan, angle);
|
|
}
|
|
else if (projectile.HasComponent<ProjectileComponent>())
|
|
{
|
|
var ammoComponent = ammo.GetComponent<AmmoComponent>();
|
|
|
|
FireProjectiles(shooter, projectile, ammoComponent.ProjectilesFired, ammoComponent.EvenSpreadAngle, angle, ammoComponent.Velocity);
|
|
|
|
if (CanMuzzleFlash)
|
|
{
|
|
ammoComponent.MuzzleFlash(Owner.Transform.GridPosition, angle);
|
|
}
|
|
|
|
if (ammoComponent.Caseless)
|
|
{
|
|
ammo.Delete();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Invalid types
|
|
throw new InvalidOperationException();
|
|
}
|
|
|
|
soundSystem.PlayAtCoords(_soundGunshot, Owner.Transform.GridPosition);
|
|
_lastFire = _gameTiming.CurTime;
|
|
|
|
return;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Drops a single cartridge / shell
|
|
/// Made as a static function just because multiple places need it
|
|
/// </summary>
|
|
/// <param name="entity"></param>
|
|
/// <param name="playSound"></param>
|
|
/// <param name="robustRandom"></param>
|
|
/// <param name="prototypeManager"></param>
|
|
/// <param name="ejectDirections"></param>
|
|
public static void EjectCasing(
|
|
IEntity entity,
|
|
bool playSound = true,
|
|
IRobustRandom robustRandom = null,
|
|
IPrototypeManager prototypeManager = null,
|
|
Direction[] ejectDirections = null)
|
|
{
|
|
if (robustRandom == null)
|
|
{
|
|
robustRandom = IoCManager.Resolve<IRobustRandom>();
|
|
}
|
|
|
|
if (ejectDirections == null)
|
|
{
|
|
ejectDirections = new[] {Direction.East, Direction.North, Direction.South, Direction.West};
|
|
}
|
|
|
|
const float ejectOffset = 0.2f;
|
|
var ammo = entity.GetComponent<AmmoComponent>();
|
|
var offsetPos = (robustRandom.NextFloat() * ejectOffset, robustRandom.NextFloat() * ejectOffset);
|
|
entity.Transform.GridPosition = entity.Transform.GridPosition.Offset(offsetPos);
|
|
entity.Transform.LocalRotation = robustRandom.Pick(ejectDirections).ToAngle();
|
|
|
|
if (ammo.SoundCollectionEject == null || !playSound)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (prototypeManager == null)
|
|
{
|
|
prototypeManager = IoCManager.Resolve<IPrototypeManager>();
|
|
}
|
|
|
|
var soundCollection = prototypeManager.Index<SoundCollectionPrototype>(ammo.SoundCollectionEject);
|
|
var randomFile = robustRandom.Pick(soundCollection.PickFiles);
|
|
EntitySystem.Get<AudioSystem>().PlayAtCoords(randomFile, entity.Transform.GridPosition, AudioParams.Default.WithVolume(-1));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Drops multiple cartridges / shells on the floor
|
|
/// Wraps EjectCasing to make it less toxic for bulk ejections
|
|
/// </summary>
|
|
/// <param name="entities"></param>
|
|
public static void EjectCasings(IEnumerable<IEntity> entities)
|
|
{
|
|
var robustRandom = IoCManager.Resolve<IRobustRandom>();
|
|
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
|
|
var ejectDirections = new[] {Direction.East, Direction.North, Direction.South, Direction.West};
|
|
var soundPlayCount = 0;
|
|
var playSound = true;
|
|
|
|
foreach (var entity in entities)
|
|
{
|
|
EjectCasing(entity, playSound, robustRandom, prototypeManager, ejectDirections);
|
|
soundPlayCount++;
|
|
if (soundPlayCount > 3)
|
|
{
|
|
playSound = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
#region Firing
|
|
/// <summary>
|
|
/// Handles firing one or many projectiles
|
|
/// </summary>
|
|
private void FireProjectiles(IEntity shooter, IEntity baseProjectile, int count, float evenSpreadAngle, Angle angle, float velocity)
|
|
{
|
|
List<Angle> sprayAngleChange = null;
|
|
if (count > 1)
|
|
{
|
|
evenSpreadAngle *= _spreadRatio;
|
|
sprayAngleChange = Linspace(-evenSpreadAngle / 2, evenSpreadAngle / 2, count);
|
|
}
|
|
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
IEntity projectile;
|
|
|
|
if (i == 0)
|
|
{
|
|
projectile = baseProjectile;
|
|
}
|
|
else
|
|
{
|
|
projectile =
|
|
Owner.EntityManager.SpawnEntity(baseProjectile.Prototype.ID, Owner.Transform.GridPosition);
|
|
}
|
|
|
|
Angle projectileAngle;
|
|
|
|
if (sprayAngleChange != null)
|
|
{
|
|
projectileAngle = angle + sprayAngleChange[i];
|
|
}
|
|
else
|
|
{
|
|
projectileAngle = angle;
|
|
}
|
|
|
|
var physicsComponent = projectile.GetComponent<PhysicsComponent>();
|
|
physicsComponent.Status = BodyStatus.InAir;
|
|
projectile.Transform.GridPosition = Owner.Transform.GridPosition;
|
|
|
|
var projectileComponent = projectile.GetComponent<ProjectileComponent>();
|
|
projectileComponent.IgnoreEntity(shooter);
|
|
projectile.GetComponent<PhysicsComponent>().LinearVelocity = projectileAngle.ToVec() * velocity;
|
|
projectile.Transform.LocalRotation = projectileAngle.Theta;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a list of numbers that form a set of equal intervals between the start and end value. Used to calculate shotgun spread angles.
|
|
/// </summary>
|
|
private List<Angle> Linspace(double start, double end, int intervals)
|
|
{
|
|
DebugTools.Assert(intervals > 1);
|
|
|
|
var linspace = new List<Angle>(intervals);
|
|
|
|
for (var i = 0; i <= intervals - 1; i++)
|
|
{
|
|
linspace.Add(Angle.FromDegrees(start + (end - start) * i / (intervals - 1)));
|
|
}
|
|
return linspace;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fires hitscan entities and then displays their effects
|
|
/// </summary>
|
|
private void FireHitscan(IEntity shooter, HitscanComponent hitscan, Angle angle)
|
|
{
|
|
var ray = new CollisionRay(Owner.Transform.GridPosition.Position, angle.ToVec(), (int) hitscan.CollisionMask);
|
|
var physicsManager = IoCManager.Resolve<IPhysicsManager>();
|
|
var rayCastResults = physicsManager.IntersectRay(Owner.Transform.MapID, ray, hitscan.MaxLength, shooter, false).ToList();
|
|
|
|
if (rayCastResults.Count >= 1)
|
|
{
|
|
var result = rayCastResults[0];
|
|
var distance = result.HitEntity != null ? result.Distance : hitscan.MaxLength;
|
|
hitscan.FireEffects(shooter, distance, angle, result.HitEntity);
|
|
|
|
if (result.HitEntity == null || !result.HitEntity.TryGetComponent(out DamageableComponent damageable))
|
|
{
|
|
return;
|
|
}
|
|
|
|
damageable.TakeDamage(
|
|
hitscan.DamageType,
|
|
(int)Math.Round(hitscan.Damage, MidpointRounding.AwayFromZero),
|
|
Owner,
|
|
shooter);
|
|
//I used Math.Round over Convert.toInt32, as toInt32 always rounds to
|
|
//even numbers if halfway between two numbers, rather than rounding to nearest
|
|
}
|
|
else
|
|
{
|
|
hitscan.FireEffects(shooter, hitscan.MaxLength, angle);
|
|
}
|
|
}
|
|
#endregion
|
|
}
|
|
}
|