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