using System;
using System.Collections.Generic;
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.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
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 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!;
///
/// How many sounds are allowed to be played on ejecting multiple casings.
///
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(OnAmmoExamine);
SubscribeLocalEvent(OnAmmoBoxInit);
SubscribeLocalEvent(OnAmmoBoxMapInit);
SubscribeLocalEvent(OnAmmoBoxExamine);
SubscribeLocalEvent(OnAmmoBoxInteractUsing);
SubscribeLocalEvent(OnAmmoBoxUse);
SubscribeLocalEvent(OnAmmoBoxInteractHand);
SubscribeLocalEvent>(OnAmmoBoxAltVerbs);
SubscribeLocalEvent(OnRangedMagInit);
SubscribeLocalEvent(OnRangedMagMapInit);
SubscribeLocalEvent(OnRangedMagUse);
SubscribeLocalEvent(OnRangedMagExamine);
SubscribeLocalEvent(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(OnBatteryInit);
SubscribeLocalEvent(OnBatteryMapInit);
SubscribeLocalEvent(OnCellSlotUpdated);
SubscribeLocalEvent(OnBoltInit);
SubscribeLocalEvent(OnBoltMapInit);
SubscribeLocalEvent(OnBoltFireAttempt);
SubscribeLocalEvent(OnBoltUse);
SubscribeLocalEvent(OnBoltInteractUsing);
SubscribeLocalEvent(OnBoltGetState);
SubscribeLocalEvent(OnBoltExamine);
SubscribeLocalEvent>(AddToggleBoltVerb);
SubscribeLocalEvent(OnMagazineInit);
SubscribeLocalEvent(OnMagazineMapInit);
SubscribeLocalEvent(OnMagazineExamine);
SubscribeLocalEvent(OnMagazineUse);
SubscribeLocalEvent(OnMagazineInteractUsing);
SubscribeLocalEvent(OnMagazineGetState);
SubscribeLocalEvent>(AddMagazineInteractionVerbs);
SubscribeLocalEvent>(AddEjectMagazineVerb);
SubscribeLocalEvent(OnPumpGetState);
SubscribeLocalEvent(OnPumpInit);
SubscribeLocalEvent(OnPumpMapInit);
SubscribeLocalEvent(OnPumpExamine);
SubscribeLocalEvent(OnPumpUse);
SubscribeLocalEvent(OnPumpInteractUsing);
SubscribeLocalEvent(OnRevolverMapInit);
SubscribeLocalEvent(OnRevolverUse);
SubscribeLocalEvent(OnRevolverInteractUsing);
SubscribeLocalEvent(OnRevolverGetState);
SubscribeLocalEvent>(AddSpinVerb);
SubscribeLocalEvent(OnSpeedLoaderInit);
SubscribeLocalEvent(OnSpeedLoaderMapInit);
SubscribeLocalEvent(OnSpeedLoaderUse);
SubscribeLocalEvent(OnSpeedLoaderAfterInteract);
SubscribeLocalEvent(OnSpeedLoaderInteractUsing);
// SubscribeLocalEvent(OnGunExamine);
SubscribeNetworkEvent(OnFirePos);
}
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()
};
}
///
/// Drops multiple cartridges / shells on the floor
/// Wraps EjectCasing to make it less toxic for bulk ejections
///
public void EjectCasings(IEnumerable entities)
{
var soundPlayCount = 0;
var playSound = true;
foreach (var entity in entities)
{
EjectCasing(entity, playSound);
soundPlayCount++;
if (soundPlayCount > EjectionSoundMax)
{
playSound = false;
}
}
}
///
/// Drops a single cartridge / shell
///
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;
}
///
/// Raised on a gun when it fires.
///
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;
}
}
}