3mo xeno archeology (first phase) (#33370)

* DAG Adjacency Matrix & Tests

* Fix sandbox type errors

* First pass on procgen

* Procgen adjustments

* Networking

* Cruft and god and beauty and analysis console

* convert to data types that dont make me want to kill myself

* starting work on console UI

* drawing nodes n shit

* damn that ui FUCKS

* XAT

* Add a bunch of basic triggers

* Fix trigger gen

* Add node info into the analysis console UI

* Add node unlocking

* more trigger cuz thats pretty cool

* final triggers + incorporate gnostic faith

* some ui changes, mostly

* Fix orphaned procgen segments

* its not random dipshit

* yeah... this one will make pjb happy....

* we call it a day for the UI

* imagine... shared power code...

* extraction WIP but we gotta sidequest momentarily

* oh hey would you look at that its the actual functionality

* distrotrased

* Small departure for randomness.

* ok yep yep indeed that is an effect very cool.

* thanos snap oldcode

* fuck it we ball

* feat: node scanner now displays triggered nodes. Removed unused old artifact systems and related code (most of it). xml-doc and minor fixups.

* refactor: most of preparations, cleanup and groundwork. also segment-related tests

* feature: all basic effects returning

* feat: finished effects lits, created weight lists for struct and handheld artifacts, fixed throw trigger and music ApplyComponent artifact effects not working

* feat: prevent non-first-time-predicted calls in shared artifact effect systems

* fix: remove gun effect from artifact effects - as it interferes with 'activate artefact' action

* fix: foam reagent selection, neat ApplyComponents art effect scenarios, handheld art is RadiationReceiver again

* fix: moved spawn/ pry&throw effect systems back to server part of code - entity duplication bugs were not quite fun

* refactor: fix protos

* refactor: fix linter

* fix: fix old artifact component names in yml

* fix: no more throwing error on artifact spawn with empty XAEFoamComponent.Reagents

* fix: removed old component usage in maps

* fix: remove more deleted components from map

* fix: ContainerContainer is now part of initial artifact entity, it won't be affecting UninitializedSaveTest

* refactor: fix tests, add loc description to toolshed commands

* Changed node scanner to tell the whole story about current artifact state

* refactor: remove excessive get of EntityCoordinates in XAE systems, removed Value access in NodeScannerDisplay

* fix: turned off TriggerInteraction, removed XAESpawn usage and system, EmpSystem now can use EntityCoordinates,

* fix: moved SharedXenoArtifactSystem.CancelUnlockingOnGraphStructureChange into RebuildXenoArtifactMetaData to lessen code coupling

* fix: XenoArtifactEffectJunkSpawn moved invalid rolls declaration

* refactor: set default value for XenoArtifactComponent.EffectsTable for tests

* fix: now explosions XAE can be activated for effect

* refactor: added some usedelay so artifactuse would'nt be spammed

* refactor: artifact-related hints improvements

* fix: artifact no longer spawns fauna into itself

* refactor: xml-doc and minor refactoring

* refactor: xml-doc for Xeno Artifact systems, renaming of questionable XAT systems

* map for playtest, TODO REVERT THIS

* fix: magboots trigger art from a mile

* refactor: bind artifact animation to unlocking state

* feat: radiation dmg now have reference to source (and artifacts won't irradiate themselves)

* fix: random artifact node durability now is rolled for max and not current value

* refactor: gas effects are more rare, hand-held artifact effects are filtered properly now, rad dmg trigger now requires only 20 dmg for activation

* feat: animations and sound effects for artifact force-use and failed finish of unlocking phase

* use only 1 file with art use animation

* refactor: minor artifact dmg triggers tuning

* feat: now nodes that CAN be unlocked are displayed with specific color in console.

* feat: now unlocking stage time is dynamic and it depends on amount of triggers player activated correctly. Failed one stops incrementing

* feat: now non-active unlocked nodes return more points if durability was not wasted

* feat: now puddle/foam effects change description of node

* fix: fix test failure

* refactor: renamed phasing effect, fixed failing test for elkridge

* minor balance changes

* refactor: split rare materials into separate effects

* feat: unlocked nodes without successor wont listen to unlocks, node unlock is not activating node

* fix: removed OnIrradiatedEvent duplicate c-tor

* revert changes of reach for playtest

* revert last row empty line removal on reach.yml

* fix: fix PVS bug, born from attempt to relay event to art nodes that were not synced yet to the client

* fix: fix elkridge for tests (again)

* refactor: xml-doc, more stuff predicted, allocation optimization in XAE/XAT systems

* refactor: naming

* refactor: extract variable refactor for XAEApplyComponentsSystem.OnActivated insides

* fix: duplicate xeno artifact unlocking sound fixed

* feat: CreatePuddle xeno artifact effect now can have min and max borders for chamicals to be drafted, minor XAECreatePuddleSystem refactor

* feat: networking for shared XAE components + xml-doc leftovers

* refactor: more xml-doc, fix XAEApplyComponentsComponent.Components not being serializable but trying to be

* refactor: xml-docs and XAEThrowThingsAroundSystem now uses circle and not box for prying tiles

* refactor: xml-docs, minor refactors

* revert XenoArtifactCommand.ArtifactPrototype being PrototId

* refactor: simplify the way ExtractionResearchLabel works

---------

Co-authored-by: EmoGarbage404 <retron404@gmail.com>
Co-authored-by: pa.pecherskij <pa.pecherskij@interfax.ru>
This commit is contained in:
Fildrance
2025-04-15 03:34:53 +03:00
committed by GitHub
parent e9d13ea565
commit d38042a0c0
233 changed files with 8162 additions and 5448 deletions

View File

@@ -0,0 +1,6 @@
using Content.Shared.Xenoarchaeology.Artifact;
namespace Content.Client.Xenoarchaeology.Artifact;
/// <inheritdoc/>
public sealed class XenoArtifactSystem : SharedXenoArtifactSystem;

View File

@@ -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;
/// <inheritdoc />
public sealed class ArtifactAnalyzerSystem : SharedArtifactAnalyzerSystem
{
[Dependency] private readonly UserInterfaceSystem _ui = default!;
/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AnalysisConsoleComponent, AfterAutoHandleStateEvent>(OnAnalysisConsoleAfterAutoHandleState);
SubscribeLocalEvent<ArtifactAnalyzerComponent, AfterAutoHandleStateEvent>(OnAnalyzerAfterAutoHandleState);
}
private void OnAnalysisConsoleAfterAutoHandleState(Entity<AnalysisConsoleComponent> ent, ref AfterAutoHandleStateEvent args)
{
UpdateBuiIfCanGetAnalysisConsoleUi(ent);
}
private void OnAnalyzerAfterAutoHandleState(Entity<ArtifactAnalyzerComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (!TryGetAnalysisConsole(ent, out var analysisConsole))
return;
UpdateBuiIfCanGetAnalysisConsoleUi(analysisConsole.Value);
}
private void UpdateBuiIfCanGetAnalysisConsoleUi(Entity<AnalysisConsoleComponent> analysisConsole)
{
if (_ui.TryGetOpenUi<AnalysisConsoleBoundUserInterface>(analysisConsole.Owner, ArtifactAnalyzerUiKey.Key, out var bui))
bui.Update(analysisConsole);
}
}

View File

@@ -0,0 +1,31 @@
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;
/// <inheritdoc cref="SharedNodeScannerSystem"/>
public sealed class NodeScannerSystem : SharedNodeScannerSystem
{
[Dependency] private readonly UserInterfaceSystem _ui = default!;
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NodeScannerComponent, AfterAutoHandleStateEvent>(OnAnalysisConsoleAfterAutoHandleState);
}
protected override void TryOpenUi(Entity<NodeScannerComponent> device, EntityUid actor)
{
_ui.TryOpenUi(device.Owner, NodeScannerUiKey.Key, actor, true);
}
private void OnAnalysisConsoleAfterAutoHandleState(Entity<NodeScannerComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (_ui.TryGetOpenUi<NodeScannerBoundUserInterface>(ent.Owner, NodeScannerUiKey.Key, out var bui))
bui.Update(ent);
}
}

View File

@@ -1,66 +1,50 @@
using Content.Shared.Xenoarchaeology.Equipment;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Content.Shared.Research.Components;
using Content.Shared.Xenoarchaeology.Equipment.Components;
using Robust.Client.UserInterface;
using JetBrains.Annotations;
namespace Content.Client.Xenoarchaeology.Ui;
/// <summary>
/// BUI for artifact analysis console, proxies server-provided UI updates
/// (related to device, connected artifact analyzer, and artifact lying on it).
/// </summary>
[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)
{
}
/// <inheritdoc />
protected override void Open()
{
base.Open();
_consoleMenu = this.CreateWindow<AnalysisConsoleMenu>();
_consoleMenu.SetOwner(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)
/// <summary>
/// Update UI state based on corresponding component.
/// </summary>
public void Update(Entity<AnalysisConsoleComponent> ent)
{
base.UpdateState(state);
switch (state)
{
case AnalysisConsoleUpdateState msg:
_consoleMenu?.SetButtonsDisabled(msg);
_consoleMenu?.UpdateInformationDisplay(msg);
_consoleMenu?.UpdateProgressBar(msg);
break;
}
_consoleMenu?.Update(ent);
}
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);

View File

@@ -1,80 +1,91 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
xmlns:ui="clr-namespace:Content.Client.Xenoarchaeology.Ui"
Title="{Loc 'analysis-console-menu-title'}"
MinSize="620 280"
SetSize="620 280">
MinSize="700 350"
SetSize="980 550">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" VerticalExpand="True">
<BoxContainer Margin="10 10 10 10" MinWidth="150" Orientation="Vertical"
VerticalExpand="True" SizeFlagsStretchRatio="1">
<BoxContainer Orientation="Vertical" VerticalExpand="True">
<Button Name="ServerSelectionButton"
Text="{Loc 'analysis-console-server-list-button'}"></Button>
<BoxContainer MinHeight="5"></BoxContainer>
<Button Name="ScanButton"
Text="{Loc 'analysis-console-scan-button'}"
ToolTip="{Loc 'analysis-console-scan-tooltip-info'}">
</Button>
<BoxContainer MinHeight="5"></BoxContainer>
<Button Name="PrintButton"
Text="{Loc 'analysis-console-print-button'}"
ToolTip="{Loc 'analysis-console-print-tooltip-info'}">
</Button>
<BoxContainer MinHeight="5"></BoxContainer>
<BoxContainer Orientation="Horizontal">
<Button Name="UpBiasButton"
Text="{Loc 'analysis-console-bias-up'}"
ToolTip="{Loc 'analysis-console-bias-button-info-up'}"
HorizontalExpand="True"
StyleClasses="OpenRight">
</Button>
<Button Name="DownBiasButton"
Text="{Loc 'analysis-console-bias-down'}"
ToolTip="{Loc 'analysis-console-bias-button-info-down'}"
HorizontalExpand="True"
StyleClasses="OpenLeft">
</Button>
<BoxContainer Margin="10 10 10 10" MaxWidth="240" SetWidth="240" Orientation="Vertical" HorizontalExpand="False" VerticalExpand="True">
<PanelContainer Name="BackPanel" HorizontalAlignment="Center">
<PanelContainer.PanelOverride>
<gfx:StyleBoxTexture Modulate="#1B1B1E" PatchMarginBottom="10" PatchMarginLeft="10" PatchMarginRight="10" PatchMarginTop="10"/>
</PanelContainer.PanelOverride>
<BoxContainer HorizontalExpand="True" VerticalExpand="True" MinSize="128 128">
<SpriteView Name="ArtifactView" Scale="4 4" HorizontalAlignment="Center" VerticalAlignment="Center" HorizontalExpand="True" VerticalExpand="True"/>
</BoxContainer>
<BoxContainer MinHeight="15"></BoxContainer>
<Button Name="ExtractButton"
Text="{Loc 'analysis-console-extract-button'}"
ToolTip="{Loc 'analysis-console-extract-button-info'}">
</Button>
</BoxContainer>
<BoxContainer Orientation="Vertical">
<Label Name="ProgressLabel"></Label>
<ProgressBar
Name="ProgressBar"
MinValue="0"
MaxValue="1"
SetHeight="20">
</ProgressBar>
</BoxContainer>
</BoxContainer>
<customControls:VSeparator StyleClasses="LowDivider" />
<PanelContainer Margin="10 10 10 10" HorizontalExpand="True" SizeFlagsStretchRatio="3">
</PanelContainer>
<customControls:HSeparator StyleClasses="HighDivider" Margin="0 15 0 10"/>
<BoxContainer Name="ExtractContainer" Orientation="Vertical" VerticalExpand="True" Visible="False">
<PanelContainer HorizontalExpand="True" VerticalExpand="True" RectClipContent="True">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#000000FF" />
</PanelContainer.PanelOverride>
<BoxContainer Margin="10 10 10 10" Orientation="Horizontal">
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
<BoxContainer VerticalExpand="True">
<RichTextLabel Name="Information"> </RichTextLabel>
<BoxContainer Margin="10 10 10 5" Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
<ScrollContainer HScrollEnabled="False" HorizontalExpand="True" VerticalExpand="True">
<BoxContainer HorizontalExpand="True" VerticalExpand="True">
<RichTextLabel Name="ExtractionResearchLabel" VerticalAlignment="Top" HorizontalAlignment="Left"/>
</BoxContainer>
</BoxContainer>
<BoxContainer VerticalExpand="False" Orientation="Vertical" MaxSize="64 64">
<SpriteView
Name="ArtifactDisplay"
OverrideDirection="South"
VerticalExpand="False"
SetSize="64 64"
MaxSize="64 64"
Scale="2 2">
</SpriteView>
</BoxContainer>
<BoxContainer VerticalExpand="True"></BoxContainer>
</ScrollContainer>
<Control MinHeight="5"/>
<RichTextLabel Name="ExtractionSumLabel" VerticalAlignment="Bottom" HorizontalAlignment="Left"/>
</BoxContainer>
</PanelContainer>
</BoxContainer>
<BoxContainer Name="NodeViewContainer" Orientation="Vertical" VerticalExpand="True">
<ScrollContainer HScrollEnabled="False" HorizontalExpand="True" VerticalExpand="True">
<BoxContainer Orientation="Vertical" HorizontalExpand="False" VerticalExpand="True">
<Label Name="NoneSelectedLabel" Text="{Loc 'analysis-console-no-node'}" HorizontalAlignment="Center" VerticalAlignment="Center" VerticalExpand="True" Visible="False"/>
<BoxContainer Name="InfoContainer" Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
<BoxContainer HorizontalExpand="True">
<RichTextLabel Name="IDLabel" HorizontalExpand="True" Text="{Loc 'analysis-console-info-id'}"/>
<RichTextLabel Name="IDValueLabel" HorizontalAlignment="Right"/>
</BoxContainer>
<BoxContainer HorizontalExpand="True">
<RichTextLabel Name="ClassLabel" HorizontalExpand="True" Text="{Loc 'analysis-console-info-class'}"/>
<RichTextLabel Name="ClassValueLabel" HorizontalAlignment="Right"/>
</BoxContainer>
<BoxContainer HorizontalExpand="True">
<RichTextLabel Name="LockedLabel" HorizontalExpand="True" Text="{Loc 'analysis-console-info-locked'}"/>
<RichTextLabel Name="LockedValueLabel" HorizontalAlignment="Right"/>
</BoxContainer>
<BoxContainer HorizontalExpand="True">
<RichTextLabel Name="DurabilityLabel" HorizontalExpand="True" Text="{Loc 'analysis-console-info-durability'}"/>
<RichTextLabel Name="DurabilityValueLabel" HorizontalAlignment="Right"/>
</BoxContainer>
<Control MinHeight="20"/>
<RichTextLabel Name="EffectLabel" Text="{Loc 'analysis-console-info-effect'}"/>
<RichTextLabel Name="EffectValueLabel" HorizontalExpand="True"/>
<RichTextLabel Name="TriggerLabel" Text="{Loc 'analysis-console-info-trigger'}"/>
<RichTextLabel Name="TriggerValueLabel" HorizontalExpand="True"/>
</BoxContainer>
</BoxContainer>
</ScrollContainer>
<Control MinHeight="5"/>
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<Button Name="ServerButton" Text="{Loc 'analysis-console-server-list-button'}" StyleClasses="OpenRight" HorizontalExpand="True" MinHeight="35"/>
<Button Name="ExtractButton" Text="{Loc 'analysis-console-extract-button'}" StyleClasses="OpenLeft" HorizontalExpand="True" MinHeight="35"/>
</BoxContainer>
</BoxContainer>
</BoxContainer>
<customControls:VSeparator StyleClasses="LowDivider" />
<BoxContainer HorizontalExpand="True" VerticalExpand="True">
<PanelContainer Margin="10 10 10 10" HorizontalExpand="True" RectClipContent="True">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#000000FF" />
</PanelContainer.PanelOverride>
<BoxContainer Margin="10 10 10 10" Orientation="Horizontal" HorizontalExpand="True" VerticalExpand="True">
<ui:XenoArtifactGraphControl Name="GraphControl" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Label Name="NoArtiLabel"
Text="{Loc 'analysis-console-info-no-artifact'}"
HorizontalExpand="True"
VerticalExpand="True"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</ui:XenoArtifactGraphControl>
</BoxContainer>
</PanelContainer>
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>

