mirror of
https://github.com/WWhiteDreamProject/wwdpublic.git
synced 2026-04-17 21:48:58 +03:00
# 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  ### Martial Artist In Action https://github.com/user-attachments/assets/7890aac2-2c74-4abd-be9f-b28c2922c865 ### Striking Calluses New Description  ### Natural Weapons Removal New Description  # 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)
284 lines
10 KiB
C#
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);
|
|
}
|
|
}
|