MeleeWeaponSystem.cs 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. using System.Linq;
  2. using Content.Client.Gameplay;
  3. using Content.Shared.CombatMode;
  4. using Content.Shared.Effects;
  5. using Content.Shared.Hands.Components;
  6. using Content.Shared.Mobs.Components;
  7. using Content.Shared.StatusEffect;
  8. using Content.Shared.Weapons.Melee;
  9. using Content.Shared.Weapons.Melee.Components;
  10. using Content.Shared.Weapons.Melee.Events;
  11. using Content.Shared.Weapons.Ranged.Components;
  12. using Robust.Client.GameObjects;
  13. using Robust.Client.Graphics;
  14. using Robust.Client.Input;
  15. using Robust.Client.Player;
  16. using Robust.Client.State;
  17. using Robust.Shared.Input;
  18. using Robust.Shared.Map;
  19. using Robust.Shared.Player;
  20. namespace Content.Client.Weapons.Melee;
  21. public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem
  22. {
  23. [Dependency] private readonly IEyeManager _eyeManager = default!;
  24. [Dependency] private readonly IInputManager _inputManager = default!;
  25. [Dependency] private readonly IPlayerManager _player = default!;
  26. [Dependency] private readonly IStateManager _stateManager = default!;
  27. [Dependency] private readonly AnimationPlayerSystem _animation = default!;
  28. [Dependency] private readonly InputSystem _inputSystem = default!;
  29. [Dependency] private readonly SharedColorFlashEffectSystem _color = default!;
  30. [Dependency] private readonly MapSystem _map = default!;
  31. private EntityQuery<TransformComponent> _xformQuery;
  32. private const string MeleeLungeKey = "melee-lunge";
  33. public override void Initialize()
  34. {
  35. base.Initialize();
  36. _xformQuery = GetEntityQuery<TransformComponent>();
  37. SubscribeNetworkEvent<MeleeLungeEvent>(OnMeleeLunge);
  38. UpdatesOutsidePrediction = true;
  39. }
  40. public override void FrameUpdate(float frameTime)
  41. {
  42. base.FrameUpdate(frameTime);
  43. UpdateEffects();
  44. }
  45. public override void Update(float frameTime)
  46. {
  47. base.Update(frameTime);
  48. if (!Timing.IsFirstTimePredicted)
  49. return;
  50. var entityNull = _player.LocalEntity;
  51. if (entityNull == null)
  52. return;
  53. var entity = entityNull.Value;
  54. if (!TryGetWeapon(entity, out var weaponUid, out var weapon))
  55. return;
  56. if (!CombatMode.IsInCombatMode(entity) || !Blocker.CanAttack(entity, weapon: (weaponUid, weapon)))
  57. {
  58. weapon.Attacking = false;
  59. return;
  60. }
  61. var useDown = _inputSystem.CmdStates.GetState(EngineKeyFunctions.Use);
  62. var altDown = _inputSystem.CmdStates.GetState(EngineKeyFunctions.UseSecondary);
  63. if (weapon.AutoAttack || useDown != BoundKeyState.Down && altDown != BoundKeyState.Down)
  64. {
  65. if (weapon.Attacking)
  66. {
  67. RaisePredictiveEvent(new StopAttackEvent(GetNetEntity(weaponUid)));
  68. }
  69. }
  70. if (weapon.Attacking || weapon.NextAttack > Timing.CurTime)
  71. {
  72. return;
  73. }
  74. // TODO using targeted actions while combat mode is enabled should NOT trigger attacks.
  75. var mousePos = _eyeManager.PixelToMap(_inputManager.MouseScreenPosition);
  76. if (mousePos.MapId == MapId.Nullspace)
  77. {
  78. return;
  79. }
  80. EntityCoordinates coordinates;
  81. if (MapManager.TryFindGridAt(mousePos, out var gridUid, out _))
  82. {
  83. coordinates = TransformSystem.ToCoordinates(gridUid, mousePos);
  84. }
  85. else
  86. {
  87. coordinates = TransformSystem.ToCoordinates(_map.GetMap(mousePos.MapId), mousePos);
  88. }
  89. // If the gun has AltFireComponent, it can be used to attack.
  90. if (TryComp<GunComponent>(weaponUid, out var gun) && gun.UseKey)
  91. {
  92. if (!TryComp<AltFireMeleeComponent>(weaponUid, out var altFireComponent) || altDown != BoundKeyState.Down)
  93. return;
  94. switch(altFireComponent.AttackType)
  95. {
  96. case AltFireAttackType.Light:
  97. ClientLightAttack(entity, mousePos, coordinates, weaponUid, weapon);
  98. break;
  99. case AltFireAttackType.Heavy:
  100. ClientHeavyAttack(entity, coordinates, weaponUid, weapon);
  101. break;
  102. case AltFireAttackType.Disarm:
  103. ClientDisarm(entity, mousePos, coordinates);
  104. break;
  105. }
  106. return;
  107. }
  108. // Heavy attack.
  109. if (altDown == BoundKeyState.Down)
  110. {
  111. // If it's an unarmed attack then do a disarm
  112. if (weapon.AltDisarm && weaponUid == entity)
  113. {
  114. ClientDisarm(entity, mousePos, coordinates);
  115. return;
  116. }
  117. ClientHeavyAttack(entity, coordinates, weaponUid, weapon);
  118. return;
  119. }
  120. // Light attack
  121. if (useDown == BoundKeyState.Down)
  122. ClientLightAttack(entity, mousePos, coordinates, weaponUid, weapon);
  123. }
  124. protected override bool InRange(EntityUid user, EntityUid target, float range, ICommonSession? session)
  125. {
  126. var xform = Transform(target);
  127. var targetCoordinates = xform.Coordinates;
  128. var targetLocalAngle = xform.LocalRotation;
  129. return Interaction.InRangeUnobstructed(user, target, targetCoordinates, targetLocalAngle, range, overlapCheck: false);
  130. }
  131. protected override void DoDamageEffect(List<EntityUid> targets, EntityUid? user, TransformComponent targetXform)
  132. {
  133. // Server never sends the event to us for predictiveeevent.
  134. _color.RaiseEffect(Color.Red, targets, Filter.Local());
  135. }
  136. protected override bool DoDisarm(EntityUid user, DisarmAttackEvent ev, EntityUid meleeUid, MeleeWeaponComponent component, ICommonSession? session)
  137. {
  138. if (!base.DoDisarm(user, ev, meleeUid, component, session))
  139. return false;
  140. if (!TryComp<CombatModeComponent>(user, out var combatMode) ||
  141. combatMode.CanDisarm != true)
  142. {
  143. return false;
  144. }
  145. var target = GetEntity(ev.Target);
  146. // They need to either have hands...
  147. if (!HasComp<HandsComponent>(target!.Value))
  148. {
  149. // or just be able to be shoved over.
  150. if (TryComp<StatusEffectsComponent>(target, out var status) && status.AllowedEffects.Contains("KnockedDown"))
  151. return true;
  152. if (Timing.IsFirstTimePredicted && HasComp<MobStateComponent>(target.Value))
  153. PopupSystem.PopupEntity(Loc.GetString("disarm-action-disarmable", ("targetName", target.Value)), target.Value);
  154. return false;
  155. }
  156. return true;
  157. }
  158. /// <summary>
  159. /// Raises a heavy attack event with the relevant attacked entities.
  160. /// This is to avoid lag effecting the client's perspective too much.
  161. /// </summary>
  162. private void ClientHeavyAttack(EntityUid user, EntityCoordinates coordinates, EntityUid meleeUid, MeleeWeaponComponent component)
  163. {
  164. // Only run on first prediction to avoid the potential raycast entities changing.
  165. if (!_xformQuery.TryGetComponent(user, out var userXform) ||
  166. !Timing.IsFirstTimePredicted)
  167. {
  168. return;
  169. }
  170. var targetMap = TransformSystem.ToMapCoordinates(coordinates);
  171. if (targetMap.MapId != userXform.MapID)
  172. return;
  173. var userPos = TransformSystem.GetWorldPosition(userXform);
  174. var direction = targetMap.Position - userPos;
  175. var distance = MathF.Min(component.Range, direction.Length());
  176. // This should really be improved. GetEntitiesInArc uses pos instead of bounding boxes.
  177. // Server will validate it with InRangeUnobstructed.
  178. var entities = GetNetEntityList(ArcRayCast(userPos, direction.ToWorldAngle(), component.Angle, distance, userXform.MapID, user).ToList());
  179. RaisePredictiveEvent(new HeavyAttackEvent(GetNetEntity(meleeUid), entities.GetRange(0, Math.Min(MaxTargets, entities.Count)), GetNetCoordinates(coordinates)));
  180. }
  181. private void ClientDisarm(EntityUid attacker, MapCoordinates mousePos, EntityCoordinates coordinates)
  182. {
  183. EntityUid? target = null;
  184. if (_stateManager.CurrentState is GameplayStateBase screen)
  185. target = screen.GetClickedEntity(mousePos);
  186. RaisePredictiveEvent(new DisarmAttackEvent(GetNetEntity(target), GetNetCoordinates(coordinates)));
  187. }
  188. private void ClientLightAttack(EntityUid attacker, MapCoordinates mousePos, EntityCoordinates coordinates, EntityUid weaponUid, MeleeWeaponComponent meleeComponent)
  189. {
  190. var attackerPos = TransformSystem.GetMapCoordinates(attacker);
  191. if (mousePos.MapId != attackerPos.MapId || (attackerPos.Position - mousePos.Position).Length() > meleeComponent.Range)
  192. return;
  193. EntityUid? target = null;
  194. if (_stateManager.CurrentState is GameplayStateBase screen)
  195. target = screen.GetClickedEntity(mousePos);
  196. // Don't light-attack if interaction will be handling this instead
  197. if (Interaction.CombatModeCanHandInteract(attacker, target))
  198. return;
  199. RaisePredictiveEvent(new LightAttackEvent(GetNetEntity(target), GetNetEntity(weaponUid), GetNetCoordinates(coordinates)));
  200. }
  201. private void OnMeleeLunge(MeleeLungeEvent ev)
  202. {
  203. var ent = GetEntity(ev.Entity);
  204. var entWeapon = GetEntity(ev.Weapon);
  205. // Entity might not have been sent by PVS.
  206. if (Exists(ent) && Exists(entWeapon))
  207. DoLunge(ent, entWeapon, ev.Angle, ev.LocalPos, ev.Animation);
  208. }
  209. }