Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds Parcel Wrap #34471

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
68 changes: 68 additions & 0 deletions Content.Shared/ParcelWrap/Components/ParcelWrapComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using Content.Shared.Item;
using Content.Shared.ParcelWrap.Systems;
using Content.Shared.Whitelist;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;

namespace Content.Shared.ParcelWrap.Components;

/// <summary>
/// This component gives its owning entity the ability to wrap items into parcels.
/// </summary>
/// <seealso cref="Components.WrappedParcelComponent"/>
[RegisterComponent, NetworkedComponent]
[Access] // Readonly, except for VV editing
public sealed partial class ParcelWrapComponent : Component
{
/// <summary>
/// The <see cref="EntityPrototype"/> of the parcel created by using this component.
/// </summary>
[DataField(required: true)]
public EntProtoId ParcelPrototype;

/// <summary>
/// If true, parcels created by this will have the same <see cref="ItemSizePrototype">size</see> as the item they
/// contain. If false, parcels created by this will always have the size specified by <see cref="FallbackItemSize"/>.
/// </summary>
[DataField]
public bool WrappedItemsMaintainSize = true;

/// <summary>
/// The <see cref="ItemSizePrototype">size</see> of parcels created by this component's entity. This is used if
/// <see cref="WrappedItemsMaintainSize"/> is false, or if the item being wrapped somehow doesn't have a size.
/// </summary>
[DataField]
public ProtoId<ItemSizePrototype> FallbackItemSize = "Ginormous";

/// <summary>
/// If true, parcels created by this will have the same shape as the item they contain. If false, parcels created by
/// this will have the default shape for their size.
/// </summary>
[DataField]
public bool WrappedItemsMaintainShape;

/// <summary>
/// How long it takes to use this to wrap something.
/// </summary>
[DataField(required: true)]
public TimeSpan WrapDelay = TimeSpan.FromSeconds(1);

/// <summary>
/// Sound played when this is used to wrap something.
/// </summary>
[DataField]
public SoundSpecifier? WrapSound;

/// <summary>
/// Defines the set of things which can be wrapped (unless it fails the <see cref="Blacklist"/>).
/// </summary>
[DataField, ViewVariables(VVAccess.ReadOnly)]
public EntityWhitelist? Whitelist;

/// <summary>
/// Defines the set of things which cannot be wrapped (even if it passes the <see cref="Whitelist"/>).
/// </summary>
[DataField, ViewVariables(VVAccess.ReadOnly)]
public EntityWhitelist? Blacklist;
}
45 changes: 45 additions & 0 deletions Content.Shared/ParcelWrap/Components/WrappedParcelComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using Content.Shared.ParcelWrap.Systems;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;

namespace Content.Shared.ParcelWrap.Components;

/// <summary>
/// This component marks its owner as being a parcel created by wrapping another item up. It can be unwrapped,
/// destroying this entity and releasing <see cref="Contents"/>.
/// </summary>
/// <seealso cref="ParcelWrapComponent"/>
[RegisterComponent, Access(typeof(SharedParcelWrappingSystem))]
public sealed partial class WrappedParcelComponent : Component
{
/// <summary>
/// The contents of this parcel.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
public ContainerSlot Contents = default!;

/// <summary>
/// Specifies the entity to spawn when this parcel is unwrapped.
/// </summary>
[DataField]
public EntProtoId? UnwrapTrash;

/// <summary>
/// How long it takes to unwrap this parcel.
/// </summary>
[DataField(required: true)]
public TimeSpan UnwrapDelay = TimeSpan.FromSeconds(1);

/// <summary>
/// Sound played when unwrapping this parcel.
/// </summary>
[DataField]
public SoundSpecifier? UnwrapSound;

/// <summary>
/// The ID of <see cref="Contents"/>.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadOnly)]
public string ContainerId = "contents";
}
13 changes: 13 additions & 0 deletions Content.Shared/ParcelWrap/Components/WrappedParcelVisuals.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Robust.Shared.Serialization;

namespace Content.Shared.ParcelWrap.Components;

/// <summary>
/// This enum is used to change the sprite used by WrappedParcels based on the parcel's size.
/// </summary>
[Serializable, NetSerializable]
public enum WrappedParcelVisuals : byte
{
Size,
Layer,
}
10 changes: 10 additions & 0 deletions Content.Shared/ParcelWrap/Systems/ParcelWrapEvents.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Content.Shared.DoAfter;
using Robust.Shared.Serialization;

namespace Content.Shared.ParcelWrap.Systems;

[Serializable, NetSerializable]
public sealed partial class ParcelWrapItemDoAfterEvent : SimpleDoAfterEvent;

[Serializable, NetSerializable]
public sealed partial class UnwrapWrappedParcelDoAfterEvent : SimpleDoAfterEvent;
135 changes: 135 additions & 0 deletions Content.Shared/ParcelWrap/Systems/ParcelWrappingSystem.ParcelWrap.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
using Content.Shared.DoAfter;
using Content.Shared.Interaction;
using Content.Shared.Item;
using Content.Shared.ParcelWrap.Components;
using Content.Shared.Verbs;
using Robust.Shared.Utility;

