diff --git a/Content.Client/EntryPoint.cs b/Content.Client/EntryPoint.cs index d3032c8720..2c7040f6f2 100644 --- a/Content.Client/EntryPoint.cs +++ b/Content.Client/EntryPoint.cs @@ -99,6 +99,9 @@ namespace Content.Client "PlayerInputMover", "Computer", "AsteroidRock", + "IdCard", + "Access", + "AccessReader", }; foreach (var ignoreName in registerIgnore) diff --git a/Content.Client/GameObjects/Components/Doors/AirlockVisualizer2D.cs b/Content.Client/GameObjects/Components/Doors/AirlockVisualizer2D.cs index 85c348e003..a565f07207 100644 --- a/Content.Client/GameObjects/Components/Doors/AirlockVisualizer2D.cs +++ b/Content.Client/GameObjects/Components/Doors/AirlockVisualizer2D.cs @@ -16,6 +16,7 @@ namespace Content.Client.GameObjects.Components.Doors private Animation CloseAnimation; private Animation OpenAnimation; + private Animation DenyAnimation; public override void LoadData(YamlMappingNode node) { @@ -23,6 +24,7 @@ namespace Content.Client.GameObjects.Components.Doors var openSound = node.GetNode("open_sound").AsString(); var closeSound = node.GetNode("close_sound").AsString(); + var denySound = node.GetNode("deny_sound").AsString(); CloseAnimation = new Animation {Length = TimeSpan.FromSeconds(1.2f)}; { @@ -57,6 +59,18 @@ namespace Content.Client.GameObjects.Components.Doors OpenAnimation.AnimationTracks.Add(sound); sound.KeyFrames.Add(new AnimationTrackPlaySound.KeyFrame(openSound, 0)); } + + DenyAnimation = new Animation {Length = TimeSpan.FromSeconds(0.45f)}; + { + var flick = new AnimationTrackSpriteFlick(); + DenyAnimation.AnimationTracks.Add(flick); + flick.LayerKey = DoorVisualLayers.Base; + flick.KeyFrames.Add(new AnimationTrackSpriteFlick.KeyFrame("deny", 0f)); + + var sound = new AnimationTrackPlaySound(); + DenyAnimation.AnimationTracks.Add(sound); + sound.KeyFrames.Add(new AnimationTrackPlaySound.KeyFrame(denySound, 0)); + } } public override void InitializeEntity(IEntity entity) @@ -102,6 +116,13 @@ namespace Content.Client.GameObjects.Components.Doors sprite.LayerSetState(DoorVisualLayers.Base, "open"); sprite.LayerSetVisible(DoorVisualLayers.BaseUnlit, false); break; + case DoorVisualState.Deny: + sprite.LayerSetVisible(DoorVisualLayers.BaseUnlit, false); + if (!animPlayer.HasRunningAnimation(AnimationKey)) + { + animPlayer.Play(DenyAnimation, AnimationKey); + } + break; default: throw new ArgumentOutOfRangeException(); } diff --git a/Content.Server/GameObjects/Components/Access/AccessComponent.cs b/Content.Server/GameObjects/Components/Access/AccessComponent.cs new file mode 100644 index 0000000000..4de9021f28 --- /dev/null +++ b/Content.Server/GameObjects/Components/Access/AccessComponent.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; + +namespace Content.Server.GameObjects.Components.Access +{ + [RegisterComponent] + public class AccessComponent : Component + { + public override string Name => "Access"; + private List _tags; + [ViewVariables] + public List Tags => _tags; + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + + serializer.DataField(ref _tags, "tags", new List()); + } + } +} diff --git a/Content.Server/GameObjects/Components/Access/AccessReaderComponent.cs b/Content.Server/GameObjects/Components/Access/AccessReaderComponent.cs new file mode 100644 index 0000000000..ffa555006e --- /dev/null +++ b/Content.Server/GameObjects/Components/Access/AccessReaderComponent.cs @@ -0,0 +1,86 @@ +using System.Collections.Generic; +using Content.Server.Interfaces.GameObjects; +using Content.Shared.GameObjects.Components.Inventory; +using JetBrains.Annotations; +using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; + +namespace Content.Server.GameObjects.Components.Access +{ + [RegisterComponent] + public class AccessReader : Component + { + public override string Name => "AccessReader"; + private List _necessaryTags; + private List _sufficientTags; + + public bool IsAllowed(IEntity entity) + { + var accessProvider = FindAccessProvider(entity); + return accessProvider != null && IsAllowed(accessProvider); + } + + private bool IsAllowed(AccessComponent accessProvider) + { + foreach (var sufficient in _sufficientTags) + { + if (accessProvider.Tags.Contains(sufficient)) + { + return true; + } + } + foreach (var necessary in _necessaryTags) + { + if (!accessProvider.Tags.Contains(necessary)) + { + return false; + } + } + return true; + } + + [CanBeNull] + private static AccessComponent FindAccessProvider(IEntity entity) + { + if (entity.TryGetComponent(out AccessComponent accessComponent)) + { + return accessComponent; + } + + if (entity.TryGetComponent(out IHandsComponent handsComponent)) + { + var activeHandEntity = handsComponent.GetActiveHand?.Owner; + if (activeHandEntity != null && + activeHandEntity.TryGetComponent(out AccessComponent handAccessComponent)) + { + return handAccessComponent; + } + } + else + { + return null; + } + if (entity.TryGetComponent(out InventoryComponent inventoryComponent)) + { + if (inventoryComponent.HasSlot(EquipmentSlotDefines.Slots.IDCARD) && + inventoryComponent.TryGetSlotItem(EquipmentSlotDefines.Slots.IDCARD, out ItemComponent item) && + item.Owner.TryGetComponent(out AccessComponent idAccessComponent) + ) + { + return idAccessComponent; + } + } + return null; + } + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + + serializer.DataField(ref _necessaryTags, "necessary" ,new List()); + serializer.DataField(ref _sufficientTags, "sufficient", new List()); + } + } +} diff --git a/Content.Server/GameObjects/Components/Access/IdCardComponent.cs b/Content.Server/GameObjects/Components/Access/IdCardComponent.cs new file mode 100644 index 0000000000..0449164baf --- /dev/null +++ b/Content.Server/GameObjects/Components/Access/IdCardComponent.cs @@ -0,0 +1,25 @@ +using System.Security.Cryptography; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; + +namespace Content.Server.GameObjects.Components.Access +{ + [RegisterComponent] + public class IdCardComponent : Component + { + public override string Name => "IdCard"; + [ViewVariables(VVAccess.ReadWrite)] + private string _fullName; + [ViewVariables(VVAccess.ReadWrite)] + private string _jobTitle; + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + + serializer.DataField(ref _fullName, "fullName", string.Empty); + serializer.DataField(ref _jobTitle, "jobTitle", string.Empty); + } + } +} diff --git a/Content.Server/GameObjects/Components/Doors/ServerDoorComponent.cs b/Content.Server/GameObjects/Components/Doors/ServerDoorComponent.cs index 3ddee7f3bf..60d9bf7197 100644 --- a/Content.Server/GameObjects/Components/Doors/ServerDoorComponent.cs +++ b/Content.Server/GameObjects/Components/Doors/ServerDoorComponent.cs @@ -1,4 +1,5 @@ using System; +using Content.Server.GameObjects.Components.Access; using Content.Server.GameObjects.EntitySystems; using Content.Shared.GameObjects.Components.Doors; using Robust.Server.GameObjects; @@ -26,6 +27,7 @@ namespace Content.Server.GameObjects private static readonly TimeSpan CloseTime = TimeSpan.FromSeconds(1.2f); private static readonly TimeSpan OpenTimeOne = TimeSpan.FromSeconds(0.3f); private static readonly TimeSpan OpenTimeTwo = TimeSpan.FromSeconds(0.9f); + private static readonly TimeSpan DenyTime = TimeSpan.FromSeconds(0.45f); public override void Initialize() { @@ -51,7 +53,7 @@ namespace Content.Server.GameObjects } else if (_state == DoorState.Closed) { - Open(); + TryOpen(eventArgs.User); } } @@ -73,11 +75,24 @@ namespace Content.Server.GameObjects return; } - Open(); + TryOpen(msg.Entity); break; } } + public void TryOpen(IEntity user) + { + if (Owner.TryGetComponent(out AccessReader accessReader)) + { + if (!accessReader.IsAllowed(user)) + { + Deny(); + return; + } + } + Open(); + } + public void Open() { if (_state != DoorState.Closed) @@ -120,6 +135,15 @@ namespace Content.Server.GameObjects return true; } + public void Deny() + { + _appearance.SetData(DoorVisuals.VisualState, DoorVisualState.Deny); + Timer.Spawn(DenyTime, () => + { + _appearance.SetData(DoorVisuals.VisualState, DoorVisualState.Closed); + }); + } + private const float AUTO_CLOSE_DELAY = 5; public void OnUpdate(float frameTime) { diff --git a/Content.Shared/GameObjects/Components/Doors/SharedDoorComponent.cs b/Content.Shared/GameObjects/Components/Doors/SharedDoorComponent.cs index 2a1a57ed3a..0a50523380 100644 --- a/Content.Shared/GameObjects/Components/Doors/SharedDoorComponent.cs +++ b/Content.Shared/GameObjects/Components/Doors/SharedDoorComponent.cs @@ -18,5 +18,6 @@ namespace Content.Shared.GameObjects.Components.Doors Opening, Open, Closing, + Deny, } } diff --git a/Resources/Audio/machines/airlock_deny.ogg b/Resources/Audio/machines/airlock_deny.ogg new file mode 100644 index 0000000000..1bd0a190cb Binary files /dev/null and b/Resources/Audio/machines/airlock_deny.ogg differ diff --git a/Resources/Prototypes/Entities/buildings/airlock_base.yml b/Resources/Prototypes/Entities/buildings/airlock_base.yml index d0c5a3aafb..0412bcd78e 100644 --- a/Resources/Prototypes/Entities/buildings/airlock_base.yml +++ b/Resources/Prototypes/Entities/buildings/airlock_base.yml @@ -34,6 +34,7 @@ - type: AirlockVisualizer2D open_sound: /Audio/machines/airlock_open.ogg close_sound: /Audio/machines/airlock_close.ogg + deny_sound: /Audio/machines/airlock_deny.ogg placement: mode: SnapgridCenter diff --git a/Resources/Prototypes/Entities/buildings/airlock_types.yml b/Resources/Prototypes/Entities/buildings/airlock_types.yml index c2e7548bfa..9c033d391c 100644 --- a/Resources/Prototypes/Entities/buildings/airlock_types.yml +++ b/Resources/Prototypes/Entities/buildings/airlock_types.yml @@ -14,6 +14,7 @@ - type: AirlockVisualizer2D open_sound: /Audio/machines/airlock_ext_open.ogg close_sound: /Audio/machines/airlock_ext_close.ogg + deny_sound: /Audio/machines/airlock_deny.ogg - type: entity parent: airlock @@ -25,3 +26,12 @@ - type: Icon sprite: Buildings/airlock_engineering.rsi + +- type: entity + parent: airlock_engineering + id: airlock_engineering_locked + name: Locked Engineering Airlock + components: + - type: AccessReader + required: ["engineering"] + diff --git a/Resources/Prototypes/Entities/items/clothing/IDs.yml b/Resources/Prototypes/Entities/items/clothing/IDs.yml index 137e135216..85b2ba7dea 100644 --- a/Resources/Prototypes/Entities/items/clothing/IDs.yml +++ b/Resources/Prototypes/Entities/items/clothing/IDs.yml @@ -5,9 +5,16 @@ description: A card necessary to access various areas aboard the station components: - type: Sprite - texture: Clothing/idcard_standard.png + sprite: Clothing/id_cards.rsi + state: assistant - type: Icon - texture: Clothing/idcard_standard.png + sprite: Clothing/id_cards.rsi + state: assistant - type: Clothing Slots: - idcard + - type: IdCard + fullName: John Doe + jobTitle: Assistant + - type: Access + tags: ["civilian", "maintenance", "engineering"] diff --git a/Resources/Textures/Clothing/id_cards.rsi/assistant.png b/Resources/Textures/Clothing/id_cards.rsi/assistant.png new file mode 100644 index 0000000000..e3ebb17277 Binary files /dev/null and b/Resources/Textures/Clothing/id_cards.rsi/assistant.png differ diff --git a/Resources/Textures/Clothing/id_cards.rsi/meta.json b/Resources/Textures/Clothing/id_cards.rsi/meta.json new file mode 100644 index 0000000000..3480e885c2 --- /dev/null +++ b/Resources/Textures/Clothing/id_cards.rsi/meta.json @@ -0,0 +1 @@ +{"version": 1, "size": {"x": 32, "y": 32}, "states": [{"name": "assistant", "directions": 1, "delays": [[1.0]]}]} \ No newline at end of file