diff --git a/Content.Client/Animations/ReusableAnimations.cs b/Content.Client/Animations/ReusableAnimations.cs new file mode 100644 index 0000000000..5b93efe5ae --- /dev/null +++ b/Content.Client/Animations/ReusableAnimations.cs @@ -0,0 +1,56 @@ +using Robust.Client.Animations; +using Robust.Client.GameObjects; +using Robust.Client.GameObjects.Components.Animations; +using Robust.Shared.Animations; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.GameObjects.Components; +using Robust.Shared.Log; +using Robust.Shared.Map; +using Robust.Shared.Maths; +using System; + +namespace Content.Client.Animations +{ + public static class ReusableAnimations + { + + public static void AnimateEntityPickup(IEntity entity, EntityCoordinates initialPosition, Vector2 finalPosition) + { + var animatableClone = entity.EntityManager.SpawnEntity("clientsideclone", initialPosition); + animatableClone.Name = entity.Name; + + if(!entity.TryGetComponent(out SpriteComponent sprite0)) + { + Logger.Error($"Entity ({0}) couldn't be animated for pickup since it doesn't have a {1}!", entity.Name, nameof(SpriteComponent)); + return; + } + var sprite = animatableClone.GetComponent(); + sprite.CopyFrom(sprite0); + + var animations = animatableClone.GetComponent(); + animations.AnimationCompleted += (s) => { + animatableClone.Delete(); + }; + + animations.Play(new Animation + { + Length = TimeSpan.FromMilliseconds(125), + AnimationTracks = + { + new AnimationTrackComponentProperty + { + ComponentType = typeof(ITransformComponent), + Property = nameof(ITransformComponent.WorldPosition), + InterpolationMode = AnimationInterpolationMode.Linear, + KeyFrames = + { + new AnimationTrackComponentProperty.KeyFrame(initialPosition.Position, 0), + new AnimationTrackComponentProperty.KeyFrame(finalPosition, 0.125f) + } + } + } + }, "fancy_pickup_anim"); + } + + } +} diff --git a/Content.Client/GameObjects/Components/Items/HandsComponent.cs b/Content.Client/GameObjects/Components/Items/HandsComponent.cs index 21f112d031..a59f0333f9 100644 --- a/Content.Client/GameObjects/Components/Items/HandsComponent.cs +++ b/Content.Client/GameObjects/Components/Items/HandsComponent.cs @@ -1,14 +1,23 @@ -#nullable enable +#nullable enable +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using Content.Client.Animations; using Content.Client.UserInterface; using Content.Shared.GameObjects.Components.Items; +using Robust.Client.Animations; using Robust.Client.GameObjects; +using Robust.Client.GameObjects.Components.Animations; using Robust.Client.Interfaces.GameObjects.Components; +using Robust.Shared.Animations; using Robust.Shared.GameObjects; using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.GameObjects.Components; +using Robust.Shared.Interfaces.Network; using Robust.Shared.IoC; +using Robust.Shared.Log; +using Robust.Shared.Players; using Robust.Shared.ViewVariables; namespace Content.Client.GameObjects.Components.Items @@ -244,6 +253,23 @@ namespace Content.Client.GameObjects.Components.Items } } + public override void HandleNetworkMessage(ComponentMessage message, INetChannel netChannel, ICommonSession? session = null) + { + base.HandleNetworkMessage(message, netChannel, session); + + switch (message) + { + case AnimatePickupEntityMessage msg: + { + if (Owner.EntityManager.TryGetEntity(msg.EntityId, out var entity)) + { + ReusableAnimations.AnimateEntityPickup(entity, msg.EntityPosition, Owner.Transform.WorldPosition); + } + break; + } + } + } + public void SendChangeHand(string index) { SendNetworkMessage(new ClientChangedHandMsg(index)); diff --git a/Content.Client/GameObjects/Components/Storage/ClientStorageComponent.cs b/Content.Client/GameObjects/Components/Storage/ClientStorageComponent.cs index 1d6043ff18..c97b1e2f91 100644 --- a/Content.Client/GameObjects/Components/Storage/ClientStorageComponent.cs +++ b/Content.Client/GameObjects/Components/Storage/ClientStorageComponent.cs @@ -1,17 +1,23 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; +using Content.Client.Animations; using Content.Client.GameObjects.Components.Items; using Content.Shared.GameObjects.Components.Storage; using Content.Shared.Interfaces.GameObjects.Components; +using Robust.Client.Animations; +using Robust.Client.GameObjects; +using Robust.Client.GameObjects.Components.Animations; using Robust.Client.Graphics.Drawing; using Robust.Client.Interfaces.GameObjects.Components; using Robust.Client.Player; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; +using Robust.Shared.Animations; using Robust.Shared.GameObjects; using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.GameObjects.Components; using Robust.Shared.Interfaces.Network; using Robust.Shared.IoC; using Robust.Shared.Maths; @@ -77,6 +83,9 @@ namespace Content.Client.GameObjects.Components.Storage case CloseStorageUIMessage _: CloseUI(); break; + case AnimateInsertingEntitiesMessage msg: + HandleAnimatingInsertingEntities(msg); + break; } } @@ -92,6 +101,24 @@ namespace Content.Client.GameObjects.Components.Storage Window.BuildEntityList(); } + /// + /// Animate the newly stored entities in flying towards this storage's position + /// + /// + private void HandleAnimatingInsertingEntities(AnimateInsertingEntitiesMessage msg) + { + for (var i = 0; msg.StoredEntities.Count > i; i++) + { + var entityId = msg.StoredEntities[i]; + var initialPosition = msg.EntityPositions[i]; + + if (Owner.EntityManager.TryGetEntity(entityId, out var entity)) + { + ReusableAnimations.AnimateEntityPickup(entity, initialPosition, Owner.Transform.WorldPosition); + } + } + } + /// /// Opens the storage UI if closed. Closes it if opened. /// diff --git a/Content.Server/GameObjects/Components/GUI/HandsComponent.cs b/Content.Server/GameObjects/Components/GUI/HandsComponent.cs index 0182636f77..0139761d21 100644 --- a/Content.Server/GameObjects/Components/GUI/HandsComponent.cs +++ b/Content.Server/GameObjects/Components/GUI/HandsComponent.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -176,9 +176,16 @@ namespace Content.Server.GameObjects.Components.GUI Dirty(); + var position = item.Owner.Transform.Coordinates; + var contained = item.Owner.IsInContainer(); var success = hand.Container.Insert(item.Owner); if (success) { + //If the entity isn't in a container, and it isn't located exactly at our position (i.e. in our own storage), then we can safely play the animation + if (position != Owner.Transform.Coordinates && !contained) + { + SendNetworkMessage(new AnimatePickupEntityMessage(item.Owner.Uid, position)); + } item.Owner.Transform.LocalPosition = Vector2.Zero; OnItemChanged?.Invoke(); } diff --git a/Content.Server/GameObjects/Components/Items/Storage/ServerStorageComponent.cs b/Content.Server/GameObjects/Components/Items/Storage/ServerStorageComponent.cs index fda487377c..b50a23bb4d 100644 --- a/Content.Server/GameObjects/Components/Items/Storage/ServerStorageComponent.cs +++ b/Content.Server/GameObjects/Components/Items/Storage/ServerStorageComponent.cs @@ -2,8 +2,10 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Content.Server.GameObjects.Components.GUI; +using Content.Server.GameObjects.EntitySystems.DoAfter; using Content.Server.Interfaces.GameObjects.Components.Items; using Content.Shared.Audio; using Content.Shared.GameObjects.Components.Storage; @@ -24,6 +26,7 @@ using Robust.Shared.GameObjects.Systems; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.Network; using Robust.Shared.Log; +using Robust.Shared.Map; using Robust.Shared.Players; using Robust.Shared.Serialization; using Robust.Shared.ViewVariables; @@ -36,7 +39,7 @@ namespace Content.Server.GameObjects.Components.Items.Storage [RegisterComponent] [ComponentReference(typeof(IActivate))] [ComponentReference(typeof(IStorageComponent))] - public class ServerStorageComponent : SharedStorageComponent, IInteractUsing, IUse, IActivate, IStorageComponent, IDestroyAct, IExAct + public class ServerStorageComponent : SharedStorageComponent, IInteractUsing, IUse, IActivate, IStorageComponent, IDestroyAct, IExAct, IAfterInteract { private const string LoggerName = "Storage"; @@ -44,6 +47,8 @@ namespace Content.Server.GameObjects.Components.Items.Storage private readonly Dictionary _sizeCache = new(); private bool _occludesLight; + private bool _quickInsert; //Can insert storables by "attacking" them with the storage entity + private bool _areaInsert; //"Attacking" with the storage entity causes it to insert all nearby storables after a delay private bool _storageInitialCalculated; private int _storageUsed; private int _storageCapacityMax; @@ -184,7 +189,7 @@ namespace Content.Server.GameObjects.Components.Items.Storage /// /// The player to insert an entity from /// true if inserted, false otherwise - public bool PlayerInsertEntity(IEntity player) + public bool PlayerInsertHeldEntity(IEntity player) { EnsureInitialCalculated(); @@ -212,6 +217,24 @@ namespace Content.Server.GameObjects.Components.Items.Storage return true; } + /// + /// Inserts an Entity () in the world into storage, informing if it fails. + /// is *NOT* held, see . + /// + /// The player to insert an entity with + /// true if inserted, false otherwise + public bool PlayerInsertEntityInWorld(IEntity player, IEntity toInsert) + { + EnsureInitialCalculated(); + + if (!Insert(toInsert)) + { + Owner.PopupMessage(player, "Can't insert."); + return false; + } + return true; + } + /// /// Opens the storage UI for an entity /// @@ -343,6 +366,8 @@ namespace Content.Server.GameObjects.Components.Items.Storage serializer.DataField(ref _storageCapacityMax, "capacity", 10000); serializer.DataField(ref _occludesLight, "occludesLight", true); + serializer.DataField(ref _quickInsert, "quickInsert", false); + serializer.DataField(ref _areaInsert, "areaInsert", false); serializer.DataField(this, x => x.StorageSoundCollection, "storageSoundCollection", string.Empty); //serializer.DataField(ref StorageUsed, "used", 0); } @@ -418,7 +443,7 @@ namespace Content.Server.GameObjects.Components.Items.Storage break; } - PlayerInsertEntity(player); + PlayerInsertHeldEntity(player); break; } @@ -449,7 +474,7 @@ namespace Content.Server.GameObjects.Components.Items.Storage return false; } - return PlayerInsertEntity(eventArgs.User); + return PlayerInsertHeldEntity(eventArgs.User); } /// @@ -469,6 +494,97 @@ namespace Content.Server.GameObjects.Components.Items.Storage ((IUse) this).UseEntity(new UseEntityEventArgs { User = eventArgs.User }); } + /// + /// Allows a user to pick up entities by clicking them, or pick up all entities in a certain radius + /// arround a click. + /// + /// + /// + async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) + { + if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true, popup: true)) return false; + + // Pick up all entities in a radius around the clicked location. + // The last half of the if is because carpets exist and this is terrible + if(_areaInsert && (eventArgs.Target == null || !eventArgs.Target.HasComponent())) + { + var validStorables = new List(); + foreach (var entity in Owner.EntityManager.GetEntitiesInRange(eventArgs.ClickLocation, 1)) + { + if (!entity.Transform.IsMapTransform + || entity == eventArgs.User + || !entity.HasComponent()) + continue; + validStorables.Add(entity); + } + + //If there's only one then let's be generous + if (validStorables.Count > 1) + { + var doAfterSystem = EntitySystem.Get(); + var doAfterArgs = new DoAfterEventArgs(eventArgs.User, 0.2f * validStorables.Count, CancellationToken.None, Owner) + { + BreakOnStun = true, + BreakOnDamage = true, + BreakOnUserMove = true, + NeedHand = true, + }; + var result = await doAfterSystem.DoAfter(doAfterArgs); + if (result != DoAfterStatus.Finished) return true; + } + + var successfullyInserted = new List(); + var successfullyInsertedPositions = new List(); + foreach (var entity in validStorables) + { + // Check again, situation may have changed for some entities, but we'll still pick up any that are valid + if (!entity.Transform.IsMapTransform + || entity == eventArgs.User + || !entity.HasComponent()) + continue; + var coords = entity.Transform.Coordinates; + if (PlayerInsertEntityInWorld(eventArgs.User, entity)) + { + successfullyInserted.Add(entity.Uid); + successfullyInsertedPositions.Add(coords); + } + } + + // If we picked up atleast one thing, play a sound and do a cool animation! + if (successfullyInserted.Count>0) + { + PlaySoundCollection(StorageSoundCollection); + SendNetworkMessage( + new AnimateInsertingEntitiesMessage( + successfullyInserted, + successfullyInsertedPositions + ) + ); + } + return true; + } + // Pick up the clicked entity + else if(_quickInsert) + { + if (eventArgs.Target == null + || !eventArgs.Target.Transform.IsMapTransform + || eventArgs.Target == eventArgs.User + || !eventArgs.Target.HasComponent()) + return false; + var position = eventArgs.Target.Transform.Coordinates; + if(PlayerInsertEntityInWorld(eventArgs.User, eventArgs.Target)) + { + SendNetworkMessage(new AnimateInsertingEntitiesMessage( + new List() { eventArgs.Target.Uid }, + new List() { position } + )); + return true; + } + return true; + } + return false; + } + void IDestroyAct.OnDestroy(DestructionEventArgs eventArgs) { var storedEntities = StoredEntities?.ToList(); diff --git a/Content.Server/GameObjects/EntitySystems/HandsSystem.cs b/Content.Server/GameObjects/EntitySystems/HandsSystem.cs index 471f9d369c..0450d0e459 100644 --- a/Content.Server/GameObjects/EntitySystems/HandsSystem.cs +++ b/Content.Server/GameObjects/EntitySystems/HandsSystem.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using Content.Server.GameObjects.Components.GUI; using Content.Server.GameObjects.Components.Items.Storage; @@ -216,7 +216,7 @@ namespace Content.Server.GameObjects.EntitySystems if (heldItem != null) { - storageComponent.PlayerInsertEntity(plyEnt); + storageComponent.PlayerInsertHeldEntity(plyEnt); } else { diff --git a/Content.Shared/GameObjects/Components/Items/SharedHandsComponent.cs b/Content.Shared/GameObjects/Components/Items/SharedHandsComponent.cs index 83e24cdadb..e3fba91eb5 100644 --- a/Content.Shared/GameObjects/Components/Items/SharedHandsComponent.cs +++ b/Content.Shared/GameObjects/Components/Items/SharedHandsComponent.cs @@ -1,7 +1,8 @@ -#nullable enable +#nullable enable using System; using Robust.Shared.GameObjects; using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Map; using Robust.Shared.Serialization; namespace Content.Shared.GameObjects.Components.Items @@ -127,4 +128,20 @@ namespace Content.Shared.GameObjects.Components.Items Middle, Right } + + /// + /// Component message for displaying an animation of an entity flying towards the owner of a HandsComponent + /// + [Serializable, NetSerializable] + public class AnimatePickupEntityMessage : ComponentMessage + { + public readonly EntityUid EntityId; + public readonly EntityCoordinates EntityPosition; + public AnimatePickupEntityMessage(EntityUid entity, EntityCoordinates entityPosition) + { + Directed = true; + EntityId = entity; + EntityPosition = entityPosition; + } + } } diff --git a/Content.Shared/GameObjects/Components/Storage/SharedStorageComponent.cs b/Content.Shared/GameObjects/Components/Storage/SharedStorageComponent.cs index 2bdbc43363..6cc256d41c 100644 --- a/Content.Shared/GameObjects/Components/Storage/SharedStorageComponent.cs +++ b/Content.Shared/GameObjects/Components/Storage/SharedStorageComponent.cs @@ -1,12 +1,14 @@ -#nullable enable +#nullable enable using System; using System.Collections.Generic; using System.Linq; +using System.Numerics; using Content.Shared.GameObjects.EntitySystems; using Content.Shared.GameObjects.EntitySystems.ActionBlocker; using Content.Shared.Interfaces.GameObjects.Components; using Robust.Shared.GameObjects; using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Map; using Robust.Shared.Serialization; namespace Content.Shared.GameObjects.Components.Storage @@ -100,6 +102,22 @@ namespace Content.Shared.GameObjects.Components.Storage } } + /// + /// Component message for displaying an animation of entities flying into a storage entity + /// + [Serializable, NetSerializable] + public class AnimateInsertingEntitiesMessage : ComponentMessage + { + public readonly List StoredEntities; + public readonly List EntityPositions; + public AnimateInsertingEntitiesMessage(List storedEntities, List entityPositions) + { + Directed = true; + StoredEntities = storedEntities; + EntityPositions = entityPositions; + } + } + /// /// Component message for removing a contained entity from the storage entity /// diff --git a/Resources/Audio/Effects/trashbag1.ogg b/Resources/Audio/Effects/trashbag1.ogg new file mode 100644 index 0000000000..2324af18c6 Binary files /dev/null and b/Resources/Audio/Effects/trashbag1.ogg differ diff --git a/Resources/Audio/Effects/trashbag2.ogg b/Resources/Audio/Effects/trashbag2.ogg new file mode 100644 index 0000000000..31f3e13a69 Binary files /dev/null and b/Resources/Audio/Effects/trashbag2.ogg differ diff --git a/Resources/Audio/Effects/trashbag3.ogg b/Resources/Audio/Effects/trashbag3.ogg new file mode 100644 index 0000000000..08719273b5 Binary files /dev/null and b/Resources/Audio/Effects/trashbag3.ogg differ diff --git a/Resources/Prototypes/Entities/Effects/Markers/clientsideclone.yml b/Resources/Prototypes/Entities/Effects/Markers/clientsideclone.yml new file mode 100644 index 0000000000..a3c662c28a --- /dev/null +++ b/Resources/Prototypes/Entities/Effects/Markers/clientsideclone.yml @@ -0,0 +1,8 @@ +- type: entity + name: clientsideclone + id: clientsideclone + abstract: true + components: + - type: Sprite + - type: Physics + - type: AnimationPlayer diff --git a/Resources/Prototypes/Entities/Objects/Consumable/trash.yml b/Resources/Prototypes/Entities/Objects/Consumable/trash.yml index 71490e6b8b..deabc3b5df 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/trash.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/trash.yml @@ -152,18 +152,7 @@ components: - type: Sprite sprite: Objects/Consumable/Trash/tastybread.rsi - - -- type: entity - name: trash bag - parent: TrashBase - id: TrashBag - components: - - type: Sprite - sprite: Objects/Consumable/Trash/trashbag.rsi - - - type: Storage - capacity: 125 + - type: entity name: tray (trash) diff --git a/Resources/Prototypes/Entities/Objects/Specific/janitor.yml b/Resources/Prototypes/Entities/Objects/Specific/janitor.yml index a21de46789..2aea0da9b9 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/janitor.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/janitor.yml @@ -234,3 +234,18 @@ reagents: - ReagentId: chem.SpaceCleaner Quantity: 100 + + +- type: entity + name: trash bag + id: TrashBag + parent: BaseItem + components: + - type: Sprite + sprite: Objects/Specific/Janitorial/trashbag.rsi + state: icon + - type: Storage + capacity: 125 + quickInsert: true + areaInsert: true + storageSoundCollection: trashBagRustle diff --git a/Resources/Prototypes/SoundCollections/storage_rustle.yml b/Resources/Prototypes/SoundCollections/storage_rustle.yml index d7f6c09586..3fbfc80535 100644 --- a/Resources/Prototypes/SoundCollections/storage_rustle.yml +++ b/Resources/Prototypes/SoundCollections/storage_rustle.yml @@ -6,3 +6,10 @@ - /Audio/Effects/rustle3.ogg - /Audio/Effects/rustle4.ogg - /Audio/Effects/rustle5.ogg + +- type: soundCollection + id: trashBagRustle + files: + - /Audio/Effects/trashbag1.ogg + - /Audio/Effects/trashbag2.ogg + - /Audio/Effects/trashbag3.ogg \ No newline at end of file diff --git a/Resources/Textures/Objects/Consumable/Trash/trashbag.rsi/icon-0.png b/Resources/Textures/Objects/Specific/Janitorial/trashbag.rsi/icon-0.png similarity index 100% rename from Resources/Textures/Objects/Consumable/Trash/trashbag.rsi/icon-0.png rename to Resources/Textures/Objects/Specific/Janitorial/trashbag.rsi/icon-0.png diff --git a/Resources/Textures/Objects/Consumable/Trash/trashbag.rsi/icon-1.png b/Resources/Textures/Objects/Specific/Janitorial/trashbag.rsi/icon-1.png similarity index 100% rename from Resources/Textures/Objects/Consumable/Trash/trashbag.rsi/icon-1.png rename to Resources/Textures/Objects/Specific/Janitorial/trashbag.rsi/icon-1.png diff --git a/Resources/Textures/Objects/Consumable/Trash/trashbag.rsi/icon-2.png b/Resources/Textures/Objects/Specific/Janitorial/trashbag.rsi/icon-2.png similarity index 100% rename from Resources/Textures/Objects/Consumable/Trash/trashbag.rsi/icon-2.png rename to Resources/Textures/Objects/Specific/Janitorial/trashbag.rsi/icon-2.png diff --git a/Resources/Textures/Objects/Consumable/Trash/trashbag.rsi/icon-3.png b/Resources/Textures/Objects/Specific/Janitorial/trashbag.rsi/icon-3.png similarity index 100% rename from Resources/Textures/Objects/Consumable/Trash/trashbag.rsi/icon-3.png rename to Resources/Textures/Objects/Specific/Janitorial/trashbag.rsi/icon-3.png diff --git a/Resources/Textures/Objects/Consumable/Trash/trashbag.rsi/icon.png b/Resources/Textures/Objects/Specific/Janitorial/trashbag.rsi/icon.png similarity index 100% rename from Resources/Textures/Objects/Consumable/Trash/trashbag.rsi/icon.png rename to Resources/Textures/Objects/Specific/Janitorial/trashbag.rsi/icon.png diff --git a/Resources/Textures/Objects/Consumable/Trash/trashbag.rsi/meta.json b/Resources/Textures/Objects/Specific/Janitorial/trashbag.rsi/meta.json similarity index 100% rename from Resources/Textures/Objects/Consumable/Trash/trashbag.rsi/meta.json rename to Resources/Textures/Objects/Specific/Janitorial/trashbag.rsi/meta.json