XenoArch [Science Overhaul] (#12204)

* multi-node xeno artifacts

* refactor existing artifact effects

* more tweaks to generation

* more shit plus fix tests

* more generation stuff plus threat levels

* doink

* now make it build

* defer the artifact activation to not cause errors

also pricing

* some changes

* all of the yaml + ui stuff for artifact analyzer

* machine linking and starting to make the ui functional

* artifact analyzer display

* a shit ton of artifact analyzer stuff

* more changes; making destroy work properly; progress bar tweaks

* getting shit going!

ALL RIGHT

* small tweaks that didn't help much

* Komm susser todd: the end of analysis

* recipes and hints and ui, oh my!

* add some in-game sources

gotta prepare for day 1 launch

* node data + ditch random seed in place of id

* bunch of triggers

* finish off the last few triggers

* implement machine examine verb

* knock, flicker, blink, throw

* shatter, foam, shuffle, heat

* fix all the shit i broke

* *some* of these have to be good, no?

25 effects

* callin' it there for effects

* comments + reword some trigger hints

* don't mind this little commit here

* byref event

* fix brokey node entry

* fix low pressure trigger

* mirror review plus fixing 0x40's bug

also the throw artifact threw incorrectly

* randomize the event message a teeny bit
This commit is contained in:
Nemanja
2022-11-06 18:05:44 -05:00
committed by GitHub
parent 0d4a605a94
commit 273e0968e4
107 changed files with 3321 additions and 358 deletions

View File

@@ -0,0 +1,64 @@
using Content.Shared.Xenoarchaeology.Equipment;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
namespace Content.Client.Xenoarchaeology.Ui;
[UsedImplicitly]
public sealed class AnalysisConsoleBoundUserInterface : BoundUserInterface
{
private AnalysisConsoleMenu? _consoleMenu;
public AnalysisConsoleBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
base.Open();
_consoleMenu = new AnalysisConsoleMenu();
_consoleMenu.OnClose += Close;
_consoleMenu.OpenCentered();
_consoleMenu.OnServerSelectionButtonPressed += _ =>
{
SendMessage(new AnalysisConsoleServerSelectionMessage());
};
_consoleMenu.OnScanButtonPressed += _ =>
{
SendMessage(new AnalysisConsoleScanButtonPressedMessage());
};
_consoleMenu.OnDestroyButtonPressed += _ =>
{
SendMessage(new AnalysisConsoleDestroyButtonPressedMessage());
};
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
switch (state)
{
case AnalysisConsoleScanUpdateState msg:
_consoleMenu?.SetDestroyButtonDisabled(msg);
_consoleMenu?.SetScanButtonDisabled(msg);
_consoleMenu?.UpdateInformationDisplay(msg);
_consoleMenu?.UpdateProgressBar(msg);
break;
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
return;
_consoleMenu?.Dispose();
}
}

View File

@@ -0,0 +1,59 @@
<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"
Title="{Loc 'analysis-console-menu-title'}"
MinSize="620 280"
SetSize="620 280">
<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="10"></BoxContainer>
<Button Name="DestroyButton"
Text="{Loc 'analysis-console-destroy-button'}"
ToolTip="{Loc 'analysis-console-destroy-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.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>
</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>
</BoxContainer>
</PanelContainer>
</BoxContainer>
</controls:FancyWindow>

View File

@@ -0,0 +1,157 @@
using Content.Client.Stylesheets;
using Content.Client.UserInterface.Controls;
using Content.Shared.Xenoarchaeology.Equipment;
using Content.Shared.Xenoarchaeology.XenoArtifacts;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.Xenoarchaeology.Ui;
[GenerateTypedNameReferences]
public sealed partial class AnalysisConsoleMenu : FancyWindow
{
[Dependency] private readonly IEntityManager _ent = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
public event Action<BaseButton.ButtonEventArgs>? OnServerSelectionButtonPressed;
public event Action<BaseButton.ButtonEventArgs>? OnScanButtonPressed;
public event Action<BaseButton.ButtonEventArgs>? OnDestroyButtonPressed;
public AnalysisConsoleMenu()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
ServerSelectionButton.OnPressed += a => OnServerSelectionButtonPressed?.Invoke(a);
ScanButton.OnPressed += a => OnScanButtonPressed?.Invoke(a);
DestroyButton.OnPressed += a => OnDestroyButtonPressed?.Invoke(a);
}
public void SetScanButtonDisabled(AnalysisConsoleScanUpdateState state)
{
var disabled = !state.CanScan;
ScanButton.Disabled = disabled;
}
public void SetDestroyButtonDisabled(AnalysisConsoleScanUpdateState state)
{
var disabled = !state.ServerConnected || !state.CanScan;
DestroyButton.Disabled = disabled;
if (disabled)
{
DestroyButton.RemoveStyleClass(StyleBase.ButtonCaution);
}
else
{
DestroyButton.AddStyleClass(StyleBase.ButtonCaution);
}
}
public void UpdateArtifactIcon(EntityUid? uid)
{
if (uid == null)
{
ArtifactDisplay.Visible = false;
return;
}
ArtifactDisplay.Visible = true;
if (!_ent.TryGetComponent<SpriteComponent>(uid, out var sprite))
return;
ArtifactDisplay.Sprite = sprite;
}
public void UpdateInformationDisplay(AnalysisConsoleScanUpdateState state)
{
var message = new FormattedMessage();
if (state.Scanning)
{
message.AddMarkup(Loc.GetString("analysis-console-info-scanner"));
Information.SetMessage(message);
return;
}
//do this here
UpdateArtifactIcon(state.Artifact);
if (state.Artifact == null)//no scan present
{
if (!state.AnalyzerConnected) //no analyzer connected
message.AddMarkup(Loc.GetString("analysis-console-info-no-scanner"));
else if (!state.CanScan) //no artifact
message.AddMarkup(Loc.GetString("analysis-console-info-no-artifact"));
else if (state.Artifact == null) //ready to go
message.AddMarkup(Loc.GetString("analysis-console-info-ready"));
}
if (state.Id != null) //node id
message.AddMarkup(Loc.GetString("analysis-console-info-id", ("id", state.Id))+"\n");
if (state.Depth != null) //node depth
message.AddMarkup(Loc.GetString("analysis-console-info-depth", ("depth", state.Depth))+"\n");
if (state.Triggered != null) //whether it has been triggered
{
var activated = state.Triggered.Value
? "analysis-console-info-triggered-true"
: "analysis-console-info-triggered-false";
message.AddMarkup(Loc.GetString(activated)+"\n");
}
message.AddMarkup("\n");
var needSecondNewline = false;
if (state.TriggerProto != null && //possible triggers
_proto.TryIndex<ArtifactTriggerPrototype>(state.TriggerProto, out var trigger) &&
trigger.TriggerHint != null)
{
message.AddMarkup(Loc.GetString("analysis-console-info-trigger",
("trigger", Loc.GetString(trigger.TriggerHint))) + "\n");
needSecondNewline = true;
}
if (state.EffectProto != null && //possible effects
_proto.TryIndex<ArtifactEffectPrototype>(state.EffectProto, out var effect) &&
effect.EffectHint != null)
{
message.AddMarkup(Loc.GetString("analysis-console-info-effect",
("effect", Loc.GetString(effect.EffectHint))) + "\n");
needSecondNewline = true;
}
if (needSecondNewline)
message.AddMarkup("\n");
if (state.Edges != null) //number of edges
message.AddMarkup(Loc.GetString("analysis-console-info-edges", ("edges", state.Edges))+"\n");
if (state.Completion != null) //completion percentage
{
message.AddMarkup(Loc.GetString("analysis-console-info-completion",
("percentage", Math.Round(state.Completion.Value * 100)))+"\n");
}
Information.SetMessage(message);
}
public void UpdateProgressBar(AnalysisConsoleScanUpdateState state)
{
ProgressBar.Visible = state.Scanning;
ProgressLabel.Visible = state.Scanning;
if (!state.Scanning)
return;
ProgressLabel.Text = Loc.GetString("analysis-console-progress-text",
("seconds", (int) state.TotalTime.TotalSeconds - (int) state.TimeRemaining.TotalSeconds));
ProgressBar.Value = (float) state.TimeRemaining.Divide(state.TotalTime);
}
}

View File

@@ -39,7 +39,6 @@ public sealed class PrototypeSaveTest
// The rest of these prototypes (probably) shouldn't be getting ignored.
// There should be an issue up tracking all of these prototypes, indicating that still need to get fixed.
"HeadSkeleton",
"CrateArtifactContainer",
// The followjng are all fixture-less phsyics entities that set can-collide to false on init.
"CarpRift",
"GasMinerOxygen",

View File

@@ -3,6 +3,7 @@ using Content.Server.Coordinates.Helpers;
using Content.Shared.Audio;
using Content.Shared.Chemistry.Components;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Map;
using Robust.Shared.Player;
@@ -19,7 +20,7 @@ namespace Content.Server.Chemistry.ReactionEffects
}
public static void SpawnFoam(string entityPrototype, EntityCoordinates coords, Solution? contents, int amount, float duration, float spreadDelay,
float removeDelay, SoundSpecifier sound, IEntityManager? entityManager = null)
float removeDelay, SoundSpecifier? sound = null, IEntityManager? entityManager = null)
{
entityManager ??= IoCManager.Resolve<IEntityManager>();
var ent = entityManager.SpawnEntity(entityPrototype, coords.SnapToGrid());
@@ -37,7 +38,8 @@ namespace Content.Server.Chemistry.ReactionEffects
areaEffectComponent.TryAddSolution(contents);
areaEffectComponent.Start(amount, duration, spreadDelay, removeDelay);
SoundSystem.Play(sound.GetSound(), Filter.Pvs(ent), ent, AudioHelpers.WithVariation(0.125f));
entityManager.EntitySysManager.GetEntitySystem<AudioSystem>()
.PlayPvs(sound, ent, AudioParams.Default.WithVariation(0.125f));
}
}
}

View File

@@ -0,0 +1,13 @@
using Content.Server.Xenoarchaeology.XenoArtifacts;
using Content.Shared.Chemistry.Reagent;
namespace Content.Server.Chemistry.ReagentEffects;
public sealed class ActivateArtifact : ReagentEffect
{
public override void Effect(ReagentEffectArgs args)
{
var artifact = args.EntityManager.EntitySysManager.GetEntitySystem<ArtifactSystem>();
artifact.TryActivateArtifact(args.SolutionEntity);
}
}

View File

@@ -54,6 +54,16 @@ namespace Content.Server.Salvage
{
public static readonly MagnetState Inactive = new (MagnetStateType.Inactive, TimeSpan.Zero);
};
public sealed class SalvageMagnetActivatedEvent : EntityEventArgs
{
public EntityUid Magnet;
public SalvageMagnetActivatedEvent(EntityUid magnet)
{
Magnet = magnet;
}
}
public enum MagnetStateType
{
Inactive,

View File

@@ -169,6 +169,7 @@ namespace Content.Server.Salvage
}
gridState.ActiveMagnets.Add(component);
component.MagnetState = new MagnetState(MagnetStateType.Attaching, gridState.CurrentTime + AttachingTime);
RaiseLocalEvent(new SalvageMagnetActivatedEvent(component.Owner));
Report(component.Owner, component.SalvageChannel, "salvage-system-report-activate-success");
break;
case MagnetStateType.Attaching:

View File

@@ -0,0 +1,46 @@
using Robust.Shared.Random;
namespace Content.Server.StationEvents.Events;
public sealed class BluespaceArtifact : StationEventSystem
{
[Dependency] private readonly IRobustRandom _random = default!;
public override string Prototype => "BluespaceArtifact";
public readonly string ArtifactSpawnerPrototype = "RandomArtifactSpawner";
public readonly string ArtifactFlashPrototype = "EffectFlashBluespace";
public readonly List<string> PossibleSighting = new()
{
"bluespace-artifact-sighting-1",
"bluespace-artifact-sighting-2",
"bluespace-artifact-sighting-3",
"bluespace-artifact-sighting-4",
"bluespace-artifact-sighting-5",
"bluespace-artifact-sighting-6",
"bluespace-artifact-sighting-7"
};
public override void Added()
{
base.Added();
var str = Loc.GetString("bluespace-artifact-event-announcement",
("sighting", Loc.GetString(_random.Pick(PossibleSighting))));
ChatSystem.DispatchGlobalAnnouncement(str, colorOverride: Color.FromHex("#18abf5"));
}
public override void Started()
{
base.Started();
if (!TryFindRandomTile(out _, out _, out _, out var coords))
return;
EntityManager.SpawnEntity(ArtifactSpawnerPrototype, coords);
EntityManager.SpawnEntity(ArtifactFlashPrototype, coords);
Sawmill.Info($"Spawning random artifact at {coords}");
}
}

