2023-07-08 14:08:32 +10:00
using System.Numerics ;
2023-12-04 09:32:17 +03:00
using Content.Shared.Administration.Logs ;
2024-01-30 03:50:41 -07:00
using Content.Shared.Camera ;
2024-07-08 11:03:53 +02:00
using Content.Shared.CCVar ;
2023-12-04 09:32:17 +03:00
using Content.Shared.Database ;
2024-07-08 11:03:53 +02:00
using Content.Shared.Friction ;
2022-07-18 19:49:52 +10:00
using Content.Shared.Gravity ;
2023-05-07 14:57:23 +12:00
using Content.Shared.Projectiles ;
2024-07-08 11:03:53 +02:00
using Robust.Shared.Configuration ;
2023-05-19 17:10:31 +10:00
using Robust.Shared.Map ;
2022-03-24 02:33:01 +13:00
using Robust.Shared.Physics ;
2022-09-14 17:26:26 +10:00
using Robust.Shared.Physics.Components ;
2022-12-28 03:58:53 +11:00
using Robust.Shared.Physics.Systems ;
2022-03-24 02:33:01 +13:00
using Robust.Shared.Timing ;
namespace Content.Shared.Throwing ;
public sealed class ThrowingSystem : EntitySystem
{
2023-05-13 09:54:37 +10:00
public const float ThrowAngularImpulse = 5f ;
2024-03-19 14:30:56 +11:00
/// <summary>
/// Speed cap on rotation in case of click-spam.
/// </summary>
public const float ThrowAngularCap = 3f * MathF . PI ;
2023-05-29 17:11:54 +10:00
public const float PushbackDefault = 2f ;
2022-03-24 02:33:01 +13:00
2024-07-08 11:03:53 +02:00
public const float FlyTimePercentage = 0.8f ;
private float _frictionModifier ;
2022-03-24 02:33:01 +13:00
2023-10-06 17:43:54 -07:00
[Dependency] private readonly IGameTiming _gameTiming = default ! ;
2022-07-18 19:49:52 +10:00
[Dependency] private readonly SharedGravitySystem _gravity = default ! ;
2022-12-28 03:58:53 +11:00
[Dependency] private readonly SharedPhysicsSystem _physics = default ! ;
2023-05-19 17:10:31 +10:00
[Dependency] private readonly SharedTransformSystem _transform = default ! ;
2022-03-24 02:33:01 +13:00
[Dependency] private readonly ThrownItemSystem _thrownSystem = default ! ;
2024-01-30 03:50:41 -07:00
[Dependency] private readonly SharedCameraRecoilSystem _recoil = default ! ;
2023-12-04 09:32:17 +03:00
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default ! ;
2024-07-08 11:03:53 +02:00
[Dependency] private readonly IConfigurationManager _configManager = default ! ;
public override void Initialize ( )
{
base . Initialize ( ) ;
Subs . CVar ( _configManager , CCVars . TileFrictionModifier , value = > _frictionModifier = value , true ) ;
}
2022-03-24 02:33:01 +13:00
2023-05-19 17:10:31 +10:00
public void TryThrow (
EntityUid uid ,
EntityCoordinates coordinates ,
2024-07-08 11:03:53 +02:00
float baseThrowSpeed = 10.0f ,
2023-05-19 17:10:31 +10:00
EntityUid ? user = null ,
float pushbackRatio = PushbackDefault ,
2024-07-08 11:03:53 +02:00
float? friction = null ,
bool compensateFriction = false ,
2024-03-19 14:30:56 +11:00
bool recoil = true ,
bool animated = true ,
2024-03-29 20:35:42 -07:00
bool playSound = true ,
bool doSpin = true )
2023-05-19 17:10:31 +10:00
{
2024-03-19 14:30:56 +11:00
var thrownPos = _transform . GetMapCoordinates ( uid ) ;
var mapPos = _transform . ToMapCoordinates ( coordinates ) ;
2023-05-19 17:10:31 +10:00
if ( mapPos . MapId ! = thrownPos . MapId )
return ;
2024-07-08 11:03:53 +02:00
TryThrow ( uid , mapPos . Position - thrownPos . Position , baseThrowSpeed , user , pushbackRatio , friction , compensateFriction : compensateFriction , recoil : recoil , animated : animated , playSound : playSound , doSpin : doSpin ) ;
2023-05-19 17:10:31 +10:00
}
2022-03-24 02:33:01 +13:00
/// <summary>
/// Tries to throw the entity if it has a physics component, otherwise does nothing.
/// </summary>
2022-07-18 19:49:52 +10:00
/// <param name="uid">The entity being thrown.</param>
2022-03-24 02:33:01 +13:00
/// <param name="direction">A vector pointing from the entity to its destination.</param>
2024-07-08 11:03:53 +02:00
/// <param name="baseThrowSpeed">Throw velocity. Gets modified if compensateFriction is true.</param>
2022-03-24 02:33:01 +13:00
/// <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>
2024-07-08 11:03:53 +02:00
/// <param name="friction">friction value used for the distance calculation. If set to null this defaults to the standard tile values</param>
/// <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>
2024-03-29 20:35:42 -07:00
/// <param name="doSpin">Whether spin will be applied to the thrown entity.</param>
2023-05-07 14:57:23 +12:00
public void TryThrow ( EntityUid uid ,
2022-03-24 02:33:01 +13:00
Vector2 direction ,
2024-07-08 11:03:53 +02:00
float baseThrowSpeed = 10.0f ,
2022-03-24 02:33:01 +13:00
EntityUid ? user = null ,
2023-05-19 17:10:31 +10:00
float pushbackRatio = PushbackDefault ,
2024-07-08 11:03:53 +02:00
float? friction = null ,
bool compensateFriction = false ,
2024-03-19 14:30:56 +11:00
bool recoil = true ,
bool animated = true ,
2024-03-29 20:35:42 -07:00
bool playSound = true ,
bool doSpin = true )
2022-03-24 02:33:01 +13:00
{
2023-05-07 14:57:23 +12:00
var physicsQuery = GetEntityQuery < PhysicsComponent > ( ) ;
if ( ! physicsQuery . TryGetComponent ( uid , out var physics ) )
2022-03-24 02:33:01 +13:00
return ;
2023-05-07 14:57:23 +12:00
var projectileQuery = GetEntityQuery < ProjectileComponent > ( ) ;
TryThrow (
uid ,
direction ,
physics ,
Transform ( uid ) ,
projectileQuery ,
2024-07-08 11:03:53 +02:00
baseThrowSpeed ,
2023-05-07 14:57:23 +12:00
user ,
2024-07-08 11:03:53 +02:00
pushbackRatio ,
friction , compensateFriction : compensateFriction , recoil : recoil , animated : animated , playSound : playSound , doSpin : doSpin ) ;
2023-05-07 14:57:23 +12:00
}
/// <summary>
/// Tries to throw the entity if it has a physics component, otherwise does nothing.
/// </summary>
/// <param name="uid">The entity being thrown.</param>
/// <param name="direction">A vector pointing from the entity to its destination.</param>
2024-07-08 11:03:53 +02:00
/// <param name="baseThrowSpeed">Throw velocity. Gets modified if compensateFriction is true.</param>
2023-05-07 14:57:23 +12:00
/// <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>
2024-07-08 11:03:53 +02:00
/// <param name="friction">friction value used for the distance calculation. If set to null this defaults to the standard tile values</param>
/// <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>
2024-03-29 20:35:42 -07:00
/// <param name="doSpin">Whether spin will be applied to the thrown entity.</param>
2023-05-07 14:57:23 +12:00
public void TryThrow ( EntityUid uid ,
Vector2 direction ,
PhysicsComponent physics ,
TransformComponent transform ,
EntityQuery < ProjectileComponent > projectileQuery ,
2024-07-08 11:03:53 +02:00
float baseThrowSpeed = 10.0f ,
2023-05-07 14:57:23 +12:00
EntityUid ? user = null ,
2023-05-19 17:10:31 +10:00
float pushbackRatio = PushbackDefault ,
2024-07-08 11:03:53 +02:00
float? friction = null ,
bool compensateFriction = false ,
2024-03-19 14:30:56 +11:00
bool recoil = true ,
bool animated = true ,
2024-03-29 20:35:42 -07:00
bool playSound = true ,
bool doSpin = true )
2023-05-07 14:57:23 +12:00
{
2024-07-08 11:03:53 +02:00
if ( baseThrowSpeed < = 0 | | direction = = Vector2Helpers . Infinity | | direction = = Vector2Helpers . NaN | | direction = = Vector2 . Zero | | friction < 0 )
2022-03-24 02:33:01 +13:00
return ;
2022-07-16 13:04:14 +10:00
if ( ( physics . BodyType & ( BodyType . Dynamic | BodyType . KinematicController ) ) = = 0x0 )
2022-03-24 02:33:01 +13:00
{
2023-08-04 18:56:39 +10:00
Log . Warning ( $"Tried to throw entity {ToPrettyString(uid)} but can't throw {physics.BodyType} bodies!" ) ;
2022-03-24 02:33:01 +13:00
return ;
}
2023-09-22 02:45:21 -07:00
// Allow throwing if this projectile only acts as a projectile when shot, otherwise disallow
if ( projectileQuery . TryGetComponent ( uid , out var proj ) & & ! proj . OnlyCollideWhenShot )
2023-05-07 14:57:23 +12:00
return ;
2024-03-19 14:30:56 +11:00
var comp = new ThrownItemComponent
{
Thrower = user ,
Animate = animated ,
} ;
2023-10-06 17:43:54 -07:00
2024-07-08 11:03:53 +02:00
// if not given, get the default friction value for distance calculation
var tileFriction = friction ? ? _frictionModifier * TileFrictionController . DefaultFriction ;
if ( tileFriction = = 0f )
compensateFriction = false ; // cannot calculate this if there is no friction
// Set the time the item is supposed to be in the air so we can apply OnGround status.
// This is a free parameter, but we should set it to something reasonable.
var flyTime = direction . Length ( ) / baseThrowSpeed ;
if ( compensateFriction )
flyTime * = FlyTimePercentage ;
2023-10-06 17:43:54 -07:00
comp . ThrownTime = _gameTiming . CurTime ;
2024-07-08 11:03:53 +02:00
comp . LandTime = comp . ThrownTime + TimeSpan . FromSeconds ( flyTime ) ;
2023-10-06 17:43:54 -07:00
comp . PlayLandSound = playSound ;
2024-01-30 03:50:41 -07:00
AddComp ( uid , comp , true ) ;
2023-10-06 17:43:54 -07:00
2023-08-04 18:56:39 +10:00
ThrowingAngleComponent ? throwingAngle = null ;
2023-05-07 14:57:23 +12:00
2022-03-24 02:33:01 +13:00
// Give it a l'il spin.
2024-03-29 20:35:42 -07:00
if ( doSpin )
2023-08-04 18:56:39 +10:00
{
2024-03-29 20:35:42 -07:00
if ( physics . InvI > 0f & & ( ! TryComp ( uid , out throwingAngle ) | | throwingAngle . AngularVelocity ) )
{
_physics . ApplyAngularImpulse ( uid , ThrowAngularImpulse / physics . InvI , body : physics ) ;
}
else
{
Resolve ( uid , ref throwingAngle , false ) ;
var gridRot = _transform . GetWorldRotation ( transform . ParentUid ) ;
var angle = direction . ToWorldAngle ( ) - gridRot ;
var offset = throwingAngle ? . Angle ? ? Angle . Zero ;
_transform . SetLocalRotation ( uid , angle + offset ) ;
}
2023-08-04 18:56:39 +10:00
}
2022-03-24 02:33:01 +13:00
2023-12-04 09:32:17 +03:00
var throwEvent = new ThrownEvent ( user , uid ) ;
RaiseLocalEvent ( uid , ref throwEvent , true ) ;
2022-03-24 02:33:01 +13:00
if ( user ! = null )
2023-12-04 09:32:17 +03:00
_adminLogger . Add ( LogType . Throw , LogImpact . Low , $"{ToPrettyString(user.Value):user} threw {ToPrettyString(uid):entity}" ) ;
2022-03-24 02:33:01 +13:00
2024-07-08 11:03:53 +02:00
// if compensateFriction==true compensate for the distance the item will slide over the floor after landing by reducing the throw speed accordingly.
// else let the item land on the cursor and from where it slides a little further.
// This is an exact formula we get from exponentially decaying velocity after landing.
// If someone changes how tile friction works at some point, this will have to be adjusted.
var throwSpeed = compensateFriction ? direction . Length ( ) / ( flyTime + 1 / tileFriction ) : baseThrowSpeed ;
var impulseVector = direction . Normalized ( ) * throwSpeed * physics . Mass ;
2023-01-15 15:38:59 +11:00
_physics . ApplyLinearImpulse ( uid , impulseVector , body : physics ) ;
2022-03-24 02:33:01 +13:00
2023-12-27 18:05:20 -05:00
if ( comp . LandTime = = null | | comp . LandTime < = TimeSpan . Zero )
2022-03-24 02:33:01 +13:00
{
2023-05-19 17:10:31 +10:00
_thrownSystem . LandComponent ( uid , comp , physics , playSound ) ;
2022-03-24 02:33:01 +13:00
}
else
{
2024-03-25 02:37:25 -04:00
_physics . SetBodyStatus ( uid , physics , BodyStatus . InAir ) ;
2022-03-24 02:33:01 +13:00
}
2024-01-30 03:50:41 -07:00
if ( user = = null )
return ;
2024-03-19 14:30:56 +11:00
if ( recoil )
_recoil . KickCamera ( user . Value , - direction * 0.04f ) ;
2024-01-30 03:50:41 -07:00
2022-03-24 02:33:01 +13:00
// Give thrower an impulse in the other direction
2024-01-30 03:50:41 -07:00
if ( pushbackRatio ! = 0.0f & &
2023-05-13 09:54:37 +10:00
physics . Mass > 0f & &
2023-05-07 14:57:23 +12:00
TryComp ( user . Value , out PhysicsComponent ? userPhysics ) & &
2022-07-18 19:49:52 +10:00
_gravity . IsWeightless ( user . Value , userPhysics ) )
2022-03-24 02:33:01 +13:00
{
var msg = new ThrowPushbackAttemptEvent ( ) ;
2023-03-11 19:26:01 +11:00
RaiseLocalEvent ( uid , msg ) ;
2024-03-25 02:37:25 -04:00
const float massLimit = 5f ;
2022-03-24 02:33:01 +13:00
if ( ! msg . Cancelled )
2024-03-25 02:37:25 -04:00
_physics . ApplyLinearImpulse ( user . Value , - impulseVector / physics . Mass * pushbackRatio * MathF . Min ( massLimit , physics . Mass ) , body : userPhysics ) ;
2022-03-24 02:33:01 +13:00
}
}
}