Janitor trashbag upgrade + FANCY ANIMATIONS (#3058)

* Janitor trashbag upgrade + FANCY ANIMATIONS

* Review, Bug fixes and Sounds
- Fixed hand-pickup animation playing if the entity originated from inside a container (e.g. bag on the ground) or from inside ourselves (e.g. something in our own inventory)

* Fix/Change. Just log if AnimateEntityPickup is called with an entity that has no SpriteComponent.

* More explicit log message. Error log.

* Merge. Fix.
This commit is contained in:
Remie Richards
2021-02-03 22:07:13 +00:00
committed by GitHub
parent 861271ea44
commit d45835e863
21 changed files with 309 additions and 23 deletions

View File

@@ -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<SpriteComponent>();
sprite.CopyFrom(sprite0);
var animations = animatableClone.GetComponent<AnimationPlayerComponent>();
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");
}
}
}

View File

@@ -1,14 +1,23 @@
#nullable enable #nullable enable
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using Content.Client.Animations;
using Content.Client.UserInterface; using Content.Client.UserInterface;
using Content.Shared.GameObjects.Components.Items; using Content.Shared.GameObjects.Components.Items;
using Robust.Client.Animations;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using Robust.Client.GameObjects.Components.Animations;
using Robust.Client.Interfaces.GameObjects.Components; using Robust.Client.Interfaces.GameObjects.Components;
using Robust.Shared.Animations;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.GameObjects.Components;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Players;
using Robust.Shared.ViewVariables; using Robust.Shared.ViewVariables;
namespace Content.Client.GameObjects.Components.Items 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) public void SendChangeHand(string index)
{ {
SendNetworkMessage(new ClientChangedHandMsg(index)); SendNetworkMessage(new ClientChangedHandMsg(index));

View File

@@ -1,17 +1,23 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Content.Client.Animations;
using Content.Client.GameObjects.Components.Items; using Content.Client.GameObjects.Components.Items;
using Content.Shared.GameObjects.Components.Storage; using Content.Shared.GameObjects.Components.Storage;
using Content.Shared.Interfaces.GameObjects.Components; 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.Graphics.Drawing;
using Robust.Client.Interfaces.GameObjects.Components; using Robust.Client.Interfaces.GameObjects.Components;
using Robust.Client.Player; using Robust.Client.Player;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Animations;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.GameObjects.Components;
using Robust.Shared.Interfaces.Network; using Robust.Shared.Interfaces.Network;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Maths; using Robust.Shared.Maths;
@@ -77,6 +83,9 @@ namespace Content.Client.GameObjects.Components.Storage
case CloseStorageUIMessage _: case CloseStorageUIMessage _:
CloseUI(); CloseUI();
break; break;
case AnimateInsertingEntitiesMessage msg:
HandleAnimatingInsertingEntities(msg);
break;
} }
} }
@@ -92,6 +101,24 @@ namespace Content.Client.GameObjects.Components.Storage
Window.BuildEntityList(); Window.BuildEntityList();
} }
/// <summary>
/// Animate the newly stored entities in <paramref name="msg"/> flying towards this storage's position
/// </summary>
/// <param name="msg"></param>
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);
}
}
}
/// <summary> /// <summary>
/// Opens the storage UI if closed. Closes it if opened. /// Opens the storage UI if closed. Closes it if opened.
/// </summary> /// </summary>

View File

@@ -1,4 +1,4 @@
#nullable enable #nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
@@ -176,9 +176,16 @@ namespace Content.Server.GameObjects.Components.GUI
Dirty(); Dirty();
var position = item.Owner.Transform.Coordinates;
var contained = item.Owner.IsInContainer();
var success = hand.Container.Insert(item.Owner); var success = hand.Container.Insert(item.Owner);
if (success) 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; item.Owner.Transform.LocalPosition = Vector2.Zero;
OnItemChanged?.Invoke(); OnItemChanged?.Invoke();
} }

View File

