diff --git a/Content.Client/Chat/Managers/ChatManager.cs b/Content.Client/Chat/Managers/ChatManager.cs
index 67b5f5202f9..18f03cd7db0 100644
--- a/Content.Client/Chat/Managers/ChatManager.cs
+++ b/Content.Client/Chat/Managers/ChatManager.cs
@@ -14,7 +14,7 @@ internal sealed class ChatManager : IChatManager
[Dependency] private readonly IEntitySystemManager _systems = default!;
private ISawmill _sawmill = default!;
-
+ public event Action? PermissionsUpdated; //Nyano - Summary: need to be able to update perms for new psionics.
public void Initialize()
{
_sawmill = Logger.GetSawmill("chat");
@@ -67,9 +67,19 @@ public void SendMessage(string text, ChatSelectChannel channel)
_consoleHost.ExecuteCommand($"whisper \"{CommandParsing.Escape(str)}\"");
break;
+ //Nyano - Summary: sends the command for telepath communication.
+ case ChatSelectChannel.Telepathic:
+ _consoleHost.ExecuteCommand($"tsay \"{CommandParsing.Escape(str)}\"");
+ break;
+
default:
throw new ArgumentOutOfRangeException(nameof(channel), channel, null);
}
}
+ //Nyano - Summary: fires off the update permissions script.
+ public void UpdatePermissions()
+ {
+ PermissionsUpdated?.Invoke();
+ }
}
}
diff --git a/Content.Client/Chat/Managers/IChatManager.cs b/Content.Client/Chat/Managers/IChatManager.cs
index 6464ca10196..a21a8194fde 100644
--- a/Content.Client/Chat/Managers/IChatManager.cs
+++ b/Content.Client/Chat/Managers/IChatManager.cs
@@ -7,5 +7,11 @@ public interface IChatManager
void Initialize();
public void SendMessage(string text, ChatSelectChannel channel);
+
+ ///
+ /// Nyano - Summary:. Will refresh perms.
+ ///
+ event Action PermissionsUpdated;
+ public void UpdatePermissions();
}
}
diff --git a/Content.Client/Nyanotrasen/CartridgeLoader/Cartridges/GlimmerMonitorUi.cs b/Content.Client/Nyanotrasen/CartridgeLoader/Cartridges/GlimmerMonitorUi.cs
new file mode 100644
index 00000000000..0b5fc7ad38c
--- /dev/null
+++ b/Content.Client/Nyanotrasen/CartridgeLoader/Cartridges/GlimmerMonitorUi.cs
@@ -0,0 +1,39 @@
+using Robust.Client.GameObjects;
+using Robust.Client.UserInterface;
+using Content.Client.UserInterface.Fragments;
+using Content.Shared.CartridgeLoader.Cartridges;
+using Content.Shared.CartridgeLoader;
+
+namespace Content.Client.Nyanotrasen.CartridgeLoader.Cartridges;
+
+public sealed partial class GlimmerMonitorUi : UIFragment
+{
+ private GlimmerMonitorUiFragment? _fragment;
+
+ public override Control GetUIFragmentRoot()
+ {
+ return _fragment!;
+ }
+
+ public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner)
+ {
+ _fragment = new GlimmerMonitorUiFragment();
+
+ _fragment.OnSync += _ => SendSyncMessage(userInterface);
+ }
+
+ public override void UpdateState(BoundUserInterfaceState state)
+ {
+ if (state is not GlimmerMonitorUiState monitorState)
+ return;
+
+ _fragment?.UpdateState(monitorState.GlimmerValues);
+ }
+
+ private void SendSyncMessage(BoundUserInterface userInterface)
+ {
+ var syncMessage = new GlimmerMonitorSyncMessageEvent();
+ var message = new CartridgeUiMessage(syncMessage);
+ userInterface.SendMessage(message);
+ }
+}
diff --git a/Content.Client/Nyanotrasen/CartridgeLoader/Cartridges/GlimmerMonitorUiFragment.xaml b/Content.Client/Nyanotrasen/CartridgeLoader/Cartridges/GlimmerMonitorUiFragment.xaml
new file mode 100644
index 00000000000..119a1831e6e
--- /dev/null
+++ b/Content.Client/Nyanotrasen/CartridgeLoader/Cartridges/GlimmerMonitorUiFragment.xaml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Nyanotrasen/CartridgeLoader/Cartridges/GlimmerMonitorUiFragment.xaml.cs b/Content.Client/Nyanotrasen/CartridgeLoader/Cartridges/GlimmerMonitorUiFragment.xaml.cs
new file mode 100644
index 00000000000..149c590d91a
--- /dev/null
+++ b/Content.Client/Nyanotrasen/CartridgeLoader/Cartridges/GlimmerMonitorUiFragment.xaml.cs
@@ -0,0 +1,116 @@
+using System.Linq;
+using System.Numerics;
+using Robust.Client.AutoGenerated;
+using Robust.Client.ResourceManagement;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Content.Client.Nyanotrasen.UserInterface.CustomControls;
+
+namespace Content.Client.Nyanotrasen.CartridgeLoader.Cartridges;
+
+[GenerateTypedNameReferences]
+public sealed partial class GlimmerMonitorUiFragment : BoxContainer
+{
+ [Dependency] private readonly IResourceCache _resourceCache = default!;
+
+ public event Action? OnSync;
+ private List _cachedValues = new();
+
+ public GlimmerMonitorUiFragment()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+
+ Orientation = LayoutOrientation.Vertical;
+ HorizontalExpand = true;
+ VerticalExpand = true;
+
+ var intervalGroup = new ButtonGroup();
+ IntervalButton1.Group = intervalGroup;
+ IntervalButton5.Group = intervalGroup;
+ IntervalButton10.Group = intervalGroup;
+
+ IntervalButton1.Pressed = true;
+
+ IntervalButton1.OnPressed += _ => UpdateState(_cachedValues);
+ IntervalButton5.OnPressed += _ => UpdateState(_cachedValues);
+ IntervalButton10.OnPressed += _ => UpdateState(_cachedValues);
+
+ SyncButton.OnPressed += _ => OnSync?.Invoke(true);
+ }
+
+ public void UpdateState(List glimmerValues)
+ {
+ _cachedValues = glimmerValues;
+ if (glimmerValues.Count < 1)
+ return;
+
+ MonitorBox.RemoveAllChildren();
+
+ var glimmerLabel = new Label();
+ glimmerLabel.Text = Loc.GetString("glimmer-monitor-current-glimmer", ("glimmer", glimmerValues[^1]));
+ MonitorBox.AddChild(glimmerLabel);
+
+ var formattedValues = FormatGlimmerValues(glimmerValues);
+ var graph = new GlimmerGraph(_resourceCache, formattedValues);
+ graph.SetSize = new Vector2(450, 250);
+ MonitorBox.AddChild(graph);
+ }
+
+
+ private List FormatGlimmerValues(List glimmerValues)
+ {
+ var returnList = glimmerValues;
+
+ if (IntervalButton5.Pressed)
+ {
+ returnList = GetAveragedList(glimmerValues, 5);
+ }
+ else if (IntervalButton10.Pressed)
+ {
+ returnList = GetAveragedList(glimmerValues, 10);
+ }
+
+ return ClipToFifteen(returnList);
+ }
+
+ ///
+ /// Format glimmer values to get <=15 data points correctly.
+ ///
+ private List ClipToFifteen(List glimmerValues)
+ {
+ List returnList;
+
+ if (glimmerValues.Count <= 15)
+ {
+ returnList = glimmerValues;
+ }
+ else
+ {
+ returnList = glimmerValues.Skip(glimmerValues.Count - 15).ToList();
+ }
+
+ return returnList;
+ }
+
+ private List GetAveragedList(IEnumerable glimmerValues, int interval)
+ {
+ var returnList = new List();
+ var subtotal = 0;
+ var elementsPassed = 0;
+ for (int i = 0; i < glimmerValues.Count(); ++i)
+ {
+ subtotal += glimmerValues.ElementAt(i);
+ ++elementsPassed;
+ if (elementsPassed == interval)
+ {
+ returnList.Add(subtotal / interval);
+ subtotal = 0;
+ elementsPassed = 0;
+ }
+ }
+ if (elementsPassed != 0)
+ returnList.Add(subtotal / elementsPassed);
+ return returnList;
+ }
+}
diff --git a/Content.Client/Nyanotrasen/Chat/PsionicChatUpdateSystem.cs b/Content.Client/Nyanotrasen/Chat/PsionicChatUpdateSystem.cs
new file mode 100644
index 00000000000..84602052fe7
--- /dev/null
+++ b/Content.Client/Nyanotrasen/Chat/PsionicChatUpdateSystem.cs
@@ -0,0 +1,32 @@
+using Content.Shared.Abilities.Psionics;
+using Content.Client.Chat.Managers;
+using Robust.Client.Player;
+
+namespace Content.Client.Nyanotrasen.Chat
+{
+ public sealed class PsionicChatUpdateSystem : EntitySystem
+ {
+ [Dependency] private readonly IChatManager _chatManager = default!;
+ [Dependency] private readonly IPlayerManager _playerManager = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnInit);
+ SubscribeLocalEvent(OnRemove);
+ }
+
+ public PsionicComponent? Player => CompOrNull(_playerManager.LocalPlayer?.ControlledEntity);
+ public bool IsPsionic => Player != null;
+
+ private void OnInit(EntityUid uid, PsionicComponent component, ComponentInit args)
+ {
+ _chatManager.UpdatePermissions();
+ }
+
+ private void OnRemove(EntityUid uid, PsionicComponent component, ComponentRemove args)
+ {
+ _chatManager.UpdatePermissions();
+ }
+ }
+}
diff --git a/Content.Client/Nyanotrasen/Psionics/Glimmer/GlimmerReactiveVisuals.cs b/Content.Client/Nyanotrasen/Psionics/Glimmer/GlimmerReactiveVisuals.cs
new file mode 100644
index 00000000000..9d1951ab3cc
--- /dev/null
+++ b/Content.Client/Nyanotrasen/Psionics/Glimmer/GlimmerReactiveVisuals.cs
@@ -0,0 +1,6 @@
+namespace Content.Client.Psionics.Glimmer;
+
+public enum GlimmerReactiveVisualLayers : byte
+{
+ GlimmerEffect,
+}
diff --git a/Content.Client/Nyanotrasen/Psionics/UI/AcceptPsionicsEUI.cs b/Content.Client/Nyanotrasen/Psionics/UI/AcceptPsionicsEUI.cs
new file mode 100644
index 00000000000..87d11a92eeb
--- /dev/null
+++ b/Content.Client/Nyanotrasen/Psionics/UI/AcceptPsionicsEUI.cs
@@ -0,0 +1,42 @@
+using Content.Client.Eui;
+using Content.Shared.Psionics;
+using JetBrains.Annotations;
+using Robust.Client.Graphics;
+
+namespace Content.Client.Psionics.UI
+{
+ [UsedImplicitly]
+ public sealed class AcceptPsionicsEui : BaseEui
+ {
+ private readonly AcceptPsionicsWindow _window;
+
+ public AcceptPsionicsEui()
+ {
+ _window = new AcceptPsionicsWindow();
+
+ _window.DenyButton.OnPressed += _ =>
+ {
+ SendMessage(new AcceptPsionicsChoiceMessage(AcceptPsionicsUiButton.Deny));
+ _window.Close();
+ };
+
+ _window.AcceptButton.OnPressed += _ =>
+ {
+ SendMessage(new AcceptPsionicsChoiceMessage(AcceptPsionicsUiButton.Accept));
+ _window.Close();
+ };
+ }
+
+ public override void Opened()
+ {
+ IoCManager.Resolve().RequestWindowAttention();
+ _window.OpenCentered();
+ }
+
+ public override void Closed()
+ {
+ _window.Close();
+ }
+
+ }
+}
diff --git a/Content.Client/Nyanotrasen/Psionics/UI/AcceptPsionicsWindow.cs b/Content.Client/Nyanotrasen/Psionics/UI/AcceptPsionicsWindow.cs
new file mode 100644
index 00000000000..883d9f07972
--- /dev/null
+++ b/Content.Client/Nyanotrasen/Psionics/UI/AcceptPsionicsWindow.cs
@@ -0,0 +1,62 @@
+using System.Numerics;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Shared.Localization;
+using static Robust.Client.UserInterface.Controls.BoxContainer;
+
+namespace Content.Client.Psionics.UI
+{
+ public sealed class AcceptPsionicsWindow : DefaultWindow
+ {
+ public readonly Button DenyButton;
+ public readonly Button AcceptButton;
+
+ public AcceptPsionicsWindow()
+ {
+
+ Title = Loc.GetString("accept-psionics-window-title");
+
+ Contents.AddChild(new BoxContainer
+ {
+ Orientation = LayoutOrientation.Vertical,
+ Children =
+ {
+ new BoxContainer
+ {
+ Orientation = LayoutOrientation.Vertical,
+ Children =
+ {
+ (new Label()
+ {
+ Text = Loc.GetString("accept-psionics-window-prompt-text-part")
+ }),
+ new BoxContainer
+ {
+ Orientation = LayoutOrientation.Horizontal,
+ Align = AlignMode.Center,
+ Children =
+ {
+ (AcceptButton = new Button
+ {
+ Text = Loc.GetString("accept-cloning-window-accept-button"),
+ }),
+
+ (new Control()
+ {
+ MinSize = new Vector2(20, 0)
+ }),
+
+ (DenyButton = new Button
+ {
+ Text = Loc.GetString("accept-cloning-window-deny-button"),
+ })
+ }
+ },
+ }
+ },
+ }
+ });
+ }
+ }
+}
diff --git a/Content.Client/Nyanotrasen/UserInterface/GlimmerGraph.cs b/Content.Client/Nyanotrasen/UserInterface/GlimmerGraph.cs
new file mode 100644
index 00000000000..b121186b14a
--- /dev/null
+++ b/Content.Client/Nyanotrasen/UserInterface/GlimmerGraph.cs
@@ -0,0 +1,52 @@
+using System.Numerics;
+using Robust.Client.UserInterface;
+using Robust.Client.Graphics;
+using Robust.Client.ResourceManagement;
+using Content.Client.Resources;
+
+namespace Content.Client.Nyanotrasen.UserInterface.CustomControls;
+
+public sealed class GlimmerGraph : Control
+{
+ private readonly IResourceCache _resourceCache;
+ private readonly List _glimmer;
+ private const int XOffset = 15;
+ private const int YOffset = 210;
+ private const int Length = 450;
+ private static int YOffsetTop => YOffset - 200;
+
+ public GlimmerGraph(IResourceCache resourceCache, List glimmer)
+ {
+ _resourceCache = resourceCache;
+ _glimmer = glimmer;
+ HorizontalAlignment = HAlignment.Left;
+ VerticalAlignment = VAlignment.Bottom;
+ }
+
+ protected override void Draw(DrawingHandleScreen handle)
+ {
+ base.Draw(handle);
+ var box = new UIBox2(new Vector2(XOffset, YOffset), new Vector2(XOffset + Length, YOffsetTop));
+ handle.DrawRect(box, Color.FromHex("#424245"));
+ var texture = _resourceCache.GetTexture("/Textures/Interface/glimmerGraph.png");
+ handle.DrawTexture(texture, new Vector2(XOffset, YOffsetTop));
+
+ if (_glimmer.Count < 2)
+ return;
+
+ var spacing = Length / (_glimmer.Count - 1);
+
+ var i = 0;
+ while (i + 1 < _glimmer.Count)
+ {
+ var vector1 = new Vector2(XOffset + i * spacing, YOffset - _glimmer[i] / 5);
+ var vector2 = new Vector2(XOffset + (i + 1) * spacing, YOffset - _glimmer[i + 1] / 5);
+ handle.DrawLine(vector1, vector2, Color.FromHex("#A200BB"));
+ handle.DrawLine(vector1 + new Vector2(0, 1), vector2 + new Vector2(0, 1), Color.FromHex("#A200BB"));
+ handle.DrawLine(vector1 - new Vector2(0, 1), vector2 - new Vector2(0, 1), Color.FromHex("#A200BB"));
+ handle.DrawLine(new Vector2(XOffset + i * spacing, YOffset), new Vector2(XOffset + i * spacing, YOffsetTop), Color.FromHex("#686868"));
+ i++;
+ }
+ }
+}
+
diff --git a/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs b/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs
index 98813992967..314b1e39e76 100644
--- a/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs
+++ b/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs
@@ -35,6 +35,7 @@
using Robust.Shared.Replays;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
+using Content.Client.Nyanotrasen.Chat; //Nyano - Summary: chat namespace.
namespace Content.Client.UserInterface.Systems.Chat;
@@ -56,6 +57,7 @@ public sealed class ChatUIController : UIController
[UISystemDependency] private readonly GhostSystem? _ghost = default;
[UISystemDependency] private readonly TypingIndicatorSystem? _typingIndicator = default;
[UISystemDependency] private readonly ChatSystem? _chatSys = default;
+ [UISystemDependency] private readonly PsionicChatUpdateSystem? _psionic = default!; //Nyano - Summary: makes the psionic chat available.
private ISawmill _sawmill = default!;
@@ -70,7 +72,8 @@ public sealed class ChatUIController : UIController
{SharedChatSystem.EmotesAltPrefix, ChatSelectChannel.Emotes},
{SharedChatSystem.AdminPrefix, ChatSelectChannel.Admin},
{SharedChatSystem.RadioCommonPrefix, ChatSelectChannel.Radio},
- {SharedChatSystem.DeadPrefix, ChatSelectChannel.Dead}
+ {SharedChatSystem.DeadPrefix, ChatSelectChannel.Dead},
+ {SharedChatSystem.TelepathicPrefix, ChatSelectChannel.Telepathic} //Nyano - Summary: adds the telepathic prefix =.
};
public static readonly Dictionary ChannelPrefixes = new()
@@ -83,7 +86,8 @@ public sealed class ChatUIController : UIController
{ChatSelectChannel.Emotes, SharedChatSystem.EmotesPrefix},
{ChatSelectChannel.Admin, SharedChatSystem.AdminPrefix},
{ChatSelectChannel.Radio, SharedChatSystem.RadioCommonPrefix},
- {ChatSelectChannel.Dead, SharedChatSystem.DeadPrefix}
+ {ChatSelectChannel.Dead, SharedChatSystem.DeadPrefix},
+ {ChatSelectChannel.Telepathic, SharedChatSystem.TelepathicPrefix } //Nyano - Summary: associates telepathic with =.
};
///
@@ -163,6 +167,7 @@ public override void Initialize()
_sawmill = Logger.GetSawmill("chat");
_sawmill.Level = LogLevel.Info;
_admin.AdminStatusUpdated += UpdateChannelPermissions;
+ _manager.PermissionsUpdated += UpdateChannelPermissions; //Nyano - Summary: the event for when permissions are updated for psionics.
_player.LocalPlayerChanged += OnLocalPlayerChanged;
_state.OnStateChanged += StateChanged;
_net.RegisterNetMessage(OnChatMessage);
@@ -524,8 +529,17 @@ private void UpdateChannelPermissions()
FilterableChannels |= ChatChannel.AdminAlert;
FilterableChannels |= ChatChannel.AdminChat;
CanSendChannels |= ChatSelectChannel.Admin;
+ FilterableChannels |= ChatChannel.Telepathic; //Nyano - Summary: makes admins able to see psionic chat.
}
+ // Nyano - Summary: - Begin modified code block to add telepathic as a channel for a psionic user.
+ if (_psionic != null && _psionic.IsPsionic)
+ {
+ FilterableChannels |= ChatChannel.Telepathic;
+ CanSendChannels |= ChatSelectChannel.Telepathic;
+ }
+ // /Nyano - End modified code block
+
SelectableChannels = CanSendChannels;
// Necessary so that we always have a channel to fall back to.
diff --git a/Content.Client/UserInterface/Systems/Chat/Controls/ChannelFilterPopup.xaml.cs b/Content.Client/UserInterface/Systems/Chat/Controls/ChannelFilterPopup.xaml.cs
index 4a3b9aa568e..daf124306cc 100644
--- a/Content.Client/UserInterface/Systems/Chat/Controls/ChannelFilterPopup.xaml.cs
+++ b/Content.Client/UserInterface/Systems/Chat/Controls/ChannelFilterPopup.xaml.cs
@@ -16,6 +16,7 @@ public sealed partial class ChannelFilterPopup : Popup
ChatChannel.Whisper,
ChatChannel.Emotes,
ChatChannel.Radio,
+ ChatChannel.Telepathic, //Nyano - Summary: adds telepathic chat to where it belongs in order in the chat.
ChatChannel.LOOC,
ChatChannel.OOC,
ChatChannel.Dead,
diff --git a/Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorButton.cs b/Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorButton.cs
index 96a7594ff6f..e28cd2bd44b 100644
--- a/Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorButton.cs
+++ b/Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorButton.cs
@@ -82,6 +82,7 @@ public Color ChannelSelectColor(ChatSelectChannel channel)
ChatSelectChannel.OOC => Color.LightSkyBlue,
ChatSelectChannel.Dead => Color.MediumPurple,
ChatSelectChannel.Admin => Color.HotPink,
+ ChatSelectChannel.Telepathic => Color.PaleVioletRed, //Nyano - Summary: determines the color for the chat.
_ => Color.DarkGray
};
}
diff --git a/Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorPopup.cs b/Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorPopup.cs
index 0852c10bb91..c1f3559d793 100644
--- a/Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorPopup.cs
+++ b/Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorPopup.cs
@@ -13,6 +13,7 @@ public sealed class ChannelSelectorPopup : Popup
ChatSelectChannel.Whisper,
ChatSelectChannel.Emotes,
ChatSelectChannel.Radio,
+ ChatSelectChannel.Telepathic, //Nyano - Summary: determines the order in which telepathic shows.
ChatSelectChannel.LOOC,
ChatSelectChannel.OOC,
ChatSelectChannel.Dead,
diff --git a/Content.Server/Anomaly/AnomalySystem.Psionics.cs b/Content.Server/Anomaly/AnomalySystem.Psionics.cs
new file mode 100644
index 00000000000..95fda1d5035
--- /dev/null
+++ b/Content.Server/Anomaly/AnomalySystem.Psionics.cs
@@ -0,0 +1,25 @@
+using Content.Server.Abilities.Psionics; //Nyano - Summary: the psniocs bin where dispel is located.
+using Content.Shared.Anomaly;
+using Content.Shared.Anomaly.Components;
+using Robust.Shared.Random;
+
+namespace Content.Server.Anomaly;
+
+public sealed partial class AnomalySystem
+{
+ [Dependency] private readonly SharedAnomalySystem _sharedAnomaly = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly DispelPowerSystem _dispel = default!;
+ private void InitializePsionics()
+ {
+ SubscribeLocalEvent(OnDispelled);
+ }
+
+ //Nyano - Summary: gives dispellable behavior to Anomalies.
+ private void OnDispelled(EntityUid uid, AnomalyComponent component, DispelledEvent args)
+ {
+ _dispel.DealDispelDamage(uid);
+ _sharedAnomaly.ChangeAnomalyHealth(uid, 0 - _random.NextFloat(0.4f, 0.8f), component);
+ args.Handled = true;
+ }
+}
diff --git a/Content.Server/Anomaly/AnomalySystem.Vessel.cs b/Content.Server/Anomaly/AnomalySystem.Vessel.cs
index 02c435d2425..c37b38b1f16 100644
--- a/Content.Server/Anomaly/AnomalySystem.Vessel.cs
+++ b/Content.Server/Anomaly/AnomalySystem.Vessel.cs
@@ -6,6 +6,7 @@
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Research.Components;
+using Content.Server.Psionics.Glimmer;
namespace Content.Server.Anomaly;
@@ -91,6 +92,14 @@ private void OnVesselInteractUsing(EntityUid uid, AnomalyVesselComponent compone
if (!TryComp(anomaly, out var anomalyComponent) || anomalyComponent.ConnectedVessel != null)
return;
+ // Nyano - Summary - Begin modified code block: tie anomaly harvesting to glimmer rate.
+ if (this.IsPowered(uid, EntityManager) &&
+ TryComp(anomaly, out var glimmerSource))
+ {
+ glimmerSource.Active = true;
+ }
+ // Nyano - End modified code block.
+
component.Anomaly = scanner.ScannedAnomaly;
anomalyComponent.ConnectedVessel = uid;
UpdateVesselAppearance(uid, component);
diff --git a/Content.Server/Anomaly/AnomalySystem.cs b/Content.Server/Anomaly/AnomalySystem.cs
index 5f6220f386f..74d6da951fc 100644
--- a/Content.Server/Anomaly/AnomalySystem.cs
+++ b/Content.Server/Anomaly/AnomalySystem.cs
@@ -44,6 +44,7 @@ public override void Initialize()
SubscribeLocalEvent(OnShutdown);
SubscribeLocalEvent(OnStartCollide);
+ InitializePsionics(); //Nyano - Summary: stats up psionic related behavior.
InitializeGenerator();
InitializeScanner();
InitializeVessel();
diff --git a/Content.Server/Chat/Systems/ChatSystem.cs b/Content.Server/Chat/Systems/ChatSystem.cs
index b8f4e116a45..02ab021cc69 100644
--- a/Content.Server/Chat/Systems/ChatSystem.cs
+++ b/Content.Server/Chat/Systems/ChatSystem.cs
@@ -6,6 +6,7 @@
using Content.Server.Chat.Managers;
using Content.Server.GameTicking;
using Content.Server.Players;
+using Content.Server.Nyanotrasen.Chat;
using Content.Server.Station.Components;
using Content.Server.Station.Systems;
using Content.Shared.ActionBlocker;
@@ -54,6 +55,9 @@ public sealed partial class ChatSystem : SharedChatSystem
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
+ //Nyano - Summary: pulls in the nyano chat system for psionics.
+ [Dependency] private readonly NyanoChatSystem _nyanoChatSystem = default!;
+
public const int VoiceRange = 10; // how far voice goes in world units
public const int WhisperClearRange = 2; // how far whisper goes while still being understandable, in world units
public const int WhisperMuffledRange = 5; // how far whisper goes at all, in world units
@@ -238,6 +242,10 @@ public void TrySendInGameICMessage(
case InGameICChatType.Emote:
SendEntityEmote(source, message, range, nameOverride, hideLog: hideLog, ignoreActionBlocker: ignoreActionBlocker);
break;
+ //Nyano - Summary: case adds the telepathic chat sending ability.
+ case InGameICChatType.Telepathic:
+ _nyanoChatSystem.SendTelepathicChat(source, message, range == ChatTransmitRange.HideChat);
+ break;
}
}
@@ -867,7 +875,8 @@ public enum InGameICChatType : byte
{
Speak,
Emote,
- Whisper
+ Whisper,
+ Telepathic //Nyano - Summary: adds telepathic as a type of message users can receive.
}
///
diff --git a/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs b/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs
index be8dbbaf52a..b0eb9c4abc8 100644
--- a/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs
+++ b/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs
@@ -3,6 +3,7 @@
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Mind.Components;
using Robust.Shared.Prototypes;
+using Content.Server.Psionics; //Nyano - Summary: pulls in the ability for the sentient creature to become psionic.
namespace Content.Server.Chemistry.ReagentEffects;
@@ -36,6 +37,7 @@ public override void Effect(ReagentEffectArgs args)
ghostRole = entityManager.AddComponent(uid);
entityManager.EnsureComponent(uid);
+ entityManager.EnsureComponent(uid); //Nyano - Summary:. Makes the animated body able to get psionics.
var entityData = entityManager.GetComponent(uid);
ghostRole.RoleName = entityData.EntityName;
diff --git a/Content.Server/Cloning/CloningSystem.cs b/Content.Server/Cloning/CloningSystem.cs
index e94f0d11839..c358ecd759f 100644
--- a/Content.Server/Cloning/CloningSystem.cs
+++ b/Content.Server/Cloning/CloningSystem.cs
@@ -35,6 +35,7 @@
using Robust.Shared.Physics.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
+using Content.Server.Psionics; //Nyano - Summary: allows the potential psionic ability to be written to the character.
namespace Content.Server.Cloning
{
@@ -241,6 +242,9 @@ public bool TryCloning(EntityUid uid, EntityUid bodyToClone, MindComponent mind,
var mob = Spawn(speciesPrototype.Prototype, Transform(uid).MapPosition);
_humanoidSystem.CloneAppearance(bodyToClone, mob);
+ ///Nyano - Summary: adds the potential psionic trait to the reanimated mob.
+ EnsureComp(mob);
+
var ev = new CloningEvent(bodyToClone, mob);
RaiseLocalEvent(bodyToClone, ref ev);
diff --git a/Content.Server/NPC/Components/NpcFactionMemberComponent.cs b/Content.Server/NPC/Components/NpcFactionMemberComponent.cs
index 72df5d0c8ab..ce7e59ea2c7 100644
--- a/Content.Server/NPC/Components/NpcFactionMemberComponent.cs
+++ b/Content.Server/NPC/Components/NpcFactionMemberComponent.cs
@@ -25,5 +25,13 @@ public sealed partial class NpcFactionMemberComponent : Component
///
[ViewVariables]
public readonly HashSet HostileFactions = new();
+
+ // Nyano - Summary - Begin modified code block: support for specific entities to be friendly.
+ ///
+ /// Permanently friendly specific entities. Our summoner, etc.
+ /// Would like to separate. Could I do that by extending this method, maybe?
+ ///
+ public HashSet ExceptionalFriendlies = new();
+ // Nyano - End modified code block.
}
}
diff --git a/Content.Server/NPC/Systems/NpcFactionSystem.cs b/Content.Server/NPC/Systems/NpcFactionSystem.cs
index 79c4bbc7af0..68ec49870de 100644
--- a/Content.Server/NPC/Systems/NpcFactionSystem.cs
+++ b/Content.Server/NPC/Systems/NpcFactionSystem.cs
@@ -6,8 +6,9 @@ namespace Content.Server.NPC.Systems;
///
/// Outlines faction relationships with each other.
+/// part of psionics rework was making this a partial class. Should've already been handled upstream, based on the linter.
///
-public sealed class NpcFactionSystem : EntitySystem
+public sealed partial class NpcFactionSystem : EntitySystem
{
[Dependency] private readonly FactionExceptionSystem _factionException = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
diff --git a/Content.Server/Nyanotrasen/Abilities/Psionics/Abilities/DispelPowerSystem.cs b/Content.Server/Nyanotrasen/Abilities/Psionics/Abilities/DispelPowerSystem.cs
new file mode 100644
index 00000000000..b67390cf040
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Abilities/Psionics/Abilities/DispelPowerSystem.cs
@@ -0,0 +1,140 @@
+using Content.Shared.Actions;
+using Content.Shared.StatusEffect;
+using Content.Shared.Abilities.Psionics;
+using Content.Shared.Damage;
+using Content.Shared.Revenant.Components;
+using Content.Server.Guardian;
+using Content.Server.Bible.Components;
+using Content.Server.Popups;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Player;
+using Robust.Shared.Random;
+using Robust.Shared.Timing;
+using Content.Shared.Mind;
+using Content.Shared.Actions.Events;
+
+namespace Content.Server.Abilities.Psionics
+{
+ public sealed class DispelPowerSystem : EntitySystem
+ {
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly StatusEffectsSystem _statusEffects = default!;
+ [Dependency] private readonly SharedActionsSystem _actions = default!;
+ [Dependency] private readonly DamageableSystem _damageableSystem = default!;
+ [Dependency] private readonly GuardianSystem _guardianSystem = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly SharedPsionicAbilitiesSystem _psionics = default!;
+ [Dependency] private readonly SharedAudioSystem _audioSystem = default!;
+ [Dependency] private readonly PopupSystem _popupSystem = default!;
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+ [Dependency] private readonly SharedMindSystem _mindSystem = default!;
+
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnInit);
+ SubscribeLocalEvent(OnShutdown);
+ SubscribeLocalEvent(OnPowerUsed);
+
+ SubscribeLocalEvent(OnDispelled);
+ SubscribeLocalEvent(OnDmgDispelled);
+ // Upstream stuff we're just gonna handle here
+ SubscribeLocalEvent(OnGuardianDispelled);
+ SubscribeLocalEvent(OnFamiliarDispelled);
+ SubscribeLocalEvent(OnRevenantDispelled);
+ }
+
+ private void OnInit(EntityUid uid, DispelPowerComponent component, ComponentInit args)
+ {
+ _actions.AddAction(uid, ref component.DispelActionEntity, component.DispelActionId );
+ _actions.TryGetActionData( component.DispelActionEntity, out var actionData );
+ if (actionData is { UseDelay: not null })
+ _actions.StartUseDelay(component.DispelActionEntity);
+ if (TryComp(uid, out var psionic) && psionic.PsionicAbility == null)
+ psionic.PsionicAbility = component.DispelActionEntity;
+ }
+
+ private void OnShutdown(EntityUid uid, DispelPowerComponent component, ComponentShutdown args)
+ {
+ _actions.RemoveAction(uid, component.DispelActionEntity);
+ }
+
+ private void OnPowerUsed(DispelPowerActionEvent args)
+ {
+ if (HasComp(args.Target))
+ return;
+
+ var ev = new DispelledEvent();
+ RaiseLocalEvent(args.Target, ev, false);
+
+ if (ev.Handled)
+ {
+ args.Handled = true;
+ _psionics.LogPowerUsed(args.Performer, "dispel");
+ }
+ }
+
+ private void OnDispelled(EntityUid uid, DispellableComponent component, DispelledEvent args)
+ {
+ QueueDel(uid);
+ Spawn("Ash", Transform(uid).Coordinates);
+ _popupSystem.PopupCoordinates(Loc.GetString("psionic-burns-up", ("item", uid)), Transform(uid).Coordinates, Filter.Pvs(uid), true, Shared.Popups.PopupType.MediumCaution);
+ _audioSystem.Play("/Audio/Effects/lightburn.ogg", Filter.Pvs(uid), uid, true);
+ args.Handled = true;
+ }
+
+ private void OnDmgDispelled(EntityUid uid, DamageOnDispelComponent component, DispelledEvent args)
+ {
+ var damage = component.Damage;
+ var modifier = (1 + component.Variance) - (_random.NextFloat(0, component.Variance * 2));
+
+ damage *= modifier;
+ DealDispelDamage(uid, damage);
+ args.Handled = true;
+ }
+
+ private void OnGuardianDispelled(EntityUid uid, GuardianComponent guardian, DispelledEvent args)
+ {
+ if (TryComp(guardian.Host, out var host))
+ _guardianSystem.ToggleGuardian(guardian.Host, host);
+
+ DealDispelDamage(uid);
+ args.Handled = true;
+ }
+
+ private void OnFamiliarDispelled(EntityUid uid, FamiliarComponent component, DispelledEvent args)
+ {
+ if (component.Source != null)
+ EnsureComp(component.Source.Value);
+
+ args.Handled = true;
+ }
+
+ private void OnRevenantDispelled(EntityUid uid, RevenantComponent component, DispelledEvent args)
+ {
+ DealDispelDamage(uid);
+ _statusEffects.TryAddStatusEffect(uid, "Corporeal", TimeSpan.FromSeconds(30), false, "Corporeal");
+ args.Handled = true;
+ }
+
+ public void DealDispelDamage(EntityUid uid, DamageSpecifier? damage = null)
+ {
+ if (Deleted(uid))
+ return;
+
+ _popupSystem.PopupCoordinates(Loc.GetString("psionic-burn-resist", ("item", uid)), Transform(uid).Coordinates, Filter.Pvs(uid), true, Shared.Popups.PopupType.SmallCaution);
+ _audioSystem.Play("/Audio/Effects/lightburn.ogg", Filter.Pvs(uid), uid, true);
+
+ if (damage == null)
+ {
+ damage = new();
+ damage.DamageDict.Add("Blunt", 100);
+ }
+ _damageableSystem.TryChangeDamage(uid, damage, true, true);
+ }
+ }
+ public sealed class DispelledEvent : HandledEntityEventArgs {}
+}
+
+
diff --git a/Content.Server/Nyanotrasen/Abilities/Psionics/Abilities/MetapsionicPowerSystem.cs b/Content.Server/Nyanotrasen/Abilities/Psionics/Abilities/MetapsionicPowerSystem.cs
new file mode 100644
index 00000000000..91df3a093dc
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Abilities/Psionics/Abilities/MetapsionicPowerSystem.cs
@@ -0,0 +1,66 @@
+using Content.Shared.Actions;
+using Content.Shared.Abilities.Psionics;
+using Content.Shared.StatusEffect;
+using Content.Shared.Popups;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
+using Content.Shared.Mind;
+using Content.Shared.Actions.Events;
+
+namespace Content.Server.Abilities.Psionics
+{
+ public sealed class MetapsionicPowerSystem : EntitySystem
+ {
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly StatusEffectsSystem _statusEffects = default!;
+ [Dependency] private readonly SharedActionsSystem _actions = default!;
+ [Dependency] private readonly EntityLookupSystem _lookup = default!;
+ [Dependency] private readonly SharedPopupSystem _popups = default!;
+ [Dependency] private readonly SharedPsionicAbilitiesSystem _psionics = default!;
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+ [Dependency] private readonly SharedMindSystem _mindSystem = default!;
+
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnInit);
+ SubscribeLocalEvent(OnShutdown);
+ SubscribeLocalEvent(OnPowerUsed);
+ }
+
+ private void OnInit(EntityUid uid, MetapsionicPowerComponent component, ComponentInit args)
+ {
+ _actions.AddAction(uid, ref component.MetapsionicActionEntity, component.MetapsionicActionId );
+ _actions.TryGetActionData( component.MetapsionicActionEntity, out var actionData );
+ if (actionData is { UseDelay: not null })
+ _actions.StartUseDelay(component.MetapsionicActionEntity);
+ if (TryComp(uid, out var psionic) && psionic.PsionicAbility == null)
+ psionic.PsionicAbility = component.MetapsionicActionEntity;
+
+ }
+
+ private void OnShutdown(EntityUid uid, MetapsionicPowerComponent component, ComponentShutdown args)
+ {
+ _actions.RemoveAction(uid, component.MetapsionicActionEntity);
+ }
+
+ private void OnPowerUsed(EntityUid uid, MetapsionicPowerComponent component, MetapsionicPowerActionEvent args)
+ {
+ foreach (var entity in _lookup.GetEntitiesInRange(uid, component.Range))
+ {
+ if (HasComp(entity) && entity != uid && !HasComp(entity) &&
+ !(HasComp(entity) && Transform(entity).ParentUid == uid))
+ {
+ _popups.PopupEntity(Loc.GetString("metapsionic-pulse-success"), uid, uid, PopupType.LargeCaution);
+ args.Handled = true;
+ return;
+ }
+ }
+ _popups.PopupEntity(Loc.GetString("metapsionic-pulse-failure"), uid, uid, PopupType.Large);
+ _psionics.LogPowerUsed(uid, "metapsionic pulse", 2, 4);
+
+ args.Handled = true;
+ }
+ }
+}
diff --git a/Content.Server/Nyanotrasen/Abilities/Psionics/Abilities/MindSwapPowerSystem.cs b/Content.Server/Nyanotrasen/Abilities/Psionics/Abilities/MindSwapPowerSystem.cs
new file mode 100644
index 00000000000..62372dcca3b
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Abilities/Psionics/Abilities/MindSwapPowerSystem.cs
@@ -0,0 +1,212 @@
+using Content.Shared.Actions;
+using Content.Shared.Abilities.Psionics;
+using Content.Shared.Speech;
+using Content.Shared.Stealth.Components;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Mobs;
+using Content.Shared.Damage;
+using Content.Server.Mind;
+using Content.Shared.Mobs.Systems;
+using Content.Server.Popups;
+using Content.Server.Psionics;
+using Content.Server.GameTicking;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
+using Content.Shared.Mind;
+using Content.Shared.Actions.Events;
+
+namespace Content.Server.Abilities.Psionics
+{
+ public sealed class MindSwapPowerSystem : EntitySystem
+ {
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly SharedActionsSystem _actions = default!;
+ [Dependency] private readonly MobStateSystem _mobStateSystem = default!;
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+ [Dependency] private readonly SharedPsionicAbilitiesSystem _psionics = default!;
+ [Dependency] private readonly PopupSystem _popupSystem = default!;
+ [Dependency] private readonly MindSystem _mindSystem = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnInit);
+ SubscribeLocalEvent(OnShutdown);
+ SubscribeLocalEvent(OnPowerUsed);
+ SubscribeLocalEvent(OnPowerReturned);
+ SubscribeLocalEvent(OnDispelled);
+ SubscribeLocalEvent(OnMobStateChanged);
+ SubscribeLocalEvent(OnGhostAttempt);
+ //
+ SubscribeLocalEvent(OnSwapInit);
+ }
+
+ private void OnInit(EntityUid uid, MindSwapPowerComponent component, ComponentInit args)
+ {
+ _actions.AddAction(uid, ref component.MindSwapActionEntity, component.MindSwapActionId );
+ _actions.TryGetActionData( component.MindSwapActionEntity, out var actionData );
+ if (actionData is { UseDelay: not null })
+ _actions.StartUseDelay(component.MindSwapActionEntity);
+ if (TryComp(uid, out var psionic) && psionic.PsionicAbility == null)
+ psionic.PsionicAbility = component.MindSwapActionEntity;
+ }
+
+ private void OnShutdown(EntityUid uid, MindSwapPowerComponent component, ComponentShutdown args)
+ {
+ _actions.RemoveAction(uid, component.MindSwapActionEntity);
+ }
+
+ private void OnPowerUsed(MindSwapPowerActionEvent args)
+ {
+ if (!(TryComp(args.Target, out var damageable) && damageable.DamageContainerID == "Biological"))
+ return;
+
+ if (HasComp(args.Target))
+ return;
+
+ Swap(args.Performer, args.Target);
+
+ _psionics.LogPowerUsed(args.Performer, "mind swap");
+ args.Handled = true;
+ }
+
+ private void OnPowerReturned(EntityUid uid, MindSwappedComponent component, MindSwapPowerReturnActionEvent args)
+ {
+ if (HasComp(component.OriginalEntity) || HasComp(uid))
+ return;
+
+ if (HasComp(uid) && !_mobStateSystem.IsAlive(uid))
+ return;
+
+ // How do we get trapped?
+ // 1. Original target doesn't exist
+ if (!component.OriginalEntity.IsValid() || Deleted(component.OriginalEntity))
+ {
+ GetTrapped(uid);
+ return;
+ }
+ // 1. Original target is no longer mindswapped
+ if (!TryComp(component.OriginalEntity, out var targetMindSwap))
+ {
+ GetTrapped(uid);
+ return;
+ }
+
+ // 2. Target has undergone a different mind swap
+ if (targetMindSwap.OriginalEntity != uid)
+ {
+ GetTrapped(uid);
+ return;
+ }
+
+ // 3. Target is dead
+ if (HasComp(component.OriginalEntity) && _mobStateSystem.IsDead(component.OriginalEntity))
+ {
+ GetTrapped(uid);
+ return;
+ }
+
+ Swap(uid, component.OriginalEntity, true);
+ }
+
+ private void OnDispelled(EntityUid uid, MindSwappedComponent component, DispelledEvent args)
+ {
+ Swap(uid, component.OriginalEntity, true);
+ args.Handled = true;
+ }
+
+ private void OnMobStateChanged(EntityUid uid, MindSwappedComponent component, MobStateChangedEvent args)
+ {
+ if (args.NewMobState == MobState.Dead)
+ RemComp(uid);
+ }
+
+ private void OnGhostAttempt(GhostAttemptHandleEvent args)
+ {
+ if (args.Handled)
+ return;
+
+ if (!HasComp(args.Mind.CurrentEntity))
+ return;
+
+ //No idea where the viaCommand went. It's on the internal OnGhostAttempt, but not this layer. Maybe unnecessary.
+ /*if (!args.viaCommand)
+ return;*/
+
+ args.Result = false;
+ args.Handled = true;
+ }
+
+ private void OnSwapInit(EntityUid uid, MindSwappedComponent component, ComponentInit args)
+ {
+ _actions.AddAction(uid, ref component.MindSwapReturnActionEntity, component.MindSwapReturnActionId );
+ _actions.TryGetActionData( component.MindSwapReturnActionEntity, out var actionData );
+ if (actionData is { UseDelay: not null })
+ _actions.StartUseDelay(component.MindSwapReturnActionEntity);
+ if (TryComp(uid, out var psionic) && psionic.PsionicAbility == null)
+ psionic.PsionicAbility = component.MindSwapReturnActionEntity;
+ }
+
+ public void Swap(EntityUid performer, EntityUid target, bool end = false)
+ {
+ if (end && (!HasComp(performer) || !HasComp(target)))
+ return;
+
+ // Get the minds first. On transfer, they'll be gone.
+ MindComponent? performerMind = null;
+ MindComponent? targetMind = null;
+
+ // This is here to prevent missing MindContainerComponent Resolve errors.
+ if(!_mindSystem.TryGetMind(performer, out var performerMindId, out performerMind)){
+ performerMind = null;
+ };
+
+ if(!_mindSystem.TryGetMind(target, out var targetMindId, out targetMind)){
+ targetMind = null;
+ };
+
+ // Do the transfer.
+ if (performerMind != null)
+ _mindSystem.TransferTo(performerMindId, target, ghostCheckOverride: true, false, performerMind);
+
+ if (targetMind != null)
+ _mindSystem.TransferTo(targetMindId, performer, ghostCheckOverride: true, false, targetMind);
+
+ if (end)
+ {
+ var performerMindPowerComp = EntityManager.GetComponent(performer);
+ var targetMindPowerComp = EntityManager.GetComponent(target);
+ _actions.RemoveAction(performer, performerMindPowerComp.MindSwapReturnActionEntity);
+ _actions.RemoveAction(target, targetMindPowerComp.MindSwapReturnActionEntity);
+
+ RemComp(performer);
+ RemComp(target);
+ return;
+ }
+
+ var perfComp = EnsureComp(performer);
+ var targetComp = EnsureComp(target);
+
+ perfComp.OriginalEntity = target;
+ targetComp.OriginalEntity = performer;
+ }
+
+ public void GetTrapped(EntityUid uid)
+ {
+
+ _popupSystem.PopupEntity(Loc.GetString("mindswap-trapped"), uid, uid, Shared.Popups.PopupType.LargeCaution);
+ var perfComp = EnsureComp(uid);
+ _actions.RemoveAction(uid, perfComp.MindSwapReturnActionEntity, null);
+
+ if (HasComp(uid))
+ {
+ RemComp(uid);
+ RemComp(uid);
+ EnsureComp(uid);
+ EnsureComp(uid);
+ MetaData(uid).EntityName = Loc.GetString("telegnostic-trapped-entity-name");
+ MetaData(uid).EntityDescription = Loc.GetString("telegnostic-trapped-entity-desc");
+ }
+ }
+ }
+}
diff --git a/Content.Server/Nyanotrasen/Abilities/Psionics/Abilities/MindSwappedComponent.cs b/Content.Server/Nyanotrasen/Abilities/Psionics/Abilities/MindSwappedComponent.cs
new file mode 100644
index 00000000000..72cd6a66ef9
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Abilities/Psionics/Abilities/MindSwappedComponent.cs
@@ -0,0 +1,18 @@
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Server.Abilities.Psionics
+{
+ [RegisterComponent]
+ public sealed partial class MindSwappedComponent : Component
+ {
+ [ViewVariables]
+ public EntityUid OriginalEntity = default!;
+ [DataField("mindSwapReturnActionId",
+ customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string? MindSwapReturnActionId = "ActionMindSwapReturn";
+
+ [DataField("mindSwapReturnActionEntity")]
+ public EntityUid? MindSwapReturnActionEntity;
+ }
+}
diff --git a/Content.Server/Nyanotrasen/Abilities/Psionics/Abilities/NoosphericZapPowerSystem.cs b/Content.Server/Nyanotrasen/Abilities/Psionics/Abilities/NoosphericZapPowerSystem.cs
new file mode 100644
index 00000000000..798d9dbdadd
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Abilities/Psionics/Abilities/NoosphericZapPowerSystem.cs
@@ -0,0 +1,66 @@
+using Content.Shared.Actions;
+using Content.Shared.Abilities.Psionics;
+using Content.Server.Psionics;
+using Content.Shared.StatusEffect;
+using Content.Server.Stunnable;
+using Content.Server.Beam;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
+using Content.Server.Mind;
+using Content.Shared.Actions.Events;
+
+namespace Content.Server.Abilities.Psionics
+{
+ public sealed class NoosphericZapPowerSystem : EntitySystem
+ {
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly SharedActionsSystem _actions = default!;
+ [Dependency] private readonly SharedPsionicAbilitiesSystem _psionics = default!;
+ [Dependency] private readonly StunSystem _stunSystem = default!;
+ [Dependency] private readonly StatusEffectsSystem _statusEffectsSystem = default!;
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+ [Dependency] private readonly BeamSystem _beam = default!;
+ [Dependency] private readonly MindSystem _mindSystem = default!;
+
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnInit);
+ SubscribeLocalEvent(OnShutdown);
+ SubscribeLocalEvent(OnPowerUsed);
+ }
+
+ private void OnInit(EntityUid uid, NoosphericZapPowerComponent component, ComponentInit args)
+ {
+ _actions.AddAction(uid, ref component.NoosphericZapActionEntity, component.NoosphericZapActionId );
+ _actions.TryGetActionData( component.NoosphericZapActionEntity, out var actionData );
+ if (actionData is { UseDelay: not null })
+ _actions.StartUseDelay(component.NoosphericZapActionEntity);
+ if (TryComp(uid, out var psionic) && psionic.PsionicAbility == null)
+ psionic.PsionicAbility = component.NoosphericZapActionEntity;
+ }
+
+ private void OnShutdown(EntityUid uid, NoosphericZapPowerComponent component, ComponentShutdown args)
+ {
+ _actions.RemoveAction(uid, component.NoosphericZapActionEntity);
+ }
+
+ private void OnPowerUsed(NoosphericZapPowerActionEvent args)
+ {
+ if (!HasComp(args.Target))
+ return;
+
+ if (HasComp(args.Target))
+ return;
+
+ _beam.TryCreateBeam(args.Performer, args.Target, "LightningNoospheric");
+
+ _stunSystem.TryParalyze(args.Target, TimeSpan.FromSeconds(5), false);
+ _statusEffectsSystem.TryAddStatusEffect(args.Target, "Stutter", TimeSpan.FromSeconds(10), false, "StutteringAccent");
+
+ _psionics.LogPowerUsed(args.Performer, "noospheric zap");
+ args.Handled = true;
+ }
+ }
+}
diff --git a/Content.Server/Nyanotrasen/Abilities/Psionics/Abilities/PsionicInvisibilityPowerSystem.cs b/Content.Server/Nyanotrasen/Abilities/Psionics/Abilities/PsionicInvisibilityPowerSystem.cs
new file mode 100644
index 00000000000..4cec80a30a0
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Abilities/Psionics/Abilities/PsionicInvisibilityPowerSystem.cs
@@ -0,0 +1,125 @@
+using Content.Shared.Actions;
+using Content.Shared.CombatMode.Pacification;
+using Content.Shared.Abilities.Psionics;
+using Content.Shared.Damage;
+using Content.Shared.Stunnable;
+using Content.Shared.Stealth;
+using Content.Shared.Stealth.Components;
+using Content.Server.Psionics;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Player;
+using Robust.Shared.Audio;
+using Robust.Shared.Timing;
+using Content.Server.Mind;
+using Content.Shared.Actions.Events;
+
+namespace Content.Server.Abilities.Psionics
+{
+ public sealed class PsionicInvisibilityPowerSystem : EntitySystem
+ {
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly SharedActionsSystem _actions = default!;
+ [Dependency] private readonly SharedStunSystem _stunSystem = default!;
+ [Dependency] private readonly SharedPsionicAbilitiesSystem _psionics = default!;
+ [Dependency] private readonly SharedStealthSystem _stealth = default!;
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+ [Dependency] private readonly MindSystem _mindSystem = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnInit);
+ SubscribeLocalEvent(OnShutdown);
+ SubscribeLocalEvent(OnPowerUsed);
+ SubscribeLocalEvent(OnPowerOff);
+ SubscribeLocalEvent(OnStart);
+ SubscribeLocalEvent(OnEnd);
+ SubscribeLocalEvent(OnDamageChanged);
+ }
+
+ private void OnInit(EntityUid uid, PsionicInvisibilityPowerComponent component, ComponentInit args)
+ {
+ _actions.AddAction(uid, ref component.PsionicInvisibilityActionEntity, component.PsionicInvisibilityActionId );
+ _actions.TryGetActionData( component.PsionicInvisibilityActionEntity, out var actionData );
+ if (actionData is { UseDelay: not null })
+ _actions.StartUseDelay(component.PsionicInvisibilityActionEntity);
+ if (TryComp(uid, out var psionic) && psionic.PsionicAbility == null)
+ psionic.PsionicAbility = component.PsionicInvisibilityActionEntity;
+ }
+
+ private void OnShutdown(EntityUid uid, PsionicInvisibilityPowerComponent component, ComponentShutdown args)
+ {
+ _actions.RemoveAction(uid, component.PsionicInvisibilityActionEntity);
+ }
+
+ private void OnPowerUsed(EntityUid uid, PsionicInvisibilityPowerComponent component, PsionicInvisibilityPowerActionEvent args)
+ {
+ if (HasComp(uid))
+ return;
+
+ ToggleInvisibility(args.Performer);
+ var action = Spawn(PsionicInvisibilityUsedComponent.PsionicInvisibilityUsedActionPrototype);
+ _actions.AddAction(uid, action, action);
+ _actions.TryGetActionData( action, out var actionData );
+ if (actionData is { UseDelay: not null })
+ _actions.StartUseDelay(action);
+
+ _psionics.LogPowerUsed(uid, "psionic invisibility");
+ args.Handled = true;
+ }
+
+ private void OnPowerOff(RemovePsionicInvisibilityOffPowerActionEvent args)
+ {
+ if (!HasComp(args.Performer))
+ return;
+
+ ToggleInvisibility(args.Performer);
+ args.Handled = true;
+ }
+
+ private void OnStart(EntityUid uid, PsionicInvisibilityUsedComponent component, ComponentInit args)
+ {
+ EnsureComp(uid);
+ EnsureComp(uid);
+ var stealth = EnsureComp(uid);
+ _stealth.SetVisibility(uid, 0.66f, stealth);
+ SoundSystem.Play("/Audio/Effects/toss.ogg", Filter.Pvs(uid), uid);
+
+ }
+
+ private void OnEnd(EntityUid uid, PsionicInvisibilityUsedComponent component, ComponentShutdown args)
+ {
+ if (Terminating(uid))
+ return;
+
+ RemComp(uid);
+ RemComp(uid);
+ RemComp(uid);
+ SoundSystem.Play("/Audio/Effects/toss.ogg", Filter.Pvs(uid), uid);
+ //Pretty sure this DOESN'T work as intended.
+ _actions.RemoveAction(uid, component.PsionicInvisibilityUsedActionEntity);
+
+ _stunSystem.TryParalyze(uid, TimeSpan.FromSeconds(8), false);
+ DirtyEntity(uid);
+ }
+
+ private void OnDamageChanged(EntityUid uid, PsionicInvisibilityUsedComponent component, DamageChangedEvent args)
+ {
+ if (!args.DamageIncreased)
+ return;
+
+ ToggleInvisibility(uid);
+ }
+
+ public void ToggleInvisibility(EntityUid uid)
+ {
+ if (!HasComp(uid))
+ {
+ EnsureComp(uid);
+ } else
+ {
+ RemComp(uid);
+ }
+ }
+ }
+}
diff --git a/Content.Server/Nyanotrasen/Abilities/Psionics/Abilities/PsionicRegenerationPowerSystem.cs b/Content.Server/Nyanotrasen/Abilities/Psionics/Abilities/PsionicRegenerationPowerSystem.cs
new file mode 100644
index 00000000000..1a6b67e5086
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Abilities/Psionics/Abilities/PsionicRegenerationPowerSystem.cs
@@ -0,0 +1,117 @@
+using Robust.Shared.Audio;
+using Robust.Server.GameObjects;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+using Content.Server.Body.Components;
+using Content.Server.Body.Systems;
+using Content.Server.Chemistry.EntitySystems;
+using Content.Server.DoAfter;
+using Content.Shared.Abilities.Psionics;
+using Content.Shared.Actions;
+using Content.Shared.Chemistry.Components;
+using Content.Shared.DoAfter;
+using Content.Shared.FixedPoint;
+using Content.Shared.Popups;
+using Content.Shared.Psionics.Events;
+using Content.Shared.Tag;
+using Content.Shared.Examine;
+using static Content.Shared.Examine.ExamineSystemShared;
+using Robust.Shared.Timing;
+using Content.Server.Mind;
+using Content.Shared.Actions.Events;
+
+namespace Content.Server.Abilities.Psionics
+{
+ public sealed class PsionicRegenerationPowerSystem : EntitySystem
+ {
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly SharedActionsSystem _actions = default!;
+ [Dependency] private readonly SolutionContainerSystem _solutionSystem = default!;
+ [Dependency] private readonly BloodstreamSystem _bloodstreamSystem = default!;
+ [Dependency] private readonly AudioSystem _audioSystem = default!;
+ [Dependency] private readonly TagSystem _tagSystem = default!;
+ [Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
+ [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
+ [Dependency] private readonly SharedPsionicAbilitiesSystem _psionics = default!;
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+ [Dependency] private readonly MindSystem _mindSystem = default!;
+
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnInit);
+ SubscribeLocalEvent(OnShutdown);
+ SubscribeLocalEvent(OnPowerUsed);
+
+ SubscribeLocalEvent(OnDispelled);
+ SubscribeLocalEvent(OnDoAfter);
+ }
+
+ private void OnInit(EntityUid uid, PsionicRegenerationPowerComponent component, ComponentInit args)
+ {
+ _actions.AddAction(uid, ref component.PsionicRegenerationActionEntity, component.PsionicRegenerationActionId );
+ _actions.TryGetActionData( component.PsionicRegenerationActionEntity, out var actionData );
+ if (actionData is { UseDelay: not null })
+ _actions.StartUseDelay(component.PsionicRegenerationActionEntity);
+ if (TryComp(uid, out var psionic) && psionic.PsionicAbility == null)
+ psionic.PsionicAbility = component.PsionicRegenerationActionEntity;
+ }
+
+ private void OnPowerUsed(EntityUid uid, PsionicRegenerationPowerComponent component, PsionicRegenerationPowerActionEvent args)
+ {
+ var ev = new PsionicRegenerationDoAfterEvent(_gameTiming.CurTime);
+ var doAfterArgs = new DoAfterArgs(EntityManager, uid, component.UseDelay, ev, uid);
+
+ _doAfterSystem.TryStartDoAfter(doAfterArgs, out var doAfterId);
+
+ component.DoAfter = doAfterId;
+
+ _popupSystem.PopupEntity(Loc.GetString("psionic-regeneration-begin", ("entity", uid)),
+ uid,
+ // TODO: Use LoS-based Filter when one is available.
+ Filter.Pvs(uid).RemoveWhereAttachedEntity(entity => !ExamineSystemShared.InRangeUnOccluded(uid, entity, ExamineRange, null)),
+ true,
+ PopupType.Medium);
+
+ _audioSystem.PlayPvs(component.SoundUse, component.Owner, AudioParams.Default.WithVolume(8f).WithMaxDistance(1.5f).WithRolloffFactor(3.5f));
+ _psionics.LogPowerUsed(uid, "psionic regeneration");
+ args.Handled = true;
+ }
+
+ private void OnShutdown(EntityUid uid, PsionicRegenerationPowerComponent component, ComponentShutdown args)
+ {
+ _actions.RemoveAction(uid, component.PsionicRegenerationActionEntity);
+ }
+
+ private void OnDispelled(EntityUid uid, PsionicRegenerationPowerComponent component, DispelledEvent args)
+ {
+ if (component.DoAfter == null)
+ return;
+
+ _doAfterSystem.Cancel(component.DoAfter);
+ component.DoAfter = null;
+
+ args.Handled = true;
+ }
+
+ private void OnDoAfter(EntityUid uid, PsionicRegenerationPowerComponent component, PsionicRegenerationDoAfterEvent args)
+ {
+ component.DoAfter = null;
+
+ if (!TryComp(uid, out var stream))
+ return;
+
+ // DoAfter has no way to run a callback during the process to give
+ // small doses of the reagent, so we wait until either the action
+ // is cancelled (by being dispelled) or complete to give the
+ // appropriate dose. A timestamp delta is used to accomplish this.
+ var percentageComplete = Math.Min(1f, (_gameTiming.CurTime - args.StartedAt).TotalSeconds / component.UseDelay);
+
+ var solution = new Solution();
+ solution.AddReagent("PsionicRegenerationEssence", FixedPoint2.New(component.EssenceAmount * percentageComplete));
+ _bloodstreamSystem.TryAddToChemicals(uid, solution, stream);
+ }
+ }
+}
+
diff --git a/Content.Server/Nyanotrasen/Abilities/Psionics/Abilities/PyrokinesisPowerSystem.cs b/Content.Server/Nyanotrasen/Abilities/Psionics/Abilities/PyrokinesisPowerSystem.cs
new file mode 100644
index 00000000000..f3fe47ef2bd
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Abilities/Psionics/Abilities/PyrokinesisPowerSystem.cs
@@ -0,0 +1,59 @@
+using Content.Shared.Actions;
+using Content.Shared.Abilities.Psionics;
+using Content.Server.Atmos.Components;
+using Content.Server.Atmos.EntitySystems;
+using Content.Server.Popups;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
+using Content.Server.Mind;
+using Content.Shared.Actions.Events;
+
+namespace Content.Server.Abilities.Psionics
+{
+ public sealed class PyrokinesisPowerSystem : EntitySystem
+ {
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly SharedActionsSystem _actions = default!;
+ [Dependency] private readonly FlammableSystem _flammableSystem = default!;
+ [Dependency] private readonly SharedPsionicAbilitiesSystem _psionics = default!;
+ [Dependency] private readonly PopupSystem _popupSystem = default!;
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+ [Dependency] private readonly MindSystem _mindSystem = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnInit);
+ SubscribeLocalEvent(OnShutdown);
+ SubscribeLocalEvent(OnPowerUsed);
+ }
+
+ private void OnInit(EntityUid uid, PyrokinesisPowerComponent component, ComponentInit args)
+ {
+ _actions.AddAction(uid, ref component.PyrokinesisActionEntity, component.PyrokinesisActionId );
+ _actions.TryGetActionData( component.PyrokinesisActionEntity, out var actionData );
+ if (actionData is { UseDelay: not null })
+ _actions.StartUseDelay(component.PyrokinesisActionEntity);
+ if (TryComp(uid, out var psionic) && psionic.PsionicAbility == null)
+ psionic.PsionicAbility = component.PyrokinesisActionEntity;
+ }
+
+ private void OnShutdown(EntityUid uid, PyrokinesisPowerComponent component, ComponentShutdown args)
+ {
+ _actions.RemoveAction(uid, component.PyrokinesisActionEntity);
+ }
+
+ private void OnPowerUsed(PyrokinesisPowerActionEvent args)
+ {
+ if (!TryComp(args.Target, out var flammableComponent))
+ return;
+
+ flammableComponent.FireStacks += 5;
+ _flammableSystem.Ignite(args.Target, args.Target);
+ _popupSystem.PopupEntity(Loc.GetString("pyrokinesis-power-used", ("target", args.Target)), args.Target, Shared.Popups.PopupType.LargeCaution);
+
+ _psionics.LogPowerUsed(args.Performer, "pyrokinesis");
+ args.Handled = true;
+ }
+ }
+}
diff --git a/Content.Server/Nyanotrasen/Abilities/Psionics/Abilities/TelegnosisPowerSystem.cs b/Content.Server/Nyanotrasen/Abilities/Psionics/Abilities/TelegnosisPowerSystem.cs
new file mode 100644
index 00000000000..b233bda6225
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Abilities/Psionics/Abilities/TelegnosisPowerSystem.cs
@@ -0,0 +1,60 @@
+using Content.Shared.Actions;
+using Content.Shared.StatusEffect;
+using Content.Shared.Abilities.Psionics;
+using Content.Shared.Mind.Components;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
+using Content.Server.Mind;
+using Content.Shared.Actions.Events;
+
+namespace Content.Server.Abilities.Psionics
+{
+ public sealed class TelegnosisPowerSystem : EntitySystem
+ {
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly StatusEffectsSystem _statusEffects = default!;
+ [Dependency] private readonly SharedActionsSystem _actions = default!;
+ [Dependency] private readonly MindSwapPowerSystem _mindSwap = default!;
+ [Dependency] private readonly SharedPsionicAbilitiesSystem _psionics = default!;
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+ [Dependency] private readonly MindSystem _mindSystem = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnInit);
+ SubscribeLocalEvent(OnShutdown);
+ SubscribeLocalEvent(OnPowerUsed);
+ SubscribeLocalEvent(OnMindRemoved);
+ }
+
+ private void OnInit(EntityUid uid, TelegnosisPowerComponent component, ComponentInit args)
+ {
+ _actions.AddAction(uid, ref component.TelegnosisActionEntity, component.TelegnosisActionId );
+ _actions.TryGetActionData( component.TelegnosisActionEntity, out var actionData );
+ if (actionData is { UseDelay: not null })
+ _actions.StartUseDelay(component.TelegnosisActionEntity);
+ if (TryComp(uid, out var psionic) && psionic.PsionicAbility == null)
+ psionic.PsionicAbility = component.TelegnosisActionEntity;
+ }
+
+ private void OnShutdown(EntityUid uid, TelegnosisPowerComponent component, ComponentShutdown args)
+ {
+ _actions.RemoveAction(uid, component.TelegnosisActionEntity);
+ }
+
+ private void OnPowerUsed(EntityUid uid, TelegnosisPowerComponent component, TelegnosisPowerActionEvent args)
+ {
+ var projection = Spawn(component.Prototype, Transform(uid).Coordinates);
+ Transform(projection).AttachToGridOrMap();
+ _mindSwap.Swap(uid, projection);
+
+ _psionics.LogPowerUsed(uid, "telegnosis");
+ args.Handled = true;
+ }
+ private void OnMindRemoved(EntityUid uid, TelegnosticProjectionComponent component, MindRemovedMessage args)
+ {
+ QueueDel(uid);
+ }
+ }
+}
diff --git a/Content.Server/Nyanotrasen/Abilities/Psionics/PsionicAbilitiesSystem.cs b/Content.Server/Nyanotrasen/Abilities/Psionics/PsionicAbilitiesSystem.cs
new file mode 100644
index 00000000000..088f6159462
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Abilities/Psionics/PsionicAbilitiesSystem.cs
@@ -0,0 +1,142 @@
+using Content.Shared.Abilities.Psionics;
+using Content.Shared.Actions;
+using Content.Shared.Psionics.Glimmer;
+using Content.Shared.Random;
+using Content.Shared.Random.Helpers;
+using Content.Server.EUI;
+using Content.Server.Psionics;
+using Content.Server.Mind;
+using Content.Shared.Mind;
+using Content.Shared.Mind.Components;
+using Content.Shared.StatusEffect;
+using Robust.Shared.Random;
+using Robust.Shared.Prototypes;
+using Robust.Server.GameObjects;
+using Robust.Server.Player;
+
+namespace Content.Server.Abilities.Psionics
+{
+ public sealed class PsionicAbilitiesSystem : EntitySystem
+ {
+ [Dependency] private readonly IComponentFactory _componentFactory = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
+ [Dependency] private readonly IPlayerManager _playerManager = default!;
+ [Dependency] private readonly EuiManager _euiManager = default!;
+ [Dependency] private readonly StatusEffectsSystem _statusEffectsSystem = default!;
+ [Dependency] private readonly GlimmerSystem _glimmerSystem = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly MindSystem _mindSystem = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnPlayerAttached);
+ }
+
+ private void OnPlayerAttached(EntityUid uid, PsionicAwaitingPlayerComponent component, PlayerAttachedEvent args)
+ {
+ if (TryComp(uid, out var bonus) && bonus.Warn == true)
+ _euiManager.OpenEui(new AcceptPsionicsEui(uid, this), args.Player);
+ else
+ AddRandomPsionicPower(uid);
+ RemCompDeferred(uid);
+ }
+
+ public void AddPsionics(EntityUid uid, bool warn = true)
+ {
+ if (Deleted(uid))
+ return;
+
+ if (HasComp(uid))
+ return;
+
+ //Don't know if this will work. New mind state vs old.
+ if (!TryComp(uid, out var mindContainer) ||
+ !_mindSystem.TryGetMind(uid, out _, out var mind ))
+ //||
+ //!_mindSystem.TryGetMind(uid, out var mind, mindContainer))
+ {
+ EnsureComp(uid);
+ return;
+ }
+
+ if (!_mindSystem.TryGetSession(mind, out var client))
+ return;
+
+ if (warn && TryComp(uid, out var actor))
+ _euiManager.OpenEui(new AcceptPsionicsEui(uid, this), client);
+ else
+ AddRandomPsionicPower(uid);
+ }
+
+ public void AddPsionics(EntityUid uid, string powerComp)
+ {
+ if (Deleted(uid))
+ return;
+
+ if (HasComp(uid))
+ return;
+
+ AddComp(uid);
+
+ var newComponent = (Component) _componentFactory.GetComponent(powerComp);
+ newComponent.Owner = uid;
+
+ EntityManager.AddComponent(uid, newComponent);
+ }
+
+ public void AddRandomPsionicPower(EntityUid uid)
+ {
+ AddComp(uid);
+
+ if (!_prototypeManager.TryIndex("RandomPsionicPowerPool", out var pool))
+ {
+ Logger.Error("Can't index the random psionic power pool!");
+ return;
+ }
+
+ // uh oh, stinky!
+ var newComponent = (Component) _componentFactory.GetComponent(pool.Pick());
+ newComponent.Owner = uid;
+
+ EntityManager.AddComponent(uid, newComponent);
+
+ _glimmerSystem.Glimmer += _random.Next(1, 5);
+ }
+
+ public void RemovePsionics(EntityUid uid)
+ {
+ if (!TryComp(uid, out var psionic))
+ return;
+
+ if (!psionic.Removable)
+ return;
+
+ if (!_prototypeManager.TryIndex("RandomPsionicPowerPool", out var pool))
+ {
+ Logger.Error("Can't index the random psionic power pool!");
+ return;
+ }
+
+ foreach (var compName in pool.Weights.Keys)
+ {
+ // component moment
+ var comp = _componentFactory.GetComponent(compName);
+ if (EntityManager.TryGetComponent(uid, comp.GetType(), out var psionicPower))
+ RemComp(uid, psionicPower);
+ }
+ if (psionic.PsionicAbility != null){
+ _actionsSystem.TryGetActionData( psionic.PsionicAbility, out var psiAbility );
+ if (psiAbility != null){
+ var owner = psiAbility.Owner;
+ _actionsSystem.RemoveAction(uid, psiAbility.Owner);
+ }
+ }
+
+ _statusEffectsSystem.TryAddStatusEffect(uid, "Stutter", TimeSpan.FromMinutes(5), false, "StutteringAccent");
+
+ RemComp(uid);
+ }
+ }
+}
diff --git a/Content.Server/Nyanotrasen/Audio/GlimmerSoundComponent.cs b/Content.Server/Nyanotrasen/Audio/GlimmerSoundComponent.cs
new file mode 100644
index 00000000000..850be3e831c
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Audio/GlimmerSoundComponent.cs
@@ -0,0 +1,24 @@
+using Content.Server.Psionics.Glimmer;
+using Content.Shared.Audio;
+using Content.Shared.Psionics.Glimmer;
+using Robust.Shared.Audio;
+using Robust.Shared.ComponentTrees;
+using Robust.Shared.GameStates;
+using Robust.Shared.Physics;
+using Robust.Shared.Serialization;
+
+namespace Content.Server.Audio
+{
+ [RegisterComponent]
+ [Access(typeof(SharedAmbientSoundSystem), typeof(GlimmerReactiveSystem))]
+ public sealed partial class GlimmerSoundComponent : Component
+ {
+ [DataField("glimmerTier", required: true), ViewVariables(VVAccess.ReadWrite)] // only for map editing
+ public Dictionary Sound { get; set; } = new();
+
+ public bool GetSound(GlimmerTier glimmerTier, out SoundSpecifier? spec)
+ {
+ return Sound.TryGetValue(glimmerTier.ToString(), out spec);
+ }
+ }
+}
diff --git a/Content.Server/Nyanotrasen/CartridgeLoader/GlimmerMonitorCartridgeComponent.cs b/Content.Server/Nyanotrasen/CartridgeLoader/GlimmerMonitorCartridgeComponent.cs
new file mode 100644
index 00000000000..40ba46647c7
--- /dev/null
+++ b/Content.Server/Nyanotrasen/CartridgeLoader/GlimmerMonitorCartridgeComponent.cs
@@ -0,0 +1,5 @@
+namespace Content.Server.CartridgeLoader.Cartridges;
+
+[RegisterComponent]
+public sealed partial class GlimmerMonitorCartridgeComponent : Component
+{ }
diff --git a/Content.Server/Nyanotrasen/CartridgeLoader/GlimmerMonitorCartridgeSystem.cs b/Content.Server/Nyanotrasen/CartridgeLoader/GlimmerMonitorCartridgeSystem.cs
new file mode 100644
index 00000000000..b4f602d1802
--- /dev/null
+++ b/Content.Server/Nyanotrasen/CartridgeLoader/GlimmerMonitorCartridgeSystem.cs
@@ -0,0 +1,43 @@
+using Content.Shared.CartridgeLoader;
+using Content.Shared.CartridgeLoader.Cartridges;
+using Content.Server.Psionics.Glimmer;
+
+namespace Content.Server.CartridgeLoader.Cartridges;
+
+public sealed class GlimmerMonitorCartridgeSystem : EntitySystem
+{
+ [Dependency] private readonly CartridgeLoaderSystem? _cartridgeLoaderSystem = default!;
+ [Dependency] private readonly PassiveGlimmerReductionSystem _glimmerReductionSystem = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnUiReady);
+ SubscribeLocalEvent(OnMessage);
+ }
+
+ ///
+ /// This gets called when the ui fragment needs to be updated for the first time after activating
+ ///
+ private void OnUiReady(EntityUid uid, GlimmerMonitorCartridgeComponent component, CartridgeUiReadyEvent args)
+ {
+ UpdateUiState(uid, args.Loader, component);
+ }
+
+ private void OnMessage(EntityUid uid, GlimmerMonitorCartridgeComponent component, CartridgeMessageEvent args)
+ {
+ if (args is not GlimmerMonitorSyncMessageEvent)
+ return;
+ ;
+ UpdateUiState(uid, EntityManager.GetEntity( args.LoaderUid ), component);
+ }
+
+ public void UpdateUiState(EntityUid uid, EntityUid loaderUid, GlimmerMonitorCartridgeComponent? component)
+ {
+ if (!Resolve(uid, ref component))
+ return;
+
+ var state = new GlimmerMonitorUiState(_glimmerReductionSystem.GlimmerValues);
+ _cartridgeLoaderSystem?.UpdateCartridgeUiState(loaderUid, state);
+ }
+}
diff --git a/Content.Server/Nyanotrasen/Chat/NyanoChatSystem.cs b/Content.Server/Nyanotrasen/Chat/NyanoChatSystem.cs
new file mode 100644
index 00000000000..58ed1782741
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Chat/NyanoChatSystem.cs
@@ -0,0 +1,128 @@
+using Content.Server.Administration.Logs;
+using Content.Server.Administration.Managers;
+using Content.Server.Chat.Managers;
+using Content.Server.Chat.Systems;
+using Content.Shared.Abilities.Psionics;
+using Content.Shared.Bed.Sleep;
+using Content.Shared.Chat;
+using Content.Shared.Database;
+using Content.Shared.Drugs;
+using Content.Shared.Mobs;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Psionics.Glimmer;
+using Robust.Shared.Network;
+using Robust.Shared.Player;
+using Robust.Shared.Random;
+using System.Linq;
+using System.Text;
+
+namespace Content.Server.Nyanotrasen.Chat
+{
+ ///
+ /// Extensions for nyano's chat stuff
+ ///
+
+ public sealed class NyanoChatSystem : EntitySystem
+ {
+ [Dependency] private readonly IAdminManager _adminManager = default!;
+ [Dependency] private readonly IChatManager _chatManager = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly IAdminLogManager _adminLogger = default!;
+ [Dependency] private readonly GlimmerSystem _glimmerSystem = default!;
+ [Dependency] private readonly ChatSystem _chatSystem = default!;
+ private IEnumerable GetPsionicChatClients()
+ {
+ return Filter.Empty()
+ .AddWhereAttachedEntity(IsEligibleForTelepathy)
+ .Recipients
+ .Select(p => p.ConnectedClient);
+ }
+
+ private IEnumerable GetAdminClients()
+ {
+ return _adminManager.ActiveAdmins
+ .Select(p => p.ConnectedClient);
+ }
+
+ private List GetDreamers(IEnumerable removeList)
+ {
+ var filtered = Filter.Empty()
+ .AddWhereAttachedEntity(entity => HasComp(entity) || HasComp(entity) && !HasComp(entity) && !HasComp(entity))
+ .Recipients
+ .Select(p => p.ConnectedClient);
+
+ var filteredList = filtered.ToList();
+
+ foreach (var entity in removeList)
+ filteredList.Remove(entity);
+
+ return filteredList;
+ }
+
+ private bool IsEligibleForTelepathy(EntityUid entity)
+ {
+ return HasComp(entity)
+ && !HasComp(entity)
+ && !HasComp(entity)
+ && (!TryComp(entity, out var mobstate) || mobstate.CurrentState == MobState.Alive);
+ }
+
+ public void SendTelepathicChat(EntityUid source, string message, bool hideChat)
+ {
+ if (!IsEligibleForTelepathy(source))
+ return;
+
+ var clients = GetPsionicChatClients();
+ var admins = GetAdminClients();
+ string messageWrap;
+ string adminMessageWrap;
+
+ messageWrap = Loc.GetString("chat-manager-send-telepathic-chat-wrap-message",
+ ("telepathicChannelName", Loc.GetString("chat-manager-telepathic-channel-name")), ("message", message));
+
+ adminMessageWrap = Loc.GetString("chat-manager-send-telepathic-chat-wrap-message-admin",
+ ("source", source), ("message", message));
+
+ _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Telepathic chat from {ToPrettyString(source):Player}: {message}");
+
+ _chatManager.ChatMessageToMany(ChatChannel.Telepathic, message, messageWrap, source, hideChat, true, clients.ToList(), Color.PaleVioletRed);
+
+ _chatManager.ChatMessageToMany(ChatChannel.Telepathic, message, adminMessageWrap, source, hideChat, true, admins, Color.PaleVioletRed);
+
+ if (_random.Prob(0.1f))
+ _glimmerSystem.Glimmer++;
+
+ if (_random.Prob(Math.Min(0.33f + ((float) _glimmerSystem.Glimmer / 1500), 1)))
+ {
+ float obfuscation = (0.25f + (float) _glimmerSystem.Glimmer / 2000);
+ var obfuscated = ObfuscateMessageReadability(message, obfuscation);
+ _chatManager.ChatMessageToMany(ChatChannel.Telepathic, obfuscated, messageWrap, source, hideChat, false, GetDreamers(clients), Color.PaleVioletRed);
+ }
+
+ foreach (var repeater in EntityQuery())
+ {
+ _chatSystem.TrySendInGameICMessage(repeater.Owner, message, InGameICChatType.Speak, false);
+ }
+ }
+
+ private string ObfuscateMessageReadability(string message, float chance)
+ {
+ var modifiedMessage = new StringBuilder(message);
+
+ for (var i = 0; i < message.Length; i++)
+ {
+ if (char.IsWhiteSpace((modifiedMessage[i])))
+ {
+ continue;
+ }
+
+ if (_random.Prob(1 - chance))
+ {
+ modifiedMessage[i] = '~';
+ }
+ }
+
+ return modifiedMessage.ToString();
+ }
+ }
+}
diff --git a/Content.Server/Nyanotrasen/Chat/TSayCommand.cs b/Content.Server/Nyanotrasen/Chat/TSayCommand.cs
new file mode 100644
index 00000000000..debbf928ce2
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Chat/TSayCommand.cs
@@ -0,0 +1,43 @@
+using Content.Server.Chat.Systems;
+using Content.Shared.Administration;
+using Robust.Server.Player;
+using Robust.Shared.Console;
+using Robust.Shared.Enums;
+
+namespace Content.Server.Chat.Commands
+{
+ [AnyCommand]
+ internal sealed class TSayCommand : IConsoleCommand
+ {
+ public string Command => "tsay";
+ public string Description => "Send chat messages to the telepathic.";
+ public string Help => "tsay ";
+
+ public void Execute(IConsoleShell shell, string argStr, string[] args)
+ {
+ if (shell.Player is not IPlayerSession player)
+ {
+ shell.WriteError("This command cannot be run from the server.");
+ return;
+ }
+
+ if (player.Status != SessionStatus.InGame)
+ return;
+
+ if (player.AttachedEntity is not {} playerEntity)
+ {
+ shell.WriteError("You don't have an entity!");
+ return;
+ }
+
+ if (args.Length < 1)
+ return;
+
+ var message = string.Join(" ", args).Trim();
+ if (string.IsNullOrEmpty(message))
+ return;
+ //Not sure if I should hide the logs from this. Default is false.
+ EntitySystem.Get().TrySendInGameICMessage(playerEntity, message, InGameICChatType.Telepathic, ChatTransmitRange.Normal, false, shell, player);
+ }
+ }
+}
diff --git a/Content.Server/Nyanotrasen/Chat/TelepathicRepeaterComponent.cs b/Content.Server/Nyanotrasen/Chat/TelepathicRepeaterComponent.cs
new file mode 100644
index 00000000000..fc199f4332a
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Chat/TelepathicRepeaterComponent.cs
@@ -0,0 +1,11 @@
+namespace Content.Server.Nyanotrasen.Chat
+{
+ ///
+ /// Repeats whatever is happening in telepathic chat.
+ ///
+ [RegisterComponent]
+ public sealed partial class TelepathicRepeaterComponent : Component
+ {
+
+ }
+}
diff --git a/Content.Server/Nyanotrasen/Chemistry/Effects/ChemRemovePsionic.cs b/Content.Server/Nyanotrasen/Chemistry/Effects/ChemRemovePsionic.cs
new file mode 100644
index 00000000000..a23a5b3d77d
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Chemistry/Effects/ChemRemovePsionic.cs
@@ -0,0 +1,27 @@
+using Content.Shared.Chemistry.Reagent;
+using Content.Server.Abilities.Psionics;
+using JetBrains.Annotations;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Chemistry.ReagentEffects
+{
+ ///
+ /// Rerolls psionics once.
+ ///
+ [UsedImplicitly]
+ public sealed partial class ChemRemovePsionic : ReagentEffect
+ {
+ protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
+ => Loc.GetString("reagent-effect-guidebook-chem-remove-psionic", ("chance", Probability));
+
+ public override void Effect(ReagentEffectArgs args)
+ {
+ if (args.Scale != 1f)
+ return;
+
+ var psySys = args.EntityManager.EntitySysManager.GetEntitySystem();
+
+ psySys.RemovePsionics(args.SolutionEntity);
+ }
+ }
+}
diff --git a/Content.Server/Nyanotrasen/Chemistry/Effects/ChemRerollPsionic.cs b/Content.Server/Nyanotrasen/Chemistry/Effects/ChemRerollPsionic.cs
new file mode 100644
index 00000000000..987a41c04a1
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Chemistry/Effects/ChemRerollPsionic.cs
@@ -0,0 +1,30 @@
+using Content.Shared.Chemistry.Reagent;
+using Content.Server.Psionics;
+using JetBrains.Annotations;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Chemistry.ReagentEffects
+{
+ ///
+ /// Rerolls psionics once.
+ ///
+ [UsedImplicitly]
+ public sealed partial class ChemRerollPsionic : ReagentEffect
+ {
+ protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
+ => Loc.GetString("reagent-effect-guidebook-chem-reroll-psionic", ("chance", Probability));
+
+ ///
+ /// Reroll multiplier.
+ ///
+ [DataField("bonusMultiplier")]
+ public float BonusMuliplier = 1f;
+
+ public override void Effect(ReagentEffectArgs args)
+ {
+ var psySys = args.EntityManager.EntitySysManager.GetEntitySystem();
+
+ psySys.RerollPsionics(args.SolutionEntity, bonusMuliplier: BonusMuliplier);
+ }
+ }
+}
diff --git a/Content.Server/Nyanotrasen/Chemistry/ReactionEffects/ChangeGlimmerReactionEffect.cs b/Content.Server/Nyanotrasen/Chemistry/ReactionEffects/ChangeGlimmerReactionEffect.cs
new file mode 100644
index 00000000000..65aaf350cba
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Chemistry/ReactionEffects/ChangeGlimmerReactionEffect.cs
@@ -0,0 +1,26 @@
+using Content.Shared.Chemistry.Reagent;
+using Content.Shared.Psionics.Glimmer;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Chemistry.ReactionEffects;
+
+[DataDefinition]
+public sealed partial class ChangeGlimmerReactionEffect : ReagentEffect
+{
+ protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
+ => Loc.GetString("reagent-effect-guidebook-change-glimmer-reaction-effect", ("chance", Probability),
+ ("count", Count));
+
+ ///
+ /// Added to glimmer when reaction occurs.
+ ///
+ [DataField("count")]
+ public int Count = 1;
+
+ public override void Effect(ReagentEffectArgs args)
+ {
+ var glimmersys = args.EntityManager.EntitySysManager.GetEntitySystem();
+
+ glimmersys.Glimmer += Count;
+ }
+}
diff --git a/Content.Server/Nyanotrasen/Chemistry/SolutionRegenerationSwitcher/SolutionRegenerationSwitcherComponent.cs b/Content.Server/Nyanotrasen/Chemistry/SolutionRegenerationSwitcher/SolutionRegenerationSwitcherComponent.cs
new file mode 100644
index 00000000000..03745113e9f
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Chemistry/SolutionRegenerationSwitcher/SolutionRegenerationSwitcherComponent.cs
@@ -0,0 +1,20 @@
+using Content.Shared.Chemistry.Components;
+
+namespace Content.Server.Chemistry.Components
+{
+ [RegisterComponent]
+ public sealed partial class SolutionRegenerationSwitcherComponent : Component
+ {
+ [DataField("options", required: true), ViewVariables(VVAccess.ReadWrite)]
+ public List Options = default!;
+
+ [DataField("currentIndex"), ViewVariables(VVAccess.ReadWrite)]
+ public int CurrentIndex = 0;
+
+ ///
+ /// Should the already generated solution be kept when switching?
+ ///
+ [DataField("keepSolution"), ViewVariables(VVAccess.ReadWrite)]
+ public bool KeepSolution = false;
+ }
+}
diff --git a/Content.Server/Nyanotrasen/Chemistry/SolutionRegenerationSwitcher/SolutionRegenerationSwitcherSystem.cs b/Content.Server/Nyanotrasen/Chemistry/SolutionRegenerationSwitcher/SolutionRegenerationSwitcherSystem.cs
new file mode 100644
index 00000000000..abe4b61e7be
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Chemistry/SolutionRegenerationSwitcher/SolutionRegenerationSwitcherSystem.cs
@@ -0,0 +1,97 @@
+using Robust.Shared.Prototypes;
+using Content.Server.Chemistry.Components;
+using Content.Server.Popups;
+using Content.Shared.Chemistry.Reagent;
+using Content.Shared.Verbs;
+
+namespace Content.Server.Chemistry.EntitySystems
+{
+ public sealed class SolutionRegenerationSwitcherSystem : EntitySystem
+ {
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly SolutionContainerSystem _solutionSystem = default!;
+ [Dependency] private readonly PopupSystem _popups = default!;
+
+ private ISawmill _sawmill = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ _sawmill = Logger.GetSawmill("chemistry");
+
+ SubscribeLocalEvent>(AddSwitchVerb);
+ }
+
+ private void AddSwitchVerb(EntityUid uid, SolutionRegenerationSwitcherComponent component, GetVerbsEvent args)
+ {
+ if (!args.CanInteract || !args.CanAccess)
+ return;
+
+ if (component.Options.Count <= 1)
+ return;
+
+ AlternativeVerb verb = new()
+ {
+ Act = () =>
+ {
+ SwitchReagent(uid, component, args.User);
+ },
+ Text = Loc.GetString("autoreagent-switch"),
+ Priority = 2
+ };
+ args.Verbs.Add(verb);
+ }
+
+ private void SwitchReagent(EntityUid uid, SolutionRegenerationSwitcherComponent component, EntityUid user)
+ {
+ if (!TryComp(uid, out var solutionRegenerationComponent))
+ {
+ _sawmill.Warning($"{ToPrettyString(uid)} has no SolutionRegenerationComponent.");
+ return;
+ }
+
+ if (component.CurrentIndex + 1 == component.Options.Count)
+ component.CurrentIndex = 0;
+ else
+ component.CurrentIndex++;
+
+ if (!_solutionSystem.TryGetSolution(uid, solutionRegenerationComponent.Solution, out var solution))
+ {
+ _sawmill.Error($"Can't get SolutionRegeneration.Solution for {ToPrettyString(uid)}");
+ return;
+ }
+
+ var newSolution = component.Options[component.CurrentIndex];
+ var primaryId = newSolution.GetPrimaryReagentId();
+ if (primaryId == null)
+ {
+ _sawmill.Error($"Can't get PrimaryReagentId for {ToPrettyString(uid)} on index {component.CurrentIndex}.");
+ return;
+ }
+ ReagentPrototype? proto;
+
+ //Only reagents with spritePath property can change appearance of transformable containers!
+ if (!string.IsNullOrWhiteSpace(primaryId?.Prototype))
+ {
+ if (!_prototypeManager.TryIndex(primaryId.Value.Prototype, out proto))
+ {
+ _sawmill.Error($"Can't get get reagent prototype {primaryId} for {ToPrettyString(uid)}");
+ return;
+ }
+ }
+ else return;
+
+ // Empty out the current solution.
+ if (!component.KeepSolution)
+ solution.RemoveAllSolution();
+
+ // Replace the generating solution with the newly selected solution.
+ var generated = solutionRegenerationComponent.Generated;
+ generated.RemoveAllSolution();
+ _solutionSystem.TryAddSolution(uid, generated, newSolution);
+
+ _popups.PopupEntity(Loc.GetString("autoregen-switched", ("reagent", proto.LocalizedName)), user, user);
+ }
+ }
+}
diff --git a/Content.Server/Nyanotrasen/NPC/Systems/Components/Faction/ClothingAddFactionComponent.cs b/Content.Server/Nyanotrasen/NPC/Systems/Components/Faction/ClothingAddFactionComponent.cs
new file mode 100644
index 00000000000..ceff6c582df
--- /dev/null
+++ b/Content.Server/Nyanotrasen/NPC/Systems/Components/Faction/ClothingAddFactionComponent.cs
@@ -0,0 +1,20 @@
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Server.NPC.Components
+{
+ [RegisterComponent]
+ ///
+ /// Allows clothing to add a faction to you when you wear it.
+ ///
+ public sealed partial class ClothingAddFactionComponent : Component
+ {
+ public bool IsActive = false;
+
+ ///
+ /// Faction added
+ ///
+ [ViewVariables(VVAccess.ReadWrite),
+ DataField("faction", required: true, customTypeSerializer:typeof(PrototypeIdSerializer))]
+ public string Faction = "";
+ }
+}
diff --git a/Content.Server/Nyanotrasen/NPC/Systems/FactionSystem.Core.cs b/Content.Server/Nyanotrasen/NPC/Systems/FactionSystem.Core.cs
new file mode 100644
index 00000000000..3ce0ca73ebb
--- /dev/null
+++ b/Content.Server/Nyanotrasen/NPC/Systems/FactionSystem.Core.cs
@@ -0,0 +1,40 @@
+using Content.Server.NPC.Components;
+
+namespace Content.Server.NPC.Systems;
+
+public partial class NpcFactionSystem : EntitySystem
+{
+ public void InitializeCore()
+ {
+ SubscribeLocalEvent(OnGetNearbyHostiles);
+ }
+
+ public bool ContainsFaction(EntityUid uid, string faction, NpcFactionMemberComponent? component = null)
+ {
+ if (!Resolve(uid, ref component, false))
+ return false;
+
+ return component.Factions.Contains(faction);
+ }
+
+ public void AddFriendlyEntity(EntityUid uid, EntityUid fEntity, NpcFactionMemberComponent? component = null)
+ {
+ if (!Resolve(uid, ref component, false))
+ return;
+
+ component.ExceptionalFriendlies.Add(fEntity);
+ }
+
+ private void OnGetNearbyHostiles(EntityUid uid, NpcFactionMemberComponent component, ref GetNearbyHostilesEvent args)
+ {
+ args.ExceptionalFriendlies.UnionWith(component.ExceptionalFriendlies);
+ }
+}
+
+///
+/// Raised on an entity when it's trying to determine which nearby entities are hostile.
+///
+/// Entities that will be counted as hostile regardless of faction. Overriden by friendlies.
+/// Entities that will be counted as friendly regardless of faction. Overrides hostiles.
+[ByRefEvent]
+public readonly record struct GetNearbyHostilesEvent(HashSet ExceptionalHostiles, HashSet ExceptionalFriendlies);
diff --git a/Content.Server/Nyanotrasen/NPC/Systems/FactionSystem.Items.cs b/Content.Server/Nyanotrasen/NPC/Systems/FactionSystem.Items.cs
new file mode 100644
index 00000000000..0b971c33946
--- /dev/null
+++ b/Content.Server/Nyanotrasen/NPC/Systems/FactionSystem.Items.cs
@@ -0,0 +1,53 @@
+using Content.Server.NPC.Components;
+using Content.Server.Store.Systems;
+using Content.Shared.Clothing.Components;
+using Content.Shared.Inventory.Events;
+
+namespace Content.Server.NPC.Systems;
+
+public partial class NpcFactionSystem : EntitySystem
+{
+ public void InitializeItems()
+ {
+ SubscribeLocalEvent(OnItemPurchased);
+
+ SubscribeLocalEvent(OnClothingEquipped);
+ SubscribeLocalEvent(OnClothingUnequipped);
+ }
+
+ ///
+ /// If we bought something we probably don't want it to start biting us after it's automatically placed in our hands.
+ /// If you do, consider finding a better solution to grenade penguin CBT.
+ ///
+ private void OnItemPurchased(EntityUid uid, NpcFactionMemberComponent component, ref ItemPurchasedEvent args)
+ {
+ component.ExceptionalFriendlies.Add(args.Purchaser);
+ }
+
+ private void OnClothingEquipped(EntityUid uid, ClothingAddFactionComponent component, GotEquippedEvent args)
+ {
+ if (!TryComp(uid, out var clothing))
+ return;
+
+ if (!clothing.Slots.HasFlag(args.SlotFlags))
+ return;
+
+ if (!TryComp(args.Equipee, out var factionComponent))
+ return;
+
+ if (factionComponent.Factions.Contains(component.Faction))
+ return;
+
+ component.IsActive = true;
+ AddFaction(args.Equipee, component.Faction);
+ }
+
+ private void OnClothingUnequipped(EntityUid uid, ClothingAddFactionComponent component, GotUnequippedEvent args)
+ {
+ if (!component.IsActive)
+ return;
+
+ component.IsActive = false;
+ RemoveFaction(args.Equipee, component.Faction);
+ }
+}
diff --git a/Content.Server/Nyanotrasen/Psionics/AcceptPsionicsEui.cs b/Content.Server/Nyanotrasen/Psionics/AcceptPsionicsEui.cs
new file mode 100644
index 00000000000..80fd8946f28
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Psionics/AcceptPsionicsEui.cs
@@ -0,0 +1,34 @@
+using Content.Shared.Psionics;
+using Content.Shared.Eui;
+using Content.Server.EUI;
+using Content.Server.Abilities.Psionics;
+
+namespace Content.Server.Psionics
+{
+ public sealed class AcceptPsionicsEui : BaseEui
+ {
+ private readonly PsionicAbilitiesSystem _psionicsSystem;
+ private readonly EntityUid _entity;
+
+ public AcceptPsionicsEui(EntityUid entity, PsionicAbilitiesSystem psionicsSys)
+ {
+ _entity = entity;
+ _psionicsSystem = psionicsSys;
+ }
+
+ public override void HandleMessage(EuiMessageBase msg)
+ {
+ base.HandleMessage(msg);
+
+ if (msg is not AcceptPsionicsChoiceMessage choice ||
+ choice.Button == AcceptPsionicsUiButton.Deny)
+ {
+ Close();
+ return;
+ }
+
+ _psionicsSystem.AddRandomPsionicPower(_entity);
+ Close();
+ }
+ }
+}
diff --git a/Content.Server/Nyanotrasen/Psionics/AntiPsychicWeaponComponent.cs b/Content.Server/Nyanotrasen/Psionics/AntiPsychicWeaponComponent.cs
new file mode 100644
index 00000000000..00528afbe95
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Psionics/AntiPsychicWeaponComponent.cs
@@ -0,0 +1,24 @@
+using Content.Shared.Damage;
+
+namespace Content.Server.Psionics
+{
+ [RegisterComponent]
+ public sealed partial class AntiPsionicWeaponComponent : Component
+ {
+
+ [DataField("modifiers", required: true)]
+ public DamageModifierSet Modifiers = default!;
+
+ [DataField("psychicStaminaDamage")]
+ public float PsychicStaminaDamage = 30f;
+
+ [DataField("disableChance")]
+ public float DisableChance = 0.3f;
+
+ ///
+ /// Punish when used against a non-psychic.
+ /// DreamSetPrototypes = new[]
+ {
+ "adjectives",
+ "names_first",
+ "verbs",
+ };
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+ _accumulator += frameTime;
+ if (_accumulator < _updateRate)
+ return;
+
+ _accumulator -= _updateRate;
+ _updateRate = _random.NextFloat(10f, 30f);
+
+ foreach (var sleeper in EntityQuery())
+ {
+ if (!TryComp(sleeper.Owner, out var actor))
+ continue;
+
+ var setName = _random.Pick(DreamSetPrototypes);
+
+ if (!_prototypeManager.TryIndex(setName, out var set))
+ return;
+
+ var msg = _random.Pick(set.Values) + "..."; //todo... does the seperator need loc?
+
+ var messageWrap = Loc.GetString("chat-manager-send-telepathic-chat-wrap-message",
+ ("telepathicChannelName", Loc.GetString("chat-manager-telepathic-channel-name")), ("message", msg));
+
+ _chatManager.ChatMessageToOne(Shared.Chat.ChatChannel.Telepathic,
+ msg, messageWrap, sleeper.Owner, false, actor.PlayerSession.ConnectedClient, Color.PaleVioletRed);
+ }
+ }
+ }
+}
diff --git a/Content.Server/Nyanotrasen/Psionics/Glimmer/GlimmerCommands.cs b/Content.Server/Nyanotrasen/Psionics/Glimmer/GlimmerCommands.cs
new file mode 100644
index 00000000000..744f4cdb9a8
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Psionics/Glimmer/GlimmerCommands.cs
@@ -0,0 +1,39 @@
+using Content.Server.Administration;
+using Content.Shared.Psionics.Glimmer;
+using Content.Shared.Administration;
+using Robust.Shared.Console;
+
+namespace Content.Server.Psionics.Glimmer;
+
+[AdminCommand(AdminFlags.Logs)]
+public sealed class GlimmerShowCommand : IConsoleCommand
+{
+ public string Command => "glimmershow";
+ public string Description => Loc.GetString("command-glimmershow-description");
+ public string Help => Loc.GetString("command-glimmershow-help");
+ public async void Execute(IConsoleShell shell, string argStr, string[] args)
+ {
+ var entMan = IoCManager.Resolve();
+ shell.WriteLine(entMan.EntitySysManager.GetEntitySystem().Glimmer.ToString());
+ }
+}
+
+[AdminCommand(AdminFlags.Debug)]
+public sealed class GlimmerSetCommand : IConsoleCommand
+{
+ public string Command => "glimmerset";
+ public string Description => Loc.GetString("command-glimmerset-description");
+ public string Help => Loc.GetString("command-glimmerset-help");
+
+ public async void Execute(IConsoleShell shell, string argStr, string[] args)
+ {
+ if (args.Length != 1)
+ return;
+
+ if (!int.TryParse(args[0], out var glimmerValue))
+ return;
+
+ var entMan = IoCManager.Resolve();
+ entMan.EntitySysManager.GetEntitySystem().Glimmer = glimmerValue;
+ }
+}
diff --git a/Content.Server/Nyanotrasen/Psionics/Glimmer/GlimmerReactiveSystem.cs b/Content.Server/Nyanotrasen/Psionics/Glimmer/GlimmerReactiveSystem.cs
new file mode 100644
index 00000000000..a164a197464
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Psionics/Glimmer/GlimmerReactiveSystem.cs
@@ -0,0 +1,403 @@
+using Content.Server.Audio;
+using Content.Server.Power.Components;
+using Content.Server.Electrocution;
+using Content.Server.Lightning;
+using Content.Server.Explosion.EntitySystems;
+using Content.Server.Construction;
+using Content.Server.Ghost;
+using Content.Server.Revenant.EntitySystems;
+using Content.Shared.Audio;
+using Content.Shared.Construction.EntitySystems;
+using Content.Shared.Coordinates.Helpers;
+using Content.Shared.GameTicking;
+using Content.Shared.Psionics.Glimmer;
+using Content.Shared.Verbs;
+using Content.Shared.StatusEffect;
+using Content.Shared.Damage;
+using Content.Shared.Destructible;
+using Content.Shared.Construction.Components;
+using Robust.Shared.Audio;
+using Robust.Shared.Map;
+using Robust.Shared.Random;
+using Robust.Shared.Physics.Components;
+using Robust.Shared.Utility;
+
+namespace Content.Server.Psionics.Glimmer
+{
+ public sealed class GlimmerReactiveSystem : EntitySystem
+ {
+ [Dependency] private readonly GlimmerSystem _glimmerSystem = default!;
+ [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
+ [Dependency] private readonly ElectrocutionSystem _electrocutionSystem = default!;
+ [Dependency] private readonly SharedAudioSystem _sharedAudioSystem = default!;
+ [Dependency] private readonly SharedAmbientSoundSystem _sharedAmbientSoundSystem = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly LightningSystem _lightning = default!;
+ [Dependency] private readonly ExplosionSystem _explosionSystem = default!;
+ [Dependency] private readonly EntityLookupSystem _entityLookupSystem = default!;
+ [Dependency] private readonly AnchorableSystem _anchorableSystem = default!;
+ [Dependency] private readonly SharedDestructibleSystem _destructibleSystem = default!;
+ [Dependency] private readonly GhostSystem _ghostSystem = default!;
+ [Dependency] private readonly RevenantSystem _revenantSystem = default!;
+ [Dependency] private readonly IMapManager _mapManager = default!;
+ [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
+ [Dependency] private readonly SharedPointLightSystem _pointLightSystem = default!;
+
+ public float Accumulator = 0;
+ public const float UpdateFrequency = 15f;
+ public float BeamCooldown = 3;
+ public GlimmerTier LastGlimmerTier = GlimmerTier.Minimal;
+ public bool GhostsVisible = false;
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(Reset);
+
+ SubscribeLocalEvent(OnMapInit);
+ SubscribeLocalEvent(OnComponentRemove);
+ SubscribeLocalEvent(OnPowerChanged);
+ SubscribeLocalEvent(OnTierChanged);
+ SubscribeLocalEvent>(AddShockVerb);
+ SubscribeLocalEvent(OnDamageChanged);
+ SubscribeLocalEvent(OnDestroyed);
+ SubscribeLocalEvent(OnUnanchorAttempt);
+ }
+
+ ///
+ /// Update relevant state on an Entity.
+ ///
+ /// The number of steps in tier
+ /// difference since last update. This can be zero for the sake of
+ /// toggling the enabled states.
+ private void UpdateEntityState(EntityUid uid, SharedGlimmerReactiveComponent component, GlimmerTier currentGlimmerTier, int glimmerTierDelta)
+ {
+ var isEnabled = true;
+
+ if (component.RequiresApcPower)
+ if (TryComp(uid, out ApcPowerReceiverComponent? apcPower))
+ isEnabled = apcPower.Powered;
+
+ _appearanceSystem.SetData(uid, GlimmerReactiveVisuals.GlimmerTier, isEnabled ? currentGlimmerTier : GlimmerTier.Minimal);
+
+ // update ambient sound
+ if (TryComp(uid, out GlimmerSoundComponent? glimmerSound)
+ && TryComp(uid, out AmbientSoundComponent? ambientSoundComponent)
+ && glimmerSound.GetSound(currentGlimmerTier, out SoundSpecifier? spec))
+ {
+ if (spec != null)
+ _sharedAmbientSoundSystem.SetSound(uid, spec, ambientSoundComponent);
+ }
+
+ if (component.ModulatesPointLight) //SharedPointLightComponent is now being fetched via TryGetLight.
+ if (_pointLightSystem.TryGetLight(uid, out var pointLight))
+ {
+ _pointLightSystem.SetEnabled(uid, isEnabled ? currentGlimmerTier != GlimmerTier.Minimal : false, pointLight);
+ // The light energy and radius are kept updated even when off
+ // to prevent the need to store additional state.
+ //
+ // Note that this doesn't handle edge cases where the
+ // PointLightComponent is removed while the
+ // GlimmerReactiveComponent is still present.
+ _pointLightSystem.SetEnergy(uid, pointLight.Energy + glimmerTierDelta * component.GlimmerToLightEnergyFactor, pointLight);
+ _pointLightSystem.SetRadius(uid, pointLight.Radius + glimmerTierDelta * component.GlimmerToLightRadiusFactor, pointLight);
+ }
+
+ }
+
+ ///
+ /// Track when the component comes online so it can be given the
+ /// current status of the glimmer tier, if it wasn't around when an
+ /// update went out.
+ ///
+ private void OnMapInit(EntityUid uid, SharedGlimmerReactiveComponent component, MapInitEvent args)
+ {
+ if (component.RequiresApcPower && !HasComp(uid))
+ Logger.Warning($"{ToPrettyString(uid)} had RequiresApcPower set to true but no ApcPowerReceiverComponent was found on init.");
+
+ UpdateEntityState(uid, component, LastGlimmerTier, (int) LastGlimmerTier);
+ }
+
+ ///
+ /// Reset the glimmer tier appearance data if the component's removed,
+ /// just in case some objects can temporarily become reactive to the
+ /// glimmer.
+ ///
+ private void OnComponentRemove(EntityUid uid, SharedGlimmerReactiveComponent component, ComponentRemove args)
+ {
+ UpdateEntityState(uid, component, GlimmerTier.Minimal, -1 * (int) LastGlimmerTier);
+ }
+
+ ///
+ /// If the Entity has RequiresApcPower set to true, this will force an
+ /// update to the entity's state.
+ ///
+ private void OnPowerChanged(EntityUid uid, SharedGlimmerReactiveComponent component, ref PowerChangedEvent args)
+ {
+ if (component.RequiresApcPower)
+ UpdateEntityState(uid, component, LastGlimmerTier, 0);
+ }
+
+ ///
+ /// Enable / disable special effects from higher tiers.
+ ///
+ private void OnTierChanged(EntityUid uid, SharedGlimmerReactiveComponent component, GlimmerTierChangedEvent args)
+ {
+ if (!TryComp(uid, out var receiver))
+ return;
+
+ if (args.CurrentTier >= GlimmerTier.Dangerous)
+ {
+ if (!Transform(uid).Anchored)
+ AnchorOrExplode(uid);
+
+ receiver.PowerDisabled = false;
+ receiver.NeedsPower = false;
+ } else
+ {
+ receiver.NeedsPower = true;
+ }
+ }
+
+ private void AddShockVerb(EntityUid uid, SharedGlimmerReactiveComponent component, GetVerbsEvent args)
+ {
+ if(!args.CanAccess || !args.CanInteract)
+ return;
+
+ if (!TryComp(uid, out var receiver))
+ return;
+
+ if (receiver.NeedsPower)
+ return;
+
+ AlternativeVerb verb = new()
+ {
+ Act = () =>
+ {
+ _sharedAudioSystem.PlayPvs(component.ShockNoises, args.User);
+ _electrocutionSystem.TryDoElectrocution(args.User, null, _glimmerSystem.Glimmer / 200, TimeSpan.FromSeconds((float) _glimmerSystem.Glimmer / 100), false);
+ },
+ Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/Spare/poweronoff.svg.192dpi.png")),
+ Text = Loc.GetString("power-switch-component-toggle-verb"),
+ Priority = -3
+ };
+ args.Verbs.Add(verb);
+ }
+
+ private void OnDamageChanged(EntityUid uid, SharedGlimmerReactiveComponent component, DamageChangedEvent args)
+ {
+ if (args.Origin == null)
+ return;
+
+ if (!_random.Prob((float) _glimmerSystem.Glimmer / 1000))
+ return;
+
+ var tier = _glimmerSystem.GetGlimmerTier();
+ if (tier < GlimmerTier.High)
+ return;
+ Beam(uid, args.Origin.Value, tier);
+ }
+
+ private void OnDestroyed(EntityUid uid, SharedGlimmerReactiveComponent component, DestructionEventArgs args)
+ {
+ Spawn("MaterialBluespace1", Transform(uid).Coordinates);
+
+ var tier = _glimmerSystem.GetGlimmerTier();
+ if (tier < GlimmerTier.High)
+ return;
+
+ var totalIntensity = (float) (_glimmerSystem.Glimmer * 2);
+ var slope = (float) (11 - _glimmerSystem.Glimmer / 100);
+ var maxIntensity = 20;
+
+ var removed = (float) _glimmerSystem.Glimmer * _random.NextFloat(0.1f, 0.15f);
+ _glimmerSystem.Glimmer -= (int) removed;
+ BeamRandomNearProber(uid, _glimmerSystem.Glimmer / 350, _glimmerSystem.Glimmer / 50);
+ _explosionSystem.QueueExplosion(uid, "Default", totalIntensity, slope, maxIntensity);
+ }
+
+ private void OnUnanchorAttempt(EntityUid uid, SharedGlimmerReactiveComponent component, UnanchorAttemptEvent args)
+ {
+ if (_glimmerSystem.GetGlimmerTier() >= GlimmerTier.Dangerous)
+ {
+ _sharedAudioSystem.PlayPvs(component.ShockNoises, args.User);
+ _electrocutionSystem.TryDoElectrocution(args.User, null, _glimmerSystem.Glimmer / 200, TimeSpan.FromSeconds((float) _glimmerSystem.Glimmer / 100), false);
+ args.Cancel();
+ }
+ }
+
+ public void BeamRandomNearProber(EntityUid prober, int targets, float range = 10f)
+ {
+ List targetList = new();
+ foreach (var target in _entityLookupSystem.GetComponentsInRange(Transform(prober).Coordinates, range))
+ {
+ if (target.AllowedEffects.Contains("Electrocution"))
+ targetList.Add(target.Owner);
+ }
+
+ foreach(var reactive in _entityLookupSystem.GetComponentsInRange(Transform(prober).Coordinates, range))
+ {
+ targetList.Add(reactive.Owner);
+ }
+
+ _random.Shuffle(targetList);
+ foreach (var target in targetList)
+ {
+ if (targets <= 0)
+ return;
+
+ Beam(prober, target, _glimmerSystem.GetGlimmerTier(), false);
+ targets--;
+ }
+ }
+
+ private void Beam(EntityUid prober, EntityUid target, GlimmerTier tier, bool obeyCD = true)
+ {
+ if (obeyCD && BeamCooldown != 0)
+ return;
+
+ if (Deleted(prober) || Deleted(target))
+ return;
+
+ var lxform = Transform(prober);
+ var txform = Transform(target);
+
+ if (!lxform.Coordinates.TryDistance(EntityManager, txform.Coordinates, out var distance))
+ return;
+ if (distance > (float) (_glimmerSystem.Glimmer / 100))
+ return;
+
+ string beamproto;
+
+ switch (tier)
+ {
+ case GlimmerTier.Dangerous:
+ beamproto = "SuperchargedLightning";
+ break;
+ case GlimmerTier.Critical:
+ beamproto = "HyperchargedLightning";
+ break;
+ default:
+ beamproto = "ChargedLightning";
+ break;
+ }
+
+
+ _lightning.ShootLightning(prober, target, beamproto);
+ BeamCooldown += 3f;
+ }
+
+ private void AnchorOrExplode(EntityUid uid)
+ {
+ var xform = Transform(uid);
+ if (xform.Anchored)
+ return;
+
+ if (!TryComp(uid, out var physics))
+ return;
+
+ var coordinates = xform.Coordinates;
+ var gridUid = xform.GridUid;
+
+ if (_mapManager.TryGetGrid(gridUid, out var grid))
+ {
+ var tileIndices = grid.TileIndicesFor(coordinates);
+
+ if (_anchorableSystem.TileFree(grid, tileIndices, physics.CollisionLayer, physics.CollisionMask) &&
+ _transformSystem.AnchorEntity(uid, xform))
+ {
+ return;
+ }
+ }
+
+ // Wasn't able to get a grid or a free tile, so explode.
+ _destructibleSystem.DestroyEntity(uid);
+ }
+
+ private void Reset(RoundRestartCleanupEvent args)
+ {
+ Accumulator = 0;
+
+ // It is necessary that the GlimmerTier is reset to the default
+ // tier on round restart. This system will persist through
+ // restarts, and an undesired event will fire as a result after the
+ // start of the new round, causing modulatable PointLights to have
+ // negative Energy if the tier was higher than Minimal on restart.
+ LastGlimmerTier = GlimmerTier.Minimal;
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+ Accumulator += frameTime;
+ BeamCooldown = Math.Max(0, BeamCooldown - frameTime);
+
+ if (Accumulator > UpdateFrequency)
+ {
+ var currentGlimmerTier = _glimmerSystem.GetGlimmerTier();
+
+ var reactives = EntityQuery();
+ if (currentGlimmerTier != LastGlimmerTier) {
+ var glimmerTierDelta = (int) currentGlimmerTier - (int) LastGlimmerTier;
+ var ev = new GlimmerTierChangedEvent(LastGlimmerTier, currentGlimmerTier, glimmerTierDelta);
+
+ foreach (var reactive in reactives)
+ {
+ UpdateEntityState(reactive.Owner, reactive, currentGlimmerTier, glimmerTierDelta);
+ RaiseLocalEvent(reactive.Owner, ev);
+ }
+
+ LastGlimmerTier = currentGlimmerTier;
+ }
+ if (currentGlimmerTier == GlimmerTier.Critical)
+ {
+ _ghostSystem.MakeVisible(true);
+ _revenantSystem.MakeVisible(true);
+ GhostsVisible = true;
+ foreach (var reactive in reactives)
+ {
+ BeamRandomNearProber(reactive.Owner, 1, 12);
+ }
+ } else if (GhostsVisible == true)
+ {
+ _ghostSystem.MakeVisible(false);
+ _revenantSystem.MakeVisible(false);
+ GhostsVisible = false;
+ }
+ Accumulator = 0;
+ }
+ }
+ }
+
+ ///
+ /// This event is fired when the broader glimmer tier has changed,
+ /// not on every single adjustment to the glimmer count.
+ ///
+ /// has the exact
+ /// values corresponding to tiers.
+ ///
+ public class GlimmerTierChangedEvent : EntityEventArgs
+ {
+ ///
+ /// What was the last glimmer tier before this event fired?
+ ///
+ public readonly GlimmerTier LastTier;
+
+ ///
+ /// What is the current glimmer tier?
+ ///
+ public readonly GlimmerTier CurrentTier;
+
+ ///
+ /// What is the change in tiers between the last and current tier?
+ ///
+ public readonly int TierDelta;
+
+ public GlimmerTierChangedEvent(GlimmerTier lastTier, GlimmerTier currentTier, int tierDelta)
+ {
+ LastTier = lastTier;
+ CurrentTier = currentTier;
+ TierDelta = tierDelta;
+ }
+ }
+}
+
diff --git a/Content.Server/Nyanotrasen/Psionics/Glimmer/PassiveGlimmerReductionSystem.cs b/Content.Server/Nyanotrasen/Psionics/Glimmer/PassiveGlimmerReductionSystem.cs
new file mode 100644
index 00000000000..f0da85ce453
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Psionics/Glimmer/PassiveGlimmerReductionSystem.cs
@@ -0,0 +1,80 @@
+using Robust.Shared.Random;
+using Robust.Shared.Timing;
+using Robust.Shared.Configuration;
+using Content.Shared.CCVar;
+using Content.Shared.Psionics.Glimmer;
+using Content.Shared.GameTicking;
+using Content.Server.CartridgeLoader.Cartridges;
+
+namespace Content.Server.Psionics.Glimmer
+{
+ ///
+ /// Handles the passive reduction of glimmer.
+ ///
+ public sealed class PassiveGlimmerReductionSystem : EntitySystem
+ {
+ [Dependency] private readonly GlimmerSystem _glimmerSystem = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly IConfigurationManager _cfg = default!;
+ [Dependency] private readonly GlimmerMonitorCartridgeSystem _cartridgeSys = default!;
+
+ /// List of glimmer values spaced by minute.
+ public List GlimmerValues = new();
+
+ public TimeSpan TargetUpdatePeriod = TimeSpan.FromSeconds(6);
+
+ private int _updateIncrementor;
+ public TimeSpan NextUpdateTime = default!;
+ public TimeSpan LastUpdateTime = default!;
+
+ private float _glimmerLostPerSecond;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnRoundRestartCleanup);
+ _cfg.OnValueChanged(CCVars.GlimmerLostPerSecond, UpdatePassiveGlimmer, true);
+ }
+
+ private void OnRoundRestartCleanup(RoundRestartCleanupEvent args)
+ {
+ GlimmerValues.Clear();
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ var curTime = _timing.CurTime;
+ if (NextUpdateTime > curTime)
+ return;
+
+
+ var delta = curTime - LastUpdateTime;
+ var maxGlimmerLost = (int) Math.Round(delta.TotalSeconds * _glimmerLostPerSecond);
+
+ // It used to be 75% to lose one glimmer per ten seconds, but now it's 50% per six seconds.
+ // The probability is exactly the same over the same span of time. (0.25 ^ 3 == 0.5 ^ 6)
+ // This math is just easier to do for pausing's sake.
+ var actualGlimmerLost = _random.Next(0, 1 + maxGlimmerLost);
+
+ _glimmerSystem.Glimmer -= actualGlimmerLost;
+
+ _updateIncrementor++;
+
+ // Since we normally update every 6 seconds, this works out to a minute.
+ if (_updateIncrementor == 10)
+ {
+ GlimmerValues.Add(_glimmerSystem.Glimmer);
+
+ _updateIncrementor = 0;
+ }
+
+ NextUpdateTime = curTime + TargetUpdatePeriod;
+ LastUpdateTime = curTime;
+ }
+
+ private void UpdatePassiveGlimmer(float value) => _glimmerLostPerSecond = value;
+ }
+}
diff --git a/Content.Server/Nyanotrasen/Psionics/Glimmer/Structures/GlimmerSourceComponent.cs b/Content.Server/Nyanotrasen/Psionics/Glimmer/Structures/GlimmerSourceComponent.cs
new file mode 100644
index 00000000000..5babb6c446d
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Psionics/Glimmer/Structures/GlimmerSourceComponent.cs
@@ -0,0 +1,27 @@
+namespace Content.Server.Psionics.Glimmer
+{
+ [RegisterComponent]
+ ///
+ /// Adds to glimmer at regular intervals. We'll use it for glimmer drains too when we get there.
+ ///
+ public sealed partial class GlimmerSourceComponent : Component
+ {
+ [DataField("accumulator")]
+ public float Accumulator = 0f;
+
+ [DataField("active")]
+ public bool Active = true;
+
+ ///
+ /// Since glimmer is an int, we'll do it like this.
+ ///
+ [DataField("secondsPerGlimmer")]
+ public float SecondsPerGlimmer = 10f;
+
+ ///
+ /// True if it produces glimmer, false if it subtracts it.
+ ///
+ [DataField("addToGlimmer")]
+ public bool AddToGlimmer = true;
+ }
+}
diff --git a/Content.Server/Nyanotrasen/Psionics/Glimmer/Structures/GlimmerStructuresSystem.cs b/Content.Server/Nyanotrasen/Psionics/Glimmer/Structures/GlimmerStructuresSystem.cs
new file mode 100644
index 00000000000..75125569cb5
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Psionics/Glimmer/Structures/GlimmerStructuresSystem.cs
@@ -0,0 +1,84 @@
+using Content.Server.Anomaly.Components;
+using Content.Server.Power.Components;
+using Content.Server.Power.EntitySystems;
+using Content.Shared.Anomaly.Components;
+using Content.Shared.Psionics.Glimmer;
+
+namespace Content.Server.Psionics.Glimmer
+{
+ ///
+ /// Handles structures which add/subtract glimmer.
+ ///
+ public sealed class GlimmerStructuresSystem : EntitySystem
+ {
+ [Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!;
+ [Dependency] private readonly GlimmerSystem _glimmerSystem = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnAnomalyVesselPowerChanged);
+
+ SubscribeLocalEvent(OnAnomalyPulse);
+ SubscribeLocalEvent(OnAnomalySupercritical);
+ }
+
+ private void OnAnomalyVesselPowerChanged(EntityUid uid, AnomalyVesselComponent component, ref PowerChangedEvent args)
+ {
+ if (TryComp(component.Anomaly, out var glimmerSource))
+ glimmerSource.Active = args.Powered;
+ }
+
+ private void OnAnomalyPulse(EntityUid uid, GlimmerSourceComponent component, ref AnomalyPulseEvent args)
+ {
+ // Anomalies are meant to have GlimmerSource on them with the
+ // active flag set to false, as they will be set to actively
+ // generate glimmer when scanned to an anomaly vessel for
+ // harvesting research points.
+ //
+ // It is not a bug that glimmer increases on pulse or
+ // supercritical with an inactive glimmer source.
+ //
+ // However, this will need to be reworked if a distinction
+ // needs to be made in the future. I suggest a GlimmerAnomaly
+ // component.
+
+ if (TryComp(uid, out var anomaly))
+ _glimmerSystem.Glimmer += (int) (5f * anomaly.Severity);
+ }
+
+ private void OnAnomalySupercritical(EntityUid uid, GlimmerSourceComponent component, ref AnomalySupercriticalEvent args)
+ {
+ _glimmerSystem.Glimmer += 100;
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+ foreach (var source in EntityQuery())
+ {
+ if (!_powerReceiverSystem.IsPowered(source.Owner))
+ continue;
+
+ if (!source.Active)
+ continue;
+
+ source.Accumulator += frameTime;
+
+ if (source.Accumulator > source.SecondsPerGlimmer)
+ {
+ source.Accumulator -= source.SecondsPerGlimmer;
+ if (source.AddToGlimmer)
+ {
+ _glimmerSystem.Glimmer++;
+ }
+ else
+ {
+ _glimmerSystem.Glimmer--;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/Content.Server/Nyanotrasen/Psionics/Invisibility/PsionicInvisibilitySystem.cs b/Content.Server/Nyanotrasen/Psionics/Invisibility/PsionicInvisibilitySystem.cs
new file mode 100644
index 00000000000..d92cd0efe5f
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Psionics/Invisibility/PsionicInvisibilitySystem.cs
@@ -0,0 +1,141 @@
+using Content.Shared.Abilities.Psionics;
+using Content.Shared.Vehicle.Components;
+using Content.Server.Abilities.Psionics;
+using Content.Shared.Eye;
+using Content.Server.NPC.Systems;
+using Robust.Shared.Containers;
+using Robust.Server.GameObjects;
+
+namespace Content.Server.Psionics
+{
+ public sealed class PsionicInvisibilitySystem : EntitySystem
+ {
+ [Dependency] private readonly VisibilitySystem _visibilitySystem = default!;
+ [Dependency] private readonly PsionicInvisibilityPowerSystem _invisSystem = default!;
+ [Dependency] private readonly NpcFactionSystem _npcFactonSystem = default!;
+ [Dependency] private readonly SharedEyeSystem _eye = default!;
+ public override void Initialize()
+ {
+ base.Initialize();
+ /// Masking
+ SubscribeLocalEvent(OnInit);
+ SubscribeLocalEvent(OnInsulInit);
+ SubscribeLocalEvent(OnInsulShutdown);
+ SubscribeLocalEvent(OnEyeInit);
+
+ /// Layer
+ SubscribeLocalEvent(OnInvisInit);
+ SubscribeLocalEvent(OnInvisShutdown);
+
+ // PVS Stuff
+ SubscribeLocalEvent(OnEntInserted);
+ SubscribeLocalEvent(OnEntRemoved);
+ }
+
+ private void OnInit(EntityUid uid, PotentialPsionicComponent component, ComponentInit args)
+ {
+ SetCanSeePsionicInvisiblity(uid, false);
+ }
+
+ private void OnInsulInit(EntityUid uid, PsionicInsulationComponent component, ComponentInit args)
+ {
+ if (!HasComp(uid))
+ return;
+
+ if (HasComp(uid))
+ _invisSystem.ToggleInvisibility(uid);
+
+ if (_npcFactonSystem.ContainsFaction(uid, "PsionicInterloper"))
+ {
+ component.SuppressedFactions.Add("PsionicInterloper");
+ _npcFactonSystem.RemoveFaction(uid, "PsionicInterloper");
+ }
+
+ if (_npcFactonSystem.ContainsFaction(uid, "GlimmerMonster"))
+ {
+ component.SuppressedFactions.Add("GlimmerMonster");
+ _npcFactonSystem.RemoveFaction(uid, "GlimmerMonster");
+ }
+
+ SetCanSeePsionicInvisiblity(uid, true);
+ }
+
+ private void OnInsulShutdown(EntityUid uid, PsionicInsulationComponent component, ComponentShutdown args)
+ {
+ if (!HasComp(uid))
+ return;
+
+ SetCanSeePsionicInvisiblity(uid, false);
+
+ if (!HasComp(uid))
+ {
+ component.SuppressedFactions.Clear();
+ return;
+ }
+
+ foreach (var faction in component.SuppressedFactions)
+ {
+ _npcFactonSystem.AddFaction(uid, faction);
+ }
+ component.SuppressedFactions.Clear();
+ }
+
+ private void OnInvisInit(EntityUid uid, PsionicallyInvisibleComponent component, ComponentInit args)
+ {
+ var visibility = EntityManager.EnsureComponent(uid);
+
+ _visibilitySystem.AddLayer(visibility, (int) VisibilityFlags.PsionicInvisibility, false);
+ _visibilitySystem.RemoveLayer(visibility, (int) VisibilityFlags.Normal, false);
+ _visibilitySystem.RefreshVisibility(visibility);
+
+ SetCanSeePsionicInvisiblity(uid, true);
+ }
+
+
+ private void OnInvisShutdown(EntityUid uid, PsionicallyInvisibleComponent component, ComponentShutdown args)
+ {
+ if (TryComp(uid, out var visibility))
+ {
+ _visibilitySystem.RemoveLayer(visibility, (int) VisibilityFlags.PsionicInvisibility, false);
+ _visibilitySystem.AddLayer(visibility, (int) VisibilityFlags.Normal, false);
+ _visibilitySystem.RefreshVisibility(visibility);
+ }
+ if (HasComp(uid) && !HasComp(uid))
+ SetCanSeePsionicInvisiblity(uid, false);
+ }
+
+ private void OnEyeInit(EntityUid uid, EyeComponent component, ComponentInit args)
+ {
+ if (HasComp(uid) || HasComp(uid))
+ return;
+
+ //SetCanSeePsionicInvisiblity(uid, true); //JJ Comment - Not allowed to modifies .yml on spawn any longer. See UninitializedSaveTest.
+ }
+ private void OnEntInserted(EntityUid uid, PsionicallyInvisibleComponent component, EntInsertedIntoContainerMessage args)
+ {
+ DirtyEntity(args.Entity);
+ }
+
+ private void OnEntRemoved(EntityUid uid, PsionicallyInvisibleComponent component, EntRemovedFromContainerMessage args)
+ {
+ DirtyEntity(args.Entity);
+ }
+
+ public void SetCanSeePsionicInvisiblity(EntityUid uid, bool set)
+ {
+ if (set == true)
+ {
+ if (EntityManager.TryGetComponent(uid, out EyeComponent? eye))
+ {
+ _eye.SetVisibilityMask(uid, eye.VisibilityMask | (int) VisibilityFlags.PsionicInvisibility, eye);
+ }
+ } else
+ {
+ if (EntityManager.TryGetComponent(uid, out EyeComponent? eye))
+ {
+ //_eye.SetVisibilityMask(uid, eye.VisibilityMask & (int) VisibilityFlags.PsionicInvisibility, eye);
+ }
+ }
+ }
+ }
+}
diff --git a/Content.Server/Nyanotrasen/Psionics/Invisibility/PsionicInvisibleContactsComponent.cs b/Content.Server/Nyanotrasen/Psionics/Invisibility/PsionicInvisibleContactsComponent.cs
new file mode 100644
index 00000000000..859ceb7b83a
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Psionics/Invisibility/PsionicInvisibleContactsComponent.cs
@@ -0,0 +1,19 @@
+using Content.Shared.Whitelist;
+using Robust.Shared.Timing;
+
+namespace Content.Server.Psionics
+{
+ [RegisterComponent]
+ public sealed partial class PsionicInvisibleContactsComponent : Component
+ {
+ [DataField("whitelist", required: true)]
+ public EntityWhitelist Whitelist = default!;
+
+ ///
+ /// This tracks how many valid entities are being contacted,
+ /// so when you stop touching one, you don't immediately lose invisibility.
+ ///
+ [DataField("stages")]
+ public int Stages = 0;
+ }
+}
diff --git a/Content.Server/Nyanotrasen/Psionics/Invisibility/PsionicInvisibleContactsSystem.cs b/Content.Server/Nyanotrasen/Psionics/Invisibility/PsionicInvisibleContactsSystem.cs
new file mode 100644
index 00000000000..cec755e3260
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Psionics/Invisibility/PsionicInvisibleContactsSystem.cs
@@ -0,0 +1,68 @@
+using Content.Shared.Stealth;
+using Content.Shared.Stealth.Components;
+using Robust.Shared.Physics.Events;
+using Robust.Shared.Physics.Systems;
+using Robust.Shared.Timing;
+
+namespace Content.Server.Psionics
+{
+ ///
+ /// Allows an entity to become psionically invisible when touching certain entities.
+ ///
+ public sealed class PsionicInvisibleContactsSystem : EntitySystem
+ {
+ [Dependency] private readonly SharedStealthSystem _stealth = default!;
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnEntityEnter);
+ SubscribeLocalEvent(OnEntityExit);
+
+ UpdatesAfter.Add(typeof(SharedPhysicsSystem));
+ }
+
+ private void OnEntityEnter(EntityUid uid, PsionicInvisibleContactsComponent component, ref StartCollideEvent args)
+ {
+ var otherUid = args.OtherEntity;
+ var ourEntity = args.OurEntity;
+
+ if (!component.Whitelist.IsValid(otherUid))
+ return;
+
+ // This will go up twice per web hit, since webs also have a flammable fixture.
+ // It goes down twice per web exit, so everything's fine.
+ ++component.Stages;
+
+ if (HasComp(ourEntity))
+ return;
+
+ EnsureComp(ourEntity);
+ var stealth = EnsureComp(ourEntity);
+ _stealth.SetVisibility(ourEntity, 0.66f, stealth);
+ }
+
+ private void OnEntityExit(EntityUid uid, PsionicInvisibleContactsComponent component, ref EndCollideEvent args)
+ {
+ var otherUid = args.OtherEntity;
+ var ourEntity = args.OurEntity;
+
+ if (!component.Whitelist.IsValid(otherUid))
+ return;
+
+ if (!HasComp(ourEntity))
+ return;
+
+ if (--component.Stages > 0)
+ return;
+
+ RemComp(ourEntity);
+ var stealth = EnsureComp(ourEntity);
+ // Just to be sure...
+ _stealth.SetVisibility(ourEntity, 1f, stealth);
+
+ RemComp(ourEntity);
+ }
+ }
+}
diff --git a/Content.Server/Nyanotrasen/Psionics/Invisibility/PsionicallyInvisibleComponent.cs b/Content.Server/Nyanotrasen/Psionics/Invisibility/PsionicallyInvisibleComponent.cs
new file mode 100644
index 00000000000..5352f5737f2
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Psionics/Invisibility/PsionicallyInvisibleComponent.cs
@@ -0,0 +1,6 @@
+namespace Content.Server.Psionics
+{
+ [RegisterComponent]
+ public sealed partial class PsionicallyInvisibleComponent : Component
+ {}
+}
diff --git a/Content.Server/Nyanotrasen/Psionics/PotentialPsionicComponent.cs b/Content.Server/Nyanotrasen/Psionics/PotentialPsionicComponent.cs
new file mode 100644
index 00000000000..9499497cd1d
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Psionics/PotentialPsionicComponent.cs
@@ -0,0 +1,14 @@
+namespace Content.Server.Psionics
+{
+ [RegisterComponent]
+ public sealed partial class PotentialPsionicComponent : Component
+ {
+ [DataField("chance")]
+ public float Chance = 0.04f;
+
+ ///
+ /// YORO (you only reroll once)
+ ///
+ public bool Rerolled = false;
+ }
+}
diff --git a/Content.Server/Nyanotrasen/Psionics/PsionicAwaitingPlayerComponent.cs b/Content.Server/Nyanotrasen/Psionics/PsionicAwaitingPlayerComponent.cs
new file mode 100644
index 00000000000..f9cc9339d4e
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Psionics/PsionicAwaitingPlayerComponent.cs
@@ -0,0 +1,9 @@
+namespace Content.Server.Psionics
+{
+ ///
+ /// Will open the 'accept psionics' UI when a player attaches.
+ ///
+ [RegisterComponent]
+ public sealed partial class PsionicAwaitingPlayerComponent : Component
+ {}
+}
diff --git a/Content.Server/Nyanotrasen/Psionics/PsionicBonusChanceComponent.cs b/Content.Server/Nyanotrasen/Psionics/PsionicBonusChanceComponent.cs
new file mode 100644
index 00000000000..d9cbc511477
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Psionics/PsionicBonusChanceComponent.cs
@@ -0,0 +1,18 @@
+namespace Content.Server.Psionics
+{
+ [RegisterComponent]
+ public sealed partial class PsionicBonusChanceComponent : Component
+ {
+ [DataField("multiplier")]
+ public float Multiplier = 1f;
+ [DataField("flatBonus")]
+ public float FlatBonus = 0;
+
+ ///
+ /// Whether we should warn the user they are about to receive psionics.
+ /// It's here because AddComponentSpecial can't overwrite a component, and this is very role dependent.
+ ///
+ [DataField("warn")]
+ public bool Warn = true;
+ }
+}
diff --git a/Content.Server/Nyanotrasen/Psionics/PsionicsCommands.cs b/Content.Server/Nyanotrasen/Psionics/PsionicsCommands.cs
new file mode 100644
index 00000000000..c0c3b8dc01b
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Psionics/PsionicsCommands.cs
@@ -0,0 +1,34 @@
+using Content.Server.Administration;
+using Content.Shared.Administration;
+using Content.Shared.Abilities.Psionics;
+using Content.Shared.Mobs.Components;
+using Robust.Shared.Console;
+using Robust.Server.GameObjects;
+using Content.Shared.Actions;
+
+namespace Content.Server.Psionics;
+
+[AdminCommand(AdminFlags.Logs)]
+public sealed class ListPsionicsCommand : IConsoleCommand
+{
+ public string Command => "lspsionics";
+ public string Description => Loc.GetString("command-lspsionic-description");
+ public string Help => Loc.GetString("command-lspsionic-help");
+ public async void Execute(IConsoleShell shell, string argStr, string[] args)
+ {
+ SharedActionsSystem actions = default!;
+ var entMan = IoCManager.Resolve();
+ foreach (var (actor, mob, psionic, meta) in entMan.EntityQuery()){
+ // filter out xenos, etc, with innate telepathy
+ actions.TryGetActionData( psionic.PsionicAbility, out var actionData );
+ if (actionData == null || actionData.ToString() == null)
+ return;
+
+ var psiPowerName = actionData.ToString();
+ if (psiPowerName == null)
+ return;
+
+ shell.WriteLine(meta.EntityName + " (" + meta.Owner + ") - " + actor.PlayerSession.Name + Loc.GetString(psiPowerName));
+ }
+ }
+}
diff --git a/Content.Server/Nyanotrasen/Psionics/PsionicsSystem.cs b/Content.Server/Nyanotrasen/Psionics/PsionicsSystem.cs
new file mode 100644
index 00000000000..8943ee01c43
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Psionics/PsionicsSystem.cs
@@ -0,0 +1,192 @@
+using Content.Shared.Abilities.Psionics;
+using Content.Shared.StatusEffect;
+using Content.Shared.Mobs;
+using Content.Shared.Psionics.Glimmer;
+using Content.Shared.Weapons.Melee.Events;
+using Content.Shared.Damage.Events;
+using Content.Shared.IdentityManagement;
+using Content.Shared.CCVar;
+using Content.Server.Abilities.Psionics;
+using Content.Server.Chat.Systems;
+using Content.Server.Electrocution;
+using Content.Server.NPC.Components;
+using Content.Server.NPC.Systems;
+using Robust.Shared.Audio;
+using Robust.Shared.Player;
+using Robust.Shared.Configuration;
+using Robust.Shared.Random;
+
+namespace Content.Server.Psionics
+{
+ public sealed class PsionicsSystem : EntitySystem
+ {
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly PsionicAbilitiesSystem _psionicAbilitiesSystem = default!;
+ [Dependency] private readonly StatusEffectsSystem _statusEffects = default!;
+ [Dependency] private readonly ElectrocutionSystem _electrocutionSystem = default!;
+ [Dependency] private readonly MindSwapPowerSystem _mindSwapPowerSystem = default!;
+ [Dependency] private readonly GlimmerSystem _glimmerSystem = default!;
+ [Dependency] private readonly ChatSystem _chat = default!;
+ [Dependency] private readonly NpcFactionSystem _npcFactonSystem = default!;
+ [Dependency] private readonly IConfigurationManager _cfg = default!;
+
+ ///
+ /// Unfortunately, since spawning as a normal role and anything else is so different,
+ /// this is the only way to unify them, for now at least.
+ ///
+ Queue<(PotentialPsionicComponent component, EntityUid uid)> _rollers = new();
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+ foreach (var roller in _rollers)
+ {
+ RollPsionics(roller.uid, roller.component, false);
+ }
+ _rollers.Clear();
+ }
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnStartup);
+ SubscribeLocalEvent(OnMeleeHit);
+ SubscribeLocalEvent(OnStamHit);
+
+ SubscribeLocalEvent(OnDeathGasp);
+
+ SubscribeLocalEvent(OnInit);
+ SubscribeLocalEvent(OnRemove);
+ }
+
+ private void OnStartup(EntityUid uid, PotentialPsionicComponent component, MapInitEvent args)
+ {
+ if (HasComp(uid))
+ return;
+
+ _rollers.Enqueue((component, uid));
+ }
+
+ private void OnMeleeHit(EntityUid uid, AntiPsionicWeaponComponent component, MeleeHitEvent args)
+ {
+ foreach (var entity in args.HitEntities)
+ {
+ if (HasComp(entity))
+ {
+ SoundSystem.Play("/Audio/Effects/lightburn.ogg", Filter.Pvs(entity), entity);
+ args.ModifiersList.Add(component.Modifiers);
+ if (_random.Prob(component.DisableChance))
+ _statusEffects.TryAddStatusEffect(entity, "PsionicsDisabled", TimeSpan.FromSeconds(10), true, "PsionicsDisabled");
+ }
+
+ if (TryComp(entity, out var swapped))
+ {
+ _mindSwapPowerSystem.Swap(entity, swapped.OriginalEntity, true);
+ return;
+ }
+
+ if (component.Punish && HasComp(entity) && !HasComp(entity) && _random.Prob(0.5f))
+ _electrocutionSystem.TryDoElectrocution(args.User, null, 20, TimeSpan.FromSeconds(5), false);
+ }
+ }
+
+ private void OnDeathGasp(EntityUid uid, PotentialPsionicComponent component, MobStateChangedEvent args)
+ {
+ if (args.NewMobState != MobState.Dead)
+ return;
+
+ string message;
+
+ switch (_glimmerSystem.GetGlimmerTier())
+ {
+ case GlimmerTier.Critical:
+ message = Loc.GetString("death-gasp-high", ("ent", Identity.Entity(uid, EntityManager)));
+ break;
+ case GlimmerTier.Dangerous:
+ message = Loc.GetString("death-gasp-medium", ("ent",Identity.Entity(uid, EntityManager)));
+ break;
+ default:
+ message = Loc.GetString("death-gasp-normal", ("ent", Identity.Entity(uid, EntityManager)));
+ break;
+ }
+ //Was force, changed to ignoreActionBlocker.
+ _chat.TrySendInGameICMessage(uid, message, InGameICChatType.Emote, false, ignoreActionBlocker:true);
+ }
+
+ private void OnInit(EntityUid uid, PsionicComponent component, ComponentInit args)
+ {
+ if (!component.Removable)
+ return;
+
+ if (!TryComp(uid, out var factions))
+ return;
+
+ if (_npcFactonSystem.ContainsFaction(uid, "GlimmerMonster", factions))
+ return;
+
+ _npcFactonSystem.AddFaction(uid, "PsionicInterloper");
+ }
+
+ private void OnRemove(EntityUid uid, PsionicComponent component, ComponentRemove args)
+ {
+ if (!TryComp(uid, out var factions))
+ return;
+
+ _npcFactonSystem.RemoveFaction(uid, "PsionicInterloper");
+ }
+
+ private void OnStamHit(EntityUid uid, AntiPsionicWeaponComponent component, StaminaMeleeHitEvent args)
+ {
+ var bonus = false;
+ foreach (var stam in args.HitList)
+ {
+ if (HasComp(stam.Entity))
+ bonus = true;
+ }
+
+ if (!bonus)
+ return;
+
+
+ args.FlatModifier += component.PsychicStaminaDamage;
+ }
+
+ public void RollPsionics(EntityUid uid, PotentialPsionicComponent component, bool applyGlimmer = true, float multiplier = 1f)
+ {
+ if (HasComp(uid))
+ return;
+
+ if (!_cfg.GetCVar(CCVars.PsionicRollsEnabled))
+ return;
+
+ var chance = component.Chance;
+ var warn = true;
+ if (TryComp(uid, out var bonus))
+ {
+ chance *= bonus.Multiplier;
+ chance += bonus.FlatBonus;
+ warn = bonus.Warn;
+ }
+
+ if (applyGlimmer)
+ chance += ((float) _glimmerSystem.Glimmer / 1000);
+
+ chance *= multiplier;
+
+ chance = Math.Clamp(chance, 0, 1);
+
+ if (_random.Prob(chance))
+ _psionicAbilitiesSystem.AddPsionics(uid, warn);
+ }
+
+ public void RerollPsionics(EntityUid uid, PotentialPsionicComponent? psionic = null, float bonusMuliplier = 1f)
+ {
+ if (!Resolve(uid, ref psionic, false))
+ return;
+
+ if (psionic.Rerolled)
+ return;
+
+ RollPsionics(uid, psionic, multiplier: bonusMuliplier);
+ psionic.Rerolled = true;
+ }
+ }
+}
diff --git a/Content.Server/Nyanotrasen/StationEvents/Components/FreeProberRuleComponent.cs b/Content.Server/Nyanotrasen/StationEvents/Components/FreeProberRuleComponent.cs
new file mode 100644
index 00000000000..1a23a496312
--- /dev/null
+++ b/Content.Server/Nyanotrasen/StationEvents/Components/FreeProberRuleComponent.cs
@@ -0,0 +1,8 @@
+using Content.Server.StationEvents.Events;
+
+namespace Content.Server.StationEvents.Components;
+
+[RegisterComponent, Access(typeof(FreeProberRule))]
+public sealed partial class FreeProberRuleComponent : Component
+{
+}
diff --git a/Content.Server/Nyanotrasen/StationEvents/Components/GlimmerEventComponent.cs b/Content.Server/Nyanotrasen/StationEvents/Components/GlimmerEventComponent.cs
new file mode 100644
index 00000000000..7d10f6a440f
--- /dev/null
+++ b/Content.Server/Nyanotrasen/StationEvents/Components/GlimmerEventComponent.cs
@@ -0,0 +1,34 @@
+namespace Content.Server.Psionics.Glimmer;
+
+[RegisterComponent]
+public sealed partial class GlimmerEventComponent : Component
+{
+ ///
+ /// Minimum glimmer value for event to be eligible. (Should be 100 at lowest.)
+ ///
+ [DataField("minimumGlimmer")]
+ public int MinimumGlimmer = 100;
+
+ ///
+ /// Maximum glimmer value for event to be eligible. (Remember 1000 is max glimmer period.)
+ ///
+ [DataField("maximumGlimmer")]
+ public int MaximumGlimmer = 1000;
+
+ ///
+ /// Will be used for _random.Next and subtracted from glimmer.
+ /// Lower bound.
+ ///
+ [DataField("glimmerBurnLower")]
+ public int GlimmerBurnLower = 25;
+
+ ///
+ /// Will be used for _random.Next and subtracted from glimmer.
+ /// Upper bound.
+ ///
+ [DataField("glimmerBurnUpper")]
+ public int GlimmerBurnUpper = 70;
+
+ [DataField("report")]
+ public string SophicReport = "glimmer-event-report-generic";
+}
diff --git a/Content.Server/Nyanotrasen/StationEvents/Components/GlimmerRandomSentienceRuleComponent.cs b/Content.Server/Nyanotrasen/StationEvents/Components/GlimmerRandomSentienceRuleComponent.cs
new file mode 100644
index 00000000000..d8ada8699c9
--- /dev/null
+++ b/Content.Server/Nyanotrasen/StationEvents/Components/GlimmerRandomSentienceRuleComponent.cs
@@ -0,0 +1,10 @@
+using Content.Server.StationEvents.Events;
+
+namespace Content.Server.StationEvents.Components;
+
+[RegisterComponent, Access(typeof(GlimmerRandomSentienceRule))]
+public sealed partial class GlimmerRandomSentienceRuleComponent : Component
+{
+ [DataField("maxMakeSentient")]
+ public int MaxMakeSentient = 4;
+}
diff --git a/Content.Server/Nyanotrasen/StationEvents/Components/GlimmerRevenantSpawnRuleComponent.cs b/Content.Server/Nyanotrasen/StationEvents/Components/GlimmerRevenantSpawnRuleComponent.cs
new file mode 100644
index 00000000000..c612ba617b4
--- /dev/null
+++ b/Content.Server/Nyanotrasen/StationEvents/Components/GlimmerRevenantSpawnRuleComponent.cs
@@ -0,0 +1,10 @@
+using Content.Server.StationEvents.Events;
+
+namespace Content.Server.StationEvents.Components;
+
+[RegisterComponent, Access(typeof(GlimmerRevenantRule))]
+public sealed partial class GlimmerRevenantRuleComponent : Component
+{
+ [DataField("prototype")]
+ public string RevenantPrototype = "MobRevenant";
+}
diff --git a/Content.Server/Nyanotrasen/StationEvents/Components/GlimmerWispRuleComponent.cs b/Content.Server/Nyanotrasen/StationEvents/Components/GlimmerWispRuleComponent.cs
new file mode 100644
index 00000000000..60477e6a6a7
--- /dev/null
+++ b/Content.Server/Nyanotrasen/StationEvents/Components/GlimmerWispRuleComponent.cs
@@ -0,0 +1,8 @@
+using Content.Server.StationEvents.Events;
+
+namespace Content.Server.StationEvents.Components;
+
+[RegisterComponent, Access(typeof(GlimmerWispRule))]
+public sealed partial class GlimmerWispRuleComponent : Component
+{
+}
diff --git a/Content.Server/Nyanotrasen/StationEvents/Components/MassMindSwapRuleComponent.cs b/Content.Server/Nyanotrasen/StationEvents/Components/MassMindSwapRuleComponent.cs
new file mode 100644
index 00000000000..8302188e805
--- /dev/null
+++ b/Content.Server/Nyanotrasen/StationEvents/Components/MassMindSwapRuleComponent.cs
@@ -0,0 +1,13 @@
+using Content.Server.StationEvents.Events;
+
+namespace Content.Server.StationEvents.Components;
+
+[RegisterComponent, Access(typeof(MassMindSwapRule))]
+public sealed partial class MassMindSwapRuleComponent : Component
+{
+ ///
+ /// The mind swap is only temporary if true.
+ ///
+ [DataField("isTemporary")]
+ public bool IsTemporary;
+}
diff --git a/Content.Server/Nyanotrasen/StationEvents/Components/MidRoundAntagRuleComponent.cs b/Content.Server/Nyanotrasen/StationEvents/Components/MidRoundAntagRuleComponent.cs
new file mode 100644
index 00000000000..429db920dca
--- /dev/null
+++ b/Content.Server/Nyanotrasen/StationEvents/Components/MidRoundAntagRuleComponent.cs
@@ -0,0 +1,16 @@
+using Content.Server.StationEvents.Events;
+
+namespace Content.Server.StationEvents.Components;
+
+[RegisterComponent, Access(typeof(MidRoundAntagRule))]
+public sealed partial class MidRoundAntagRuleComponent : Component
+{
+ [DataField("antags")]
+ public IReadOnlyList MidRoundAntags = new[]
+ {
+ "SpawnPointGhostRatKing",
+ "SpawnPointGhostVampSpider",
+ "SpawnPointGhostFugitive",
+ "MobEvilTwinSpawn"
+ };
+}
diff --git a/Content.Server/Nyanotrasen/StationEvents/Components/MidRoundAntagSpawnLocationComponent.cs b/Content.Server/Nyanotrasen/StationEvents/Components/MidRoundAntagSpawnLocationComponent.cs
new file mode 100644
index 00000000000..655e0a50710
--- /dev/null
+++ b/Content.Server/Nyanotrasen/StationEvents/Components/MidRoundAntagSpawnLocationComponent.cs
@@ -0,0 +1,8 @@
+namespace Content.Server.StationEvents
+{
+ [RegisterComponent]
+ public sealed partial class MidRoundAntagSpawnLocationComponent : Component
+ {
+
+ }
+}
diff --git a/Content.Server/Nyanotrasen/StationEvents/Components/MundaneDischargeRuleComponent.cs b/Content.Server/Nyanotrasen/StationEvents/Components/MundaneDischargeRuleComponent.cs
new file mode 100644
index 00000000000..ac188f3d9aa
--- /dev/null
+++ b/Content.Server/Nyanotrasen/StationEvents/Components/MundaneDischargeRuleComponent.cs
@@ -0,0 +1,8 @@
+using Content.Server.StationEvents.Events;
+
+namespace Content.Server.StationEvents.Components;
+
+[RegisterComponent, Access(typeof(MundaneDischargeRule))]
+public sealed partial class MundaneDischargeRuleComponent : Component
+{
+}
diff --git a/Content.Server/Nyanotrasen/StationEvents/Components/NoosphericFryRuleComponent.cs b/Content.Server/Nyanotrasen/StationEvents/Components/NoosphericFryRuleComponent.cs
new file mode 100644
index 00000000000..845cef461c7
--- /dev/null
+++ b/Content.Server/Nyanotrasen/StationEvents/Components/NoosphericFryRuleComponent.cs
@@ -0,0 +1,8 @@
+using Content.Server.StationEvents.Events;
+
+namespace Content.Server.StationEvents.Components;
+
+[RegisterComponent, Access(typeof(NoosphericFryRule))]
+public sealed partial class NoosphericFryRuleComponent : Component
+{
+}
diff --git a/Content.Server/Nyanotrasen/StationEvents/Components/NoosphericStormRuleComponent.cs b/Content.Server/Nyanotrasen/StationEvents/Components/NoosphericStormRuleComponent.cs
new file mode 100644
index 00000000000..3720d938a3a
--- /dev/null
+++ b/Content.Server/Nyanotrasen/StationEvents/Components/NoosphericStormRuleComponent.cs
@@ -0,0 +1,29 @@
+using Content.Server.StationEvents.Events;
+
+namespace Content.Server.StationEvents.Components;
+
+[RegisterComponent, Access(typeof(NoosphericStormRule))]
+public sealed partial class NoosphericStormRuleComponent : Component
+{
+ ///
+ /// How many potential psionics should be awakened at most.
+ ///
+ [DataField("maxAwaken")]
+ public int MaxAwaken = 3;
+
+ ///
+ ///
+ [DataField("baseGlimmerAddMin")]
+ public int BaseGlimmerAddMin = 65;
+
+ ///
+ ///
+ [DataField("baseGlimmerAddMax")]
+ public int BaseGlimmerAddMax = 85;
+
+ ///
+ /// Multiply the EventSeverityModifier by this to determine how much extra glimmer to add.
+ ///
+ [DataField("glimmerSeverityCoefficient")]
+ public float GlimmerSeverityCoefficient = 0.25f;
+}
diff --git a/Content.Server/Nyanotrasen/StationEvents/Components/NoosphericZapRuleComponent.cs b/Content.Server/Nyanotrasen/StationEvents/Components/NoosphericZapRuleComponent.cs
new file mode 100644
index 00000000000..bfa82644cd6
--- /dev/null
+++ b/Content.Server/Nyanotrasen/StationEvents/Components/NoosphericZapRuleComponent.cs
@@ -0,0 +1,8 @@
+using Content.Server.StationEvents.Events;
+
+namespace Content.Server.StationEvents.Components;
+
+[RegisterComponent, Access(typeof(NoosphericZapRule))]
+public sealed partial class NoosphericZapRuleComponent : Component
+{
+}
diff --git a/Content.Server/Nyanotrasen/StationEvents/Components/PsionicCatGotYourTongueRuleComponent.cs b/Content.Server/Nyanotrasen/StationEvents/Components/PsionicCatGotYourTongueRuleComponent.cs
new file mode 100644
index 00000000000..bc94eb8d677
--- /dev/null
+++ b/Content.Server/Nyanotrasen/StationEvents/Components/PsionicCatGotYourTongueRuleComponent.cs
@@ -0,0 +1,17 @@
+using Robust.Shared.Audio;
+using Content.Server.StationEvents.Events;
+
+namespace Content.Server.StationEvents.Components;
+
+[RegisterComponent, Access(typeof(PsionicCatGotYourTongueRule))]
+public sealed partial class PsionicCatGotYourTongueRuleComponent : Component
+{
+ [DataField("minDuration")]
+ public TimeSpan MinDuration = TimeSpan.FromSeconds(20);
+
+ [DataField("maxDuration")]
+ public TimeSpan MaxDuration = TimeSpan.FromSeconds(80);
+
+ [DataField("sound")]
+ public SoundSpecifier Sound = new SoundPathSpecifier("/Audio/Nyanotrasen/Voice/Felinid/cat_scream1.ogg");
+}
diff --git a/Content.Server/Nyanotrasen/StationEvents/Events/FreeProberRule.cs b/Content.Server/Nyanotrasen/StationEvents/Events/FreeProberRule.cs
new file mode 100644
index 00000000000..0aa8ecc47cc
--- /dev/null
+++ b/Content.Server/Nyanotrasen/StationEvents/Events/FreeProberRule.cs
@@ -0,0 +1,81 @@
+using Robust.Shared.Map;
+using Robust.Shared.Random;
+using Content.Server.GameTicking.Rules.Components;
+using Content.Server.Power.Components;
+using Content.Server.Station.Systems;
+using Content.Server.StationEvents.Components;
+using Content.Server.Psionics.Glimmer;
+using Content.Shared.Construction.EntitySystems;
+using Content.Shared.Psionics.Glimmer;
+
+namespace Content.Server.StationEvents.Events;
+
+internal sealed class FreeProberRule : StationEventSystem
+{
+ [Dependency] private readonly IRobustRandom _robustRandom = default!;
+ [Dependency] private readonly IMapManager _mapManager = default!;
+ [Dependency] private readonly AnchorableSystem _anchorable = default!;
+ [Dependency] private readonly GlimmerSystem _glimmerSystem = default!;
+ [Dependency] private readonly StationSystem _stationSystem = default!;
+
+ private static readonly string ProberPrototype = "GlimmerProber";
+ private static readonly int SpawnDirections = 4;
+
+ protected override void Started(EntityUid uid, FreeProberRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
+ {
+ base.Started(uid, component, gameRule, args);
+
+ List PossibleSpawns = new();
+
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var glimmerSource, out var glimmerSourceComponent))
+ {
+ if (glimmerSourceComponent.AddToGlimmer && glimmerSourceComponent.Active)
+ {
+ PossibleSpawns.Add(glimmerSource);
+ }
+ }
+
+ if (PossibleSpawns.Count == 0 || _glimmerSystem.Glimmer >= 500 || _robustRandom.Prob(0.25f))
+ {
+ var queryBattery = EntityQueryEnumerator();
+ while (query.MoveNext(out var battery, out var _))
+ {
+ PossibleSpawns.Add(battery);
+ }
+ }
+
+ if (PossibleSpawns.Count > 0)
+ {
+ _robustRandom.Shuffle(PossibleSpawns);
+
+ foreach (var source in PossibleSpawns)
+ {
+ var xform = Transform(source);
+
+ if (_stationSystem.GetOwningStation(source, xform) == null)
+ continue;
+
+ var coordinates = xform.Coordinates;
+ var gridUid = xform.GridUid;
+ if (!_mapManager.TryGetGrid(gridUid, out var grid))
+ continue;
+
+ var tileIndices = grid.TileIndicesFor(coordinates);
+
+ for (var i = 0; i < SpawnDirections; i++)
+ {
+ var direction = (DirectionFlag) (1 << i);
+ var offsetIndices = tileIndices.Offset(direction.AsDir());
+
+ // This doesn't check against the prober's mask/layer, because it hasn't spawned yet...
+ if (!_anchorable.TileFree(grid, offsetIndices))
+ continue;
+
+ Spawn(ProberPrototype, grid.GridTileToLocal(offsetIndices));
+ return;
+ }
+ }
+ }
+ }
+}
diff --git a/Content.Server/Nyanotrasen/StationEvents/Events/GlimmerEventSystem.cs b/Content.Server/Nyanotrasen/StationEvents/Events/GlimmerEventSystem.cs
new file mode 100644
index 00000000000..a3d36ae7157
--- /dev/null
+++ b/Content.Server/Nyanotrasen/StationEvents/Events/GlimmerEventSystem.cs
@@ -0,0 +1,34 @@
+using Content.Server.GameTicking.Rules.Components;
+using Content.Server.Psionics.Glimmer;
+using Content.Shared.Psionics.Glimmer;
+
+namespace Content.Server.StationEvents.Events
+{
+ public sealed class GlimmerEventSystem : StationEventSystem
+ {
+ [Dependency] private readonly GlimmerSystem _glimmerSystem = default!;
+
+ protected override void Ended(EntityUid uid, GlimmerEventComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args)
+ {
+ base.Ended(uid, component, gameRule, args);
+
+ var glimmerBurned = RobustRandom.Next(component.GlimmerBurnLower, component.GlimmerBurnUpper);
+ _glimmerSystem.Glimmer -= glimmerBurned;
+
+ var reportEv = new GlimmerEventEndedEvent(component.SophicReport, glimmerBurned);
+ RaiseLocalEvent(reportEv);
+ }
+ }
+
+ public sealed class GlimmerEventEndedEvent : EntityEventArgs
+ {
+ public string Message = "";
+ public int GlimmerBurned = 0;
+
+ public GlimmerEventEndedEvent(string message, int glimmerBurned)
+ {
+ Message = message;
+ GlimmerBurned = glimmerBurned;
+ }
+ }
+}
diff --git a/Content.Server/Nyanotrasen/StationEvents/Events/GlimmerRandomSentienceRule.cs b/Content.Server/Nyanotrasen/StationEvents/Events/GlimmerRandomSentienceRule.cs
new file mode 100644
index 00000000000..36683e7ff5d
--- /dev/null
+++ b/Content.Server/Nyanotrasen/StationEvents/Events/GlimmerRandomSentienceRule.cs
@@ -0,0 +1,56 @@
+using System.Linq;
+using Content.Server.GameTicking.Rules.Components;
+using Content.Server.Ghost.Roles.Components;
+using Content.Server.Psionics;
+using Content.Server.Speech.Components;
+using Content.Server.StationEvents.Components;
+using Content.Shared.Mobs.Systems;
+
+namespace Content.Server.StationEvents.Events;
+
+///