Gun refactor (#8301)

Co-authored-by: Kara <lunarautomaton6@gmail.com>
Co-authored-by: T-Stalker <le0nel_1van@hotmail.com>
Co-authored-by: T-Stalker <43253663+DogZeroX@users.noreply.github.com>
Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
Co-authored-by: metalgearsloth <metalgearsloth@gmail.com>
This commit is contained in:
metalgearsloth
2022-06-01 19:59:58 +10:00
committed by GitHub
parent 1ced3c5002
commit fb943a61dc
1051 changed files with 8230 additions and 99090 deletions

View File

@@ -9,6 +9,7 @@ namespace Content.Server.Entry
"StasisBedVisuals",
"InteractionOutline",
"MeleeWeaponArcAnimation",
"EffectVisuals",
"AnimationsTest",
"ItemStatus",
"VehicleVisuals",
@@ -21,11 +22,12 @@ namespace Content.Server.Entry
"LatheVisuals",
"DiseaseMachineVisuals",
"HandheldGPS",
"SpentAmmoVisuals",
"ToggleableLightVisuals",
"CableVisualizer",
"PotencyVisuals",
"PaperVisuals",
"SurveillanceCameraVisuals"
"SurveillanceCameraVisuals",
};
}
}

View File

@@ -3,7 +3,6 @@ using Content.Server.CombatMode;
using Content.Server.Hands.Components;
using Content.Server.Pulling;
using Content.Server.Storage.Components;
using Content.Server.Weapon.Ranged.Barrels.Components;
using Content.Shared.ActionBlocker;
using Content.Shared.Database;
using Content.Shared.DragDrop;
@@ -13,7 +12,6 @@ using Content.Shared.Interaction.Events;
using Content.Shared.Item;
using Content.Shared.Pulling.Components;
using Content.Shared.Weapons.Melee;
using Content.Shared.Weapons.Ranged.Components;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Containers;

View File

@@ -15,29 +15,26 @@ namespace Content.Server.Power.Components
private CellChargerStatus _status;
[DataField("chargeRate")]
private int _chargeRate = 100;
[DataField("transferEfficiency")]
private float _transferEfficiency = 0.85f;
public int ChargeRate = 20;
[DataField("chargerSlot", required: true)]
public ItemSlot ChargerSlot = new();
private CellChargerStatus GetStatus()
{
if (_entMan.TryGetComponent(Owner, out ApcPowerReceiverComponent? receiver) &&
!receiver.Powered)
if (!_entMan.TryGetComponent<TransformComponent>(Owner, out var xform) ||
!xform.Anchored ||
_entMan.TryGetComponent(Owner, out ApcPowerReceiverComponent? receiver) && !receiver.Powered)
{
return CellChargerStatus.Off;
}
if (!ChargerSlot.HasItem)
{
return CellChargerStatus.Empty;
}
if (HeldBattery != null && Math.Abs(HeldBattery.MaxCharge - HeldBattery.CurrentCharge) < 0.01)
{
return CellChargerStatus.Charged;
}
return CellChargerStatus.Charging;
}
@@ -66,7 +63,7 @@ namespace Content.Server.Power.Components
appearance?.SetData(CellVisual.Light, CellChargerStatus.Empty);
break;
case CellChargerStatus.Charging:
receiver.Load = (int) (_chargeRate / _transferEfficiency);
receiver.Load = ChargeRate;
appearance?.SetData(CellVisual.Light, CellChargerStatus.Charging);
break;
case CellChargerStatus.Charged:
@@ -83,9 +80,8 @@ namespace Content.Server.Power.Components
public void OnUpdate(float frameTime) //todo: make single system for this
{
if (_status == CellChargerStatus.Empty || _status == CellChargerStatus.Charged || !ChargerSlot.HasItem)
{
return;
}
TransferPower(frameTime);
}
@@ -98,16 +94,15 @@ namespace Content.Server.Power.Components
}
if (HeldBattery == null)
{
return;
}
HeldBattery.CurrentCharge += _chargeRate * frameTime;
HeldBattery.CurrentCharge += ChargeRate * frameTime;
// Just so the sprite won't be set to 99.99999% visibility
if (HeldBattery.MaxCharge - HeldBattery.CurrentCharge < 0.01)
{
HeldBattery.CurrentCharge = HeldBattery.MaxCharge;
}
UpdateStatus();
}
}

View File

@@ -1,6 +1,7 @@
using Content.Server.Power.Components;
using Content.Server.PowerCell;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Examine;
using Content.Shared.PowerCell.Components;
using JetBrains.Annotations;
using Robust.Shared.Containers;
@@ -15,16 +16,18 @@ internal sealed class ChargerSystem : EntitySystem
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ChargerComponent, ComponentInit>(OnChargerInit);
SubscribeLocalEvent<ChargerComponent, ComponentRemove>(OnChargerRemove);
SubscribeLocalEvent<ChargerComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<ChargerComponent, EntInsertedIntoContainerMessage>(OnInserted);
SubscribeLocalEvent<ChargerComponent, EntRemovedFromContainerMessage>(OnRemoved);
SubscribeLocalEvent<ChargerComponent, ContainerIsInsertingAttemptEvent>(OnInsertAttempt);
SubscribeLocalEvent<ChargerComponent, ExaminedEvent>(OnChargerExamine);
}
private void OnChargerExamine(EntityUid uid, ChargerComponent component, ExaminedEvent args)
{
args.PushMarkup(Loc.GetString("charger-examine", ("color", "yellow"), ("chargeRate", component.ChargeRate)));
}
public override void Update(float frameTime)
@@ -63,7 +66,7 @@ internal sealed class ChargerSystem : EntitySystem
// or by checking for a power cell slot on the inserted entity
_cellSystem.TryGetBatteryFromSlot(args.Entity, out component.HeldBattery);
}
component.UpdateStatus();
}

View File

@@ -1,166 +0,0 @@
using Content.Server.Weapon.Ranged;
using Content.Shared.Damage;
using Content.Shared.Physics;
using Content.Shared.Sound;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Timing;
namespace Content.Server.Projectiles.Components
{
/// <summary>
/// Lasers etc.
/// </summary>
[RegisterComponent]
public sealed class HitscanComponent : Component
{
[Dependency] private readonly IEntityManager _entMan = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
public CollisionGroup CollisionMask => (CollisionGroup) _collisionMask;
[DataField("layers")] //todo WithFormat.Flags<CollisionLayer>()
private int _collisionMask = (int) CollisionGroup.Opaque;
[DataField("damage", required: true)]
[ViewVariables(VVAccess.ReadWrite)]
public DamageSpecifier Damage = default!;
public float MaxLength => 20.0f;
private TimeSpan _startTime;
private TimeSpan _deathTime;
public float ColorModifier { get; set; } = 1.0f;
[DataField("spriteName")]
private string _spriteName = "Objects/Weapons/Guns/Projectiles/laser.png";
[DataField("muzzleFlash")]
private string? _muzzleFlash;
[DataField("impactFlash")]
private string? _impactFlash;
[DataField("soundHit")]
public SoundSpecifier? SoundHit;
[DataField("soundForce")]
public bool ForceSound = false;
public void FireEffects(EntityUid user, float distance, Angle angle, EntityUid? hitEntity = null)
{
var effectSystem = EntitySystem.Get<EffectSystem>();
_startTime = _gameTiming.CurTime;
_deathTime = _startTime + TimeSpan.FromSeconds(1);
var mapManager = IoCManager.Resolve<IMapManager>();
// We'll get the effects relative to the grid / map of the firer
var gridOrMap = _entMan.GetComponent<TransformComponent>(user).GridID == GridId.Invalid ? mapManager.GetMapEntityId(_entMan.GetComponent<TransformComponent>(user).MapID) :
mapManager.GetGrid(_entMan.GetComponent<TransformComponent>(user).GridID).GridEntityId;
var parentXform = _entMan.GetComponent<TransformComponent>(gridOrMap);
var localCoordinates = new EntityCoordinates(gridOrMap, parentXform.InvWorldMatrix.Transform(_entMan.GetComponent<TransformComponent>(user).WorldPosition));
var localAngle = angle - parentXform.WorldRotation;
var afterEffect = AfterEffects(localCoordinates, localAngle, distance, 1.0f);
if (afterEffect != null)
{
effectSystem.CreateParticle(afterEffect);
}
// if we're too close we'll stop the impact and muzzle / impact sprites from clipping
if (distance > 1.0f)
{
var impactEffect = ImpactFlash(distance, localAngle);
if (impactEffect != null)
{
effectSystem.CreateParticle(impactEffect);
}
var muzzleEffect = MuzzleFlash(localCoordinates, localAngle);
if (muzzleEffect != null)
{
effectSystem.CreateParticle(muzzleEffect);
}
}
Owner.SpawnTimer((int) _deathTime.TotalMilliseconds, () =>
{
if (!_entMan.Deleted(Owner))
{
_entMan.DeleteEntity(Owner);
}
});
}
private EffectSystemMessage? MuzzleFlash(EntityCoordinates grid, Angle angle)
{
if (_muzzleFlash == null)
{
return null;
}
var offset = angle.ToVec().Normalized / 2;
var message = new EffectSystemMessage
{
EffectSprite = _muzzleFlash,
Born = _startTime,
DeathTime = _deathTime,
Coordinates = grid.Offset(offset),
//Rotated from east facing
Rotation = (float) angle.Theta,
Color = Vector4.Multiply(new Vector4(255, 255, 255, 750), ColorModifier),
ColorDelta = new Vector4(0, 0, 0, -1500f),
Shaded = false
};
return message;
}
private EffectSystemMessage AfterEffects(EntityCoordinates origin, Angle angle, float distance, float offset = 0.0f)
{
var midPointOffset = angle.ToVec() * distance / 2;
var message = new EffectSystemMessage
{
EffectSprite = _spriteName,
Born = _startTime,
DeathTime = _deathTime,
Size = new Vector2(distance - offset, 1f),
Coordinates = origin.Offset(midPointOffset),
//Rotated from east facing
Rotation = (float) angle.Theta,
Color = Vector4.Multiply(new Vector4(255, 255, 255, 750), ColorModifier),
ColorDelta = new Vector4(0, 0, 0, -1500f),
Shaded = false
};
return message;
}
private EffectSystemMessage? ImpactFlash(float distance, Angle angle)
{
if (_impactFlash == null)
{
return null;
}
var message = new EffectSystemMessage
{
EffectSprite = _impactFlash,
Born = _startTime,
DeathTime = _deathTime,
Coordinates = _entMan.GetComponent<TransformComponent>(Owner).Coordinates.Offset(angle.ToVec() * distance),
//Rotated from east facing
Rotation = (float) angle.FlipPositive(),
Color = Vector4.Multiply(new Vector4(255, 255, 255, 750), ColorModifier),
ColorDelta = new Vector4(0, 0, 0, -1500f),
Shaded = false
};
return message;
}
}
}

View File

@@ -14,6 +14,7 @@ using Robust.Shared.Audio;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using GunSystem = Content.Server.Weapon.Ranged.Systems.GunSystem;
namespace Content.Server.Projectiles
{
@@ -51,7 +52,7 @@ namespace Content.Server.Projectiles
$"Projectile {ToPrettyString(component.Owner):projectile} shot by {ToPrettyString(component.Shooter):user} hit {ToPrettyString(otherEntity):target} and dealt {modifiedDamage.Total:damage} damage");
}
_guns.PlaySound(otherEntity, modifiedDamage, component.SoundHit, component.ForceSound);
_guns.PlayImpactSound(otherEntity, modifiedDamage, component.SoundHit, component.ForceSound);
// Damaging it can delete it
if (HasComp<CameraRecoilComponent>(otherEntity))

View File