View File

@@ -0,0 +1,24 @@
using Robust.Shared.Audio;
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 class ActiveArtifactAnalyzerComponent : Component
{
/// <summary>
/// When did the scanning start?
/// </summary>
[DataField("startTime", customTypeSerializer: typeof(TimespanSerializer))]
public TimeSpan StartTime;
/// <summary>
/// What is being scanned?
/// </summary>
[ViewVariables]
public EntityUid Artifact;
}

View File

@@ -0,0 +1,22 @@
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 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

@@ -0,0 +1,31 @@
using Content.Shared.MachineLinking;
using Robust.Shared.Audio;
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 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<TransmitterPortPrototype>))]
public readonly string LinkingPort = "ArtifactAnalyzerSender";
/// <summary>
/// The sound played when an artifact is destroyed.
/// </summary>
[DataField("destroySound")]
public SoundSpecifier DestroySound = new SoundPathSpecifier("/Audio/Effects/radpulse11.ogg");
}

View File

@@ -0,0 +1,67 @@
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 and destroy artifacts.
/// </summary>
[RegisterComponent]
public sealed class ArtifactAnalyzerComponent : Component
{
/// <summary>
/// How long it takes to analyze an artifact
/// </summary>
[DataField("analysisDuration", customTypeSerializer: typeof(TimespanSerializer))]
public TimeSpan AnalysisDuration = TimeSpan.FromSeconds(60);
/// <summary>
/// A mulitplier on the duration of analysis.
/// Used for machine upgrading.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public float AnalysisDurationMulitplier = 1;
/// <summary>
/// The machine part that modifies analysis duration.
/// </summary>
[DataField("machinePartAnalysisDuration", customTypeSerializer: typeof(PrototypeIdSerializer<MachinePartPrototype>))]
public string MachinePartAnalysisDuration = "ScanningModule";
/// <summary>
/// The modifier raised to the part rating to determine the duration multiplier.
/// </summary>
[DataField("partRatingAnalysisDurationMultiplier")]
public float PartRatingAnalysisDurationMultiplier = 0.75f;
/// <summary>
/// The corresponding console entity.
/// Can be null if not linked.
/// </summary>
[ViewVariables]
public EntityUid? Console;
/// <summary>
/// All of the valid artifacts currently touching the analyzer.
/// </summary>
[ViewVariables]
public HashSet<EntityUid> Contacts = new();
[DataField("scanFinishedSound")]
public readonly SoundSpecifier ScanFinishedSound = new SoundPathSpecifier("/Audio/Machines/scan_finish.ogg");
#region Analysis Data
[ViewVariables]
public EntityUid? LastAnalyzedArtifact;
[ViewVariables]
public ArtifactNode? LastAnalyzedNode;
[ViewVariables(VVAccess.ReadWrite)]
public float? LastAnalyzedCompletion;
#endregion
}

View File

@@ -1,4 +1,4 @@
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Equipment.Components;
namespace Content.Server.Xenoarchaeology.Equipment.Components;
/// <summary>
/// Suppress artifact activation, when entity is placed inside this container.

View File

@@ -0,0 +1,413 @@
using System.Linq;
using Content.Server.Construction;
using Content.Server.MachineLinking.Events;
using Content.Server.Power.Components;
using Content.Server.Research;
using Content.Server.Research.Components;
using Content.Server.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.MachineLinking.Events;
using Content.Shared.Popups;
using Content.Shared.Research.Components;
using Content.Shared.Xenoarchaeology.Equipment;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Audio;
using Robust.Shared.Physics.Events;
using Robust.Shared.Player;
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 SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!;
[Dependency] private readonly ArtifactSystem _artifact = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<ActiveScannedArtifactComponent, MoveEvent>(OnScannedMoved);
SubscribeLocalEvent<ActiveScannedArtifactComponent, ArtifactActivatedEvent>(OnArtifactActivated);
SubscribeLocalEvent<ActiveArtifactAnalyzerComponent, ComponentStartup>(OnAnalyzeStart);
SubscribeLocalEvent<ActiveArtifactAnalyzerComponent, ComponentShutdown>(OnAnalyzeEnd);
SubscribeLocalEvent<ActiveArtifactAnalyzerComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<ArtifactAnalyzerComponent, UpgradeExamineEvent>(OnUpgradeExamine);
SubscribeLocalEvent<ArtifactAnalyzerComponent, RefreshPartsEvent>(OnRefreshParts);
SubscribeLocalEvent<ArtifactAnalyzerComponent, StartCollideEvent>(OnCollide);
SubscribeLocalEvent<ArtifactAnalyzerComponent, EndCollideEvent>(OnEndCollide);
SubscribeLocalEvent<AnalysisConsoleComponent, NewLinkEvent>(OnNewLink);
SubscribeLocalEvent<AnalysisConsoleComponent, PortDisconnectedEvent>(OnPortDisconnected);
SubscribeLocalEvent<AnalysisConsoleComponent, AnalysisConsoleServerSelectionMessage>(OnServerSelectionMessage);
SubscribeLocalEvent<AnalysisConsoleComponent, AnalysisConsoleScanButtonPressedMessage>(OnScanButton);
SubscribeLocalEvent<AnalysisConsoleComponent, AnalysisConsoleDestroyButtonPressedMessage>(OnDestroyButton);
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);
foreach (var (active, scan) in EntityQuery<ActiveArtifactAnalyzerComponent, ArtifactAnalyzerComponent>())
{
if (scan.Console != null)
UpdateUserInterface(scan.Console.Value);
if (_timing.CurTime - active.StartTime < (scan.AnalysisDuration * scan.AnalysisDurationMulitplier))
continue;
FinishScan(scan.Owner, 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;
UpdateAnalyzerInformation(uid, component);
}
/// <summary>
/// Goes through the current contacts on
/// the analyzer and returns a valid artifact
/// </summary>
/// <param name="uid"></param>
/// <param name="component"></param>
/// <returns></returns>
private EntityUid? GetArtifactForAnalysis(EntityUid? uid, ArtifactAnalyzerComponent? component = null)
{
if (uid == null)
return null;
if (!Resolve(uid.Value, ref component))
return null;
var validEnts = component.Contacts.Where(HasComp<ArtifactComponent>).ToHashSet();
return validEnts.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.LastAnalyzedCompletion = null;
component.LastAnalyzedNode = null;
}
else if (TryComp<ArtifactComponent>(component.LastAnalyzedArtifact, out var artifact))
{
var lastNode = (ArtifactNode?) artifact.CurrentNode?.Clone();
component.LastAnalyzedNode = lastNode;
if (artifact.NodeTree != null)
{
var discoveredNodes = artifact.NodeTree.AllNodes.Count(x => x.Discovered && x.Triggered);
component.LastAnalyzedCompletion = (float) discoveredNodes / artifact.NodeTree.AllNodes.Count;
}
}
}
private void OnNewLink(EntityUid uid, AnalysisConsoleComponent component, NewLinkEvent args)
{
if (!TryComp<ArtifactAnalyzerComponent>(args.Receiver, out var analyzer))
return;
component.AnalyzerEntity = args.Receiver;
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))
return;
EntityUid? artifact = null;
ArtifactNode? node = null;
float? completion = null;
var totalTime = TimeSpan.Zero;
var canScan = false;
if (component.AnalyzerEntity != null && TryComp<ArtifactAnalyzerComponent>(component.AnalyzerEntity, out var analyzer))
{
artifact = analyzer.LastAnalyzedArtifact;
node = analyzer.LastAnalyzedNode;
completion = analyzer.LastAnalyzedCompletion;
totalTime = analyzer.AnalysisDuration * analyzer.AnalysisDurationMulitplier;
canScan = analyzer.Contacts.Any();
}
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 remaining = active != null ? _timing.CurTime - active.StartTime : TimeSpan.Zero;
var state = new AnalysisConsoleScanUpdateState(artifact, analyzerConnected, serverConnected, canScan,
node?.Id, node?.Depth, node?.Edges.Count, node?.Triggered, node?.Effect.ID, node?.Trigger.ID, completion,
scanning, remaining, totalTime);
var bui = _ui.GetUi(uid, ArtifactAnalzyerUiKey.Key);
_ui.SetUiState(bui, 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.TryOpen(uid, ResearchClientUiKey.Key, (IPlayerSession) args.Session);
}
/// <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.Artifact = ent.Value;
var activeArtifact = EnsureComp<ActiveScannedArtifactComponent>(ent.Value);
activeArtifact.Scanner = component.AnalyzerEntity.Value;
}
/// <summary>
/// destroys the artifact and updates the server points
/// </summary>
/// <param name="uid"></param>
/// <param name="component"></param>
/// <param name="args"></param>
private void OnDestroyButton(EntityUid uid, AnalysisConsoleComponent component, AnalysisConsoleDestroyButtonPressedMessage args)
{
if (!TryComp<ResearchClientComponent>(uid, out var client) || client.Server == null || component.AnalyzerEntity == null)
return;
var entToDestroy = GetArtifactForAnalysis(component.AnalyzerEntity);
if (entToDestroy == null)
return;
if (TryComp<ArtifactAnalyzerComponent>(component.AnalyzerEntity.Value, out var analyzer) &&
analyzer.LastAnalyzedArtifact == entToDestroy)
{
ResetAnalyzer(component.AnalyzerEntity.Value);
}
client.Server.Points += _artifact.GetResearchPointValue(entToDestroy.Value);
EntityManager.DeleteEntity(entToDestroy.Value);
_audio.PlayPvs(component.DestroySound, component.AnalyzerEntity.Value, AudioParams.Default.WithVolume(2f));
_popup.PopupEntity(Loc.GetString("analyzer-artifact-destroy-popup"),
component.AnalyzerEntity.Value, Filter.Pvs(component.AnalyzerEntity.Value), PopupType.Large);
UpdateUserInterface(uid, component);
}
/// <summary>
/// Cancels scans if the artifact changes nodes (is activated) during the scan.
/// </summary>
/// <param name="uid"></param>
/// <param name="component"></param>
/// <param name="args"></param>
private void OnArtifactActivated(EntityUid uid, ActiveScannedArtifactComponent component, ArtifactActivatedEvent args)
{
CancelScan(uid);
}
/// <summary>
/// Checks to make sure that the currently scanned artifact isn't moved off of the scanner
/// </summary>
/// <param name="uid"></param>
/// <param name="component"></param>
/// <param name="args"></param>
private void OnScannedMoved(EntityUid uid, ActiveScannedArtifactComponent component, ref MoveEvent args)
{
if (!TryComp<ArtifactAnalyzerComponent>(component.Scanner, out var analyzer))
return;
if (analyzer.Contacts.Contains(uid))
return;
CancelScan(uid, component, analyzer);
}
/// <summary>
/// Stops the current scan
/// </summary>
/// <param name="artifact">The artifact being scanned</param>
/// <param name="component"></param>
/// <param name="analyzer">The artifact analyzer component</param>
[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>
/// <param name="uid">The analyzer that is scanning</param>
/// <param name="component"></param>
/// <param name="active"></param>
[PublicAPI]
public void FinishScan(EntityUid uid, ArtifactAnalyzerComponent? component = null, ActiveArtifactAnalyzerComponent? active = null)
{
if (!Resolve(uid, ref component, ref active))
return;
_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);
}
private void OnRefreshParts(EntityUid uid, ArtifactAnalyzerComponent component, RefreshPartsEvent args)
{
var analysisRating = args.PartRatings[component.MachinePartAnalysisDuration];
component.AnalysisDurationMulitplier = MathF.Pow(component.PartRatingAnalysisDurationMultiplier, analysisRating - 1);
}
private void OnUpgradeExamine(EntityUid uid, ArtifactAnalyzerComponent component, UpgradeExamineEvent args)
{
args.AddPercentageUpgrade("analyzer-artifact-component-upgrade-analysis", component.AnalysisDurationMulitplier);
}
private void OnCollide(EntityUid uid, ArtifactAnalyzerComponent component, ref StartCollideEvent args)
{
var otherEnt = args.OtherFixture.Body.Owner;
if (!HasComp<ArtifactComponent>(otherEnt))
return;
component.Contacts.Add(otherEnt);
if (component.Console != null)
UpdateUserInterface(component.Console.Value);
}
private void OnEndCollide(EntityUid uid, ArtifactAnalyzerComponent component, ref EndCollideEvent args)
{
var otherEnt = args.OtherFixture.Body.Owner;
if (!HasComp<ArtifactComponent>(otherEnt))
return;
component.Contacts.Remove(otherEnt);
if (component.Console != null)
UpdateUserInterface(component.Console.Value);
}
private void OnAnalyzeStart(EntityUid uid, ActiveArtifactAnalyzerComponent component, ComponentStartup args)
{
if (TryComp<ApcPowerReceiverComponent>(uid, out var powa))
powa.NeedsPower = true;
if (TryComp<AmbientSoundComponent>(uid, out var ambientSound))
{
ambientSound.Enabled = true;
Dirty(ambientSound);
}
}
private void OnAnalyzeEnd(EntityUid uid, ActiveArtifactAnalyzerComponent component, ComponentShutdown args)
{
if (TryComp<ApcPowerReceiverComponent>(uid, out var powa))
powa.NeedsPower = false;
if (TryComp<AmbientSoundComponent>(uid, out var ambientSound))
{
ambientSound.Enabled = false;
Dirty(ambientSound);
}
}
private void OnPowerChanged(EntityUid uid, ActiveArtifactAnalyzerComponent component, ref PowerChangedEvent args)
{
if (!args.Powered)
CancelScan(component.Artifact);
}
}