namespace Content.Shared.ParcelWrap.Systems;

// This part handles Parcel Wrap.
public sealed partial class SharedParcelWrappingSystem
{
private void InitializeParcelWrap()
{
SubscribeLocalEvent<ParcelWrapComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<ParcelWrapComponent, GetVerbsEvent<UtilityVerb>>(OnGetVerbsForParcelWrap);
SubscribeLocalEvent<ParcelWrapComponent, ParcelWrapItemDoAfterEvent>(OnWrapItemDoAfter);
}

private void OnAfterInteract(Entity<ParcelWrapComponent> entity, ref AfterInteractEvent args)
{
if (args.Handled ||
args.Target is not { } target ||
!args.CanReach ||
!IsWrappable(entity, target))
return;

args.Handled = TryStartWrapDoAfter(args.User, entity, target);
}

private void OnGetVerbsForParcelWrap(Entity<ParcelWrapComponent> entity, ref GetVerbsEvent<UtilityVerb> args)
{
if (!args.CanAccess || !IsWrappable(entity, args.Target))
return;

// "Capture" the values from `args` because C# doesn't like doing the capturing for `ref` values.
var user = args.User;
var target = args.Target;

// "Wrap" verb for when just left-clicking doesn't work.
args.Verbs.Add(new UtilityVerb
{
Text = Loc.GetString("parcel-wrap-verb-wrap"),
Act = () => TryStartWrapDoAfter(user, entity, target),
});
}

private void OnWrapItemDoAfter(Entity<ParcelWrapComponent> wrapper, ref ParcelWrapItemDoAfterEvent args)
{
if (args.Handled || args.Cancelled)
return;

if (args.Target is { } target)
{
WrapInternal(args.User, wrapper, target);
args.Handled = true;
}
}

private bool TryStartWrapDoAfter(EntityUid user, Entity<ParcelWrapComponent> wrapper, EntityUid target)
{
return _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager,
user,
wrapper.Comp.WrapDelay,
new ParcelWrapItemDoAfterEvent(),
wrapper, // Raise the event on the wrapper because that's what the event handler expects.
target,
wrapper)
{
NeedHand = true,
BreakOnMove = true,
BreakOnDamage = true,
});
}

/// <summary>
/// Spawns a WrappedParcel containing <paramref name="target"/>.
/// </summary>
/// <param name="user">The entity using <paramref name="wrapper"/> to wrap <paramref name="target"/>.</param>
/// <param name="wrapper">The wrapping being used. Determines appearance of the spawned parcel.</param>
/// <param name="target">The entity being wrapped.</param>
private void WrapInternal(EntityUid user, Entity<ParcelWrapComponent> wrapper, EntityUid target)
{
if (_net.IsServer)
{
var spawned = Spawn(wrapper.Comp.ParcelPrototype, Transform(target).Coordinates);

// If this wrap maintains the size when wrapping, set the parcel's size to the target's size. Otherwise use the
// wrap's fallback size.
TryComp(target, out ItemComponent? targetItemComp);
var size = wrapper.Comp.FallbackItemSize;
if (wrapper.Comp.WrappedItemsMaintainSize && targetItemComp is not null)
{
size = targetItemComp.Size;
}

// ParcelWrap's spawned entity should always have an `ItemComp`. As of writing, the only use has it hardcoded on
// its prototype.
var item = Comp<ItemComponent>(spawned);
_item.SetSize(spawned, size, item);
_appearance.SetData(spawned, WrappedParcelVisuals.Size, size.Id);

// If this wrap maintains the shape when wrapping and the item has a shape override, copy the shape override to
// the parcel.
if (wrapper.Comp.WrappedItemsMaintainShape && targetItemComp is { Shape: { } shape })
{
_item.SetShape(spawned, shape, item);
}

// If the target's in a container, try to put the parcel in its place in the container.
if (_container.TryGetContainingContainer((target, null, null), out var containerOfTarget))
{
_container.Remove(target, containerOfTarget);
_container.InsertOrDrop((spawned, null, null), containerOfTarget);
}

// Insert the target into the parcel.
var parcel = EnsureComp<WrappedParcelComponent>(spawned);
if (!_container.Insert(target, parcel.Contents))
{
DebugTools.Assert(
$"Failed to insert target entity into newly spawned parcel. target={PrettyPrint.PrintUserFacing(target)}");
QueueDel(spawned);
}
}

// Consume a `use` on the wrapper, and delete the wrapper if it's empty.
_charges.UseCharge(wrapper);
if (_net.IsServer && _charges.IsEmpty(wrapper))
QueueDel(wrapper);

// Play a wrapping sound.
_audio.PlayPredicted(wrapper.Comp.WrapSound, target, user);
}
}
Loading
Loading