View File

@@ -1,11 +1,18 @@
using Content.Client.Stylesheets;
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.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 +21,213 @@ namespace Content.Client.Xenoarchaeology.Ui;
[GenerateTypedNameReferences]
public sealed partial class AnalysisConsoleMenu : FancyWindow
{
private static readonly TimeSpan ExtractInfoDisplayForDuration = TimeSpan.FromSeconds(3);
[Dependency] private readonly IEntityManager _ent = default!;
[Dependency] private readonly IResourceCache _resCache = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private readonly ArtifactAnalyzerSystem _artifactAnalyzer;
private readonly XenoArtifactSystem _xenoArtifact;
private readonly AudioSystem _audio;
private readonly MetaDataSystem _meta = default!;
private Entity<AnalysisConsoleComponent> _owner;
private Entity<XenoArtifactNodeComponent>? _currentNode;
private TimeSpan? _hideExtractInfoIn;
private int _extractionSum;
public event Action? OnServerSelectionButtonPressed;
public event Action? OnScanButtonPressed;
public event Action? OnPrintButtonPressed;
public event Action? OnExtractButtonPressed;
public event Action? OnUpBiasButtonPressed;
public event Action? OnDownBiasButtonPressed;
// For rendering the progress bar, updated from BUI state
private TimeSpan? _startTime;
private TimeSpan? _totalTime;
private TimeSpan? _accumulatedRunTime;
private bool _paused;
public AnalysisConsoleMenu()
{
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<XenoArtifactSystem>();
_artifactAnalyzer = _ent.System<ArtifactAnalyzerSystem>();
_audio = _ent.System<AudioSystem>();
_meta = _ent.System<MetaDataSystem>();
var buttonGroup = new ButtonGroup(false);
UpBiasButton.Group = buttonGroup;
DownBiasButton.Group = buttonGroup;
if (BackPanel.PanelOverride is StyleBoxTexture tex)
tex.Texture = _resCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png");
GraphControl.OnNodeSelected += node =>
{
_currentNode = node;
SetSelectedNode(node);
};
ServerButton.OnPressed += _ =>
{
OnServerSelectionButtonPressed?.Invoke();
};
ExtractButton.OnPressed += StartExtract;
}
/// <summary>
/// Set entity that corresponds analysis console, for which window is opened.
/// Closes window if <see cref="AnalysisConsoleComponent"/> is not present on entity.
/// </summary>
public void SetOwner(EntityUid owner)
{
if (!_ent.TryGetComponent<AnalysisConsoleComponent>(owner, out var comp))
{
Close();
return;
}
_owner = (owner, comp);
Update(_owner);
}
private void StartExtract(BaseButton.ButtonEventArgs obj)
{
if (!_artifactAnalyzer.TryGetArtifactFromConsole(_owner, out var artifact))
return;
ExtractContainer.Visible = true;
NodeViewContainer.Visible = false;
_extractionSum = 0;
var extractionMessage = new FormattedMessage();
var nodes = _xenoArtifact.GetAllNodes(artifact.Value);
var count = 0;
foreach (var node in nodes)
{
var pointValue = _xenoArtifact.GetResearchValue(node);
if (pointValue <= 0)
continue;
count++;
var nodeId = _xenoArtifact.GetNodeId(node);
var text = Loc.GetString("analysis-console-extract-value", ("id", nodeId), ("value", pointValue));
extractionMessage.AddMarkupOrThrow(text);
extractionMessage.PushNewline();
}
if (count == 0)
extractionMessage.AddMarkupOrThrow(Loc.GetString("analysis-console-extract-none"));
_hideExtractInfoIn = _timing.CurTime + ExtractInfoDisplayForDuration;
ExtractionResearchLabel.SetMessage(extractionMessage);
ExtractionSumLabel.SetMarkup(Loc.GetString("analysis-console-extract-sum", ("value", _extractionSum)));
_audio.PlayGlobal(_owner.Comp.ScanFinishedSound, _owner, AudioParams.Default.WithVolume(1f));
OnExtractButtonPressed?.Invoke();
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (_startTime is not { } start || _totalTime is not { } total || _accumulatedRunTime is not { } accumulated)
if (_hideExtractInfoIn == null || _timing.CurTime + _meta.GetPauseTime(_owner) < _hideExtractInfoIn)
return;
var remaining = total - accumulated;
if (!_paused)
{
// If the analyzer is running, its remaining time is further discounted by the time it's been running for.
remaining += start - _timing.CurTime;
}
var secsText = Math.Max((int) remaining.TotalSeconds, 0);
ProgressLabel.Text = Loc.GetString("analysis-console-progress-text",
("seconds", secsText));
// 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);
ExtractContainer.Visible = false;
NodeViewContainer.Visible = true;
_hideExtractInfoIn = null;
}
public void SetButtonsDisabled(AnalysisConsoleUpdateState state)
public void Update(Entity<AnalysisConsoleComponent> ent)
{
ScanButton.Disabled = !state.CanScan;
PrintButton.Disabled = !state.CanPrint;
if (state.IsTraversalDown)
DownBiasButton.Pressed = true;
_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
UpBiasButton.Pressed = true;
NoArtiLabel.Visible = false;
ExtractButton.Disabled = false;
if (!state.ServerConnected)
if (_currentNode == null
|| arti == null
|| !_xenoArtifact.TryGetIndex((arti.Value, arti.Value), _currentNode.Value, out _))
{
ExtractButton.Disabled = true;
ExtractButton.ToolTip = Loc.GetString("analysis-console-no-server-connected");
SetSelectedNode(null);
}
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 (ExtractButton.Disabled)
public void SetSelectedNode(Entity<XenoArtifactNodeComponent>? node)
{
ExtractButton.RemoveStyleClass("ButtonColorGreen");
}
else
{
ExtractButton.AddStyleClass("ButtonColorGreen");
ExtractButton.ToolTip = null;
}
}
private void UpdateArtifactIcon(EntityUid? uid)
{
if (uid == null)
{
ArtifactDisplay.Visible = false;
InfoContainer.Visible = node != null;
if (!_artifactAnalyzer.TryGetArtifactFromConsole(_owner, out var artifact))
return;
}
ArtifactDisplay.Visible = true;
ArtifactDisplay.SetEntity(uid);
}
NoneSelectedLabel.Visible = node == null;
public void UpdateInformationDisplay(AnalysisConsoleUpdateState state)
{
var message = new FormattedMessage();
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
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
{
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"));
>= 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<MetaDataComponent>(node.Value)?.EntityDescription ?? string.Empty)));
var predecessorNodes = _xenoArtifact.GetPredecessorNodes(artifact.Value.Owner, node.Value);
if (!hasInfo)
{
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!));
Information.SetMessage(message);
}
public void UpdateProgressBar(AnalysisConsoleUpdateState state)
foreach (var predecessor in predecessorNodes)
{
ProgressBar.Visible = state.Scanning;
ProgressLabel.Visible = state.Scanning;
triggerStr.AppendLine();
triggerStr.Append("- ");
triggerStr.Append(Loc.GetString(predecessor.Comp.TriggerTip!));
}
TriggerValueLabel.SetMarkup(Loc.GetString("analysis-console-info-triggered-value", ("triggers", triggerStr.ToString())));
}
_startTime = state.StartTime;
_totalTime = state.TotalTime;
_accumulatedRunTime = state.AccumulatedRunTime;
_paused = state.Paused;
ClassValueLabel.SetMarkup(Loc.GetString("analysis-console-info-class-value",
("class", Loc.GetString($"artifact-node-class-{Math.Min(6, predecessorNodes.Count + 1)}"))));
}
}

View File

@@ -0,0 +1,42 @@
using Content.Shared.Xenoarchaeology.Equipment.Components;
using Robust.Client.UserInterface;
namespace Content.Client.Xenoarchaeology.Ui;
/// <summary>
/// BUI for hand-held xeno artifact scanner, server-provided UI updates.
/// </summary>
public sealed class NodeScannerBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
[ViewVariables]
private NodeScannerDisplay? _scannerDisplay;
/// <inheritdoc />
protected override void Open()
{
base.Open();
_scannerDisplay = this.CreateWindow<NodeScannerDisplay>();
_scannerDisplay.SetOwner(Owner);
_scannerDisplay.OnClose += Close;
}
/// <summary>
/// Update UI state based on corresponding component.
/// </summary>
public void Update(Entity<NodeScannerComponent> ent)
{
_scannerDisplay?.Update(ent);
}
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
return;
_scannerDisplay?.Dispose();
}
}

View File

@@ -0,0 +1,20 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'node-scan-display-title'}"
MinSize="305 180"
SetSize="305 180"
Resizable="False"
>
<BoxContainer Orientation="Vertical" >
<controls:StripeBack>
<Label Name="NodeScannerState" HorizontalAlignment="Center" StyleClasses="LabelSubText" Margin="4 0 0 4" />
</controls:StripeBack>
<BoxContainer Orientation="Horizontal">
<Label Name="NoActiveNodeDataLabel" Text="{Loc 'node-scan-no-data'}" Margin="45 25 0 0" MinHeight="47" />
<GridContainer Name="ActiveNodesList" Columns="4" Rows="2" Visible="True" MinHeight="47" />
</BoxContainer>
<controls:StripeBack>
<Label Name="ArtifactStateLabel" HorizontalAlignment="Center" StyleClasses="LabelSubText" Margin="4 0 0 4" />
</controls:StripeBack>
</BoxContainer>
</controls:FancyWindow>

View File

@@ -0,0 +1,86 @@
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()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
}
/// <summary>
/// Sets entity that represents hand-held xeno artifact node scanner for which window is opened.
/// Closes window if <see cref="NodeScannerComponent"/> is not present on entity.
/// </summary>
public void SetOwner(EntityUid scannerEntityUid)
{
if (!_ent.TryGetComponent<NodeScannerComponent>(scannerEntityUid, out var scannerComponent))
{
Close();
return;
}
Update((scannerEntityUid, scannerComponent));
}
/// <summary>
/// Updates labels with scanned artifact data and list of triggered nodes from component.
/// </summary>
public void Update(Entity<NodeScannerComponent> 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<NodeScannerComponent> 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")
};
}
}

View File

@@ -0,0 +1,6 @@
<controls:XenoArtifactGraphControl
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.Xenoarchaeology.Ui"
HorizontalExpand="True"
VerticalExpand="True"
MouseFilter="Stop"/>

View File

@@ -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<XenoArtifactComponent>? _artifact;
private Entity<XenoArtifactNodeComponent>? _hoveredNode;
private readonly Font _font;
public event Action<Entity<XenoArtifactNodeComponent>>? 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<XenoArtifactSystem>();
var fontResource = IoCManager.Resolve<IResourceCache>()
.GetResource<FontResource>("/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<XenoArtifactComponent>? 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();
}
/// <summary>
/// Renders artifact node graph control, consisting of nodes and edges connecting them.
/// </summary>
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 fromNode = 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 toNode = GetNodePos(successorNode, ySpacing, segments, ref bottomLeft) + new Vector2(0, NodeRadius);
handle.DrawLine(fromNode, toNode, color);
}
}
bottomLeft.X += GetBiggestWidth(segment) + segmentSpacing;
}
}
private Vector2 GetNodePos(Entity<XenoArtifactNodeComponent> node, float ySpacing, List<List<Entity<XenoArtifactNodeComponent>>> 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<Entity<XenoArtifactNodeComponent>> nodes)
{
var num = _artifactSystem.GetDepthOrderedNodes(nodes)
.Max(p => p.Value.Count);
return (NodeDiameter * num) + MinXSpacing * (num - 1);
}
}

View File

