| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062 |
- using System.Diagnostics.CodeAnalysis;
- using System.Linq;
- using System.Numerics;
- using Content.Shared._RMC14.CCVar;
- using Content.Shared._RMC14.Random;
- using Content.Shared._RMC14.Weapons.Ranged.Prediction;
- using Content.Shared.ActionBlocker;
- using Content.Shared.Actions;
- using Content.Shared.Administration.Logs;
- using Content.Shared.Audio;
- using Content.Shared.Camera;
- using Content.Shared.CombatMode;
- using Content.Shared.Containers.ItemSlots;
- using Content.Shared.Damage;
- using Content.Shared.Damage.Components;
- using Content.Shared.Damage.Systems;
- using Content.Shared.Database;
- using Content.Shared.Effects;
- using Content.Shared.Examine;
- using Content.Shared.Gravity;
- using Content.Shared.Hands;
- using Content.Shared.Hands.Components;
- using Content.Shared.Interaction;
- using Content.Shared.Interaction.Components;
- using Content.Shared.Popups;
- using Content.Shared.Projectiles;
- using Content.Shared.Stunnable;
- 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 Content.Shared.Weapons.Reflect;
- using Content.Shared.Whitelist;
- using Robust.Shared.Audio;
- using Robust.Shared.Audio.Systems;
- using Robust.Shared.Configuration;
- using Robust.Shared.Containers;
- using Robust.Shared.Map;
- using Robust.Shared.Network;
- using Robust.Shared.Physics;
- using Robust.Shared.Physics.Components;
- using Robust.Shared.Physics.Systems;
- using Robust.Shared.Player;
- 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!;
- [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
- [Dependency] private readonly StaminaSystem _stamina = default!;
- [Dependency] private readonly SharedStunSystem _stun = default!;
- [Dependency] private readonly SharedColorFlashEffectSystem _color = default!;
- [Dependency] private readonly SharedCameraRecoilSystem _recoil = default!;
- [Dependency] private readonly IConfigurationManager _config = 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 const float GunClumsyChance = 0.5f;
- private const float DamagePitchVariation = 0.05f;
- public bool GunPrediction { get; private set; }
- public override void Initialize()
- {
- 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);
- Subs.CVar(_config, RMCCVars.RMCGunPrediction, v => GunPrediction = v, true);
- }
- 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(uid, component);
- }
- }
- 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;
- 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 List<EntityUid>? AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun, List<int>? predictedProjectiles = null, ICommonSession? userSession = null)
- {
- if (gun.FireRateModified <= 0f ||
- !_actionBlockerSystem.CanAttack(user))
- return null;
- var toCoordinates = gun.ShootCoordinates;
- if (toCoordinates == null)
- return null;
- var curTime = Timing.CurTime;
- // check if anything wants to prevent shooting
- var prevention = new ShotAttemptedEvent
- {
- User = user,
- Used = (gunUid, gun)
- };
- RaiseLocalEvent(gunUid, ref prevention);
- if (prevention.Cancelled)
- return null;
- RaiseLocalEvent(user, ref prevention);
- if (prevention.Cancelled)
- return null;
- // 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 null;
- 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 null;
- }
- if (!Timing.IsFirstTimePredicted)
- return null;
- 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 null;
- }
- return null;
- }
- // Shoot confirmed - sounds also played here in case it's invalid (e.g. cartridge already spent).
- var projectiles = Shoot(gunUid, gun, ev.Ammo, fromCoordinates, toCoordinates.Value, out var userImpulse, user, throwItems: attemptEv.ThrowItems, predictedProjectiles, userSession);
- 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);
- return projectiles;
- }
- 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);
- }
- /// <summary>
- /// Fires one or more projectiles or hitscan shots from a gun using the provided ammo, handling recoil, effects, and impact logic.
- /// </summary>
- /// <param name="gunUid">The entity UID of the gun firing.</param>
- /// <param name="gun">The gun component associated with the firing gun.</param>
- /// <param name="ammo">A list of ammo entities and their shootable components to be fired.</param>
- /// <param name="fromCoordinates">The origin coordinates of the shot.</param>
- /// <param name="toCoordinates">The target coordinates of the shot.</param>
- /// <param name="userImpulse">Set to true if the user should receive recoil impulse; false if not.</param>
- /// <param name="user">The entity UID of the user firing the gun, if any.</param>
- /// <param name="throwItems">If true, items are thrown instead of shot as projectiles.</param>
- /// <param name="predictedProjectiles">Optional list of predicted projectile indices for client-side prediction.</param>
- /// <param name="userSession">Optional session of the user for prediction purposes.</param>
- /// <returns>A list of entity UIDs representing the fired projectiles, or null if none were fired.</returns>
- public List<EntityUid>? Shoot(
- EntityUid gunUid,
- GunComponent gun,
- List<(EntityUid? Entity, IShootable Shootable)> ammo,
- EntityCoordinates fromCoordinates,
- EntityCoordinates toCoordinates,
- out bool userImpulse,
- EntityUid? user = null,
- bool throwItems = false,
- List<int>? predictedProjectiles = null,
- ICommonSession? userSession = null)
- {
- userImpulse = true;
- var fromMap = fromCoordinates.ToMap(EntityManager, TransformSystem);
- var toMap = toCoordinates.ToMapPos(EntityManager, TransformSystem);
- var mapDirection = toMap - fromMap.Position;
- var mapAngle = mapDirection.ToAngle();
- var angle = GetRecoilAngle(Timing.CurTime, gun, mapDirection.ToAngle());
- // If applicable, this ensures the projectile is parented to grid on spawn, instead of the map.
- var fromEnt = MapManager.TryFindGridAt(fromMap, out var gridUid, out var grid)
- ? fromCoordinates.WithEntityId(gridUid, EntityManager)
- : new EntityCoordinates(MapManager.GetMapEntityId(fromMap.MapId), fromMap.Position);
- // Update shot based on the recoil
- toMap = fromMap.Position + angle.ToVec() * mapDirection.Length();
- mapDirection = toMap - fromMap.Position;
- var gunVelocity = Physics.GetMapLinearVelocity(fromEnt);
- // I must be high because this was getting tripped even when true.
- // DebugTools.Assert(direction != Vector2.Zero);
- var shotProjectiles = new List<EntityUid>(ammo.Count);
- void MarkPredicted(EntityUid projectile, int index)
- {
- if (!GunPrediction)
- return;
- if (predictedProjectiles == null || userSession == null)
- return;
- if (predictedProjectiles.TryGetValue(index, out var predicted))
- {
- var comp = new PredictedProjectileServerComponent
- {
- Shooter = userSession,
- ClientId = predicted,
- ClientEnt = user,
- };
- AddComp(projectile, comp, true);
- Dirty(projectile, comp);
- }
- }
- foreach (var (ent, shootable) in ammo)
- {
- // pneumatic cannon doesn't shoot bullets it just throws them, ignore ammo handling
- if (throwItems && ent != null)
- {
- Recoil(user, mapDirection, gun.CameraRecoilScalarModified);
- ShootOrThrow(ent.Value, mapDirection, gunVelocity, gun, gunUid, user);
- continue;
- }
- switch (shootable)
- {
- // Cartridge shoots something else
- case CartridgeAmmoComponent cartridge:
- if (!cartridge.Spent)
- {
- if (_netManager.IsServer || GunPrediction)
- {
- var uid = Spawn(cartridge.Prototype, fromEnt);
- shotProjectiles.Add(uid);
- CreateAndFireProjectiles(uid, cartridge);
- RaiseLocalEvent(ent!.Value, new AmmoShotEvent()
- {
- FiredProjectiles = shotProjectiles,
- });
- SetCartridgeSpent(ent.Value, cartridge, true);
- if (cartridge.DeleteOnSpawn &&
- (_netManager.IsServer || IsClientSide(ent.Value)))
- {
- Del(ent.Value);
- }
- }
- else
- {
- MuzzleFlash(gunUid, cartridge, mapDirection.ToAngle(), user);
- Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
- }
- Recoil(user, mapDirection, gun.CameraRecoilScalarModified);
- }
- else
- {
- userImpulse = false;
- Audio.PlayPredicted(gun.SoundEmpty, gunUid, user);
- }
- // Something like ballistic might want to leave it in the container still
- if (!cartridge.DeleteOnSpawn && !Containers.IsEntityInContainer(ent!.Value))
- EjectCartridge(ent.Value, angle);
- if (IsClientSide(ent!.Value))
- Del(ent.Value);
- else
- Dirty(ent!.Value, cartridge);
- break;
- // Ammo shoots itself
- case AmmoComponent newAmmo:
- if (_netManager.IsServer || GunPrediction)
- {
- shotProjectiles.Add(ent!.Value);
- CreateAndFireProjectiles(ent.Value, newAmmo);
- }
- else
- {
- MuzzleFlash(gunUid, newAmmo, mapDirection.ToAngle(), user);
- Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
- }
- Recoil(user, mapDirection, gun.CameraRecoilScalarModified);
- if (IsClientSide(ent!.Value))
- Del(ent.Value);
- else if (_netManager.IsClient)
- RemoveShootable(ent.Value);
- MarkPredicted(ent!.Value, 0);
- break;
- case HitscanPrototype hitscan:
- EntityUid? lastHit = null;
- var from = fromMap;
- // can't use map coords above because funny FireEffects
- var fromEffect = fromCoordinates;
- var dir = mapDirection.Normalized();
- //in the situation when user == null, means that the cannon fires on its own (via signals). And we need the gun to not fire by itself in this case
- var lastUser = user ?? gunUid;
- if (hitscan.Reflective != ReflectType.None)
- {
- for (var reflectAttempt = 0; reflectAttempt < 3; reflectAttempt++)
- {
- var ray = new CollisionRay(from.Position, dir, hitscan.CollisionMask);
- var rayCastResults =
- Physics.IntersectRay(from.MapId, ray, hitscan.MaxLength, lastUser, false).ToList();
- if (!rayCastResults.Any())
- break;
- var result = rayCastResults[0];
- // Check if laser is shot from in a container
- if (!Containers.IsEntityOrParentInContainer(lastUser))
- {
- // Checks if the laser should pass over unless targeted by its user
- foreach (var collide in rayCastResults)
- {
- if (collide.HitEntity != gun.Target &&
- CompOrNull<RequireProjectileTargetComponent>(collide.HitEntity)?.Active == true)
- {
- continue;
- }
- result = collide;
- break;
- }
- }
- var hit = result.HitEntity;
- lastHit = hit;
- FireEffects(fromEffect, result.Distance, dir.Normalized().ToAngle(), hitscan, hit);
- var ev = new HitScanReflectAttemptEvent(user, gunUid, hitscan.Reflective, dir, false);
- RaiseLocalEvent(hit, ref ev);
- if (!ev.Reflected)
- break;
- fromEffect = Transform(hit).Coordinates;
- from = fromEffect.ToMap(EntityManager, TransformSystem);
- dir = ev.Direction;
- lastUser = hit;
- }
- }
- if (lastHit != null)
- {
- var hitEntity = lastHit.Value;
- if (hitscan.StaminaDamage > 0f)
- _stamina.TakeStaminaDamage(hitEntity, hitscan.StaminaDamage, source: user, immediate: true);
- var dmg = hitscan.Damage;
- var hitName = ToPrettyString(hitEntity);
- if (dmg != null)
- dmg = Damageable.TryChangeDamage(hitEntity, dmg, origin: user);
- // check null again, as TryChangeDamage returns modified damage values
- if (dmg != null)
- {
- if (!Deleted(hitEntity))
- {
- if (dmg.AnyPositive())
- {
- _color.RaiseEffect(Color.Red, new List<EntityUid>() { hitEntity }, Filter.Pvs(hitEntity, entityManager: EntityManager));
- }
- // TODO get fallback position for playing hit sound.
- PlayImpactSound(hitEntity, dmg, hitscan.Sound, hitscan.ForceSound);
- }
- if (user != null)
- {
- Logs.Add(LogType.HitScanHit,
- $"{ToPrettyString(user.Value):user} hit {hitName:target} using hitscan and dealt {dmg.GetTotal():damage} damage");
- }
- else
- {
- Logs.Add(LogType.HitScanHit,
- $"{hitName:target} hit by hitscan dealing {dmg.GetTotal():damage} damage");
- }
- }
- }
- else
- {
- FireEffects(fromEffect, hitscan.MaxLength, dir.ToAngle(), hitscan);
- }
- Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
- Recoil(user, mapDirection, gun.CameraRecoilScalarModified);
- break;
- default:
- throw new ArgumentOutOfRangeException();
- }
- }
- RaiseLocalEvent(gunUid, new AmmoShotEvent()
- {
- FiredProjectiles = shotProjectiles,
- });
- void CreateAndFireProjectiles(EntityUid ammoEnt, AmmoComponent ammoComp)
- {
- predictedProjectiles ??= new List<int>();
- MarkPredicted(ammoEnt, 0);
- if (TryComp<ProjectileSpreadComponent>(ammoEnt, out var ammoSpreadComp))
- {
- var spreadEvent = new GunGetAmmoSpreadEvent(ammoSpreadComp.Spread);
- RaiseLocalEvent(gunUid, ref spreadEvent);
- var angles = LinearSpread(mapAngle - spreadEvent.Spread / 2,
- mapAngle + spreadEvent.Spread / 2, ammoSpreadComp.Count);
- ShootOrThrow(ammoEnt, angles[0].ToVec(), gunVelocity, gun, gunUid, user);
- shotProjectiles.Add(ammoEnt);
- for (var i = 1; i < ammoSpreadComp.Count; i++)
- {
- var newuid = Spawn(ammoSpreadComp.Proto, fromEnt);
- ShootOrThrow(newuid, angles[i].ToVec(), gunVelocity, gun, gunUid, user);
- shotProjectiles.Add(newuid);
- MarkPredicted(newuid, i + 1);
- }
- }
- else
- {
- ShootOrThrow(ammoEnt, mapDirection, gunVelocity, gun, gunUid, user);
- shotProjectiles.Add(ammoEnt);
- }
- MuzzleFlash(gunUid, ammoComp, mapDirection.ToAngle(), user);
- Audio.PlayPredicted(gun.SoundGunshotModified, gunUid, user);
- }
- return shotProjectiles;
- }
- private Angle GetRecoilAngle(TimeSpan curTime, GunComponent component, Angle direction)
- {
- var timeSinceLastFire = (curTime - component.LastFire).TotalSeconds;
- var newTheta = MathHelper.Clamp(component.CurrentAngle.Theta + component.AngleIncreaseModified.Theta - component.AngleDecayModified.Theta * timeSinceLastFire, component.MinAngleModified.Theta, component.MaxAngleModified.Theta);
- component.CurrentAngle = new Angle(newTheta);
- component.LastFire = component.NextFire;
- // Convert it so angle can go either side.
- long tick = Timing.CurTick.Value;
- tick = tick << 32;
- tick = tick | (uint)GetNetEntity(component.Owner).Id;
- Logger.Info(Timing.CurTick.ToString());
- var random = new Xoroshiro64S(tick).NextFloat(-0.5f, 0.5f);
- var spread = component.CurrentAngle.Theta * random;
- var angle = new Angle(direction.Theta + component.CurrentAngle.Theta * random);
- DebugTools.Assert(spread <= component.MaxAngleModified.Theta);
- return angle;
- }
- private void ShootOrThrow(EntityUid uid, Vector2 mapDirection, Vector2 gunVelocity, GunComponent gun, EntityUid gunUid, EntityUid? user)
- {
- if (gun.Target is { } target && !TerminatingOrDeleted(target))
- {
- var targeted = EnsureComp<TargetedProjectileComponent>(uid);
- targeted.Target = target;
- Dirty(uid, targeted);
- }
- // Do a throw
- if (!HasComp<ProjectileComponent>(uid))
- {
- RemoveShootable(uid);
- // TODO: Someone can probably yeet this a billion miles so need to pre-validate input somewhere up the call stack.
- ThrowingSystem.TryThrow(uid, mapDirection, gun.ProjectileSpeedModified, user);
- return;
- }
- ShootProjectile(uid, mapDirection, gunVelocity, gunUid, user, gun.ProjectileSpeedModified);
- }
- #region Hitscan effects
- private void FireEffects(EntityCoordinates fromCoordinates, float distance, Angle mapDirection, HitscanPrototype hitscan, EntityUid? hitEntity = null)
- {
- // Lord
- // Forgive me for the shitcode I am about to do
- // Effects tempt me not
- var sprites = new List<(NetCoordinates coordinates, Angle angle, SpriteSpecifier sprite, float scale)>();
- var gridUid = fromCoordinates.GetGridUid(EntityManager);
- var angle = mapDirection;
- // We'll get the effects relative to the grid / map of the firer
- // Look you could probably optimise this a bit with redundant transforms at this point.
- var xformQuery = GetEntityQuery<TransformComponent>();
- if (xformQuery.TryGetComponent(gridUid, out var gridXform))
- {
- var (_, gridRot, gridInvMatrix) = TransformSystem.GetWorldPositionRotationInvMatrix(gridXform, xformQuery);
- fromCoordinates = new EntityCoordinates(gridUid.Value,
- Vector2.Transform(fromCoordinates.ToMapPos(EntityManager, TransformSystem), gridInvMatrix));
- // Use the fallback angle I guess?
- angle -= gridRot;
- }
- if (distance >= 1f)
- {
- if (hitscan.MuzzleFlash != null)
- {
- var coords = fromCoordinates.Offset(angle.ToVec().Normalized() / 2);
- var netCoords = GetNetCoordinates(coords);
- sprites.Add((netCoords, angle, hitscan.MuzzleFlash, 1f));
- }
- if (hitscan.TravelFlash != null)
- {
- var coords = fromCoordinates.Offset(angle.ToVec() * (distance + 0.5f) / 2);
- var netCoords = GetNetCoordinates(coords);
- sprites.Add((netCoords, angle, hitscan.TravelFlash, distance - 1.5f));
- }
- }
- if (hitscan.ImpactFlash != null)
- {
- var coords = fromCoordinates.Offset(angle.ToVec() * distance);
- var netCoords = GetNetCoordinates(coords);
- sprites.Add((netCoords, angle.FlipPositive(), hitscan.ImpactFlash, 1f));
- }
- if (_netManager.IsServer && sprites.Count > 0)
- {
- RaiseNetworkEvent(new HitscanEvent
- {
- Sprites = sprites,
- }, Filter.Pvs(fromCoordinates, entityMan: EntityManager));
- }
- }
- #endregion
- /// <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;
- }
- public void PlayImpactSound(EntityUid otherEntity, DamageSpecifier? modifiedDamage, SoundSpecifier? weaponSound, bool forceWeaponSound, Filter? filter = null, EntityUid? projectile = null)
- {
- DebugTools.Assert(!Deleted(otherEntity), "Impact sound entity was deleted");
- // Like projectiles and melee,
- // 1. Entity specific sound
- // 2. Ammo's sound
- // 3. Nothing
- if (_netManager.IsClient && HasComp<PredictedProjectileServerComponent>(projectile))
- return;
- filter ??= Filter.Pvs(otherEntity);
- var playedSound = false;
- if (!forceWeaponSound && modifiedDamage != null && modifiedDamage.GetTotal() > 0 && TryComp<RangedDamageSoundComponent>(otherEntity, out var rangedSound))
- {
- var type = SharedMeleeWeaponSystem.GetHighestDamageSound(modifiedDamage, ProtoManager);
- if (type != null &&
- rangedSound.SoundTypes?.TryGetValue(type, out var damageSoundType) == true &&
- filter.Count > 0)
- {
- Audio.PlayEntity(damageSoundType, filter, otherEntity, true, AudioParams.Default.WithVariation(DamagePitchVariation));
- playedSound = true;
- }
- else if (type != null &&
- rangedSound.SoundGroups?.TryGetValue(type, out var damageSoundGroup) == true &&
- filter.Count > 0)
- {
- Audio.PlayEntity(damageSoundGroup, filter, otherEntity, true, AudioParams.Default.WithVariation(DamagePitchVariation));
- playedSound = true;
- }
- }
- if (!playedSound && weaponSound != null && filter.Count > 0)
- {
- Audio.PlayEntity(weaponSound, filter, otherEntity, true);
- }
- }
- private void Recoil(EntityUid? user, Vector2 recoil, float recoilScalar)
- {
- if (_netManager.IsServer)
- return;
- if (!Timing.IsFirstTimePredicted || user == null || recoil == Vector2.Zero || recoilScalar == 0)
- return;
- _recoil.KickCamera(user.Value, recoil.Normalized() * 0.5f * recoilScalar);
- }
- public virtual 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.SetWorldRotationNoLerp(uid, direction.ToWorldAngle() + projectile.Angle);
- }
- public List<EntityUid>? ShootRequested(NetEntity netGun, NetCoordinates coordinates, NetEntity? target, List<int>? projectiles, ICommonSession session)
- {
- var user = session.AttachedEntity;
- if (user == null ||
- !_combatMode.IsInCombatMode(user) ||
- !TryGetGun(user.Value, out var ent, out var gun))
- {
- return null;
- }
- if (ent != GetEntity(netGun))
- return null;
- gun.ShootCoordinates = GetCoordinates(coordinates);
- gun.Target = GetEntity(target);
- return AttemptShoot(user.Value, ent, gun, projectiles, session);
- }
- 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 virtual 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, Angle worldAngle, 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, worldAngle);
- CreateEffect(gun, ev, gun, 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 gunUid, MuzzleFlashEvent message, EntityUid? user = null, EntityUid? player = 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,
- }
|