--- /dev/null
+using Content.Shared.Paper;
+using Robust.Client.GameObjects;
+
+namespace Content.Client.Paper;
+
+public sealed class EnvelopeSystem : VisualizerSystem<EnvelopeComponent>
+{
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent<EnvelopeComponent, AfterAutoHandleStateEvent>(OnAfterAutoHandleState);
+ }
+
+ private void OnAfterAutoHandleState(Entity<EnvelopeComponent> ent, ref AfterAutoHandleStateEvent args)
+ {
+ UpdateAppearance(ent);
+ }
+
+ private void UpdateAppearance(Entity<EnvelopeComponent> 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
+ }
+}
--- /dev/null
+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
+{
+ /// <summary>
+ /// The current open/sealed/torn state of the envelope
+ /// </summary>
+ [ViewVariables, DataField, AutoNetworkedField]
+ public EnvelopeState State = EnvelopeState.Open;
+
+ [DataField, ViewVariables]
+ public string SlotId = "letter_slot";
+
+ /// <summary>
+ /// Stores the current sealing/tearing doafter of the envelope
+ /// to prevent doafter spam/prediction issues
+ /// </summary>
+ [DataField, ViewVariables]
+ public DoAfterId? EnvelopeDoAfter;
+
+ /// <summary>
+ /// How long it takes to seal the envelope closed
+ /// </summary>
+ [DataField, ViewVariables]
+ public TimeSpan SealDelay = TimeSpan.FromSeconds(1);
+
+ /// <summary>
+ /// How long it takes to tear open the envelope
+ /// </summary>
+ [DataField, ViewVariables]
+ public TimeSpan TearDelay = TimeSpan.FromSeconds(1);
+
+ /// <summary>
+ /// The sound to play when the envelope is sealed closed
+ /// </summary>
+ [DataField, ViewVariables]
+ public SoundPathSpecifier? SealSound = new SoundPathSpecifier("/Audio/Effects/packetrip.ogg");
+
+ /// <summary>
+ /// The sound to play when the envelope is torn open
+ /// </summary>
+ [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
+{
+}
--- /dev/null
+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<EnvelopeComponent, ItemSlotInsertAttemptEvent>(OnInsertAttempt);
+ SubscribeLocalEvent<EnvelopeComponent, ItemSlotEjectAttemptEvent>(OnEjectAttempt);
+ SubscribeLocalEvent<EnvelopeComponent, GetVerbsEvent<AlternativeVerb>>(OnGetAltVerbs);
+ SubscribeLocalEvent<EnvelopeComponent, EnvelopeDoAfterEvent>(OnDoAfter);
+ SubscribeLocalEvent<EnvelopeComponent, ExaminedEvent>(OnExamine);
+ }
+
+ private void OnExamine(Entity<EnvelopeComponent> 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<EnvelopeComponent> ent, ref GetVerbsEvent<AlternativeVerb> 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<EnvelopeComponent> ent, ref ItemSlotInsertAttemptEvent args)
+ {
+ args.Cancelled |= ent.Comp.State != EnvelopeComponent.EnvelopeState.Open;
+ }
+
+ private void OnEjectAttempt(Entity<EnvelopeComponent> ent, ref ItemSlotEjectAttemptEvent args)
+ {
+ args.Cancelled |= ent.Comp.State == EnvelopeComponent.EnvelopeState.Sealed;
+ }
+
+ private void TryStartDoAfter(Entity<EnvelopeComponent> 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<EnvelopeComponent> 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);
+ }
+ }
+}
--- /dev/null
+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
- 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
- id: BoxFolderRed
- id: BoxFolderYellow
- id: NewtonCradle
+ - id: BoxEnvelope
- type: entity
id: CrateServiceFaxMachine
RubberStampApproved: 1
RubberStampDenied: 1
Paper: 10
+ Envelope: 10
EncryptionKeyCargo: 2
EncryptionKeyEngineering: 2
EncryptionKeyMedical: 2
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
--- /dev/null
+sample:
+ filter: true
},
{
"name": "vials"
+ },
+ {
+ "name": "envelope"
}
]
}