@@ -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 @@ public sealed class RandomArtifactSpriteSystem : VisualizerSystem<RandomArtifact
if (!AppearanceSystem.TryGetData<int>(uid, SharedArtifactsVisuals.SpriteIndex, out var spriteIndex, args.Component))
return;
if (!AppearanceSystem.TryGetData<bool>(uid, SharedArtifactsVisuals.IsUnlocking, out var isUnlocking, args.Component))
isUnlocking = false;
if (!AppearanceSystem.TryGetData<bool>(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 @@ public sealed class RandomArtifactSpriteSystem : VisualizerSystem<RandomArtifact
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
}

View File

@@ -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
";
/// <summary>
/// Checks that adding nodes and edges properly adds them into the adjacency matrix
/// </summary>
[Test]
public async Task XenoArtifactAddNodeTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entManager = server.ResolveDependency<IEntityManager>();
var artifactSystem = entManager.System<SharedXenoArtifactSystem>();
await server.WaitPost(() =>
{
var artifactUid = entManager.Spawn("TestArtifact");
var artifactEnt = (artifactUid, comp: entManager.GetComponent<XenoArtifactComponent>(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();
}
/// <summary>
/// Checks to make sure that removing nodes properly cleans up all connections.
/// </summary>
[Test]
public async Task XenoArtifactRemoveNodeTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entManager = server.ResolveDependency<IEntityManager>();
var artifactSystem = entManager.System<SharedXenoArtifactSystem>();
await server.WaitPost(() =>
{
var artifactUid = entManager.Spawn("TestArtifact");
var artifactEnt = (artifactUid, comp: entManager.GetComponent<XenoArtifactComponent>(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();
}
/// <summary>
/// Sets up series of linked nodes and ensures that resizing the adjacency matrix doesn't disturb the connections
/// </summary>
[Test]
public async Task XenoArtifactResizeTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entManager = server.ResolveDependency<IEntityManager>();
var artifactSystem = entManager.System<SharedXenoArtifactSystem>();
await server.WaitPost(() =>
{
var artifactUid = entManager.Spawn("TestArtifact");
var artifactEnt = (artifactUid, comp: entManager.GetComponent<XenoArtifactComponent>(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();
}
/// <summary>
/// Checks if removing a node and adding a new node into its place in the adjacency matrix doesn't accidentally retain extra data.
/// </summary>
[Test]
public async Task XenoArtifactReplaceTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entManager = server.ResolveDependency<IEntityManager>();
var artifactSystem = entManager.System<SharedXenoArtifactSystem>();
await server.WaitPost(() =>
{
var artifactUid = entManager.Spawn("TestArtifact");
var artifactEnt = (artifactUid, comp: entManager.GetComponent<XenoArtifactComponent>(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();
}
/// <summary>
/// Checks if the active nodes are properly detected.
/// </summary>
[Test]
public async Task XenoArtifactBuildActiveNodesTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entManager = server.ResolveDependency<IEntityManager>();
var artifactSystem = entManager.System<SharedXenoArtifactSystem>();
await server.WaitPost(() =>
{
var artifactUid = entManager.Spawn("TestArtifact");
Entity<XenoArtifactComponent> artifactEnt = (artifactUid, entManager.GetComponent<XenoArtifactComponent>(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<IEntityManager>();
var artifactSystem = entManager.System<SharedXenoArtifactSystem>();
await server.WaitPost(() =>
{
var artifact1Uid = entManager.Spawn("TestGenArtifactFlat");
Entity<XenoArtifactComponent> artifact1Ent = (artifact1Uid, entManager.GetComponent<XenoArtifactComponent>(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<XenoArtifactComponent> artifact2Ent = (artifact2Uid, entManager.GetComponent<XenoArtifactComponent>(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<XenoArtifactComponent> artifact3Ent = (artifact3Uid, entManager.GetComponent<XenoArtifactComponent>(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();
}
}

View File

@@ -10,8 +10,6 @@ using Content.Server.Mind;
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 @@ namespace Content.Server.Administration.Systems
[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!;
@@ -454,29 +451,6 @@ namespace Content.Server.Administration.Systems
args.Verbs.Add(verb);
}
// XenoArcheology
if (_adminManager.IsAdmin(player) && TryComp<ArtifactComponent>(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<ArtifactTimerTriggerComponent>(args.Target),
Disabled = EntityManager.HasComponent<ArtifactTimerTriggerComponent>(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 &&

View File

@@ -44,6 +44,22 @@ public sealed class EmpSystem : SharedEmpSystem
Spawn(EmpPulseEffectPrototype, coordinates);
}
/// <summary>
/// Triggers an EMP pulse at the given location, by first raising an <see cref="EmpAttemptEvent"/>, then a raising <see cref="EmpPulseEvent"/> on all entities in range.
/// </summary>
/// <param name="coordinates">The location to trigger the EMP pulse at.</param>
/// <param name="range">The range of the EMP pulse.</param>
/// <param name="energyConsumption">The amount of energy consumed by the EMP pulse.</param>
/// <param name="duration">The duration of the EMP effects.</param>
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);
}
/// <summary>
/// Attempts to apply the effects of an EMP pulse onto an entity by first raising an <see cref="EmpAttemptEvent"/>, followed by raising a <see cref="EmpPulseEvent"/> on it.
/// </summary>

View File

@@ -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<ArtifactSystem>();
artifact.TryActivateArtifact(args.TargetEntity, logMissing: false);
}
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) =>
Loc.GetString("reagent-effect-guidebook-activate-artifact", ("chance", Probability));
}

View File

@@ -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<ActivatableUIComponent>(Owner)?.CurrentSingleUser
?? _entMan.GetComponentOrNull<ActorComponent>(Owner)?.PlayerSession.AttachedEntity;
}
[RegisterComponent]
public sealed partial class ActiveInstrumentComponent : Component
{
}

View File

@@ -8,6 +8,7 @@ using Content.Shared.PAI;
using Content.Shared.Popups;
using Robust.Shared.Random;
using System.Text;
using Content.Shared.Instruments;
using Robust.Shared.Player;
namespace Content.Server.PAI;

View File

@@ -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 sealed partial class RadiationSystem : EntitySystem
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);
}

View File

@@ -8,6 +8,7 @@ using Content.Shared.Station.Components;
using JetBrains.Annotations;
using Robust.Shared.Random;
using System.Linq;
using Content.Shared.Chemistry.Reaction;
namespace Content.Server.StationEvents.Events;
@@ -47,7 +48,7 @@ public sealed class VentClogRule : StationEventSystem<VentClogRuleComponent>
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);

View File

@@ -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<RandomArtifactSpriteComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<RandomArtifactSpriteComponent, ArtifactActivatedEvent>(OnActivated);
SubscribeLocalEvent<RandomArtifactSpriteComponent, ArtifactUnlockingStartedEvent>(UnlockingStageStarted);
SubscribeLocalEvent<RandomArtifactSpriteComponent, ArtifactUnlockingFinishedEvent>(UnlockingStageFinished);
SubscribeLocalEvent<RandomArtifactSpriteComponent, XenoArtifactActivatedEvent>(ArtifactActivated);
}
public override void Update(float frameTime)
@@ -47,9 +50,19 @@ public sealed class RandomArtifactSpriteSystem : EntitySystem
_item.SetHeldPrefix(uid, "ano" + randomSprite.ToString("D2")); //set item artifact inhands
}
private void OnActivated(EntityUid uid, RandomArtifactSpriteComponent component, ArtifactActivatedEvent args)
private void UnlockingStageStarted(Entity<RandomArtifactSpriteComponent> ent, ref ArtifactUnlockingStartedEvent args)
{
_appearance.SetData(uid, SharedArtifactsVisuals.IsActivated, true);
component.ActivationStart = _time.CurTime;
_appearance.SetData(ent, SharedArtifactsVisuals.IsUnlocking, true);
}
private void UnlockingStageFinished(Entity<RandomArtifactSpriteComponent> ent, ref ArtifactUnlockingFinishedEvent args)
{
_appearance.SetData(ent, SharedArtifactsVisuals.IsUnlocking, false);
}
private void ArtifactActivated(Entity<RandomArtifactSpriteComponent> ent, ref XenoArtifactActivatedEvent args)
{
_appearance.SetData(ent, SharedArtifactsVisuals.IsActivated, true);
ent.Comp.ActivationStart = _time.CurTime;
}
}

View File

@@ -0,0 +1,14 @@
namespace Content.Server.Xenoarchaeology.Artifact.XAE.Components;
/// <summary>
/// This is used for recharging all nearby batteries when activated.
/// </summary>
[RegisterComponent, Access(typeof(XAEChargeBatterySystem))]
public sealed partial class XAEChargeBatteryComponent : Component
{
/// <summary>
/// The radius of entities that will be affected.
/// </summary>
[DataField("radius")]
public float Radius = 15f;
}

View File

@@ -0,0 +1,16 @@
using Content.Shared.Atmos;
namespace Content.Server.Xenoarchaeology.Artifact.XAE.Components;
/// <summary>
/// XenoArtifact effect that creates gas in atmosphere.
/// </summary>
[RegisterComponent, Access(typeof(XAECreateGasSystem))]
public sealed partial class XAECreateGasComponent : Component
{
/// <summary>
/// The gases and how many moles will be created of each.
/// </summary>
[DataField]
public Dictionary<Gas, float> Gases = new();
}

View File

@@ -0,0 +1,46 @@
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Destructible.Thresholds;
using Robust.Shared.Prototypes;
namespace Content.Server.Xenoarchaeology.Artifact.XAE.Components;
/// <summary>
/// This is used for an artifact that creates a puddle of
/// random chemicals upon being triggered.
/// </summary>
[RegisterComponent, Access(typeof(XAECreatePuddleSystem))]
public sealed partial class XAECreatePuddleComponent : Component
{
/// <summary>
/// The solution where all the chemicals are stored.
/// </summary>
[DataField(required: true), ViewVariables(VVAccess.ReadWrite)]
public Solution ChemicalSolution = default!;
/// <summary>
/// The different chemicals that can be spawned by this effect.
/// </summary>
[DataField]
public List<ProtoId<ReagentPrototype>> PossibleChemicals = new();
/// <summary>
/// The number of chemicals in the puddle.
/// </summary>
[DataField]
public MinMax ChemAmount = new MinMax(1, 3);
/// <summary>
/// List of reagents selected for this node. Selected ones are chosen on first activation
/// and are picked from <see cref="PossibleChemicals"/> and is calculated separately for each node.
/// </summary>
[DataField]
public List<ProtoId<ReagentPrototype>>? SelectedChemicals;
/// <summary>
/// Marker, if entity where this component is placed should have description replaced with selected chemicals
/// on component init.
/// </summary>
[DataField]
public bool ReplaceDescription;
}

View File

@@ -0,0 +1,26 @@
namespace Content.Server.Xenoarchaeology.Artifact.XAE.Components;
/// <summary>
/// Effect of EMP on activation.
/// </summary>
[RegisterComponent, Access(typeof(XAEEmpInAreaSystem))]
public sealed partial class XAEEmpInAreaComponent : Component
{
/// <summary>
/// Range of EMP effect.
/// </summary>
[DataField]
public float Range = 4f;
/// <summary>
/// Energy to be consumed from energy containers.
/// </summary>
[DataField]
public float EnergyConsumption = 1000000;
/// <summary>
/// Duration (in seconds) for which devices going to be disabled.
/// </summary>
[DataField]
public float DisableDuration = 60f;
}

View File

@@ -0,0 +1,56 @@
using Content.Shared.Chemistry.Reagent;
using Robust.Shared.Prototypes;
namespace Content.Server.Xenoarchaeology.Artifact.XAE.Components;
/// <summary>
/// Generates foam from the artifact when activated.
/// </summary>
[RegisterComponent, Access(typeof(XAEFoamSystem))]
public sealed partial class XAEFoamComponent : Component
{
/// <summary>
/// The list of reagents that will randomly be picked from
/// to choose the foam reagent.
/// </summary>
[DataField(required: true)]
public List<ProtoId<ReagentPrototype>> Reagents = new();
/// <summary>
/// The foam reagent.
/// </summary>
[DataField]
public string? SelectedReagent;
/// <summary>
/// How long does the foam last?
/// </summary>
[DataField]
public float Duration = 10f;
/// <summary>
/// How much reagent is in the foam?
/// </summary>
[DataField]
public float ReagentAmount = 100f;
/// <summary>
/// Minimum radius of foam spawned.
/// </summary>
[DataField]
public int MinFoamAmount = 15;
/// <summary>
/// Maximum radius of foam spawned.
/// </summary>
[DataField]
public int MaxFoamAmount = 20;
/// <summary>
/// Marker, if entity where this component is placed should have description replaced with selected chemicals
/// on component init.
/// </summary>
[DataField]
public bool ReplaceDescription;
}

View File

@@ -0,0 +1,22 @@
using Content.Shared.Destructible.Thresholds;
namespace Content.Server.Xenoarchaeology.Artifact.XAE.Components;
/// <summary>
/// Artifact that ignites surrounding entities when triggered.
/// </summary>
[RegisterComponent, Access(typeof(XAEIgniteSystem))]
public sealed partial class XAEIgniteComponent : Component
{
/// <summary>
/// Range, inside which all entities going be set on fire.
/// </summary>
[DataField]
public float Range = 2f;
/// <summary>
/// Amount of fire stacks to apply
/// </summary>
[DataField]
public MinMax FireStack = new(2, 5);
}

View File

@@ -1,20 +1,20 @@
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
namespace Content.Server.Xenoarchaeology.Artifact.XAE.Components;
/// <summary>
/// Flickers all the lights within a certain radius.
/// </summary>
[RegisterComponent]
public sealed partial class LightFlickerArtifactComponent : Component
[RegisterComponent, Access(typeof(XAELightFlickerSystem))]
public sealed partial class XAELightFlickerComponent : Component
{
/// <summary>
/// Lights within this radius will be flickered on activation
/// Lights within this radius will be flickered on activation.
/// </summary>
[DataField("radius")]
[DataField]
public float Radius = 4;
/// <summary>
/// The chance that the light will flicker
/// The chance that the light will flicker.
/// </summary>
[DataField("flickerChance")]
[DataField]
public float FlickerChance = 0.75f;
}

View File

@@ -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;
/// <summary>
/// Artifact polymorphs surrounding entities when triggered.
/// Artifact polymorphs entities when triggered.
/// </summary>
[RegisterComponent]
[Access(typeof(PolyOthersArtifactSystem))]
public sealed partial class PolyOthersArtifactComponent : Component
[RegisterComponent, Access(typeof(XAEPolymorphSystem))]
public sealed partial class XAEPolymorphComponent : Component
{
/// <summary>
/// The polymorph effect to trigger.
@@ -19,7 +17,7 @@ public sealed partial class PolyOthersArtifactComponent : Component
public ProtoId<PolymorphPrototype> PolymorphPrototypeName = "ArtifactMonkey";
/// <summary>
/// range of the effect.
/// Range of the effect.
/// </summary>
[DataField]
public float Range = 2f;

View File

@@ -1,11 +1,11 @@
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
namespace Content.Server.Xenoarchaeology.Artifact.XAE.Components;
/// <summary>
/// Harmless artifact that broadcast "thoughts" to players nearby.
/// Thoughts are shown as popups and unique for each player.
/// </summary>
[RegisterComponent]
public sealed partial class TelepathicArtifactComponent : Component
[RegisterComponent, Access(typeof(XAETelepathicSystem))]
public sealed partial class XAETelepathicComponent : Component
{
/// <summary>
/// Loc string ids of telepathic messages.

View File

@@ -1,12 +1,12 @@
using Content.Shared.Atmos;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
namespace Content.Server.Xenoarchaeology.Artifact.XAE.Components;
/// <summary>
/// Change atmospherics temperature until it reach target.
/// </summary>
[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;

View File

@@ -1,11 +1,11 @@
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
namespace Content.Server.Xenoarchaeology.Artifact.XAE.Components;
/// <summary>
/// Throws all nearby entities backwards.
/// Also pries nearby tiles.
/// </summary>
[RegisterComponent]
public sealed partial class ThrowArtifactComponent : Component
[RegisterComponent, Access(typeof(XAEThrowThingsAroundSystem))]
public sealed partial class XAEThrowThingsAroundComponent : Component
{
/// <summary>
/// How close do you have to be to get yeeted?

View File

@@ -0,0 +1,9 @@
using Content.Shared.Explosion.Components.OnTrigger;
namespace Content.Server.Xenoarchaeology.Artifact.XAE.Components;
/// <summary>
/// Activates 'trigger' for <see cref="ExplodeOnTriggerComponent"/>.
/// </summary>
[RegisterComponent, Access(typeof(XAETriggerExplosivesSystem))]
public sealed partial class XAETriggerExplosivesComponent : Component;

View File

@@ -0,0 +1,31 @@
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;
/// <summary>
/// System for xeno artifact activation effect that is fully charging batteries in certain range.
/// </summary>
public sealed class XAEChargeBatterySystem : BaseXAESystem<XAEChargeBatteryComponent>
{
[Dependency] private readonly BatterySystem _battery = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
/// <summary> Pre-allocated and re-used collection.</summary>
private readonly HashSet<Entity<BatteryComponent>> _batteryEntities = new();
/// <inheritdoc />
protected override void OnActivated(Entity<XAEChargeBatteryComponent> ent, ref XenoArtifactNodeActivatedEvent args)
{
var chargeBatteryComponent = ent.Comp;
_batteryEntities.Clear();
_lookup.GetEntitiesInRange(args.Coordinates, chargeBatteryComponent.Radius, _batteryEntities);
foreach (var battery in _batteryEntities)
{
_battery.SetCharge(battery, battery.Comp.MaxCharge, battery);
}
}
}

View File

@@ -0,0 +1,52 @@
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.Collections;
using Robust.Shared.Map.Components;
namespace Content.Server.Xenoarchaeology.Artifact.XAE;
/// <summary>
/// System for xeno artifact effect that creates certain atmospheric gas on artifact tile / adjacent tiles.
/// </summary>
public sealed class XAECreateGasSystem : BaseXAESystem<XAECreateGasComponent>
{
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly MapSystem _map = default!;
protected override void OnActivated(Entity<XAECreateGasComponent> ent, ref XenoArtifactNodeActivatedEvent args)
{
var grid = _transform.GetGrid(args.Coordinates);
var map = _transform.GetMap(args.Coordinates);
if (map == null || !TryComp<MapGridComponent>(grid, out var gridComp))
return;
var tile = _map.LocalToTile(grid.Value, gridComp, args.Coordinates);
var mixtures = new ValueList<GasMixture>();
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);
}
}
}
}

View File

@@ -0,0 +1,77 @@
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;
/// <summary>
/// System for xeno artifact effect that creates puddle of chemical reagents under artifact.
/// </summary>
public sealed class XAECreatePuddleSystem: BaseXAESystem<XAECreatePuddleComponent>
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly PuddleSystem _puddle = default!;
[Dependency] private readonly MetaDataSystem _metaData= default!;
[Dependency] private readonly IPrototypeManager _prototypeManager= default!;
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<XAECreatePuddleComponent, MapInitEvent>(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<ProtoId<ReagentPrototype>>();
var chemAmount = component.ChemAmount.Next(_random);
for (var i = 0; i < chemAmount; i++)
{
var chemProto = _random.Pick(component.PossibleChemicals);
chemicalList.Add(chemProto);
}
component.SelectedChemicals = chemicalList;
}
if (component.ReplaceDescription)
{
var reagentNames = new HashSet<string>();
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);
}
}
/// <inheritdoc />
protected override void OnActivated(Entity<XAECreatePuddleComponent> ent, ref XenoArtifactNodeActivatedEvent args)
{
var component = ent.Comp;
if (component.SelectedChemicals == null)
return;
var amountPerChem = component.ChemicalSolution.MaxVolume / component.SelectedChemicals.Count;
foreach (var reagent in component.SelectedChemicals)
{
component.ChemicalSolution.AddReagent(reagent, amountPerChem);
}
_puddle.TrySpillAt(ent, component.ChemicalSolution, out _);
}
}

View File

@@ -0,0 +1,20 @@
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;
/// <summary>
/// System for xeno artifact effect that creates EMP on use.
/// </summary>
public sealed class XAEEmpInAreaSystem : BaseXAESystem<XAEEmpInAreaComponent>
{
[Dependency] private readonly EmpSystem _emp = default!;
/// <inheritdoc />
protected override void OnActivated(Entity<XAEEmpInAreaComponent> ent, ref XenoArtifactNodeActivatedEvent args)
{
_emp.EmpPulse(args.Coordinates, ent.Comp.Range, ent.Comp.EnergyConsumption, ent.Comp.DisableDuration);
}
}

View File

@@ -0,0 +1,63 @@
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;
/// <summary>
/// System for xeno artifact effect that starts Foam chemical reaction with random-ish reagents inside.
/// </summary>
public sealed class XAEFoamSystem : BaseXAESystem<XAEFoamComponent>
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SmokeSystem _smoke = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager= default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<XAEFoamComponent, MapInitEvent>(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<ReagentPrototype>(component.SelectedReagent);
var newEntityDescription = Loc.GetString("xenoarch-effect-foam", ("reagent", reagent.LocalizedName));
_metaData.SetEntityDescription(uid, newEntityDescription);
}
}
/// <inheritdoc />
protected override void OnActivated(Entity<XAEFoamComponent> 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);
}
}

View File

@@ -0,0 +1,47 @@
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;
/// <summary>
/// System for xeno artifact activation effect that ignites any flammable entity in range.
/// </summary>
public sealed class XAEIgniteSystem : BaseXAESystem<XAEIgniteComponent>
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly FlammableSystem _flammable = default!;
private EntityQuery<FlammableComponent> _flammables;
/// <summary> Pre-allocated and re-used collection.</summary>
private readonly HashSet<EntityUid> _entities = new();
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
_flammables = GetEntityQuery<FlammableComponent>();
}
/// <inheritdoc />
protected override void OnActivated(Entity<XAEIgniteComponent> ent, ref XenoArtifactNodeActivatedEvent args)
{
var component = ent.Comp;
_entities.Clear();
_lookup.GetEntitiesInRange(ent.Owner, component.Range, _entities);
foreach (var target in _entities)
{
if (!_flammables.TryGetComponent(target, out var fl))
continue;
fl.FireStacks += component.FireStack.Next(_random);
_flammable.Ignite(target, ent.Owner, fl);
}
}
}

View File

@@ -0,0 +1,49 @@
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;
/// <summary>
/// System for xeno artifact activation effect that flickers light on and off.
/// </summary>
public sealed class XAELightFlickerSystem : BaseXAESystem<XAELightFlickerComponent>
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly GhostSystem _ghost = default!;
private EntityQuery<PoweredLightComponent> _lights;
/// <summary> Pre-allocated and re-used collection.</summary>
private readonly HashSet<EntityUid> _entities = new();
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
_lights = GetEntityQuery<PoweredLightComponent>();
}
/// <inheritdoc />
protected override void OnActivated(Entity<XAELightFlickerComponent> ent, ref XenoArtifactNodeActivatedEvent args)
{
_entities.Clear();
_lookup.GetEntitiesInRange(ent.Owner, ent.Comp.Radius, _entities, LookupFlags.StaticSundries);
foreach (var light in _entities)
{
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);
}
}
}

View File

@@ -0,0 +1,39 @@
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;
/// <summary>
/// System for xeno artifact activation effect that is polymorphing all humanoid entities in range.
/// </summary>
public sealed class XAEPolymorphSystem : BaseXAESystem<XAEPolymorphComponent>
{
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly MobStateSystem _mob = default!;
[Dependency] private readonly PolymorphSystem _poly = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
/// <summary> Pre-allocated and re-used collection.</summary>
private readonly HashSet<Entity<HumanoidAppearanceComponent>> _humanoids = new();
/// <inheritdoc />
protected override void OnActivated(Entity<XAEPolymorphComponent> ent, ref XenoArtifactNodeActivatedEvent args)
{
_humanoids.Clear();
_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);
}
}
}

View File

@@ -1,31 +1,34 @@
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
/// <summary>
/// System for xeno artifact activation effect that sends sublime telepathic messages.
/// </summary>
public sealed class XAETelepathicSystem : BaseXAESystem<XAETelepathicComponent>
{
[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<TelepathicArtifactComponent, ArtifactActivatedEvent>(OnActivate);
}
/// <summary> Pre-allocated and re-used collection.</summary>
private readonly HashSet<EntityUid> _entities = new();
private void OnActivate(EntityUid uid, TelepathicArtifactComponent component, ArtifactActivatedEvent args)
/// <inheritdoc />
protected override void OnActivated(Entity<XAETelepathicComponent> ent, ref XenoArtifactNodeActivatedEvent args)
{
var component = ent.Comp;
// try to find victims nearby
var victims = _lookup.GetEntitiesInRange(uid, component.Range);
foreach (var victimUid in victims)
_entities.Clear();
_lookup.GetEntitiesInRange(ent, component.Range, _entities);
foreach (var victimUid in _entities)
{
if (!EntityManager.HasComponent<ActorComponent>(victimUid))
if (!HasComp<ActorComponent>(victimUid))
continue;
// roll if msg should be usual or drastic

View File

@@ -0,0 +1,49 @@
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;
namespace Content.Server.Xenoarchaeology.Artifact.XAE;
/// <summary>
/// System for xeno artifact effect that changes atmospheric temperature on adjacent tiles.
/// </summary>
public sealed class XAETemperatureSystem : BaseXAESystem<XAETemperatureComponent>
{
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly TransformSystem _transformSystem = default!;
/// <inheritdoc />
protected override void OnActivated(Entity<XAETemperatureComponent> ent, ref XenoArtifactNodeActivatedEvent args)
{
var component = ent.Comp;
var transform = Transform(ent);
var center = _atmosphereSystem.GetContainingMixture(ent.Owner, false, true);
if (center == null)
return;
UpdateTileTemperature(component, center);
if (component.AffectAdjacentTiles && transform.GridUid != null)
{
var position = _transformSystem.GetGridOrMapTilePosition(ent, transform);
var enumerator = _atmosphereSystem.GetAdjacentTileMixtures(transform.GridUid.Value, position, excite: true);
while (enumerator.MoveNext(out var mixture))
{
UpdateTileTemperature(component, mixture);
}
}
}
private void UpdateTileTemperature(XAETemperatureComponent component, GasMixture environment)
{
var dif = component.TargetTemperature - environment.Temperature;
var absDif = Math.Abs(dif);
var step = Math.Min(absDif, component.SpawnTemperature);
environment.Temperature += dif > 0 ? step : -step;
}
}

View File

@@ -0,0 +1,72 @@
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;
namespace Content.Server.Xenoarchaeology.Artifact.XAE;
/// <summary>
/// System for xeno artifact activation effect that pries tiles and throws stuff around.
/// </summary>
public sealed class XAEThrowThingsAroundSystem : BaseXAESystem<XAEThrowThingsAroundComponent>
{
[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!;
private EntityQuery<PhysicsComponent> _physQuery;
/// <summary> Pre-allocated and re-used collection.</summary>
private readonly HashSet<EntityUid> _entities = new();
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
_physQuery = GetEntityQuery<PhysicsComponent>();
}
/// <inheritdoc />
protected override void OnActivated(Entity<XAEThrowThingsAroundComponent> ent, ref XenoArtifactNodeActivatedEvent args)
{
var component = ent.Comp;
var xform = Transform(ent);
if (TryComp<MapGridComponent>(xform.GridUid, out var grid))
{
var areaForTilesPry = new Circle(_transform.GetWorldPosition(xform), component.Range);
var tiles = _map.GetTilesIntersecting(xform.GridUid.Value, grid, areaForTilesPry, true);
foreach (var tile in tiles)
{
if (!_random.Prob(component.TilePryChance))
continue;
_tile.PryTile(tile);
}
}
_entities.Clear();
_lookup.GetEntitiesInRange(ent, component.Range, _entities, LookupFlags.Dynamic | LookupFlags.Sundries);
foreach (var entity in _entities)
{
if (_physQuery.TryGetComponent(entity, out var phys)
&& (phys.CollisionMask & (int)CollisionGroup.GhostImpassable) != 0)
continue;
var tempXform = Transform(entity);
var foo = _transform.GetWorldPosition(tempXform) - _transform.GetWorldPosition(xform);
_throwing.TryThrow(entity, foo * 2, component.ThrowStrength, ent, 0);
}
}
}

View File

@@ -0,0 +1,24 @@
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;
/// <summary>
/// System for xeno artifact effect of triggering explosion.
/// </summary>
public sealed class XAETriggerExplosivesSystem : BaseXAESystem<XAETriggerExplosivesComponent>
{
[Dependency] private readonly ExplosionSystem _explosion = default!;
/// <inheritdoc />
protected override void OnActivated(Entity<XAETriggerExplosivesComponent> ent, ref XenoArtifactNodeActivatedEvent args)
{
if(!TryComp<ExplosiveComponent>(ent, out var explosiveComp))
return;
_explosion.TriggerExplosive(ent, explosiveComp);
}
}

View File

@@ -0,0 +1,28 @@
using Content.Shared.Atmos;
namespace Content.Server.Xenoarchaeology.Artifact.XAT.Components;
/// <summary>
/// This is used for an artifact that is activated by having a certain amount of gas around it.
/// </summary>
[RegisterComponent, Access(typeof(XATGasSystem))]
public sealed partial class XATGasComponent : Component
{
/// <summary>
/// The gas that is related to trigger.
/// </summary>
[DataField]
public Gas TargetGas;
/// <summary>
/// The amount of gas needed.
/// </summary>
[DataField]
public float Moles = Atmospherics.MolesCellStandard * 0.1f;
/// <summary>
/// Marker, if mentioned gas should be present in entity tile for trigger to activate, or it should not.
/// </summary>
[DataField]
public bool ShouldBePresent = true;
}

View File

@@ -0,0 +1,21 @@
namespace Content.Server.Xenoarchaeology.Artifact.XAT.Components;
/// <summary>
/// Component for triggering node on getting activated by powerful magnets.
/// </summary>
[RegisterComponent, Access(typeof(XATMagnetSystem))]
public sealed partial class XATMagnetComponent : Component
{
/// <summary>
/// How close to the magnet do you have to be?
/// </summary>
[DataField]
public float MagnetRange = 40f;
/// <summary>
/// How close do active magboots have to be?
/// This is smaller because they are weaker magnets
/// </summary>
[DataField]
public float MagbootsRange = 2f;
}

View File

@@ -0,0 +1,20 @@
namespace Content.Server.Xenoarchaeology.Artifact.XAT.Components;
/// <summary>
/// This is used for an artifact that activates when above or below a certain pressure.
/// </summary>
[RegisterComponent, Access(typeof(XATPressureSystem))]
public sealed partial class XATPressureComponent : Component
{
/// <summary>
/// The lower-end pressure threshold. Is not considered when null.
/// </summary>
[DataField]
public float? MinPressureThreshold;
/// <summary>
/// The higher-end pressure threshold. Is not considered when null.
/// </summary>
[DataField]
public float? MaxPressureThreshold;
}

View File

@@ -0,0 +1,20 @@
namespace Content.Server.Xenoarchaeology.Artifact.XAT.Components;
/// <summary>
/// This is used for an artifact that is activated by having a certain temperature near it.
/// </summary>
[RegisterComponent, Access(typeof(XATTemperatureSystem))]
public sealed partial class XATTemperatureComponent : Component
{
/// <summary>
/// Threshold temperature for trigger activation.
/// </summary>
[DataField]
public float TargetTemperature;
/// <summary>
/// Marker, if temp needs to be above or below the target.
/// </summary>
[DataField]
public bool TriggerOnHigherTemp = true;
}

View File

@@ -0,0 +1,36 @@
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;
/// <summary>
/// System for xeno artifact trigger, which gets activated from some gas being on the same time as artifact with certain concentration.
/// </summary>
public sealed class XATGasSystem : BaseQueryUpdateXATSystem<XATGasComponent>
{
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
protected override void UpdateXAT(Entity<XenoArtifactComponent> artifact, Entity<XATGasComponent, XenoArtifactNodeComponent> node, float frameTime)
{
var xform = Transform(artifact);
if (_atmosphere.GetTileMixture((artifact, xform)) is not { } mixture)
return;
var gasTrigger = node.Comp1;
var moles = mixture.GetMoles(gasTrigger.TargetGas);
if (gasTrigger.ShouldBePresent)
{
if (moles >= gasTrigger.Moles)
Trigger(artifact, node);
}
else
{
if (moles <= gasTrigger.Moles)
Trigger(artifact, node);
}
}
}

View File

@@ -0,0 +1,67 @@
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;
/// <summary>
/// System for checking if magnets-related xeno artifact node should be triggered.
/// Works with magboots and salvage magnet, salvage magnet triggers only upon pulsing on activation.
/// </summary>
public sealed class XATMagnetSystem : BaseQueryUpdateXATSystem<XATMagnetComponent>
{
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
/// <summary> Pre-allocated and re-used collection.</summary>
private HashSet<Entity<MagbootsComponent>> _magbootEntities = new();
/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SalvageMagnetActivatedEvent>(OnMagnetActivated);
}
/// <inheritdoc />
protected override void UpdateXAT(Entity<XenoArtifactComponent> artifact, Entity<XATMagnetComponent, XenoArtifactNodeComponent> node, float frameTime)
{
var coords = Transform(artifact.Owner).Coordinates;
_magbootEntities.Clear();
_lookup.GetEntitiesInRange(coords, node.Comp1.MagbootsRange, _magbootEntities);
foreach (var ent in _magbootEntities)
{
if(!TryComp<ItemToggleComponent>(ent, out var itemToggle) || !itemToggle.Activated)
continue;
Trigger(artifact, node);
break;
}
}
private void OnMagnetActivated(ref SalvageMagnetActivatedEvent args)
{
var magnetCoordinates = Transform(args.Magnet).Coordinates;
var query = EntityQueryEnumerator<XATMagnetComponent, XenoArtifactNodeComponent>();
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));
}
}
}

