MeleeWeaponSystem.cs 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. using Content.Server.Chat.Systems;
  2. using Content.Server.CombatMode.Disarm;
  3. using Content.Server.Movement.Systems;
  4. using Content.Shared.Actions.Events;
  5. using Content.Shared.Administration.Components;
  6. using Content.Shared.CombatMode;
  7. using Content.Shared.Damage.Events;
  8. using Content.Shared.Damage.Systems;
  9. using Content.Shared.Database;
  10. using Content.Shared.Effects;
  11. using Content.Shared.Hands.Components;
  12. using Content.Shared.IdentityManagement;
  13. using Content.Shared.Mobs.Systems;
  14. using Content.Shared.Popups;
  15. using Content.Shared.Speech.Components;
  16. using Content.Shared.StatusEffect;
  17. using Content.Shared.Weapons.Melee;
  18. using Content.Shared.Weapons.Melee.Events;
  19. using Robust.Shared.Audio;
  20. using Robust.Shared.Audio.Systems;
  21. using Robust.Shared.Map;
  22. using Robust.Shared.Player;
  23. using Robust.Shared.Random;
  24. using System.Linq;
  25. using System.Numerics;
  26. namespace Content.Server.Weapons.Melee;
  27. public sealed class MeleeWeaponSystem : SharedMeleeWeaponSystem
  28. {
  29. [Dependency] private readonly SharedAudioSystem _audio = default!;
  30. [Dependency] private readonly IRobustRandom _random = default!;
  31. [Dependency] private readonly ChatSystem _chat = default!;
  32. [Dependency] private readonly DamageExamineSystem _damageExamine = default!;
  33. [Dependency] private readonly LagCompensationSystem _lag = default!;
  34. [Dependency] private readonly MobStateSystem _mobState = default!;
  35. [Dependency] private readonly SharedColorFlashEffectSystem _color = default!;
  36. public override void Initialize()
  37. {
  38. base.Initialize();
  39. SubscribeLocalEvent<MeleeSpeechComponent, MeleeHitEvent>(OnSpeechHit);
  40. SubscribeLocalEvent<MeleeWeaponComponent, DamageExamineEvent>(OnMeleeExamineDamage);
  41. }
  42. private void OnMeleeExamineDamage(EntityUid uid, MeleeWeaponComponent component, ref DamageExamineEvent args)
  43. {
  44. if (component.Hidden)
  45. return;
  46. var damageSpec = GetDamage(uid, args.User, component);
  47. if (damageSpec.Empty)
  48. return;
  49. _damageExamine.AddDamageExamine(args.Message, Damageable.ApplyUniversalAllModifiers(damageSpec), Loc.GetString("damage-melee"));
  50. }
  51. protected override bool ArcRaySuccessful(EntityUid targetUid,
  52. Vector2 position,
  53. Angle angle,
  54. Angle arcWidth,
  55. float range,
  56. MapId mapId,
  57. EntityUid ignore,
  58. ICommonSession? session)
  59. {
  60. // Originally the client didn't predict damage effects so you'd intuit some level of how far
  61. // in the future you'd need to predict, but then there was a lot of complaining like "why would you add artifical delay" as if ping is a choice.
  62. // Now damage effects are predicted but for wide attacks it differs significantly from client and server so your game could be lying to you on hits.
  63. // This isn't fair in the slightest because it makes ping a huge advantage and this would be a hidden system.
  64. // Now the client tells us what they hit and we validate if it's plausible.
  65. // Even if the client is sending entities they shouldn't be able to hit:
  66. // A) Wide-damage is split anyway
  67. // B) We run the same validation we do for click attacks.
  68. // Could also check the arc though future effort + if they're aimbotting it's not really going to make a difference.
  69. // (This runs lagcomp internally and is what clickattacks use)
  70. if (!Interaction.InRangeUnobstructed(ignore, targetUid, range + 0.1f, overlapCheck: false))
  71. return false;
  72. // TODO: Check arc though due to the aforementioned aimbot + damage split comments it's less important.
  73. return true;
  74. }
  75. protected override bool DoDisarm(EntityUid user, DisarmAttackEvent ev, EntityUid meleeUid, MeleeWeaponComponent component, ICommonSession? session)
  76. {
  77. if (!base.DoDisarm(user, ev, meleeUid, component, session))
  78. return false;
  79. if (!TryComp<CombatModeComponent>(user, out var combatMode) ||
  80. combatMode.CanDisarm != true)
  81. {
  82. return false;
  83. }
  84. var target = GetEntity(ev.Target!.Value);
  85. if (_mobState.IsIncapacitated(target))
  86. {
  87. return false;
  88. }
  89. if (!TryComp<HandsComponent>(target, out var targetHandsComponent))
  90. {
  91. if (!TryComp<StatusEffectsComponent>(target, out var status) || !status.AllowedEffects.Contains("KnockedDown"))
  92. return false;
  93. }
  94. if (!InRange(user, target, component.Range, session))
  95. {
  96. return false;
  97. }
  98. EntityUid? inTargetHand = null;
  99. if (targetHandsComponent?.ActiveHand is { IsEmpty: false })
  100. {
  101. inTargetHand = targetHandsComponent.ActiveHand.HeldEntity!.Value;
  102. }
  103. Interaction.DoContactInteraction(user, target);
  104. var attemptEvent = new DisarmAttemptEvent(target, user, inTargetHand);
  105. if (inTargetHand != null)
  106. {
  107. RaiseLocalEvent(inTargetHand.Value, attemptEvent);
  108. }
  109. RaiseLocalEvent(target, attemptEvent);
  110. if (attemptEvent.Cancelled)
  111. return false;
  112. var chance = CalculateDisarmChance(user, target, inTargetHand, combatMode);
  113. if (_random.Prob(chance))
  114. {
  115. // Yknow something tells me this comment is hilariously out of date...
  116. // Don't play a sound as the swing is already predicted.
  117. // Also don't play popups because most disarms will miss.
  118. return false;
  119. }
  120. AdminLogger.Add(LogType.DisarmedAction, $"{ToPrettyString(user):user} used disarm on {ToPrettyString(target):target}");
  121. var eventArgs = new DisarmedEvent { Target = target, Source = user, PushProbability = 1 - chance };
  122. RaiseLocalEvent(target, eventArgs);
  123. if (!eventArgs.Handled)
  124. {
  125. return false;
  126. }
  127. _audio.PlayPvs(combatMode.DisarmSuccessSound, user, AudioParams.Default.WithVariation(0.025f).WithVolume(5f));
  128. AdminLogger.Add(LogType.DisarmedAction, $"{ToPrettyString(user):user} used disarm on {ToPrettyString(target):target}");
  129. var targetEnt = Identity.Entity(target, EntityManager);
  130. var userEnt = Identity.Entity(user, EntityManager);
  131. var msgOther = Loc.GetString(
  132. eventArgs.PopupPrefix + "popup-message-other-clients",
  133. ("performerName", userEnt),
  134. ("targetName", targetEnt));
  135. var msgUser = Loc.GetString(eventArgs.PopupPrefix + "popup-message-cursor", ("targetName", targetEnt));
  136. var filterOther = Filter.PvsExcept(user, entityManager: EntityManager);
  137. PopupSystem.PopupEntity(msgOther, user, filterOther, true);
  138. PopupSystem.PopupEntity(msgUser, target, user);
  139. if (eventArgs.IsStunned)
  140. {
  141. PopupSystem.PopupEntity(Loc.GetString("stunned-component-disarm-success-others", ("source", userEnt), ("target", targetEnt)), targetEnt, Filter.PvsExcept(user), true, PopupType.LargeCaution);
  142. PopupSystem.PopupCursor(Loc.GetString("stunned-component-disarm-success", ("target", targetEnt)), user, PopupType.Large);
  143. AdminLogger.Add(LogType.DisarmedKnockdown, LogImpact.Medium, $"{ToPrettyString(user):user} knocked down {ToPrettyString(target):target}");
  144. }
  145. return true;
  146. }
  147. protected override bool InRange(EntityUid user, EntityUid target, float range, ICommonSession? session)
  148. {
  149. EntityCoordinates targetCoordinates;
  150. Angle targetLocalAngle;
  151. if (session is { } pSession)
  152. {
  153. (targetCoordinates, targetLocalAngle) = _lag.GetCoordinatesAngle(target, pSession);
  154. return Interaction.InRangeUnobstructed(user, target, targetCoordinates, targetLocalAngle, range, overlapCheck: false);
  155. }
  156. return Interaction.InRangeUnobstructed(user, target, range);
  157. }
  158. protected override void DoDamageEffect(List<EntityUid> targets, EntityUid? user, TransformComponent targetXform)
  159. {
  160. var filter = Filter.Pvs(targetXform.Coordinates, entityMan: EntityManager).RemoveWhereAttachedEntity(o => o == user);
  161. _color.RaiseEffect(Color.Red, targets, filter);
  162. }
  163. private float CalculateDisarmChance(EntityUid disarmer, EntityUid disarmed, EntityUid? inTargetHand, CombatModeComponent disarmerComp)
  164. {
  165. if (HasComp<DisarmProneComponent>(disarmer))
  166. return 1.0f;
  167. if (HasComp<DisarmProneComponent>(disarmed))
  168. return 0.0f;
  169. var chance = disarmerComp.BaseDisarmFailChance;
  170. if (inTargetHand != null && TryComp<DisarmMalusComponent>(inTargetHand, out var malus))
  171. {
  172. chance += malus.Malus;
  173. }
  174. return Math.Clamp(chance, 0f, 1f);
  175. }
  176. public override void DoLunge(EntityUid user, EntityUid weapon, Angle angle, Vector2 localPos, string? animation, bool predicted = true)
  177. {
  178. Filter filter;
  179. if (predicted)
  180. {
  181. filter = Filter.PvsExcept(user, entityManager: EntityManager);
  182. }
  183. else
  184. {
  185. filter = Filter.Pvs(user, entityManager: EntityManager);
  186. }
  187. RaiseNetworkEvent(new MeleeLungeEvent(GetNetEntity(user), GetNetEntity(weapon), angle, localPos, animation), filter);
  188. }
  189. private void OnSpeechHit(EntityUid owner, MeleeSpeechComponent comp, MeleeHitEvent args)
  190. {
  191. if (!args.IsHit ||
  192. !args.HitEntities.Any())
  193. {
  194. return;
  195. }
  196. if (comp.Battlecry != null)//If the battlecry is set to empty, doesn't speak
  197. {
  198. _chat.TrySendInGameICMessage(args.User, comp.Battlecry, InGameICChatType.Speak, true, true, checkRadioPrefix: false); //Speech that isn't sent to chat or adminlogs
  199. }
  200. }
  201. }