diff --git a/Content.Client/GameObjects/Components/AcceptCloningBoundUserInterface.cs b/Content.Client/GameObjects/Components/AcceptCloningBoundUserInterface.cs new file mode 100644 index 0000000000..9b38059110 --- /dev/null +++ b/Content.Client/GameObjects/Components/AcceptCloningBoundUserInterface.cs @@ -0,0 +1,45 @@ +using Content.Shared.GameObjects.Components; +using JetBrains.Annotations; +using Robust.Client.GameObjects.Components.UserInterface; + +namespace Content.Client.GameObjects.Components +{ + [UsedImplicitly] + public class AcceptCloningBoundUserInterface : BoundUserInterface + { + + public AcceptCloningBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + { + } + + private AcceptCloningWindow _window; + + protected override void Open() + { + base.Open(); + + _window = new AcceptCloningWindow(); + _window.OnClose += Close; + _window.DenyButton.OnPressed += _ => _window.Close(); + _window.ConfirmButton.OnPressed += _ => + { + SendMessage( + new SharedAcceptCloningComponent.UiButtonPressedMessage( + SharedAcceptCloningComponent.UiButton.Accept)); + _window.Close(); + }; + _window.OpenCentered(); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + _window?.Dispose(); + } + } + + } +} diff --git a/Content.Client/GameObjects/Components/AcceptCloningWindow.cs b/Content.Client/GameObjects/Components/AcceptCloningWindow.cs new file mode 100644 index 0000000000..1731cc9676 --- /dev/null +++ b/Content.Client/GameObjects/Components/AcceptCloningWindow.cs @@ -0,0 +1,50 @@ +#nullable enable +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; +using Robust.Shared.Localization; + +namespace Content.Client.GameObjects.Components +{ + public sealed class AcceptCloningWindow : SS14Window + { + public readonly Button DenyButton; + public readonly Button ConfirmButton; + + public AcceptCloningWindow() + { + + Title = Loc.GetString("Cloning Machine"); + + Contents.AddChild(new VBoxContainer + { + Children = + { + new VBoxContainer + { + Children = + { + (new Label + { + Text = Loc.GetString("You are being cloned! Transfer your soul to the clone body?") + }), + new HBoxContainer + { + Children = + { + (ConfirmButton = new Button + { + Text = Loc.GetString("Yes"), + }), + (DenyButton = new Button + { + Text = Loc.GetString("No"), + }) + } + }, + } + }, + } + }); + } + } +} diff --git a/Content.Client/GameObjects/Components/Actor/CharacterInterface.cs b/Content.Client/GameObjects/Components/Actor/CharacterInterface.cs index f776411554..0e6e534971 100644 --- a/Content.Client/GameObjects/Components/Actor/CharacterInterface.cs +++ b/Content.Client/GameObjects/Components/Actor/CharacterInterface.cs @@ -2,6 +2,7 @@ using System.Linq; using Content.Client.GameObjects.Components.Mobs; using Content.Client.UserInterface; +using Content.Shared.GameObjects.Components.Mobs; using Content.Shared.Input; using Robust.Client.GameObjects; using Robust.Client.Interfaces.Input; diff --git a/Content.Client/GameObjects/Components/Body/BodyManagerComponent.cs b/Content.Client/GameObjects/Components/Body/BodyManagerComponent.cs index fc54c88a78..83df6704b7 100644 --- a/Content.Client/GameObjects/Components/Body/BodyManagerComponent.cs +++ b/Content.Client/GameObjects/Components/Body/BodyManagerComponent.cs @@ -1,8 +1,10 @@ #nullable enable using Content.Client.GameObjects.Components.Disposal; +using Content.Client.GameObjects.Components.MedicalScanner; using Content.Client.Interfaces.GameObjects.Components.Interaction; using Content.Shared.GameObjects.Components.Body; using Content.Shared.GameObjects.Components.Damage; +using Content.Shared.GameObjects.Components.Medical; using Robust.Client.Interfaces.GameObjects.Components; using Robust.Shared.GameObjects; using Robust.Shared.Interfaces.GameObjects; @@ -21,7 +23,13 @@ namespace Content.Client.GameObjects.Components.Body public bool ClientCanDropOn(CanDropEventArgs eventArgs) { - return eventArgs.Target.HasComponent(); + if ( + eventArgs.Target.HasComponent()|| + eventArgs.Target.HasComponent()) + { + return true; + } + return false; } public bool ClientCanDrag(CanDragEventArgs eventArgs) diff --git a/Content.Client/GameObjects/Components/CloningPod/CloningPodBoundUserInterface.cs b/Content.Client/GameObjects/Components/CloningPod/CloningPodBoundUserInterface.cs new file mode 100644 index 0000000000..63760a880e --- /dev/null +++ b/Content.Client/GameObjects/Components/CloningPod/CloningPodBoundUserInterface.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using Content.Shared.GameObjects.Components.Medical; +using JetBrains.Annotations; +using Robust.Client.GameObjects.Components.UserInterface; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Components.UserInterface; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using static Content.Shared.GameObjects.Components.Medical.SharedCloningPodComponent; + +namespace Content.Client.GameObjects.Components.CloningPod +{ + [UsedImplicitly] + public class CloningPodBoundUserInterface : BoundUserInterface + { + public CloningPodBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + { + } + + private CloningPodWindow _window; + + protected override void Open() + { + base.Open(); + + + _window = new CloningPodWindow(new Dictionary()); + _window.OnClose += Close; + _window.CloneButton.OnPressed += _ => + { + if (_window.SelectedScan != null) + { + SendMessage(new CloningPodUiButtonPressedMessage(UiButton.Clone, (int) _window.SelectedScan)); + } + }; + _window.EjectButton.OnPressed += _ => + { + SendMessage(new CloningPodUiButtonPressedMessage(UiButton.Eject, null)); + }; + _window.OpenCentered(); + } + + protected override void UpdateState(BoundUserInterfaceState state) + { + base.UpdateState(state); + _window.Populate((CloningPodBoundUserInterfaceState) state); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + _window?.Dispose(); + } + } + } +} diff --git a/Content.Client/GameObjects/Components/CloningPod/CloningPodVisualizer.cs b/Content.Client/GameObjects/Components/CloningPod/CloningPodVisualizer.cs new file mode 100644 index 0000000000..6838794e8c --- /dev/null +++ b/Content.Client/GameObjects/Components/CloningPod/CloningPodVisualizer.cs @@ -0,0 +1,41 @@ +using System; +using Content.Shared.GameObjects.Components.Medical; +using Robust.Client.GameObjects; +using Robust.Client.Interfaces.GameObjects.Components; +using static Content.Shared.GameObjects.Components.Medical.SharedCloningPodComponent; +using static Content.Shared.GameObjects.Components.Medical.SharedCloningPodComponent.CloningPodStatus; + +namespace Content.Client.GameObjects.Components.CloningPod +{ + public class CloningPodVisualizer : AppearanceVisualizer + { + public override void OnChangeData(AppearanceComponent component) + { + base.OnChangeData(component); + + var sprite = component.Owner.GetComponent(); + if (!component.TryGetData(CloningPodVisuals.Status, out CloningPodStatus status)) return; + sprite.LayerSetState(CloningPodVisualLayers.Machine, StatusToMachineStateId(status)); + } + + private string StatusToMachineStateId(CloningPodStatus status) + { + //TODO: implement NoMind for if the mind is not yet in the body + //TODO: Find a use for GORE POD + switch (status) + { + case Cloning: return "pod_1"; + case NoMind: return "pod_e"; + case Gore: return "pod_g"; + case Idle: return "pod_0"; + default: + throw new ArgumentOutOfRangeException(nameof(status), status, "unknown CloningPodStatus"); + } + } + + public enum CloningPodVisualLayers + { + Machine, + } + } +} diff --git a/Content.Client/GameObjects/Components/CloningPod/CloningPodWindow.cs b/Content.Client/GameObjects/Components/CloningPod/CloningPodWindow.cs new file mode 100644 index 0000000000..8cc1c3b909 --- /dev/null +++ b/Content.Client/GameObjects/Components/CloningPod/CloningPodWindow.cs @@ -0,0 +1,442 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; +using Robust.Shared.Localization; +using Robust.Shared.Maths; +using Robust.Shared.Timing; +using Robust.Shared.Utility; +using Robust.Shared.Localization; +using static Content.Shared.GameObjects.Components.Medical.SharedCloningPodComponent; + +namespace Content.Client.GameObjects.Components.CloningPod +{ + public sealed class CloningPodWindow : SS14Window + { + private Dictionary _scanManager; + + private readonly VBoxContainer _mainVBox; + private readonly ScanListContainer _scanList; + private readonly LineEdit _searchBar; + private readonly Button _clearButton; + public readonly Button CloneButton; + public readonly Button EjectButton; + private readonly CloningScanButton _measureButton; + private CloningScanButton? _selectedButton; + private Label _progressLabel; + private readonly ProgressBar _cloningProgressBar; + private Label _mindState; + + protected override Vector2 ContentsMinimumSize => _mainVBox?.CombinedMinimumSize ?? Vector2.Zero; + private CloningPodBoundUserInterfaceState _lastUpdate = null!; + + // List of scans that are visible based on current filter criteria. + private readonly Dictionary _filteredScans = new Dictionary(); + + // The indices of the visible scans last time UpdateVisibleScans was ran. + // This is inclusive, so end is the index of the last scan, not right after it. + private (int start, int end) _lastScanIndices; + + public int? SelectedScan; + + protected override Vector2? CustomSize => (250, 300); + + public CloningPodWindow( + Dictionary scanManager) + { + _scanManager = scanManager; + + + Title = Loc.GetString("Cloning Machine"); + + Contents.AddChild(_mainVBox = new VBoxContainer + { + Children = + { + new HBoxContainer + { + Children = + { + (_searchBar = new LineEdit + { + SizeFlagsHorizontal = SizeFlags.FillExpand, + PlaceHolder = Loc.GetString("Search") + }), + + (_clearButton = new Button + { + Disabled = true, + Text = Loc.GetString("Clear"), + }) + } + }, + new ScrollContainer + { + CustomMinimumSize = new Vector2(200.0f, 0.0f), + SizeFlagsVertical = SizeFlags.FillExpand, + Children = + { + (_scanList = new ScanListContainer()) + } + }, + new VBoxContainer + { + Children = + { + (CloneButton = new Button + { + Text = Loc.GetString("Clone") + }) + } + }, + (_measureButton = new CloningScanButton {Visible = false}), + (_cloningProgressBar = new ProgressBar + { + CustomMinimumSize = (200, 20), + SizeFlagsHorizontal = SizeFlags.Fill, + MinValue = 0, + MaxValue = 10, + Page = 0, + Value = 0.5f, + Children = + { + (_progressLabel = new Label()) + } + }), + (EjectButton = new Button + { + Text = Loc.GetString("Eject Body") + }), + new HBoxContainer + { + Children = + { + new Label() + { + Text = Loc.GetString("Neural Interface: ") + }, + (_mindState = new Label() + { + Text = Loc.GetString("No Activity"), + FontColorOverride = Color.Red + }), + } + } + } + }); + + + _searchBar.OnTextChanged += OnSearchBarTextChanged; + _clearButton.OnPressed += OnClearButtonPressed; + + BuildEntityList(); + + _searchBar.GrabKeyboardFocus(); + } + + public void Populate(CloningPodBoundUserInterfaceState state) + { + //Ignore useless updates or we can't interact with the UI + //TODO: come up with a better comparision, probably write a comparator because '.Equals' doesn't work + if (_lastUpdate == null || _lastUpdate.MindIdName.Count != state.MindIdName.Count) + { + _scanManager = state.MindIdName; + BuildEntityList(); + _lastUpdate = state; + } + + var percentage = state.Progress / _cloningProgressBar.MaxValue * 100; + _progressLabel.Text = $"{percentage:0}%"; + + _cloningProgressBar.Value = state.Progress; + _mindState.Text = Loc.GetString(state.MindPresent ? "Consciousness Detected" : "No Activity"); + _mindState.FontColorOverride = state.MindPresent ? Color.Green : Color.Red; + } + + private void OnSearchBarTextChanged(LineEdit.LineEditEventArgs args) + { + BuildEntityList(args.Text); + _clearButton.Disabled = string.IsNullOrEmpty(args.Text); + } + + private void OnClearButtonPressed(BaseButton.ButtonEventArgs args) + { + _searchBar.Clear(); + BuildEntityList(""); + } + + + private void BuildEntityList(string? searchStr = null) + { + _filteredScans.Clear(); + _scanList.RemoveAllChildren(); + // Reset last scan indices so it automatically updates the entire list. + _lastScanIndices = (0, -1); + _scanList.RemoveAllChildren(); + _selectedButton = null; + searchStr = searchStr?.ToLowerInvariant(); + + foreach (var scan in _scanManager) + { + if (searchStr != null && !_doesScanMatchSearch(scan.Value, searchStr)) + { + continue; + } + + _filteredScans.Add(scan.Key, scan.Value); + } + + //TODO: set up sort + //_filteredScans.Sort((a, b) => string.Compare(a.ToString(), b.ToString(), StringComparison.Ordinal)); + + _scanList.TotalItemCount = _filteredScans.Count; + } + + private void UpdateVisibleScans() + { + // Update visible buttons in the scan list. + + // Calculate index of first scan to render based on current scroll. + var height = _measureButton.CombinedMinimumSize.Y + ScanListContainer.Separation; + var offset = -_scanList.Position.Y; + var startIndex = (int) Math.Floor(offset / height); + _scanList.ItemOffset = startIndex; + + var (prevStart, prevEnd) = _lastScanIndices; + + // Calculate index of final one. + var endIndex = startIndex - 1; + var spaceUsed = -height; // -height instead of 0 because else it cuts off the last button. + + while (spaceUsed < _scanList.Parent!.Height) + { + spaceUsed += height; + endIndex += 1; + } + + endIndex = Math.Min(endIndex, _filteredScans.Count - 1); + + if (endIndex == prevEnd && startIndex == prevStart) + { + // Nothing changed so bye. + return; + } + + _lastScanIndices = (startIndex, endIndex); + + // Delete buttons at the start of the list that are no longer visible (scrolling down). + for (var i = prevStart; i < startIndex && i <= prevEnd; i++) + { + var control = (CloningScanButton) _scanList.GetChild(0); + DebugTools.Assert(control.Index == i); + _scanList.RemoveChild(control); + } + + // Delete buttons at the end of the list that are no longer visible (scrolling up). + for (var i = prevEnd; i > endIndex && i >= prevStart; i--) + { + var control = (CloningScanButton) _scanList.GetChild(_scanList.ChildCount - 1); + DebugTools.Assert(control.Index == i); + _scanList.RemoveChild(control); + } + + var array = _filteredScans.ToArray(); + + // Create buttons at the start of the list that are now visible (scrolling up). + for (var i = Math.Min(prevStart - 1, endIndex); i >= startIndex; i--) + { + InsertEntityButton(array[i], true, i); + } + + // Create buttons at the end of the list that are now visible (scrolling down). + for (var i = Math.Max(prevEnd + 1, startIndex); i <= endIndex; i++) + { + InsertEntityButton(array[i], false, i); + } + } + + // Create a spawn button and insert it into the start or end of the list. + private void InsertEntityButton(KeyValuePair scan, bool insertFirst, int index) + { + var button = new CloningScanButton + { + Scan = scan.Value, + Id = scan.Key, + Index = index // We track this index purely for debugging. + }; + button.ActualButton.OnToggled += OnItemButtonToggled; + var entityLabelText = scan.Value; + + button.EntityLabel.Text = entityLabelText; + + if (scan.Key == SelectedScan) + { + _selectedButton = button; + _selectedButton.ActualButton.Pressed = true; + } + + //TODO: replace with body's face + /*var tex = IconComponent.GetScanIcon(scan, resourceCache); + var rect = button.EntityTextureRect; + if (tex != null) + { + rect.Texture = tex.Default; + } + else + { + rect.Dispose(); + } + + rect.Dispose(); + */ + + _scanList.AddChild(button); + if (insertFirst) + { + button.SetPositionInParent(0); + } + } + + private static bool _doesScanMatchSearch(string scan, string searchStr) + { + return scan.ToLowerInvariant().Contains(searchStr); + } + + private void OnItemButtonToggled(BaseButton.ButtonToggledEventArgs args) + { + var item = (CloningScanButton) args.Button.Parent!; + if (_selectedButton == item) + { + _selectedButton = null; + SelectedScan = null; + return; + } + else if (_selectedButton != null) + { + _selectedButton.ActualButton.Pressed = false; + } + + _selectedButton = null; + SelectedScan = null; + + _selectedButton = item; + SelectedScan = item.Id; + } + + protected override void FrameUpdate(FrameEventArgs args) + { + base.FrameUpdate(args); + UpdateVisibleScans(); + } + + private class ScanListContainer : Container + { + // Quick and dirty container to do virtualization of the list. + // Basically, get total item count and offset to put the current buttons at. + // Get a constant minimum height and move the buttons in the list up to match the scrollbar. + private int _totalItemCount; + private int _itemOffset; + + public int TotalItemCount + { + get => _totalItemCount; + set + { + _totalItemCount = value; + MinimumSizeChanged(); + } + } + + public int ItemOffset + { + get => _itemOffset; + set + { + _itemOffset = value; + UpdateLayout(); + } + } + + public const float Separation = 2; + + protected override Vector2 CalculateMinimumSize() + { + if (ChildCount == 0) + { + return Vector2.Zero; + } + + var first = GetChild(0); + + var (minX, minY) = first.CombinedMinimumSize; + + return (minX, minY * TotalItemCount + (TotalItemCount - 1) * Separation); + } + + protected override void LayoutUpdateOverride() + { + if (ChildCount == 0) + { + return; + } + + var first = GetChild(0); + + var height = first.CombinedMinimumSize.Y; + var offset = ItemOffset * height + (ItemOffset - 1) * Separation; + + foreach (var child in Children) + { + FitChildInBox(child, UIBox2.FromDimensions(0, offset, Width, height)); + offset += Separation + height; + } + } + } + + [DebuggerDisplay("cloningbutton {" + nameof(Index) + "}")] + private class CloningScanButton : Control + { + public string Scan { get; set; } = default!; + public int Id { get; set; } + public Button ActualButton { get; private set; } + public Label EntityLabel { get; private set; } + public TextureRect EntityTextureRect { get; private set; } + public int Index { get; set; } + + public CloningScanButton() + { + AddChild(ActualButton = new Button + { + SizeFlagsHorizontal = SizeFlags.FillExpand, + SizeFlagsVertical = SizeFlags.FillExpand, + ToggleMode = true, + }); + + AddChild(new HBoxContainer + { + Children = + { + (EntityTextureRect = new TextureRect + { + CustomMinimumSize = (32, 32), + SizeFlagsHorizontal = SizeFlags.ShrinkCenter, + SizeFlagsVertical = SizeFlags.ShrinkCenter, + Stretch = TextureRect.StretchMode.KeepAspectCentered, + CanShrink = true + }), + (EntityLabel = new Label + { + SizeFlagsVertical = SizeFlags.ShrinkCenter, + SizeFlagsHorizontal = SizeFlags.FillExpand, + Text = "", + ClipText = true + }) + } + }); + } + } + } +} diff --git a/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerComponent.cs b/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerComponent.cs new file mode 100644 index 0000000000..62bae7d387 --- /dev/null +++ b/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerComponent.cs @@ -0,0 +1,13 @@ +using Content.Shared.GameObjects.Components.Medical; +using Robust.Shared.GameObjects; + +namespace Content.Client.GameObjects.Components.MedicalScanner +{ + + [RegisterComponent] + [ComponentReference(typeof(SharedMedicalScannerComponent))] + public class MedicalScannerComponent : SharedMedicalScannerComponent + { + + } +} diff --git a/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerWindow.cs b/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerWindow.cs index 4668854fbf..59edf052f3 100644 --- a/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerWindow.cs +++ b/Content.Client/GameObjects/Components/MedicalScanner/MedicalScannerWindow.cs @@ -24,7 +24,7 @@ namespace Content.Client.GameObjects.Components.MedicalScanner { (ScanButton = new Button { - Text = "Scan and Save DNA" + Text = Loc.GetString("Scan and Save DNA") }), (_diagnostics = new Label { diff --git a/Content.Client/GameObjects/Components/Observer/GhostComponent.cs b/Content.Client/GameObjects/Components/Observer/GhostComponent.cs index 077eae387c..f25c709136 100644 --- a/Content.Client/GameObjects/Components/Observer/GhostComponent.cs +++ b/Content.Client/GameObjects/Components/Observer/GhostComponent.cs @@ -18,8 +18,7 @@ namespace Content.Client.GameObjects.Components.Observer private GhostGui _gui; - [ViewVariables(VVAccess.ReadOnly)] - public bool CanReturnToBody { get; private set; } = true; + [ViewVariables(VVAccess.ReadOnly)] public bool CanReturnToBody { get; private set; } = true; private bool _isAttached; @@ -51,7 +50,8 @@ namespace Content.Client.GameObjects.Components.Observer base.Initialize(); if (Owner.TryGetComponent(out SpriteComponent component)) - component.Visible = _playerManager.LocalPlayer.ControlledEntity?.HasComponent() ?? false; + component.Visible = + _playerManager.LocalPlayer.ControlledEntity?.HasComponent() ?? false; } public override void HandleMessage(ComponentMessage message, IComponent component) @@ -98,7 +98,6 @@ namespace Content.Client.GameObjects.Components.Observer { _gui?.Update(); } - } } } diff --git a/Content.Client/IgnoredComponents.cs b/Content.Client/IgnoredComponents.cs index 43269da4ad..af320568fa 100644 --- a/Content.Client/IgnoredComponents.cs +++ b/Content.Client/IgnoredComponents.cs @@ -9,6 +9,7 @@ "Breakable", "Pickaxe", "Interactable", + "CloningPod", "Destructible", "Temperature", "Explosive", @@ -58,7 +59,6 @@ "AccessReader", "IdCardConsole", "Airlock", - "MedicalScanner", "WirePlacer", "Drink", "Food", diff --git a/Content.Client/UserInterface/GhostGui.cs b/Content.Client/UserInterface/GhostGui.cs index 5ac6e1a7cc..b998597790 100644 --- a/Content.Client/UserInterface/GhostGui.cs +++ b/Content.Client/UserInterface/GhostGui.cs @@ -2,12 +2,14 @@ using Content.Client.GameObjects.Components.Observer; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Shared.IoC; +using Robust.Shared.Localization; namespace Content.Client.UserInterface { public class GhostGui : Control { - public Button ReturnToBody = new Button(){Text = "Return to body"}; + + public readonly Button ReturnToBody = new Button() {Text = Loc.GetString("Return to body")}; private GhostComponent _owner; public GhostGui(GhostComponent owner) diff --git a/Content.Server/GameObjects/Components/Medical/CloningPodComponent.cs b/Content.Server/GameObjects/Components/Medical/CloningPodComponent.cs new file mode 100644 index 0000000000..4eefd651e3 --- /dev/null +++ b/Content.Server/GameObjects/Components/Medical/CloningPodComponent.cs @@ -0,0 +1,235 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Content.Server.GameObjects.Components.Mobs; +using Content.Server.GameObjects.Components.Observer; +using Content.Server.GameObjects.Components.Power.ApcNetComponents; +using Content.Server.GameObjects.EntitySystems; +using Content.Server.Interfaces; +using Content.Server.Mobs; +using Content.Server.Utility; +using Content.Shared.GameObjects.Components.Damage; +using Content.Shared.GameObjects.Components.Medical; +using Content.Shared.Interfaces.GameObjects.Components; +using Content.Shared.Preferences; +using Robust.Server.GameObjects; +using Robust.Server.GameObjects.Components.Container; +using Robust.Server.GameObjects.Components.UserInterface; +using Robust.Server.Interfaces.GameObjects; +using Robust.Server.Interfaces.Player; +using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Maths; +using Robust.Shared.Network; +using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; + +namespace Content.Server.GameObjects.Components.Medical +{ + [RegisterComponent] + [ComponentReference(typeof(IActivate))] + public class CloningPodComponent : SharedCloningPodComponent, IActivate + { + [Dependency] private readonly IServerPreferencesManager _prefsManager = null!; + [Dependency] private readonly IEntityManager _entityManager = null!; + [Dependency] private readonly IPlayerManager _playerManager = null!; + + [ViewVariables] + private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered; + + [ViewVariables] + private BoundUserInterface? UserInterface => + Owner.GetUIOrNull(CloningPodUIKey.Key); + + private ContainerSlot _bodyContainer = default!; + private Mind? _capturedMind; + private CloningPodStatus _status; + private float _cloningProgress = 0; + private float _cloningTime; + + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + serializer.DataField(ref _cloningTime, "cloningTime", 10f); + } + + public override void Initialize() + { + base.Initialize(); + if (UserInterface != null) + { + UserInterface.OnReceiveMessage += OnUiReceiveMessage; + } + + _bodyContainer = ContainerManagerComponent.Ensure($"{Name}-bodyContainer", Owner); + + //TODO: write this so that it checks for a change in power events for GORE POD cases + var newState = GetUserInterfaceState(); + UserInterface?.SetState(newState); + + UpdateUserInterface(); + + Owner.EntityManager.EventBus.SubscribeEvent(EventSource.Local, this, + HandleGhostReturn); + } + + public void Update(float frametime) + { + if (_bodyContainer.ContainedEntity != null && + Powered) + { + _cloningProgress += frametime; + _cloningProgress = MathHelper.Clamp(_cloningProgress, 0f, _cloningTime); + } + + if (_cloningProgress >= _cloningTime && + _bodyContainer.ContainedEntity != null && + _capturedMind?.Session.AttachedEntity == _bodyContainer.ContainedEntity && + Powered) + { + _bodyContainer.Remove(_bodyContainer.ContainedEntity); + _capturedMind = null; + _cloningProgress = 0f; + + _status = CloningPodStatus.Idle; + UpdateAppearance(); + } + + UpdateUserInterface(); + } + + public override void OnRemove() + { + if (UserInterface != null) + { + UserInterface.OnReceiveMessage -= OnUiReceiveMessage; + } + + Owner.EntityManager.EventBus.UnsubscribeEvent(EventSource.Local, this); + + base.OnRemove(); + } + + private void UpdateUserInterface() + { + if (!Powered) return; + + UserInterface?.SetState(GetUserInterfaceState()); + } + + private CloningPodBoundUserInterfaceState GetUserInterfaceState() + { + return new CloningPodBoundUserInterfaceState(CloningSystem.getIdToUser(), _cloningProgress, + (_status == CloningPodStatus.Cloning)); + } + + private void UpdateAppearance() + { + if (Owner.TryGetComponent(out AppearanceComponent? appearance)) + { + appearance.SetData(CloningPodVisuals.Status, _status); + } + } + + public void Activate(ActivateEventArgs eventArgs) + { + if (!Powered || + !eventArgs.User.TryGetComponent(out IActorComponent? actor)) + { + return; + } + + UserInterface?.Open(actor.playerSession); + } + + private async void OnUiReceiveMessage(ServerBoundUserInterfaceMessage obj) + { + if (!(obj.Message is CloningPodUiButtonPressedMessage message)) return; + + switch (message.Button) + { + case UiButton.Clone: + + if (message.ScanId == null) return; + + if (_bodyContainer.ContainedEntity != null || + !CloningSystem.Minds.TryGetValue(message.ScanId.Value, out var mind)) + { + return; + } + + var dead = + mind.OwnedEntity.TryGetComponent(out var damageable) && + damageable.CurrentDamageState == DamageState.Dead; + if (!dead) return; + + + var mob = _entityManager.SpawnEntity("HumanMob_Content", Owner.Transform.MapPosition); + var client = _playerManager + .GetPlayersBy(x => x.SessionId == mind.SessionId).First(); + mob.GetComponent() + .UpdateFromProfile(GetPlayerProfileAsync(client.Name).Result); + mob.Name = GetPlayerProfileAsync(client.Name).Result.Name; + + _bodyContainer.Insert(mob); + _capturedMind = mind; + + Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, + new CloningStartedMessage(_capturedMind)); + _status = CloningPodStatus.NoMind; + UpdateAppearance(); + + break; + + case UiButton.Eject: + if (_bodyContainer.ContainedEntity == null || _cloningProgress < _cloningTime) break; + + _bodyContainer.Remove(_bodyContainer.ContainedEntity!); + _capturedMind = null; + _cloningProgress = 0f; + _status = CloningPodStatus.Idle; + UpdateAppearance(); + break; + + default: + throw new ArgumentOutOfRangeException(); + } + } + + public class CloningStartedMessage : EntitySystemMessage + { + public CloningStartedMessage(Mind capturedMind) + { + CapturedMind = capturedMind; + } + + public Mind CapturedMind { get; } + } + + + private async Task GetPlayerProfileAsync(string username) + { + return (HumanoidCharacterProfile) (await _prefsManager.GetPreferencesAsync(username)) + .SelectedCharacter; + } + + private void HandleGhostReturn(GhostComponent.GhostReturnMessage message) + { + if (message.Sender == _capturedMind) + { + //If the captured mind is in a ghost, we want to get rid of it. + _capturedMind.VisitingEntity?.Delete(); + + //Transfer the mind to the new mob + _capturedMind.TransferTo(_bodyContainer.ContainedEntity); + + _status = CloningPodStatus.Cloning; + UpdateAppearance(); + } + } + } +} diff --git a/Content.Server/GameObjects/Components/Medical/MedicalScannerComponent.cs b/Content.Server/GameObjects/Components/Medical/MedicalScannerComponent.cs index cb2a11fb40..5a297b71c2 100644 --- a/Content.Server/GameObjects/Components/Medical/MedicalScannerComponent.cs +++ b/Content.Server/GameObjects/Components/Medical/MedicalScannerComponent.cs @@ -1,9 +1,14 @@ #nullable enable using System; using System.Collections.Generic; +using System.Linq; +using Content.Server.GameObjects.Components.Body; +using Content.Server.GameObjects.Components.Mobs; using Content.Server.GameObjects.Components.Power.ApcNetComponents; using Content.Server.GameObjects.EntitySystems; +using Content.Server.Players; using Content.Server.Utility; +using Content.Shared.Damage; using Content.Shared.GameObjects.Components.Damage; using Content.Shared.GameObjects.Components.Medical; using Content.Shared.GameObjects.EntitySystems; @@ -13,21 +18,24 @@ using Robust.Server.GameObjects; using Robust.Server.GameObjects.Components.Container; using Robust.Server.GameObjects.Components.UserInterface; using Robust.Server.Interfaces.GameObjects; +using Robust.Server.Interfaces.Player; using Robust.Shared.GameObjects; using Robust.Shared.Interfaces.GameObjects; -using Robust.Shared.Maths; -using Content.Shared.Damage; +using Robust.Shared.IoC; using Robust.Shared.Localization; +using Robust.Shared.Maths; using Robust.Shared.ViewVariables; namespace Content.Server.GameObjects.Components.Medical { [RegisterComponent] [ComponentReference(typeof(IActivate))] - public class MedicalScannerComponent : SharedMedicalScannerComponent, IActivate + public class MedicalScannerComponent : SharedMedicalScannerComponent, IActivate, IDragDropOn { private ContainerSlot _bodyContainer = default!; private readonly Vector2 _ejectOffset = new Vector2(-0.5f, 0f); + + [Dependency] private readonly IPlayerManager _playerManager = null!; public bool IsOccupied => _bodyContainer.ContainedEntity != null; [ViewVariables] @@ -68,13 +76,12 @@ namespace Content.Server.GameObjects.Components.Medical if (Owner.TryGetComponent(out AppearanceComponent? appearance)) { appearance?.SetData(MedicalScannerVisuals.Status, MedicalScannerStatus.Open); - }; + } return EmptyUIState; } - if (!body.TryGetComponent(out IDamageableComponent? damageable) || - damageable.CurrentDamageState == DamageState.Dead) + if (!body.TryGetComponent(out IDamageableComponent? damageable)) { return EmptyUIState; } @@ -82,7 +89,14 @@ namespace Content.Server.GameObjects.Components.Medical var classes = new Dictionary(damageable.DamageClasses); var types = new Dictionary(damageable.DamageTypes); - return new MedicalScannerBoundUserInterfaceState(body.Uid, classes, types, CloningSystem.HasUid(body.Uid)); + if (_bodyContainer.ContainedEntity?.Uid == null) + { + return new MedicalScannerBoundUserInterfaceState(body.Uid, classes, types, true); + } + + + return new MedicalScannerBoundUserInterfaceState(body.Uid, classes, types, + CloningSystem.HasDnaScan(_bodyContainer.ContainedEntity.GetComponent().Mind)); } private void UpdateUserInterface() @@ -207,22 +221,41 @@ namespace Content.Server.GameObjects.Components.Medical private void OnUiReceiveMessage(ServerBoundUserInterfaceMessage obj) { - if (!(obj.Message is UiButtonPressedMessage message)) - { - return; - } + if (!(obj.Message is UiButtonPressedMessage message)) return; switch (message.Button) { case UiButton.ScanDNA: if (_bodyContainer.ContainedEntity != null) { - CloningSystem.AddToScannedUids(_bodyContainer.ContainedEntity.Uid); + //TODO: Show a 'ERROR: Body is completely devoid of soul' if no Mind owns the entity. + CloningSystem.AddToDnaScans(_playerManager + .GetPlayersBy(playerSession => + { + var mindOwnedMob = playerSession.ContentData()?.Mind?.OwnedEntity; + + return mindOwnedMob != null && mindOwnedMob == + _bodyContainer.ContainedEntity; + }).Single() + .ContentData() + ?.Mind); } + break; default: throw new ArgumentOutOfRangeException(); } } + + public bool CanDragDropOn(DragDropEventArgs eventArgs) + { + return eventArgs.Dropped.HasComponent(); + } + + public bool DragDropOn(DragDropEventArgs eventArgs) + { + _bodyContainer.Insert(eventArgs.Dropped); + return true; + } } } diff --git a/Content.Server/GameObjects/Components/Mobs/MindComponent.cs b/Content.Server/GameObjects/Components/Mobs/MindComponent.cs index b5f9eba7e1..9c199a2cd9 100644 --- a/Content.Server/GameObjects/Components/Mobs/MindComponent.cs +++ b/Content.Server/GameObjects/Components/Mobs/MindComponent.cs @@ -1,9 +1,16 @@ #nullable enable +using System; +using Content.Server.GameObjects.Components.Body; +using Content.Server.GameObjects.Components.Medical; using Content.Server.GameObjects.Components.Observer; using Content.Server.Interfaces.GameTicking; using Content.Server.Mobs; +using Content.Server.Utility; +using Content.Shared.GameObjects.Components; using Content.Shared.GameObjects.Components.Damage; +using Content.Shared.GameObjects.Components.Mobs; using Content.Shared.GameObjects.EntitySystems; +using Robust.Server.GameObjects.Components.UserInterface; using Robust.Shared.GameObjects; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.Map; @@ -14,6 +21,7 @@ using Robust.Shared.Serialization; using Robust.Shared.Timers; using Robust.Shared.Utility; using Robust.Shared.ViewVariables; +using Serilog.Debugging; namespace Content.Server.GameObjects.Components.Mobs { @@ -50,6 +58,45 @@ namespace Content.Server.GameObjects.Components.Mobs set => _showExamineInfo = value; } + [ViewVariables] + private BoundUserInterface? UserInterface => + Owner.GetUIOrNull(SharedAcceptCloningComponent.AcceptCloningUiKey.Key); + + + public override void Initialize() + { + base.Initialize(); + Owner.EntityManager.EventBus.SubscribeEvent( + EventSource.Local, this, + HandleCloningStartedMessage); + + if (UserInterface != null) + { + UserInterface.OnReceiveMessage += OnUiAcceptCloningMessage; + } + } + + private void HandleCloningStartedMessage(CloningPodComponent.CloningStartedMessage ev) + { + if (ev.CapturedMind == Mind) + { + UserInterface?.Open(Mind.Session); + } + } + + private void OnUiAcceptCloningMessage(ServerBoundUserInterfaceMessage obj) + { + if (!(obj.Message is SharedAcceptCloningComponent.UiButtonPressedMessage message)) return; + Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new GhostComponent.GhostReturnMessage(Mind)); + } + + public override void OnRemove() + { + base.OnRemove(); + Owner.EntityManager.EventBus.UnsubscribeEvent(EventSource.Local, this); + if (UserInterface != null) UserInterface.OnReceiveMessage -= OnUiAcceptCloningMessage; + } + /// /// Don't call this unless you know what the hell you're doing. /// Use instead. @@ -133,13 +180,19 @@ namespace Content.Server.GameObjects.Components.Mobs if (!HasMind) { message.AddMarkup(!dead - ? $"[color=red]" + Loc.GetString("{0:They} {0:are} totally catatonic. The stresses of life in deep-space must have been too much for {0:them}. Any recovery is unlikely.", Owner) + "[/color]" + ? $"[color=red]" + + Loc.GetString( + "{0:They} {0:are} totally catatonic. The stresses of life in deep-space must have been too much for {0:them}. Any recovery is unlikely.", + Owner) + "[/color]" : $"[color=purple]" + Loc.GetString("{0:Their} soul has departed.", Owner) + "[/color]"); } else if (Mind?.Session == null) { - if(!dead) - message.AddMarkup("[color=yellow]" + Loc.GetString("{0:They} {0:have} a blank, absent-minded stare and appears completely unresponsive to anything. {0:They} may snap out of it soon.", Owner) + "[/color]"); + if (!dead) + message.AddMarkup("[color=yellow]" + + Loc.GetString( + "{0:They} {0:have} a blank, absent-minded stare and appears completely unresponsive to anything. {0:They} may snap out of it soon.", + Owner) + "[/color]"); } } } diff --git a/Content.Server/GameObjects/Components/Observer/GhostComponent.cs b/Content.Server/GameObjects/Components/Observer/GhostComponent.cs index cb0c938419..10f3e8a6c6 100644 --- a/Content.Server/GameObjects/Components/Observer/GhostComponent.cs +++ b/Content.Server/GameObjects/Components/Observer/GhostComponent.cs @@ -1,4 +1,6 @@ -using Content.Server.Players; +using Content.Server.GameObjects.Components.Mobs; +using Content.Server.Mobs; +using Content.Server.Players; using Content.Shared.GameObjects.Components.Observer; using Robust.Server.GameObjects; using Robust.Server.GameObjects.Components; @@ -31,7 +33,7 @@ namespace Content.Server.GameObjects.Components.Observer { base.Initialize(); - Owner.EnsureComponent().Layer = (int)VisibilityFlags.Ghost; + Owner.EnsureComponent().Layer = (int) VisibilityFlags.Ghost; } public override ComponentState GetComponentState() => new GhostComponentState(CanReturnToBody); @@ -43,18 +45,19 @@ namespace Content.Server.GameObjects.Components.Observer switch (message) { case PlayerAttachedMsg msg: - msg.NewPlayer.VisibilityMask |= (int)VisibilityFlags.Ghost; + msg.NewPlayer.VisibilityMask |= (int) VisibilityFlags.Ghost; Dirty(); break; case PlayerDetachedMsg msg: - msg.OldPlayer.VisibilityMask &= ~(int)VisibilityFlags.Ghost; + msg.OldPlayer.VisibilityMask &= ~(int) VisibilityFlags.Ghost; break; default: break; } } - public override void HandleNetworkMessage(ComponentMessage message, INetChannel netChannel, ICommonSession session = null) + public override void HandleNetworkMessage(ComponentMessage message, INetChannel netChannel, + ICommonSession session = null) { base.HandleNetworkMessage(message, netChannel, session); @@ -67,10 +70,29 @@ namespace Content.Server.GameObjects.Components.Observer actor.playerSession.ContentData().Mind.UnVisit(); Owner.Delete(); } + + break; + case ReturnToCloneComponentMessage reenter: + + if (Owner.TryGetComponent(out VisitingMindComponent mind)) + { + Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new GhostReturnMessage(mind.Mind)); + } + break; default: break; } } + + public class GhostReturnMessage : EntitySystemMessage + { + public GhostReturnMessage(Mind sender) + { + Sender = sender; + } + + public Mind Sender { get; } + } } } diff --git a/Content.Server/GameObjects/EntitySystems/CloningSystem.cs b/Content.Server/GameObjects/EntitySystems/CloningSystem.cs index 469c0f6157..e9bb60a850 100644 --- a/Content.Server/GameObjects/EntitySystems/CloningSystem.cs +++ b/Content.Server/GameObjects/EntitySystems/CloningSystem.cs @@ -1,24 +1,42 @@ using System.Collections.Generic; +using System.Linq; +using Content.Server.GameObjects.Components.Medical; +using Content.Server.Mobs; using Robust.Shared.GameObjects; using Robust.Shared.GameObjects.Systems; +using Content.Shared.GameObjects.Components.Medical; +using Microsoft.AspNetCore.Server.Kestrel.Core; namespace Content.Server.GameObjects.EntitySystems { internal sealed class CloningSystem : EntitySystem { - public static List scannedUids = new List(); - - public static void AddToScannedUids(EntityUid uid) + public override void Update(float frameTime) { - if (!scannedUids.Contains(uid)) + foreach (var comp in ComponentManager.EntityQuery()) { - scannedUids.Add(uid); + comp.Update(frameTime); } } - public static bool HasUid(EntityUid uid) + public static Dictionary Minds = new Dictionary(); + + public static void AddToDnaScans(Mind mind) { - return scannedUids.Contains(uid); + if (!Minds.ContainsValue(mind)) + { + Minds.Add(Minds.Count(), mind); + } + } + + public static bool HasDnaScan(Mind mind) + { + return Minds.ContainsValue(mind); + } + + public static Dictionary getIdToUser() + { + return Minds.ToDictionary(m => m.Key, m => m.Value.CharacterName); } } } diff --git a/Content.Shared/GameObjects/Components/Medical/SharedCloningPodComponent.cs b/Content.Shared/GameObjects/Components/Medical/SharedCloningPodComponent.cs new file mode 100644 index 0000000000..6c0ed3f87e --- /dev/null +++ b/Content.Shared/GameObjects/Components/Medical/SharedCloningPodComponent.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Components.UserInterface; +using Robust.Shared.Serialization; + +namespace Content.Shared.GameObjects.Components.Medical +{ + + public class SharedCloningPodComponent : Component + { + public override string Name => "CloningPod"; + + [Serializable, NetSerializable] + public class CloningPodBoundUserInterfaceState : BoundUserInterfaceState + { + public readonly Dictionary MindIdName; + public readonly float Progress; + public readonly bool MindPresent; + + public CloningPodBoundUserInterfaceState(Dictionary mindIdName, float progress, bool mindPresent) + { + MindIdName = mindIdName; + Progress = progress; + MindPresent = mindPresent; + } + } + + + [Serializable, NetSerializable] + public enum CloningPodUIKey + { + Key + } + + [Serializable, NetSerializable] + public enum CloningPodVisuals + { + Status + } + + [Serializable, NetSerializable] + public enum CloningPodStatus + { + Idle, + Cloning, + Gore, + NoMind + } + + [Serializable, NetSerializable] + public enum UiButton + { + Clone, + Eject + } + + [Serializable, NetSerializable] + public class CloningPodUiButtonPressedMessage : BoundUserInterfaceMessage + { + public readonly UiButton Button; + public readonly int? ScanId; + + public CloningPodUiButtonPressedMessage(UiButton button, int? scanId) + { + Button = button; + ScanId = scanId; + } + } + + } +} diff --git a/Content.Shared/GameObjects/Components/Observer/SharedGhostComponent.cs b/Content.Shared/GameObjects/Components/Observer/SharedGhostComponent.cs index 771c0b0b6f..e9a029271c 100644 --- a/Content.Shared/GameObjects/Components/Observer/SharedGhostComponent.cs +++ b/Content.Shared/GameObjects/Components/Observer/SharedGhostComponent.cs @@ -35,4 +35,11 @@ namespace Content.Shared.GameObjects.Components.Observer { public ReturnToBodyComponentMessage() => Directed = true; } + + + [Serializable, NetSerializable] + public class ReturnToCloneComponentMessage : ComponentMessage + { + public ReturnToCloneComponentMessage() => Directed = true; + } } diff --git a/Content.Shared/GameObjects/Components/SharedAcceptCloningComponent.cs b/Content.Shared/GameObjects/Components/SharedAcceptCloningComponent.cs new file mode 100644 index 0000000000..94383004c5 --- /dev/null +++ b/Content.Shared/GameObjects/Components/SharedAcceptCloningComponent.cs @@ -0,0 +1,36 @@ +using System; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Components.UserInterface; +using Robust.Shared.Serialization; + +namespace Content.Shared.GameObjects.Components +{ + public class SharedAcceptCloningComponent : Component + { + public override string Name => "AcceptCloning"; + + [Serializable, NetSerializable] + public enum AcceptCloningUiKey + { + Key + } + + [Serializable, NetSerializable] + public enum UiButton + { + Accept + } + + [Serializable, NetSerializable] + public class UiButtonPressedMessage : BoundUserInterfaceMessage + { + public readonly UiButton Button; + + public UiButtonPressedMessage(UiButton button) + { + Button = button; + } + } + + } +} diff --git a/Resources/Prototypes/Entities/Constructible/Power/cloning_machine.yml b/Resources/Prototypes/Entities/Constructible/Power/cloning_machine.yml new file mode 100644 index 0000000000..11125bcc92 --- /dev/null +++ b/Resources/Prototypes/Entities/Constructible/Power/cloning_machine.yml @@ -0,0 +1,44 @@ +- type: entity + id: CloningPod + name: Cloning Pod + description: A Cloning Pod. 50% reliable. + components: + - type: Sprite + netsync: false + sprite: Objects/Specific/Medical/cloning.rsi + layers: + - state: pod_0 + map: ["enum.CloningPodVisualLayers.Machine"] + - type: PowerReceiver + - type: Icon + sprite: Objects/Specific/Medical/cloning.rsi + state: pod_0 + - type: Anchorable + - type: Clickable + - type: InteractionOutline + - type: Collidable + shapes: + - !type:PhysShapeAabb + bounds: "-0.5,-0.25,0.5,0.25" + layer: + - Opaque + - Impassable + - MobImpassable + - VaultImpassable + IsScrapingFloor: true + - type: Physics + mass: 25 + anchored: true + - type: SnapGrid + offset: Center + - type: CloningPod + cloningTime: 10.0 + - type: Destructible + deadThreshold: 100 + - type: Appearance + visuals: + - type: CloningPodVisualizer + - type: UserInterface + interfaces: + - key: enum.CloningPodUIKey.Key + type: CloningPodBoundUserInterface diff --git a/Resources/Prototypes/Entities/Mobs/Species/human.yml b/Resources/Prototypes/Entities/Mobs/Species/human.yml index 43a4507138..f309bea3d5 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/human.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/human.yml @@ -92,7 +92,7 @@ state: r_foot - map: ["enum.HumanoidVisualLayers.Handcuffs"] color: "#ffffff" - sprite: Objects/Misc/handcuffs.rsi + sprite: Objects/Misc/handcuffs.rsi state: body-overlay-2 visible: false - map: ["enum.Slots.IDCARD"] @@ -169,6 +169,8 @@ interfaces: - key: enum.StrippingUiKey.Key type: StrippableBoundUserInterface + - key: enum.AcceptCloningUiKey.Key + type: AcceptCloningBoundUserInterface - type: entity @@ -230,7 +232,7 @@ state: r_hand - map: ["enum.HumanoidVisualLayers.Handcuffs"] color: "#ffffff" - sprite: Objects/Misc/handcuffs.rsi + sprite: Objects/Misc/handcuffs.rsi state: body-overlay-2 visible: false - map: ["enum.Slots.IDCARD"] diff --git a/Resources/Textures/Objects/Specific/Medical/Cloning.rsi/meta.json b/Resources/Textures/Objects/Specific/Medical/BodyScanner.rsi/meta.json similarity index 100% rename from Resources/Textures/Objects/Specific/Medical/Cloning.rsi/meta.json rename to Resources/Textures/Objects/Specific/Medical/BodyScanner.rsi/meta.json diff --git a/Resources/Textures/Objects/Specific/Medical/Cloning.rsi/pod_0.png b/Resources/Textures/Objects/Specific/Medical/BodyScanner.rsi/pod_0.png similarity index 100% rename from Resources/Textures/Objects/Specific/Medical/Cloning.rsi/pod_0.png rename to Resources/Textures/Objects/Specific/Medical/BodyScanner.rsi/pod_0.png diff --git a/Resources/Textures/Objects/Specific/Medical/Cloning.rsi/scanner.png b/Resources/Textures/Objects/Specific/Medical/BodyScanner.rsi/scanner.png similarity index 100% rename from Resources/Textures/Objects/Specific/Medical/Cloning.rsi/scanner.png rename to Resources/Textures/Objects/Specific/Medical/BodyScanner.rsi/scanner.png diff --git a/Resources/Textures/Objects/Specific/Medical/Cloning.rsi/scanner_maintenance.png b/Resources/Textures/Objects/Specific/Medical/BodyScanner.rsi/scanner_maintenance.png similarity index 100% rename from Resources/Textures/Objects/Specific/Medical/Cloning.rsi/scanner_maintenance.png rename to Resources/Textures/Objects/Specific/Medical/BodyScanner.rsi/scanner_maintenance.png diff --git a/Resources/Textures/Objects/Specific/Medical/Cloning.rsi/scanner_occupied.png b/Resources/Textures/Objects/Specific/Medical/BodyScanner.rsi/scanner_occupied.png similarity index 100% rename from Resources/Textures/Objects/Specific/Medical/Cloning.rsi/scanner_occupied.png rename to Resources/Textures/Objects/Specific/Medical/BodyScanner.rsi/scanner_occupied.png diff --git a/Resources/Textures/Objects/Specific/Medical/Cloning.rsi/scanner_open.png b/Resources/Textures/Objects/Specific/Medical/BodyScanner.rsi/scanner_open.png similarity index 100% rename from Resources/Textures/Objects/Specific/Medical/Cloning.rsi/scanner_open.png rename to Resources/Textures/Objects/Specific/Medical/BodyScanner.rsi/scanner_open.png diff --git a/Resources/Textures/Objects/Specific/Medical/Cloning.rsi/scanner_open_maintenance.png b/Resources/Textures/Objects/Specific/Medical/BodyScanner.rsi/scanner_open_maintenance.png similarity index 100% rename from Resources/Textures/Objects/Specific/Medical/Cloning.rsi/scanner_open_maintenance.png rename to Resources/Textures/Objects/Specific/Medical/BodyScanner.rsi/scanner_open_maintenance.png diff --git a/Resources/Textures/Objects/Specific/Medical/Cloning.rsi/scanner_open_unpowered.png b/Resources/Textures/Objects/Specific/Medical/BodyScanner.rsi/scanner_open_unpowered.png similarity index 100% rename from Resources/Textures/Objects/Specific/Medical/Cloning.rsi/scanner_open_unpowered.png rename to Resources/Textures/Objects/Specific/Medical/BodyScanner.rsi/scanner_open_unpowered.png diff --git a/Resources/Textures/Objects/Specific/Medical/Cloning.rsi/scanner_unpowered.png b/Resources/Textures/Objects/Specific/Medical/BodyScanner.rsi/scanner_unpowered.png similarity index 100% rename from Resources/Textures/Objects/Specific/Medical/Cloning.rsi/scanner_unpowered.png rename to Resources/Textures/Objects/Specific/Medical/BodyScanner.rsi/scanner_unpowered.png diff --git a/Resources/Textures/Objects/Specific/Medical/cloning.rsi/meta.json b/Resources/Textures/Objects/Specific/Medical/cloning.rsi/meta.json new file mode 100644 index 0000000000..ac8957b3fe --- /dev/null +++ b/Resources/Textures/Objects/Specific/Medical/cloning.rsi/meta.json @@ -0,0 +1,56 @@ +{ + "version": 1, + "size": { + "x": 32, + "y": 32 + }, + "license": "CC BY-SA 3.0", + "copyright": "Taken from https://github.com/tgstation/tgstation at commit 9bebd81ae0b0a7f952b59886a765c681205de31f", + "states": [ + { + "name": "pod_0", + "directions": 1, + "delays": [ + [ + 1.0 + ] + ] + }, + { + "name": "pod_1", + "directions": 1, + "delays": [ + [ + 0.2, + 0.2, + 0.2, + 0.2 + ] + ] + }, + { + "name": "pod_e", + "directions": 1, + "delays": [ + [ + 0.2, + 0.2, + 0.2, + 0.2 + ] + ] + }, + { + "name": "pod_g", + "directions": 1, + "delays": [ + [ + 0.2, + 0.2, + 0.2, + 0.2 + ] + ] + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Objects/Specific/Medical/cloning.rsi/pod_0.png b/Resources/Textures/Objects/Specific/Medical/cloning.rsi/pod_0.png new file mode 100644 index 0000000000..13d289fd6c Binary files /dev/null and b/Resources/Textures/Objects/Specific/Medical/cloning.rsi/pod_0.png differ diff --git a/Resources/Textures/Objects/Specific/Medical/cloning.rsi/pod_1.png b/Resources/Textures/Objects/Specific/Medical/cloning.rsi/pod_1.png new file mode 100644 index 0000000000..eef65e1aef Binary files /dev/null and b/Resources/Textures/Objects/Specific/Medical/cloning.rsi/pod_1.png differ diff --git a/Resources/Textures/Objects/Specific/Medical/cloning.rsi/pod_e.png b/Resources/Textures/Objects/Specific/Medical/cloning.rsi/pod_e.png new file mode 100644 index 0000000000..acd9b6c194 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Medical/cloning.rsi/pod_e.png differ diff --git a/Resources/Textures/Objects/Specific/Medical/cloning.rsi/pod_g.png b/Resources/Textures/Objects/Specific/Medical/cloning.rsi/pod_g.png new file mode 100644 index 0000000000..ea3e002156 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Medical/cloning.rsi/pod_g.png differ