View File

@@ -0,0 +1,29 @@
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;
/// <summary>
/// System for checking if pressure-related xeno artifact node should be triggered.
/// </summary>
public sealed class XATPressureSystem : BaseQueryUpdateXATSystem<XATPressureComponent>
{
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
/// <inheritdoc />
protected override void UpdateXAT(Entity<XenoArtifactComponent> artifact, Entity<XATPressureComponent, XenoArtifactNodeComponent> 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);
}
}
}

View File

@@ -0,0 +1,37 @@
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;
/// <summary>
/// System for checking if temperature-related xeno artifact node should be triggered.
/// </summary>
public sealed class XATTemperatureSystem : BaseQueryUpdateXATSystem<XATTemperatureComponent>
{
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
/// <inheritdoc />
protected override void UpdateXAT(Entity<XenoArtifactComponent> artifact, Entity<XATTemperatureComponent, XenoArtifactNodeComponent> 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);
}
}
}

View File

@@ -0,0 +1,125 @@
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;
/// <summary>
/// Toolshed commands for manipulating xeno artifact.
/// </summary>
[ToolshedCommand, AdminCommand(AdminFlags.Debug)]
public sealed class XenoArtifactCommand : ToolshedCommand
{
[ValidatePrototypeId<EntityPrototype>]
public const string ArtifactPrototype = "BaseXenoArtifact";
/// <summary> List existing artifacts. </summary>
[CommandImplementation("list")]
public IEnumerable<EntityUid> List()
{
var query = EntityManager.EntityQueryEnumerator<XenoArtifactComponent>();
while (query.MoveNext(out var uid, out _))
{
yield return uid;
}
}
/// <summary>
/// Output matrix of artifact nodes and how they are connected.
/// </summary>
[CommandImplementation("printMatrix")]
public string PrintMatrix([PipedArgument] EntityUid artifactEntitUid)
{
var comp = EntityManager.GetComponent<XenoArtifactComponent>(artifactEntitUid);
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($"---+");
}
}
}
/// <summary> Output total research points artifact contains. </summary>
[CommandImplementation("totalResearch")]
public int TotalResearch([PipedArgument] EntityUid artifactEntityUid)
{
var artiSys = EntityManager.System<XenoArtifactSystem>();
var comp = EntityManager.GetComponent<XenoArtifactComponent>(artifactEntityUid);
var sum = 0;
var nodes = artiSys.GetAllNodes((artifactEntityUid, comp));
foreach (var node in nodes)
{
sum += node.Comp.ResearchValue;
}
return sum;
}
/// <summary>
/// Spawns a bunch of artifacts and gets average total research points they can yield.
/// </summary>
[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;
}
/// <summary> Unlocks all nodes of artifact. </summary>
[CommandImplementation("unlockAllNodes")]
public void UnlockAllNodes([PipedArgument] EntityUid artifactEntityUid)
{
var artiSys = EntityManager.System<XenoArtifactSystem>();
var comp = EntityManager.GetComponent<XenoArtifactComponent>(artifactEntityUid);
var nodes = artiSys.GetAllNodes((artifactEntityUid, comp));
foreach (var node in nodes)
{
artiSys.SetNodeUnlocked((node, node.Comp));
}
}
}