View File

@@ -1,16 +1,11 @@
using Content.Server.Cargo.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts.Equipment.Components;
using Content.Server.Xenoarchaeology.Equipment.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts;
using Robust.Shared.Containers;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Equipment.Systems;
namespace Content.Server.Xenoarchaeology.Equipment.Systems;
public sealed class SuppressArtifactContainerSystem : EntitySystem
{
/// <summary>
/// Artifacts go from 2k to 4k, 1.5k net profit (considering the container price
/// </summary>
public const double ContainedArtifactModifier = 2;
public override void Initialize()
{
base.Initialize();
@@ -24,11 +19,6 @@ public sealed class SuppressArtifactContainerSystem : EntitySystem
return;
artifact.IsSuppressed = true;
if (TryComp<StaticPriceComponent>(args.Entity, out var price))
{
price.Price *= ContainedArtifactModifier;
}
}
private void OnRemoved(EntityUid uid, SuppressArtifactContainerComponent component, EntRemovedFromContainerMessage args)
@@ -37,10 +27,5 @@ public sealed class SuppressArtifactContainerSystem : EntitySystem
return;
artifact.IsSuppressed = false;
if (TryComp<StaticPriceComponent>(args.Entity, out var price))
{
price.Price /= ContainedArtifactModifier;
}
}
}

View File

@@ -1,39 +1,143 @@
using Content.Shared.Xenoarchaeology.XenoArtifacts;
using Robust.Shared.Serialization.TypeSerializers.Implementations;
namespace Content.Server.Xenoarchaeology.XenoArtifacts;
[RegisterComponent]
public sealed class ArtifactComponent : Component
{
/// <summary>
/// Should artifact pick a random trigger on startup?
/// The artifact's node tree.
/// </summary>
[DataField("randomTrigger")]
public bool RandomTrigger = true;
[ViewVariables]
public ArtifactTree? NodeTree;
/// <summary>
/// List of all possible triggers activations.
/// Should be same as components names.
/// The current node the artifact is on.
/// </summary>
[DataField("possibleTriggers")]
public string[] PossibleTriggers = {
"ArtifactInteractionTrigger",
"ArtifactGasTrigger",
"ArtifactHeatTrigger",
"ArtifactElectricityTrigger",
};
[ViewVariables]
public ArtifactNode? CurrentNode;
#region Node Tree Gen
/// <summary>
/// Minimum number of nodes to generate, inclusive
/// </summary>
[DataField("nodesMin")]
public int NodesMin = 3;
/// <summary>
/// Cooldown time between artifact activations (in seconds).
/// Maximum number of nodes to generate, exclusive
/// </summary>
[DataField("timer")]
[DataField("nodesMax")]
public int NodesMax = 9;
#endregion
/// <summary>
/// Cooldown time between artifact activations (in seconds).
/// </summary>
[DataField("timer", customTypeSerializer: typeof(TimespanSerializer))]
[ViewVariables(VVAccess.ReadWrite)]
public double CooldownTime = 10;
public TimeSpan CooldownTime = TimeSpan.FromSeconds(5);
/// <summary>
/// Is this artifact under some suppression device?
/// If true, will ignore all trigger activations attempts.
/// Is this artifact under some suppression device?
/// f true, will ignore all trigger activations attempts.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public bool IsSuppressed;
/// <summary>
/// The last time the artifact was activated.
/// </summary>
[DataField("lastActivationTime", customTypeSerializer: typeof(TimespanSerializer))]
public TimeSpan LastActivationTime;
}
/// <summary>
/// A tree of nodes.
/// </summary>
[DataDefinition]
public sealed class ArtifactTree
{
/// <summary>
/// The first node of the tree
/// </summary>
[ViewVariables]
public ArtifactNode StartNode = default!;
/// <summary>
/// Every node contained in the tree
/// </summary>
[ViewVariables]
public readonly List<ArtifactNode> AllNodes = new();
}
/// <summary>
/// A single "node" of an artifact that contains various data about it.
/// </summary>
[DataDefinition]
public sealed class ArtifactNode : ICloneable
{
/// <summary>
/// A numeric id corresponding to each node. used for display purposes
/// </summary>
[ViewVariables]
public int Id;
/// <summary>
/// how "deep" into the node tree. used for generation and price/value calculations
/// </summary>
[ViewVariables]
public int Depth = 0;
/// <summary>
/// A list of surrounding nodes. Used for tree traversal
/// </summary>
[ViewVariables]
public List<ArtifactNode> Edges = new();
/// <summary>
/// Whether or not the node has been entered
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public bool Discovered = false;
/// <summary>
/// The trigger for the node
/// </summary>
[ViewVariables]
public ArtifactTriggerPrototype Trigger = default!;
/// <summary>
/// Whether or not the node has been triggered
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public bool Triggered = false;
/// <summary>
/// The effect when the node is activated
/// </summary>
[ViewVariables]
public ArtifactEffectPrototype Effect = default!;
/// <summary>
/// Used for storing cumulative information about nodes
/// </summary>
[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

@@ -0,0 +1,198 @@
using System.Linq;
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
using Content.Shared.Xenoarchaeology.XenoArtifacts;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Serialization.Manager;
namespace Content.Server.Xenoarchaeology.XenoArtifacts;
public sealed partial class ArtifactSystem
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IComponentFactory _componentFactory = default!;
[Dependency] private readonly ISerializationManager _serialization = default!;
private const int MaxEdgesPerNode = 3;
/// <summary>
/// Generate an Artifact tree with fully developed nodes.
/// </summary>
/// <param name="tree">The tree being generated.</param>
/// <param name="nodeAmount">The amount of nodes it has.</param>
private void GenerateArtifactNodeTree(ref ArtifactTree tree, int nodeAmount)
{
if (nodeAmount < 1)
{
Logger.Error($"nodeAmount {nodeAmount} is less than 1. Aborting artifact tree generation.");
return;
}
var uninitializedNodes = new List<ArtifactNode> { new() };
tree.StartNode = uninitializedNodes.First(); //the first node
while (uninitializedNodes.Any())
{
GenerateNode(ref uninitializedNodes, ref tree, nodeAmount);
}
}
/// <summary>
/// Generate an individual node on the tree.
/// </summary>
private void GenerateNode(ref List<ArtifactNode> uninitializedNodes, ref ArtifactTree tree, int targetNodeAmount)
{
if (!uninitializedNodes.Any())
return;
var node = uninitializedNodes.First();
uninitializedNodes.Remove(node);
node.Id = _random.Next(0, 10000);
//Generate the connected nodes
var maxEdges = Math.Max(1, targetNodeAmount - tree.AllNodes.Count - uninitializedNodes.Count - 1);
maxEdges = Math.Min(maxEdges, MaxEdgesPerNode);
var minEdges = Math.Clamp(targetNodeAmount - tree.AllNodes.Count - uninitializedNodes.Count - 1, 0, 1);
var edgeAmount = _random.Next(minEdges, maxEdges);
for (var i = 0; i < edgeAmount; i++)
{
var neighbor = new ArtifactNode
{
Depth = node.Depth + 1
};
node.Edges.Add(neighbor);
neighbor.Edges.Add(node);
uninitializedNodes.Add(neighbor);
}
node.Trigger = GetRandomTrigger(ref node);
node.Effect = GetRandomEffect(ref node);
tree.AllNodes.Add(node);
}
//yeah these two functions are near duplicates but i don't
//want to implement an interface or abstract parent
private ArtifactTriggerPrototype GetRandomTrigger(ref ArtifactNode node)
{
var allTriggers = _prototype.EnumeratePrototypes<ArtifactTriggerPrototype>().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);
}
private ArtifactEffectPrototype GetRandomEffect(ref ArtifactNode node)
{
var allEffects = _prototype.EnumeratePrototypes<ArtifactEffectPrototype>().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);
}
/// <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)
{
var weights = new Dictionary<int, float>();
foreach (var d in depths)
{
//TODO: is this equation sus? idk. -emo
// 0.3 / (|current_iterated_depth - our_actual_depth| + 1)^2
var w = 0.3f / MathF.Pow(Math.Abs(d - targetDepth) + 1, 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.CurrentNode != null)
{
ExitNode(uid, component);
}
component.CurrentNode = node;
node.Discovered = true;
var allComponents = node.Effect.Components.Concat(node.Effect.PermanentComponents).Concat(node.Trigger.Components);
foreach (var (name, entry) in allComponents)
{
var reg = _componentFactory.GetRegistration(name);
var comp = (Component) _componentFactory.GetComponent(reg);
comp.Owner = uid;
var temp = (object) comp;
_serialization.Copy(entry.Component, ref temp);
EntityManager.AddComponent(uid, (Component) temp!, true);
}
RaiseLocalEvent(uid, new ArtifactNodeEnteredEvent(component.CurrentNode.Id));
}
/// <summary>
/// Exit a node: remove the relevant components.
/// </summary>
private void ExitNode(EntityUid uid, ArtifactComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
var node = component.CurrentNode;
if (node == null)
return;
foreach (var name in node.Effect.Components.Keys.Concat(node.Trigger.Components.Keys))
{
var comp = _componentFactory.GetRegistration(name);
EntityManager.RemoveComponentDeferred(uid, comp.Type);
}
component.CurrentNode = null;
}
}

View File

