ThrowingSystem.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. using System.Numerics;
  2. using Content.Shared.Administration.Logs;
  3. using Content.Shared.Camera;
  4. using Content.Shared.CCVar;
  5. using Content.Shared.Construction.Components;
  6. using Content.Shared.Database;
  7. using Content.Shared.Friction;
  8. using Content.Shared.Gravity;
  9. using Content.Shared.Projectiles;
  10. using Robust.Shared.Configuration;
  11. using Robust.Shared.Map;
  12. using Robust.Shared.Physics;
  13. using Robust.Shared.Physics.Components;
  14. using Robust.Shared.Physics.Systems;
  15. using Robust.Shared.Timing;
  16. namespace Content.Shared.Throwing;
  17. public sealed class ThrowingSystem : EntitySystem
  18. {
  19. public const float ThrowAngularImpulse = 5f;
  20. /// <summary>
  21. /// Speed cap on rotation in case of click-spam.
  22. /// </summary>
  23. public const float ThrowAngularCap = 3f * MathF.PI;
  24. public const float PushbackDefault = 2f;
  25. public const float FlyTimePercentage = 0.8f;
  26. private float _frictionModifier;
  27. [Dependency] private readonly IGameTiming _gameTiming = default!;
  28. [Dependency] private readonly SharedGravitySystem _gravity = default!;
  29. [Dependency] private readonly SharedPhysicsSystem _physics = default!;
  30. [Dependency] private readonly SharedTransformSystem _transform = default!;
  31. [Dependency] private readonly ThrownItemSystem _thrownSystem = default!;
  32. [Dependency] private readonly SharedCameraRecoilSystem _recoil = default!;
  33. [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
  34. [Dependency] private readonly IConfigurationManager _configManager = default!;
  35. public override void Initialize()
  36. {
  37. base.Initialize();
  38. Subs.CVar(_configManager, CCVars.TileFrictionModifier, value => _frictionModifier = value, true);
  39. }
  40. public void TryThrow(
  41. EntityUid uid,
  42. EntityCoordinates coordinates,
  43. float baseThrowSpeed = 10.0f,
  44. EntityUid? user = null,
  45. float pushbackRatio = PushbackDefault,
  46. float? friction = null,
  47. bool compensateFriction = false,
  48. bool recoil = true,
  49. bool animated = true,
  50. bool playSound = true,
  51. bool doSpin = true,
  52. bool unanchor = false)
  53. {
  54. var thrownPos = _transform.GetMapCoordinates(uid);
  55. var mapPos = _transform.ToMapCoordinates(coordinates);
  56. if (mapPos.MapId != thrownPos.MapId)
  57. return;
  58. TryThrow(uid, mapPos.Position - thrownPos.Position, baseThrowSpeed, user, pushbackRatio, friction, compensateFriction: compensateFriction, recoil: recoil, animated: animated, playSound: playSound, doSpin: doSpin, unanchor: unanchor);
  59. }
  60. /// <summary>
  61. /// Tries to throw the entity if it has a physics component, otherwise does nothing.
  62. /// </summary>
  63. /// <param name="uid">The entity being thrown.</param>
  64. /// <param name="direction">A vector pointing from the entity to its destination.</param>
  65. /// <param name="baseThrowSpeed">Throw velocity. Gets modified if compensateFriction is true.</param>
  66. /// <param name="pushbackRatio">The ratio of impulse applied to the thrower - defaults to 10 because otherwise it's not enough to properly recover from getting spaced</param>
  67. /// <param name="friction">friction value used for the distance calculation. If set to null this defaults to the standard tile values</param>
  68. /// <param name="compensateFriction">True will adjust the throw so the item stops at the target coordinates. False means it will land at the target and keep sliding.</param>
  69. /// <param name="doSpin">Whether spin will be applied to the thrown entity.</param>
  70. /// <param name="unanchor">If true and the thrown entity has <see cref="AnchorableComponent"/>, unanchor the thrown entity</param>
  71. public void TryThrow(EntityUid uid,
  72. Vector2 direction,
  73. float baseThrowSpeed = 10.0f,
  74. EntityUid? user = null,
  75. float pushbackRatio = PushbackDefault,
  76. float? friction = null,
  77. bool compensateFriction = false,
  78. bool recoil = true,
  79. bool animated = true,
  80. bool playSound = true,
  81. bool doSpin = true,
  82. bool unanchor = false)
  83. {
  84. var physicsQuery = GetEntityQuery<PhysicsComponent>();
  85. if (!physicsQuery.TryGetComponent(uid, out var physics))
  86. return;
  87. var projectileQuery = GetEntityQuery<ProjectileComponent>();
  88. TryThrow(
  89. uid,
  90. direction,
  91. physics,
  92. Transform(uid),
  93. projectileQuery,
  94. baseThrowSpeed,
  95. user,
  96. pushbackRatio,
  97. friction, compensateFriction: compensateFriction, recoil: recoil, animated: animated, playSound: playSound, doSpin: doSpin, unanchor: unanchor);
  98. }
  99. /// <summary>
  100. /// Tries to throw the entity if it has a physics component, otherwise does nothing.
  101. /// </summary>
  102. /// <param name="uid">The entity being thrown.</param>
  103. /// <param name="direction">A vector pointing from the entity to its destination.</param>
  104. /// <param name="baseThrowSpeed">Throw velocity. Gets modified if compensateFriction is true.</param>
  105. /// <param name="pushbackRatio">The ratio of impulse applied to the thrower - defaults to 10 because otherwise it's not enough to properly recover from getting spaced</param>
  106. /// <param name="friction">friction value used for the distance calculation. If set to null this defaults to the standard tile values</param>
  107. /// <param name="compensateFriction">True will adjust the throw so the item stops at the target coordinates. False means it will land at the target and keep sliding.</param>
  108. /// <param name="doSpin">Whether spin will be applied to the thrown entity.</param>
  109. /// <param name="unanchor">If true and the thrown entity has <see cref="AnchorableComponent"/>, unanchor the thrown entity</param>
  110. public void TryThrow(EntityUid uid,
  111. Vector2 direction,
  112. PhysicsComponent physics,
  113. TransformComponent transform,
  114. EntityQuery<ProjectileComponent> projectileQuery,
  115. float baseThrowSpeed = 10.0f,
  116. EntityUid? user = null,
  117. float pushbackRatio = PushbackDefault,
  118. float? friction = null,
  119. bool compensateFriction = false,
  120. bool recoil = true,
  121. bool animated = true,
  122. bool playSound = true,
  123. bool doSpin = true,
  124. bool unanchor = false)
  125. {
  126. if (baseThrowSpeed <= 0 || direction == Vector2Helpers.Infinity || direction == Vector2Helpers.NaN || direction == Vector2.Zero || friction < 0)
  127. return;
  128. if (unanchor && HasComp<AnchorableComponent>(uid))
  129. _transform.Unanchor(uid);
  130. if ((physics.BodyType & (BodyType.Dynamic | BodyType.KinematicController)) == 0x0)
  131. return;
  132. // Allow throwing if this projectile only acts as a projectile when shot, otherwise disallow
  133. if (projectileQuery.TryGetComponent(uid, out var proj) && !proj.OnlyCollideWhenShot)
  134. return;
  135. var comp = new ThrownItemComponent
  136. {
  137. Thrower = user,
  138. Animate = animated,
  139. };
  140. // if not given, get the default friction value for distance calculation
  141. var tileFriction = friction ?? _frictionModifier * TileFrictionController.DefaultFriction;
  142. if (tileFriction == 0f)
  143. compensateFriction = false; // cannot calculate this if there is no friction
  144. // Set the time the item is supposed to be in the air so we can apply OnGround status.
  145. // This is a free parameter, but we should set it to something reasonable.
  146. var flyTime = direction.Length() / baseThrowSpeed;
  147. if (compensateFriction)
  148. flyTime *= FlyTimePercentage;
  149. comp.ThrownTime = _gameTiming.CurTime;
  150. comp.LandTime = comp.ThrownTime + TimeSpan.FromSeconds(flyTime);
  151. comp.PlayLandSound = playSound;
  152. AddComp(uid, comp, true);
  153. ThrowingAngleComponent? throwingAngle = null;
  154. // Give it a l'il spin.
  155. if (doSpin)
  156. {
  157. if (physics.InvI > 0f && (!TryComp(uid, out throwingAngle) || throwingAngle.AngularVelocity))
  158. {
  159. _physics.ApplyAngularImpulse(uid, ThrowAngularImpulse / physics.InvI, body: physics);
  160. }
  161. else
  162. {
  163. Resolve(uid, ref throwingAngle, false);
  164. var gridRot = _transform.GetWorldRotation(transform.ParentUid);
  165. var angle = direction.ToWorldAngle() - gridRot;
  166. var offset = throwingAngle?.Angle ?? Angle.Zero;
  167. _transform.SetLocalRotation(uid, angle + offset);
  168. }
  169. }
  170. var throwEvent = new ThrownEvent(user, uid);
  171. RaiseLocalEvent(uid, ref throwEvent, true);
  172. if (user != null)
  173. _adminLogger.Add(LogType.Throw, LogImpact.Low, $"{ToPrettyString(user.Value):user} threw {ToPrettyString(uid):entity}");
  174. // if compensateFriction==true compensate for the distance the item will slide over the floor after landing by reducing the throw speed accordingly.
  175. // else let the item land on the cursor and from where it slides a little further.
  176. // This is an exact formula we get from exponentially decaying velocity after landing.
  177. // If someone changes how tile friction works at some point, this will have to be adjusted.
  178. var throwSpeed = compensateFriction ? direction.Length() / (flyTime + 1 / tileFriction) : baseThrowSpeed;
  179. var impulseVector = direction.Normalized() * throwSpeed * physics.Mass;
  180. _physics.ApplyLinearImpulse(uid, impulseVector, body: physics);
  181. if (comp.LandTime == null || comp.LandTime <= TimeSpan.Zero)
  182. {
  183. _thrownSystem.LandComponent(uid, comp, physics, playSound);
  184. }
  185. else
  186. {
  187. _physics.SetBodyStatus(uid, physics, BodyStatus.InAir);
  188. }
  189. if (user == null)
  190. return;
  191. if (recoil)
  192. _recoil.KickCamera(user.Value, -direction * 0.04f);
  193. // Give thrower an impulse in the other direction
  194. if (pushbackRatio != 0.0f &&
  195. physics.Mass > 0f &&
  196. TryComp(user.Value, out PhysicsComponent? userPhysics) &&
  197. _gravity.IsWeightless(user.Value, userPhysics))
  198. {
  199. var msg = new ThrowPushbackAttemptEvent();
  200. RaiseLocalEvent(uid, msg);
  201. const float massLimit = 5f;
  202. if (!msg.Cancelled)
  203. _physics.ApplyLinearImpulse(user.Value, -impulseVector / physics.Mass * pushbackRatio * MathF.Min(massLimit, physics.Mass), body: userPhysics);
  204. }
  205. }
  206. }