View File

@@ -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<XenoArtifactComponent> 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));
}
/// <summary>
/// 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.
/// </summary>
/// <param name="ent">Artifact for which pool should be created.</param>
/// <param name="size">
/// Max size of pool. Resulting pool is not guaranteed to be exactly as large, but it will 100% won't be bigger.
/// </param>
private List<XenoArchTriggerPrototype> CreateTriggerPool(Entity<XenoArtifactComponent> ent, int size)
{
var triggerPool = new List<XenoArchTriggerPrototype>(size);
var weightsProto = PrototypeManager.Index(ent.Comp.TriggerWeights);
var weightsByTriggersLeft = new Dictionary<string, float>(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<XenoArchTriggerPrototype>(triggerId);
if (_entityWhitelist.IsWhitelistFail(trigger.Whitelist, ent))
continue;
triggerPool.Add(trigger);
}
return triggerPool;
}
/// <summary>
/// Generates segment of artifact - isolated graph, nodes inside which are interconnected.
/// As size of segment is randomized - it is subtracted from node count.
/// </summary>
private void GenerateArtifactSegment(
Entity<XenoArtifactComponent> ent,
List<XenoArchTriggerPrototype> 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);
}
}
}
}
/// <summary>
/// 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).
/// </summary>
private List<Entity<XenoArtifactNodeComponent>> PopulateArtifactSegmentRecursive(
Entity<XenoArtifactComponent> ent,
List<XenoArchTriggerPrototype> 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<Entity<XenoArtifactNodeComponent>>();
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;
}
/// <summary>
/// Rolls segment size, based on amount of nodes left and XenoArtifactComponent settings.
/// </summary>
private int GetArtifactSegmentSize(Entity<XenoArtifactComponent> 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;
}
}

View File

@@ -0,0 +1,35 @@
using Content.Server.Cargo.Systems;
using Content.Shared.Xenoarchaeology.Artifact;
using Content.Shared.Xenoarchaeology.Artifact.Components;
namespace Content.Server.Xenoarchaeology.Artifact;
/// <inheritdoc cref="SharedXenoArtifactSystem"/>
public sealed partial class XenoArtifactSystem : SharedXenoArtifactSystem
{
/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<XenoArtifactComponent, MapInitEvent>(OnArtifactMapInit);
SubscribeLocalEvent<XenoArtifactComponent, PriceCalculationEvent>(OnCalculatePrice);
}
private void OnArtifactMapInit(Entity<XenoArtifactComponent> ent, ref MapInitEvent args)
{
if (ent.Comp.IsGenerationRequired)
GenerateArtifactStructure(ent);
}
private void OnCalculatePrice(Entity<XenoArtifactComponent> ent, ref PriceCalculationEvent args)
{
foreach (var node in GetAllNodes(ent))
{
if (node.Comp.Locked)
continue;
args.Price += node.Comp.ResearchValue * ent.Comp.PriceMultiplier;
}
}
}

View File

@@ -0,0 +1,84 @@
using Content.Server.Administration;
using Content.Shared.Administration;
using Content.Shared.Xenoarchaeology.Artifact.Components;
using Robust.Shared.Console;
namespace Content.Server.Xenoarchaeology.Artifact;
/// <summary> Command for unlocking specific node of xeno artifact. </summary>
[AdminCommand(AdminFlags.Debug)]
public sealed class XenoArtifactUnlockNodeCommand : LocalizedCommands
{
[Dependency] private readonly EntityManager _entities = default!;
/// <inheritdoc />
public override string Command => "unlocknode";
/// <inheritdoc />
public override string Description => Loc.GetString("cmd-unlocknode-desc");
/// <inheritdoc />
public override string Help => Loc.GetString("cmd-unlocknode-help");
/// <inheritdoc />
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<XenoArtifactSystem>()
.SetNodeUnlocked(entityUid.Value);
}
/// <inheritdoc />
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
var query = _entities.EntityQueryEnumerator<XenoArtifactComponent>();
var completionOptions = new List<CompletionOption>();
while (query.MoveNext(out var uid, out _))
{
completionOptions.Add(new CompletionOption(uid.ToString()));
}
return CompletionResult.FromHintOptions(completionOptions, "<artifact uid>");
}
if (args.Length == 2 &&
NetEntity.TryParse(args[0], out var netEnt) &&
_entities.TryGetEntity(netEnt, out var artifactUid) &&
_entities.TryGetComponent<XenoArtifactComponent>(artifactUid, out var comp))
{
var artifactSystem = _entities.System<XenoArtifactSystem>();
var result = new List<CompletionOption>();
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, "<node uid>");
}
return CompletionResult.Empty;
}
}

View File

@@ -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;
/// <inheritdoc />
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!;
/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AnalysisConsoleComponent, AnalysisConsoleExtractButtonPressedMessage>(OnExtractButtonPressed);
}
private void OnExtractButtonPressed(Entity<AnalysisConsoleComponent> 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);
}
}

View File

@@ -1,36 +0,0 @@
using Robust.Shared.Serialization.TypeSerializers.Implementations;
namespace Content.Server.Xenoarchaeology.Equipment.Components;
/// <summary>
/// Activecomp used for tracking artifact analyzers that are currently
/// in the process of scanning an artifact.
/// </summary>
[RegisterComponent]
public sealed partial class ActiveArtifactAnalyzerComponent : Component
{
/// <summary>
/// When did the scanning start or last resume?
/// </summary>
[DataField("startTime", customTypeSerializer: typeof(TimespanSerializer))]
public TimeSpan StartTime;
/// <summary>
/// When pausing, this will store the duration the scan has already been running for.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public TimeSpan AccumulatedRunTime;
/// <summary>
/// Is analysis paused?
/// It could be when the Artifact Analyzer has no power, for example.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public bool AnalysisPaused = false;
/// <summary>
/// What is being scanned?
/// </summary>
[DataField]
public EntityUid Artifact;
}

View File

@@ -1,22 +0,0 @@
using Robust.Shared.Audio;
namespace Content.Server.Xenoarchaeology.Equipment.Components;
/// <summary>
/// This is used for tracking artifacts that are currently
/// being scanned by <see cref="ActiveArtifactAnalyzerComponent"/>
/// </summary>
[RegisterComponent]
public sealed partial class ActiveScannedArtifactComponent : Component
{
/// <summary>
/// The scanner that is scanning this artifact
/// </summary>
[ViewVariables]
public EntityUid Scanner;
/// <summary>
/// The sound that plays when the scan fails
/// </summary>
public readonly SoundSpecifier ScanFailureSound = new SoundPathSpecifier("/Audio/Machines/custom_deny.ogg");
}

View File

