From 7444c8ea4abeaf33c3fd151537d925ce69d4878a Mon Sep 17 00:00:00 2001 From: chromiumboy <50505512+chromiumboy@users.noreply.github.com> Date: Mon, 15 Sep 2025 09:18:32 -0500 Subject: [PATCH] The station AI can be destroyed (#39588) * Initial commit * Fixing merge conflict * Merge conflict fixed * Anchorable entities can now be marked as 'unanchorable' * Revert "Anchorable entities can now be marked as 'unanchorable'" This reverts commit 6a502e62a703cf06bd36ed3bdefe655fc074cfc5 This functionality will be made into a separate PR * Error sprite * Update AI core appearance with sustained damage, spawn scrap on destroyed * Added intellicard sprite * AI damage overlays * Added fixtures * AI core accent changes when damaged or low on power * Bug fix and pop up messages for inserting AIs into inoperable cores * Updated 'dead' sprite * Destroying the AI core reduces the number of AI job slots available * AI battery duration set to 10 minutes * Initial commit * Allow MMIs used in the construction of AI cores to take them over * Initial resources commit * Initial code commit * Sprite update * Bug fixes and updates * Basic console UI * Code refactor * Added lock screen * Added all outstanding UI features * Added purge sprites * Better appearance handling * Fixed issue with purge sprite * Finalized UI design * Major components finalized * Bit of clean up * Removed some code that was used for testing * Tweaked some text * Removed extra space * Added the circuitboard to the RD's locker * Addressed reviewer comments plus tweaks * Addressed reviewer comments plus tweaks * Removed instances of granular damage * Various improvements * Removed testing code * Fixed issue with disabled buttons * Finalized code * Addressed review comments * Added a spare Station AI core electronics to the research director's locker * Fixing build failure * Addressed review comments * Addressed review comments * Added reverse path for construction graph * Removed unneeded reference * Parts can be purchased through cargo * Fixing merge conflict * Merge conflict resolved * Fixing merge conflict * Code update * Code updates * Increased AI core health and gave it a sell price to fix test fail * Added screen static sprite * Added better support for ghosted AI players plus code tweaks * Various improvements and clean up * Increased purge duration to 60 seconds * Fixed needless complication * Addressed reviewer comments part 1 * Addressed reviewer comments part 2 * Further fixes * Trying lower battery values to see if it fixes the test fail * Adjusted power values again * Addressed review comments * Addressed review comments * Fixed test fail * Fixed bug with endless rebooting. Using rejuvenation on an AI core revives the AI inside. * Added pop up text * Bug fix * Tweaks and fixes * Fixed restoration console not updating when the AI finishes rebooting * Update SharedStationAiSystem.Held.cs --------- Co-authored-by: ScarKy0 --- ...StationAiFixerConsoleBoundUserInterface.cs | 42 ++ ...ationAiFixerConsoleConfirmationDialog.xaml | 22 + ...onAiFixerConsoleConfirmationDialog.xaml.cs | 30 ++ .../StationAi/StationAiFixerConsoleSystem.cs | 24 + .../StationAiFixerConsoleWindow.xaml | 172 ++++++++ .../StationAiFixerConsoleWindow.xaml.cs | 198 +++++++++ .../Silicons/StationAi/StationAiSystem.cs | 6 +- Content.Server/Holopad/HolopadSystem.cs | 48 +- .../StationAi/StationAiFixerConsoleSystem.cs | 64 +++ .../Silicons/StationAi/StationAiSystem.cs | 307 ++++++++++++- .../ContainerSpawnPointSystem.cs | 12 +- .../SharedStationAiFixerConsoleSystem.cs | 411 ++++++++++++++++++ .../SharedStationAiSystem.Customization.cs | 83 +++- .../StationAi/SharedStationAiSystem.Held.cs | 41 +- .../StationAi/SharedStationAiSystem.cs | 181 +++++--- .../StationAi/StationAiCoreComponent.cs | 10 +- .../StationAiCustomizationComponent.cs | 12 + .../StationAiFixerConsoleComponent.cs | 144 ++++++ Resources/Locale/en-US/generic.ftl | 1 + Resources/Locale/en-US/recipes/components.ftl | 1 + Resources/Locale/en-US/recipes/tags.ftl | 1 + .../silicons/station-ai-fixer-console.ftl | 37 ++ .../Locale/en-US/silicons/station-ai.ftl | 4 + .../Catalog/Cargo/cargo_science.yml | 10 + .../Catalog/Fills/Crates/science.yml | 16 + .../Catalog/Fills/Lockers/heads.yml | 1 + Resources/Prototypes/Chat/notifications.yml | 14 + .../Entities/Mobs/Player/silicon.yml | 172 +++++++- .../Devices/Circuitboards/computer.yml | 13 +- .../Devices/Electronics/station_ai_core.yml | 14 + .../Objects/Specific/Robotics/mmi.yml | 2 + .../Weapons/Guns/Turrets/turrets_base.yml | 4 +- .../Machines/Computers/computers.yml | 83 ++++ .../Graphs/structures/station_ai_core.yml | 144 ++++++ .../Recipes/Construction/structures.yml | 13 + Resources/Prototypes/tags.yml | 3 + .../Mobs/Silicon/station_ai.rsi/ai_dead.png | Bin 4405 -> 7247 bytes .../Mobs/Silicon/station_ai.rsi/ai_error.png | Bin 0 -> 312 bytes .../Mobs/Silicon/station_ai.rsi/ai_fuzz.png | Bin 0 -> 7062 bytes .../Silicon/station_ai.rsi/ai_unpowered.png | Bin 0 -> 2273 bytes .../Mobs/Silicon/station_ai.rsi/frame_0.png | Bin 0 -> 593 bytes .../Mobs/Silicon/station_ai.rsi/frame_1.png | Bin 0 -> 611 bytes .../Mobs/Silicon/station_ai.rsi/frame_2.png | Bin 0 -> 611 bytes .../Mobs/Silicon/station_ai.rsi/frame_3.png | Bin 0 -> 763 bytes .../Mobs/Silicon/station_ai.rsi/frame_3b.png | Bin 0 -> 1149 bytes .../Mobs/Silicon/station_ai.rsi/frame_4.png | Bin 0 -> 452 bytes .../Mobs/Silicon/station_ai.rsi/meta.json | 43 ++ .../DamageOverlay_100.png | Bin 0 -> 276 bytes .../DamageOverlay_125.png | Bin 0 -> 331 bytes .../DamageOverlay_150.png | Bin 0 -> 404 bytes .../DamageOverlay_175.png | Bin 0 -> 496 bytes .../DamageOverlay_25.png | Bin 0 -> 101 bytes .../DamageOverlay_50.png | Bin 0 -> 143 bytes .../DamageOverlay_75.png | Bin 0 -> 231 bytes .../Silicon/station_ai_cracks.rsi/meta.json | 39 ++ .../Objects/Devices/ai_card.rsi/dead.png | Bin 0 -> 283 bytes .../Objects/Devices/ai_card.rsi/meta.json | 9 + .../Specific/Robotics/mmi.rsi/meta.json | 3 + .../Specific/Robotics/mmi.rsi/mmi_icon.png | Bin 0 -> 912 bytes .../Machines/computers.rsi/ai-fixer-404.png | Bin 270 -> 621 bytes .../Machines/computers.rsi/ai-fixer-empty.png | Bin 371 -> 909 bytes .../Machines/computers.rsi/ai-fixer-full.png | Bin 594 -> 921 bytes .../computers.rsi/ai-fixer-progress-0.png | Bin 0 -> 2553 bytes .../computers.rsi/ai-fixer-progress-1.png | Bin 0 -> 2577 bytes .../computers.rsi/ai-fixer-progress-2.png | Bin 0 -> 2576 bytes .../computers.rsi/ai-fixer-progress-3.png | Bin 0 -> 2545 bytes .../computers.rsi/ai-fixer-purge-0.png | Bin 0 -> 2319 bytes .../computers.rsi/ai-fixer-purge-1.png | Bin 0 -> 2366 bytes .../computers.rsi/ai-fixer-purge-2.png | Bin 0 -> 2406 bytes .../computers.rsi/ai-fixer-purge-3.png | Bin 0 -> 2373 bytes .../Machines/computers.rsi/ai-fixer.png | Bin 635 -> 569 bytes .../Machines/computers.rsi/meta.json | 242 ++++++++++- 72 files changed, 2548 insertions(+), 128 deletions(-) create mode 100644 Content.Client/Silicons/StationAi/StationAiFixerConsoleBoundUserInterface.cs create mode 100644 Content.Client/Silicons/StationAi/StationAiFixerConsoleConfirmationDialog.xaml create mode 100644 Content.Client/Silicons/StationAi/StationAiFixerConsoleConfirmationDialog.xaml.cs create mode 100644 Content.Client/Silicons/StationAi/StationAiFixerConsoleSystem.cs create mode 100644 Content.Client/Silicons/StationAi/StationAiFixerConsoleWindow.xaml create mode 100644 Content.Client/Silicons/StationAi/StationAiFixerConsoleWindow.xaml.cs create mode 100644 Content.Server/Silicons/StationAi/StationAiFixerConsoleSystem.cs create mode 100644 Content.Shared/Silicons/StationAi/SharedStationAiFixerConsoleSystem.cs create mode 100644 Content.Shared/Silicons/StationAi/StationAiFixerConsoleComponent.cs create mode 100644 Resources/Locale/en-US/silicons/station-ai-fixer-console.ftl create mode 100644 Resources/Prototypes/Entities/Objects/Devices/Electronics/station_ai_core.yml create mode 100644 Resources/Prototypes/Recipes/Construction/Graphs/structures/station_ai_core.yml create mode 100644 Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_error.png create mode 100644 Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_fuzz.png create mode 100644 Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_unpowered.png create mode 100644 Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_0.png create mode 100644 Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_1.png create mode 100644 Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_2.png create mode 100644 Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_3.png create mode 100644 Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_3b.png create mode 100644 Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_4.png create mode 100644 Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_100.png create mode 100644 Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_125.png create mode 100644 Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_150.png create mode 100644 Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_175.png create mode 100644 Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_25.png create mode 100644 Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_50.png create mode 100644 Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_75.png create mode 100644 Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/meta.json create mode 100644 Resources/Textures/Objects/Devices/ai_card.rsi/dead.png create mode 100644 Resources/Textures/Objects/Specific/Robotics/mmi.rsi/mmi_icon.png create mode 100644 Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-progress-0.png create mode 100644 Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-progress-1.png create mode 100644 Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-progress-2.png create mode 100644 Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-progress-3.png create mode 100644 Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-purge-0.png create mode 100644 Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-purge-1.png create mode 100644 Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-purge-2.png create mode 100644 Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-purge-3.png diff --git a/Content.Client/Silicons/StationAi/StationAiFixerConsoleBoundUserInterface.cs b/Content.Client/Silicons/StationAi/StationAiFixerConsoleBoundUserInterface.cs new file mode 100644 index 0000000000..63183c2334 --- /dev/null +++ b/Content.Client/Silicons/StationAi/StationAiFixerConsoleBoundUserInterface.cs @@ -0,0 +1,42 @@ +using Content.Shared.Silicons.StationAi; +using Robust.Client.UserInterface; + +namespace Content.Client.Silicons.StationAi; + +public sealed class StationAiFixerConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey) +{ + private StationAiFixerConsoleWindow? _window; + + protected override void Open() + { + base.Open(); + + _window = this.CreateWindow(); + _window.SetOwner(Owner); + + _window.SendStationAiFixerConsoleMessageAction += SendStationAiFixerConsoleMessage; + _window.OpenConfirmationDialogAction += OpenConfirmationDialog; + } + + public override void Update() + { + base.Update(); + _window?.UpdateState(); + } + + private void OpenConfirmationDialog() + { + if (_window == null) + return; + + _window.ConfirmationDialog?.Close(); + _window.ConfirmationDialog = new StationAiFixerConsoleConfirmationDialog(); + _window.ConfirmationDialog.OpenCentered(); + _window.ConfirmationDialog.SendStationAiFixerConsoleMessageAction += SendStationAiFixerConsoleMessage; + } + + private void SendStationAiFixerConsoleMessage(StationAiFixerConsoleAction action) + { + SendPredictedMessage(new StationAiFixerConsoleMessage(action)); + } +} diff --git a/Content.Client/Silicons/StationAi/StationAiFixerConsoleConfirmationDialog.xaml b/Content.Client/Silicons/StationAi/StationAiFixerConsoleConfirmationDialog.xaml new file mode 100644 index 0000000000..fa61d614e0 --- /dev/null +++ b/Content.Client/Silicons/StationAi/StationAiFixerConsoleConfirmationDialog.xaml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Content.Client/Silicons/StationAi/StationAiFixerConsoleWindow.xaml.cs b/Content.Client/Silicons/StationAi/StationAiFixerConsoleWindow.xaml.cs new file mode 100644 index 0000000000..0c3140a13e --- /dev/null +++ b/Content.Client/Silicons/StationAi/StationAiFixerConsoleWindow.xaml.cs @@ -0,0 +1,198 @@ +using Content.Client.UserInterface.Controls; +using Content.Shared.Lock; +using Content.Shared.Silicons.StationAi; +using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Timing; +using Robust.Shared.Utility; +using System.Numerics; + +namespace Content.Client.Silicons.StationAi; + +[GenerateTypedNameReferences] +public sealed partial class StationAiFixerConsoleWindow : FancyWindow +{ + [Dependency] private readonly IEntityManager _entManager = default!; + [Dependency] private readonly IGameTiming _timing = default!; + + private readonly StationAiFixerConsoleSystem _stationAiFixerConsole; + private readonly SharedStationAiSystem _stationAi; + + private EntityUid? _owner; + + private readonly SpriteSpecifier.Rsi _emptyPortrait = new(new("Mobs/Silicon/station_ai.rsi"), "ai_empty"); + private readonly SpriteSpecifier.Rsi _rebootingPortrait = new(new("Mobs/Silicon/station_ai.rsi"), "ai_fuzz"); + private SpriteSpecifier? _currentPortrait; + + public event Action? SendStationAiFixerConsoleMessageAction; + public event Action? OpenConfirmationDialogAction; + + public StationAiFixerConsoleConfirmationDialog? ConfirmationDialog; + + private readonly Dictionary _statusColors = new() + { + [StationAiState.Empty] = Color.FromHex("#464966"), + [StationAiState.Occupied] = Color.FromHex("#3E6C45"), + [StationAiState.Rebooting] = Color.FromHex("#A5762F"), + [StationAiState.Dead] = Color.FromHex("#BB3232"), + }; + + public StationAiFixerConsoleWindow() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + + _stationAiFixerConsole = _entManager.System(); + _stationAi = _entManager.System(); + + StationAiPortraitTexture.DisplayRect.TextureScale = new Vector2(4f, 4f); + + CancelButton.OnButtonDown += _ => OnSendStationAiFixerConsoleMessage(StationAiFixerConsoleAction.Cancel); + EjectButton.OnButtonDown += _ => OnSendStationAiFixerConsoleMessage(StationAiFixerConsoleAction.Eject); + RepairButton.OnButtonDown += _ => OnSendStationAiFixerConsoleMessage(StationAiFixerConsoleAction.Repair); + PurgeButton.OnButtonDown += _ => OnOpenConfirmationDialog(); + + CancelButton.Label.HorizontalAlignment = HAlignment.Left; + EjectButton.Label.HorizontalAlignment = HAlignment.Left; + RepairButton.Label.HorizontalAlignment = HAlignment.Left; + PurgeButton.Label.HorizontalAlignment = HAlignment.Left; + + CancelButton.Label.Margin = new Thickness(40, 0, 0, 0); + EjectButton.Label.Margin = new Thickness(40, 0, 0, 0); + RepairButton.Label.Margin = new Thickness(40, 0, 0, 0); + PurgeButton.Label.Margin = new Thickness(40, 0, 0, 0); + } + + public void OnSendStationAiFixerConsoleMessage(StationAiFixerConsoleAction action) + { + SendStationAiFixerConsoleMessageAction?.Invoke(action); + } + + public void OnOpenConfirmationDialog() + { + OpenConfirmationDialogAction?.Invoke(); + } + + public override void Close() + { + base.Close(); + ConfirmationDialog?.Close(); + } + + public void SetOwner(EntityUid owner) + { + _owner = owner; + UpdateState(); + } + + public void UpdateState() + { + if (!_entManager.TryGetComponent(_owner, out var stationAiFixerConsole)) + return; + + var ent = (_owner.Value, stationAiFixerConsole); + var isLocked = _entManager.TryGetComponent(_owner, out var lockable) && lockable.Locked; + + var stationAiHolderInserted = _stationAiFixerConsole.IsStationAiHolderInserted((_owner.Value, stationAiFixerConsole)); + var stationAi = stationAiFixerConsole.ActionTarget; + var stationAiState = StationAiState.Empty; + + if (_entManager.TryGetComponent(stationAi, out var stationAiCustomization)) + { + stationAiState = stationAiCustomization.State; + } + + // Set subscreen visibility + LockScreen.Visible = isLocked; + MainControls.Visible = !isLocked && !_stationAiFixerConsole.IsActionInProgress(ent); + ActionProgressScreen.Visible = !isLocked && _stationAiFixerConsole.IsActionInProgress(ent); + + // Update station AI name + StationAiNameLabel.Text = GetStationAiName(stationAi); + StationAiStatusLabel.Text = Loc.GetString("station-ai-fixer-console-window-no-station-ai-status"); + + // Update station AI portrait + var portrait = _emptyPortrait; + var statusColor = _statusColors[StationAiState.Empty]; + + if (stationAiState == StationAiState.Rebooting) + { + portrait = _rebootingPortrait; + StationAiStatusLabel.Text = Loc.GetString("station-ai-fixer-console-window-station-ai-rebooting"); + _statusColors.TryGetValue(StationAiState.Rebooting, out statusColor); + } + else if (stationAi != null && + stationAiCustomization != null && + _stationAi.TryGetCustomizedAppearanceData((stationAi.Value, stationAiCustomization), out var layerData)) + { + StationAiStatusLabel.Text = stationAiState == StationAiState.Occupied ? + Loc.GetString("station-ai-fixer-console-window-station-ai-online") : + Loc.GetString("station-ai-fixer-console-window-station-ai-offline"); + + if (layerData.TryGetValue(stationAiState.ToString(), out var stateData) && stateData is { RsiPath: not null, State: not null }) + { + portrait = new SpriteSpecifier.Rsi(new ResPath(stateData.RsiPath), stateData.State); + } + + _statusColors.TryGetValue(stationAiState, out statusColor); + } + + if (_currentPortrait == null || !_currentPortrait.Equals(portrait)) + { + StationAiPortraitTexture.SetFromSpriteSpecifier(portrait); + _currentPortrait = portrait; + } + + StationAiStatus.PanelOverride = new StyleBoxFlat + { + BackgroundColor = statusColor, + }; + + // Update buttons + EjectButton.Disabled = !stationAiHolderInserted; + RepairButton.Disabled = !stationAiHolderInserted || stationAiState != StationAiState.Dead; + PurgeButton.Disabled = !stationAiHolderInserted || stationAiState == StationAiState.Empty; + + // Update progress bar + if (ActionProgressScreen.Visible) + UpdateProgressBar(ent); + } + + public void UpdateProgressBar(Entity ent) + { + ActionInProgressLabel.Text = ent.Comp.ActionType == StationAiFixerConsoleAction.Repair ? + Loc.GetString("station-ai-fixer-console-window-action-progress-repair") : + Loc.GetString("station-ai-fixer-console-window-action-progress-purge"); + + var fullTimeSpan = ent.Comp.ActionEndTime - ent.Comp.ActionStartTime; + var remainingTimeSpan = ent.Comp.ActionEndTime - _timing.CurTime; + + var time = remainingTimeSpan.TotalSeconds > 60 ? remainingTimeSpan.TotalMinutes : remainingTimeSpan.TotalSeconds; + var units = remainingTimeSpan.TotalSeconds > 60 ? Loc.GetString("generic-minutes") : Loc.GetString("generic-seconds"); + ActionProgressEtaLabel.Text = Loc.GetString("station-ai-fixer-console-window-action-progress-eta", ("time", (int)time), ("units", units)); + + ActionProgressBar.Value = 1f - (float)remainingTimeSpan.Divide(fullTimeSpan); + } + + private string GetStationAiName(EntityUid? uid) + { + if (_entManager.TryGetComponent(uid, out var metadata)) + { + return metadata.EntityName; + } + + return Loc.GetString("station-ai-fixer-console-window-no-station-ai"); + } + + protected override void FrameUpdate(FrameEventArgs args) + { + if (!ActionProgressScreen.Visible) + return; + + if (!_entManager.TryGetComponent(_owner, out var stationAiFixerConsole)) + return; + + UpdateProgressBar((_owner.Value, stationAiFixerConsole)); + } +} diff --git a/Content.Client/Silicons/StationAi/StationAiSystem.cs b/Content.Client/Silicons/StationAi/StationAiSystem.cs index 9b0a9fb7ea..d4a8b9dbd8 100644 --- a/Content.Client/Silicons/StationAi/StationAiSystem.cs +++ b/Content.Client/Silicons/StationAi/StationAiSystem.cs @@ -81,10 +81,10 @@ public sealed partial class StationAiSystem : SharedStationAiSystem if (args.Sprite == null) return; - if (_appearance.TryGetData(entity.Owner, StationAiVisualState.Key, out var layerData, args.Component)) - _sprite.LayerSetData((entity.Owner, args.Sprite), StationAiVisualState.Key, layerData); + if (_appearance.TryGetData(entity.Owner, StationAiVisualLayers.Icon, out var layerData, args.Component)) + _sprite.LayerSetData((entity.Owner, args.Sprite), StationAiVisualLayers.Icon, layerData); - _sprite.LayerSetVisible((entity.Owner, args.Sprite), StationAiVisualState.Key, layerData != null); + _sprite.LayerSetVisible((entity.Owner, args.Sprite), StationAiVisualLayers.Icon, layerData != null); } public override void Shutdown() diff --git a/Content.Server/Holopad/HolopadSystem.cs b/Content.Server/Holopad/HolopadSystem.cs index 884fb3ae71..0cba4824db 100644 --- a/Content.Server/Holopad/HolopadSystem.cs +++ b/Content.Server/Holopad/HolopadSystem.cs @@ -8,6 +8,8 @@ using Content.Shared.Chat.TypingIndicator; using Content.Shared.Holopad; using Content.Shared.IdentityManagement; using Content.Shared.Labels.Components; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Systems; using Content.Shared.Power; using Content.Shared.Silicons.StationAi; using Content.Shared.Speech; @@ -38,6 +40,7 @@ public sealed class HolopadSystem : SharedHolopadSystem [Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly PvsOverrideSystem _pvs = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; private float _updateTimer = 1.0f; private const float UpdateTime = 1.0f; @@ -77,6 +80,8 @@ public sealed class HolopadSystem : SharedHolopadSystem SubscribeLocalEvent(OnAiRemove); SubscribeLocalEvent(OnParentChanged); SubscribeLocalEvent(OnPowerChanged); + SubscribeLocalEvent(OnMobStateChanged); + } #region: Holopad UI bound user interface messages @@ -226,7 +231,7 @@ public sealed class HolopadSystem : SharedHolopadSystem if (!_stationAiSystem.TryGetHeld((receiver, receiverStationAiCore), out var insertedAi)) continue; - if (_userInterfaceSystem.TryOpenUi(receiverUid, HolopadUiKey.AiRequestWindow, insertedAi)) + if (_userInterfaceSystem.TryOpenUi(receiverUid, HolopadUiKey.AiRequestWindow, insertedAi.Value)) LinkHolopadToUser(entity, args.Actor); } @@ -446,6 +451,17 @@ public sealed class HolopadSystem : SharedHolopadSystem UpdateHolopadControlLockoutStartTime(entity); } + private void OnMobStateChanged(Entity ent, ref MobStateChangedEvent args) + { + if (!HasComp(ent)) + return; + + foreach (var holopad in ent.Comp.LinkedHolopads) + { + ShutDownHolopad(holopad); + } + } + #endregion public override void Update(float frameTime) @@ -605,25 +621,23 @@ public sealed class HolopadSystem : SharedHolopadSystem if (entity.Comp.Hologram != null) DeleteHologram(entity.Comp.Hologram.Value, entity); - if (entity.Comp.User != null) + // Check if the associated holopad user is an AI + if (HasComp(entity.Comp.User) && + _stationAiSystem.TryGetCore(entity.Comp.User.Value, out var stationAiCore)) { - // Check if the associated holopad user is an AI - if (TryComp(entity.Comp.User, out var stationAiHeld) && - _stationAiSystem.TryGetCore(entity.Comp.User.Value, out var stationAiCore)) - { - // Return the AI eye to free roaming - _stationAiSystem.SwitchRemoteEntityMode(stationAiCore, true); + // Return the AI eye to free roaming + _stationAiSystem.SwitchRemoteEntityMode(stationAiCore, true); - // If the AI core is still broadcasting, end its calls - if (entity.Owner != stationAiCore.Owner && - TryComp(stationAiCore, out var stationAiCoreTelephone) && - _telephoneSystem.IsTelephoneEngaged((stationAiCore.Owner, stationAiCoreTelephone))) - { - _telephoneSystem.EndTelephoneCalls((stationAiCore.Owner, stationAiCoreTelephone)); - } + // If the AI core is still broadcasting, end its calls + if (TryComp(stationAiCore, out var stationAiCoreTelephone) && + _telephoneSystem.IsTelephoneEngaged((stationAiCore.Owner, stationAiCoreTelephone))) + { + _telephoneSystem.EndTelephoneCalls((stationAiCore.Owner, stationAiCoreTelephone)); } - - UnlinkHolopadFromUser(entity, entity.Comp.User.Value); + } + else + { + UnlinkHolopadFromUser(entity, entity.Comp.User); } Dirty(entity); diff --git a/Content.Server/Silicons/StationAi/StationAiFixerConsoleSystem.cs b/Content.Server/Silicons/StationAi/StationAiFixerConsoleSystem.cs new file mode 100644 index 0000000000..cc6f54c446 --- /dev/null +++ b/Content.Server/Silicons/StationAi/StationAiFixerConsoleSystem.cs @@ -0,0 +1,64 @@ +using Content.Shared.Silicons.StationAi; +using Content.Server.EUI; +using Content.Server.Ghost; +using Content.Server.Mind; +using Robust.Shared.Audio.Systems; +using Robust.Server.Player; +using Content.Shared.Popups; + +namespace Content.Server.Silicons.StationAi; + +public sealed partial class StationAiFixerConsoleSystem : SharedStationAiFixerConsoleSystem +{ + [Dependency] private readonly EuiManager _eui = default!; + [Dependency] private readonly IPlayerManager _player = default!; + [Dependency] private readonly MindSystem _mind = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + + protected override void FinalizeAction(Entity ent) + { + if (IsActionInProgress(ent) && ent.Comp.ActionTarget != null) + { + switch (ent.Comp.ActionType) + { + case StationAiFixerConsoleAction.Repair: + + // Send message to disembodied player that they are being revived + if (_mind.TryGetMind(ent.Comp.ActionTarget.Value, out _, out var mind) && + mind.IsVisitingEntity && + _player.TryGetSessionById(mind.UserId, out var session)) + { + _eui.OpenEui(new ReturnToBodyEui(mind, _mind, _player), session); + _popup.PopupEntity(Loc.GetString("station-ai-fixer-console-repair-finished"), ent); + } + else + { + _popup.PopupEntity(Loc.GetString("station-ai-fixer-console-repair-successful"), ent); + } + + // TODO: make predicted once a user is not required + if (ent.Comp.RepairFinishedSound != null) + { + _audio.PlayPvs(ent.Comp.RepairFinishedSound, ent); + } + + break; + + case StationAiFixerConsoleAction.Purge: + + _popup.PopupEntity(Loc.GetString("station-ai-fixer-console-purge-successful"), ent); + + // TODO: make predicted once a user is not required + if (ent.Comp.PurgeFinishedSound != null) + { + _audio.PlayPvs(ent.Comp.PurgeFinishedSound, ent); + } + + break; + } + } + + base.FinalizeAction(ent); + } +} diff --git a/Content.Server/Silicons/StationAi/StationAiSystem.cs b/Content.Server/Silicons/StationAi/StationAiSystem.cs index 45b3dda431..73c5670c1e 100644 --- a/Content.Server/Silicons/StationAi/StationAiSystem.cs +++ b/Content.Server/Silicons/StationAi/StationAiSystem.cs @@ -1,10 +1,34 @@ using Content.Server.Chat.Systems; +using Content.Server.Construction; +using Content.Server.Destructible; +using Content.Server.Ghost; +using Content.Server.Mind; +using Content.Server.Power.Components; +using Content.Server.Power.EntitySystems; +using Content.Server.Roles; +using Content.Server.Spawners.Components; +using Content.Server.Spawners.EntitySystems; +using Content.Server.Station.Systems; +using Content.Shared.Alert; using Content.Shared.Chat.Prototypes; +using Content.Shared.Containers.ItemSlots; +using Content.Shared.Damage; +using Content.Shared.Destructible; using Content.Shared.DeviceNetwork.Components; +using Content.Shared.DoAfter; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Systems; +using Content.Shared.Popups; +using Content.Shared.Power.Components; +using Content.Shared.Rejuvenate; +using Content.Shared.Roles; using Content.Shared.Silicons.StationAi; +using Content.Shared.Speech.Components; using Content.Shared.StationAi; using Content.Shared.Turrets; using Content.Shared.Weapons.Ranged.Events; +using Robust.Server.Containers; +using Robust.Shared.Containers; using Robust.Shared.Map.Components; using Robust.Shared.Player; using Robust.Shared.Prototypes; @@ -16,19 +40,300 @@ public sealed class StationAiSystem : SharedStationAiSystem { [Dependency] private readonly EntityLookupSystem _lookup = default!; [Dependency] private readonly SharedTransformSystem _xforms = default!; + [Dependency] private readonly ContainerSystem _container = default!; + [Dependency] private readonly MindSystem _mind = default!; + [Dependency] private readonly RoleSystem _roles = default!; + [Dependency] private readonly ItemSlotsSystem _slots = default!; + [Dependency] private readonly GhostSystem _ghost = default!; + [Dependency] private readonly AlertsSystem _alerts = default!; + [Dependency] private readonly DestructibleSystem _destructible = default!; + [Dependency] private readonly BatterySystem _battery = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly SharedPopupSystem _popups = default!; + [Dependency] private readonly StationSystem _station = default!; + [Dependency] private readonly StationJobsSystem _stationJobs = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; private readonly HashSet> _stationAiCores = new(); + private readonly ProtoId _turretIsAttackingChatNotificationPrototype = "TurretIsAttacking"; private readonly ProtoId _aiWireSnippedChatNotificationPrototype = "AiWireSnipped"; + private readonly ProtoId _aiLosingPowerChatNotificationPrototype = "AiLosingPower"; + private readonly ProtoId _aiCriticalPowerChatNotificationPrototype = "AiCriticalPower"; + + private readonly ProtoId _stationAiJob = "StationAi"; + private readonly EntProtoId _stationAiBrain = "StationAiBrain"; + + private readonly ProtoId _batteryAlert = "BorgBattery"; + private readonly ProtoId _damageAlert = "BorgHealth"; public override void Initialize() { base.Initialize(); + SubscribeLocalEvent(AfterConstructionChangeEntity); + SubscribeLocalEvent(OnContainerSpawn); + SubscribeLocalEvent(OnApcBatteryChanged); + SubscribeLocalEvent(OnChargeChanged); + SubscribeLocalEvent(OnDamageChanged); + SubscribeLocalEvent(OnDestruction); + SubscribeLocalEvent>(OnDoAfterAttempt); + SubscribeLocalEvent(OnRejuvenate); + SubscribeLocalEvent(OnExpandICChatRecipients); SubscribeLocalEvent(OnAmmoShot); } + private void AfterConstructionChangeEntity(Entity ent, ref AfterConstructionChangeEntityEvent args) + { + if (!_container.TryGetContainer(ent, StationAiCoreComponent.BrainContainer, out var container) || + container.Count == 0) + { + return; + } + + var brain = container.ContainedEntities[0]; + + if (_mind.TryGetMind(brain, out var mindId, out var mind)) + { + // Found an existing mind to transfer into the AI core + var aiBrain = Spawn(_stationAiBrain, Transform(ent.Owner).Coordinates); + _roles.MindAddJobRole(mindId, mind, false, _stationAiJob); + _mind.TransferTo(mindId, aiBrain); + + if (!TryComp(ent, out var targetHolder) || + !_slots.TryInsert(ent, targetHolder.Slot, aiBrain, null)) + { + QueueDel(aiBrain); + } + } + + // TODO: We should consider keeping the borg brain inside the AI core. + // When the core is destroyed, the station AI can be transferred into the brain, + // then dropped on the ground. The deceased AI can then be revived later, + // instead of being lost forever. + QueueDel(brain); + } + + private void OnContainerSpawn(Entity ent, ref ContainerSpawnEvent args) + { + // Ensure that players that recently joined the round will spawn + // into an AI core that has a full battery and full integrity. + if (TryComp(ent, out var battery)) + { + _battery.SetCharge(ent, battery.MaxCharge); + } + + if (TryComp(ent, out var damageable)) + { + _damageable.SetAllDamage(ent, damageable, 0); + } + } + + protected override void OnAiInsert(Entity ent, ref EntInsertedIntoContainerMessage args) + { + base.OnAiInsert(ent, ref args); + + UpdateBatteryAlert(ent); + UpdateCoreIntegrityAlert(ent); + UpdateDamagedAccent(ent); + } + + protected override void OnAiRemove(Entity ent, ref EntRemovedFromContainerMessage args) + { + base.OnAiRemove(ent, ref args); + + _alerts.ClearAlert(args.Entity, _batteryAlert); + _alerts.ClearAlert(args.Entity, _damageAlert); + + if (TryComp(args.Entity, out var accent)) + { + accent.OverrideChargeLevel = null; + accent.OverrideTotalDamage = null; + accent.DamageAtMaxCorruption = null; + } + } + + protected override void OnMobStateChanged(Entity ent, ref MobStateChangedEvent args) + { + if (args.NewMobState != MobState.Alive) + { + SetStationAiState(ent, StationAiState.Dead); + return; + } + + var state = StationAiState.Rebooting; + + if (_mind.TryGetMind(ent, out var _, out var mind) && !mind.IsVisitingEntity) + { + state = StationAiState.Occupied; + } + + if (TryGetCore(ent, out var aiCore) && aiCore.Comp != null) + { + var aiCoreEnt = (aiCore.Owner, aiCore.Comp); + + if (SetupEye(aiCoreEnt)) + AttachEye(aiCoreEnt); + } + + SetStationAiState(ent, state); + } + + private void OnDestruction(Entity ent, ref DestructionEventArgs args) + { + var station = _station.GetOwningStation(ent); + + if (station == null) + return; + + if (!HasComp(ent)) + return; + + // If the destroyed core could act as a player spawn point, + // reduce the number of available AI jobs by one + _stationJobs.TryAdjustJobSlot(station.Value, _stationAiJob, -1, false, true); + } + + private void OnApcBatteryChanged(Entity ent, ref ApcPowerReceiverBatteryChangedEvent args) + { + if (!args.Enabled) + return; + + if (!TryGetHeld((ent.Owner, ent.Comp), out var held)) + return; + + var ev = new ChatNotificationEvent(_aiLosingPowerChatNotificationPrototype, ent); + RaiseLocalEvent(held.Value, ref ev); + } + + private void OnChargeChanged(Entity entity, ref ChargeChangedEvent args) + { + UpdateBatteryAlert(entity); + UpdateDamagedAccent(entity); + } + + private void OnDamageChanged(Entity entity, ref DamageChangedEvent args) + { + UpdateCoreIntegrityAlert(entity); + UpdateDamagedAccent(entity); + } + + private void UpdateDamagedAccent(Entity ent) + { + if (!TryGetHeld((ent.Owner, ent.Comp), out var held)) + return; + + if (!TryComp(held, out var accent)) + return; + + if (TryComp(ent, out var battery)) + accent.OverrideChargeLevel = battery.CurrentCharge / battery.MaxCharge; + + if (TryComp(ent, out var damageable)) + accent.OverrideTotalDamage = damageable.TotalDamage; + + if (TryComp(ent, out var destructible)) + accent.DamageAtMaxCorruption = _destructible.DestroyedAt(ent, destructible); + + Dirty(held.Value, accent); + } + + private void UpdateBatteryAlert(Entity ent) + { + if (!TryComp(ent, out var battery)) + return; + + if (!TryGetHeld((ent.Owner, ent.Comp), out var held)) + return; + + if (!_proto.TryIndex(_batteryAlert, out var proto)) + return; + + var chargePercent = battery.CurrentCharge / battery.MaxCharge; + var chargeLevel = Math.Round(chargePercent * proto.MaxSeverity); + + _alerts.ShowAlert(held.Value, _batteryAlert, (short)Math.Clamp(chargeLevel, 0, proto.MaxSeverity)); + + if (TryComp(ent, out var apcBattery) && + apcBattery.Enabled && + chargePercent < 0.2) + { + var ev = new ChatNotificationEvent(_aiCriticalPowerChatNotificationPrototype, ent); + RaiseLocalEvent(held.Value, ref ev); + } + } + + private void UpdateCoreIntegrityAlert(Entity ent) + { + if (!TryComp(ent, out var damageable)) + return; + + if (!TryComp(ent, out var destructible)) + return; + + if (!TryGetHeld((ent.Owner, ent.Comp), out var held)) + return; + + if (!_proto.TryIndex(_damageAlert, out var proto)) + return; + + var damagePercent = damageable.TotalDamage / _destructible.DestroyedAt(ent, destructible); + var damageLevel = Math.Round(damagePercent.Float() * proto.MaxSeverity); + + _alerts.ShowAlert(held.Value, _damageAlert, (short)Math.Clamp(damageLevel, 0, proto.MaxSeverity)); + } + + private void OnDoAfterAttempt(Entity ent, ref DoAfterAttemptEvent args) + { + if (TryGetHeld((ent.Owner, ent.Comp), out _)) + return; + + // Prevent AIs from being uploaded into an unpowered or broken AI core. + + if (TryComp(ent, out var apcPower) && !apcPower.Powered) + { + _popups.PopupEntity(Loc.GetString("station-ai-has-no-power-for-upload"), ent, args.Event.User); + args.Cancel(); + } + else if (TryComp(ent, out var destructible) && destructible.IsBroken) + { + _popups.PopupEntity(Loc.GetString("station-ai-is-too-damaged-for-upload"), ent, args.Event.User); + args.Cancel(); + } + } + + public override void KillHeldAi(Entity ent) + { + base.KillHeldAi(ent); + + if (TryGetHeld((ent.Owner, ent.Comp), out var held) && + _mind.TryGetMind(held.Value, out var mindId, out var mind)) + { + _ghost.OnGhostAttempt(mindId, canReturnGlobal: true, mind: mind); + RemComp(held.Value); + } + + ClearEye(ent); + } + + private void OnRejuvenate(Entity ent, ref RejuvenateEvent args) + { + if (TryGetHeld((ent.Owner, ent.Comp), out var held)) + { + _mobState.ChangeMobState(held.Value, MobState.Alive); + EnsureComp(held.Value); + } + + if (TryComp(ent, out var holder)) + { + _appearance.SetData(ent, StationAiVisuals.Broken, false); + UpdateAppearance((ent, holder)); + } + } + private void OnExpandICChatRecipients(ExpandICChatRecipientsEvent ev) { var xformQuery = GetEntityQuery(); @@ -147,7 +452,7 @@ public sealed class StationAiSystem : SharedStationAiSystem if (!TryGetHeld((stationAiCore, stationAiCore.Comp), out var insertedAi)) continue; - hashSet.Add(insertedAi); + hashSet.Add(insertedAi.Value); } return hashSet; diff --git a/Content.Server/Spawners/EntitySystems/ContainerSpawnPointSystem.cs b/Content.Server/Spawners/EntitySystems/ContainerSpawnPointSystem.cs index 1a592b9929..1763d5f6a1 100644 --- a/Content.Server/Spawners/EntitySystems/ContainerSpawnPointSystem.cs +++ b/Content.Server/Spawners/EntitySystems/ContainerSpawnPointSystem.cs @@ -1,8 +1,7 @@ -using Content.Server.GameTicking; +using Content.Server.GameTicking; using Content.Server.Spawners.Components; using Content.Server.Station.Systems; using Content.Shared.Preferences; -using Content.Shared.Roles; using Robust.Server.Containers; using Robust.Shared.Containers; using Robust.Shared.Prototypes; @@ -87,6 +86,9 @@ public sealed class ContainerSpawnPointSystem : EntitySystem if (!_container.Insert(args.SpawnResult.Value, container, containerXform: xform)) continue; + var ev = new ContainerSpawnEvent(args.SpawnResult.Value); + RaiseLocalEvent(uid, ref ev); + return; } @@ -94,3 +96,9 @@ public sealed class ContainerSpawnPointSystem : EntitySystem args.SpawnResult = null; } } + +/// +/// Raised on a container when a player is spawned into it. +/// +[ByRefEvent] +public record struct ContainerSpawnEvent(EntityUid Player); diff --git a/Content.Shared/Silicons/StationAi/SharedStationAiFixerConsoleSystem.cs b/Content.Shared/Silicons/StationAi/SharedStationAiFixerConsoleSystem.cs new file mode 100644 index 0000000000..1abafd7cb2 --- /dev/null +++ b/Content.Shared/Silicons/StationAi/SharedStationAiFixerConsoleSystem.cs @@ -0,0 +1,411 @@ +using Content.Shared.Administration.Logs; +using Content.Shared.Containers.ItemSlots; +using Content.Shared.Database; +using Content.Shared.Examine; +using Content.Shared.Lock; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Components; +using Content.Shared.Mobs.Systems; +using Content.Shared.Power; +using Robust.Shared.Containers; +using Robust.Shared.Timing; +using System.Diagnostics.CodeAnalysis; + +namespace Content.Shared.Silicons.StationAi; + +/// +/// This system is used to handle the actions of AI Restoration Consoles. +/// These consoles can be used to revive dead station AIs, or destroy them. +/// +public abstract partial class SharedStationAiFixerConsoleSystem : EntitySystem +{ + [Dependency] private readonly SharedUserInterfaceSystem _userInterface = default!; + [Dependency] private readonly ItemSlotsSystem _itemSlots = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnInserted); + SubscribeLocalEvent(OnRemoved); + SubscribeLocalEvent(OnLockToggle); + SubscribeLocalEvent(OnMessage); + SubscribeLocalEvent(OnPowerChanged); + SubscribeLocalEvent(OnExamined); + + SubscribeLocalEvent(OnStationAiCustomizationStateChanged); + } + + private void OnInserted(Entity ent, ref EntInsertedIntoContainerMessage args) + { + if (args.Container.ID != ent.Comp.StationAiHolderSlot) + return; + + if (TryGetTarget(ent, out var target)) + { + ent.Comp.ActionTarget = target; + Dirty(ent); + } + + UpdateAppearance(ent); + } + + private void OnRemoved(Entity ent, ref EntRemovedFromContainerMessage args) + { + if (args.Container.ID != ent.Comp.StationAiHolderSlot) + return; + + ent.Comp.ActionTarget = null; + + StopAction(ent); + } + + private void OnLockToggle(Entity ent, ref LockToggledEvent args) + { + if (_userInterface.TryGetOpenUi(ent.Owner, StationAiFixerConsoleUiKey.Key, out var bui)) + bui.Update(); + } + + private void OnMessage(Entity ent, ref StationAiFixerConsoleMessage args) + { + if (TryComp(ent, out var lockable) && lockable.Locked) + return; + + switch (args.Action) + { + case StationAiFixerConsoleAction.Eject: + EjectStationAiHolder(ent, args.Actor); + break; + case StationAiFixerConsoleAction.Repair: + RepairStationAi(ent, args.Actor); + break; + case StationAiFixerConsoleAction.Purge: + PurgeStationAi(ent, args.Actor); + break; + case StationAiFixerConsoleAction.Cancel: + CancelAction(ent, args.Actor); + break; + } + } + + private void OnPowerChanged(Entity ent, ref PowerChangedEvent args) + { + if (args.Powered) + return; + + StopAction(ent); + } + + private void OnExamined(Entity ent, ref ExaminedEvent args) + { + var message = TryGetStationAiHolder(ent, out var holder) ? + Loc.GetString("station-ai-fixer-console-examination-station-ai-holder-present", ("holder", Name(holder.Value))) : + Loc.GetString("station-ai-fixer-console-examination-station-ai-holder-absent"); + + args.PushMarkup(message); + } + + private void OnStationAiCustomizationStateChanged(Entity ent, ref StationAiCustomizationStateChanged args) + { + if (_container.TryGetOuterContainer(ent, Transform(ent), out var outerContainer) && + TryComp(outerContainer.Owner, out var stationAiFixerConsole)) + { + UpdateAppearance((outerContainer.Owner, stationAiFixerConsole)); + } + } + + private void EjectStationAiHolder(Entity ent, EntityUid user) + { + if (!TryComp(ent, out var slots)) + return; + + if (!_itemSlots.TryGetSlot(ent, ent.Comp.StationAiHolderSlot, out var holderSlot, slots)) + return; + + if (_itemSlots.TryEjectToHands(ent, holderSlot, user, true)) + _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(user):user} ejected a station AI holder from AI restoration console ({ToPrettyString(ent.Owner)})"); + } + + private void RepairStationAi(Entity ent, EntityUid user) + { + if (ent.Comp.ActionTarget == null) + return; + + _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(user):user} started a repair of {ToPrettyString(ent.Comp.ActionTarget)} using an AI restoration console ({ToPrettyString(ent.Owner)})"); + StartAction(ent, StationAiFixerConsoleAction.Repair); + } + + private void PurgeStationAi(Entity ent, EntityUid user) + { + if (ent.Comp.ActionTarget == null) + return; + + _adminLogger.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(user):user} started a purge of {ToPrettyString(ent.Comp.ActionTarget)} using {ToPrettyString(ent.Owner)}"); + StartAction(ent, StationAiFixerConsoleAction.Purge); + } + + private void CancelAction(Entity ent, EntityUid user) + { + if (!IsActionInProgress(ent)) + return; + + _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(user):user} canceled operation involving {ToPrettyString(ent.Comp.ActionTarget)} and {ToPrettyString(ent.Owner)} ({ent.Comp.ActionType} action)"); + StopAction(ent); + } + + /// + /// Initiates an action upon a target entity by the specified console. + /// + /// The console. + /// The action to be enacted on the target. + private void StartAction(Entity ent, StationAiFixerConsoleAction actionType) + { + if (IsActionInProgress(ent)) + { + StopAction(ent); + } + + if (IsTargetValid(ent, actionType)) + { + var duration = actionType == StationAiFixerConsoleAction.Repair ? + ent.Comp.RepairDuration : + ent.Comp.PurgeDuration; + + ent.Comp.ActionType = actionType; + ent.Comp.ActionStartTime = _timing.CurTime; + ent.Comp.ActionEndTime = _timing.CurTime + duration; + ent.Comp.CurrentActionStage = 0; + Dirty(ent); + } + + UpdateAppearance(ent); + } + + /// + /// Updates the current action being conducted by the specified console. + /// + /// The console. + private void UpdateAction(Entity ent) + { + if (IsActionInProgress(ent)) + { + if (ent.Comp.ActionTarget == null) + { + StopAction(ent); + return; + } + + if (_timing.CurTime >= ent.Comp.ActionEndTime) + { + FinalizeAction(ent); + return; + } + + var currentStage = CalculateActionStage(ent); + + if (currentStage != ent.Comp.CurrentActionStage) + { + ent.Comp.CurrentActionStage = currentStage; + Dirty(ent); + } + } + + UpdateAppearance(ent); + } + + /// + /// Terminates any action being conducted by the specified console. + /// + /// The console. + private void StopAction(Entity ent) + { + ent.Comp.ActionType = StationAiFixerConsoleAction.None; + Dirty(ent); + + UpdateAppearance(ent); + } + + /// + /// Finalizes the action being conducted by the specified console + /// (i.e., repairing or purging a target). + /// + /// The console. + protected virtual void FinalizeAction(Entity ent) + { + if (IsActionInProgress(ent) && ent.Comp.ActionTarget != null) + { + if (ent.Comp.ActionType == StationAiFixerConsoleAction.Repair) + { + _mobState.ChangeMobState(ent.Comp.ActionTarget.Value, MobState.Alive); + } + else if (ent.Comp.ActionType == StationAiFixerConsoleAction.Purge && + TryGetStationAiHolder(ent, out var holder)) + { + _container.RemoveEntity(holder.Value, ent.Comp.ActionTarget.Value, force: true); + PredictedQueueDel(ent.Comp.ActionTarget); + + ent.Comp.ActionTarget = null; + Dirty(ent); + } + } + + StopAction(ent); + } + + /// + /// Updates the appearance of the specified console based on its current state. + /// + /// The console. + private void UpdateAppearance(Entity ent) + { + if (!TryComp(ent, out var appearance)) + return; + + if (IsActionInProgress(ent)) + { + var currentStage = ent.Comp.ActionType + ent.Comp.CurrentActionStage.ToString(); + + if (!_appearance.TryGetData(ent, StationAiFixerConsoleVisuals.Key, out string oldStage, appearance) || + oldStage != currentStage) + { + _appearance.SetData(ent, StationAiFixerConsoleVisuals.Key, currentStage, appearance); + } + + return; + } + + var target = ent.Comp.ActionTarget; + var state = StationAiState.Empty; + + if (TryComp(target, out var customization) && !EntityManager.IsQueuedForDeletion(target.Value)) + { + state = customization.State; + } + + _appearance.SetData(ent, StationAiFixerConsoleVisuals.Key, state.ToString(), appearance); + } + + /// + /// Calculates the current stage of any in-progress actions. + /// + /// The console. + /// The current stage. + private int CalculateActionStage(Entity ent) + { + var completionPercentage = (_timing.CurTime - ent.Comp.ActionStartTime) / (ent.Comp.ActionEndTime - ent.Comp.ActionStartTime); + + return (int)(completionPercentage * ent.Comp.ActionStageCount); + } + + /// + /// Try to find a valid target being stored inside the specified console. + /// + /// The console. + /// The found target. + /// True if a valid target was found. + public bool TryGetTarget(Entity ent, [NotNullWhen(true)] out EntityUid? target) + { + target = null; + + if (!TryGetStationAiHolder(ent, out var holder)) + return false; + + if (!_container.TryGetContainer(holder.Value, ent.Comp.StationAiMindSlot, out var stationAiMindSlot) || stationAiMindSlot.Count == 0) + return false; + + var stationAi = stationAiMindSlot.ContainedEntities[0]; + + if (!HasComp(stationAi)) + return false; + + target = stationAi; + + return !EntityManager.IsQueuedForDeletion(target.Value); + } + + /// + /// Try to find a station AI holder being stored inside the specified console. + /// + /// The console. + /// The found holder. + /// True if a valid holder was found. + public bool TryGetStationAiHolder(Entity ent, [NotNullWhen(true)] out EntityUid? holder) + { + holder = null; + + if (!_container.TryGetContainer(ent, ent.Comp.StationAiHolderSlot, out var holderContainer) || + holderContainer.Count == 0) + { + return false; + } + + holder = holderContainer.ContainedEntities[0]; + + return true; + } + + /// + /// Determines if the specified console can act upon its action target. + /// + /// The console. + /// The action to be enacted on the target. + /// True, if the target is valid for the specified console action. + public bool IsTargetValid(Entity ent, StationAiFixerConsoleAction actionType) + { + if (ent.Comp.ActionTarget == null) + return false; + + if (actionType == StationAiFixerConsoleAction.Purge) + return true; + + if (actionType == StationAiFixerConsoleAction.Repair && + _mobState.IsDead(ent.Comp.ActionTarget.Value)) + { + return true; + } + + return false; + } + + /// + /// Returns whether an station AI holder is inserted into the specified console. + /// + /// The console. + /// True if a station AI holder is inserted. + public bool IsStationAiHolderInserted(Entity ent) + { + return TryGetStationAiHolder(ent, out var _); + } + + /// + /// Returns whether the specified console has an action in progress. + /// + /// The console. + /// Ture, if an action is in progress. + public bool IsActionInProgress(Entity ent) + { + return ent.Comp.ActionType != StationAiFixerConsoleAction.None; + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = AllEntityQuery(); + + while (query.MoveNext(out var uid, out var stationAiFixerConsole)) + { + var ent = (uid, stationAiFixerConsole); + + if (!IsActionInProgress(ent)) + continue; + + UpdateAction(ent); + } + } +} diff --git a/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Customization.cs b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Customization.cs index 7a5131c9a1..4361b86d12 100644 --- a/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Customization.cs +++ b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Customization.cs @@ -1,5 +1,9 @@ using Content.Shared.Holopad; +using Content.Shared.Mobs; +using Robust.Shared.Player; using Robust.Shared.Prototypes; +using Robust.Shared.Utility; +using System.Diagnostics.CodeAnalysis; namespace Content.Shared.Silicons.StationAi; @@ -8,9 +12,15 @@ public abstract partial class SharedStationAiSystem private ProtoId _stationAiCoreCustomGroupProtoId = "StationAiCoreIconography"; private ProtoId _stationAiHologramCustomGroupProtoId = "StationAiHolograms"; + private readonly SpriteSpecifier.Rsi _stationAiRebooting = new(new ResPath("Mobs/Silicon/station_ai.rsi"), "ai_fuzz"); + private void InitializeCustomization() { SubscribeLocalEvent(OnStationAiCustomization); + + SubscribeLocalEvent(OnPlayerAttached); + SubscribeLocalEvent(OnPlayerDetached); + SubscribeLocalEvent(OnMobStateChanged); } private void OnStationAiCustomization(Entity entity, ref StationAiCustomizationMessage args) @@ -29,17 +39,53 @@ public abstract partial class SharedStationAiSystem stationAiCustomization.ProtoIds[args.GroupProtoId] = args.CustomizationProtoId; - Dirty(held, stationAiCustomization); + Dirty(held.Value, stationAiCustomization); // Update hologram if (groupPrototype.Category == StationAiCustomizationType.Hologram) - UpdateHolographicAvatar((held, stationAiCustomization)); + UpdateHolographicAvatar((held.Value, stationAiCustomization)); // Update core iconography if (groupPrototype.Category == StationAiCustomizationType.CoreIconography && TryComp(entity, out var stationAiHolder)) UpdateAppearance((entity, stationAiHolder)); } + private void OnPlayerAttached(Entity ent, ref PlayerAttachedEvent args) + { + var state = _mobState.IsDead(ent) ? StationAiState.Dead : StationAiState.Occupied; + SetStationAiState(ent, state); + } + + private void OnPlayerDetached(Entity ent, ref PlayerDetachedEvent args) + { + var state = _mobState.IsDead(ent) ? StationAiState.Dead : StationAiState.Rebooting; + SetStationAiState(ent, state); + } + + protected virtual void OnMobStateChanged(Entity ent, ref MobStateChangedEvent args) + { + var state = (args.NewMobState == MobState.Dead) ? StationAiState.Dead : StationAiState.Rebooting; + SetStationAiState(ent, state); + } + + protected void SetStationAiState(Entity ent, StationAiState state) + { + if (ent.Comp.State != state) + { + ent.Comp.State = state; + Dirty(ent); + + var ev = new StationAiCustomizationStateChanged(state); + RaiseLocalEvent(ent, ref ev); + } + + if (_containers.TryGetContainingContainer(ent.Owner, out var container) && + TryComp(container.Owner, out var holder)) + { + UpdateAppearance((container.Owner, holder)); + } + } + private void UpdateHolographicAvatar(Entity entity) { if (!TryComp(entity, out var avatar)) @@ -62,21 +108,36 @@ public abstract partial class SharedStationAiSystem { var stationAi = GetInsertedAI(entity); - if (stationAi == null) + if (!TryComp(stationAi, out var stationAiCustomization) || + !TryGetCustomizedAppearanceData((stationAi.Value, stationAiCustomization), out var layerData) || + !layerData.TryGetValue(state.ToString(), out var stateData)) { - _appearance.RemoveData(entity.Owner, StationAiVisualState.Key); return; } - if (!TryComp(stationAi, out var stationAiCustomization) || - !stationAiCustomization.ProtoIds.TryGetValue(_stationAiCoreCustomGroupProtoId, out var protoId) || - !_protoManager.Resolve(protoId, out var prototype) || - !prototype.LayerData.TryGetValue(state.ToString(), out var layerData)) + // This data is handled manually in the client StationAiSystem + _appearance.SetData(entity.Owner, StationAiVisualLayers.Icon, stateData); + } + + /// + /// Returns a dictionary containing the station AI's appearance for different states. + /// + /// The station AI. + /// The apperance data, indexed by possible AI states. + /// True if the apperance data was found. + public bool TryGetCustomizedAppearanceData(Entity entity, [NotNullWhen(true)] out Dictionary? layerData) + { + layerData = null; + + if (!entity.Comp.ProtoIds.TryGetValue(_stationAiCoreCustomGroupProtoId, out var protoId) || + !_protoManager.Resolve(protoId, out var prototype) || + prototype.LayerData.Count == 0) { - return; + return false; } - // This data is handled manually in the client StationAiSystem - _appearance.SetData(entity.Owner, StationAiVisualState.Key, layerData); + layerData = prototype.LayerData; + + return true; } } diff --git a/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Held.cs b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Held.cs index 1c9c57dccf..c82e92b451 100644 --- a/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Held.cs +++ b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Held.cs @@ -5,6 +5,7 @@ using Content.Shared.Popups; using Content.Shared.Verbs; using Robust.Shared.Serialization; using Robust.Shared.Utility; +using System.Diagnostics.CodeAnalysis; namespace Content.Shared.Silicons.StationAi; @@ -26,6 +27,7 @@ public abstract partial class SharedStationAiSystem SubscribeLocalEvent(OnHeldInteraction); SubscribeLocalEvent(OnHeldRelay); SubscribeLocalEvent(OnCoreJump); + SubscribeLocalEvent(OnTryGetIdentityShortInfo); } @@ -49,20 +51,23 @@ public abstract partial class SharedStationAiSystem if (!TryGetCore(ent.Owner, out var core) || core.Comp?.RemoteEntity == null) return; - _xforms.DropNextTo(core.Comp.RemoteEntity.Value, core.Owner) ; + _xforms.DropNextTo(core.Comp.RemoteEntity.Value, core.Owner); } /// - /// Tries to get the entity held in the AI core using StationAiCore. + /// Tries to find an AI being held in by an entity using . /// - public bool TryGetHeld(Entity entity, out EntityUid held) + /// The station AI holder. + /// The found AI. + /// True if an AI is found. + public bool TryGetHeld(Entity entity, [NotNullWhen(true)] out EntityUid? held) { held = EntityUid.Invalid; if (!Resolve(entity.Owner, ref entity.Comp)) return false; - if (!_containers.TryGetContainer(entity.Owner, StationAiCoreComponent.Container, out var container) || + if (!_containers.TryGetContainer(entity.Owner, StationAiHolderComponent.Container, out var container) || container.ContainedEntities.Count == 0) return false; @@ -70,26 +75,32 @@ public abstract partial class SharedStationAiSystem return true; } + /// - /// Tries to get the entity held in the AI using StationAiHolder. + /// Tries to find an AI being held in by an entity using . /// - public bool TryGetHeld(Entity entity, out EntityUid held) + /// The station AI core. + /// The found AI. + /// True if an AI is found. + public bool TryGetHeld(Entity entity, [NotNullWhen(true)] out EntityUid? held) { - TryComp(entity.Owner, out var stationAiCore); + held = null; - return TryGetHeld((entity.Owner, stationAiCore), out held); + return TryComp(entity.Owner, out var holder) && + TryGetHeld((entity, holder), out held); } + /// + /// Tries to find the station AI core holding an AI. + /// + /// The AI. + /// The found AI core. + /// True if an AI core is found. public bool TryGetCore(EntityUid entity, out Entity core) { - var xform = Transform(entity); - var meta = MetaData(entity); - var ent = new Entity(entity, xform, meta); - - if (!_containers.TryGetContainingContainer(ent, out var container) || + if (!_containers.TryGetContainingContainer(entity, out var container) || container.ID != StationAiCoreComponent.Container || - !TryComp(container.Owner, out StationAiCoreComponent? coreComp) || - coreComp.RemoteEntity == null) + !TryComp(container.Owner, out StationAiCoreComponent? coreComp)) { core = (EntityUid.Invalid, null); return false; diff --git a/Content.Shared/Silicons/StationAi/SharedStationAiSystem.cs b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.cs index 1a3d4c788e..e109c23fe6 100644 --- a/Content.Shared/Silicons/StationAi/SharedStationAiSystem.cs +++ b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.cs @@ -4,6 +4,7 @@ using Content.Shared.Administration.Managers; using Content.Shared.Chat.Prototypes; using Content.Shared.Containers.ItemSlots; using Content.Shared.Database; +using Content.Shared.Destructible; using Content.Shared.Doors.Systems; using Content.Shared.DoAfter; using Content.Shared.Electrocution; @@ -11,11 +12,14 @@ using Content.Shared.Intellicard; using Content.Shared.Interaction; using Content.Shared.Item.ItemToggle; using Content.Shared.Mind; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Systems; using Content.Shared.Movement.Components; using Content.Shared.Movement.Systems; using Content.Shared.Popups; using Content.Shared.Power; using Content.Shared.Power.EntitySystems; +using Content.Shared.Repairable; using Content.Shared.StationAi; using Content.Shared.Verbs; using Robust.Shared.Audio.Systems; @@ -28,36 +32,36 @@ using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.Timing; using Robust.Shared.Utility; -using System.Diagnostics.CodeAnalysis; namespace Content.Shared.Silicons.StationAi; public abstract partial class SharedStationAiSystem : EntitySystem { - [Dependency] private readonly ISharedAdminManager _admin = default!; - [Dependency] private readonly IGameTiming _timing = default!; - [Dependency] private readonly INetManager _net = default!; - [Dependency] private readonly ItemSlotsSystem _slots = default!; - [Dependency] private readonly ItemToggleSystem _toggles = default!; - [Dependency] private readonly ActionBlockerSystem _blocker = default!; - [Dependency] private readonly MetaDataSystem _metadata = default!; - [Dependency] private readonly SharedAirlockSystem _airlocks = default!; - [Dependency] private readonly SharedAppearanceSystem _appearance = default!; - [Dependency] private readonly SharedAudioSystem _audio = default!; - [Dependency] private readonly SharedContainerSystem _containers = default!; - [Dependency] private readonly SharedDoorSystem _doors = default!; - [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; - [Dependency] private readonly SharedElectrocutionSystem _electrify = default!; - [Dependency] private readonly SharedEyeSystem _eye = default!; + [Dependency] private readonly ISharedAdminManager _admin = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly ItemSlotsSystem _slots = default!; + [Dependency] private readonly ItemToggleSystem _toggles = default!; + [Dependency] private readonly ActionBlockerSystem _blocker = default!; + [Dependency] private readonly MetaDataSystem _metadata = default!; + [Dependency] private readonly SharedAirlockSystem _airlocks = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedContainerSystem _containers = default!; + [Dependency] private readonly SharedDoorSystem _doors = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly SharedElectrocutionSystem _electrify = default!; + [Dependency] private readonly SharedEyeSystem _eye = default!; [Dependency] protected readonly SharedMapSystem Maps = default!; - [Dependency] private readonly SharedMindSystem _mind = default!; - [Dependency] private readonly SharedMoverController _mover = default!; - [Dependency] private readonly SharedPopupSystem _popup = default!; - [Dependency] private readonly SharedPowerReceiverSystem PowerReceiver = default!; - [Dependency] private readonly SharedTransformSystem _xforms = default!; - [Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!; - [Dependency] private readonly StationAiVisionSystem _vision = default!; - [Dependency] private readonly IPrototypeManager _protoManager = default!; + [Dependency] private readonly SharedMindSystem _mind = default!; + [Dependency] private readonly SharedMoverController _mover = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedPowerReceiverSystem PowerReceiver = default!; + [Dependency] private readonly SharedTransformSystem _xforms = default!; + [Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!; + [Dependency] private readonly StationAiVisionSystem _vision = default!; + [Dependency] private readonly IPrototypeManager _protoManager = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; // StationAiHeld is added to anything inside of an AI core. // StationAiHolder indicates it can hold an AI positronic brain (e.g. holocard / core). @@ -72,8 +76,6 @@ public abstract partial class SharedStationAiSystem : EntitySystem private static readonly EntProtoId DefaultAi = "StationAiBrain"; private readonly ProtoId _downloadChatNotificationPrototype = "IntellicardDownload"; - private const float MaxVisionMultiplier = 5f; - public override void Initialize() { base.Initialize(); @@ -102,10 +104,12 @@ public abstract partial class SharedStationAiSystem : EntitySystem SubscribeLocalEvent(OnAiInsert); SubscribeLocalEvent(OnAiRemove); - SubscribeLocalEvent(OnAiMapInit); SubscribeLocalEvent(OnAiShutdown); SubscribeLocalEvent(OnCorePower); SubscribeLocalEvent>(OnCoreVerbs); + + SubscribeLocalEvent(OnBroken); + SubscribeLocalEvent(OnRepaired); } private void OnCoreVerbs(Entity ent, ref GetVerbsEvent args) @@ -137,7 +141,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem args.Verbs.Add(new Verb() { Text = Loc.GetString("station-ai-customization-menu"), - Act = () => _uiSystem.TryOpenUi(ent.Owner, StationAiCustomizationUiKey.Key, insertedAi), + Act = () => _uiSystem.TryOpenUi(ent.Owner, StationAiCustomizationUiKey.Key, insertedAi.Value), Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/emotes.svg.192dpi.png")), }); } @@ -271,8 +275,8 @@ public abstract partial class SharedStationAiSystem : EntitySystem if (!TryComp(args.Used, out IntellicardComponent? intelliComp)) return; - var cardHasAi = _slots.CanEject(ent.Owner, args.User, ent.Comp.Slot); - var coreHasAi = _slots.CanEject(args.Target.Value, args.User, targetHolder.Slot); + var cardHasAi = ent.Comp.Slot.Item != null; + var coreHasAi = targetHolder.Slot.Item != null; if (cardHasAi && coreHasAi) { @@ -290,7 +294,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem if (TryGetHeld((args.Target.Value, targetHolder), out var held)) { var ev = new ChatNotificationEvent(_downloadChatNotificationPrototype, args.Used, args.User); - RaiseLocalEvent(held, ref ev); + RaiseLocalEvent(held.Value, ref ev); } var doAfterArgs = new DoAfterArgs(EntityManager, args.User, cardHasAi ? intelliComp.UploadTime : intelliComp.DownloadTime, new IntellicardDoAfterEvent(), args.Target, ent.Owner) @@ -298,7 +302,8 @@ public abstract partial class SharedStationAiSystem : EntitySystem BreakOnDamage = true, BreakOnMove = true, NeedHand = true, - BreakOnDropItem = true + BreakOnDropItem = true, + AttemptFrequency = AttemptFrequency.EveryTick, }; _doAfter.TryStartDoAfter(doAfterArgs); @@ -327,7 +332,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem private void OnHolderMapInit(Entity ent, ref MapInitEvent args) { - UpdateAppearance(ent.Owner); + UpdateAppearance((ent.Owner, ent.Comp)); } private void OnAiShutdown(Entity ent, ref ComponentShutdown args) @@ -342,24 +347,32 @@ public abstract partial class SharedStationAiSystem : EntitySystem private void OnCorePower(Entity ent, ref PowerChangedEvent args) { - // TODO: I think in 13 they just straightup die so maybe implement that - if (args.Powered) - { - if (!SetupEye(ent)) - return; - - AttachEye(ent); - } - else + if (!args.Powered) { - ClearEye(ent); + KillHeldAi(ent); } } - private void OnAiMapInit(Entity ent, ref MapInitEvent args) + private void OnBroken(Entity ent, ref BreakageEventArgs args) { - SetupEye(ent); - AttachEye(ent); + KillHeldAi(ent); + + if (TryComp(ent, out var appearance)) + _appearance.SetData(ent, StationAiVisuals.Broken, true, appearance); + } + + private void OnRepaired(Entity ent, ref RepairedEvent args) + { + if (TryComp(ent, out var appearance)) + _appearance.SetData(ent, StationAiVisuals.Broken, false, appearance); + } + + public virtual void KillHeldAi(Entity ent) + { + if (TryGetHeld((ent.Owner, ent.Comp), out var held)) + { + _mobState.ChangeMobState(held.Value, MobState.Dead); + } } public void SwitchRemoteEntityMode(Entity entity, bool isRemote) @@ -395,7 +408,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem _eye.SetDrawFov(user.Value, !isRemote); } - private bool SetupEye(Entity ent, EntityCoordinates? coords = null) + protected bool SetupEye(Entity ent, EntityCoordinates? coords = null) { if (_net.IsClient) return false; @@ -420,7 +433,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem return true; } - private void ClearEye(Entity ent) + protected void ClearEye(Entity ent) { if (_net.IsClient) return; @@ -428,9 +441,16 @@ public abstract partial class SharedStationAiSystem : EntitySystem QueueDel(ent.Comp.RemoteEntity); ent.Comp.RemoteEntity = null; Dirty(ent); + + if (TryGetHeld((ent, ent.Comp), out var held) && + TryComp(held, out EyeComponent? eyeComp)) + { + _eye.SetDrawFov(held.Value, true, eyeComp); + _eye.SetTarget(held.Value, null, eyeComp); + } } - private void AttachEye(Entity ent) + protected void AttachEye(Entity ent) { if (ent.Comp.RemoteEntity == null) return; @@ -467,7 +487,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem return container.ContainedEntities[0]; } - private void OnAiInsert(Entity ent, ref EntInsertedIntoContainerMessage args) + protected virtual void OnAiInsert(Entity ent, ref EntInsertedIntoContainerMessage args) { if (args.Container.ID != StationAiCoreComponent.Container) return; @@ -475,17 +495,21 @@ public abstract partial class SharedStationAiSystem : EntitySystem if (_timing.ApplyingState) return; + ClearEye(ent); ent.Comp.Remote = true; - SetupEye(ent); // Just so text and the likes works properly _metadata.SetEntityName(ent.Owner, MetaData(args.Entity).EntityName); - AttachEye(ent); + if (SetupEye(ent)) + AttachEye(ent); } - private void OnAiRemove(Entity ent, ref EntRemovedFromContainerMessage args) + protected virtual void OnAiRemove(Entity ent, ref EntRemovedFromContainerMessage args) { + if (args.Container.ID != StationAiCoreComponent.Container) + return; + if (_timing.ApplyingState) return; @@ -506,26 +530,49 @@ public abstract partial class SharedStationAiSystem : EntitySystem ClearEye(ent); } - private void UpdateAppearance(Entity entity) + protected void UpdateAppearance(Entity entity) { if (!Resolve(entity.Owner, ref entity.Comp, false)) return; - // Todo: when AIs can die, add a check to see if the AI is in the 'dead' state var state = StationAiState.Empty; - if (_containers.TryGetContainer(entity.Owner, StationAiHolderComponent.Container, out var container) && container.Count > 0) - state = StationAiState.Occupied; + // Get what visual state the held AI holder is in + if (TryGetHeld(entity, out var stationAi) && + TryComp(stationAi, out var customization)) + { + state = customization.State; + } + + // If the entity is not an AI core, let generic visualizers handle the appearance update + if (!TryComp(entity, out var stationAiCore)) + { + _appearance.SetData(entity.Owner, StationAiVisualLayers.Icon, state); + return; + } - // If the entity is a station AI core, attempt to customize its appearance - if (TryComp(entity, out var stationAiCore)) + // The AI core is empty + if (state == StationAiState.Empty) { - CustomizeAppearance((entity, stationAiCore), state); + _appearance.RemoveData(entity.Owner, StationAiVisualLayers.Icon); + return; + } + + // The AI core is rebooting + if (state == StationAiState.Rebooting) + { + var rebootingData = new PrototypeLayerData() + { + RsiPath = _stationAiRebooting.RsiPath.ToString(), + State = _stationAiRebooting.RsiState, + }; + + _appearance.SetData(entity.Owner, StationAiVisualLayers.Icon, rebootingData); return; } - // Otherwise let generic visualizers handle the appearance update - _appearance.SetData(entity.Owner, StationAiVisualState.Key, state); + // Otherwise attempt to set the AI core's appearance + CustomizeAppearance((entity, stationAiCore), state); } public virtual bool SetVisionEnabled(Entity entity, bool enabled, bool announce = false) @@ -573,15 +620,16 @@ public sealed partial class JumpToCoreEvent : InstantActionEvent public sealed partial class IntellicardDoAfterEvent : SimpleDoAfterEvent; [Serializable, NetSerializable] -public enum StationAiVisualState : byte +public enum StationAiVisualLayers : byte { - Key, + Base, + Icon, } [Serializable, NetSerializable] -public enum StationAiSpriteState : byte +public enum StationAiVisuals : byte { - Key, + Broken, } [Serializable, NetSerializable] @@ -590,5 +638,6 @@ public enum StationAiState : byte Empty, Occupied, Dead, + Rebooting, Hologram, } diff --git a/Content.Shared/Silicons/StationAi/StationAiCoreComponent.cs b/Content.Shared/Silicons/StationAi/StationAiCoreComponent.cs index a795c9eda6..ec3f308104 100644 --- a/Content.Shared/Silicons/StationAi/StationAiCoreComponent.cs +++ b/Content.Shared/Silicons/StationAi/StationAiCoreComponent.cs @@ -38,11 +38,19 @@ public sealed partial class StationAiCoreComponent : Component [DataField(readOnly: true)] public EntProtoId? PhysicalEntityProto = "StationAiHoloLocal"; + /// + /// Name of the container slot that holds the inhabiting AI's mind + /// public const string Container = "station_ai_mind_slot"; + + /// + /// Name of the container slot that holds the 'brain' used to construct the AI core + /// + public const string BrainContainer = "station_ai_brain_slot"; } /// -/// This event is raised on a station AI 'eye' that is being replaced with a new one +/// This event is raised on a station AI 'eye' that is being replaced with a new one /// /// The entity UID of the replacement entity [ByRefEvent] diff --git a/Content.Shared/Silicons/StationAi/StationAiCustomizationComponent.cs b/Content.Shared/Silicons/StationAi/StationAiCustomizationComponent.cs index a2b713edfe..520b7f98c5 100644 --- a/Content.Shared/Silicons/StationAi/StationAiCustomizationComponent.cs +++ b/Content.Shared/Silicons/StationAi/StationAiCustomizationComponent.cs @@ -15,6 +15,12 @@ public sealed partial class StationAiCustomizationComponent : Component /// [DataField, AutoNetworkedField] public Dictionary, ProtoId> ProtoIds = new(); + + /// + /// The current visual state of the associated entity. + /// + [DataField, AutoNetworkedField] + public StationAiState State = StationAiState.Occupied; } /// @@ -33,6 +39,12 @@ public sealed class StationAiCustomizationMessage : BoundUserInterfaceMessage } } +/// +/// Event raised when the station AI customization visual state changes +/// +[ByRefEvent] +public record StationAiCustomizationStateChanged(StationAiState NewState); + /// /// Key for opening the station AI customization UI /// diff --git a/Content.Shared/Silicons/StationAi/StationAiFixerConsoleComponent.cs b/Content.Shared/Silicons/StationAi/StationAiFixerConsoleComponent.cs new file mode 100644 index 0000000000..0b872b1b05 --- /dev/null +++ b/Content.Shared/Silicons/StationAi/StationAiFixerConsoleComponent.cs @@ -0,0 +1,144 @@ +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; + +namespace Content.Shared.Silicons.StationAi; + +/// +/// This component holds data needed for AI Restoration Consoles to function. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause] +[Access(typeof(SharedStationAiFixerConsoleSystem))] +public sealed partial class StationAiFixerConsoleComponent : Component +{ + /// + /// Determines how long a repair takes to complete (in seconds). + /// + [DataField] + public TimeSpan RepairDuration = TimeSpan.FromSeconds(30); + + /// + /// Determines how long a purge takes to complete (in seconds). + /// + [DataField] + public TimeSpan PurgeDuration = TimeSpan.FromSeconds(30); + + /// + /// The number of stages that a console action (repair or purge) + /// progresses through before it concludes. Each stage has an equal + /// duration. The appearance data of the entity is updated with + /// each new stage reached. + /// + [DataField] + public int ActionStageCount = 4; + + /// + /// The time at which the current action commenced. + /// + [DataField, AutoNetworkedField, AutoPausedField] + public TimeSpan ActionStartTime = TimeSpan.FromSeconds(0); + + /// + /// The time at which the current action will end. + /// + [DataField, AutoNetworkedField, AutoPausedField] + public TimeSpan ActionEndTime = TimeSpan.FromSeconds(0); + + /// + /// The type of action that is currently in progress. + /// + [DataField, AutoNetworkedField] + public StationAiFixerConsoleAction ActionType = StationAiFixerConsoleAction.None; + + /// + /// The target of the current action. + /// + [DataField, AutoNetworkedField] + public EntityUid? ActionTarget; + + /// + /// The current stage of the action in progress. + /// + [DataField, AutoNetworkedField] + public int CurrentActionStage; + + /// + /// Sound clip that is played when a repair is completed. + /// + [DataField] + public SoundSpecifier? RepairFinishedSound = new SoundPathSpecifier("/Audio/Items/beep.ogg"); + + /// + /// Sound clip that is played when a repair is completed. + /// + [DataField] + public SoundSpecifier? PurgeFinishedSound = new SoundPathSpecifier("/Audio/Machines/beep.ogg"); + + /// + /// The name of the console slot which is used to contain station AI holders. + /// + [DataField] + public string StationAiHolderSlot = "station_ai_holder"; + + /// + /// The name of the station AI holder slot which actually contains the station AI. + /// + [DataField] + public string StationAiMindSlot = "station_ai_mind_slot"; +} + +/// +/// Message sent from the server to the client to update the UI of AI Restoration Consoles. +/// +[Serializable, NetSerializable] +public sealed class StationAiFixerConsoleBoundUserInterfaceState : BoundUserInterfaceState; + +/// +/// Message sent from the client to the server to handle player UI inputs from AI Restoration Consoles. +/// +[Serializable, NetSerializable] +public sealed class StationAiFixerConsoleMessage : BoundUserInterfaceMessage +{ + public StationAiFixerConsoleAction Action; + + public StationAiFixerConsoleMessage(StationAiFixerConsoleAction action) + { + Action = action; + } +} + +/// +/// Potential actions that AI Restoration Consoles can perform. +/// +[Serializable, NetSerializable] +public enum StationAiFixerConsoleAction +{ + None, + Eject, + Repair, + Purge, + Cancel, +} + +/// +/// Appearance keys for AI Restoration Consoles. +/// +[Serializable, NetSerializable] +public enum StationAiFixerConsoleVisuals : byte +{ + Key, + ActionProgress, + MobState, + RepairProgress, + PurgeProgress, +} + +/// +/// Interactable UI key for AI Restoration Consoles. +/// +[Serializable, NetSerializable] +public enum StationAiFixerConsoleUiKey +{ + Key, +} + diff --git a/Resources/Locale/en-US/generic.ftl b/Resources/Locale/en-US/generic.ftl index cdca0f2493..c963f0e0fb 100644 --- a/Resources/Locale/en-US/generic.ftl +++ b/Resources/Locale/en-US/generic.ftl @@ -14,6 +14,7 @@ generic-invalid = invalid generic-hours = hours generic-minutes = minutes +generic-seconds = seconds generic-playtime-title = Playtime diff --git a/Resources/Locale/en-US/recipes/components.ftl b/Resources/Locale/en-US/recipes/components.ftl index 236097532c..d67c661ecd 100644 --- a/Resources/Locale/en-US/recipes/components.ftl +++ b/Resources/Locale/en-US/recipes/components.ftl @@ -5,3 +5,4 @@ construction-graph-component-second-flash = second flash construction-graph-component-power-cell = power cell construction-graph-component-apc-electronics = APC electronics construction-graph-component-payload-trigger = trigger +construction-graph-component-borg-brain = MMI or positronic brain diff --git a/Resources/Locale/en-US/recipes/tags.ftl b/Resources/Locale/en-US/recipes/tags.ftl index 34eadc37d8..96c0729881 100644 --- a/Resources/Locale/en-US/recipes/tags.ftl +++ b/Resources/Locale/en-US/recipes/tags.ftl @@ -103,6 +103,7 @@ construction-graph-tag-ripley-peripherals-control-module = ripley peripherals co construction-graph-tag-door-electronics-circuit-board = door electronics circuit board construction-graph-tag-firelock-electronics-circuit-board = firelock electronics circuit board construction-graph-tag-conveyor-belt-assembly = conveyor belt assembly +construction-graph-tag-station-ai-core-electronics = station AI core electronics # tools construction-graph-tag-multitool = a multitool diff --git a/Resources/Locale/en-US/silicons/station-ai-fixer-console.ftl b/Resources/Locale/en-US/silicons/station-ai-fixer-console.ftl new file mode 100644 index 0000000000..a6940f2306 --- /dev/null +++ b/Resources/Locale/en-US/silicons/station-ai-fixer-console.ftl @@ -0,0 +1,37 @@ +# System +station-ai-fixer-console-is-locked = The console is locked. +station-ai-fixer-console-station-ai-holder-required = Only AI storage units can be inserted into the console. +station-ai-fixer-console-examination-station-ai-holder-present = There is {INDEFINITE($holder)} [color=cyan]{$holder}[/color] inserted in the console. +station-ai-fixer-console-examination-station-ai-holder-absent = There is an unoccupied slot for an [color=cyan]AI storage unit[/color]. +station-ai-fixer-console-repair-finished = Repair complete. Attempting to reboot AI... +station-ai-fixer-console-repair-successful = Repair complete. AI successfully rebooted. +station-ai-fixer-console-purge-successful = Purge complete. AI successfully deleted. + +# UI +station-ai-fixer-console-window = AI restoration console +station-ai-fixer-console-window-no-station-ai = No AI detected +station-ai-fixer-console-window-no-station-ai-status = Waiting +station-ai-fixer-console-window-station-ai-online = Online +station-ai-fixer-console-window-station-ai-offline = Offline +station-ai-fixer-console-window-station-ai-rebooting = Rebooting... + +station-ai-fixer-console-window-controls-locked = Controls locked + +station-ai-fixer-console-window-station-ai-eject = Eject storage unit +station-ai-fixer-console-window-station-ai-repair = Run repair tool +station-ai-fixer-console-window-station-ai-purge = Initiate AI purge + +station-ai-fixer-console-window-action-progress-repair = Repair in progress... +station-ai-fixer-console-window-action-progress-purge = Purge in progress... +station-ai-fixer-console-window-action-progress-eta = Time remaining: {$time} {$units} + +station-ai-fixer-console-window-flavor-left = Lock this console when it is not in use +station-ai-fixer-console-window-flavor-right = v4.0.4 + +station-ai-fixer-console-window-continue-action = Continue +station-ai-fixer-console-window-cancel-action = Cancel + +station-ai-fixer-console-window-purge-warning-title = Initiating AI purge +station-ai-fixer-console-window-purge-warning-1 = You are about to permanently delete an artifical intelligence. +station-ai-fixer-console-window-purge-warning-2 = Once this operation is complete, the intelligence will be gone and cannot be revived. +station-ai-fixer-console-window-purge-warning-3 = Do you wish to proceed? \ No newline at end of file diff --git a/Resources/Locale/en-US/silicons/station-ai.ftl b/Resources/Locale/en-US/silicons/station-ai.ftl index 442782f9a1..11c51ddea4 100644 --- a/Resources/Locale/en-US/silicons/station-ai.ftl +++ b/Resources/Locale/en-US/silicons/station-ai.ftl @@ -4,6 +4,10 @@ wire-name-ai-vision-light = AIV wire-name-ai-act-light = AIA station-ai-takeover = AI takeover station-ai-eye-name = AI eye - {$name} +station-ai-has-no-power-for-upload = Upload failed - the AI core is unpowered. +station-ai-is-too-damaged-for-upload = Upload failed - the AI core must be repaired. +station-ai-core-losing-power = Your AI core is now running on reserve battery power. +station-ai-core-critical-power = Your AI core is critically low on power. External power must be re-established or severe data corruption may occur! # Radial actions ai-open = Open actions diff --git a/Resources/Prototypes/Catalog/Cargo/cargo_science.yml b/Resources/Prototypes/Catalog/Cargo/cargo_science.yml index aa428b7d55..cefcca5fab 100644 --- a/Resources/Prototypes/Catalog/Cargo/cargo_science.yml +++ b/Resources/Prototypes/Catalog/Cargo/cargo_science.yml @@ -47,3 +47,13 @@ cost: 2000 category: cargoproduct-category-name-science group: market + +- type: cargoProduct + id: StationAiCore + icon: + sprite: Mobs/Silicon/station_ai.rsi + state: frame_4 + product: CrateStationAiCore + cost: 10000 + category: cargoproduct-category-name-science + group: market \ No newline at end of file diff --git a/Resources/Prototypes/Catalog/Fills/Crates/science.yml b/Resources/Prototypes/Catalog/Fills/Crates/science.yml index 6adf5942a4..4ed07d607f 100644 --- a/Resources/Prototypes/Catalog/Fills/Crates/science.yml +++ b/Resources/Prototypes/Catalog/Fills/Crates/science.yml @@ -24,3 +24,19 @@ - id: CrewMonitoringServerFlatpack - id: CrewMonitoringComputerFlatpack amount: 3 + +- type: entity + id: CrateStationAiCore + parent: CrateScienceSecure + name: station AI core crate + description: Contains the components for constructing a station AI core. Positronic brain not included. Requires Science access to open. + components: + - type: StorageFill + contents: + - id: StationAiCoreElectronics + - id: SheetPlasteel1 + amount: 4 + - id: CableApcStack1 + amount: 1 + - id: SheetRGlass1 + amount: 2 \ No newline at end of file diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml b/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml index 6b1efddad1..ae904f7f95 100644 --- a/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml +++ b/Resources/Prototypes/Catalog/Fills/Lockers/heads.yml @@ -282,6 +282,7 @@ - id: ProtolatheMachineCircuitboard - id: ResearchComputerCircuitboard - id: CargoRequestScienceComputerCircuitboard + - id: StationAiFixerCircuitboard - id: RubberStampRd # Hardsuit table, used for suit storage as well diff --git a/Resources/Prototypes/Chat/notifications.yml b/Resources/Prototypes/Chat/notifications.yml index c1aee755c6..cea67fa0ee 100644 --- a/Resources/Prototypes/Chat/notifications.yml +++ b/Resources/Prototypes/Chat/notifications.yml @@ -19,3 +19,17 @@ color: Pink nextDelay: 12 notifyBySource: true + +- type: chatNotification + id: AiLosingPower + message: station-ai-core-losing-power + sound: /Audio/Misc/notice2.ogg + color: Orange + nextDelay: 30 + +- type: chatNotification + id: AiCriticalPower + message: station-ai-core-critical-power + sound: /Audio/Effects/alert.ogg + color: Red + nextDelay: 120 \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml index e18100ab8a..845971be35 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml @@ -144,10 +144,12 @@ - type: Appearance - type: GenericVisualizer visuals: - enum.StationAiVisualState.Key: + enum.StationAiVisualLayers.Icon: unshaded: Empty: { state: empty } Occupied: { state: full } + Rebooting: { state: dead } + Dead: { state: dead } - type: Intellicard - type: entity @@ -161,6 +163,7 @@ - state: ai shader: unshaded +# Empty AI core - type: entity id: PlayerStationAiEmpty name: AI Core @@ -178,23 +181,69 @@ blacklist: tags: - GhostOnlyWarp + - type: Fixtures + fixtures: + fix1: + shape: + !type:PhysShapeAabb + bounds: "-0.5,-0.5,0.5,0.5" + mask: + - MachineMask + layer: + - MachineLayer + density: 200 - type: ContainerComp proto: AiHeld container: station_ai_mind_slot + - type: Damageable + damageModifierSet: StrongMetallic + - type: Repairable + doAfterDelay: 10 + allowSelfRepair: false - type: Destructible thresholds: - trigger: !type:DamageTrigger - damage: 100 + damage: 400 behaviors: - !type:PlaySoundBehavior sound: collection: MetalBreak - !type:DoActsBehavior - acts: [ "Destruction" ] + acts: [ "Breakage" ] + - trigger: + !type:DamageTrigger + damage: 800 + behaviors: + - !type:PlaySoundBehavior + sound: + collection: MetalBreak + - !type:SpawnEntitiesBehavior + spawn: + ShardGlassReinforced: + min: 1 + max: 2 + SheetPlasteel: + min: 2 + max: 2 + - !type:DoActsBehavior + acts: ["Destruction"] + - type: DamageVisuals + thresholds: [25, 50, 75, 100, 125, 150, 175] + damageDivisor: 4 + trackAllDamage: true + damageOverlay: + sprite: Mobs/Silicon/station_ai_cracks.rsi - type: ApcPowerReceiver - powerLoad: 1000 - needsPower: false + powerLoad: 500 + - type: ExtensionCableReceiver + - type: Battery + maxCharge: 300000 + startingCharge: 300000 + - type: ApcPowerReceiverBattery + idleLoad: 500 + batteryRechargeRate: 1000 + batteryRechargeEfficiency: 0 # Setting to zero until the light flickering issue associated with dynamic power loads is fixed - type: StationAiCore - type: StationAiVision - type: InteractionOutline @@ -204,12 +253,26 @@ layers: - state: base - state: ai_empty + map: ["enum.StationAiVisualLayers.Base"] shader: unshaded - state: ai - map: ["enum.StationAiVisualState.Key"] + map: ["enum.StationAiVisualLayers.Icon"] shader: unshaded visible: false + - state: ai_unpowered + map: ["enum.PowerDeviceVisualLayers.Powered"] + visible: false - type: Appearance + - type: GenericVisualizer + visuals: + enum.PowerDeviceVisuals.Powered: + enum.PowerDeviceVisualLayers.Powered: + False: { visible: true } + True: { visible: false } + enum.StationAiVisuals.Broken: + enum.StationAiVisualLayers.Base: + False: { state: ai_empty } + True: { state: ai_error } - type: InteractionPopup interactSuccessString: petting-success-station-ai interactFailureString: petting-failure-station-ai @@ -234,7 +297,22 @@ type: HolopadBoundUserInterface enum.StationAiCustomizationUiKey.Key: type: StationAiCustomizationBoundUserInterface - + - type: Construction + graph: StationAiCore + node: stationAiCore + - type: ContainerContainer + containers: + board: !type:Container + station_ai_brain_slot: !type:Container + station_ai_mind_slot: !type:ContainerSlot + showEnts: true + - type: ContainerFill + containers: + board: + - StationAiCoreElectronics + - type: StaticPrice + price: 5000 + # The job-ready version of an AI spawn. - type: entity id: PlayerStationAi @@ -245,6 +323,77 @@ containerId: station_ai_mind_slot job: StationAi +# The station AI core assembly +- type: entity + parent: BaseStructure + id: PlayerStationAiAssembly + name: AI Core Assembly + description: An unfinished computer core for housing an artifical intelligence. + components: + - type: Anchorable + flags: + - Anchorable + - type: Rotatable + - type: Sprite + snapCardinals: true + sprite: Mobs/Silicon/station_ai.rsi + layers: + - state: frame_0 + map: [ "enum.ConstructionVisuals.Layer" ] + - type: Appearance + - type: GenericVisualizer + visuals: + enum.ConstructionVisuals.Key: + enum.ConstructionVisuals.Layer: + frame: { state: frame_0 } + frameWithElectronics: { state: frame_1 } + frameWithSecuredElectronics: { state: frame_2 } + frameWithWires: { state: frame_3 } + frameWithBrain: { state: frame_3b } + frameWithBrainFinished: { state: frame_4 } + frameWithoutBrainFinished: { state: frame_4 } + - type: InteractionOutline + - type: Fixtures + fixtures: + fix1: + shape: + !type:PhysShapeAabb + bounds: "-0.5,-0.5,0.5,0.5" + mask: + - MachineMask + layer: + - MachineLayer + density: 200 + - type: Damageable + damageModifierSet: StrongMetallic + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 400 + behaviors: + - !type:PlaySoundBehavior + sound: + collection: MetalBreak + - !type:SpawnEntitiesBehavior + spawn: + SheetPlasteel: + min: 2 + max: 4 + - !type:EmptyContainersBehaviour + containers: + - station_ai_brain_slot + - board + - !type:DoActsBehavior + acts: ["Destruction"] + - type: Construction + graph: StationAiCore + node: frame + - type: ContainerContainer + containers: + board: !type:Container + station_ai_brain_slot: !type:Container + # The actual brain inside the core - type: entity id: StationAiBrain @@ -254,8 +403,6 @@ - type: Sprite # Once it's in a core it's pretty much an abstract entity at that point. visible: false - - type: BlockMovement - blockInteraction: false - type: SiliconLawProvider laws: Crewsimov - type: SiliconLawBound @@ -277,9 +424,16 @@ drawFov: false - type: Examiner - type: InputMover + - type: BlockMovement + blockInteraction: false + - type: GhostOnMove + mustBeDead: true - type: Speech speechVerb: Robotic speechSounds: Borg + - type: DamagedSiliconAccent + startPowerCorruptionAtCharIdx: 4 + maxPowerCorruptionAtCharIdx: 20 - type: Tag tags: - HideContextMenu diff --git a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml index 459030d8a9..8c90308417 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/computer.yml @@ -554,9 +554,20 @@ parent: BaseComputerCircuitboard id: StationAiUploadCircuitboard name: AI upload console board - description: A computer printed circuit board for a AI upload console. + description: A computer printed circuit board for an AI upload console. components: - type: Sprite state: cpu_science - type: ComputerBoard prototype: StationAiUploadComputer + +- type: entity + parent: BaseComputerCircuitboard + id: StationAiFixerCircuitboard + name: AI restoration console + description: A computer printed circuit board for an AI restoration console console. + components: + - type: Sprite + state: cpu_science + - type: ComputerBoard + prototype: StationAiFixerComputer \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Objects/Devices/Electronics/station_ai_core.yml b/Resources/Prototypes/Entities/Objects/Devices/Electronics/station_ai_core.yml new file mode 100644 index 0000000000..637d7e6a54 --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Devices/Electronics/station_ai_core.yml @@ -0,0 +1,14 @@ +- type: entity + id: StationAiCoreElectronics + parent: BaseElectronics + name: station AI core electronics + description: An electronics board used in station AI cores. + components: + - type: Sprite + sprite: Objects/Misc/module.rsi + state: mainboard + - type: Tag + tags: + - StationAiCoreElectronics + - type: StaticPrice + price: 404 diff --git a/Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml b/Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml index 8f181900b7..4d27a0f07a 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml @@ -115,9 +115,11 @@ proto: robot - type: Speech speechSounds: Pai + - type: Alerts - type: MobState allowedStates: - Alive + - Dead - type: Appearance - type: Tag tags: diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Turrets/turrets_base.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Turrets/turrets_base.yml index 0e412b014b..f60297d223 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Turrets/turrets_base.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Turrets/turrets_base.yml @@ -125,7 +125,7 @@ fireCost: 100 - type: Battery maxCharge: 2000 - startingCharge: 0 + startingCharge: 2000 - type: ApcPowerReceiverBattery idleLoad: 5 batteryRechargeRate: 200 @@ -136,3 +136,5 @@ - type: HTN rootTask: task: EnergyTurretCompound + - type: StaticPrice + price: 200 \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml index f6538ba64e..e275bef0e9 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml @@ -1636,3 +1636,86 @@ containers: circuit_holder: !type:ContainerSlot board: !type:Container + +- type: entity + id: StationAiFixerComputer + parent: BaseComputer + name: AI restoration console + description: Used to repair damaged artifical intelligences. + components: + - type: Sprite + layers: + - map: [ "computerLayerBody" ] + state: computer + - map: [ "computerLayerKeyboard" ] + state: generic_keyboard + - map: [ "computerLayerScreen" ] + state: ai-fixer-empty + - map: [ "computerLayerKeys" ] + state: rd_key + - map: [ "enum.WiresVisualLayers.MaintenancePanel" ] + state: generic_panel_open + - type: Appearance + - type: GenericVisualizer + visuals: + enum.ComputerVisuals.Powered: + computerLayerScreen: + True: { visible: true, shader: unshaded } + False: { visible: false } + computerLayerKeys: + True: { visible: true, shader: unshaded } + False: { visible: true, shader: shaded } + enum.StationAiFixerConsoleVisuals.Key: + computerLayerScreen: + Repair0: { state: ai-fixer-progress-0 } + Repair1: { state: ai-fixer-progress-1 } + Repair2: { state: ai-fixer-progress-2 } + Repair3: { state: ai-fixer-progress-3 } + Purge0: { state: ai-fixer-purge-0 } + Purge1: { state: ai-fixer-purge-1 } + Purge2: { state: ai-fixer-purge-2 } + Purge3: { state: ai-fixer-purge-3 } + Empty: { state: ai-fixer-empty } + Occupied: { state: ai-fixer-full } + Rebooting: { state: ai-fixer-404 } + Dead: { state: ai-fixer-404 } + enum.WiresVisuals.MaintenancePanelState: + enum.WiresVisualLayers.MaintenancePanel: + True: { visible: false } + False: { visible: true } + - type: ApcPowerReceiver + powerLoad: 1000 + - type: Computer + board: StationAiFixerCircuitboard + - type: AccessReader + access: [ [ "ResearchDirector" ] ] + - type: Lock + unlockOnClick: false + - type: StationAiFixerConsole + - type: ItemSlotsLock + slots: + - station_ai_holder + - type: ItemSlotRequiresPower + - type: ItemSlots + slots: + station_ai_holder: + ejectOnBreak: true + lockedFailPopup: station-ai-fixer-console-is-locked + whitelistFailPopup: station-ai-fixer-console-station-ai-holder-required + whitelist: + requireAll: true + components: + - StationAiHolder + - Item + - type: ContainerContainer + containers: + station_ai_holder: !type:ContainerSlot + board: !type:Container + - type: ActivatableUI + key: enum.StationAiFixerConsoleUiKey.Key + - type: UserInterface + interfaces: + enum.StationAiFixerConsoleUiKey.Key: + type: StationAiFixerConsoleBoundUserInterface + enum.WiresUiKey.Key: + type: WiresBoundUserInterface \ No newline at end of file diff --git a/Resources/Prototypes/Recipes/Construction/Graphs/structures/station_ai_core.yml b/Resources/Prototypes/Recipes/Construction/Graphs/structures/station_ai_core.yml new file mode 100644 index 0000000000..a04c9b009f --- /dev/null +++ b/Resources/Prototypes/Recipes/Construction/Graphs/structures/station_ai_core.yml @@ -0,0 +1,144 @@ +- type: constructionGraph + id: StationAiCore + start: start + graph: + - node: start + edges: + - to: frame + steps: + - material: Plasteel + amount: 4 + doAfter: 4 + + - node: frame + entity: PlayerStationAiAssembly + actions: + - !type:AppearanceChange + edges: + - to: frameWithElectronics + steps: + - tag: StationAiCoreElectronics + name: construction-graph-tag-station-ai-core-electronics + store: board + icon: + sprite: "Objects/Misc/module.rsi" + state: "mainboard" + - to: start + completed: + - !type:SpawnPrototype + prototype: SheetPlasteel1 + amount: 4 + - !type:DeleteEntity {} + steps: + - tool: Welding + doAfter: 8 + + - node: frameWithElectronics + actions: + - !type:AppearanceChange + edges: + - to: frameWithSecuredElectronics + steps: + - tool: Screwing + doAfter: 2 + - to: frame + completed: + - !type:EmptyContainer + container: board + steps: + - tool: Prying + doAfter: 2 + + - node: frameWithSecuredElectronics + actions: + - !type:AppearanceChange + edges: + - to: frameWithWires + steps: + - material: Cable + amount: 1 + doAfter: 1 + - to: frameWithElectronics + steps: + - tool: Screwing + doAfter: 2 + + - node: frameWithWires + actions: + - !type:AppearanceChange + edges: + - to: frameWithBrain + steps: + - component: BorgBrain + name: construction-graph-component-borg-brain + store: station_ai_brain_slot + icon: + sprite: "Objects/Specific/Robotics/mmi.rsi" + state: "mmi_icon" + - to: frameWithoutBrainFinished + steps: + - material: ReinforcedGlass + amount: 2 + doAfter: 2 + - to: frameWithSecuredElectronics + completed: + - !type:SpawnPrototype + prototype: CableApcStack1 + amount: 1 + steps: + - tool: Cutting + doAfter: 2 + + - node: frameWithBrain + actions: + - !type:AppearanceChange + edges: + - to: frameWithBrainFinished + steps: + - material: ReinforcedGlass + amount: 2 + doAfter: 2 + - to: frameWithWires + completed: + - !type:EmptyContainer + container: station_ai_brain_slot + steps: + - tool: Prying + doAfter: 4 + + - node: frameWithBrainFinished + actions: + - !type:AppearanceChange + edges: + - to: stationAiCore + steps: + - tool: Screwing + doAfter: 2 + - to: frameWithBrain + completed: + - !type:SpawnPrototype + prototype: SheetRGlass1 + amount: 2 + steps: + - tool: Prying + doAfter: 4 + + - node: frameWithoutBrainFinished + actions: + - !type:AppearanceChange + edges: + - to: stationAiCore + steps: + - tool: Screwing + doAfter: 2 + - to: frameWithWires + completed: + - !type:SpawnPrototype + prototype: SheetRGlass1 + amount: 2 + steps: + - tool: Prying + doAfter: 4 + + - node: stationAiCore + entity: PlayerStationAiEmpty \ No newline at end of file diff --git a/Resources/Prototypes/Recipes/Construction/structures.yml b/Resources/Prototypes/Recipes/Construction/structures.yml index ed533bcc52..1f568a1629 100644 --- a/Resources/Prototypes/Recipes/Construction/structures.yml +++ b/Resources/Prototypes/Recipes/Construction/structures.yml @@ -1315,3 +1315,16 @@ canBuildInImpassable: false conditions: - !type:TileNotBlocked + +- type: construction + id: StationAiCore + graph: StationAiCore + startNode: start + targetNode: stationAiCore + category: construction-category-structures + objectType: Structure + placementMode: SnapgridCenter + canRotate: false + canBuildInImpassable: false + conditions: + - !type:TileNotBlocked \ No newline at end of file diff --git a/Resources/Prototypes/tags.yml b/Resources/Prototypes/tags.yml index 174374beb8..14dfa9499e 100644 --- a/Resources/Prototypes/tags.yml +++ b/Resources/Prototypes/tags.yml @@ -1343,6 +1343,9 @@ - type: Tag id: StationAi +- type: Tag + id: StationAiCoreElectronics + - type: Tag id: StationMapElectronics diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_dead.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_dead.png index eb74655e027f45f7ce830da6a337dcf2aa74c248..b015ef9a8a438e085de2f9c743585b33ee4caf65 100644 GIT binary patch delta 3557 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 JuQc2wngH delta 1017 zcmVBYyzJdQ@0+Qek%>aB^>EX>4U6ba`-PAZ2)IW&i+q+HFw54Z|=9 z{4+&Iz(5>4j?<)4H|Y2UL!zYVNj?LI&KTG}?-x8AR0L$2wYFN(AWa%|3lsx=a6uV7 zKq*;%sly?VmDp~MRBQI{^>P@*)h(J%M*2))xe86vUw@49AyD>-e2O)VS{gBhP|~2s zx3D&y&tF(Jyh7uE(~_=ZBE07^+xFJU-eIDR3!^c!!~pC^fV%~1Tv)u(8oc7nTyqjz z$iF@9fG;@$SC*an{wrRS*OpAkGd>iY`*(|B^zB7!Qv7@!fqNci#a*qrz0P zD+;KZWu)RUF`HWzdtMPhKZ2OSpu|i)kzUNeb9~*y$Je_E&+ocD=CtRyMK=fo2RU6A;Z>$1yloQn<%JTqiuQuD+iVzJo4atE`Lp%PCM zhZR+$d?D+y!g-6cTB)(tJ^2fRd2J=lb(*7yV+jc)AwotCWmI4xLaRoKi4^U}Jp3b$ zKS?f`TxBqFET9S%lH-2||AXJ%nuV!JHz^ncx?gPjV+`oo1)6o+{yw(t<_X|`2ClTW zzuExiK1r{)weS%zunk;Xw>5bWxZDATpLEHP9LY~pC=`JAGy0|+Fmwy_uDQLn_Hp_E zWT>m<8{ps&7%x)xy2rZ%oxS~grq$mMzcq5jT?(rs000ekX;hPE3ICI(3U!lt3ow&U z3o4VT3%VpYG%;j2H8(IVIAmfrEi_>GBY+bFg7tZH#9LeG&CX#ARr(|Nlj2XR%LQ?X>V>lA~G&9FfKDRlUEI#qYV!M zlgkkyv*Hpz2o{zrt-$~Q00v@9M??Vs0RI60puMM)lQ$b4f9MDf4Fe?knk)PO007@f zL_t(o!|hhV4Z|P|G=WbiaGz||tdJ?1ZbP=>StjTU(MJ>ng_fk1L8>?j2{6i$JKG6B zM@M6@csaW|w6o_GY4A!+ev4K^5|<7AKtu=9tpf;#bPf?+RsqgAt^a|tA(y{^wU*UV zI^-H!m!XLKe?JJN7C#@y%b{E|#<03cC<{s03PZ_)O=hK*xAWZD*I1%-^*->61>qZ) z6v(Nx>tnRt%$)A)hNfvm_&5Uqz*Yrbfoxe2A>{hH3ic*kx8RTk#!MB+Cn%SpCScZu nPDo1r;n||}&(_iL_jm%eX8Ur@lvkzz0000hoRx@jy9ccP zcNDt^x>4|zFdTIX)?C3m9!DOFaj^CM&h0T8XDvP0000< KMNUMnLSTZl*o2b+ literal 0 HcmV?d00001 diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_fuzz.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_fuzz.png new file mode 100644 index 0000000000000000000000000000000000000000..dcc48a40eca7ca86496ececd1963d987c421dde1 GIT binary patch literal 7062 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 literal 0 HcmV?d00001 diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_2.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_2.png new file mode 100644 index 0000000000000000000000000000000000000000..21ecc5e71caae1462a2b1c72223fb27d8177dce0 GIT binary patch literal 611 zcmV-p0-XJcP)gPRG9iw-VgqCrPV+`1q&F%JBVsEH0jLP|n{gAOj*cTIuPQraSI{3Y#O^LqEb zd++Yrqq5oT510rQ3Wdz{4;0>-&88{N?RGma0GW2VVA5tY#PLVF4ezxYsdogRb2J!1 z??9g+f8nAkFgg^gI?Fnl21$CX<2VlUYc_UnF+F-w%yOLtcO!kw}C%M(XuC zqsTd6rfa}pL@OTbp@(bdV*PaD?O)CHZ002ovPDHLkV1l&Y32p!Y literal 0 HcmV?d00001 diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_3.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_3.png new file mode 100644 index 0000000000000000000000000000000000000000..afddf9f5197a1c0f2b28699397ae2346af6b06d2 GIT binary patch literal 763 zcmV!1;kf+mWO8{^@m!qQ#3v(g&+nxd2Lu;KrUtI}I7sA?TG-&I|Nvf`W@z{&SA`J!u zxd1l|R_<0RCNa?O_i1l+Z`z^5%2{W<+*vls`FNn9;P4e-*aIu}`{k9-^yc*`NPc?zqAb$}P0o`tw^7*`{1Hx(5cv>Na^9_mygdbcE0oiPpUg;^;N;$&j zr+g!X2=0UXy~N7fkBfBcYHBI~s_OMRYi&9@M?p%5?S22Jz zY}sj$LAeMyz2*`k-T^YZ%$FaecfqVE6fD<B^(atgxqSi z$^sax%F0kE2=hWbIWd92@UYZQ3Se^Xm=9m#ny*h` z-+uy4M@|^d==_din1RF3Tsi@zrKR}AT|lh@UD&kBnOHK2{fnL-Pojp}4>XP&zB(n< z-`yIMA1Ffn8efchDz_h(w zO+lMxP>nJ$O2k{Y)noF{`-mff*FJei2-}@-Qr&m`ikub_(U3s|`{opUxcL-uo|_fG ziy}1zrxl}pR94zz2-Cq;v8WJZefLoONi81yaTE46YY}@nicwE5EGtV;`31143+N!5 z#BZn5R)1D0aJgLgmTTpn+q^e6QF-|;f&3Lkz+&0#&4`$^OH>SD=0+JN>5yxl7)F3? z$(Pm-w+9CY%~N2r*`$s;u_f4Qd4sb*T%|iV>W?17t`Y&$i5Ubu9%;-p2~shAQ$W6z z`orVW{qfdzss8@!14&;{1em5k6y;f!Q{mRZ&t1y-5W|!>hGnkTh|R1u_B5IFOVvfss|FBPUKaJ${Q+;RId325e!?+GFBD@Q1T@px3~w^GF3 zh9mN9iUj=r{BhFP1@e`88mbXCOo8?cP>t-Og$BleF5ZICWPen!AS`O;rfDzO(*{R- zd)j2O-UUYI_aoZt91ULScEO6Jj&+CxUf@|MEIoe&W6bFwGJAvdQDHLQs#!HzPmZ2bGG$*y}GCxra(s3dlhGc27{wmO~Lk$ zo`WfMDc~sZTT#?5s=0ui(dUvOxeDfBE_Kow*MJ&@7N;}$EN)pU)%PH$A-;jmh93DV zq^R~&1Z!#L8GLaXE-Gor1k**R(%S-FUEc^7&9l3$yJ3boeUkCJAX_*bvTMKJeAQ@u zMj{bo0e=k*VJ`6Y_Tv8_<7YYl6U^t2NtyulPi4L#3E=&hEAa0*a*6){g0iMW@VxP4 P00000NkvXXu0mjf@&F)! literal 0 HcmV?d00001 diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_4.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/frame_4.png new file mode 100644 index 0000000000000000000000000000000000000000..70c4834a3e9850520847e3ae717af25138262c9b GIT binary patch literal 452 zcmV;#0XzPQP)XJT{QHPQb-E*#14fA76J-WgF8eSt<9K@jNV zgMp7UO|`%s$5|Rc9Zn{2Fdi!be=r8`i$yzL89+Ers8D~QMt|aDI)$Pr28ElZ2}zRB z2KsVccV|ddRa@_}EF-@q4RmnJvV;)!%XE!KBgpeyY#@$f@O>X_+vea*a$R8fk;NJC zJP+<45Xi5X4OEnm&&P0eVXY0It1t|k(F|1R?diIS!XsxOr~HdUpkm7!1L2rKqQv} zi~{}@Wp+dq92v;!7A>U)e%fZJe)u;<8o>6cYfN?lQc%&(5=KDO!^swB;AK8n0wvpW uyc4Lrm~0v7ZYrwP8o>RiOL#p8o%#lyHLb8QkC(mx0000?)WDzEdIOv|2tbXF)=YVM#skg+SBw>)vn4+5?xA literal 0 HcmV?d00001 diff --git a/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_125.png b/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_125.png new file mode 100644 index 0000000000000000000000000000000000000000..642132e99e650ff7a7a8472199353a71854faabf GIT binary patch literal 331 zcmV-R0kr;!P)y} zcWIv3kmKLU#q~b}0|SGryT^Y6L!rftz`(!&vz%POM2_WbY-|j$ z_!blrqA2KzP6J4J{NIjkTbUW?4UkJYY623Xfblvl9KcAN?}@8e=u))~nR9 zwT94Q-nz(H&V-zE&pmTz09v$YQ3GbqrI+>F&VLY50h!|!6~Hy+fQzq`S_F&@Gq_*e z4L452KZKaCysEKb2I)1^o~#r5Ebl*ahfvjYx^FFtqGm#8+LLush^pz#uxyreU{^3J z&48-uY%maV`gDj^p|7{xJXmLTI(=_u%RAfz!_o_CZK2_xptB`2$L67a_D79gX~~A`01m zF`Y61fRocRnsqqs0xCi-2*S4Ck}T6A#JL3|6TY~;*|v^S+D((!VtsPT?Hf#atIs)z yxaTsxE_n*FT`Y|}FdDt*n|Nq+phb%oe~d4U3y@H;6EH6T0000O{T7H3BZegIYWL6E)>Q4KNQJ^}=10-A7R#o#aG$6(RT%;c&QI1nq|{S! z{5eLi+aWSFr9+r67(C#c0D$>?F0CpF@;%6FKg?2@Gsehp&?hbdP1C?R_Y|nw_KXtx z5|*8fA#}sH>BoG*5Sh~q-e2F4Or@Zwbv!kjUh`%6oWxG2z$L*2&-sg(rLq73#uyYu z0RT)V6C_h9F&y-1(X&6rRTbIu>`%jS5>YIbo^ttuA#N)b0RWd**CKcv+q1ooVOVob(>%YAbzSVzcq@U` zAQnx_Yt(JGJnI-^Yt7yC9~42;%|8YG1ft#{l7onR%Xl4?1c|o$@QWpEX6&8*FakmM myo}@rC36T+#E22&pYaQkx7BGb!r4Xu0000kch)?&pYxmIB>8SUWq-h yzpJ>BbJE`}Y-+)*3^EnA_F{cUAAe^!FqPHRh@G+X&XrD}J_b)$KbLh*2~7a+#vMff literal 0 HcmV?d00001 diff --git a/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_50.png b/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_50.png new file mode 100644 index 0000000000000000000000000000000000000000..54d2b3bf9c58ff1ac1b8f0f7d667cb687b3e7a24 GIT binary patch literal 143 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJFi#i9kch*{2@p#pP&C^)?p7%gUuT@SQr4on|nK(Qx814ef93+^Ar4X oe4N#m_Jj1g9n_Fu-NMAcl_tDtviYQMKpPl5UHx3vIVCg!0BaaBRR910 literal 0 HcmV?d00001 diff --git a/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_75.png b/Resources/Textures/Mobs/Silicon/station_ai_cracks.rsi/DamageOverlay_75.png new file mode 100644 index 0000000000000000000000000000000000000000..4dea5cb6e4f33442b0c8601ea2bb08a7286d86fb GIT binary patch literal 231 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJb)GJcArXg@6C_v{Cy4YkP2`hU znHfv)iA02YodNyv$ht?~kBDC_lHUPFYdm-;?Lg{rmj>{(kuv z&luFjug~99v}t;N%=H8;JLd4CWlccrxZi08e%u$zr}Je#*r-H zFCqZ;4D*4r9^he!5r)4$J?ZBU~ei5=%+jI#RJP6bzto};D(0Eb__<;0RR910H8mB{=m-r z1ABdxMhg?}tCt(QDSd-NolIU2B$sCHS}Zb*hyqC#E6=u<1)n_NzoQPUo(0@jq`D~> hW3m7(8UO&u&J#K8skm^ysM!Dj002ovPDHLkV1gxSb{zl! literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Devices/ai_card.rsi/meta.json b/Resources/Textures/Objects/Devices/ai_card.rsi/meta.json index 140b77fbee..8a12aec8cf 100644 --- a/Resources/Textures/Objects/Devices/ai_card.rsi/meta.json +++ b/Resources/Textures/Objects/Devices/ai_card.rsi/meta.json @@ -63,6 +63,15 @@ ] ] }, + { + "name": "dead", + "delays": [ + [ + 0.4, + 0.4 + ] + ] + }, { "name": "full", "delays": [ diff --git a/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/meta.json b/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/meta.json index fcdd9e1b26..1192d1a208 100644 --- a/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/meta.json +++ b/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/meta.json @@ -7,6 +7,9 @@ "y": 32 }, "states": [ + { + "name": "mmi_icon" + }, { "name": "mmi_off" }, diff --git a/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/mmi_icon.png b/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/mmi_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e109ee0f318c882c503cfef20e09714a2cb8bcbd GIT binary patch literal 912 zcmV;B18@9^P) zU1$?o7>1veWRhrg3~g;mT0|)Iuhc@LE0ySlDE>e}*W%TRB6{IUm69T;>y2jVf-Lxh zNG($6NDWopa`!^S0GG#}nv}6JH@cVkTZoUGK7(Mh!wIka%cZLbRvQ+p4ffW` zMunJ-5O4c&H^?Pa&tTqIzjSn~PsWGJuNBRAPH=~H0xsYp#70i3s2Y}qs#fyPH#zMi* z8ExMPKsXd+-!U)R9x+sc!-dP`8b=9y$y`gG4A)?o5nvwhdiWW68>--o8#Rs+06CQt zX&o3TE7B3W2E)2}^@7E8e|S41W@7w3xqE5=*mAwaSQCn(C~G%+d)z}*UIurcscVWj zW$a3F$@TXTv7M`#DsmGVdaB!3k|a|~2SibneT~fk^p-6pT33!ZIYC}x0_WC}8ICC9 zj&LI8yS!u8)-VJ8-S`@t2?To53y>tq)OM#yogZ-IBcAW$W7`nUt;H;u zjNz=@MDe<6q>DePanFC0hYV3IN-=vG(AnkZddoJ?`lt({j37CE?jHvpuVdefGaJJ z$W6o;4~Fr0JP?U}t$8Gd$Kzo<7$!Fn<4VgTmHb+3N6PF|eu_m=ly~kvgxkFqr_;&) zz1sm;RZ$H<<={)*O{k2OZXf9HRudQ<9VHOxF(zO(5r8U0k|a~|ani0SS!W{8co}I$ z06HO(@0~vOIv0__BlBYy%kNkl8+_6on6~ln5zEp^6;Ig{z6+AyN=&(#<>Y z8oUM#a*flca&@sq`S1&H32TlE~L9(r8mj&0V+}ufq$sfS-ML*=YStg8<;=N z0Ra8Q_|0LS>oONMf71FH05BR|y%*r_B*1aDes`_toDH@qSe2p_v=X3b;-&-Uck^?k zkF)igsMi%C6V}#&X3Ay(GsiB&hk6ds`Wd6qRjCV#98h=C`NcIaWImmavo%ztkZcp{ zfK?VCYpkr%ynh4QbV2?;n|91VeU!SDQlM;G6M-mQ1M>Z;{OxYoUDx#dcGH- zvi@zKF{%jL#pe))VHk#C7=~dOhG7_n`6_(YcSF}>sa+aVkwQF;y^9b}W2#c5ZA}0G z5P|S2LOhMBYJs*@0s4zEL?FDRgA65!Kr}ACHm(l$7k^`@Na1ogbSA>(a7ZdrtyG?y zRfcB5YyM}Ip*mHkRvfT-+IX%1_ZMSqo;H3Qupb;w%HR8Q!0P@dl5K+h;NV_>o0scX z5j>FfyB{2I^Ky+p`0X|YmaFAi1Ycy6U#^xux+emNWGLx&G9lNy!0lv0r|p;i9gfjj z6F{W3CQcbj=K{!3k~s!X&);+&glhrJzP-Jdm{IU0fcKZO%n3dO=*%0buQi}k-v`_~ jfMFPhVHk#Cm^qM6vg<~8!00000NkvXXu0mjfHwhQL delta 243 zcmVI!54=E?p;m?7K)q6rcN6e%%< z|GPqzC)@af2mk=UL%sGNnH7!|&i$9ScK~mv3ZOcP>eM=mi^qQgpQ9I4CkbKrO+W}k z^H6nC!}4iOdAFN(qX{VG71_&tyCsDIu$YCa8u0@#dHaq?^+`wFQGp|WVoj0EeSgOB-=pG=u+ieQ-F7&_M tZ~9i!ci5Zn{#o(b0ln`80001RA79IVf?w7G&pH4A002ovPDHLkV1lhaZ2QhG>QImp0|_O=O_^X z000000N~Q7C2bt`hmp*SJ=>EO@)tf$UMDq~W4ZOqq?xwk;eX|UOw&x<=UOg#wnt9S z$s{&;ok$m2tQV;}RzQOqCn23m4_k$0AhucnpTc14xf*{yOpfE&IM2>HE z2H1{fpY|nfljTtVr~?Ynmo0%=Nh*QOV-cA2I4J_hH{*SzZAY^YQFp2lWQ<>=zHqE; z22PjLqsO_Hn}00>vYzPSK=VYt4*Ftpk2^A6&bFf&dA3LX!vB=Lhd~qs0o{GOE64${ z2viw`-1_m?$-d8F_KH%fJOliNPfpKCt-K1IX#E19I4Q+v;%|@Jlmt|PA96OezU5E_ZL1nBd7BEmCk_qYR=mK zuR_0B<|UrW*1TnVy=x0}d5o`0KeYsv)gON<)~5U42n?vwucuw(iU}rZ$+msy+1w-!$>%oh^T#aMStJkO6$Mp*4KhC45LP)5j7f(h~q{~g?^>) z0}R8c6*0F|MPD0+{b9uQQG^cqTEK09i+2D30Dk}g0000000000fV1aZdi6QT-5V;} zv$?*PnOi?E^}K`6`UTpmjnApi>VRQ?7&$$MtvQxkpVa}^olfN09^2r#^|hG;L}wgV zgDkf``y9}^AJBX?XFGXreRc=DxKHFSe6|i{Zhh7gB+`D_UX!)y(jWR=>WOpfYmotB z@Ni!(&L|3Qebx+Ew%7FQ`C0mxE?|7c8DMR?tS5dB#?N&fF50rgFpNa6{H5q?GY1F^ z!(d&Qz^%{b0M~L^2hXj~T7oz~0_N7&!U3184*&oF00000002-?{sDRV@$1X^x5XV?y-A6-TyJ$ zRf*5@{Zh5JtbY5nl7DggE-_nl?SFUX-m;%}(k$Mu-5M5^U4Fm1-li$+l5~Fd+BxT+ zD;}@W*el-lR8`H^jsXT5Ci+g-jLM&RuKvRsi_-3O)1qGM?&Ns+hrK$SeYtl3^;NGxfiju zf3`H}^WsZKSGrqVd-C*O!K9 oU%B-2<4cqMpECoEI#6HBy!`X$AG>#NTM9DS)78&qol`;+0C{<&cmMzZ diff --git a/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-full.png b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-full.png index da7937d84d73c0ff45b158a96e5a3f62386ac43b..fe7d222f1cef82ddd9233fd0e90d307e98910e1b 100644 GIT binary patch delta 899 zcmV-}1AP3_1ephrBYy*6Nkl0|y1d&}G9MWBM;l&D# zGnabQix3ozQQmt~)&ej32Mh)dM-jF>CAuLIUUcC_cQ%_)izJ6$WJIBkoM-cNp<8O3 z?96%2wC4l6*g5At@AH1&@AIDb>={511VIqQ|B5QzLAH(+vwvyKS-7k_;=?=w%K=rP z382Yz;?s?GT&yU?mCkkm!2IMZe2>Q9=vXpesLooomho0H}0Z4}A_EJCiAJY1WUpAYvz;005L|icnIbLTpFG zqN7!^145AydVgQ{W|acjrl3^PrC`Ps_#G8dTBgGIqrDlveSZYeEfGcAoBue#Wpkxw zddnF+H0xW=U{1lhY_9CC+jpSWTqvY2&I6CYA zL~uRbshAxdcC3B*Dt!?wtx#b%k-%(V9sqEiy$p4c2AlENu@12eG?*0t}eruswJRM}Obx>;v+#2xaO&+dYO9VGD5| zLJ$N&5ClOG1VIo4K@ew!)B4>|?pV~hHsmZEyv<(4MesIzMJYwvya@mRjG0kX1aGrf zlv<$8s{oa5D;P7QNII}}tjL&|6Bl3ACk>?1ZGQ!4;h;AdWD`MeFo>Lm%S+{1lWRjh z%!6?XgxmJ1<-nYs5h2lz#PYd-7dcXf#rZ| z(AuW>i_oC8P1!#_uyw3Byd;Wp?*xaJM6qDrNZC48b^!|MbvzZ+dF+cI2!bF8f*{T* Ze*pRRSS@~~-J$>h002ovPDHLkV1lI&qHX{H delta 570 zcmV-A0>%BA2hs$PBYy%JNklLBz#up=4_C4I&+!JGCivNq>OQ5FA8^L;nE-0YNM#P{gVzJ|8i;@8viYHy>!5 zr(hI;@aA|j&ylok|7WD9aC7RAL|Hvjw*l-QS~4|_-FNQ;*9Cq88>=O#wuim9ui<|Ffc8&?jnxvO zC(m>{_pUfSegd8X3`57FJu9)_XAQ~R+aZ>}bi$bS~3?kLGso06+GrS2$6WD7D^ z6A3#%A|fIpA|fIpA|fIpqG4slt$}%O3niC)O=J*?V8t!Thw=ECuZh$h6>1TzxFvN* z9jX9<^8tTjsbOMtc|DXCwBnYGF0W&$5jf+~`viVhKV5PAUVwDPm2Pz5fjgkQQ`Rz5 zGY4JXH8Yv18I*U*;Z3cDbqNj1B@7^(-_XEpo`{Huh=_*LFX~6%Do!pI(EtDd07*qo IM6N<$g7LBaxc~qF diff --git a/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-progress-0.png b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-progress-0.png new file mode 100644 index 0000000000000000000000000000000000000000..1bd832bc63589ef8e365dd8e5822e5f95568c4d7 GIT binary patch literal 2553 zcmZ`*S5y-S77e2GCMYm;6va^~Iw%;FUIZyp0|Y4|(u+gYkbp?p-Lq$Yc0b;IKkvME&b_ZCKeRCB=aJw6008_Z_YJI1e9TYd z;yA%xE%#vnfVp=mwen$IKGYvq?O+2uDMEnY*Si`lK(Jcw%7awF?1Ki zXdr+ZH_r@G!8iSvw1C8c9)R<1?MO|-4jYA`g z1?x=hRpGpMT_V`BcVzqWHX|I))A;&}|FGEI)%wT-Br053%x0&2cVl_noz%dhYYxAy&ee5VxW}GY+RlYioN;h=n(I&o{Qb; zOz=d$XDIc>pPUwU9??_TZEwZRi-ytOq~S9Z2K|(2*&d91FOcToX})A7$DFjq2oQeqSlpuBJf9O;U(m;F+i!kiC(|;9%biiO8dKRN4N_~8y~Edk`EfR^u|$EcCD;P2XsclL zUE1GwoKuUCo6UNBo37Oj7L)OqkC_r6p48KzLItwd(G}|`?(b$XjvZ+);2dB2)!wr_ zPZ8f7-5gzrsKzcqYRX03=S20=!$Ce=ZR3etVib`CovaN_DQnf3^N}!&=&nSGaB;21 zN-T1V3B)FtP$0p8DipZMKc$@9olEId&+HS}25A76oDyH7xm{%M6xfd@uCB=p!_OAm z@+JW;44b?8H#z_>hbmOZ(%WW?cIx*8=C<5RL3mhrCC>b)r7`lAJ>}Yx-moS{PO_Sg z5YriA@Z~{Q5+Pri1_Iork6wkF?Y6(=*wyTczUMCNMIN(WxRO&v>?CziWxumTZfCR^ zmDgqvK#>v!{%|3LTzYdnKD6-sh$2Knmz$v@ZPRUsune*z=a zA39bwlRHl-eqInH7*w@bbL{VT+%a|7PE{HiO!nw6)NQrpcZm4%_FGurNFlgL{lcGJ zOyjSiUfr863Js}MgMAFOy}A3V^2utg0voAX8H7dZ9UZ$njJpnuF+A^oCi357`M=;8 zJy_`;y$6d2ae`<)y)A?(-aREs z@s6R%+Y@}EM6lkz(*}*R8gd$SU_W7k4H~p&d&3s1Q8%9X47Zw zf{>!@xxRKN>xXy>mYD;5&>KmvT^1q>+0gheYVnRqr+mO?J<2^^lD{{PP)lC5)J*`J zBe7bU*y=Cp@6VhKQ27w2WG5TrihEq4aJ3QR(bXYhDBBw-fbs5GOAKjKQZ23}F7AyS z?1}>+^1WZvyvADJ?Z&*LpyTw=&0pH+IODhZM$d?s#5piFepik!Ma)y!YfM>I@Wg2n zzT<{Un(t(CnpSXJwzDD)E4Jw@Zb@t5tEPcEgE?$g$rNqjQN$|Br={r^vi5+Sf0>wO z?>%pa8TFUBot$syeZz-}=Mdz5+cwgrq_XI}lJkr(bx$b?ajw3gtS;_WJk5EVx&B9&GZdc<$-t zDJMU?2cPOY+e%w(&cW7kd17we_^Wg_qsnRSTu&?C%@3xQETuZ9R$?2EOW+L3wGd)h z8!Jwo=ovO0-Vo{sluO*yGEuUtbP{ztNwdvsN*#7CuP>oiSzF3zSJ&g#&R3S@Sbx$= z>JfLdo#LS?b)WcbyA&*Pov5xns{!y#ooEM~aKv5;VMC$s0{(lU|5*jcy901WGzJz1 zS@q0URFr)%dqUd@!%gZkCkh1YRR%@l*4b1!#O<8OeDZn(9WL2dj z0-vQ%{$$!2w(({oXjBshTp%rXuniGtDh>6+GwR2uPq{(9G@d#$ar*vG5&CQ z1DwQ*Mv!rmX#R*7z68ZJaFTrQ`mP6B>9G?`HDgt$A)(0gK1~P?$%bb~xk&k3$Z>`tq`_8LX=2$y1 z9?Tf#zEk!iL===y_tZS{eZNrIin;RpYHnd1jN^}JX@ir8MjgcBkW#L<91}|Nm#M9c z)@rysiJ6#HA!YP3bHrxXryk)N39IGUk1|%Dj(P0SUyb97U8>>Qh5^$HxVq&5n?N=o s>-1GGL>dEVSDyNX54_ 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 S=z4tV0j$j6mn%$MpZp8Xv=!(8 literal 0 HcmV?d00001 diff --git a/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-progress-3.png b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-progress-3.png new file mode 100644 index 0000000000000000000000000000000000000000..80dcc81ec3c8cdccd8c57b29d07f71619b1e63bf GIT binary patch literal 2545 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 PMS$b>NccP3OSpdntctt& literal 0 HcmV?d00001 diff --git a/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-purge-2.png b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-purge-2.png new file mode 100644 index 0000000000000000000000000000000000000000..56dfc2f2d82c4390eb527da96c2df0d34f6ea70e GIT binary patch literal 2406 zcmZuzdpy$(7oWtu%O$Z^gnH6bxs384WURT9B$vlrHWnqhAT9 zKNi*E4zMQZC^Pgl3T9ZyqCmCnZ`~^KDem_5^v($=|1s-(>wDOj>Qt!{=4nDmgxk|A zD)>vM0e(`_a7JPb&x1Or&CFz z8ezTl)50doUTl_KEf}Jm9ec2AL$d{=jj@qDuBH_fV~uiFEFnL7?heX1RCy2m zyHOin_Lu~9@NcYWaM=SRj@XBxE<_}_C_JUn+Y8H=P!t+%lrLeKTm9ZNWt3_l zlY8LQ>C|99kE(AZA@Mo|V@EUt3NTaPeoYxM+3jij_6J88sM&00>-751OlWu|Ki>Xs z_f1%tl-lQR92S*4mYDO7+m{e?q;R`J<&4`dPw++g`l@~MFRg11nw}XEDkfGkG@z(- z=1enw(nqq}oATLnptYazmy&w|fXPJ2{}ikBFqwUXrAcZWFbLu}G|aDl=C}+hVujKp z{elq5!+0aNk)jz+Wf^PE#CXKETgNdcAftDDb+?V!6>YX?6F`?foObt6930ZzcBtk! zKF$74xN-0KhB;1Fy)n@#wln&nv?cE;ve*ir@MfHLYHb%8p7&P|o$GJ&m?^GNQ_K)o zjMIRfeedg;__Db4>@JbZTf<%JL(kwK+^kIW=DbM1@0+}NOvuU*=%>6J=y+kju%MNJ z_N(+c7TpB%P*s_BDP9P6Bka-=z4BSNoec$W)HqCtPLadsnT_|s8pn)ppUPQN^IA_s z8Hex$`~Q3(%h@_8K1d+u1Q4U3w#fO1C*ZUth$cPF9IMG=-tBf}srK0@S?BfirmH$N z#xX2L)$}s-G=c=8fGAq4pfNn!0IT#45SvSYV+E?>Vao0;Jh`W73Ls(fZARVoz|{%fIs`{@6`bljus5_~*Yo~A$>%S;L5 zle6*-2MHe_b$a~6F5mslN|fG~)DUO2o*aWe#pz4x`ZkpBe~qp>$G%qxl}Vb+au=JlU6%gx#_`2v5#LadVj-lnAbuoAmKOT}d;*D)1 zVM9+Eu7AB;X0_$43$d)_;-1GpW>DIC&8~L?1F(ksE1>kW5C%M$+`8zvU~OALf~Xg8 zeLiS6=v5yBxhE4rrYAE?Iy3}|CmU+dpQ%ljr*g|fT2<;g4e}9J%ADuJqc0-u_^1z_ zt1%%5SJjGjNbAs{Pcw#4<#e|oe%=M}6s|_j z!eIc}@cRv?0Y>8m`pF@O=@=IfVANx=V`lIB?zetVD|5+~Yk}eX14~|IFucCYiQ{%F zU-+w_^S0}pjzIFNRU9br2HBdc#WdAuOy`qu;h2)RmfZcRfaXAOG9+;l{CaLPBIBJd zmeZ;UnT6`$kdxk$hwi&89TL5;6_J zkJ{fU|)U*o+(lClB5&r9HjOsP`mz%1}n_NsK(rM8CH zz4{R}#TSC(EvCMU8Xr5%{gmuJUMz8L6wRtUG$O6tl|O6Y7HYu_u(2B4~7ZQ?#U~zD?M2 zziF644_XwfZ`15~{rRmkj=uA%m9hnqJRUhe&1N0V;vI&NJ)cirocL7(oiYmqPAgjn zVwjur;Ia@9i8={3=7K8fBZdB{!jaqS#G_GbafFx};&R(!-_+^)f<9}2xI^@7t=?+? zuhu)H5AEp+E&OJH2~Dlpid`E^#vI*Eqld+bn$0H1kh&fv%NdDA6cu z+DUFICAXepx%T*m+2tC$xcvc*lOwWu=nho%{bwW+TNd-oq0K&3_oTWYrB$WU0snA9 zn-g!3(Rk~|0ReeKUz*CB^0}{sryt=*OeAut7-52PasQETa&tT H*u=j9U~jsR literal 0 HcmV?d00001 diff --git a/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-purge-3.png b/Resources/Textures/Structures/Machines/computers.rsi/ai-fixer-purge-3.png new file mode 100644 index 0000000000000000000000000000000000000000..4f6dbe43d1159d72c09d21990bf772a139f01bc4 GIT binary patch literal 2373 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 diff --git a/Resources/Textures/Structures/Machines/computers.rsi/meta.json b/Resources/Textures/Structures/Machines/computers.rsi/meta.json index 28b6b7fb79..ebb9a875dd 100644 --- a/Resources/Textures/Structures/Machines/computers.rsi/meta.json +++ b/Resources/Textures/Structures/Machines/computers.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/bd6873fd4dd6a61d7e46f1d75cd4d90f64c40894. comm_syndie made by Veritius, based on comm. generic_panel_open made by Errant, commit https://github.com/space-wizards/space-station-14/pull/32273, comms_wizard and wizard_key by ScarKy0, request- variants transfer made by EmoGarbage404 (github), xenorobot by Samuka-C (github)", + "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/bd6873fd4dd6a61d7e46f1d75cd4d90f64c40894. comm_syndie made by Veritius, based on comm. generic_panel_open made by Errant, commit https://github.com/space-wizards/space-station-14/pull/32273, comms_wizard and wizard_key by ScarKy0, request- variants transfer made by EmoGarbage404 (github), xenorobot by Samuka-C (github), ai-fixer-progress and -purge sprites made by chromiumboy", "size": { "x": 32, "y": 32 @@ -75,6 +75,246 @@ ] ] }, + { + "name": "ai-fixer-progress-0", + "directions": 4, + "delays": [ + [ + 0.1, + 0.1, + 0.1, + 0.1 + ], + [ + 0.1, + 0.1, + 0.1, + 0.1 + ], + [ + 0.1, + 0.1, + 0.1, + 0.1 + ], + [ + 0.1, + 0.1, + 0.1, + 0.1 + ] + ] + }, + { + "name": "ai-fixer-progress-1", + "directions": 4, + "delays": [ + [ + 0.1, + 0.1, + 0.1, + 0.1 + ], + [ + 0.1, + 0.1, + 0.1, + 0.1 + ], + [ + 0.1, + 0.1, + 0.1, + 0.1 + ], + [ + 0.1, + 0.1, + 0.1, + 0.1 + ] + ] + }, + { + "name": "ai-fixer-progress-2", + "directions": 4, + "delays": [ + [ + 0.1, + 0.1, + 0.1, + 0.1 + ], + [ + 0.1, + 0.1, + 0.1, + 0.1 + ], + [ + 0.1, + 0.1, + 0.1, + 0.1 + ], + [ + 0.1, + 0.1, + 0.1, + 0.1 + ] + ] + }, + { + "name": "ai-fixer-progress-3", + "directions": 4, + "delays": [ + [ + 0.1, + 0.1, + 0.1, + 0.1 + ], + [ + 0.1, + 0.1, + 0.1, + 0.1 + ], + [ + 0.1, + 0.1, + 0.1, + 0.1 + ], + [ + 0.1, + 0.1, + 0.1, + 0.1 + ] + ] + }, + { + "name": "ai-fixer-purge-0", + "directions": 4, + "delays": [ + [ + 0.1, + 0.1, + 0.1, + 0.1 + ], + [ + 0.1, + 0.1, + 0.1, + 0.1 + ], + [ + 0.1, + 0.1, + 0.1, + 0.1 + ], + [ + 0.1, + 0.1, + 0.1, + 0.1 + ] + ] + }, + { + "name": "ai-fixer-purge-1", + "directions": 4, + "delays": [ + [ + 0.1, + 0.1, + 0.1, + 0.1 + ], + [ + 0.1, + 0.1, + 0.1, + 0.1 + ], + [ + 0.1, + 0.1, + 0.1, + 0.1 + ], + [ + 0.1, + 0.1, + 0.1, + 0.1 + ] + ] + }, + { + "name": "ai-fixer-purge-2", + "directions": 4, + "delays": [ + [ + 0.1, + 0.1, + 0.1, + 0.1 + ], + [ + 0.1, + 0.1, + 0.1, + 0.1 + ], + [ + 0.1, + 0.1, + 0.1, + 0.1 + ], + [ + 0.1, + 0.1, + 0.1, + 0.1 + ] + ] + }, + { + "name": "ai-fixer-purge-3", + "directions": 4, + "delays": [ + [ + 0.1, + 0.1, + 0.1, + 0.1 + ], + [ + 0.1, + 0.1, + 0.1, + 0.1 + ], + [ + 0.1, + 0.1, + 0.1, + 0.1 + ], + [ + 0.1, + 0.1, + 0.1, + 0.1 + ] + ] + }, { "name": "aiupload", "directions": 4, -- 2.51.2