mirror of
https://github.com/WWhiteDreamProject/wwdpublic.git
synced 2026-04-17 13:37:47 +03:00
# Description This PR is yet another step on an ongoing set of refactors for guns. This restores the functionality for weapons to operate as "Manually Cycled", IE: Bolt Action or Pump Action. I've also fixed some older bugs related to firearms, such as manually loading cartridges not updating the ammo display, and manually cycling the gun not ejecting the cartridge. And even further, fixing instances where guns would fail to correctly eject cartridges using ejection force variables set by the gun itself. There's also some other small balancing changes to other guns that make use of the DamageModifier datafield added in my previous rework. Namely the Cobra and Mosin both use this datafield now. The .25 cartridge has had its damage reduced to 15 per shot(from 19), so that it is properly smaller than the .35 cartridge. This is largely to address issues of the FPA-90 and R25 rifles being kinda overpowered in their damage output. To keep this nerf from affecting the Cobra however, the Cobra has picked up an innate damage modifier that restores it to the original damage output. The Mosin's been rebalanced around its hunting rifle powerhouse aesthetic, it has a beefy damage modifier that brings it up to 42 damage per shot. This is acting as a fun tradeoff for it having only a 5 round internal magazine, and being changed to a bolt action weapon. It's still insanely cheap, to a point a traitor or a head revolutionary can afford 40 of them. This 42 damage calculation is specifically set such that you can consistently drop "wound into crit" unarmored crew in 2 shots (84+ HEAVY bleed will drop them consistently), while shooting security requires the full 5 rounds. The general gist of what these bolt action guns is that they'll typically have better characteristics for "Single source damage" than their non-bolt action counterparts. While for Shotguns, we can now make shotguns follow various common shotgun tropes that players expect from video games, such as the "Super shotgun" that obliterates anything at pointblank, or the "Street sweeper" that trades some of its accuracy for extra room coverage. Or the "hunting shotgun" that beats all of them at midrange. <details><summary><h1>Media</h1></summary> <p> https://github.com/user-attachments/assets/00ae2b34-f3f2-4fd9-adad-7147507a6c31 </p> </details> # Changelog 🆑 - add: Added "Bolt Action" or "Pump Action" gun options. - add: Added new sound fx for shotshells when landing on the ground or colliding with objects after being thrown. - tweak: Kammerer and Enforcer are now pump action shotguns, featuring a choke that gives them 50% tighter spreads in return for needing to be pumped (press Z) after each shot. - tweak: Mosin Nagant is now a proper bolt action rifle. The bolt must be worked manually (press Z) after each shot. The fun tradeoff is that it now hits like a truck with 42 damage a shot. - tweak: Bulldog shotgun now has a wide choke, it fires 33% more projectiles per shotshell, while also having a 50% wider spread. Cover the station hallways in lead. Pairs nicely with birdshot if you really want to sweep rooms. - tweak: R25 and FPA-90 both now deal 15 damage per shot, instead of 19. - fix: Fixed guns not correctly ejecting cartridges(they were instead dropping them at your feet). - fix: Fixed guns not updating the ammo counter UI when loaded manually. (cherry picked from commit a86362b8ad712678f1316e2cd55f5e888736f718)
720 lines
26 KiB
C#
720 lines
26 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.Contests;
|
|
using Content.Shared.Damage;
|
|
using Content.Shared.Examine;
|
|
using Content.Shared.Gravity;
|
|
using Content.Shared.Hands;
|
|
using Content.Shared.Hands.Components;
|
|
using Content.Shared.Hands.EntitySystems;
|
|
using Content.Shared.Item;
|
|
using Content.Shared.Mech.Components; // Goobstation
|
|
using Content.Shared.MouseRotator;
|
|
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 Content.Shared.Whitelist;
|
|
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!;
|
|
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
|
|
[Dependency] private readonly ContestsSystem _contests = default!; // WWDP
|
|
|
|
private const float InteractNextFire = 0.3f;
|
|
private const double SafetyNextFire = 0.5;
|
|
private const float EjectOffset = 0.4f;
|
|
protected const string AmmoExamineColor = "yellow";
|
|
public const string ModeExamineColor = "crimson"; // WWDP examine
|
|
public const string ModeExamineBadColor = "pink"; // WWDP examine
|
|
|
|
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);
|
|
SubscribeLocalEvent<GunComponent, HolderMoveEvent>(OnHolderMove); // WWDP
|
|
}
|
|
|
|
// WWDP EDIT START
|
|
private void OnHolderMove(EntityUid uid, GunComponent comp, ref HolderMoveEvent _args)
|
|
{
|
|
if (Timing.ApplyingState)
|
|
return;
|
|
MoveEvent args = _args.Ev;
|
|
double posDiff = 0;
|
|
if (!args.ParentChanged)
|
|
posDiff = (args.OldPosition.Position - args.NewPosition.Position).Length();
|
|
double rotDiff = Math.Abs(Angle.ShortestDistance(args.NewRotation, args.OldRotation).Degrees);
|
|
|
|
UpdateBonusAngles(Timing.CurTime, comp, posDiff * comp.BonusAngleIncreaseMove + rotDiff * comp.BonusAngleIncreaseTurn);
|
|
Dirty(uid, comp);
|
|
}
|
|
|
|
/// <summary>
|
|
/// For "proper" recoil prediction
|
|
/// </summary>
|
|
protected void UpdateAngles(TimeSpan curTime, GunComponent component, double angleIncrease = 0)
|
|
{
|
|
var timeSinceLastFire = (curTime - component.CurrentAngleLastUpdate).TotalSeconds;
|
|
// Two clamps, because first we need to compute how much CurrentAngle has decreased since the last time we fired
|
|
// If we ignore the first clamp, CurrentAngle may "go" into negatives, making the first shot after a while have less or even no inaccuracy
|
|
var oldTheta = MathHelper.Clamp(component.CurrentAngle - component.AngleDecayModified * timeSinceLastFire, component.MinAngleModified, component.MaxAngleModified);
|
|
var newTheta = MathHelper.Clamp(oldTheta + angleIncrease, component.MinAngleModified, component.MaxAngleModified.Theta);
|
|
component.CurrentAngle = new Angle(newTheta);
|
|
component.CurrentAngleLastUpdate = curTime;
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// For "proper" recoil prediction
|
|
/// </summary>
|
|
/// <param name="curTime"></param>
|
|
/// <param name="component"></param>
|
|
protected void UpdateBonusAngles(TimeSpan curTime, GunComponent component, double angleIncrease = 0)
|
|
{
|
|
var timeSinceBonusUpdate = (curTime - component.BonusAngleLastUpdate).TotalSeconds;
|
|
component.BonusAngle = MathHelper.Clamp(component.BonusAngle + angleIncrease - component.BonusAngleDecayModified * timeSinceBonusUpdate, 0, component.MaxBonusAngleModified);
|
|
component.BonusAngleLastUpdate = curTime;
|
|
}
|
|
// WWDP EDIT END
|
|
|
|
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 = Timing.CurTime + TimeSpan.FromSeconds(1f / component.FireRateModified); // WWDP delay based on the gun not melee so its shorter
|
|
Dirty(uid, component);
|
|
}
|
|
}
|
|
|
|
private void OnShootRequest(RequestShootEvent msg, EntitySessionEventArgs args)
|
|
{
|
|
var user = args.SenderSession.AttachedEntity;
|
|
|
|
if (user == null ||
|
|
!_combatMode.IsInCombatMode(user))
|
|
return;
|
|
|
|
if (TryComp<MechPilotComponent>(user.Value, out var mechPilot))
|
|
user = mechPilot.Mech;
|
|
|
|
if (!TryGetGun(user.Value, out var ent, out var gun) ||
|
|
HasComp<ItemComponent>(user))
|
|
return;
|
|
|
|
if (ent != GetEntity(msg.Gun))
|
|
return;
|
|
|
|
gun.ShootCoordinates = GetCoordinates(msg.Coordinates);
|
|
gun.Target = GetEntity(msg.Target);
|
|
AttemptShoot(user.Value, ent, gun);
|
|
}
|
|
|
|
private void OnStopShootRequest(RequestStopShootEvent ev, EntitySessionEventArgs args)
|
|
{
|
|
var gunUid = GetEntity(ev.Gun);
|
|
|
|
var user = args.SenderSession.AttachedEntity;
|
|
|
|
if (user == null)
|
|
return;
|
|
|
|
if (TryComp<MechPilotComponent>(user.Value, out var mechPilot))
|
|
user = mechPilot.Mech;
|
|
|
|
if (!TryGetGun(user.Value, out var ent, out var gun))
|
|
return;
|
|
|
|
if (ent != gunUid)
|
|
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 (TryComp<MechComponent>(entity, out var mech) &&
|
|
mech.CurrentSelectedEquipment.HasValue &&
|
|
TryComp<GunComponent>(mech.CurrentSelectedEquipment.Value, out var mechGun))
|
|
{
|
|
gunEntity = mech.CurrentSelectedEquipment.Value;
|
|
gunComp = mechGun;
|
|
return true;
|
|
}
|
|
|
|
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, gun.DefaultDirection);
|
|
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, gun)
|
|
};
|
|
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);
|
|
|
|
if (gun.SelectedMode == SelectiveFire.Burst || gun.BurstActivated)
|
|
fireRate = TimeSpan.FromSeconds(1f / gun.BurstFireRate);
|
|
|
|
// 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.
|
|
if (!gun.BurstActivated)
|
|
{
|
|
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}!");
|
|
}
|
|
} else
|
|
shots = Math.Min(shots, gun.ShotsPerBurstModified - gun.ShotCounter);
|
|
|
|
var attemptEv = new AttemptShootEvent(user, null);
|
|
RaiseLocalEvent(gunUid, ref attemptEv);
|
|
|
|
if (attemptEv.Cancelled)
|
|
{
|
|
if (attemptEv.Message != null)
|
|
PopupSystem.PopupClient(attemptEv.Message, gunUid, user);
|
|
|
|
gun.BurstActivated = false;
|
|
gun.BurstShotsCount = 0;
|
|
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);
|
|
|
|
gun.BurstActivated = false;
|
|
gun.BurstShotsCount = 0;
|
|
gun.NextFire += TimeSpan.FromSeconds(gun.BurstCooldown);
|
|
|
|
// 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;
|
|
}
|
|
|
|
// Handle burstfire
|
|
if (gun.SelectedMode == SelectiveFire.Burst)
|
|
{
|
|
gun.BurstActivated = true;
|
|
}
|
|
if (gun.BurstActivated)
|
|
{
|
|
gun.BurstShotsCount += shots;
|
|
if (gun.BurstShotsCount >= gun.ShotsPerBurstModified)
|
|
{
|
|
gun.NextFire += TimeSpan.FromSeconds(gun.BurstCooldown);
|
|
gun.BurstActivated = false;
|
|
gun.BurstShotsCount = 0;
|
|
}
|
|
}
|
|
UpdateAngles(curTime, gun); // WWDP
|
|
// 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);
|
|
}
|
|
UpdateAngles(curTime, gun, gun.AngleIncreaseModified); // WWDP
|
|
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() + projectile.Angle);
|
|
}
|
|
|
|
/// <summary>
|
|
/// WWDP - Manually sets the targeted entity of the gun.
|
|
/// Used for NPCs
|
|
/// </summary>
|
|
public void SetTarget(GunComponent gun, EntityUid target)
|
|
{
|
|
gun.Target = target;
|
|
}
|
|
|
|
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,
|
|
GunComponent? gunComp = null)
|
|
{
|
|
var throwingForce = 0.01f;
|
|
var throwingSpeed = 5f;
|
|
var ejectAngleOffset = 3.7f;
|
|
if (gunComp is not null)
|
|
{
|
|
throwingForce = gunComp.EjectionForce;
|
|
throwingSpeed = gunComp.EjectionSpeed;
|
|
ejectAngleOffset = gunComp.EjectAngleOffset;
|
|
}
|
|
|
|
// TODO: Sound limit version.
|
|
var offsetPos = Random.NextVector2(EjectOffset);
|
|
var xform = Transform(entity);
|
|
|
|
var coordinates = xform.Coordinates;
|
|
coordinates = coordinates.Offset(offsetPos);
|
|
|
|
TransformSystem.SetLocalRotation(entity, Random.NextAngle(), xform);
|
|
TransformSystem.SetCoordinates(entity, xform, coordinates);
|
|
if (angle is null)
|
|
angle = Random.NextAngle();
|
|
|
|
Angle ejectAngle = angle.Value;
|
|
ejectAngle += ejectAngleOffset; // 212 degrees; casings should eject slightly to the right and behind of a gun
|
|
ThrowingSystem.TryThrow(entity, ejectAngle.ToVec().Normalized() * throwingForce, throwingSpeed);
|
|
|
|
if (playSound && TryComp(entity, out CartridgeAmmoComponent? 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, user);
|
|
}
|
|
|
|
public void CauseImpulse(EntityCoordinates fromCoordinates, EntityCoordinates toCoordinates, EntityUid user, PhysicsComponent userPhysics)
|
|
{
|
|
var fromMap = TransformSystem.ToMapCoordinates(fromCoordinates).Position;
|
|
var toMap = TransformSystem.ToMapCoordinates(toCoordinates).Position;
|
|
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.BonusAngleDecay, // WWDP EDIT
|
|
comp.MaxBonusAngle, // WWDP EDIT
|
|
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 = ClampAngle(ev.MaxAngle); // WWDP EDIT
|
|
comp.MinAngleModified = ClampAngle(ev.MinAngle); // WWDP EDIT
|
|
comp.BonusAngleDecayModified = ev.BonusAngleDecay; // WWDP
|
|
comp.MaxBonusAngleModified = ClampAngle(ev.MaxBonusAngle); // WWDP
|
|
comp.ShotsPerBurstModified = ev.ShotsPerBurst;
|
|
comp.FireRateModified = ev.FireRate;
|
|
comp.ProjectileSpeedModified = ev.ProjectileSpeed;
|
|
|
|
Dirty(gun);
|
|
|
|
Angle ClampAngle(Angle ang) => Math.Clamp(ang, 0, Math.Tau); // WWDP
|
|
}
|
|
|
|
// Goobstation
|
|
public void SetTarget(EntityUid projectile,
|
|
EntityUid? target,
|
|
out TargetedProjectileComponent targeted,
|
|
bool dirty = true)
|
|
{
|
|
targeted = EnsureComp<TargetedProjectileComponent>(projectile);
|
|
targeted.Target = target;
|
|
if (dirty)
|
|
Dirty(projectile, targeted);
|
|
}
|
|
|
|
public void SetFireRate(GunComponent component, float fireRate) => component.FireRate = fireRate;
|
|
|
|
public void SetUseKey(GunComponent component, bool useKey) => component.UseKey = useKey;
|
|
|
|
public void SetSoundGunshot(GunComponent component, SoundSpecifier? sound) => component.SoundGunshot = sound;
|
|
|
|
public void SetClumsyProof(GunComponent component, bool clumsyProof) => component.ClumsyProof = clumsyProof;
|
|
|
|
protected abstract void CreateEffect(EntityUid gunUid, 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,
|
|
}
|