diff --git a/Content.Client/Paper/EnvelopeSystem.cs b/Content.Client/Paper/EnvelopeSystem.cs new file mode 100644 index 0000000000..f405ed1518 --- /dev/null +++ b/Content.Client/Paper/EnvelopeSystem.cs @@ -0,0 +1,35 @@ +using Content.Shared.Paper; +using Robust.Client.GameObjects; + +namespace Content.Client.Paper; + +public sealed class EnvelopeSystem : VisualizerSystem +{ + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnAfterAutoHandleState); + } + + private void OnAfterAutoHandleState(Entity ent, ref AfterAutoHandleStateEvent args) + { + UpdateAppearance(ent); + } + + private void UpdateAppearance(Entity ent, SpriteComponent? sprite = null) + { + if (!Resolve(ent.Owner, ref sprite)) + return; + + sprite.LayerSetVisible(EnvelopeVisualLayers.Open, ent.Comp.State == EnvelopeComponent.EnvelopeState.Open); + sprite.LayerSetVisible(EnvelopeVisualLayers.Sealed, ent.Comp.State == EnvelopeComponent.EnvelopeState.Sealed); + sprite.LayerSetVisible(EnvelopeVisualLayers.Torn, ent.Comp.State == EnvelopeComponent.EnvelopeState.Torn); + } + + public enum EnvelopeVisualLayers : byte + { + Open, + Sealed, + Torn + } +} diff --git a/Content.Shared/Paper/EnvelopeComponent.cs b/Content.Shared/Paper/EnvelopeComponent.cs new file mode 100644 index 0000000000..e56fbd85af --- /dev/null +++ b/Content.Shared/Paper/EnvelopeComponent.cs @@ -0,0 +1,63 @@ +using Content.Shared.DoAfter; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; + +namespace Content.Shared.Paper; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] +public sealed partial class EnvelopeComponent : Component +{ + /// + /// The current open/sealed/torn state of the envelope + /// + [ViewVariables, DataField, AutoNetworkedField] + public EnvelopeState State = EnvelopeState.Open; + + [DataField, ViewVariables] + public string SlotId = "letter_slot"; + + /// + /// Stores the current sealing/tearing doafter of the envelope + /// to prevent doafter spam/prediction issues + /// + [DataField, ViewVariables] + public DoAfterId? EnvelopeDoAfter; + + /// + /// How long it takes to seal the envelope closed + /// + [DataField, ViewVariables] + public TimeSpan SealDelay = TimeSpan.FromSeconds(1); + + /// + /// How long it takes to tear open the envelope + /// + [DataField, ViewVariables] + public TimeSpan TearDelay = TimeSpan.FromSeconds(1); + + /// + /// The sound to play when the envelope is sealed closed + /// + [DataField, ViewVariables] + public SoundPathSpecifier? SealSound = new SoundPathSpecifier("/Audio/Effects/packetrip.ogg"); + + /// + /// The sound to play when the envelope is torn open + /// + [DataField, ViewVariables] + public SoundPathSpecifier? TearSound = new SoundPathSpecifier("/Audio/Effects/poster_broken.ogg"); + + [Serializable, NetSerializable] + public enum EnvelopeState : byte + { + Open, + Sealed, + Torn + } +} + +[Serializable, NetSerializable] +public sealed partial class EnvelopeDoAfterEvent : SimpleDoAfterEvent +{ +} diff --git a/Content.Shared/Paper/EnvelopeSystem.cs b/Content.Shared/Paper/EnvelopeSystem.cs new file mode 100644 index 0000000000..560c2c82f9 --- /dev/null +++ b/Content.Shared/Paper/EnvelopeSystem.cs @@ -0,0 +1,108 @@ +using Content.Shared.DoAfter; +using Content.Shared.Containers.ItemSlots; +using Content.Shared.Verbs; +using Robust.Shared.Audio.Systems; +using Content.Shared.Examine; + +namespace Content.Shared.Paper; + +public sealed class EnvelopeSystem : EntitySystem +{ + [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; + [Dependency] private readonly SharedAudioSystem _audioSystem = default!; + [Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnInsertAttempt); + SubscribeLocalEvent(OnEjectAttempt); + SubscribeLocalEvent>(OnGetAltVerbs); + SubscribeLocalEvent(OnDoAfter); + SubscribeLocalEvent(OnExamine); + } + + private void OnExamine(Entity ent, ref ExaminedEvent args) + { + if (ent.Comp.State == EnvelopeComponent.EnvelopeState.Sealed) + { + args.PushMarkup(Loc.GetString("envelope-sealed-examine", ("envelope", ent.Owner))); + } + else if (ent.Comp.State == EnvelopeComponent.EnvelopeState.Torn) + { + args.PushMarkup(Loc.GetString("envelope-torn-examine", ("envelope", ent.Owner))); + } + } + + private void OnGetAltVerbs(Entity ent, ref GetVerbsEvent args) + { + if (!args.CanAccess || !args.CanInteract || args.Hands == null) + return; + + if (ent.Comp.State == EnvelopeComponent.EnvelopeState.Torn) + return; + + var user = args.User; + args.Verbs.Add(new AlternativeVerb() + { + Text = Loc.GetString(ent.Comp.State == EnvelopeComponent.EnvelopeState.Open ? "envelope-verb-seal" : "envelope-verb-tear"), + IconEntity = GetNetEntity(ent.Owner), + Act = () => + { + TryStartDoAfter(ent, user, ent.Comp.State == EnvelopeComponent.EnvelopeState.Open ? ent.Comp.SealDelay : ent.Comp.TearDelay); + }, + }); + } + + private void OnInsertAttempt(Entity ent, ref ItemSlotInsertAttemptEvent args) + { + args.Cancelled |= ent.Comp.State != EnvelopeComponent.EnvelopeState.Open; + } + + private void OnEjectAttempt(Entity ent, ref ItemSlotEjectAttemptEvent args) + { + args.Cancelled |= ent.Comp.State == EnvelopeComponent.EnvelopeState.Sealed; + } + + private void TryStartDoAfter(Entity ent, EntityUid user, TimeSpan delay) + { + if (ent.Comp.EnvelopeDoAfter.HasValue) + return; + + var doAfterEventArgs = new DoAfterArgs(EntityManager, user, delay, new EnvelopeDoAfterEvent(), ent.Owner, ent.Owner) + { + BreakOnDamage = true, + NeedHand = true, + BreakOnHandChange = true, + MovementThreshold = 0.01f, + DistanceThreshold = 1.0f, + }; + + if (_doAfterSystem.TryStartDoAfter(doAfterEventArgs, out var doAfterId)) + ent.Comp.EnvelopeDoAfter = doAfterId; + } + private void OnDoAfter(Entity ent, ref EnvelopeDoAfterEvent args) + { + ent.Comp.EnvelopeDoAfter = null; + + if (args.Cancelled) + return; + + if (ent.Comp.State == EnvelopeComponent.EnvelopeState.Open) + { + _audioSystem.PlayPredicted(ent.Comp.SealSound, ent.Owner, args.User); + ent.Comp.State = EnvelopeComponent.EnvelopeState.Sealed; + Dirty(ent.Owner, ent.Comp); + } + else if (ent.Comp.State == EnvelopeComponent.EnvelopeState.Sealed) + { + _audioSystem.PlayPredicted(ent.Comp.TearSound, ent.Owner, args.User); + ent.Comp.State = EnvelopeComponent.EnvelopeState.Torn; + Dirty(ent.Owner, ent.Comp); + + if (_itemSlotsSystem.TryGetSlot(ent.Owner, ent.Comp.SlotId, out var slotComp)) + _itemSlotsSystem.TryEjectToHands(ent.Owner, slotComp, args.User); + } + } +} diff --git a/Resources/Locale/en-US/paper/envelope.ftl b/Resources/Locale/en-US/paper/envelope.ftl new file mode 100644 index 0000000000..bb7993d284 --- /dev/null +++ b/Resources/Locale/en-US/paper/envelope.ftl @@ -0,0 +1,11 @@ +envelope-verb-seal = Seal +envelope-verb-tear = Tear + +envelope-letter-slot = Letter + +envelope-sealed-examine = [color=gray]{CAPITALIZE(THE($envelope))} is sealed.[/color] +envelope-torn-examine = [color=yellow]{CAPITALIZE(THE($envelope))} is torn and unusable![/color] + +envelope-default-message = TO: + + FROM: \ No newline at end of file diff --git a/Resources/Prototypes/Catalog/Fills/Boxes/general.yml b/Resources/Prototypes/Catalog/Fills/Boxes/general.yml index 85ee9f9ab8..cadf413f47 100644 --- a/Resources/Prototypes/Catalog/Fills/Boxes/general.yml +++ b/Resources/Prototypes/Catalog/Fills/Boxes/general.yml @@ -432,3 +432,17 @@ - id: DartYellow amount: 2 +- type: entity + name: envelope box + parent: BoxCardboard + id: BoxEnvelope + description: A box filled with envelopes. + components: + - type: Sprite + layers: + - state: box + - state: envelope + - type: StorageFill + contents: + - id: Envelope + amount: 9 \ No newline at end of file diff --git a/Resources/Prototypes/Catalog/Fills/Crates/service.yml b/Resources/Prototypes/Catalog/Fills/Crates/service.yml index 693ddab14e..d922056a8b 100644 --- a/Resources/Prototypes/Catalog/Fills/Crates/service.yml +++ b/Resources/Prototypes/Catalog/Fills/Crates/service.yml @@ -128,6 +128,7 @@ - id: BoxFolderRed - id: BoxFolderYellow - id: NewtonCradle + - id: BoxEnvelope - type: entity id: CrateServiceFaxMachine diff --git a/Resources/Prototypes/Catalog/VendingMachines/Inventories/cart.yml b/Resources/Prototypes/Catalog/VendingMachines/Inventories/cart.yml index cb0ef3246d..303b209053 100644 --- a/Resources/Prototypes/Catalog/VendingMachines/Inventories/cart.yml +++ b/Resources/Prototypes/Catalog/VendingMachines/Inventories/cart.yml @@ -8,6 +8,7 @@ RubberStampApproved: 1 RubberStampDenied: 1 Paper: 10 + Envelope: 10 EncryptionKeyCargo: 2 EncryptionKeyEngineering: 2 EncryptionKeyMedical: 2 diff --git a/Resources/Prototypes/Entities/Objects/Misc/paper.yml b/Resources/Prototypes/Entities/Objects/Misc/paper.yml index 42b850f2b2..12e1a09a0c 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/paper.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/paper.yml @@ -564,3 +564,81 @@ Blunt: 10 - type: StealTarget stealGroup: BoxFolderQmClipboard + +- type: entity + name: envelope + parent: BaseItem + id: Envelope + description: 'A small envelope for keeping prying eyes off of your sensitive documents.' + components: + - type: Sprite + sprite: Objects/Misc/bureaucracy.rsi + layers: + - state: envelope_open + map: ["enum.EnvelopeVisualLayers.Open"] + - state: envelope_closed + map: ["enum.EnvelopeVisualLayers.Sealed"] + visible: false + - state: envelope_torn + map: ["enum.EnvelopeVisualLayers.Torn"] + visible: false + - state: paper_stamp-generic + map: ["enum.PaperVisualLayers.Stamp"] + visible: false + - type: Paper + escapeFormatting: false + content: envelope-default-message + - type: PaperVisuals + headerImagePath: "/Textures/Interface/Paper/paper_heading_postage_stamp.svg.96dpi.png" + headerMargin: 216.0, 0.0, 0.0, 0.0 + contentMargin: 0.0, 0.0, 0.0, 0.0 + maxWritableArea: 368.0, 256.0 + - type: Envelope + - type: ContainerContainer + containers: + letter_slot: !type:ContainerSlot + - type: ItemSlots + slots: + letter_slot: + name: envelope-letter-slot + insertSound: /Audio/Effects/packetrip.ogg + ejectSound: /Audio/Effects/packetrip.ogg + whitelist: + tags: + - Paper + - type: ActivatableUI + key: enum.PaperUiKey.Key + requireHands: false + - type: UserInterface + interfaces: + enum.PaperUiKey.Key: + type: PaperBoundUserInterface + - type: Item + size: Tiny + - type: Tag + tags: + - Trash + - Document + #- type: Appearance, hide stamp marks until we have some kind of displacement + - type: Flammable + fireSpread: true + canResistFire: false + alwaysCombustible: true + canExtinguish: true + damage: + types: + Heat: 1 + - type: FireVisuals + sprite: Effects/fire.rsi + normalState: fire + - type: Damageable + damageModifierSet: Wood + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 15 + behaviors: + - !type:EmptyAllContainersBehaviour + - !type:DoActsBehavior + acts: [ "Destruction" ] \ No newline at end of file diff --git a/Resources/Textures/Interface/Paper/paper_heading_postage_stamp.svg.96dpi.png b/Resources/Textures/Interface/Paper/paper_heading_postage_stamp.svg.96dpi.png new file mode 100644 index 0000000000..ec3566f63e Binary files /dev/null and b/Resources/Textures/Interface/Paper/paper_heading_postage_stamp.svg.96dpi.png differ diff --git a/Resources/Textures/Interface/Paper/paper_heading_postage_stamp.svg.96dpi.png.yml b/Resources/Textures/Interface/Paper/paper_heading_postage_stamp.svg.96dpi.png.yml new file mode 100644 index 0000000000..5c43e23305 --- /dev/null +++ b/Resources/Textures/Interface/Paper/paper_heading_postage_stamp.svg.96dpi.png.yml @@ -0,0 +1,2 @@ +sample: + filter: true diff --git a/Resources/Textures/Objects/Storage/boxes.rsi/envelope.png b/Resources/Textures/Objects/Storage/boxes.rsi/envelope.png new file mode 100644 index 0000000000..93d52099fc Binary files /dev/null and b/Resources/Textures/Objects/Storage/boxes.rsi/envelope.png differ diff --git a/Resources/Textures/Objects/Storage/boxes.rsi/meta.json b/Resources/Textures/Objects/Storage/boxes.rsi/meta.json index 6963a9cdad..cdae38099d 100644 --- a/Resources/Textures/Objects/Storage/boxes.rsi/meta.json +++ b/Resources/Textures/Objects/Storage/boxes.rsi/meta.json @@ -220,6 +220,9 @@ }, { "name": "vials" + }, + { + "name": "envelope" } ] }