@@ -2,8 +2,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Server.GameObjects.Components.GUI; using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.EntitySystems.DoAfter;
using Content.Server.Interfaces.GameObjects.Components.Items; using Content.Server.Interfaces.GameObjects.Components.Items;
using Content.Shared.Audio; using Content.Shared.Audio;
using Content.Shared.GameObjects.Components.Storage; using Content.Shared.GameObjects.Components.Storage;
@@ -24,6 +26,7 @@ using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Network; using Robust.Shared.Interfaces.Network;
using Robust.Shared.Log; using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Players; using Robust.Shared.Players;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables; using Robust.Shared.ViewVariables;
@@ -36,7 +39,7 @@ namespace Content.Server.GameObjects.Components.Items.Storage
[RegisterComponent] [RegisterComponent]
[ComponentReference(typeof(IActivate))] [ComponentReference(typeof(IActivate))]
[ComponentReference(typeof(IStorageComponent))] [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"; private const string LoggerName = "Storage";
@@ -44,6 +47,8 @@ namespace Content.Server.GameObjects.Components.Items.Storage
private readonly Dictionary<IEntity, int> _sizeCache = new(); private readonly Dictionary<IEntity, int> _sizeCache = new();
private bool _occludesLight; 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 bool _storageInitialCalculated;
private int _storageUsed; private int _storageUsed;
private int _storageCapacityMax; private int _storageCapacityMax;
@@ -184,7 +189,7 @@ namespace Content.Server.GameObjects.Components.Items.Storage
/// </summary> /// </summary>
/// <param name="player">The player to insert an entity from</param> /// <param name="player">The player to insert an entity from</param>
/// <returns>true if inserted, false otherwise</returns> /// <returns>true if inserted, false otherwise</returns>
public bool PlayerInsertEntity(IEntity player) public bool PlayerInsertHeldEntity(IEntity player)
{ {
EnsureInitialCalculated(); EnsureInitialCalculated();
@@ -212,6 +217,24 @@ namespace Content.Server.GameObjects.Components.Items.Storage
return true; return true;
} }
/// <summary>
/// Inserts an Entity (<paramref name="toInsert"/>) in the world into storage, informing <paramref name="player"/> if it fails.
/// <paramref name="toInsert"/> is *NOT* held, see <see cref="PlayerInsertHeldEntity(IEntity)"/>.
/// </summary>
/// <param name="player">The player to insert an entity with</param>
/// <returns>true if inserted, false otherwise</returns>
public bool PlayerInsertEntityInWorld(IEntity player, IEntity toInsert)
{
EnsureInitialCalculated();
if (!Insert(toInsert))
{
Owner.PopupMessage(player, "Can't insert.");
return false;
}
return true;
}
/// <summary> /// <summary>
/// Opens the storage UI for an entity /// Opens the storage UI for an entity
/// </summary> /// </summary>
@@ -343,6 +366,8 @@ namespace Content.Server.GameObjects.Components.Items.Storage
serializer.DataField(ref _storageCapacityMax, "capacity", 10000); serializer.DataField(ref _storageCapacityMax, "capacity", 10000);
serializer.DataField(ref _occludesLight, "occludesLight", true); 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(this, x => x.StorageSoundCollection, "storageSoundCollection", string.Empty);
//serializer.DataField(ref StorageUsed, "used", 0); //serializer.DataField(ref StorageUsed, "used", 0);
} }
@@ -418,7 +443,7 @@ namespace Content.Server.GameObjects.Components.Items.Storage
break; break;
} }
PlayerInsertEntity(player); PlayerInsertHeldEntity(player);
break; break;
} }
@@ -449,7 +474,7 @@ namespace Content.Server.GameObjects.Components.Items.Storage
return false; return false;
} }
return PlayerInsertEntity(eventArgs.User); return PlayerInsertHeldEntity(eventArgs.User);
} }
/// <summary> /// <summary>
@@ -469,6 +494,97 @@ namespace Content.Server.GameObjects.Components.Items.Storage
((IUse) this).UseEntity(new UseEntityEventArgs { User = eventArgs.User }); ((IUse) this).UseEntity(new UseEntityEventArgs { User = eventArgs.User });
} }
/// <summary>
/// Allows a user to pick up entities by clicking them, or pick up all entities in a certain radius
/// arround a click.
/// </summary>
/// <param name="eventArgs"></param>
/// <returns></returns>
async Task<bool> 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<StorableComponent>()))
{
var validStorables = new List<IEntity>();
foreach (var entity in Owner.EntityManager.GetEntitiesInRange(eventArgs.ClickLocation, 1))
{
if (!entity.Transform.IsMapTransform
|| entity == eventArgs.User
|| !entity.HasComponent<StorableComponent>())
continue;
validStorables.Add(entity);
}
//If there's only one then let's be generous
if (validStorables.Count > 1)
{
var doAfterSystem = EntitySystem.Get<DoAfterSystem>();
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<EntityUid>();
var successfullyInsertedPositions = new List<EntityCoordinates>();
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<StorableComponent>())
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<StorableComponent>())
return false;
var position = eventArgs.Target.Transform.Coordinates;
if(PlayerInsertEntityInWorld(eventArgs.User, eventArgs.Target))
{
SendNetworkMessage(new AnimateInsertingEntitiesMessage(
new List<EntityUid>() { eventArgs.Target.Uid },
new List<EntityCoordinates>() { position }
));
return true;
}
return true;
}
return false;
}
void IDestroyAct.OnDestroy(DestructionEventArgs eventArgs) void IDestroyAct.OnDestroy(DestructionEventArgs eventArgs)
{ {
var storedEntities = StoredEntities?.ToList(); var storedEntities = StoredEntities?.ToList();

View File

@@ -1,4 +1,4 @@
using System; using System;
using System.Linq; using System.Linq;
using Content.Server.GameObjects.Components.GUI; using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Items.Storage; using Content.Server.GameObjects.Components.Items.Storage;
@@ -216,7 +216,7 @@ namespace Content.Server.GameObjects.EntitySystems
if (heldItem != null) if (heldItem != null)
{ {
storageComponent.PlayerInsertEntity(plyEnt); storageComponent.PlayerInsertHeldEntity(plyEnt);
} }
else else
{ {

View File

@@ -1,7 +1,8 @@
#nullable enable #nullable enable
using System; using System;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
namespace Content.Shared.GameObjects.Components.Items namespace Content.Shared.GameObjects.Components.Items
@@ -127,4 +128,20 @@ namespace Content.Shared.GameObjects.Components.Items
Middle, Middle,
Right Right
} }
/// <summary>
/// Component message for displaying an animation of an entity flying towards the owner of a HandsComponent
/// </summary>
[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;
}
}
} }

