diff --git a/Content.Client/Xenoarchaeology/Artifact/XenoArtifactSystem.cs b/Content.Client/Xenoarchaeology/Artifact/XenoArtifactSystem.cs
new file mode 100644
index 000000000000..0e8680e22a88
--- /dev/null
+++ b/Content.Client/Xenoarchaeology/Artifact/XenoArtifactSystem.cs
@@ -0,0 +1,6 @@
+using Content.Shared.Xenoarchaeology.Artifact;
+
+namespace Content.Client.Xenoarchaeology.Artifact;
+
+///
+public sealed class XenoArtifactSystem : SharedXenoArtifactSystem;
diff --git a/Content.Client/Xenoarchaeology/Equipment/ArtifactAnalyzerSystem.cs b/Content.Client/Xenoarchaeology/Equipment/ArtifactAnalyzerSystem.cs
new file mode 100644
index 000000000000..557d46f46b6c
--- /dev/null
+++ b/Content.Client/Xenoarchaeology/Equipment/ArtifactAnalyzerSystem.cs
@@ -0,0 +1,40 @@
+using Content.Client.Xenoarchaeology.Ui;
+using Content.Shared.Xenoarchaeology.Equipment;
+using Content.Shared.Xenoarchaeology.Equipment.Components;
+using Robust.Client.GameObjects;
+
+namespace Content.Client.Xenoarchaeology.Equipment;
+
+///
+public sealed class ArtifactAnalyzerSystem : SharedArtifactAnalyzerSystem
+{
+ [Dependency] private readonly UserInterfaceSystem _ui = default!;
+
+ ///
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnAnalysisConsoleAfterAutoHandleState);
+ SubscribeLocalEvent(OnAnalyzerAfterAutoHandleState);
+ }
+
+ private void OnAnalysisConsoleAfterAutoHandleState(Entity ent, ref AfterAutoHandleStateEvent args)
+ {
+ UpdateBuiIfCanGetAnalysisConsoleUi(ent);
+ }
+
+ private void OnAnalyzerAfterAutoHandleState(Entity ent, ref AfterAutoHandleStateEvent args)
+ {
+ if (!TryGetAnalysisConsole(ent, out var analysisConsole))
+ return;
+
+ UpdateBuiIfCanGetAnalysisConsoleUi(analysisConsole.Value);
+ }
+
+ private void UpdateBuiIfCanGetAnalysisConsoleUi(Entity analysisConsole)
+ {
+ if (_ui.TryGetOpenUi(analysisConsole.Owner, ArtifactAnalyzerUiKey.Key, out var bui))
+ bui.Update(analysisConsole);
+ }
+}
diff --git a/Content.Client/Xenoarchaeology/Equipment/ArtifactCrusherSystem.cs b/Content.Client/Xenoarchaeology/Equipment/ArtifactCrusherSystem.cs
index a57ef5557475..d4a88cdd0378 100644
--- a/Content.Client/Xenoarchaeology/Equipment/ArtifactCrusherSystem.cs
+++ b/Content.Client/Xenoarchaeology/Equipment/ArtifactCrusherSystem.cs
@@ -6,4 +6,4 @@ namespace Content.Client.Xenoarchaeology.Equipment;
public sealed class ArtifactCrusherSystem : SharedArtifactCrusherSystem
{
-}
+}
\ No newline at end of file
diff --git a/Content.Client/Xenoarchaeology/Equipment/NodeScannerSystem.cs b/Content.Client/Xenoarchaeology/Equipment/NodeScannerSystem.cs
new file mode 100644
index 000000000000..35a60682fc40
--- /dev/null
+++ b/Content.Client/Xenoarchaeology/Equipment/NodeScannerSystem.cs
@@ -0,0 +1,30 @@
+using Content.Client.Xenoarchaeology.Ui;
+using Content.Shared.Xenoarchaeology.Equipment;
+using Content.Shared.Xenoarchaeology.Equipment.Components;
+using Robust.Client.GameObjects;
+
+namespace Content.Client.Xenoarchaeology.Equipment;
+
+public sealed class NodeScannerSystem : SharedNodeScannerSystem
+{
+ [Dependency] private readonly UserInterfaceSystem _ui = default!;
+
+ ///
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnAnalysisConsoleAfterAutoHandleState);
+ }
+
+ protected override void TryOpenUi(Entity device, EntityUid actor)
+ {
+ _ui.TryOpenUi(device.Owner, NodeScannerUiKey.Key, actor, true);
+ }
+
+ private void OnAnalysisConsoleAfterAutoHandleState(Entity ent, ref AfterAutoHandleStateEvent args)
+ {
+ if (_ui.TryGetOpenUi(ent.Owner, NodeScannerUiKey.Key, out var bui))
+ bui.Update(ent);
+ }
+}
diff --git a/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleBoundUserInterface.cs b/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleBoundUserInterface.cs
index c7a74815b6b8..9c275540ab18 100644
--- a/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleBoundUserInterface.cs
+++ b/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleBoundUserInterface.cs
@@ -1,66 +1,44 @@
-using Content.Shared.Xenoarchaeology.Equipment;
+using Content.Shared.Research.Components;
+using Content.Shared.Xenoarchaeology.Equipment.Components;
using JetBrains.Annotations;
-using Robust.Client.GameObjects;
-using Robust.Client.UserInterface;
namespace Content.Client.Xenoarchaeology.Ui;
[UsedImplicitly]
-public sealed class AnalysisConsoleBoundUserInterface : BoundUserInterface
+public sealed class AnalysisConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
[ViewVariables]
private AnalysisConsoleMenu? _consoleMenu;
- public AnalysisConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
- {
- }
-
+ ///
protected override void Open()
{
base.Open();
- _consoleMenu = this.CreateWindow();
+ _consoleMenu = new AnalysisConsoleMenu(Owner);
+
+ _consoleMenu.OnClose += Close;
+ _consoleMenu.OpenCentered();
_consoleMenu.OnServerSelectionButtonPressed += () =>
{
- SendMessage(new AnalysisConsoleServerSelectionMessage());
- };
- _consoleMenu.OnScanButtonPressed += () =>
- {
- SendMessage(new AnalysisConsoleScanButtonPressedMessage());
- };
- _consoleMenu.OnPrintButtonPressed += () =>
- {
- SendMessage(new AnalysisConsolePrintButtonPressedMessage());
+ SendMessage(new ConsoleServerSelectionMessage());
};
_consoleMenu.OnExtractButtonPressed += () =>
{
SendMessage(new AnalysisConsoleExtractButtonPressedMessage());
};
- _consoleMenu.OnUpBiasButtonPressed += () =>
- {
- SendMessage(new AnalysisConsoleBiasButtonPressedMessage(false));
- };
- _consoleMenu.OnDownBiasButtonPressed += () =>
- {
- SendMessage(new AnalysisConsoleBiasButtonPressedMessage(true));
- };
}
- protected override void UpdateState(BoundUserInterfaceState state)
+ ///
+ /// Update UI state based on corresponding component.
+ ///
+ public void Update(Entity ent)
{
- base.UpdateState(state);
-
- switch (state)
- {
- case AnalysisConsoleUpdateState msg:
- _consoleMenu?.SetButtonsDisabled(msg);
- _consoleMenu?.UpdateInformationDisplay(msg);
- _consoleMenu?.UpdateProgressBar(msg);
- break;
- }
+ _consoleMenu?.Update(ent);
}
+ ///
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
diff --git a/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml b/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml
index 29f4a5484790..30cd8647f9a3 100644
--- a/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml
+++ b/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml
@@ -1,80 +1,91 @@
-
+ MinSize="700 350"
+ SetSize="980 550">
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
diff --git a/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml.cs b/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml.cs
index 40a6c6a1d9a8..48573d2b20cc 100644
--- a/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml.cs
+++ b/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml.cs
@@ -1,11 +1,20 @@
-using Content.Client.Stylesheets;
+using System.Linq;
+using System.Text;
+using Content.Client.Message;
+using Content.Client.Resources;
using Content.Client.UserInterface.Controls;
-using Content.Shared.Xenoarchaeology.Equipment;
-using Microsoft.VisualBasic;
+using Content.Client.Xenoarchaeology.Artifact;
+using Content.Client.Xenoarchaeology.Equipment;
+using Content.Shared.Xenoarchaeology.Artifact.Components;
+using Content.Shared.Xenoarchaeology.Equipment.Components;
+using Robust.Client.Audio;
using Robust.Client.AutoGenerated;
-using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.Player;
+using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Audio;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -14,170 +23,233 @@ namespace Content.Client.Xenoarchaeology.Ui;
[GenerateTypedNameReferences]
public sealed partial class AnalysisConsoleMenu : FancyWindow
{
+ /// Time for which extract point messages going to stay on display before screen clear.
+ private static readonly TimeSpan ExtractNonEmptyShowDelaySpan = TimeSpan.FromSeconds(3);
+ /// Time for which zero extracted points message is going to stay on display before screen clear.
+ private static readonly TimeSpan ExtractEmptyShowDelaySpan = TimeSpan.FromSeconds(0.25);
+
[Dependency] private readonly IEntityManager _ent = default!;
+ [Dependency] private readonly IResourceCache _resCache = default!;
+ [Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
- public event Action? OnServerSelectionButtonPressed;
- public event Action? OnScanButtonPressed;
- public event Action? OnPrintButtonPressed;
- public event Action? OnExtractButtonPressed;
- public event Action? OnUpBiasButtonPressed;
- public event Action? OnDownBiasButtonPressed;
+ private readonly ArtifactAnalyzerSystem _artifactAnalyzer;
+ private readonly XenoArtifactSystem _xenoArtifact;
+ private readonly AudioSystem _audio;
- // For rendering the progress bar, updated from BUI state
- private TimeSpan? _startTime;
- private TimeSpan? _totalTime;
- private TimeSpan? _accumulatedRunTime;
+ private readonly Entity _owner;
+ private Entity? _currentNode;
- private bool _paused;
+ /// Queue of node info to output into extraction window.
+ private readonly List<(string NodeId, int ExtractedPoints)> _nodeExtractionsToProcess = new();
+ private TimeSpan? _nextExtractStringTime;
+ private int _extractionSum;
+ private readonly FormattedMessage _extractionMessage = new();
- public AnalysisConsoleMenu()
+ public event Action? OnServerSelectionButtonPressed;
+ public event Action? OnExtractButtonPressed;
+
+ public AnalysisConsoleMenu(EntityUid owner)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
- ServerSelectionButton.OnPressed += _ => OnServerSelectionButtonPressed?.Invoke();
- ScanButton.OnPressed += _ => OnScanButtonPressed?.Invoke();
- PrintButton.OnPressed += _ => OnPrintButtonPressed?.Invoke();
- ExtractButton.OnPressed += _ => OnExtractButtonPressed?.Invoke();
- UpBiasButton.OnPressed += _ => OnUpBiasButtonPressed?.Invoke();
- DownBiasButton.OnPressed += _ => OnDownBiasButtonPressed?.Invoke();
+ _xenoArtifact = _ent.System();
+ _artifactAnalyzer = _ent.System();
+ _audio = _ent.System();
+
+ if (BackPanel.PanelOverride is StyleBoxTexture tex)
+ tex.Texture = _resCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png");
- var buttonGroup = new ButtonGroup(false);
- UpBiasButton.Group = buttonGroup;
- DownBiasButton.Group = buttonGroup;
+ InitStaticLabels();
+
+ GraphControl.OnNodeSelected += node =>
+ {
+ _currentNode = node;
+ SetSelectedNode(node);
+ };
+
+ ServerButton.OnPressed += _ =>
+ {
+ OnServerSelectionButtonPressed?.Invoke();
+ };
+
+ ExtractButton.OnPressed += StartExtract;
+
+ var comp = _ent.GetComponent(owner);
+ _owner = (owner, comp);
+ Update(_owner);
}
- protected override void FrameUpdate(FrameEventArgs args)
+ private void StartExtract(BaseButton.ButtonEventArgs obj)
{
- base.FrameUpdate(args);
-
- if (_startTime is not { } start || _totalTime is not { } total || _accumulatedRunTime is not { } accumulated)
+ if (!_artifactAnalyzer.TryGetArtifactFromConsole(_owner, out var artifact))
return;
- var remaining = total - accumulated;
- if (!_paused)
+ ExtractContainer.Visible = true;
+ NodeViewContainer.Visible = false;
+
+ _nodeExtractionsToProcess.Clear();
+ _extractionSum = 0;
+ _extractionMessage.Clear();
+ _nextExtractStringTime = _timing.CurTime;
+
+ var nodes = _xenoArtifact.GetAllNodes(artifact.Value);
+ foreach (var node in nodes)
{
- // If the analyzer is running, its remaining time is further discounted by the time it's been running for.
- remaining += start - _timing.CurTime;
+ var pointValue = _xenoArtifact.GetResearchValue(node);
+ if (pointValue <= 0)
+ continue;
+
+ var nodeId = _xenoArtifact.GetNodeId(node);
+
+ var text = Loc.GetString("analysis-console-extract-value", ("id", nodeId), ("value", pointValue));
+ _nodeExtractionsToProcess.Add((text, pointValue));
}
- var secsText = Math.Max((int) remaining.TotalSeconds, 0);
- ProgressLabel.Text = Loc.GetString("analysis-console-progress-text",
- ("seconds", secsText));
+ if (_nodeExtractionsToProcess.Count == 0)
+ _nodeExtractionsToProcess.Add((Loc.GetString("analysis-console-extract-none"), 0));
- // 1.0 - div because we want it to tick up not down
- ProgressBar.Value = Math.Clamp(1.0f - (float) remaining.Divide(total), 0.0f, 1.0f);
+ _nodeExtractionsToProcess.Sort((x, y) => x.ExtractedPoints.CompareTo(y.ExtractedPoints));
}
- public void SetButtonsDisabled(AnalysisConsoleUpdateState state)
+ protected override void FrameUpdate(FrameEventArgs args)
{
- ScanButton.Disabled = !state.CanScan;
- PrintButton.Disabled = !state.CanPrint;
- if (state.IsTraversalDown)
- DownBiasButton.Pressed = true;
- else
- UpBiasButton.Pressed = true;
+ base.FrameUpdate(args);
- ExtractButton.Disabled = false;
- if (!state.ServerConnected)
- {
- ExtractButton.Disabled = true;
- ExtractButton.ToolTip = Loc.GetString("analysis-console-no-server-connected");
- }
- else if (!state.CanScan)
- {
- ExtractButton.Disabled = true;
-
- // CanScan can be false if either there's no analyzer connected or if there's
- // no entity on the scanner. The `Information` text will always tell the user
- // of the former case, but in the latter, it'll only show a message if a scan
- // has never been performed, so add a tooltip to indicate that the artifact
- // is gone.
- if (state.AnalyzerConnected)
- {
- ExtractButton.ToolTip = Loc.GetString("analysis-console-no-artifact-placed");
- }
- else
- {
- ExtractButton.ToolTip = null;
- }
- }
- else if (state.PointAmount <= 0)
- {
- ExtractButton.Disabled = true;
- ExtractButton.ToolTip = Loc.GetString("analysis-console-no-points-to-extract");
- }
+ if (_nextExtractStringTime == null || _timing.CurTime < _nextExtractStringTime)
+ return;
- if (ExtractButton.Disabled)
+ if (_nodeExtractionsToProcess.Count == 0)
{
- ExtractButton.RemoveStyleClass("ButtonColorGreen");
+ ExtractContainer.Visible = false;
+ NodeViewContainer.Visible = true;
+ _nextExtractStringTime = null;
+
+ return;
}
- else
+
+ var (message, value) = _nodeExtractionsToProcess.Pop();
+ _extractionMessage.AddMarkupOrThrow(message);
+ _extractionMessage.PushNewline();
+ ExtractionResearchLabel.SetMessage(_extractionMessage);
+
+ var delay = _nodeExtractionsToProcess.Count == 0
+ ? ExtractNonEmptyShowDelaySpan
+ : ExtractEmptyShowDelaySpan;
+ _nextExtractStringTime = _timing.CurTime + delay;
+ _extractionSum += value;
+ ExtractionSumLabel.SetMarkup(Loc.GetString("analysis-console-extract-sum", ("value", _extractionSum)));
+
+ if (_playerManager.LocalSession?.AttachedEntity is { } attachedEntity)
{
- ExtractButton.AddStyleClass("ButtonColorGreen");
- ExtractButton.ToolTip = null;
+ var volume = _nodeExtractionsToProcess.Count == 0 ? 1f : -10f;
+ _audio.PlayGlobal(_owner.Comp.ScanFinishedSound, attachedEntity, AudioParams.Default.WithVolume(volume));
}
+
+ if (_nodeExtractionsToProcess.Count == 0)
+ OnExtractButtonPressed?.Invoke();
}
- private void UpdateArtifactIcon(EntityUid? uid)
+
+ public void Update(Entity ent)
{
- if (uid == null)
- {
- ArtifactDisplay.Visible = false;
- return;
- }
+ _artifactAnalyzer.TryGetArtifactFromConsole(ent, out var arti);
+ ArtifactView.SetEntity(arti);
+ GraphControl.SetArtifact(arti);
+
+ ExtractButton.Disabled = arti == null;
+
+ if (arti == null)
+ NoneSelectedLabel.Visible = false;
+
+ NoArtiLabel.Visible = true;
+ if (!_artifactAnalyzer.TryGetAnalyzer(ent, out _))
+ NoArtiLabel.Text = Loc.GetString("analysis-console-info-no-scanner");
+ else if (arti == null)
+ NoArtiLabel.Text = Loc.GetString("analysis-console-info-no-artifact");
+ else
+ NoArtiLabel.Visible = false;
- ArtifactDisplay.Visible = true;
- ArtifactDisplay.SetEntity(uid);
+ if (_currentNode == null
+ || arti == null
+ || !_xenoArtifact.TryGetIndex((arti.Value, arti.Value), _currentNode.Value, out _))
+ SetSelectedNode(null);
}
- public void UpdateInformationDisplay(AnalysisConsoleUpdateState state)
+ public void SetSelectedNode(Entity? node)
{
- var message = new FormattedMessage();
+ InfoContainer.Visible = node != null;
+ if (!_artifactAnalyzer.TryGetArtifactFromConsole(_owner, out var artifact))
+ return;
- if (state.Scanning)
- {
- if (state.Paused)
- {
- message.AddMarkupOrThrow(Loc.GetString("analysis-console-info-scanner-paused"));
- }
- else
- {
- message.AddMarkupOrThrow(Loc.GetString("analysis-console-info-scanner"));
- }
- Information.SetMessage(message);
- UpdateArtifactIcon(null); //set it to blank
+ NoneSelectedLabel.Visible = node == null;
+
+ if (node == null)
return;
- }
- UpdateArtifactIcon(_ent.GetEntity(state.Artifact));
+ var nodeId = _xenoArtifact.GetNodeId(node.Value);
+ IDValueLabel.SetMarkup(Loc.GetString("analysis-console-info-id-value", ("id", nodeId)));
- if (state.ScanReport == null)
+ // If active, state is 2. else, it is 0 or 1 based on whether it is unlocked, or not.
+ int lockedState;
+ if (_xenoArtifact.IsNodeActive(artifact.Value, node.Value))
+ lockedState = 2;
+ else
+ lockedState = node.Value.Comp.Locked ? 0 : 1;
+
+ LockedValueLabel.SetMarkup(Loc.GetString("analysis-console-info-locked-value",
+ ("state", lockedState)));
+
+ var percent = (float) node.Value.Comp.Durability / node.Value.Comp.MaxDurability;
+ var color = percent switch
+ {
+ >= 0.75f => Color.Lime,
+ >= 0.50f => Color.Yellow,
+ _ => Color.Red
+ };
+ DurabilityValueLabel.SetMarkup(Loc.GetString("analysis-console-info-durability-value",
+ ("color", color),
+ ("current", node.Value.Comp.Durability),
+ ("max", node.Value.Comp.MaxDurability)));
+
+ var hasInfo = _xenoArtifact.HasUnlockedPredecessor(artifact.Value, node.Value);
+
+ EffectValueLabel.SetMarkup(Loc.GetString("analysis-console-info-effect-value",
+ ("state", hasInfo),
+ ("info", _ent.GetComponentOrNull(node.Value)?.EntityDescription ?? string.Empty)));
+
+ var predecessorNodes = _xenoArtifact.GetPredecessorNodes(artifact.Value.Owner, node.Value);
+ if (!hasInfo)
{
- if (!state.AnalyzerConnected) //no analyzer connected
- message.AddMarkupOrThrow(Loc.GetString("analysis-console-info-no-scanner"));
- else if (!state.CanScan) //no artifact
- message.AddMarkupOrThrow(Loc.GetString("analysis-console-info-no-artifact"));
- else if (state.Artifact == null) //ready to go
- message.AddMarkupOrThrow(Loc.GetString("analysis-console-info-ready"));
+ TriggerValueLabel.SetMarkup(Loc.GetString("analysis-console-info-effect-value", ("state", false)));
}
else
{
- message.AddMessage(state.ScanReport);
+ var triggerStr = new StringBuilder();
+ triggerStr.Append("- ");
+ triggerStr.Append(Loc.GetString(node.Value.Comp.TriggerTip!));
+
+ foreach (var predecessor in predecessorNodes)
+ {
+ triggerStr.AppendLine();
+ triggerStr.Append("- ");
+ triggerStr.Append(Loc.GetString(predecessor.Comp.TriggerTip!));
+ }
+ TriggerValueLabel.SetMarkup(Loc.GetString("analysis-console-info-triggered-value", ("triggers", triggerStr.ToString())));
}
- Information.SetMessage(message);
+ ClassValueLabel.SetMarkup(Loc.GetString("analysis-console-info-class-value",
+ ("class", Loc.GetString($"artifact-node-class-{Math.Min(6, predecessorNodes.Count + 1)}"))));
}
- public void UpdateProgressBar(AnalysisConsoleUpdateState state)
+ private void InitStaticLabels()
{
- ProgressBar.Visible = state.Scanning;
- ProgressLabel.Visible = state.Scanning;
-
- _startTime = state.StartTime;
- _totalTime = state.TotalTime;
- _accumulatedRunTime = state.AccumulatedRunTime;
- _paused = state.Paused;
+ IDLabel.SetMarkup(Loc.GetString("analysis-console-info-id"));
+ ClassLabel.SetMarkup(Loc.GetString("analysis-console-info-class"));
+ LockedLabel.SetMarkup(Loc.GetString("analysis-console-info-locked"));
+ EffectLabel.SetMarkup(Loc.GetString("analysis-console-info-effect"));
+ TriggerLabel.SetMarkup(Loc.GetString("analysis-console-info-trigger"));
+ DurabilityLabel.SetMarkup(Loc.GetString("analysis-console-info-durability"));
}
}
diff --git a/Content.Client/Xenoarchaeology/Ui/NodeScannerBoundUserInterface.cs b/Content.Client/Xenoarchaeology/Ui/NodeScannerBoundUserInterface.cs
new file mode 100644
index 000000000000..5ddf921fdaca
--- /dev/null
+++ b/Content.Client/Xenoarchaeology/Ui/NodeScannerBoundUserInterface.cs
@@ -0,0 +1,38 @@
+using Content.Shared.Xenoarchaeology.Equipment.Components;
+
+namespace Content.Client.Xenoarchaeology.Ui;
+
+public sealed class NodeScannerBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
+{
+ [ViewVariables]
+ private NodeScannerDisplay? _scannerDisplay;
+
+ ///
+ protected override void Open()
+ {
+ base.Open();
+
+ _scannerDisplay = new NodeScannerDisplay(Owner);
+ _scannerDisplay.OpenCentered();
+ _scannerDisplay.OnClose += Close;
+ }
+
+ ///
+ /// Update UI state based on corresponding component.
+ ///
+ public void Update(Entity ent)
+ {
+ _scannerDisplay?.Update(ent);
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+
+ if (!disposing)
+ return;
+
+ _scannerDisplay?.Dispose();
+ }
+}
diff --git a/Content.Client/Xenoarchaeology/Ui/NodeScannerDisplay.xaml b/Content.Client/Xenoarchaeology/Ui/NodeScannerDisplay.xaml
new file mode 100644
index 000000000000..448acd02234a
--- /dev/null
+++ b/Content.Client/Xenoarchaeology/Ui/NodeScannerDisplay.xaml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Xenoarchaeology/Ui/NodeScannerDisplay.xaml.cs b/Content.Client/Xenoarchaeology/Ui/NodeScannerDisplay.xaml.cs
new file mode 100644
index 000000000000..becf277a836f
--- /dev/null
+++ b/Content.Client/Xenoarchaeology/Ui/NodeScannerDisplay.xaml.cs
@@ -0,0 +1,75 @@
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Xenoarchaeology.Equipment.Components;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Xenoarchaeology.Ui;
+
+[GenerateTypedNameReferences]
+public sealed partial class NodeScannerDisplay : FancyWindow
+{
+ [Dependency] private readonly IEntityManager _ent = default!;
+
+ public NodeScannerDisplay(EntityUid scannerEntityUid)
+ {
+ RobustXamlLoader.Load(this);
+
+ IoCManager.InjectDependencies(this);
+
+ var scannerComponent = _ent.GetComponent(scannerEntityUid);
+
+ Update((scannerEntityUid, scannerComponent));
+ }
+
+ ///
+ /// Updates labels with scanned artifact data and list of triggered nodes from component.
+ ///
+ public void Update(Entity ent)
+ {
+ ArtifactStateLabel.Text = GetState(ent);
+ var scannedAt = ent.Comp.ScannedAt;
+ NodeScannerState.Text = scannedAt > TimeSpan.Zero
+ ? Loc.GetString("node-scanner-artifact-scanned-time", ("time", scannedAt.Value.ToString(@"hh\:mm\:ss")))
+ : Loc.GetString("node-scanner-artifact-scanned-time-none");
+
+ ActiveNodesList.Children.Clear();
+
+ var triggeredNodesSnapshot = ent.Comp.TriggeredNodesSnapshot;
+ if (triggeredNodesSnapshot.Count > 0)
+ {
+ // show list of triggered nodes instead of 'no data' placeholder
+ NoActiveNodeDataLabel.Visible = false;
+ ActiveNodesList.Visible = true;
+
+ foreach (var nodeId in triggeredNodesSnapshot)
+ {
+ var nodeLabel = new Button
+ {
+ Text = nodeId,
+ Margin = new Thickness(15, 5, 0, 0),
+ MaxHeight = 40,
+ Disabled = true
+ };
+ ActiveNodesList.Children.Add(nodeLabel);
+ }
+ }
+ else
+ {
+ // clear list of activated nodes (done previously), show 'no data' placeholder
+ NoActiveNodeDataLabel.Visible = true;
+ ActiveNodesList.Visible = false;
+ }
+ }
+
+ private string GetState(Entity ent)
+ {
+ return ent.Comp.ArtifactState switch
+ {
+ ArtifactState.None => "\u2800", // placeholder for line to not be squeezed
+ ArtifactState.Ready => Loc.GetString("node-scanner-artifact-state-ready"),
+ ArtifactState.Unlocking => Loc.GetString("node-scanner-artifact-state-unlocking"),
+ ArtifactState.Cooldown => Loc.GetString("node-scanner-artifact-state-cooldown")
+ };
+ }
+}
diff --git a/Content.Client/Xenoarchaeology/Ui/XenoArtifactGraphControl.xaml b/Content.Client/Xenoarchaeology/Ui/XenoArtifactGraphControl.xaml
new file mode 100644
index 000000000000..54133875bdf2
--- /dev/null
+++ b/Content.Client/Xenoarchaeology/Ui/XenoArtifactGraphControl.xaml
@@ -0,0 +1,6 @@
+
diff --git a/Content.Client/Xenoarchaeology/Ui/XenoArtifactGraphControl.xaml.cs b/Content.Client/Xenoarchaeology/Ui/XenoArtifactGraphControl.xaml.cs
new file mode 100644
index 000000000000..d5bc6770a1b0
--- /dev/null
+++ b/Content.Client/Xenoarchaeology/Ui/XenoArtifactGraphControl.xaml.cs
@@ -0,0 +1,208 @@
+using System.Linq;
+using System.Numerics;
+using Content.Client.Xenoarchaeology.Artifact;
+using Content.Shared.Xenoarchaeology.Artifact.Components;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.ResourceManagement;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Input;
+
+namespace Content.Client.Xenoarchaeology.Ui;
+
+[GenerateTypedNameReferences]
+public sealed partial class XenoArtifactGraphControl : BoxContainer
+{
+ [Dependency] private readonly IEntityManager _entityManager = default!;
+
+ private readonly XenoArtifactSystem _artifactSystem;
+
+ private Entity? _artifact;
+
+ private Entity? _hoveredNode;
+
+ private readonly Font _font;
+
+ public event Action>? OnNodeSelected;
+
+ private float NodeRadius => 25 * UIScale;
+ private float NodeDiameter => NodeRadius * 2;
+ private float MinYSpacing => NodeDiameter * 0.75f;
+ private float MaxYSpacing => NodeDiameter * 1.5f;
+ private float MinXSpacing => NodeDiameter * 0.33f;
+ private float MaxXSpacing => NodeDiameter * 1f;
+ private float MinXSegmentSpacing => NodeDiameter * 0.5f;
+ private float MaxXSegmentSpacing => NodeDiameter * 3f;
+
+ public XenoArtifactGraphControl()
+ {
+ IoCManager.InjectDependencies(this);
+ RobustXamlLoader.Load(this);
+
+ _artifactSystem = _entityManager.System();
+
+ var fontResource = IoCManager.Resolve()
+ .GetResource("/EngineFonts/NotoSans/NotoSansMono-Regular.ttf");
+ _font = new VectorFont(fontResource, 16);
+ }
+
+ public Color LockedNodeColor { get; set; } = Color.FromHex("#777777");
+ public Color ActiveNodeColor { get; set; } = Color.Plum;
+ public Color UnlockedNodeColor { get; set; } = Color.White;
+ public Color HoveredNodeColor { get; set; } = Color.DimGray;
+ public Color UnlockableNodeColor { get; set; } = Color.LightSlateGray;
+
+ public void SetArtifact(Entity? artifact)
+ {
+ _artifact = artifact;
+ }
+
+ protected override void KeyBindDown(GUIBoundKeyEventArgs args)
+ {
+ base.KeyBindDown(args);
+
+ if (args.Handled || args.Function != EngineKeyFunctions.UIClick)
+ return;
+
+ if (_hoveredNode == null)
+ return;
+
+ OnNodeSelected?.Invoke(_hoveredNode.Value);
+ UserInterfaceManager.ClickSound();
+ }
+
+ ///
+ /// Renders artifact node graph control, consisting of nodes and edges connecting them.
+ ///
+ protected override void Draw(DrawingHandleScreen handle)
+ {
+ base.Draw(handle);
+
+ _hoveredNode = null;
+ if (_artifact == null)
+ return;
+ var artifact = _artifact.Value;
+
+ var maxDepth = _artifactSystem.GetAllNodes(artifact)
+ .Max(s => s.Comp.Depth);
+ var segments = _artifactSystem.GetSegments(artifact);
+
+ var bottomLeft = Position // the position
+ + new Vector2(0, Size.Y * UIScale) // the scaled height of the control
+ + new Vector2(NodeRadius, -NodeRadius); // offset half a node so we don't render off screen
+
+ var controlHeight = bottomLeft.Y;
+ var controlWidth = Size.X * UIScale - NodeRadius;
+
+ // select y spacing based on max number of nodes we have on Y axis - that is max depth of artifact graph node
+ var ySpacing = 0f;
+ if (maxDepth != 0)
+ ySpacing = Math.Clamp((controlHeight - ((maxDepth + 1) * NodeDiameter)) / maxDepth, MinYSpacing, MaxYSpacing);
+
+ // gets settings for visualizing segments (groups of interconnected nodes - there may be 1 or more per artifact).
+ var segmentWidths = segments.Sum(GetBiggestWidth);
+ var segmentSpacing = Math.Clamp((controlWidth - segmentWidths) / (segments.Count - 1), MinXSegmentSpacing, MaxXSegmentSpacing);
+ var segmentOffset = Math.Max((controlWidth - (segmentWidths) - (segmentSpacing * (segments.Count - 1))) / 2, 0);
+
+ bottomLeft.X += segmentOffset;
+ bottomLeft.Y -= (controlHeight - (ySpacing * maxDepth) - (NodeDiameter * (maxDepth + 1))) / 2;
+
+ var cursor = (UserInterfaceManager.MousePositionScaled.Position * UIScale) - GlobalPixelPosition;
+
+ foreach (var segment in segments)
+ {
+ // For each segment we draw nodes in order of depth. Method returns List of nodes for each depth level.
+ var orderedNodes = _artifactSystem.GetDepthOrderedNodes(segment);
+ foreach (var (_, nodes) in orderedNodes)
+ {
+ for (var i = 0; i < nodes.Count; i++)
+ {
+ // selecting color for node based on its state
+ var node = nodes[i];
+ var color = LockedNodeColor;
+ if (_artifactSystem.IsNodeActive(artifact, node))
+ {
+ color = ActiveNodeColor;
+ }
+ else if (!node.Comp.Locked)
+ {
+ color = UnlockedNodeColor;
+ }
+ else
+ {
+ var directPredecessorNodes = _artifactSystem.GetDirectPredecessorNodes((artifact, artifact), node);
+ if (directPredecessorNodes.Count == 0 || directPredecessorNodes.All(x => !x.Comp.Locked))
+ {
+ color = UnlockableNodeColor;
+ }
+ }
+
+ var pos = GetNodePos(node, ySpacing, segments, ref bottomLeft);
+ var hovered = (cursor - pos).LengthSquared() <= NodeRadius * NodeRadius;
+ if (hovered)
+ {
+ // render hovered node if we have one
+ _hoveredNode = node;
+ handle.DrawCircle(pos, NodeRadius, HoveredNodeColor);
+ }
+
+ // render circle and text with node id inside
+ handle.DrawCircle(pos, NodeRadius, Color.ToSrgb(color), false);
+
+ var text = _artifactSystem.GetNodeId(node);
+ var dimensions = handle.GetDimensions(_font, text, 1);
+ handle.DrawString(_font, pos - new Vector2(dimensions.X / 2, dimensions.Y / 2), text, color);
+ }
+ }
+
+ // draw edges for each segment and each node that have successors
+ foreach (var node in segment)
+ {
+ var from = GetNodePos(node, ySpacing, segments, ref bottomLeft) + new Vector2(0, -NodeRadius);
+ var successorNodes = _artifactSystem.GetDirectSuccessorNodes((artifact, artifact), node);
+ foreach (var successorNode in successorNodes)
+ {
+ var color = node.Comp.Locked
+ ? LockedNodeColor
+ : UnlockedNodeColor;
+
+ var to = GetNodePos(successorNode, ySpacing, segments, ref bottomLeft) + new Vector2(0, NodeRadius);
+ handle.DrawLine(from, to, color);
+ }
+ }
+
+ bottomLeft.X += GetBiggestWidth(segment) + segmentSpacing;
+ }
+ }
+
+ private Vector2 GetNodePos(Entity node, float ySpacing, List>> segments, ref Vector2 bottomLeft)
+ {
+ var yPos = -(NodeDiameter + ySpacing) * node.Comp.Depth;
+
+ var segment = segments.First(s => s.Contains(node));
+ var depthOrderedNodes = _artifactSystem.GetDepthOrderedNodes(segment);
+ var biggestTier = depthOrderedNodes.Max(s => s.Value.Count);
+ var nodesInLayer = depthOrderedNodes.GetValueOrDefault(node.Comp.Depth)!.Count;
+ var biggestWidth = (NodeDiameter + MinXSpacing) * biggestTier;
+
+ var xSpacing = Math.Clamp((biggestWidth - (NodeDiameter * nodesInLayer)) / (nodesInLayer - 1), MinXSpacing, MaxXSpacing);
+ var layerXOffset = (biggestWidth - (xSpacing * (nodesInLayer - 1)) - (NodeDiameter * nodesInLayer)) / 2;
+
+ // get index of node in current segment's row (row per depth level)
+ var index = depthOrderedNodes.GetValueOrDefault(node.Comp.Depth)!.IndexOf(node);
+
+ var xPos = NodeDiameter * index + (xSpacing * index) + layerXOffset;
+
+ return bottomLeft + new Vector2(xPos, yPos);
+ }
+
+ private float GetBiggestWidth(List> nodes)
+ {
+ var num = _artifactSystem.GetDepthOrderedNodes(nodes)
+ .Max(p => p.Value.Count);
+ return (NodeDiameter * num) + MinXSpacing * (num - 1);
+ }
+}
+
diff --git a/Content.Client/Xenoarchaeology/XenoArtifacts/RandomArtifactSpriteSystem.cs b/Content.Client/Xenoarchaeology/XenoArtifacts/RandomArtifactSpriteSystem.cs
index 3be9b027484b..b02c44f920c7 100644
--- a/Content.Client/Xenoarchaeology/XenoArtifacts/RandomArtifactSpriteSystem.cs
+++ b/Content.Client/Xenoarchaeology/XenoArtifacts/RandomArtifactSpriteSystem.cs
@@ -1,4 +1,4 @@
-using Content.Shared.Xenoarchaeology.XenoArtifacts;
+using Content.Shared.Xenoarchaeology.XenoArtifacts;
using Robust.Client.GameObjects;
namespace Content.Client.Xenoarchaeology.XenoArtifacts;
@@ -13,19 +13,28 @@ protected override void OnAppearanceChange(EntityUid uid, RandomArtifactSpriteCo
if (!AppearanceSystem.TryGetData(uid, SharedArtifactsVisuals.SpriteIndex, out var spriteIndex, args.Component))
return;
+ if (!AppearanceSystem.TryGetData(uid, SharedArtifactsVisuals.IsUnlocking, out var isUnlocking, args.Component))
+ isUnlocking = false;
+
if (!AppearanceSystem.TryGetData(uid, SharedArtifactsVisuals.IsActivated, out var isActivated, args.Component))
isActivated = false;
var spriteIndexStr = spriteIndex.ToString("D2");
- var spritePrefix = isActivated ? "_on" : "";
+ var spritePrefix = isUnlocking ? "_on" : "";
// layered artifact sprite
- if (args.Sprite.LayerMapTryGet(ArtifactsVisualLayers.Effect, out var layer))
+ if (args.Sprite.LayerMapTryGet(ArtifactsVisualLayers.UnlockingEffect, out var layer))
{
var spriteState = "ano" + spriteIndexStr;
args.Sprite.LayerSetState(ArtifactsVisualLayers.Base, spriteState);
args.Sprite.LayerSetState(layer, spriteState + "_on");
- args.Sprite.LayerSetVisible(layer, isActivated);
+ args.Sprite.LayerSetVisible(layer, isUnlocking);
+
+ if (args.Sprite.LayerMapTryGet(ArtifactsVisualLayers.ActivationEffect, out var activationEffectLayer))
+ {
+ args.Sprite.LayerSetState(activationEffectLayer, "artifact-activation");
+ args.Sprite.LayerSetVisible(activationEffectLayer, isActivated);
+ }
}
// non-layered
else
@@ -33,12 +42,12 @@ protected override void OnAppearanceChange(EntityUid uid, RandomArtifactSpriteCo
var spriteState = "ano" + spriteIndexStr + spritePrefix;
args.Sprite.LayerSetState(ArtifactsVisualLayers.Base, spriteState);
}
-
}
}
public enum ArtifactsVisualLayers : byte
{
Base,
- Effect // doesn't have to use this
+ UnlockingEffect, // doesn't have to use this
+ ActivationEffect
}
diff --git a/Content.IntegrationTests/Tests/XenoArtifactTest.cs b/Content.IntegrationTests/Tests/XenoArtifactTest.cs
new file mode 100644
index 000000000000..ac4c58c52cce
--- /dev/null
+++ b/Content.IntegrationTests/Tests/XenoArtifactTest.cs
@@ -0,0 +1,419 @@
+using System.Linq;
+using Content.Shared.Xenoarchaeology.Artifact;
+using Content.Shared.Xenoarchaeology.Artifact.Components;
+using Robust.Shared.GameObjects;
+
+namespace Content.IntegrationTests.Tests;
+
+[TestFixture]
+public sealed class XenoArtifactTest
+{
+ [TestPrototypes]
+ private const string Prototypes = @"
+- type: entity
+ id: TestArtifact
+ parent: BaseXenoArtifact
+ name: artifact
+ components:
+ - type: XenoArtifact
+ isGenerationRequired: false
+ effectsTable: !type:NestedSelector
+ tableId: XenoArtifactEffectsDefaultTable
+
+- type: entity
+ id: TestGenArtifactFlat
+ parent: BaseXenoArtifact
+ name: artifact
+ components:
+ - type: XenoArtifact
+ isGenerationRequired: true
+ nodeCount:
+ min: 2
+ max: 2
+ segmentSize:
+ min: 1
+ max: 1
+ nodesPerSegmentLayer:
+ min: 1
+ max: 1
+ effectsTable: !type:NestedSelector
+ tableId: XenoArtifactEffectsDefaultTable
+
+- type: entity
+ id: TestGenArtifactTall
+ parent: BaseXenoArtifact
+ name: artifact
+ components:
+ - type: XenoArtifact
+ isGenerationRequired: true
+ nodeCount:
+ min: 2
+ max: 2
+ segmentSize:
+ min: 2
+ max: 2
+ nodesPerSegmentLayer:
+ min: 1
+ max: 1
+ effectsTable: !type:NestedSelector
+ tableId: XenoArtifactEffectsDefaultTable
+
+- type: entity
+ id: TestGenArtifactFull
+ name: artifact
+ components:
+ - type: XenoArtifact
+ isGenerationRequired: true
+ nodeCount:
+ min: 6
+ max: 6
+ segmentSize:
+ min: 6
+ max: 6
+ nodesPerSegmentLayer:
+ min: 2
+ max: 2
+ effectsTable: !type:NestedSelector
+ tableId: XenoArtifactEffectsDefaultTable
+
+- type: entity
+ id: TestArtifactNode
+ name: artifact node
+ components:
+ - type: XenoArtifactNode
+ maxDurability: 3
+";
+
+ ///
+ /// Checks that adding nodes and edges properly adds them into the adjacency matrix
+ ///
+ [Test]
+ public async Task XenoArtifactAddNodeTest()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var entManager = server.ResolveDependency();
+ var artifactSystem = entManager.System();
+
+ await server.WaitPost(() =>
+ {
+ var artifactUid = entManager.Spawn("TestArtifact");
+ var artifactEnt = (artifactUid, comp: entManager.GetComponent(artifactUid));
+
+ // Create 3 nodes
+ Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node1, false));
+ Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node2, false));
+ Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node3, false));
+
+ Assert.That(artifactSystem.GetAllNodeIndices(artifactEnt).Count(), Is.EqualTo(3));
+
+ // Add connection from 1 -> 2 and 2-> 3
+ artifactSystem.AddEdge(artifactEnt, node1!.Value, node2!.Value, false);
+ artifactSystem.AddEdge(artifactEnt, node2!.Value, node3!.Value, false);
+
+ // Assert that successors and direct successors are counted correctly for node 1.
+ Assert.That(artifactSystem.GetDirectSuccessorNodes(artifactEnt, node1!.Value).Count, Is.EqualTo(1));
+ Assert.That(artifactSystem.GetSuccessorNodes(artifactEnt, node1!.Value).Count, Is.EqualTo(2));
+ // Assert that we didn't somehow get predecessors on node 1.
+ Assert.That(artifactSystem.GetDirectPredecessorNodes(artifactEnt, node1!.Value), Is.Empty);
+ Assert.That(artifactSystem.GetPredecessorNodes(artifactEnt, node1!.Value), Is.Empty);
+
+ // Assert that successors and direct successors are counted correctly for node 2.
+ Assert.That(artifactSystem.GetDirectSuccessorNodes(artifactEnt, node2!.Value), Has.Count.EqualTo(1));
+ Assert.That(artifactSystem.GetSuccessorNodes(artifactEnt, node2!.Value), Has.Count.EqualTo(1));
+ // Assert that predecessors and direct predecessors are counted correctly for node 2.
+ Assert.That(artifactSystem.GetDirectPredecessorNodes(artifactEnt, node2!.Value), Has.Count.EqualTo(1));
+ Assert.That(artifactSystem.GetPredecessorNodes(artifactEnt, node2!.Value), Has.Count.EqualTo(1));
+
+ // Assert that successors and direct successors are counted correctly for node 3.
+ Assert.That(artifactSystem.GetDirectSuccessorNodes(artifactEnt, node3!.Value), Is.Empty);
+ Assert.That(artifactSystem.GetSuccessorNodes(artifactEnt, node3!.Value), Is.Empty);
+ // Assert that predecessors and direct predecessors are counted correctly for node 3.
+ Assert.That(artifactSystem.GetDirectPredecessorNodes(artifactEnt, node3!.Value), Has.Count.EqualTo(1));
+ Assert.That(artifactSystem.GetPredecessorNodes(artifactEnt, node3!.Value), Has.Count.EqualTo(2));
+ });
+ await server.WaitRunTicks(1);
+
+ await pair.CleanReturnAsync();
+ }
+
+ ///
+ /// Checks to make sure that removing nodes properly cleans up all connections.
+ ///
+ [Test]
+ public async Task XenoArtifactRemoveNodeTest()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var entManager = server.ResolveDependency();
+ var artifactSystem = entManager.System();
+
+ await server.WaitPost(() =>
+ {
+ var artifactUid = entManager.Spawn("TestArtifact");
+ var artifactEnt = (artifactUid, comp: entManager.GetComponent(artifactUid));
+
+ // Create 3 nodes
+ Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node1, false));
+ Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node2, false));
+ Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node3, false));
+ Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node4, false));
+ Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node5, false));
+
+ Assert.That(artifactSystem.GetAllNodeIndices(artifactEnt).Count(), Is.EqualTo(5));
+
+ // Add connection: 1 -> 2 -> 3 -> 4 -> 5
+ artifactSystem.AddEdge(artifactEnt, node1!.Value, node2!.Value, false);
+ artifactSystem.AddEdge(artifactEnt, node2!.Value, node3!.Value, false);
+ artifactSystem.AddEdge(artifactEnt, node3!.Value, node4!.Value, false);
+ artifactSystem.AddEdge(artifactEnt, node4!.Value, node5!.Value, false);
+
+ // Make sure we have a continuous connection between the two ends of the graph.
+ Assert.That(artifactSystem.GetSuccessorNodes(artifactEnt, node1.Value), Has.Count.EqualTo(4));
+ Assert.That(artifactSystem.GetPredecessorNodes(artifactEnt, node5.Value), Has.Count.EqualTo(4));
+
+ // Remove the node and make sure it's no longer in the artifact.
+ Assert.That(artifactSystem.RemoveNode(artifactEnt, node3!.Value, false));
+ Assert.That(artifactSystem.TryGetIndex(artifactEnt, node3!.Value, out _), Is.False, "Node 3 still present in artifact.");
+
+ // Check to make sure that we got rid of all the connections.
+ Assert.That(artifactSystem.GetSuccessorNodes(artifactEnt, node2!.Value), Is.Empty);
+ Assert.That(artifactSystem.GetPredecessorNodes(artifactEnt, node4!.Value), Is.Empty);
+ });
+ await server.WaitRunTicks(1);
+
+ await pair.CleanReturnAsync();
+ }
+
+ ///
+ /// Sets up series of linked nodes and ensures that resizing the adjacency matrix doesn't disturb the connections
+ ///
+ [Test]
+ public async Task XenoArtifactResizeTest()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var entManager = server.ResolveDependency();
+ var artifactSystem = entManager.System();
+
+ await server.WaitPost(() =>
+ {
+ var artifactUid = entManager.Spawn("TestArtifact");
+ var artifactEnt = (artifactUid, comp: entManager.GetComponent(artifactUid));
+
+ // Create 3 nodes
+ Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node1, false));
+ Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node2, false));
+ Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node3, false));
+
+ // Add connection: 1 -> 2 -> 3
+ artifactSystem.AddEdge(artifactEnt, node1!.Value, node2!.Value, false);
+ artifactSystem.AddEdge(artifactEnt, node2!.Value, node3!.Value, false);
+
+ // Make sure our connection is set up
+ Assert.That(artifactSystem.NodeHasEdge(artifactEnt, node1.Value, node2.Value));
+ Assert.That(artifactSystem.NodeHasEdge(artifactEnt, node2.Value, node3.Value));
+ Assert.That(artifactSystem.NodeHasEdge(artifactEnt, node2.Value, node1.Value), Is.False);
+ Assert.That(artifactSystem.NodeHasEdge(artifactEnt, node3.Value, node2.Value), Is.False);
+ Assert.That(artifactSystem.NodeHasEdge(artifactEnt, node1.Value, node3.Value), Is.False);
+ Assert.That(artifactSystem.NodeHasEdge(artifactEnt, node3.Value, node1.Value), Is.False);
+
+ Assert.That(artifactSystem.GetIndex(artifactEnt, node1!.Value), Is.EqualTo(0));
+ Assert.That(artifactSystem.GetIndex(artifactEnt, node2!.Value), Is.EqualTo(1));
+ Assert.That(artifactSystem.GetIndex(artifactEnt, node3!.Value), Is.EqualTo(2));
+
+ // Add a new node, resizing the original adjacency matrix and array.
+ Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node4));
+
+ // Check that our connections haven't changed.
+ Assert.That(artifactSystem.NodeHasEdge(artifactEnt, node1.Value, node2.Value));
+ Assert.That(artifactSystem.NodeHasEdge(artifactEnt, node2.Value, node3.Value));
+ Assert.That(artifactSystem.NodeHasEdge(artifactEnt, node2.Value, node1.Value), Is.False);
+ Assert.That(artifactSystem.NodeHasEdge(artifactEnt, node3.Value, node2.Value), Is.False);
+ Assert.That(artifactSystem.NodeHasEdge(artifactEnt, node1.Value, node3.Value), Is.False);
+ Assert.That(artifactSystem.NodeHasEdge(artifactEnt, node3.Value, node1.Value), Is.False);
+
+ // Has our array shifted any when we resized?
+ Assert.That(artifactSystem.GetIndex(artifactEnt, node1!.Value), Is.EqualTo(0));
+ Assert.That(artifactSystem.GetIndex(artifactEnt, node2!.Value), Is.EqualTo(1));
+ Assert.That(artifactSystem.GetIndex(artifactEnt, node3!.Value), Is.EqualTo(2));
+
+ // Check that 4 didn't somehow end up with connections
+ Assert.That(artifactSystem.GetPredecessorNodes(artifactEnt, node4!.Value), Is.Empty);
+ Assert.That(artifactSystem.GetSuccessorNodes(artifactEnt, node4!.Value), Is.Empty);
+ });
+ await server.WaitRunTicks(1);
+
+ await pair.CleanReturnAsync();
+ }
+
+ ///
+ /// Checks if removing a node and adding a new node into its place in the adjacency matrix doesn't accidentally retain extra data.
+ ///
+ [Test]
+ public async Task XenoArtifactReplaceTest()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var entManager = server.ResolveDependency();
+ var artifactSystem = entManager.System();
+
+ await server.WaitPost(() =>
+ {
+ var artifactUid = entManager.Spawn("TestArtifact");
+ var artifactEnt = (artifactUid, comp: entManager.GetComponent(artifactUid));
+
+ // Create 3 nodes
+ Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node1, false));
+ Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node2, false));
+ Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node3, false));
+
+ // Add connection: 1 -> 2 -> 3
+ artifactSystem.AddEdge(artifactEnt, node1!.Value, node2!.Value, false);
+ artifactSystem.AddEdge(artifactEnt, node2!.Value, node3!.Value, false);
+
+ // Make sure our connection is set up
+ Assert.That(artifactSystem.NodeHasEdge(artifactEnt, node1.Value, node2.Value));
+ Assert.That(artifactSystem.NodeHasEdge(artifactEnt, node2.Value, node3.Value));
+
+ // Remove middle node, severing connections
+ artifactSystem.RemoveNode(artifactEnt, node2!.Value, false);
+
+ // Make sure our connection are properly severed.
+ Assert.That(artifactSystem.GetSuccessorNodes(artifactEnt, node1.Value), Is.Empty);
+ Assert.That(artifactSystem.GetPredecessorNodes(artifactEnt, node3.Value), Is.Empty);
+
+ // Make sure our matrix is 3x3
+ Assert.That(artifactEnt.Item2.NodeAdjacencyMatrixRows, Is.EqualTo(3));
+ Assert.That(artifactEnt.Item2.NodeAdjacencyMatrixColumns, Is.EqualTo(3));
+
+ Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node4, false));
+
+ // Make sure that adding in a new node didn't add a new slot but instead re-used the middle slot.
+ Assert.That(artifactEnt.Item2.NodeAdjacencyMatrixRows, Is.EqualTo(3));
+ Assert.That(artifactEnt.Item2.NodeAdjacencyMatrixColumns, Is.EqualTo(3));
+
+ // Ensure that all connections are still severed
+ Assert.That(artifactSystem.GetSuccessorNodes(artifactEnt, node1.Value), Is.Empty);
+ Assert.That(artifactSystem.GetPredecessorNodes(artifactEnt, node3.Value), Is.Empty);
+ Assert.That(artifactSystem.GetSuccessorNodes(artifactEnt, node4!.Value), Is.Empty);
+ Assert.That(artifactSystem.GetPredecessorNodes(artifactEnt, node4!.Value), Is.Empty);
+
+ });
+ await server.WaitRunTicks(1);
+
+ await pair.CleanReturnAsync();
+ }
+
+ ///
+ /// Checks if the active nodes are properly detected.
+ ///
+ [Test]
+ public async Task XenoArtifactBuildActiveNodesTest()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var entManager = server.ResolveDependency();
+ var artifactSystem = entManager.System();
+
+ await server.WaitPost(() =>
+ {
+ var artifactUid = entManager.Spawn("TestArtifact");
+ Entity artifactEnt = (artifactUid, entManager.GetComponent(artifactUid));
+
+ Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node1, false));
+ Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node2, false));
+ Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node3, false));
+ Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node4, false));
+ Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node5, false));
+ Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node6, false));
+ Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node7, false));
+ Assert.That(artifactSystem.AddNode(artifactEnt, "TestArtifactNode", out var node8, false));
+
+ // /----( 6 )
+ // /----[*3 ]-/----( 7 )----( 8 )
+ // /
+ // / /----[*5 ]
+ // [ 1 ]--/----[ 2 ]--/----( 4 )
+ // Diagram of the example generation. Nodes in [brackets] are unlocked, nodes in (braces) are locked
+ // and nodes with an *asterisk are supposed to be active.
+ artifactSystem.AddEdge(artifactEnt, node1!.Value, node2!.Value, false);
+ artifactSystem.AddEdge(artifactEnt, node1!.Value, node3!.Value, false);
+
+ artifactSystem.AddEdge(artifactEnt, node2!.Value, node4!.Value, false);
+ artifactSystem.AddEdge(artifactEnt, node2!.Value, node5!.Value, false);
+
+ artifactSystem.AddEdge(artifactEnt, node3!.Value, node6!.Value, false);
+ artifactSystem.AddEdge(artifactEnt, node3!.Value, node7!.Value, false);
+
+ artifactSystem.AddEdge(artifactEnt, node7!.Value, node8!.Value, false);
+
+ artifactSystem.SetNodeUnlocked(node1!.Value);
+ artifactSystem.SetNodeUnlocked(node2!.Value);
+ artifactSystem.SetNodeUnlocked(node3!.Value);
+ artifactSystem.SetNodeUnlocked(node5!.Value);
+
+ NetEntity[] expectedActiveNodes =
+ [
+ entManager.GetNetEntity(node3!.Value.Owner),
+ entManager.GetNetEntity(node5!.Value.Owner)
+ ];
+ Assert.That(artifactEnt.Comp.CachedActiveNodes, Is.SupersetOf(expectedActiveNodes));
+ Assert.That(artifactEnt.Comp.CachedActiveNodes, Has.Count.EqualTo(expectedActiveNodes.Length));
+
+ });
+ await server.WaitRunTicks(1);
+
+ await pair.CleanReturnAsync();
+ }
+
+ [Test]
+ public async Task XenoArtifactGenerateSegmentsTest()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var entManager = server.ResolveDependency();
+ var artifactSystem = entManager.System();
+
+ await server.WaitPost(() =>
+ {
+ var artifact1Uid = entManager.Spawn("TestGenArtifactFlat");
+ Entity artifact1Ent = (artifact1Uid, entManager.GetComponent(artifact1Uid));
+
+ var segments1 = artifactSystem.GetSegments(artifact1Ent);
+ Assert.That(segments1.Count, Is.EqualTo(2));
+ Assert.That(segments1[0].Count, Is.EqualTo(1));
+ Assert.That(segments1[1].Count, Is.EqualTo(1));
+
+ var artifact2Uid = entManager.Spawn("TestGenArtifactTall");
+ Entity artifact2Ent = (artifact2Uid, entManager.GetComponent(artifact2Uid));
+
+ var segments2 = artifactSystem.GetSegments(artifact2Ent);
+ Assert.That(segments2.Count, Is.EqualTo(1));
+ Assert.That(segments2[0].Count, Is.EqualTo(2));
+
+ var artifact3Uid = entManager.Spawn("TestGenArtifactFull");
+ Entity artifact3Ent = (artifact3Uid, entManager.GetComponent(artifact3Uid));
+
+ var segments3 = artifactSystem.GetSegments(artifact3Ent);
+ Assert.That(segments3.Count, Is.EqualTo(1));
+ Assert.That(segments3.Sum(x => x.Count), Is.EqualTo(6));
+ var nodesDepths = segments3[0].Select(x => x.Comp.Depth).ToArray();
+ Assert.That(nodesDepths.Distinct().Count(), Is.EqualTo(3));
+ var grouped = nodesDepths.ToLookup(x => x);
+ Assert.That(grouped[0].Count(), Is.EqualTo(2));
+ Assert.That(grouped[1].Count(), Is.GreaterThanOrEqualTo(2)); // tree is attempting sometimes to get wider (so it will look like a tree)
+ Assert.That(grouped[2].Count(), Is.LessThanOrEqualTo(2)); // maintain same width or, if we used 3 nodes on previous layer - we only have 1 left!
+
+ });
+ await server.WaitRunTicks(1);
+
+ await pair.CleanReturnAsync();
+ }
+}
diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.cs b/Content.Server/Administration/Systems/AdminVerbSystem.cs
index 39ae699683da..6ce881328dda 100644
--- a/Content.Server/Administration/Systems/AdminVerbSystem.cs
+++ b/Content.Server/Administration/Systems/AdminVerbSystem.cs
@@ -10,8 +10,6 @@
using Content.Server.Mind.Commands;
using Content.Server.Prayer;
using Content.Server.Station.Systems;
-using Content.Server.Xenoarchaeology.XenoArtifacts;
-using Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components;
using Content.Shared.Administration;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
@@ -60,7 +58,6 @@ public sealed partial class AdminVerbSystem : EntitySystem
[Dependency] private readonly EuiManager _euiManager = default!;
[Dependency] private readonly GameTicker _ticker = default!;
[Dependency] private readonly GhostRoleSystem _ghostRoleSystem = default!;
- [Dependency] private readonly ArtifactSystem _artifactSystem = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly PrayerSystem _prayerSystem = default!;
[Dependency] private readonly MindSystem _mindSystem = default!;
@@ -447,29 +444,6 @@ private void AddDebugVerbs(GetVerbsEvent args)
args.Verbs.Add(verb);
}
- // XenoArcheology
- if (_adminManager.IsAdmin(player) && TryComp(args.Target, out var artifact))
- {
- // make artifact always active (by adding timer trigger)
- args.Verbs.Add(new Verb()
- {
- Text = Loc.GetString("artifact-verb-make-always-active"),
- Category = VerbCategory.Debug,
- Act = () => EntityManager.AddComponent(args.Target),
- Disabled = EntityManager.HasComponent(args.Target),
- Impact = LogImpact.High
- });
-
- // force to activate artifact ignoring timeout
- args.Verbs.Add(new Verb()
- {
- Text = Loc.GetString("artifact-verb-activate"),
- Category = VerbCategory.Debug,
- Act = () => _artifactSystem.ForceActivateArtifact(args.Target, component: artifact),
- Impact = LogImpact.High
- });
- }
-
// Make Sentient verb
if (_groupController.CanCommand(player, "makesentient") &&
args.User != args.Target &&
diff --git a/Content.Server/Emp/EmpSystem.cs b/Content.Server/Emp/EmpSystem.cs
index d8eab0c5d1f1..4b5143aa4002 100644
--- a/Content.Server/Emp/EmpSystem.cs
+++ b/Content.Server/Emp/EmpSystem.cs
@@ -44,6 +44,22 @@ public void EmpPulse(MapCoordinates coordinates, float range, float energyConsum
Spawn(EmpPulseEffectPrototype, coordinates);
}
+ ///
+ /// Triggers an EMP pulse at the given location, by first raising an , then a raising on all entities in range.
+ ///
+ /// The location to trigger the EMP pulse at.
+ /// The range of the EMP pulse.
+ /// The amount of energy consumed by the EMP pulse.
+ /// The duration of the EMP effects.
+ public void EmpPulse(EntityCoordinates coordinates, float range, float energyConsumption, float duration)
+ {
+ foreach (var uid in _lookup.GetEntitiesInRange(coordinates, range))
+ {
+ TryEmpEffects(uid, energyConsumption, duration);
+ }
+ Spawn(EmpPulseEffectPrototype, coordinates);
+ }
+
///
/// Attempts to apply the effects of an EMP pulse onto an entity by first raising an , followed by raising a on it.
///
diff --git a/Content.Server/EntityEffects/Effects/ActivateArtifact.cs b/Content.Server/EntityEffects/Effects/ActivateArtifact.cs
deleted file mode 100644
index 8540478362fc..000000000000
--- a/Content.Server/EntityEffects/Effects/ActivateArtifact.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using Content.Server.Xenoarchaeology.XenoArtifacts;
-using Content.Shared.Chemistry.Reagent;
-using Robust.Shared.Prototypes;
-using Content.Shared.EntityEffects;
-
-namespace Content.Server.EntityEffects.Effects;
-
-public sealed partial class ActivateArtifact : EntityEffect
-{
- public override void Effect(EntityEffectBaseArgs args)
- {
- var artifact = args.EntityManager.EntitySysManager.GetEntitySystem();
- artifact.TryActivateArtifact(args.TargetEntity, logMissing: false);
- }
-
- protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) =>
- Loc.GetString("reagent-effect-guidebook-activate-artifact", ("chance", Probability));
-}
diff --git a/Content.Server/Instruments/InstrumentComponent.cs b/Content.Server/Instruments/InstrumentComponent.cs
index db9dbb375bca..ab1fd2ce0469 100644
--- a/Content.Server/Instruments/InstrumentComponent.cs
+++ b/Content.Server/Instruments/InstrumentComponent.cs
@@ -1,4 +1,3 @@
-using Content.Server.UserInterface;
using Content.Shared.Instruments;
using Robust.Shared.Player;
using ActivatableUIComponent = Content.Shared.UserInterface.ActivatableUIComponent;
@@ -21,8 +20,3 @@ public sealed partial class InstrumentComponent : SharedInstrumentComponent
_entMan.GetComponentOrNull(Owner)?.CurrentSingleUser
?? _entMan.GetComponentOrNull(Owner)?.PlayerSession.AttachedEntity;
}
-
-[RegisterComponent]
-public sealed partial class ActiveInstrumentComponent : Component
-{
-}
diff --git a/Content.Server/PAI/PAISystem.cs b/Content.Server/PAI/PAISystem.cs
index b0f4f2476d8a..fd581f40e43c 100644
--- a/Content.Server/PAI/PAISystem.cs
+++ b/Content.Server/PAI/PAISystem.cs
@@ -8,6 +8,7 @@
using Content.Shared.Popups;
using Robust.Shared.Random;
using System.Text;
+using Content.Shared.Instruments;
using Robust.Shared.Player;
namespace Content.Server.PAI;
diff --git a/Content.Server/Radiation/Systems/RadiationSystem.cs b/Content.Server/Radiation/Systems/RadiationSystem.cs
index 3ba393959c25..7402a7270101 100644
--- a/Content.Server/Radiation/Systems/RadiationSystem.cs
+++ b/Content.Server/Radiation/Systems/RadiationSystem.cs
@@ -1,4 +1,4 @@
-using Content.Server.Radiation.Components;
+using Content.Server.Radiation.Components;
using Content.Shared.Radiation.Components;
using Content.Shared.Radiation.Events;
using Content.Shared.Stacks;
@@ -50,7 +50,7 @@ public override void Update(float frameTime)
public void IrradiateEntity(EntityUid uid, float radsPerSecond, float time)
{
- var msg = new OnIrradiatedEvent(time, radsPerSecond);
+ var msg = new OnIrradiatedEvent(time, radsPerSecond, uid);
RaiseLocalEvent(uid, msg);
}
diff --git a/Content.Server/StationEvents/Events/VentClogRule.cs b/Content.Server/StationEvents/Events/VentClogRule.cs
index f14958631d1c..3b6eeebe8515 100644
--- a/Content.Server/StationEvents/Events/VentClogRule.cs
+++ b/Content.Server/StationEvents/Events/VentClogRule.cs
@@ -8,6 +8,7 @@
using JetBrains.Annotations;
using Robust.Shared.Random;
using System.Linq;
+using Content.Shared.Chemistry.Reaction;
namespace Content.Server.StationEvents.Events;
@@ -47,7 +48,7 @@ protected override void Started(EntityUid uid, VentClogRuleComponent component,
var quantity = weak ? component.WeakReagentQuantity : component.ReagentQuantity;
solution.AddReagent(reagent, quantity);
- var foamEnt = Spawn("Foam", transform.Coordinates);
+ var foamEnt = Spawn(ChemicalReactionSystem.FoamReaction, transform.Coordinates);
var spreadAmount = weak ? component.WeakSpread : component.Spread;
_smoke.StartSmoke(foamEnt, solution, component.Time, spreadAmount);
Audio.PlayPvs(component.Sound, transform.Coordinates);
diff --git a/Content.Server/Xenoarchaeology/XenoArtifacts/RandomArtifactSpriteSystem.cs b/Content.Server/Xenoarchaeology/Artifact/RandomArtifactSpriteSystem.cs
similarity index 64%
rename from Content.Server/Xenoarchaeology/XenoArtifacts/RandomArtifactSpriteSystem.cs
rename to Content.Server/Xenoarchaeology/Artifact/RandomArtifactSpriteSystem.cs
index 091441df21a7..645dac24e207 100644
--- a/Content.Server/Xenoarchaeology/XenoArtifacts/RandomArtifactSpriteSystem.cs
+++ b/Content.Server/Xenoarchaeology/Artifact/RandomArtifactSpriteSystem.cs
@@ -1,11 +1,11 @@
-using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
using Content.Shared.Item;
+using Content.Shared.Xenoarchaeology.Artifact;
using Content.Shared.Xenoarchaeology.XenoArtifacts;
using Robust.Server.GameObjects;
using Robust.Shared.Random;
using Robust.Shared.Timing;
-namespace Content.Server.Xenoarchaeology.XenoArtifacts;
+namespace Content.Server.Xenoarchaeology.Artifact;
public sealed class RandomArtifactSpriteSystem : EntitySystem
{
@@ -17,8 +17,11 @@ public sealed class RandomArtifactSpriteSystem : EntitySystem
public override void Initialize()
{
base.Initialize();
+
SubscribeLocalEvent(OnMapInit);
- SubscribeLocalEvent(OnActivated);
+ SubscribeLocalEvent(UnlockingStageStarted);
+ SubscribeLocalEvent(UnlockingStageFinished);
+ SubscribeLocalEvent(ArtifactActivated);
}
public override void Update(float frameTime)
@@ -47,9 +50,19 @@ private void OnMapInit(EntityUid uid, RandomArtifactSpriteComponent component, M
_item.SetHeldPrefix(uid, "ano" + randomSprite.ToString("D2")); //set item artifact inhands
}
- private void OnActivated(EntityUid uid, RandomArtifactSpriteComponent component, ArtifactActivatedEvent args)
+ private void UnlockingStageStarted(Entity ent, ref ArtifactUnlockingStartedEvent args)
+ {
+ _appearance.SetData(ent, SharedArtifactsVisuals.IsUnlocking, true);
+ }
+
+ private void UnlockingStageFinished(Entity ent, ref ArtifactUnlockingFinishedEvent args)
+ {
+ _appearance.SetData(ent, SharedArtifactsVisuals.IsUnlocking, false);
+ }
+
+ private void ArtifactActivated(Entity ent, ref XenoArtifactActivatedEvent args)
{
- _appearance.SetData(uid, SharedArtifactsVisuals.IsActivated, true);
- component.ActivationStart = _time.CurTime;
+ _appearance.SetData(ent, SharedArtifactsVisuals.IsActivated, true);
+ ent.Comp.ActivationStart = _time.CurTime;
}
}
diff --git a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Components/ChargeBatteryArtifactComponent.cs b/Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAEChargeBatteryComponent.cs
similarity index 57%
rename from Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Components/ChargeBatteryArtifactComponent.cs
rename to Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAEChargeBatteryComponent.cs
index a46dd9e81358..792c13663d60 100644
--- a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Components/ChargeBatteryArtifactComponent.cs
+++ b/Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAEChargeBatteryComponent.cs
@@ -1,10 +1,10 @@
-namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
+namespace Content.Server.Xenoarchaeology.Artifact.XAE.Components;
///
/// This is used for recharging all nearby batteries when activated
///
-[RegisterComponent]
-public sealed partial class ChargeBatteryArtifactComponent : Component
+[RegisterComponent, Access(typeof(XAEChargeBatterySystem))]
+public partial class XAEChargeBatteryComponent : Component
{
///
/// The radius of entities that will be affected
diff --git a/Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAECreateGasComponent.cs b/Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAECreateGasComponent.cs
new file mode 100644
index 000000000000..9812e617d45e
--- /dev/null
+++ b/Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAECreateGasComponent.cs
@@ -0,0 +1,14 @@
+using Content.Shared.Atmos;
+
+namespace Content.Server.Xenoarchaeology.Artifact.XAE.Components;
+
+
+[RegisterComponent, Access(typeof(XAECreateGasSystem))]
+public sealed partial class XAECreateGasComponent : Component
+{
+ ///
+ /// The gases and how many moles will be created of each.
+ ///
+ [DataField]
+ public Dictionary Gases = new();
+}
diff --git a/Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAECreatePuddleComponent.cs b/Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAECreatePuddleComponent.cs
new file mode 100644
index 000000000000..4b807ba0b523
--- /dev/null
+++ b/Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAECreatePuddleComponent.cs
@@ -0,0 +1,44 @@
+using Content.Shared.Chemistry.Components;
+using Content.Shared.Chemistry.Reagent;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Xenoarchaeology.Artifact.XAE.Components;
+
+///
+/// This is used for an artifact that creates a puddle of
+/// random chemicals upon being triggered.
+///
+[RegisterComponent, Access(typeof(XAECreatePuddleSystem))]
+public sealed partial class XAECreatePuddleComponent : Component
+{
+ ///
+ /// The solution where all the chemicals are stored
+ ///
+ [DataField(required: true), ViewVariables(VVAccess.ReadWrite)]
+ public Solution ChemicalSolution = default!;
+
+ ///
+ /// The different chemicals that can be spawned by this effect
+ ///
+ [DataField]
+ public List> PossibleChemicals = default!;
+
+ ///
+ /// The number of chemicals in the puddle
+ ///
+ [DataField]
+ public int ChemAmount = 3;
+
+ ///
+ /// List of reagents selected for this node. Selected ones are chosen on first activation
+ /// and are picked from .
+ ///
+ public List>? SelectedChemicals;
+
+ ///
+ /// Marker, if entity where this component is placed should have description replaced with selected chemicals
+ /// on component init.
+ ///
+ [DataField]
+ public bool ReplaceDescription;
+}
diff --git a/Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAEEmpInAreaComponent.cs b/Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAEEmpInAreaComponent.cs
new file mode 100644
index 000000000000..4e4952c55fd0
--- /dev/null
+++ b/Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAEEmpInAreaComponent.cs
@@ -0,0 +1,17 @@
+namespace Content.Server.Xenoarchaeology.Artifact.XAE.Components;
+
+///
+/// Effect of EMP on activation.
+///
+[RegisterComponent, Access(typeof(XAEEmpInAreaSystem))]
+public sealed partial class XAEEmpInAreaComponent : Component
+{
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public float Range = 4f;
+
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public float EnergyConsumption = 1000000;
+
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public float DisableDuration = 60f;
+}
diff --git a/Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAEFoamComponent.cs b/Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAEFoamComponent.cs
new file mode 100644
index 000000000000..7e5069d3c22a
--- /dev/null
+++ b/Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAEFoamComponent.cs
@@ -0,0 +1,56 @@
+using Content.Shared.Chemistry.Reagent;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Xenoarchaeology.Artifact.XAE.Components;
+
+///
+/// Generates foam from the artifact when activated
+///
+[RegisterComponent, Access(typeof(XAEFoamSystem))]
+public sealed partial class XAEFoamComponent : Component
+{
+ ///
+ /// The list of reagents that will randomly be picked from
+ /// to choose the foam reagent
+ ///
+ [DataField(required: true)]
+ public List> Reagents = new();
+
+ ///
+ /// The foam reagent
+ ///
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public string? SelectedReagent;
+
+ ///
+ /// How long does the foam last?
+ ///
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public float Duration = 10f;
+
+ ///
+ /// How much reagent is in the foam?
+ ///
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public float ReagentAmount = 100f;
+
+ ///
+ /// Minimum radius of foam spawned
+ ///
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public int MinFoamAmount = 15;
+
+ ///
+ /// Maximum radius of foam spawned
+ ///
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public int MaxFoamAmount = 20;
+
+ ///
+ /// Marker, if entity where this component is placed should have description replaced with selected chemicals
+ /// on component init.
+ ///
+ [DataField]
+ public bool ReplaceDescription;
+}
+
diff --git a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Components/IgniteArtifactComponent.cs b/Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAEIgniteComponent.cs
similarity index 61%
rename from Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Components/IgniteArtifactComponent.cs
rename to Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAEIgniteComponent.cs
index 436790a16f02..ee1ea1551dca 100644
--- a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Components/IgniteArtifactComponent.cs
+++ b/Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAEIgniteComponent.cs
@@ -1,10 +1,10 @@
-namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
+namespace Content.Server.Xenoarchaeology.Artifact.XAE.Components;
///
/// Artifact that ignites surrounding entities when triggered.
///
-[RegisterComponent]
-public sealed partial class IgniteArtifactComponent : Component
+[RegisterComponent, Access(typeof(XAEIgniteSystem))]
+public sealed partial class XAEIgniteComponent : Component
{
[DataField("range")]
public float Range = 2f;
diff --git a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Components/LightFlickerArtifactComponent.cs b/Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAELightFlickerComponent.cs
similarity index 60%
rename from Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Components/LightFlickerArtifactComponent.cs
rename to Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAELightFlickerComponent.cs
index 24578d722d5e..37f4b836c591 100644
--- a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Components/LightFlickerArtifactComponent.cs
+++ b/Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAELightFlickerComponent.cs
@@ -1,20 +1,20 @@
-namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
+namespace Content.Server.Xenoarchaeology.Artifact.XAE.Components;
///
/// Flickers all the lights within a certain radius.
///
-[RegisterComponent]
-public sealed partial class LightFlickerArtifactComponent : Component
+[RegisterComponent, Access(typeof(XAELightFlickerSystem))]
+public sealed partial class XAELightFlickerComponent : Component
{
///
/// Lights within this radius will be flickered on activation
///
- [DataField("radius")]
+ [DataField]
public float Radius = 4;
///
/// The chance that the light will flicker
///
- [DataField("flickerChance")]
+ [DataField]
public float FlickerChance = 0.75f;
}
diff --git a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Components/PolyOthersArtifactComponent.cs b/Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAEPolymorphComponent.cs
similarity index 62%
rename from Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Components/PolyOthersArtifactComponent.cs
rename to Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAEPolymorphComponent.cs
index 2611d78b088a..02aaa3f4ad2a 100644
--- a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Components/PolyOthersArtifactComponent.cs
+++ b/Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAEPolymorphComponent.cs
@@ -1,16 +1,14 @@
-using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
-using Robust.Shared.Audio;
using Content.Shared.Polymorph;
+using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
-namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
+namespace Content.Server.Xenoarchaeology.Artifact.XAE.Components;
///
-/// Artifact polymorphs surrounding entities when triggered.
+/// Artifact polymorphs entities when triggered.
///
-[RegisterComponent]
-[Access(typeof(PolyOthersArtifactSystem))]
-public sealed partial class PolyOthersArtifactComponent : Component
+[RegisterComponent, Access(typeof(XAEPolymorphSystem))]
+public sealed partial class XAEPolymorphComponent : Component
{
///
/// The polymorph effect to trigger.
@@ -19,7 +17,7 @@ public sealed partial class PolyOthersArtifactComponent : Component
public ProtoId PolymorphPrototypeName = "ArtifactMonkey";
///
- /// range of the effect.
+ /// Range of the effect.
///
[DataField]
public float Range = 2f;
diff --git a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Components/TelepathicArtifactComponent.cs b/Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAETelepathicComponent.cs
similarity index 85%
rename from Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Components/TelepathicArtifactComponent.cs
rename to Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAETelepathicComponent.cs
index a7073a743a52..2354d2f16bd2 100644
--- a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Components/TelepathicArtifactComponent.cs
+++ b/Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAETelepathicComponent.cs
@@ -1,11 +1,11 @@
-namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
+namespace Content.Server.Xenoarchaeology.Artifact.XAE.Components;
///
/// Harmless artifact that broadcast "thoughts" to players nearby.
/// Thoughts are shown as popups and unique for each player.
///
-[RegisterComponent]
-public sealed partial class TelepathicArtifactComponent : Component
+[RegisterComponent, Access(typeof(XAETelepathicSystem))]
+public sealed partial class XAETelepathicComponent : Component
{
///
/// Loc string ids of telepathic messages.
diff --git a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Components/TemperatureArtifactComponent.cs b/Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAETemperatureComponent.cs
similarity index 75%
rename from Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Components/TemperatureArtifactComponent.cs
rename to Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAETemperatureComponent.cs
index 0988617cdc74..00d3775a8dff 100644
--- a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Components/TemperatureArtifactComponent.cs
+++ b/Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAETemperatureComponent.cs
@@ -1,12 +1,12 @@
using Content.Shared.Atmos;
-namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
+namespace Content.Server.Xenoarchaeology.Artifact.XAE.Components;
///
/// Change atmospherics temperature until it reach target.
///
-[RegisterComponent]
-public sealed partial class TemperatureArtifactComponent : Component
+[RegisterComponent, Access(typeof(XAETemperatureSystem))]
+public sealed partial class XAETemperatureComponent : Component
{
[DataField("targetTemp"), ViewVariables(VVAccess.ReadWrite)]
public float TargetTemperature = Atmospherics.T0C;
diff --git a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Components/ThrowArtifactComponent.cs b/Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAEThrowThingsAroundComponent.cs
similarity index 75%
rename from Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Components/ThrowArtifactComponent.cs
rename to Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAEThrowThingsAroundComponent.cs
index 0c2739220820..674bfd32519d 100644
--- a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Components/ThrowArtifactComponent.cs
+++ b/Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAEThrowThingsAroundComponent.cs
@@ -1,11 +1,11 @@
-namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
+namespace Content.Server.Xenoarchaeology.Artifact.XAE.Components;
///
/// Throws all nearby entities backwards.
/// Also pries nearby tiles.
///
-[RegisterComponent]
-public sealed partial class ThrowArtifactComponent : Component
+[RegisterComponent, Access(typeof(XAEThrowThingsAroundSystem))]
+public sealed partial class XAEThrowThingsAroundComponent : Component
{
///
/// How close do you have to be to get yeeted?
diff --git a/Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAETriggerExplosivesComponent.cs b/Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAETriggerExplosivesComponent.cs
new file mode 100644
index 000000000000..b9e23f036b98
--- /dev/null
+++ b/Content.Server/Xenoarchaeology/Artifact/XAE/Components/XAETriggerExplosivesComponent.cs
@@ -0,0 +1,9 @@
+using Content.Shared.Explosion.Components.OnTrigger;
+
+namespace Content.Server.Xenoarchaeology.Artifact.XAE.Components;
+
+///
+/// Activates 'trigger' for .
+///
+[RegisterComponent, Access(typeof(XAETriggerExplosivesSystem))]
+public sealed partial class XAETriggerExplosivesComponent : Component;
diff --git a/Content.Server/Xenoarchaeology/Artifact/XAE/XAEChargeBatterySystem.cs b/Content.Server/Xenoarchaeology/Artifact/XAE/XAEChargeBatterySystem.cs
new file mode 100644
index 000000000000..35fa452eb26b
--- /dev/null
+++ b/Content.Server/Xenoarchaeology/Artifact/XAE/XAEChargeBatterySystem.cs
@@ -0,0 +1,27 @@
+using Content.Server.Power.Components;
+using Content.Server.Power.EntitySystems;
+using Content.Server.Xenoarchaeology.Artifact.XAE.Components;
+using Content.Shared.Xenoarchaeology.Artifact;
+using Content.Shared.Xenoarchaeology.Artifact.XAE;
+
+namespace Content.Server.Xenoarchaeology.Artifact.XAE;
+
+///
+/// System for artifact activation effect that is fully charging batteries in certain range.
+///
+public sealed class XAEChargeBatterySystem : BaseXAESystem
+{
+ [Dependency] private readonly BatterySystem _battery = default!;
+ [Dependency] private readonly EntityLookupSystem _lookup = default!;
+
+ ///
+ protected override void OnActivated(Entity ent, ref XenoArtifactNodeActivatedEvent args)
+ {
+ var chargeBatteryComponent = ent.Comp;
+
+ foreach (var battery in _lookup.GetEntitiesInRange(args.Coordinates, chargeBatteryComponent.Radius))
+ {
+ _battery.SetCharge(battery, battery.Comp.MaxCharge, battery);
+ }
+ }
+}
diff --git a/Content.Server/Xenoarchaeology/Artifact/XAE/XAECreateGasSystem.cs b/Content.Server/Xenoarchaeology/Artifact/XAE/XAECreateGasSystem.cs
new file mode 100644
index 000000000000..b02c8f32e758
--- /dev/null
+++ b/Content.Server/Xenoarchaeology/Artifact/XAE/XAECreateGasSystem.cs
@@ -0,0 +1,48 @@
+using Content.Server.Atmos.EntitySystems;
+using Content.Server.Xenoarchaeology.Artifact.XAE.Components;
+using Content.Shared.Atmos;
+using Content.Shared.Xenoarchaeology.Artifact;
+using Content.Shared.Xenoarchaeology.Artifact.XAE;
+using Robust.Server.GameObjects;
+using Robust.Shared.Map.Components;
+
+namespace Content.Server.Xenoarchaeology.Artifact.XAE;
+
+public sealed class XAECreateGasSystem : BaseXAESystem
+{
+ [Dependency] private readonly AtmosphereSystem _atmosphere = default!;
+ [Dependency] private readonly TransformSystem _transform = default!;
+ [Dependency] private readonly MapSystem _map = default!;
+
+ protected override void OnActivated(Entity ent, ref XenoArtifactNodeActivatedEvent args)
+ {
+ var grid = _transform.GetGrid(args.Coordinates);
+ var map = _transform.GetMap(args.Coordinates);
+ if (map == null || !TryComp(grid, out var gridComp))
+ return;
+
+ var tile = _map.LocalToTile(grid.Value, gridComp, args.Coordinates);
+
+ var mixtures = new List();
+ if (_atmosphere.GetTileMixture(grid.Value, map.Value, tile, excite: true) is { } localMixture)
+ mixtures.Add(localMixture);
+
+ if (_atmosphere.GetAdjacentTileMixtures(grid.Value, tile, excite: true) is var adjacentTileMixtures)
+ {
+ while (adjacentTileMixtures.MoveNext(out var adjacentMixture))
+ {
+ mixtures.Add(adjacentMixture);
+ }
+ }
+
+ foreach (var (gas, moles) in ent.Comp.Gases)
+ {
+ var molesPerMixture = moles / mixtures.Count;
+
+ foreach (var mixture in mixtures)
+ {
+ mixture.AdjustMoles(gas, molesPerMixture);
+ }
+ }
+ }
+}
diff --git a/Content.Server/Xenoarchaeology/Artifact/XAE/XAECreatePuddleSystem.cs b/Content.Server/Xenoarchaeology/Artifact/XAE/XAECreatePuddleSystem.cs
new file mode 100644
index 000000000000..44c3830e05e5
--- /dev/null
+++ b/Content.Server/Xenoarchaeology/Artifact/XAE/XAECreatePuddleSystem.cs
@@ -0,0 +1,70 @@
+using Content.Server.Fluids.EntitySystems;
+using Content.Server.Xenoarchaeology.Artifact.XAE.Components;
+using Content.Shared.Chemistry.Reagent;
+using Content.Shared.Xenoarchaeology.Artifact;
+using Content.Shared.Xenoarchaeology.Artifact.XAE;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Server.Xenoarchaeology.Artifact.XAE;
+
+public sealed class XAECreatePuddleSystem: BaseXAESystem
+{
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly PuddleSystem _puddle = default!;
+ [Dependency] private readonly MetaDataSystem _metaData= default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager= default!;
+
+ ///
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(OnInit);
+ }
+
+ private void OnInit(EntityUid uid, XAECreatePuddleComponent component, MapInitEvent _)
+ {
+ if (component.PossibleChemicals == null || component.PossibleChemicals.Count == 0)
+ {
+ return;
+ }
+
+ if (component.SelectedChemicals == null)
+ {
+ var chemicalList = new List>();
+ for (var i = 0; i < component.ChemAmount; i++)
+ {
+ var chemProto = _random.Pick(component.PossibleChemicals);
+ chemicalList.Add(chemProto);
+ }
+
+ component.SelectedChemicals = chemicalList;
+ }
+
+ if (component.ReplaceDescription)
+ {
+ var reagentNames = new HashSet();
+ foreach (var chemProtoId in component.SelectedChemicals)
+ {
+ var reagent = _prototypeManager.Index(chemProtoId);
+ reagentNames.Add(reagent.LocalizedName);
+ }
+
+ var reagentNamesStr = string.Join(", ", reagentNames);
+ var newEntityDescription = Loc.GetString("xenoarch-effect-puddle", ("reagent", reagentNamesStr));
+ _metaData.SetEntityDescription(uid, newEntityDescription);
+ }
+ }
+
+ ///
+ protected override void OnActivated(Entity ent, ref XenoArtifactNodeActivatedEvent args)
+ {
+ var component = ent.Comp;
+ var amountPerChem = component.ChemicalSolution.MaxVolume / component.ChemAmount;
+ foreach (var reagent in component.SelectedChemicals!)
+ {
+ component.ChemicalSolution.AddReagent(reagent, amountPerChem);
+ }
+
+ _puddle.TrySpillAt(ent, component.ChemicalSolution, out _);
+ }
+}
diff --git a/Content.Server/Xenoarchaeology/Artifact/XAE/XAEEmpInAreaSystem.cs b/Content.Server/Xenoarchaeology/Artifact/XAE/XAEEmpInAreaSystem.cs
new file mode 100644
index 000000000000..72a4591f5aac
--- /dev/null
+++ b/Content.Server/Xenoarchaeology/Artifact/XAE/XAEEmpInAreaSystem.cs
@@ -0,0 +1,17 @@
+using Content.Server.Emp;
+using Content.Server.Xenoarchaeology.Artifact.XAE.Components;
+using Content.Shared.Xenoarchaeology.Artifact;
+using Content.Shared.Xenoarchaeology.Artifact.XAE;
+
+namespace Content.Server.Xenoarchaeology.Artifact.XAE;
+
+public sealed class XAEEmpInAreaSystem : BaseXAESystem
+{
+ [Dependency] private readonly EmpSystem _emp = default!;
+
+ ///
+ protected override void OnActivated(Entity ent, ref XenoArtifactNodeActivatedEvent args)
+ {
+ _emp.EmpPulse(args.Coordinates, ent.Comp.Range, ent.Comp.EnergyConsumption, ent.Comp.DisableDuration);
+ }
+}
diff --git a/Content.Server/Xenoarchaeology/Artifact/XAE/XAEFoamSystem.cs b/Content.Server/Xenoarchaeology/Artifact/XAE/XAEFoamSystem.cs
new file mode 100644
index 000000000000..3ce1ad03c64e
--- /dev/null
+++ b/Content.Server/Xenoarchaeology/Artifact/XAE/XAEFoamSystem.cs
@@ -0,0 +1,60 @@
+using Content.Server.Fluids.EntitySystems;
+using Content.Server.Xenoarchaeology.Artifact.XAE.Components;
+using Content.Shared.Chemistry.Components;
+using Content.Shared.Chemistry.Reaction;
+using Content.Shared.Chemistry.Reagent;
+using Content.Shared.Xenoarchaeology.Artifact;
+using Content.Shared.Xenoarchaeology.Artifact.XAE;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Server.Xenoarchaeology.Artifact.XAE;
+
+public sealed class XAEFoamSystem : BaseXAESystem
+{
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly SmokeSystem _smoke = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager= default!;
+ [Dependency] private readonly MetaDataSystem _metaData = default!;
+
+ ///
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnMapInit);
+ }
+
+ private void OnMapInit(EntityUid uid, XAEFoamComponent component, MapInitEvent args)
+ {
+ if (component.SelectedReagent != null)
+ return;
+
+ if (component.Reagents.Count == 0)
+ return;
+
+ component.SelectedReagent = _random.Pick(component.Reagents);
+
+ if (component.ReplaceDescription)
+ {
+ var reagent = _prototypeManager.Index(component.SelectedReagent);
+ var newEntityDescription = Loc.GetString("xenoarch-effect-foam", ("reagent", reagent.LocalizedName));
+ _metaData.SetEntityDescription(uid, newEntityDescription);
+ }
+ }
+
+ ///
+ protected override void OnActivated(Entity ent, ref XenoArtifactNodeActivatedEvent args)
+ {
+ var component = ent.Comp;
+ if (component.SelectedReagent == null)
+ return;
+
+ var sol = new Solution();
+ var range = (int)MathF.Round(MathHelper.Lerp(component.MinFoamAmount, component.MaxFoamAmount, _random.NextFloat(0, 1f)));
+ sol.AddReagent(component.SelectedReagent, component.ReagentAmount);
+ var foamEnt = Spawn(ChemicalReactionSystem.FoamReaction, args.Coordinates);
+ var spreadAmount = range * 4;
+ _smoke.StartSmoke(foamEnt, sol, component.Duration, spreadAmount);
+ }
+}
diff --git a/Content.Server/Xenoarchaeology/Artifact/XAE/XAEIgniteSystem.cs b/Content.Server/Xenoarchaeology/Artifact/XAE/XAEIgniteSystem.cs
new file mode 100644
index 000000000000..c9e9cbeb8d7c
--- /dev/null
+++ b/Content.Server/Xenoarchaeology/Artifact/XAE/XAEIgniteSystem.cs
@@ -0,0 +1,30 @@
+using Content.Server.Atmos.Components;
+using Content.Server.Atmos.EntitySystems;
+using Content.Server.Xenoarchaeology.Artifact.XAE.Components;
+using Content.Shared.Xenoarchaeology.Artifact;
+using Content.Shared.Xenoarchaeology.Artifact.XAE;
+using Robust.Shared.Random;
+
+namespace Content.Server.Xenoarchaeology.Artifact.XAE;
+
+public sealed class XAEIgniteSystem : BaseXAESystem
+{
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly EntityLookupSystem _lookup = default!;
+ [Dependency] private readonly FlammableSystem _flammable = default!;
+
+ ///
+ protected override void OnActivated(Entity ent, ref XenoArtifactNodeActivatedEvent args)
+ {
+ var component = ent.Comp;
+ var flammable = GetEntityQuery();
+ foreach (var target in _lookup.GetEntitiesInRange(ent.Owner, component.Range))
+ {
+ if (!flammable.TryGetComponent(target, out var fl))
+ continue;
+
+ fl.FireStacks += _random.Next(component.MinFireStack, component.MaxFireStack);
+ _flammable.Ignite(target, ent.Owner, fl);
+ }
+ }
+}
diff --git a/Content.Server/Xenoarchaeology/Artifact/XAE/XAELightFlickerSystem.cs b/Content.Server/Xenoarchaeology/Artifact/XAE/XAELightFlickerSystem.cs
new file mode 100644
index 000000000000..5389def4255d
--- /dev/null
+++ b/Content.Server/Xenoarchaeology/Artifact/XAE/XAELightFlickerSystem.cs
@@ -0,0 +1,32 @@
+using Content.Server.Ghost;
+using Content.Server.Light.Components;
+using Content.Server.Xenoarchaeology.Artifact.XAE.Components;
+using Content.Shared.Xenoarchaeology.Artifact;
+using Content.Shared.Xenoarchaeology.Artifact.XAE;
+using Robust.Shared.Random;
+
+namespace Content.Server.Xenoarchaeology.Artifact.XAE;
+
+public sealed class XAELightFlickerSystem : BaseXAESystem
+{
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly EntityLookupSystem _lookup = default!;
+ [Dependency] private readonly GhostSystem _ghost = default!;
+
+ ///
+ protected override void OnActivated(Entity ent, ref XenoArtifactNodeActivatedEvent args)
+ {
+ var lights = GetEntityQuery();
+ foreach (var light in _lookup.GetEntitiesInRange(ent.Owner, ent.Comp.Radius, LookupFlags.StaticSundries))
+ {
+ if (!lights.HasComponent(light))
+ continue;
+
+ if (!_random.Prob(ent.Comp.FlickerChance))
+ continue;
+
+ //todo: extract effect from ghost system, update power system accordingly
+ _ghost.DoGhostBooEvent(light);
+ }
+ }
+}
diff --git a/Content.Server/Xenoarchaeology/Artifact/XAE/XAEPolymorphSystem.cs b/Content.Server/Xenoarchaeology/Artifact/XAE/XAEPolymorphSystem.cs
new file mode 100644
index 000000000000..839333bc29ef
--- /dev/null
+++ b/Content.Server/Xenoarchaeology/Artifact/XAE/XAEPolymorphSystem.cs
@@ -0,0 +1,34 @@
+using Content.Server.Polymorph.Systems;
+using Content.Server.Xenoarchaeology.Artifact.XAE.Components;
+using Content.Shared.Humanoid;
+using Content.Shared.Mobs.Systems;
+using Content.Shared.Xenoarchaeology.Artifact;
+using Content.Shared.Xenoarchaeology.Artifact.XAE;
+using Robust.Shared.Audio.Systems;
+
+namespace Content.Server.Xenoarchaeology.Artifact.XAE;
+
+public sealed class XAEPolymorphSystem : BaseXAESystem
+{
+ [Dependency] private readonly EntityLookupSystem _lookup = default!;
+ [Dependency] private readonly MobStateSystem _mob = default!;
+ [Dependency] private readonly PolymorphSystem _poly = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+
+ ///
+ protected override void OnActivated(Entity ent, ref XenoArtifactNodeActivatedEvent args)
+ {
+ var humanoids = new HashSet>();
+ _lookup.GetEntitiesInRange(args.Coordinates, ent.Comp.Range, humanoids);
+
+ foreach (var comp in humanoids)
+ {
+ var target = comp.Owner;
+ if (!_mob.IsAlive(target))
+ continue;
+
+ _poly.PolymorphEntity(target, ent.Comp.PolymorphPrototypeName);
+ _audio.PlayPvs(ent.Comp.PolySound, ent);
+ }
+ }
+}
diff --git a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/TelepathicArtifactSystem.cs b/Content.Server/Xenoarchaeology/Artifact/XAE/XAETelepathicSystem.cs
similarity index 64%
rename from Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/TelepathicArtifactSystem.cs
rename to Content.Server/Xenoarchaeology/Artifact/XAE/XAETelepathicSystem.cs
index df06d4ab0175..f010df3c8a3b 100644
--- a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/TelepathicArtifactSystem.cs
+++ b/Content.Server/Xenoarchaeology/Artifact/XAE/XAETelepathicSystem.cs
@@ -1,28 +1,24 @@
-using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
-using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
+using Content.Server.Xenoarchaeology.Artifact.XAE.Components;
using Content.Shared.Popups;
-using Robust.Server.GameObjects;
+using Content.Shared.Xenoarchaeology.Artifact;
+using Content.Shared.Xenoarchaeology.Artifact.XAE;
using Robust.Shared.Player;
using Robust.Shared.Random;
-namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
+namespace Content.Server.Xenoarchaeology.Artifact.XAE;
-public sealed class TelepathicArtifactSystem : EntitySystem
+public sealed class XAETelepathicSystem : BaseXAESystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
- public override void Initialize()
- {
- base.Initialize();
- SubscribeLocalEvent(OnActivate);
- }
-
- private void OnActivate(EntityUid uid, TelepathicArtifactComponent component, ArtifactActivatedEvent args)
+ ///
+ protected override void OnActivated(Entity ent, ref XenoArtifactNodeActivatedEvent args)
{
+ var component = ent.Comp;
// try to find victims nearby
- var victims = _lookup.GetEntitiesInRange(uid, component.Range);
+ var victims = _lookup.GetEntitiesInRange(ent, component.Range);
foreach (var victimUid in victims)
{
if (!EntityManager.HasComponent(victimUid))
diff --git a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/TemperatureArtifactSystem.cs b/Content.Server/Xenoarchaeology/Artifact/XAE/XAETemperatureSystem.cs
similarity index 50%
rename from Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/TemperatureArtifactSystem.cs
rename to Content.Server/Xenoarchaeology/Artifact/XAE/XAETemperatureSystem.cs
index e62ce36b5b9c..80b8550c1c77 100644
--- a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/TemperatureArtifactSystem.cs
+++ b/Content.Server/Xenoarchaeology/Artifact/XAE/XAETemperatureSystem.cs
@@ -1,36 +1,33 @@
-using Content.Server.Atmos;
using Content.Server.Atmos.EntitySystems;
-using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
-using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
+using Content.Server.Xenoarchaeology.Artifact.XAE.Components;
using Content.Shared.Atmos;
+using Content.Shared.Xenoarchaeology.Artifact;
+using Content.Shared.Xenoarchaeology.Artifact.XAE;
using Robust.Server.GameObjects;
-namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
+namespace Content.Server.Xenoarchaeology.Artifact.XAE;
-public sealed class TemperatureArtifactSystem : EntitySystem
+public sealed class XAETemperatureSystem : BaseXAESystem
{
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly TransformSystem _transformSystem = default!;
- public override void Initialize()
+ ///
+ protected override void OnActivated(Entity ent, ref XenoArtifactNodeActivatedEvent args)
{
- base.Initialize();
- SubscribeLocalEvent(OnActivate);
- }
-
- private void OnActivate(EntityUid uid, TemperatureArtifactComponent component, ArtifactActivatedEvent args)
- {
- var transform = Transform(uid);
+ var component = ent.Comp;
+ var transform = Transform(ent);
- var center = _atmosphereSystem.GetContainingMixture(uid, false, true);
+ var center = _atmosphereSystem.GetContainingMixture(ent.Owner, false, true);
if (center == null)
return;
+
UpdateTileTemperature(component, center);
if (component.AffectAdjacentTiles && transform.GridUid != null)
{
- var enumerator = _atmosphereSystem.GetAdjacentTileMixtures(transform.GridUid.Value,
- _transformSystem.GetGridOrMapTilePosition(uid, transform), excite: true);
+ var position = _transformSystem.GetGridOrMapTilePosition(ent, transform);
+ var enumerator = _atmosphereSystem.GetAdjacentTileMixtures(transform.GridUid.Value, position, excite: true);
while (enumerator.MoveNext(out var mixture))
{
@@ -39,7 +36,7 @@ private void OnActivate(EntityUid uid, TemperatureArtifactComponent component, A
}
}
- private void UpdateTileTemperature(TemperatureArtifactComponent component, GasMixture environment)
+ private void UpdateTileTemperature(XAETemperatureComponent component, GasMixture environment)
{
var dif = component.TargetTemperature - environment.Temperature;
var absDif = Math.Abs(dif);
diff --git a/Content.Server/Xenoarchaeology/Artifact/XAE/XAEThrowThingsAroundSystem.cs b/Content.Server/Xenoarchaeology/Artifact/XAE/XAEThrowThingsAroundSystem.cs
new file mode 100644
index 000000000000..9a30ba2c1149
--- /dev/null
+++ b/Content.Server/Xenoarchaeology/Artifact/XAE/XAEThrowThingsAroundSystem.cs
@@ -0,0 +1,61 @@
+using System.Numerics;
+using Content.Server.Xenoarchaeology.Artifact.XAE.Components;
+using Content.Shared.Maps;
+using Content.Shared.Physics;
+using Content.Shared.Throwing;
+using Content.Shared.Xenoarchaeology.Artifact;
+using Content.Shared.Xenoarchaeology.Artifact.XAE;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Physics.Components;
+using Robust.Shared.Random;
+using Robust.Shared.Timing;
+
+namespace Content.Server.Xenoarchaeology.Artifact.XAE;
+
+public sealed class XAEThrowThingsAroundSystem : BaseXAESystem
+{
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly EntityLookupSystem _lookup = default!;
+ [Dependency] private readonly ThrowingSystem _throwing = default!;
+ [Dependency] private readonly TileSystem _tile = default!;
+ [Dependency] private readonly SharedTransformSystem _transform = default!;
+ [Dependency] private readonly SharedMapSystem _map = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+
+ ///
+ protected override void OnActivated(Entity ent, ref XenoArtifactNodeActivatedEvent args)
+ {
+ if (!_timing.IsFirstTimePredicted)
+ return;
+
+ var component = ent.Comp;
+ var xform = Transform(ent);
+ if (TryComp(xform.GridUid, out var grid))
+ {
+ var boxForTilesPry = Box2.CenteredAround(_transform.GetWorldPosition(xform), new Vector2(component.Range * 2, component.Range));
+ var tiles = _map.GetTilesIntersecting(xform.GridUid.Value, grid, boxForTilesPry, true);
+
+ foreach (var tile in tiles)
+ {
+ if (!_random.Prob(component.TilePryChance))
+ continue;
+
+ _tile.PryTile(tile);
+ }
+ }
+
+ var lookup = _lookup.GetEntitiesInRange(ent, component.Range, LookupFlags.Dynamic | LookupFlags.Sundries);
+ var physQuery = GetEntityQuery();
+ foreach (var entity in lookup)
+ {
+ if (physQuery.TryGetComponent(entity, out var phys)
+ && (phys.CollisionMask & (int)CollisionGroup.GhostImpassable) != 0)
+ continue;
+
+ var tempXform = Transform(entity);
+
+ var foo = _transform.GetMapCoordinates(entity, xform: tempXform).Position - _transform.GetMapCoordinates(ent, xform: xform).Position;
+ _throwing.TryThrow(entity, foo * 2, component.ThrowStrength, ent, 0);
+ }
+ }
+}
diff --git a/Content.Server/Xenoarchaeology/Artifact/XAE/XAETriggerExplosivesSystem.cs b/Content.Server/Xenoarchaeology/Artifact/XAE/XAETriggerExplosivesSystem.cs
new file mode 100644
index 000000000000..e4244dd84451
--- /dev/null
+++ b/Content.Server/Xenoarchaeology/Artifact/XAE/XAETriggerExplosivesSystem.cs
@@ -0,0 +1,21 @@
+using Content.Server.Explosion.EntitySystems;
+using Content.Server.Xenoarchaeology.Artifact.XAE.Components;
+using Content.Shared.Explosion.Components;
+using Content.Shared.Xenoarchaeology.Artifact;
+using Content.Shared.Xenoarchaeology.Artifact.XAE;
+
+namespace Content.Server.Xenoarchaeology.Artifact.XAE;
+
+public sealed class XAETriggerExplosivesSystem : BaseXAESystem
+{
+ [Dependency] private readonly ExplosionSystem _explosion = default!;
+
+ ///
+ protected override void OnActivated(Entity ent, ref XenoArtifactNodeActivatedEvent args)
+ {
+ if(!TryComp(ent, out var explosiveComp))
+ return;
+
+ _explosion.TriggerExplosive(ent, explosiveComp);
+ }
+}
diff --git a/Content.Server/Xenoarchaeology/Artifact/XAT/Components/XATGasComponent.cs b/Content.Server/Xenoarchaeology/Artifact/XAT/Components/XATGasComponent.cs
new file mode 100644
index 000000000000..a5a9a4447bd3
--- /dev/null
+++ b/Content.Server/Xenoarchaeology/Artifact/XAT/Components/XATGasComponent.cs
@@ -0,0 +1,28 @@
+using Content.Shared.Atmos;
+
+namespace Content.Server.Xenoarchaeology.Artifact.XAT.Components;
+
+///
+/// This is used for an artifact that is activated by having a certain amount of gas around it.
+///
+[RegisterComponent, Access(typeof(XATGasSystem))]
+public sealed partial class XATGasComponent : Component
+{
+ ///
+ /// The gas that is related to trigger.
+ ///
+ [DataField]
+ public Gas TargetGas;
+
+ ///
+ /// The amount of gas needed.
+ ///
+ [DataField]
+ public float Moles = Atmospherics.MolesCellStandard * 0.1f;
+
+ ///
+ /// Marker, if mentioned gas should be present in entity tile for trigger to activate, or it should not.
+ ///
+ [DataField]
+ public bool ShouldBePresent = true;
+}
diff --git a/Content.Server/Xenoarchaeology/Artifact/XAT/Components/XATMagnetComponent.cs b/Content.Server/Xenoarchaeology/Artifact/XAT/Components/XATMagnetComponent.cs
new file mode 100644
index 000000000000..ae1c55b59293
--- /dev/null
+++ b/Content.Server/Xenoarchaeology/Artifact/XAT/Components/XATMagnetComponent.cs
@@ -0,0 +1,21 @@
+namespace Content.Server.Xenoarchaeology.Artifact.XAT.Components;
+
+///
+/// Component for triggering node on getting activated by powerful magnets.
+///
+[RegisterComponent, Access(typeof(XATMagnetSystem))]
+public sealed partial class XATMagnetComponent : Component
+{
+ ///
+ /// how close to the magnet do you have to be?
+ ///
+ [DataField]
+ public float MagnetRange = 40f;
+
+ ///
+ /// How close do active magboots have to be?
+ /// This is smaller because they are weaker magnets
+ ///
+ [DataField]
+ public float MagbootsRange = 2f;
+}
diff --git a/Content.Server/Xenoarchaeology/Artifact/XAT/Components/XATPressureComponent.cs b/Content.Server/Xenoarchaeology/Artifact/XAT/Components/XATPressureComponent.cs
new file mode 100644
index 000000000000..bdd2f21e3322
--- /dev/null
+++ b/Content.Server/Xenoarchaeology/Artifact/XAT/Components/XATPressureComponent.cs
@@ -0,0 +1,20 @@
+namespace Content.Server.Xenoarchaeology.Artifact.XAT.Components;
+
+///
+/// This is used for an artifact that activates when above or below a certain pressure.
+///
+[RegisterComponent, Access(typeof(XATPressureSystem))]
+public sealed partial class XATPressureComponent : Component
+{
+ ///
+ /// The lower-end pressure threshold. Is not considered when null.
+ ///
+ [DataField]
+ public float? MinPressureThreshold;
+
+ ///
+ /// The higher-end pressure threshold. Is not considered when null.
+ ///
+ [DataField]
+ public float? MaxPressureThreshold;
+}
diff --git a/Content.Server/Xenoarchaeology/Artifact/XAT/Components/XATTemperatureComponent.cs b/Content.Server/Xenoarchaeology/Artifact/XAT/Components/XATTemperatureComponent.cs
new file mode 100644
index 000000000000..b8e080ccd9ca
--- /dev/null
+++ b/Content.Server/Xenoarchaeology/Artifact/XAT/Components/XATTemperatureComponent.cs
@@ -0,0 +1,20 @@
+namespace Content.Server.Xenoarchaeology.Artifact.XAT.Components;
+
+///
+/// This is used for an artifact that is activated by having a certain temperature near it.
+///
+[RegisterComponent, Access(typeof(XATTemperatureSystem))]
+public sealed partial class XATTemperatureComponent : Component
+{
+ ///
+ /// Threshold temperature for trigger activation.
+ ///
+ [DataField]
+ public float TargetTemperature;
+
+ ///
+ /// Marker, if temp needs to be above or below the target.
+ ///
+ [DataField]
+ public bool TriggerOnHigherTemp = true;
+}
diff --git a/Content.Server/Xenoarchaeology/Artifact/XAT/XATGasSystem.cs b/Content.Server/Xenoarchaeology/Artifact/XAT/XATGasSystem.cs
new file mode 100644
index 000000000000..29be5b944ad2
--- /dev/null
+++ b/Content.Server/Xenoarchaeology/Artifact/XAT/XATGasSystem.cs
@@ -0,0 +1,32 @@
+using Content.Server.Atmos.EntitySystems;
+using Content.Server.Xenoarchaeology.Artifact.XAT.Components;
+using Content.Shared.Xenoarchaeology.Artifact.Components;
+using Content.Shared.Xenoarchaeology.Artifact.XAT;
+
+namespace Content.Server.Xenoarchaeology.Artifact.XAT;
+
+public sealed class XATGasSystem : BaseQueryUpdateXATSystem
+{
+ [Dependency] private readonly AtmosphereSystem _atmosphere = default!;
+
+ protected override void UpdateXAT(Entity artifact, Entity node, float frameTime)
+ {
+ var xform = Transform(artifact);
+
+ if (_atmosphere.GetTileMixture((artifact, xform)) is not { } mixture)
+ return;
+
+ var moles = mixture.GetMoles(node.Comp1.TargetGas);
+
+ if (node.Comp1.ShouldBePresent)
+ {
+ if (moles >= node.Comp1.Moles)
+ Trigger(artifact, node);
+ }
+ else
+ {
+ if (moles <= node.Comp1.Moles)
+ Trigger(artifact, node);
+ }
+ }
+}
diff --git a/Content.Server/Xenoarchaeology/Artifact/XAT/XATMagnetSystem.cs b/Content.Server/Xenoarchaeology/Artifact/XAT/XATMagnetSystem.cs
new file mode 100644
index 000000000000..9a2697dc9058
--- /dev/null
+++ b/Content.Server/Xenoarchaeology/Artifact/XAT/XATMagnetSystem.cs
@@ -0,0 +1,58 @@
+using Content.Server.Salvage;
+using Content.Server.Xenoarchaeology.Artifact.XAT.Components;
+using Content.Shared.Clothing;
+using Content.Shared.Item.ItemToggle.Components;
+using Content.Shared.Xenoarchaeology.Artifact.Components;
+using Content.Shared.Xenoarchaeology.Artifact.XAT;
+
+namespace Content.Server.Xenoarchaeology.Artifact.XAT;
+
+public sealed class XATMagnetSystem : BaseQueryUpdateXATSystem
+{
+ [Dependency] private readonly SharedTransformSystem _transform = default!;
+
+ ///
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnMagnetActivated);
+ }
+
+ private void OnMagnetActivated(ref SalvageMagnetActivatedEvent args)
+ {
+ var magnetCoordinates = Transform(args.Magnet).Coordinates;
+
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var comp, out var node))
+ {
+ if (node.Attached == null)
+ continue;
+
+ var artifact = _xenoArtifactQuery.Get(GetEntity(node.Attached.Value));
+
+ if (!CanTrigger(artifact, (uid, node)))
+ continue;
+
+ var artifactCoordinates = Transform(artifact).Coordinates;
+ if (_transform.InRange(magnetCoordinates, artifactCoordinates, comp.MagnetRange))
+ Trigger(artifact, (uid, comp, node));
+ }
+ }
+
+ protected override void UpdateXAT(Entity artifact, Entity node, float frameTime)
+ {
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out _, out _, out var itemToggle, out var xform))
+ {
+ if (!itemToggle.Activated)
+ continue;
+
+ if (!_transform.InRange(xform.Coordinates, Transform(artifact).Coordinates, node.Comp1.MagbootsRange))
+ continue;
+
+ Trigger(artifact, node);
+ break;
+ }
+ }
+}
diff --git a/Content.Server/Xenoarchaeology/Artifact/XAT/XATPressureSystem.cs b/Content.Server/Xenoarchaeology/Artifact/XAT/XATPressureSystem.cs
new file mode 100644
index 000000000000..3ee69f1e9a35
--- /dev/null
+++ b/Content.Server/Xenoarchaeology/Artifact/XAT/XATPressureSystem.cs
@@ -0,0 +1,25 @@
+using Content.Server.Atmos.EntitySystems;
+using Content.Server.Xenoarchaeology.Artifact.XAT.Components;
+using Content.Shared.Xenoarchaeology.Artifact.Components;
+using Content.Shared.Xenoarchaeology.Artifact.XAT;
+
+namespace Content.Server.Xenoarchaeology.Artifact.XAT;
+
+public sealed class XATPressureSystem : BaseQueryUpdateXATSystem
+{
+ [Dependency] private readonly AtmosphereSystem _atmosphere = default!;
+
+ protected override void UpdateXAT(Entity artifact, Entity node, float frameTime)
+ {
+ var xform = Transform(artifact);
+
+ if (_atmosphere.GetTileMixture((artifact, xform)) is not { } mixture)
+ return;
+
+ var pressure = mixture.Pressure;
+ if (pressure >= node.Comp1.MaxPressureThreshold || pressure <= node.Comp1.MinPressureThreshold)
+ {
+ Trigger(artifact, node);
+ }
+ }
+}
diff --git a/Content.Server/Xenoarchaeology/Artifact/XAT/XATTemperatureSystem.cs b/Content.Server/Xenoarchaeology/Artifact/XAT/XATTemperatureSystem.cs
new file mode 100644
index 000000000000..46eba7c49050
--- /dev/null
+++ b/Content.Server/Xenoarchaeology/Artifact/XAT/XATTemperatureSystem.cs
@@ -0,0 +1,33 @@
+using Content.Server.Atmos.EntitySystems;
+using Content.Server.Xenoarchaeology.Artifact.XAT.Components;
+using Content.Shared.Xenoarchaeology.Artifact.Components;
+using Content.Shared.Xenoarchaeology.Artifact.XAT;
+
+namespace Content.Server.Xenoarchaeology.Artifact.XAT;
+
+public sealed class XATTemperatureSystem : BaseQueryUpdateXATSystem
+{
+ [Dependency] private readonly AtmosphereSystem _atmosphere = default!;
+
+ protected override void UpdateXAT(Entity artifact, Entity node, float frameTime)
+ {
+ var xform = Transform(artifact);
+
+ if (_atmosphere.GetTileMixture((artifact, xform)) is not { } mixture)
+ return;
+
+ var curTemp = mixture.Temperature;
+
+ var temperatureTriggerComponent = node.Comp1;
+ if (temperatureTriggerComponent.TriggerOnHigherTemp)
+ {
+ if (curTemp >= temperatureTriggerComponent.TargetTemperature)
+ Trigger(artifact, node);
+ }
+ else
+ {
+ if (curTemp <= temperatureTriggerComponent.TargetTemperature)
+ Trigger(artifact, node);
+ }
+ }
+}
diff --git a/Content.Server/Xenoarchaeology/Artifact/XenoArtifactCommands.cs b/Content.Server/Xenoarchaeology/Artifact/XenoArtifactCommands.cs
new file mode 100644
index 000000000000..c2be09b39429
--- /dev/null
+++ b/Content.Server/Xenoarchaeology/Artifact/XenoArtifactCommands.cs
@@ -0,0 +1,113 @@
+using System.Text;
+using Content.Server.Administration;
+using Content.Shared.Administration;
+using Content.Shared.Xenoarchaeology.Artifact.Components;
+using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Toolshed;
+
+namespace Content.Server.Xenoarchaeology.Artifact;
+
+[ToolshedCommand, AdminCommand(AdminFlags.Debug)]
+public sealed class XenoArtifactCommand : ToolshedCommand
+{
+ [CommandImplementation("list")]
+ public IEnumerable List()
+ {
+ var query = EntityManager.EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out _))
+ {
+ yield return uid;
+ }
+ }
+
+ [CommandImplementation("printMatrix")]
+ public string PrintMatrix([PipedArgument] EntityUid ent)
+ {
+ var comp = EntityManager.GetComponent(ent);
+
+ var nodeCount = comp.NodeVertices.Length;
+
+ var sb = new StringBuilder("\n |");
+ for (var i = 0; i < nodeCount; i++)
+ {
+ sb.Append($" {i:D2}|");
+ }
+
+ AddHorizontalFiller(sb);
+
+ for (var i = 0; i < nodeCount; i++)
+ {
+ sb.Append($"\n{i:D2}|");
+ for (var j = 0; j < nodeCount; j++)
+ {
+ var value = comp.NodeAdjacencyMatrix[i][j]
+ ? "X"
+ : " ";
+ sb.Append($" {value} |");
+ }
+ AddHorizontalFiller(sb);
+ }
+
+ return sb.ToString();
+
+ void AddHorizontalFiller(StringBuilder builder)
+ {
+ builder.AppendLine();
+ builder.Append("--+");
+ for (var i = 0; i < nodeCount; i++)
+ {
+ builder.Append($"---+");
+ }
+ }
+ }
+
+ [CommandImplementation("totalResearch")]
+ public int TotalResearch([PipedArgument] EntityUid ent)
+ {
+ var artiSys = EntityManager.System();
+ var comp = EntityManager.GetComponent(ent);
+
+ var sum = 0;
+
+ var nodes = artiSys.GetAllNodes((ent, comp));
+ foreach (var node in nodes)
+ {
+ sum += node.Comp.ResearchValue;
+ }
+
+ return sum;
+ }
+
+ [ValidatePrototypeId]
+ public const string ArtifactPrototype = "BaseXenoArtifact";
+
+ [CommandImplementation("averageResearch")]
+ public float AverageResearch()
+ {
+ const int n = 100;
+ var sum = 0;
+
+ for (var i = 0; i < n; i++)
+ {
+ var ent = Spawn(ArtifactPrototype, MapCoordinates.Nullspace);
+ sum += TotalResearch(ent);
+ Del(ent);
+ }
+
+ return (float) sum / n;
+ }
+
+ [CommandImplementation("unlockAllNodes")]
+ public void UnlockAllNodes([PipedArgument] EntityUid ent)
+ {
+ var artiSys = EntityManager.System();
+ var comp = EntityManager.GetComponent(ent);
+
+ var nodes = artiSys.GetAllNodes((ent, comp));
+ foreach (var node in nodes)
+ {
+ artiSys.SetNodeUnlocked((node, node.Comp));
+ }
+ }
+}
diff --git a/Content.Server/Xenoarchaeology/Artifact/XenoArtifactSystem.ProcGen.cs b/Content.Server/Xenoarchaeology/Artifact/XenoArtifactSystem.ProcGen.cs
new file mode 100644
index 000000000000..9da98dca61c8
--- /dev/null
+++ b/Content.Server/Xenoarchaeology/Artifact/XenoArtifactSystem.ProcGen.cs
@@ -0,0 +1,219 @@
+using System.Linq;
+using Content.Shared.Random.Helpers;
+using Content.Shared.Whitelist;
+using Content.Shared.Xenoarchaeology.Artifact.Components;
+using Content.Shared.Xenoarchaeology.Artifact.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Server.Xenoarchaeology.Artifact;
+
+public sealed partial class XenoArtifactSystem
+{
+ [Dependency] private readonly EntityWhitelistSystem _entityWhitelist = default!;
+
+ private void GenerateArtifactStructure(Entity ent)
+ {
+ var nodeCount = ent.Comp.NodeCount.Next(RobustRandom);
+ var triggerPool = CreateTriggerPool(ent, nodeCount);
+ // trigger pool could be smaller, then requested node count
+ nodeCount = triggerPool.Count;
+ ResizeNodeGraph(ent, nodeCount);
+ while (nodeCount > 0)
+ {
+ GenerateArtifactSegment(ent, triggerPool, ref nodeCount);
+ }
+
+ RebuildXenoArtifactMetaData((ent, ent));
+ }
+
+ ///
+ /// Creates pool from all node triggers that current artifact can support.
+ /// As artifact cannot re-use triggers, pool will be growing smaller
+ /// and smaller with each node generated.
+ ///
+ /// Artifact for which pool should be created.
+ ///
+ /// Max size of pool. Resulting pool is not guaranteed to be exactly as large, but it will 100% won't be bigger.
+ ///
+ private List CreateTriggerPool(Entity ent, int size)
+ {
+ var triggerPool = new List(size);
+ var weightsProto = PrototypeManager.Index(ent.Comp.TriggerWeights);
+ var weightsByTriggersLeft = new Dictionary(weightsProto.Weights);
+
+ while (triggerPool.Count < size)
+ {
+ // OOPS! We ran out of triggers.
+ if (weightsByTriggersLeft.Count == 0)
+ {
+ Log.Error($"Insufficient triggers for generating {ToPrettyString(ent)}! Needed {size} but had {triggerPool.Count}");
+ return triggerPool;
+ }
+
+ var triggerId = RobustRandom.Pick(weightsByTriggersLeft);
+ weightsByTriggersLeft.Remove(triggerId);
+ var trigger = PrototypeManager.Index(triggerId);
+ if (_entityWhitelist.IsWhitelistFail(trigger.Whitelist, ent))
+ continue;
+
+ triggerPool.Add(trigger);
+ }
+
+ return triggerPool;
+ }
+
+ ///
+ /// Generates segment of artifact - isolated graph, nodes inside which are interconnected.
+ /// As size of segment is randomized - it is subtracted from node count.
+ ///
+ private void GenerateArtifactSegment(
+ Entity ent,
+ List triggerPool,
+ ref int nodeCount
+ )
+ {
+ var segmentSize = GetArtifactSegmentSize(ent, nodeCount);
+ nodeCount -= segmentSize;
+ var populatedNodes = PopulateArtifactSegmentRecursive(ent, triggerPool, ref segmentSize);
+
+ var segments = GetSegmentsFromNodes(ent, populatedNodes).ToList();
+
+ // We didn't connect all of our nodes: do extra work to make sure there's a connection.
+ if (segments.Count > 1)
+ {
+ var parent = segments.MaxBy(s => s.Count)!;
+ var minP = parent.Min(n => n.Comp.Depth);
+ var maxP = parent.Max(n => n.Comp.Depth);
+
+ segments.Remove(parent);
+ foreach (var segment in segments)
+ {
+ // calculate the range of the depth of the nodes in the segment
+ var minS = segment.Min(n => n.Comp.Depth);
+ var maxS = segment.Max(n => n.Comp.Depth);
+
+ // Figure out the range of depths that allows for a connection between these two.
+ // The range is essentially the lower values + 1 on each side.
+ var min = Math.Max(minS, minP) - 1;
+ var max = Math.Min(maxS, maxP) + 1;
+
+ // how the fuck did you do this? you don't even deserve to get a parent. fuck you.
+ if (min > max || min == max)
+ continue;
+
+ var node1Options = segment.Where(n => n.Comp.Depth >= min && n.Comp.Depth <= max)
+ .ToList();
+ if (node1Options.Count == 0)
+ {
+ continue;
+ }
+
+ var node1 = RobustRandom.Pick(node1Options);
+ var node1Depth = node1.Comp.Depth;
+
+ var node2Options = parent.Where(n => n.Comp.Depth >= node1Depth - 1 && n.Comp.Depth <= node1Depth + 1 && n.Comp.Depth != node1Depth)
+ .ToList();
+ if (node2Options.Count == 0)
+ {
+ continue;
+ }
+
+ var node2 = RobustRandom.Pick(node2Options);
+
+ if (node1.Comp.Depth < node2.Comp.Depth)
+ {
+ AddEdge((ent, ent.Comp), node1, node2, false);
+ }
+ else
+ {
+ AddEdge((ent, ent.Comp), node2, node1, false);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Recursively populate layers of artifact segment - isolated graph, nodes inside which are interconnected.
+ /// Each next iteration is going to have more chances to have more nodes (so it goes 'from top to bottom' of
+ /// the tree, creating its peak nodes first, and then making layers with more and more branches).
+ ///
+ private List> PopulateArtifactSegmentRecursive(
+ Entity ent,
+ List triggerPool,
+ ref int segmentSize,
+ int iteration = 0
+ )
+ {
+ if (segmentSize == 0)
+ return new();
+
+ // Try and get larger as we create more layers. Prevents excessive layers.
+ var mod = RobustRandom.Next((int) (iteration / 1.5f), iteration + 1);
+
+ var layerMin = Math.Min(ent.Comp.NodesPerSegmentLayer.Min + mod, segmentSize);
+ var layerMax = Math.Min(ent.Comp.NodesPerSegmentLayer.Max + mod, segmentSize);
+
+ // Default to one node if we had shenanigans and ended up with weird layer counts.
+ var nodeCount = 1;
+ if (layerMax >= layerMin)
+ nodeCount = RobustRandom.Next(layerMin, layerMax + 1); // account for non-inclusive max
+
+ segmentSize -= nodeCount;
+ var nodes = new List>();
+ for (var i = 0; i < nodeCount; i++)
+ {
+ var trigger = RobustRandom.PickAndTake(triggerPool);
+ nodes.Add(CreateNode(ent, trigger, iteration));
+ }
+
+ var successors = PopulateArtifactSegmentRecursive(
+ ent,
+ triggerPool,
+ ref segmentSize,
+ iteration: iteration + 1
+ );
+
+ if (successors.Count == 0)
+ return nodes;
+
+ foreach (var successor in successors)
+ {
+ var node = RobustRandom.Pick(nodes);
+ AddEdge((ent, ent), node, successor, dirty: false);
+ }
+
+ // randomly add in some extra edges for variance.
+ var scatterCount = ent.Comp.ScatterPerLayer.Next(RobustRandom);
+ for (var i = 0; i < scatterCount; i++)
+ {
+ var node = RobustRandom.Pick(nodes);
+ var successor = RobustRandom.Pick(successors);
+ AddEdge((ent, ent), node, successor, dirty: false);
+ }
+
+ return nodes;
+ }
+
+ ///
+ /// Rolls segment size, based on amount of nodes left and XenoArtifactComponent settings.
+ ///
+ private int GetArtifactSegmentSize(Entity ent, int nodeCount)
+ {
+ // Make sure we can't generate a single segment artifact.
+ // We always want to have at least 2 segments. For variety.
+ var segmentMin = ent.Comp.SegmentSize.Min;
+ var segmentMax = Math.Min(ent.Comp.SegmentSize.Max, Math.Max(nodeCount / 2, segmentMin));
+
+ var segmentSize = RobustRandom.Next(segmentMin, segmentMax + 1); // account for non-inclusive max
+ var remainder = nodeCount - segmentSize;
+
+ // If our next segment is going to be undersized, then we just absorb it into this segment.
+ if (remainder < ent.Comp.SegmentSize.Min)
+ segmentSize += remainder;
+
+ // Sanity check to make sure we don't exceed the node count. (it shouldn't happen prior anyway but oh well)
+ segmentSize = Math.Min(nodeCount, segmentSize);
+
+ return segmentSize;
+ }
+}
diff --git a/Content.Server/Xenoarchaeology/Artifact/XenoArtifactSystem.cs b/Content.Server/Xenoarchaeology/Artifact/XenoArtifactSystem.cs
new file mode 100644
index 000000000000..c72e5fbd086f
--- /dev/null
+++ b/Content.Server/Xenoarchaeology/Artifact/XenoArtifactSystem.cs
@@ -0,0 +1,34 @@
+using Content.Server.Cargo.Systems;
+using Content.Shared.Xenoarchaeology.Artifact;
+using Content.Shared.Xenoarchaeology.Artifact.Components;
+
+namespace Content.Server.Xenoarchaeology.Artifact;
+
+public sealed partial class XenoArtifactSystem : SharedXenoArtifactSystem
+{
+ ///
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnArtifactMapInit);
+ SubscribeLocalEvent(OnCalculatePrice);
+ }
+
+ private void OnArtifactMapInit(Entity ent, ref MapInitEvent args)
+ {
+ if (ent.Comp.IsGenerationRequired)
+ GenerateArtifactStructure(ent);
+ }
+
+ private void OnCalculatePrice(Entity ent, ref PriceCalculationEvent args)
+ {
+ foreach (var node in GetAllNodes(ent))
+ {
+ if (node.Comp.Locked)
+ continue;
+
+ args.Price += node.Comp.ResearchValue * ent.Comp.PriceMultiplier;
+ }
+ }
+}
diff --git a/Content.Server/Xenoarchaeology/Artifact/XenoArtifactUnlockNodeCommand.cs b/Content.Server/Xenoarchaeology/Artifact/XenoArtifactUnlockNodeCommand.cs
new file mode 100644
index 000000000000..8fb523e1eb9c
--- /dev/null
+++ b/Content.Server/Xenoarchaeology/Artifact/XenoArtifactUnlockNodeCommand.cs
@@ -0,0 +1,83 @@
+using Content.Server.Administration;
+using Content.Shared.Administration;
+using Content.Shared.Xenoarchaeology.Artifact.Components;
+using Robust.Shared.Console;
+
+namespace Content.Server.Xenoarchaeology.Artifact;
+
+[AdminCommand(AdminFlags.Debug)]
+public sealed class XenoArtifactUnlockNodeCommand : LocalizedCommands
+{
+ [Dependency] private readonly EntityManager _entities = default!;
+
+ ///
+ public override string Command => "unlocknode";
+
+ ///
+ public override string Description => Loc.GetString("cmd-unlocknode-desc");
+
+ ///
+ public override string Help => Loc.GetString("cmd-unlocknode-help");
+
+ ///
+ public override void Execute(IConsoleShell shell, string argStr, string[] args)
+ {
+ if (args.Length != 2)
+ {
+ shell.WriteError(Loc.GetString("cmd-parse-failure-unlocknode-arg-num"));
+ return;
+ }
+
+ if (!NetEntity.TryParse(args[1], out var netNode))
+ {
+ shell.WriteError(Loc.GetString("cmd-parse-failure-unlocknode-invalid-entity"));
+ return;
+ }
+
+ if (!_entities.TryGetEntity(netNode, out var entityUid))
+ {
+ shell.WriteError(Loc.GetString("cmd-parse-failure-unlocknode-invalid-entity"));
+ return;
+ }
+ _entities.System()
+ .SetNodeUnlocked(entityUid.Value);
+ }
+
+ ///
+ public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
+ {
+ if (args.Length == 1)
+ {
+ var query = _entities.EntityQueryEnumerator();
+ var completionOptions = new List();
+ while (query.MoveNext(out var uid, out _))
+ {
+ completionOptions.Add(new CompletionOption(uid.ToString()));
+ }
+
+ return CompletionResult.FromHintOptions(completionOptions, "");
+ }
+
+ if (args.Length == 2 &&
+ NetEntity.TryParse(args[0], out var netEnt) &&
+ _entities.TryGetEntity(netEnt, out var artifactUid) &&
+ _entities.TryGetComponent(artifactUid, out var comp))
+ {
+ var artifactSystem = _entities.System();
+
+ var result = new List();
+ foreach (var node in artifactSystem.GetAllNodes((artifactUid.Value, comp)))
+ {
+ var metaData = _entities.MetaQuery.Comp(artifactUid.Value);
+ var entityUidStr = _entities.GetNetEntity(node)
+ .ToString();
+ var completionOption = new CompletionOption(entityUidStr, metaData.EntityName);
+ result.Add(completionOption);
+ }
+
+ return CompletionResult.FromHintOptions(result, "");
+ }
+
+ return CompletionResult.Empty;
+ }
+}
diff --git a/Content.Server/Xenoarchaeology/Equipment/ArtifactAnalyzerSystem.cs b/Content.Server/Xenoarchaeology/Equipment/ArtifactAnalyzerSystem.cs
new file mode 100644
index 000000000000..2d5aa459fbe3
--- /dev/null
+++ b/Content.Server/Xenoarchaeology/Equipment/ArtifactAnalyzerSystem.cs
@@ -0,0 +1,50 @@
+using Content.Server.Research.Systems;
+using Content.Server.Xenoarchaeology.Artifact;
+using Content.Shared.Popups;
+using Content.Shared.Xenoarchaeology.Equipment;
+using Content.Shared.Xenoarchaeology.Equipment.Components;
+using Robust.Shared.Audio.Systems;
+
+namespace Content.Server.Xenoarchaeology.Equipment;
+
+///
+public sealed class ArtifactAnalyzerSystem : SharedArtifactAnalyzerSystem
+{
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+ [Dependency] private readonly ResearchSystem _research = default!;
+ [Dependency] private readonly XenoArtifactSystem _xenoArtifact = default!;
+
+ ///
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnExtractButtonPressed);
+ }
+
+ private void OnExtractButtonPressed(Entity ent, ref AnalysisConsoleExtractButtonPressedMessage args)
+ {
+ if (!TryGetArtifactFromConsole(ent, out var artifact))
+ return;
+
+ if (!_research.TryGetClientServer(ent, out var server, out var serverComponent))
+ return;
+
+ var sumResearch = 0;
+ foreach (var node in _xenoArtifact.GetAllNodes(artifact.Value))
+ {
+ var research = _xenoArtifact.GetResearchValue(node);
+ _xenoArtifact.SetConsumedResearchValue(node, node.Comp.ConsumedResearchValue + research);
+ sumResearch += research;
+ }
+
+ if (sumResearch == 0)
+ return;
+
+ _research.ModifyServerPoints(server.Value, sumResearch, serverComponent);
+ _audio.PlayPvs(ent.Comp.ExtractSound, artifact.Value);
+ _popup.PopupEntity(Loc.GetString("analyzer-artifact-extract-popup"), artifact.Value, PopupType.Large);
+ }
+}
+
diff --git a/Content.Server/Xenoarchaeology/Equipment/Components/ActiveArtifactAnalyzerComponent.cs b/Content.Server/Xenoarchaeology/Equipment/Components/ActiveArtifactAnalyzerComponent.cs
deleted file mode 100644
index 7c1fe288fc10..000000000000
--- a/Content.Server/Xenoarchaeology/Equipment/Components/ActiveArtifactAnalyzerComponent.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using Robust.Shared.Serialization.TypeSerializers.Implementations;
-
-namespace Content.Server.Xenoarchaeology.Equipment.Components;
-
-///
-/// Activecomp used for tracking artifact analyzers that are currently
-/// in the process of scanning an artifact.
-///
-[RegisterComponent]
-public sealed partial class ActiveArtifactAnalyzerComponent : Component
-{
- ///
- /// When did the scanning start or last resume?
- ///
- [DataField("startTime", customTypeSerializer: typeof(TimespanSerializer))]
- public TimeSpan StartTime;
-
- ///
- /// When pausing, this will store the duration the scan has already been running for.
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- public TimeSpan AccumulatedRunTime;
-
- ///
- /// Is analysis paused?
- /// It could be when the Artifact Analyzer has no power, for example.
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- public bool AnalysisPaused = false;
-
- ///
- /// What is being scanned?
- ///
- [DataField]
- public EntityUid Artifact;
-}
diff --git a/Content.Server/Xenoarchaeology/Equipment/Components/ActiveScannedArtifactComponent.cs b/Content.Server/Xenoarchaeology/Equipment/Components/ActiveScannedArtifactComponent.cs
deleted file mode 100644
index da6b35dab0a8..000000000000
--- a/Content.Server/Xenoarchaeology/Equipment/Components/ActiveScannedArtifactComponent.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using Robust.Shared.Audio;
-
-namespace Content.Server.Xenoarchaeology.Equipment.Components;
-
-///
-/// This is used for tracking artifacts that are currently
-/// being scanned by
-///
-[RegisterComponent]
-public sealed partial class ActiveScannedArtifactComponent : Component
-{
- ///
- /// The scanner that is scanning this artifact
- ///
- [ViewVariables]
- public EntityUid Scanner;
-
- ///
- /// The sound that plays when the scan fails
- ///
- public readonly SoundSpecifier ScanFailureSound = new SoundPathSpecifier("/Audio/Machines/custom_deny.ogg");
-}
diff --git a/Content.Server/Xenoarchaeology/Equipment/Components/AnalysisConsoleComponent.cs b/Content.Server/Xenoarchaeology/Equipment/Components/AnalysisConsoleComponent.cs
deleted file mode 100644
index 892e24495bb4..000000000000
--- a/Content.Server/Xenoarchaeology/Equipment/Components/AnalysisConsoleComponent.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using Content.Shared.DeviceLinking;
-using Robust.Shared.Audio;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-
-namespace Content.Server.Xenoarchaeology.Equipment.Components;
-
-///
-/// The console that is used for artifact analysis
-///
-[RegisterComponent]
-public sealed partial class AnalysisConsoleComponent : Component
-{
- ///
- /// The analyzer entity the console is linked.
- /// Can be null if not linked.
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- public EntityUid? AnalyzerEntity;
-
- ///
- /// The machine linking port for the analyzer
- ///
- [DataField("linkingPort", customTypeSerializer: typeof(PrototypeIdSerializer))]
- public string LinkingPort = "ArtifactAnalyzerSender";
-
- ///
- /// The sound played when an artifact has points extracted.
- ///
- [DataField("extractSound")]
- public SoundSpecifier ExtractSound = new SoundPathSpecifier("/Audio/Effects/radpulse11.ogg");
-
- ///
- /// The entity spawned by a report.
- ///
- [DataField("reportEntityId", customTypeSerializer: typeof(PrototypeIdSerializer))]
- public string ReportEntityId = "Paper";
-}
diff --git a/Content.Server/Xenoarchaeology/Equipment/Components/ArtifactAnalyzerComponent.cs b/Content.Server/Xenoarchaeology/Equipment/Components/ArtifactAnalyzerComponent.cs
deleted file mode 100644
index 949717676fca..000000000000
--- a/Content.Server/Xenoarchaeology/Equipment/Components/ArtifactAnalyzerComponent.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-using Content.Server.Xenoarchaeology.XenoArtifacts;
-using Content.Shared.Construction.Prototypes;
-using Robust.Shared.Audio;
-using Robust.Shared.Serialization.TypeSerializers.Implementations;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-
-namespace Content.Server.Xenoarchaeology.Equipment.Components;
-
-///
-/// A machine that is combined and linked to the
-/// in order to analyze artifacts and extract points.
-///
-[RegisterComponent]
-public sealed partial class ArtifactAnalyzerComponent : Component
-{
- ///
- /// How long it takes to analyze an artifact
- ///
- [DataField("analysisDuration", customTypeSerializer: typeof(TimespanSerializer))]
- public TimeSpan AnalysisDuration = TimeSpan.FromSeconds(30);
-
- ///
- /// The corresponding console entity.
- /// Can be null if not linked.
- ///
- [ViewVariables]
- public EntityUid? Console;
-
- [ViewVariables(VVAccess.ReadWrite)]
- public bool ReadyToPrint = false;
-
- [DataField("scanFinishedSound")]
- public SoundSpecifier ScanFinishedSound = new SoundPathSpecifier("/Audio/Machines/scan_finish.ogg");
-
- #region Analysis Data
- [DataField]
- public EntityUid? LastAnalyzedArtifact;
-
- [ViewVariables]
- public ArtifactNode? LastAnalyzedNode;
-
- [ViewVariables(VVAccess.ReadWrite)]
- public int? LastAnalyzerPointValue;
- #endregion
-}
diff --git a/Content.Server/Xenoarchaeology/Equipment/Components/BiasedArtifactComponent.cs b/Content.Server/Xenoarchaeology/Equipment/Components/BiasedArtifactComponent.cs
deleted file mode 100644
index 16bd34d7eaee..000000000000
--- a/Content.Server/Xenoarchaeology/Equipment/Components/BiasedArtifactComponent.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-namespace Content.Server.Xenoarchaeology.Equipment.Components;
-
-///
-/// This is used for artifacts that are biased to move
-/// in a particular direction via the
-///
-[RegisterComponent]
-public sealed partial class BiasedArtifactComponent : Component
-{
- [ViewVariables]
- public EntityUid Provider;
-}
diff --git a/Content.Server/Xenoarchaeology/Equipment/Components/NodeScannerComponent.cs b/Content.Server/Xenoarchaeology/Equipment/Components/NodeScannerComponent.cs
deleted file mode 100644
index 36cc5f830efb..000000000000
--- a/Content.Server/Xenoarchaeology/Equipment/Components/NodeScannerComponent.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace Content.Server.Xenoarchaeology.Equipment.Components;
-
-[RegisterComponent]
-public sealed partial class NodeScannerComponent : Component
-{
-
-}
diff --git a/Content.Server/Xenoarchaeology/Equipment/Components/SuppressArtifactContainerComponent.cs b/Content.Server/Xenoarchaeology/Equipment/Components/SuppressArtifactContainerComponent.cs
deleted file mode 100644
index c555f314ad72..000000000000
--- a/Content.Server/Xenoarchaeology/Equipment/Components/SuppressArtifactContainerComponent.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace Content.Server.Xenoarchaeology.Equipment.Components;
-
-///
-/// Suppress artifact activation, when entity is placed inside this container.
-///
-[RegisterComponent]
-public sealed partial class SuppressArtifactContainerComponent : Component
-{
-
-}
diff --git a/Content.Server/Xenoarchaeology/Equipment/Components/TraversalDistorterComponent.cs b/Content.Server/Xenoarchaeology/Equipment/Components/TraversalDistorterComponent.cs
deleted file mode 100644
index fe95114f7378..000000000000
--- a/Content.Server/Xenoarchaeology/Equipment/Components/TraversalDistorterComponent.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-namespace Content.Server.Xenoarchaeology.Equipment.Components;
-
-///
-/// This is used for a machine that biases
-/// an artifact placed on it to move up/down
-///
-[RegisterComponent]
-public sealed partial class TraversalDistorterComponent : Component
-{
- [ViewVariables(VVAccess.ReadWrite)]
- public BiasDirection BiasDirection = BiasDirection.Up;
-
- public TimeSpan NextActivation = default!;
- public TimeSpan ActivationDelay = TimeSpan.FromSeconds(1);
-}
-
-public enum BiasDirection : byte
-{
- Up, //Towards depth 0
- Down, //Away from depth 0
-}
diff --git a/Content.Server/Xenoarchaeology/Equipment/Systems/ArtifactAnalyzerSystem.cs b/Content.Server/Xenoarchaeology/Equipment/Systems/ArtifactAnalyzerSystem.cs
deleted file mode 100644
index 67c67de57f3b..000000000000
--- a/Content.Server/Xenoarchaeology/Equipment/Systems/ArtifactAnalyzerSystem.cs
+++ /dev/null
@@ -1,521 +0,0 @@
-using System.Linq;
-using Content.Server.Power.Components;
-using Content.Server.Research.Systems;
-using Content.Shared.UserInterface;
-using Content.Server.Xenoarchaeology.Equipment.Components;
-using Content.Server.Xenoarchaeology.XenoArtifacts;
-using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
-using Content.Shared.Audio;
-using Content.Shared.DeviceLinking;
-using Content.Shared.DeviceLinking.Events;
-using Content.Shared.Paper;
-using Content.Shared.Placeable;
-using Content.Shared.Popups;
-using Content.Shared.Power;
-using Content.Shared.Research.Components;
-using Content.Shared.Xenoarchaeology.Equipment;
-using Content.Shared.Xenoarchaeology.XenoArtifacts;
-using JetBrains.Annotations;
-using Robust.Server.GameObjects;
-using Robust.Shared.Audio;
-using Robust.Shared.Audio.Systems;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Timing;
-using Robust.Shared.Utility;
-
-namespace Content.Server.Xenoarchaeology.Equipment.Systems;
-
-///
-/// This system is used for managing the artifact analyzer as well as the analysis console.
-/// It also hanadles scanning and ui updates for both systems.
-///
-public sealed class ArtifactAnalyzerSystem : EntitySystem
-{
- [Dependency] private readonly IGameTiming _timing = default!;
- [Dependency] private readonly IPrototypeManager _prototype = default!;
- [Dependency] private readonly SharedAudioSystem _audio = default!;
- [Dependency] private readonly SharedAmbientSoundSystem _ambientSound = default!;
- [Dependency] private readonly SharedPopupSystem _popup = default!;
- [Dependency] private readonly UserInterfaceSystem _ui = default!;
- [Dependency] private readonly ArtifactSystem _artifact = default!;
- [Dependency] private readonly PaperSystem _paper = default!;
- [Dependency] private readonly ResearchSystem _research = default!;
- [Dependency] private readonly MetaDataSystem _metaSystem = default!;
- [Dependency] private readonly TraversalDistorterSystem _traversalDistorter = default!;
-
- ///
- public override void Initialize()
- {
- SubscribeLocalEvent(OnArtifactActivated);
-
- SubscribeLocalEvent(OnAnalyzeStart);
- SubscribeLocalEvent(OnAnalyzeEnd);
- SubscribeLocalEvent(OnPowerChanged);
-
- SubscribeLocalEvent(OnItemPlaced);
- SubscribeLocalEvent(OnItemRemoved);
-
- SubscribeLocalEvent(OnMapInit);
- SubscribeLocalEvent(OnNewLink);
- SubscribeLocalEvent(OnPortDisconnected);
-
- SubscribeLocalEvent(OnServerSelectionMessage);
- SubscribeLocalEvent(OnScanButton);
- SubscribeLocalEvent(OnPrintButton);
- SubscribeLocalEvent(OnExtractButton);
- SubscribeLocalEvent(OnBiasButton);
-
- SubscribeLocalEvent((e, c, _) => UpdateUserInterface(e, c),
- after: new[] { typeof(ResearchSystem) });
- SubscribeLocalEvent((e, c, _) => UpdateUserInterface(e, c),
- after: new[] { typeof(ResearchSystem) });
- SubscribeLocalEvent((e, c, _) => UpdateUserInterface(e, c));
- }
-
- public override void Update(float frameTime)
- {
- base.Update(frameTime);
-
- var query = EntityQueryEnumerator();
- while (query.MoveNext(out var uid, out var active, out var scan))
- {
- if (active.AnalysisPaused)
- continue;
-
- if (_timing.CurTime - active.StartTime < scan.AnalysisDuration - active.AccumulatedRunTime)
- continue;
-
- FinishScan(uid, scan, active);
- }
- }
-
- ///
- /// Resets the current scan on the artifact analyzer
- ///
- /// The analyzer being reset
- ///
- [PublicAPI]
- public void ResetAnalyzer(EntityUid uid, ArtifactAnalyzerComponent? component = null)
- {
- if (!Resolve(uid, ref component))
- return;
-
- component.LastAnalyzedArtifact = null;
- component.ReadyToPrint = false;
- UpdateAnalyzerInformation(uid, component);
- }
-
- ///
- /// Goes through the current entities on
- /// the analyzer and returns a valid artifact
- ///
- ///
- ///
- ///
- private EntityUid? GetArtifactForAnalysis(EntityUid? uid, ItemPlacerComponent? placer = null)
- {
- if (uid == null || !Resolve(uid.Value, ref placer))
- return null;
-
- return placer.PlacedEntities.FirstOrNull();
- }
-
- ///
- /// Updates the current scan information based on
- /// the last artifact that was scanned.
- ///
- ///
- ///
- private void UpdateAnalyzerInformation(EntityUid uid, ArtifactAnalyzerComponent? component = null)
- {
- if (!Resolve(uid, ref component))
- return;
-
- if (component.LastAnalyzedArtifact == null)
- {
- component.LastAnalyzerPointValue = null;
- component.LastAnalyzedNode = null;
- }
- else if (TryComp(component.LastAnalyzedArtifact, out var artifact))
- {
- var lastNode = artifact.CurrentNodeId == null
- ? null
- : (ArtifactNode?) _artifact.GetNodeFromId(artifact.CurrentNodeId.Value, artifact).Clone();
- component.LastAnalyzedNode = lastNode;
- component.LastAnalyzerPointValue = _artifact.GetResearchPointValue(component.LastAnalyzedArtifact.Value, artifact);
- }
- }
-
- private void OnMapInit(EntityUid uid, ArtifactAnalyzerComponent component, MapInitEvent args)
- {
- if (!TryComp(uid, out var sink))
- return;
-
- foreach (var source in sink.LinkedSources)
- {
- if (!TryComp(source, out var analysis))
- continue;
- component.Console = source;
- analysis.AnalyzerEntity = uid;
- return;
- }
- }
-
- private void OnNewLink(EntityUid uid, AnalysisConsoleComponent component, NewLinkEvent args)
- {
- if (!TryComp(args.Sink, out var analyzer))
- return;
-
- component.AnalyzerEntity = args.Sink;
- analyzer.Console = uid;
-
- UpdateUserInterface(uid, component);
- }
-
- private void OnPortDisconnected(EntityUid uid, AnalysisConsoleComponent component, PortDisconnectedEvent args)
- {
- if (args.Port == component.LinkingPort && component.AnalyzerEntity != null)
- {
- if (TryComp(component.AnalyzerEntity, out var analyzezr))
- analyzezr.Console = null;
- component.AnalyzerEntity = null;
- }
-
- UpdateUserInterface(uid, component);
- }
-
- private void UpdateUserInterface(EntityUid uid, AnalysisConsoleComponent? component = null)
- {
- if (!Resolve(uid, ref component, false))
- return;
-
- EntityUid? artifact = null;
- FormattedMessage? msg = null;
- TimeSpan? totalTime = null;
- var canScan = false;
- var canPrint = false;
- var points = 0;
-
- if (TryComp(component.AnalyzerEntity, out var analyzer))
- {
- artifact = analyzer.LastAnalyzedArtifact;
- msg = GetArtifactScanMessage(analyzer);
- totalTime = analyzer.AnalysisDuration;
- if (TryComp(component.AnalyzerEntity, out var placer))
- canScan = placer.PlacedEntities.Any();
- canPrint = analyzer.ReadyToPrint;
-
- // the artifact that's actually on the scanner right now.
- if (GetArtifactForAnalysis(component.AnalyzerEntity, placer) is { } current)
- points = _artifact.GetResearchPointValue(current);
- }
-
- var analyzerConnected = component.AnalyzerEntity != null;
- var serverConnected = TryComp(uid, out var client) && client.ConnectedToServer;
-
- var scanning = TryComp(component.AnalyzerEntity, out var active);
- var paused = active != null ? active.AnalysisPaused : false;
-
- var biasDirection = BiasDirection.Up;
-
- if (TryComp(component.AnalyzerEntity, out var trav))
- biasDirection = trav.BiasDirection;
-
- var state = new AnalysisConsoleUpdateState(GetNetEntity(artifact), analyzerConnected, serverConnected,
- canScan, canPrint, msg, scanning, paused, active?.StartTime, active?.AccumulatedRunTime, totalTime, points, biasDirection == BiasDirection.Down);
-
- _ui.SetUiState(uid, ArtifactAnalzyerUiKey.Key, state);
- }
-
- ///
- /// opens the server selection menu.
- ///
- ///
- ///
- ///
- private void OnServerSelectionMessage(EntityUid uid, AnalysisConsoleComponent component, AnalysisConsoleServerSelectionMessage args)
- {
- _ui.OpenUi(uid, ResearchClientUiKey.Key, args.Actor);
- }
-
- ///
- /// Starts scanning the artifact.
- ///
- ///
- ///
- ///
- private void OnScanButton(EntityUid uid, AnalysisConsoleComponent component, AnalysisConsoleScanButtonPressedMessage args)
- {
- if (component.AnalyzerEntity == null)
- return;
-
- if (HasComp(component.AnalyzerEntity))
- return;
-
- var ent = GetArtifactForAnalysis(component.AnalyzerEntity);
- if (ent == null)
- return;
-
- var activeComp = EnsureComp(component.AnalyzerEntity.Value);
- activeComp.StartTime = _timing.CurTime;
- activeComp.AccumulatedRunTime = TimeSpan.Zero;
- activeComp.Artifact = ent.Value;
-
- if (TryComp(component.AnalyzerEntity.Value, out var powa))
- activeComp.AnalysisPaused = !powa.Powered;
-
- var activeArtifact = EnsureComp(ent.Value);
- activeArtifact.Scanner = component.AnalyzerEntity.Value;
- UpdateUserInterface(uid, component);
- }
-
- private void OnPrintButton(EntityUid uid, AnalysisConsoleComponent component, AnalysisConsolePrintButtonPressedMessage args)
- {
- if (component.AnalyzerEntity == null)
- return;
-
- if (!TryComp(component.AnalyzerEntity, out var analyzer) ||
- analyzer.LastAnalyzedNode == null ||
- analyzer.LastAnalyzerPointValue == null ||
- !analyzer.ReadyToPrint)
- {
- return;
- }
- analyzer.ReadyToPrint = false;
-
- var report = Spawn(component.ReportEntityId, Transform(uid).Coordinates);
- _metaSystem.SetEntityName(report, Loc.GetString("analysis-report-title", ("id", analyzer.LastAnalyzedNode.Id)));
-
- var msg = GetArtifactScanMessage(analyzer);
- if (msg == null)
- return;
-
- _popup.PopupEntity(Loc.GetString("analysis-console-print-popup"), uid);
- if (TryComp(report, out var paperComp))
- _paper.SetContent((report, paperComp), msg.ToMarkup());
- UpdateUserInterface(uid, component);
- }
-
- private FormattedMessage? GetArtifactScanMessage(ArtifactAnalyzerComponent component)
- {
- var msg = new FormattedMessage();
- if (component.LastAnalyzedNode == null)
- return null;
-
- var n = component.LastAnalyzedNode;
-
- msg.AddMarkupOrThrow(Loc.GetString("analysis-console-info-id", ("id", n.Id)));
- msg.PushNewline();
- msg.AddMarkupOrThrow(Loc.GetString("analysis-console-info-depth", ("depth", n.Depth)));
- msg.PushNewline();
-
- var activated = n.Triggered
- ? "analysis-console-info-triggered-true"
- : "analysis-console-info-triggered-false";
- msg.AddMarkupOrThrow(Loc.GetString(activated));
- msg.PushNewline();
-
- msg.PushNewline();
- var needSecondNewline = false;
-
- var triggerProto = _prototype.Index(n.Trigger);
- if (triggerProto.TriggerHint != null)
- {
- msg.AddMarkupOrThrow(Loc.GetString("analysis-console-info-trigger",
- ("trigger", Loc.GetString(triggerProto.TriggerHint))) + "\n");
- needSecondNewline = true;
- }
-
- var effectproto = _prototype.Index(n.Effect);
- if (effectproto.EffectHint != null)
- {
- msg.AddMarkupOrThrow(Loc.GetString("analysis-console-info-effect",
- ("effect", Loc.GetString(effectproto.EffectHint))) + "\n");
- needSecondNewline = true;
- }
-
- if (needSecondNewline)
- msg.PushNewline();
-
- msg.AddMarkupOrThrow(Loc.GetString("analysis-console-info-edges", ("edges", n.Edges.Count)));
- msg.PushNewline();
-
- if (component.LastAnalyzerPointValue != null)
- msg.AddMarkupOrThrow(Loc.GetString("analysis-console-info-value", ("value", component.LastAnalyzerPointValue)));
-
- return msg;
- }
-
- ///
- /// Extracts points from the artifact and updates the server points
- ///
- ///
- ///
- ///
- private void OnExtractButton(EntityUid uid, AnalysisConsoleComponent component, AnalysisConsoleExtractButtonPressedMessage args)
- {
- if (component.AnalyzerEntity == null)
- return;
-
- if (!_research.TryGetClientServer(uid, out var server, out var serverComponent))
- return;
-
- var artifact = GetArtifactForAnalysis(component.AnalyzerEntity);
- if (artifact == null)
- return;
-
- var pointValue = _artifact.GetResearchPointValue(artifact.Value);
-
- // no new nodes triggered so nothing to add
- if (pointValue == 0)
- return;
-
- _research.ModifyServerPoints(server.Value, pointValue, serverComponent);
- _artifact.AdjustConsumedPoints(artifact.Value, pointValue);
-
- _audio.PlayPvs(component.ExtractSound, component.AnalyzerEntity.Value, AudioParams.Default.WithVolume(2f));
-
- _popup.PopupEntity(Loc.GetString("analyzer-artifact-extract-popup"),
- component.AnalyzerEntity.Value, PopupType.Large);
-
- UpdateUserInterface(uid, component);
- }
-
- private void OnBiasButton(EntityUid uid, AnalysisConsoleComponent component, AnalysisConsoleBiasButtonPressedMessage args)
- {
- if (component.AnalyzerEntity == null)
- return;
-
- if (!TryComp(component.AnalyzerEntity, out var trav))
- return;
-
- if (!_traversalDistorter.SetState(component.AnalyzerEntity.Value, trav, args.IsDown))
- return;
-
- UpdateUserInterface(uid, component);
- }
-
- ///
- /// Cancels scans if the artifact changes nodes (is activated) during the scan.
- ///
- private void OnArtifactActivated(EntityUid uid, ActiveScannedArtifactComponent component, ArtifactActivatedEvent args)
- {
- CancelScan(uid);
- }
-
- ///
- /// Stops the current scan
- ///
- [PublicAPI]
- public void CancelScan(EntityUid artifact, ActiveScannedArtifactComponent? component = null, ArtifactAnalyzerComponent? analyzer = null)
- {
- if (!Resolve(artifact, ref component, false))
- return;
-
- if (!Resolve(component.Scanner, ref analyzer))
- return;
-
- _audio.PlayPvs(component.ScanFailureSound, component.Scanner, AudioParams.Default.WithVolume(3f));
-
- RemComp(component.Scanner);
- if (analyzer.Console != null)
- UpdateUserInterface(analyzer.Console.Value);
-
- RemCompDeferred(artifact, component);
- }
-
- ///
- /// Finishes the current scan.
- ///
- [PublicAPI]
- public void FinishScan(EntityUid uid, ArtifactAnalyzerComponent? component = null, ActiveArtifactAnalyzerComponent? active = null)
- {
- if (!Resolve(uid, ref component, ref active))
- return;
-
- component.ReadyToPrint = true;
- _audio.PlayPvs(component.ScanFinishedSound, uid);
- component.LastAnalyzedArtifact = active.Artifact;
- UpdateAnalyzerInformation(uid, component);
-
- RemComp(active.Artifact);
- RemComp(uid, active);
- if (component.Console != null)
- UpdateUserInterface(component.Console.Value);
- }
-
- [PublicAPI]
- public void PauseScan(EntityUid uid, ArtifactAnalyzerComponent? component = null, ActiveArtifactAnalyzerComponent? active = null)
- {
- if (!Resolve(uid, ref component, ref active) || active.AnalysisPaused)
- return;
-
- active.AnalysisPaused = true;
- // As we pause, we store what was already completed.
- active.AccumulatedRunTime = (_timing.CurTime - active.StartTime) + active.AccumulatedRunTime;
-
- if (Exists(component.Console))
- UpdateUserInterface(component.Console.Value);
- }
-
- [PublicAPI]
- public void ResumeScan(EntityUid uid, ArtifactAnalyzerComponent? component = null, ActiveArtifactAnalyzerComponent? active = null)
- {
- if (!Resolve(uid, ref component, ref active) || !active.AnalysisPaused)
- return;
-
- active.StartTime = _timing.CurTime;
- active.AnalysisPaused = false;
-
- if (Exists(component.Console))
- UpdateUserInterface(component.Console.Value);
- }
-
- private void OnItemPlaced(EntityUid uid, ArtifactAnalyzerComponent component, ref ItemPlacedEvent args)
- {
- if (component.Console != null && Exists(component.Console))
- UpdateUserInterface(component.Console.Value);
- }
-
- private void OnItemRemoved(EntityUid uid, ArtifactAnalyzerComponent component, ref ItemRemovedEvent args)
- {
- // Scanners shouldn't give permanent remove vision to an artifact, and the scanned artifact doesn't have any
- // component to track analyzers that have scanned it for removal if the artifact gets deleted.
- // So we always clear this on removal.
- component.LastAnalyzedArtifact = null;
-
- // cancel the scan if the artifact moves off the analyzer
- CancelScan(args.OtherEntity);
- if (Exists(component.Console))
- UpdateUserInterface(component.Console.Value);
- }
-
- private void OnAnalyzeStart(EntityUid uid, ActiveArtifactAnalyzerComponent component, ComponentStartup args)
- {
- if (TryComp(uid, out var powa))
- powa.NeedsPower = true;
-
- _ambientSound.SetAmbience(uid, true);
- }
-
- private void OnAnalyzeEnd(EntityUid uid, ActiveArtifactAnalyzerComponent component, ComponentShutdown args)
- {
- if (TryComp(uid, out var powa))
- powa.NeedsPower = false;
-
- _ambientSound.SetAmbience(uid, false);
- }
-
- private void OnPowerChanged(EntityUid uid, ActiveArtifactAnalyzerComponent active, ref PowerChangedEvent args)
- {
- if (!args.Powered)
- {
- PauseScan(uid, null, active);
- }
- else
- {
- ResumeScan(uid, null, active);
- }
- }
-}
-
diff --git a/Content.Server/Xenoarchaeology/Equipment/Systems/ArtifactCrusherSystem.cs b/Content.Server/Xenoarchaeology/Equipment/Systems/ArtifactCrusherSystem.cs
index f841ea910e77..8bc87cb37860 100644
--- a/Content.Server/Xenoarchaeology/Equipment/Systems/ArtifactCrusherSystem.cs
+++ b/Content.Server/Xenoarchaeology/Equipment/Systems/ArtifactCrusherSystem.cs
@@ -1,16 +1,15 @@
using Content.Server.Body.Systems;
using Content.Server.Popups;
-using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.Stack;
using Content.Server.Storage.Components;
-using Content.Server.Xenoarchaeology.XenoArtifacts;
using Content.Shared.Body.Components;
using Content.Shared.Damage;
using Content.Shared.Power;
using Content.Shared.Verbs;
using Content.Shared.Whitelist;
using Content.Shared.Xenoarchaeology.Equipment;
+using Content.Shared.Xenoarchaeology.Equipment.Components;
using Robust.Shared.Collections;
using Robust.Shared.Random;
using Robust.Shared.Timing;
@@ -22,7 +21,6 @@ public sealed class ArtifactCrusherSystem : SharedArtifactCrusherSystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IRobustRandom _random = default!;
- [Dependency] private readonly ArtifactSystem _artifact = default!;
[Dependency] private readonly BodySystem _body = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly StackSystem _stack = default!;
@@ -103,7 +101,6 @@ public void FinishCrushing(Entity(contained, out var body))
diff --git a/Content.Server/Xenoarchaeology/Equipment/Systems/NodeScannerSystem.cs b/Content.Server/Xenoarchaeology/Equipment/Systems/NodeScannerSystem.cs
index b388f3a6d49a..a941b3d4b96e 100644
--- a/Content.Server/Xenoarchaeology/Equipment/Systems/NodeScannerSystem.cs
+++ b/Content.Server/Xenoarchaeology/Equipment/Systems/NodeScannerSystem.cs
@@ -1,63 +1,12 @@
-using Content.Server.Popups;
-using Content.Server.Xenoarchaeology.Equipment.Components;
-using Content.Server.Xenoarchaeology.XenoArtifacts;
-using Content.Shared.Interaction;
-using Content.Shared.Timing;
-using Content.Shared.Verbs;
+using Content.Shared.Xenoarchaeology.Equipment;
+using Content.Shared.Xenoarchaeology.Equipment.Components;
namespace Content.Server.Xenoarchaeology.Equipment.Systems;
-public sealed class NodeScannerSystem : EntitySystem
+public sealed class NodeScannerSystem : SharedNodeScannerSystem
{
- [Dependency] private readonly UseDelaySystem _useDelay = default!;
- [Dependency] private readonly PopupSystem _popupSystem = default!;
-
- ///
- public override void Initialize()
- {
- SubscribeLocalEvent(OnBeforeRangedInteract);
- SubscribeLocalEvent>(AddScanVerb);
- }
-
- private void OnBeforeRangedInteract(EntityUid uid, NodeScannerComponent component, BeforeRangedInteractEvent args)
- {
- if (args.Handled || !args.CanReach || args.Target is not {} target)
- return;
-
- if (!TryComp(target, out var artifact) || artifact.CurrentNodeId == null)
- return;
-
- CreatePopup(uid, target, artifact);
- args.Handled = true;
- }
-
- private void AddScanVerb(EntityUid uid, NodeScannerComponent component, GetVerbsEvent args)
- {
- if (!args.CanAccess)
- return;
-
- if (!TryComp(args.Target, out var artifact) || artifact.CurrentNodeId == null)
- return;
-
- var verb = new UtilityVerb()
- {
- Act = () =>
- {
- CreatePopup(uid, args.Target, artifact);
- },
- Text = Loc.GetString("node-scan-tooltip")
- };
-
- args.Verbs.Add(verb);
- }
-
- private void CreatePopup(EntityUid uid, EntityUid target, ArtifactComponent artifact)
+ protected override void TryOpenUi(Entity device, EntityUid actor)
{
- if (TryComp(uid, out UseDelayComponent? useDelay)
- && !_useDelay.TryResetDelay((uid, useDelay), true))
- return;
-
- _popupSystem.PopupEntity(Loc.GetString("node-scan-popup",
- ("id", $"{artifact.CurrentNodeId}")), target);
+ // no-op
}
}
diff --git a/Content.Server/Xenoarchaeology/Equipment/Systems/TraversalDistorterSystem.cs b/Content.Server/Xenoarchaeology/Equipment/Systems/TraversalDistorterSystem.cs
deleted file mode 100644
index d277792243d8..000000000000
--- a/Content.Server/Xenoarchaeology/Equipment/Systems/TraversalDistorterSystem.cs
+++ /dev/null
@@ -1,79 +0,0 @@
-using Content.Server.Popups;
-using Content.Server.Power.EntitySystems;
-using Content.Server.Xenoarchaeology.Equipment.Components;
-using Content.Shared.Examine;
-using Content.Shared.Interaction;
-using Content.Shared.Placeable;
-using Robust.Shared.Timing;
-
-namespace Content.Server.Xenoarchaeology.Equipment.Systems;
-
-public sealed class TraversalDistorterSystem : EntitySystem
-{
- [Dependency] private readonly IGameTiming _timing = default!;
-
- ///
- public override void Initialize()
- {
- SubscribeLocalEvent(OnInit);
- SubscribeLocalEvent(OnExamine);
-
- SubscribeLocalEvent(OnItemPlaced);
- SubscribeLocalEvent(OnItemRemoved);
- }
-
- private void OnInit(EntityUid uid, TraversalDistorterComponent component, MapInitEvent args)
- {
- component.NextActivation = _timing.CurTime;
- }
-
- ///
- /// Switches the state of the traversal distorter between up and down.
- ///
- /// The distorter's entity
- /// The component on the entity
- /// If the distorter changed state
- public bool SetState(EntityUid uid, TraversalDistorterComponent component, bool isDown)
- {
- if (!this.IsPowered(uid, EntityManager))
- return false;
-
- if (_timing.CurTime < component.NextActivation)
- return false;
-
- component.NextActivation = _timing.CurTime + component.ActivationDelay;
-
- component.BiasDirection = isDown ? BiasDirection.Down : BiasDirection.Up;
-
- return true;
- }
-
- private void OnExamine(EntityUid uid, TraversalDistorterComponent component, ExaminedEvent args)
- {
- string examine = string.Empty;
- switch (component.BiasDirection)
- {
- case BiasDirection.Up:
- examine = Loc.GetString("traversal-distorter-desc-up");
- break;
- case BiasDirection.Down:
- examine = Loc.GetString("traversal-distorter-desc-down");
- break;
- }
-
- args.PushMarkup(examine);
- }
-
- private void OnItemPlaced(EntityUid uid, TraversalDistorterComponent component, ref ItemPlacedEvent args)
- {
- var bias = EnsureComp(args.OtherEntity);
- bias.Provider = uid;
- }
-
- private void OnItemRemoved(EntityUid uid, TraversalDistorterComponent component, ref ItemRemovedEvent args)
- {
- var otherEnt = args.OtherEntity;
- if (TryComp(otherEnt, out var bias) && bias.Provider == uid)
- RemComp(otherEnt, bias);
- }
-}
diff --git a/Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactComponent.cs b/Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactComponent.cs
deleted file mode 100644
index 4afd8af21cfd..000000000000
--- a/Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactComponent.cs
+++ /dev/null
@@ -1,166 +0,0 @@
-using Content.Shared.Xenoarchaeology.XenoArtifacts;
-using Robust.Shared.Audio;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-
-namespace Content.Server.Xenoarchaeology.XenoArtifacts;
-
-[RegisterComponent, Access(typeof(ArtifactSystem))]
-public sealed partial class ArtifactComponent : Component
-{
- ///
- /// Every node contained in the tree
- ///
- [DataField("nodeTree"), ViewVariables]
- public List NodeTree = new();
-
- ///
- /// The current node the artifact is on.
- ///
- [DataField("currentNodeId"), ViewVariables]
- public int? CurrentNodeId;
-
- #region Node Tree Gen
- ///
- /// Minimum number of nodes to generate, inclusive
- ///
- [DataField("nodesMin")]
- public int NodesMin = 3;
-
- ///
- /// Maximum number of nodes to generate, exclusive
- ///
- [DataField("nodesMax")]
- public int NodesMax = 9;
- #endregion
-
- ///
- /// Cooldown time between artifact activations (in seconds).
- ///
- [DataField("timer"), ViewVariables(VVAccess.ReadWrite)]
- public TimeSpan CooldownTime = TimeSpan.FromSeconds(5);
-
- ///
- /// Is this artifact under some suppression device?
- /// f true, will ignore all trigger activations attempts.
- ///
- [DataField("isSuppressed"), ViewVariables(VVAccess.ReadWrite)]
- public bool IsSuppressed;
-
- ///
- /// The last time the artifact was activated.
- ///
- [DataField("lastActivationTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
- public TimeSpan LastActivationTime;
-
- ///
- /// A multiplier applied to the calculated point value
- /// to determine the monetary value of the artifact
- ///
- [DataField("priceMultiplier"), ViewVariables(VVAccess.ReadWrite)]
- public float PriceMultiplier = 0.05f;
-
- ///
- /// The base amount of research points for each artifact node.
- ///
- [DataField("pointsPerNode"), ViewVariables(VVAccess.ReadWrite)]
- public int PointsPerNode = 6500;
-
- ///
- /// Research points which have been "consumed" from the theoretical max value of the artifact.
- ///
- [DataField("consumedPoints"), ViewVariables(VVAccess.ReadWrite)]
- public int ConsumedPoints;
-
- ///
- /// A multiplier that is raised to the power of the average depth of a node.
- /// Used for calculating the research point value of an artifact node.
- ///
- [DataField("pointDangerMultiplier"), ViewVariables(VVAccess.ReadWrite)]
- public float PointDangerMultiplier = 1.35f;
-
- ///
- /// The sound that plays when an artifact is activated
- ///
- [DataField("activationSound")]
- public SoundSpecifier ActivationSound = new SoundCollectionSpecifier("ArtifactActivation")
- {
- Params = new()
- {
- Variation = 0.1f,
- Volume = 3f
- }
- };
-
- [DataField("activateActionEntity")] public EntityUid? ActivateActionEntity;
-}
-
-///
-/// A single "node" of an artifact that contains various data about it.
-///
-[DataDefinition]
-public sealed partial class ArtifactNode : ICloneable
-{
- ///
- /// A numeric id corresponding to each node.
- ///
- [DataField("id"), ViewVariables]
- public int Id;
-
- ///
- /// how "deep" into the node tree. used for generation and price/value calculations
- ///
- [DataField("depth"), ViewVariables]
- public int Depth;
-
- ///
- /// A list of surrounding nodes. Used for tree traversal
- ///
- [DataField("edges"), ViewVariables]
- public HashSet Edges = new();
-
- ///
- /// Whether or not the node has been entered
- ///
- [DataField("discovered"), ViewVariables(VVAccess.ReadWrite)]
- public bool Discovered;
-
- ///
- /// The trigger for the node
- ///
- [DataField("trigger", customTypeSerializer: typeof(PrototypeIdSerializer), required: true), ViewVariables]
- public string Trigger = default!;
-
- ///
- /// Whether or not the node has been triggered
- ///
- [DataField("triggered"), ViewVariables(VVAccess.ReadWrite)]
- public bool Triggered;
-
- ///
- /// The effect when the node is activated
- ///
- [DataField("effect", customTypeSerializer: typeof(PrototypeIdSerializer), required: true), ViewVariables]
- public string Effect = default!;
-
- ///
- /// Used for storing cumulative information about nodes
- ///
- [DataField("nodeData"), ViewVariables]
- public Dictionary NodeData = new();
-
- public object Clone()
- {
- return new ArtifactNode
- {
- Id = Id,
- Depth = Depth,
- Edges = Edges,
- Discovered = Discovered,
- Trigger = Trigger,
- Triggered = Triggered,
- Effect = Effect,
- NodeData = NodeData
- };
- }
-}
diff --git a/Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactSystem.Actions.cs b/Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactSystem.Actions.cs
deleted file mode 100644
index 6c4089a2dbd2..000000000000
--- a/Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactSystem.Actions.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-using Content.Server.Actions;
-using Content.Server.Popups;
-using Content.Shared.Xenoarchaeology.XenoArtifacts;
-using Robust.Shared.Prototypes;
-
-namespace Content.Server.Xenoarchaeology.XenoArtifacts;
-
-public partial class ArtifactSystem
-{
- [Dependency] private readonly ActionsSystem _actions = default!;
- [Dependency] private readonly PopupSystem _popup = default!;
-
- [ValidatePrototypeId] private const string ArtifactActivateActionId = "ActionArtifactActivate";
-
- ///
- /// Used to add the artifact activation action (hehe), which lets sentient artifacts activate themselves,
- /// either through admemery or the sentience effect.
- ///
- public void InitializeActions()
- {
- SubscribeLocalEvent(OnMapInit);
- SubscribeLocalEvent(OnRemove);
-
- SubscribeLocalEvent(OnSelfActivate);
- }
-
- private void OnMapInit(EntityUid uid, ArtifactComponent component, MapInitEvent args)
- {
- RandomizeArtifact(uid, component);
- _actions.AddAction(uid, ref component.ActivateActionEntity, ArtifactActivateActionId);
- }
-
- private void OnRemove(EntityUid uid, ArtifactComponent component, ComponentRemove args)
- {
- _actions.RemoveAction(uid, component.ActivateActionEntity);
- }
-
- private void OnSelfActivate(EntityUid uid, ArtifactComponent component, ArtifactSelfActivateEvent args)
- {
- if (component.CurrentNodeId == null)
- return;
-
- var curNode = GetNodeFromId(component.CurrentNodeId.Value, component).Id;
- _popup.PopupEntity(Loc.GetString("activate-artifact-popup-self", ("node", curNode)), uid, uid);
- TryActivateArtifact(uid, uid, component);
-
- args.Handled = true;
- }
-}
diff --git a/Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactSystem.Commands.cs b/Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactSystem.Commands.cs
deleted file mode 100644
index d840a1f209d1..000000000000
--- a/Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactSystem.Commands.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-using System.Linq;
-using Content.Server.Administration;
-using Content.Shared.Administration;
-using Robust.Shared.Console;
-
-namespace Content.Server.Xenoarchaeology.XenoArtifacts;
-
-public partial class ArtifactSystem
-{
- [Dependency] private readonly IConsoleHost _conHost = default!;
-
- public void InitializeCommands()
- {
- _conHost.RegisterCommand("forceartifactnode", "Forces an artifact to traverse to a given node", "forceartifacteffect ",
- ForceArtifactNode,
- ForceArtifactNodeCompletions);
-
- _conHost.RegisterCommand("getartifactmaxvalue", "Reports the maximum research point value for a given artifact", "forceartifacteffect ",
- GetArtifactMaxValue);
- }
-
- [AdminCommand(AdminFlags.Fun)]
- private void ForceArtifactNode(IConsoleShell shell, string argstr, string[] args)
- {
- if (args.Length != 2)
- {
- shell.WriteError("Argument length must be 2");
- return;
- }
-
- if (!NetEntity.TryParse(args[0], out var uidNet) || !TryGetEntity(uidNet, out var uid) || !int.TryParse(args[1], out var id))
- return;
-
- if (!TryComp(uid, out var artifact))
- return;
-
- if (artifact.NodeTree.FirstOrDefault(n => n.Id == id) is { } node)
- {
- EnterNode(uid.Value, ref node);
- }
- }
-
- private CompletionResult ForceArtifactNodeCompletions(IConsoleShell shell, string[] args)
- {
- if (args.Length == 2 && NetEntity.TryParse(args[0], out var uidNet) && TryGetEntity(uidNet, out var uid))
- {
- if (TryComp(uid, out var artifact))
- {
- return CompletionResult.FromHintOptions(artifact.NodeTree.Select(s => s.Id.ToString()), "");
- }
- }
-
- return CompletionResult.Empty;
- }
-
- [AdminCommand(AdminFlags.Debug)]
- private void GetArtifactMaxValue(IConsoleShell shell, string argstr, string[] args)
- {
- if (args.Length != 1)
- shell.WriteError("Argument length must be 1");
-
- if (!NetEntity.TryParse(args[0], out var uidNet) || !TryGetEntity(uidNet, out var uid))
- return;
-
- if (!TryComp(uid, out var artifact))
- return;
-
- var pointSum = GetResearchPointValue(uid.Value, artifact, true);
- shell.WriteLine($"Max point value for {ToPrettyString(uid.Value)} with {artifact.NodeTree.Count} nodes: {pointSum}");
- }
-}
diff --git a/Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactSystem.Nodes.cs b/Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactSystem.Nodes.cs
deleted file mode 100644
index a0e8420011fc..000000000000
--- a/Content.Server/Xenoarchaeology/XenoArtifacts/ArtifactSystem.Nodes.cs
+++ /dev/null
@@ -1,244 +0,0 @@
-using System.Linq;
-using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
-using Content.Shared.Whitelist;
-using Content.Shared.Xenoarchaeology.XenoArtifacts;
-using JetBrains.Annotations;
-using Robust.Shared.Random;
-
-namespace Content.Server.Xenoarchaeology.XenoArtifacts;
-
-public sealed partial class ArtifactSystem
-{
- [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
-
- private const int MaxEdgesPerNode = 4;
-
- private readonly HashSet _usedNodeIds = new();
-
- ///
- /// Generate an Artifact tree with fully developed nodes.
- ///
- ///
- ///
- /// The amount of nodes it has.
- private void GenerateArtifactNodeTree(EntityUid artifact, List allNodes, int nodesToCreate)
- {
- if (nodesToCreate < 1)
- {
- Log.Error($"nodesToCreate {nodesToCreate} is less than 1. Aborting artifact tree generation.");
- return;
- }
-
- _usedNodeIds.Clear();
-
- var uninitializedNodes = new List { new(){ Id = GetValidNodeId() } };
- var createdNodes = 1;
-
- while (uninitializedNodes.Count > 0)
- {
- var node = uninitializedNodes[0];
- uninitializedNodes.Remove(node);
-
- node.Trigger = GetRandomTrigger(artifact, ref node);
- node.Effect = GetRandomEffect(artifact, ref node);
-
- var maxChildren = _random.Next(1, MaxEdgesPerNode - 1);
-
- for (var i = 0; i < maxChildren; i++)
- {
- if (nodesToCreate <= createdNodes)
- {
- break;
- }
-
- var child = new ArtifactNode {Id = GetValidNodeId(), Depth = node.Depth + 1};
- node.Edges.Add(child.Id);
- child.Edges.Add(node.Id);
-
- uninitializedNodes.Add(child);
- createdNodes++;
- }
-
- allNodes.Add(node);
- }
- }
-
- private int GetValidNodeId()
- {
- var id = _random.Next(100, 1000);
- while (_usedNodeIds.Contains(id))
- {
- id = _random.Next(100, 1000);
- }
-
- _usedNodeIds.Add(id);
-
- return id;
- }
-
- //yeah these two functions are near duplicates but i don't
- //want to implement an interface or abstract parent
-
- private string GetRandomTrigger(EntityUid artifact, ref ArtifactNode node)
- {
- var allTriggers = _prototype.EnumeratePrototypes()
- .Where(x => _whitelistSystem.IsWhitelistPassOrNull(x.Whitelist, artifact) &&
- _whitelistSystem.IsBlacklistFailOrNull(x.Blacklist, artifact)).ToList();
- var validDepth = allTriggers.Select(x => x.TargetDepth).Distinct().ToList();
-
- var weights = GetDepthWeights(validDepth, node.Depth);
- var selectedRandomTargetDepth = GetRandomTargetDepth(weights);
- var targetTriggers = allTriggers
- .Where(x => x.TargetDepth == selectedRandomTargetDepth).ToList();
-
- return _random.Pick(targetTriggers).ID;
- }
-
- private string GetRandomEffect(EntityUid artifact, ref ArtifactNode node)
- {
- var allEffects = _prototype.EnumeratePrototypes()
- .Where(x => _whitelistSystem.IsWhitelistPassOrNull(x.Whitelist, artifact) &&
- _whitelistSystem.IsBlacklistFailOrNull(x.Blacklist, artifact)).ToList();
- var validDepth = allEffects.Select(x => x.TargetDepth).Distinct().ToList();
-
- var weights = GetDepthWeights(validDepth, node.Depth);
- var selectedRandomTargetDepth = GetRandomTargetDepth(weights);
- var targetEffects = allEffects
- .Where(x => x.TargetDepth == selectedRandomTargetDepth).ToList();
-
- return _random.Pick(targetEffects).ID;
- }
-
- ///