1
0

ThrownItemSystem.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. using System.Linq;
  2. using Content.Shared.Administration.Logs;
  3. using Content.Shared.Database;
  4. using Content.Shared.Gravity;
  5. using Content.Shared.Physics;
  6. using Content.Shared.Movement.Pulling.Events;
  7. using Robust.Shared.Physics;
  8. using Robust.Shared.Physics.Components;
  9. using Robust.Shared.Physics.Events;
  10. using Robust.Shared.Physics.Systems;
  11. using Robust.Shared.Timing;
  12. using Content.Shared.Barricade;
  13. using Robust.Shared.Random;
  14. namespace Content.Shared.Throwing
  15. {
  16. /// <summary>
  17. /// Handles throwing landing and collisions.
  18. /// </summary>
  19. public sealed class ThrownItemSystem : EntitySystem
  20. {
  21. [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
  22. [Dependency] private readonly IGameTiming _gameTiming = default!;
  23. [Dependency] private readonly SharedBroadphaseSystem _broadphase = default!;
  24. [Dependency] private readonly FixtureSystem _fixtures = default!;
  25. [Dependency] private readonly SharedPhysicsSystem _physics = default!;
  26. [Dependency] private readonly SharedGravitySystem _gravity = default!;
  27. [Dependency] private readonly SharedTransformSystem _transform = default!; private const string ThrowingFixture = "throw-fixture";
  28. [Dependency] private readonly IRobustRandom _random = default!;
  29. [Dependency] private readonly ILogManager _log = default!;
  30. private ISawmill _sawmill = default!;
  31. public override void Initialize()
  32. {
  33. base.Initialize();
  34. SubscribeLocalEvent<ThrownItemComponent, MapInitEvent>(OnMapInit);
  35. SubscribeLocalEvent<ThrownItemComponent, PhysicsSleepEvent>(OnSleep);
  36. SubscribeLocalEvent<ThrownItemComponent, StartCollideEvent>(HandleCollision);
  37. SubscribeLocalEvent<ThrownItemComponent, PreventCollideEvent>(PreventCollision);
  38. SubscribeLocalEvent<ThrownItemComponent, ThrownEvent>(ThrowItem);
  39. SubscribeLocalEvent<PullStartedMessage>(HandlePullStarted);
  40. _sawmill = _log.GetSawmill("throwing");
  41. }
  42. private void OnMapInit(EntityUid uid, ThrownItemComponent component, MapInitEvent args)
  43. {
  44. component.ThrownTime ??= _gameTiming.CurTime;
  45. }
  46. private void ThrowItem(EntityUid uid, ThrownItemComponent component, ref ThrownEvent @event)
  47. {
  48. if (!EntityManager.TryGetComponent(uid, out FixturesComponent? fixturesComponent) ||
  49. fixturesComponent.Fixtures.Count != 1 ||
  50. !TryComp<PhysicsComponent>(uid, out var body))
  51. {
  52. return;
  53. }
  54. var fixture = fixturesComponent.Fixtures.Values.First();
  55. var shape = fixture.Shape;
  56. _fixtures.TryCreateFixture(uid, shape, ThrowingFixture, hard: false, collisionMask: (int)CollisionGroup.ThrownItem, manager: fixturesComponent, body: body);
  57. }
  58. private void HandleCollision(EntityUid uid, ThrownItemComponent component, ref StartCollideEvent args)
  59. {
  60. if (!args.OtherFixture.Hard)
  61. return;
  62. if (args.OtherEntity == component.Thrower)
  63. return;
  64. ThrowCollideInteraction(component, args.OurEntity, args.OtherEntity);
  65. }
  66. private void PreventCollision(EntityUid uid, ThrownItemComponent component, ref PreventCollideEvent args)
  67. {
  68. if (args.OtherEntity == component.Thrower)
  69. {
  70. args.Cancelled = true;
  71. }
  72. //check for barricade component (percentage of chance to hit/pass over)
  73. if (TryComp(args.OtherEntity, out BarricadeComponent? barricade))
  74. {
  75. var alwaysPassThrough = false;
  76. //_sawmill.Info("Checking barricade...");
  77. if (component.Thrower is { } shooterUid && Exists(shooterUid))
  78. {
  79. // Condition 1: Directions are the same (using cardinal directions).
  80. // Or, if bidirectional, directions can be opposite.
  81. var shooterWorldRotation = _transform.GetWorldRotation(shooterUid);
  82. var barricadeWorldRotation = _transform.GetWorldRotation(args.OtherEntity);
  83. var shooterDir = shooterWorldRotation.GetCardinalDir();
  84. var barricadeDir = barricadeWorldRotation.GetCardinalDir();
  85. bool directionallyAllowed = false;
  86. if (shooterDir == barricadeDir)
  87. {
  88. directionallyAllowed = true;
  89. //_sawmill.Debug("Shooter and barricade facing same cardinal direction.");
  90. }
  91. else if (barricade.Bidirectional)
  92. {
  93. var oppositeBarricadeDir = (Direction)(((int)barricadeDir + 4) % 8);
  94. if (shooterDir == oppositeBarricadeDir)
  95. {
  96. directionallyAllowed = true;
  97. //_sawmill.Debug("Shooter and barricade facing opposite cardinal directions (bidirectional pass).");
  98. }
  99. }
  100. if (directionallyAllowed)
  101. {
  102. // Condition 2: Firer is within 1 tile of the barricade.
  103. var shooterCoords = Transform(shooterUid).Coordinates;
  104. var barricadeCoords = Transform(args.OtherEntity).Coordinates;
  105. if (shooterCoords.TryDistance(EntityManager, barricadeCoords, out var distance) &&
  106. distance <= 1.5f)
  107. {
  108. alwaysPassThrough = true;
  109. }
  110. }
  111. }
  112. if (alwaysPassThrough)
  113. {
  114. args.Cancelled = true;
  115. }
  116. else
  117. {
  118. //_sawmill.Debug("Barricade direction/distance check failed or shooter not valid.");
  119. // Standard barricade blocking logic if the special conditions are not met.
  120. var rando = _random.NextFloat(0.0f, 100.0f);
  121. if (rando >= 12)
  122. {
  123. args.Cancelled = true;
  124. return;
  125. }
  126. else
  127. {
  128. return;
  129. }
  130. }
  131. }
  132. }
  133. private void OnSleep(EntityUid uid, ThrownItemComponent thrownItem, ref PhysicsSleepEvent @event)
  134. {
  135. StopThrow(uid, thrownItem);
  136. }
  137. private void HandlePullStarted(PullStartedMessage message)
  138. {
  139. // TODO: this isn't directed so things have to be done the bad way
  140. if (EntityManager.TryGetComponent(message.PulledUid, out ThrownItemComponent? thrownItemComponent))
  141. StopThrow(message.PulledUid, thrownItemComponent);
  142. }
  143. public void StopThrow(EntityUid uid, ThrownItemComponent thrownItemComponent)
  144. {
  145. if (TryComp<PhysicsComponent>(uid, out var physics))
  146. {
  147. _physics.SetBodyStatus(uid, physics, BodyStatus.OnGround);
  148. if (physics.Awake)
  149. _broadphase.RegenerateContacts((uid, physics));
  150. }
  151. if (EntityManager.TryGetComponent(uid, out FixturesComponent? manager))
  152. {
  153. var fixture = _fixtures.GetFixtureOrNull(uid, ThrowingFixture, manager: manager);
  154. if (fixture != null)
  155. {
  156. _fixtures.DestroyFixture(uid, ThrowingFixture, fixture, manager: manager);
  157. }
  158. }
  159. EntityManager.EventBus.RaiseLocalEvent(uid, new StopThrowEvent { User = thrownItemComponent.Thrower }, true);
  160. EntityManager.RemoveComponent<ThrownItemComponent>(uid);
  161. }
  162. public void LandComponent(EntityUid uid, ThrownItemComponent thrownItem, PhysicsComponent physics, bool playSound)
  163. {
  164. if (thrownItem.Landed || thrownItem.Deleted || _gravity.IsWeightless(uid) || Deleted(uid))
  165. return;
  166. thrownItem.Landed = true;
  167. // Assume it's uninteresting if it has no thrower. For now anyway.
  168. if (thrownItem.Thrower is not null)
  169. _adminLogger.Add(LogType.Landed, LogImpact.Low, $"{ToPrettyString(uid):entity} thrown by {ToPrettyString(thrownItem.Thrower.Value):thrower} landed.");
  170. _broadphase.RegenerateContacts((uid, physics));
  171. var landEvent = new LandEvent(thrownItem.Thrower, playSound);
  172. RaiseLocalEvent(uid, ref landEvent);
  173. }
  174. /// <summary>
  175. /// Raises collision events on the thrown and target entities.
  176. /// </summary>
  177. public void ThrowCollideInteraction(ThrownItemComponent component, EntityUid thrown, EntityUid target)
  178. {
  179. if (component.Thrower is not null)
  180. _adminLogger.Add(LogType.ThrowHit, LogImpact.Low,
  181. $"{ToPrettyString(thrown):thrown} thrown by {ToPrettyString(component.Thrower.Value):thrower} hit {ToPrettyString(target):target}.");
  182. RaiseLocalEvent(target, new ThrowHitByEvent(thrown, target, component), true);
  183. RaiseLocalEvent(thrown, new ThrowDoHitEvent(thrown, target, component), true);
  184. }
  185. public override void Update(float frameTime)
  186. {
  187. base.Update(frameTime);
  188. // TODO predicted throwing - remove this check
  189. // We don't want to predict landing or stopping, since throwing isn't actually predicted.
  190. // If we do, the landing/stop will occur prematurely on the client.
  191. if (_gameTiming.InPrediction)
  192. return;
  193. var query = EntityQueryEnumerator<ThrownItemComponent, PhysicsComponent>();
  194. while (query.MoveNext(out var uid, out var thrown, out var physics))
  195. {
  196. if (thrown.LandTime <= _gameTiming.CurTime)
  197. {
  198. LandComponent(uid, thrown, physics, thrown.PlayLandSound);
  199. }
  200. var stopThrowTime = thrown.LandTime ?? thrown.ThrownTime;
  201. if (stopThrowTime <= _gameTiming.CurTime)
  202. {
  203. StopThrow(uid, thrown);
  204. }
  205. }
  206. }
  207. }
  208. }