From: slarticodefast <161409025+slarticodefast@users.noreply.github.com> Date: Wed, 15 Oct 2025 20:19:34 +0000 (+0200) Subject: Make parcelwrap able to wrap humanoids (#40911) X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=9562e009bf0e06d5d94742e2cd9de0512bad4262;p=space-station-14.git Make parcelwrap able to wrap humanoids (#40911) * parcel * help, I'm being forced to add admin abuse features * review --- diff --git a/Content.Shared/ParcelWrap/Components/ParcelWrapComponent.cs b/Content.Shared/ParcelWrap/Components/ParcelWrapComponent.cs index b5afb49dca..41375e5c2a 100644 --- a/Content.Shared/ParcelWrap/Components/ParcelWrapComponent.cs +++ b/Content.Shared/ParcelWrap/Components/ParcelWrapComponent.cs @@ -10,58 +10,64 @@ namespace Content.Shared.ParcelWrap.Components; /// This component gives its owning entity the ability to wrap items into parcels. /// /// -[RegisterComponent, NetworkedComponent] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] [Access] // Readonly, except for VV editing public sealed partial class ParcelWrapComponent : Component { /// /// The of the parcel created by using this component. /// - [DataField(required: true)] + [DataField(required: true), AutoNetworkedField] public EntProtoId ParcelPrototype; /// /// If true, parcels created by this will have the same size as the item they /// contain. If false, parcels created by this will always have the size specified by . /// - [DataField] + [DataField, AutoNetworkedField] public bool WrappedItemsMaintainSize = true; /// /// The size of parcels created by this component's entity. This is used if /// is false, or if the item being wrapped somehow doesn't have a size. /// - [DataField] + [DataField, AutoNetworkedField] public ProtoId FallbackItemSize = "Ginormous"; /// /// 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. /// - [DataField] + [DataField, AutoNetworkedField] public bool WrappedItemsMaintainShape; /// /// How long it takes to use this to wrap something. /// - [DataField(required: true)] + [DataField(required: true), AutoNetworkedField] public TimeSpan WrapDelay = TimeSpan.FromSeconds(1); /// /// Sound played when this is used to wrap something. /// - [DataField] + [DataField, AutoNetworkedField] public SoundSpecifier? WrapSound; /// /// Defines the set of things which can be wrapped (unless it fails the ). /// - [DataField] + [DataField, AutoNetworkedField] public EntityWhitelist? Whitelist; /// /// Defines the set of things which cannot be wrapped (even if it passes the ). /// - [DataField] + [DataField, AutoNetworkedField] public EntityWhitelist? Blacklist; + + /// + /// If a player trapped inside this parcel can escape from it by unwrapping it. + /// + [DataField, AutoNetworkedField] + public bool CanSelfUnwrap = true; } diff --git a/Content.Shared/ParcelWrap/Components/ParcelWrapOverrideComponent.cs b/Content.Shared/ParcelWrap/Components/ParcelWrapOverrideComponent.cs new file mode 100644 index 0000000000..e401d30a7f --- /dev/null +++ b/Content.Shared/ParcelWrap/Components/ParcelWrapOverrideComponent.cs @@ -0,0 +1,24 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.ParcelWrap.Components; + +/// +/// Added to an entity to override the datafields in when it is being wrapped. +/// Use this for special, non-item parcel types, for example Urist-shaped parcels. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class ParcelWrapOverrideComponent : Component +{ + /// + /// The of the parcel created by wrapping this entity. + /// + [DataField(required: true), AutoNetworkedField] + public EntProtoId? ParcelPrototype; + + /// + /// How long it takes to use this to wrap something. + /// + [DataField(required: true), AutoNetworkedField] + public TimeSpan? WrapDelay; +} diff --git a/Content.Shared/ParcelWrap/Components/WrappedParcelComponent.cs b/Content.Shared/ParcelWrap/Components/WrappedParcelComponent.cs index e0ceaeae49..2ed018e344 100644 --- a/Content.Shared/ParcelWrap/Components/WrappedParcelComponent.cs +++ b/Content.Shared/ParcelWrap/Components/WrappedParcelComponent.cs @@ -11,7 +11,8 @@ namespace Content.Shared.ParcelWrap.Components; /// destroying this entity and releasing . /// /// -[RegisterComponent, NetworkedComponent, Access(typeof(ParcelWrappingSystem))] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(ParcelWrappingSystem))] public sealed partial class WrappedParcelComponent : Component { /// @@ -23,19 +24,19 @@ public sealed partial class WrappedParcelComponent : Component /// /// Specifies the entity to spawn when this parcel is unwrapped. /// - [DataField] + [DataField, AutoNetworkedField] public EntProtoId? UnwrapTrash; /// /// How long it takes to unwrap this parcel. /// - [DataField(required: true)] + [DataField(required: true), AutoNetworkedField] public TimeSpan UnwrapDelay = TimeSpan.FromSeconds(1); /// /// Sound played when unwrapping this parcel. /// - [DataField] + [DataField, AutoNetworkedField] public SoundSpecifier? UnwrapSound; /// @@ -43,4 +44,11 @@ public sealed partial class WrappedParcelComponent : Component /// [DataField, ViewVariables(VVAccess.ReadOnly)] public string ContainerId = "contents"; + + /// + /// If a player trapped inside this parcel can escape from it by unwrapping it. + /// This is set by the used to create the parcel. + /// + [DataField, AutoNetworkedField] + public bool CanSelfUnwrap = true; } diff --git a/Content.Shared/ParcelWrap/Systems/ParcelWrappingSystem.ParcelWrap.cs b/Content.Shared/ParcelWrap/Systems/ParcelWrappingSystem.ParcelWrap.cs index f924a7c6c9..bd2e1eb47f 100644 --- a/Content.Shared/ParcelWrap/Systems/ParcelWrappingSystem.ParcelWrap.cs +++ b/Content.Shared/ParcelWrap/Systems/ParcelWrappingSystem.ParcelWrap.cs @@ -1,7 +1,9 @@ using Content.Shared.DoAfter; +using Content.Shared.IdentityManagement; using Content.Shared.Interaction; using Content.Shared.Item; using Content.Shared.ParcelWrap.Components; +using Content.Shared.Popups; using Content.Shared.Verbs; using Robust.Shared.Utility; @@ -59,9 +61,26 @@ public sealed partial class ParcelWrappingSystem private bool TryStartWrapDoAfter(EntityUid user, Entity wrapper, EntityUid target) { + var duration = wrapper.Comp.WrapDelay; + + if (TryComp(target, out var overrideComp) && overrideComp.WrapDelay != null) + duration = overrideComp.WrapDelay.Value; + + // In case the target is a player inform them with a popup. + if (target == user) + { + var selfMsg = Loc.GetString("parcel-wrap-popup-being-wrapped-self"); + _popup.PopupClient(selfMsg, user, user); + } + else + { + var othersMsg = Loc.GetString("parcel-wrap-popup-being-wrapped", ("user", Identity.Entity(user, EntityManager))); + _popup.PopupEntity(othersMsg, target, target, PopupType.MediumCaution); + } + return _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, - wrapper.Comp.WrapDelay, + duration, new ParcelWrapItemDoAfterEvent(), wrapper, // Raise the event on the wrapper because that's what the event handler expects. target, @@ -81,18 +100,34 @@ public sealed partial class ParcelWrappingSystem /// The entity being wrapped. private void WrapInternal(EntityUid user, Entity wrapper, EntityUid target) { - if (_net.IsServer) + // Consume a `use` on the wrapper, and delete the wrapper if it's empty. + _charges.TryUseCharges(wrapper.Owner, 1); + if (_charges.IsEmpty(wrapper.Owner)) + PredictedQueueDel(wrapper); + + // Play a wrapping sound. + _audio.PlayPredicted(wrapper.Comp.WrapSound, target, user); + + if (_net.IsClient) + return; // Predicted spawns can't be interacted with yet. + + EntityUid spawned; + var targetTransform = Transform(target); + // Check if the target has a pre-defined parcel type to be used. + if (TryComp(target, out var overrideComp)) + { + spawned = Spawn(overrideComp.ParcelPrototype, targetTransform.Coordinates); + } + else // Create a parcel with the same size and generic sprites instead. { - var spawned = Spawn(wrapper.Comp.ParcelPrototype, Transform(target).Coordinates); + spawned = Spawn(wrapper.Comp.ParcelPrototype, targetTransform.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. @@ -103,33 +138,28 @@ public sealed partial class ParcelWrappingSystem // 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(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.TryUseCharges(wrapper.Owner, 1); - if (_net.IsServer && _charges.IsEmpty(wrapper.Owner)) - QueueDel(wrapper); + _transform.SetLocalRotation(spawned, targetTransform.LocalRotation); - // Play a wrapping sound. - _audio.PlayPredicted(wrapper.Comp.WrapSound, target, user); + // If the target is 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(spawned); + parcel.CanSelfUnwrap = wrapper.Comp.CanSelfUnwrap; + Dirty(spawned, parcel); + + if (!_container.Insert(target, parcel.Contents)) + { + DebugTools.Assert( + $"Failed to insert target entity into newly spawned parcel. target={PrettyPrint.PrintUserFacing(target)}"); + PredictedDel(spawned); + } } } diff --git a/Content.Shared/ParcelWrap/Systems/ParcelWrappingSystem.WrappedParcel.cs b/Content.Shared/ParcelWrap/Systems/ParcelWrappingSystem.WrappedParcel.cs index 3f1aa2fa80..460ba7caf5 100644 --- a/Content.Shared/ParcelWrap/Systems/ParcelWrappingSystem.WrappedParcel.cs +++ b/Content.Shared/ParcelWrap/Systems/ParcelWrappingSystem.WrappedParcel.cs @@ -41,7 +41,7 @@ public sealed partial class ParcelWrappingSystem if (!args.CanAccess || !args.CanComplexInteract) return; - if (entity.Comp.Contents.Contains(args.User)) + if (!entity.Comp.CanSelfUnwrap && entity.Comp.Contents.Contains(args.User)) return; // "Capture" the values from `args` because C# doesn't like doing the capturing for `ref` values. diff --git a/Resources/Locale/en-US/pacel-wrap.ftl b/Resources/Locale/en-US/parcel-wrap.ftl similarity index 71% rename from Resources/Locale/en-US/pacel-wrap.ftl rename to Resources/Locale/en-US/parcel-wrap.ftl index 2351892fa5..560ed78dc7 100644 --- a/Resources/Locale/en-US/pacel-wrap.ftl +++ b/Resources/Locale/en-US/parcel-wrap.ftl @@ -2,6 +2,8 @@ parcel-wrap-verb-wrap = Wrap parcel-wrap-verb-unwrap = Unwrap parcel-wrap-popup-parcel-destroyed = The wrapping containing { THE($contents) } is destroyed! +parcel-wrap-popup-being-wrapped = {CAPITALIZE(THE($user))} is trying to parcel wrap you! +parcel-wrap-popup-being-wrapped-self = You start parcel wrapping yourself. # Shown when parcel wrap is examined in details range parcel-wrap-examine-detail-uses = { $uses -> diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index d2a8b21a0b..f33cee8b20 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -176,6 +176,9 @@ factions: - NanoTrasen - type: CreamPied + - type: ParcelWrapOverride + parcelPrototype: WrappedParcelHumanoid + wrapDelay: 5 - type: Stripping - type: UserInterface interfaces: diff --git a/Resources/Prototypes/Entities/Objects/Misc/parcel_wrap.yml b/Resources/Prototypes/Entities/Objects/Misc/parcel_wrap.yml index 8d7baa4339..df4b5541ff 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/parcel_wrap.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/parcel_wrap.yml @@ -18,6 +18,7 @@ whitelist: components: - Item + - ParcelWrapOverride blacklist: components: - NukeDisk # Don't try to hide the disk. @@ -38,48 +39,24 @@ - type: ParcelWrap whitelist: null blacklist: null + canSelfUnwrap: false # admins held me at gunpoint until I added this bool - type: entity - parent: BaseItem - id: WrappedParcel - categories: [ HideSpawnMenu ] + abstract: true + id: BaseWrappedParcel name: wrapped parcel description: Something wrapped up in paper. I wonder what's inside... components: + - type: Physics + bodyType: Dynamic + - type: GravityAffected + - type: Clickable + - type: InteractionOutline + - type: Pullable - type: ContainerContainer containers: contents: !type:ContainerSlot paper_label: !type:ContainerSlot - - type: Appearance - - type: GenericVisualizer - visuals: - enum.WrappedParcelVisuals.Size: - enum.WrappedParcelVisuals.Layer: - "Tiny": { state: "parcel-tiny" } - "Small": { state: "parcel-small" } - "Medium": { state: "parcel-medium" } - "Large": { state: "parcel-medium" } - "Huge": { state: "parcel-large" } - "Ginormous": { state: "parcel-large" } - enum.PaperLabelVisuals.HasLabel: - enum.PaperLabelVisuals.Layer: - True: { visible: true } - False: { visible: false } - enum.PaperLabelVisuals.LabelType: - enum.PaperLabelVisuals.Layer: - Paper: { state: paper } - Bounty: { state: bounty } - CaptainsPaper: { state: captains_paper } - Invoice: { state: invoice } - - type: Sprite - sprite: Objects/Misc/ParcelWrap/wrapped_parcel.rsi - layers: - - state: parcel-medium - map: [ "enum.WrappedParcelVisuals.Layer" ] - - state: paper - visible: false - sprite: Objects/Misc/ParcelWrap/paper_labels.rsi - map: ["enum.PaperLabelVisuals.Layer"] - type: WrappedParcel unwrapDelay: 0.5 unwrapSound: @@ -118,6 +95,67 @@ tags: - Recyclable # Parcel entity is recyclable, and when it's destroyed, it'll drop its contents. +- type: entity + parent: [BaseItem, BaseWrappedParcel] + id: WrappedParcel + components: + - type: Appearance + - type: GenericVisualizer + visuals: + enum.WrappedParcelVisuals.Size: + enum.WrappedParcelVisuals.Layer: + "Tiny": { state: "parcel-tiny" } + "Small": { state: "parcel-small" } + "Medium": { state: "parcel-medium" } + "Large": { state: "parcel-medium" } + "Huge": { state: "parcel-large" } + "Ginormous": { state: "parcel-large" } + enum.PaperLabelVisuals.HasLabel: + enum.PaperLabelVisuals.Layer: + True: { visible: true } + False: { visible: false } + enum.PaperLabelVisuals.LabelType: + enum.PaperLabelVisuals.Layer: + Paper: { state: paper } + Bounty: { state: bounty } + CaptainsPaper: { state: captains_paper } + Invoice: { state: invoice } + - type: Sprite + sprite: Objects/Misc/ParcelWrap/wrapped_parcel.rsi + layers: + - state: parcel-medium + map: [ "enum.WrappedParcelVisuals.Layer" ] + - state: paper + visible: false + sprite: Objects/Misc/ParcelWrap/paper_labels.rsi + map: ["enum.PaperLabelVisuals.Layer"] + + +- type: entity + parent: BaseWrappedParcel + id: WrappedParcelHumanoid + description: Something wrapped up in paper. It's suspiciously human-shaped. + components: + - type: Physics + fixedRotation: false + - type: Sprite + sprite: Objects/Misc/ParcelWrap/wrapped_parcel.rsi + state: human + noRot: true + drawdepth: Mobs + - type: Fixtures + fixtures: + fix1: + shape: + !type:PhysShapeCircle + radius: 0.35 + density: 185 + restitution: 0.0 + mask: + - MobMask + layer: + - MobLayer + - type: entity parent: BaseItem id: ParcelWrapTrash diff --git a/Resources/Textures/Objects/Misc/ParcelWrap/wrapped_parcel.rsi/human.png b/Resources/Textures/Objects/Misc/ParcelWrap/wrapped_parcel.rsi/human.png new file mode 100644 index 0000000000..b8729670b2 Binary files /dev/null and b/Resources/Textures/Objects/Misc/ParcelWrap/wrapped_parcel.rsi/human.png differ diff --git a/Resources/Textures/Objects/Misc/ParcelWrap/wrapped_parcel.rsi/meta.json b/Resources/Textures/Objects/Misc/ParcelWrap/wrapped_parcel.rsi/meta.json index f07107aaa8..ec930e60c6 100644 --- a/Resources/Textures/Objects/Misc/ParcelWrap/wrapped_parcel.rsi/meta.json +++ b/Resources/Textures/Objects/Misc/ParcelWrap/wrapped_parcel.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-4.0", - "copyright": "created for ss14 by ps3moira (github) based on package sprites from vgstation at https://github.com/vgstation-coders/vgstation13/pull/36993", + "copyright": "created for ss14 by ps3moira (github) based on package sprites from vgstation at https://github.com/vgstation-coders/vgstation13/pull/36993 | human by Davyei (Discord)", "size": { "x": 32, "y": 32 @@ -34,6 +34,10 @@ { "name": "tall-crate", "directions": 1 + }, + { + "name": "human", + "directions": 4 } ] }