@@ -1,50 +1,122 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Cargo.Systems;
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
using JetBrains.Annotations;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.Xenoarchaeology.XenoArtifacts;
public sealed class ArtifactSystem : EntitySystem
public sealed partial class ArtifactSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IComponentFactory _componentFactory = default!;
private const int PricePerNode = 500;
private const int PointsPerNode = 5000;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ArtifactComponent, MapInitEvent>(OnInit);
SubscribeLocalEvent<ArtifactComponent, PriceCalculationEvent>(GetPrice);
}
private void OnInit(EntityUid uid, ArtifactComponent component, MapInitEvent args)
{
if (component.RandomTrigger)
{
AddRandomTrigger(uid, component);
}
RandomizeArtifact(component);
}
private void AddRandomTrigger(EntityUid uid, ArtifactComponent? component = null)
/// <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)
{
if (!Resolve(uid, ref component))
if (component.NodeTree == null)
return;
var triggerName = _random.Pick(component.PossibleTriggers);
var trigger = (Component) _componentFactory.GetComponent(triggerName);
trigger.Owner = uid;
var price = component.NodeTree.AllNodes.Sum(GetNodePrice);
if (EntityManager.HasComponent(uid, trigger.GetType()))
{
Logger.Error($"Attempted to add a random artifact trigger ({triggerName}) to an entity ({ToPrettyString(uid)}), but it already has the trigger");
return;
}
// 25% bonus for fully exploring every node.
var fullyExploredBonus = component.NodeTree.AllNodes.Any(x => !x.Triggered) ? 1 : 1.25f;
EntityManager.AddComponent(uid, trigger);
RaiseLocalEvent(uid, new RandomizeTriggerEvent(), true);
args.Price =+ price * fullyExploredBonus;
}
public bool TryActivateArtifact(EntityUid uid, EntityUid? user = null,
ArtifactComponent? component = null)
private float GetNodePrice(ArtifactNode node)
{
if (!node.Discovered) //no money for undiscovered nodes.
return 0;
//quarter price if not triggered
var priceMultiplier = node.Triggered ? 1f : 0.25f;
//the danger is the average of node depth, effect danger, and trigger danger.
var nodeDanger = (node.Depth + node.Effect.TargetDepth + node.Trigger.TargetDepth) / 3;
var price = MathF.Pow(2f, nodeDanger) * PricePerNode * priceMultiplier;
return price;
}
/// <summary>
/// Calculates how many research points the artifact is worht
/// </summary>
/// <remarks>
/// Rebalance this shit at some point. Definitely OP.
/// </remarks>
public int GetResearchPointValue(EntityUid uid, ArtifactComponent? component = null)
{
if (!Resolve(uid, ref component) || component.NodeTree == null)
return 0;
var sumValue = component.NodeTree.AllNodes.Sum(GetNodePointValue);
var fullyExploredBonus = component.NodeTree.AllNodes.Any(x => !x.Triggered) ? 1 : 1.25f;
var pointValue = (int) (sumValue * fullyExploredBonus);
return pointValue;
}
private float GetNodePointValue(ArtifactNode node)
{
if (!node.Discovered)
return 0;
var valueDeduction = !node.Triggered ? 0.5f : 1;
var nodeDanger = (node.Depth + node.Effect.TargetDepth + node.Trigger.TargetDepth) / 3;
return (nodeDanger+1) * PointsPerNode * valueDeduction;
}
/// <summary>
/// Randomize a given artifact.
/// </summary>
[PublicAPI]
public void RandomizeArtifact(ArtifactComponent component)
{
var nodeAmount = _random.Next(component.NodesMin, component.NodesMax);
component.NodeTree = new ArtifactTree();
GenerateArtifactNodeTree(ref component.NodeTree, nodeAmount);
EnterNode(component.Owner, ref component.NodeTree.StartNode, component);
}
/// <summary>
/// Tries to activate the artifact
/// </summary>
/// <param name="uid"></param>
/// <param name="user"></param>
/// <param name="component"></param>
/// <returns></returns>
public bool TryActivateArtifact(EntityUid uid, EntityUid? user = null, ArtifactComponent? component = null)
{
if (!Resolve(uid, ref component))
return false;
@@ -55,25 +127,85 @@ public sealed class ArtifactSystem : EntitySystem
// check if artifact isn't under cooldown
var timeDif = _gameTiming.CurTime - component.LastActivationTime;
if (timeDif.TotalSeconds < component.CooldownTime)
if (timeDif < component.CooldownTime)
return false;
ForceActivateArtifact(uid, user, component);
return true;
}
public void ForceActivateArtifact(EntityUid uid, EntityUid? user = null,
ArtifactComponent? component = null)
/// <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.CurrentNode == null)
return;
component.LastActivationTime = _gameTiming.CurTime;
var ev = new ArtifactActivatedEvent()
var ev = new ArtifactActivatedEvent
{
Activator = user
};
RaiseLocalEvent(uid, ev, true);
component.CurrentNode.Triggered = true;
if (component.CurrentNode.Edges.Any())
{
var newNode = _random.Pick(component.CurrentNode.Edges);
EnterNode(uid, ref 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.CurrentNode == null)
return false;
if (component.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.CurrentNode == null)
return;
component.CurrentNode.NodeData[key] = value;
}
}

View File

@@ -0,0 +1,44 @@
using Content.Shared.Damage;
using Content.Shared.Whitelist;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
/// <summary>
/// When activated, damages nearby entities.
/// </summary>
[RegisterComponent]
public sealed class DamageNearbyArtifactComponent : Component
{
/// <summary>
/// The radius of entities that will be affected
/// </summary>
[DataField("radius")]
public float Radius = 3f;
/// <summary>
/// A whitelist for filtering certain damage.
/// </summary>
/// <remarks>
/// TODO: The component portion, since it uses an array, does not work currently.
/// </remarks>
[DataField("whitelist")]
public EntityWhitelist? Whitelist;
/// <summary>
/// The damage that is applied
/// </summary>
[DataField("damage", required: true)]
public DamageSpecifier Damage = default!;
/// <summary>
/// The chance that damage is applied to each individual entity
/// </summary>
[DataField("damageChance")]
public float DamageChance = 1f;
/// <summary>
/// Whether or not this should ignore resistances for the damage
/// </summary>
[DataField("ignoreResistances")]
public bool IgnoreResistances;
}

View File

@@ -1,4 +1,5 @@
using Content.Shared.Disease;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
/// <summary>
@@ -8,10 +9,15 @@ namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
public sealed class DiseaseArtifactComponent : Component
{
/// <summary>
/// Disease the artifact will spawn
/// If empty, picks a random one from its list
/// The diseases that the artifact can use.
/// </summary>
[DataField("diseasePrototype", customTypeSerializer: typeof(PrototypeIdListSerializer<DiseasePrototype>))]
public List<string> DiseasePrototypes = new();
/// <summary>
/// Disease the artifact will spawn
/// Picks a random one from its list
/// </summary>
[DataField("disease")]
[ViewVariables(VVAccess.ReadWrite)]
public DiseasePrototype? SpawnDisease;
@@ -19,7 +25,6 @@ public sealed class DiseaseArtifactComponent : Component
/// How far away it will check for people
/// If empty, picks a random one from its list
/// </summary>
[DataField("range")]
[ViewVariables(VVAccess.ReadWrite)]
[DataField("range"), ViewVariables(VVAccess.ReadWrite)]
public float Range = 5f;
}

View File

@@ -0,0 +1,54 @@
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]
public sealed 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>
[ViewVariables(VVAccess.ReadWrite)]
public string? SelectedReagent;
/// <summary>
/// How long does the foam last?
/// </summary>
[DataField("duration")]
public float Duration = 10;
/// <summary>
/// How much reagent is in the foam?
/// </summary>
[DataField("reagentAmount")]
public float ReagentAmount = 100;
/// <summary>
/// Minimum radius of foam spawned
/// </summary>
[DataField("minFoamAmount")]
public int MinFoamAmount = 2;
/// <summary>
/// Maximum radius of foam spawned
/// </summary>
[DataField("maxFoamAmount")]
public int MaxFoamAmount = 6;
/// <summary>
/// How long it takes for each tile of foam to spawn
/// </summary>
[DataField("spreadDuration")]
public float SpreadDuration = 1;
}

View File

@@ -20,7 +20,7 @@ public sealed class GasArtifactComponent : Component
/// List of possible activation gases to pick on startup.
/// </summary>
[DataField("possibleGas")]
public Gas[] PossibleGases =
public List<Gas> PossibleGases = new()
{
Gas.Oxygen,
Gas.Plasma,

View File

@@ -0,0 +1,14 @@
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 class KnockArtifactComponent : Component
{
/// <summary>
/// The range of the spell
/// </summary>
[DataField("knockRange")]
public float KnockRange = 4f;
}

View File

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

View File

@@ -1,17 +0,0 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
/// <summary>
/// Spawn RadiationPulse when artifact activated.
/// </summary>
[RegisterComponent]
public sealed class RadiateArtifactComponent : Component
{
/// <summary>
/// Radiation pulse prototype to spawn.
/// </summary>
[DataField("pulsePrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
public string PulsePrototype = "RadiationPulse";
}

View File

@@ -0,0 +1,15 @@
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
/// <summary>
/// When activated, will teleport the artifact
/// to a random position within a certain radius
/// </summary>
[RegisterComponent]
public sealed class RandomTeleportArtifactComponent : Component
{
/// <summary>
/// The max distance that the artifact will teleport.
/// </summary>
[DataField("range")]
public float Range = 7.5f;
}

View File

@@ -0,0 +1,12 @@
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 class ShuffleArtifactComponent : Component
{
[DataField("radius")]
public float Radius = 7.5f;
}

View File

@@ -11,21 +11,35 @@ namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
[RegisterComponent]
public sealed class SpawnArtifactComponent : Component
{
[DataField("random")]
public bool RandomPrototype = true;
/// <summary>
/// The list of possible prototypes to spawn that it picks from.
/// </summary>
[DataField("possiblePrototypes", customTypeSerializer:typeof(PrototypeIdListSerializer<EntityPrototype>))]
public List<string> PossiblePrototypes = new();
/// <summary>
/// The prototype it selected to spawn.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("prototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
public string? Prototype;
/// <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 = 20;
public int SpawnsCount = 0;
/// <summary>
/// Whether or not the artifact spawns the same entity every time
/// or picks through the list each time.
/// </summary>
[DataField("consistentSpawn")]
public bool ConsistentSpawn = true;
}

View File

@@ -13,7 +13,7 @@ public sealed class TelepathicArtifactComponent : Component
/// </summary>
[DataField("messages")]
[ViewVariables(VVAccess.ReadWrite)]
public string[] Messages = default!;
public List<string> Messages = default!;
/// <summary>
/// Loc string ids of telepathic messages (spooky version).
@@ -21,7 +21,7 @@ public sealed class TelepathicArtifactComponent : Component
/// </summary>
[DataField("drastic")]
[ViewVariables(VVAccess.ReadWrite)]
public string[] DrasticMessages = default!;
public List<string>? DrasticMessages;
/// <summary>
/// Probability to pick drastic version of message.

View File

@@ -8,7 +8,7 @@ namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
[RegisterComponent]
public sealed class TemperatureArtifactComponent : Component
{
[DataField("targetTemp")]
[DataField("targetTemp"), ViewVariables(VVAccess.ReadWrite)]
public float TargetTemperature = Atmospherics.T0C;
[DataField("spawnTemp")]
@@ -21,6 +21,6 @@ public sealed class TemperatureArtifactComponent : Component
/// If true, artifact will heat/cool not only its current tile, but surrounding tiles too.
/// This will change room temperature much faster.
/// </summary>
[DataField("effectAdjacent")]
public bool EffectAdjacentTiles = true;
[DataField("affectAdjacent")]
public bool AffectAdjacentTiles = true;
}

View File

@@ -0,0 +1,27 @@
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
/// <summary>
/// Throws all nearby entities backwards.
/// Also pries nearby tiles.
/// </summary>
[RegisterComponent]
public sealed class ThrowArtifactComponent : Component
{
/// <summary>
/// How close do you have to be to get yeeted?
/// </summary>
[DataField("range")]
public float Range = 2f;
/// <summary>
/// How likely is it that an individual tile will get pried?
/// </summary>
[DataField("tilePryChance")]
public float TilePryChance = 0.5f;
/// <summary>
/// How strongly does stuff get thrown?
/// </summary>
[DataField("throwStrength"), ViewVariables(VVAccess.ReadWrite)]
public float ThrowStrength = 5f;
}

View File

@@ -0,0 +1,36 @@
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
using Content.Shared.Damage;
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!;
/// <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 (component.Whitelist != null && !component.Whitelist.IsValid(ent))
continue;
if (!_random.Prob(component.DamageChance))
return;
_damageable.TryChangeDamage(ent, component.Damage, component.IgnoreResistances);
}
}
}

View File

@@ -1,9 +1,9 @@
using System.Linq;
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
using Content.Shared.Disease;
using Content.Server.Disease;
using Content.Server.Disease.Components;
using Robust.Shared.Random;
using Robust.Shared.Prototypes;
using Content.Shared.Interaction;
@@ -15,38 +15,25 @@ namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems
public sealed class DiseaseArtifactSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly DiseaseSystem _disease = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
// TODO: YAML Serializer won't catch this.
[ViewVariables(VVAccess.ReadWrite)]
public readonly IReadOnlyList<string> ArtifactDiseases = new[]
{
"VanAusdallsRobovirus",
"OwOnavirus",
"BleedersBite",
"Ultragigacancer",
"MemeticAmirmir",
"TongueTwister",
"AMIV"
};
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DiseaseArtifactComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<DiseaseArtifactComponent, ArtifactNodeEnteredEvent>(OnNodeEntered);
SubscribeLocalEvent<DiseaseArtifactComponent, ArtifactActivatedEvent>(OnActivate);
}
/// <summary>
/// Makes sure this artifact is assigned a disease
/// </summary>
private void OnMapInit(EntityUid uid, DiseaseArtifactComponent component, MapInitEvent args)
private void OnNodeEntered(EntityUid uid, DiseaseArtifactComponent component, ArtifactNodeEnteredEvent args)
{
if (component.SpawnDisease != null || ArtifactDiseases.Count == 0) return;
var diseaseName = _random.Pick(ArtifactDiseases);
if (component.SpawnDisease != null || !component.DiseasePrototypes.Any())
return;
var diseaseName = component.DiseasePrototypes[args.RandomSeed % component.DiseasePrototypes.Count];
if (!_prototypeManager.TryIndex<DiseasePrototype>(diseaseName, out var disease))
{

View File

@@ -0,0 +1,42 @@
using System.Linq;
using Content.Server.Chemistry.ReactionEffects;
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!;
/// <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);
sol.AddReagent(component.SelectedReagent, component.ReagentAmount);
FoamAreaReactionEffect.SpawnFoam("Foam", xform.Coordinates, sol,
_random.Next(component.MinFoamAmount, component.MaxFoamAmount), component.Duration,
component.SpreadDuration, component.SpreadDuration, entityManager: EntityManager);
}
}

