diff --git a/Content.Client/Throwing/ThrownItemVisualizerSystem.cs b/Content.Client/Throwing/ThrownItemVisualizerSystem.cs
new file mode 100644
index 0000000000..bbd3673110
--- /dev/null
+++ b/Content.Client/Throwing/ThrownItemVisualizerSystem.cs
@@ -0,0 +1,87 @@
+using Content.Shared.Throwing;
+using Robust.Client.Animations;
+using Robust.Client.GameObjects;
+using Robust.Shared.Animations;
+
+namespace Content.Client.Throwing;
+
+///
+/// Handles animating thrown items.
+///
+public sealed class ThrownItemVisualizerSystem : EntitySystem
+{
+ [Dependency] private readonly AnimationPlayerSystem _anim = default!;
+
+ private const string AnimationKey = "thrown-item";
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnAutoHandleState);
+ SubscribeLocalEvent(OnShutdown);
+ }
+
+ private void OnAutoHandleState(EntityUid uid, ThrownItemComponent component, ref AfterAutoHandleStateEvent args)
+ {
+ if (!TryComp(uid, out var sprite))
+ return;
+
+ var animationPlayer = EnsureComp(uid);
+
+ if (_anim.HasRunningAnimation(uid, animationPlayer, AnimationKey))
+ return;
+
+ var anim = GetAnimation((uid, component, sprite));
+ if (anim == null)
+ return;
+
+ component.OriginalScale = sprite.Scale;
+ _anim.Play((uid, animationPlayer), anim, AnimationKey);
+ }
+
+ private void OnShutdown(EntityUid uid, ThrownItemComponent component, ComponentShutdown args)
+ {
+ if (!_anim.HasRunningAnimation(uid, AnimationKey))
+ return;
+
+ if (TryComp(uid, out var sprite) && component.OriginalScale != null)
+ sprite.Scale = component.OriginalScale.Value;
+
+ _anim.Stop(uid, AnimationKey);
+ }
+
+ private static Animation? GetAnimation(Entity ent)
+ {
+ if (ent.Comp1.LandTime - ent.Comp1.ThrownTime is not { } length)
+ return null;
+
+ if (length <= TimeSpan.Zero)
+ return null;
+
+ length += TimeSpan.FromSeconds(ThrowingSystem.FlyTime);
+ var scale = ent.Comp2.Scale;
+ var lenFloat = (float) length.TotalSeconds;
+
+ // TODO use like actual easings here
+ return new Animation
+ {
+ Length = length,
+ AnimationTracks =
+ {
+ new AnimationTrackComponentProperty
+ {
+ ComponentType = typeof(SpriteComponent),
+ Property = nameof(SpriteComponent.Scale),
+ KeyFrames =
+ {
+ new AnimationTrackProperty.KeyFrame(scale, 0.0f),
+ new AnimationTrackProperty.KeyFrame(scale * 1.4f, lenFloat * 0.25f),
+ new AnimationTrackProperty.KeyFrame(scale, lenFloat * 0.75f)
+ },
+ InterpolationMode = AnimationInterpolationMode.Linear
+ }
+ }
+ };
+ }
+}
diff --git a/Content.Shared/Throwing/ThrowingSystem.cs b/Content.Shared/Throwing/ThrowingSystem.cs
index e631546411..5fe02a0571 100644
--- a/Content.Shared/Throwing/ThrowingSystem.cs
+++ b/Content.Shared/Throwing/ThrowingSystem.cs
@@ -1,5 +1,6 @@
using System.Numerics;
using Content.Shared.Administration.Logs;
+using Content.Shared.Camera;
using Content.Shared.Database;
using Content.Shared.Gravity;
using Content.Shared.Hands.Components;
@@ -32,6 +33,7 @@ public sealed class ThrowingSystem : EntitySystem
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly ThrownItemSystem _thrownSystem = default!;
+ [Dependency] private readonly SharedCameraRecoilSystem _recoil = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
public void TryThrow(
@@ -114,7 +116,7 @@ public sealed class ThrowingSystem : EntitySystem
if (projectileQuery.TryGetComponent(uid, out var proj) && !proj.OnlyCollideWhenShot)
return;
- var comp = EnsureComp(uid);
+ var comp = new ThrownItemComponent();
comp.Thrower = user;
// Estimate time to arrival so we can apply OnGround status and slow it much faster.
@@ -126,6 +128,7 @@ public sealed class ThrowingSystem : EntitySystem
else
comp.LandTime = time < FlyTime ? default : comp.ThrownTime + TimeSpan.FromSeconds(time - FlyTime);
comp.PlayLandSound = playSound;
+ AddComp(uid, comp, true);
ThrowingAngleComponent? throwingAngle = null;
@@ -160,9 +163,13 @@ public sealed class ThrowingSystem : EntitySystem
_physics.SetBodyStatus(physics, BodyStatus.InAir);
}
+ if (user == null)
+ return;
+
+ _recoil.KickCamera(user.Value, -direction * 0.3f);
+
// Give thrower an impulse in the other direction
- if (user != null &&
- pushbackRatio != 0.0f &&
+ if (pushbackRatio != 0.0f &&
physics.Mass > 0f &&
TryComp(user.Value, out PhysicsComponent? userPhysics) &&
_gravity.IsWeightless(user.Value, userPhysics))
diff --git a/Content.Shared/Throwing/ThrownItemComponent.cs b/Content.Shared/Throwing/ThrownItemComponent.cs
index c6c9c4a446..ab80e07938 100644
--- a/Content.Shared/Throwing/ThrownItemComponent.cs
+++ b/Content.Shared/Throwing/ThrownItemComponent.cs
@@ -1,54 +1,47 @@
+using System.Numerics;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
namespace Content.Shared.Throwing
{
- [RegisterComponent, NetworkedComponent]
+ [RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
public sealed partial class ThrownItemComponent : Component
{
///
/// The entity that threw this entity.
///
- [DataField, ViewVariables(VVAccess.ReadWrite)]
+ [DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
public EntityUid? Thrower;
///
/// The timestamp at which this entity was thrown.
///
- [DataField, ViewVariables(VVAccess.ReadWrite)]
+ [DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
public TimeSpan? ThrownTime;
///
/// Compared to to land this entity, if any.
///
- [DataField, ViewVariables(VVAccess.ReadWrite)]
+ [DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
public TimeSpan? LandTime;
///
/// Whether or not this entity was already landed.
///
- [DataField, ViewVariables(VVAccess.ReadWrite)]
+ [DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
public bool Landed;
///
/// Whether or not to play a sound when the entity lands.
///
- [DataField, ViewVariables(VVAccess.ReadWrite)]
+ [DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
public bool PlayLandSound;
- }
- [Serializable, NetSerializable]
- public sealed class ThrownItemComponentState : ComponentState
- {
- public NetEntity? Thrower;
-
- public TimeSpan? ThrownTime;
-
- public TimeSpan? LandTime;
-
- public bool Landed;
-
- public bool PlayLandSound;
+ ///
+ /// Used to restore state after the throwing scale animation is finished.
+ ///
+ [DataField]
+ public Vector2? OriginalScale = null;
}
}
diff --git a/Content.Shared/Throwing/ThrownItemSystem.cs b/Content.Shared/Throwing/ThrownItemSystem.cs
index 450c38d639..0e5817ff92 100644
--- a/Content.Shared/Throwing/ThrownItemSystem.cs
+++ b/Content.Shared/Throwing/ThrownItemSystem.cs
@@ -36,41 +36,10 @@ namespace Content.Shared.Throwing
SubscribeLocalEvent(PreventCollision);
SubscribeLocalEvent(ThrowItem);
SubscribeLocalEvent(OnThrownUnpaused);
- SubscribeLocalEvent(OnThrownGetState);
- SubscribeLocalEvent(OnThrownHandleState);
SubscribeLocalEvent(HandlePullStarted);
}
- private void OnThrownGetState(EntityUid uid, ThrownItemComponent component, ref ComponentGetState args)
- {
- // TODO: Throwing needs to handle this properly I just want the bad asserts to stop getting in my way.
- TryGetNetEntity(component.Thrower, out var nent);
-
- args.State = new ThrownItemComponentState()
- {
- ThrownTime = component.ThrownTime,
- LandTime = component.LandTime,
- Thrower = nent,
- Landed = component.Landed,
- PlayLandSound = component.PlayLandSound,
- };
- }
-
- private void OnThrownHandleState(EntityUid uid, ThrownItemComponent component, ref ComponentHandleState args)
- {
- if (args.Current is not ThrownItemComponentState state)
- return;
-
- TryGetEntity(state.Thrower, out var thrower);
-
- component.ThrownTime = state.ThrownTime;
- component.LandTime = state.LandTime;
- component.Thrower = thrower;
- component.Landed = state.Landed;
- component.PlayLandSound = state.PlayLandSound;
- }
-
private void OnMapInit(EntityUid uid, ThrownItemComponent component, MapInitEvent args)
{
component.ThrownTime ??= _gameTiming.CurTime;