Skip to content

Commit

Permalink
Pistol-Whipping (Guns as Melee Weapons) (#1335)
Browse files Browse the repository at this point in the history
# Description

Adds the ability to use guns as melee weapons and throwing weapons. 

The gun melee attack is a Light Attack done with a right click. The
attack rate is slower than average melee weapons.

The cooldown on melee attacks after shooting has been removed entirely,
so you can right-click immediately after shooting (like in hero shooters
😎). The cooldown on shooting after a melee attack has been set to a
constant 0.5 seconds.

## Balance

Technically speaking, weaving shooting and pistol-whipping lowers your
overall DPS in most cases, because you can't shoot for 0.5 seconds after
doing a melee attack. With _skillful_ usage, however, it provides some
key tactical advantages:
- Preserving ammo by dealing damage without firing a shot.
- Deal stamina damage as a natural effect of dealing melee Blunt damage.
- Most non-pistol guns have increased blunt stamina damage factors to
help with this.
- Bypassing Piercing resists of armors with a higher Piercing resist
than Blunt resist like plate carriers.
- Doing the combo of right-clicking immediately after shooting deals a
big burst of damage.

Pistol-whipping also helps as a last resort when you run out of ammo.
However, it's almost always better to use a proper melee weapon instead
of a gun as a pure melee weapon, because you can't power attack with
guns and the guns' melee attack rate are slower by design than most
melee weapons.

Shotguns benefit the most from pistol-whipping, because their ideal
range is close-range where a melee attack can be performed, and their
low fire rate means they're not affected too much by the 0.5s shooting
cooldown.

Guns have received throwing damage. You can throw guns at the enemy once
you're out of ammo to deal extra damage. I think this makes fights a
little more spectacular to watch.

Melee damage sorted by group (from least to greatest):

1. Revolver
2. Pistol (+ Energy Pistol)
3. Sniper rifle
4. Rifle  (+ Energy Rifle)
5. Sub Machine Gun
6. Shotgun
7. Light Machine Gun (L6 saw)
8. Heavy Machine Gun

## Media

**mk 58**


![image](https://github.com/user-attachments/assets/d17bc1c7-7ec5-4124-93c3-306026f7a23f)

**Kardashev-Mosin (Wielded)**


![image](https://github.com/user-attachments/assets/52132262-48ae-48fa-a72c-3df5ae6bfd17)

**Basic Combat**


https://github.com/user-attachments/assets/922998d1-0cd0-4fea-8f0b-365bcff3c12b

**Particle Decelerator Combo (80 damage)**


https://github.com/user-attachments/assets/ce62334a-13dd-46d9-9c0e-453e26bf1261

Combo: Shoot + Power Attack, wait 1.6s then Power Attack + Throw

This combo costs 90 stamina which almost depletes 100 stamina leaving
you vulnerable, so the Vigor trait can help you pull off this combo.

## Changelog

:cl: Skubman
- add: Pistol-whipping has been added. You can press right click with a
gun to perform a Light Attack. Most guns will deal Blunt damage, apart
from the Kardashev-Mosin dealing Piercing/Slash damage with its bayonet.
Weaving bullets and melee attacks correctly will give you the upper hand
in combat.
- add: Guns can now be thrown to deal the same damage as their melee
damage.
  • Loading branch information
angelofallars authored Dec 11, 2024
1 parent 149afb6 commit 5899f4e
Show file tree
Hide file tree
Showing 23 changed files with 528 additions and 78 deletions.
10 changes: 10 additions & 0 deletions Content.Client/Weapons/Melee/MeleeWeaponSystem.Effects.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ private Animation GetThrustAnimation(SpriteComponent sprite, float distance, Ang
{
const float thrustEnd = 0.05f;
const float length = 0.15f;
var rotation = sprite.Rotation + spriteRotation;
var startOffset = sprite.Rotation.RotateVec(new Vector2(0f, -distance / 5f));
var endOffset = sprite.Rotation.RotateVec(new Vector2(0f, -distance));

Expand All @@ -144,6 +145,15 @@ private Animation GetThrustAnimation(SpriteComponent sprite, float distance, Ang
Length = TimeSpan.FromSeconds(length),
AnimationTracks =
{
new AnimationTrackComponentProperty()
{
ComponentType = typeof(SpriteComponent),
Property = nameof(SpriteComponent.Rotation),
KeyFrames =
{
new AnimationTrackProperty.KeyFrame(rotation, 0f),
}
},
new AnimationTrackComponentProperty()
{
ComponentType = typeof(SpriteComponent),
Expand Down
37 changes: 21 additions & 16 deletions Content.Client/Weapons/Melee/MeleeWeaponSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Content.Shared.Weapons.Melee;
using Content.Shared.Weapons.Melee.Events;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Wieldable.Components;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Input;
Expand Down Expand Up @@ -71,34 +72,36 @@ public override void Update(float frameTime)
return;
}

var useDown = _inputSystem.CmdStates.GetState(EngineKeyFunctions.Use);
var altDown = _inputSystem.CmdStates.GetState(EngineKeyFunctions.UseSecondary);
var useDown = _inputSystem.CmdStates.GetState(EngineKeyFunctions.Use) == BoundKeyState.Down;
var altDown = _inputSystem.CmdStates.GetState(EngineKeyFunctions.UseSecondary) == BoundKeyState.Down;

if (weapon.AutoAttack || useDown != BoundKeyState.Down && altDown != BoundKeyState.Down)
// Disregard inputs to the shoot binding
if (TryComp<GunComponent>(weaponUid, out var gun) &&
// Except if can't shoot due to being unwielded
(!HasComp<GunRequiresWieldComponent>(weaponUid) ||
(TryComp<WieldableComponent>(weaponUid, out var wieldable) && wieldable.Wielded)))
{
if (gun.UseKey)
useDown = false;
else
altDown = false;
}

if (weapon.AutoAttack || !useDown && !altDown)
{
if (weapon.Attacking)
{
RaisePredictiveEvent(new StopAttackEvent(GetNetEntity(weaponUid)));
}
}

if (weapon.Attacking || weapon.NextAttack > Timing.CurTime)
if (weapon.Attacking || weapon.NextAttack > Timing.CurTime || (!useDown && !altDown))
{
return;
}

// TODO using targeted actions while combat mode is enabled should NOT trigger attacks.

// TODO: Need to make alt-fire melee its own component I guess?
// Melee and guns share a lot in the middle but share virtually nothing at the start and end so
// it's kinda tricky.
// I think as long as we make secondaries their own component it's probably fine
// as long as guncomp has an alt-use key then it shouldn't be too much of a PITA to deal with.
if (TryComp<GunComponent>(weaponUid, out var gun) && gun.UseKey)
{
return;
}

var mousePos = _eyeManager.PixelToMap(_inputManager.MouseScreenPosition);

if (mousePos.MapId == MapId.Nullspace)
Expand All @@ -118,7 +121,8 @@ public override void Update(float frameTime)
}

// Heavy attack.
if (altDown == BoundKeyState.Down)
if (!weapon.DisableHeavy &&
(!weapon.SwapKeys ? altDown : useDown))
{
// If it's an unarmed attack then do a disarm
if (weapon.AltDisarm && weaponUid == entity)
Expand All @@ -139,7 +143,8 @@ public override void Update(float frameTime)
}

// Light attack
if (useDown == BoundKeyState.Down)
if (!weapon.DisableClick &&
(!weapon.SwapKeys ? useDown : altDown))
{
var attackerPos = Transform(entity).MapPosition;

Expand Down
25 changes: 14 additions & 11 deletions Content.Server/Weapons/Melee/MeleeWeaponSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,22 +53,25 @@ private void OnMeleeExamineDamage(EntityUid uid, MeleeWeaponComponent component,
return;

var damageSpec = GetDamage(uid, args.User, component);

if (damageSpec.Empty)
return;

_damageExamine.AddDamageExamine(args.Message, damageSpec, Loc.GetString("damage-melee"));

if (damageSpec * component.HeavyDamageBaseModifier != damageSpec)
_damageExamine.AddDamageExamine(args.Message, damageSpec * component.HeavyDamageBaseModifier, Loc.GetString("damage-melee-heavy"));
if (!component.DisableClick)
_damageExamine.AddDamageExamine(args.Message, damageSpec, Loc.GetString("damage-melee"));

if (component.HeavyStaminaCost != 0)
if (!component.DisableHeavy)
{
var staminaCostMarkup = FormattedMessage.FromMarkupOrThrow(
Loc.GetString("damage-stamina-cost",
("type", Loc.GetString("damage-melee-heavy")), ("cost", component.HeavyStaminaCost)));
args.Message.PushNewline();
args.Message.AddMessage(staminaCostMarkup);
if (damageSpec * component.HeavyDamageBaseModifier != damageSpec)
_damageExamine.AddDamageExamine(args.Message, damageSpec * component.HeavyDamageBaseModifier, Loc.GetString("damage-melee-heavy"));

if (component.HeavyStaminaCost != 0)
{
var staminaCostMarkup = FormattedMessage.FromMarkupOrThrow(
Loc.GetString("damage-stamina-cost",
("type", Loc.GetString("damage-melee-heavy")), ("cost", component.HeavyStaminaCost)));
args.Message.PushNewline();
args.Message.AddMessage(staminaCostMarkup);
}
}
}

Expand Down
20 changes: 20 additions & 0 deletions Content.Shared/Weapons/Melee/MeleeWeaponComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,26 @@ public sealed partial class MeleeWeaponComponent : Component
[DataField]
public bool ResetOnHandSelected = true;

/// <summary>
/// If true, swaps the keybinds for light attacks and heavy attacks.
/// </summary>
[DataField]
public bool SwapKeys = false;

/// <summary>
/// If true, disables heavy attacks for this weapon, and prevents the heavy damage values appearing
/// when the damage values are examined.
/// </summary>
[DataField]
public bool DisableHeavy = false;

/// <summary>
/// If true, disables single-target attacks for this weapon, and prevents the single-target damage values appearing
/// when the damage values are examined.
/// </summary>
[DataField]
public bool DisableClick = false;

/*
* Melee combat works based around 2 types of attacks:
* 1. Click attacks with left-click. This attacks whatever is under your mnouse
Expand Down
42 changes: 8 additions & 34 deletions Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,6 @@ public override void Initialize()
base.Initialize();

SubscribeLocalEvent<MeleeWeaponComponent, HandSelectedEvent>(OnMeleeSelected);
SubscribeLocalEvent<MeleeWeaponComponent, ShotAttemptedEvent>(OnMeleeShotAttempted);
SubscribeLocalEvent<MeleeWeaponComponent, GunShotEvent>(OnMeleeShot);
SubscribeLocalEvent<BonusMeleeDamageComponent, GetMeleeDamageEvent>(OnGetBonusMeleeDamage);
SubscribeLocalEvent<BonusMeleeDamageComponent, GetHeavyDamageModifierEvent>(OnGetBonusHeavyDamageModifier);
SubscribeLocalEvent<BonusMeleeAttackRateComponent, GetMeleeAttackRateEvent>(OnGetBonusMeleeAttackRate);
Expand All @@ -86,24 +84,6 @@ private void OnMapInit(EntityUid uid, MeleeWeaponComponent component, MapInitEve
#endif
}

private void OnMeleeShotAttempted(EntityUid uid, MeleeWeaponComponent comp, ref ShotAttemptedEvent args)
{
if (comp.NextAttack > Timing.CurTime)
args.Cancel();
}

private void OnMeleeShot(EntityUid uid, MeleeWeaponComponent component, ref GunShotEvent args)
{
if (!TryComp<GunComponent>(uid, out var gun))
return;

if (gun.NextFire > component.NextAttack)
{
component.NextAttack = gun.NextFire;
Dirty(uid, component);
}
}

private void OnMeleeSelected(EntityUid uid, MeleeWeaponComponent component, HandSelectedEvent args)
{
var attackRate = GetAttackRate(uid, args.User, component);
Expand Down Expand Up @@ -169,29 +149,23 @@ private void OnStopAttack(StopAttackEvent msg, EntitySessionEventArgs args)

private void OnLightAttack(LightAttackEvent msg, EntitySessionEventArgs args)
{
if (args.SenderSession.AttachedEntity is not {} user)
if (args.SenderSession.AttachedEntity is not {} user ||
!TryGetWeapon(user, out var weaponUid, out var weapon) ||
weaponUid != GetEntity(msg.Weapon) ||
weapon.DisableClick)
return;

if (!TryGetWeapon(user, out var weaponUid, out var weapon) ||
weaponUid != GetEntity(msg.Weapon))
{
return;
}

AttemptAttack(user, weaponUid, weapon, msg, args.SenderSession);
}

private void OnHeavyAttack(HeavyAttackEvent msg, EntitySessionEventArgs args)
{
if (args.SenderSession.AttachedEntity is not {} user)
if (args.SenderSession.AttachedEntity is not {} user ||
!TryGetWeapon(user, out var weaponUid, out var weapon) ||
weaponUid != GetEntity(msg.Weapon) ||
weapon.DisableHeavy)
return;

if (!TryGetWeapon(user, out var weaponUid, out var weapon) ||
weaponUid != GetEntity(msg.Weapon))
{
return;
}

AttemptAttack(user, weaponUid, weapon, msg, args.SenderSession);
}

Expand Down
6 changes: 6 additions & 0 deletions Content.Shared/Weapons/Ranged/Components/GunComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,12 @@ public sealed partial class GunComponent : Component
[AutoPausedField]
public TimeSpan NextFire = TimeSpan.Zero;

/// <summary>
/// After dealing a melee attack with this gun, the minimum cooldown in seconds before the gun can shoot again.
/// </summary>
[DataField]
public float MeleeCooldown = 0.528f;

/// <summary>
/// What firemodes can be selected.
/// </summary>
Expand Down
18 changes: 11 additions & 7 deletions Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,18 @@ private void OnMapInit(Entity<GunComponent> gun, ref MapInitEvent args)

private void OnGunMelee(EntityUid uid, GunComponent component, MeleeHitEvent args)
{
if (!TryComp<MeleeWeaponComponent>(uid, out var melee))
return;
var curTime = Timing.CurTime;

if (melee.NextAttack > component.NextFire)
{
component.NextFire = melee.NextAttack;
Dirty(uid, component);
}
if (component.NextFire < curTime)
component.NextFire = curTime;

var meleeCooldown = TimeSpan.FromSeconds(component.MeleeCooldown);

component.NextFire += meleeCooldown;
while (component.NextFire <= curTime)
component.NextFire += meleeCooldown;

Dirty(uid, component);
}

private void OnShootRequest(RequestShootEvent msg, EntitySessionEventArgs args)
Expand Down
2 changes: 1 addition & 1 deletion Resources/Locale/en-US/store/uplink-catalog.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ uplink-pistol-cobra-name = Cobra
uplink-pistol-cobra-desc = A rugged, robust operator handgun with inbuilt silencer. Uses pistol magazines (.25 caseless).
uplink-rifle-mosin-name = Surplus Rifle
uplink-rifle-mosin-desc = A bolt action service rifle that has seen many wars. Not modern by any standard, hand loaded, and terrible recoil, but it is cheap.
uplink-rifle-mosin-desc = A bolt action service rifle that has seen many wars. Not modern by any standard, hand loaded, and terrible recoil, but it is cheap. The attached bayonet allows it to be used as an improvised spear.
uplink-esword-name = Energy Sword
uplink-esword-desc = A very dangerous energy sword that can reflect shots. Can be stored in pockets when turned off. Makes a lot of noise when used or turned on.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,15 @@
Disabler: { state: mode-disabler }
Lethal: { state: mode-lethal }
Special: { state: mode-stun } # Unused
- type: MeleeWeapon
attackRate: 1.2
damage:
types:
Blunt: 7.5
bluntStaminaDamageFactor: 1.0
wideAnimationRotation: 135
- type: DamageOtherOnHit
staminaCost: 5

- type: entity
name: miniature energy gun
Expand Down Expand Up @@ -232,6 +241,15 @@
- Sidearm
- type: StaticPrice
price: 750
- type: MeleeWeapon
attackRate: 1.2
damage:
types:
Blunt: 7.5
bluntStaminaDamageFactor: 1.0
wideAnimationRotation: 135
- type: DamageOtherOnHit
staminaCost: 5

- type: entity
name: PDW-9 Energy Pistol
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
- type: entity
- type: entity
name: Experimental L6 SAW
parent: BaseItem
id: WeaponLightMachineGunL6Borg
Expand Down Expand Up @@ -38,4 +38,21 @@
# - type: DynamicPrice
# price: 500
- type: Appearance

- type: MeleeWeapon
attackRate: 1.4
damage:
types:
Blunt: 11
bluntStaminaDamageFactor: 1.3333
swapKeys: true
disableHeavy: true
animation: WeaponArcThrust
wideAnimationRotation: 180
soundHit:
collection: MetalThud
- type: IncreaseDamageOnWield
damage:
types:
Blunt: 3
- type: DamageOtherOnHit
staminaCost: 12
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,20 @@
- Belt
- type: UseDelay
delay: 1
- type: MeleeWeapon
attackRate: 1.3333
damage:
types:
Blunt: 9.0
swapKeys: true
disableHeavy: true
animation: WeaponArcThrust
wideAnimationRotation: 180
soundHit:
collection: MetalThud
- type: IncreaseDamageOnWield
damage:
types:
Blunt: 2.5
- type: DamageOtherOnHit
staminaCost: 8
Loading

0 comments on commit 5899f4e

Please sign in to comment.