@@ -64,7 +64,7 @@ namespace Content.Server.Tools
return;
// Sprite is optional.
Resolve(uid, ref sprite);
Resolve(uid, ref sprite, false);
if (multiple.Entries.Length == 0)
{

View File

@@ -1,49 +0,0 @@
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Weapon.Ranged.Ammunition.Components
{
/// <summary>
/// Stores ammo and can quickly transfer ammo into a magazine.
/// </summary>
[RegisterComponent]
[Friend(typeof(GunSystem))]
public sealed class AmmoBoxComponent : Component
{
[DataField("caliber")]
public BallisticCaliber Caliber = BallisticCaliber.Unspecified;
[DataField("capacity")]
public int Capacity
{
get => _capacity;
set
{
_capacity = value;
SpawnedAmmo = new Stack<EntityUid>(value);
}
}
private int _capacity = 30;
public int AmmoLeft => SpawnedAmmo.Count + UnspawnedCount;
public Stack<EntityUid> SpawnedAmmo = new();
/// <summary>
/// Container that holds any instantiated ammo.
/// </summary>
public Container AmmoContainer = default!;
/// <summary>
/// How many more deferred entities can be spawned. We defer these to avoid instantiating the entities until needed for performance reasons.
/// </summary>
public int UnspawnedCount;
/// <summary>
/// The prototype of the ammo to be retrieved when getting ammo.
/// </summary>
[DataField("fillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
public string? FillPrototype;
}
}

View File

@@ -1,113 +0,0 @@
using Content.Shared.Sound;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Utility;
namespace Content.Server.Weapon.Ranged.Ammunition.Components
{
/// <summary>
/// Allows this entity to be loaded into a ranged weapon (if the caliber matches)
/// Generally used for bullets but can be used for other things like bananas
/// </summary>
[RegisterComponent]
[Friend(typeof(GunSystem))]
public sealed class AmmoComponent : Component, ISerializationHooks
{
[DataField("caliber")]
public BallisticCaliber Caliber { get; } = BallisticCaliber.Unspecified;
public bool Spent
{
get
{
if (AmmoIsProjectile)
{
return false;
}
return _spent;
}
set => _spent = value;
}
private bool _spent;
// TODO: Make it so null projectile = dis
/// <summary>
/// Used for anything without a case that fires itself
/// </summary>
[DataField("isProjectile")] public bool AmmoIsProjectile;
/// <summary>
/// Used for something that is deleted when the projectile is retrieved
/// </summary>
[DataField("caseless")]
public bool Caseless { get; }
// Rather than managing bullet / case state seemed easier to just have 2 toggles
// ammoIsProjectile being for a beanbag for example and caseless being for ClRifle rounds
/// <summary>
/// For shotguns where they might shoot multiple entities
/// </summary>
[DataField("projectilesFired")]
public int ProjectilesFired { get; } = 1;
[DataField("projectile", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string? ProjectileId;
// How far apart each entity is if multiple are shot
[DataField("ammoSpread")]
public float EvenSpreadAngle { get; } = default;
/// <summary>
/// How fast the shot entities travel
/// </summary>
[DataField("ammoVelocity")]
public float Velocity { get; } = 20f;
[DataField("muzzleFlash")]
public ResourcePath? MuzzleFlashSprite = new("Objects/Weapons/Guns/Projectiles/bullet_muzzle.png");
[DataField("soundCollectionEject")]
public SoundSpecifier SoundCollectionEject { get; } = new SoundCollectionSpecifier("CasingEject");
void ISerializationHooks.AfterDeserialization()
{
// Being both caseless and shooting yourself doesn't make sense
DebugTools.Assert(!(AmmoIsProjectile && Caseless));
if (ProjectilesFired < 1)
{
Logger.Error("Ammo can't have less than 1 projectile");
}
if (EvenSpreadAngle > 0 && ProjectilesFired == 1)
{
Logger.Error("Can't have an even spread if only 1 projectile is fired");
throw new InvalidOperationException();
}
}
}
public enum BallisticCaliber
{
Unspecified = 0,
A357, // Placeholder?
ClRifle,
SRifle,
Pistol,
A35, // Placeholder?
LRifle,
HRifle,
Magnum,
AntiMaterial,
Shotgun,
Cap,
Rocket,
Dart, // Placeholder
Grenade,
Energy,
}
}

View File

@@ -1,8 +0,0 @@
using Robust.Shared.Serialization;
namespace Content.Server.Weapon.Ranged.Ammunition.Components
{
public sealed partial class AmmoComponentData : ISerializationHooks
{
}
}

View File

@@ -1,34 +0,0 @@
using Content.Server.Weapon.Ranged.Barrels.Components;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Weapon.Ranged.Ammunition.Components
{
[RegisterComponent]
public sealed class RangedMagazineComponent : Component
{
public readonly Stack<EntityUid> SpawnedAmmo = new();
public Container AmmoContainer = default!;
public int ShotsLeft => SpawnedAmmo.Count + UnspawnedCount;
public int Capacity => _capacity;
[DataField("capacity")]
private int _capacity = 20;
public MagazineType MagazineType => _magazineType;
[DataField("magazineType")]
private MagazineType _magazineType = MagazineType.Unspecified;
public BallisticCaliber Caliber => _caliber;
[DataField("caliber")]
private BallisticCaliber _caliber = BallisticCaliber.Unspecified;
// If there's anything already in the magazine
[DataField("fillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
public string? FillPrototype;
// By default the magazine won't spawn the entity until needed so we need to keep track of how many left we can spawn
// Generally you probablt don't want to use this
public int UnspawnedCount;
}
}

View File

@@ -1,27 +0,0 @@
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Weapon.Ranged.Ammunition.Components
{
/// <summary>
/// Used to load certain ranged weapons quickly
/// </summary>
[RegisterComponent]
public sealed class SpeedLoaderComponent : Component
{
[DataField("caliber")] public BallisticCaliber Caliber = BallisticCaliber.Unspecified;
public int Capacity => _capacity;
[DataField("capacity")]
private int _capacity = 6;
public Container AmmoContainer = default!;
public Stack<EntityUid> SpawnedAmmo = new();
public int UnspawnedCount;
public int AmmoLeft => SpawnedAmmo.Count + UnspawnedCount;
[DataField("fillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
public string? FillPrototype;
}
}

View File

@@ -1,55 +0,0 @@
using Content.Server.PowerCell;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Weapon.Ranged.Barrels.Components
{
[RegisterComponent, NetworkedComponent, ComponentReference(typeof(ServerRangedBarrelComponent))]
public sealed class BatteryBarrelComponent : ServerRangedBarrelComponent
{
// The minimum change we need before we can fire
[DataField("lowerChargeLimit")]
[ViewVariables]
public float LowerChargeLimit = 10;
[DataField("fireCost")]
[ViewVariables]
public int BaseFireCost = 300;
// What gets fired
[DataField("ammoPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
[ViewVariables]
public string? AmmoPrototype;
public ContainerSlot AmmoContainer = default!;
public override int ShotsLeft
{
get
{
if (!EntitySystem.Get<PowerCellSystem>().TryGetBatteryFromSlot(Owner, out var battery))
{
return 0;
}
return (int) Math.Ceiling(battery.CurrentCharge / BaseFireCost);
}
}
public override int Capacity
{
get
{
if (!EntitySystem.Get<PowerCellSystem>().TryGetBatteryFromSlot(Owner, out var battery))
{
return 0;
}
return (int) Math.Ceiling(battery.MaxCharge / BaseFireCost);
}
}
}
}

View File

@@ -1,89 +0,0 @@
using Content.Server.Weapon.Ranged.Ammunition.Components;
using Content.Shared.Sound;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Weapon.Ranged.Barrels.Components
{
/// <summary>
/// Shotguns mostly
/// </summary>
[RegisterComponent, NetworkedComponent, ComponentReference(typeof(ServerRangedBarrelComponent))]
public sealed class BoltActionBarrelComponent : ServerRangedBarrelComponent
{
// Originally I had this logic shared with PumpBarrel and used a couple of variables to control things
// but it felt a lot messier to play around with, especially when adding verbs
public override int ShotsLeft
{
get
{
var chamberCount = ChamberContainer.ContainedEntity != null ? 1 : 0;
return chamberCount + SpawnedAmmo.Count + UnspawnedCount;
}
}
public override int Capacity => _capacity;
[DataField("capacity")]
internal int _capacity = 6;
public ContainerSlot ChamberContainer = default!;
public Stack<EntityUid> SpawnedAmmo = default!;
public Container AmmoContainer = default!;
[ViewVariables]
[DataField("caliber")]
public BallisticCaliber Caliber = BallisticCaliber.Unspecified;
[ViewVariables]
[DataField("fillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
public string? FillPrototype;
[ViewVariables]
public int UnspawnedCount;
public bool BoltOpen
{
get => _boltOpen;
set
{
if (_boltOpen == value)
{
return;
}
var gunSystem = EntitySystem.Get<GunSystem>();
if (value)
{
gunSystem.TryEjectChamber(this);
SoundSystem.Play(Filter.Pvs(Owner), _soundBoltOpen.GetSound(), Owner, AudioParams.Default.WithVolume(-2));
}
else
{
gunSystem.TryFeedChamber(this);
SoundSystem.Play(Filter.Pvs(Owner), _soundBoltClosed.GetSound(), Owner, AudioParams.Default.WithVolume(-2));
}
_boltOpen = value;
gunSystem.UpdateBoltAppearance(this);
Dirty();
}
}
private bool _boltOpen;
[DataField("autoCycle")] public bool AutoCycle;
// Sounds
[DataField("soundCycle")] public SoundSpecifier SoundCycle = new SoundPathSpecifier("/Audio/Weapons/Guns/Cock/sf_rifle_cock.ogg");
[DataField("soundBoltOpen")]
private SoundSpecifier _soundBoltOpen = new SoundPathSpecifier("/Audio/Weapons/Guns/Bolt/rifle_bolt_open.ogg");
[DataField("soundBoltClosed")]
private SoundSpecifier _soundBoltClosed = new SoundPathSpecifier("/Audio/Weapons/Guns/Bolt/rifle_bolt_closed.ogg");
[DataField("soundInsert")] public SoundSpecifier SoundInsert = new SoundPathSpecifier("/Audio/Weapons/Guns/MagIn/bullet_insert.ogg");
}
}

View File

@@ -1,131 +0,0 @@
using Content.Server.Weapon.Ranged.Ammunition.Components;
using Content.Shared.Sound;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Weapon.Ranged.Barrels.Components
{
[RegisterComponent, NetworkedComponent, ComponentReference(typeof(ServerRangedBarrelComponent))]
public sealed class MagazineBarrelComponent : ServerRangedBarrelComponent
{
[Dependency] private readonly IEntityManager _entities = default!;
[ViewVariables] public ContainerSlot ChamberContainer = default!;
[ViewVariables] public bool HasMagazine => MagazineContainer.ContainedEntity != null;
public ContainerSlot MagazineContainer = default!;
[ViewVariables] public MagazineType MagazineTypes => _magazineTypes;
[DataField("magazineTypes")]
private MagazineType _magazineTypes = default;
[ViewVariables] public BallisticCaliber Caliber => _caliber;
[DataField("caliber")]
private BallisticCaliber _caliber = BallisticCaliber.Unspecified;
public override int ShotsLeft
{
get
{
var count = 0;
if (ChamberContainer.ContainedEntity != null)
{
count++;
}
if (MagazineContainer.ContainedEntity is {Valid: true} magazine)
{
count += _entities.GetComponent<RangedMagazineComponent>(magazine).ShotsLeft;
}
return count;
}
}
public override int Capacity
{
get
{
// Chamber
var count = 1;
if (MagazineContainer.ContainedEntity is {Valid: true} magazine)
{
count += _entities.GetComponent<RangedMagazineComponent>(magazine).Capacity;
}
return count;
}
}
[DataField("magFillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
public string? MagFillPrototype;
public bool BoltOpen
{
get => _boltOpen;
set
{
if (_boltOpen == value)
{
return;
}
var gunSystem = EntitySystem.Get<GunSystem>();
if (value)
{
gunSystem.TryEjectChamber(this);
SoundSystem.Play(Filter.Pvs(Owner), SoundBoltOpen.GetSound(), Owner, AudioParams.Default.WithVolume(-2));
}
else
{
gunSystem.TryFeedChamber(this);
SoundSystem.Play(Filter.Pvs(Owner), SoundBoltClosed.GetSound(), Owner, AudioParams.Default.WithVolume(-2));
}
_boltOpen = value;
gunSystem.UpdateMagazineAppearance(this);
Dirty(_entities);
}
}
private bool _boltOpen = true;
[DataField("autoEjectMag")] public bool AutoEjectMag;
// If the bolt needs to be open before we can insert / remove the mag (i.e. for LMGs)
public bool MagNeedsOpenBolt => _magNeedsOpenBolt;
[DataField("magNeedsOpenBolt")]
private bool _magNeedsOpenBolt = default;
// Sounds
[DataField("soundBoltOpen", required: true)]
public SoundSpecifier SoundBoltOpen = default!;
[DataField("soundBoltClosed", required: true)]
public SoundSpecifier SoundBoltClosed = default!;
[DataField("soundRack", required: true)]
public SoundSpecifier SoundRack = default!;
[DataField("soundMagInsert", required: true)]
public SoundSpecifier SoundMagInsert = default!;
[DataField("soundMagEject", required: true)]
public SoundSpecifier SoundMagEject = default!;
[DataField("soundAutoEject")] public SoundSpecifier SoundAutoEject = new SoundPathSpecifier("/Audio/Weapons/Guns/EmptyAlarm/smg_empty_alarm.ogg");
}
[Flags]
public enum MagazineType
{
Unspecified = 0,
LPistol = 1 << 0, // Placeholder?
Pistol = 1 << 1,
HCPistol = 1 << 2,
Smg = 1 << 3,
SmgTopMounted = 1 << 4,
Rifle = 1 << 5,
IH = 1 << 6, // Placeholder?
Box = 1 << 7,
Pan = 1 << 8,
Dart = 1 << 9, // Placeholder
CalicoTopMounted = 1 << 10,
}
}

View File

@@ -1,57 +0,0 @@
using Content.Server.Weapon.Ranged.Ammunition.Components;
using Content.Shared.Sound;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Weapon.Ranged.Barrels.Components
{
/// <summary>
/// Bolt-action rifles
/// </summary>
[RegisterComponent, NetworkedComponent, ComponentReference(typeof(ServerRangedBarrelComponent))]
public sealed class PumpBarrelComponent : ServerRangedBarrelComponent, ISerializationHooks
{
public override int ShotsLeft
{
get
{
var chamberCount = ChamberContainer.ContainedEntity != null ? 1 : 0;
return chamberCount + SpawnedAmmo.Count + UnspawnedCount;
}
}
private const int DefaultCapacity = 6;
[DataField("capacity")]
public override int Capacity { get; } = DefaultCapacity;
// Even a point having a chamber? I guess it makes some of the below code cleaner
public ContainerSlot ChamberContainer = default!;
public Stack<EntityUid> SpawnedAmmo = new(DefaultCapacity - 1);
public Container AmmoContainer = default!;
[ViewVariables]
[DataField("caliber")]
public BallisticCaliber Caliber = BallisticCaliber.Unspecified;
[ViewVariables]
[DataField("fillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
public string? FillPrototype;
[ViewVariables] public int UnspawnedCount;
[DataField("manualCycle")] public bool ManualCycle = true;
// Sounds
[DataField("soundCycle")] public SoundSpecifier SoundCycle = new SoundPathSpecifier("/Audio/Weapons/Guns/Cock/sf_rifle_cock.ogg");
[DataField("soundInsert")] public SoundSpecifier SoundInsert = new SoundPathSpecifier("/Audio/Weapons/Guns/MagIn/bullet_insert.ogg");
void ISerializationHooks.AfterDeserialization()
{
SpawnedAmmo = new Stack<EntityUid>(Capacity - 1);
}
}
}

View File

@@ -1,60 +0,0 @@
using Content.Server.Weapon.Ranged.Ammunition.Components;
using Content.Shared.Sound;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Weapon.Ranged.Barrels.Components
{
[RegisterComponent, NetworkedComponent, ComponentReference(typeof(ServerRangedBarrelComponent))]
public sealed class RevolverBarrelComponent : ServerRangedBarrelComponent, ISerializationHooks
{
[ViewVariables]
[DataField("caliber")]
public BallisticCaliber Caliber = BallisticCaliber.Unspecified;
public Container AmmoContainer = default!;
[ViewVariables]
public int CurrentSlot;
public override int Capacity => AmmoSlots.Length;
[DataField("capacity")]
private int _serializedCapacity = 6;
[DataField("ammoSlots", readOnly: true)]
public EntityUid?[] AmmoSlots = Array.Empty<EntityUid?>();
public override int ShotsLeft => AmmoContainer.ContainedEntities.Count;
[ViewVariables]
[DataField("fillPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
public string? FillPrototype;
[ViewVariables]
public int UnspawnedCount;
// Sounds
[DataField("soundEject")]
public SoundSpecifier SoundEject = new SoundPathSpecifier("/Audio/Weapons/Guns/MagOut/revolver_magout.ogg");
[DataField("soundInsert")]
public SoundSpecifier SoundInsert = new SoundPathSpecifier("/Audio/Weapons/Guns/MagIn/revolver_magin.ogg");
[DataField("soundSpin")]
public SoundSpecifier SoundSpin = new SoundPathSpecifier("/Audio/Weapons/Guns/Misc/revolver_spin.ogg");
void ISerializationHooks.BeforeSerialization()
{
_serializedCapacity = AmmoSlots.Length;
}
void ISerializationHooks.AfterDeserialization()
{
AmmoSlots = new EntityUid?[_serializedCapacity];
}
}
}

View File

@@ -1,136 +0,0 @@
using Content.Shared.Sound;
using Content.Shared.Weapons.Ranged.Components;
using Robust.Shared.Serialization;
namespace Content.Server.Weapon.Ranged.Barrels.Components
{
/// <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>
[Friend(typeof(GunSystem))]
public abstract class ServerRangedBarrelComponent : SharedRangedBarrelComponent, ISerializationHooks
{
public override FireRateSelector FireRateSelector => _fireRateSelector;
[DataField("currentSelector")]
private FireRateSelector _fireRateSelector = FireRateSelector.Safety;
public override FireRateSelector AllRateSelectors => _fireRateSelector;
[DataField("fireRate")]
public override float FireRate { get; } = 2f;
// _lastFire is when we actually fired (so if we hold the button then recoil doesn't build up if we're not firing)
public TimeSpan LastFire;
// Recoil / spray control
[DataField("minAngle")]
private float _minAngleDegrees;
public Angle MinAngle { get; private set; }
[DataField("maxAngle")]
private float _maxAngleDegrees = 45;
public Angle MaxAngle { get; private set; }
public Angle CurrentAngle = Angle.Zero;
[DataField("angleDecay")]
private float _angleDecayDegrees = 20;
/// <summary>
/// How slowly the angle's theta decays per second in radians
/// </summary>
public float AngleDecay { get; private set; }
[DataField("angleIncrease")]
private float? _angleIncreaseDegrees;
/// <summary>
/// How quickly the angle's theta builds for every shot fired in radians
/// </summary>
public float AngleIncrease { get; private set; }
// Multiplies the ammo spread to get the final spread of each pellet
[DataField("ammoSpreadRatio")]
public float SpreadRatio { get; private set; }
[DataField("canMuzzleFlash")]
public bool CanMuzzleFlash { get; } = true;
// Sounds
[DataField("soundGunshot", required: true)]
public SoundSpecifier SoundGunshot { get; set; } = default!;
[DataField("soundEmpty")]
public SoundSpecifier SoundEmpty { get; } = new SoundPathSpecifier("/Audio/Weapons/Guns/Empty/empty.ogg");
void ISerializationHooks.BeforeSerialization()
{
_minAngleDegrees = (float) (MinAngle.Degrees * 2);
_maxAngleDegrees = (float) (MaxAngle.Degrees * 2);
_angleIncreaseDegrees = MathF.Round(AngleIncrease / ((float) Math.PI / 180f), 2);
AngleDecay = MathF.Round(AngleDecay / ((float) Math.PI / 180f), 2);
}
void ISerializationHooks.AfterDeserialization()
{
// This hard-to-read area's dealing with recoil
// Use degrees in yaml as it's easier to read compared to "0.0125f"
MinAngle = Angle.FromDegrees(_minAngleDegrees / 2f);
// Random doubles it as it's +/- so uhh we'll just half it here for readability
MaxAngle = Angle.FromDegrees(_maxAngleDegrees / 2f);
_angleIncreaseDegrees ??= 40 / FireRate;
AngleIncrease = _angleIncreaseDegrees.Value * (float) Math.PI / 180f;
AngleDecay = _angleDecayDegrees * (float) Math.PI / 180f;
// 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();
}
}
}
/// <summary>
/// Raised on a gun when it fires projectiles.
/// </summary>
public sealed class GunShotEvent : EntityEventArgs
{
/// <summary>
/// Uid of the entity that shot.
/// </summary>
public EntityUid Uid;
public readonly EntityUid[] FiredProjectiles;
public GunShotEvent(EntityUid[] firedProjectiles)
{
FiredProjectiles = firedProjectiles;
}
}
/// <summary>
/// Raised on ammo when it is fired.
/// </summary>
public sealed class AmmoShotEvent : EntityEventArgs
{
/// <summary>
/// Uid of the entity that shot.
/// </summary>
public EntityUid Uid;
public readonly EntityUid[] FiredProjectiles;
public AmmoShotEvent(EntityUid[] firedProjectiles)
{
FiredProjectiles = firedProjectiles;
}
}
}

View File

@@ -0,0 +1,6 @@
using Content.Shared.Weapons.Ranged.Components;
namespace Content.Server.Weapon.Ranged.Components;
[RegisterComponent]
public sealed class AmmoCounterComponent : SharedAmmoCounterComponent {}

View File

@@ -1,4 +1,4 @@
namespace Content.Server.Weapon.Ranged.Ammunition.Components
namespace Content.Server.Weapon.Ranged.Components
{
[RegisterComponent]
public sealed class ChemicalAmmoComponent : Component

View File

@@ -2,7 +2,7 @@ using Content.Shared.Damage.Prototypes;
using Content.Shared.Sound;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
namespace Content.Server.Weapon.Ranged;
namespace Content.Server.Weapon.Ranged.Components;
/// <summary>
/// Plays the specified sound upon receiving damage of that type.

View File

@@ -1,5 +0,0 @@
using Content.Shared.Weapons.Ranged;
namespace Content.Server.Weapon.Ranged;
public sealed class FlyBySoundSystem : SharedFlyBySoundSystem {}

View File

@@ -1,91 +0,0 @@
using Content.Server.Weapon.Ranged.Ammunition.Components;
using Content.Shared.Examine;
using Content.Shared.Weapons.Ranged.Barrels.Components;
using Robust.Shared.Map;
namespace Content.Server.Weapon.Ranged;
public sealed partial class GunSystem
{
private void OnAmmoExamine(EntityUid uid, AmmoComponent component, ExaminedEvent args)
{
var text = Loc.GetString("ammo-component-on-examine",("caliber", component.Caliber));
args.PushMarkup(text);
}
public EntityUid? TakeBullet(AmmoComponent component, EntityCoordinates spawnAt)
{
if (component.AmmoIsProjectile)
{
return component.Owner;
}
if (component.Spent)
{
return null;
}
component.Spent = true;
if (TryComp(component.Owner, out AppearanceComponent? appearanceComponent))
{
appearanceComponent.SetData(AmmoVisuals.Spent, true);
}
var entity = EntityManager.SpawnEntity(component.ProjectileId, spawnAt);
return entity;
}
public void MuzzleFlash(EntityUid entity, AmmoComponent component, Angle angle)
{
if (component.MuzzleFlashSprite == null)
{
return;
}
var time = _gameTiming.CurTime;
var deathTime = time + TimeSpan.FromMilliseconds(200);
// Offset the sprite so it actually looks like it's coming from the gun
var offset = new Vector2(0.0f, -0.5f);
var message = new EffectSystemMessage
{
EffectSprite = component.MuzzleFlashSprite.ToString(),
Born = time,
DeathTime = deathTime,
AttachedEntityUid = entity,
AttachedOffset = offset,
//Rotated from east facing
Rotation = -MathF.PI / 2f,
Color = Vector4.Multiply(new Vector4(255, 255, 255, 255), 1.0f),
ColorDelta = new Vector4(0, 0, 0, -1500f),
Shaded = false
};
/* TODO: Fix rotation when shooting sideways. This was the closest I got but still had issues.
* var time = _gameTiming.CurTime;
var deathTime = time + TimeSpan.FromMilliseconds(200);
var entityRotation = EntityManager.GetComponent<TransformComponent>(entity).WorldRotation;
var localAngle = entityRotation - (angle + MathF.PI / 2f);
// Offset the sprite so it actually looks like it's coming from the gun
var offset = localAngle.RotateVec(new Vector2(0.0f, -0.5f));
var message = new EffectSystemMessage
{
EffectSprite = component.MuzzleFlashSprite.ToString(),
Born = time,
DeathTime = deathTime,
AttachedEntityUid = entity,
AttachedOffset = offset,
//Rotated from east facing
Rotation = (float) (localAngle - MathF.PI / 2),
Color = Vector4.Multiply(new Vector4(255, 255, 255, 255), 1.0f),
ColorDelta = new Vector4(0, 0, 0, -1500f),
Shaded = false
};
*/
_effects.CreateParticle(message);
}
}

View File

@@ -1,206 +0,0 @@
using Content.Server.Hands.Components;
using Content.Server.Weapon.Ranged.Ammunition.Components;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Verbs;
using Content.Shared.Weapons.Ranged.Barrels.Components;
using Robust.Shared.Containers;
using Robust.Shared.Player;
namespace Content.Server.Weapon.Ranged;
public sealed partial class GunSystem
{
// Probably needs combining with magazines in future given the common functionality.
private void OnAmmoBoxAltVerbs(EntityUid uid, AmmoBoxComponent component, GetVerbsEvent<AlternativeVerb> args)
{
if (args.Hands == null || !args.CanAccess || !args.CanInteract)
return;
if (component.AmmoLeft == 0)
return;
AlternativeVerb verb = new()
{
Text = Loc.GetString("dump-vert-get-data-text"),
IconTexture = "/Textures/Interface/VerbIcons/eject.svg.192dpi.png",
Act = () => AmmoBoxEjectContents(component, 10)
};
args.Verbs.Add(verb);
}
private void OnAmmoBoxInteractHand(EntityUid uid, AmmoBoxComponent component, InteractHandEvent args)
{
if (args.Handled) return;
TryUse(args.User, component);
}
private void OnAmmoBoxUse(EntityUid uid, AmmoBoxComponent component, UseInHandEvent args)
{
if (args.Handled) return;
TryUse(args.User, component);
}
private void OnAmmoBoxInteractUsing(EntityUid uid, AmmoBoxComponent component, InteractUsingEvent args)
{
if (args.Handled) return;
if (TryComp(args.Used, out AmmoComponent? ammoComponent))
{
if (TryInsertAmmo(args.User, args.Used, component, ammoComponent))
{
args.Handled = true;
}
return;
}
if (!TryComp(args.Used, out RangedMagazineComponent? rangedMagazine)) return;
for (var i = 0; i < Math.Max(10, rangedMagazine.ShotsLeft); i++)
{
if (TakeAmmo(rangedMagazine) is not {Valid: true} ammo)
{
continue;
}
if (!TryInsertAmmo(args.User, ammo, component))
{
TryInsertAmmo(args.User, ammo, rangedMagazine);
args.Handled = true;
return;
}
}
args.Handled = true;
}
private void OnAmmoBoxInit(EntityUid uid, AmmoBoxComponent component, ComponentInit args)
{
component.AmmoContainer = uid.EnsureContainer<Container>($"{component.Name}-container", out var existing);
if (existing)
{
foreach (var entity in component.AmmoContainer.ContainedEntities)
{
component.UnspawnedCount--;
component.SpawnedAmmo.Push(entity);
component.AmmoContainer.Insert(entity);
}
}
}
private void OnAmmoBoxExamine(EntityUid uid, AmmoBoxComponent component, ExaminedEvent args)
{
args.PushMarkup(Loc.GetString("ammo-box-component-on-examine-caliber-description", ("caliber", component.Caliber)));
args.PushMarkup(Loc.GetString("ammo-box-component-on-examine-remaining-ammo-description", ("ammoLeft", component.AmmoLeft),("capacity", component.Capacity)));
}
private void OnAmmoBoxMapInit(EntityUid uid, AmmoBoxComponent component, MapInitEvent args)
{
component.UnspawnedCount += component.Capacity;
UpdateAmmoBoxAppearance(uid, component);
}
private void UpdateAmmoBoxAppearance(EntityUid uid, AmmoBoxComponent ammoBox, AppearanceComponent? appearanceComponent = null)
{
if (!Resolve(uid, ref appearanceComponent, false)) return;
appearanceComponent.SetData(MagazineBarrelVisuals.MagLoaded, true);
appearanceComponent.SetData(AmmoVisuals.AmmoCount, ammoBox.AmmoLeft);
appearanceComponent.SetData(AmmoVisuals.AmmoMax, ammoBox.Capacity);
}
private void AmmoBoxEjectContents(AmmoBoxComponent ammoBox, int count)
{
var ejectCount = Math.Min(count, ammoBox.Capacity);
var ejectAmmo = new List<EntityUid>(ejectCount);
for (var i = 0; i < Math.Min(count, ammoBox.Capacity); i++)
{
if (TakeAmmo(ammoBox) is not { } ammo)
{
break;
}
ejectAmmo.Add(ammo);
}
EjectCasings(ejectAmmo);
UpdateAmmoBoxAppearance(ammoBox.Owner, ammoBox);
}
private bool TryUse(EntityUid user, AmmoBoxComponent ammoBox)
{
if (!TryComp(user, out HandsComponent? handsComponent))
{
return false;
}
if (TakeAmmo(ammoBox) is not { } ammo)
{
return false;
}
if (!_handsSystem.TryPickup(user, ammo, handsComp: handsComponent))
{
TryInsertAmmo(user, ammo, ammoBox);
return false;
}
UpdateAmmoBoxAppearance(ammoBox.Owner, ammoBox);
return true;
}
public bool TryInsertAmmo(EntityUid user, EntityUid ammo, AmmoBoxComponent ammoBox, AmmoComponent? ammoComponent = null)
{
if (!Resolve(ammo, ref ammoComponent, false))
{
return false;
}
if (ammoComponent.Caliber != ammoBox.Caliber)
{
_popup.PopupEntity(Loc.GetString("ammo-box-component-try-insert-ammo-wrong-caliber"), ammo, Filter.Entities(user));
return false;
}
if (ammoBox.AmmoLeft >= ammoBox.Capacity)
{
_popup.PopupEntity(Loc.GetString("ammo-box-component-try-insert-ammo-no-room"), ammo, Filter.Entities(user));
return false;
}
ammoBox.SpawnedAmmo.Push(ammo);
ammoBox.AmmoContainer.Insert(ammo);
UpdateAmmoBoxAppearance(ammoBox.Owner, ammoBox);
return true;
}
public EntityUid? TakeAmmo(AmmoBoxComponent ammoBox, TransformComponent? xform = null)
{
if (!Resolve(ammoBox.Owner, ref xform)) return null;
if (ammoBox.SpawnedAmmo.TryPop(out var ammo))
{
ammoBox.AmmoContainer.Remove(ammo);
return ammo;
}
if (ammoBox.UnspawnedCount > 0)
{
ammo = EntityManager.SpawnEntity(ammoBox.FillPrototype, xform.Coordinates);
// when dumping from held ammo box, this detaches the spawned ammo from the player.
EntityManager.GetComponent<TransformComponent>(ammo).AttachParentToContainerOrGrid();
ammoBox.UnspawnedCount--;
}
return ammo;
}
}

View File

@@ -1,106 +0,0 @@
using Content.Server.Projectiles.Components;
using Content.Server.Weapon.Ranged.Barrels.Components;
using Content.Shared.PowerCell.Components;
using Content.Shared.Weapons.Ranged.Barrels.Components;
using Robust.Shared.Containers;
using Robust.Shared.Map;
namespace Content.Server.Weapon.Ranged;
public sealed partial class GunSystem
{
private void OnBatteryInit(EntityUid uid, BatteryBarrelComponent component, ComponentInit args)
{
if (component.AmmoPrototype != null)
{
component.AmmoContainer = uid.EnsureContainer<ContainerSlot>($"{component.GetType()}-ammo-container");
}
component.Dirty(EntityManager);
}
private void OnBatteryMapInit(EntityUid uid, BatteryBarrelComponent component, MapInitEvent args)
{
UpdateBatteryAppearance(component);
}
private void OnCellSlotUpdated(EntityUid uid, BatteryBarrelComponent component, PowerCellChangedEvent args)
{
UpdateBatteryAppearance(component);
}
public void UpdateBatteryAppearance(BatteryBarrelComponent component)
{
if (!EntityManager.TryGetComponent(component.Owner, out AppearanceComponent? appearanceComponent)) return;
appearanceComponent.SetData(MagazineBarrelVisuals.MagLoaded, _cell.TryGetBatteryFromSlot(component.Owner, out _));
appearanceComponent.SetData(AmmoVisuals.AmmoCount, component.ShotsLeft);
appearanceComponent.SetData(AmmoVisuals.AmmoMax, component.Capacity);
}
public EntityUid? PeekAmmo(BatteryBarrelComponent component)
{
// Spawn a dummy entity because it's easier to work with I guess
// This will get re-used for the projectile
var ammo = component.AmmoContainer.ContainedEntity;
if (ammo == null)
{
ammo = EntityManager.SpawnEntity(component.AmmoPrototype, Transform(component.Owner).Coordinates);
component.AmmoContainer.Insert(ammo.Value);
}
return ammo.Value;
}
public EntityUid? TakeProjectile(BatteryBarrelComponent component, EntityCoordinates spawnAt)
{
if (!_cell.TryGetBatteryFromSlot(component.Owner, out var capacitor))
return null;
if (capacitor.CurrentCharge < component.LowerChargeLimit)
return null;
// Can fire confirmed
// Multiply the entity's damage / whatever by the percentage of charge the shot has.
EntityUid? entity;
var chargeChange = Math.Min(capacitor.CurrentCharge, component.BaseFireCost);
if (capacitor.UseCharge(chargeChange) < component.LowerChargeLimit)
{
// Handling of funny exploding cells.
return null;
}
var energyRatio = chargeChange / component.BaseFireCost;
if (component.AmmoContainer.ContainedEntity != null)
{
entity = component.AmmoContainer.ContainedEntity;
component.AmmoContainer.Remove(entity.Value);
Transform(entity.Value).Coordinates = spawnAt;
}
else
{
entity = EntityManager.SpawnEntity(component.AmmoPrototype, spawnAt);
}
if (TryComp(entity.Value, out ProjectileComponent? projectileComponent))
{
if (energyRatio < 1.0)
{
projectileComponent.Damage *= energyRatio;
}
}
else if (TryComp(entity.Value, out HitscanComponent? hitscanComponent))
{
hitscanComponent.Damage *= energyRatio;
hitscanComponent.ColorModifier = energyRatio;
}
else
{
throw new InvalidOperationException("Ammo doesn't have hitscan or projectile?");
}
// capacitor.UseCharge() triggers a PowerCellChangedEvent which will cause appearance to be updated.
// So let's not double-call UpdateAppearance() here.
return entity.Value;
}
}

View File

@@ -1,266 +0,0 @@
using Content.Server.Weapon.Ranged.Ammunition.Components;
using Content.Server.Weapon.Ranged.Barrels.Components;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Verbs;
using Content.Shared.Weapons.Ranged.Barrels.Components;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Player;
namespace Content.Server.Weapon.Ranged;
public sealed partial class GunSystem
{
private void AddToggleBoltVerb(EntityUid uid, BoltActionBarrelComponent component, GetVerbsEvent<InteractionVerb> args)
{
if (args.Hands == null ||
!args.CanAccess ||
!args.CanInteract)
return;
InteractionVerb verb = new()
{
Text = component.BoltOpen
? Loc.GetString("close-bolt-verb-get-data-text")
: Loc.GetString("open-bolt-verb-get-data-text"),
Act = () => component.BoltOpen = !component.BoltOpen
};
args.Verbs.Add(verb);
}
private void OnBoltExamine(EntityUid uid, BoltActionBarrelComponent component, ExaminedEvent args)
{
args.PushMarkup(Loc.GetString("bolt-action-barrel-component-on-examine", ("caliber", component.Caliber)));
}
private void OnBoltFireAttempt(EntityUid uid, BoltActionBarrelComponent component, GunFireAttemptEvent args)
{
if (args.Cancelled) return;
if (component.BoltOpen || component.ChamberContainer.ContainedEntity == null)
args.Cancel();
}
private void OnBoltMapInit(EntityUid uid, BoltActionBarrelComponent component, MapInitEvent args)
{
if (component.FillPrototype != null)
{
component.UnspawnedCount += component.Capacity;
if (component.UnspawnedCount > 0)
{
component.UnspawnedCount--;
var chamberEntity = EntityManager.SpawnEntity(component.FillPrototype, EntityManager.GetComponent<TransformComponent>(uid).Coordinates);
component.ChamberContainer.Insert(chamberEntity);
}
}
UpdateBoltAppearance(component);
}
public void UpdateBoltAppearance(BoltActionBarrelComponent component)
{
if (!TryComp(component.Owner, out AppearanceComponent? appearanceComponent)) return;
appearanceComponent.SetData(BarrelBoltVisuals.BoltOpen, component.BoltOpen);
appearanceComponent.SetData(AmmoVisuals.AmmoCount, component.ShotsLeft);
appearanceComponent.SetData(AmmoVisuals.AmmoMax, component.Capacity);
}
private void OnBoltInit(EntityUid uid, BoltActionBarrelComponent component, ComponentInit args)
{
component.SpawnedAmmo = new Stack<EntityUid>(component.Capacity - 1);
component.AmmoContainer = uid.EnsureContainer<Container>($"{component.GetType()}-ammo-container", out var existing);
if (existing)
{
foreach (var entity in component.AmmoContainer.ContainedEntities)
{
component.SpawnedAmmo.Push(entity);
component.UnspawnedCount--;
}
}
component.ChamberContainer = uid.EnsureContainer<ContainerSlot>($"{component.GetType()}-chamber-container");
if (TryComp(uid, out AppearanceComponent? appearanceComponent))
{
appearanceComponent.SetData(MagazineBarrelVisuals.MagLoaded, true);
}
component.Dirty(EntityManager);
UpdateBoltAppearance(component);
}
private void OnBoltUse(EntityUid uid, BoltActionBarrelComponent component, UseInHandEvent args)
{
if (args.Handled) return;
args.Handled = true;
if (component.BoltOpen)
{
component.BoltOpen = false;
_popup.PopupEntity(Loc.GetString("bolt-action-barrel-component-bolt-closed"), uid, Filter.Entities(args.User));
return;
}
CycleBolt(component, true);
}
private void CycleBolt(BoltActionBarrelComponent component, bool manual = false)
{
TryEjectChamber(component);
TryFeedChamber(component);
if (component.ChamberContainer.ContainedEntity == null && manual)
{
component.BoltOpen = true;
if (_container.TryGetContainingContainer(component.Owner, out var container))
{
_popup.PopupEntity(Loc.GetString("bolt-action-barrel-component-bolt-opened"), container.Owner, Filter.Entities(container.Owner));
}
return;
}
else
{
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundCycle.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
}
component.Dirty(EntityManager);
UpdateBoltAppearance(component);
}
public bool TryEjectChamber(BoltActionBarrelComponent component)
{
if (component.ChamberContainer.ContainedEntity is {Valid: true} chambered)
{
if (!component.ChamberContainer.Remove(chambered))
return false;
if (TryComp(chambered, out AmmoComponent? ammoComponent) && !ammoComponent.Caseless)
EjectCasing(chambered);
return true;
}
return false;
}
public bool TryFeedChamber(BoltActionBarrelComponent component)
{
if (component.ChamberContainer.ContainedEntity != null)
{
return false;
}
if (component.SpawnedAmmo.TryPop(out var next))
{
component.AmmoContainer.Remove(next);
component.ChamberContainer.Insert(next);
return true;
}
else if (component.UnspawnedCount > 0)
{
component.UnspawnedCount--;
var ammoEntity = EntityManager.SpawnEntity(component.FillPrototype, EntityManager.GetComponent<TransformComponent>(component.Owner).Coordinates);
component.ChamberContainer.Insert(ammoEntity);
return true;
}
return false;
}
private void OnBoltInteractUsing(EntityUid uid, BoltActionBarrelComponent component, InteractUsingEvent args)
{
if (args.Handled) return;
if (TryInsertBullet(args.User, args.Used, component))
args.Handled = true;
}
public bool TryInsertBullet(EntityUid user, EntityUid ammo, BoltActionBarrelComponent component)
{
if (!TryComp(ammo, out AmmoComponent? ammoComponent))
return false;
if (ammoComponent.Caliber != component.Caliber)
{
_popup.PopupEntity(Loc.GetString("bolt-action-barrel-component-try-insert-bullet-wrong-caliber"), component.Owner, Filter.Entities(user));
return false;
}
if (!component.BoltOpen)
{
_popup.PopupEntity(Loc.GetString("bolt-action-barrel-component-try-insert-bullet-bolt-closed"), component.Owner, Filter.Entities(user));
return false;
}
if (component.ChamberContainer.ContainedEntity == null)
{
component.ChamberContainer.Insert(ammo);
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundInsert.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
component.Dirty(EntityManager);
UpdateBoltAppearance(component);
return true;
}
if (component.AmmoContainer.ContainedEntities.Count < component.Capacity - 1)
{
component.AmmoContainer.Insert(ammo);
component.SpawnedAmmo.Push(ammo);
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundInsert.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
component.Dirty(EntityManager);
UpdateBoltAppearance(component);
return true;
}
_popup.PopupEntity(Loc.GetString("bolt-action-barrel-component-try-insert-bullet-no-room"), component.Owner, Filter.Entities(user));
return false;
}
private void OnBoltGetState(EntityUid uid, BoltActionBarrelComponent component, ref ComponentGetState args)
{
(int, int)? count = (component.ShotsLeft, component.Capacity);
var chamberedExists = component.ChamberContainer.ContainedEntity != null;
// (Is one chambered?, is the bullet spend)
var chamber = (chamberedExists, false);
if (chamberedExists && TryComp<AmmoComponent?>(component.ChamberContainer.ContainedEntity!.Value, out var ammo))
{
chamber.Item2 = ammo.Spent;
}
args.State = new BoltActionBarrelComponentState(
chamber,
component.FireRateSelector,
count,
component.SoundGunshot.GetSound());
}
public EntityUid? PeekAmmo(BoltActionBarrelComponent component)
{
return component.ChamberContainer.ContainedEntity;
}
public EntityUid? TakeProjectile(BoltActionBarrelComponent component, EntityCoordinates spawnAt)
{
if (component.AutoCycle)
{
CycleBolt(component);
}
else
{
component.Dirty(EntityManager);
}
if (component.ChamberContainer.ContainedEntity is not {Valid: true} chamberEntity) return null;
var ammoComponent = EntityManager.GetComponentOrNull<AmmoComponent>(chamberEntity);
return ammoComponent == null ? null : TakeBullet(ammoComponent, spawnAt);
}
}

View File

@@ -1,302 +0,0 @@
using System.Linq;
using Content.Server.CombatMode;
using Content.Server.Hands.Components;
using Content.Server.Interaction.Components;
using Content.Server.Projectiles.Components;
using Content.Server.Weapon.Melee;
using Content.Server.Weapon.Ranged.Ammunition.Components;
using Content.Server.Weapon.Ranged.Barrels.Components;
using Content.Shared.Audio;
using Content.Shared.Camera;
using Content.Shared.Damage;
using Content.Shared.Database;
using Content.Shared.Interaction.Events;
using Content.Shared.Popups;
using Content.Shared.Sound;
using Robust.Shared.Audio;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Player;
using Robust.Shared.Utility;
namespace Content.Server.Weapon.Ranged;
public sealed partial class GunSystem
{
private void OnMeleeAttempt(EntityUid uid, ServerRangedWeaponComponent component, ref MeleeAttackAttemptEvent args)
{
args.Cancelled = true;
}
/// <summary>
/// Tries to fire a round of ammo out of the weapon.
/// </summary>
private void TryFire(EntityUid user, EntityCoordinates targetCoords, ServerRangedWeaponComponent gun)
{
if (!TryComp(gun.Owner, out ServerRangedBarrelComponent? barrel)) return;
if (!TryComp(user, out HandsComponent? hands) || hands.ActiveHand?.HeldEntity != gun.Owner) return;
if (!TryComp(user, out CombatModeComponent? combat) ||
!combat.IsInCombatMode ||
!_blocker.CanInteract(user, gun.Owner)) return;
var fireAttempt = new GunFireAttemptEvent(user, gun);
EntityManager.EventBus.RaiseLocalEvent(gun.Owner, fireAttempt);
if (fireAttempt.Cancelled) return;
var curTime = _gameTiming.CurTime;
var span = curTime - gun.LastFireTime;
if (span.TotalSeconds < 1 / barrel.FireRate) return;
// TODO: Clumsy should be eventbus I think?
gun.LastFireTime = curTime;
var coordinates = Transform(gun.Owner).Coordinates;
if (gun.ClumsyCheck && EntityManager.TryGetComponent<ClumsyComponent>(user, out var clumsyComponent) && ClumsyComponent.TryRollClumsy(user, gun.ClumsyExplodeChance))
{
//Wound them
_damageable.TryChangeDamage(user, clumsyComponent.ClumsyDamage);
_stun.TryParalyze(user, TimeSpan.FromSeconds(3f), true);
// Apply salt to the wound ("Honk!")
SoundSystem.Play(
Filter.Pvs(gun.Owner), gun.ClumsyWeaponHandlingSound.GetSound(),
coordinates, AudioParams.Default.WithMaxDistance(5));
SoundSystem.Play(
Filter.Pvs(gun.Owner), gun.ClumsyWeaponShotSound.GetSound(),
coordinates, AudioParams.Default.WithMaxDistance(5));
user.PopupMessage(Loc.GetString("server-ranged-weapon-component-try-fire-clumsy"));
EntityManager.DeleteEntity(gun.Owner);
return;
}
// Firing confirmed
if (gun.CanHotspot)
_atmos.HotspotExpose(coordinates, 700, 50);
EntityManager.EventBus.RaiseLocalEvent(gun.Owner, new GunShotEvent());
Fire(user, barrel, targetCoords);
}
/// <summary>
/// Fires a round of ammo out of the weapon.
/// </summary>
private void Fire(EntityUid shooter, ServerRangedBarrelComponent component, EntityCoordinates coordinates)
{
if (component.ShotsLeft == 0)
{
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundEmpty.GetSound(), component.Owner);
return;
}
var ammo = PeekAtAmmo(component);
if (TakeOutProjectile(component, Transform(shooter).Coordinates) is not {Valid: true} projectile)
{
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundEmpty.GetSound(), component.Owner);
return;
}
var targetPos = coordinates.ToMapPos(EntityManager);
// At this point firing is confirmed
var direction = (targetPos - Transform(shooter).WorldPosition).ToAngle();
var angle = GetRecoilAngle(component, direction);
// This should really be client-side but for now we'll just leave it here
if (HasComp<CameraRecoilComponent>(shooter))
{
var kick = -angle.ToVec() * 0.15f;
_recoil.KickCamera(shooter, kick);
}
// This section probably needs tweaking so there can be caseless hitscan etc.
if (TryComp(projectile, out HitscanComponent? hitscan))
{
FireHitscan(shooter, hitscan, component, angle);
}
else if (HasComp<ProjectileComponent>(projectile) &&
TryComp(ammo, out AmmoComponent? ammoComponent))
{
FireProjectiles(shooter, projectile, component, ammoComponent.ProjectilesFired, ammoComponent.EvenSpreadAngle, angle, ammoComponent.Velocity, ammo!.Value);
if (component.CanMuzzleFlash)
{
MuzzleFlash(component.Owner, ammoComponent, angle);
}
if (ammoComponent.Caseless)
{
EntityManager.DeleteEntity(ammo.Value);
}
}
else
{
// Invalid types
throw new InvalidOperationException();
}
SoundSystem.Play(Filter.Broadcast(), component.SoundGunshot.GetSound(), component.Owner);
component.Dirty(EntityManager);
component.LastFire = _gameTiming.CurTime;
}
#region Firing
/// <summary>
/// Handles firing one or many projectiles
/// </summary>
private void FireProjectiles(EntityUid shooter, EntityUid baseProjectile, ServerRangedBarrelComponent component, int count, float evenSpreadAngle, Angle angle, float velocity, EntityUid ammo)
{
List<Angle>? sprayAngleChange = null;
if (count > 1)
{
evenSpreadAngle *= component.SpreadRatio;
sprayAngleChange = Linspace(-evenSpreadAngle / 2, evenSpreadAngle / 2, count);
}
var firedProjectiles = new EntityUid[count];
for (var i = 0; i < count; i++)
{
EntityUid projectile;
if (i == 0)
{
projectile = baseProjectile;
}
else
{
// TODO: Cursed as bruh
projectile = EntityManager.SpawnEntity(
MetaData(baseProjectile).EntityPrototype?.ID,
Transform(baseProjectile).Coordinates);
}
firedProjectiles[i] = projectile;
Angle projectileAngle;
if (sprayAngleChange != null)
{
projectileAngle = angle + sprayAngleChange[i];
}
else
{
projectileAngle = angle;
}
var physics = EntityManager.GetComponent<IPhysBody>(projectile);
physics.BodyStatus = BodyStatus.InAir;
var projectileComponent = EntityManager.GetComponent<ProjectileComponent>(projectile);
projectileComponent.IgnoreEntity(shooter);
// FIXME: Work around issue where inserting and removing an entity from a container,
// then setting its linear velocity in the same tick resets velocity back to zero.
// See SharedBroadphaseSystem.HandleContainerInsert()... It sets Awake to false, which causes this.
projectile.SpawnTimer(TimeSpan.FromMilliseconds(25), () =>
{
EntityManager.GetComponent<IPhysBody>(projectile)
.LinearVelocity = projectileAngle.ToVec() * velocity;
});
Transform(projectile).WorldRotation = projectileAngle + MathHelper.PiOver2;
}
EntityManager.EventBus.RaiseLocalEvent(component.Owner, new Barrels.Components.GunShotEvent(firedProjectiles));
EntityManager.EventBus.RaiseLocalEvent(ammo, new AmmoShotEvent(firedProjectiles));
}
/// <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(EntityUid shooter, HitscanComponent hitscan, ServerRangedBarrelComponent component, Angle angle)
{
var ray = new CollisionRay(Transform(component.Owner).WorldPosition, angle.ToVec(), (int) hitscan.CollisionMask);
var rayCastResults = _physics.IntersectRay(Transform(component.Owner).MapID, ray, hitscan.MaxLength, shooter, false).ToList();
if (rayCastResults.Count >= 1)
{
var result = rayCastResults[0];
var distance = result.Distance;
hitscan.FireEffects(shooter, distance, angle, result.HitEntity);
var modifiedDamage = _damageable.TryChangeDamage(result.HitEntity, hitscan.Damage);
if (modifiedDamage != null)
_adminLogger.Add(LogType.HitScanHit,
$"{EntityManager.ToPrettyString(shooter):user} hit {EntityManager.ToPrettyString(result.HitEntity):target} using {EntityManager.ToPrettyString(hitscan.Owner):used} and dealt {modifiedDamage.Total:damage} damage");
PlaySound(rayCastResults[0].HitEntity, modifiedDamage, hitscan.SoundHit, hitscan.ForceSound);
}
else
{
hitscan.FireEffects(shooter, hitscan.MaxLength, angle);
}
}
#endregion
#region Impact sounds
public void PlaySound(EntityUid otherEntity, DamageSpecifier? modifiedDamage, SoundSpecifier? weaponSound, bool forceWeaponSound)
{
// Like projectiles and melee,
// 1. Entity specific sound
// 2. Ammo's sound
// 3. Nothing
var playedSound = false;
if (!forceWeaponSound && modifiedDamage != null && modifiedDamage.Total > 0 && TryComp<RangedDamageSoundComponent>(otherEntity, out var rangedSound))
{
var type = MeleeWeaponSystem.GetHighestDamageSound(modifiedDamage, _protoManager);
if (type != null && rangedSound.SoundTypes?.TryGetValue(type, out var damageSoundType) == true)
{
SoundSystem.Play(
Filter.Pvs(otherEntity, entityManager: EntityManager),
damageSoundType!.GetSound(),
otherEntity,
AudioHelpers.WithVariation(DamagePitchVariation));
playedSound = true;
}
else if (type != null && rangedSound.SoundGroups?.TryGetValue(type, out var damageSoundGroup) == true)
{
SoundSystem.Play(
Filter.Pvs(otherEntity, entityManager: EntityManager),
damageSoundGroup!.GetSound(),
otherEntity,
AudioHelpers.WithVariation(DamagePitchVariation));
playedSound = true;
}
}
if (!playedSound && weaponSound != null)
SoundSystem.Play(Filter.Pvs(otherEntity, entityManager: EntityManager), weaponSound.GetSound(), otherEntity);
}
#endregion
}

View File

@@ -1,375 +0,0 @@
using Content.Server.Weapon.Ranged.Ammunition.Components;
using Content.Server.Weapon.Ranged.Barrels.Components;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Verbs;
using Content.Shared.Weapons.Ranged;
using Content.Shared.Weapons.Ranged.Barrels.Components;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Player;
namespace Content.Server.Weapon.Ranged;
public sealed partial class GunSystem
{
private void AddEjectMagazineVerb(EntityUid uid, MagazineBarrelComponent component, GetVerbsEvent<AlternativeVerb> args)
{
if (args.Hands == null ||
!args.CanAccess ||
!args.CanInteract ||
component.MagazineContainer.ContainedEntity is not EntityUid mag ||
!_blocker.CanPickup(args.User, mag))
return;
if (component.MagNeedsOpenBolt && !component.BoltOpen)
return;
AlternativeVerb verb = new()
{
Text = MetaData(mag).EntityName,
Category = VerbCategory.Eject,
Act = () => RemoveMagazine(args.User, component)
};
args.Verbs.Add(verb);
}
private void AddMagazineInteractionVerbs(EntityUid uid, MagazineBarrelComponent component, GetVerbsEvent<InteractionVerb> args)
{
if (args.Hands == null ||
!args.CanAccess ||
!args.CanInteract)
return;
// Toggle bolt verb
InteractionVerb toggleBolt = new()
{
Text = component.BoltOpen
? Loc.GetString("close-bolt-verb-get-data-text")
: Loc.GetString("open-bolt-verb-get-data-text"),
Act = () => component.BoltOpen = !component.BoltOpen
};
args.Verbs.Add(toggleBolt);
// Are we holding a mag that we can insert?
if (args.Using is not {Valid: true} @using ||
!CanInsertMagazine(args.User, @using, component) ||
!_blocker.CanDrop(args.User))
return;
// Insert mag verb
InteractionVerb insert = new()
{
Text = MetaData(@using).EntityName,
Category = VerbCategory.Insert,
Act = () => InsertMagazine(args.User, @using, component)
};
args.Verbs.Add(insert);
}
private void OnMagazineExamine(EntityUid uid, MagazineBarrelComponent component, ExaminedEvent args)
{
args.PushMarkup(Loc.GetString("server-magazine-barrel-component-on-examine", ("caliber", component.Caliber)));
foreach (var magazineType in GetMagazineTypes(component))
{
args.PushMarkup(Loc.GetString("server-magazine-barrel-component-on-examine-magazine-type", ("magazineType", magazineType)));
}
}
private void OnMagazineUse(EntityUid uid, MagazineBarrelComponent component, UseInHandEvent args)
{
if (args.Handled) return;
// Behavior:
// If bolt open just close it
// If bolt closed then cycle
// If we cycle then get next round
// If no more round then open bolt
args.Handled = true;
if (component.BoltOpen)
{
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundBoltClosed.GetSound(), component.Owner, AudioParams.Default.WithVolume(-5));
_popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-use-entity-bolt-closed"), component.Owner, Filter.Entities(args.User));
component.BoltOpen = false;
return;
}
// Could play a rack-slide specific sound here if you're so inclined (if the chamber is empty but rounds are available)
CycleMagazine(component, true);
return;
}
public void UpdateMagazineAppearance(MagazineBarrelComponent component)
{
if (!TryComp(component.Owner, out AppearanceComponent? appearanceComponent)) return;
appearanceComponent.SetData(BarrelBoltVisuals.BoltOpen, component.BoltOpen);
appearanceComponent.SetData(MagazineBarrelVisuals.MagLoaded, component.MagazineContainer.ContainedEntity != null);
appearanceComponent.SetData(AmmoVisuals.AmmoCount, component.ShotsLeft);
appearanceComponent.SetData(AmmoVisuals.AmmoMax, component.Capacity);
}
private void OnMagazineGetState(EntityUid uid, MagazineBarrelComponent component, ref ComponentGetState args)
{
(int, int)? count = null;
if (component.MagazineContainer.ContainedEntity is {Valid: true} magazine &&
TryComp(magazine, out RangedMagazineComponent? rangedMagazineComponent))
{
count = (rangedMagazineComponent.ShotsLeft, rangedMagazineComponent.Capacity);
}
args.State = new MagazineBarrelComponentState(
component.ChamberContainer.ContainedEntity != null,
component.FireRateSelector,
count,
component.SoundGunshot.GetSound());
}
private void OnMagazineInteractUsing(EntityUid uid, MagazineBarrelComponent component, InteractUsingEvent args)
{
if (args.Handled) return;
if (CanInsertMagazine(args.User, args.Used, component, false))
{
InsertMagazine(args.User, args.Used, component);
args.Handled = true;
return;
}
// Insert 1 ammo
if (TryComp(args.Used, out AmmoComponent? ammoComponent))
{
if (!component.BoltOpen)
{
_popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-interact-using-ammo-bolt-closed"), component.Owner, Filter.Entities(args.User));
return;
}
if (ammoComponent.Caliber != component.Caliber)
{
_popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-interact-using-wrong-caliber"), component.Owner, Filter.Entities(args.User));
return;
}
if (component.ChamberContainer.ContainedEntity == null)
{
_popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-interact-using-ammo-success"), component.Owner, Filter.Entities(args.User));
component.ChamberContainer.Insert(args.Used);
component.Dirty(EntityManager);
UpdateMagazineAppearance(component);
args.Handled = true;
return;
}
_popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-interact-using-ammo-full"), component.Owner, Filter.Entities(args.User));
}
}
private void OnMagazineInit(EntityUid uid, MagazineBarrelComponent component, ComponentInit args)
{
component.ChamberContainer = uid.EnsureContainer<ContainerSlot>($"{component.GetType()}-chamber");
component.MagazineContainer = uid.EnsureContainer<ContainerSlot>($"{component.GetType()}-magazine", out var existing);
if (!existing && component.MagFillPrototype != null)
{
var magEntity = EntityManager.SpawnEntity(component.MagFillPrototype, Transform(uid).Coordinates);
component.MagazineContainer.Insert(magEntity);
}
// Temporary coz client doesn't know about magfill.
component.Dirty(EntityManager);
}
private void OnMagazineMapInit(EntityUid uid, MagazineBarrelComponent component, MapInitEvent args)
{
UpdateMagazineAppearance(component);
}
public bool TryEjectChamber(MagazineBarrelComponent component)
{
if (component.ChamberContainer.ContainedEntity is {Valid: true} chamberEntity)
{
if (!component.ChamberContainer.Remove(chamberEntity))
{
return false;
}
var ammoComponent = EntityManager.GetComponent<AmmoComponent>(chamberEntity);
if (!ammoComponent.Caseless)
{
EjectCasing(chamberEntity);
}
return true;
}
return false;
}
public bool TryFeedChamber(MagazineBarrelComponent component)
{
if (component.ChamberContainer.ContainedEntity != null)
{
return false;
}
// Try and pull a round from the magazine to replace the chamber if possible
var magazine = component.MagazineContainer.ContainedEntity;
var magComp = EntityManager.GetComponentOrNull<RangedMagazineComponent>(magazine);
if (magComp == null || TakeAmmo(magComp) is not {Valid: true} nextRound)
{
return false;
}
component.ChamberContainer.Insert(nextRound);
if (component.AutoEjectMag && magazine != null && EntityManager.GetComponent<RangedMagazineComponent>(magazine.Value).ShotsLeft == 0)
{
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundAutoEject.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
component.MagazineContainer.Remove(magazine.Value);
// TODO: Should be a state or something, waste of bandwidth
RaiseNetworkEvent(new MagazineAutoEjectEvent {Uid = component.Owner});
}
return true;
}
private void CycleMagazine(MagazineBarrelComponent component, bool manual = false)
{
if (component.BoltOpen)
return;
TryEjectChamber(component);
TryFeedChamber(component);
if (component.ChamberContainer.ContainedEntity == null && !component.BoltOpen)
{
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundBoltOpen.GetSound(), component.Owner, AudioParams.Default.WithVolume(-5));
if (_container.TryGetContainingContainer(component.Owner, out var container))
{
_popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-cycle-bolt-open"), component.Owner, Filter.Entities(container.Owner));
}
component.BoltOpen = true;
return;
}
if (manual)
{
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundRack.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
}
component.Dirty(EntityManager);
UpdateMagazineAppearance(component);
}
public EntityUid? PeekAmmo(MagazineBarrelComponent component)
{
return component.BoltOpen ? null : component.ChamberContainer.ContainedEntity;
}
public EntityUid? TakeProjectile(MagazineBarrelComponent component, EntityCoordinates spawnAt)
{
if (component.BoltOpen)
return null;
var entity = component.ChamberContainer.ContainedEntity;
CycleMagazine(component);
return entity != null ? TakeBullet(EntityManager.GetComponent<AmmoComponent>(entity.Value), spawnAt) : null;
}
public List<MagazineType> GetMagazineTypes(MagazineBarrelComponent component)
{
var types = new List<MagazineType>();
foreach (MagazineType mag in Enum.GetValues(typeof(MagazineType)))
{
if ((component.MagazineTypes & mag) != 0)
{
types.Add(mag);
}
}
return types;
}
public void RemoveMagazine(EntityUid user, MagazineBarrelComponent component)
{
var mag = component.MagazineContainer.ContainedEntity;
if (mag == null)
return;
if (component.MagNeedsOpenBolt && !component.BoltOpen)
{
_popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-remove-magazine-bolt-closed"), component.Owner, Filter.Entities(user));
return;
}
component.MagazineContainer.Remove(mag.Value);
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundMagEject.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
_handsSystem.PickupOrDrop(user, mag.Value);
component.Dirty(EntityManager);
UpdateMagazineAppearance(component);
}
public bool CanInsertMagazine(EntityUid user, EntityUid magazine, MagazineBarrelComponent component, bool quiet = true)
{
if (!TryComp(magazine, out RangedMagazineComponent? magazineComponent))
{
return false;
}
if ((component.MagazineTypes & magazineComponent.MagazineType) == 0)
{
if (!quiet)
_popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-interact-using-wrong-magazine-type"), component.Owner, Filter.Entities(user));
return false;
}
if (magazineComponent.Caliber != component.Caliber)
{
if (!quiet)
_popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-interact-using-wrong-caliber"), component.Owner, Filter.Entities(user));
return false;
}
if (component.MagNeedsOpenBolt && !component.BoltOpen)
{
if (!quiet)
_popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-interact-using-bolt-closed"), component.Owner, Filter.Entities(user));
return false;
}
if (component.MagazineContainer.ContainedEntity == null)
return true;
if (!quiet)
_popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-interact-using-already-holding-magazine"), component.Owner, Filter.Entities(user));
return false;
}
public void InsertMagazine(EntityUid user, EntityUid magazine, MagazineBarrelComponent component)
{
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundMagInsert.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
_popup.PopupEntity(Loc.GetString("server-magazine-barrel-component-interact-using-success"), component.Owner, Filter.Entities(user));
component.MagazineContainer.Insert(magazine);
component.Dirty(EntityManager);
UpdateMagazineAppearance(component);
}
}

View File

@@ -1,190 +0,0 @@
using Content.Server.Weapon.Ranged.Ammunition.Components;
using Content.Server.Weapon.Ranged.Barrels.Components;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Weapons.Ranged.Barrels.Components;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Player;
namespace Content.Server.Weapon.Ranged;
public sealed partial class GunSystem
{
private void OnPumpExamine(EntityUid uid, PumpBarrelComponent component, ExaminedEvent args)
{
args.PushMarkup(Loc.GetString("pump-barrel-component-on-examine", ("caliber", component.Caliber)));
}
private void OnPumpGetState(EntityUid uid, PumpBarrelComponent component, ref ComponentGetState args)
{
(int, int)? count = (component.ShotsLeft, component.Capacity);
var chamberedExists = component.ChamberContainer.ContainedEntity != null;
// (Is one chambered?, is the bullet spend)
var chamber = (chamberedExists, false);
if (chamberedExists && TryComp<AmmoComponent?>(component.ChamberContainer.ContainedEntity!.Value, out var ammo))
{
chamber.Item2 = ammo.Spent;
}
args.State = new PumpBarrelComponentState(
chamber,
component.FireRateSelector,
count,
component.SoundGunshot.GetSound());
}
private void OnPumpMapInit(EntityUid uid, PumpBarrelComponent component, MapInitEvent args)
{
if (component.FillPrototype != null)
{
component.UnspawnedCount += component.Capacity - 1;
}
UpdatePumpAppearance(component);
}
private void UpdatePumpAppearance(PumpBarrelComponent component)
{
if (!TryComp(component.Owner, out AppearanceComponent? appearanceComponent)) return;
appearanceComponent.SetData(AmmoVisuals.AmmoCount, component.ShotsLeft);
appearanceComponent.SetData(AmmoVisuals.AmmoMax, component.Capacity);
}
private void OnPumpInit(EntityUid uid, PumpBarrelComponent component, ComponentInit args)
{
component.AmmoContainer =
uid.EnsureContainer<Container>($"{component.GetType()}-ammo-container", out var existing);
if (existing)
{
foreach (var entity in component.AmmoContainer.ContainedEntities)
{
component.SpawnedAmmo.Push(entity);
component.UnspawnedCount--;
}
}
component.ChamberContainer =
uid.EnsureContainer<ContainerSlot>($"{component.GetType()}-chamber-container", out existing);
if (existing)
{
component.UnspawnedCount--;
}
if (TryComp(uid, out AppearanceComponent? appearanceComponent))
{
appearanceComponent.SetData(MagazineBarrelVisuals.MagLoaded, true);
}
component.Dirty(EntityManager);
UpdatePumpAppearance(component);
}
private void OnPumpUse(EntityUid uid, PumpBarrelComponent component, UseInHandEvent args)
{
if (args.Handled) return;
args.Handled = true;
CyclePump(component, true);
}
private void OnPumpInteractUsing(EntityUid uid, PumpBarrelComponent component, InteractUsingEvent args)
{
if (args.Handled) return;
if (TryInsertBullet(component, args))
args.Handled = true;
}
public bool TryInsertBullet(PumpBarrelComponent component, InteractUsingEvent args)
{
if (!TryComp(args.Used, out AmmoComponent? ammoComponent))
{
return false;
}
if (ammoComponent.Caliber != component.Caliber)
{
_popup.PopupEntity(Loc.GetString("pump-barrel-component-try-insert-bullet-wrong-caliber"), component.Owner, Filter.Entities(args.User));
return false;
}
if (component.AmmoContainer.ContainedEntities.Count < component.Capacity - 1)
{
component.AmmoContainer.Insert(args.Used);
component.SpawnedAmmo.Push(args.Used);
component.Dirty(EntityManager);
UpdatePumpAppearance(component);
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundInsert.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
return true;
}
_popup.PopupEntity(Loc.GetString("pump-barrel-component-try-insert-bullet-no-room"), component.Owner, Filter.Entities(args.User));
return false;
}
private void CyclePump(PumpBarrelComponent component, bool manual = false)
{
if (component.ChamberContainer.ContainedEntity is {Valid: true} chamberedEntity)
{
component.ChamberContainer.Remove(chamberedEntity);
var ammoComponent = EntityManager.GetComponent<AmmoComponent>(chamberedEntity);
if (!ammoComponent.Caseless)
{
EjectCasing(chamberedEntity);
}
}
if (component.SpawnedAmmo.TryPop(out var next))
{
component.AmmoContainer.Remove(next);
component.ChamberContainer.Insert(next);
}
if (component.UnspawnedCount > 0)
{
component.UnspawnedCount--;
var ammoEntity = EntityManager.SpawnEntity(component.FillPrototype, Transform(component.Owner).Coordinates);
component.ChamberContainer.Insert(ammoEntity);
}
if (manual)
{
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundCycle.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
}
component.Dirty(EntityManager);
UpdatePumpAppearance(component);
}
public EntityUid? PeekAmmo(PumpBarrelComponent component)
{
return component.ChamberContainer.ContainedEntity;
}
public EntityUid? TakeProjectile(PumpBarrelComponent component, EntityCoordinates spawnAt)
{
if (!component.ManualCycle)
{
CyclePump(component);
}
else
{
component.Dirty(EntityManager);
}
if (component.ChamberContainer.ContainedEntity is not {Valid: true} chamberEntity) return null;
var ammoComponent = EntityManager.GetComponentOrNull<AmmoComponent>(chamberEntity);
return ammoComponent == null ? null : TakeBullet(ammoComponent, spawnAt);
}
}

View File

@@ -1,130 +0,0 @@
using Content.Server.Hands.Components;
using Content.Server.Weapon.Ranged.Ammunition.Components;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Weapons.Ranged.Barrels.Components;
using Robust.Shared.Containers;
using Robust.Shared.Player;
namespace Content.Server.Weapon.Ranged;
public sealed partial class GunSystem
{
private void OnRangedMagMapInit(EntityUid uid, RangedMagazineComponent component, MapInitEvent args)
{
if (component.FillPrototype != null)
{
component.UnspawnedCount += component.Capacity;
}
UpdateRangedMagAppearance(component);
}
private void OnRangedMagInit(EntityUid uid, RangedMagazineComponent component, ComponentInit args)
{
component.AmmoContainer = uid.EnsureContainer<Container>($"{component.GetType()}-magazine", out var existing);
if (existing)
{
if (component.AmmoContainer.ContainedEntities.Count > component.Capacity)
{
throw new InvalidOperationException("Initialized capacity of magazine higher than its actual capacity");
}
foreach (var entity in component.AmmoContainer.ContainedEntities)
{
component.SpawnedAmmo.Push(entity);
component.UnspawnedCount--;
}
}
if (TryComp(component.Owner, out AppearanceComponent? appearanceComponent))
{
appearanceComponent.SetData(MagazineBarrelVisuals.MagLoaded, true);
}
}
private void UpdateRangedMagAppearance(RangedMagazineComponent component)
{
if (!TryComp(component.Owner, out AppearanceComponent? appearanceComponent)) return;
appearanceComponent.SetData(AmmoVisuals.AmmoCount, component.ShotsLeft);
appearanceComponent.SetData(AmmoVisuals.AmmoMax, component.Capacity);
}
private void OnRangedMagUse(EntityUid uid, RangedMagazineComponent component, UseInHandEvent args)
{
if (args.Handled) return;
if (!TryComp(args.User, out HandsComponent? handsComponent))
{
return;
}
if (TakeAmmo(component) is not {Valid: true} ammo)
return;
_handsSystem.PickupOrDrop(args.User, ammo, handsComp: handsComponent);
EjectCasing(ammo);
args.Handled = true;
}
private void OnRangedMagExamine(EntityUid uid, RangedMagazineComponent component, ExaminedEvent args)
{
args.PushMarkup(Loc.GetString("ranged-magazine-component-on-examine", ("magazineType", component.MagazineType),("caliber", component.Caliber)));
}
private void OnRangedMagInteractUsing(EntityUid uid, RangedMagazineComponent component, InteractUsingEvent args)
{
if (args.Handled) return;
if (TryInsertAmmo(args.User, args.Used, component))
args.Handled = true;
}
public bool TryInsertAmmo(EntityUid user, EntityUid ammo, RangedMagazineComponent component)
{
if (!TryComp(ammo, out AmmoComponent? ammoComponent))
{
return false;
}
if (ammoComponent.Caliber != component.Caliber)
{
_popup.PopupEntity(Loc.GetString("ranged-magazine-component-try-insert-ammo-wrong-caliber"), component.Owner, Filter.Entities(user));
return false;
}
if (component.ShotsLeft >= component.Capacity)
{
_popup.PopupEntity(Loc.GetString("ranged-magazine-component-try-insert-ammo-is-full "), component.Owner, Filter.Entities(user));
return false;
}
component.AmmoContainer.Insert(ammo);
component.SpawnedAmmo.Push(ammo);
UpdateRangedMagAppearance(component);
return true;
}
public EntityUid? TakeAmmo(RangedMagazineComponent component)
{
EntityUid? ammo = null;
// If anything's spawned use that first, otherwise use the fill prototype as a fallback (if we have spawn count left)
if (component.SpawnedAmmo.TryPop(out var entity))
{
ammo = entity;
component.AmmoContainer.Remove(entity);
}
else if (component.UnspawnedCount > 0)
{
component.UnspawnedCount--;
ammo = EntityManager.SpawnEntity(component.FillPrototype, Transform(component.Owner).Coordinates);
}
UpdateRangedMagAppearance(component);
return ammo;
}
}

View File

@@ -1,226 +0,0 @@
using Content.Server.Weapon.Ranged.Ammunition.Components;
using Content.Server.Weapon.Ranged.Barrels.Components;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Popups;
using Content.Shared.Verbs;
using Content.Shared.Weapons.Ranged.Barrels.Components;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Player;
namespace Content.Server.Weapon.Ranged;
public sealed partial class GunSystem
{
private void OnRevolverUse(EntityUid uid, RevolverBarrelComponent component, UseInHandEvent args)
{
if (args.Handled) return;
EjectAllSlots(component);
component.Dirty(EntityManager);
UpdateRevolverAppearance(component);
args.Handled = true;
}
private void OnRevolverInteractUsing(EntityUid uid, RevolverBarrelComponent component, InteractUsingEvent args)
{
if (args.Handled) return;
if (TryInsertBullet(args.User, args.Used, component))
args.Handled = true;
}
public bool TryInsertBullet(EntityUid user, EntityUid entity, RevolverBarrelComponent component)
{
if (!TryComp(entity, out AmmoComponent? ammoComponent))
{
return false;
}
if (ammoComponent.Caliber != component.Caliber)
{
_popup.PopupEntity(Loc.GetString("revolver-barrel-component-try-insert-bullet-wrong-caliber"), component.Owner, Filter.Entities(user));
return false;
}
// Functions like a stack
// These are inserted in reverse order but then when fired Cycle will go through in order
// The reason we don't just use an actual stack is because spin can select a random slot to point at
for (var i = component.AmmoSlots.Length - 1; i >= 0; i--)
{
var slot = component.AmmoSlots[i];
if (slot == default)
{
component.CurrentSlot = i;
component.AmmoSlots[i] = entity;
component.AmmoContainer.Insert(entity);
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundInsert.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
component.Dirty(EntityManager);
UpdateRevolverAppearance(component);
return true;
}
}
_popup.PopupEntity(Loc.GetString("revolver-barrel-component-try-insert-bullet-ammo-full"), ammoComponent.Owner, Filter.Entities(user));
return false;
}
/// <summary>
/// Russian Roulette
/// </summary>
public void SpinRevolver(RevolverBarrelComponent component)
{
var random = _random.Next(component.AmmoSlots.Length - 1);
component.CurrentSlot = random;
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundSpin.GetSound(), component.Owner, AudioParams.Default.WithVolume(-2));
component.Dirty(EntityManager);
}
public void CycleRevolver(RevolverBarrelComponent component)
{
// Move up a slot
component.CurrentSlot = (component.CurrentSlot + 1) % component.AmmoSlots.Length;
component.Dirty(EntityManager);
UpdateRevolverAppearance(component);
}
private void EjectAllSlots(RevolverBarrelComponent component)
{
for (var i = 0; i < component.AmmoSlots.Length; i++)
{
var entity = component.AmmoSlots[i];
if (entity == null) continue;
component.AmmoContainer.Remove(entity.Value);
EjectCasing(entity.Value);
component.AmmoSlots[i] = null;
}
if (component.AmmoContainer.ContainedEntities.Count > 0)
{
SoundSystem.Play(Filter.Pvs(component.Owner), component.SoundEject.GetSound(), component.Owner, AudioParams.Default.WithVolume(-1));
}
// May as well point back at the end?
component.CurrentSlot = component.AmmoSlots.Length - 1;
}
private void OnRevolverGetState(EntityUid uid, RevolverBarrelComponent component, ref ComponentGetState args)
{
var slotsSpent = new bool?[component.Capacity];
for (var i = 0; i < component.Capacity; i++)
{
slotsSpent[i] = null;
var ammoEntity = component.AmmoSlots[i];
if (ammoEntity != default && TryComp(ammoEntity, out AmmoComponent? ammo))
{
slotsSpent[i] = ammo.Spent;
}
}
//TODO: make yaml var to not sent currentSlot/UI? (for russian roulette)
args.State = new RevolverBarrelComponentState(
component.CurrentSlot,
component.FireRateSelector,
slotsSpent,
component.SoundGunshot.GetSound());
}
private void OnRevolverMapInit(EntityUid uid, RevolverBarrelComponent component, MapInitEvent args)
{
component.UnspawnedCount = component.Capacity;
var idx = 0;
component.AmmoContainer = component.Owner.EnsureContainer<Container>($"{component.GetType()}-ammoContainer", out var existing);
if (existing)
{
foreach (var entity in component.AmmoContainer.ContainedEntities)
{
component.UnspawnedCount--;
component.AmmoSlots[idx] = entity;
idx++;
}
}
// TODO: Revolvers should also defer spawning T B H
var xform = EntityManager.GetComponent<TransformComponent>(uid);
for (var i = 0; i < component.UnspawnedCount; i++)
{
var entity = EntityManager.SpawnEntity(component.FillPrototype, xform.Coordinates);
component.AmmoSlots[idx] = entity;
component.AmmoContainer.Insert(entity);
idx++;
}
UpdateRevolverAppearance(component);
component.Dirty(EntityManager);
}
private void UpdateRevolverAppearance(RevolverBarrelComponent component)
{
if (!TryComp(component.Owner, out AppearanceComponent? appearance))
{
return;
}
// Placeholder, at this stage it's just here for the RPG
appearance.SetData(MagazineBarrelVisuals.MagLoaded, component.ShotsLeft > 0);
appearance.SetData(AmmoVisuals.AmmoCount, component.ShotsLeft);
appearance.SetData(AmmoVisuals.AmmoMax, component.Capacity);
}
private void AddSpinVerb(EntityUid uid, RevolverBarrelComponent component, GetVerbsEvent<AlternativeVerb> args)
{
if (args.Hands == null || !args.CanAccess || !args.CanInteract)
return;
if (component.Capacity <= 1 || component.ShotsLeft == 0)
return;
AlternativeVerb verb = new()
{
Text = Loc.GetString("spin-revolver-verb-get-data-text"),
IconTexture = "/Textures/Interface/VerbIcons/refresh.svg.192dpi.png",
Act = () =>
{
SpinRevolver(component);
component.Owner.PopupMessage(args.User, Loc.GetString("spin-revolver-verb-on-activate"));
}
};
args.Verbs.Add(verb);
}
public EntityUid? PeekAmmo(RevolverBarrelComponent component)
{
return component.AmmoSlots[component.CurrentSlot];
}
/// <summary>
/// Takes a projectile out if possible
/// IEnumerable just to make supporting shotguns saner
/// </summary>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public EntityUid? TakeProjectile(RevolverBarrelComponent component, EntityCoordinates spawnAt)
{
var ammo = component.AmmoSlots[component.CurrentSlot];
EntityUid? bullet = null;
if (ammo != null)
{
var ammoComponent = EntityManager.GetComponent<AmmoComponent>(ammo.Value);
bullet = TakeBullet(ammoComponent, spawnAt);
if (ammoComponent.Caseless)
{
component.AmmoSlots[component.CurrentSlot] = null;
component.AmmoContainer.Remove(ammo.Value);
}
}
CycleRevolver(component);
UpdateRevolverAppearance(component);
return bullet;
}
}

View File

@@ -1,181 +0,0 @@
using Content.Server.Hands.Components;
using Content.Server.Weapon.Ranged.Ammunition.Components;
using Content.Server.Weapon.Ranged.Barrels.Components;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Weapons.Ranged.Barrels.Components;
using Robust.Shared.Containers;
using Robust.Shared.Player;
namespace Content.Server.Weapon.Ranged;
public sealed partial class GunSystem
{
private void OnSpeedLoaderInit(EntityUid uid, SpeedLoaderComponent component, ComponentInit args)
{
component.AmmoContainer = uid.EnsureContainer<Container>($"{component.GetType()}-container", out var existing);
if (existing)
{
foreach (var ammo in component.AmmoContainer.ContainedEntities)
{
component.UnspawnedCount--;
component.SpawnedAmmo.Push(ammo);
}
}
}
private void OnSpeedLoaderMapInit(EntityUid uid, SpeedLoaderComponent component, MapInitEvent args)
{
component.UnspawnedCount += component.Capacity;
UpdateSpeedLoaderAppearance(component);
}
private void UpdateSpeedLoaderAppearance(SpeedLoaderComponent component)
{
if (!TryComp(component.Owner, out AppearanceComponent? appearanceComponent)) return;
appearanceComponent.SetData(MagazineBarrelVisuals.MagLoaded, true);
appearanceComponent.SetData(AmmoVisuals.AmmoCount, component.AmmoLeft);
appearanceComponent.SetData(AmmoVisuals.AmmoMax, component.Capacity);
}
private EntityUid? TakeAmmo(SpeedLoaderComponent component)
{
if (component.SpawnedAmmo.TryPop(out var entity))
{
component.AmmoContainer.Remove(entity);
return entity;
}
if (component.UnspawnedCount > 0)
{
component.UnspawnedCount--;
return EntityManager.SpawnEntity(component.FillPrototype, Transform(component.Owner).Coordinates);
}
return null;
}
private void OnSpeedLoaderUse(EntityUid uid, SpeedLoaderComponent component, UseInHandEvent args)
{
if (args.Handled) return;
if (!TryComp(args.User, out HandsComponent? handsComponent))
{
return;
}
var ammo = TakeAmmo(component);
if (ammo == null)
{
return;
}
if (!_handsSystem.TryPickup(args.User, ammo.Value, handsComp: handsComponent))
{
EjectCasing(ammo.Value);
}
UpdateSpeedLoaderAppearance(component);
args.Handled = true;
}
private void OnSpeedLoaderAfterInteract(EntityUid uid, SpeedLoaderComponent component, AfterInteractEvent args)
{
if (args.Handled || !args.CanReach) return;
if (args.Target == null)
{
return;
}
// This area is dirty but not sure of an easier way to do it besides add an interface or somethin
var changed = false;
if (TryComp(args.Target.Value, out RevolverBarrelComponent? revolverBarrel))
{
for (var i = 0; i < component.Capacity; i++)
{
var ammo = TakeAmmo(component);
if (ammo == null)
{
break;
}
if (TryInsertBullet(args.User, ammo.Value, revolverBarrel))
{
changed = true;
continue;
}
// Take the ammo back
TryInsertAmmo(args.User, ammo.Value, component);
break;
}
}
else if (TryComp(args.Target.Value, out BoltActionBarrelComponent? boltActionBarrel))
{
for (var i = 0; i < component.Capacity; i++)
{
var ammo = TakeAmmo(component);
if (ammo == null)
{
break;
}
if (TryInsertBullet(args.User, ammo.Value, boltActionBarrel))
{
changed = true;
continue;
}
// Take the ammo back
TryInsertAmmo(args.User, ammo.Value, component);
break;
}
}
if (changed)
{
UpdateSpeedLoaderAppearance(component);
}
args.Handled = true;
}
public bool TryInsertAmmo(EntityUid user, EntityUid entity, SpeedLoaderComponent component)
{
if (!TryComp(entity, out AmmoComponent? ammoComponent))
{
return false;
}
if (ammoComponent.Caliber != component.Caliber)
{
_popup.PopupEntity(Loc.GetString("speed-loader-component-try-insert-ammo-wrong-caliber"), component.Owner, Filter.Entities(user));
return false;
}
if (component.AmmoLeft >= component.Capacity)
{
_popup.PopupEntity(Loc.GetString("speed-loader-component-try-insert-ammo-no-room"), component.Owner, Filter.Entities(user));
return false;
}
component.SpawnedAmmo.Push(entity);
component.AmmoContainer.Insert(entity);
UpdateSpeedLoaderAppearance(component);
return true;
}
private void OnSpeedLoaderInteractUsing(EntityUid uid, SpeedLoaderComponent component, InteractUsingEvent args)
{
if (args.Handled) return;
if (TryInsertAmmo(args.User, args.Used, component))
args.Handled = true;
}
}

View File

@@ -1,258 +0,0 @@
using Content.Server.Administration.Logs;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Hands.Components;
using Content.Server.PowerCell;
using Content.Server.Stunnable;
using Content.Server.Weapon.Melee;
using Content.Server.Weapon.Ranged.Ammunition.Components;
using Content.Server.Weapon.Ranged.Barrels.Components;
using Content.Shared.ActionBlocker;
using Content.Shared.Camera;
using Content.Shared.Damage;
using Content.Shared.Examine;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Popups;
using Content.Shared.PowerCell.Components;
using Content.Shared.Verbs;
using Content.Shared.Weapons.Ranged.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.Weapon.Ranged;
public sealed partial class GunSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ActionBlockerSystem _blocker = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly AtmosphereSystem _atmos = default!;
[Dependency] private readonly CameraRecoilSystem _recoil = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly EffectSystem _effects = default!;
[Dependency] private readonly PowerCellSystem _cell = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly StunSystem _stun = default!;
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
public const float DamagePitchVariation = MeleeWeaponSystem.DamagePitchVariation;
/// <summary>
/// How many sounds are allowed to be played on ejecting multiple casings.
/// </summary>
private const int EjectionSoundMax = 3;
public override void Initialize()
{
base.Initialize();
// TODO: So at the time I thought there might've been a need to keep magazines
// and ammo boxes separate.
// There isn't.
// They should be combined.
SubscribeLocalEvent<AmmoComponent, ExaminedEvent>(OnAmmoExamine);
SubscribeLocalEvent<AmmoBoxComponent, ComponentInit>(OnAmmoBoxInit);
SubscribeLocalEvent<AmmoBoxComponent, MapInitEvent>(OnAmmoBoxMapInit);
SubscribeLocalEvent<AmmoBoxComponent, ExaminedEvent>(OnAmmoBoxExamine);
SubscribeLocalEvent<AmmoBoxComponent, InteractUsingEvent>(OnAmmoBoxInteractUsing);
SubscribeLocalEvent<AmmoBoxComponent, UseInHandEvent>(OnAmmoBoxUse);
SubscribeLocalEvent<AmmoBoxComponent, InteractHandEvent>(OnAmmoBoxInteractHand);
SubscribeLocalEvent<AmmoBoxComponent, GetVerbsEvent<AlternativeVerb>>(OnAmmoBoxAltVerbs);
SubscribeLocalEvent<RangedMagazineComponent, ComponentInit>(OnRangedMagInit);
SubscribeLocalEvent<RangedMagazineComponent, MapInitEvent>(OnRangedMagMapInit);
SubscribeLocalEvent<RangedMagazineComponent, UseInHandEvent>(OnRangedMagUse);
SubscribeLocalEvent<RangedMagazineComponent, ExaminedEvent>(OnRangedMagExamine);
SubscribeLocalEvent<RangedMagazineComponent, InteractUsingEvent>(OnRangedMagInteractUsing);
// Whenever I get around to refactoring guns this is all going to change.
// Essentially the idea is
// You have GunComponent and ChamberedGunComponent (which is just guncomp + containerslot for chamber)
// GunComponent has a component for an ammo provider on it (e.g. battery) and asks it for ammo to shoot
// ALTERNATIVELY, it has a "MagazineAmmoProvider" that has its own containerslot that it can ask
// (All of these would be comp references so max you only ever have 2 components on the gun).
SubscribeLocalEvent<BatteryBarrelComponent, ComponentInit>(OnBatteryInit);
SubscribeLocalEvent<BatteryBarrelComponent, MapInitEvent>(OnBatteryMapInit);
SubscribeLocalEvent<BatteryBarrelComponent, PowerCellChangedEvent>(OnCellSlotUpdated);
SubscribeLocalEvent<BoltActionBarrelComponent, ComponentInit>(OnBoltInit);
SubscribeLocalEvent<BoltActionBarrelComponent, MapInitEvent>(OnBoltMapInit);
SubscribeLocalEvent<BoltActionBarrelComponent, GunFireAttemptEvent>(OnBoltFireAttempt);
SubscribeLocalEvent<BoltActionBarrelComponent, UseInHandEvent>(OnBoltUse);
SubscribeLocalEvent<BoltActionBarrelComponent, InteractUsingEvent>(OnBoltInteractUsing);
SubscribeLocalEvent<BoltActionBarrelComponent, ComponentGetState>(OnBoltGetState);
SubscribeLocalEvent<BoltActionBarrelComponent, ExaminedEvent>(OnBoltExamine);
SubscribeLocalEvent<BoltActionBarrelComponent, GetVerbsEvent<InteractionVerb>>(AddToggleBoltVerb);
SubscribeLocalEvent<MagazineBarrelComponent, ComponentInit>(OnMagazineInit);
SubscribeLocalEvent<MagazineBarrelComponent, MapInitEvent>(OnMagazineMapInit);
SubscribeLocalEvent<MagazineBarrelComponent, ExaminedEvent>(OnMagazineExamine);
SubscribeLocalEvent<MagazineBarrelComponent, UseInHandEvent>(OnMagazineUse);
SubscribeLocalEvent<MagazineBarrelComponent, InteractUsingEvent>(OnMagazineInteractUsing);
SubscribeLocalEvent<MagazineBarrelComponent, ComponentGetState>(OnMagazineGetState);
SubscribeLocalEvent<MagazineBarrelComponent, GetVerbsEvent<InteractionVerb>>(AddMagazineInteractionVerbs);
SubscribeLocalEvent<MagazineBarrelComponent, GetVerbsEvent<AlternativeVerb>>(AddEjectMagazineVerb);
SubscribeLocalEvent<PumpBarrelComponent, ComponentGetState>(OnPumpGetState);
SubscribeLocalEvent<PumpBarrelComponent, ComponentInit>(OnPumpInit);
SubscribeLocalEvent<PumpBarrelComponent, MapInitEvent>(OnPumpMapInit);
SubscribeLocalEvent<PumpBarrelComponent, ExaminedEvent>(OnPumpExamine);
SubscribeLocalEvent<PumpBarrelComponent, UseInHandEvent>(OnPumpUse);
SubscribeLocalEvent<PumpBarrelComponent, InteractUsingEvent>(OnPumpInteractUsing);
SubscribeLocalEvent<RevolverBarrelComponent, MapInitEvent>(OnRevolverMapInit);
SubscribeLocalEvent<RevolverBarrelComponent, UseInHandEvent>(OnRevolverUse);
SubscribeLocalEvent<RevolverBarrelComponent, InteractUsingEvent>(OnRevolverInteractUsing);
SubscribeLocalEvent<RevolverBarrelComponent, ComponentGetState>(OnRevolverGetState);
SubscribeLocalEvent<RevolverBarrelComponent, GetVerbsEvent<AlternativeVerb>>(AddSpinVerb);
SubscribeLocalEvent<SpeedLoaderComponent, ComponentInit>(OnSpeedLoaderInit);
SubscribeLocalEvent<SpeedLoaderComponent, MapInitEvent>(OnSpeedLoaderMapInit);
SubscribeLocalEvent<SpeedLoaderComponent, UseInHandEvent>(OnSpeedLoaderUse);
SubscribeLocalEvent<SpeedLoaderComponent, AfterInteractEvent>(OnSpeedLoaderAfterInteract);
SubscribeLocalEvent<SpeedLoaderComponent, InteractUsingEvent>(OnSpeedLoaderInteractUsing);
// SubscribeLocalEvent<ServerRangedWeaponComponent, ExaminedEvent>(OnGunExamine);
SubscribeNetworkEvent<FirePosEvent>(OnFirePos);
SubscribeLocalEvent<ServerRangedWeaponComponent, MeleeAttackAttemptEvent>(OnMeleeAttempt);
}
private void OnFirePos(FirePosEvent msg, EntitySessionEventArgs args)
{
if (args.SenderSession.AttachedEntity is not {Valid: true} user)
return;
if (!msg.Coordinates.IsValid(EntityManager))
return;
if (!TryComp(user, out HandsComponent? handsComponent))
return;
// TODO: Not exactly robust
var gun = handsComponent.ActiveHand?.HeldEntity;
if (gun == null || !TryComp(gun, out ServerRangedWeaponComponent? weapon))
return;
// map pos
TryFire(user, msg.Coordinates, weapon);
}
public EntityUid? PeekAtAmmo(ServerRangedBarrelComponent component)
{
return component switch
{
BatteryBarrelComponent battery => PeekAmmo(battery),
BoltActionBarrelComponent bolt => PeekAmmo(bolt),
MagazineBarrelComponent mag => PeekAmmo(mag),
PumpBarrelComponent pump => PeekAmmo(pump),
RevolverBarrelComponent revolver => PeekAmmo(revolver),
_ => throw new NotImplementedException()
};
}
public EntityUid? TakeOutProjectile(ServerRangedBarrelComponent component, EntityCoordinates spawnAt)
{
return component switch
{
BatteryBarrelComponent battery => TakeProjectile(battery, spawnAt),
BoltActionBarrelComponent bolt => TakeProjectile(bolt, spawnAt),
MagazineBarrelComponent mag => TakeProjectile(mag, spawnAt),
PumpBarrelComponent pump => TakeProjectile(pump, spawnAt),
RevolverBarrelComponent revolver => TakeProjectile(revolver, spawnAt),
_ => throw new NotImplementedException()
};
}
/// <summary>
/// Drops multiple cartridges / shells on the floor
/// Wraps EjectCasing to make it less toxic for bulk ejections
/// </summary>
public void EjectCasings(IEnumerable<EntityUid> entities)
{
var soundPlayCount = 0;
var playSound = true;
foreach (var entity in entities)
{
EjectCasing(entity, playSound);
soundPlayCount++;
if (soundPlayCount > EjectionSoundMax)
{
playSound = false;
}
}
}
/// <summary>
/// Drops a single cartridge / shell
/// </summary>
public void EjectCasing(
EntityUid entity,
bool playSound = true,
AmmoComponent? ammoComponent = null)
{
const float ejectOffset = 0.4f;
if (!Resolve(entity, ref ammoComponent)) return;
var offsetPos = (_random.NextFloat(-ejectOffset, ejectOffset), _random.NextFloat(-ejectOffset, ejectOffset));
var xform = Transform(entity);
var coordinates = xform.Coordinates;
coordinates = coordinates.Offset(offsetPos);
xform.LocalRotation = _random.NextFloat(MathF.Tau);
xform.Coordinates = coordinates;
if (playSound)
SoundSystem.Play(Filter.Pvs(entity), ammoComponent.SoundCollectionEject.GetSound(), coordinates, AudioParams.Default.WithVolume(-1));
}
private Angle GetRecoilAngle(ServerRangedBarrelComponent component, Angle direction)
{
var currentTime = _gameTiming.CurTime;
var timeSinceLastFire = (currentTime - component.LastFire).TotalSeconds;
var newTheta = MathHelper.Clamp(component.CurrentAngle.Theta + component.AngleIncrease - component.AngleDecay * timeSinceLastFire, component.MinAngle.Theta, component.MaxAngle.Theta);
component.CurrentAngle = new Angle(newTheta);
var random = (_random.NextDouble(-1, 1));
var angle = Angle.FromDegrees(direction.Degrees + component.CurrentAngle.Degrees * random);
return angle;
}
/// <summary>
/// Raised on a gun when it fires.
/// </summary>
public sealed class GunShotEvent : EntityEventArgs
{
}
public sealed class GunFireAttemptEvent : CancellableEntityEventArgs
{
public EntityUid? User = null;
public ServerRangedWeaponComponent Weapon;
public GunFireAttemptEvent(EntityUid? user, ServerRangedWeaponComponent weapon)
{
User = user;
Weapon = weapon;
}
}
}

View File

@@ -1,22 +0,0 @@
using Content.Shared.Hands;
namespace Content.Server.Weapon.Ranged
{
public sealed class RangedWeaponSysten : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ServerRangedWeaponComponent, HandSelectedEvent>(OnHandSelected);
}
private void OnHandSelected(EntityUid uid, ServerRangedWeaponComponent component, HandSelectedEvent args)
{
// Instead of dirtying on hand-select this component should probably by dirtied whenever it needs to be.
// I take no responsibility for this code. It was like this when I got here.
Dirty(component);
}
}
}

View File

@@ -1,30 +0,0 @@
using Content.Shared.Sound;
using Content.Shared.Weapons.Ranged.Components;
namespace Content.Server.Weapon.Ranged
{
[RegisterComponent]
public sealed class ServerRangedWeaponComponent : SharedRangedWeaponComponent
{
public TimeSpan LastFireTime;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("clumsyCheck")]
public bool ClumsyCheck { get; set; } = true;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("clumsyExplodeChance")]
public float ClumsyExplodeChance { get; set; } = 0.5f;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("canHotspot")]
public bool CanHotspot = true;
[DataField("clumsyWeaponHandlingSound")]
public SoundSpecifier ClumsyWeaponHandlingSound = new SoundPathSpecifier("/Audio/Items/bikehorn.ogg");
[DataField("clumsyWeaponShotSound")]
public SoundSpecifier ClumsyWeaponShotSound = new SoundPathSpecifier("/Audio/Weapons/Guns/Gunshots/bang.ogg");
}
}

View File

@@ -1,14 +1,14 @@
using System.Linq;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.Weapon.Ranged.Ammunition.Components;
using Content.Server.Weapon.Ranged.Barrels.Components;
using Content.Server.Weapon.Ranged.Components;
using Content.Shared.Chemistry.Components;
using Content.Shared.Weapons.Ranged.Events;
namespace Content.Server.Weapon.Ranged
namespace Content.Server.Weapon.Ranged.Systems
{
public sealed class ChemicalAmmoSystem : EntitySystem
{
[Dependency] private SolutionContainerSystem _solutionSystem = default!;
[Dependency] private readonly SolutionContainerSystem _solutionSystem = default!;
public override void Initialize()
{

View File

@@ -0,0 +1,5 @@
using Content.Shared.Weapons.Ranged.Systems;
namespace Content.Server.Weapon.Ranged.Systems;
public sealed class FlyBySoundSystem : SharedFlyBySoundSystem {}

View File

@@ -0,0 +1,31 @@
using Content.Shared.Weapons.Ranged.Components;
using Robust.Shared.Map;
namespace Content.Server.Weapon.Ranged.Systems;
public sealed partial class GunSystem
{
protected override void Cycle(BallisticAmmoProviderComponent component, MapCoordinates coordinates)
{
EntityUid? ent = null;
// TODO: Combine with TakeAmmo
if (component.Entities.Count > 0)
{
var existing = component.Entities[^1];
component.Entities.RemoveAt(component.Entities.Count - 1);
component.Container.Remove(existing);
EnsureComp<AmmoComponent>(existing);
}
else if (component.UnspawnedCount > 0)
{
component.UnspawnedCount--;
ent = Spawn(component.FillProto, coordinates);
EnsureComp<AmmoComponent>(ent.Value);
}
if (ent != null)
EjectCartridge(ent.Value);
}
}

View File

@@ -0,0 +1,59 @@
using Content.Server.Power.Components;
using Content.Shared.Weapons.Ranged.Components;
namespace Content.Server.Weapon.Ranged.Systems;
public sealed partial class GunSystem
{
protected override void InitializeBattery()
{
base.InitializeBattery();
// Hitscan
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, ComponentStartup>(OnBatteryStartup);
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, ChargeChangedEvent>(OnBatteryChargeChange);
// Projectile
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, ComponentStartup>(OnBatteryStartup);
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, ChargeChangedEvent>(OnBatteryChargeChange);
}
private void OnBatteryStartup(EntityUid uid, BatteryAmmoProviderComponent component, ComponentStartup args)
{
UpdateShots(uid, component);
}
private void OnBatteryChargeChange(EntityUid uid, BatteryAmmoProviderComponent component, ChargeChangedEvent args)
{
UpdateShots(uid, component);
}
private void UpdateShots(EntityUid uid, BatteryAmmoProviderComponent component)
{
if (!TryComp<BatteryComponent>(uid, out var battery)) return;
UpdateShots(component, battery);
}
private void UpdateShots(BatteryAmmoProviderComponent component, BatteryComponent battery)
{
var shots = (int) (battery.CurrentCharge / component.FireCost);
var maxShots = (int) (battery.MaxCharge / component.FireCost);
if (component.Shots != shots || component.Capacity != maxShots)
{
Dirty(component);
}
component.Shots = shots;
component.Capacity = maxShots;
UpdateBatteryAppearance(component.Owner, component);
}
protected override void TakeCharge(EntityUid uid, BatteryAmmoProviderComponent component)
{
if (!TryComp<BatteryComponent>(uid, out var battery)) return;
battery.CurrentCharge -= component.FireCost;
UpdateShots(component, battery);
}
}

View File

@@ -0,0 +1,17 @@
using Content.Shared.Weapons.Ranged.Components;
namespace Content.Server.Weapon.Ranged.Systems;
public sealed partial class GunSystem
{
protected override void SpinRevolver(RevolverAmmoProviderComponent component, EntityUid? user = null)
{
base.SpinRevolver(component, user);
var index = Random.Next(component.Capacity);
if (component.CurrentIndex == index) return;
component.CurrentIndex = index;
Dirty(component);
}
}

View File

@@ -0,0 +1,298 @@
using System.Linq;
using Content.Server.Projectiles.Components;
using Content.Server.Weapon.Melee;
using Content.Server.Weapon.Ranged.Components;
using Content.Shared.Audio;
using Content.Shared.Damage;
using Content.Shared.Database;
using Content.Shared.Sound;
using Content.Shared.Throwing;
using Content.Shared.Weapons.Ranged;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Utility;
using SharedGunSystem = Content.Shared.Weapons.Ranged.Systems.SharedGunSystem;
namespace Content.Server.Weapon.Ranged.Systems;
public sealed partial class GunSystem : SharedGunSystem
{
[Dependency] private readonly EffectSystem _effects = default!;
public const float DamagePitchVariation = MeleeWeaponSystem.DamagePitchVariation;
public override void Shoot(GunComponent gun, List<IShootable> ammo, EntityCoordinates fromCoordinates, EntityCoordinates toCoordinates, EntityUid? user = null)
{
var fromMap = fromCoordinates.ToMap(EntityManager);
var toMap = toCoordinates.ToMapPos(EntityManager);
var mapDirection = toMap - fromMap.Position;
var mapAngle = mapDirection.ToAngle();
var angle = GetRecoilAngle(Timing.CurTime, gun, mapDirection.ToAngle());
// Update shot based on the recoil
toMap = fromMap.Position + angle.ToVec() * mapDirection.Length;
mapDirection = toMap - fromMap.Position;
var entityDirection = Transform(fromCoordinates.EntityId).InvWorldMatrix.Transform(toMap) - fromCoordinates.Position;
// I must be high because this was getting tripped even when true.
// DebugTools.Assert(direction != Vector2.Zero);
var shotProjectiles = new List<EntityUid>(ammo.Count);
foreach (var shootable in ammo)
{
switch (shootable)
{
// Cartridge shoots something else
case CartridgeAmmoComponent cartridge:
if (!cartridge.Spent)
{
if (cartridge.Count > 1)
{
var angles = LinearSpread(mapAngle - Angle.FromDegrees(cartridge.Spread / 2f),
mapAngle + Angle.FromDegrees(cartridge.Spread / 2f), cartridge.Count);
for (var i = 0; i < cartridge.Count; i++)
{
var uid = Spawn(cartridge.Prototype, fromCoordinates);
ShootProjectile(uid, angles[i].ToVec(), user);
shotProjectiles.Add(uid);
}
}
else
{
var uid = Spawn(cartridge.Prototype, fromCoordinates);
ShootProjectile(uid, mapDirection, user);
shotProjectiles.Add(uid);
}
SetCartridgeSpent(cartridge, true);
MuzzleFlash(gun.Owner, cartridge, user);
if (cartridge.DeleteOnSpawn)
Del(cartridge.Owner);
}
else
{
PlaySound(gun.Owner, gun.SoundEmpty?.GetSound(Random, ProtoManager), user);
}
// Something like ballistic might want to leave it in the container still
if (!cartridge.DeleteOnSpawn && !Containers.IsEntityInContainer(cartridge.Owner))
EjectCartridge(cartridge.Owner);
Dirty(cartridge);
break;
// Ammo shoots itself
case AmmoComponent newAmmo:
shotProjectiles.Add(newAmmo.Owner);
MuzzleFlash(gun.Owner, newAmmo, user);
// Do a throw
if (!HasComp<ProjectileComponent>(newAmmo.Owner))
{
RemComp<AmmoComponent>(newAmmo.Owner);
// TODO: Someone can probably yeet this a billion miles so need to pre-validate input somewhere up the call stack.
ThrowingSystem.TryThrow(newAmmo.Owner, mapDirection, 20f, user);
break;
}
ShootProjectile(newAmmo.Owner, mapDirection, user);
break;
case HitscanPrototype hitscan:
var ray = new CollisionRay(fromMap.Position, mapDirection.Normalized, hitscan.CollisionMask);
var rayCastResults = Physics.IntersectRay(fromMap.MapId, ray, hitscan.MaxLength, user, false).ToList();
if (rayCastResults.Count >= 1)
{
var result = rayCastResults[0];
var distance = result.Distance;
FireEffects(fromCoordinates, distance, entityDirection.ToAngle(), hitscan, result.HitEntity);
var dmg = hitscan.Damage;
if (dmg != null)
dmg = Damageable.TryChangeDamage(result.HitEntity, dmg);
if (dmg != null)
{
if (user != null)
{
Logs.Add(LogType.HitScanHit,
$"{ToPrettyString(user.Value):user} hit {ToPrettyString(result.HitEntity):target} using hitscan and dealt {dmg.Total:damage} damage");
}
else
{
Logs.Add(LogType.HitScanHit,
$"Hit {ToPrettyString(result.HitEntity):target} using hitscan and dealt {dmg.Total:damage} damage");
}
}
}
else
{
FireEffects(fromCoordinates, hitscan.MaxLength, entityDirection.ToAngle(), hitscan);
}
break;
default:
throw new ArgumentOutOfRangeException();
}
}
RaiseLocalEvent(gun.Owner, new AmmoShotEvent()
{
FiredProjectiles = shotProjectiles,
}, false);
}
private void ShootProjectile(EntityUid uid, Vector2 direction, EntityUid? user = null)
{
var physics = EnsureComp<PhysicsComponent>(uid);
physics.BodyStatus = BodyStatus.InAir;
physics.LinearVelocity = direction.Normalized * 20f;
if (user != null)
{
var projectile = EnsureComp<ProjectileComponent>(uid);
projectile.IgnoreEntity(user.Value);
}
Transform(uid).WorldRotation = direction.ToWorldAngle();
}
/// <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;
}
private Angle GetRecoilAngle(TimeSpan curTime, GunComponent component, Angle direction)
{
var timeSinceLastFire = (curTime - component.LastFire).TotalSeconds;
var newTheta = MathHelper.Clamp(component.CurrentAngle.Theta + component.AngleIncrease.Theta - component.AngleDecay.Theta * timeSinceLastFire, component.MinAngle.Theta, component.MaxAngle.Theta);
component.CurrentAngle = new Angle(newTheta);
component.LastFire = component.NextFire;
// Convert it so angle can go either side.
var random = Random.NextGaussian(0, 0.5);
var angle = new Angle(direction.Theta + component.CurrentAngle.Theta * random);
return angle;
}
protected override void PlaySound(EntityUid gun, string? sound, EntityUid? user = null)
{
if (sound == null) return;
SoundSystem.Play(Filter.Pvs(gun, entityManager: EntityManager).RemoveWhereAttachedEntity(e => e == user), sound, gun);
}
protected override void Popup(string message, EntityUid? uid, EntityUid? user) {}
protected override void CreateEffect(EffectSystemMessage message, EntityUid? user = null)
{
// TODO: Fucking bad
if (TryComp<ActorComponent>(user, out var actor))
{
_effects.CreateParticle(message, actor.PlayerSession);
}
else
{
_effects.CreateParticle(message);
}
}
public void PlayImpactSound(EntityUid otherEntity, DamageSpecifier? modifiedDamage, SoundSpecifier? weaponSound, bool forceWeaponSound)
{
// Like projectiles and melee,
// 1. Entity specific sound
// 2. Ammo's sound
// 3. Nothing
var playedSound = false;
if (!forceWeaponSound && modifiedDamage != null && modifiedDamage.Total > 0 && TryComp<RangedDamageSoundComponent>(otherEntity, out var rangedSound))
{
var type = MeleeWeaponSystem.GetHighestDamageSound(modifiedDamage, ProtoManager);
if (type != null && rangedSound.SoundTypes?.TryGetValue(type, out var damageSoundType) == true)
{
SoundSystem.Play(
Filter.Pvs(otherEntity, entityManager: EntityManager),
damageSoundType!.GetSound(),
otherEntity,
AudioHelpers.WithVariation(DamagePitchVariation));
playedSound = true;
}
else if (type != null && rangedSound.SoundGroups?.TryGetValue(type, out var damageSoundGroup) == true)
{
SoundSystem.Play(
Filter.Pvs(otherEntity, entityManager: EntityManager),
damageSoundGroup!.GetSound(),
otherEntity,
AudioHelpers.WithVariation(DamagePitchVariation));
playedSound = true;
}
}
if (!playedSound && weaponSound != null)
SoundSystem.Play(Filter.Pvs(otherEntity, entityManager: EntityManager), weaponSound.GetSound(), otherEntity);
}
// TODO: Pseudo RNG so the client can predict these.
#region Hitscan effects
private void FireEffects(EntityCoordinates fromCoordinates, float distance, Angle angle, HitscanPrototype hitscan, EntityUid? hitEntity = null)
{
// Lord
// Forgive me for the shitcode I am about to do
// Effects tempt me not
var sprites = new List<(EntityCoordinates coordinates, Angle angle, SpriteSpecifier sprite, float scale)>();
// We'll get the effects relative to the grid / map of the firer
if (distance >= 1f)
{
if (hitscan.MuzzleFlash != null)
{
sprites.Add((fromCoordinates.Offset(angle.ToVec().Normalized / 2), angle, hitscan.MuzzleFlash, 1f));
}
if (hitscan.TravelFlash != null)
{
sprites.Add((fromCoordinates.Offset(angle.ToVec() * (distance + 0.5f) / 2), angle, hitscan.TravelFlash, distance - 1.5f));
}
}
if (hitscan.ImpactFlash != null)
{
sprites.Add((fromCoordinates.Offset(angle.ToVec() * distance), angle.FlipPositive(), hitscan.ImpactFlash, 1f));
}
if (sprites.Count > 0)
{
RaiseNetworkEvent(new HitscanEvent()
{
Sprites = sprites,
}, Filter.Pvs(fromCoordinates, entityMan: EntityManager));
}
}
#endregion
}

View File

@@ -1,6 +1,6 @@
using Content.Server.Ghost.Components;
using Content.Shared.Administration;
using Content.Shared.Weapons.Ranged;
using Content.Shared.Weapons.Ranged.Systems;
using Robust.Server.Console;
using Robust.Server.Player;
using Robust.Shared.Containers;
@@ -11,7 +11,7 @@ using Robust.Shared.Players;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.Weapon.Ranged;
namespace Content.Server.Weapon.Ranged.Systems;
public sealed class TetherGunSystem : SharedTetherGunSystem
{