ReflectSystem.cs 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. using System.Diagnostics.CodeAnalysis;
  2. using System.Numerics;
  3. using Content.Shared.Administration.Logs;
  4. using Content.Shared.Alert;
  5. using Content.Shared.Audio;
  6. using Content.Shared.Database;
  7. using Content.Shared.Hands;
  8. using Content.Shared.Inventory;
  9. using Content.Shared.Inventory.Events;
  10. using Content.Shared.Item.ItemToggle;
  11. using Content.Shared.Item.ItemToggle.Components;
  12. using Content.Shared.Popups;
  13. using Content.Shared.Projectiles;
  14. using Content.Shared.Weapons.Ranged.Components;
  15. using Content.Shared.Weapons.Ranged.Events;
  16. using Robust.Shared.Audio;
  17. using Robust.Shared.Audio.Systems;
  18. using Robust.Shared.Network;
  19. using Robust.Shared.Physics.Components;
  20. using Robust.Shared.Physics.Systems;
  21. using Robust.Shared.Random;
  22. using Robust.Shared.Timing;
  23. namespace Content.Shared.Weapons.Reflect;
  24. /// <summary>
  25. /// This handles reflecting projectiles and hitscan shots.
  26. /// </summary>
  27. public sealed class ReflectSystem : EntitySystem
  28. {
  29. [Dependency] private readonly IGameTiming _gameTiming = default!;
  30. [Dependency] private readonly INetManager _netManager = default!;
  31. [Dependency] private readonly IRobustRandom _random = default!;
  32. [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
  33. [Dependency] private readonly ItemToggleSystem _toggle = default!;
  34. [Dependency] private readonly SharedPopupSystem _popup = default!;
  35. [Dependency] private readonly SharedPhysicsSystem _physics = default!;
  36. [Dependency] private readonly SharedAudioSystem _audio = default!;
  37. [Dependency] private readonly SharedTransformSystem _transform = default!;
  38. [Dependency] private readonly InventorySystem _inventorySystem = default!;
  39. public override void Initialize()
  40. {
  41. base.Initialize();
  42. SubscribeLocalEvent<ReflectComponent, ProjectileReflectAttemptEvent>(OnReflectCollide);
  43. SubscribeLocalEvent<ReflectComponent, HitScanReflectAttemptEvent>(OnReflectHitscan);
  44. SubscribeLocalEvent<ReflectComponent, GotEquippedEvent>(OnReflectEquipped);
  45. SubscribeLocalEvent<ReflectComponent, GotUnequippedEvent>(OnReflectUnequipped);
  46. SubscribeLocalEvent<ReflectComponent, GotEquippedHandEvent>(OnReflectHandEquipped);
  47. SubscribeLocalEvent<ReflectComponent, GotUnequippedHandEvent>(OnReflectHandUnequipped);
  48. SubscribeLocalEvent<ReflectComponent, ItemToggledEvent>(OnToggleReflect);
  49. SubscribeLocalEvent<ReflectUserComponent, ProjectileReflectAttemptEvent>(OnReflectUserCollide);
  50. SubscribeLocalEvent<ReflectUserComponent, HitScanReflectAttemptEvent>(OnReflectUserHitscan);
  51. }
  52. private void OnReflectUserHitscan(EntityUid uid, ReflectUserComponent component, ref HitScanReflectAttemptEvent args)
  53. {
  54. if (args.Reflected)
  55. return;
  56. foreach (var ent in _inventorySystem.GetHandOrInventoryEntities(uid, SlotFlags.All & ~SlotFlags.POCKET))
  57. {
  58. if (!TryReflectHitscan(uid, ent, args.Shooter, args.SourceItem, args.Direction, out var dir))
  59. continue;
  60. args.Direction = dir.Value;
  61. args.Reflected = true;
  62. break;
  63. }
  64. }
  65. private void OnReflectUserCollide(EntityUid uid, ReflectUserComponent component, ref ProjectileReflectAttemptEvent args)
  66. {
  67. foreach (var ent in _inventorySystem.GetHandOrInventoryEntities(uid, SlotFlags.All & ~SlotFlags.POCKET))
  68. {
  69. if (!TryReflectProjectile(uid, ent, args.ProjUid))
  70. continue;
  71. args.Cancelled = true;
  72. break;
  73. }
  74. }
  75. private void OnReflectCollide(EntityUid uid, ReflectComponent component, ref ProjectileReflectAttemptEvent args)
  76. {
  77. if (args.Cancelled)
  78. return;
  79. if (TryReflectProjectile(uid, uid, args.ProjUid, reflect: component))
  80. args.Cancelled = true;
  81. }
  82. private bool TryReflectProjectile(EntityUid user, EntityUid reflector, EntityUid projectile, ProjectileComponent? projectileComp = null, ReflectComponent? reflect = null)
  83. {
  84. if (!Resolve(reflector, ref reflect, false) ||
  85. !_toggle.IsActivated(reflector) ||
  86. !TryComp<ReflectiveComponent>(projectile, out var reflective) ||
  87. (reflect.Reflects & reflective.Reflective) == 0x0 ||
  88. !_random.Prob(reflect.ReflectProb) ||
  89. !TryComp<PhysicsComponent>(projectile, out var physics))
  90. {
  91. return false;
  92. }
  93. var rotation = _random.NextAngle(-reflect.Spread / 2, reflect.Spread / 2).Opposite();
  94. var existingVelocity = _physics.GetMapLinearVelocity(projectile, component: physics);
  95. var relativeVelocity = existingVelocity - _physics.GetMapLinearVelocity(user);
  96. var newVelocity = rotation.RotateVec(relativeVelocity);
  97. // Have the velocity in world terms above so need to convert it back to local.
  98. var difference = newVelocity - existingVelocity;
  99. _physics.SetLinearVelocity(projectile, physics.LinearVelocity + difference, body: physics);
  100. var locRot = Transform(projectile).LocalRotation;
  101. var newRot = rotation.RotateVec(locRot.ToVec());
  102. _transform.SetLocalRotation(projectile, newRot.ToAngle());
  103. if (_netManager.IsServer)
  104. {
  105. _popup.PopupEntity(Loc.GetString("reflect-shot"), user);
  106. _audio.PlayPvs(reflect.SoundOnReflect, user, AudioHelpers.WithVariation(0.05f, _random));
  107. }
  108. if (Resolve(projectile, ref projectileComp, false))
  109. {
  110. _adminLogger.Add(LogType.BulletHit, LogImpact.Medium, $"{ToPrettyString(user)} reflected {ToPrettyString(projectile)} from {ToPrettyString(projectileComp.Weapon)} shot by {projectileComp.Shooter}");
  111. projectileComp.Shooter = user;
  112. projectileComp.Weapon = user;
  113. Dirty(projectile, projectileComp);
  114. }
  115. else
  116. {
  117. _adminLogger.Add(LogType.BulletHit, LogImpact.Medium, $"{ToPrettyString(user)} reflected {ToPrettyString(projectile)}");
  118. }
  119. return true;
  120. }
  121. private void OnReflectHitscan(EntityUid uid, ReflectComponent component, ref HitScanReflectAttemptEvent args)
  122. {
  123. if (args.Reflected ||
  124. (component.Reflects & args.Reflective) == 0x0)
  125. {
  126. return;
  127. }
  128. if (TryReflectHitscan(uid, uid, args.Shooter, args.SourceItem, args.Direction, out var dir))
  129. {
  130. args.Direction = dir.Value;
  131. args.Reflected = true;
  132. }
  133. }
  134. private bool TryReflectHitscan(
  135. EntityUid user,
  136. EntityUid reflector,
  137. EntityUid? shooter,
  138. EntityUid shotSource,
  139. Vector2 direction,
  140. [NotNullWhen(true)] out Vector2? newDirection)
  141. {
  142. if (!TryComp<ReflectComponent>(reflector, out var reflect) ||
  143. !_toggle.IsActivated(reflector) ||
  144. !_random.Prob(reflect.ReflectProb))
  145. {
  146. newDirection = null;
  147. return false;
  148. }
  149. if (_netManager.IsServer)
  150. {
  151. _popup.PopupEntity(Loc.GetString("reflect-shot"), user);
  152. _audio.PlayPvs(reflect.SoundOnReflect, user, AudioHelpers.WithVariation(0.05f, _random));
  153. }
  154. var spread = _random.NextAngle(-reflect.Spread / 2, reflect.Spread / 2);
  155. newDirection = -spread.RotateVec(direction);
  156. if (shooter != null)
  157. _adminLogger.Add(LogType.HitScanHit, LogImpact.Medium, $"{ToPrettyString(user)} reflected hitscan from {ToPrettyString(shotSource)} shot by {ToPrettyString(shooter.Value)}");
  158. else
  159. _adminLogger.Add(LogType.HitScanHit, LogImpact.Medium, $"{ToPrettyString(user)} reflected hitscan from {ToPrettyString(shotSource)}");
  160. return true;
  161. }
  162. private void OnReflectEquipped(EntityUid uid, ReflectComponent component, GotEquippedEvent args)
  163. {
  164. if (_gameTiming.ApplyingState)
  165. return;
  166. EnsureComp<ReflectUserComponent>(args.Equipee);
  167. }
  168. private void OnReflectUnequipped(EntityUid uid, ReflectComponent comp, GotUnequippedEvent args)
  169. {
  170. RefreshReflectUser(args.Equipee);
  171. }
  172. private void OnReflectHandEquipped(EntityUid uid, ReflectComponent component, GotEquippedHandEvent args)
  173. {
  174. if (_gameTiming.ApplyingState)
  175. return;
  176. EnsureComp<ReflectUserComponent>(args.User);
  177. }
  178. private void OnReflectHandUnequipped(EntityUid uid, ReflectComponent component, GotUnequippedHandEvent args)
  179. {
  180. RefreshReflectUser(args.User);
  181. }
  182. private void OnToggleReflect(EntityUid uid, ReflectComponent comp, ref ItemToggledEvent args)
  183. {
  184. if (args.User is {} user)
  185. RefreshReflectUser(user);
  186. }
  187. /// <summary>
  188. /// Refreshes whether someone has reflection potential so we can raise directed events on them.
  189. /// </summary>
  190. private void RefreshReflectUser(EntityUid user)
  191. {
  192. foreach (var ent in _inventorySystem.GetHandOrInventoryEntities(user, SlotFlags.All & ~SlotFlags.POCKET))
  193. {
  194. if (!HasComp<ReflectComponent>(ent) || !_toggle.IsActivated(ent))
  195. continue;
  196. EnsureComp<ReflectUserComponent>(user);
  197. return;
  198. }
  199. RemCompDeferred<ReflectUserComponent>(user);
  200. }
  201. }