diff --git a/Content.Server/Clothing/Components/MaskComponent.cs b/Content.Server/Clothing/Components/MaskComponent.cs new file mode 100644 index 0000000000..f2ea000d0e --- /dev/null +++ b/Content.Server/Clothing/Components/MaskComponent.cs @@ -0,0 +1,20 @@ +using Content.Shared.Actions; +using Content.Shared.Actions.ActionTypes; + +namespace Content.Server.Clothing.Components +{ + [Access(typeof(MaskSystem))] + [RegisterComponent] + public sealed class MaskComponent : Component + { + /// + /// This mask can be toggled (pulled up/down) + /// + [DataField("toggleAction")] + public InstantAction? ToggleAction = null; + + public bool IsToggled = false; + } + + public sealed class ToggleMaskEvent : InstantActionEvent { } +} diff --git a/Content.Server/Clothing/MaskSystem.cs b/Content.Server/Clothing/MaskSystem.cs new file mode 100644 index 0000000000..9b3d3a876b --- /dev/null +++ b/Content.Server/Clothing/MaskSystem.cs @@ -0,0 +1,106 @@ +using Content.Shared.Actions; +using Content.Shared.Toggleable; +using Content.Shared.Inventory; +using Content.Shared.Inventory.Events; +using Content.Shared.Item; +using Content.Server.Actions; +using Content.Server.Atmos.Components; +using Content.Server.Body.Components; +using Content.Server.Clothing.Components; +using Content.Server.Disease.Components; +using Content.Server.Nutrition.EntitySystems; +using Content.Server.Popups; +using Robust.Shared.Player; + +namespace Content.Server.Clothing +{ + public sealed class MaskSystem : EntitySystem + { + [Dependency] private readonly PopupSystem _popupSystem = default!; + [Dependency] private readonly InventorySystem _inventorySystem = default!; + [Dependency] private readonly ActionsSystem _actionSystem = default!; + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnToggleMask); + SubscribeLocalEvent(OnGetActions); + SubscribeLocalEvent(OnGotUnequipped); + } + + private void OnGetActions(EntityUid uid, MaskComponent component, GetItemActionsEvent args) + { + if (component.ToggleAction != null && !args.InHands) + args.Actions.Add(component.ToggleAction); + } + + private void OnToggleMask(EntityUid uid, MaskComponent mask, ToggleMaskEvent args) + { + if (mask.ToggleAction == null) + return; + + if (!_inventorySystem.TryGetSlotEntity(args.Performer, "mask", out var existing) || !mask.Owner.Equals(existing)) + return; + + mask.IsToggled ^= true; + _actionSystem.SetToggled(mask.ToggleAction, mask.IsToggled); + + if (mask.IsToggled) + _popupSystem.PopupEntity(Loc.GetString("action-mask-pull-down-popup-message", ("mask", mask.Owner)), args.Performer, Filter.Entities(args.Performer)); + else + _popupSystem.PopupEntity(Loc.GetString("action-mask-pull-up-popup-message", ("mask", mask.Owner)), args.Performer, Filter.Entities(args.Performer)); + + ToggleMaskComponents(uid, mask, args.Performer); + } + + // set to untoggled when unequipped, so it isn't left in a 'pulled down' state + private void OnGotUnequipped(EntityUid uid, MaskComponent mask, GotUnequippedEvent args) + { + if (mask.ToggleAction == null) + return; + + mask.IsToggled = false; + _actionSystem.SetToggled(mask.ToggleAction, mask.IsToggled); + + ToggleMaskComponents(uid, mask, args.Equipee, true); + } + + private void ToggleMaskComponents(EntityUid uid, MaskComponent mask, EntityUid wearer, bool isEquip = false) + { + //toggle visuals + if (TryComp(mask.Owner, out var item)) + { + //TODO: sprites for 'pulled down' state. defaults to invisible due to no sprite with this prefix + item.EquippedPrefix = mask.IsToggled ? "toggled" : null; + Dirty(item); + } + + //toggle ingestion blocking + if (TryComp(uid, out var blocker)) + blocker.Enabled = !mask.IsToggled; + + //toggle disease protection + if (TryComp(uid, out var diseaseProtection)) + diseaseProtection.IsActive = !mask.IsToggled; + + //toggle breath tool connection (skip during equip since that is handled in LungSystem) + if (isEquip || !TryComp(uid, out var breathTool)) + return; + + if (mask.IsToggled) + { + breathTool.DisconnectInternals(); + } + else + { + breathTool.IsFunctional = true; + + if (TryComp(wearer, out InternalsComponent? internals)) + { + breathTool.ConnectedInternalsEntity = wearer; + internals.ConnectBreathTool(uid); + } + } + } + } +} diff --git a/Content.Server/Nutrition/Components/IngestionBlockerComponent.cs b/Content.Server/Nutrition/Components/IngestionBlockerComponent.cs index 7b1d3c64a4..c1764a7e28 100644 --- a/Content.Server/Nutrition/Components/IngestionBlockerComponent.cs +++ b/Content.Server/Nutrition/Components/IngestionBlockerComponent.cs @@ -1,3 +1,5 @@ +using Content.Server.Clothing; + namespace Content.Server.Nutrition.EntitySystems; /// @@ -7,7 +9,7 @@ namespace Content.Server.Nutrition.EntitySystems; /// In the event that more head-wear & mask functionality is added (like identity systems, or raising/lowering of /// masks), then this component might become redundant. /// -[RegisterComponent, Access(typeof(FoodSystem), typeof(DrinkSystem))] +[RegisterComponent, Access(typeof(FoodSystem), typeof(DrinkSystem), typeof(MaskSystem))] public sealed class IngestionBlockerComponent : Component { /// diff --git a/Resources/Locale/en-US/actions/actions/mask.ftl b/Resources/Locale/en-US/actions/actions/mask.ftl new file mode 100644 index 0000000000..ec1113e4e6 --- /dev/null +++ b/Resources/Locale/en-US/actions/actions/mask.ftl @@ -0,0 +1,4 @@ +action-name-mask = Toggle Mask +action-description-mask-toggle = Handy, but prevents insertion of pie into your pie hole. +action-mask-pull-up-popup-message = You pull up your {$mask}. +action-mask-pull-down-popup-message = You pull down your {$mask}. \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Clothing/Masks/base_clothingmask.yml b/Resources/Prototypes/Entities/Clothing/Masks/base_clothingmask.yml index a27d5b63db..d194d0fe18 100644 --- a/Resources/Prototypes/Entities/Clothing/Masks/base_clothingmask.yml +++ b/Resources/Prototypes/Entities/Clothing/Masks/base_clothingmask.yml @@ -7,3 +7,16 @@ state: icon - type: Clothing Slots: [mask] + +- type: entity + abstract: true + parent: ClothingMaskBase + id: ClothingMaskPullableBase + components: + - type: Mask + toggleAction: + name: action-name-mask + description: action-description-mask-toggle + icon: Clothing/Mask/gas.rsi/icon.png + iconOn: Interface/Inventory/blocked.png + event: !type:ToggleMaskEvent \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Clothing/Masks/masks.yml b/Resources/Prototypes/Entities/Clothing/Masks/masks.yml index 505ab33f56..d260371acd 100644 --- a/Resources/Prototypes/Entities/Clothing/Masks/masks.yml +++ b/Resources/Prototypes/Entities/Clothing/Masks/masks.yml @@ -1,5 +1,5 @@ - type: entity - parent: ClothingMaskBase + parent: ClothingMaskPullableBase id: ClothingMaskGas name: gas mask description: A face-covering mask that can be connected to an air supply. @@ -14,7 +14,7 @@ protection: 0.05 - type: entity - parent: ClothingMaskBase + parent: ClothingMaskPullableBase id: ClothingMaskGasSecurity name: security gas mask description: A standard issue Security gas mask. @@ -29,7 +29,7 @@ protection: 0.05 - type: entity - parent: ClothingMaskBase + parent: ClothingMaskPullableBase id: ClothingMaskGasSyndicate name: syndicate gas mask description: A close-fitting tactical mask that can be connected to an air supply. @@ -45,7 +45,7 @@ - type: FlashImmunity - type: entity - parent: ClothingMaskBase + parent: ClothingMaskPullableBase id: ClothingMaskGasAtmos name: atmospheric gas mask description: Improved gas mask utilized by atmospheric technicians. It's flameproof! @@ -94,7 +94,7 @@ protection: 0.05 - type: entity - parent: ClothingMaskBase + parent: ClothingMaskPullableBase id: ClothingMaskGasExplorer name: explorer gas mask description: A military-grade gas mask that can be connected to an air supply. @@ -116,7 +116,7 @@ Heat: 0.95 - type: entity - parent: ClothingMaskBase + parent: ClothingMaskPullableBase id: ClothingMaskBreathMedical name: medical mask description: A close-fitting sterile mask that can be connected to an air supply. @@ -131,7 +131,7 @@ protection: 0.10 - type: entity - parent: ClothingMaskBase + parent: ClothingMaskPullableBase id: ClothingMaskBreath name: breath mask description: Might as well keep this on 24/7. @@ -182,7 +182,7 @@ - type: BreathMask - type: entity - parent: ClothingMaskBase + parent: ClothingMaskPullableBase id: ClothingMaskSterile name: sterile mask description: A sterile mask designed to help prevent the spread of diseases. @@ -211,7 +211,7 @@ replacement: mumble - type: entity - parent: ClothingMaskBase + parent: ClothingMaskPullableBase id: ClothingMaskPlague name: plague doctor mask description: A bad omen.