--- /dev/null
+using Content.Shared.Throwing;
+using Robust.Client.Animations;
+using Robust.Client.GameObjects;
+using Robust.Shared.Animations;
+
+namespace Content.Client.Throwing;
+
+/// <summary>
+/// Handles animating thrown items.
+/// </summary>
+public sealed class ThrownItemVisualizerSystem : EntitySystem
+{
+ [Dependency] private readonly AnimationPlayerSystem _anim = default!;
+
+ private const string AnimationKey = "thrown-item";
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent<ThrownItemComponent, AfterAutoHandleStateEvent>(OnAutoHandleState);
+ SubscribeLocalEvent<ThrownItemComponent, ComponentShutdown>(OnShutdown);
+ }
+
+ private void OnAutoHandleState(EntityUid uid, ThrownItemComponent component, ref AfterAutoHandleStateEvent args)
+ {
+ if (!TryComp<SpriteComponent>(uid, out var sprite))
+ return;
+
+ var animationPlayer = EnsureComp<AnimationPlayerComponent>(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<SpriteComponent>(uid, out var sprite) && component.OriginalScale != null)
+ sprite.Scale = component.OriginalScale.Value;
+
+ _anim.Stop(uid, AnimationKey);
+ }
+
+ private static Animation? GetAnimation(Entity<ThrownItemComponent, SpriteComponent> 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
+ }
+ }
+ };
+ }
+}
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;
[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(
if (projectileQuery.TryGetComponent(uid, out var proj) && !proj.OnlyCollideWhenShot)
return;
- var comp = EnsureComp<ThrownItemComponent>(uid);
+ var comp = new ThrownItemComponent();
comp.Thrower = user;
// Estimate time to arrival so we can apply OnGround status and slow it much faster.
else
comp.LandTime = time < FlyTime ? default : comp.ThrownTime + TimeSpan.FromSeconds(time - FlyTime);
comp.PlayLandSound = playSound;
+ AddComp(uid, comp, true);
ThrowingAngleComponent? throwingAngle = null;
_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))
+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
{
/// <summary>
/// The entity that threw this entity.
/// </summary>
- [DataField, ViewVariables(VVAccess.ReadWrite)]
+ [DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
public EntityUid? Thrower;
/// <summary>
/// The <see cref="IGameTiming.CurTime"/> timestamp at which this entity was thrown.
/// </summary>
- [DataField, ViewVariables(VVAccess.ReadWrite)]
+ [DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
public TimeSpan? ThrownTime;
/// <summary>
/// Compared to <see cref="IGameTiming.CurTime"/> to land this entity, if any.
/// </summary>
- [DataField, ViewVariables(VVAccess.ReadWrite)]
+ [DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
public TimeSpan? LandTime;
/// <summary>
/// Whether or not this entity was already landed.
/// </summary>
- [DataField, ViewVariables(VVAccess.ReadWrite)]
+ [DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
public bool Landed;
/// <summary>
/// Whether or not to play a sound when the entity lands.
/// </summary>
- [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;
+ /// <summary>
+ /// Used to restore state after the throwing scale animation is finished.
+ /// </summary>
+ [DataField]
+ public Vector2? OriginalScale = null;
}
}
SubscribeLocalEvent<ThrownItemComponent, PreventCollideEvent>(PreventCollision);
SubscribeLocalEvent<ThrownItemComponent, ThrownEvent>(ThrowItem);
SubscribeLocalEvent<ThrownItemComponent, EntityUnpausedEvent>(OnThrownUnpaused);
- SubscribeLocalEvent<ThrownItemComponent, ComponentGetState>(OnThrownGetState);
- SubscribeLocalEvent<ThrownItemComponent, ComponentHandleState>(OnThrownHandleState);
SubscribeLocalEvent<PullStartedMessage>(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;