View File

@@ -2,33 +2,32 @@
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 GasArtifactSystem : EntitySystem
{
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly IRobustRandom _random = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GasArtifactComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<GasArtifactComponent, ArtifactNodeEnteredEvent>(OnNodeEntered);
SubscribeLocalEvent<GasArtifactComponent, ArtifactActivatedEvent>(OnActivate);
}
private void OnMapInit(EntityUid uid, GasArtifactComponent component, MapInitEvent args)
private void OnNodeEntered(EntityUid uid, GasArtifactComponent component, ArtifactNodeEnteredEvent args)
{
if (component.SpawnGas == null && component.PossibleGases.Length != 0)
if (component.SpawnGas == null && component.PossibleGases.Count != 0)
{
var gas = _random.Pick(component.PossibleGases);
var gas = component.PossibleGases[args.RandomSeed % component.PossibleGases.Count];
component.SpawnGas = gas;
}
if (component.SpawnTemperature == null)
{
var temp = _random.NextFloat(component.MinRandomTemperature, component.MaxRandomTemperature);
var temp = args.RandomSeed % component.MaxRandomTemperature - component.MinRandomTemperature +
component.MinRandomTemperature;
component.SpawnTemperature = temp;
}
}
@@ -38,8 +37,6 @@ public sealed class GasArtifactSystem : EntitySystem
if (component.SpawnGas == null || component.SpawnTemperature == null)
return;
var transform = Transform(uid);
var environment = _atmosphereSystem.GetContainingMixture(uid, false, true);
if (environment == null)
return;

View File

@@ -0,0 +1,24 @@
using Content.Server.Magic.Events;
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts.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

@@ -0,0 +1,38 @@
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,20 +0,0 @@
using Content.Server.Radiation;
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
public sealed class RadiateArtifactSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RadiateArtifactComponent, ArtifactActivatedEvent>(OnActivate);
}
private void OnActivate(EntityUid uid, RadiateArtifactComponent component, ArtifactActivatedEvent args)
{
var transform = Transform(uid);
EntityManager.SpawnEntity(component.PulsePrototype, transform.Coordinates);
}
}

View File

@@ -0,0 +1,30 @@
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
using Content.Shared.Popups;
using Robust.Shared.Player;
using Robust.Shared.Random;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
/// <summary>
/// This handles...
/// </summary>
public sealed class RandomTeleportArtifactSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<RandomTeleportArtifactComponent, ArtifactActivatedEvent>(OnActivate);
}
private void OnActivate(EntityUid uid, RandomTeleportArtifactComponent component, ArtifactActivatedEvent args)
{
var xform = Transform(uid);
_popup.PopupCoordinates(Loc.GetString("blink-artifact-popup"), xform.Coordinates, Filter.Pvs(uid), PopupType.Medium);
xform.Coordinates = xform.Coordinates.Offset(_random.NextVector2(component.Range));
}
}

View File

@@ -0,0 +1,43 @@
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
using Content.Shared.MobState.Components;
using Robust.Shared.Map;
using Robust.Shared.Random;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
public sealed class ShuffleArtifactSystem : EntitySystem
{
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly IRobustRandom _random = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<ShuffleArtifactComponent, ArtifactActivatedEvent>(OnActivated);
}
private void OnActivated(EntityUid uid, ShuffleArtifactComponent component, ArtifactActivatedEvent args)
{
var mobState = GetEntityQuery<MobStateComponent>();
List<EntityCoordinates> allCoords = new();
List<TransformComponent> toShuffle = new();
foreach (var ent in _lookup.GetEntitiesInRange(uid, component.Radius, LookupFlags.Dynamic | LookupFlags.Sundries))
{
if (!mobState.HasComponent(ent))
continue;
var xform = Transform(ent);
toShuffle.Add(xform);
allCoords.Add(xform.Coordinates);
}
foreach (var xform in toShuffle)
{
xform.Coordinates = _random.PickAndTake(allCoords);
}
}
}

View File

@@ -9,25 +9,40 @@ public sealed class SpawnArtifactSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly ArtifactSystem _artifact = default!;
public const string NodeDataSpawnAmount = "nodeDataSpawnAmount";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SpawnArtifactComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<SpawnArtifactComponent, ArtifactNodeEnteredEvent>(OnNodeEntered);
SubscribeLocalEvent<SpawnArtifactComponent, ArtifactActivatedEvent>(OnActivate);
}
private void OnMapInit(EntityUid uid, SpawnArtifactComponent component, MapInitEvent args)
private void OnNodeEntered(EntityUid uid, SpawnArtifactComponent component, ArtifactNodeEnteredEvent args)
{
ChooseRandomPrototype(uid, component);
if (component.PossiblePrototypes.Count == 0)
return;
var proto = component.PossiblePrototypes[args.RandomSeed % component.PossiblePrototypes.Count];
component.Prototype = proto;
}
private void OnActivate(EntityUid uid, SpawnArtifactComponent component, ArtifactActivatedEvent args)
{
if (component.Prototype == null)
return;
if (component.SpawnsCount >= component.MaxSpawns)
if (!_artifact.TryGetNodeData(uid, NodeDataSpawnAmount, out int amount))
amount = 0;
if (amount >= component.MaxSpawns)
return;
var toSpawn = component.Prototype;
if (!component.ConsistentSpawn)
toSpawn = _random.Pick(component.PossiblePrototypes);
// select spawn position near artifact
var artifactCord = Transform(uid).Coordinates;
var dx = _random.NextFloat(-component.Range, component.Range);
@@ -35,25 +50,11 @@ public sealed class SpawnArtifactSystem : EntitySystem
var spawnCord = artifactCord.Offset(new Vector2(dx, dy));
// spawn entity
var spawned = EntityManager.SpawnEntity(component.Prototype, spawnCord);
component.SpawnsCount++;
var spawned = EntityManager.SpawnEntity(toSpawn, spawnCord);
_artifact.SetNodeData(uid, NodeDataSpawnAmount, amount+1);
// if there is an user - try to put spawned item in their hands
// doesn't work for spawners
_handsSystem.PickupOrDrop(args.Activator, spawned);
}
private void ChooseRandomPrototype(EntityUid uid, SpawnArtifactComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
if (!component.RandomPrototype)
return;
if (component.PossiblePrototypes.Count == 0)
return;
var proto = _random.Pick(component.PossiblePrototypes);
component.Prototype = proto;
}
}

View File

@@ -29,8 +29,15 @@ public sealed class TelepathicArtifactSystem : EntitySystem
continue;
// roll if msg should be usual or drastic
var isDrastic = _random.NextFloat() <= component.DrasticMessageProb;
var msgArr = isDrastic ? component.DrasticMessages : component.Messages;
List<string> msgArr;
if (_random.NextFloat() <= component.DrasticMessageProb && component.DrasticMessages != null)
{
msgArr = component.DrasticMessages;
}
else
{
msgArr = component.Messages;
}
// pick a random message
var msgId = _random.Pick(msgArr);

View File

@@ -26,7 +26,7 @@ public sealed class TemperatureArtifactSystem : EntitySystem
return;
UpdateTileTemperature(component, center);
if (component.EffectAdjacentTiles && transform.GridUid != null)
if (component.AffectAdjacentTiles && transform.GridUid != null)
{
var adjacent = _atmosphereSystem.GetAdjacentTileMixtures(transform.GridUid.Value,
_transformSystem.GetGridOrMapTilePosition(uid, transform), excite: true);

View File

@@ -0,0 +1,49 @@
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
using Content.Shared.Maps;
using Content.Shared.Throwing;
using Robust.Shared.Map;
using Robust.Shared.Random;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
public sealed class ThrowArtifactSystem : EntitySystem
{
[Dependency] private readonly IMapManager _map = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly ThrowingSystem _throwing = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<ThrowArtifactComponent, ArtifactActivatedEvent>(OnActivated);
}
private void OnActivated(EntityUid uid, ThrowArtifactComponent component, ArtifactActivatedEvent args)
{
var xform = Transform(uid);
if (_map.TryGetGrid(xform.GridUid, out var grid))
{
var tiles = grid.GetTilesIntersecting(
Box2.CenteredAround(xform.WorldPosition, (component.Range*2, component.Range)));
foreach (var tile in tiles)
{
if (!_random.Prob(component.TilePryChance))
continue;
tile.PryTile();
}
}
var lookup = _lookup.GetEntitiesInRange(uid, component.Range, LookupFlags.Dynamic | LookupFlags.Sundries);
foreach (var ent in lookup)
{
var tempXform = Transform(ent);
var foo = tempXform.MapPosition.Position - xform.MapPosition.Position;
_throwing.TryThrow(ent, foo*2, component.ThrowStrength, uid, 0);
}
}
}

View File

@@ -12,3 +12,20 @@ public sealed class ArtifactActivatedEvent : EntityEventArgs
/// </summary>
public EntityUid? Activator;
}
/// <summary>
/// Force to randomize artifact triggers.
/// </summary>
public sealed class ArtifactNodeEnteredEvent : EntityEventArgs
{
/// <summary>
/// An entity-specific seed that can be used to
/// generate random values.
/// </summary>
public readonly int RandomSeed;
public ArtifactNodeEnteredEvent(int randomSeed)
{
RandomSeed = randomSeed;
}
}

View File

@@ -1,9 +0,0 @@
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Events;
/// <summary>
/// Force to randomize artifact triggers.
/// </summary>
public sealed class RandomizeTriggerEvent : EntityEventArgs
{
}

View File

@@ -1,5 +1,6 @@
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
using Content.Shared.Xenoarchaeology.XenoArtifacts;
using Robust.Server.GameObjects;
using Robust.Shared.Random;
using Robust.Shared.Timing;
@@ -9,6 +10,7 @@ public sealed class RandomArtifactSpriteSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IGameTiming _time = default!;
[Dependency] private readonly AppearanceSystem _appearance = default!;
public override void Initialize()
{
@@ -29,7 +31,7 @@ public sealed class RandomArtifactSpriteSystem : EntitySystem
var timeDif = _time.CurTime - component.ActivationStart.Value;
if (timeDif.Seconds >= component.ActivationTime)
{
appearance.SetData(SharedArtifactsVisuals.IsActivated, false);
_appearance.SetData(appearance.Owner, SharedArtifactsVisuals.IsActivated, false, appearance);
component.ActivationStart = null;
}
}
@@ -37,19 +39,13 @@ public sealed class RandomArtifactSpriteSystem : EntitySystem
private void OnMapInit(EntityUid uid, RandomArtifactSpriteComponent component, MapInitEvent args)
{
if (!TryComp(uid, out AppearanceComponent? appearance))
return;
var randomSprite = _random.Next(component.MinSprite, component.MaxSprite + 1);
appearance.SetData(SharedArtifactsVisuals.SpriteIndex, randomSprite);
_appearance.SetData(uid, SharedArtifactsVisuals.SpriteIndex, randomSprite);
}
private void OnActivated(EntityUid uid, RandomArtifactSpriteComponent component, ArtifactActivatedEvent args)
{
if (!TryComp(uid, out AppearanceComponent? appearance))
return;
appearance.SetData(SharedArtifactsVisuals.IsActivated, true);
_appearance.SetData(uid, SharedArtifactsVisuals.IsActivated, true);
component.ActivationStart = _time.CurTime;
}
}

View File

@@ -0,0 +1,13 @@
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components;
/// <summary>
/// Triggers when an artifact is anchored
/// </summary>
/// <remarks>
/// Not every trigger can be a winner
/// </remarks>
[RegisterComponent]
public sealed class ArtifactAnchorTriggerComponent : Component
{
}