View File

@@ -1,12 +1,14 @@
#nullable enable #nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics;
using Content.Shared.GameObjects.EntitySystems; using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.GameObjects.EntitySystems.ActionBlocker; using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
using Content.Shared.Interfaces.GameObjects.Components; using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
namespace Content.Shared.GameObjects.Components.Storage namespace Content.Shared.GameObjects.Components.Storage
@@ -100,6 +102,22 @@ namespace Content.Shared.GameObjects.Components.Storage
} }
} }
/// <summary>
/// Component message for displaying an animation of entities flying into a storage entity
/// </summary>
[Serializable, NetSerializable]
public class AnimateInsertingEntitiesMessage : ComponentMessage
{
public readonly List<EntityUid> StoredEntities;
public readonly List<EntityCoordinates> EntityPositions;
public AnimateInsertingEntitiesMessage(List<EntityUid> storedEntities, List<EntityCoordinates> entityPositions)
{
Directed = true;
StoredEntities = storedEntities;
EntityPositions = entityPositions;
}
}
/// <summary> /// <summary>
/// Component message for removing a contained entity from the storage entity /// Component message for removing a contained entity from the storage entity
/// </summary> /// </summary>

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,8 @@
- type: entity
name: clientsideclone
id: clientsideclone
abstract: true
components:
- type: Sprite
- type: Physics
- type: AnimationPlayer

View File

@@ -154,17 +154,6 @@
sprite: Objects/Consumable/Trash/tastybread.rsi 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 - type: entity
name: tray (trash) name: tray (trash)
parent: TrashBase parent: TrashBase

View File

@@ -234,3 +234,18 @@
reagents: reagents:
- ReagentId: chem.SpaceCleaner - ReagentId: chem.SpaceCleaner
Quantity: 100 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

View File

@@ -6,3 +6,10 @@
- /Audio/Effects/rustle3.ogg - /Audio/Effects/rustle3.ogg
- /Audio/Effects/rustle4.ogg - /Audio/Effects/rustle4.ogg
- /Audio/Effects/rustle5.ogg - /Audio/Effects/rustle5.ogg
- type: soundCollection
id: trashBagRustle
files:
- /Audio/Effects/trashbag1.ogg
- /Audio/Effects/trashbag2.ogg
- /Audio/Effects/trashbag3.ogg

View File

Before

Width:  |  Height:  |  Size: 529 B

After

Width:  |  Height:  |  Size: 529 B