254 lines
12 KiB
C#
254 lines
12 KiB
C#
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.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.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 IRobustRandom _random = default!;
|
|
[Dependency] private readonly ActionBlockerSystem _blocker = default!;
|
|
[Dependency] private readonly AdminLogSystem _logs = 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!;
|
|
|
|
/// <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;
|
|
}
|
|
}
|
|
}
|