@@ -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;
/// <summary>
/// The console that is used for artifact analysis
/// </summary>
[RegisterComponent]
public sealed partial class AnalysisConsoleComponent : Component
{
/// <summary>
/// The analyzer entity the console is linked.
/// Can be null if not linked.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public EntityUid? AnalyzerEntity;
/// <summary>
/// The machine linking port for the analyzer
/// </summary>
[DataField("linkingPort", customTypeSerializer: typeof(PrototypeIdSerializer<SourcePortPrototype>))]
public string LinkingPort = "ArtifactAnalyzerSender";
/// <summary>
/// The sound played when an artifact has points extracted.
/// </summary>
[DataField("extractSound")]
public SoundSpecifier ExtractSound = new SoundPathSpecifier("/Audio/Effects/radpulse11.ogg");
/// <summary>
/// The entity spawned by a report.
/// </summary>
[DataField("reportEntityId", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string ReportEntityId = "Paper";
}

View File

@@ -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;
/// <summary>
/// A machine that is combined and linked to the <see cref="AnalysisConsoleComponent"/>
/// in order to analyze artifacts and extract points.
/// </summary>
[RegisterComponent]
public sealed partial class ArtifactAnalyzerComponent : Component
{
/// <summary>
/// How long it takes to analyze an artifact
/// </summary>
[DataField("analysisDuration", customTypeSerializer: typeof(TimespanSerializer))]
public TimeSpan AnalysisDuration = TimeSpan.FromSeconds(30);
/// <summary>
/// The corresponding console entity.
/// Can be null if not linked.
/// </summary>
[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
}

View File

@@ -1,12 +0,0 @@
namespace Content.Server.Xenoarchaeology.Equipment.Components;
/// <summary>
/// This is used for artifacts that are biased to move
/// in a particular direction via the <see cref="TraversalDistorterComponent"/>
/// </summary>
[RegisterComponent]
public sealed partial class BiasedArtifactComponent : Component
{
[ViewVariables]
public EntityUid Provider;
}

View File

@@ -1,7 +0,0 @@
namespace Content.Server.Xenoarchaeology.Equipment.Components;
[RegisterComponent]
public sealed partial class NodeScannerComponent : Component
{
}

View File

@@ -1,10 +0,0 @@
namespace Content.Server.Xenoarchaeology.Equipment.Components;
/// <summary>
/// Suppress artifact activation, when entity is placed inside this container.
/// </summary>
[RegisterComponent]
public sealed partial class SuppressArtifactContainerComponent : Component
{
}

View File

@@ -1,21 +0,0 @@
namespace Content.Server.Xenoarchaeology.Equipment.Components;
/// <summary>
/// This is used for a machine that biases
/// an artifact placed on it to move up/down
/// </summary>
[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
}

View File

@@ -1,519 +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.Power.EntitySystems;
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;
/// <summary>
/// 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.
/// </summary>
public sealed class ArtifactAnalyzerSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly ArtifactSystem _artifact = default!;
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
[Dependency] private readonly PaperSystem _paper = default!;
[Dependency] private readonly ResearchSystem _research = default!;
[Dependency] private readonly SharedAmbientSoundSystem _ambientSound = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedPowerReceiverSystem _receiver = default!;
[Dependency] private readonly TraversalDistorterSystem _traversalDistorter = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<ActiveScannedArtifactComponent, ArtifactActivatedEvent>(OnArtifactActivated);
SubscribeLocalEvent<ActiveArtifactAnalyzerComponent, ComponentStartup>(OnAnalyzeStart);
SubscribeLocalEvent<ActiveArtifactAnalyzerComponent, ComponentShutdown>(OnAnalyzeEnd);
SubscribeLocalEvent<ActiveArtifactAnalyzerComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<ArtifactAnalyzerComponent, ItemPlacedEvent>(OnItemPlaced);
SubscribeLocalEvent<ArtifactAnalyzerComponent, ItemRemovedEvent>(OnItemRemoved);
SubscribeLocalEvent<ArtifactAnalyzerComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<AnalysisConsoleComponent, NewLinkEvent>(OnNewLink);
SubscribeLocalEvent<AnalysisConsoleComponent, PortDisconnectedEvent>(OnPortDisconnected);
SubscribeLocalEvent<AnalysisConsoleComponent, AnalysisConsoleServerSelectionMessage>(OnServerSelectionMessage);
SubscribeLocalEvent<AnalysisConsoleComponent, AnalysisConsoleScanButtonPressedMessage>(OnScanButton);
SubscribeLocalEvent<AnalysisConsoleComponent, AnalysisConsolePrintButtonPressedMessage>(OnPrintButton);
SubscribeLocalEvent<AnalysisConsoleComponent, AnalysisConsoleExtractButtonPressedMessage>(OnExtractButton);
SubscribeLocalEvent<AnalysisConsoleComponent, AnalysisConsoleBiasButtonPressedMessage>(OnBiasButton);
SubscribeLocalEvent<AnalysisConsoleComponent, ResearchClientServerSelectedMessage>((e, c, _) => UpdateUserInterface(e, c),
after: new[] { typeof(ResearchSystem) });
SubscribeLocalEvent<AnalysisConsoleComponent, ResearchClientServerDeselectedMessage>((e, c, _) => UpdateUserInterface(e, c),
after: new[] { typeof(ResearchSystem) });
SubscribeLocalEvent<AnalysisConsoleComponent, BeforeActivatableUIOpenEvent>((e, c, _) => UpdateUserInterface(e, c));
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<ActiveArtifactAnalyzerComponent, ArtifactAnalyzerComponent>();
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);
}
}
/// <summary>
/// Resets the current scan on the artifact analyzer
/// </summary>
/// <param name="uid">The analyzer being reset</param>
/// <param name="component"></param>
[PublicAPI]
public void ResetAnalyzer(EntityUid uid, ArtifactAnalyzerComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
component.LastAnalyzedArtifact = null;
component.ReadyToPrint = false;
UpdateAnalyzerInformation(uid, component);
}
/// <summary>
/// Goes through the current entities on
/// the analyzer and returns a valid artifact
/// </summary>
/// <param name="uid"></param>
/// <param name="placer"></param>
/// <returns></returns>
private EntityUid? GetArtifactForAnalysis(EntityUid? uid, ItemPlacerComponent? placer = null)
{
if (uid == null || !Resolve(uid.Value, ref placer))
return null;
return placer.PlacedEntities.FirstOrNull();
}
/// <summary>
/// Updates the current scan information based on
/// the last artifact that was scanned.
/// </summary>
/// <param name="uid"></param>
/// <param name="component"></param>
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<ArtifactComponent>(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<DeviceLinkSinkComponent>(uid, out var sink))
return;
foreach (var source in sink.LinkedSources)
{
if (!TryComp<AnalysisConsoleComponent>(source, out var analysis))
continue;
component.Console = source;
analysis.AnalyzerEntity = uid;
return;
}
}
private void OnNewLink(EntityUid uid, AnalysisConsoleComponent component, NewLinkEvent args)
{
if (!TryComp<ArtifactAnalyzerComponent>(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<ArtifactAnalyzerComponent>(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<ArtifactAnalyzerComponent>(component.AnalyzerEntity, out var analyzer))
{
artifact = analyzer.LastAnalyzedArtifact;
msg = GetArtifactScanMessage(analyzer);
totalTime = analyzer.AnalysisDuration;
if (TryComp<ItemPlacerComponent>(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<ResearchClientComponent>(uid, out var client) && client.ConnectedToServer;
var scanning = TryComp<ActiveArtifactAnalyzerComponent>(component.AnalyzerEntity, out var active);
var paused = active != null ? active.AnalysisPaused : false;
var biasDirection = BiasDirection.Up;
if (TryComp<TraversalDistorterComponent>(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);
}
/// <summary>
/// opens the server selection menu.
/// </summary>
/// <param name="uid"></param>
/// <param name="component"></param>
/// <param name="args"></param>
private void OnServerSelectionMessage(EntityUid uid, AnalysisConsoleComponent component, AnalysisConsoleServerSelectionMessage args)
{
_ui.OpenUi(uid, ResearchClientUiKey.Key, args.Actor);
}
/// <summary>
/// Starts scanning the artifact.
/// </summary>
/// <param name="uid"></param>
/// <param name="component"></param>
/// <param name="args"></param>
private void OnScanButton(EntityUid uid, AnalysisConsoleComponent component, AnalysisConsoleScanButtonPressedMessage args)
{
if (component.AnalyzerEntity == null)
return;
if (HasComp<ActiveArtifactAnalyzerComponent>(component.AnalyzerEntity))
return;
var ent = GetArtifactForAnalysis(component.AnalyzerEntity);
if (ent == null)
return;
var activeComp = EnsureComp<ActiveArtifactAnalyzerComponent>(component.AnalyzerEntity.Value);
activeComp.StartTime = _timing.CurTime;
activeComp.AccumulatedRunTime = TimeSpan.Zero;
activeComp.Artifact = ent.Value;
if (TryComp<ApcPowerReceiverComponent>(component.AnalyzerEntity.Value, out var powa))
activeComp.AnalysisPaused = !powa.Powered;
var activeArtifact = EnsureComp<ActiveScannedArtifactComponent>(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<ArtifactAnalyzerComponent>(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<PaperComponent>(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<ArtifactTriggerPrototype>(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<ArtifactEffectPrototype>(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;
}
/// <summary>
/// Extracts points from the artifact and updates the server points
/// </summary>
/// <param name="uid"></param>
/// <param name="component"></param>
/// <param name="args"></param>
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<TraversalDistorterComponent>(component.AnalyzerEntity, out var trav))
return;
if (!_traversalDistorter.SetState(component.AnalyzerEntity.Value, trav, args.IsDown))
return;
UpdateUserInterface(uid, component);
}
/// <summary>
/// Cancels scans if the artifact changes nodes (is activated) during the scan.
/// </summary>
private void OnArtifactActivated(EntityUid uid, ActiveScannedArtifactComponent component, ArtifactActivatedEvent args)
{
CancelScan(uid);
}
/// <summary>
/// Stops the current scan
/// </summary>
[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<ActiveArtifactAnalyzerComponent>(component.Scanner);
if (analyzer.Console != null)
UpdateUserInterface(analyzer.Console.Value);
RemCompDeferred(artifact, component);
}
/// <summary>
/// Finishes the current scan.
/// </summary>
[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<ActiveScannedArtifactComponent>(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)
{
_receiver.SetNeedsPower(uid, true);
_ambientSound.SetAmbience(uid, true);
}
private void OnAnalyzeEnd(EntityUid uid, ActiveArtifactAnalyzerComponent component, ComponentShutdown args)
{
_receiver.SetNeedsPower(uid, 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);
}
}
}

View File

@@ -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 sealed class ArtifactCrusherSystem : SharedArtifactCrusherSystem
{
ContainerSystem.Insert((stack, null, null, null), crusher.OutputContainer);
}
_artifact.ForceActivateArtifact(contained);
}
if (!TryComp<BodyComponent>(contained, out var body))

View File

@@ -1,63 +1,13 @@
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
/// <inheritdoc cref="SharedNodeScannerSystem"/>
public sealed class NodeScannerSystem : SharedNodeScannerSystem
{
[Dependency] private readonly UseDelaySystem _useDelay = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
/// <inheritdoc/>
public override void Initialize()
protected override void TryOpenUi(Entity<NodeScannerComponent> device, EntityUid actor)
{
SubscribeLocalEvent<NodeScannerComponent, BeforeRangedInteractEvent>(OnBeforeRangedInteract);
SubscribeLocalEvent<NodeScannerComponent, GetVerbsEvent<UtilityVerb>>(AddScanVerb);
}
private void OnBeforeRangedInteract(EntityUid uid, NodeScannerComponent component, BeforeRangedInteractEvent args)
{
if (args.Handled || !args.CanReach || args.Target is not {} target)
return;
if (!TryComp<ArtifactComponent>(target, out var artifact) || artifact.CurrentNodeId == null)
return;
CreatePopup(uid, target, artifact);
args.Handled = true;
}
private void AddScanVerb(EntityUid uid, NodeScannerComponent component, GetVerbsEvent<UtilityVerb> args)
{
if (!args.CanAccess)
return;
if (!TryComp<ArtifactComponent>(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)
{
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
}
}

View File

@@ -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!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<TraversalDistorterComponent, MapInitEvent>(OnInit);
SubscribeLocalEvent<TraversalDistorterComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<TraversalDistorterComponent, ItemPlacedEvent>(OnItemPlaced);
SubscribeLocalEvent<TraversalDistorterComponent, ItemRemovedEvent>(OnItemRemoved);
}
private void OnInit(EntityUid uid, TraversalDistorterComponent component, MapInitEvent args)
{
component.NextActivation = _timing.CurTime;
}
/// <summary>
/// Switches the state of the traversal distorter between up and down.
/// </summary>
/// <param name="uid">The distorter's entity</param>
/// <param name="component">The component on the entity</param>
/// <returns>If the distorter changed state</returns>
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<BiasedArtifactComponent>(args.OtherEntity);
bias.Provider = uid;
}
private void OnItemRemoved(EntityUid uid, TraversalDistorterComponent component, ref ItemRemovedEvent args)
{
var otherEnt = args.OtherEntity;
if (TryComp<BiasedArtifactComponent>(otherEnt, out var bias) && bias.Provider == uid)
RemComp(otherEnt, bias);
}
}

View File

@@ -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
{
/// <summary>
/// Every node contained in the tree
/// </summary>
[DataField("nodeTree"), ViewVariables]
public List<ArtifactNode> NodeTree = new();
/// <summary>
/// The current node the artifact is on.
/// </summary>
[DataField("currentNodeId"), ViewVariables]
public int? CurrentNodeId;
#region Node Tree Gen
/// <summary>
/// Minimum number of nodes to generate, inclusive
/// </summary>
[DataField("nodesMin")]
public int NodesMin = 3;
/// <summary>
/// Maximum number of nodes to generate, exclusive
/// </summary>
[DataField("nodesMax")]
public int NodesMax = 9;
#endregion
/// <summary>
/// Cooldown time between artifact activations (in seconds).
/// </summary>
[DataField("timer"), ViewVariables(VVAccess.ReadWrite)]
public TimeSpan CooldownTime = TimeSpan.FromSeconds(5);
/// <summary>
/// Is this artifact under some suppression device?
/// f true, will ignore all trigger activations attempts.
/// </summary>
[DataField("isSuppressed"), ViewVariables(VVAccess.ReadWrite)]
public bool IsSuppressed;
/// <summary>
/// The last time the artifact was activated.
/// </summary>
[DataField("lastActivationTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan LastActivationTime;
/// <summary>
/// A multiplier applied to the calculated point value
/// to determine the monetary value of the artifact
/// </summary>
[DataField("priceMultiplier"), ViewVariables(VVAccess.ReadWrite)]
public float PriceMultiplier = 0.05f;
/// <summary>
/// The base amount of research points for each artifact node.
/// </summary>
[DataField("pointsPerNode"), ViewVariables(VVAccess.ReadWrite)]
public int PointsPerNode = 6500;
/// <summary>
/// Research points which have been "consumed" from the theoretical max value of the artifact.
/// </summary>
[DataField("consumedPoints"), ViewVariables(VVAccess.ReadWrite)]
public int ConsumedPoints;
/// <summary>
/// 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.
/// </summary>
[DataField("pointDangerMultiplier"), ViewVariables(VVAccess.ReadWrite)]
public float PointDangerMultiplier = 1.35f;
/// <summary>
/// The sound that plays when an artifact is activated
/// </summary>
[DataField("activationSound")]
public SoundSpecifier ActivationSound = new SoundCollectionSpecifier("ArtifactActivation")
{
Params = new()
{
Variation = 0.1f,
Volume = 3f
}
};
[DataField("activateActionEntity")] public EntityUid? ActivateActionEntity;
}
/// <summary>
/// A single "node" of an artifact that contains various data about it.
/// </summary>
[DataDefinition]
public sealed partial class ArtifactNode : ICloneable
{
/// <summary>
/// A numeric id corresponding to each node.
/// </summary>
[DataField("id"), ViewVariables]
public int Id;
/// <summary>
/// how "deep" into the node tree. used for generation and price/value calculations
/// </summary>
[DataField("depth"), ViewVariables]
public int Depth;
/// <summary>
/// A list of surrounding nodes. Used for tree traversal
/// </summary>
[DataField("edges"), ViewVariables]
public HashSet<int> Edges = new();
/// <summary>
/// Whether or not the node has been entered
/// </summary>
[DataField("discovered"), ViewVariables(VVAccess.ReadWrite)]
public bool Discovered;
/// <summary>
/// The trigger for the node
/// </summary>
[DataField("trigger", customTypeSerializer: typeof(PrototypeIdSerializer<ArtifactTriggerPrototype>), required: true), ViewVariables]
public string Trigger = default!;
/// <summary>
/// Whether or not the node has been triggered
/// </summary>
[DataField("triggered"), ViewVariables(VVAccess.ReadWrite)]
public bool Triggered;
/// <summary>
/// The effect when the node is activated
/// </summary>
[DataField("effect", customTypeSerializer: typeof(PrototypeIdSerializer<ArtifactEffectPrototype>), required: true), ViewVariables]
public string Effect = default!;
/// <summary>
/// Used for storing cumulative information about nodes
/// </summary>
[DataField("nodeData"), ViewVariables]
public Dictionary<string, object> NodeData = new();
public object Clone()
{
return new ArtifactNode
{
Id = Id,
Depth = Depth,
Edges = Edges,
Discovered = Discovered,
Trigger = Trigger,
Triggered = Triggered,
Effect = Effect,
NodeData = NodeData
};
}
}

View File

@@ -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<EntityPrototype>] private const string ArtifactActivateActionId = "ActionArtifactActivate";
/// <summary>
/// Used to add the artifact activation action (hehe), which lets sentient artifacts activate themselves,
/// either through admemery or the sentience effect.
/// </summary>
public void InitializeActions()
{
SubscribeLocalEvent<ArtifactComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<ArtifactComponent, ComponentRemove>(OnRemove);
SubscribeLocalEvent<ArtifactComponent, ArtifactSelfActivateEvent>(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;
}
}

View File

@@ -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 <uid> <node ID>",
ForceArtifactNode,
ForceArtifactNodeCompletions);
_conHost.RegisterCommand("getartifactmaxvalue", "Reports the maximum research point value for a given artifact", "forceartifacteffect <uid>",
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<ArtifactComponent>(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<ArtifactComponent>(uid, out var artifact))
{
return CompletionResult.FromHintOptions(artifact.NodeTree.Select(s => s.Id.ToString()), "<node id>");
}
}
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<ArtifactComponent>(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}");
}
}

View File

@@ -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<int> _usedNodeIds = new();
/// <summary>
/// Generate an Artifact tree with fully developed nodes.
/// </summary>
/// <param name="artifact"></param>
/// <param name="allNodes"></param>
/// <param name="nodesToCreate">The amount of nodes it has.</param>
private void GenerateArtifactNodeTree(EntityUid artifact, List<ArtifactNode> 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<ArtifactNode> { 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<ArtifactTriggerPrototype>()
.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<ArtifactEffectPrototype>()
.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;
}
/// <remarks>
/// The goal is that the depth that is closest to targetDepth has the highest chance of appearing.
/// The issue is that we also want some variance, so levels that are +/- 1 should also have a
/// decent shot of appearing. This function should probably get some tweaking at some point.
/// </remarks>
private Dictionary<int, float> GetDepthWeights(IEnumerable<int> depths, int targetDepth)
{
// this function is just a normal distribution with a
// mean of target depth and standard deviation of 0.75
var weights = new Dictionary<int, float>();
foreach (var d in depths)
{
var w = 10f / (0.75f * MathF.Sqrt(2 * MathF.PI)) * MathF.Pow(MathF.E, -MathF.Pow((d - targetDepth) / 0.75f, 2));
weights.Add(d, w);
}
return weights;
}
/// <summary>
/// Uses a weighted random system to get a random depth.
/// </summary>
private int GetRandomTargetDepth(Dictionary<int, float> weights)
{
var sum = weights.Values.Sum();
var accumulated = 0f;
var rand = _random.NextFloat() * sum;
foreach (var (key, weight) in weights)
{
accumulated += weight;
if (accumulated >= rand)
{
return key;
}
}
return _random.Pick(weights.Keys); //shouldn't happen
}
/// <summary>
/// Enter a node: attach the relevant components
/// </summary>
private void EnterNode(EntityUid uid, ref ArtifactNode node, ArtifactComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
if (component.CurrentNodeId != null)
{
ExitNode(uid, component);
}
component.CurrentNodeId = node.Id;
var trigger = _prototype.Index<ArtifactTriggerPrototype>(node.Trigger);
var effect = _prototype.Index<ArtifactEffectPrototype>(node.Effect);
var allComponents = effect.Components.Concat(effect.PermanentComponents).Concat(trigger.Components);
foreach (var (name, entry) in allComponents)
{
var reg = _componentFactory.GetRegistration(name);
if (node.Discovered && EntityManager.HasComponent(uid, reg.Type))
{
// Don't re-add permanent components unless this is the first time you've entered this node
if (effect.PermanentComponents.ContainsKey(name))
continue;
EntityManager.RemoveComponent(uid, reg.Type);
}
var comp = (Component)_componentFactory.GetComponent(reg);
var temp = (object)comp;
_serialization.CopyTo(entry.Component, ref temp);
EntityManager.RemoveComponent(uid, temp!.GetType());
EntityManager.AddComponent(uid, (Component)temp!);
}
node.Discovered = true;
RaiseLocalEvent(uid, new ArtifactNodeEnteredEvent(component.CurrentNodeId.Value));
}
/// <summary>
/// Exit a node: remove the relevant components.
/// </summary>
private void ExitNode(EntityUid uid, ArtifactComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
if (component.CurrentNodeId == null)
return;
var currentNode = GetNodeFromId(component.CurrentNodeId.Value, component);
var trigger = _prototype.Index<ArtifactTriggerPrototype>(currentNode.Trigger);
var effect = _prototype.Index<ArtifactEffectPrototype>(currentNode.Effect);
var entityPrototype = MetaData(uid).EntityPrototype;
var toRemove = effect.Components.Keys.Concat(trigger.Components.Keys).ToList();
foreach (var name in toRemove)
{
// if the entity prototype contained the component originally
if (entityPrototype?.Components.TryGetComponent(name, out var entry) ?? false)
{
var comp = (Component)_componentFactory.GetComponent(name);
var temp = (object)comp;
_serialization.CopyTo(entry, ref temp);
EntityManager.RemoveComponent(uid, temp!.GetType());
EntityManager.AddComponent(uid, (Component)temp);
continue;
}
EntityManager.RemoveComponentDeferred(uid, _componentFactory.GetRegistration(name).Type);
}
component.CurrentNodeId = null;
}
[PublicAPI]
public ArtifactNode GetNodeFromId(int id, ArtifactComponent component)
{
return component.NodeTree.First(x => x.Id == id);
}
[PublicAPI]
public ArtifactNode GetNodeFromId(int id, IEnumerable<ArtifactNode> nodes)
{
return nodes.First(x => x.Id == id);
}
}

View File

@@ -1,299 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Cargo.Systems;
using Content.Server.GameTicking;
using Content.Server.Power.EntitySystems;
using Content.Server.Xenoarchaeology.Equipment.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
using Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components;
using Content.Shared.CCVar;
using Content.Shared.Xenoarchaeology.XenoArtifacts;
using JetBrains.Annotations;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Timing;
namespace Content.Server.Xenoarchaeology.XenoArtifacts;
public sealed partial class ArtifactSystem : EntitySystem
{
[Dependency] private readonly IComponentFactory _componentFactory = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ISerializationManager _serialization = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ArtifactComponent, PriceCalculationEvent>(GetPrice);
InitializeCommands();
InitializeActions();
}
/// <summary>
/// Calculates the price of an artifact based on
/// how many nodes have been unlocked/triggered
/// </summary>
/// <remarks>
/// General balancing (for fully unlocked artifacts):
/// Simple (1-2 Nodes): 1-2K
/// Medium (5-8 Nodes): 6-7K
/// Complex (7-12 Nodes): 10-11K
/// </remarks>
private void GetPrice(EntityUid uid, ArtifactComponent component, ref PriceCalculationEvent args)
{
args.Price += (GetResearchPointValue(uid, component) + component.ConsumedPoints) * component.PriceMultiplier;
}
/// <summary>
/// Calculates how many research points the artifact is worth
/// </summary>
/// <remarks>
/// General balancing (for fully unlocked artifacts):
/// Simple (1-2 Nodes): ~10K
/// Medium (5-8 Nodes): ~30-40K
/// Complex (7-12 Nodes): ~60-80K
///
/// Simple artifacts should be enough to unlock a few techs.
/// Medium should get you partway through a tree.
/// Complex should get you through a full tree and then some.
/// </remarks>
public int GetResearchPointValue(EntityUid uid, ArtifactComponent? component = null, bool getMaxPrice = false)
{
if (!Resolve(uid, ref component))
return 0;
var sumValue = component.NodeTree.Sum(n => GetNodePointValue(n, component, getMaxPrice));
var fullyExploredBonus = component.NodeTree.All(x => x.Triggered) || getMaxPrice ? 1.25f : 1;
return (int) (sumValue * fullyExploredBonus) - component.ConsumedPoints;
}
/// <summary>
/// Adjusts how many points on the artifact have been consumed
/// </summary>
public void AdjustConsumedPoints(EntityUid uid, int amount, ArtifactComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
component.ConsumedPoints += amount;
}
/// <summary>
/// Sets whether or not the artifact is suppressed,
/// preventing it from activating
/// </summary>
public void SetIsSuppressed(EntityUid uid, bool suppressed, ArtifactComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
component.IsSuppressed = suppressed;
}
/// <summary>
/// Gets the point value for an individual node
/// </summary>
private float GetNodePointValue(ArtifactNode node, ArtifactComponent component, bool getMaxPrice = false)
{
var valueDeduction = 1f;
if (!getMaxPrice)
{
if (!node.Discovered)
return 0;
valueDeduction = !node.Triggered ? 0.25f : 1;
}
var triggerProto = _prototype.Index<ArtifactTriggerPrototype>(node.Trigger);
var effectProto = _prototype.Index<ArtifactEffectPrototype>(node.Effect);
var nodeDanger = (node.Depth + effectProto.TargetDepth + triggerProto.TargetDepth) / 3;
return component.PointsPerNode * MathF.Pow(component.PointDangerMultiplier, nodeDanger) * valueDeduction;
}
/// <summary>
/// Randomize a given artifact.
/// </summary>
[PublicAPI]
public void RandomizeArtifact(EntityUid uid, ArtifactComponent component)
{
var nodeAmount = _random.Next(component.NodesMin, component.NodesMax);
GenerateArtifactNodeTree(uid, component.NodeTree, nodeAmount);
var firstNode = GetRootNode(component.NodeTree);
EnterNode(uid, ref firstNode, component);
}
/// <summary>
/// Tries to activate the artifact
/// </summary>
/// <param name="uid"></param>
/// <param name="user"></param>
/// <param name="component"></param>
/// <param name="logMissing">Set this to false if you don't know if the entity is an artifact.</param>
/// <returns></returns>
public bool TryActivateArtifact(EntityUid uid, EntityUid? user = null, ArtifactComponent? component = null, bool logMissing = true)
{
if (!Resolve(uid, ref component, logMissing))
return false;
// check if artifact is under suppression field
if (component.IsSuppressed)
return false;
// check if artifact isn't under cooldown
var timeDif = _gameTiming.CurTime - component.LastActivationTime;
if (timeDif < component.CooldownTime)
return false;
ForceActivateArtifact(uid, user, component);
return true;
}
/// <summary>
/// Forces an artifact to activate
/// </summary>
/// <param name="uid"></param>
/// <param name="user"></param>
/// <param name="component"></param>
public void ForceActivateArtifact(EntityUid uid, EntityUid? user = null, ArtifactComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
if (component.CurrentNodeId == null)
return;
_audio.PlayPvs(component.ActivationSound, uid);
component.LastActivationTime = _gameTiming.CurTime;
var ev = new ArtifactActivatedEvent
{
Activator = user
};
RaiseLocalEvent(uid, ev, true);
var currentNode = GetNodeFromId(component.CurrentNodeId.Value, component);
currentNode.Triggered = true;
if (currentNode.Edges.Count == 0)
return;
var newNode = GetNewNode(uid, component);
if (newNode == null)
return;
EnterNode(uid, ref newNode, component);
}
private ArtifactNode? GetNewNode(EntityUid uid, ArtifactComponent component)
{
if (component.CurrentNodeId == null)
return null;
var currentNode = GetNodeFromId(component.CurrentNodeId.Value, component);
var allNodes = currentNode.Edges;
Log.Debug($"our node: {currentNode.Id}");
Log.Debug($"other nodes: {string.Join(", ", allNodes)}");
if (TryComp<BiasedArtifactComponent>(uid, out var bias) &&
TryComp<TraversalDistorterComponent>(bias.Provider, out var trav) &&
this.IsPowered(bias.Provider, EntityManager))
{
switch (trav.BiasDirection)
{
case BiasDirection.Up:
var upNodes = allNodes.Where(x => GetNodeFromId(x, component).Depth < currentNode.Depth).ToHashSet();
if (upNodes.Count != 0)
allNodes = upNodes;
break;
case BiasDirection.Down:
var downNodes = allNodes.Where(x => GetNodeFromId(x, component).Depth > currentNode.Depth).ToHashSet();
if (downNodes.Count != 0)
allNodes = downNodes;
break;
}
}
var undiscoveredNodes = allNodes.Where(x => !GetNodeFromId(x, component).Discovered).ToList();
Log.Debug($"Undiscovered nodes: {string.Join(", ", undiscoveredNodes)}");
var newNode = _random.Pick(allNodes);
if (undiscoveredNodes.Count != 0 && _random.Prob(0.75f))
{
newNode = _random.Pick(undiscoveredNodes);
}
Log.Debug($"Going to node {newNode}");
return GetNodeFromId(newNode, component);
}
/// <summary>
/// Try and get a data object from a node
/// </summary>
/// <param name="uid">The entity you're getting the data from</param>
/// <param name="key">The data's key</param>
/// <param name="data">The data you are trying to get.</param>
/// <param name="component"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public bool TryGetNodeData<T>(EntityUid uid, string key, [NotNullWhen(true)] out T? data, ArtifactComponent? component = null)
{
data = default;
if (!Resolve(uid, ref component))
return false;
if (component.CurrentNodeId == null)
return false;
var currentNode = GetNodeFromId(component.CurrentNodeId.Value, component);
if (currentNode.NodeData.TryGetValue(key, out var dat) && dat is T value)
{
data = value;
return true;
}
return false;
}
/// <summary>
/// Sets the node data to a certain value
/// </summary>
/// <param name="uid">The artifact</param>
/// <param name="key">The key being set</param>
/// <param name="value">The value it's being set to</param>
/// <param name="component"></param>
public void SetNodeData(EntityUid uid, string key, object value, ArtifactComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
if (component.CurrentNodeId == null)
return;
var currentNode = GetNodeFromId(component.CurrentNodeId.Value, component);
currentNode.NodeData[key] = value;
}
/// <summary>
/// Gets the base node (depth 0) of an artifact's node graph
/// </summary>
/// <param name="allNodes"></param>
/// <returns></returns>
public ArtifactNode GetRootNode(List<ArtifactNode> allNodes)
{
return allNodes.First(n => n.Depth == 0);
}
}

View File

@@ -1,14 +0,0 @@
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
/// <summary>
/// This is used for recharging all nearby batteries when activated
/// </summary>
[RegisterComponent]
public sealed partial class ChargeBatteryArtifactComponent : Component
{
/// <summary>
/// The radius of entities that will be affected
/// </summary>
[DataField("radius")]
public float Radius = 15f;
}

View File

@@ -1,32 +0,0 @@
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
/// <summary>
/// This is used for an artifact that creates a puddle of
/// random chemicals upon being triggered.
/// </summary>
[RegisterComponent, Access(typeof(ChemicalPuddleArtifactSystem))]
public sealed partial class ChemicalPuddleArtifactComponent : Component
{
/// <summary>
/// The solution where all the chemicals are stored
/// </summary>
[DataField("chemicalSolution", required: true), ViewVariables(VVAccess.ReadWrite)]
public Solution ChemicalSolution = default!;
/// <summary>
/// The different chemicals that can be spawned by this effect
/// </summary>
[DataField("possibleChemicals", required: true, customTypeSerializer: typeof(PrototypeIdListSerializer<ReagentPrototype>))]
public List<string> PossibleChemicals = default!;
/// <summary>
/// The number of chemicals in the puddle
/// </summary>
[DataField("chemAmount")]
public int ChemAmount = 3;
}

View File

@@ -1,20 +0,0 @@
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
/// <summary>
/// Artifact that EMP
/// </summary>
[RegisterComponent]
[Access(typeof(EmpArtifactSystem))]
public sealed partial class EmpArtifactComponent : Component
{
[DataField("range"), ViewVariables(VVAccess.ReadWrite)]
public float Range = 4f;
[DataField("energyConsumption"), ViewVariables(VVAccess.ReadWrite)]
public float EnergyConsumption = 1000000;
[DataField("disableDuration"), ViewVariables(VVAccess.ReadWrite)]
public float DisableDuration = 60f;
}

View File

@@ -1,49 +0,0 @@
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
using Content.Shared.Chemistry.Reagent;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
/// <summary>
/// Generates foam from the artifact when activated
/// </summary>
[RegisterComponent, Access(typeof(FoamArtifactSystem))]
public sealed partial class FoamArtifactComponent : Component
{
/// <summary>
/// The list of reagents that will randomly be picked from
/// to choose the foam reagent
/// </summary>
[DataField("reagents", required: true, customTypeSerializer: typeof(PrototypeIdListSerializer<ReagentPrototype>))]
public List<string> Reagents = new();
/// <summary>
/// The foam reagent
/// </summary>
[DataField("selectedReagent"), ViewVariables(VVAccess.ReadWrite)]
public string? SelectedReagent;
/// <summary>
/// How long does the foam last?
/// </summary>
[DataField("duration"), ViewVariables(VVAccess.ReadWrite)]
public float Duration = 10;
/// <summary>
/// How much reagent is in the foam?
/// </summary>
[DataField("reagentAmount"), ViewVariables(VVAccess.ReadWrite)]
public float ReagentAmount = 100;
/// <summary>
/// Minimum radius of foam spawned
/// </summary>
[DataField("minFoamAmount"), ViewVariables(VVAccess.ReadWrite)]
public int MinFoamAmount = 15;
/// <summary>
/// Maximum radius of foam spawned
/// </summary>
[DataField("maxFoamAmount"), ViewVariables(VVAccess.ReadWrite)]
public int MaxFoamAmount = 20;
}

View File

@@ -1,63 +0,0 @@
using Content.Shared.Atmos;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
/// <summary>
/// Spawn a random gas with random temperature when artifact activated.
/// </summary>
[RegisterComponent]
public sealed partial class GasArtifactComponent : Component
{
/// <summary>
/// Gas that will be spawned when artifact activated.
/// If null it will be picked on startup from <see cref="PossibleGases"/>.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("spawnGas")]
public Gas? SpawnGas;
/// <summary>
/// List of possible activation gases to pick on startup.
/// </summary>
[DataField("possibleGas")]
public List<Gas> PossibleGases = new()
{
Gas.Oxygen,
Gas.Plasma,
Gas.Nitrogen,
Gas.CarbonDioxide,
Gas.Tritium,
Gas.Ammonia,
Gas.NitrousOxide,
Gas.Frezon
};
/// <summary>
/// Temperature of spawned gas. If null it will be picked on startup from range from
/// <see cref="MinRandomTemperature"/> to <see cref="MaxRandomTemperature"/>.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("spawnTemperature")]
public float? SpawnTemperature;
[DataField("minRandomTemp")]
public float MinRandomTemperature = 100;
[DataField("maxRandomTemp")]
public float MaxRandomTemperature = 400;
/// <summary>
/// Max allowed external atmospheric pressure.
/// Artifact will stop spawn gas.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("maxExternalPressure")]
public float MaxExternalPressure = Atmospherics.GasMinerDefaultMaxExternalPressure;
/// <summary>
/// Moles of gas to spawn each time when artifact activated.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("spawnAmount")]
public float SpawnAmount = Atmospherics.MolesCellStandard * 3;
}

View File

@@ -1,17 +0,0 @@
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
/// <summary>
/// Artifact that ignites surrounding entities when triggered.
/// </summary>
[RegisterComponent]
public sealed partial class IgniteArtifactComponent : Component
{
[DataField("range")]
public float Range = 2f;
[DataField("minFireStack")]
public int MinFireStack = 2;
[DataField("maxFireStack")]
public int MaxFireStack = 5;
}

View File

@@ -1,14 +0,0 @@
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
/// <summary>
/// This is used for using the "knock" spell when the artifact is activated
/// </summary>
[RegisterComponent]
public sealed partial class KnockArtifactComponent : Component
{
/// <summary>
/// The range of the spell
/// </summary>
[DataField("knockRange")]
public float KnockRange = 4f;
}

View File

@@ -1,10 +0,0 @@
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
/// <summary>
/// Removes the masks/layers of hard fixtures from the artifact when added, allowing it to pass through walls
/// and such.
/// </summary>
[RegisterComponent]
public sealed partial class PhasingArtifactComponent : Component
{
}

View File

@@ -1,14 +0,0 @@
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
using Robust.Shared.Prototypes;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
/// <summary>
/// When activated artifact will spawn an pair portals. First - right in artifact, Second - at random point of station.
/// </summary>
[RegisterComponent, Access(typeof(PortalArtifactSystem))]
public sealed partial class PortalArtifactComponent : Component
{
[DataField]
public EntProtoId PortalProto = "PortalArtifact";
}

View File

@@ -1,7 +0,0 @@
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
[RegisterComponent]
public sealed partial class RandomInstrumentArtifactComponent : Component
{
}

View File

@@ -1,12 +0,0 @@
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
/// <summary>
/// When activated, will shuffle the position of all players
/// within a certain radius.
/// </summary>
[RegisterComponent]
public sealed partial class ShuffleArtifactComponent : Component
{
[DataField("radius")]
public float Radius = 7.5f;
}

View File

@@ -1,26 +0,0 @@
using Content.Shared.Storage;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
/// <summary>
/// When activated artifact will spawn an entity from prototype.
/// It could be an angry mob or some random item.
/// </summary>
[RegisterComponent]
public sealed partial class SpawnArtifactComponent : Component
{
[DataField("spawns")]
public List<EntitySpawnEntry>? Spawns;
/// <summary>
/// The range around the artifact that it will spawn the entity
/// </summary>
[DataField("range")]
public float Range = 0.5f;
/// <summary>
/// The maximum number of times the spawn will occur
/// </summary>
[DataField("maxSpawns")]
public int MaxSpawns = 10;
}

View File

@@ -1,10 +0,0 @@
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
/// <summary>
/// This is used for an artifact that triggers when activated.
/// </summary>
[RegisterComponent]
public sealed partial class TriggerArtifactComponent : Component
{
}

View File

@@ -1,31 +0,0 @@
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
using Robust.Server.GameObjects;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
/// <summary>
/// This handles <see cref="ChargeBatteryArtifactComponent"/>
/// </summary>
public sealed class ChargeBatteryArtifactSystem : EntitySystem
{
[Dependency] private readonly BatterySystem _battery = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly TransformSystem _transform = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<ChargeBatteryArtifactComponent, ArtifactActivatedEvent>(OnActivated);
}
private void OnActivated(EntityUid uid, ChargeBatteryArtifactComponent component, ArtifactActivatedEvent args)
{
foreach (var battery in _lookup.GetEntitiesInRange<BatteryComponent>(_transform.GetMapCoordinates(uid), component.Radius))
{
_battery.SetCharge(battery, battery.Comp.MaxCharge, battery);
}
}
}

View File

@@ -1,54 +0,0 @@
using Content.Server.Fluids.EntitySystems;
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
using Robust.Shared.Random;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
/// <summary>
/// This handles <see cref="ChemicalPuddleArtifactComponent"/>
/// </summary>
public sealed class ChemicalPuddleArtifactSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ArtifactSystem _artifact = default!;
[Dependency] private readonly PuddleSystem _puddle = default!;
/// <summary>
/// The key for the node data entry containing
/// the chemicals that the puddle is made of.
/// </summary>
public const string NodeDataChemicalList = "nodeDataChemicalList";
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<ChemicalPuddleArtifactComponent, ArtifactActivatedEvent>(OnActivated);
}
private void OnActivated(EntityUid uid, ChemicalPuddleArtifactComponent component, ArtifactActivatedEvent args)
{
if (!TryComp<ArtifactComponent>(uid, out var artifact))
return;
if (!_artifact.TryGetNodeData(uid, NodeDataChemicalList, out List<string>? chemicalList, artifact))
{
chemicalList = new();
for (var i = 0; i < component.ChemAmount; i++)
{
var chemProto = _random.Pick(component.PossibleChemicals);
chemicalList.Add(chemProto);
}
_artifact.SetNodeData(uid, NodeDataChemicalList, chemicalList, artifact);
}
var amountPerChem = component.ChemicalSolution.MaxVolume / component.ChemAmount;
foreach (var reagent in chemicalList)
{
component.ChemicalSolution.AddReagent(reagent, amountPerChem);
}
_puddle.TrySpillAt(uid, component.ChemicalSolution, out _);
}
}

