mirror of
https://github.com/WWhiteDreamProject/wwdpublic.git
synced 2026-04-17 05:27:38 +03:00
# Description This is a manual cherry-pick of the following PRs: https://github.com/space-wizards/space-station-14/pull/27905 https://github.com/space-wizards/space-station-14/pull/28072 https://github.com/space-wizards/space-station-14/pull/28571 I REQUIRE these for my work in PR #11 , and cannot complete said PR without these cherry-picks. This set of PRs from Wizden adds a feature where entities can selectively opt-out of being shot at unless a player intentionally targets them, which I can use as a simple and elegant solution to one of the largest glaring issues for Segmented Entities. I could simply give Lamia segments the new RequireProjectileTargetComponent, which adds them to the system. Future segmented entities such as the hypothetical "Heretic Worm" may or may not use this feature, depending on their intended implementation. --------- Co-authored-by: Danger Revolution! <142105406+DangerRevolution@users.noreply.github.com>
575 lines
20 KiB
C#
575 lines
20 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.Item; // Delta-V: Felinids in duffelbags can't shoot.
|
|
using Content.Shared.Popups;
|
|
using Content.Shared.Projectiles;
|
|
using Content.Shared.Tag;
|
|
using Content.Shared.Throwing;
|
|
using Content.Shared.Timing;
|
|
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!;
|
|
[Dependency] private readonly UseDelaySystem _useDelay = 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";
|
|
public 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, MapInitEvent>(OnMapInit);
|
|
}
|
|
|
|
private void OnMapInit(Entity<GunComponent> gun, ref MapInitEvent args)
|
|
{
|
|
#if DEBUG
|
|
if (gun.Comp.NextFire > Timing.CurTime)
|
|
Log.Warning($"Initializing a map that contains an entity that is on cooldown. Entity: {ToPrettyString(gun)}");
|
|
|
|
DebugTools.Assert((gun.Comp.AvailableModes & gun.Comp.SelectedMode) != 0x0);
|
|
#endif
|
|
|
|
RefreshModifiers((gun, gun));
|
|
}
|
|
|
|
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 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) ||
|
|
HasComp<ItemComponent>(user)) // Delta-V: Felinids in duffelbags can't shoot.
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (ent != GetEntity(msg.Gun))
|
|
return;
|
|
|
|
gun.ShootCoordinates = GetCoordinates(msg.Coordinates);
|
|
Log.Debug($"Set shoot coordinates to {gun.ShootCoordinates}");
|
|
gun.Target = GetEntity(msg.Target);
|
|
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;
|
|
gun.Target = 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.FireRateModified <= 0f ||
|
|
!_actionBlockerSystem.CanAttack(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.FireRateModified);
|
|
|
|
// 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, gun.ShotsPerBurstModified - 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(uid, 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, bool prediction = true) {}
|
|
|
|
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 attemptEv = new GunMuzzleFlashAttemptEvent();
|
|
RaiseLocalEvent(gun, ref attemptEv);
|
|
if (attemptEv.Cancelled)
|
|
return;
|
|
|
|
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);
|
|
}
|
|
|
|
public void RefreshModifiers(Entity<GunComponent?> gun)
|
|
{
|
|
if (!Resolve(gun, ref gun.Comp))
|
|
return;
|
|
|
|
var comp = gun.Comp;
|
|
var ev = new GunRefreshModifiersEvent(
|
|
(gun, comp),
|
|
comp.SoundGunshot,
|
|
comp.CameraRecoilScalar,
|
|
comp.AngleIncrease,
|
|
comp.AngleDecay,
|
|
comp.MaxAngle,
|
|
comp.MinAngle,
|
|
comp.ShotsPerBurst,
|
|
comp.FireRate,
|
|
comp.ProjectileSpeed
|
|
);
|
|
|
|
RaiseLocalEvent(gun, ref ev);
|
|
|
|
comp.SoundGunshotModified = ev.SoundGunshot;
|
|
comp.CameraRecoilScalarModified = ev.CameraRecoilScalar;
|
|
comp.AngleIncreaseModified = ev.AngleIncrease;
|
|
comp.AngleDecayModified = ev.AngleDecay;
|
|
comp.MaxAngleModified = ev.MaxAngle;
|
|
comp.MinAngleModified = ev.MinAngle;
|
|
comp.ShotsPerBurstModified = ev.ShotsPerBurst;
|
|
comp.FireRateModified = ev.FireRate;
|
|
comp.ProjectileSpeedModified = ev.ProjectileSpeed;
|
|
|
|
Dirty(gun);
|
|
}
|
|
|
|
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,
|
|
}
|