PacificationSystem.cs 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. using System.Diagnostics.CodeAnalysis;
  2. using Content.Shared.Actions;
  3. using Content.Shared.Alert;
  4. using Content.Shared.FixedPoint;
  5. using Content.Shared.IdentityManagement;
  6. using Content.Shared.Interaction.Events;
  7. using Content.Shared.Popups;
  8. using Content.Shared.Throwing;
  9. using Content.Shared.Weapons.Ranged.Events;
  10. using Robust.Shared.Timing;
  11. namespace Content.Shared.CombatMode.Pacification;
  12. public sealed class PacificationSystem : EntitySystem
  13. {
  14. [Dependency] private readonly AlertsSystem _alertsSystem = default!;
  15. [Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
  16. [Dependency] private readonly SharedCombatModeSystem _combatSystem = default!;
  17. [Dependency] private readonly SharedPopupSystem _popup = default!;
  18. [Dependency] private readonly IGameTiming _timing = default!;
  19. public override void Initialize()
  20. {
  21. base.Initialize();
  22. SubscribeLocalEvent<PacifiedComponent, ComponentStartup>(OnStartup);
  23. SubscribeLocalEvent<PacifiedComponent, ComponentShutdown>(OnShutdown);
  24. SubscribeLocalEvent<PacifiedComponent, BeforeThrowEvent>(OnBeforeThrow);
  25. SubscribeLocalEvent<PacifiedComponent, AttackAttemptEvent>(OnAttackAttempt);
  26. SubscribeLocalEvent<PacifiedComponent, ShotAttemptedEvent>(OnShootAttempt);
  27. SubscribeLocalEvent<PacifismDangerousAttackComponent, AttemptPacifiedAttackEvent>(OnPacifiedDangerousAttack);
  28. }
  29. private bool PacifiedCanAttack(EntityUid user, EntityUid target, [NotNullWhen(false)] out string? reason)
  30. {
  31. var ev = new AttemptPacifiedAttackEvent(user);
  32. RaiseLocalEvent(target, ref ev);
  33. if (ev.Cancelled)
  34. {
  35. reason = ev.Reason;
  36. return false;
  37. }
  38. reason = null;
  39. return true;
  40. }
  41. private void ShowPopup(Entity<PacifiedComponent> user, EntityUid target, string reason)
  42. {
  43. // Popup logic.
  44. // Cooldown is needed because the input events for melee/shooting etc. will fire continuously
  45. if (target == user.Comp.LastAttackedEntity
  46. && !(_timing.CurTime > user.Comp.NextPopupTime))
  47. return;
  48. var targetName = Identity.Entity(target, EntityManager);
  49. _popup.PopupClient(Loc.GetString(reason, ("entity", targetName)), user, user);
  50. user.Comp.NextPopupTime = _timing.CurTime + user.Comp.PopupCooldown;
  51. user.Comp.LastAttackedEntity = target;
  52. }
  53. private void OnShootAttempt(Entity<PacifiedComponent> ent, ref ShotAttemptedEvent args)
  54. {
  55. if (HasComp<PacifismAllowedGunComponent>(args.Used))
  56. return;
  57. // Disallow firing guns in all cases.
  58. ShowPopup(ent, args.Used, "pacified-cannot-fire-gun");
  59. args.Cancel();
  60. }
  61. private void OnAttackAttempt(EntityUid uid, PacifiedComponent component, AttackAttemptEvent args)
  62. {
  63. if (component.DisallowAllCombat || args.Disarm && component.DisallowDisarm)
  64. {
  65. args.Cancel();
  66. return;
  67. }
  68. // If it's a disarm, let it go through (unless we disallow them, which is handled earlier)
  69. if (args.Disarm)
  70. return;
  71. // Allow attacking with no target. This should be fine.
  72. // If it's a wide swing, that will be handled with a later AttackAttemptEvent raise.
  73. if (args.Target == null)
  74. return;
  75. // If we would do zero damage, it should be fine.
  76. if (args.Weapon != null && args.Weapon.Value.Comp.Damage.GetTotal() == FixedPoint2.Zero)
  77. return;
  78. if (PacifiedCanAttack(uid, args.Target.Value, out var reason))
  79. return;
  80. ShowPopup((uid, component), args.Target.Value, reason);
  81. args.Cancel();
  82. }
  83. private void OnStartup(EntityUid uid, PacifiedComponent component, ComponentStartup args)
  84. {
  85. if (!TryComp<CombatModeComponent>(uid, out var combatMode))
  86. return;
  87. if (component.DisallowDisarm && combatMode.CanDisarm != null)
  88. _combatSystem.SetCanDisarm(uid, false, combatMode);
  89. if (component.DisallowAllCombat)
  90. {
  91. _combatSystem.SetInCombatMode(uid, false, combatMode);
  92. _actionsSystem.SetEnabled(combatMode.CombatToggleActionEntity, false);
  93. }
  94. _alertsSystem.ShowAlert(uid, component.PacifiedAlert);
  95. }
  96. private void OnShutdown(EntityUid uid, PacifiedComponent component, ComponentShutdown args)
  97. {
  98. if (!TryComp<CombatModeComponent>(uid, out var combatMode))
  99. return;
  100. if (combatMode.CanDisarm != null)
  101. _combatSystem.SetCanDisarm(uid, true, combatMode);
  102. _actionsSystem.SetEnabled(combatMode.CombatToggleActionEntity, true);
  103. _alertsSystem.ClearAlert(uid, component.PacifiedAlert);
  104. }
  105. private void OnBeforeThrow(Entity<PacifiedComponent> ent, ref BeforeThrowEvent args)
  106. {
  107. var thrownItem = args.ItemUid;
  108. var itemName = Identity.Entity(thrownItem, EntityManager);
  109. // Raise an AttemptPacifiedThrow event and rely on other systems to check
  110. // whether the candidate item is OK to throw:
  111. var ev = new AttemptPacifiedThrowEvent(thrownItem, ent);
  112. RaiseLocalEvent(thrownItem, ref ev);
  113. if (!ev.Cancelled)
  114. return;
  115. args.Cancelled = true;
  116. // Tell the player why they can’t throw stuff:
  117. var cannotThrowMessage = ev.CancelReasonMessageId ?? "pacified-cannot-throw";
  118. _popup.PopupEntity(Loc.GetString(cannotThrowMessage, ("projectile", itemName)), ent, ent);
  119. }
  120. private void OnPacifiedDangerousAttack(Entity<PacifismDangerousAttackComponent> ent, ref AttemptPacifiedAttackEvent args)
  121. {
  122. args.Cancelled = true;
  123. args.Reason = "pacified-cannot-harm-indirect";
  124. }
  125. }
  126. /// <summary>
  127. /// Raised when a Pacified entity attempts to throw something.
  128. /// The throw is only permitted if this event is not cancelled.
  129. /// </summary>
  130. [ByRefEvent]
  131. public struct AttemptPacifiedThrowEvent
  132. {
  133. public EntityUid ItemUid;
  134. public EntityUid PlayerUid;
  135. public AttemptPacifiedThrowEvent(EntityUid itemUid, EntityUid playerUid)
  136. {
  137. ItemUid = itemUid;
  138. PlayerUid = playerUid;
  139. }
  140. public bool Cancelled { get; private set; } = false;
  141. public string? CancelReasonMessageId { get; private set; }
  142. /// <param name="reasonMessageId">
  143. /// Localization string ID for the reason this event has been cancelled.
  144. /// If null, a generic message will be shown to the player.
  145. /// Note that any supplied localization string MUST accept a '$projectile'
  146. /// parameter specifying the name of the thrown entity.
  147. /// </param>
  148. public void Cancel(string? reasonMessageId = null)
  149. {
  150. Cancelled = true;
  151. CancelReasonMessageId = reasonMessageId;
  152. }
  153. }
  154. /// <summary>
  155. /// Raised ref directed on an entity when a pacified user is attempting to attack it.
  156. /// If <see cref="Cancelled"/> is true, don't allow attacking.
  157. /// <see cref="Reason"/> should be a loc string, if there needs to be special text for why the user isn't able to attack this.
  158. /// </summary>
  159. [ByRefEvent]
  160. public record struct AttemptPacifiedAttackEvent(EntityUid User, bool Cancelled = false, string Reason = "pacified-cannot-harm-directly");