Files
wwdpublic/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs
Skubman 41fde3eb0b Martial Artist Rework + Fix MeleeWeapon Wizmerge Bugs (#1560)
# Description

Reworks the Martial Artist trait, making it a more impactful and
visually distinct trait.
- Left-clicks are now single-target power attacks, requiring less aim
than before.
- **50%** damage bonus reduced to **20%** damage bonus (same overall DPS
with next change).
- Gain **25%** attack rate bonus.
- The attack rate bonus helps make the trait feel and look distinct from
non-Martial Artist melee attacks.
- **50%** range bonus reduced to **10%** range bonus.
- The damage bonus is now also applied to Asphyxiation and Poison
damage, which Lamia unarmed attacks deal.
- Trait cost increased from **-3** points to **-5** points.
- Striking Calluses (which requires Martial Artist) cost reduced from
**-4** points to **-3** points to prevent the Martial Artist/Striking
Calluses combo from being too expensive. The combo used to cost **-7**
points, now it is **-8** points.

The reworked Martial Artist trait is also given to the Boxers, Martial
Artists and Gladiators.

Also reverted some wizmerge messery that messed up the melee attack rate
**again**, and messed up pistol whipping by making the cooldowns of
gunshots and melee attacks intertwined.

Also while we're at it, Natural Weapons Removal has been disabled for
all species whose damage is pure Blunt, including Diona, Dwarf, Arachne
and IPC. (IPC have 6 blunt so the trait would literally be a 0-point
negative trait for them)

## Technical Details

A new trait function has been added for Martial Artist:
`TraitModifyUnarmed`, which modifies the player entity's
`MeleeWeaponComponent`.

The Claws, Talons, Natural Weapon Removal, and Striking Calluses traits
have also been refactored under the hood to use `TraitModifyUnarmed`,
instead of replacing `MeleeWeaponComponent` which would wipe out all the
changes made by the Martial Artist trait.

## Media

### New Description

![martialartist](https://github.com/user-attachments/assets/de51530f-5f2a-41a8-b686-c12f746b5e4a)

### Martial Artist In Action

https://github.com/user-attachments/assets/7890aac2-2c74-4abd-be9f-b28c2922c865

### Striking Calluses New Description

![strikingcalluses](https://github.com/user-attachments/assets/bc15425e-3460-49f2-88f5-840c686e0053)

### Natural Weapons Removal New Description

![naturalweaponsremoval](https://github.com/user-attachments/assets/583adaa4-48c0-46e3-882b-1bb0f470018c)

# Changelog

🆑 Skubman
- add: Martial Artist Rework: Martial Artist now costs 5 points, but it
turns all unarmed melee attacks into single-target power attacks, with
20% bonus damage, 25% bonus attack rate and 10% bonus attack range.
- tweak: The reworked Martial Artist trait is now given for free to
Boxers, Martial Artists, and Gladiators.
- tweak: Martial Artists (the job) and Gladiators can now select the
Striking Calluses trait.
- tweak: The Martial Artist trait now applies bonus damage to the
Lamiae's unarmed Asphyxiation and Poison damage.
- tweak: The cost of Striking Calluses has been reduced from 4 points to
3 points.
- fix: Fixed a bug where slow weapons were fast and fast weapons were
slow.
- fix: You can pistol whip (right-click melee) immediately after firing
a gun again, and the cooldown on firing the gun after pistol whipping is
always 0.528 seconds again.
- fix: Prevented Dionas, Arachnae and IPCs, who all have pure Blunt
damage from selecting the redundant Natural Weapons Removal trait.

(cherry picked from commit 6c43d005e0804fe29007371a46a56e27bccbd4f8)
2025-01-20 20:50:21 +03:00

284 lines
10 KiB
C#

using System.Linq;
using Content.Client.Gameplay;
using Content.Shared._White.Blink;
using Content.Shared.CCVar;
using Content.Shared.CombatMode;
using Content.Shared.Effects;
using Content.Shared.Hands.Components;
using Content.Shared.Mobs.Components;
using Content.Shared.StatusEffect;
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;
using Robust.Client.Player;
using Robust.Client.State;
using Robust.Shared.Input;
using Robust.Shared.Map;
using Robust.Shared.Player;
namespace Content.Client.Weapons.Melee;
public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem
{
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IStateManager _stateManager = default!;
[Dependency] private readonly AnimationPlayerSystem _animation = default!;
[Dependency] private readonly InputSystem _inputSystem = default!;
[Dependency] private readonly SharedColorFlashEffectSystem _color = default!;
private EntityQuery<TransformComponent> _xformQuery;
private const string MeleeLungeKey = "melee-lunge";
public override void Initialize()
{
base.Initialize();
_xformQuery = GetEntityQuery<TransformComponent>();
SubscribeNetworkEvent<MeleeLungeEvent>(OnMeleeLunge);
UpdatesOutsidePrediction = true;
}
public override void FrameUpdate(float frameTime)
{
base.FrameUpdate(frameTime);
UpdateEffects();
}
public override void Update(float frameTime)
{
base.Update(frameTime);
if (!Timing.IsFirstTimePredicted)
return;
var entityNull = _player.LocalEntity;
if (entityNull == null)
return;
var entity = entityNull.Value;
if (!TryGetWeapon(entity, out var weaponUid, out var weapon))
return;
if (!CombatMode.IsInCombatMode(entity) || !Blocker.CanAttack(entity))
{
weapon.Attacking = false;
return;
}
var useDown = _inputSystem.CmdStates.GetState(EngineKeyFunctions.Use) == BoundKeyState.Down;
var altDown = _inputSystem.CmdStates.GetState(EngineKeyFunctions.UseSecondary) == 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 || (!useDown && !altDown))
{
return;
}
// TODO using targeted actions while combat mode is enabled should NOT trigger attacks.
var mousePos = _eyeManager.PixelToMap(_inputManager.MouseScreenPosition);
if (mousePos.MapId == MapId.Nullspace)
{
return;
}
EntityCoordinates coordinates;
if (MapManager.TryFindGridAt(mousePos, out var gridUid, out _))
{
coordinates = EntityCoordinates.FromMap(gridUid, mousePos, TransformSystem, EntityManager);
}
else
{
coordinates = EntityCoordinates.FromMap(MapManager.GetMapEntityId(mousePos.MapId), mousePos, TransformSystem, EntityManager);
}
// Heavy attack.
if (!weapon.DisableHeavy &&
(!weapon.SwapKeys ? altDown : useDown)
&& weapon.CanHeavyAttack) // WD EDIT
{
// If it's an unarmed attack then do a disarm
if (weapon.AltDisarm && weaponUid == entity)
{
EntityUid? target = null;
if (_stateManager.CurrentState is GameplayStateBase screen)
{
target = screen.GetClickedEntity(mousePos);
}
EntityManager.RaisePredictiveEvent(new DisarmAttackEvent(GetNetEntity(target), GetNetCoordinates(coordinates)));
return;
}
// WD EDIT START
if (HasComp<BlinkComponent>(weaponUid))
{
if (!_xformQuery.TryGetComponent(entity, out var userXform) || !Timing.IsFirstTimePredicted)
return;
var targetMap = coordinates.ToMap(EntityManager, TransformSystem);
if (targetMap.MapId != userXform.MapID)
return;
var userPos = TransformSystem.GetWorldPosition(userXform);
var direction = targetMap.Position - userPos;
RaiseNetworkEvent(new BlinkEvent(GetNetEntity(weaponUid), direction));
return;
}
// WD EDIT END
ClientHeavyAttack(entity, coordinates, weaponUid, weapon);
return;
}
// Light attack
if (!weapon.DisableClick &&
(!weapon.SwapKeys ? useDown : altDown))
{
var attackerPos = TransformSystem.GetMapCoordinates(entity);
if (mousePos.MapId != attackerPos.MapId ||
(attackerPos.Position - mousePos.Position).Length() > weapon.Range)
{
if (weapon.HeavyOnLightMiss)
ClientHeavyAttack(entity, coordinates, weaponUid, weapon);
return;
}
EntityUid? target = null;
if (_stateManager.CurrentState is GameplayStateBase screen)
{
target = screen.GetClickedEntity(mousePos);
}
// Don't light-attack if interaction will be handling this instead
if (Interaction.CombatModeCanHandInteract(entity, target))
return;
if (weapon.HeavyOnLightMiss && !CanDoLightAttack(entity, target, weapon, out _))
{
ClientHeavyAttack(entity, coordinates, weaponUid, weapon);
return;
}
RaisePredictiveEvent(new LightAttackEvent(GetNetEntity(target), GetNetEntity(weaponUid), GetNetCoordinates(coordinates)));
}
}
protected override bool InRange(EntityUid user, EntityUid target, float range, ICommonSession? session)
{
var xform = Transform(target);
var targetCoordinates = xform.Coordinates;
var targetLocalAngle = xform.LocalRotation;
return Interaction.InRangeUnobstructed(user, target, targetCoordinates, targetLocalAngle, range);
}
protected override void DoDamageEffect(List<EntityUid> targets, EntityUid? user, TransformComponent targetXform)
{
// Server never sends the event to us for predictiveeevent.
_color.RaiseEffect(Color.Red, targets, Filter.Local());
}
protected override bool DoDisarm(EntityUid user, DisarmAttackEvent ev, EntityUid meleeUid, MeleeWeaponComponent component, ICommonSession? session)
{
if (!base.DoDisarm(user, ev, meleeUid, component, session))
return false;
if (!TryComp<CombatModeComponent>(user, out var combatMode) ||
combatMode.CanDisarm != true)
{
return false;
}
var target = GetEntity(ev.Target);
// They need to either have hands...
if (!HasComp<HandsComponent>(target!.Value))
{
// or just be able to be shoved over.
if (TryComp<StatusEffectsComponent>(target, out var status) && status.AllowedEffects.Contains("KnockedDown"))
return true;
if (Timing.IsFirstTimePredicted && HasComp<MobStateComponent>(target.Value))
PopupSystem.PopupEntity(Loc.GetString("disarm-action-disarmable", ("targetName", target.Value)), target.Value);
return false;
}
return true;
}
/// <summary>
/// Raises a heavy attack event with the relevant attacked entities.
/// This is to avoid lag effecting the client's perspective too much.
/// </summary>
private void ClientHeavyAttack(EntityUid user, EntityCoordinates coordinates, EntityUid meleeUid, MeleeWeaponComponent component)
{
// Only run on first prediction to avoid the potential raycast entities changing.
if (!_xformQuery.TryGetComponent(user, out var userXform) ||
!Timing.IsFirstTimePredicted)
{
return;
}
var targetMap = coordinates.ToMap(EntityManager, TransformSystem);
if (targetMap.MapId != userXform.MapID)
return;
var userPos = TransformSystem.GetWorldPosition(userXform);
var direction = targetMap.Position - userPos;
var distance = MathF.Min(component.Range * component.HeavyRangeModifier, direction.Length());
// This should really be improved. GetEntitiesInArc uses pos instead of bounding boxes.
// Server will validate it with InRangeUnobstructed.
var entities = GetNetEntityList(ArcRayCast(userPos, direction.ToWorldAngle(), component.Angle, distance, userXform.MapID, user).ToList());
RaisePredictiveEvent(new HeavyAttackEvent(GetNetEntity(meleeUid), entities.GetRange(0, Math.Min(component.MaxTargets, entities.Count)), GetNetCoordinates(coordinates)));
}
private void OnMeleeLunge(MeleeLungeEvent ev)
{
var ent = GetEntity(ev.Entity);
var entWeapon = GetEntity(ev.Weapon);
// Entity might not have been sent by PVS.
if (Exists(ent) && Exists(entWeapon))
DoLunge(ent, entWeapon, ev.Angle, ev.LocalPos, ev.Animation);
}
}