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. // 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. // There should be an issue up tracking all of these prototypes, indicating that still need to get fixed.
"HeadSkeleton", "HeadSkeleton",
"CrateArtifactContainer",
// The followjng are all fixture-less phsyics entities that set can-collide to false on init. // The followjng are all fixture-less phsyics entities that set can-collide to false on init.
"CarpRift", "CarpRift",
"GasMinerOxygen", "GasMinerOxygen",

View File

@@ -3,6 +3,7 @@ using Content.Server.Coordinates.Helpers;
using Content.Shared.Audio; using Content.Shared.Audio;
using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Components;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Player; 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, 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>(); entityManager ??= IoCManager.Resolve<IEntityManager>();
var ent = entityManager.SpawnEntity(entityPrototype, coords.SnapToGrid()); var ent = entityManager.SpawnEntity(entityPrototype, coords.SnapToGrid());
@@ -37,7 +38,8 @@ namespace Content.Server.Chemistry.ReactionEffects
areaEffectComponent.TryAddSolution(contents); areaEffectComponent.TryAddSolution(contents);
areaEffectComponent.Start(amount, duration, spreadDelay, removeDelay); 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 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 public enum MagnetStateType
{ {
Inactive, Inactive,

View File

@@ -169,6 +169,7 @@ namespace Content.Server.Salvage
} }
gridState.ActiveMagnets.Add(component); gridState.ActiveMagnets.Add(component);
component.MagnetState = new MagnetState(MagnetStateType.Attaching, gridState.CurrentTime + AttachingTime); component.MagnetState = new MagnetState(MagnetStateType.Attaching, gridState.CurrentTime + AttachingTime);
RaiseLocalEvent(new SalvageMagnetActivatedEvent(component.Owner));
Report(component.Owner, component.SalvageChannel, "salvage-system-report-activate-success"); Report(component.Owner, component.SalvageChannel, "salvage-system-report-activate-success");
break; break;
case MagnetStateType.Attaching: 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> /// <summary>
/// Suppress artifact activation, when entity is placed inside this container. /// 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.Equipment.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts.Equipment.Components; using Content.Server.Xenoarchaeology.XenoArtifacts;
using Robust.Shared.Containers; using Robust.Shared.Containers;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Equipment.Systems; namespace Content.Server.Xenoarchaeology.Equipment.Systems;
public sealed class SuppressArtifactContainerSystem : EntitySystem 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() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
@@ -24,11 +19,6 @@ public sealed class SuppressArtifactContainerSystem : EntitySystem
return; return;
artifact.IsSuppressed = true; artifact.IsSuppressed = true;
if (TryComp<StaticPriceComponent>(args.Entity, out var price))
{
price.Price *= ContainedArtifactModifier;
}
} }
private void OnRemoved(EntityUid uid, SuppressArtifactContainerComponent component, EntRemovedFromContainerMessage args) private void OnRemoved(EntityUid uid, SuppressArtifactContainerComponent component, EntRemovedFromContainerMessage args)
@@ -37,10 +27,5 @@ public sealed class SuppressArtifactContainerSystem : EntitySystem
return; return;
artifact.IsSuppressed = false; 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; namespace Content.Server.Xenoarchaeology.XenoArtifacts;
[RegisterComponent] [RegisterComponent]
public sealed class ArtifactComponent : Component public sealed class ArtifactComponent : Component
{ {
/// <summary> /// <summary>
/// Should artifact pick a random trigger on startup? /// The artifact's node tree.
/// </summary> /// </summary>
[DataField("randomTrigger")] [ViewVariables]
public bool RandomTrigger = true; public ArtifactTree? NodeTree;
/// <summary> /// <summary>
/// List of all possible triggers activations. /// The current node the artifact is on.
/// Should be same as components names.
/// </summary> /// </summary>
[DataField("possibleTriggers")] [ViewVariables]
public string[] PossibleTriggers = { public ArtifactNode? CurrentNode;
"ArtifactInteractionTrigger",
"ArtifactGasTrigger", #region Node Tree Gen
"ArtifactHeatTrigger", /// <summary>
"ArtifactElectricityTrigger", /// Minimum number of nodes to generate, inclusive
}; /// </summary>
[DataField("nodesMin")]
public int NodesMin = 3;
/// <summary>
/// Maximum number of nodes to generate, exclusive
/// </summary>
[DataField("nodesMax")]
public int NodesMax = 9;
#endregion
/// <summary> /// <summary>
/// Cooldown time between artifact activations (in seconds). /// Cooldown time between artifact activations (in seconds).
/// </summary> /// </summary>
[DataField("timer")] [DataField("timer", customTypeSerializer: typeof(TimespanSerializer))]
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
public double CooldownTime = 10; public TimeSpan CooldownTime = TimeSpan.FromSeconds(5);
/// <summary> /// <summary>
/// Is this artifact under some suppression device? /// Is this artifact under some suppression device?
/// If true, will ignore all trigger activations attempts. /// f true, will ignore all trigger activations attempts.
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
public bool IsSuppressed; public bool IsSuppressed;
/// <summary>
/// The last time the artifact was activated.
/// </summary>
[DataField("lastActivationTime", customTypeSerializer: typeof(TimespanSerializer))]
public TimeSpan LastActivationTime; 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 Content.Server.Xenoarchaeology.XenoArtifacts.Events;
using JetBrains.Annotations;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Timing; using Robust.Shared.Timing;
namespace Content.Server.Xenoarchaeology.XenoArtifacts; 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 IGameTiming _gameTiming = default!;
[Dependency] private readonly IRobustRandom _random = 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() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<ArtifactComponent, MapInitEvent>(OnInit); SubscribeLocalEvent<ArtifactComponent, MapInitEvent>(OnInit);
SubscribeLocalEvent<ArtifactComponent, PriceCalculationEvent>(GetPrice);
} }
private void OnInit(EntityUid uid, ArtifactComponent component, MapInitEvent args) private void OnInit(EntityUid uid, ArtifactComponent component, MapInitEvent args)
{ {
if (component.RandomTrigger) RandomizeArtifact(component);
{
AddRandomTrigger(uid, 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; return;
var triggerName = _random.Pick(component.PossibleTriggers); var price = component.NodeTree.AllNodes.Sum(GetNodePrice);
var trigger = (Component) _componentFactory.GetComponent(triggerName);
trigger.Owner = uid;
if (EntityManager.HasComponent(uid, trigger.GetType())) // 25% bonus for fully exploring every node.
var fullyExploredBonus = component.NodeTree.AllNodes.Any(x => !x.Triggered) ? 1 : 1.25f;
args.Price =+ price * fullyExploredBonus;
}
private float GetNodePrice(ArtifactNode node)
{ {
Logger.Error($"Attempted to add a random artifact trigger ({triggerName}) to an entity ({ToPrettyString(uid)}), but it already has the trigger"); if (!node.Discovered) //no money for undiscovered nodes.
return; 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;
} }
EntityManager.AddComponent(uid, trigger); /// <summary>
RaiseLocalEvent(uid, new RandomizeTriggerEvent(), true); /// 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;
} }
public bool TryActivateArtifact(EntityUid uid, EntityUid? user = null, private float GetNodePointValue(ArtifactNode node)
ArtifactComponent? component = null) {
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)) if (!Resolve(uid, ref component))
return false; return false;
@@ -55,25 +127,85 @@ public sealed class ArtifactSystem : EntitySystem
// check if artifact isn't under cooldown // check if artifact isn't under cooldown
var timeDif = _gameTiming.CurTime - component.LastActivationTime; var timeDif = _gameTiming.CurTime - component.LastActivationTime;
if (timeDif.TotalSeconds < component.CooldownTime) if (timeDif < component.CooldownTime)
return false; return false;
ForceActivateArtifact(uid, user, component); ForceActivateArtifact(uid, user, component);
return true; return true;
} }
public void ForceActivateArtifact(EntityUid uid, EntityUid? user = null, /// <summary>
ArtifactComponent? component = null) /// 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)) if (!Resolve(uid, ref component))
return; return;
if (component.CurrentNode == null)
return;
component.LastActivationTime = _gameTiming.CurTime; component.LastActivationTime = _gameTiming.CurTime;
var ev = new ArtifactActivatedEvent() var ev = new ArtifactActivatedEvent
{ {
Activator = user Activator = user
}; };
RaiseLocalEvent(uid, ev, true); 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 Content.Shared.Disease;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components; namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
/// <summary> /// <summary>
@@ -8,10 +9,15 @@ namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
public sealed class DiseaseArtifactComponent : Component public sealed class DiseaseArtifactComponent : Component
{ {
/// <summary> /// <summary>
/// Disease the artifact will spawn /// The diseases that the artifact can use.
/// If empty, picks a random one from its list /// </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> /// </summary>
[DataField("disease")]
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
public DiseasePrototype? SpawnDisease; public DiseasePrototype? SpawnDisease;
@@ -19,7 +25,6 @@ public sealed class DiseaseArtifactComponent : Component
/// How far away it will check for people /// How far away it will check for people
/// If empty, picks a random one from its list /// If empty, picks a random one from its list
/// </summary> /// </summary>
[DataField("range")] [DataField("range"), ViewVariables(VVAccess.ReadWrite)]
[ViewVariables(VVAccess.ReadWrite)]
public float Range = 5f; 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. /// List of possible activation gases to pick on startup.
/// </summary> /// </summary>
[DataField("possibleGas")] [DataField("possibleGas")]
public Gas[] PossibleGases = public List<Gas> PossibleGases = new()
{ {
Gas.Oxygen, Gas.Oxygen,
Gas.Plasma, 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] [RegisterComponent]
public sealed class SpawnArtifactComponent : Component public sealed class SpawnArtifactComponent : Component
{ {
[DataField("random")] /// <summary>
public bool RandomPrototype = true; /// The list of possible prototypes to spawn that it picks from.
/// </summary>
[DataField("possiblePrototypes", customTypeSerializer:typeof(PrototypeIdListSerializer<EntityPrototype>))] [DataField("possiblePrototypes", customTypeSerializer:typeof(PrototypeIdListSerializer<EntityPrototype>))]
public List<string> PossiblePrototypes = new(); public List<string> PossiblePrototypes = new();
/// <summary>
/// The prototype it selected to spawn.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
[DataField("prototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))] [DataField("prototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
public string? Prototype; public string? Prototype;
/// <summary>
/// The range around the artifact that it will spawn the entity
/// </summary>
[DataField("range")] [DataField("range")]
public float Range = 0.5f; public float Range = 0.5f;
/// <summary>
/// The maximum number of times the spawn will occur
/// </summary>
[DataField("maxSpawns")] [DataField("maxSpawns")]
public int MaxSpawns = 20; 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> /// </summary>
[DataField("messages")] [DataField("messages")]
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
public string[] Messages = default!; public List<string> Messages = default!;
/// <summary> /// <summary>
/// Loc string ids of telepathic messages (spooky version). /// Loc string ids of telepathic messages (spooky version).
@@ -21,7 +21,7 @@ public sealed class TelepathicArtifactComponent : Component
/// </summary> /// </summary>
[DataField("drastic")] [DataField("drastic")]
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
public string[] DrasticMessages = default!; public List<string>? DrasticMessages;
/// <summary> /// <summary>
/// Probability to pick drastic version of message. /// Probability to pick drastic version of message.

View File

@@ -8,7 +8,7 @@ namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
[RegisterComponent] [RegisterComponent]
public sealed class TemperatureArtifactComponent : Component public sealed class TemperatureArtifactComponent : Component
{ {
[DataField("targetTemp")] [DataField("targetTemp"), ViewVariables(VVAccess.ReadWrite)]
public float TargetTemperature = Atmospherics.T0C; public float TargetTemperature = Atmospherics.T0C;
[DataField("spawnTemp")] [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. /// If true, artifact will heat/cool not only its current tile, but surrounding tiles too.
/// This will change room temperature much faster. /// This will change room temperature much faster.
/// </summary> /// </summary>
[DataField("effectAdjacent")] [DataField("affectAdjacent")]
public bool EffectAdjacentTiles = true; 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.Events;
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components; using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
using Content.Shared.Disease; using Content.Shared.Disease;
using Content.Server.Disease; using Content.Server.Disease;
using Content.Server.Disease.Components; using Content.Server.Disease.Components;
using Robust.Shared.Random;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Content.Shared.Interaction; using Content.Shared.Interaction;
@@ -15,38 +15,25 @@ namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems
public sealed class DiseaseArtifactSystem : EntitySystem public sealed class DiseaseArtifactSystem : EntitySystem
{ {
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly DiseaseSystem _disease = default!; [Dependency] private readonly DiseaseSystem _disease = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = 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() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<DiseaseArtifactComponent, MapInitEvent>(OnMapInit); SubscribeLocalEvent<DiseaseArtifactComponent, ArtifactNodeEnteredEvent>(OnNodeEntered);
SubscribeLocalEvent<DiseaseArtifactComponent, ArtifactActivatedEvent>(OnActivate); SubscribeLocalEvent<DiseaseArtifactComponent, ArtifactActivatedEvent>(OnActivate);
} }
/// <summary> /// <summary>
/// Makes sure this artifact is assigned a disease /// Makes sure this artifact is assigned a disease
/// </summary> /// </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; if (component.SpawnDisease != null || !component.DiseasePrototypes.Any())
var diseaseName = _random.Pick(ArtifactDiseases); return;
var diseaseName = component.DiseasePrototypes[args.RandomSeed % component.DiseasePrototypes.Count];
if (!_prototypeManager.TryIndex<DiseasePrototype>(diseaseName, out var disease)) 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.Atmos.EntitySystems;
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components; using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
using Content.Server.Xenoarchaeology.XenoArtifacts.Events; using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
using Robust.Shared.Random;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems; namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
public sealed class GasArtifactSystem : EntitySystem public sealed class GasArtifactSystem : EntitySystem
{ {
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!; [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly IRobustRandom _random = default!;
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<GasArtifactComponent, MapInitEvent>(OnMapInit); SubscribeLocalEvent<GasArtifactComponent, ArtifactNodeEnteredEvent>(OnNodeEntered);
SubscribeLocalEvent<GasArtifactComponent, ArtifactActivatedEvent>(OnActivate); 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; component.SpawnGas = gas;
} }
if (component.SpawnTemperature == null) 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; component.SpawnTemperature = temp;
} }
} }
@@ -38,8 +37,6 @@ public sealed class GasArtifactSystem : EntitySystem
if (component.SpawnGas == null || component.SpawnTemperature == null) if (component.SpawnGas == null || component.SpawnTemperature == null)
return; return;
var transform = Transform(uid);
var environment = _atmosphereSystem.GetContainingMixture(uid, false, true); var environment = _atmosphereSystem.GetContainingMixture(uid, false, true);
if (environment == null) if (environment == null)
return; 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 IRobustRandom _random = default!;
[Dependency] private readonly SharedHandsSystem _handsSystem = default!; [Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly ArtifactSystem _artifact = default!;
public const string NodeDataSpawnAmount = "nodeDataSpawnAmount";
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<SpawnArtifactComponent, MapInitEvent>(OnMapInit); SubscribeLocalEvent<SpawnArtifactComponent, ArtifactNodeEnteredEvent>(OnNodeEntered);
SubscribeLocalEvent<SpawnArtifactComponent, ArtifactActivatedEvent>(OnActivate); 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) private void OnActivate(EntityUid uid, SpawnArtifactComponent component, ArtifactActivatedEvent args)
{ {
if (component.Prototype == null) if (component.Prototype == null)
return; return;
if (component.SpawnsCount >= component.MaxSpawns)
if (!_artifact.TryGetNodeData(uid, NodeDataSpawnAmount, out int amount))
amount = 0;
if (amount >= component.MaxSpawns)
return; return;
var toSpawn = component.Prototype;
if (!component.ConsistentSpawn)
toSpawn = _random.Pick(component.PossiblePrototypes);
// select spawn position near artifact // select spawn position near artifact
var artifactCord = Transform(uid).Coordinates; var artifactCord = Transform(uid).Coordinates;
var dx = _random.NextFloat(-component.Range, component.Range); 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)); var spawnCord = artifactCord.Offset(new Vector2(dx, dy));
// spawn entity // spawn entity
var spawned = EntityManager.SpawnEntity(component.Prototype, spawnCord); var spawned = EntityManager.SpawnEntity(toSpawn, spawnCord);
component.SpawnsCount++; _artifact.SetNodeData(uid, NodeDataSpawnAmount, amount+1);
// if there is an user - try to put spawned item in their hands // if there is an user - try to put spawned item in their hands
// doesn't work for spawners // doesn't work for spawners
_handsSystem.PickupOrDrop(args.Activator, spawned); _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; continue;
// roll if msg should be usual or drastic // roll if msg should be usual or drastic
var isDrastic = _random.NextFloat() <= component.DrasticMessageProb; List<string> msgArr;
var msgArr = isDrastic ? component.DrasticMessages : component.Messages; if (_random.NextFloat() <= component.DrasticMessageProb && component.DrasticMessages != null)
{
msgArr = component.DrasticMessages;
}
else
{
msgArr = component.Messages;
}
// pick a random message // pick a random message
var msgId = _random.Pick(msgArr); var msgId = _random.Pick(msgArr);

View File

@@ -26,7 +26,7 @@ public sealed class TemperatureArtifactSystem : EntitySystem
return; return;
UpdateTileTemperature(component, center); UpdateTileTemperature(component, center);
if (component.EffectAdjacentTiles && transform.GridUid != null) if (component.AffectAdjacentTiles && transform.GridUid != null)
{ {
var adjacent = _atmosphereSystem.GetAdjacentTileMixtures(transform.GridUid.Value, var adjacent = _atmosphereSystem.GetAdjacentTileMixtures(transform.GridUid.Value,
_transformSystem.GetGridOrMapTilePosition(uid, transform), excite: true); _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> /// </summary>
public EntityUid? Activator; 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.Server.Xenoarchaeology.XenoArtifacts.Events;
using Content.Shared.Xenoarchaeology.XenoArtifacts; using Content.Shared.Xenoarchaeology.XenoArtifacts;
using Robust.Server.GameObjects;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Timing; using Robust.Shared.Timing;
@@ -9,6 +10,7 @@ public sealed class RandomArtifactSpriteSystem : EntitySystem
{ {
[Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IGameTiming _time = default!; [Dependency] private readonly IGameTiming _time = default!;
[Dependency] private readonly AppearanceSystem _appearance = default!;
public override void Initialize() public override void Initialize()
{ {
@@ -29,7 +31,7 @@ public sealed class RandomArtifactSpriteSystem : EntitySystem
var timeDif = _time.CurTime - component.ActivationStart.Value; var timeDif = _time.CurTime - component.ActivationStart.Value;
if (timeDif.Seconds >= component.ActivationTime) if (timeDif.Seconds >= component.ActivationTime)
{ {
appearance.SetData(SharedArtifactsVisuals.IsActivated, false); _appearance.SetData(appearance.Owner, SharedArtifactsVisuals.IsActivated, false, appearance);
component.ActivationStart = null; component.ActivationStart = null;
} }
} }
@@ -37,19 +39,13 @@ public sealed class RandomArtifactSpriteSystem : EntitySystem
private void OnMapInit(EntityUid uid, RandomArtifactSpriteComponent component, MapInitEvent args) 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); 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) private void OnActivated(EntityUid uid, RandomArtifactSpriteComponent component, ArtifactActivatedEvent args)
{ {
if (!TryComp(uid, out AppearanceComponent? appearance)) _appearance.SetData(uid, SharedArtifactsVisuals.IsActivated, true);
return;
appearance.SetData(SharedArtifactsVisuals.IsActivated, true);
component.ActivationStart = _time.CurTime; 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. /// List of possible activation gases to pick on startup.
/// </summary> /// </summary>
[DataField("possibleGas")] [DataField("possibleGas")]
public Gas[] PossibleGases = public List<Gas> PossibleGases = new()
{ {
Gas.Oxygen, Gas.Oxygen,
Gas.Plasma, 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) public override void Update(float frameTime)
{ {
base.Update(frameTime); base.Update(frameTime);
var query = EntityManager.EntityQuery<ArtifactElectricityTriggerComponent, PowerConsumerComponent, ArtifactComponent>(); List<ArtifactComponent> toUpdate = new();
foreach (var (trigger, power, artifact) in query) foreach (var (trigger, power, artifact) in EntityQuery<ArtifactElectricityTriggerComponent, PowerConsumerComponent, ArtifactComponent>())
{ {
if (power.ReceivedPower <= trigger.MinPower) if (power.ReceivedPower <= trigger.MinPower)
continue; 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.Events;
using Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components; using Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Shared.Random;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Systems; namespace Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Systems;
public sealed class ArtifactGasTriggerSystem : EntitySystem public sealed class ArtifactGasTriggerSystem : EntitySystem
{ {
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!; [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly ArtifactSystem _artifactSystem = default!; [Dependency] private readonly ArtifactSystem _artifactSystem = default!;
[Dependency] private readonly TransformSystem _transformSystem = default!; [Dependency] private readonly TransformSystem _transformSystem = default!;
@@ -16,23 +14,24 @@ public sealed class ArtifactGasTriggerSystem : EntitySystem
public override void Initialize() public override void Initialize()
{ {
base.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) if (component.ActivationGas != null)
{ return;
var gas = _random.Pick(component.PossibleGases);
var gas = component.PossibleGases[args.RandomSeed % component.PossibleGases.Count];
component.ActivationGas = gas; component.ActivationGas = gas;
} }
}
public override void Update(float frameTime) public override void Update(float frameTime)
{ {
base.Update(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; var uid = trigger.Owner;
@@ -50,7 +49,12 @@ public sealed class ArtifactGasTriggerSystem : EntitySystem
if (moles < trigger.ActivationMoles) if (moles < trigger.ActivationMoles)
continue; 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.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Temperature; using Content.Shared.Temperature;
using Content.Shared.Weapons.Melee;
using Content.Shared.Weapons.Melee.Events; using Content.Shared.Weapons.Melee.Events;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
@@ -25,8 +24,8 @@ public sealed class ArtifactHeatTriggerSystem : EntitySystem
{ {
base.Update(frameTime); base.Update(frameTime);
var query = EntityManager.EntityQuery<ArtifactHeatTriggerComponent, TransformComponent, ArtifactComponent>(); List<ArtifactComponent> toUpdate = new();
foreach (var (trigger, transform, artifact) in query) foreach (var (trigger, transform, artifact) in EntityQuery<ArtifactHeatTriggerComponent, TransformComponent, ArtifactComponent>())
{ {
var uid = trigger.Owner; var uid = trigger.Owner;
var environment = _atmosphereSystem.GetTileMixture(transform.GridUid, transform.MapUid, var environment = _atmosphereSystem.GetTileMixture(transform.GridUid, transform.MapUid,
@@ -37,7 +36,12 @@ public sealed class ArtifactHeatTriggerSystem : EntitySystem
if (environment.Temperature < trigger.ActivationTemperature) if (environment.Temperature < trigger.ActivationTemperature)
continue; 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) private bool CheckHot(EntityUid usedUid)
{ {
var hotEvent = new IsHotEvent(); var hotEvent = new IsHotEvent();
RaiseLocalEvent(usedUid, hotEvent, false); RaiseLocalEvent(usedUid, hotEvent);
return hotEvent.IsHot; return hotEvent.IsHot;
} }
} }

View File

@@ -1,7 +1,6 @@
using Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components; using Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Physics.Pull; using Content.Shared.Physics.Pull;
using Content.Shared.Weapons.Melee;
using Content.Shared.Weapons.Melee.Events; using Content.Shared.Weapons.Melee.Events;
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Systems; 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 IGameTiming _time = default!;
[Dependency] private readonly ArtifactSystem _artifactSystem = 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) public override void Update(float frameTime)
{ {
base.Update(frameTime); base.Update(frameTime);
var query = EntityManager.EntityQuery<ArtifactTimerTriggerComponent, ArtifactComponent>(); List<ArtifactComponent> toUpdate = new();
foreach (var (trigger, artifact) in query) foreach (var (trigger, artifact) in EntityQuery<ArtifactTimerTriggerComponent, ArtifactComponent>())
{ {
var timeDif = _time.CurTime - trigger.LastActivation; var timeDif = _time.CurTime - trigger.LastActivation;
if (timeDif <= trigger.ActivationRate) if (timeDif <= trigger.ActivationRate)
continue; continue;
_artifactSystem.TryActivateArtifact(trigger.Owner, component: artifact); toUpdate.Add(artifact);
trigger.LastActivation = _time.CurTime; 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 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 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-name-med-scanner-receiver = Medical scanner
signal-port-description-med-scanner-receiver = Medical scanner signal receiver 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 parent: 52
type: Transform type: Transform
- uid: 79 - uid: 79
type: AngryMobsSpawnArtifact type: RandomArtifactSpawner
components: components:
- pos: -0.5,-0.5 - pos: -0.5,-0.5
parent: 52 parent: 52

View File

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

View File

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

View File

@@ -7,3 +7,13 @@
cost: 500 cost: 500
category: Science category: Science
group: market 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: DoorRemoteResearch
- id: RubberStampRd - id: RubberStampRd
- id: ClothingHeadsetAltScience - id: ClothingHeadsetAltScience
- id: AnalysisComputerCircuitboard
- id: ArtifactAnalyzerMachineCircuitboard
- type: entity - type: entity
id: LockerHeadOfSecurityFilled id: LockerHeadOfSecurityFilled

View File

@@ -320,6 +320,8 @@
- SolarControlComputerCircuitboard - SolarControlComputerCircuitboard
- PowerComputerCircuitboard - PowerComputerCircuitboard
- GeneratorPlasmaMachineCircuitboard - GeneratorPlasmaMachineCircuitboard
- AnalysisComputerCircuitboard
- ArtifactAnalyzerMachineCircuitboard
- Signaller - Signaller
- SignalTrigger - SignalTrigger
- VoiceTrigger - 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 - texture: Objects/Specific/Xenoarchaeology/xeno_artifacts.rsi/ano01.png
- type: RandomSpawner - type: RandomSpawner
prototypes: prototypes:
- BadfeelingArtifact - SimpleXenoArtifact
- GoodfeelingArtifact - MediumXenoArtifact
- AngryMobsSpawnArtifact - MediumXenoArtifact
- JunkSpawnArtifact - MediumXenoArtifact
- BananaSpawnArtifact - ComplexXenoArtifact
- HeatArtifact - ComplexXenoArtifact
- ColdArtifact
- RadiateArtifact
- GasArtifact
- DiseaseArtifact
chance: 1 chance: 1
- type: entity - type: entity

View File

@@ -131,6 +131,22 @@
materialRequirements: materialRequirements:
Cable: 5 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 - type: entity
id: ThermomachineFreezerMachineCircuitBoard id: ThermomachineFreezerMachineCircuitBoard
parent: BaseMachineCircuitboard parent: BaseMachineCircuitboard

View File

@@ -145,6 +145,17 @@
- type: ComputerBoard - type: ComputerBoard
prototype: ComputerResearchAndDevelopment 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 - type: entity
parent: BaseComputerCircuitboard parent: BaseComputerCircuitboard
id: CrewMonitoringComputerCircuitboard id: CrewMonitoringComputerCircuitboard

View File

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

View File

@@ -10,6 +10,8 @@
sprite: Objects/Specific/Xenoarchaeology/xeno_artifacts.rsi sprite: Objects/Specific/Xenoarchaeology/xeno_artifacts.rsi
netsync: false netsync: false
state: ano01 state: ano01
noRot: true
- type: Damageable
- type: Physics - type: Physics
bodyType: Dynamic bodyType: Dynamic
- type: Transform - type: Transform
@@ -31,152 +33,34 @@
- type: Appearance - type: Appearance
visuals: visuals:
- type: RandomArtifactVisualizer - 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 - type: StaticPrice
price: 2000 price: 500
- 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
- type: entity - type: entity
parent: BaseXenoArtifact parent: BaseXenoArtifact
id: GoodfeelingArtifact id: SimpleXenoArtifact
suffix: Goodfeeling suffix: Simple
components: components:
- type: TelepathicArtifact - type: Artifact
messages: nodesMin: 2
- goodfeeling-artifact-1 nodesMax: 5
- 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: entity - type: entity
parent: BaseXenoArtifact parent: BaseXenoArtifact
id: JunkSpawnArtifact id: MediumXenoArtifact
suffix: Junk Spawn suffix: Medium
components: components:
- type: SpawnArtifact - type: Artifact
maxSpawns: 10 nodesMin: 5
possiblePrototypes: nodesMax: 9
- FoodPacketSyndiTrash
- FoodPacketSemkiTrash
- RandomInstruments
- ToySpawner
- type: entity - type: entity
parent: BaseXenoArtifact parent: BaseXenoArtifact
id: BananaSpawnArtifact id: ComplexXenoArtifact
suffix: Banana Spawn suffix: Complex
components: components:
- type: SpawnArtifact - type: Artifact
maxSpawns: 20 nodesMin: 9
possiblePrototypes: nodesMax: 13
- FoodBanana
- 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 energy: 1.6
color: "#b53ca1" 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 - type: entity
parent: BaseComputer parent: BaseComputer
id: ComputerId 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 - EmitterCircuitboard
- GasRecyclerMachineCircuitboard - GasRecyclerMachineCircuitboard
- SeedExtractorMachineCircuitboard - SeedExtractorMachineCircuitboard
- AnalysisComputerCircuitboard
- ArtifactAnalyzerMachineCircuitboard
- type: MaterialStorage - type: MaterialStorage
whitelist: whitelist:
tags: tags:

View File

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

View File

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

View File

@@ -62,3 +62,8 @@
id: MedicalScannerReceiver id: MedicalScannerReceiver
name: signal-port-name-med-scanner-receiver name: signal-port-name-med-scanner-receiver
description: signal-port-description-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 id: MedicalScannerSender
name: signal-port-name-med-scanner-sender name: signal-port-name-med-scanner-sender
description: signal-port-description-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 Glass: 900
Gold: 100 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 - type: latheRecipe
id: ReagentGrinderMachineCircuitboard id: ReagentGrinderMachineCircuitboard
@@ -186,6 +195,16 @@
Steel: 100 Steel: 100
Glass: 900 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 - type: latheRecipe
id: CrewMonitoringComputerCircuitboard id: CrewMonitoringComputerCircuitboard
icon: Objects/Misc/module.rsi/id_mod.png icon: Objects/Misc/module.rsi/id_mod.png

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