View File

@@ -0,0 +1,29 @@
using Content.Shared.Damage.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components;
/// <summary>
/// Triggers when a certain threshold of damage of certain types is reached
/// </summary>
[RegisterComponent]
public sealed class ArtifactDamageTriggerComponent : Component
{
/// <summary>
/// What damage types are accumulated for the trigger?
/// </summary>
[DataField("damageTypes", customTypeSerializer: typeof(PrototypeIdListSerializer<DamageTypePrototype>))]
public List<string>? DamageTypes;
/// <summary>
/// What threshold has to be reached before it is activated?
/// </summary>
[DataField("damageThreshold", required: true)]
public float DamageThreshold;
/// <summary>
/// How much damage has been accumulated on the artifact so far
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public float AccumulatedDamage = 0;
}

View File

@@ -0,0 +1,14 @@
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components;
/// <summary>
/// Triggers when a nearby entity dies
/// </summary>
[RegisterComponent]
public sealed class ArtifactDeathTriggerComponent : Component
{
/// <summary>
/// How close to the death the artifact has to be for it to trigger.
/// </summary>
[DataField("range")]
public float Range = 15f;
}

View File

@@ -0,0 +1,10 @@
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components;
/// <summary>
/// Triggers when the artifact is examined.
/// </summary>
[RegisterComponent]
public sealed class ArtifactExamineTriggerComponent : Component
{
}

View File

@@ -12,7 +12,7 @@ public sealed class ArtifactGasTriggerComponent : Component
/// List of possible activation gases to pick on startup.
/// </summary>
[DataField("possibleGas")]
public Gas[] PossibleGases =
public List<Gas> PossibleGases = new()
{
Gas.Oxygen,
Gas.Plasma,

View File

@@ -0,0 +1,14 @@
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components;
/// <summary>
/// Triggers when the salvage magnet is activated
/// </summary>
[RegisterComponent]
public sealed class ArtifactMagnetTriggerComponent : Component
{
/// <summary>
/// how close to the magnet do you have to be?
/// </summary>
[DataField("range")]
public float Range = 40f;
}

View File

@@ -0,0 +1,14 @@
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components;
/// <summary>
/// Triggers when an instrument is played nearby
/// </summary>
[RegisterComponent]
public sealed class ArtifactMusicTriggerComponent : Component
{
/// <summary>
/// how close does the artifact have to be to the instrument to activate
/// </summary>
[DataField("range")]
public float Range = 5;
}

View File

@@ -0,0 +1,20 @@
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components;
/// <summary>
/// Triggers when a certain pressure threshold is hit
/// </summary>
[RegisterComponent]
public sealed class ArtifactPressureTriggerComponent : Component
{
/// <summary>
/// The lower-end pressure threshold
/// </summary>
[DataField("minPressureThreshold")]
public float? MinPressureThreshold;
/// <summary>
/// The higher-end pressure threshold
/// </summary>
[DataField("maxPressureThreshold")]
public float? MaxPressureThreshold;
}

View File

@@ -0,0 +1,22 @@
using Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Systems;
public sealed class ArtifactAnchorTriggerSystem : EntitySystem
{
[Dependency] private readonly ArtifactSystem _artifact = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<ArtifactAnchorTriggerComponent, AnchorStateChangedEvent>(OnAnchorStateChanged);
}
private void OnAnchorStateChanged(EntityUid uid, ArtifactAnchorTriggerComponent component, ref AnchorStateChangedEvent args)
{
if (args.Detaching)
return;
_artifact.TryActivateArtifact(uid);
}
}

View File

@@ -0,0 +1,35 @@
using Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components;
using Content.Shared.Damage;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Systems;
public sealed class ArtifactDamageTriggerSystem : EntitySystem
{
[Dependency] private readonly ArtifactSystem _artifact = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<ArtifactDamageTriggerComponent, DamageChangedEvent>(OnDamageChanged);
}
private void OnDamageChanged(EntityUid uid, ArtifactDamageTriggerComponent component, DamageChangedEvent args)
{
if (!args.DamageIncreased)
return;
if (args.DamageDelta == null)
return;
foreach (var (type, amount) in args.DamageDelta.DamageDict)
{
if (component.DamageTypes != null && !component.DamageTypes.Contains(type))
continue;
component.AccumulatedDamage += (float) amount;
}
if (component.AccumulatedDamage >= component.DamageThreshold)
_artifact.TryActivateArtifact(uid, args.Origin);
}
}

View File

@@ -0,0 +1,40 @@
using Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components;
using Content.Shared.MobState;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Systems;
public sealed class ArtifactDeathTriggerSystem : EntitySystem
{
[Dependency] private readonly ArtifactSystem _artifact = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<MobStateChangedEvent>(OnMobStateChanged);
}
private void OnMobStateChanged(MobStateChangedEvent ev)
{
if (ev.CurrentMobState != DamageState.Dead)
return;
var deathXform = Transform(ev.Entity);
var toActivate = new List<ArtifactDeathTriggerComponent>();
foreach (var (trigger, xform) in EntityQuery<ArtifactDeathTriggerComponent, TransformComponent>())
{
if (!deathXform.Coordinates.TryDistance(EntityManager, xform.Coordinates, out var distance))
continue;
if (distance > trigger.Range)
continue;
toActivate.Add(trigger);
}
foreach (var a in toActivate)
{
_artifact.TryActivateArtifact(a.Owner);
}
}
}

View File

@@ -20,13 +20,18 @@ public sealed class ArtifactElectricityTriggerSystem : EntitySystem
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityManager.EntityQuery<ArtifactElectricityTriggerComponent, PowerConsumerComponent, ArtifactComponent>();
foreach (var (trigger, power, artifact) in query)
List<ArtifactComponent> toUpdate = new();
foreach (var (trigger, power, artifact) in EntityQuery<ArtifactElectricityTriggerComponent, PowerConsumerComponent, ArtifactComponent>())
{
if (power.ReceivedPower <= trigger.MinPower)
continue;
_artifactSystem.TryActivateArtifact(trigger.Owner, component: artifact);
toUpdate.Add(artifact);
}
foreach (var a in toUpdate)
{
_artifactSystem.TryActivateArtifact(a.Owner, null, a);
}
}

View File

@@ -0,0 +1,20 @@
using Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components;
using Content.Shared.Examine;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Systems;
public sealed class ArtifactExamineTriggerSystem : EntitySystem
{
[Dependency] private readonly ArtifactSystem _artifact = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<ArtifactExamineTriggerComponent, ExaminedEvent>(OnExamine);
}
private void OnExamine(EntityUid uid, ArtifactExamineTriggerComponent component, ExaminedEvent args)
{
_artifact.TryActivateArtifact(uid);
}
}

View File

@@ -2,13 +2,11 @@ using Content.Server.Atmos.EntitySystems;
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
using Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Random;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Systems;
public sealed class ArtifactGasTriggerSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly ArtifactSystem _artifactSystem = default!;
[Dependency] private readonly TransformSystem _transformSystem = default!;
@@ -16,23 +14,24 @@ public sealed class ArtifactGasTriggerSystem : EntitySystem
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ArtifactGasTriggerComponent, RandomizeTriggerEvent>(OnRandomizeTrigger);
SubscribeLocalEvent<ArtifactGasTriggerComponent, ArtifactNodeEnteredEvent>(OnRandomizeTrigger);
}
private void OnRandomizeTrigger(EntityUid uid, ArtifactGasTriggerComponent component, RandomizeTriggerEvent args)
private void OnRandomizeTrigger(EntityUid uid, ArtifactGasTriggerComponent component, ArtifactNodeEnteredEvent args)
{
if (component.ActivationGas == null)
{
var gas = _random.Pick(component.PossibleGases);
component.ActivationGas = gas;
}
if (component.ActivationGas != null)
return;
var gas = component.PossibleGases[args.RandomSeed % component.PossibleGases.Count];
component.ActivationGas = gas;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityManager.EntityQuery<ArtifactGasTriggerComponent, TransformComponent>();
foreach (var (trigger, transform) in query)
List<ArtifactComponent> toUpdate = new();
foreach (var (trigger, artifact, transform) in EntityQuery<ArtifactGasTriggerComponent, ArtifactComponent, TransformComponent>())
{
var uid = trigger.Owner;
@@ -50,7 +49,12 @@ public sealed class ArtifactGasTriggerSystem : EntitySystem
if (moles < trigger.ActivationMoles)
continue;
_artifactSystem.TryActivateArtifact(trigger.Owner);
toUpdate.Add(artifact);
}
foreach (var a in toUpdate)
{
_artifactSystem.TryActivateArtifact(a.Owner, null, a);
}
}
}

View File

@@ -2,7 +2,6 @@
using Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components;
using Content.Shared.Interaction;
using Content.Shared.Temperature;
using Content.Shared.Weapons.Melee;
using Content.Shared.Weapons.Melee.Events;
using Robust.Server.GameObjects;
@@ -25,8 +24,8 @@ public sealed class ArtifactHeatTriggerSystem : EntitySystem
{
base.Update(frameTime);
var query = EntityManager.EntityQuery<ArtifactHeatTriggerComponent, TransformComponent, ArtifactComponent>();
foreach (var (trigger, transform, artifact) in query)
List<ArtifactComponent> toUpdate = new();
foreach (var (trigger, transform, artifact) in EntityQuery<ArtifactHeatTriggerComponent, TransformComponent, ArtifactComponent>())
{
var uid = trigger.Owner;
var environment = _atmosphereSystem.GetTileMixture(transform.GridUid, transform.MapUid,
@@ -37,7 +36,12 @@ public sealed class ArtifactHeatTriggerSystem : EntitySystem
if (environment.Temperature < trigger.ActivationTemperature)
continue;
_artifactSystem.TryActivateArtifact(trigger.Owner, component: artifact);
toUpdate.Add(artifact);
}
foreach (var a in toUpdate)
{
_artifactSystem.TryActivateArtifact(a.Owner, null, a);
}
}
@@ -61,7 +65,7 @@ public sealed class ArtifactHeatTriggerSystem : EntitySystem
private bool CheckHot(EntityUid usedUid)
{
var hotEvent = new IsHotEvent();
RaiseLocalEvent(usedUid, hotEvent, false);
RaiseLocalEvent(usedUid, hotEvent);
return hotEvent.IsHot;
}
}

View File

@@ -1,7 +1,6 @@
using Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components;
using Content.Shared.Interaction;
using Content.Shared.Physics.Pull;
using Content.Shared.Weapons.Melee;
using Content.Shared.Weapons.Melee.Events;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Systems;

View File

@@ -0,0 +1,40 @@
using Content.Server.Salvage;
using Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Systems;
/// <summary>
/// This handles...
/// </summary>
public sealed class ArtifactMagnetTriggerSystem : EntitySystem
{
[Dependency] private readonly ArtifactSystem _artifact = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<SalvageMagnetActivatedEvent>(OnMagnetActivated);
}
private void OnMagnetActivated(SalvageMagnetActivatedEvent ev)
{
var magXform = Transform(ev.Magnet);
var toActivate = new List<EntityUid>();
foreach (var (artifact, xform) in EntityQuery<ArtifactMagnetTriggerComponent, TransformComponent>())
{
if (!magXform.Coordinates.TryDistance(EntityManager, xform.Coordinates, out var distance))
continue;
if (distance > artifact.Range)
continue;
toActivate.Add(artifact.Owner);
}
foreach (var a in toActivate)
{
_artifact.TryActivateArtifact(a);
}
}
}

View File

@@ -0,0 +1,46 @@
using System.Linq;
using Content.Server.Instruments;
using Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Systems;
/// <summary>
/// This handles activating an artifact when music is playing nearby
/// </summary>
public sealed class ArtifactMusicTriggerSystem : EntitySystem
{
[Dependency] private readonly ArtifactSystem _artifact = default!;
public override void Update(float frameTime)
{
base.Update(frameTime);
var artifactQuery = EntityQuery<ArtifactMusicTriggerComponent, TransformComponent>().ToArray();
if (!artifactQuery.Any())
return;
List<EntityUid> toActivate = new();
//assume that there's more instruments than artifacts
foreach (var activeinstrument in EntityQuery<ActiveInstrumentComponent>())
{
var instXform = Transform(activeinstrument.Owner);
foreach (var (trigger, xform) in artifactQuery)
{
if (!instXform.Coordinates.TryDistance(EntityManager, xform.Coordinates, out var distance))
continue;
if (distance > trigger.Range)
continue;
toActivate.Add(trigger.Owner);
}
}
foreach (var a in toActivate)
{
_artifact.TryActivateArtifact(a);
}
}
}