View File

@@ -1,38 +0,0 @@
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
using Content.Shared.Damage;
using Content.Shared.Whitelist;
using Robust.Shared.Random;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
public sealed class BreakWindowArtifactSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<DamageNearbyArtifactComponent, ArtifactActivatedEvent>(OnActivated);
}
private void OnActivated(EntityUid uid, DamageNearbyArtifactComponent component, ArtifactActivatedEvent args)
{
var ents = _lookup.GetEntitiesInRange(uid, component.Radius);
if (args.Activator != null)
ents.Add(args.Activator.Value);
foreach (var ent in ents)
{
if (_whitelistSystem.IsWhitelistFail(component.Whitelist, ent))
continue;
if (!_random.Prob(component.DamageChance))
return;
_damageable.TryChangeDamage(ent, component.Damage, component.IgnoreResistances);
}
}
}

View File

@@ -1,23 +0,0 @@
using Content.Server.Emp;
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
using Robust.Server.GameObjects;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
public sealed class EmpArtifactSystem : EntitySystem
{
[Dependency] private readonly EmpSystem _emp = default!;
[Dependency] private readonly TransformSystem _transform = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<EmpArtifactComponent, ArtifactActivatedEvent>(OnActivate);
}
private void OnActivate(EntityUid uid, EmpArtifactComponent component, ArtifactActivatedEvent args)
{
_emp.EmpPulse(_transform.GetMapCoordinates(uid), component.Range, component.EnergyConsumption, component.DisableDuration);
}
}

