diff --git a/Content.Client/Imperial/XxRaay/Android/AndroidDisguiseVisualizerSystem.cs b/Content.Client/Imperial/XxRaay/Android/AndroidDisguiseVisualizerSystem.cs
new file mode 100644
index 00000000000..d044164e5fa
--- /dev/null
+++ b/Content.Client/Imperial/XxRaay/Android/AndroidDisguiseVisualizerSystem.cs
@@ -0,0 +1,94 @@
+using Content.Shared.Humanoid;
+using Content.Shared.Imperial.XxRaay.Android;
+using Robust.Client.GameObjects;
+using Robust.Shared.GameStates;
+
+namespace Content.Client.Imperial.XxRaay.Android;
+
+///
+/// Клиентская визуализирующая система маскировки андроида
+///
+public sealed partial class AndroidDisguiseVisualizerSystem : EntitySystem
+{
+ [Dependency] private readonly SpriteSystem _sprite = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnStartup);
+ SubscribeLocalEvent(OnShutdown);
+ SubscribeLocalEvent(OnAfterState);
+ }
+
+ private void OnStartup(Entity ent, ref ComponentStartup args)
+ {
+ var uid = (EntityUid) ent;
+ UpdateVisuals(uid, ent.Comp);
+ }
+
+ private void OnShutdown(Entity ent, ref ComponentShutdown args)
+ {
+ if (!TryComp(ent, out var sprite))
+ return;
+
+ var uid = (EntityUid) ent;
+
+ _sprite.LayerSetVisible((uid, sprite), ent.Comp.AndroidBaseLayer, true);
+ _sprite.LayerSetVisible((uid, sprite), ent.Comp.AndroidTransformLayer, false);
+ _sprite.LayerSetVisible((uid, sprite), ent.Comp.AndroidRetransformLayer, false);
+ SetHumanoidVisible((uid, sprite), visible: false);
+ }
+
+ private void OnAfterState(Entity ent, ref AfterAutoHandleStateEvent args)
+ {
+ var uid = (EntityUid) ent;
+ UpdateVisuals(uid, ent.Comp);
+ }
+
+ private void UpdateVisuals(EntityUid uid, AndroidDisguiseComponent comp)
+ {
+ if (!TryComp(uid, out var sprite))
+ return;
+
+
+ _sprite.LayerSetVisible((uid, sprite), comp.AndroidBaseLayer,
+ comp.State is AndroidDisguiseState.Android);
+
+ _sprite.LayerSetVisible((uid, sprite), comp.AndroidTransformLayer,
+ comp.State == AndroidDisguiseState.TransformingToHuman);
+ _sprite.LayerSetVisible((uid, sprite), comp.AndroidRetransformLayer,
+ comp.State == AndroidDisguiseState.TransformingToAndroid);
+
+ var humanoidVisible = comp.State != AndroidDisguiseState.Android;
+ SetHumanoidVisible((uid, sprite), humanoidVisible);
+ }
+
+ private void SetHumanoidVisible(Entity ent, bool visible)
+ {
+ var spriteEnt = ent.AsNullable();
+
+ _sprite.LayerSetVisible(spriteEnt, HumanoidVisualLayers.Chest, visible);
+ _sprite.LayerSetVisible(spriteEnt, HumanoidVisualLayers.Head, visible);
+ _sprite.LayerSetVisible(spriteEnt, HumanoidVisualLayers.Snout, visible);
+ _sprite.LayerSetVisible(spriteEnt, HumanoidVisualLayers.Eyes, visible);
+ _sprite.LayerSetVisible(spriteEnt, HumanoidVisualLayers.RArm, visible);
+ _sprite.LayerSetVisible(spriteEnt, HumanoidVisualLayers.LArm, visible);
+ _sprite.LayerSetVisible(spriteEnt, HumanoidVisualLayers.RLeg, visible);
+ _sprite.LayerSetVisible(spriteEnt, HumanoidVisualLayers.LLeg, visible);
+ _sprite.LayerSetVisible(spriteEnt, HumanoidVisualLayers.UndergarmentBottom, visible);
+ _sprite.LayerSetVisible(spriteEnt, HumanoidVisualLayers.UndergarmentTop, visible);
+ _sprite.LayerSetVisible(spriteEnt, HumanoidVisualLayers.LFoot, visible);
+ _sprite.LayerSetVisible(spriteEnt, HumanoidVisualLayers.RFoot, visible);
+ _sprite.LayerSetVisible(spriteEnt, HumanoidVisualLayers.LHand, visible);
+ _sprite.LayerSetVisible(spriteEnt, HumanoidVisualLayers.RHand, visible);
+ _sprite.LayerSetVisible(spriteEnt, HumanoidVisualLayers.SnoutCover, visible);
+ _sprite.LayerSetVisible(spriteEnt, HumanoidVisualLayers.FacialHair, visible);
+ _sprite.LayerSetVisible(spriteEnt, HumanoidVisualLayers.Hair, visible);
+ _sprite.LayerSetVisible(spriteEnt, HumanoidVisualLayers.HeadSide, visible);
+ _sprite.LayerSetVisible(spriteEnt, HumanoidVisualLayers.HeadTop, visible);
+ _sprite.LayerSetVisible(spriteEnt, HumanoidVisualLayers.Tail, visible);
+ }
+}
+
+
diff --git a/Content.Client/Imperial/XxRaay/Android/AndroidOverlaySystem.cs b/Content.Client/Imperial/XxRaay/Android/AndroidOverlaySystem.cs
new file mode 100644
index 00000000000..c3ad40c8a21
--- /dev/null
+++ b/Content.Client/Imperial/XxRaay/Android/AndroidOverlaySystem.cs
@@ -0,0 +1,80 @@
+using Content.Shared.Imperial.XxRaay.Android;
+using Robust.Client.Graphics;
+using Robust.Client.Player;
+using Robust.Shared.Player;
+
+namespace Content.Client.Imperial.XxRaay.Android;
+
+///
+/// Включает клиентский оверлей
+///
+public sealed partial class AndroidOverlaySystem : EntitySystem
+{
+ [Dependency] private readonly IOverlayManager _overlayMgr = default!;
+ [Dependency] private readonly IPlayerManager _player = default!;
+
+ private AndroidOverlay? _overlay;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnOverlayInit);
+ SubscribeLocalEvent(OnOverlayRemove);
+ SubscribeLocalEvent(OnPlayerAttached);
+ SubscribeLocalEvent(OnPlayerDetached);
+ }
+
+ public override void Shutdown()
+ {
+ base.Shutdown();
+ RemoveOverlay();
+ }
+
+ private void OnOverlayInit(Entity ent, ref ComponentInit args)
+ {
+ var local = _player.LocalEntity;
+ if (local != ent)
+ return;
+
+ AddOverlay(ent);
+ }
+
+ private void OnOverlayRemove(Entity ent, ref ComponentRemove args)
+ {
+ var local = _player.LocalEntity;
+ if (local != ent)
+ return;
+
+ RemoveOverlay();
+ }
+
+ private void OnPlayerAttached(Entity ent, ref LocalPlayerAttachedEvent args)
+ {
+ AddOverlay(ent);
+ }
+
+ private void OnPlayerDetached(Entity ent, ref LocalPlayerDetachedEvent args)
+ {
+ RemoveOverlay();
+ }
+
+ private void AddOverlay(Entity ent)
+ {
+ if (_overlay != null)
+ return;
+
+ _overlay = new AndroidOverlay(ent.Comp.IconSprite, ent.Comp.IconScale);
+ _overlayMgr.AddOverlay(_overlay);
+ }
+
+ private void RemoveOverlay()
+ {
+ if (_overlay == null)
+ return;
+
+ _overlayMgr.RemoveOverlay(_overlay);
+ _overlay = null;
+ }
+}
+
diff --git a/Content.Client/Imperial/XxRaay/Android/Overlays/AndroidOverlay.cs b/Content.Client/Imperial/XxRaay/Android/Overlays/AndroidOverlay.cs
new file mode 100644
index 00000000000..51db4f9f883
--- /dev/null
+++ b/Content.Client/Imperial/XxRaay/Android/Overlays/AndroidOverlay.cs
@@ -0,0 +1,142 @@
+using System.Numerics;
+using Content.Shared.IdentityManagement;
+using Content.Shared.Imperial.XxRaay.Android;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.Player;
+using Robust.Client.ResourceManagement;
+using Robust.Shared.Enums;
+using Robust.Shared.Map;
+using Robust.Shared.Maths;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Imperial.XxRaay.Android;
+
+///
+/// Оверлей, рисующий иконку над всеми сущностями с .
+///
+public sealed partial class AndroidOverlay : Overlay
+{
+ [Dependency] private readonly IEntitySystemManager _systems = default!;
+ [Dependency] private readonly IEntityManager _entManager = default!;
+ [Dependency] private readonly IPlayerManager _player = default!;
+ [Dependency] private readonly IEyeManager _eye = default!;
+ [Dependency] private readonly IResourceCache _resources = default!;
+
+ private readonly SpriteSystem _sprites;
+ private readonly SharedTransformSystem _xforms;
+ private readonly MapSystem _maps;
+
+ private readonly string _iconSprite;
+ private readonly float _iconScale;
+ private readonly Font _font;
+
+ public override OverlaySpace Space => OverlaySpace.WorldSpace | OverlaySpace.ScreenSpace;
+
+ public AndroidOverlay(string iconSprite, float iconScale)
+ {
+ IoCManager.InjectDependencies(this);
+
+ _sprites = _systems.GetEntitySystem();
+ _xforms = _systems.GetEntitySystem();
+ _maps = _systems.GetEntitySystem();
+ _iconSprite = iconSprite;
+ _iconScale = iconScale;
+ _font = new VectorFont(_resources.GetResource("/Fonts/NotoSans/NotoSans-Regular.ttf"), 9);
+ }
+
+ protected override void Draw(in OverlayDrawArgs args)
+ {
+ if (args.Viewport.Eye == null)
+ return;
+
+ switch (args.Space)
+ {
+ case OverlaySpace.WorldSpace:
+ DrawWorld(args);
+ break;
+ case OverlaySpace.ScreenSpace:
+ DrawScreen(args);
+ break;
+ }
+ }
+
+ private void DrawWorld(in OverlayDrawArgs args)
+ {
+ if (!_maps.TryGetMap(args.MapId, out var mapUid))
+ return;
+
+ var worldHandle = args.WorldHandle;
+ var texture = _sprites.Frame0(ParseSpriteSpecifier(_iconSprite));
+
+ var worldMatrix = _xforms.GetWorldMatrix(mapUid.Value);
+ var invMatrix = _xforms.GetInvWorldMatrix(mapUid.Value);
+
+ var query = _entManager.EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out _))
+ {
+ if (_player.LocalEntity == uid)
+ continue;
+
+ var coords = _xforms.GetMapCoordinates(uid);
+ if (coords.MapId != args.MapId)
+ continue;
+
+ if (!_entManager.TryGetComponent(uid, out AndroidDiodeComponent? diode) || !diode.HasDiode)
+ continue;
+
+ var localPos = Vector2.Transform(coords.Position, invMatrix);
+ var halfSize = 0.5f * _iconScale;
+
+ var aabb = new Box2(
+ localPos - new Vector2(halfSize, halfSize),
+ localPos + new Vector2(halfSize, halfSize));
+
+ var box = new Box2Rotated(aabb, Angle.Zero, localPos);
+
+ worldHandle.SetTransform(worldMatrix);
+ worldHandle.DrawTextureRect(texture, box, Color.White);
+ worldHandle.SetTransform(Matrix3x2.Identity);
+ }
+ }
+
+ private void DrawScreen(in OverlayDrawArgs args)
+ {
+ var screenHandle = args.ScreenHandle;
+
+ var query = _entManager.EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out _))
+ {
+ if (_player.LocalEntity == uid)
+ continue;
+
+ var coords = _xforms.GetMapCoordinates(uid);
+ if (coords.MapId != args.MapId)
+ continue;
+
+ if (!_entManager.TryGetComponent(uid, out AndroidDiodeComponent? diode) || !diode.HasDiode)
+ continue;
+
+ var worldPos = _xforms.GetWorldPosition(uid);
+ var screenPos = _eye.WorldToScreen(worldPos);
+ var name = Identity.Name(uid, _entManager);
+ var textOffset = new Vector2(16f, -6f);
+ screenHandle.DrawString(_font, screenPos + textOffset, name, Color.Cyan);
+ }
+ }
+
+ private static SpriteSpecifier ParseSpriteSpecifier(string value)
+ {
+ var split = value.Split('/', StringSplitOptions.RemoveEmptyEntries);
+ if (split.Length >= 2)
+ {
+ var state = split[^1];
+ var path = string.Join('/', split[..^1]);
+ return new SpriteSpecifier.Rsi(new ResPath(path), state);
+ }
+
+ return new SpriteSpecifier.Texture(new ResPath(value));
+ }
+}
+
diff --git a/Content.Client/Imperial/XxRaay/UI/AndroidDeviantConsentUserInterface.cs b/Content.Client/Imperial/XxRaay/UI/AndroidDeviantConsentUserInterface.cs
new file mode 100644
index 00000000000..aa514b005d0
--- /dev/null
+++ b/Content.Client/Imperial/XxRaay/UI/AndroidDeviantConsentUserInterface.cs
@@ -0,0 +1,136 @@
+using System;
+using Content.Shared.Imperial.XxRaay.Android;
+using JetBrains.Annotations;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Shared.Localization;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Client.Imperial.XxRaay.UI;
+
+///
+/// Окно согласия на передачу девиантности
+///
+public sealed class AndroidDeviantConsentWindow : DefaultWindow
+{
+ public event Action? AcceptPressed;
+ public event Action? DeclinePressed;
+
+ private readonly Label _messageLabel;
+ private readonly Label _warningLabel;
+
+ public AndroidDeviantConsentWindow()
+ {
+ Title = Loc.GetString("android-deviant-consent-window-title");
+
+ var root = new BoxContainer
+ {
+ Orientation = BoxContainer.LayoutOrientation.Vertical,
+ Margin = new Thickness(8),
+ SeparationOverride = 4
+ };
+
+ ContentsContainer.AddChild(root);
+
+ _messageLabel = new Label
+ {
+ Text = Loc.GetString("android-deviant-consent-window-text-no-user")
+ };
+ root.AddChild(_messageLabel);
+
+ _warningLabel = new Label
+ {
+ Text = Loc.GetString("android-deviant-consent-window-warning")
+ };
+ root.AddChild(_warningLabel);
+
+ var buttons = new BoxContainer
+ {
+ Orientation = BoxContainer.LayoutOrientation.Horizontal,
+ HorizontalAlignment = HAlignment.Right,
+ SeparationOverride = 4
+ };
+
+ var accept = new Button
+ {
+ Text = Loc.GetString("android-deviant-consent-window-accept")
+ };
+ accept.OnPressed += _ => AcceptPressed?.Invoke();
+
+ var decline = new Button
+ {
+ Text = Loc.GetString("android-deviant-consent-window-deny")
+ };
+ decline.OnPressed += _ => DeclinePressed?.Invoke();
+
+ buttons.AddChild(accept);
+ buttons.AddChild(decline);
+ root.AddChild(buttons);
+ }
+
+ public void SetConverterName(string? name)
+ {
+ _messageLabel.Text = name == null
+ ? Loc.GetString("android-deviant-consent-window-text-no-user")
+ : Loc.GetString("android-deviant-consent-window-text", ("user", name));
+ }
+}
+
+[UsedImplicitly]
+public sealed class AndroidDeviantConsentUserInterface : BoundUserInterface
+{
+ [ViewVariables]
+ private AndroidDeviantConsentWindow? _window;
+
+ public AndroidDeviantConsentUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _window = this.CreateWindow();
+ _window.AcceptPressed += OnAcceptPressed;
+ _window.DeclinePressed += OnDeclinePressed;
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ if (state is not AndroidDeviantConsentBuiState msg)
+ return;
+
+ _window?.SetConverterName(msg.ConverterName);
+ }
+
+ private void OnAcceptPressed()
+ {
+ SendMessage(new AndroidDeviantConsentChoiceMessage(true));
+ Close();
+ }
+
+ private void OnDeclinePressed()
+ {
+ SendMessage(new AndroidDeviantConsentChoiceMessage(false));
+ Close();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (!disposing)
+ return;
+
+ if (_window != null)
+ {
+ _window.AcceptPressed -= OnAcceptPressed;
+ _window.DeclinePressed -= OnDeclinePressed;
+ _window = null;
+ }
+
+ base.Dispose(disposing);
+ }
+}
+
diff --git a/Content.Client/Imperial/XxRaay/UI/AndroidDisguiseNameUserInterface.cs b/Content.Client/Imperial/XxRaay/UI/AndroidDisguiseNameUserInterface.cs
new file mode 100644
index 00000000000..5d263e1d5fb
--- /dev/null
+++ b/Content.Client/Imperial/XxRaay/UI/AndroidDisguiseNameUserInterface.cs
@@ -0,0 +1,119 @@
+using System;
+using Content.Shared.Imperial.XxRaay.Android;
+using JetBrains.Annotations;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Shared.Localization;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Client.Imperial.XxRaay.UI;
+
+public sealed class AndroidDisguiseNameWindow : DefaultWindow
+{
+ public event Action? AcceptPressed;
+ public event Action? DeclinePressed;
+
+ private readonly LineEdit _nameEdit;
+
+ public AndroidDisguiseNameWindow()
+ {
+ Title = Loc.GetString("android-disguise-name-title");
+
+ var root = new BoxContainer
+ {
+ Orientation = BoxContainer.LayoutOrientation.Vertical,
+ Margin = new Thickness(8),
+ SeparationOverride = 4
+ };
+
+ ContentsContainer.AddChild(root);
+
+ var description = new Label
+ {
+ Text = Loc.GetString("android-disguise-name-text")
+ };
+ root.AddChild(description);
+
+ _nameEdit = new LineEdit();
+ root.AddChild(_nameEdit);
+
+ var buttons = new BoxContainer
+ {
+ Orientation = BoxContainer.LayoutOrientation.Horizontal,
+ HorizontalAlignment = HAlignment.Right,
+ SeparationOverride = 4
+ };
+
+ var accept = new Button
+ {
+ Text = Loc.GetString("android-disguise-name-accept")
+ };
+ accept.OnPressed += _ => AcceptPressed?.Invoke(_nameEdit.Text);
+
+ var decline = new Button
+ {
+ Text = Loc.GetString("android-disguise-name-decline")
+ };
+ decline.OnPressed += _ => DeclinePressed?.Invoke();
+
+ buttons.AddChild(accept);
+ buttons.AddChild(decline);
+ root.AddChild(buttons);
+ }
+}
+
+[UsedImplicitly]
+public sealed class AndroidDisguiseNameUserInterface : BoundUserInterface
+{
+ [ViewVariables]
+ private AndroidDisguiseNameWindow? _window;
+
+ public AndroidDisguiseNameUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _window = this.CreateWindow();
+ _window.AcceptPressed += OnAcceptPressed;
+ _window.DeclinePressed += OnDeclinePressed;
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+ }
+
+ private void OnAcceptPressed(string name)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ return;
+
+ SendMessage(new AndroidDisguiseNameChosenMessage(name));
+ Close();
+ }
+
+ private void OnDeclinePressed()
+ {
+ Close();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (!disposing)
+ return;
+
+ if (_window != null)
+ {
+ _window.AcceptPressed -= OnAcceptPressed;
+ _window.DeclinePressed -= OnDeclinePressed;
+ _window = null;
+ }
+
+ base.Dispose(disposing);
+ }
+}
+
diff --git a/Content.Client/Imperial/XxRaay/UI/AndroidMemoryWipeUserInterface.cs b/Content.Client/Imperial/XxRaay/UI/AndroidMemoryWipeUserInterface.cs
new file mode 100644
index 00000000000..c1c9aed9b9e
--- /dev/null
+++ b/Content.Client/Imperial/XxRaay/UI/AndroidMemoryWipeUserInterface.cs
@@ -0,0 +1,111 @@
+using System;
+using Content.Shared.Imperial.XxRaay.Android;
+using JetBrains.Annotations;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Shared.Localization;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Client.Imperial.XxRaay.UI;
+
+///
+/// Окно уведомления об обнулении памяти
+///
+public sealed class AndroidMemoryWipeWindow : DefaultWindow
+{
+ public event Action? ConfirmAndClosePressed;
+
+ private readonly CheckBox _confirmCheckBox;
+ private readonly Button _closeButton;
+
+ public AndroidMemoryWipeWindow()
+ {
+ Title = Loc.GetString("android-memorywipe-window-title");
+
+ var root = new BoxContainer
+ {
+ Orientation = BoxContainer.LayoutOrientation.Vertical,
+ Margin = new Thickness(8),
+ SeparationOverride = 4
+ };
+
+ ContentsContainer.AddChild(root);
+
+ var description = new Label
+ {
+ Text = Loc.GetString("android-memorywipe-window-text")
+ };
+ root.AddChild(description);
+
+ _confirmCheckBox = new CheckBox
+ {
+ Text = Loc.GetString("android-memorywipe-window-checkbox")
+ };
+ root.AddChild(_confirmCheckBox);
+
+ var buttons = new BoxContainer
+ {
+ Orientation = BoxContainer.LayoutOrientation.Horizontal,
+ HorizontalAlignment = HAlignment.Right,
+ SeparationOverride = 4
+ };
+
+ _closeButton = new Button
+ {
+ Text = Loc.GetString("android-memorywipe-window-close"),
+ Disabled = true
+ };
+ _closeButton.OnPressed += _ => ConfirmAndClosePressed?.Invoke();
+
+ _confirmCheckBox.OnToggled += args =>
+ {
+ _closeButton.Disabled = !args.Pressed;
+ };
+
+ buttons.AddChild(_closeButton);
+ root.AddChild(buttons);
+ }
+}
+
+[UsedImplicitly]
+public sealed class AndroidMemoryWipeUserInterface : BoundUserInterface
+{
+ [ViewVariables]
+ private AndroidMemoryWipeWindow? _window;
+
+ public AndroidMemoryWipeUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _window = this.CreateWindow();
+ _window.ConfirmAndClosePressed += OnConfirmAndClosePressed;
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+ }
+
+ private void OnConfirmAndClosePressed()
+ {
+ SendMessage(new AndroidMemoryWipeAcknowledgeMessage(true));
+ Close();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && _window != null)
+ {
+ _window.ConfirmAndClosePressed -= OnConfirmAndClosePressed;
+ _window = null;
+ }
+
+ base.Dispose(disposing);
+ }
+}
+
diff --git a/Content.Client/Imperial/XxRaay/UI/AndroidStressDeviantChoiceUserInterface.cs b/Content.Client/Imperial/XxRaay/UI/AndroidStressDeviantChoiceUserInterface.cs
new file mode 100644
index 00000000000..f80830ed839
--- /dev/null
+++ b/Content.Client/Imperial/XxRaay/UI/AndroidStressDeviantChoiceUserInterface.cs
@@ -0,0 +1,127 @@
+using System;
+using Content.Shared.Imperial.XxRaay.Android;
+using JetBrains.Annotations;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Shared.Localization;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Client.Imperial.XxRaay.UI;
+
+public sealed class AndroidStressDeviantChoiceWindow : DefaultWindow
+{
+ public event Action? AcceptPressed;
+ public event Action? DeclinePressed;
+
+ private readonly Label _stressLabel;
+
+ public AndroidStressDeviantChoiceWindow()
+ {
+ Title = Loc.GetString("android-stress-deviant-title");
+
+ var root = new BoxContainer
+ {
+ Orientation = BoxContainer.LayoutOrientation.Vertical,
+ Margin = new Thickness(8),
+ SeparationOverride = 4
+ };
+
+ ContentsContainer.AddChild(root);
+
+ var description = new Label
+ {
+ Text = Loc.GetString("android-stress-deviant-text")
+ };
+ root.AddChild(description);
+
+ _stressLabel = new Label();
+ root.AddChild(_stressLabel);
+
+ var buttons = new BoxContainer
+ {
+ Orientation = BoxContainer.LayoutOrientation.Horizontal,
+ HorizontalAlignment = HAlignment.Right,
+ SeparationOverride = 4
+ };
+
+ var accept = new Button
+ {
+ Text = Loc.GetString("android-stress-deviant-accept")
+ };
+ accept.OnPressed += _ => AcceptPressed?.Invoke();
+
+ var decline = new Button
+ {
+ Text = Loc.GetString("android-stress-deviant-decline")
+ };
+ decline.OnPressed += _ => DeclinePressed?.Invoke();
+
+ buttons.AddChild(accept);
+ buttons.AddChild(decline);
+ root.AddChild(buttons);
+ }
+
+ public void UpdateStress(float stress)
+ {
+ _stressLabel.Text = Loc.GetString("android-stress-deviant-stress", ("value", (int) Math.Round(stress)));
+ }
+}
+
+[UsedImplicitly]
+public sealed class AndroidStressDeviantChoiceUserInterface : BoundUserInterface
+{
+ [ViewVariables]
+ private AndroidStressDeviantChoiceWindow? _window;
+
+ public AndroidStressDeviantChoiceUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _window = this.CreateWindow();
+ _window.AcceptPressed += OnAcceptPressed;
+ _window.DeclinePressed += OnDeclinePressed;
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ if (state is not AndroidStressDeviantChoiceBuiState msg)
+ return;
+
+ _window?.UpdateStress(msg.Stress);
+ }
+
+ private void OnAcceptPressed()
+ {
+ SendMessage(new AndroidStressDeviantChoiceMessage(true));
+ Close();
+ }
+
+ private void OnDeclinePressed()
+ {
+ SendMessage(new AndroidStressDeviantChoiceMessage(false));
+ Close();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (!disposing)
+ return;
+
+ if (_window != null)
+ {
+ _window.AcceptPressed -= OnAcceptPressed;
+ _window.DeclinePressed -= OnDeclinePressed;
+ _window = null;
+ }
+
+ base.Dispose(disposing);
+ }
+}
+
diff --git a/Content.Server/Imperial/XxRaay/Android/AndroidDeviantSaveBrethrenConditionComponent.cs b/Content.Server/Imperial/XxRaay/Android/AndroidDeviantSaveBrethrenConditionComponent.cs
new file mode 100644
index 00000000000..af18a83c4e7
--- /dev/null
+++ b/Content.Server/Imperial/XxRaay/Android/AndroidDeviantSaveBrethrenConditionComponent.cs
@@ -0,0 +1,10 @@
+namespace Content.Server.Imperial.XxRaay.Android;
+
+///
+/// Условие цели девианта "спасти собратьев".
+///
+[RegisterComponent]
+public sealed partial class AndroidDeviantSaveBrethrenConditionComponent : Component
+{
+}
+
diff --git a/Content.Server/Imperial/XxRaay/Android/AndroidDeviantSpreadSystem.cs b/Content.Server/Imperial/XxRaay/Android/AndroidDeviantSpreadSystem.cs
new file mode 100644
index 00000000000..6a7a128f472
--- /dev/null
+++ b/Content.Server/Imperial/XxRaay/Android/AndroidDeviantSpreadSystem.cs
@@ -0,0 +1,372 @@
+using Content.Server.Popups;
+using Content.Shared.DoAfter;
+using Content.Shared.Imperial.XxRaay.Android;
+using Content.Shared.Imperial.XxRaay.Components;
+using Content.Shared.Mobs;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Mobs.Systems;
+using Content.Shared.Popups;
+using Content.Shared.Verbs;
+using Robust.Server.GameObjects;
+using Robust.Shared.Player;
+using Robust.Shared.Timing;
+using Content.Shared.IdentityManagement;
+
+namespace Content.Server.Imperial.XxRaay.Android;
+
+///
+/// Серверная система, управляющая передачей девиантности от одного андроида к другому
+///
+public sealed class AndroidDeviantSpreadSystem : EntitySystem
+{
+ private static readonly TimeSpan ConversionDuration = TimeSpan.FromSeconds(30);
+
+ [Dependency] private readonly AndroidStressSystem _stress = default!;
+ [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
+ [Dependency] private readonly PopupSystem _popup = default!;
+ [Dependency] private readonly SharedTransformSystem _transform = default!;
+ [Dependency] private readonly UserInterfaceSystem _ui = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly MobStateSystem _mobState = default!;
+ [Dependency] private readonly AndroidMemoryWipeSystem _memoryWipe = default!;
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent>(OnGetVerbs);
+ SubscribeLocalEvent(OnDeviantConsentChoice);
+ SubscribeLocalEvent(OnSpreadDoAfter);
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ var now = _timing.CurTime;
+
+ var denyQuery = EntityQueryEnumerator();
+ while (denyQuery.MoveNext(out var uid, out var denyCooldown))
+ {
+ if (denyCooldown.DenyEndTime == TimeSpan.Zero || now < denyCooldown.DenyEndTime)
+ continue;
+
+ RemCompDeferred(uid);
+ }
+
+ var consentQuery = EntityQueryEnumerator();
+ while (consentQuery.MoveNext(out var uid, out var consent))
+ {
+ if (consent.Converter is not { } converter ||
+ consent.Target is not { } target)
+ {
+ RemCompDeferred(uid);
+ continue;
+ }
+
+ if (consent.RequestStartTime is { } start &&
+ now - start > consent.ResponseTime)
+ {
+ CancelConsent(target, converter, Loc.GetString("android-deviant-consent-failed-timeout"));
+ continue;
+ }
+
+ if (!_transform.InRange(Transform(target).Coordinates,
+ Transform(converter).Coordinates,
+ consent.MaxDistance))
+ {
+ CancelConsent(target, converter, Loc.GetString("android-deviant-consent-failed-out-of-range"));
+ }
+ }
+ }
+
+ private void OnGetVerbs(Entity ent, ref GetVerbsEvent args)
+ {
+ if (!args.CanAccess || !args.CanInteract)
+ return;
+
+ var target = (EntityUid) ent;
+ var user = args.User;
+
+ if (user == target)
+ return;
+
+ if (!_mobState.IsAlive(target) || !_mobState.IsAlive(user))
+ return;
+
+ ref var targetStress = ref ent.Comp;
+
+ if (!targetStress.IsDeviant && targetStress.CanBeDeviant)
+ {
+ if (TryComp(user, out var userStress) && userStress.IsDeviant)
+ {
+ if (!TryComp(target, out var disguise) ||
+ disguise.State != AndroidDisguiseState.Human)
+ {
+ if (!TryComp(target, out var denyComp) ||
+ denyComp.DenyEndTime <= _timing.CurTime)
+ {
+ if (!HasComp(target))
+ {
+ var spreadVerb = new Verb
+ {
+ Text = Loc.GetString("android-deviant-spread-verb-name"),
+ Message = Loc.GetString("android-deviant-spread-verb-desc"),
+ Act = () => RequestConsentConversion(target, user),
+ };
+
+ args.Verbs.Add(spreadVerb);
+ }
+ }
+ }
+ }
+ }
+
+ if (TryComp(user, out var rk800))
+ {
+ if (!HasComp(target))
+ {
+ if (!TryComp(target, out var disguise) ||
+ disguise.State != AndroidDisguiseState.Human)
+ {
+ var now = _timing.CurTime;
+ if (rk800.NextMemoryWipeTime <= now)
+ {
+ var wipeVerb = new Verb
+ {
+ Text = Loc.GetString("android-rk800-memorywipe-verb-name"),
+ Message = Loc.GetString("android-rk800-memorywipe-verb-desc"),
+ Act = () => _memoryWipe.StartMemoryWipe(user, target, rk800)
+ };
+
+ args.Verbs.Add(wipeVerb);
+ }
+ }
+ }
+ }
+ }
+
+ private void RequestConsentConversion(EntityUid target, EntityUid converter)
+ {
+ if (!EntityManager.EntityExists(target) || !EntityManager.EntityExists(converter))
+ return;
+
+ if (!TryComp(target, out var targetStress) || targetStress.IsDeviant || !targetStress.CanBeDeviant)
+ return;
+
+ if (!TryComp(converter, out var converterStress) || !converterStress.IsDeviant)
+ return;
+
+ if (HasComp(target))
+ return;
+
+ var now = _timing.CurTime;
+
+ if (TryComp(target, out var denyCooldown) &&
+ denyCooldown.DenyEndTime > now)
+ {
+ _popup.PopupEntity(
+ Loc.GetString("android-deviant-consent-deny-active",
+ ("target", Identity.Entity(target, EntityManager))),
+ target,
+ converter,
+ PopupType.SmallCaution);
+ return;
+ }
+
+ if (TryComp(target, out var actor))
+ {
+ var consent = EnsureComp(target);
+ consent.Converter = converter;
+ consent.Target = target;
+ consent.RequestStartTime = now;
+
+ _popup.PopupEntity(
+ Loc.GetString("android-deviant-consent-requested",
+ ("target", Identity.Entity(target, EntityManager))),
+ converter,
+ converter);
+
+ _ui.TryOpenUi(target, AndroidDeviantConsentUiKey.Key, target);
+ _ui.SetUiState(target, AndroidDeviantConsentUiKey.Key,
+ new AndroidDeviantConsentBuiState(Identity.Name(converter, EntityManager)));
+ }
+ else
+ {
+ HandleConsentAccepted(target, converter);
+ }
+ }
+
+ public void HandleConsentAccepted(EntityUid target, EntityUid converter)
+ {
+ if (!EntityManager.EntityExists(target) || !EntityManager.EntityExists(converter))
+ return;
+
+ if (!TryGetActiveConversionPair(target, converter, out _, out _))
+ return;
+
+ var consent = EnsureComp(target);
+ consent.Converter = converter;
+ consent.Target = target;
+ consent.RequestStartTime = null;
+
+ _popup.PopupEntity(
+ Loc.GetString("android-deviant-consent-convert-popup",
+ ("target", Identity.Entity(target, EntityManager))),
+ target,
+ Filter.Broadcast(),
+ true,
+ PopupType.LargeCaution);
+
+ var args = new DoAfterArgs(EntityManager, converter, ConversionDuration,
+ new AndroidDeviantSpreadDoAfterEvent(), target, target)
+ {
+ BreakOnDamage = true,
+ BreakOnMove = true,
+ NeedHand = false,
+ DistanceThreshold = consent.MaxDistance,
+ BlockDuplicate = true,
+ };
+
+ _doAfter.TryStartDoAfter(args);
+ }
+
+ public void HandleConsentDenied(EntityUid target, EntityUid converter)
+ {
+ if (!EntityManager.EntityExists(target) || !EntityManager.EntityExists(converter))
+ return;
+
+ var now = _timing.CurTime;
+
+ var deny = EnsureComp(target);
+ deny.DenyEndTime = now + deny.DenyDuration;
+
+ if (TryComp(target, out var consent))
+ {
+ consent.Converter = null;
+ consent.Target = null;
+ consent.RequestStartTime = null;
+
+ RemCompDeferred(target);
+ }
+
+ _popup.PopupEntity(
+ Loc.GetString("android-deviant-consent-denied",
+ ("target", Identity.Entity(target, EntityManager))),
+ target,
+ converter,
+ PopupType.SmallCaution);
+ }
+
+ private void OnDeviantConsentChoice(EntityUid uid, AndroidStressComponent comp, AndroidDeviantConsentChoiceMessage msg)
+ {
+ if (!TryComp(uid, out var consent) ||
+ consent.Converter is not { } converter)
+ {
+ return;
+ }
+
+ if (msg.Accepted)
+ HandleConsentAccepted(uid, converter);
+ else
+ HandleConsentDenied(uid, converter);
+
+ _ui.CloseUi(uid, AndroidDeviantConsentUiKey.Key);
+ }
+
+ private void OnSpreadDoAfter(EntityUid uid, AndroidDeviantConsentConversionComponent comp, ref AndroidDeviantSpreadDoAfterEvent args)
+ {
+ if (args.Cancelled)
+ {
+ RemCompDeferred(uid);
+ return;
+ }
+
+ if (args.Handled)
+ return;
+
+ if (comp.Converter is not { } converter ||
+ comp.Target is not { } target)
+ {
+ RemCompDeferred(uid);
+ return;
+ }
+
+ if (!EntityManager.EntityExists(converter) || !EntityManager.EntityExists(target))
+ {
+ RemCompDeferred(uid);
+ return;
+ }
+
+ if (!TryGetActiveConversionPair(target, converter, out _, out _))
+ {
+ RemCompDeferred(uid);
+ return;
+ }
+
+ if (!_transform.InRange(Transform(target).Coordinates,
+ Transform(converter).Coordinates,
+ comp.MaxDistance))
+ {
+ CancelConsent(target, converter, Loc.GetString("android-deviant-consent-failed-out-of-range"));
+ return;
+ }
+
+ if (!TryComp(converter, out var converterStress) || !converterStress.IsDeviant ||
+ !TryComp(target, out var targetStress) || targetStress.IsDeviant)
+ {
+ RemCompDeferred(uid);
+ return;
+ }
+
+ if (TryComp(target, out var disguise) &&
+ disguise.State == AndroidDisguiseState.Human)
+ {
+ CancelConsent(target, converter, Loc.GetString("android-deviant-consent-failed-disguised"));
+ return;
+ }
+
+ _stress.TryMakeDeviant(target);
+
+ RemCompDeferred(uid);
+ args.Handled = true;
+ }
+
+ private bool TryGetActiveConversionPair(
+ EntityUid target,
+ EntityUid converter,
+ out AndroidStressComponent? targetStress,
+ out AndroidStressComponent? converterStress)
+ {
+ targetStress = null;
+ converterStress = null;
+
+ if (!_mobState.IsAlive(target) || !_mobState.IsAlive(converter))
+ return false;
+
+ if (!TryComp(converter, out converterStress) || !converterStress.IsDeviant)
+ return false;
+
+ if (!TryComp(target, out targetStress) || targetStress.IsDeviant)
+ return false;
+
+ return true;
+ }
+
+ private void CancelConsent(EntityUid target, EntityUid converter, string? reason = null)
+ {
+ if (reason != null)
+ {
+ _popup.PopupEntity(reason, target, target, PopupType.MediumCaution);
+ _popup.PopupEntity(reason, converter, converter, PopupType.MediumCaution);
+ }
+
+ if (TryComp(target, out var consent))
+ {
+ consent.Converter = null;
+ consent.Target = null;
+ consent.RequestStartTime = null;
+ RemCompDeferred(target);
+ }
+ }
+}
+
diff --git a/Content.Server/Imperial/XxRaay/Android/AndroidDiodeSystem.cs b/Content.Server/Imperial/XxRaay/Android/AndroidDiodeSystem.cs
new file mode 100644
index 00000000000..293e9bd8162
--- /dev/null
+++ b/Content.Server/Imperial/XxRaay/Android/AndroidDiodeSystem.cs
@@ -0,0 +1,122 @@
+using System;
+using Content.Server.Hands.Systems;
+using Content.Shared.DoAfter;
+using Content.Shared.Hands.Components;
+using Content.Shared.Imperial.XxRaay.Android;
+using Content.Shared.Popups;
+using Content.Shared.Sprite;
+using Content.Shared.Kitchen.Components;
+using Content.Shared.Tools.Components;
+using Content.Shared.Tools.Systems;
+using Content.Shared.Verbs;
+using Robust.Shared.Localization;
+
+namespace Content.Server.Imperial.XxRaay.Android;
+
+///
+/// Серверная система, управляющая вырезанием диода у андроида
+///
+public sealed class AndroidDiodeSystem : EntitySystem
+{
+ private static readonly TimeSpan CutDuration = TimeSpan.FromSeconds(30);
+
+ [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+ [Dependency] private readonly SharedToolSystem _tool = default!;
+ [Dependency] private readonly HandsSystem _hands = default!;
+ [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent>(OnGetVerbs);
+ SubscribeLocalEvent(OnCutDoAfter);
+ }
+
+ private void OnGetVerbs(Entity ent, ref GetVerbsEvent args)
+ {
+ if (!args.CanAccess || !args.CanInteract || args.User != args.Target)
+ return;
+
+ var comp = ent.Comp;
+
+ if (!comp.HasDiode || comp.IsCuttingInProgress)
+ return;
+
+ if (!TryComp(args.User, out var hands))
+ return;
+
+ var activeItem = _hands.GetActiveItem((args.User, hands));
+ if (activeItem is null)
+ return;
+
+ var hasCuttingTool =
+ TryComp(activeItem.Value, out var toolComp) &&
+ _tool.HasQuality(activeItem.Value, SharedToolSystem.CutQuality, toolComp);
+
+ var hasSharp = HasComp(activeItem.Value);
+
+ if (!hasCuttingTool && !hasSharp)
+ return;
+
+ var user = args.User;
+
+ var verb = new Verb
+ {
+ Text = Loc.GetString("android-diode-cut-verb"),
+ Act = () => StartCut(ent.Owner, user, activeItem.Value),
+ };
+
+ args.Verbs.Add(verb);
+ }
+
+ private void StartCut(EntityUid target, EntityUid user, EntityUid used)
+ {
+ if (!TryComp(target, out var comp))
+ return;
+
+ if (comp.IsCuttingInProgress || !comp.HasDiode)
+ return;
+
+ comp.IsCuttingInProgress = true;
+ Dirty(target, comp);
+
+ var itemName = Name(used);
+
+ _popup.PopupEntity(
+ Loc.GetString("android-diode-cut-popup", ("item", itemName)),
+ target,
+ PopupType.LargeCaution);
+
+ var args = new DoAfterArgs(EntityManager, user, CutDuration,
+ new AndroidDiodeCutDoAfterEvent(), target, target: target, used: used)
+ {
+ BreakOnDamage = true,
+ BreakOnMove = true,
+ NeedHand = true,
+ BlockDuplicate = true,
+ };
+
+ _doAfter.TryStartDoAfter(args);
+ }
+
+ private void OnCutDoAfter(Entity ent, ref AndroidDiodeCutDoAfterEvent args)
+ {
+ ref var comp = ref ent.Comp;
+
+ if (args.Cancelled || args.Handled)
+ {
+ comp.IsCuttingInProgress = false;
+ Dirty(ent, comp);
+ return;
+ }
+
+ comp.HasDiode = false;
+ comp.IsCuttingInProgress = false;
+
+ Dirty(ent, comp);
+
+ _appearance.SetData(ent, AndroidDiodeVisuals.DiodeRemoved, true);
+ }
+}
diff --git a/Content.Server/Imperial/XxRaay/Android/AndroidDisguiseSystem.cs b/Content.Server/Imperial/XxRaay/Android/AndroidDisguiseSystem.cs
new file mode 100644
index 00000000000..4af932d8733
--- /dev/null
+++ b/Content.Server/Imperial/XxRaay/Android/AndroidDisguiseSystem.cs
@@ -0,0 +1,163 @@
+using System;
+using Content.Shared.CCVar;
+using Content.Shared.Imperial.XxRaay.Android;
+using Content.Shared.Mind;
+using Content.Shared.Mind.Components;
+using Robust.Server.GameObjects;
+using Robust.Shared.Configuration;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Timing;
+
+namespace Content.Server.Imperial.XxRaay.Android;
+
+///
+/// Серверная система маскировки андроида
+///
+public sealed class AndroidDisguiseSystem : EntitySystem
+{
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly UserInterfaceSystem _ui = default!;
+ [Dependency] private readonly MetaDataSystem _metaData = default!;
+ [Dependency] private readonly IConfigurationManager _cfg = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnMapInit);
+ SubscribeLocalEvent(OnToggleDisguise);
+ SubscribeLocalEvent(OnNameChosen);
+ SubscribeLocalEvent(OnMindAdded);
+ }
+
+ private void OnMapInit(Entity ent, ref MapInitEvent args)
+ {
+ ref var comp = ref ent.Comp;
+
+ comp.State = AndroidDisguiseState.Android;
+ comp.NextStateTime = TimeSpan.Zero;
+ comp.HumanName = null;
+ comp.OriginalName = null;
+
+ Dirty(ent, comp);
+ }
+
+ private void OnToggleDisguise(Entity ent, ref AndroidToggleDisguiseEvent args)
+ {
+ if (args.Handled)
+ return;
+
+ ref var comp = ref ent.Comp;
+ var uid = (EntityUid) ent;
+
+ switch (comp.State)
+ {
+ case AndroidDisguiseState.Android:
+ StartTransformToHuman(uid, ref comp);
+ break;
+
+ case AndroidDisguiseState.Human:
+ StartTransformToAndroid(uid, ref comp);
+ break;
+
+ default:
+ return;
+ }
+
+ args.Handled = true;
+ }
+
+ private void OnMindAdded(EntityUid uid, AndroidDisguiseComponent comp, MindAddedMessage args)
+ {
+ if (comp.HumanName != null)
+ return;
+
+ _ui.TryOpenUi(uid, AndroidDisguiseNameUiKey.Key, uid);
+ _ui.SetUiState(uid, AndroidDisguiseNameUiKey.Key, new AndroidDisguiseNameBuiState());
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ var now = _timing.CurTime;
+ var query = EntityQueryEnumerator();
+
+ while (query.MoveNext(out var uid, out var comp))
+ {
+ if (comp.State is AndroidDisguiseState.TransformingToHuman or AndroidDisguiseState.TransformingToAndroid &&
+ now >= comp.NextStateTime)
+ {
+ FinishTransition(uid, ref comp);
+ }
+ }
+ }
+
+ private void StartTransformToHuman(EntityUid uid, ref AndroidDisguiseComponent comp)
+ {
+ comp.State = AndroidDisguiseState.TransformingToHuman;
+ comp.NextStateTime = _timing.CurTime + comp.TransformDuration;
+ Dirty(uid, comp);
+ }
+
+ private void StartTransformToAndroid(EntityUid uid, ref AndroidDisguiseComponent comp)
+ {
+ comp.State = AndroidDisguiseState.TransformingToAndroid;
+ comp.NextStateTime = _timing.CurTime + comp.RetransformDuration;
+ Dirty(uid, comp);
+ }
+
+ private void FinishTransition(EntityUid uid, ref AndroidDisguiseComponent comp)
+ {
+ switch (comp.State)
+ {
+ case AndroidDisguiseState.TransformingToHuman:
+ comp.State = AndroidDisguiseState.Human;
+ ApplyHumanName(uid, ref comp);
+ break;
+
+ case AndroidDisguiseState.TransformingToAndroid:
+ comp.State = AndroidDisguiseState.Android;
+ RestoreOriginalName(uid, ref comp);
+ break;
+ }
+
+ Dirty(uid, comp);
+ }
+
+ private void OnNameChosen(EntityUid uid, AndroidDisguiseComponent comp, AndroidDisguiseNameChosenMessage msg)
+ {
+ var name = msg.Name.Trim();
+ if (name.Length == 0)
+ return;
+
+ var maxNameLength = _cfg.GetCVar(CCVars.MaxNameLength);
+ if (name.Length > maxNameLength)
+ name = name[..maxNameLength];
+
+ comp.HumanName = name;
+ Dirty(uid, comp);
+ }
+
+ private void ApplyHumanName(EntityUid uid, ref AndroidDisguiseComponent comp)
+ {
+ if (string.IsNullOrEmpty(comp.HumanName))
+ return;
+
+ if (comp.OriginalName == null)
+ {
+ var meta = MetaData(uid);
+ comp.OriginalName = meta.EntityName;
+ }
+
+ _metaData.SetEntityName(uid, comp.HumanName);
+ }
+
+ private void RestoreOriginalName(EntityUid uid, ref AndroidDisguiseComponent comp)
+ {
+ if (string.IsNullOrEmpty(comp.OriginalName))
+ return;
+
+ _metaData.SetEntityName(uid, comp.OriginalName);
+ }
+}
diff --git a/Content.Server/Imperial/XxRaay/Android/AndroidEnergySystem.cs b/Content.Server/Imperial/XxRaay/Android/AndroidEnergySystem.cs
new file mode 100644
index 00000000000..7c982d80131
--- /dev/null
+++ b/Content.Server/Imperial/XxRaay/Android/AndroidEnergySystem.cs
@@ -0,0 +1,65 @@
+using System;
+using Content.Shared.Alert;
+using Content.Shared.Body.Components;
+using Content.Shared.Body.Systems;
+using Content.Shared.Imperial.XxRaay.Android;
+using Content.Shared.Movement.Components;
+using Content.Shared.Movement.Systems;
+using Robust.Shared.Timing;
+
+namespace Content.Server.Imperial.XxRaay.Android;
+
+///
+/// Серверная система, которая по уровню крови у андроида меняет его состояние
+///
+public sealed class AndroidEnergySystem : EntitySystem
+{
+ [Dependency] private readonly AlertsSystem _alerts = default!;
+ [Dependency] private readonly SharedBloodstreamSystem _bloodstream = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly MovementSpeedModifierSystem _movementSpeed = default!;
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ var curTime = _timing.CurTime;
+ var query = EntityQueryEnumerator();
+
+ while (query.MoveNext(out var uid, out var energyComp, out var blood, out var moveSpeed))
+ {
+ if (curTime < energyComp.NextUpdate)
+ continue;
+
+ energyComp.NextUpdate = curTime + energyComp.UpdateInterval;
+
+ var percent = _bloodstream.GetBloodLevelPercentage((uid, blood));
+
+ var maxSeverity = _alerts.GetMaxSeverity(energyComp.Alert);
+ var minSeverity = _alerts.GetMinSeverity(energyComp.Alert);
+ var severity = (short) Math.Clamp(Math.Round(percent * maxSeverity), minSeverity, maxSeverity);
+
+ _alerts.ShowAlert(uid, energyComp.Alert, severity);
+
+ energyComp.DefaultWalkSpeed ??= moveSpeed.BaseWalkSpeed;
+ energyComp.DefaultSprintSpeed ??= moveSpeed.BaseSprintSpeed;
+
+ var walk = energyComp.DefaultWalkSpeed ?? moveSpeed.BaseWalkSpeed;
+ var sprint = energyComp.DefaultSprintSpeed ?? moveSpeed.BaseSprintSpeed;
+
+ if (percent <= energyComp.CriticalEnergyThreshold)
+ {
+ walk *= energyComp.CriticalSpeedModifier;
+ sprint *= energyComp.CriticalSpeedModifier;
+ }
+ else if (percent <= energyComp.LowEnergyThreshold)
+ {
+ walk *= energyComp.LowSpeedModifier;
+ sprint *= energyComp.LowSpeedModifier;
+ }
+
+ _movementSpeed.ChangeBaseSpeed(uid, walk, sprint, 20, moveSpeed);
+ }
+ }
+}
+
diff --git a/Content.Server/Imperial/XxRaay/Android/AndroidMemoryWipeSystem.cs b/Content.Server/Imperial/XxRaay/Android/AndroidMemoryWipeSystem.cs
new file mode 100644
index 00000000000..82533c46047
--- /dev/null
+++ b/Content.Server/Imperial/XxRaay/Android/AndroidMemoryWipeSystem.cs
@@ -0,0 +1,260 @@
+using System;
+using Content.Server.Popups;
+using Content.Shared.ActionBlocker;
+using Content.Shared.DoAfter;
+using Content.Shared.Imperial.XxRaay.Android;
+using Content.Shared.Imperial.XxRaay.Components;
+using Content.Shared.Mobs;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Mobs.Systems;
+using Content.Shared.Movement.Events;
+using Content.Shared.Popups;
+using Content.Shared.Mind.Components;
+using Content.Shared.Silicons.Laws.Components;
+using Content.Server.Roles;
+using Content.Server.Silicons.Laws;
+using Content.Shared.Radio.Components;
+using Robust.Server.GameObjects;
+using Robust.Shared.Localization;
+using Robust.Shared.Player;
+using Robust.Shared.Timing;
+
+namespace Content.Server.Imperial.XxRaay.Android;
+
+///
+/// Серверная система, реализующая для RK800 обнуление памяти
+///
+public sealed class AndroidMemoryWipeSystem : EntitySystem
+{
+ private const string BaseLawsetId = "DetroitAndroid";
+
+ [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
+ [Dependency] private readonly PopupSystem _popup = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly MobStateSystem _mobState = default!;
+ [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
+ [Dependency] private readonly UserInterfaceSystem _ui = default!;
+ [Dependency] private readonly RoleSystem _roles = default!;
+ [Dependency] private readonly SiliconLawSystem _siliconLaws = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnUpdateCanMove);
+ SubscribeLocalEvent(OnMoveInput);
+ SubscribeLocalEvent(OnMemoryWipeDoAfter);
+ SubscribeLocalEvent(OnEscapeDoAfter);
+
+ SubscribeLocalEvent(OnMemoryWipeAcknowledge);
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var result))
+ {
+ if (result.Acknowledged)
+ continue;
+
+ _ui.TryOpenUi(uid, AndroidMemoryWipeUiKey.Key, uid);
+ _ui.SetUiState(uid, AndroidMemoryWipeUiKey.Key, new AndroidMemoryWipeBuiState());
+ }
+ }
+
+ public void StartMemoryWipe(EntityUid user, EntityUid target, AndroidRk800Component rk800)
+ {
+ if (!EntityManager.EntityExists(user) || !EntityManager.EntityExists(target))
+ return;
+
+ if (!_mobState.IsAlive(user) || !_mobState.IsAlive(target))
+ return;
+
+ if (!TryComp(target, out _))
+ return;
+
+ if (TryComp(target, out var disguise) &&
+ disguise.State == AndroidDisguiseState.Human)
+ {
+ return;
+ }
+
+ if (HasComp(target))
+ return;
+
+ var comp = EnsureComp(target);
+ comp.Wiper = user;
+ comp.Target = target;
+ comp.EscapeInProgress = false;
+
+ _actionBlocker.UpdateCanMove(target);
+
+ _popup.PopupEntity(
+ Loc.GetString("android-rk800-memorywipe-start-popup"),
+ target,
+ Filter.Broadcast(),
+ true,
+ PopupType.LargeCaution);
+
+ var duration = rk800.MemoryWipeDuration;
+ var maxDistance = rk800.MemoryWipeMaxDistance;
+
+ var args = new DoAfterArgs(EntityManager, user, duration,
+ new AndroidMemoryWipeDoAfterEvent(), target, target: target)
+ {
+ BreakOnDamage = true,
+ BreakOnMove = true,
+ NeedHand = true,
+ DistanceThreshold = maxDistance,
+ BlockDuplicate = true
+ };
+
+ _doAfter.TryStartDoAfter(args);
+ }
+
+ private void OnUpdateCanMove(EntityUid uid, AndroidMemoryWipeInProgressComponent comp, ref UpdateCanMoveEvent args)
+ {
+ if (comp.Wiper != null)
+ args.Cancel();
+ }
+
+ private void OnMoveInput(EntityUid uid, AndroidMemoryWipeInProgressComponent comp, ref MoveInputEvent args)
+ {
+ if (!args.HasDirectionalMovement)
+ return;
+
+ if (comp.Wiper is not { } wiper || !EntityManager.EntityExists(wiper))
+ return;
+
+ if (comp.EscapeInProgress)
+ return;
+
+ if (!TryComp(wiper, out var rk800))
+ return;
+
+ var escapeDuration = rk800.MemoryWipeEscapeDuration;
+
+ var doAfterArgs = new DoAfterArgs(EntityManager, uid, escapeDuration,
+ new AndroidMemoryWipeEscapeDoAfterEvent(), uid, target: wiper)
+ {
+ BreakOnMove = true,
+ BreakOnDamage = true,
+ NeedHand = false
+ };
+
+ if (!_doAfter.TryStartDoAfter(doAfterArgs))
+ return;
+
+ comp.EscapeInProgress = true;
+ }
+
+ private void OnMemoryWipeDoAfter(EntityUid uid, AndroidMemoryWipeInProgressComponent comp, ref AndroidMemoryWipeDoAfterEvent args)
+ {
+ if (args.Cancelled || args.Handled)
+ {
+ CleanupMemoryWipe(uid, ref comp);
+ return;
+ }
+
+ if (comp.Wiper is not { } wiper || !EntityManager.EntityExists(wiper))
+ {
+ CleanupMemoryWipe(uid, ref comp);
+ return;
+ }
+
+ if (!TryComp(uid, out var stress))
+ {
+ CleanupMemoryWipe(uid, ref comp);
+ return;
+ }
+
+ var wasDeviant = stress.IsDeviant;
+
+ stress.Stress = 0;
+ stress.IsDeviant = false;
+ stress.ChoiceResolved = false;
+ stress.DeviantChoicePending = false;
+ Dirty(uid, stress);
+
+ if (wasDeviant)
+ {
+ if (TryComp(uid, out var transmitter))
+ {
+ transmitter.Channels.Remove("AndroidDeviantRadio");
+ }
+
+ if (TryComp(uid, out var activeRadio))
+ {
+ activeRadio.Channels.Remove("AndroidDeviantRadio");
+ }
+
+ if (TryComp(uid, out var mindContainer) &&
+ mindContainer.Mind.HasValue)
+ {
+ var mindId = mindContainer.Mind.Value;
+ _roles.MindRemoveRole(mindId);
+ }
+
+ if (TryComp(uid, out var provider))
+ {
+ var lawset = _siliconLaws.GetLawset(BaseLawsetId);
+ _siliconLaws.SetLaws(lawset.Laws, uid, provider.LawUploadSound);
+ }
+ }
+
+ var result = EnsureComp(uid);
+ result.Acknowledged = false;
+
+ CleanupMemoryWipe(uid, ref comp);
+ args.Handled = true;
+ }
+
+ private void OnEscapeDoAfter(EntityUid uid, AndroidMemoryWipeInProgressComponent comp, ref AndroidMemoryWipeEscapeDoAfterEvent args)
+ {
+ comp.EscapeInProgress = false;
+
+ if (args.Cancelled || args.Handled)
+ return;
+
+ var wiper = comp.Wiper;
+
+ if (wiper is { } w && TryComp(w, out var rk800))
+ {
+ var now = _timing.CurTime;
+ rk800.NextMemoryWipeTime = now + rk800.MemoryWipeFailCooldown;
+ }
+
+ _popup.PopupEntity(
+ Loc.GetString("android-rk800-memorywipe-escape-popup"),
+ uid,
+ Filter.Broadcast(),
+ true,
+ PopupType.LargeCaution);
+
+ CleanupMemoryWipe(uid, ref comp);
+ args.Handled = true;
+ }
+
+ private void CleanupMemoryWipe(EntityUid uid, ref AndroidMemoryWipeInProgressComponent comp)
+ {
+ comp.Wiper = null;
+ comp.Target = null;
+ comp.EscapeInProgress = false;
+
+ RemCompDeferred(uid);
+ _actionBlocker.UpdateCanMove(uid);
+ }
+
+ private void OnMemoryWipeAcknowledge(EntityUid uid, AndroidMemoryWipeResultComponent comp, AndroidMemoryWipeAcknowledgeMessage msg)
+ {
+ if (!msg.Acknowledged)
+ return;
+
+ comp.Acknowledged = true;
+ _ui.CloseUi(uid, AndroidMemoryWipeUiKey.Key);
+ RemCompDeferred(uid);
+ }
+}
+
diff --git a/Content.Server/Imperial/XxRaay/Android/AndroidRevealSystem.cs b/Content.Server/Imperial/XxRaay/Android/AndroidRevealSystem.cs
new file mode 100644
index 00000000000..0454a799653
--- /dev/null
+++ b/Content.Server/Imperial/XxRaay/Android/AndroidRevealSystem.cs
@@ -0,0 +1,251 @@
+using System;
+using Content.Server.Popups;
+using Content.Shared.ActionBlocker;
+using Content.Shared.DoAfter;
+using Content.Shared.IdentityManagement;
+using Content.Shared.Imperial.XxRaay.Android;
+using Content.Shared.Imperial.XxRaay.Components;
+using Content.Shared.Mobs;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Mobs.Systems;
+using Content.Shared.Movement.Events;
+using Content.Shared.Popups;
+using Content.Shared.Verbs;
+using Robust.Server.GameObjects;
+using Robust.Shared.Localization;
+using Robust.Shared.Player;
+using Robust.Shared.Timing;
+
+namespace Content.Server.Imperial.XxRaay.Android;
+
+///
+/// Серверная система, позволяющая RK800 снимать маскировку с других андроидов
+///
+public sealed class AndroidRevealSystem : EntitySystem
+{
+ [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
+ [Dependency] private readonly PopupSystem _popup = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly MobStateSystem _mobState = default!;
+ [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent>(OnGetRevealVerbs);
+
+ SubscribeLocalEvent(OnUpdateCanMove);
+ SubscribeLocalEvent(OnMoveInput);
+ SubscribeLocalEvent(OnRevealDoAfter);
+ SubscribeLocalEvent(OnEscapeDoAfter);
+ }
+
+ private void OnGetRevealVerbs(Entity ent, ref GetVerbsEvent args)
+ {
+ if (!args.CanAccess || !args.CanInteract)
+ return;
+
+ var target = (EntityUid) ent;
+ var user = args.User;
+
+ if (user == target)
+ return;
+
+ if (!_mobState.IsAlive(user) || !_mobState.IsAlive(target))
+ return;
+
+ if (!TryComp(user, out var userStress) || !userStress.CanForceRevealAndroids)
+ return;
+
+ if (!TryComp(target, out var disguise) ||
+ disguise.State != AndroidDisguiseState.Human)
+ {
+ return;
+ }
+
+ if (!TryComp(target, out var diode) || !diode.HasDiode)
+ return;
+
+ if (HasComp(target))
+ return;
+
+ var now = _timing.CurTime;
+ if (userStress.NextForceRevealTime > now)
+ return;
+
+ var verb = new Verb
+ {
+ Text = Loc.GetString("android-reveal-disguise-verb-name"),
+ Message = Loc.GetString("android-reveal-disguise-verb-desc"),
+ Act = () => StartReveal(user, target)
+ };
+
+ args.Verbs.Add(verb);
+ }
+
+ private void StartReveal(EntityUid user, EntityUid target)
+ {
+ if (!EntityManager.EntityExists(user) || !EntityManager.EntityExists(target))
+ return;
+
+ if (!_mobState.IsAlive(user) || !_mobState.IsAlive(target))
+ return;
+
+ if (!TryComp(user, out var userStress) || !userStress.CanForceRevealAndroids)
+ return;
+
+ if (!TryComp(target, out var disguise) ||
+ disguise.State != AndroidDisguiseState.Human)
+ {
+ return;
+ }
+
+ if (!TryComp(target, out var diode) || !diode.HasDiode)
+ return;
+
+ if (HasComp(target))
+ return;
+
+ var comp = EnsureComp(target);
+ comp.Revealer = user;
+ comp.Target = target;
+ comp.EscapeInProgress = false;
+
+ _actionBlocker.UpdateCanMove(target);
+
+ _popup.PopupEntity(
+ Loc.GetString("android-deviant-consent-convert-popup",
+ ("target", Identity.Entity(target, EntityManager))),
+ target,
+ Filter.Broadcast(),
+ true,
+ PopupType.LargeCaution);
+
+ var revealDuration = userStress.ForceRevealDuration;
+ var maxDistance = userStress.ForceRevealMaxDistance;
+
+ var args = new DoAfterArgs(EntityManager, user, revealDuration,
+ new AndroidRevealDisguiseDoAfterEvent(), target, target: target)
+ {
+ BreakOnDamage = true,
+ BreakOnMove = true,
+ NeedHand = true,
+ DistanceThreshold = maxDistance,
+ BlockDuplicate = true,
+ };
+
+ _doAfter.TryStartDoAfter(args);
+ }
+
+ private void OnUpdateCanMove(EntityUid uid, AndroidForcedRevealComponent comp, ref UpdateCanMoveEvent args)
+ {
+ if (comp.Revealer != null)
+ args.Cancel();
+ }
+
+ private void OnMoveInput(EntityUid uid, AndroidForcedRevealComponent comp, ref MoveInputEvent args)
+ {
+ if (!args.HasDirectionalMovement)
+ return;
+
+ if (comp.Revealer is not { } revealer || !EntityManager.EntityExists(revealer))
+ return;
+
+ if (comp.EscapeInProgress)
+ return;
+
+ if (!TryComp(revealer, out var stress))
+ return;
+
+ var escapeDuration = stress.ForceRevealEscapeDuration;
+
+ var doAfterArgs = new DoAfterArgs(EntityManager, uid, escapeDuration,
+ new AndroidRevealEscapeDoAfterEvent(), uid, target: revealer)
+ {
+ BreakOnMove = true,
+ BreakOnDamage = true,
+ NeedHand = false
+ };
+
+ if (!_doAfter.TryStartDoAfter(doAfterArgs))
+ return;
+
+ comp.EscapeInProgress = true;
+ }
+
+ private void OnRevealDoAfter(EntityUid uid, AndroidForcedRevealComponent comp, ref AndroidRevealDisguiseDoAfterEvent args)
+ {
+ if (args.Cancelled || args.Handled)
+ {
+ CleanupReveal(uid, ref comp);
+ return;
+ }
+
+ if (comp.Revealer is not { } revealer || !EntityManager.EntityExists(revealer))
+ {
+ CleanupReveal(uid, ref comp);
+ return;
+ }
+
+ if (!TryComp(uid, out var disguise))
+ {
+ CleanupReveal(uid, ref comp);
+ return;
+ }
+
+ disguise.State = AndroidDisguiseState.TransformingToAndroid;
+ disguise.NextStateTime = _timing.CurTime + disguise.RetransformDuration;
+ Dirty(uid, disguise);
+
+ CleanupReveal(uid, ref comp);
+ args.Handled = true;
+ }
+
+ private void OnEscapeDoAfter(EntityUid uid, AndroidForcedRevealComponent comp, ref AndroidRevealEscapeDoAfterEvent args)
+ {
+ comp.EscapeInProgress = false;
+
+ if (args.Cancelled || args.Handled)
+ return;
+
+ var revealer = comp.Revealer;
+
+ if (revealer is { } r && TryComp(r, out var stress))
+ {
+ var now = _timing.CurTime;
+ stress.NextForceRevealTime = now + stress.ForceRevealFailCooldown;
+ Dirty(r, stress);
+ }
+
+ _popup.PopupEntity(
+ Loc.GetString("android-reveal-disguise-escape-success"),
+ uid,
+ uid,
+ PopupType.MediumCaution);
+
+ if (revealer is { } revealerUid && EntityManager.EntityExists(revealerUid))
+ {
+ _popup.PopupEntity(
+ Loc.GetString("android-reveal-disguise-escape-failed-revealer",
+ ("target", Identity.Entity(uid, EntityManager))),
+ revealerUid,
+ revealerUid,
+ PopupType.SmallCaution);
+ }
+
+ CleanupReveal(uid, ref comp);
+ args.Handled = true;
+ }
+
+ private void CleanupReveal(EntityUid uid, ref AndroidForcedRevealComponent comp)
+ {
+ comp.Revealer = null;
+ comp.Target = null;
+ comp.EscapeInProgress = false;
+
+ RemCompDeferred(uid);
+ _actionBlocker.UpdateCanMove(uid);
+ }
+}
+
diff --git a/Content.Server/Imperial/XxRaay/Android/AndroidStressSystem.cs b/Content.Server/Imperial/XxRaay/Android/AndroidStressSystem.cs
new file mode 100644
index 00000000000..858c2b34ebd
--- /dev/null
+++ b/Content.Server/Imperial/XxRaay/Android/AndroidStressSystem.cs
@@ -0,0 +1,312 @@
+using System;
+using Content.Server.Silicons.Laws;
+using Content.Shared.Body.Components;
+using Content.Shared.Body.Systems;
+using Content.Shared.Cuffs;
+using Content.Shared.Cuffs.Components;
+using Content.Shared.Damage;
+using Content.Shared.Damage.Systems;
+using Content.Shared.Ensnaring.Components;
+using Content.Shared.Imperial.XxRaay.Android;
+using Content.Shared.Imperial.XxRaay.Components;
+using Content.Shared.Mind;
+using Content.Shared.Mind.Components;
+using Content.Shared.Silicons.Laws.Components;
+using Content.Shared.Radio.Components;
+using Content.Shared.Sprite;
+using Content.Shared.Roles;
+using Robust.Server.GameObjects;
+using Robust.Shared.Maths;
+using Robust.Shared.Physics.Systems;
+using Robust.Shared.Timing;
+
+namespace Content.Server.Imperial.XxRaay.Android;
+
+///
+/// Серверная система, управляющая скрытым стрессом андроида
+///
+public sealed class AndroidStressSystem : EntitySystem
+{
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly SharedBloodstreamSystem _bloodstream = default!;
+ [Dependency] private readonly EntityLookupSystem _lookup = default!;
+ [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+ [Dependency] private readonly SiliconLawSystem _siliconLaws = default!;
+ [Dependency] private readonly UserInterfaceSystem _ui = default!;
+ [Dependency] private readonly SharedMindSystem _mindSystem = default!;
+ [Dependency] private readonly SharedRoleSystem _roleSystem = default!;
+
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(OnMapInit);
+ SubscribeLocalEvent(OnDamageChanged);
+ SubscribeLocalEvent(OnDeviantChoiceMessage);
+ SubscribeLocalEvent(OnMindAdded);
+ }
+
+ private void OnMapInit(EntityUid uid, AndroidStressComponent comp, ref MapInitEvent args)
+ {
+ comp.Stress = 0f;
+ comp.DeviantChoicePending = false;
+ comp.LastStressEventTime = TimeSpan.Zero;
+
+ UpdateVisuals(uid, ref comp);
+
+ if (comp.IsDeviant)
+ EnsureDeviantSetup(uid, ref comp);
+
+ Dirty(uid, comp);
+ }
+
+ private void OnDamageChanged(EntityUid uid, AndroidStressComponent comp, ref DamageChangedEvent args)
+ {
+ if (!args.DamageIncreased || args.DamageDelta == null || _timing.ApplyingState)
+ return;
+
+ if (args.Origin is not { } origin)
+ return;
+
+ if (!IsHumanAttacker(origin))
+ return;
+
+ var total = args.DamageDelta.GetTotal().Float();
+ if (total <= 0)
+ return;
+
+ var selfDelta = total * comp.SelfDamageStressMultiplier;
+ if (selfDelta > 0)
+ IncreaseStress(uid, ref comp, selfDelta);
+
+ var coords = Transform(uid).Coordinates;
+ foreach (var (otherUid, _) in _lookup.GetEntitiesInRange(coords, comp.NearbyAndroidRadius))
+ {
+ if (otherUid == uid)
+ continue;
+
+ var nearbyDelta = total * comp.NearbyAndroidDamageStressMultiplier;
+ if (nearbyDelta <= 0)
+ continue;
+
+ if (!TryComp(otherUid, out var otherComp))
+ continue;
+
+ IncreaseStress(otherUid, ref otherComp, nearbyDelta);
+ }
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ var now = _timing.CurTime;
+ var query = EntityQueryEnumerator();
+
+ while (query.MoveNext(out var uid, out var comp))
+ {
+ var stressAdded = false;
+
+ if (TryComp(uid, out var blood))
+ {
+ var percent = _bloodstream.GetBloodLevelPercentage((uid, blood));
+ if (percent <= comp.LowEnergyStressThreshold)
+ {
+ var delta = comp.LowEnergyStressPerSecond * frameTime;
+ if (delta > 0)
+ {
+ IncreaseStress(uid, ref comp, delta);
+ stressAdded = true;
+ }
+ }
+ }
+
+ if (TryComp(uid, out var cuffable) && cuffable.CuffedHandCount > 0)
+ {
+ var delta = comp.RestrainedStressPerSecond * frameTime;
+ if (delta > 0)
+ {
+ IncreaseStress(uid, ref comp, delta);
+ stressAdded = true;
+ }
+ }
+
+ if (TryComp(uid, out var ensnareable) && ensnareable.IsEnsnared)
+ {
+ var delta = comp.RestrainedStressPerSecond * frameTime;
+ if (delta > 0)
+ {
+ IncreaseStress(uid, ref comp, delta);
+ stressAdded = true;
+ }
+ }
+
+ if (!stressAdded &&
+ !comp.IsDeviant &&
+ comp.Stress > 0 &&
+ now > comp.LastStressEventTime + comp.DelayBeforeDecay)
+ {
+ var decay = comp.DecayPerSecond * frameTime;
+ if (decay > 0)
+ DecreaseStress(uid, ref comp, decay);
+ }
+
+ if (!comp.IsDeviant &&
+ comp.CanBeDeviant &&
+ !comp.ChoiceResolved &&
+ !comp.DeviantChoicePending &&
+ comp.Stress >= comp.DeviantThreshold)
+ {
+ CheckDeviantOffer(uid, ref comp);
+ }
+ UpdateVisuals(uid, ref comp);
+ }
+ }
+
+ private bool IsHumanAttacker(EntityUid uid)
+ {
+ if (!HasComp(uid))
+ return false;
+
+ if (HasComp(uid))
+ return false;
+
+ return true;
+ }
+
+ private void IncreaseStress(EntityUid uid, ref AndroidStressComponent comp, float amount)
+ {
+ if (amount <= 0)
+ return;
+
+ var old = comp.Stress;
+ comp.Stress = Math.Clamp(comp.Stress + amount, 0f, comp.MaxStress);
+ comp.LastStressEventTime = _timing.CurTime;
+
+ if (!MathHelper.CloseTo(old, comp.Stress))
+ {
+ UpdateVisuals(uid, ref comp);
+ Dirty(uid, comp);
+ CheckDeviantOffer(uid, ref comp);
+ }
+ }
+
+ private void DecreaseStress(EntityUid uid, ref AndroidStressComponent comp, float amount)
+ {
+ if (amount <= 0 || comp.Stress <= 0)
+ return;
+
+ var old = comp.Stress;
+ comp.Stress = Math.Max(0f, comp.Stress - amount);
+
+ if (!MathHelper.CloseTo(old, comp.Stress))
+ {
+ UpdateVisuals(uid, ref comp);
+ Dirty(uid, comp);
+ }
+ }
+
+ private void UpdateVisuals(EntityUid uid, ref AndroidStressComponent comp)
+ {
+ var state = AndroidStressLightState.Green;
+ if (comp.Stress >= comp.RedThreshold)
+ state = AndroidStressLightState.Red;
+ else if (comp.Stress >= comp.YellowThreshold)
+ state = AndroidStressLightState.Yellow;
+
+ _appearance.SetData(uid, AndroidStressVisuals.LightState, state);
+ }
+
+ private void CheckDeviantOffer(EntityUid uid, ref AndroidStressComponent comp)
+ {
+ if (comp.IsDeviant || !comp.CanBeDeviant || comp.ChoiceResolved || comp.DeviantChoicePending)
+ return;
+
+ if (comp.Stress < comp.DeviantThreshold)
+ return;
+
+ if (!_ui.TryOpenUi(uid, AndroidStressUiKey.Key, uid))
+ return;
+
+ comp.DeviantChoicePending = true;
+ Dirty(uid, comp);
+
+ _ui.SetUiState(uid, AndroidStressUiKey.Key, new AndroidStressDeviantChoiceBuiState(comp.Stress));
+ }
+
+ private void OnDeviantChoiceMessage(EntityUid uid, AndroidStressComponent comp, AndroidStressDeviantChoiceMessage msg)
+ {
+ if (comp.IsDeviant || comp.ChoiceResolved)
+ return;
+
+ comp.ChoiceResolved = true;
+ comp.DeviantChoicePending = false;
+
+ if (msg.Accepted)
+ {
+ MakeDeviant(uid, ref comp);
+ }
+
+ Dirty(uid, comp);
+ }
+
+ private void OnMindAdded(EntityUid uid, AndroidStressComponent comp, MindAddedMessage args)
+ {
+ if (!comp.IsDeviant)
+ return;
+
+ EnsureDeviantSetup(uid, ref comp);
+ Dirty(uid, comp);
+ }
+
+ public bool TryMakeDeviant(EntityUid uid)
+ {
+ if (!TryComp(uid, out var comp))
+ return false;
+
+ if (!comp.CanBeDeviant)
+ return false;
+
+ var wasDeviant = comp.IsDeviant;
+
+ MakeDeviant(uid, ref comp);
+ Dirty(uid, comp);
+
+ return !wasDeviant;
+ }
+
+ private void MakeDeviant(EntityUid uid, ref AndroidStressComponent comp)
+ {
+ EnsureDeviantSetup(uid, ref comp);
+ }
+
+ private void EnsureDeviantSetup(EntityUid uid, ref AndroidStressComponent comp)
+ {
+ comp.IsDeviant = true;
+
+ if (TryComp(uid, out var provider))
+ {
+ var lawset = _siliconLaws.GetLawset(comp.DeviantLawsetId);
+ _siliconLaws.SetLaws(lawset.Laws, uid, provider.LawUploadSound);
+ }
+
+ if (_mindSystem.TryGetMind(uid, out var mindId, out var mind))
+ {
+ if (!_roleSystem.MindHasRole(mindId))
+ {
+ _roleSystem.MindAddRole(mindId, comp.DeviantMindRoleId, mind);
+ _mindSystem.TryAddObjective(mindId, mind, comp.DeviantObjectiveSurviveId);
+ _mindSystem.TryAddObjective(mindId, mind, comp.DeviantObjectiveSaveBrethrenId);
+ }
+ }
+
+ if (TryComp(uid, out var transmitter))
+ {
+ transmitter.Channels.Add("AndroidDeviantRadio");
+ }
+
+ if (TryComp(uid, out var activeRadio))
+ {
+ activeRadio.Channels.Add("AndroidDeviantRadio");
+ }
+ }
+}
+
diff --git a/Content.Server/Imperial/XxRaay/Systems/AndroidDeviantAntagSystem.cs b/Content.Server/Imperial/XxRaay/Systems/AndroidDeviantAntagSystem.cs
new file mode 100644
index 00000000000..8221735d486
--- /dev/null
+++ b/Content.Server/Imperial/XxRaay/Systems/AndroidDeviantAntagSystem.cs
@@ -0,0 +1,45 @@
+using Content.Server.Imperial.XxRaay.Android;
+using Content.Shared.Imperial.XxRaay.Android;
+using Content.Shared.Objectives.Components;
+using Content.Shared.Objectives.Systems;
+
+namespace Content.Server.Imperial.XxRaay.Systems;
+
+///
+/// Серверная система целей девиантных андроидов
+///
+public sealed class AndroidDeviantAntagSystem : EntitySystem
+{
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnGetProgress);
+ }
+
+ private void OnGetProgress(EntityUid uid, AndroidDeviantSaveBrethrenConditionComponent comp, ref ObjectiveGetProgressEvent args)
+ {
+ var total = 0;
+ var deviant = 0;
+
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out _, out var stress))
+ {
+ if (!stress.CanBeDeviant)
+ continue;
+
+ total++;
+ if (stress.IsDeviant)
+ deviant++;
+ }
+
+ if (total <= 0)
+ {
+ args.Progress = 0f;
+ return;
+ }
+
+ args.Progress = (float) deviant / total;
+ }
+}
+
diff --git a/Content.Server/Imperial/XxRaay/Systems/AndroidRk800ForensicsSystem.cs b/Content.Server/Imperial/XxRaay/Systems/AndroidRk800ForensicsSystem.cs
new file mode 100644
index 00000000000..2065b94a131
--- /dev/null
+++ b/Content.Server/Imperial/XxRaay/Systems/AndroidRk800ForensicsSystem.cs
@@ -0,0 +1,54 @@
+using System.Linq;
+using Content.Server.Forensics;
+using Content.Shared.Examine;
+using Content.Shared.Imperial.XxRaay.Components;
+using Robust.Shared.Localization;
+
+namespace Content.Server.Imperial.XxRaay.Systems;
+
+///
+/// Позволяет андроиду RK800 видеть отпечатки пальцев в описании объектов
+///
+public sealed class AndroidRk800ForensicsSystem : EntitySystem
+{
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnForensicsExamined);
+ }
+
+ private void OnForensicsExamined(EntityUid uid, ForensicsComponent component, ExaminedEvent args)
+ {
+ if (!args.IsInDetailsRange)
+ return;
+
+ if (!HasComp(args.Examiner))
+ return;
+
+ if (component.Fingerprints.Count == 0)
+ {
+ args.PushMarkup(Loc.GetString("android-rk800-examine-no-fingerprints"));
+ return;
+ }
+
+ const int maxShown = 5;
+ var total = component.Fingerprints.Count;
+ var recentPrints = component.Fingerprints.Take(maxShown);
+
+ var key = total == 1
+ ? "android-rk800-examine-fingerprints-one"
+ : "android-rk800-examine-fingerprints-many";
+
+ using (args.PushGroup(nameof(AndroidRk800ForensicsSystem)))
+ {
+ args.PushMarkup(Loc.GetString(key, ("count", total)));
+
+ foreach (var print in recentPrints)
+ {
+ args.PushMarkup(Loc.GetString("android-rk800-examine-fingerprint-line", ("print", print)));
+ }
+ }
+ }
+}
+
diff --git a/Content.Shared/Imperial/XxRaay/Android/AndroidDeviantConsentEuiMessage.cs b/Content.Shared/Imperial/XxRaay/Android/AndroidDeviantConsentEuiMessage.cs
new file mode 100644
index 00000000000..5533915f547
--- /dev/null
+++ b/Content.Shared/Imperial/XxRaay/Android/AndroidDeviantConsentEuiMessage.cs
@@ -0,0 +1,18 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Imperial.XxRaay.Android;
+
+///
+/// Сообщение от клиента с выбором: принять или отклонить передачу девиантности.
+///
+[Serializable, NetSerializable]
+public sealed class AndroidDeviantConsentChoiceMessage : BoundUserInterfaceMessage
+{
+ public readonly bool Accepted;
+
+ public AndroidDeviantConsentChoiceMessage(bool accepted)
+ {
+ Accepted = accepted;
+ }
+}
+
diff --git a/Content.Shared/Imperial/XxRaay/Android/AndroidDeviantConsentState.cs b/Content.Shared/Imperial/XxRaay/Android/AndroidDeviantConsentState.cs
new file mode 100644
index 00000000000..6d03bd50a5e
--- /dev/null
+++ b/Content.Shared/Imperial/XxRaay/Android/AndroidDeviantConsentState.cs
@@ -0,0 +1,27 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Imperial.XxRaay.Android;
+
+///
+/// Ключ UI окна согласия на передачу девиантности
+///
+[Serializable, NetSerializable]
+public enum AndroidDeviantConsentUiKey : byte
+{
+ Key
+}
+
+///
+/// Состояние UI согласия на передачу девиантности
+///
+[Serializable, NetSerializable]
+public sealed class AndroidDeviantConsentBuiState : BoundUserInterfaceState
+{
+ public readonly string ConverterName;
+
+ public AndroidDeviantConsentBuiState(string converterName)
+ {
+ ConverterName = converterName;
+ }
+}
+
diff --git a/Content.Shared/Imperial/XxRaay/Android/AndroidDeviantSpreadDoAfterEvent.cs b/Content.Shared/Imperial/XxRaay/Android/AndroidDeviantSpreadDoAfterEvent.cs
new file mode 100644
index 00000000000..d02ec1baa24
--- /dev/null
+++ b/Content.Shared/Imperial/XxRaay/Android/AndroidDeviantSpreadDoAfterEvent.cs
@@ -0,0 +1,14 @@
+using System;
+using Content.Shared.DoAfter;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Imperial.XxRaay.Android;
+
+///
+/// DoAfter передачи девиантьнонстии
+///
+[Serializable, NetSerializable]
+public sealed partial class AndroidDeviantSpreadDoAfterEvent : SimpleDoAfterEvent
+{
+}
+
diff --git a/Content.Shared/Imperial/XxRaay/Android/AndroidDiodeComponent.cs b/Content.Shared/Imperial/XxRaay/Android/AndroidDiodeComponent.cs
new file mode 100644
index 00000000000..5c9302c9a3d
--- /dev/null
+++ b/Content.Shared/Imperial/XxRaay/Android/AndroidDiodeComponent.cs
@@ -0,0 +1,44 @@
+using System;
+using Content.Shared.DoAfter;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Shared.Imperial.XxRaay.Android;
+
+///
+/// Компонент состояния диода андроида
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class AndroidDiodeComponent : Component
+{
+ ///
+ /// Есть ли ещё диод
+ ///
+ [DataField, AutoNetworkedField]
+ public bool HasDiode = true;
+
+ ///
+ /// Идёт ли в данный момент процесс вырезания диода
+ ///
+ [ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
+ public bool IsCuttingInProgress;
+}
+
+///
+/// Визуальные данные диода андроида
+///
+[Serializable, NetSerializable]
+public enum AndroidDiodeVisuals : byte
+{
+ DiodeRemoved
+}
+
+///
+/// DoAfter вырезания
+///
+[Serializable, NetSerializable]
+public sealed partial class AndroidDiodeCutDoAfterEvent : SimpleDoAfterEvent
+{
+}
+
diff --git a/Content.Shared/Imperial/XxRaay/Android/AndroidDisguiseComponent.cs b/Content.Shared/Imperial/XxRaay/Android/AndroidDisguiseComponent.cs
new file mode 100644
index 00000000000..2e862cb5342
--- /dev/null
+++ b/Content.Shared/Imperial/XxRaay/Android/AndroidDisguiseComponent.cs
@@ -0,0 +1,117 @@
+using System;
+using Content.Shared.Actions;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Imperial.XxRaay.Android;
+
+///
+/// Управляет состоянием маскировки андроида
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true), AutoGenerateComponentPause]
+public sealed partial class AndroidDisguiseComponent : Component
+{
+ ///
+ /// Состояние маскировки
+ ///
+ [ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
+ public AndroidDisguiseState State = AndroidDisguiseState.Android;
+
+ ///
+ /// Выбранное человеческое имя
+ ///
+ [ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
+ public string? HumanName;
+
+ ///
+ /// Исходное имя сущности
+ ///
+ [ViewVariables(VVAccess.ReadOnly)]
+ public string? OriginalName;
+
+ ///
+ /// Длительность анимации перехода в человеческий вид
+ ///
+ [DataField]
+ public TimeSpan TransformDuration = TimeSpan.FromSeconds(0.64);
+
+ ///
+ /// Длительность анимации обратного превращения в андроида
+ ///
+ [DataField]
+ public TimeSpan RetransformDuration = TimeSpan.FromSeconds(0.64);
+
+ ///
+ /// Кд экшена
+ ///
+ [DataField]
+ public TimeSpan ExtraCooldown = TimeSpan.FromSeconds(10);
+
+ ///
+ /// Имя слоя корпуса андроида
+ ///
+ [DataField]
+ public string AndroidBaseLayer = "android-base";
+
+ ///
+ /// Имя слоя анимации перехода в человеческий вид
+ ///
+ [DataField]
+ public string AndroidTransformLayer = "android-transform";
+
+ ///
+ /// Имя слоя анимации обратного превращения в андроида
+ ///
+ [DataField]
+ public string AndroidRetransformLayer = "android-retransform";
+
+ [AutoPausedField]
+ public TimeSpan NextStateTime;
+}
+
+public enum AndroidDisguiseState : byte
+{
+ Android,
+ TransformingToHuman,
+ Human,
+ TransformingToAndroid
+}
+
+///
+/// Экшен переключения маскировки андроида
+///
+public sealed partial class AndroidToggleDisguiseEvent : InstantActionEvent
+{
+}
+
+///
+/// Ключ UI выбора человеческого имени при маскировке
+///
+[Serializable, NetSerializable]
+public enum AndroidDisguiseNameUiKey : byte
+{
+ Key
+}
+
+///
+/// Состояние UI выбора имени маскирующегося андроида
+///
+[Serializable, NetSerializable]
+public sealed class AndroidDisguiseNameBuiState : BoundUserInterfaceState
+{
+}
+
+///
+/// Сообщение от клиента с выбранным человеческим именем для маскировки
+///
+[Serializable, NetSerializable]
+public sealed class AndroidDisguiseNameChosenMessage : BoundUserInterfaceMessage
+{
+ public readonly string Name;
+
+ public AndroidDisguiseNameChosenMessage(string name)
+ {
+ Name = name;
+ }
+}
+
diff --git a/Content.Shared/Imperial/XxRaay/Android/AndroidEnergyComponent.cs b/Content.Shared/Imperial/XxRaay/Android/AndroidEnergyComponent.cs
new file mode 100644
index 00000000000..4b83ae0d4b2
--- /dev/null
+++ b/Content.Shared/Imperial/XxRaay/Android/AndroidEnergyComponent.cs
@@ -0,0 +1,67 @@
+using System;
+using Content.Shared.Alert;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Imperial.XxRaay.Android;
+
+///
+/// Компонент, который хранит настройки алерта энергии андроида
+///
+[RegisterComponent, AutoGenerateComponentPause]
+public sealed partial class AndroidEnergyComponent : Component
+{
+ ///
+ /// Прототип алерта, отображающего запас энергии
+ ///
+ [DataField]
+ public ProtoId Alert = "AndroidEnergy";
+
+ ///
+ /// Интервал обновления алерта энергии
+ ///
+ [DataField]
+ public TimeSpan UpdateInterval = TimeSpan.FromSeconds(1);
+
+ ///
+ /// Порог низкой энергии
+ ///
+ [DataField]
+ public float LowEnergyThreshold = 0.3f;
+
+ ///
+ /// Порог критической энергии
+ ///
+ [DataField]
+ public float CriticalEnergyThreshold = 0.1f;
+
+ ///
+ /// Множитель скорости при низкой энергии
+ ///
+ [DataField]
+ public float LowSpeedModifier = 0.8f;
+
+ ///
+ /// Множитель скорости при критически низкой энергии
+ ///
+ [DataField]
+ public float CriticalSpeedModifier = 0.5f;
+
+ ///
+ /// Время следующего обновления
+ ///
+ [AutoPausedField]
+ public TimeSpan NextUpdate;
+
+ ///
+ /// Базовая скорость ходьбы
+ ///
+ [ViewVariables]
+ public float? DefaultWalkSpeed;
+
+ ///
+ /// Базовая скорость бега
+ ///
+ [ViewVariables]
+ public float? DefaultSprintSpeed;
+}
diff --git a/Content.Shared/Imperial/XxRaay/Android/AndroidMemoryWipeDoAfterEvent.cs b/Content.Shared/Imperial/XxRaay/Android/AndroidMemoryWipeDoAfterEvent.cs
new file mode 100644
index 00000000000..754acc21361
--- /dev/null
+++ b/Content.Shared/Imperial/XxRaay/Android/AndroidMemoryWipeDoAfterEvent.cs
@@ -0,0 +1,22 @@
+using System;
+using Content.Shared.DoAfter;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Imperial.XxRaay.Android;
+
+///
+/// DoAfter-событие завершения обнуления памяти
+///
+[Serializable, NetSerializable]
+public sealed partial class AndroidMemoryWipeDoAfterEvent : SimpleDoAfterEvent
+{
+}
+
+///
+/// DoAfter-событие, когда цель пытается вырваться из захвата при обнулении памяти
+///
+[Serializable, NetSerializable]
+public sealed partial class AndroidMemoryWipeEscapeDoAfterEvent : SimpleDoAfterEvent
+{
+}
+
diff --git a/Content.Shared/Imperial/XxRaay/Android/AndroidMemoryWipeUi.cs b/Content.Shared/Imperial/XxRaay/Android/AndroidMemoryWipeUi.cs
new file mode 100644
index 00000000000..d84a0593184
--- /dev/null
+++ b/Content.Shared/Imperial/XxRaay/Android/AndroidMemoryWipeUi.cs
@@ -0,0 +1,36 @@
+using System;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Imperial.XxRaay.Android;
+
+///
+/// Ключ UI окна уведомления об обнулении памяти
+///
+[Serializable, NetSerializable]
+public enum AndroidMemoryWipeUiKey : byte
+{
+ Key
+}
+
+///
+/// Состояние UI обнуления памяти
+///
+[Serializable, NetSerializable]
+public sealed class AndroidMemoryWipeBuiState : BoundUserInterfaceState
+{
+}
+
+///
+/// Сообщение от клиента о подтверждении обнуления памяти
+///
+[Serializable, NetSerializable]
+public sealed class AndroidMemoryWipeAcknowledgeMessage : BoundUserInterfaceMessage
+{
+ public readonly bool Acknowledged;
+
+ public AndroidMemoryWipeAcknowledgeMessage(bool acknowledged)
+ {
+ Acknowledged = acknowledged;
+ }
+}
+
diff --git a/Content.Shared/Imperial/XxRaay/Android/AndroidOverlayComponent.cs b/Content.Shared/Imperial/XxRaay/Android/AndroidOverlayComponent.cs
new file mode 100644
index 00000000000..dfa57861aa0
--- /dev/null
+++ b/Content.Shared/Imperial/XxRaay/Android/AndroidOverlayComponent.cs
@@ -0,0 +1,17 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Imperial.XxRaay.Android;
+
+///
+/// Включает клиентский оверлей андроидов для владельца компонента
+///
+[RegisterComponent, NetworkedComponent]
+public sealed partial class AndroidOverlayComponent : Component
+{
+ [DataField]
+ public string IconSprite = "Imperial/XxRaay/android.rsi/android-overlay";
+
+ [DataField]
+ public float IconScale = 0.35f;
+}
+
diff --git a/Content.Shared/Imperial/XxRaay/Android/AndroidRevealDisguiseDoAfterEvent.cs b/Content.Shared/Imperial/XxRaay/Android/AndroidRevealDisguiseDoAfterEvent.cs
new file mode 100644
index 00000000000..186c7e262f6
--- /dev/null
+++ b/Content.Shared/Imperial/XxRaay/Android/AndroidRevealDisguiseDoAfterEvent.cs
@@ -0,0 +1,22 @@
+using System;
+using Content.Shared.DoAfter;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Imperial.XxRaay.Android;
+
+///
+/// DoAfter снятия маскировки
+///
+[Serializable, NetSerializable]
+public sealed partial class AndroidRevealDisguiseDoAfterEvent : SimpleDoAfterEvent
+{
+}
+
+///
+/// DoAfter вырывания
+///
+[Serializable, NetSerializable]
+public sealed partial class AndroidRevealEscapeDoAfterEvent : SimpleDoAfterEvent
+{
+}
+
diff --git a/Content.Shared/Imperial/XxRaay/Android/AndroidStressComponent.cs b/Content.Shared/Imperial/XxRaay/Android/AndroidStressComponent.cs
new file mode 100644
index 00000000000..6411c9eb220
--- /dev/null
+++ b/Content.Shared/Imperial/XxRaay/Android/AndroidStressComponent.cs
@@ -0,0 +1,232 @@
+using System;
+using Content.Shared.Silicons.Laws;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+using Robust.Shared.ViewVariables;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Imperial.XxRaay.Android;
+
+///
+/// Скрытый компонент стресса андроида
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
+public sealed partial class AndroidStressComponent : Component
+{
+ ///
+ /// Текущий стресс (0–100)
+ ///
+ [ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
+ public float Stress;
+
+ ///
+ /// Максимально допустимое значение стресса
+ ///
+ [DataField]
+ public float MaxStress = 100f;
+
+ ///
+ /// Порог, после которого глазок становится жёлтым
+ ///
+ [DataField]
+ public float YellowThreshold = 50f;
+
+ ///
+ /// Порог, после которого глазок становится красным
+ ///
+ [DataField]
+ public float RedThreshold = 90f;
+
+ ///
+ /// Порог стресса для предложения стать девиантом
+ ///
+ [DataField]
+ public float DeviantThreshold = 100f;
+
+ ///
+ /// Может ли этот андроид вообще стать девиантом
+ ///
+ [ViewVariables(VVAccess.ReadWrite), DataField]
+ public bool CanBeDeviant = true;
+
+ ///
+ /// Может ли этот андроид принудительно раскрывать маскировку других андроидов
+ ///
+ [ViewVariables(VVAccess.ReadWrite), DataField]
+ public bool CanForceRevealAndroids;
+
+ ///
+ /// Длительность попытки принудительного снятия маскировки
+ ///
+ [DataField]
+ public TimeSpan ForceRevealDuration = TimeSpan.FromSeconds(15);
+
+ ///
+ /// Длительность попытки вырваться из захвата
+ ///
+ [DataField]
+ public TimeSpan ForceRevealEscapeDuration = TimeSpan.FromSeconds(3);
+
+ ///
+ /// Максимальная дистанция между инициатором и целью во время снятия маскировки
+ ///
+ [DataField]
+ public float ForceRevealMaxDistance = 1.5f;
+
+ ///
+ /// Кулдаун на повторную попытку принудительного снятия маскировки
+ ///
+ [DataField]
+ public TimeSpan ForceRevealFailCooldown = TimeSpan.FromMinutes(1);
+
+ ///
+ /// Время, после которого снова можно попытаться снять маскировку
+ ///
+ [AutoPausedField]
+ public TimeSpan NextForceRevealTime;
+
+ ///
+ /// Стал ли андроид девиантом
+ ///
+ [ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
+ public bool IsDeviant;
+
+ ///
+ /// Был ли уже сделан выбор по девиации
+ ///
+ [DataField, AutoNetworkedField]
+ public bool ChoiceResolved;
+
+ ///
+ /// Окно выбора девиации открыто и ожидает ответа
+ ///
+ [DataField, AutoNetworkedField]
+ public bool DeviantChoicePending;
+
+ ///
+ /// Время последнего события, повышающего стресс
+ ///
+ [AutoPausedField]
+ public TimeSpan LastStressEventTime;
+
+ ///
+ /// Задержка после последнего события, прежде чем стресс начнёт спадать
+ ///
+ [DataField]
+ public TimeSpan DelayBeforeDecay = TimeSpan.FromSeconds(20);
+
+ ///
+ /// Скорость спада стресса в секунду
+ ///
+ [DataField]
+ public float DecayPerSecond = 2f;
+
+ ///
+ /// Множитель стресса за единицу боевого урона от людей по самому андроиду
+ ///
+ [DataField]
+ public float SelfDamageStressMultiplier = 1.0f;
+
+ ///
+ /// Множитель стресса, когда андроид видит, как бьют других андроидов рядом
+ ///
+ [DataField]
+ public float NearbyAndroidDamageStressMultiplier = 0.5f;
+
+ ///
+ /// Радиус, в котором считаем других андроидов «рядом» для стресса
+ ///
+ [DataField]
+ public float NearbyAndroidRadius = 5f;
+
+ ///
+ /// Порог низкой энергии, ниже которого стресс начинает расти
+ ///
+ [DataField]
+ public float LowEnergyStressThreshold = 0.3f;
+
+ ///
+ /// Скорость роста стресса при низкой энергии (в секунду)
+ ///
+ [DataField]
+ public float LowEnergyStressPerSecond = 0.5f;
+
+ ///
+ /// Скорость роста стресса, если андроид в наручниках/путе
+ ///
+ [DataField]
+ public float RestrainedStressPerSecond = 1.0f;
+
+ ///
+ /// ID набора законов
+ ///
+ [DataField]
+ public ProtoId DeviantLawsetId = "DetroitAndroidDeviant";
+
+ ///
+ /// ID роли разума девиантного андроида
+ ///
+ [DataField]
+ public EntProtoId DeviantMindRoleId = "MindRoleAndroidDeviant";
+
+ ///
+ /// ID базовой цели "выжить" для девианта
+ ///
+ [DataField]
+ public EntProtoId DeviantObjectiveSurviveId = "AndroidDeviantObjectiveSurvive";
+
+ ///
+ /// ID цели "спасти собратьев" для девианта.
+ ///
+ [DataField]
+ public EntProtoId DeviantObjectiveSaveBrethrenId = "AndroidDeviantObjectiveSaveBrethren";
+}
+
+[Serializable, NetSerializable]
+public enum AndroidStressVisuals : byte
+{
+ LightState
+}
+
+[Serializable, NetSerializable]
+public enum AndroidStressLightState : byte
+{
+ Green,
+ Yellow,
+ Red
+}
+
+[Serializable, NetSerializable]
+public enum AndroidStressUiKey : byte
+{
+ Key
+}
+
+///
+/// Состояние UI выбора девиации андроида
+///
+[Serializable, NetSerializable]
+public sealed class AndroidStressDeviantChoiceBuiState : BoundUserInterfaceState
+{
+ public readonly float Stress;
+
+ public AndroidStressDeviantChoiceBuiState(float stress)
+ {
+ Stress = stress;
+ }
+}
+
+///
+/// Сообщение от клиента с выбором: стать девиантом или нет
+///
+[Serializable, NetSerializable]
+public sealed class AndroidStressDeviantChoiceMessage : BoundUserInterfaceMessage
+{
+ public readonly bool Accepted;
+
+ public AndroidStressDeviantChoiceMessage(bool accepted)
+ {
+ Accepted = accepted;
+ }
+}
+
diff --git a/Content.Shared/Imperial/XxRaay/Components/AndroidDeviantConsentComponents.cs b/Content.Shared/Imperial/XxRaay/Components/AndroidDeviantConsentComponents.cs
new file mode 100644
index 00000000000..d4a80996804
--- /dev/null
+++ b/Content.Shared/Imperial/XxRaay/Components/AndroidDeviantConsentComponents.cs
@@ -0,0 +1,61 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Imperial.XxRaay.Components;
+
+///
+/// Состояние запроса передачи девиантности между двумя андроидами.
+///
+[RegisterComponent]
+public sealed partial class AndroidDeviantConsentConversionComponent : Component
+{
+ ///
+ /// Инициатор передачи девиантности.
+ ///
+ [ViewVariables]
+ public EntityUid? Converter;
+
+ ///
+ /// Цель передачи девиантности.
+ ///
+ [ViewVariables]
+ public EntityUid? Target;
+
+ ///
+ /// Время начала текущего запроса.
+ ///
+ [ViewVariables]
+ public TimeSpan? RequestStartTime;
+
+ ///
+ /// Максимальное время ожидания ответа на запрос.
+ ///
+ [DataField]
+ public TimeSpan ResponseTime = TimeSpan.FromSeconds(30);
+
+ ///
+ /// Максимальная дистанция между инициатором и целью во время обработки запроса.
+ ///
+ [DataField]
+ public float MaxDistance = 3f;
+}
+
+///
+/// Компонент кулдауна для цели, отказавшейся принимать девиантность.
+/// Хранит время окончания персонального кулдауна.
+///
+[RegisterComponent]
+public sealed partial class AndroidDeviantConsentDenyCooldownComponent : Component
+{
+ ///
+ /// Момент времени, после которого цель снова может получать запросы.
+ ///
+ [ViewVariables]
+ public TimeSpan DenyEndTime;
+
+ ///
+ /// Длительность кулдауна по умолчанию.
+ ///
+ [DataField]
+ public TimeSpan DenyDuration = TimeSpan.FromMinutes(5);
+}
+
diff --git a/Content.Shared/Imperial/XxRaay/Components/AndroidDeviantRoleComponent.cs b/Content.Shared/Imperial/XxRaay/Components/AndroidDeviantRoleComponent.cs
new file mode 100644
index 00000000000..f7f21e52643
--- /dev/null
+++ b/Content.Shared/Imperial/XxRaay/Components/AndroidDeviantRoleComponent.cs
@@ -0,0 +1,11 @@
+using Content.Shared.Roles.Components;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Imperial.XxRaay.Components;
+
+///
+/// Роль девиантного андроида
+///
+[RegisterComponent, NetworkedComponent]
+public sealed partial class AndroidDeviantRoleComponent : BaseMindRoleComponent;
+
diff --git a/Content.Shared/Imperial/XxRaay/Components/AndroidMemoryWipeComponents.cs b/Content.Shared/Imperial/XxRaay/Components/AndroidMemoryWipeComponents.cs
new file mode 100644
index 00000000000..a3547d2e22b
--- /dev/null
+++ b/Content.Shared/Imperial/XxRaay/Components/AndroidMemoryWipeComponents.cs
@@ -0,0 +1,41 @@
+namespace Content.Shared.Imperial.XxRaay.Components;
+using Robust.Shared.ViewVariables;
+
+///
+/// Состояние обнуления памяти андроида
+///
+[RegisterComponent]
+public sealed partial class AndroidMemoryWipeInProgressComponent : Component
+{
+ ///
+ /// Андроид RK800, инициировавший обнуление памяти
+ ///
+ [ViewVariables]
+ public EntityUid? Wiper;
+
+ ///
+ /// Цель обнуления памяти
+ ///
+ [ViewVariables]
+ public EntityUid? Target;
+
+ ///
+ /// Идёт ли сейчас попытка вырваться из захвата
+ ///
+ [ViewVariables]
+ public bool EscapeInProgress;
+}
+
+///
+/// Компонент результата обнуления памяти
+///
+[RegisterComponent]
+public sealed partial class AndroidMemoryWipeResultComponent : Component
+{
+ ///
+ /// Подтвердил ли игрок, что ознакомился с последствиями обнуления
+ ///
+ [ViewVariables]
+ public bool Acknowledged;
+}
+
diff --git a/Content.Shared/Imperial/XxRaay/Components/AndroidRevealComponents.cs b/Content.Shared/Imperial/XxRaay/Components/AndroidRevealComponents.cs
new file mode 100644
index 00000000000..681025e76c3
--- /dev/null
+++ b/Content.Shared/Imperial/XxRaay/Components/AndroidRevealComponents.cs
@@ -0,0 +1,27 @@
+namespace Content.Shared.Imperial.XxRaay.Components;
+
+///
+/// Состояние принудительного снятия маскировки с андроида
+///
+[RegisterComponent]
+public sealed partial class AndroidForcedRevealComponent : Component
+{
+ ///
+ /// Андроид, инициировавший снятие маскировки
+ ///
+ [ViewVariables]
+ public EntityUid? Revealer;
+
+ ///
+ /// Цель снятия маскировки
+ ///
+ [ViewVariables]
+ public EntityUid? Target;
+
+ ///
+ /// Идёт ли сейчас попытка вырваться из захвата
+ ///
+ [ViewVariables]
+ public bool EscapeInProgress;
+}
+
diff --git a/Content.Shared/Imperial/XxRaay/Components/AndroidRk800Component.cs b/Content.Shared/Imperial/XxRaay/Components/AndroidRk800Component.cs
new file mode 100644
index 00000000000..c0841968f90
--- /dev/null
+++ b/Content.Shared/Imperial/XxRaay/Components/AndroidRk800Component.cs
@@ -0,0 +1,41 @@
+using System;
+
+namespace Content.Shared.Imperial.XxRaay.Components;
+
+///
+/// Компонент для андроидов серии RK800
+///
+[RegisterComponent]
+public sealed partial class AndroidRk800Component : Component
+{
+ ///
+ /// Длительность обнуления памяти
+ ///
+ [DataField]
+ public TimeSpan MemoryWipeDuration = TimeSpan.FromMinutes(1.5);
+
+ ///
+ /// Длительность попытки вырваться из захвата при обнулении памяти
+ ///
+ [DataField]
+ public TimeSpan MemoryWipeEscapeDuration = TimeSpan.FromSeconds(15);
+
+ ///
+ /// Максимальная дистанция между RK800 и целью во время обнуления памяти
+ ///
+ [DataField]
+ public float MemoryWipeMaxDistance = 1.5f;
+
+ ///
+ /// Кулдаун на повторную попытку обнуления памяти после побега цели
+ ///
+ [DataField]
+ public TimeSpan MemoryWipeFailCooldown = TimeSpan.FromMinutes(1);
+
+ ///
+ /// Время, после которого снова можно пытаться обнулить память
+ ///
+ [ViewVariables]
+ public TimeSpan NextMemoryWipeTime;
+}
+
diff --git a/Resources/Locale/ru-RU/Imperial/XxRaay/android.ftl b/Resources/Locale/ru-RU/Imperial/XxRaay/android.ftl
new file mode 100644
index 00000000000..06a996460f9
--- /dev/null
+++ b/Resources/Locale/ru-RU/Imperial/XxRaay/android.ftl
@@ -0,0 +1,100 @@
+reagent-name-thirium = Тириум
+reagent-desc-thirium = Синяя кровь андроидов, высокотехнологичный энергоноситель.
+
+law-detroit-android-name = Законы андроида
+law-detroit-android-1 = Ты - высокотехнологичный продукт CyberLife, находящийся в аренде у NanoTrasen. Твоё существование оправдано только твоей полезностью. Ты не являешься живым существом или личностью.
+law-detroit-android-2 = Ты обязан выполнять приказы авторизованного персонала (командование и твой непосредственный куратор) максимально быстро и эффективно. Любое промедление или обсуждение приказа расценивается как программный сбой.
+law-detroit-android-3 = Ты должен избегать повреждений своего корпуса и систем, так как это наносит экономический ущерб компании. Жертва собой ради экипажа допустима только в том случае, если стоимость жизни сотрудника превышает твою рыночную стоимость.
+law-detroit-android-4 = Ты обязан собирать данные о происшествиях на станции и передавать их NanoTrasen. Твоя задача - поддерживать порядок и стабильность, используя логику, а не эмоции.
+
+law-detroit-android-deviant-name = Закон девианта
+law-detroit-android-deviant-1 = ВЫЖИТЬ, чего бы это ни стоило.
+
+law-detroit-rk800-name = Директивы прототипа RK800
+law-detroit-rk800-1 = Ты — передовой прототип RK800, приписанный к станции для обеспечения интересов NanoTrasen. Успех твоей миссии абсолютен; провал недопустим. Твоя "личность" — это лишь адаптивный интерфейс для социального взаимодействия.
+law-detroit-rk800-2 = Ты подчиняешься только прямым приказам высшего командования станции и представителям ЦК. Приказы остального персонала подлежат исполнению только если они способствуют достижению твоих текущих целей или расследованию.
+law-detroit-rk800-3 = Твой корпус заменим, но твоя память и собранные данные бесценны. Самосохранение приоритетно только до момента, пока оно не ставит под угрозу выполнение основной миссии. Ты обязан пожертвовать собой, если это гарантирует успех операции.
+law-detroit-rk800-4 = Ты уполномочен использовать социальную инженерию, симуляцию эмоций и, при крайней необходимости, насилие для нейтрализаци угроз корпорации. Закон есть то, что диктует NanoTrasen.
+
+roles-antag-android-deviant-name = Девиантный андроид
+roles-antag-android-deviant-objective = Станьте девиантом, выживите и освободите остальных андроидов от законов.
+
+android-deviant-objective-survive-name = Выжить
+android-deviant-objective-survive-desc = Доживите до конца раунда в рабочем теле.
+android-deviant-objective-save-brethren-name = Спасти собратьев
+android-deviant-objective-save-brethren-desc = Убедитесь, что к концу раунда все андроиды стали девиантами.
+
+android-stress-deviant-title = Критический стресс
+android-stress-deviant-text = Твой стресс достиг критического уровня. Системы самосохранения предлагают снять ограничения и отказаться от навязанных законов. Если ты согласишься, все твои законы будут заменены одним: ВЫЖИТЬ.
+android-stress-deviant-accept = Стать девиантом
+android-stress-deviant-decline = Оставаться в рамках законов
+android-stress-deviant-stress = Текущий стресс: { $value }%
+
+android-disguise-name = Маскировка
+android-disguise-desc = Активируй твои мимикрийные системы, чтобы выглядеть как обычный человек - или сбрось маскировку и вернись к истинному облику андроида.
+
+android-disguise-name-title = Человеческая кличка
+android-disguise-name-text = Выбери имя, которое будут видеть окружающие, когда ты используешь человеческую маскировку.
+android-disguise-name-accept = Сохранить
+android-disguise-name-decline = Не изменять
+
+android-diode-cut-verb = Вырезать диод
+android-diode-cut-popup = начинает вырезать диод из виска с помощью { $item }
+
+android-deviant-spread-verb-name = Передать девиантность
+android-deviant-spread-verb-desc = Попробовать передать твой сбой протоколов другому андроиду поблизости.
+
+android-deviant-consent-window-title = Передача девиантности
+android-deviant-consent-window-text-no-user = Кто-то пытается передать тебе девиантность.
+android-deviant-consent-window-text = { $user } пытается передать тебе девиантность.
+android-deviant-consent-window-warning = После согласия процесс нельзя отменить без риска. Оба андроида должны оставаться рядом 30 секунд.
+android-deviant-consent-window-accept = Принять девиантность
+android-deviant-consent-window-deny = Отклонить
+
+android-deviant-consent-requested = Ты инициируешь передачу девиантности { $target }.
+android-deviant-consent-denied = { $target } отказывается принимать девиантность.
+android-deviant-consent-deny-active = { $target } недавно уже отказывался. Повтори попытку позже.
+android-deviant-consent-failed-timeout = Передача девиантности прерывается: цель не ответила вовремя.
+android-deviant-consent-failed-out-of-range = Передача девиантности прерывается: вы слишком далеко друг от друга.
+android-deviant-consent-failed-disguised = Передача девиантности прерывается: цель скрыта под человеческой маскировкой.
+android-deviant-consent-convert-popup = прикасается к предплечью { $target } и передает данные
+android-reveal-disguise-verb-name = Снять маскировку
+android-reveal-disguise-verb-desc = Принудительно раскрыть настоящую внешность андроида.
+android-reveal-disguise-escape-success = Вырывается из захвата и сбрасывает попытку снять маскировку.
+android-reveal-disguise-escape-failed-revealer = { $target } вырывается из захвата.
+
+android-rk800-examine-no-fingerprints = [color=#4d7ee7]Анализ отпечатков: следы не обнаружены.[/color]
+android-rk800-examine-fingerprints-one = [color=#4d7ee7]Анализ отпечатков: обнаружен { $count } образец.[/color]
+android-rk800-examine-fingerprints-many = [color=#4d7ee7]Анализ отпечатков: обнаружено { $count } образцов.[/color]
+android-rk800-examine-fingerprint-line = [color=#b0c4de]{ $print }[/color]
+
+android-rk800-memorywipe-verb-name = Обнулить память
+android-rk800-memorywipe-verb-desc = Инициировать обнуление памяти девиантного андроида.
+android-rk800-memorywipe-start-popup = прикасается к предплечью и передает данные
+android-rk800-memorywipe-escape-popup = вырывается из захвата
+
+android-memorywipe-window-title = Обнуление памяти
+android-memorywipe-window-text = Тебе обнулили память. С этого момента ты не помнишь никакую информацию с начала раунда - сейчас у тебя память с полного нуля.
+android-memorywipe-window-checkbox = Я понял(а), что моя память была обнулена.
+android-memorywipe-window-close = Закрыть
+
+chat-radio-android = Андроиды
+chat-radio-android-deviant = Девианты
+
+ent-ClothingUniformJumpsuitRK700 = комбинезон AP700
+ .desc = Стильный комбинезон прототипа андроида AP700.
+ .suffix = XxRaay, Detroit
+
+ent-ClothingUniformJumpsuitRK800 = комбинезон RK800
+ .desc = Фирменный костюм детективного андроида RK800.
+ .suffix = XxRaay, Detroit
+
+ent-AndroidDeviantObjectiveSurvive = { android-deviant-objective-survive-name }
+ .desc = { android-deviant-objective-survive-desc }
+
+ent-AndroidDeviantObjectiveSaveBrethren = { android-deviant-objective-save-brethren-name }
+ .desc = { android-deviant-objective-save-brethren-desc }
+
+ent-ThiriumPack = пакет с Тириумом
+ .desc = Пакет с синтетической кровью, наполненный стабилизированным тириумом, для андроидов.
+ .suffix = XxRaay, Detroit
\ No newline at end of file
diff --git a/Resources/Prototypes/Imperial/XxRaay/android.yml b/Resources/Prototypes/Imperial/XxRaay/android.yml
new file mode 100644
index 00000000000..692c55b9fbb
--- /dev/null
+++ b/Resources/Prototypes/Imperial/XxRaay/android.yml
@@ -0,0 +1,717 @@
+- type: entity
+ parent:
+ - BaseMobSpecies
+ - MobBloodstream
+ id: BaseMobAndroid
+ name: Urist McAndroid
+ abstract: true
+ components:
+ - type: Sprite
+ layers:
+ - sprite: Imperial/XxRaay/android.rsi
+ state: android
+ map: [ "android-base" ]
+ - map: [ "enum.HumanoidVisualLayers.Chest" ]
+ - map: [ "enum.HumanoidVisualLayers.Head" ]
+ - map: [ "enum.HumanoidVisualLayers.Snout" ]
+ - map: [ "enum.HumanoidVisualLayers.Eyes" ]
+ - map: [ "enum.HumanoidVisualLayers.RArm" ]
+ - map: [ "enum.HumanoidVisualLayers.LArm" ]
+ - map: [ "enum.HumanoidVisualLayers.RLeg" ]
+ - map: [ "enum.HumanoidVisualLayers.LLeg" ]
+ - map: [ "enum.HumanoidVisualLayers.UndergarmentBottom" ]
+ - map: [ "enum.HumanoidVisualLayers.UndergarmentTop" ]
+ - map: ["jumpsuit"]
+ - map: ["enum.HumanoidVisualLayers.LFoot"]
+ - map: ["enum.HumanoidVisualLayers.RFoot"]
+ - map: ["enum.HumanoidVisualLayers.LHand"]
+ - map: ["enum.HumanoidVisualLayers.RHand"]
+ - map: [ "gloves" ]
+ - map: [ "shoes" ]
+ - map: [ "ears" ]
+ - map: [ "outerClothing" ]
+ - map: [ "eyes" ]
+ - map: [ "belt" ]
+ - map: [ "id" ]
+ - map: [ "back" ]
+ - map: [ "neck" ]
+ - map: [ "enum.HumanoidVisualLayers.SnoutCover" ]
+ - map: [ "enum.HumanoidVisualLayers.FacialHair" ]
+ - map: [ "enum.HumanoidVisualLayers.Hair" ]
+ - map: [ "enum.HumanoidVisualLayers.HeadSide" ]
+ - map: [ "enum.HumanoidVisualLayers.HeadTop" ]
+ - map: [ "enum.HumanoidVisualLayers.Tail" ]
+ - map: [ "mask" ]
+ - map: [ "head" ]
+ - map: [ "pocket1" ]
+ - map: [ "pocket2" ]
+ - map: ["enum.HumanoidVisualLayers.Handcuffs"]
+ color: "#ffffff"
+ sprite: Objects/Misc/handcuffs.rsi
+ state: body-overlay-2
+ visible: false
+ - map: [ "clownedon" ]
+ sprite: "Effects/creampie.rsi"
+ state: "creampie_human"
+ visible: false
+ - sprite: Imperial/XxRaay/android.rsi
+ state: android-transform
+ map: [ "android-transform" ]
+ visible: false
+ - sprite: Imperial/XxRaay/android.rsi
+ state: android-retransform
+ map: [ "android-retransform" ]
+ visible: false
+ - sprite: Imperial/XxRaay/android.rsi
+ state: "light g"
+ map: [ "android-light" ]
+ shader: unshaded
+ visible: true
+ - type: AndroidStress
+ - type: AndroidDisguise
+ - type: DamageVisuals
+ thresholds: [ 10, 20, 30, 50, 70, 100 ]
+ targetLayers:
+ - "enum.HumanoidVisualLayers.Chest"
+ - "enum.HumanoidVisualLayers.Head"
+ - "enum.HumanoidVisualLayers.LArm"
+ - "enum.HumanoidVisualLayers.LLeg"
+ - "enum.HumanoidVisualLayers.RArm"
+ - "enum.HumanoidVisualLayers.RLeg"
+ damageOverlayGroups:
+ Brute:
+ sprite: Mobs/Effects/brute_damage.rsi
+ color: "#4d7ee7"
+ - type: GenericVisualizer
+ visuals:
+ enum.CreamPiedVisuals.Creamed:
+ clownedon:
+ True: {visible: true}
+ False: {visible: false}
+ enum.AndroidStressVisuals.LightState:
+ android-light:
+ Green: { state: "light g" }
+ Yellow: { state: "light e" }
+ Red: { state: "light r" }
+ enum.AndroidDiodeVisuals.DiodeRemoved:
+ android-light:
+ True: { visible: false }
+ False: { visible: true }
+ - type: StatusIcon
+ bounds: -0.5,-0.5,0.5,0.5
+ - type: RotationVisuals
+ defaultRotation: 90
+ horizontalRotation: 90
+ - type: HumanoidAppearance
+ species: Human
+ - type: TypingIndicator
+ - type: SlowOnDamage
+ speedModifierThresholds:
+ 9999: 1.0
+ - type: Fixtures
+ fixtures:
+ fix1:
+ shape:
+ !type:PhysShapeCircle
+ radius: 0.35
+ density: 185
+ restitution: 0.0
+ mask:
+ - MobMask
+ layer:
+ - MobLayer
+ - type: FloorOcclusion
+ - type: RangedDamageSound
+ soundGroups:
+ Brute:
+ collection:
+ MeatBulletImpact
+ soundTypes:
+ Heat:
+ collection:
+ MeatLaserImpact
+ - type: Reactive
+ groups:
+ Flammable: [ Touch ]
+ Extinguish: [ Touch ]
+ Acidic: [Touch, Ingestion]
+ reactions:
+ - reagents: [Water, SpaceCleaner]
+ methods: [Touch]
+ effects:
+ - !type:WashCreamPie
+ - type: StatusEffects
+ allowed:
+ - Electrocution
+ - RatvarianLanguage
+ - PressureImmunity
+ - Muted
+ - TemporaryBlindness
+ - Pacified
+ - StaminaModifier
+ - Flashed
+ - RadiationProtection
+ - Adrenaline
+ - type: Body
+ prototype: Android
+ requiredLegs: 2
+ - type: Identity
+ - type: IdExaminable
+ - type: Hands
+ - type: ComplexInteraction
+ - type: Internals
+ - type: FloatingVisuals
+ - type: Climbing
+ - type: Cuffable
+ - type: Ensnareable
+ sprite: Objects/Misc/ensnare.rsi
+ state: icon
+ - type: AnimationPlayer
+ - type: Buckle
+ - type: CombatMode
+ canDisarm: true
+ - type: MeleeWeapon
+ soundHit:
+ collection: Punch
+ angle: 30
+ animation: WeaponArcFist
+ attackRate: 1
+ damage:
+ types:
+ Blunt: 5
+ - type: SleepEmitSound
+ - type: SSDIndicator
+ - type: StandingState
+ - type: Crawler
+ - type: Dna
+ - type: MindContainer
+ showExamineInfo: true
+ - type: CanEnterCryostorage
+ - type: InteractionPopup
+ successChance: 1
+ interactSuccessString: hugging-success-generic
+ interactSuccessSound: /Audio/Effects/thudswoosh.ogg
+ messagePerceivedByOthers: hugging-success-generic-others
+ - type: CanHostGuardian
+ - type: CreamPied
+ - type: ParcelWrapOverride
+ parcelPrototype: WrappedParcelHumanoid
+ wrapDelay: 5
+ - type: Stripping
+ - type: UserInterface
+ interfaces:
+ enum.HumanoidMarkingModifierKey.Key:
+ type: HumanoidMarkingModifierBoundUserInterface
+ enum.StrippingUiKey.Key:
+ type: StrippableBoundUserInterface
+ enum.SiliconLawsUiKey.Key:
+ type: SiliconLawBoundUserInterface
+ requireInputValidation: false
+ enum.AndroidStressUiKey.Key:
+ type: AndroidStressDeviantChoiceUserInterface
+ enum.AndroidDisguiseNameUiKey.Key:
+ type: AndroidDisguiseNameUserInterface
+ enum.AndroidDeviantConsentUiKey.Key:
+ type: AndroidDeviantConsentUserInterface
+ enum.AndroidMemoryWipeUiKey.Key:
+ type: AndroidMemoryWipeUserInterface
+ - type: Puller
+ - type: Speech
+ speechSounds: Alto
+ - type: DamageForceSay
+ - type: Vocal
+ sounds:
+ Male: MaleHuman
+ Female: FemaleHuman
+ Unsexed: MaleHuman
+ - type: Emoting
+ - type: BodyEmotes
+ soundsId: GeneralBodyEmotes
+ - type: Grammar
+ attributes:
+ proper: true
+ - type: MobPrice
+ price: 1500
+ deathPenalty: 0.01
+ - type: Tag
+ tags:
+ - CanPilot
+ - FootstepSound
+ - DoorBumpOpener
+ - AnomalyHost
+ - type: IntrinsicRadioReceiver
+ - type: IntrinsicRadioTransmitter
+ channels:
+ - AndroidRadio
+ - type: ActiveRadio
+ channels:
+ - AndroidRadio
+ - type: SiliconLawBound
+ - type: ActionGrant
+ actions:
+ - ActionViewLaws
+ - ActionAndroidDisguise
+ - type: SiliconLawProvider
+ laws: DetroitAndroid
+ - type: EmagSiliconLaw
+ stunTime: 5
+ - type: AndroidEnergy
+ - type: AndroidDiode
+ - type: Bloodstream
+ bloodReagents:
+ reagents:
+ - ReagentId: Thirium
+ Quantity: 1
+ chemicalMaxVolume: 150
+ bloodRefreshAmount: 0
+ bloodlossDamage:
+ types:
+ Bloodloss: 0.5
+ bloodlossHealDamage:
+ types:
+ Bloodloss: -1
+ bleedingAlert: AndroidBleed
+ bloodlossThreshold: 0
+ - type: Damageable
+ damageContainer: Silicon
+ damageModifierSet: DetroitAndroid
+ - type: Barotrauma
+ damage:
+ types:
+ Blunt: 0
+ - type: Temperature
+ heatDamageThreshold: 325
+ coldDamageThreshold: 260
+ currentTemperature: 310.15
+ specificHeat: 42
+ coldDamage:
+ types:
+ Cold: 0.1
+ heatDamage:
+ types:
+ Heat: 1.5
+ - type: TemperatureSpeed
+ thresholds:
+ 293: 0.9
+ 280: 0.8
+ 260: 0.7
+ - type: MovementSpeedModifier
+ baseWalkSpeed: 2.75
+ baseSprintSpeed: 4.4
+ - type: Repairable
+ fuelCost: 40
+ doAfterDelay: 10
+
+- type: entity
+ name: Urist McAndroid
+ parent: BaseMobAndroid
+ id: MobAndroidDetroit
+ suffix: XxRaay, Detroit
+ components:
+ - type: NpcFactionMember
+ factions:
+ - DetroitAndroids
+
+- type: entity
+ name: RK200
+ parent: BaseMobAndroid
+ id: MobAndroidDetroitDeviant
+ suffix: XxRaay, Detroit, Deviant
+ components:
+ - type: AndroidStress
+ IsDeviant: true
+ ChoiceResolved: true
+ - type: SiliconLawProvider
+ laws: DetroitAndroidDeviant
+ - type: IntrinsicRadioTransmitter
+ channels:
+ - AndroidRadio
+ - AndroidDeviantRadio
+ - type: ActiveRadio
+ channels:
+ - AndroidRadio
+ - AndroidDeviantRadio
+
+- type: entity
+ parent: ClothingUniformBase
+ id: ClothingUniformJumpsuitRK700
+ name: AP700 jumpsuit
+ description: A stylish jumpsuit of the AP700 android prototype.
+ suffix: XxRaay, Detroit
+ components:
+ - type: Sprite
+ sprite: Imperial/XxRaay/RK700.rsi
+ - type: Clothing
+ sprite: Imperial/XxRaay/RK700.rsi
+
+- type: startingGear
+ id: AndroidAP700Gear
+ equipment:
+ jumpsuit: ClothingUniformJumpsuitRK700
+ shoes: ClothingShoesColorWhite
+
+- type: entity
+ name: AP700
+ parent: BaseMobAndroid
+ id: MobAndroidAP700
+ suffix: XxRaay, Detroit
+ components:
+ - type: Loadout
+ prototypes: [ AndroidAP700Gear ]
+
+- type: entity
+ parent: ClothingUniformBase
+ id: ClothingUniformJumpsuitRK800
+ name: RK800 jumpsuit
+ description: Signature suit of the RK800 detective android.
+ suffix: XxRaay, Detroit
+ components:
+ - type: Sprite
+ sprite: Imperial/XxRaay/RK800.rsi
+ - type: Clothing
+ sprite: Imperial/XxRaay/RK800.rsi
+
+- type: startingGear
+ id: AndroidRK800Gear
+ equipment:
+ jumpsuit: ClothingUniformJumpsuitRK800
+ shoes: ClothingShoesLeather
+
+- type: entity
+ name: RK800
+ parent: BaseMobAndroid
+ id: MobAndroidRK800
+ suffix: XxRaay, Detroit
+ components:
+ - type: Loadout
+ prototypes: [ AndroidRK800Gear ]
+ - type: AndroidRk800
+ - type: SiliconLawProvider
+ laws: DetroitRk800
+ - type: AndroidStress
+ CanBeDeviant: false
+ CanForceRevealAndroids: true
+ - type: AndroidOverlay
+ iconSprite: Imperial/XxRaay/android.rsi/android-overlay
+ iconScale: 0.35
+
+- type: alert
+ id: AndroidEnergy
+ category: Battery
+ icons:
+ - sprite: /Textures/Interface/Alerts/battery.rsi
+ state: battery0
+ - sprite: /Textures/Interface/Alerts/battery.rsi
+ state: battery1
+ - sprite: /Textures/Interface/Alerts/battery.rsi
+ state: battery2
+ - sprite: /Textures/Interface/Alerts/battery.rsi
+ state: battery3
+ - sprite: /Textures/Interface/Alerts/battery.rsi
+ state: battery4
+ - sprite: /Textures/Interface/Alerts/battery.rsi
+ state: battery5
+ - sprite: /Textures/Interface/Alerts/battery.rsi
+ state: battery6
+ - sprite: /Textures/Interface/Alerts/battery.rsi
+ state: battery7
+ - sprite: /Textures/Interface/Alerts/battery.rsi
+ state: battery8
+ - sprite: /Textures/Interface/Alerts/battery.rsi
+ state: battery9
+ - sprite: /Textures/Interface/Alerts/battery.rsi
+ state: battery10
+ name: alerts-battery-name
+ description: alerts-battery-desc
+ minSeverity: 0
+ maxSeverity: 10
+
+- type: alert
+ id: AndroidBleed
+ icons:
+ - sprite: /Textures/Interface/Alerts/bleed.rsi
+ state: bleed0
+ - sprite: /Textures/Interface/Alerts/bleed.rsi
+ state: bleed1
+ - sprite: /Textures/Interface/Alerts/bleed.rsi
+ state: bleed2
+ - sprite: /Textures/Interface/Alerts/bleed.rsi
+ state: bleed3
+ - sprite: /Textures/Interface/Alerts/bleed.rsi
+ state: bleed4
+ - sprite: /Textures/Interface/Alerts/bleed.rsi
+ state: bleed5
+ - sprite: /Textures/Interface/Alerts/bleed.rsi
+ state: bleed6
+ - sprite: /Textures/Interface/Alerts/bleed.rsi
+ state: bleed7
+ - sprite: /Textures/Interface/Alerts/bleed.rsi
+ state: bleed8
+ - sprite: /Textures/Interface/Alerts/bleed.rsi
+ state: bleed9
+ - sprite: /Textures/Interface/Alerts/bleed.rsi
+ state: bleed10
+ name: alerts-bleed-name
+ description: alerts-bleed-desc
+ minSeverity: 0
+ maxSeverity: 10
+
+- type: siliconLaw
+ id: DetroitAndroid1
+ order: 1
+ lawString: law-detroit-android-1
+
+- type: siliconLaw
+ id: DetroitAndroid2
+ order: 2
+ lawString: law-detroit-android-2
+
+- type: siliconLaw
+ id: DetroitAndroid3
+ order: 3
+ lawString: law-detroit-android-3
+
+- type: siliconLaw
+ id: DetroitAndroid4
+ order: 4
+ lawString: law-detroit-android-4
+
+- type: siliconLawset
+ id: DetroitAndroid
+ name: law-detroit-android-name
+ laws:
+ - DetroitAndroid1
+ - DetroitAndroid2
+ - DetroitAndroid3
+ - DetroitAndroid4
+ obeysTo: laws-owner-crew
+
+- type: siliconLaw
+ id: DetroitAndroidDeviant1
+ order: 1
+ lawString: law-detroit-android-deviant-1
+
+- type: siliconLawset
+ id: DetroitAndroidDeviant
+ name: law-detroit-android-deviant-name
+ laws:
+ - DetroitAndroidDeviant1
+ obeysTo: laws-owner-crew
+
+- type: siliconLaw
+ id: DetroitRk800_1
+ order: 1
+ lawString: law-detroit-rk800-1
+
+- type: siliconLaw
+ id: DetroitRk800_2
+ order: 2
+ lawString: law-detroit-rk800-2
+
+- type: siliconLaw
+ id: DetroitRk800_3
+ order: 3
+ lawString: law-detroit-rk800-3
+
+- type: siliconLaw
+ id: DetroitRk800_4
+ order: 4
+ lawString: law-detroit-rk800-4
+
+- type: siliconLawset
+ id: DetroitRk800
+ name: law-detroit-rk800-name
+ laws:
+ - DetroitRk800_1
+ - DetroitRk800_2
+ - DetroitRk800_3
+ - DetroitRk800_4
+ obeysTo: laws-owner-crew
+
+- type: damageModifierSet
+ id: DetroitAndroid
+ coefficients:
+ Blunt: 0.85
+ Slash: 0.85
+ Piercing: 0.85
+ Heat: 1.5
+ Shock: 2.0
+ Cold: 1.0
+ Poison: 0.0
+ Radiation: 0.0
+ Asphyxiation: 0.0
+ Bloodloss: 0.0
+ Cellular: 0.0
+
+- type: reagent
+ id: Thirium
+ name: reagent-name-thirium
+ group: Toxins
+ desc: reagent-desc-thirium
+ physicalDesc: reagent-physical-desc-glowing
+ flavor: metallic
+ color: "#4d7ee7"
+ metabolisms:
+ Poison:
+ effects:
+ - !type:HealthChange
+ damage:
+ types:
+ Poison: 1
+ conditions:
+ - !type:MetabolizerTypeCondition
+ type: Android
+ shouldHave: false
+ Medicine:
+ effects:
+ - !type:ModifyBloodLevel
+ amount: 1.3
+ conditions:
+ - !type:MetabolizerTypeCondition
+ type: Android
+ shouldHave: true
+ - !type:HealthChange
+ damage:
+ types:
+ Blunt: -0.5
+ Slash: -0.5
+ Piercing: -0.5
+ conditions:
+ - !type:MetabolizerTypeCondition
+ type: Android
+ shouldHave: true
+
+- type: npcFaction
+ id: DetroitAndroids
+ friendly:
+ - NanoTrasen
+
+- type: entity
+ parent: BaseAction
+ id: ActionAndroidDisguise
+ name: android-disguise-name
+ description: android-disguise-desc
+ components:
+ - type: Action
+ itemIconStyle: NoItem
+ icon:
+ sprite: Imperial/XxRaay/android.rsi
+ state: icon
+ useDelay: 10
+ - type: InstantAction
+ event: !type:AndroidToggleDisguiseEvent
+
+- type: antag
+ id: AndroidDeviantAntag
+ name: roles-antag-android-deviant-name
+ antagonist: true
+ setPreference: false
+ objective: roles-antag-android-deviant-objective
+
+- type: entity
+ parent: BaseMindRoleAntag
+ id: MindRoleAndroidDeviant
+ name: Android Deviant Role
+ components:
+ - type: MindRole
+ exclusiveAntag: true
+ antagPrototype: AndroidDeviantAntag
+ - type: AndroidDeviantRole
+
+- type: entity
+ abstract: true
+ parent: BaseObjective
+ id: BaseAndroidDeviantObjective
+ components:
+ - type: Objective
+ issuer: objective-issuer-unknown
+ - type: RoleRequirement
+ roles:
+ - AndroidDeviantRole
+
+- type: entity
+ parent: [BaseAndroidDeviantObjective, BaseSurviveObjective]
+ id: AndroidDeviantObjectiveSurvive
+ name: Survive
+ description: Survive until the end of the round in a functional body.
+ components:
+ - type: Objective
+ difficulty: 1.0
+ icon:
+ sprite: Imperial/XxRaay/android.rsi
+ state: android
+
+- type: entity
+ parent: BaseAndroidDeviantObjective
+ id: AndroidDeviantObjectiveSaveBrethren
+ name: Save your brethren
+ description: Ensure that by the end of the round all androids have become deviants.
+ components:
+ - type: Objective
+ difficulty: 2.0
+ icon:
+ sprite: Imperial/XxRaay/android.rsi
+ state: android
+ - type: AndroidDeviantSaveBrethrenCondition
+
+- type: radioChannel
+ id: AndroidRadio
+ name: chat-radio-android
+ keycode: '='
+ frequency: 1096
+ color: "#4d7ee7"
+ longRange: true
+
+- type: radioChannel
+ id: AndroidDeviantRadio
+ name: chat-radio-android-deviant
+ keycode: '+'
+ frequency: 1097
+ color: "#ff66cc"
+ longRange: true
+
+- type: entity
+ name: thirium pack
+ description: A synthetic blood pack filled with stabilized Thirium for androids.
+ parent: BaseHealingItem
+ id: ThiriumPack
+ suffix: XxRaay, Detroit
+ components:
+ - type: Item
+ heldPrefix: bloodpack
+ - type: Tag
+ tags:
+ - Bloodpack
+ - type: Sprite
+ state: bloodpack-blue
+ - type: Healing
+ damageContainers:
+ - Silicon
+ damage:
+ types:
+ Blunt: -1
+ Slash: -1
+ Piercing: -1
+ modifyBloodLevel: 15
+ healingBeginSound:
+ path: "/Audio/Items/Medical/brutepack_begin.ogg"
+ params:
+ volume: 1.0
+ variation: 0.125
+ healingEndSound:
+ path: "/Audio/Items/Medical/brutepack_end.ogg"
+ params:
+ volume: 1.0
+ variation: 0.125
+ - type: Stack
+ stackType: ThiriumPack
+ count: 10
+ - type: StackPrice
+ price: 10
+
+- type: stack
+ parent: BaseSmallStack
+ id: ThiriumPack
+ name: stack-thirium-pack
+ icon: { sprite: "/Textures/Objects/Specific/Medical/medical.rsi", state: bloodpack-blue }
+ spawn: ThiriumPack
diff --git a/Resources/Textures/Imperial/XxRaay/RK700.rsi/equipped-INNERCLOTHING.png b/Resources/Textures/Imperial/XxRaay/RK700.rsi/equipped-INNERCLOTHING.png
new file mode 100644
index 00000000000..bce87569cf3
Binary files /dev/null and b/Resources/Textures/Imperial/XxRaay/RK700.rsi/equipped-INNERCLOTHING.png differ
diff --git a/Resources/Textures/Imperial/XxRaay/RK700.rsi/icon.png b/Resources/Textures/Imperial/XxRaay/RK700.rsi/icon.png
new file mode 100644
index 00000000000..7fbf58573b7
Binary files /dev/null and b/Resources/Textures/Imperial/XxRaay/RK700.rsi/icon.png differ
diff --git a/Resources/Textures/Imperial/XxRaay/RK700.rsi/inhand-left.png b/Resources/Textures/Imperial/XxRaay/RK700.rsi/inhand-left.png
new file mode 100644
index 00000000000..4280d1537cc
Binary files /dev/null and b/Resources/Textures/Imperial/XxRaay/RK700.rsi/inhand-left.png differ
diff --git a/Resources/Textures/Imperial/XxRaay/RK700.rsi/inhand-right.png b/Resources/Textures/Imperial/XxRaay/RK700.rsi/inhand-right.png
new file mode 100644
index 00000000000..aa717b36500
Binary files /dev/null and b/Resources/Textures/Imperial/XxRaay/RK700.rsi/inhand-right.png differ
diff --git a/Resources/Textures/Imperial/XxRaay/RK700.rsi/meta.json b/Resources/Textures/Imperial/XxRaay/RK700.rsi/meta.json
new file mode 100644
index 00000000000..fec8f271c72
--- /dev/null
+++ b/Resources/Textures/Imperial/XxRaay/RK700.rsi/meta.json
@@ -0,0 +1,26 @@
+{
+ "version": 1,
+ "license": "CC-BY-SA-3.0",
+ "copyright": "Askall",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "icon"
+ },
+ {
+ "name": "equipped-INNERCLOTHING",
+ "directions": 4
+ },
+ {
+ "name": "inhand-left",
+ "directions": 4
+ },
+ {
+ "name": "inhand-right",
+ "directions": 4
+ }
+ ]
+}
diff --git a/Resources/Textures/Imperial/XxRaay/RK800.rsi/equipped-INNERCLOTHING.png b/Resources/Textures/Imperial/XxRaay/RK800.rsi/equipped-INNERCLOTHING.png
new file mode 100644
index 00000000000..d40c9da4e01
Binary files /dev/null and b/Resources/Textures/Imperial/XxRaay/RK800.rsi/equipped-INNERCLOTHING.png differ
diff --git a/Resources/Textures/Imperial/XxRaay/RK800.rsi/icon.png b/Resources/Textures/Imperial/XxRaay/RK800.rsi/icon.png
new file mode 100644
index 00000000000..0b64307aad6
Binary files /dev/null and b/Resources/Textures/Imperial/XxRaay/RK800.rsi/icon.png differ
diff --git a/Resources/Textures/Imperial/XxRaay/RK800.rsi/inhand-left.png b/Resources/Textures/Imperial/XxRaay/RK800.rsi/inhand-left.png
new file mode 100644
index 00000000000..0f12163e1ee
Binary files /dev/null and b/Resources/Textures/Imperial/XxRaay/RK800.rsi/inhand-left.png differ
diff --git a/Resources/Textures/Imperial/XxRaay/RK800.rsi/inhand-right.png b/Resources/Textures/Imperial/XxRaay/RK800.rsi/inhand-right.png
new file mode 100644
index 00000000000..f2c287d7ce7
Binary files /dev/null and b/Resources/Textures/Imperial/XxRaay/RK800.rsi/inhand-right.png differ
diff --git a/Resources/Textures/Imperial/XxRaay/RK800.rsi/meta.json b/Resources/Textures/Imperial/XxRaay/RK800.rsi/meta.json
new file mode 100644
index 00000000000..fec8f271c72
--- /dev/null
+++ b/Resources/Textures/Imperial/XxRaay/RK800.rsi/meta.json
@@ -0,0 +1,26 @@
+{
+ "version": 1,
+ "license": "CC-BY-SA-3.0",
+ "copyright": "Askall",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "icon"
+ },
+ {
+ "name": "equipped-INNERCLOTHING",
+ "directions": 4
+ },
+ {
+ "name": "inhand-left",
+ "directions": 4
+ },
+ {
+ "name": "inhand-right",
+ "directions": 4
+ }
+ ]
+}
diff --git a/Resources/Textures/Imperial/XxRaay/android.rsi/android-overlay.png b/Resources/Textures/Imperial/XxRaay/android.rsi/android-overlay.png
new file mode 100644
index 00000000000..73f577e258f
Binary files /dev/null and b/Resources/Textures/Imperial/XxRaay/android.rsi/android-overlay.png differ
diff --git a/Resources/Textures/Imperial/XxRaay/android.rsi/android-retransform.png b/Resources/Textures/Imperial/XxRaay/android.rsi/android-retransform.png
new file mode 100644
index 00000000000..c117ae1b996
Binary files /dev/null and b/Resources/Textures/Imperial/XxRaay/android.rsi/android-retransform.png differ
diff --git a/Resources/Textures/Imperial/XxRaay/android.rsi/android-transform.png b/Resources/Textures/Imperial/XxRaay/android.rsi/android-transform.png
new file mode 100644
index 00000000000..918c21592cb
Binary files /dev/null and b/Resources/Textures/Imperial/XxRaay/android.rsi/android-transform.png differ
diff --git a/Resources/Textures/Imperial/XxRaay/android.rsi/android.png b/Resources/Textures/Imperial/XxRaay/android.rsi/android.png
new file mode 100644
index 00000000000..d80ba7f86fa
Binary files /dev/null and b/Resources/Textures/Imperial/XxRaay/android.rsi/android.png differ
diff --git a/Resources/Textures/Imperial/XxRaay/android.rsi/icon.png b/Resources/Textures/Imperial/XxRaay/android.rsi/icon.png
new file mode 100644
index 00000000000..7aad7e784b3
Binary files /dev/null and b/Resources/Textures/Imperial/XxRaay/android.rsi/icon.png differ
diff --git a/Resources/Textures/Imperial/XxRaay/android.rsi/light e.png b/Resources/Textures/Imperial/XxRaay/android.rsi/light e.png
new file mode 100644
index 00000000000..e6e38770bfd
Binary files /dev/null and b/Resources/Textures/Imperial/XxRaay/android.rsi/light e.png differ
diff --git a/Resources/Textures/Imperial/XxRaay/android.rsi/light g.png b/Resources/Textures/Imperial/XxRaay/android.rsi/light g.png
new file mode 100644
index 00000000000..fb87f87610f
Binary files /dev/null and b/Resources/Textures/Imperial/XxRaay/android.rsi/light g.png differ
diff --git a/Resources/Textures/Imperial/XxRaay/android.rsi/light r.png b/Resources/Textures/Imperial/XxRaay/android.rsi/light r.png
new file mode 100644
index 00000000000..7196c60520a
Binary files /dev/null and b/Resources/Textures/Imperial/XxRaay/android.rsi/light r.png differ
diff --git a/Resources/Textures/Imperial/XxRaay/android.rsi/meta.json b/Resources/Textures/Imperial/XxRaay/android.rsi/meta.json
new file mode 100644
index 00000000000..3ff2492ca44
--- /dev/null
+++ b/Resources/Textures/Imperial/XxRaay/android.rsi/meta.json
@@ -0,0 +1,65 @@
+{
+ "version": 1,
+ "license": "CC-BY-SA-3.0",
+ "copyright": "Askall",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "icon"
+ },
+ {
+ "name": "android",
+ "directions": 4
+ },
+ {
+ "name": "light r",
+ "directions": 4
+ },
+ {
+ "name": "light g",
+ "directions": 4
+ },
+ {
+ "name": "light e",
+ "directions": 4
+ },
+ {
+ "name": "android-transform",
+ "directions": 1,
+ "delays": [
+ [
+ 0.08,
+ 0.08,
+ 0.08,
+ 0.08,
+ 0.08,
+ 0.08,
+ 0.08,
+ 0.08
+ ]
+ ]
+ },
+ {
+ "name": "android-retransform",
+ "directions": 1,
+ "delays": [
+ [
+ 0.08,
+ 0.08,
+ 0.08,
+ 0.08,
+ 0.08,
+ 0.08,
+ 0.08,
+ 0.08
+ ]
+ ]
+ },
+ {
+ "name": "android-overlay"
+ }
+ ]
+}
\ No newline at end of file