View File

@@ -0,0 +1,40 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components;
using Robust.Server.GameObjects;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Systems;
/// <summary>
/// This handles activation upon certain pressure thresholds.
/// </summary>
public sealed class ArtifactPressureTriggerSystem : EntitySystem
{
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly ArtifactSystem _artifactSystem = default!;
[Dependency] private readonly TransformSystem _transformSystem = default!;
public override void Update(float frameTime)
{
base.Update(frameTime);
List<ArtifactComponent> toUpdate = new();
foreach (var (trigger, artifact, transform) in EntityQuery<ArtifactPressureTriggerComponent, ArtifactComponent, TransformComponent>())
{
var uid = trigger.Owner;
var environment = _atmosphereSystem.GetTileMixture(transform.GridUid, transform.MapUid,
_transformSystem.GetGridOrMapTilePosition(uid, transform));
if (environment == null)
continue;
var pressure = environment.Pressure;
if (pressure >= trigger.MaxPressureThreshold || pressure <= trigger.MinPressureThreshold)
toUpdate.Add(artifact);
}
foreach (var a in toUpdate)
{
_artifactSystem.TryActivateArtifact(a.Owner, null, a);
}
}
}

View File

@@ -8,19 +8,36 @@ public sealed class ArtifactTimerTriggerSystem : EntitySystem
[Dependency] private readonly IGameTiming _time = default!;
[Dependency] private readonly ArtifactSystem _artifactSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ArtifactTimerTriggerComponent, ComponentStartup>(OnStartup);
}
private void OnStartup(EntityUid uid, ArtifactTimerTriggerComponent component, ComponentStartup args)
{
component.LastActivation = _time.CurTime;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityManager.EntityQuery<ArtifactTimerTriggerComponent, ArtifactComponent>();
foreach (var (trigger, artifact) in query)
List<ArtifactComponent> toUpdate = new();
foreach (var (trigger, artifact) in EntityQuery<ArtifactTimerTriggerComponent, ArtifactComponent>())
{
var timeDif = _time.CurTime - trigger.LastActivation;
if (timeDif <= trigger.ActivationRate)
continue;
_artifactSystem.TryActivateArtifact(trigger.Owner, component: artifact);
toUpdate.Add(artifact);
trigger.LastActivation = _time.CurTime;
}
foreach (var a in toUpdate)
{
_artifactSystem.TryActivateArtifact(a.Owner, null, a);
}
}
}

View File

@@ -0,0 +1,78 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Xenoarchaeology.Equipment;
[Serializable, NetSerializable]
public enum ArtifactAnalzyerUiKey : byte
{
Key
}
[Serializable, NetSerializable]
public sealed class AnalysisConsoleServerSelectionMessage : BoundUserInterfaceMessage
{
}
[Serializable, NetSerializable]
public sealed class AnalysisConsoleScanButtonPressedMessage : BoundUserInterfaceMessage
{
}
[Serializable, NetSerializable]
public sealed class AnalysisConsoleDestroyButtonPressedMessage : BoundUserInterfaceMessage
{
}
[Serializable, NetSerializable]
public sealed class AnalysisConsoleScanUpdateState : BoundUserInterfaceState
{
public EntityUid? Artifact;
public bool AnalyzerConnected;
public bool ServerConnected;
public bool CanScan;
public int? Id;
public int? Depth;
public int? Edges;
public bool? Triggered;
public string? EffectProto;
public string? TriggerProto;
public float? Completion;
public bool Scanning;
public TimeSpan TimeRemaining;
public TimeSpan TotalTime;
public AnalysisConsoleScanUpdateState(EntityUid? artifact, bool analyzerConnected, bool serverConnected, bool canScan,
int? id, int? depth, int? edges, bool? triggered, string? effectProto, string? triggerProto, float? completion,
bool scanning, TimeSpan timeRemaining, TimeSpan totalTime)
{
Artifact = artifact;
AnalyzerConnected = analyzerConnected;
ServerConnected = serverConnected;
CanScan = canScan;
Id = id;
Depth = depth;
Edges = edges;
Triggered = triggered;
EffectProto = effectProto;
TriggerProto = triggerProto;
Completion = completion;
Scanning = scanning;
TimeRemaining = timeRemaining;
TotalTime = totalTime;
}
}

View File

@@ -0,0 +1,36 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.Xenoarchaeology.XenoArtifacts;
/// <summary>
/// This is a prototype for...
/// </summary>
[Prototype("artifactEffect")]
[DataDefinition]
public sealed class ArtifactEffectPrototype : IPrototype
{
/// <inheritdoc/>
[IdDataField]
public string ID { get; } = default!;
/// <summary>
/// Components that are added to the artifact when the specfic effect is active.
/// These are removed after the node is exited and the effect is changed.
/// </summary>
[DataField("components", serverOnly: true)]
public EntityPrototype.ComponentRegistry Components = new();
/// <summary>
/// Components that are permanently added to an entity when the effect's node is entered.
/// </summary>
[DataField("permanentComponents")]
public EntityPrototype.ComponentRegistry PermanentComponents = new();
//TODO: make this a list so we can have multiple target depths
[DataField("targetDepth")]
public int TargetDepth = 0;
[DataField("effectHint")]
public string? EffectHint;
}

View File

@@ -0,0 +1,25 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.Xenoarchaeology.XenoArtifacts;
/// <summary>
/// This is a prototype for...
/// </summary>
[Prototype("artifactTrigger")]
[DataDefinition]
public sealed class ArtifactTriggerPrototype : IPrototype
{
/// <inheritdoc/>
[IdDataField]
public string ID { get; } = default!;
[DataField("components", serverOnly: true)]
public EntityPrototype.ComponentRegistry Components = new();
[DataField("targetDepth")]
public int TargetDepth = 0;
[DataField("triggerHint")]
public string? TriggerHint;
}

Binary file not shown.

View File

@@ -17,3 +17,7 @@ reclaimer_startup.ogg - https://freesound.org/people/cmorris035/sounds/319152/,
airlock_open.ogg and airlock_close.ogg are from: https://github.com/tgstation/tgstation/tree/5f5002b21253354c20ea224be3f604d79299b37e/sound/machines
machine_vend_hot_drink.ogg original from https://freesound.org/people/waxsocks/sounds/402603/ CC0 (by waxsocks) and edited
scan_loop.ogg from https://freesound.org/people/steaq/sounds/509249/ CC-0 by steaq
scan_finish.ogg from https://freesound.org/people/pan14/sounds/263133/ CC-0 by pan14

Binary file not shown.

Binary file not shown.

View File

@@ -45,3 +45,9 @@ signal-port-description-med-scanner-sender = Medical scanner signal sender
signal-port-name-med-scanner-receiver = Medical scanner
signal-port-description-med-scanner-receiver = Medical scanner signal receiver
signal-port-name-artifact-analyzer-sender = Console
signal-port-description-artifact-analyzer-sender = Analysis console signal sender
signal-port-name-artifact-analyzer-receiver = Pad
signal-port-description-artifact-analyzer-receiver = Artifact analyzer signal receiver

View File

@@ -0,0 +1,9 @@
bluespace-artifact-event-announcement = Our readings have detected an incoming anomalous object. Please inform the research team of { $sighting }.
bluespace-artifact-sighting-1 = bright flashes of light
bluespace-artifact-sighting-2 = strange sounds coming from maintenance tunnels
bluespace-artifact-sighting-3 = otherworldly structures
bluespace-artifact-sighting-4 = incomprehensible alien objects
bluespace-artifact-sighting-5 = unfamiliar objects in strange places
bluespace-artifact-sighting-6 = unknown alien artifacts
bluespace-artifact-sighting-7 = explosions of light accompanied by weird sounds

View File

@@ -0,0 +1,29 @@
analysis-console-menu-title = analysis console
analysis-console-server-list-button = Server List
analysis-console-scan-button = Scan
analysis-console-scan-tooltip-info = Scan artifacts to learn information about their structure.
analysis-console-destroy-button = Destroy
analysis-console-destroy-button-info = Destroy artifacts to generate points based on how much has been unlocked.
analysis-console-info-no-scanner = No analyzer connected! Please connect one using a multitool.
analysis-console-info-no-artifact = No artifact present! Place one on the pad then scan for information.
analysis-console-info-ready = Systems operational. Ready to scan.
analysis-console-info-id = NODE_ID: {$id}
analysis-console-info-depth = DEPTH: {$depth}
analysis-console-info-triggered-true = ACTIVATED: TRUE
analysis-console-info-triggered-false = ACTIVATED: FALSE
analysis-console-info-effect = REACTION: {$effect}
analysis-console-info-trigger = STIMULUS: {$trigger}
analysis-console-info-edges = EDGES: {$edges}
analysis-console-info-completion = COMPLETION_PERCENTAGE: {$percentage}%
analysis-console-info-scanner = Scanning...
analysis-console-progress-text = {$seconds ->
[one] T-{$seconds} second
*[other] T-{$seconds} seconds
}
analyzer-artifact-component-upgrade-analysis = analysis duration
analyzer-artifact-destroy-popup = The artifact disintegrated into energy!

View File

@@ -0,0 +1,27 @@
# you shouldn't be creating new hints for every effect/trigger
# try and reuse them so that a hint isn't a dead giveaway. -emo
artifact-effect-hint-mental = Cerebral influence
artifact-effect-hint-environment = Environmental disruption
artifact-effect-hint-electrical-interference = Electrical interference
artifact-effect-hint-displacement = Metaphysical displacement
artifact-effect-hint-creation = Matter creation
artifact-effect-hint-consumption = Energy consumption
artifact-effect-hint-release = Energy release
artifact-effect-hint-biochemical = Biochemical disruption
artifact-effect-hint-destruction = Station-wide destruction
# the triggers should be more obvious than the effects
# gives people an idea of what to do: don't be too specific (i.e. no "welders")
artifact-trigger-hint-electricity = Electricity
artifact-trigger-hint-heat = High temperatures
artifact-trigger-hint-physical = Physical trauma
artifact-trigger-hint-tool = Tool usage
artifact-trigger-hint-music = Sonic vibrations
artifact-trigger-hint-water = Hydro-reactive
artifact-trigger-hint-magnet = Magnetic waves
artifact-trigger-hint-death = Life essence
artifact-trigger-hint-radiation = Radiation
artifact-trigger-hint-pressure = Extreme pressure
artifact-trigger-hint-gas = Gas

View File

@@ -0,0 +1,4 @@
blink-artifact-popup = The artifact disappeared in an instant!
foam-artifact-popup = Strange foam pours out of the artifact!
shuffle-artifact-popup = You feel yourself teleport instantly!

View File

@@ -749,7 +749,7 @@ entities:
parent: 52
type: Transform
- uid: 79
type: AngryMobsSpawnArtifact
type: RandomArtifactSpawner
components:
- pos: -0.5,-0.5
parent: 52

View File

@@ -207974,7 +207974,7 @@ entities:
parent: 82
type: Transform
- uid: 25103
type: JunkSpawnArtifact
type: RandomArtifactSpawner
components:
- pos: -69.5,-47.5
parent: 82

View File

@@ -129076,7 +129076,7 @@ entities:
parent: 30
type: Transform
- uid: 15282
type: BananaSpawnArtifact
type: RandomArtifactSpawner
components:
- pos: 36.5,12.5
parent: 30

View File

@@ -7,3 +7,13 @@
cost: 500
category: Science
group: market
- type: cargoProduct
id: RandomArtifact
icon:
sprite: Objects/Specific/Xenoarchaeology/xeno_artifacts.rsi
state: ano13
product: RandomArtifactSpawner
cost: 2000
category: Science
group: market

View File

@@ -157,6 +157,8 @@
- id: DoorRemoteResearch
- id: RubberStampRd
- id: ClothingHeadsetAltScience
- id: AnalysisComputerCircuitboard
- id: ArtifactAnalyzerMachineCircuitboard
- type: entity
id: LockerHeadOfSecurityFilled

View File

@@ -320,6 +320,8 @@
- SolarControlComputerCircuitboard
- PowerComputerCircuitboard
- GeneratorPlasmaMachineCircuitboard
- AnalysisComputerCircuitboard
- ArtifactAnalyzerMachineCircuitboard
- Signaller
- SignalTrigger
- VoiceTrigger

View File