View File

@@ -1,43 +0,0 @@
using System.Linq;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
using Content.Shared.Chemistry.Components;
using Robust.Shared.Random;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
public sealed class FoamArtifactSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SmokeSystem _smoke = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<FoamArtifactComponent, ArtifactNodeEnteredEvent>(OnNodeEntered);
SubscribeLocalEvent<FoamArtifactComponent, ArtifactActivatedEvent>(OnActivated);
}
private void OnNodeEntered(EntityUid uid, FoamArtifactComponent component, ArtifactNodeEnteredEvent args)
{
if (!component.Reagents.Any())
return;
component.SelectedReagent = component.Reagents[args.RandomSeed % component.Reagents.Count];
}
private void OnActivated(EntityUid uid, FoamArtifactComponent component, ArtifactActivatedEvent args)
{
if (component.SelectedReagent == null)
return;
var sol = new Solution();
var xform = Transform(uid);
var range = (int) MathF.Round(MathHelper.Lerp(component.MinFoamAmount, component.MaxFoamAmount, _random.NextFloat(0, 1f)));
sol.AddReagent(component.SelectedReagent, component.ReagentAmount);
var foamEnt = Spawn("Foam", xform.Coordinates);
var spreadAmount = range * 4;
_smoke.StartSmoke(foamEnt, sol, component.Duration, spreadAmount);
}
}

View File

@@ -1,53 +0,0 @@
using Content.Server.Atmos;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
using Content.Shared.Atmos;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
public sealed class GasArtifactSystem : EntitySystem
{
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GasArtifactComponent, ArtifactNodeEnteredEvent>(OnNodeEntered);
SubscribeLocalEvent<GasArtifactComponent, ArtifactActivatedEvent>(OnActivate);
}
private void OnNodeEntered(EntityUid uid, GasArtifactComponent component, ArtifactNodeEnteredEvent args)
{
if (component.SpawnGas == null && component.PossibleGases.Count != 0)
{
var gas = component.PossibleGases[args.RandomSeed % component.PossibleGases.Count];
component.SpawnGas = gas;
}
if (component.SpawnTemperature == null)
{
var temp = args.RandomSeed % component.MaxRandomTemperature - component.MinRandomTemperature +
component.MinRandomTemperature;
component.SpawnTemperature = temp;
}
}
private void OnActivate(EntityUid uid, GasArtifactComponent component, ArtifactActivatedEvent args)
{
if (component.SpawnGas == null || component.SpawnTemperature == null)
return;
var environment = _atmosphereSystem.GetContainingMixture(uid, false, true);
if (environment == null)
return;
if (environment.Pressure >= component.MaxExternalPressure)
return;
var merger = new GasMixture(1) { Temperature = component.SpawnTemperature.Value };
merger.SetMoles(component.SpawnGas.Value, component.SpawnAmount);
_atmosphereSystem.Merge(environment, merger);
}
}

View File

@@ -1,33 +0,0 @@
using System.Linq;
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
using Robust.Shared.Random;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
public sealed class IgniteArtifactSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly FlammableSystem _flammable = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<IgniteArtifactComponent, ArtifactActivatedEvent>(OnActivate);
}
private void OnActivate(EntityUid uid, IgniteArtifactComponent component, ArtifactActivatedEvent args)
{
var flammable = GetEntityQuery<FlammableComponent>();
foreach (var target in _lookup.GetEntitiesInRange(uid, component.Range))
{
if (!flammable.TryGetComponent(target, out var fl))
continue;
fl.FireStacks += _random.Next(component.MinFireStack, component.MaxFireStack);
_flammable.Ignite(target, uid, fl);
}
}
}

View File

@@ -1,24 +0,0 @@
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
using Content.Shared.Magic.Events;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
public sealed class KnockArtifactSystem : EntitySystem
{
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<KnockArtifactComponent, ArtifactActivatedEvent>(OnActivated);
}
private void OnActivated(EntityUid uid, KnockArtifactComponent component, ArtifactActivatedEvent args)
{
var ev = new KnockSpellEvent
{
Performer = uid,
Range = component.KnockRange
};
RaiseLocalEvent(ev);
}
}

View File

@@ -1,38 +0,0 @@
using Content.Server.Ghost;
using Content.Server.Light.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
using Robust.Shared.Random;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
/// <summary>
/// This handles...
/// </summary>
public sealed class LightFlickerArtifactSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly GhostSystem _ghost = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<LightFlickerArtifactComponent, ArtifactActivatedEvent>(OnActivated);
}
private void OnActivated(EntityUid uid, LightFlickerArtifactComponent component, ArtifactActivatedEvent args)
{
var lights = GetEntityQuery<PoweredLightComponent>();
foreach (var light in _lookup.GetEntitiesInRange(uid, component.Radius, LookupFlags.StaticSundries ))
{
if (!lights.HasComponent(light))
continue;
if (!_random.Prob(component.FlickerChance))
continue;
_ghost.DoGhostBooEvent(light);
}
}
}

View File

@@ -1,36 +0,0 @@
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
using Content.Shared.Physics;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Systems;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
/// <summary>
/// Handles allowing activated artifacts to phase through walls.
/// </summary>
public sealed class PhasingArtifactSystem : EntitySystem
{
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PhasingArtifactComponent, ArtifactActivatedEvent>(OnActivate);
}
private void OnActivate(EntityUid uid, PhasingArtifactComponent component, ArtifactActivatedEvent args)
{
if (!TryComp<FixturesComponent>(uid, out var fixtures))
return;
foreach (var fixture in fixtures.Fixtures.Values)
{
_physics.SetHard(uid, fixture, false, fixtures);
}
}
}

View File

@@ -1,44 +0,0 @@
using Content.Server.Polymorph.Systems;
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
using Content.Shared.Humanoid;
using Content.Shared.Mobs.Systems;
using Robust.Shared.Audio.Systems;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
public sealed class PolyOthersArtifactSystem : EntitySystem
{
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly MobStateSystem _mob = default!;
[Dependency] private readonly PolymorphSystem _poly = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
/// <summary>
/// On effect trigger polymorphs targets in range.
/// </summary>
public override void Initialize()
{
SubscribeLocalEvent<PolyOthersArtifactComponent, ArtifactActivatedEvent>(OnActivate);
}
/// <summary>
/// Provided target is alive and is not a zombie, polymorphs the target.
/// </summary>
private void OnActivate(Entity<PolyOthersArtifactComponent> ent, ref ArtifactActivatedEvent args)
{
var xform = Transform(ent);
var humanoids = new HashSet<Entity<HumanoidAppearanceComponent>>();
_lookup.GetEntitiesInRange(xform.Coordinates, ent.Comp.Range, humanoids);
foreach (var comp in humanoids)
{
var target = comp.Owner;
if (_mob.IsAlive(target))
{
_poly.PolymorphEntity(target, ent.Comp.PolymorphPrototypeName);
_audio.PlayPvs(ent.Comp.PolySound, ent);
}
}
}
}

View File

@@ -1,23 +0,0 @@
using Content.Server.Instruments;
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
using Content.Shared.Instruments;
using Robust.Shared.Random;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
public sealed class RandomInstrumentArtifactSystem : EntitySystem
{
[Dependency] private readonly InstrumentSystem _instrument = default!;
[Dependency] private readonly IRobustRandom _random = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<RandomInstrumentArtifactComponent, ComponentStartup>(OnStartup);
}
private void OnStartup(EntityUid uid, RandomInstrumentArtifactComponent component, ComponentStartup args)
{
var instrument = EnsureComp<InstrumentComponent>(uid);
_instrument.SetInstrumentProgram(uid, instrument, (byte) _random.Next(0, 127), 0);
}
}

Some files were not shown because too many files have changed in this diff Show More