* setup codebase * make auto-fire, but its broken * collider problem * fix rate, add toggle port * add laser * power cages * ginormous cells * fix inhand * add pirate cannon * salvage gun * functional Nuke cannon * rewrite to standart grenade * fix naming, add emp sprite * grenade cartridge * thruster fix * nuke cannon * audio + visual polish * balance tweak * tweaks * laser balance tweak: new Electronic damage modifier set, reduce structural cannon damage * resprite energy cages, start implementing in game * fix cage recharger craft * add small laser gun * fix colliders * add lasers and ptk to research and crafting * finish implementing weapon to rnd and sec * some fixes * splitted grenades and cannon balls * integrate new cannon balls * tweaks stick * move circuits to sectechfab, fix * fix? * add ability to E shoot, without signals * fix! * fix?!?! and naming tweak * go! * Lank fix * oh * mornings don't start with coffee. * the morning starts with bug fixes. * fucking bugs! * finally * it is now possible to craft projectiles separately from cartridges * +2 fix * refactor * piu * More weight * add AutoShootGunComponent * move autoshoot to partial * SetEnabled() * some fixes * remove CanShootWithoutUser field * remove null-checks ToCoordinates from AttemptShoot() * war without reason * return to home * ? * forgot remove it * review * Fix formatting and update path --------- Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
534 lines
19 KiB
C#
534 lines
19 KiB
C#
using System.Diagnostics.CodeAnalysis;
|
|
using System.Numerics;
|
|
using Content.Shared.ActionBlocker;
|
|
using Content.Shared.Actions;
|
|
using Content.Shared.Administration.Logs;
|
|
using Content.Shared.Audio;
|
|
using Content.Shared.CombatMode;
|
|
using Content.Shared.Containers.ItemSlots;
|
|
using Content.Shared.Damage;
|
|
using Content.Shared.Examine;
|
|
using Content.Shared.Gravity;
|
|
using Content.Shared.Hands;
|
|
using Content.Shared.Hands.Components;
|
|
using Content.Shared.Popups;
|
|
using Content.Shared.Projectiles;
|
|
using Content.Shared.Tag;
|
|
using Content.Shared.Throwing;
|
|
using Content.Shared.Verbs;
|
|
using Content.Shared.Weapons.Melee;
|
|
using Content.Shared.Weapons.Melee.Events;
|
|
using Content.Shared.Weapons.Ranged.Components;
|
|
using Content.Shared.Weapons.Ranged.Events;
|
|
using Robust.Shared.Audio;
|
|
using Robust.Shared.Audio.Systems;
|
|
using Robust.Shared.Containers;
|
|
using Robust.Shared.Map;
|
|
using Robust.Shared.Network;
|
|
using Robust.Shared.Physics.Components;
|
|
using Robust.Shared.Physics.Systems;
|
|
using Robust.Shared.Prototypes;
|
|
using Robust.Shared.Random;
|
|
using Robust.Shared.Serialization;
|
|
using Robust.Shared.Timing;
|
|
using Robust.Shared.Utility;
|
|
|
|
namespace Content.Shared.Weapons.Ranged.Systems;
|
|
|
|
public abstract partial class SharedGunSystem : EntitySystem
|
|
{
|
|
[Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
|
|
[Dependency] protected readonly IGameTiming Timing = default!;
|
|
[Dependency] protected readonly IMapManager MapManager = default!;
|
|
[Dependency] private readonly INetManager _netManager = default!;
|
|
[Dependency] protected readonly IPrototypeManager ProtoManager = default!;
|
|
[Dependency] protected readonly IRobustRandom Random = default!;
|
|
[Dependency] protected readonly ISharedAdminLogManager Logs = default!;
|
|
[Dependency] protected readonly DamageableSystem Damageable = default!;
|
|
[Dependency] protected readonly ExamineSystemShared Examine = default!;
|
|
[Dependency] private readonly ItemSlotsSystem _slots = default!;
|
|
[Dependency] private readonly RechargeBasicEntityAmmoSystem _recharge = default!;
|
|
[Dependency] protected readonly SharedActionsSystem Actions = default!;
|
|
[Dependency] protected readonly SharedAppearanceSystem Appearance = default!;
|
|
[Dependency] protected readonly SharedAudioSystem Audio = default!;
|
|
[Dependency] private readonly SharedCombatModeSystem _combatMode = default!;
|
|
[Dependency] protected readonly SharedContainerSystem Containers = default!;
|
|
[Dependency] private readonly SharedGravitySystem _gravity = default!;
|
|
[Dependency] protected readonly SharedPointLightSystem Lights = default!;
|
|
[Dependency] protected readonly SharedPopupSystem PopupSystem = default!;
|
|
[Dependency] protected readonly SharedPhysicsSystem Physics = default!;
|
|
[Dependency] protected readonly SharedProjectileSystem Projectiles = default!;
|
|
[Dependency] protected readonly SharedTransformSystem TransformSystem = default!;
|
|
[Dependency] protected readonly TagSystem TagSystem = default!;
|
|
[Dependency] protected readonly ThrowingSystem ThrowingSystem = default!;
|
|
|
|
private const float InteractNextFire = 0.3f;
|
|
private const double SafetyNextFire = 0.5;
|
|
private const float EjectOffset = 0.4f;
|
|
protected const string AmmoExamineColor = "yellow";
|
|
protected const string FireRateExamineColor = "yellow";
|
|
protected const string ModeExamineColor = "cyan";
|
|
|
|
public override void Initialize()
|
|
{
|
|
SubscribeAllEvent<RequestShootEvent>(OnShootRequest);
|
|
SubscribeAllEvent<RequestStopShootEvent>(OnStopShootRequest);
|
|
SubscribeLocalEvent<GunComponent, MeleeHitEvent>(OnGunMelee);
|
|
|
|
// Ammo providers
|
|
InitializeBallistic();
|
|
InitializeBattery();
|
|
InitializeCartridge();
|
|
InitializeChamberMagazine();
|
|
InitializeMagazine();
|
|
InitializeRevolver();
|
|
InitializeBasicEntity();
|
|
InitializeClothing();
|
|
InitializeContainer();
|
|
InitializeSolution();
|
|
|
|
// Interactions
|
|
SubscribeLocalEvent<GunComponent, GetVerbsEvent<AlternativeVerb>>(OnAltVerb);
|
|
SubscribeLocalEvent<GunComponent, ExaminedEvent>(OnExamine);
|
|
SubscribeLocalEvent<GunComponent, CycleModeEvent>(OnCycleMode);
|
|
SubscribeLocalEvent<GunComponent, HandSelectedEvent>(OnGunSelected);
|
|
SubscribeLocalEvent<GunComponent, EntityUnpausedEvent>(OnGunUnpaused);
|
|
|
|
#if DEBUG
|
|
SubscribeLocalEvent<GunComponent, MapInitEvent>(OnMapInit);
|
|
}
|
|
|
|
private void OnMapInit(EntityUid uid, GunComponent component, MapInitEvent args)
|
|
{
|
|
if (component.NextFire > Timing.CurTime)
|
|
Log.Warning($"Initializing a map that contains an entity that is on cooldown. Entity: {ToPrettyString(uid)}");
|
|
|
|
DebugTools.Assert((component.AvailableModes & component.SelectedMode) != 0x0);
|
|
#endif
|
|
}
|
|
|
|
private void OnGunMelee(EntityUid uid, GunComponent component, MeleeHitEvent args)
|
|
{
|
|
if (!TryComp<MeleeWeaponComponent>(uid, out var melee))
|
|
return;
|
|
|
|
if (melee.NextAttack > component.NextFire)
|
|
{
|
|
component.NextFire = melee.NextAttack;
|
|
Dirty(component);
|
|
}
|
|
}
|
|
|
|
private void OnGunUnpaused(EntityUid uid, GunComponent component, ref EntityUnpausedEvent args)
|
|
{
|
|
component.NextFire += args.PausedTime;
|
|
}
|
|
|
|
private void OnShootRequest(RequestShootEvent msg, EntitySessionEventArgs args)
|
|
{
|
|
var user = args.SenderSession.AttachedEntity;
|
|
|
|
if (user == null ||
|
|
!_combatMode.IsInCombatMode(user) ||
|
|
!TryGetGun(user.Value, out var ent, out var gun))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (ent != GetEntity(msg.Gun))
|
|
return;
|
|
|
|
gun.ShootCoordinates = GetCoordinates(msg.Coordinates);
|
|
Log.Debug($"Set shoot coordinates to {gun.ShootCoordinates}");
|
|
AttemptShoot(user.Value, ent, gun);
|
|
}
|
|
|
|
private void OnStopShootRequest(RequestStopShootEvent ev, EntitySessionEventArgs args)
|
|
{
|
|
var gunUid = GetEntity(ev.Gun);
|
|
|
|
if (args.SenderSession.AttachedEntity == null ||
|
|
!TryComp<GunComponent>(gunUid, out var gun) ||
|
|
!TryGetGun(args.SenderSession.AttachedEntity.Value, out _, out var userGun))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (userGun != gun)
|
|
return;
|
|
|
|
StopShooting(gunUid, gun);
|
|
}
|
|
|
|
public bool CanShoot(GunComponent component)
|
|
{
|
|
if (component.NextFire > Timing.CurTime)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
public bool TryGetGun(EntityUid entity, out EntityUid gunEntity, [NotNullWhen(true)] out GunComponent? gunComp)
|
|
{
|
|
gunEntity = default;
|
|
gunComp = null;
|
|
|
|
if (EntityManager.TryGetComponent(entity, out HandsComponent? hands) &&
|
|
hands.ActiveHandEntity is { } held &&
|
|
TryComp(held, out GunComponent? gun))
|
|
{
|
|
gunEntity = held;
|
|
gunComp = gun;
|
|
return true;
|
|
}
|
|
|
|
// Last resort is check if the entity itself is a gun.
|
|
if (TryComp(entity, out gun))
|
|
{
|
|
gunEntity = entity;
|
|
gunComp = gun;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void StopShooting(EntityUid uid, GunComponent gun)
|
|
{
|
|
if (gun.ShotCounter == 0)
|
|
return;
|
|
|
|
Log.Debug($"Stopped shooting {ToPrettyString(uid)}");
|
|
gun.ShotCounter = 0;
|
|
gun.ShootCoordinates = null;
|
|
Dirty(uid, gun);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to shoot at the target coordinates. Resets the shot counter after every shot.
|
|
/// </summary>
|
|
public void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun, EntityCoordinates toCoordinates)
|
|
{
|
|
gun.ShootCoordinates = toCoordinates;
|
|
AttemptShoot(user, gunUid, gun);
|
|
gun.ShotCounter = 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Shoots by assuming the gun is the user at default coordinates.
|
|
/// </summary>
|
|
public void AttemptShoot(EntityUid gunUid, GunComponent gun)
|
|
{
|
|
var coordinates = new EntityCoordinates(gunUid, new Vector2(0, -1));
|
|
gun.ShootCoordinates = coordinates;
|
|
AttemptShoot(gunUid, gunUid, gun);
|
|
gun.ShotCounter = 0;
|
|
}
|
|
|
|
private void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun)
|
|
{
|
|
if (gun.FireRate <= 0f ||
|
|
!_actionBlockerSystem.CanUseHeldEntity(user))
|
|
return;
|
|
|
|
var toCoordinates = gun.ShootCoordinates;
|
|
|
|
if (toCoordinates == null)
|
|
return;
|
|
|
|
var curTime = Timing.CurTime;
|
|
|
|
// check if anything wants to prevent shooting
|
|
var prevention = new ShotAttemptedEvent
|
|
{
|
|
User = user,
|
|
Used = gunUid
|
|
};
|
|
RaiseLocalEvent(gunUid, ref prevention);
|
|
if (prevention.Cancelled)
|
|
return;
|
|
|
|
RaiseLocalEvent(user, ref prevention);
|
|
if (prevention.Cancelled)
|
|
return;
|
|
|
|
// Need to do this to play the clicking sound for empty automatic weapons
|
|
// but not play anything for burst fire.
|
|
if (gun.NextFire > curTime)
|
|
return;
|
|
|
|
var fireRate = TimeSpan.FromSeconds(1f / gun.FireRate);
|
|
|
|
// First shot
|
|
// Previously we checked shotcounter but in some cases all the bullets got dumped at once
|
|
// curTime - fireRate is insufficient because if you time it just right you can get a 3rd shot out slightly quicker.
|
|
if (gun.NextFire < curTime - fireRate || gun.ShotCounter == 0 && gun.NextFire < curTime)
|
|
gun.NextFire = curTime;
|
|
|
|
var shots = 0;
|
|
var lastFire = gun.NextFire;
|
|
|
|
while (gun.NextFire <= curTime)
|
|
{
|
|
gun.NextFire += fireRate;
|
|
shots++;
|
|
}
|
|
|
|
// NextFire has been touched regardless so need to dirty the gun.
|
|
Dirty(gunUid, gun);
|
|
|
|
// Get how many shots we're actually allowed to make, due to clip size or otherwise.
|
|
// Don't do this in the loop so we still reset NextFire.
|
|
switch (gun.SelectedMode)
|
|
{
|
|
case SelectiveFire.SemiAuto:
|
|
shots = Math.Min(shots, 1 - gun.ShotCounter);
|
|
break;
|
|
case SelectiveFire.Burst:
|
|
shots = Math.Min(shots, 3 - gun.ShotCounter);
|
|
break;
|
|
case SelectiveFire.FullAuto:
|
|
break;
|
|
default:
|
|
throw new ArgumentOutOfRangeException($"No implemented shooting behavior for {gun.SelectedMode}!");
|
|
}
|
|
|
|
var attemptEv = new AttemptShootEvent(user, null);
|
|
RaiseLocalEvent(gunUid, ref attemptEv);
|
|
|
|
if (attemptEv.Cancelled)
|
|
{
|
|
if (attemptEv.Message != null)
|
|
{
|
|
PopupSystem.PopupClient(attemptEv.Message, gunUid, user);
|
|
}
|
|
|
|
gun.NextFire = TimeSpan.FromSeconds(Math.Max(lastFire.TotalSeconds + SafetyNextFire, gun.NextFire.TotalSeconds));
|
|
return;
|
|
}
|
|
|
|
var fromCoordinates = Transform(user).Coordinates;
|
|
// Remove ammo
|
|
var ev = new TakeAmmoEvent(shots, new List<(EntityUid? Entity, IShootable Shootable)>(), fromCoordinates, user);
|
|
|
|
// Listen it just makes the other code around it easier if shots == 0 to do this.
|
|
if (shots > 0)
|
|
RaiseLocalEvent(gunUid, ev);
|
|
|
|
DebugTools.Assert(ev.Ammo.Count <= shots);
|
|
DebugTools.Assert(shots >= 0);
|
|
UpdateAmmoCount(gunUid);
|
|
|
|
// Even if we don't actually shoot update the ShotCounter. This is to avoid spamming empty sounds
|
|
// where the gun may be SemiAuto or Burst.
|
|
gun.ShotCounter += shots;
|
|
|
|
if (ev.Ammo.Count <= 0)
|
|
{
|
|
// triggers effects on the gun if it's empty
|
|
var emptyGunShotEvent = new OnEmptyGunShotEvent();
|
|
RaiseLocalEvent(gunUid, ref emptyGunShotEvent);
|
|
|
|
// Play empty gun sounds if relevant
|
|
// If they're firing an existing clip then don't play anything.
|
|
if (shots > 0)
|
|
{
|
|
if (ev.Reason != null && Timing.IsFirstTimePredicted)
|
|
{
|
|
PopupSystem.PopupCursor(ev.Reason);
|
|
}
|
|
|
|
// Don't spam safety sounds at gun fire rate, play it at a reduced rate.
|
|
// May cause prediction issues? Needs more tweaking
|
|
gun.NextFire = TimeSpan.FromSeconds(Math.Max(lastFire.TotalSeconds + SafetyNextFire, gun.NextFire.TotalSeconds));
|
|
Audio.PlayPredicted(gun.SoundEmpty, gunUid, user);
|
|
return;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Shoot confirmed - sounds also played here in case it's invalid (e.g. cartridge already spent).
|
|
Shoot(gunUid, gun, ev.Ammo, fromCoordinates, toCoordinates.Value, out var userImpulse, user, throwItems: attemptEv.ThrowItems);
|
|
var shotEv = new GunShotEvent(user, ev.Ammo);
|
|
RaiseLocalEvent(gunUid, ref shotEv);
|
|
|
|
if (userImpulse && TryComp<PhysicsComponent>(user, out var userPhysics))
|
|
{
|
|
if (_gravity.IsWeightless(user, userPhysics))
|
|
CauseImpulse(fromCoordinates, toCoordinates.Value, user, userPhysics);
|
|
}
|
|
|
|
Dirty(gunUid, gun);
|
|
}
|
|
|
|
public void Shoot(
|
|
EntityUid gunUid,
|
|
GunComponent gun,
|
|
EntityUid ammo,
|
|
EntityCoordinates fromCoordinates,
|
|
EntityCoordinates toCoordinates,
|
|
out bool userImpulse,
|
|
EntityUid? user = null,
|
|
bool throwItems = false)
|
|
{
|
|
var shootable = EnsureShootable(ammo);
|
|
Shoot(gunUid, gun, new List<(EntityUid? Entity, IShootable Shootable)>(1) { (ammo, shootable) }, fromCoordinates, toCoordinates, out userImpulse, user, throwItems);
|
|
}
|
|
|
|
public abstract void Shoot(
|
|
EntityUid gunUid,
|
|
GunComponent gun,
|
|
List<(EntityUid? Entity, IShootable Shootable)> ammo,
|
|
EntityCoordinates fromCoordinates,
|
|
EntityCoordinates toCoordinates,
|
|
out bool userImpulse,
|
|
EntityUid? user = null,
|
|
bool throwItems = false);
|
|
|
|
public void ShootProjectile(EntityUid uid, Vector2 direction, Vector2 gunVelocity, EntityUid gunUid, EntityUid? user = null, float speed = 20f)
|
|
{
|
|
var physics = EnsureComp<PhysicsComponent>(uid);
|
|
Physics.SetBodyStatus(physics, BodyStatus.InAir);
|
|
|
|
var targetMapVelocity = gunVelocity + direction.Normalized() * speed;
|
|
var currentMapVelocity = Physics.GetMapLinearVelocity(uid, physics);
|
|
var finalLinear = physics.LinearVelocity + targetMapVelocity - currentMapVelocity;
|
|
Physics.SetLinearVelocity(uid, finalLinear, body: physics);
|
|
|
|
var projectile = EnsureComp<ProjectileComponent>(uid);
|
|
Projectiles.SetShooter(uid, projectile, user ?? gunUid);
|
|
projectile.Weapon = gunUid;
|
|
|
|
TransformSystem.SetWorldRotation(uid, direction.ToWorldAngle());
|
|
}
|
|
|
|
protected abstract void Popup(string message, EntityUid? uid, EntityUid? user);
|
|
|
|
/// <summary>
|
|
/// Call this whenever the ammo count for a gun changes.
|
|
/// </summary>
|
|
protected virtual void UpdateAmmoCount(EntityUid uid) {}
|
|
|
|
protected void SetCartridgeSpent(EntityUid uid, CartridgeAmmoComponent cartridge, bool spent)
|
|
{
|
|
if (cartridge.Spent != spent)
|
|
Dirty(uid, cartridge);
|
|
|
|
cartridge.Spent = spent;
|
|
Appearance.SetData(uid, AmmoVisuals.Spent, spent);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Drops a single cartridge / shell
|
|
/// </summary>
|
|
protected void EjectCartridge(
|
|
EntityUid entity,
|
|
Angle? angle = null,
|
|
bool playSound = true)
|
|
{
|
|
// TODO: Sound limit version.
|
|
var offsetPos = Random.NextVector2(EjectOffset);
|
|
var xform = Transform(entity);
|
|
|
|
var coordinates = xform.Coordinates;
|
|
coordinates = coordinates.Offset(offsetPos);
|
|
|
|
TransformSystem.SetLocalRotation(xform, Random.NextAngle());
|
|
TransformSystem.SetCoordinates(entity, xform, coordinates);
|
|
|
|
// decides direction the casing ejects and only when not cycling
|
|
if (angle != null)
|
|
{
|
|
Angle ejectAngle = angle.Value;
|
|
ejectAngle += 3.7f; // 212 degrees; casings should eject slightly to the right and behind of a gun
|
|
ThrowingSystem.TryThrow(entity, ejectAngle.ToVec().Normalized() / 100, 5f);
|
|
}
|
|
if (playSound && TryComp<CartridgeAmmoComponent>(entity, out var cartridge))
|
|
{
|
|
Audio.PlayPvs(cartridge.EjectSound, entity, AudioParams.Default.WithVariation(SharedContentAudioSystem.DefaultVariation).WithVolume(-1f));
|
|
}
|
|
}
|
|
|
|
protected IShootable EnsureShootable(EntityUid uid)
|
|
{
|
|
if (TryComp<CartridgeAmmoComponent>(uid, out var cartridge))
|
|
return cartridge;
|
|
|
|
return EnsureComp<AmmoComponent>(uid);
|
|
}
|
|
|
|
protected void RemoveShootable(EntityUid uid)
|
|
{
|
|
RemCompDeferred<CartridgeAmmoComponent>(uid);
|
|
RemCompDeferred<AmmoComponent>(uid);
|
|
}
|
|
|
|
protected void MuzzleFlash(EntityUid gun, AmmoComponent component, EntityUid? user = null)
|
|
{
|
|
var sprite = component.MuzzleFlash;
|
|
|
|
if (sprite == null)
|
|
return;
|
|
|
|
var ev = new MuzzleFlashEvent(GetNetEntity(gun), sprite, user == gun);
|
|
CreateEffect(gun, ev, user);
|
|
}
|
|
|
|
public void CauseImpulse(EntityCoordinates fromCoordinates, EntityCoordinates toCoordinates, EntityUid user, PhysicsComponent userPhysics)
|
|
{
|
|
var fromMap = fromCoordinates.ToMapPos(EntityManager, TransformSystem);
|
|
var toMap = toCoordinates.ToMapPos(EntityManager, TransformSystem);
|
|
var shotDirection = (toMap - fromMap).Normalized();
|
|
|
|
const float impulseStrength = 25.0f;
|
|
var impulseVector = shotDirection * impulseStrength;
|
|
Physics.ApplyLinearImpulse(user, -impulseVector, body: userPhysics);
|
|
}
|
|
protected abstract void CreateEffect(EntityUid uid, MuzzleFlashEvent message, EntityUid? user = null);
|
|
|
|
/// <summary>
|
|
/// Used for animated effects on the client.
|
|
/// </summary>
|
|
[Serializable, NetSerializable]
|
|
public sealed class HitscanEvent : EntityEventArgs
|
|
{
|
|
public List<(NetCoordinates coordinates, Angle angle, SpriteSpecifier Sprite, float Distance)> Sprites = new();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Raised directed on the gun before firing to see if the shot should go through.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Handling this in server exclusively will lead to mispredicts.
|
|
/// </remarks>
|
|
/// <param name="User">The user that attempted to fire this gun.</param>
|
|
/// <param name="Cancelled">Set this to true if the shot should be cancelled.</param>
|
|
/// <param name="ThrowItems">Set this to true if the ammo shouldn't actually be fired, just thrown.</param>
|
|
[ByRefEvent]
|
|
public record struct AttemptShootEvent(EntityUid User, string? Message, bool Cancelled = false, bool ThrowItems = false);
|
|
|
|
/// <summary>
|
|
/// Raised directed on the gun after firing.
|
|
/// </summary>
|
|
/// <param name="User">The user that fired this gun.</param>
|
|
[ByRefEvent]
|
|
public record struct GunShotEvent(EntityUid User, List<(EntityUid? Uid, IShootable Shootable)> Ammo);
|
|
|
|
public enum EffectLayers : byte
|
|
{
|
|
Unshaded,
|
|
}
|
|
|
|
[Serializable, NetSerializable]
|
|
public enum AmmoVisuals : byte
|
|
{
|
|
Spent,
|
|
AmmoCount,
|
|
AmmoMax,
|
|
HasAmmo, // used for generic visualizers. c# stuff can just check ammocount != 0
|
|
MagLoaded,
|
|
BoltClosed,
|
|
}
|