@@ -0,0 +1,13 @@
- type: entity
id: EffectFlashBluespace
noSpawn: true
components:
- type: PointLight
radius: 10.5
energy: 15
color: "#18abf5"
- type: TimedDespawn
lifetime: 1
- type: EmitSoundOnSpawn
sound:
path: /Audio/Effects/Lightning/lightningbolt.ogg

View File

@@ -9,16 +9,12 @@
- texture: Objects/Specific/Xenoarchaeology/xeno_artifacts.rsi/ano01.png
- type: RandomSpawner
prototypes:
- BadfeelingArtifact
- GoodfeelingArtifact
- AngryMobsSpawnArtifact
- JunkSpawnArtifact
- BananaSpawnArtifact
- HeatArtifact
- ColdArtifact
- RadiateArtifact
- GasArtifact
- DiseaseArtifact
- SimpleXenoArtifact
- MediumXenoArtifact
- MediumXenoArtifact
- MediumXenoArtifact
- ComplexXenoArtifact
- ComplexXenoArtifact
chance: 1
- type: entity

View File

@@ -131,6 +131,22 @@
materialRequirements:
Cable: 5
- type: entity
id: ArtifactAnalyzerMachineCircuitboard
parent: BaseMachineCircuitboard
name: artifact analyzer machine board
description: A machine printed circuit board for an artifact analyzer
components:
- type: Sprite
state: science
- type: MachineBoard
prototype: MachineArtifactAnalyzer
requirements:
ScanningModule: 3
Capacitor: 1
materialRequirements:
Glass: 5
- type: entity
id: ThermomachineFreezerMachineCircuitBoard
parent: BaseMachineCircuitboard

View File

@@ -145,6 +145,17 @@
- type: ComputerBoard
prototype: ComputerResearchAndDevelopment
- type: entity
parent: BaseComputerCircuitboard
id: AnalysisComputerCircuitboard
name: analysis computer board
description: A computer printed circuit board for an analysis console.
components:
- type: Sprite
state: cpu_science
- type: ComputerBoard
prototype: ComputerAnalysisConsole
- type: entity
parent: BaseComputerCircuitboard
id: CrewMonitoringComputerCircuitboard

View File

@@ -44,6 +44,7 @@
- type: Weldable
- type: SuppressArtifactContainer
- type: PlaceableSurface
isPlaceable: false
- type: Damageable
damageContainer: Inorganic
damageModifierSet: Metallic

View File

@@ -10,6 +10,8 @@
sprite: Objects/Specific/Xenoarchaeology/xeno_artifacts.rsi
netsync: false
state: ano01
noRot: true
- type: Damageable
- type: Physics
bodyType: Dynamic
- type: Transform
@@ -31,152 +33,34 @@
- type: Appearance
visuals:
- type: RandomArtifactVisualizer
- type: PowerConsumer
voltage: Medium
drawRate: 500
- type: NodeContainer
nodes:
medium:
!type:CableDeviceNode
nodeGroupID: MVPower
# sadly, HVPower and Apc cables doesn't work right now
- type: StaticPrice
price: 2000
- type: Electrified
requirePower: true
noWindowInTile: true
highVoltageNode: high
mediumVoltageNode: medium
lowVoltageNode: low
# Telepathic
- type: entity
parent: BaseXenoArtifact
id: BadfeelingArtifact
suffix: Badfeeling
components:
- type: TelepathicArtifact
messages:
- badfeeling-artifact-1
- badfeeling-artifact-2
- badfeeling-artifact-3
- badfeeling-artifact-4
- badfeeling-artifact-5
- badfeeling-artifact-6
- badfeeling-artifact-7
- badfeeling-artifact-8
- badfeeling-artifact-9
- badfeeling-artifact-10
- badfeeling-artifact-11
- badfeeling-artifact-12
- badfeeling-artifact-13
- badfeeling-artifact-14
- badfeeling-artifact-15
drastic:
- badfeeling-artifact-drastic-1
- badfeeling-artifact-drastic-2
- badfeeling-artifact-drastic-3
- badfeeling-artifact-drastic-4
- badfeeling-artifact-drastic-5
- badfeeling-artifact-drastic-6
price: 500
- type: entity
parent: BaseXenoArtifact
id: GoodfeelingArtifact
suffix: Goodfeeling
id: SimpleXenoArtifact
suffix: Simple
components:
- type: TelepathicArtifact
messages:
- goodfeeling-artifact-1
- goodfeeling-artifact-2
- goodfeeling-artifact-3
- goodfeeling-artifact-4
- goodfeeling-artifact-5
- goodfeeling-artifact-6
- goodfeeling-artifact-7
- goodfeeling-artifact-8
- goodfeeling-artifact-9
- goodfeeling-artifact-10
- goodfeeling-artifact-11
- goodfeeling-artifact-12
- goodfeeling-artifact-13
- goodfeeling-artifact-14
drastic:
- goodfeeling-artifact-drastic-1
- goodfeeling-artifact-drastic-2
- goodfeeling-artifact-drastic-3
- goodfeeling-artifact-drastic-4
- goodfeeling-artifact-drastic-5
- goodfeeling-artifact-drastic-6
# Spawners
- type: entity
parent: BaseXenoArtifact
id: AngryMobsSpawnArtifact
suffix: Angry Mobs Spawn
components:
- type: SpawnArtifact
maxSpawns: 5
possiblePrototypes:
- MobCarpHolo
- MobCarpMagic
- type: Artifact
nodesMin: 2
nodesMax: 5
- type: entity
parent: BaseXenoArtifact
id: JunkSpawnArtifact
suffix: Junk Spawn
id: MediumXenoArtifact
suffix: Medium
components:
- type: SpawnArtifact
maxSpawns: 10
possiblePrototypes:
- FoodPacketSyndiTrash
- FoodPacketSemkiTrash
- RandomInstruments
- ToySpawner
- type: Artifact
nodesMin: 5
nodesMax: 9
- type: entity
parent: BaseXenoArtifact
id: BananaSpawnArtifact
suffix: Banana Spawn
id: ComplexXenoArtifact
suffix: Complex
components:
- type: SpawnArtifact
maxSpawns: 20
possiblePrototypes:
- FoodBanana
- type: Artifact
nodesMin: 9
nodesMax: 13
- type: entity
parent: BaseXenoArtifact
id: HeatArtifact
suffix: Heat
components:
- type: TemperatureArtifact
targetTemp: 400 # around 125 celsius
- type: entity
parent: BaseXenoArtifact
id: ColdArtifact
suffix: Cold
components:
- type: TemperatureArtifact
targetTemp: 150 # around -125 celsius
- type: entity
parent: BaseXenoArtifact
id: RadiateArtifact
suffix: Radiation
components:
- type: RadiateArtifact
- type: entity
parent: BaseXenoArtifact
id: GasArtifact
suffix: Gas
components:
- type: GasArtifact
- type: entity
parent: BaseXenoArtifact
id: DiseaseArtifact
suffix: Disease
components:
- type: DiseaseArtifact

View File

@@ -335,6 +335,46 @@
energy: 1.6
color: "#b53ca1"
- type: entity
parent: BaseComputer
id: ComputerAnalysisConsole
name: analysis console
description: A computer used to interface with the artifact analyzer.
components:
- type: Appearance
visuals:
- type: ComputerVisualizer
key: tech_key
screen: artifact
- type: ResearchClient
- type: AnalysisConsole
- type: DeviceList
- type: DeviceNetwork
deviceNetId: Wired
- type: SignalTransmitter
transmissionRange: 5
outputs:
ArtifactAnalyzerSender: []
- type: ActivatableUI
key: enum.ArtifactAnalzyerUiKey.Key
- type: ActivatableUIRequiresPower
- type: UserInterface
interfaces:
- key: enum.ArtifactAnalzyerUiKey.Key
type: AnalysisConsoleBoundUserInterface
- key: enum.ResearchClientUiKey.Key
type: ResearchClientBoundUserInterface
- type: ApcPowerReceiver
powerLoad: 1000
priority: Low
- type: ExtensionCableReceiver
- type: Computer
board: AnalysisComputerCircuitboard
- type: PointLight
radius: 1.5
energy: 1.6
color: "#b53ca1"
- type: entity
parent: BaseComputer
id: ComputerId

View File

@@ -0,0 +1,68 @@
- type: entity
id: MachineArtifactAnalyzer
parent: [ BaseMachinePowered, ConstructibleMachine ]
name: artifact analyzer
description: A platform capable of performing analysis on various types of artifacts.
components:
- type: Sprite
noRot: true
netsync: false
sprite: Structures/Machines/artifact_analyzer.rsi
drawdepth: FloorObjects
layers:
- state: icon
- state: unshaded
shader: unshaded
map: ["enum.PowerDeviceVisualLayers.Powered"]
- type: Physics
bodyType: Static
canCollide: true
- type: AmbientSound
enabled: false
sound:
path: /Audio/Machines/scan_loop.ogg
range: 5
volume: -8
- type: Fixtures
fixtures:
- shape:
!type:PhysShapeAabb
bounds: "-0.35,-0.35,0.35,0.35"
density: 190
mask:
- MachineMask
layer:
- Impassable
- MidImpassable
- LowImpassable
hard: False
- type: Transform
anchored: true
noRot: false
- type: ApcPowerReceiver
powerLoad: 15000 #really freaking high
needsPower: false #only turns on when scanning
- type: UpgradePowerDraw
powerDrawMultiplier: 0.80
scaling: Exponential
- type: ArtifactAnalyzer
- type: DeviceNetwork
deviceNetId: Wired
- type: DeviceList
- type: SignalReceiver
inputs:
ArtifactAnalyzerReceiver: []
- type: Machine
board: ArtifactAnalyzerMachineCircuitboard
- type: PointLight
radius: 1.5
energy: 1.6
color: "#b53ca1"
- type: LitOnPowered
- type: Appearance
- type: GenericVisualizer
visuals:
enum.PowerDeviceVisuals.Powered:
enum.PowerDeviceVisualLayers.Powered:
True: { visible: true }
False: { visible: false }

View File

@@ -298,6 +298,8 @@
- EmitterCircuitboard
- GasRecyclerMachineCircuitboard
- SeedExtractorMachineCircuitboard
- AnalysisComputerCircuitboard
- ArtifactAnalyzerMachineCircuitboard
- type: MaterialStorage
whitelist:
tags:

View File

@@ -52,7 +52,7 @@
map: ["enum.PowerDeviceVisualLayers.Powered"]
- type: ResearchClient
- type: ResearchPointSource
pointspersecond: 100
pointspersecond: 25
active: true
- type: PointLight
radius: 1.5

View File

@@ -1,4 +1,14 @@
- type: gameRule
id: BluespaceArtifact
config:
!type:StationEventRuleConfiguration
id: BluespaceArtifact
weight: 5
maxOccurrences: 5
startAfter: 30
endAfter: 35
- type: gameRule
id: BreakerFlip
config:
!type:StationEventRuleConfiguration

View File

@@ -62,3 +62,8 @@
id: MedicalScannerReceiver
name: signal-port-name-med-scanner-receiver
description: signal-port-description-med-scanner-receiver
- type: receiverPort
id: ArtifactAnalyzerReceiver
name: signal-port-name-artifact-analyzer-receiver
description: signal-port-description-artifact-analyzer-receiver

View File

@@ -49,3 +49,9 @@
id: MedicalScannerSender
name: signal-port-name-med-scanner-sender
description: signal-port-description-med-scanner-sender
- type: transmitterPort
id: ArtifactAnalyzerSender
name: signal-port-name-artifact-analyzer-sender
description: signal-port-description-artifact-analyzer-sender
defaultLinks: [ ArtifactAnalyzerReceiver ]

View File

@@ -176,6 +176,15 @@
Glass: 900
Gold: 100
- type: latheRecipe
id: ArtifactAnalyzerMachineCircuitboard
icon: Objects/Misc/module.rsi/science.png
result: ArtifactAnalyzerMachineCircuitboard
completetime: 4
materials:
Steel: 100
Glass: 900
Gold: 100
- type: latheRecipe
id: ReagentGrinderMachineCircuitboard
@@ -186,6 +195,16 @@
Steel: 100
Glass: 900
- type: latheRecipe
id: AnalysisComputerCircuitboard
icon: Objects/Misc/module.rsi/cpu_science.png
result: AnalysisComputerCircuitboard
completetime: 4
materials:
Steel: 100
Glass: 900
Gold: 100
- type: latheRecipe
id: CrewMonitoringComputerCircuitboard
icon: Objects/Misc/module.rsi/id_mod.png

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