diff --git a/Content.Client/UserInterface/Controls/RadialMenu.cs b/Content.Client/UserInterface/Controls/RadialMenu.cs
index 5f56ad7f866..bbe3c0e1e35 100644
--- a/Content.Client/UserInterface/Controls/RadialMenu.cs
+++ b/Content.Client/UserInterface/Controls/RadialMenu.cs
@@ -3,6 +3,7 @@
using Robust.Client.UserInterface.CustomControls;
using System.Linq;
using System.Numerics;
+using Robust.Client.Graphics;
namespace Content.Client.UserInterface.Controls;
@@ -16,7 +17,7 @@ public class RadialMenu : BaseWindow
///
/// Set a style class to be applied to the contextual button when it is set to move the user back through previous layers of the radial menu
- ///
+ ///
public string? BackButtonStyleClass
{
get
@@ -60,8 +61,8 @@ public string? CloseButtonStyleClass
/// A free floating menu which enables the quick display of one or more radial containers
///
///
- /// Only one radial container is visible at a time (each container forming a separate 'layer' within
- /// the menu), along with a contextual button at the menu center, which will either return the user
+ /// Only one radial container is visible at a time (each container forming a separate 'layer' within
+ /// the menu), along with a contextual button at the menu center, which will either return the user
/// to the previous layer or close the menu if there are no previous layers left to traverse.
/// To create a functional radial menu, simply parent one or more named radial containers to it,
/// and populate the radial containers with RadialMenuButtons. Setting the TargetLayer field of these
diff --git a/Content.Client/_Sunrise/BloodCult/CultPentagramSystem.cs b/Content.Client/_Sunrise/BloodCult/CultPentagramSystem.cs
new file mode 100644
index 00000000000..61442b9fb95
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/CultPentagramSystem.cs
@@ -0,0 +1,65 @@
+using System.Numerics;
+using Robust.Client.GameObjects;
+using Robust.Shared.Random;
+using Robust.Shared.Utility;
+
+namespace Content.Client._Sunrise.BloodCult;
+
+public sealed class CultPentagramSystem : EntitySystem
+{
+ [Dependency] private readonly IRobustRandom _robustRandom = default!;
+
+ private const string Rsi = "_Sunrise/BloodCult/pentagram.rsi";
+ private static readonly string[] States =
+ {
+ "halo1",
+ "halo2",
+ "halo3",
+ "halo4",
+ "halo5",
+ "halo6"
+ };
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(PentagramAdded);
+ SubscribeLocalEvent(PentagramRemoved);
+ }
+
+ private void PentagramAdded(EntityUid uid, PentagramComponent component, ComponentStartup args)
+ {
+ if (!TryComp(uid, out var sprite))
+ return;
+
+ if (sprite.LayerMapTryGet(PentagramKey.Key, out var _))
+ return;
+
+ var adj = sprite.Bounds.Height / 2 + ((1.0f/32) * 10.0f);
+
+ var randomIndex = _robustRandom.Next(0, States.Length);
+
+ var randomState = States[randomIndex];
+
+ var layer = sprite.AddLayer(new SpriteSpecifier.Rsi(new ResPath(Rsi), randomState));
+
+ sprite.LayerMapSet(PentagramKey.Key, layer);
+ sprite.LayerSetOffset(layer, new Vector2(0.0f, adj));
+ }
+
+ private void PentagramRemoved(EntityUid uid, PentagramComponent component, ComponentShutdown args)
+ {
+ if (!TryComp(uid, out var sprite))
+ return;
+
+ if (!sprite.LayerMapTryGet(PentagramKey.Key, out var layer))
+ return;
+
+ sprite.RemoveLayer(layer);
+ }
+
+ private enum PentagramKey
+ {
+ Key
+ }
+}
diff --git a/Content.Client/_Sunrise/BloodCult/Items/VeilShifter/VeilVisualizerSystem.cs b/Content.Client/_Sunrise/BloodCult/Items/VeilShifter/VeilVisualizerSystem.cs
new file mode 100644
index 00000000000..1d98e6e9de1
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/Items/VeilShifter/VeilVisualizerSystem.cs
@@ -0,0 +1,40 @@
+using Content.Shared._Sunrise.BloodCult.Items;
+using Robust.Client.GameObjects;
+
+namespace Content.Client._Sunrise.BloodCult.Items.VeilShifter;
+
+public sealed class VeilVisualizerSystem : VisualizerSystem
+{
+ private const string StateOn = "icon-on";
+ private const string StateOff = "icon";
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnInit);
+ }
+
+ private void OnInit(EntityUid uid, VoidTeleportComponent component, ComponentInit args)
+ {
+ if (!TryComp(uid, out var sprite)
+ || !AppearanceSystem.TryGetData(uid, VeilVisuals.Activated, out var activated))
+ return;
+
+ sprite.LayerSetState(VeilVisualsLayers.Activated, activated ? StateOn : StateOff);
+ }
+
+ protected override void OnAppearanceChange(EntityUid uid, VeilVisualsComponent component, ref AppearanceChangeEvent args)
+ {
+ if (args.Sprite == null
+ || !AppearanceSystem.TryGetData(uid, VeilVisuals.Activated, out var activated))
+ return;
+
+ args.Sprite.LayerSetState(VeilVisualsLayers.Activated, activated ? component.StateOn : component.StateOff);
+ }
+}
+
+public enum VeilVisualsLayers : byte
+{
+ Activated
+}
diff --git a/Content.Client/_Sunrise/BloodCult/Items/VeilShifter/VeilVisualsComponent.cs b/Content.Client/_Sunrise/BloodCult/Items/VeilShifter/VeilVisualsComponent.cs
new file mode 100644
index 00000000000..bad151df58a
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/Items/VeilShifter/VeilVisualsComponent.cs
@@ -0,0 +1,13 @@
+namespace Content.Client._Sunrise.BloodCult.Items.VeilShifter;
+
+[RegisterComponent]
+public sealed partial class VeilVisualsComponent : Component
+{
+ [DataField("stateOn")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public string? StateOn = "icon-on";
+
+ [DataField("stateOff")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public string? StateOff = "icon";
+}
diff --git a/Content.Client/_Sunrise/BloodCult/Items/VoidTorch/VoidTorchVisualizerSystem.cs b/Content.Client/_Sunrise/BloodCult/Items/VoidTorch/VoidTorchVisualizerSystem.cs
new file mode 100644
index 00000000000..0d61250eb48
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/Items/VoidTorch/VoidTorchVisualizerSystem.cs
@@ -0,0 +1,23 @@
+using Content.Shared._Sunrise.BloodCult.Items;
+using Robust.Client.GameObjects;
+
+namespace Content.Client._Sunrise.BloodCult.Items.VoidTorch;
+
+public sealed class VoidTorchVisualizerSystem : VisualizerSystem
+{
+ protected override void OnAppearanceChange(EntityUid uid, VoidTorchVisualsComponent component, ref AppearanceChangeEvent args)
+ {
+ base.OnAppearanceChange(uid, component, ref args);
+
+ if (args.Sprite == null
+ || !AppearanceSystem.TryGetData(uid, VoidTorchVisuals.Activated, out var activated))
+ return;
+
+ args.Sprite.LayerSetState(VoidTorchVisualsLayers.Activated, activated ? component.StateOn : component.StateOff);
+ }
+}
+
+public enum VoidTorchVisualsLayers : byte
+{
+ Activated
+}
diff --git a/Content.Client/_Sunrise/BloodCult/Items/VoidTorch/VoidTorchVisualsComponent.cs b/Content.Client/_Sunrise/BloodCult/Items/VoidTorch/VoidTorchVisualsComponent.cs
new file mode 100644
index 00000000000..add595d2b36
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/Items/VoidTorch/VoidTorchVisualsComponent.cs
@@ -0,0 +1,13 @@
+namespace Content.Client._Sunrise.BloodCult.Items.VoidTorch;
+
+[RegisterComponent]
+public sealed partial class VoidTorchVisualsComponent : Component
+{
+ [DataField("stateOn")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public string? StateOn = "icon-on";
+
+ [DataField("stateOff")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public string? StateOff = "icon";
+}
diff --git a/Content.Client/_Sunrise/BloodCult/Narsie/NarsieLayer.cs b/Content.Client/_Sunrise/BloodCult/Narsie/NarsieLayer.cs
new file mode 100644
index 00000000000..86f10f081a7
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/Narsie/NarsieLayer.cs
@@ -0,0 +1,6 @@
+namespace Content.Client._Sunrise.BloodCult.Narsie;
+
+public enum NarsieLayer
+{
+ Default
+}
diff --git a/Content.Client/_Sunrise/BloodCult/Narsie/NarsieVisualizer.cs b/Content.Client/_Sunrise/BloodCult/Narsie/NarsieVisualizer.cs
new file mode 100644
index 00000000000..a5c51bb0930
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/Narsie/NarsieVisualizer.cs
@@ -0,0 +1,69 @@
+using Content.Shared._Sunrise.BloodCult;
+using Robust.Client.Animations;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+
+namespace Content.Client._Sunrise.BloodCult.Narsie;
+
+public sealed class NarsieVisualizer : VisualizerSystem
+{
+ [Dependency] private readonly AnimationPlayerSystem _animationSystem = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnAnimationCompleted);
+ }
+
+ private void OnAnimationCompleted(EntityUid uid, NarsieComponent component, AnimationCompletedEvent args)
+ {
+ SetDefaultState(Comp(uid));
+ }
+
+ protected override void OnAppearanceChange(EntityUid uid, NarsieComponent component, ref AppearanceChangeEvent args)
+ {
+ base.OnAppearanceChange(uid, component, ref args);
+
+ if(args.Sprite == null) return;
+
+ if (!args.AppearanceData.TryGetValue(NarsieVisualState.VisualState, out var narsieVisualsObject) || narsieVisualsObject is not NarsieVisuals narsieVisual)
+ return;
+
+ switch (narsieVisual)
+ {
+ case NarsieVisuals.Spawning:
+ PlaySpawnAnimation(uid);
+ break;
+ case NarsieVisuals.Spawned:
+ if(_animationSystem.HasRunningAnimation(uid, "narsie_spawn")) break;
+ SetDefaultState(args.Sprite);
+ break;
+ }
+
+ }
+
+ private void PlaySpawnAnimation(EntityUid uid)
+ {
+ _animationSystem.Play(uid, NarsieSpawnAnimation, "narsie_spawn");
+ }
+
+ private void SetDefaultState(SpriteComponent component)
+ {
+ component.LayerSetVisible(NarsieLayer.Default, true);
+ component.LayerSetState(NarsieLayer.Default, new RSI.StateId("narsie"));
+ component.LayerSetAutoAnimated(NarsieLayer.Default, true);
+ }
+
+ private static readonly Animation NarsieSpawnAnimation = new()
+ {
+ Length = TimeSpan.FromSeconds(3.5),
+ AnimationTracks =
+ {
+ new AnimationTrackSpriteFlick()
+ {
+ LayerKey = NarsieLayer.Default,
+ KeyFrames = {new AnimationTrackSpriteFlick.KeyFrame(new RSI.StateId("narsie_spawn_anim"), 0f)}
+ }
+ }
+ };
+}
diff --git a/Content.Client/_Sunrise/BloodCult/PentagramComponent.cs b/Content.Client/_Sunrise/BloodCult/PentagramComponent.cs
new file mode 100644
index 00000000000..37579a0578a
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/PentagramComponent.cs
@@ -0,0 +1,9 @@
+using Content.Shared._Sunrise.BloodCult.Pentagram;
+using Robust.Shared.GameStates;
+
+namespace Content.Client._Sunrise.BloodCult;
+
+[NetworkedComponent, RegisterComponent]
+public sealed partial class PentagramComponent : SharedPentagramComponent
+{
+}
diff --git a/Content.Client/_Sunrise/BloodCult/Pylon/PylonVisualizerSystem.cs b/Content.Client/_Sunrise/BloodCult/Pylon/PylonVisualizerSystem.cs
new file mode 100644
index 00000000000..d787bccd5a2
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/Pylon/PylonVisualizerSystem.cs
@@ -0,0 +1,41 @@
+using Content.Shared._Sunrise.BloodCult.Pylon;
+using Robust.Client.GameObjects;
+using SharedPylonComponent = Content.Shared._Sunrise.BloodCult.Pylon.SharedPylonComponent;
+
+namespace Content.Client._Sunrise.BloodCult.Pylon;
+
+public sealed class PylonVisualizerSystem : VisualizerSystem
+{
+ private const string StateOn = "pylon";
+ private const string StateOff = "pylon_off";
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnInit);
+ }
+
+ private void OnInit(EntityUid uid, SharedPylonComponent component, ComponentInit args)
+ {
+ if (!TryComp(uid, out var sprite)
+ || !AppearanceSystem.TryGetData(uid, PylonVisualsLayers.Activated, out var activated))
+ return;
+
+ sprite.LayerSetState(PylonVisualsLayers.Activated, activated ? StateOn : StateOff);
+ }
+
+ protected override void OnAppearanceChange(EntityUid uid, PylonVisualsComponent component, ref AppearanceChangeEvent args)
+ {
+ if (args.Sprite == null
+ || !AppearanceSystem.TryGetData(uid, PylonVisuals.Activated, out var activated))
+ return;
+
+ args.Sprite.LayerSetState(PylonVisualsLayers.Activated, activated ? component.StateOn : component.StateOff);
+ }
+}
+
+public enum PylonVisualsLayers : byte
+{
+ Activated
+}
diff --git a/Content.Client/_Sunrise/BloodCult/Pylon/PylonVisualsComponent.cs b/Content.Client/_Sunrise/BloodCult/Pylon/PylonVisualsComponent.cs
new file mode 100644
index 00000000000..f5c63e906c9
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/Pylon/PylonVisualsComponent.cs
@@ -0,0 +1,13 @@
+namespace Content.Client._Sunrise.BloodCult.Pylon;
+
+[RegisterComponent]
+public sealed partial class PylonVisualsComponent : Component
+{
+ [DataField("stateOn")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public string? StateOn = "pylon";
+
+ [DataField("stateOff")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public string? StateOff = "pylon_off";
+}
diff --git a/Content.Client/_Sunrise/BloodCult/ShowCultHudSystem.cs b/Content.Client/_Sunrise/BloodCult/ShowCultHudSystem.cs
new file mode 100644
index 00000000000..47745109ca8
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/ShowCultHudSystem.cs
@@ -0,0 +1,29 @@
+using Content.Shared._Sunrise.BloodCult.Components;
+using Content.Shared.StatusIcon.Components;
+using Robust.Client.Player;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client._Sunrise.BloodCult;
+public sealed class ShowCultHudSystem : EntitySystem
+{
+ [Dependency] private readonly IPlayerManager _player = default!;
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnGetStatusIconsEvent);
+ }
+
+ private void OnGetStatusIconsEvent(EntityUid uid, BloodCultistComponent bloodCultistComponent, ref GetStatusIconsEvent args)
+ {
+ var ent = _player.LocalSession?.AttachedEntity;
+ if (!HasComp(ent))
+ return;
+
+ if (_prototype.TryIndex(bloodCultistComponent.StatusIcon, out var iconPrototype))
+ args.StatusIcons.Add(iconPrototype);
+ }
+}
+
diff --git a/Content.Client/_Sunrise/BloodCult/Structures/CultCraftStructureVisualizerSystem.cs b/Content.Client/_Sunrise/BloodCult/Structures/CultCraftStructureVisualizerSystem.cs
new file mode 100644
index 00000000000..f22cccc4fd2
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/Structures/CultCraftStructureVisualizerSystem.cs
@@ -0,0 +1,23 @@
+using Content.Shared._Sunrise.BloodCult;
+using Robust.Client.GameObjects;
+
+namespace Content.Client._Sunrise.BloodCult.Structures;
+
+public sealed class CultCraftStructureVisualizerSystem : VisualizerSystem
+{
+ protected override void OnAppearanceChange(EntityUid uid, CultCraftStructureVisualsComponent component, ref AppearanceChangeEvent args)
+ {
+ base.OnAppearanceChange(uid, component, ref args);
+
+ if (args.Sprite == null
+ || !AppearanceSystem.TryGetData(uid, CultCraftStructureVisuals.Activated, out var activated))
+ return;
+
+ args.Sprite.LayerSetState(CultCraftStructureVisualsLayers.Activated, activated ? component.StateOn : component.StateOff);
+ }
+}
+
+public enum CultCraftStructureVisualsLayers : byte
+{
+ Activated
+}
diff --git a/Content.Client/_Sunrise/BloodCult/Structures/CultCraftStructureVisualsComponent.cs b/Content.Client/_Sunrise/BloodCult/Structures/CultCraftStructureVisualsComponent.cs
new file mode 100644
index 00000000000..26368afbd75
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/Structures/CultCraftStructureVisualsComponent.cs
@@ -0,0 +1,13 @@
+namespace Content.Client._Sunrise.BloodCult.Structures;
+
+[RegisterComponent]
+public sealed partial class CultCraftStructureVisualsComponent : Component
+{
+ [DataField("stateOn")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public string? StateOn = "icon";
+
+ [DataField("stateOff")]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public string? StateOff = "icon-off";
+}
diff --git a/Content.Client/_Sunrise/BloodCult/UI/Altar/AltarBUI.cs b/Content.Client/_Sunrise/BloodCult/UI/Altar/AltarBUI.cs
new file mode 100644
index 00000000000..5c701673cc4
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/UI/Altar/AltarBUI.cs
@@ -0,0 +1,53 @@
+using Content.Shared._Sunrise.BloodCult.UI;
+using JetBrains.Annotations;
+
+namespace Content.Client._Sunrise.BloodCult.UI.Altar;
+
+[UsedImplicitly]
+public sealed class AltarBUI : BoundUserInterface
+{
+ private AltarWindow? _window;
+
+ public AltarBUI(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ IoCManager.InjectDependencies(this);
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _window = new AltarWindow();
+
+ _window.OnClose += Close;
+ _window.OnItemSelected += OnItemSelected;
+ }
+
+ private void OnItemSelected(string item)
+ {
+ var evt = new AltarBuyRequest(item);
+ SendMessage(evt);
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+
+ if (!disposing) return;
+ _window?.Dispose();
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ if (state is AltarListingBUIState listingState)
+ {
+ _window?.SetListing(listingState.Items);
+ }
+ else if(state is AltarTimerBUIState timerState)
+ {
+ _window?.SetTimer(timerState.NextTimeUse);
+ }
+ }
+}
diff --git a/Content.Client/_Sunrise/BloodCult/UI/Altar/AltarListingControl.xaml b/Content.Client/_Sunrise/BloodCult/UI/Altar/AltarListingControl.xaml
new file mode 100644
index 00000000000..04353875206
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/UI/Altar/AltarListingControl.xaml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_Sunrise/BloodCult/UI/Altar/AltarListingControl.xaml.cs b/Content.Client/_Sunrise/BloodCult/UI/Altar/AltarListingControl.xaml.cs
new file mode 100644
index 00000000000..56a9be69c1b
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/UI/Altar/AltarListingControl.xaml.cs
@@ -0,0 +1,20 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client._Sunrise.BloodCult.UI.Altar;
+
+[GenerateTypedNameReferences]
+public partial class AltarListingControl : Control
+{
+ public AltarListingControl(EntityPrototype prototype, Robust.Client.Graphics.Texture icon, Action? clickAction)
+ {
+ RobustXamlLoader.Load(this);
+
+ ToolTip = $"{prototype.Name}\n{prototype.Description}";
+
+ BuyListingButton.TextureNormal = icon;
+ BuyListingButton.OnButtonDown += _ => clickAction?.Invoke(prototype.ID);
+ }
+}
diff --git a/Content.Client/_Sunrise/BloodCult/UI/Altar/AltarWindow.xaml b/Content.Client/_Sunrise/BloodCult/UI/Altar/AltarWindow.xaml
new file mode 100644
index 00000000000..715cfdfd930
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/UI/Altar/AltarWindow.xaml
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/Content.Client/_Sunrise/BloodCult/UI/Altar/AltarWindow.xaml.cs b/Content.Client/_Sunrise/BloodCult/UI/Altar/AltarWindow.xaml.cs
new file mode 100644
index 00000000000..4b20e55faa2
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/UI/Altar/AltarWindow.xaml.cs
@@ -0,0 +1,89 @@
+using Content.Client.TextScreen;
+using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
+
+namespace Content.Client._Sunrise.BloodCult.UI.Altar;
+
+[GenerateTypedNameReferences]
+public partial class AltarWindow : DefaultWindow
+{
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+ [Dependency] private readonly SpriteSystem _spriteSystem = default!;
+ [Dependency] private readonly PrototypeManager _prototypeManager = default!;
+
+
+ public event Action? OnItemSelected;
+ private TimeSpan? _nextTimeUse = null!;
+
+ private List _listingControls = new();
+
+ public AltarWindow()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+ }
+
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ base.FrameUpdate(args);
+
+ if (_nextTimeUse == null) return;
+
+ var remainingTime = _nextTimeUse.Value - _gameTiming.CurTime;
+
+ if (remainingTime.TotalSeconds < 0)
+ {
+ remainingTime = TimeSpan.Zero;
+ }
+
+ var remainingTimeText = TextScreenSystem.TimeToString(remainingTime);
+
+ TimerLabel.SetMessage(remainingTimeText);
+ }
+
+ public void SetListing(List prototypes)
+ {
+ foreach (var prototypeId in prototypes)
+ {
+ var prototype = _prototypeManager.Index(prototypeId);
+ if(prototype == null) return;
+ var prototypeIcon = _spriteSystem.GetPrototypeIcon(prototype).Default;
+ AddListingControl(prototype);
+ }
+ }
+
+ public void AddListingControl(EntityPrototype entityPrototype)
+ {
+ var icon = _spriteSystem.GetPrototypeIcon(entityPrototype).Default;
+ var control = new AltarListingControl(entityPrototype, icon, OnItemSelected);
+
+ ListingContainer.AddChild(control);
+ _listingControls.Add(control);
+ }
+
+ public void SetTimer(TimeSpan? timer)
+ {
+ _nextTimeUse = timer;
+
+ if (timer == null)
+ {
+ TimerLabel.SetMessage("Алтарь готов к использованию");
+ SetListingButtonsState(true);
+ return;
+ }
+
+ SetListingButtonsState(false);
+ }
+
+ private void SetListingButtonsState(bool enabled)
+ {
+ foreach (var listingControl in _listingControls)
+ {
+ listingControl.BuyListingButton.Disabled = enabled;
+ }
+ }
+}
diff --git a/Content.Client/_Sunrise/BloodCult/UI/BloodCultMenu.xaml b/Content.Client/_Sunrise/BloodCult/UI/BloodCultMenu.xaml
new file mode 100644
index 00000000000..d56fc832898
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/UI/BloodCultMenu.xaml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
diff --git a/Content.Client/_Sunrise/BloodCult/UI/BloodCultMenu.xaml.cs b/Content.Client/_Sunrise/BloodCult/UI/BloodCultMenu.xaml.cs
new file mode 100644
index 00000000000..8440f7fdcfc
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/UI/BloodCultMenu.xaml.cs
@@ -0,0 +1,70 @@
+using System.Numerics;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Chat.Prototypes;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client._Sunrise.BloodCult.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class BloodCultMenu : RadialMenu
+{
+ [Dependency] private readonly EntityManager _entManager = default!;
+
+ private EntityUid _owner;
+
+ public BloodCultMenu()
+ {
+ IoCManager.InjectDependencies(this);
+ RobustXamlLoader.Load(this);
+ }
+
+ public void SetEntity(EntityUid uid)
+ {
+ _owner = uid;
+
+ if (!_entManager.EntityExists(_owner))
+ {
+ Close();
+ return;
+ }
+ }
+
+ public RadialMenuTextureButton AddButton(string tooltip, Robust.Client.Graphics.Texture texture)
+ {
+ var button = new RadialMenuTextureButton()
+ {
+ StyleClasses = { "RadialMenuButton" },
+ SetSize = new Vector2(64f, 64f),
+ ToolTip = tooltip,
+ };
+ var scale = Vector2.One;
+
+ if (texture.Width <= 32)
+ {
+ scale *= 2;
+ }
+
+ var tex = new TextureRect
+ {
+ VerticalAlignment = VAlignment.Center,
+ HorizontalAlignment = HAlignment.Center,
+ Texture = texture,
+ TextureScale = scale,
+ };
+
+ button.AddChild(tex);
+ var main = FindControl("Main");
+ main.AddChild(button);
+
+ return button;
+ }
+}
+
+
+public sealed class EmoteMenuButton : RadialMenuTextureButton
+{
+ public ProtoId ProtoId { get; set; }
+}
diff --git a/Content.Client/_Sunrise/BloodCult/UI/BloodSpellSelector/BloodSpellSelectorBUI.cs b/Content.Client/_Sunrise/BloodCult/UI/BloodSpellSelector/BloodSpellSelectorBUI.cs
new file mode 100644
index 00000000000..8d39b2478bd
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/UI/BloodSpellSelector/BloodSpellSelectorBUI.cs
@@ -0,0 +1,63 @@
+using Content.Shared._Sunrise.BloodCult.Items;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.Input;
+using Robust.Client.UserInterface;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client._Sunrise.BloodCult.UI.BloodSpellSelector;
+
+public sealed class BloodSpellSelectorBUI : BoundUserInterface
+{
+ [Dependency] private readonly IClyde _displayManager = default!;
+ [Dependency] private readonly IInputManager _inputManager = default!;
+ private BloodCultMenu? _menu;
+
+ public BloodSpellSelectorBUI(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ IoCManager.InjectDependencies(this);
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+ _menu = this.CreateWindow();
+
+ var protoMan = IoCManager.Resolve();
+ var entityMan = IoCManager.Resolve();
+ var sprite = entityMan.System();
+
+ if (protoMan.TryIndex("CultBloodOrb", out EntityPrototype? bloodOrb))
+ {
+ var texture = sprite.GetPrototypeIcon(bloodOrb);
+ var button = _menu.AddButton($"{bloodOrb.Name} (50)", texture.Default);
+
+ button.OnPressed += _ =>
+ {
+ SendMessage(new CultBloodSpellCreateOrbBuiMessage());
+ Close();
+ };
+ }
+
+ if (protoMan.TryIndex("BloodSpear", out EntityPrototype? bloodSpear))
+ {
+ var texture = sprite.GetPrototypeIcon(bloodSpear);
+ var button = _menu.AddButton($"{bloodSpear.Name} (150)", texture.Default);
+
+ button.OnPressed += _ =>
+ {
+ SendMessage(new CultBloodSpellCreateBloodSpearBuiMessage());
+ Close();
+ };
+ }
+
+ var vpSize = _displayManager.ScreenSize;
+ _menu.OpenCenteredAt(_inputManager.MouseScreenPosition.Position / vpSize);
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ _menu?.Close();
+ }
+}
diff --git a/Content.Client/_Sunrise/BloodCult/UI/ConstructSelector/ConstructSelectorBui.cs b/Content.Client/_Sunrise/BloodCult/UI/ConstructSelector/ConstructSelectorBui.cs
new file mode 100644
index 00000000000..1fd6bb9d26e
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/UI/ConstructSelector/ConstructSelectorBui.cs
@@ -0,0 +1,58 @@
+using Content.Shared._Sunrise.BloodCult.Components;
+using Content.Shared._Sunrise.BloodCult.UI;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.Input;
+using Robust.Client.UserInterface;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Client._Sunrise.BloodCult.UI.ConstructSelector;
+
+public sealed class ConstructSelectorBui : BoundUserInterface
+{
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly IEntityManager _entityManager = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly IClyde _displayManager = default!;
+ [Dependency] private readonly IInputManager _inputManager = default!;
+ private SpriteSystem _spriteSystem = default!;
+
+ private bool _selected;
+ private BloodCultMenu? _menu;
+
+ public ConstructSelectorBui(EntityUid owner, Enum uiKey) : base(owner, uiKey) { }
+
+ protected override void Open()
+ {
+ base.Open();
+ _menu = this.CreateWindow();
+
+ _spriteSystem = _entityManager.EntitySysManager.GetEntitySystem();
+ var shellComponent = _entityManager.GetComponent(Owner);
+
+ _menu.OnClose += () =>
+ {
+ if(_selected)
+ return;
+
+ SendMessage(new ConstructFormSelectedEvent(_random.Pick(shellComponent.ConstructForms)));
+ };
+
+ foreach (var form in shellComponent.ConstructForms)
+ {
+ var formPrototype = _prototypeManager.Index(form);
+ var button = _menu.AddButton(formPrototype.Name, _spriteSystem.GetPrototypeIcon(formPrototype).Default);
+
+ button.OnPressed += _ =>
+ {
+ _selected = true;
+ SendMessage(new ConstructFormSelectedEvent(form));
+ _menu.Close();
+ };
+ }
+
+ var vpSize = _displayManager.ScreenSize;
+ _menu.OpenCenteredAt(_inputManager.MouseScreenPosition.Position / vpSize);
+ }
+}
diff --git a/Content.Client/_Sunrise/BloodCult/UI/CountSelector/CountSelectorBUI.cs b/Content.Client/_Sunrise/BloodCult/UI/CountSelector/CountSelectorBUI.cs
new file mode 100644
index 00000000000..2e11085468e
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/UI/CountSelector/CountSelectorBUI.cs
@@ -0,0 +1,47 @@
+using Content.Shared._Sunrise.BloodCult.Items;
+namespace Content.Client._Sunrise.BloodCult.UI.CountSelector;
+
+public sealed class CountSelectorBUI : BoundUserInterface
+{
+ public CountSelectorBUI(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ }
+
+ private CountSelectorWindow? _window;
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _window = new();
+ _window.OpenCentered();
+ _window.OnCountChange += OnNameSelected;
+ _window.OnClose += Close;
+ }
+
+ private void OnNameSelected(string name)
+ {
+ if (int.TryParse(name, out var count) && count >= 50)
+ {
+ SendMessage(new CountSelectorMessage(count));
+ Close();
+ }
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ if (state is not CountSelectorBuiState cast || _window == null)
+ {
+ return;
+ }
+
+ _window.UpdateState(cast.Count);
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+
+ _window?.Close();
+ }
+}
diff --git a/Content.Client/_Sunrise/BloodCult/UI/CountSelector/CountSelectorWindow.xaml b/Content.Client/_Sunrise/BloodCult/UI/CountSelector/CountSelectorWindow.xaml
new file mode 100644
index 00000000000..5e5cc0e397f
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/UI/CountSelector/CountSelectorWindow.xaml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_Sunrise/BloodCult/UI/CountSelector/CountSelectorWindow.xaml.cs b/Content.Client/_Sunrise/BloodCult/UI/CountSelector/CountSelectorWindow.xaml.cs
new file mode 100644
index 00000000000..3036b771476
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/UI/CountSelector/CountSelectorWindow.xaml.cs
@@ -0,0 +1,26 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client._Sunrise.BloodCult.UI.CountSelector;
+
+[GenerateTypedNameReferences]
+public sealed partial class CountSelectorWindow: DefaultWindow
+{
+ public Action? OnCountChange;
+
+ public CountSelectorWindow()
+ {
+ RobustXamlLoader.Load(this);
+
+ CountSelectorSet.OnPressed += _ =>
+ {
+ OnCountChange!(CountSelector.Text);
+ };
+ }
+
+ public void UpdateState(int count)
+ {
+ CountSelector.Text = $"{count}";
+ }
+}
diff --git a/Content.Client/_Sunrise/BloodCult/UI/CultistFactory/CultistFactoryBUI.cs b/Content.Client/_Sunrise/BloodCult/UI/CultistFactory/CultistFactoryBUI.cs
new file mode 100644
index 00000000000..73576b0a9d6
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/UI/CultistFactory/CultistFactoryBUI.cs
@@ -0,0 +1,97 @@
+using Content.Client.UserInterface.Controls;
+using Content.Shared._Sunrise.BloodCult;
+using Content.Shared._Sunrise.BloodCult.UI;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.Input;
+using Robust.Client.UserInterface;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client._Sunrise.BloodCult.UI.CultistFactory;
+
+public sealed class CultistFactoryBUI : BoundUserInterface
+{
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly IEntityManager _entityManager = default!;
+ [Dependency] private readonly IClyde _displayManager = default!;
+ [Dependency] private readonly IInputManager _inputManager = default!;
+ private BloodCultMenu? _menu;
+
+ private bool _updated = false;
+
+ public CultistFactoryBUI(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ IoCManager.InjectDependencies(this);
+ }
+ private void ResetUI()
+ {
+ _menu?.Close();
+ _menu = null;
+ _updated = false;
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+ _menu = this.CreateWindow();
+
+ if (State != null)
+ UpdateState(State);
+ }
+
+ private void PopulateRadial(IReadOnlyCollection ids)
+ {
+ var spriteSys = _entityManager.EntitySysManager.GetEntitySystem();
+
+ foreach (var id in ids)
+ {
+ if (!_prototypeManager.TryIndex(id, out var prototype))
+ return;
+
+ if (_menu == null)
+ continue;
+
+ if (prototype.Icon == null)
+ continue;
+
+ var button = _menu.AddButton(prototype.Name, spriteSys.Frame0(prototype.Icon));
+ button.OnPressed += _ =>
+ {
+ Select(id);
+ };
+ }
+ }
+
+ private void Select(string id)
+ {
+ SendMessage(new CultistFactoryItemSelectedMessage(id));
+ ResetUI();
+ Close();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ ResetUI();
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ if (_updated)
+ return;
+
+ if (state is CultistFactoryBUIState newState)
+ {
+ PopulateRadial(newState.Ids);
+ }
+
+ if (_menu == null)
+ return;
+
+ var vpSize = _displayManager.ScreenSize;
+ _menu.OpenCenteredAt(_inputManager.MouseScreenPosition.Position / vpSize);
+ _updated = true;
+ }
+}
diff --git a/Content.Client/_Sunrise/BloodCult/UI/ListViewSelector/ListViewSelectorBUI.cs b/Content.Client/_Sunrise/BloodCult/UI/ListViewSelector/ListViewSelectorBUI.cs
new file mode 100644
index 00000000000..8e3e463ec1f
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/UI/ListViewSelector/ListViewSelectorBUI.cs
@@ -0,0 +1,54 @@
+using Content.Shared._Sunrise.BloodCult.UI;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client._Sunrise.BloodCult.UI.ListViewSelector;
+
+
+public sealed class ListViewSelectorBUI : BoundUserInterface
+{
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly IEntityManager _entityManager = default!;
+
+ private ListViewSelectorWindow? _window;
+
+ public ListViewSelectorBUI(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ IoCManager.InjectDependencies(this);
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _window = new ListViewSelectorWindow(_prototypeManager, _entityManager);
+ _window.OpenCentered();
+ _window.OnClose += Close;
+
+ _window.ItemSelected += (item, index) =>
+ {
+ var msg = new ListViewItemSelectedMessage(item, index);
+ SendMessage(msg);
+ };
+
+ if(State != null)
+ UpdateState(State);
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ if (state is ListViewBUIState newState)
+ {
+ _window?.PopulateList(newState.Items);
+ }
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ if (!disposing)
+ return;
+ _window?.Close();
+ }
+}
diff --git a/Content.Client/_Sunrise/BloodCult/UI/ListViewSelector/ListViewSelectorWindow.xaml b/Content.Client/_Sunrise/BloodCult/UI/ListViewSelector/ListViewSelectorWindow.xaml
new file mode 100644
index 00000000000..ec6d017795f
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/UI/ListViewSelector/ListViewSelectorWindow.xaml
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/Content.Client/_Sunrise/BloodCult/UI/ListViewSelector/ListViewSelectorWindow.xaml.cs b/Content.Client/_Sunrise/BloodCult/UI/ListViewSelector/ListViewSelectorWindow.xaml.cs
new file mode 100644
index 00000000000..a65c4081944
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/UI/ListViewSelector/ListViewSelectorWindow.xaml.cs
@@ -0,0 +1,63 @@
+using System.Numerics;
+using Content.Client.Stylesheets;
+using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client._Sunrise.BloodCult.UI.ListViewSelector;
+
+[GenerateTypedNameReferences]
+public partial class ListViewSelectorWindow : DefaultWindow
+{
+ public Action? ItemSelected;
+
+ private readonly IPrototypeManager _prototypeManager;
+ private readonly SpriteSystem _sprite;
+ public ListViewSelectorWindow(IPrototypeManager prototypeManager, IEntityManager entityManager)
+ {
+ RobustXamlLoader.Load(this);
+ _prototypeManager = prototypeManager;
+ _sprite = entityManager.System();
+ }
+
+ public void PopulateList(List items)
+ {
+ ClearGrid();
+
+ foreach (var item in items)
+ {
+ if (!_prototypeManager.TryIndex(item, out EntityPrototype? runeProto))
+ continue;
+
+ var button = new Button
+ {
+ MinSize = new Vector2(100, 100),
+ MaxSize = new Vector2(100, 100),
+ HorizontalExpand = true,
+ StyleClasses = {StyleBase.ButtonSquare},
+ ToggleMode = false,
+ ToolTip = Loc.GetString($"ent-{item}"),
+ TooltipDelay = 0.01f,
+ };
+
+ button.OnPressed += _ => ItemSelected?.Invoke(item, items.IndexOf(item));
+ ItemsGrid.AddChild(button);
+
+ var texture = _sprite.GetPrototypeIcon(runeProto);
+ button.AddChild(new TextureRect
+ {
+ Stretch = TextureRect.StretchMode.KeepAspectCentered,
+ Texture = texture.Default,
+ Modulate = Color.FromHex("#F80000")
+ });
+ }
+ }
+
+ private void ClearGrid()
+ {
+ ItemsGrid.RemoveAllChildren();
+ }
+}
diff --git a/Content.Client/_Sunrise/BloodCult/UI/NameSelector/NameSelectorBUI.cs b/Content.Client/_Sunrise/BloodCult/UI/NameSelector/NameSelectorBUI.cs
new file mode 100644
index 00000000000..0b463246e6f
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/UI/NameSelector/NameSelectorBUI.cs
@@ -0,0 +1,44 @@
+using Content.Shared._Sunrise.BloodCult.UI;
+
+namespace Content.Client._Sunrise.BloodCult.UI.NameSelector;
+
+public sealed class NameSelectorBUI : BoundUserInterface
+{
+ public NameSelectorBUI(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ }
+
+ private NameSelectorWindow? _window;
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _window = new();
+ _window.OpenCentered();
+ _window.OnNameChange += OnNameSelected;
+ _window.OnClose += Close;
+ }
+
+ private void OnNameSelected(string name)
+ {
+ SendMessage(new NameSelectorMessage(name));
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ if (state is not NameSelectorBuiState cast || _window == null)
+ {
+ return;
+ }
+
+ _window.UpdateState(cast.Name);
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+
+ _window?.Close();
+ }
+}
diff --git a/Content.Client/_Sunrise/BloodCult/UI/NameSelector/NameSelectorWindow.xaml b/Content.Client/_Sunrise/BloodCult/UI/NameSelector/NameSelectorWindow.xaml
new file mode 100644
index 00000000000..34003c46aad
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/UI/NameSelector/NameSelectorWindow.xaml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
diff --git a/Content.Client/_Sunrise/BloodCult/UI/NameSelector/NameSelectorWindow.xaml.cs b/Content.Client/_Sunrise/BloodCult/UI/NameSelector/NameSelectorWindow.xaml.cs
new file mode 100644
index 00000000000..913d689c2da
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/UI/NameSelector/NameSelectorWindow.xaml.cs
@@ -0,0 +1,26 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client._Sunrise.BloodCult.UI.NameSelector;
+
+[GenerateTypedNameReferences]
+public sealed partial class NameSelectorWindow: DefaultWindow
+{
+ public Action? OnNameChange;
+
+ public NameSelectorWindow()
+ {
+ RobustXamlLoader.Load(this);
+
+ NameSelectorSet.OnPressed += _ =>
+ {
+ OnNameChange!(NameSelector.Text);
+ };
+ }
+
+ public void UpdateState(string name)
+ {
+ NameSelector.Text = name;
+ }
+}
diff --git a/Content.Client/_Sunrise/BloodCult/UI/SpellSelector/SpellSelectorBUI.cs b/Content.Client/_Sunrise/BloodCult/UI/SpellSelector/SpellSelectorBUI.cs
new file mode 100644
index 00000000000..6544108ac25
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/UI/SpellSelector/SpellSelectorBUI.cs
@@ -0,0 +1,71 @@
+using Content.Shared._Sunrise.BloodCult.Components;
+using Content.Shared._Sunrise.BloodCult.Items;
+using Content.Shared.Actions;
+using Robust.Client.Graphics;
+using Robust.Client.Input;
+using Robust.Client.UserInterface;
+using Robust.Client.Utility;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Client._Sunrise.BloodCult.UI.SpellSelector;
+
+public sealed class SpellSelectorBUI : BoundUserInterface
+{
+ [Dependency] private readonly IClyde _displayManager = default!;
+ [Dependency] private readonly IInputManager _inputManager = default!;
+
+ private BloodCultMenu? _menu;
+
+ public SpellSelectorBUI(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ IoCManager.InjectDependencies(this);
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+ _menu = this.CreateWindow();
+ _menu.SetEntity(Owner);
+ _menu.OnClose += Close;
+
+ var protoMan = IoCManager.Resolve();
+
+ foreach (var action in BloodCultistComponent.CultistActions)
+ {
+ if (!protoMan.TryIndex(action, out var proto))
+ continue;
+
+ SpriteSpecifier? icon;
+ if (action.StartsWith("InstantAction") && proto.TryGetComponent(out InstantActionComponent? instantComp))
+ icon = instantComp.Icon;
+ else
+ {
+ if (!proto.TryGetComponent(out EntityTargetActionComponent? targetComp))
+ continue;
+ icon = targetComp.Icon;
+ }
+
+ if (icon == null)
+ continue;
+
+ var texture = icon.Frame0();
+ var button = _menu.AddButton(proto.Name, texture);
+
+ button.OnPressed += _ =>
+ {
+ SendMessage(new CultSpellProviderSelectedBuiMessage(action));
+ Close();
+ };
+ }
+
+ var vpSize = _displayManager.ScreenSize;
+ _menu.OpenCenteredAt(_inputManager.MouseScreenPosition.Position / vpSize);
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ _menu?.Close();
+ }
+}
diff --git a/Content.Client/_Sunrise/BloodCult/UI/StructureRadial/StructureCraftBoundUserInterface.cs b/Content.Client/_Sunrise/BloodCult/UI/StructureRadial/StructureCraftBoundUserInterface.cs
new file mode 100644
index 00000000000..20586df8b5e
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/UI/StructureRadial/StructureCraftBoundUserInterface.cs
@@ -0,0 +1,137 @@
+using Content.Client.Construction;
+using Content.Client.Resources;
+using Content.Client.UserInterface.Controls;
+using Content.Shared._Sunrise.BloodCult.Structures;
+using Content.Shared.Construction.Prototypes;
+using Robust.Client.Graphics;
+using Robust.Client.Input;
+using Robust.Client.Placement;
+using Robust.Client.Player;
+using Robust.Client.ResourceManagement;
+using Robust.Client.UserInterface;
+using Robust.Shared.Enums;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client._Sunrise.BloodCult.UI.StructureRadial;
+
+public sealed class StructureCraftBoundUserInterface : BoundUserInterface
+{
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly IPlacementManager _placement = default!;
+ [Dependency] private readonly IEntitySystemManager _systemManager = default!;
+ [Dependency] private readonly IPlayerManager _player = default!;
+ [Dependency] private readonly IEntityManager _entMan = default!;
+ [Dependency] private readonly IClyde _displayManager = default!;
+ [Dependency] private readonly IInputManager _inputManager = default!;
+
+ private BloodCultMenu? _menu;
+
+ public StructureCraftBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ IoCManager.InjectDependencies(this);
+ }
+
+ private void CreateUI()
+ {
+ if (_menu != null)
+ ResetUI();
+
+ _menu = this.CreateWindow();
+
+ foreach (var prototype in _prototypeManager.EnumeratePrototypes())
+ {
+ var texture = IoCManager.Resolve().GetTexture(prototype.Icon);
+ var radialButton = _menu.AddButton(prototype.StructureName, texture);
+ radialButton.OnPressed += _ =>
+ {
+ Select(prototype.StructureId);
+ };
+ }
+
+ var vpSize = _displayManager.ScreenSize;
+ _menu.OpenCenteredAt(_inputManager.MouseScreenPosition.Position / vpSize);
+ }
+
+ private void ResetUI()
+ {
+ _menu?.Close();
+ _menu = null;
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ CreateUI();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+
+ ResetUI();
+ }
+
+ private void Select(string id)
+ {
+ CreateBlueprint(id);
+ ResetUI();
+ Close();
+ }
+
+ private void CreateBlueprint(string id)
+ {
+ var newObj = new PlacementInformation
+ {
+ Range = 2,
+ IsTile = false,
+ EntityType = id,
+ PlacementOption = "SnapgridCenter"
+ };
+
+ _prototypeManager.TryIndex(id, out var construct);
+
+ if (construct == null)
+ return;
+
+ var player = _player.LocalSession?.AttachedEntity;
+
+ if (player == null)
+ return;
+
+ // Хуйня которая не работает
+ // if (construct.ID == "CultPylon" && CheckForStructure(player, id))
+ // {
+ // var popup = _entMan.System();
+ // popup.PopupClient(Loc.GetString("cult-structure-craft-another-structure-nearby"), player.Value, player.Value);
+ // return;
+ // }
+
+ var constructSystem = _systemManager.GetEntitySystem();
+ var hijack = new ConstructionPlacementHijack(constructSystem, construct);
+
+ _placement.BeginPlacing(newObj, hijack);
+ }
+
+ private bool CheckForStructure(EntityUid? uid, string id)
+ {
+ if (uid == null)
+ return false;
+
+ if (!_entMan.TryGetComponent(uid, out var transform))
+ return false;
+
+ var lookupSystem = _entMan.System();
+ var entities = lookupSystem.GetEntitiesInRange(transform.Coordinates, 15f);
+ foreach (var ent in entities)
+ {
+ if (!_entMan.TryGetComponent(ent, out var metadata))
+ continue;
+
+ if (metadata.EntityPrototype?.ID == id)
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/Content.Client/_Sunrise/BloodCult/UI/SummonCultistList/SummonCultistListWindow.xaml b/Content.Client/_Sunrise/BloodCult/UI/SummonCultistList/SummonCultistListWindow.xaml
new file mode 100644
index 00000000000..3bcdb737d68
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/UI/SummonCultistList/SummonCultistListWindow.xaml
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/Content.Client/_Sunrise/BloodCult/UI/SummonCultistList/SummonCultistListWindow.xaml.cs b/Content.Client/_Sunrise/BloodCult/UI/SummonCultistList/SummonCultistListWindow.xaml.cs
new file mode 100644
index 00000000000..104e8e702bd
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/UI/SummonCultistList/SummonCultistListWindow.xaml.cs
@@ -0,0 +1,36 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client._Sunrise.BloodCult.UI.SummonCultistList;
+
+[GenerateTypedNameReferences]
+public partial class SummonCultistListWindow : DefaultWindow
+{
+ public Action? ItemSelected;
+
+ public SummonCultistListWindow()
+ {
+ RobustXamlLoader.Load(this);
+ }
+
+ public void PopulateList(List items, List labels)
+ {
+ ItemsContainer.RemoveAllChildren();
+
+ var count = Math.Min(items.Count, labels.Count);
+
+ for (var i = 0; i < count; i++)
+ {
+ var item = items[i];
+ var button = new Button();
+
+ button.Text = labels[i];
+
+ button.OnPressed += _ => ItemSelected?.Invoke(item, items.IndexOf(item));
+
+ ItemsContainer.AddChild(button);
+ }
+ }
+}
diff --git a/Content.Client/_Sunrise/BloodCult/UI/SummonCultistList/SummonCultistListWindowBUI.cs b/Content.Client/_Sunrise/BloodCult/UI/SummonCultistList/SummonCultistListWindowBUI.cs
new file mode 100644
index 00000000000..b65d75325f0
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/UI/SummonCultistList/SummonCultistListWindowBUI.cs
@@ -0,0 +1,42 @@
+using Content.Shared._Sunrise.BloodCult.UI;
+
+namespace Content.Client._Sunrise.BloodCult.UI.SummonCultistList;
+
+public sealed class SummonCultistListWindowBUI : BoundUserInterface
+{
+ private _Sunrise.BloodCult.UI.SummonCultistList.SummonCultistListWindow? _window;
+
+ public SummonCultistListWindowBUI(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ IoCManager.InjectDependencies(this);
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _window = new();
+ _window.OpenCentered();
+ _window.OnClose += Close;
+
+ _window.ItemSelected += (item, index) =>
+ {
+ var msg = new SummonCultistListWindowItemSelectedMessage(item, index);
+ SendMessage(msg);
+ _window.Close();
+ };
+
+ if (State != null)
+ UpdateState(State);
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ if (state is SummonCultistListWindowBUIState newState)
+ {
+ _window?.PopulateList(newState.Items, newState.Label);
+ }
+ }
+}
diff --git a/Content.Client/_Sunrise/BloodCult/UI/TeleportRunesList/TeleportRunesListWindow.xaml b/Content.Client/_Sunrise/BloodCult/UI/TeleportRunesList/TeleportRunesListWindow.xaml
new file mode 100644
index 00000000000..0a4e40f89a6
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/UI/TeleportRunesList/TeleportRunesListWindow.xaml
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/Content.Client/_Sunrise/BloodCult/UI/TeleportRunesList/TeleportRunesListWindow.xaml.cs b/Content.Client/_Sunrise/BloodCult/UI/TeleportRunesList/TeleportRunesListWindow.xaml.cs
new file mode 100644
index 00000000000..46e1cfa0893
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/UI/TeleportRunesList/TeleportRunesListWindow.xaml.cs
@@ -0,0 +1,41 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client._Sunrise.BloodCult.UI.TeleportRunesList;
+
+[GenerateTypedNameReferences]
+public partial class TeleportRunesListWindow : DefaultWindow
+{
+ public Action? ItemSelected;
+
+ public TeleportRunesListWindow()
+ {
+ RobustXamlLoader.Load(this);
+ }
+
+ public void PopulateList(List items, List labels)
+ {
+ ItemsContainer.RemoveAllChildren();
+
+ var count = Math.Min(items.Count, labels.Count);
+
+ for (var i = 0; i < count; i++)
+ {
+ var item = items[i];
+ var button = new Button();
+
+ button.Text = labels[i];
+
+ button.OnPressed += _ => ItemSelected?.Invoke(item, items.IndexOf(item));
+
+ ItemsContainer.AddChild(button);
+ }
+ }
+
+ public void Clear()
+ {
+ ItemsContainer.RemoveAllChildren();
+ }
+}
diff --git a/Content.Client/_Sunrise/BloodCult/UI/TeleportRunesList/TeleportRunesListWindowBUI.cs b/Content.Client/_Sunrise/BloodCult/UI/TeleportRunesList/TeleportRunesListWindowBUI.cs
new file mode 100644
index 00000000000..08e6987aefa
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/UI/TeleportRunesList/TeleportRunesListWindowBUI.cs
@@ -0,0 +1,42 @@
+using Content.Shared._Sunrise.BloodCult.UI;
+
+namespace Content.Client._Sunrise.BloodCult.UI.TeleportRunesList;
+
+public sealed class TeleportRunesListWindowBUI : BoundUserInterface
+{
+ private TeleportRunesListWindow? _window;
+
+ public TeleportRunesListWindowBUI(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ IoCManager.InjectDependencies(this);
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _window = new();
+ _window.OpenCentered();
+ _window.OnClose += Close;
+
+ _window.ItemSelected += (item, index) =>
+ {
+ var msg = new TeleportRunesListWindowItemSelectedMessage(item, index);
+ SendMessage(msg);
+ _window.Close();
+ };
+
+ if (State != null)
+ UpdateState(State);
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ if (state is TeleportRunesListWindowBUIState newState)
+ {
+ _window?.PopulateList(newState.Items, newState.Label);
+ }
+ }
+}
diff --git a/Content.Client/_Sunrise/BloodCult/UI/TeleportSpell/TeleportSpellEui.cs b/Content.Client/_Sunrise/BloodCult/UI/TeleportSpell/TeleportSpellEui.cs
new file mode 100644
index 00000000000..32cd7eeef92
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/UI/TeleportSpell/TeleportSpellEui.cs
@@ -0,0 +1,40 @@
+using System.Linq;
+using Content.Client._Sunrise.BloodCult.UI.TeleportRunesList;
+using Content.Client.Eui;
+using Content.Shared._Sunrise.BloodCult.UI;
+using Content.Shared.Eui;
+
+namespace Content.Client._Sunrise.BloodCult.UI.TeleportSpell;
+
+public sealed class TeleportSpellEui : BaseEui
+{
+ private TeleportRunesListWindow _window;
+
+ public TeleportSpellEui()
+ {
+ _window = new TeleportRunesListWindow();
+ }
+
+ public override void Opened()
+ {
+ _window.OpenCentered();
+ _window.ItemSelected += (index, _) => SendMessage(new TeleportSpellTargetRuneSelected(){RuneUid = index});
+ _window.OnClose += () => SendMessage(new CloseEuiMessage());
+
+ base.Opened();
+ }
+
+ public override void Closed()
+ {
+ base.Closed();
+ _window.Close();
+ }
+
+ public override void HandleState(EuiStateBase state)
+ {
+ if(state is not TeleportSpellEuiState cast) return;
+
+ _window.Clear();
+ _window.PopulateList(cast.Runes.Keys.ToList(), cast.Runes.Values.ToList());
+ }
+}
diff --git a/Content.Client/_Sunrise/BloodCult/UI/Torch/TorchWindow.xaml b/Content.Client/_Sunrise/BloodCult/UI/Torch/TorchWindow.xaml
new file mode 100644
index 00000000000..4176e8374a3
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/UI/Torch/TorchWindow.xaml
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/Content.Client/_Sunrise/BloodCult/UI/Torch/TorchWindow.xaml.cs b/Content.Client/_Sunrise/BloodCult/UI/Torch/TorchWindow.xaml.cs
new file mode 100644
index 00000000000..15cc5eea5e5
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/UI/Torch/TorchWindow.xaml.cs
@@ -0,0 +1,34 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client._Sunrise.BloodCult.UI.Torch;
+
+[GenerateTypedNameReferences]
+public partial class TorchWindow : DefaultWindow
+{
+ public Action? ItemSelected;
+
+ public TorchWindow()
+ {
+ RobustXamlLoader.Load(this);
+ }
+
+ public void PopulateList(Dictionary items)
+ {
+ ItemsContainer.RemoveAllChildren();
+
+ foreach (var item in items.Keys)
+ {
+ var button = new Button();
+ var itemName = items[item];
+
+ button.Text = itemName;
+
+ button.OnPressed += _ => ItemSelected?.Invoke(item, items[item]);
+
+ ItemsContainer.AddChild(button);
+ }
+ }
+}
diff --git a/Content.Client/_Sunrise/BloodCult/UI/Torch/TorchWindowBUI.cs b/Content.Client/_Sunrise/BloodCult/UI/Torch/TorchWindowBUI.cs
new file mode 100644
index 00000000000..93b660ec703
--- /dev/null
+++ b/Content.Client/_Sunrise/BloodCult/UI/Torch/TorchWindowBUI.cs
@@ -0,0 +1,42 @@
+using Content.Shared._Sunrise.BloodCult.Items;
+
+namespace Content.Client._Sunrise.BloodCult.UI.Torch;
+
+public sealed class TorchWindowBUI : BoundUserInterface
+{
+ private TorchWindow? _window;
+
+ public TorchWindowBUI(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ IoCManager.InjectDependencies(this);
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _window = new();
+ _window.OpenCentered();
+ _window.OnClose += Close;
+
+ _window.ItemSelected += (uid, item) =>
+ {
+ var msg = new TorchWindowItemSelectedMessage(uid, item);
+ SendMessage(msg);
+ _window.Close();
+ };
+
+ if (State != null)
+ UpdateState(State);
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ if (state is TorchWindowBUIState newState)
+ {
+ _window?.PopulateList(newState.Items);
+ }
+ }
+}
diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs
index b0551fe5973..1f29e6b3795 100644
--- a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs
+++ b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs
@@ -1,5 +1,6 @@
using Content.Server._Sunrise.AssaultOps;
using Content.Server._Sunrise.FleshCult.GameRule;
+using Content.Server._Sunrise.BloodCult.GameRule;
using Content.Server.Administration.Commands;
using Content.Server.Antag;
using Content.Server.GameTicking.Rules.Components;
@@ -154,6 +155,7 @@ private void AddAntagVerbs(GetVerbsEvent args)
};
args.Verbs.Add(thief);
+ // Sunrise-Start
Verb ling = new()
{
Text = Loc.GetString("admin-verb-text-make-changeling"),
@@ -211,5 +213,21 @@ private void AddAntagVerbs(GetVerbsEvent args)
};
// На время пока не будут закончены все новые режимы.
//args.Verbs.Add(fleshCultist);
+
+ Verb bloodCultist = new()
+ {
+ Text = Loc.GetString("admin-verb-text-make-cultist"),
+ Category = VerbCategory.Antag,
+ Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Objects/Weapons/Melee/cult_dagger.rsi"), "icon"),
+ Act = () =>
+ {
+ _antag.ForceMakeAntag(targetPlayer, "BloodCult");
+ },
+ Impact = LogImpact.High,
+ Message = Loc.GetString("admin-verb-make-cultist"),
+ };
+ // На время пока не будут закончены все новые режимы.
+ //args.Verbs.Add(bloodCultist);
+ // Sunrise-End
}
}
diff --git a/Content.Server/Body/Systems/StomachSystem.cs b/Content.Server/Body/Systems/StomachSystem.cs
index a8cf946f63b..e8e5905cfa2 100644
--- a/Content.Server/Body/Systems/StomachSystem.cs
+++ b/Content.Server/Body/Systems/StomachSystem.cs
@@ -1,8 +1,10 @@
+using System.Linq;
using Content.Server.Body.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Body.Organ;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
+using Content.Shared.Chemistry.Reagent;
using Content.Shared.Whitelist;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -136,5 +138,45 @@ public void SetSpecialDigestible(StomachComponent component, EntityWhitelist? wh
{
component.SpecialDigestible = whitelist;
}
+
+ // Sunrise-Start
+ public bool TryChangeReagent(EntityUid uid, string fromReagent, string toReagent,
+ StomachComponent? stomach = null,
+ SolutionContainerManagerComponent? solutions = null)
+ {
+ if (!Resolve(uid, ref stomach, ref solutions, false))
+ return false;
+
+ if (!_solutionContainerSystem.ResolveSolution((uid, solutions), DefaultSolutionName, ref stomach.Solution))
+ return false;
+
+ foreach (var reagent in stomach.Solution.Value.Comp.Solution.Contents.ToList())
+ {
+ if (reagent.Reagent.Prototype != fromReagent)
+ continue;
+
+ var amount = reagent.Quantity;
+
+ stomach.Solution.Value.Comp.Solution.RemoveReagent(reagent.Reagent.Prototype, amount);
+ foreach (var stomachReagentDelta in stomach.ReagentDeltas.ToList())
+ {
+ if (stomachReagentDelta.ReagentQuantity.Reagent.Prototype != reagent.Reagent.Prototype)
+ continue;
+
+ stomach.ReagentDeltas.Remove(stomachReagentDelta);
+ var newDelta = new StomachComponent.ReagentDelta(new ReagentQuantity(
+ new ReagentId(toReagent, stomachReagentDelta.ReagentQuantity.Reagent.Data),
+ stomachReagentDelta.ReagentQuantity.Quantity));
+ stomach.ReagentDeltas.Add(newDelta);
+ }
+
+ stomach.Solution.Value.Comp.Solution.AddReagent(toReagent, amount);
+
+ return true;
+ }
+
+ return false;
+ }
+ // Sunrise-End
}
}
diff --git a/Content.Server/RoundEnd/RoundEndSystem.cs b/Content.Server/RoundEnd/RoundEndSystem.cs
index 2c10e6d929d..07f344325c8 100644
--- a/Content.Server/RoundEnd/RoundEndSystem.cs
+++ b/Content.Server/RoundEnd/RoundEndSystem.cs
@@ -372,6 +372,32 @@ public TimeSpan TimeToCallShuttle()
: _cfg.GetCVar(CCVars.EmergencyShuttleAutoCallTime);
return AutoCallStartTime + TimeSpan.FromMinutes(autoCalledBefore);
}
+
+ public void DelayCursedShuttle(TimeSpan delay)
+ {
+ if (_gameTicker.RunLevel != GameRunLevel.InRound)
+ return;
+
+ if (_countdownTokenSource == null)
+ return;
+
+ var countdown = ExpectedCountdownEnd - _gameTiming.CurTime + delay;
+ ExpectedCountdownEnd = _gameTiming.CurTime + countdown;
+
+ _countdownTokenSource.Cancel();
+ _countdownTokenSource = new ();
+
+ if (countdown != null)
+ Timer.Spawn(countdown.Value, _shuttle.DockEmergencyShuttle, _countdownTokenSource.Token);
+
+ _chatSystem.DispatchGlobalAnnouncement(Loc.GetString("round-end-system-shuttle-curse-delayed-announcement"),
+ Loc.GetString("Station"), colorOverride: Color.Gold);
+ }
+
+ public bool ShuttleCalled()
+ {
+ return ExpectedCountdownEnd != null;
+ }
// Sunrise-end
}
diff --git a/Content.Server/_Sunrise/BecomeDustOnDeathSystem/BecomeDustOnDeathComponent.cs b/Content.Server/_Sunrise/BecomeDustOnDeathSystem/BecomeDustOnDeathComponent.cs
new file mode 100644
index 00000000000..e6210b576f5
--- /dev/null
+++ b/Content.Server/_Sunrise/BecomeDustOnDeathSystem/BecomeDustOnDeathComponent.cs
@@ -0,0 +1,11 @@
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Server._Sunrise.BecomeDustOnDeathSystem;
+
+[RegisterComponent]
+public sealed partial class BecomeDustOnDeathComponent : Component
+{
+ [DataField("sprite", customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string SpawnOnDeathPrototype = "Ectoplasm";
+}
diff --git a/Content.Server/_Sunrise/BecomeDustOnDeathSystem/BecomeDustOnDeathSystem.cs b/Content.Server/_Sunrise/BecomeDustOnDeathSystem/BecomeDustOnDeathSystem.cs
new file mode 100644
index 00000000000..0254f8afef5
--- /dev/null
+++ b/Content.Server/_Sunrise/BecomeDustOnDeathSystem/BecomeDustOnDeathSystem.cs
@@ -0,0 +1,19 @@
+using Content.Shared.Mobs;
+
+namespace Content.Server._Sunrise.BecomeDustOnDeathSystem;
+
+public sealed class BecomeDustOnDeathSystem : EntitySystem
+{
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(OnMobStateChanged);
+ }
+
+ private void OnMobStateChanged(EntityUid uid, BecomeDustOnDeathComponent component, MobStateChangedEvent args)
+ {
+ var xform = Transform(uid);
+ Spawn(component.SpawnOnDeathPrototype, xform.Coordinates);
+
+ QueueDel(uid);
+ }
+}
diff --git a/Content.Server/_Sunrise/BloodCult/ConstructComponent.cs b/Content.Server/_Sunrise/BloodCult/ConstructComponent.cs
new file mode 100644
index 00000000000..aa0f79c77b6
--- /dev/null
+++ b/Content.Server/_Sunrise/BloodCult/ConstructComponent.cs
@@ -0,0 +1,8 @@
+namespace Content.Server._Sunrise.BloodCult;
+
+[RegisterComponent]
+public sealed partial class ConstructComponent : Component
+{
+ [DataField("actions")]
+ public List Actions = new();
+}
diff --git a/Content.Server/_Sunrise/BloodCult/CultistRoleComponent.cs b/Content.Server/_Sunrise/BloodCult/CultistRoleComponent.cs
new file mode 100644
index 00000000000..42326a3954b
--- /dev/null
+++ b/Content.Server/_Sunrise/BloodCult/CultistRoleComponent.cs
@@ -0,0 +1,8 @@
+using Content.Shared.Roles;
+
+namespace Content.Server._Sunrise.BloodCult;
+
+[RegisterComponent]
+public sealed partial class BloodCultistRoleComponent : BaseMindRoleComponent
+{
+}
diff --git a/Content.Server/_Sunrise/BloodCult/GameRule/BloodCultRuleComponent.cs b/Content.Server/_Sunrise/BloodCult/GameRule/BloodCultRuleComponent.cs
new file mode 100644
index 00000000000..e2ca7a5a538
--- /dev/null
+++ b/Content.Server/_Sunrise/BloodCult/GameRule/BloodCultRuleComponent.cs
@@ -0,0 +1,63 @@
+using Content.Shared.Roles;
+using Robust.Shared.Audio;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
+
+namespace Content.Server._Sunrise.BloodCult.GameRule;
+
+[RegisterComponent, Access(typeof(BloodCultRuleSystem))]
+public sealed partial class BloodCultRuleComponent : Component
+{
+ public readonly SoundSpecifier GreatingsSound = new SoundPathSpecifier("/Audio/_Sunrise/BloodCult/blood_cult_greeting.ogg");
+
+ [DataField("cultistPrototypeId", customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public static string CultistPrototypeId = "BloodCultist";
+
+ [DataField("reaperPrototype", customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public static string ReaperPrototype = "ReaperConstruct";
+
+ [ViewVariables(VVAccess.ReadOnly), DataField("tileId")]
+ public static string CultFloor = "CultFloor";
+
+ [DataField("eyeColor")]
+ public static Color EyeColor = Color.FromHex("#f80000");
+
+ [DataField("redEyeThreshold")]
+ public static int ReadEyeThreshold = 5;
+
+ [DataField("pentagramThreshold")]
+ public static int PentagramThreshold = 8;
+
+ public List StarCandidates = new();
+
+ [DataField("cultistStartingItems", customTypeSerializer: typeof(PrototypeIdListSerializer))]
+ public List StartingItems = new();
+
+ public List CultTargets = new();
+
+ public CultWinCondition WinCondition;
+
+ [DataField]
+ public int MinTargets = 1;
+
+ [DataField]
+ public int TargetsPerPlayer = 30;
+
+ [DataField]
+ public int MaxTargets = 3;
+
+ [DataField]
+ public int CultMembersForSummonGod = 10;
+}
+
+public enum CultWinCondition : byte
+{
+ CultWin,
+ CultFailure
+}
+
+public sealed class CultNarsieSummoned : EntityEventArgs
+{
+}
diff --git a/Content.Server/_Sunrise/BloodCult/GameRule/BloodCultRuleSystem.cs b/Content.Server/_Sunrise/BloodCult/GameRule/BloodCultRuleSystem.cs
new file mode 100644
index 00000000000..41e485eb3c0
--- /dev/null
+++ b/Content.Server/_Sunrise/BloodCult/GameRule/BloodCultRuleSystem.cs
@@ -0,0 +1,373 @@
+using System.Linq;
+using Content.Server.Antag;
+using Content.Server.Chat.Managers;
+using Content.Server.GameTicking;
+using Content.Server.GameTicking.Rules;
+using Content.Server.RoundEnd;
+using Content.Server.Storage.EntitySystems;
+using Content.Shared._Sunrise.BloodCult.Components;
+using Content.Shared.Body.Systems;
+using Content.Shared.Clumsy;
+using Content.Shared.GameTicking.Components;
+using Content.Shared.Humanoid;
+using Content.Shared.Inventory;
+using Content.Shared.Mind;
+using Content.Shared.Mind.Components;
+using Content.Shared.Mobs;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Mobs.Systems;
+using Content.Shared.NPC.Systems;
+using Content.Shared.Tag;
+using Robust.Shared.Audio;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Player;
+using Robust.Shared.Random;
+using CultMemberComponent = Content.Shared._Sunrise.BloodCult.Components.CultMemberComponent;
+
+namespace Content.Server._Sunrise.BloodCult.GameRule;
+
+public sealed class BloodCultRuleSystem : GameRuleSystem
+{
+ [Dependency] private readonly IChatManager _chatManager = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly InventorySystem _inventorySystem = default!;
+ [Dependency] private readonly StorageSystem _storageSystem = default!;
+ [Dependency] private readonly NpcFactionSystem _factionSystem = default!;
+ [Dependency] private readonly MobStateSystem _mobStateSystem = default!;
+ [Dependency] private readonly SharedAudioSystem _audioSystem = default!;
+ [Dependency] private readonly RoundEndSystem _roundEndSystem = default!;
+ [Dependency] private readonly SharedBodySystem _bodySystem = default!;
+ [Dependency] private readonly SharedMindSystem _mindSystem = default!;
+ [Dependency] private readonly AntagSelectionSystem _antagSelection = default!;
+ [Dependency] private readonly TagSystem _tagSystem = default!;
+
+ private ISawmill _sawmill = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ _sawmill = Logger.GetSawmill("preset");
+
+ SubscribeLocalEvent(OnNarsieSummon);
+
+ SubscribeLocalEvent(OnCultistComponentInit);
+ SubscribeLocalEvent(OnCultistComponentRemoved);
+ SubscribeLocalEvent(OnCultistsStateChanged);
+
+ SubscribeLocalEvent(AfterEntitySelected);
+ SubscribeLocalEvent(OnAfterAntagSelectionComplete);
+ }
+
+ protected override void Added(EntityUid uid, BloodCultRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
+ {
+ base.Added(uid, component, gameRule, args);
+ //SetCodewords(component, args.RuleEntity);
+ }
+
+ protected override void AppendRoundEndText(EntityUid uid,
+ BloodCultRuleComponent component,
+ GameRuleComponent gameRule,
+ ref RoundEndTextAppendEvent args)
+ {
+ var winText = Loc.GetString($"cult-cond-{component.WinCondition.ToString().ToLower()}");
+ args.AddLine(winText);
+
+ args.AddLine(Loc.GetString("cultists-list-start"));
+
+ var antags = _antagSelection.GetAntagIdentifiers(uid);
+
+ foreach (var (_, sessionData, name) in antags)
+ {
+ var lising = Loc.GetString("cultists-list-name", ("name", name), ("user", sessionData.UserName));
+ args.AddLine(lising);
+ }
+ }
+
+ private void AfterEntitySelected(Entity ent, ref AfterAntagEntitySelectedEvent args)
+ {
+ Log.Debug($"AfterAntagEntitySelected {ToPrettyString(ent)}");
+ MakeCultist(args.EntityUid, ent.Comp);
+ }
+
+ private void OnCultistsStateChanged(EntityUid uid, BloodCultistComponent component, MobStateChangedEvent ev)
+ {
+ if (ev.NewMobState == MobState.Dead)
+ {
+ CheckRoundShouldEnd();
+ }
+ }
+
+ public BloodCultRuleComponent? GetRule()
+ {
+ var rule = EntityQuery().FirstOrDefault();
+ return rule;
+ }
+
+ private void OnAfterAntagSelectionComplete(Entity ent, ref AntagSelectionCompleteEvent args)
+ {
+ var selectedCultist = new List();
+ foreach (var selectedMind in args.GameRule.Comp.SelectedMinds)
+ {
+ selectedCultist.Add(selectedMind.Item1);
+ }
+
+ var potentialTargets = FindPotentialTargets(selectedCultist);
+
+ var numTargets = MathHelper.Clamp(selectedCultist.Count / ent.Comp.TargetsPerPlayer, 1, ent.Comp.MaxTargets);
+
+ var selectedVictims = new List();
+
+ for (var i = 0; i < numTargets && potentialTargets.Count > 0; i++)
+ {
+ var index = _random.Next(potentialTargets.Count);
+ var selectedVictim = potentialTargets[index];
+ potentialTargets.RemoveAt(index);
+ selectedVictims.Add(selectedVictim.Mind!.Value);
+ }
+
+ ent.Comp.CultTargets.AddRange(selectedVictims);
+ }
+
+ public List GetTargets()
+ {
+ var querry = EntityQueryEnumerator();
+
+ var targetMinds = new List();
+
+ while (querry.MoveNext(out _, out var cultRuleComponent, out _))
+ {
+ foreach (var cultTarget in cultRuleComponent.CultTargets)
+ {
+ if (_mindSystem.TryGetMind(cultTarget, out var mindId, out var mind))
+ targetMinds.Add(mind);
+ }
+ }
+
+ return targetMinds;
+ }
+
+ public bool CanSummonNarsie()
+ {
+ var querry = EntityQueryEnumerator();
+
+ while (querry.MoveNext(out _, out var cultRuleComponent, out _))
+ {
+ var cultists = new List();
+ var cultisQuery = EntityQueryEnumerator();
+ while (cultisQuery.MoveNext(out var cultistUid, out _))
+ {
+ cultists.Add(cultistUid);
+ }
+ var constructs = new List();
+ var constructQuery = EntityQueryEnumerator();
+ while (constructQuery.MoveNext(out var constructUid, out _))
+ {
+ constructs.Add(constructUid);
+ }
+ var enoughCultists = cultists.Count + constructs.Count > cultRuleComponent.CultMembersForSummonGod;
+
+ if (!enoughCultists)
+ {
+ return false;
+ }
+
+ var targetsKilled = true;
+
+ var targets = GetTargets();
+ foreach (var mindComponent in targets)
+ {
+ targetsKilled = _mindSystem.IsCharacterDeadIc(mindComponent);
+ }
+
+ if (targetsKilled)
+ return true;
+ }
+
+ return false;
+ }
+
+ private void CheckRoundShouldEnd()
+ {
+ var querry = EntityQueryEnumerator();
+ var aliveCultistsCount = 0;
+
+ while (querry.MoveNext(out _, out var cultRuleComponent, out _))
+ {
+ var cultisQuery = EntityQueryEnumerator();
+ while (cultisQuery.MoveNext(out var cultistUid, out _))
+ {
+ if (!TryComp(cultistUid, out var mobState))
+ continue;
+
+ if (_mobStateSystem.IsAlive(cultistUid, mobState))
+ {
+ aliveCultistsCount++;
+ }
+ }
+
+ if (aliveCultistsCount != 0)
+ continue;
+
+ cultRuleComponent.WinCondition = CultWinCondition.CultFailure;
+ _roundEndSystem.EndRound();
+ }
+ }
+
+ private void OnCultistComponentInit(EntityUid uid, BloodCultistComponent component, ComponentInit args)
+ {
+ var query = EntityQueryEnumerator();
+
+ while (query.MoveNext(out var ruleEnt, out var cultRuleComponent, out _))
+ {
+ if (!GameTicker.IsGameRuleAdded(ruleEnt))
+ continue;
+
+ UpdateCultistsAppearance(cultRuleComponent);
+ }
+ }
+
+ private void OnCultistComponentRemoved(EntityUid uid, BloodCultistComponent component, ComponentRemove args)
+ {
+ var query = EntityQueryEnumerator();
+
+ while (query.MoveNext(out var ruleEnt, out var cultRuleComponent, out _))
+ {
+ if (!GameTicker.IsGameRuleAdded(ruleEnt))
+ continue;
+
+ RemoveCultistAppearance(uid);
+
+ CheckRoundShouldEnd();
+ }
+ }
+
+ private void RemoveCultistAppearance(EntityUid cultist)
+ {
+ if (TryComp(cultist, out var appearanceComponent))
+ {
+ //Потому что я так сказал
+ appearanceComponent.EyeColor = Color.White;
+ Dirty(cultist, appearanceComponent);
+ }
+
+ RemComp(cultist);
+ }
+
+ private void UpdateCultistsAppearance(BloodCultRuleComponent bloodCultRuleComponent)
+ {
+ var cultists = new List();
+ var cultisQuery = EntityQueryEnumerator();
+ while (cultisQuery.MoveNext(out var cultistUid, out _))
+ {
+ cultists.Add(cultistUid);
+ }
+ var constructs = new List();
+ var constructQuery = EntityQueryEnumerator();
+ while (constructQuery.MoveNext(out var constructUid, out _))
+ {
+ constructs.Add(constructUid);
+ }
+
+ var totalCultMembers = cultists.Count + constructs.Count;
+ if (totalCultMembers < BloodCultRuleComponent.ReadEyeThreshold)
+ return;
+
+ foreach (var cultist in cultists)
+ {
+ if (TryComp(cultist, out var appearanceComponent))
+ {
+ appearanceComponent.EyeColor = BloodCultRuleComponent.EyeColor;
+ Dirty(cultist, appearanceComponent);
+ }
+
+ if (totalCultMembers < BloodCultRuleComponent.PentagramThreshold)
+ return;
+
+ EnsureComp(cultist);
+ }
+ }
+
+ private List FindPotentialTargets(List exclude = null!)
+ {
+ var potentialTargets = new List();
+
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var mind, out _, out var actor))
+ {
+ var entity = mind.Mind;
+
+ if (entity == default)
+ continue;
+
+ if (exclude?.Contains(uid) is true)
+ {
+ continue;
+ }
+
+ potentialTargets.Add(mind);
+ }
+
+ return potentialTargets;
+ }
+
+ public bool MakeCultist(EntityUid cultist, BloodCultRuleComponent rule)
+ {
+ if (!_mindSystem.TryGetMind(cultist, out var mindId, out var mind))
+ return false;
+
+ EnsureComp(cultist);
+
+ if (HasComp(cultist))
+ RemComp(cultist);
+
+ EnsureComp(cultist);
+
+ _tagSystem.AddTag(cultist, "BloodCultist");
+
+ _factionSystem.RemoveFaction(cultist, "NanoTrasen", false);
+ _factionSystem.AddFaction(cultist, "BloodCult");
+
+ if (_inventorySystem.TryGetSlotEntity(cultist, "back", out var backPack))
+ {
+ foreach (var itemPrototype in rule.StartingItems)
+ {
+ var itemEntity = Spawn(itemPrototype, Transform(cultist).Coordinates);
+
+ if (backPack != null)
+ {
+ _storageSystem.Insert(backPack.Value, itemEntity, out _);
+ }
+ }
+ }
+
+ _audioSystem.PlayGlobal(rule.GreatingsSound, Filter.Empty().AddPlayer(mind.Session!), false,
+ AudioParams.Default);
+
+ _chatManager.DispatchServerMessage(mind.Session!, Loc.GetString("cult-role-greeting"));
+
+ _mindSystem.TryAddObjective(mindId, mind, "CultistKillObjective");
+
+ return true;
+ }
+
+ private void OnNarsieSummon(CultNarsieSummoned ev)
+ {
+ var query = EntityQuery().ToList();
+
+ foreach (var (mobState, mindContainer, _) in query)
+ {
+ if (!mindContainer.HasMind || mindContainer.Mind is null)
+ {
+ continue;
+ }
+
+ var reaper = Spawn(BloodCultRuleComponent.ReaperPrototype, Transform(mobState.Owner).Coordinates);
+ _mindSystem.TransferTo(mindContainer.Mind.Value, reaper);
+
+ _bodySystem.GibBody(mobState.Owner);
+ }
+
+ _roundEndSystem.EndRound();
+ }
+}
diff --git a/Content.Server/_Sunrise/BloodCult/HolyWater/BibleWaterConvertComponent.cs b/Content.Server/_Sunrise/BloodCult/HolyWater/BibleWaterConvertComponent.cs
new file mode 100644
index 00000000000..e7f3eaedb19
--- /dev/null
+++ b/Content.Server/_Sunrise/BloodCult/HolyWater/BibleWaterConvertComponent.cs
@@ -0,0 +1,11 @@
+namespace Content.Server._Sunrise.BloodCult.HolyWater;
+
+[RegisterComponent]
+public sealed partial class BibleWaterConvertComponent : Component
+{
+ [DataField("convertedId"), ViewVariables(VVAccess.ReadWrite)]
+ public string ConvertedId = "Water";
+
+ [DataField("ConvertedToId"), ViewVariables(VVAccess.ReadWrite)]
+ public string ConvertedToId = "Holywater";
+}
diff --git a/Content.Server/_Sunrise/BloodCult/HolyWater/DeconvertCultist.cs b/Content.Server/_Sunrise/BloodCult/HolyWater/DeconvertCultist.cs
new file mode 100644
index 00000000000..31768711598
--- /dev/null
+++ b/Content.Server/_Sunrise/BloodCult/HolyWater/DeconvertCultist.cs
@@ -0,0 +1,64 @@
+using System.Threading;
+using Content.Server.Popups;
+using Content.Server.Stunnable;
+using Content.Shared._Sunrise.BloodCult.Components;
+using Content.Shared.EntityEffects;
+using Content.Shared.IdentityManagement;
+using Content.Shared.Tag;
+using JetBrains.Annotations;
+using Robust.Shared.Prototypes;
+using Timer = Robust.Shared.Timing.Timer;
+
+namespace Content.Server._Sunrise.BloodCult.HolyWater;
+
+[ImplicitDataDefinitionForInheritors]
+[MeansImplicitUse]
+public sealed partial class DeconvertCultist : EntityEffect
+{
+ public override bool ShouldLog => true;
+
+ protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
+ {
+ return Loc.GetString("reagent-effect-guidebook-deconvert-cultist");
+ }
+
+ public override void Effect(EntityEffectBaseArgs args)
+ {
+ var uid = args.TargetEntity;
+
+ if (!args.EntityManager.TryGetComponent(uid, out BloodCultistComponent? component))
+ return;
+
+ if (component.HolyConvertToken != null)
+ return;
+
+ var random = new Random();
+ var convert = random.Next(1, 101) <= component.HolyConvertChance;
+ if (!convert)
+ return;
+
+ args.EntityManager.System()
+ .TryParalyze(uid, TimeSpan.FromSeconds(5f), true);
+ var target = Identity.Name(uid, args.EntityManager);
+ args.EntityManager.System()
+ .PopupEntity(Loc.GetString("holy-water-started-converting", ("target", target)), uid);
+
+ component.HolyConvertToken = new CancellationTokenSource();
+ Timer.Spawn(TimeSpan.FromSeconds(component.HolyConvertTime), () => ConvertCultist(uid, args.EntityManager),
+ component.HolyConvertToken.Token);
+ }
+
+ private void ConvertCultist(EntityUid uid, IEntityManager entityManager)
+ {
+ if (!entityManager.TryGetComponent(uid, out var cultist))
+ return;
+
+ cultist.HolyConvertToken = null;
+ entityManager.RemoveComponent(uid);
+ if (entityManager.HasComponent(uid))
+ entityManager.RemoveComponent(uid);
+ if (entityManager.HasComponent(uid))
+ entityManager.RemoveComponent(uid);
+ entityManager.System().RemoveTag(uid, "BloodCultist");
+ }
+}
diff --git a/Content.Server/_Sunrise/BloodCult/HolyWater/HolyWaterSystem.cs b/Content.Server/_Sunrise/BloodCult/HolyWater/HolyWaterSystem.cs
new file mode 100644
index 00000000000..a2849d40605
--- /dev/null
+++ b/Content.Server/_Sunrise/BloodCult/HolyWater/HolyWaterSystem.cs
@@ -0,0 +1,53 @@
+using System.Linq;
+using Content.Server.Stunnable;
+using Content.Shared.Chemistry.Components.SolutionManager;
+using Content.Shared.Interaction;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Popups;
+using Robust.Server.Audio;
+
+namespace Content.Server._Sunrise.BloodCult.HolyWater;
+
+public sealed class HolyWaterSystem : EntitySystem
+{
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+ [Dependency] private readonly AudioSystem _audio = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnBibleInteract);
+ }
+
+ private void OnBibleInteract(EntityUid uid, BibleWaterConvertComponent component, AfterInteractEvent args)
+ {
+ if (HasComp(uid))
+ return;
+
+ if (!TryComp(args.Target, out var container) || container.Solutions == null)
+ return;
+
+ foreach (var solution in container.Solutions!.Values.Where(solution => solution.ContainsReagent(component.ConvertedId, null)))
+ {
+ foreach (var reagent in solution.Contents)
+ {
+ if (reagent.Reagent.Prototype != component.ConvertedId)
+ continue;
+
+ var amount = reagent.Quantity;
+
+ solution.RemoveReagent(reagent.Reagent.Prototype, reagent.Quantity);
+ solution.AddReagent(component.ConvertedToId, amount);
+
+ if (args.Target == null)
+ return;
+
+ _popup.PopupEntity(Loc.GetString("holy-water-converted"), args.Target.Value, args.User);
+ _audio.PlayPvs("/Audio/Effects/holy.ogg", args.Target.Value);
+
+ return;
+ }
+ }
+ }
+}
diff --git a/Content.Server/_Sunrise/BloodCult/Items/Components/CultRobeModifierComponent.cs b/Content.Server/_Sunrise/BloodCult/Items/Components/CultRobeModifierComponent.cs
new file mode 100644
index 00000000000..fdc3716eebc
--- /dev/null
+++ b/Content.Server/_Sunrise/BloodCult/Items/Components/CultRobeModifierComponent.cs
@@ -0,0 +1,13 @@
+namespace Content.Server._Sunrise.BloodCult.Items.Components;
+
+[RegisterComponent]
+public sealed partial class CultRobeModifierComponent : Component
+{
+ [ViewVariables(VVAccess.ReadWrite), DataField("speedModifier")]
+ public float SpeedModifier = 1.45f;
+
+ [ViewVariables(VVAccess.ReadOnly), DataField("damageModifierSetId")]
+ public string DamageModifierSetId = "CultRobe";
+
+ public string? StoredDamageSetId { get; set; }
+}
diff --git a/Content.Server/_Sunrise/BloodCult/Items/Components/ReturnItemOnThrowComponent.cs b/Content.Server/_Sunrise/BloodCult/Items/Components/ReturnItemOnThrowComponent.cs
new file mode 100644
index 00000000000..8699e611fd7
--- /dev/null
+++ b/Content.Server/_Sunrise/BloodCult/Items/Components/ReturnItemOnThrowComponent.cs
@@ -0,0 +1,8 @@
+namespace Content.Server._Sunrise.BloodCult.Items.Components;
+
+[RegisterComponent]
+public sealed partial class ReturnItemOnThrowComponent : Component
+{
+ [ViewVariables(VVAccess.ReadWrite), DataField("stunTime")]
+ public float StunTime = 1f;
+}
diff --git a/Content.Server/_Sunrise/BloodCult/Items/Components/ShuttleCurseComponent.cs b/Content.Server/_Sunrise/BloodCult/Items/Components/ShuttleCurseComponent.cs
new file mode 100644
index 00000000000..c1a4cae616f
--- /dev/null
+++ b/Content.Server/_Sunrise/BloodCult/Items/Components/ShuttleCurseComponent.cs
@@ -0,0 +1,11 @@
+namespace Content.Server._Sunrise.BloodCult.Items.Components;
+
+[RegisterComponent]
+public sealed partial class ShuttleCurseComponent : Component
+{
+ [ViewVariables(VVAccess.ReadWrite), DataField("delayTime")]
+ public TimeSpan DelayTime = TimeSpan.FromSeconds(120);
+
+ [ViewVariables(VVAccess.ReadWrite), DataField("cooldown")]
+ public TimeSpan Cooldown = TimeSpan.FromSeconds(180);
+}
diff --git a/Content.Server/_Sunrise/BloodCult/Items/Components/TorchCultistsProviderComponent.cs b/Content.Server/_Sunrise/BloodCult/Items/Components/TorchCultistsProviderComponent.cs
new file mode 100644
index 00000000000..246079bfb5b
--- /dev/null
+++ b/Content.Server/_Sunrise/BloodCult/Items/Components/TorchCultistsProviderComponent.cs
@@ -0,0 +1,25 @@
+using Content.Shared._Sunrise.BloodCult.Items;
+
+namespace Content.Server._Sunrise.BloodCult.Items.Components;
+
+[RegisterComponent]
+public sealed partial class TorchCultistsProviderComponent : Component
+{
+ [ViewVariables(VVAccess.ReadOnly)]
+ public Enum UserInterfaceKey = CultTeleporterUiKey.Key;
+
+ [ViewVariables(VVAccess.ReadOnly)]
+ public EntityUid? ItemSelected;
+
+ [ViewVariables(VVAccess.ReadWrite), DataField("cooldown")]
+ public TimeSpan Cooldown = TimeSpan.FromSeconds(30);
+
+ [ViewVariables(VVAccess.ReadWrite), DataField("usesLeft")]
+ public int UsesLeft = 3;
+
+ [ViewVariables(VVAccess.ReadOnly)]
+ public TimeSpan NextUse = TimeSpan.Zero;
+
+ [ViewVariables(VVAccess.ReadOnly)]
+ public bool Active = true;
+}
diff --git a/Content.Server/_Sunrise/BloodCult/Items/Systems/CultBloodSpearSystem.cs b/Content.Server/_Sunrise/BloodCult/Items/Systems/CultBloodSpearSystem.cs
new file mode 100644
index 00000000000..4d59ff4a56f
--- /dev/null
+++ b/Content.Server/_Sunrise/BloodCult/Items/Systems/CultBloodSpearSystem.cs
@@ -0,0 +1,129 @@
+using System.Numerics;
+using Content.Server.Hands.Systems;
+using Content.Server.Popups;
+using Content.Shared._Sunrise.BloodCult.Actions;
+using Content.Shared._Sunrise.BloodCult.Components;
+using Content.Shared._Sunrise.BloodCult.Items;
+using Content.Shared.Actions;
+using Content.Shared.Damage;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Popups;
+using Content.Shared.Stunnable;
+using Content.Shared.Throwing;
+using Robust.Shared.Audio.Systems;
+
+namespace Content.Server._Sunrise.BloodCult.Items.Systems;
+
+public sealed class CultBloodSpearSystem : EntitySystem
+{
+ [Dependency] private readonly SharedStunSystem _stunSystem = default!;
+ [Dependency] private readonly DamageableSystem _damageableSystem = default!;
+ [Dependency] private readonly HandsSystem _handsSystem = default!;
+ [Dependency] private readonly EntityManager _entityManager = default!;
+ [Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
+ [Dependency] private readonly ThrowingSystem _throwingSystem = default!;
+ [Dependency] private readonly SharedTransformSystem _transform = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly PopupSystem _popupSystem = default!;
+
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnDoHit);
+ SubscribeLocalEvent(OnShutdown);
+ SubscribeLocalEvent(OnReturnSpear);
+ SubscribeLocalEvent(OnStartup);
+ }
+
+ private void OnStartup(EntityUid uid, BloodSpearOwnerComponent component, ComponentStartup args)
+ {
+ _actionsSystem.AddAction(uid, component.ReturnSpearActionId);
+ }
+
+ private void OnReturnSpear(EntityUid uid, BloodSpearOwnerComponent component, CultReturnBloodSpearActionEvent args)
+ {
+ if (component.Spear == null)
+ return;
+
+ if (!_entityManager.EntityExists(component.Spear))
+ return;
+
+ _handsSystem.TryDrop(component.Spear.Value);
+
+ var direction = CalculateDirection(component.Spear.Value, uid);
+
+ if (direction == null)
+ {
+ _popupSystem.PopupEntity($"Копье не найдено", uid,uid, PopupType.Large);
+ return;
+ }
+
+ if (direction.Value.Length() > component.MaxReturnDistance)
+ {
+ _popupSystem.PopupEntity($"Слишком далеко", uid,uid, PopupType.Large);
+ return;
+ }
+
+ _throwingSystem.TryThrow(component.Spear.Value, direction.Value * 1.2f, 10f);
+ args.Handled = true;
+ }
+
+ private Vector2? CalculateDirection(EntityUid pinUid, EntityUid trgUid)
+ {
+ var xformQuery = GetEntityQuery();
+
+ // check if entities have transform component
+ if (!xformQuery.TryGetComponent(pinUid, out var pin))
+ return null;
+ if (!xformQuery.TryGetComponent(trgUid, out var trg))
+ return null;
+
+ // check if they are on same map
+ if (pin.MapID != trg.MapID)
+ return null;
+
+ // get world direction vector
+ var dir = _transform.GetWorldPosition(trg, xformQuery) - _transform.GetWorldPosition(pin, xformQuery);
+ return dir;
+ }
+
+ private void OnShutdown(EntityUid uid, CultBloodSpearComponent component, ComponentShutdown args)
+ {
+ if (!_entityManager.TryGetComponent(component.SpearOwner, out var actionsComponent))
+ return;
+ if (!_entityManager.TryGetComponent(component.SpearOwner, out var spearOwnerComponent))
+ return;
+ foreach (var userAction in actionsComponent.Actions)
+ {
+ var entityPrototypeId = MetaData(userAction).EntityPrototype?.ID;
+ if (entityPrototypeId == spearOwnerComponent.ReturnSpearActionId)
+ _actionsSystem.RemoveAction(component.SpearOwner.Value, userAction, actionsComponent);
+ }
+
+ if (HasComp(component.SpearOwner.Value))
+ RemComp(component.SpearOwner.Value);
+ }
+
+ private void OnDoHit(EntityUid uid, CultBloodSpearComponent component, ThrowDoHitEvent args)
+ {
+ if (HasComp(args.Target))
+ return;
+
+ if (HasComp(args.Target))
+ {
+ _handsSystem.TryPickup(args.Target, uid, checkActionBlocker: false);
+ }
+ else
+ {
+ if (HasComp(args.Target))
+ {
+ _stunSystem.TryParalyze(args.Target, TimeSpan.FromSeconds(component.StuhTime), true);
+ _damageableSystem.TryChangeDamage(args.Target, component.Damage, origin: uid);
+ _audio.PlayPvs(component.BreakSound, uid);
+ QueueDel(uid);
+ }
+ }
+ }
+}
diff --git a/Content.Server/_Sunrise/BloodCult/Items/Systems/CultBloodSpellSystem.cs b/Content.Server/_Sunrise/BloodCult/Items/Systems/CultBloodSpellSystem.cs
new file mode 100644
index 00000000000..824606f5464
--- /dev/null
+++ b/Content.Server/_Sunrise/BloodCult/Items/Systems/CultBloodSpellSystem.cs
@@ -0,0 +1,342 @@
+using System.Linq;
+using Content.Server.Body.Components;
+using Content.Server.Body.Systems;
+using Content.Server.Hands.Systems;
+using Content.Server.Popups;
+using Content.Shared._Sunrise.BloodCult.Components;
+using Content.Shared._Sunrise.BloodCult.Items;
+using Content.Shared.Chemistry.Components;
+using Content.Shared.Chemistry.EntitySystems;
+using Content.Shared.Damage;
+using Content.Shared.Damage.Prototypes;
+using Content.Shared.Examine;
+using Content.Shared.FixedPoint;
+using Content.Shared.Fluids.Components;
+using Content.Shared.Hands;
+using Content.Shared.Interaction;
+using Content.Shared.Interaction.Events;
+using Content.Shared.Popups;
+using Content.Shared.Timing;
+using Robust.Server.GameObjects;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Collections;
+using Robust.Shared.Map;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server._Sunrise.BloodCult.Items.Systems;
+
+public sealed class CultBloodSpellSystem: EntitySystem
+{
+ [Dependency] private readonly PopupSystem _popupSystem = default!;
+ [Dependency] private readonly EntityLookupSystem _lookup = default!;
+ [Dependency] private readonly SharedSolutionContainerSystem _solutionSystem = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly UseDelaySystem _useDelay = default!;
+ [Dependency] private readonly SharedAudioSystem _audioSystem = default!;
+ [Dependency] private readonly DamageableSystem _damageableSystem = default!;
+ [Dependency] private readonly BloodstreamSystem _bloodstreamSystem = default!;
+ [Dependency] private readonly UserInterfaceSystem _ui = default!;
+ [Dependency] private readonly HandsSystem _handsSystem = default!;
+ [Dependency] private readonly TransformSystem _transformSystem = default!;
+
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnGotUnequippedHand);
+ SubscribeLocalEvent(OnInteractEvent);
+ SubscribeLocalEvent(OnUseInHand);
+ SubscribeLocalEvent(OnRequestCreateOrb);
+ SubscribeLocalEvent(OnCreateOrb);
+ SubscribeLocalEvent(OnRequestCreateBloodSpear);
+ SubscribeLocalEvent(OnExamine);
+ }
+
+ private void OnExamine(EntityUid uid, CultBloodSpellComponent component, ExaminedEvent args)
+ {
+ if (!TryComp(args.Examiner, out var cultistComponent))
+ return;
+
+ args.PushMarkup($"[bold][color=white]Доступно {cultistComponent.BloodCharges} зарядов[/color][bold]");
+ }
+
+
+ private void OnCreateOrb(EntityUid uid, CultBloodSpellComponent component, CountSelectorMessage args)
+ {
+ if (!TryComp(args.Actor, out var comp))
+ return;
+
+ var count = Math.Min(component.BloodOrbMinCost, args.Count);
+
+ if (comp.BloodCharges < count)
+ return;
+
+ var orb = Spawn(component.BlodOrbSpawnId, _transformSystem.GetMapCoordinates(uid));
+ var bloodOrb = EnsureComp(orb);
+ bloodOrb.BloodCharges = args.Count;
+ }
+
+ private void OnRequestCreateOrb(EntityUid uid, CultBloodSpellComponent component, CultBloodSpellCreateOrbBuiMessage args)
+ {
+ if (!TryComp(args.Actor, out var comp))
+ return;
+
+ if (!TryComp(args.Actor, out var actorComponent))
+ return;
+
+ _ui.OpenUi(uid, CountSelectorUIKey.Key, args.Actor);
+ }
+
+ private void OnRequestCreateBloodSpear(EntityUid uid, CultBloodSpellComponent component, CultBloodSpellCreateBloodSpearBuiMessage args)
+ {
+ if (!TryComp(args.Actor, out var comp))
+ return;
+
+ if (!TryComp(args.Actor, out var actorComponent))
+ return;
+
+ if (comp.BloodCharges < component.BloodSpearCost)
+ return;
+
+ var bloodSpear = Spawn(component.BloodSpearSpawnId, _transformSystem.GetMapCoordinates(uid));
+ var bloodSpearOwner = EnsureComp(args.Actor);
+ bloodSpearOwner.Spear = bloodSpear;
+ var bloodSpearComp = EnsureComp(bloodSpear);
+ bloodSpearComp.SpearOwner = args.Actor;
+ _handsSystem.TryDrop(args.Actor, uid, checkActionBlocker: false);
+ _handsSystem.TryPickup(args.Actor, bloodSpear, checkActionBlocker: false);
+ comp.BloodCharges -= component.BloodSpearCost;
+ }
+
+ private void OnUseInHand(EntityUid uid, CultBloodSpellComponent component, UseInHandEvent args)
+ {
+ if (!TryComp(args.User, out _) || !TryComp(args.User, out var actor))
+ return;
+
+ _ui.OpenUi(uid, CultBloodSpellUiKey.Key, actor.PlayerSession);
+ }
+
+ private void OnInteractEvent(EntityUid uid, CultBloodSpellComponent component, AfterInteractEvent args)
+ {
+ if (!args.CanReach || args.Handled)
+ return;
+
+ if (!TryComp(uid, out UseDelayComponent? useDelay) || _useDelay.IsDelayed((uid, useDelay)))
+ return;
+
+ if (!TryComp(args.User, out var cultistComponent))
+ return;
+
+ if (TryComp(args.Target, out var bloodOrbComponent))
+ {
+ cultistComponent.BloodCharges += bloodOrbComponent.BloodCharges;
+ QueueDel(args.Target);
+ _popupSystem.PopupEntity($"Собрано {bloodOrbComponent.BloodCharges} зарядов",
+ args.User, args.User, PopupType.Large);
+ _audioSystem.PlayPvs(component.BloodAbsorbSound, args.User, component.BloodAbsorbSound.Params);
+ args.Handled = true;
+ return;
+ }
+
+ if (HasComp(args.Target) || HasComp(args.Target))
+ {
+ args.Handled = HealCultist(args.Target.Value, args.User, cultistComponent, component);
+ return;
+ }
+
+ if (TryComp(args.Target, out var bloodstreamComponent))
+ {
+ if (_solutionSystem.ResolveSolution(args.Target.Value, bloodstreamComponent.BloodSolutionName,
+ ref bloodstreamComponent.BloodSolution, out var bloodSolution))
+ {
+ if (bloodSolution.Volume > 250)
+ {
+ var blood = bloodSolution.SplitSolutionWithOnly(
+ 100, "Blood");
+ cultistComponent.BloodCharges += blood.Volume / 2;
+ _popupSystem.PopupEntity($"Собрано {blood.Volume / 2} зарядов",
+ args.User, args.User, PopupType.Large);
+ _audioSystem.PlayPvs(component.BloodAbsorbSound, args.User, component.BloodAbsorbSound.Params);
+ blood.RemoveAllSolution();
+ }
+ }
+ args.Handled = true;
+ return;
+ }
+
+ var getCharges = AbsorbBloodPools(args.User, args.ClickLocation, component);
+ cultistComponent.BloodCharges += getCharges;
+ }
+
+ private FixedPoint2 AbsorbBloodPools(EntityUid user, EntityCoordinates coordinates, CultBloodSpellComponent bloodSpell)
+ {
+ var puddles = new ValueList<(EntityUid Entity, string Solution)>();
+ puddles.Clear();
+ foreach (var entity in _lookup.GetEntitiesInRange(coordinates, bloodSpell.RadiusAbsorbBloodPools))
+ {
+ if (TryComp(entity, out var puddle))
+ {
+ puddles.Add((entity, puddle.SolutionName));
+ }
+ }
+
+ if (puddles.Count == 0)
+ {
+ return 0;
+ }
+
+ var absorbBlood = new Solution();
+ foreach (var (puddle, solution) in puddles)
+ {
+ if (!_solutionSystem.TryGetSolution(puddle, solution, out var puddleSolution))
+ {
+ continue;
+ }
+ foreach (var puddleSolutionContent in puddleSolution.Value.Comp.Solution.ToList())
+ {
+ if (puddleSolutionContent.Reagent.Prototype != "Blood")
+ continue;
+
+ var blood = puddleSolution.Value.Comp.Solution.SplitSolutionWithOnly(
+ puddleSolutionContent.Quantity, puddleSolutionContent.Reagent.Prototype);
+
+ if (blood.Volume == 0)
+ continue;
+
+ absorbBlood.AddSolution(blood, _prototypeManager);
+ Spawn("CultTileSpawnEffect", Transform(puddle).Coordinates);
+
+ var ev = new SolutionContainerChangedEvent(puddleSolution.Value.Comp.Solution, solution);
+ RaiseLocalEvent(puddle, ref ev);
+ }
+ }
+
+ if (absorbBlood.Volume == 0)
+ return 0;
+
+ var getCharges = absorbBlood.Volume / 2;
+ _popupSystem.PopupEntity($"Собрано {getCharges} зарядов",
+ user, user, PopupType.Large);
+ _audioSystem.PlayPvs(bloodSpell.BloodAbsorbSound, user, bloodSpell.BloodAbsorbSound.Params);
+ absorbBlood.RemoveAllSolution();
+ return getCharges;
+ }
+
+ private bool HealCultist(EntityUid target, EntityUid user, BloodCultistComponent bloodCultistComponent, CultBloodSpellComponent bloodSpell)
+ {
+ var selfHeal = target == user;
+
+ var availableCharges = bloodCultistComponent.BloodCharges;
+
+ if (availableCharges <= 0)
+ return false;
+
+ var fillBlood = FixedPoint2.Zero;
+
+ if (TryComp(target, out var bloodstreamComponent))
+ {
+ if (_solutionSystem.ResolveSolution(target, bloodstreamComponent.BloodSolutionName,
+ ref bloodstreamComponent.BloodSolution, out var bloodSolution))
+ {
+ var lossBlood = bloodSolution.MaxVolume - bloodSolution.Volume;
+ if (lossBlood > 0)
+ {
+ fillBlood = FixedPoint2.Min(lossBlood, availableCharges / 2);
+ _bloodstreamSystem.TryModifyBloodLevel(target, fillBlood, bloodstreamComponent);
+ availableCharges -= fillBlood * 2;
+ bloodCultistComponent.BloodCharges -= fillBlood * 2;
+ }
+ }
+ }
+
+ var totalHeal = FixedPoint2.Zero;
+
+ var healingDamage = new DamageSpecifier();
+
+ if (TryComp(target, out var damageableComponent))
+ {
+ var totalDamage = FixedPoint2.Zero;
+
+ if (selfHeal)
+ availableCharges /= 1.65f;
+
+ foreach (var (damageGroup, damage) in damageableComponent.DamagePerGroup.ToList())
+ {
+ if (!bloodSpell.HealingGroups.Contains(damageGroup))
+ continue;
+
+ totalDamage += damage;
+ }
+
+ foreach (var (damageGroup, damage) in damageableComponent.DamagePerGroup.ToList())
+ {
+ if (availableCharges <= 0)
+ break;
+
+ if (!bloodSpell.HealingGroups.Contains(damageGroup))
+ continue;
+
+ var damageGroupSpecifier = _prototypeManager.Index(damageGroup);
+
+ // Calculate the total damage in the group
+ var totalDamageInGroup = FixedPoint2.Zero;
+
+ foreach (var damageType in damageGroupSpecifier.DamageTypes)
+ {
+ totalDamageInGroup += damageableComponent.Damage.DamageDict[damageType];
+ }
+
+ if (totalDamageInGroup == 0 || totalDamage == 0)
+ continue;
+
+ // Distribute healing proportionally to the total damage in the group
+ var proportionalHealGroup = (availableCharges * (damage / totalDamage));
+
+ // Ensure that the proportionalHealGroup does not exceed the availableHeal
+ proportionalHealGroup = FixedPoint2.Min(proportionalHealGroup, availableCharges);
+
+ // Update availableHeal by subtracting the allocated healing for the group
+ availableCharges -= proportionalHealGroup;
+
+ // Distribute healing within the group proportionally to each type
+ foreach (var damageType in damageGroupSpecifier.DamageTypes.ToList())
+ {
+ var damageInType = damageableComponent.Damage.DamageDict[damageType];
+
+ // Calculate the proportional share of healing for the current damageType
+ var proportionalHealType = (proportionalHealGroup * (damageInType / totalDamageInGroup));
+
+ // Ensure that the proportionalHealType does not exceed the availableHeal for the type
+ proportionalHealType = FixedPoint2.Min(proportionalHealType, damageInType);
+
+ // Update the healingDamage dictionary with the proportionalHealType for the current damageType
+ healingDamage.DamageDict.Add(damageType, -proportionalHealType);
+ totalHeal += proportionalHealType;
+ }
+ }
+ }
+
+ if (totalHeal == 0 && fillBlood == 0)
+ {
+ return false;
+ }
+
+ var usedCharges = totalHeal;
+ if (selfHeal)
+ usedCharges *= 1.65;
+
+ bloodCultistComponent.BloodCharges -= usedCharges;
+ _damageableSystem.TryChangeDamage(target, healingDamage, ignoreResistances: true);
+ _popupSystem.PopupEntity($"Излечено {totalHeal} урона и восстановлено {fillBlood} крови",
+ user, user, PopupType.Large);
+ _audioSystem.PlayPvs(bloodSpell.BloodAbsorbSound, user, bloodSpell.BloodAbsorbSound.Params);
+ return true;
+ }
+
+ private void OnGotUnequippedHand(EntityUid uid, CultBloodSpellComponent component, GotUnequippedHandEvent args)
+ {
+ QueueDel(uid);
+ }
+}
diff --git a/Content.Server/_Sunrise/BloodCult/Items/Systems/CultRobeModifierSystem.cs b/Content.Server/_Sunrise/BloodCult/Items/Systems/CultRobeModifierSystem.cs
new file mode 100644
index 00000000000..8496ed8880a
--- /dev/null
+++ b/Content.Server/_Sunrise/BloodCult/Items/Systems/CultRobeModifierSystem.cs
@@ -0,0 +1,81 @@
+using Content.Server._Sunrise.BloodCult.Items.Components;
+using Content.Shared._Sunrise.BloodCult.Components;
+using Content.Shared.Damage;
+using Content.Shared.Inventory.Events;
+using Content.Shared.Movement.Components;
+using Content.Shared.Movement.Systems;
+
+namespace Content.Server._Sunrise.BloodCult.Items.Systems;
+
+public sealed class CultRobeModifierSystem : EntitySystem
+{
+ [Dependency] private readonly MovementSpeedModifierSystem _movement = default!;
+ [Dependency] private readonly DamageableSystem _damageable = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnEquip);
+ SubscribeLocalEvent(OnUnequip);
+ }
+
+ private void OnEquip(EntityUid uid, CultRobeModifierComponent component, GotEquippedEvent args)
+ {
+ if (!HasComp(args.Equipee))
+ return;
+
+ if (args.Slot != "outerClothing")
+ return;
+
+ ModifySpeed(args.Equipee, component, true);
+ ModifyDamage(args.Equipee, component, true);
+ }
+
+ private void OnUnequip(EntityUid uid, CultRobeModifierComponent component, GotUnequippedEvent args)
+ {
+ if (!HasComp(args.Equipee))
+ return;
+
+ if (args.Slot != "outerClothing")
+ return;
+
+ ModifySpeed(args.Equipee, component, false);
+ ModifyDamage(args.Equipee, component, false);
+ }
+
+ private void ModifySpeed(EntityUid uid, CultRobeModifierComponent comp, bool increase)
+ {
+ if (!TryComp(uid, out var move))
+ return;
+
+ var walkSpeed = increase ? move.BaseWalkSpeed * comp.SpeedModifier : move.BaseWalkSpeed / comp.SpeedModifier;
+
+ var sprintSpeed =
+ increase ? move.BaseSprintSpeed * comp.SpeedModifier : move.BaseSprintSpeed / comp.SpeedModifier;
+
+ _movement.ChangeBaseSpeed(uid, walkSpeed, sprintSpeed, move.Acceleration, move);
+ }
+
+ private void ModifyDamage(EntityUid uid, CultRobeModifierComponent comp, bool increase)
+ {
+ var damageSet = string.Empty;
+ if (increase)
+ {
+ if (!TryComp(uid, out var damage))
+ return;
+
+ comp.StoredDamageSetId = damage.DamageModifierSetId;
+ damageSet = comp.DamageModifierSetId;
+ }
+ else
+ {
+ if (comp.StoredDamageSetId != null)
+ damageSet = comp.StoredDamageSetId;
+
+ comp.StoredDamageSetId = null;
+ }
+
+ _damageable.SetDamageModifierSetId(uid, damageSet);
+ }
+}
diff --git a/Content.Server/_Sunrise/BloodCult/Items/Systems/CultSpellProviderSystem.cs b/Content.Server/_Sunrise/BloodCult/Items/Systems/CultSpellProviderSystem.cs
new file mode 100644
index 00000000000..379874c4007
--- /dev/null
+++ b/Content.Server/_Sunrise/BloodCult/Items/Systems/CultSpellProviderSystem.cs
@@ -0,0 +1,166 @@
+using System.Linq;
+using Content.Server.Body.Components;
+using Content.Server.Body.Systems;
+using Content.Server.DoAfter;
+using Content.Server.Popups;
+using Content.Shared._Sunrise.BloodCult.Actions;
+using Content.Shared._Sunrise.BloodCult.Components;
+using Content.Shared._Sunrise.BloodCult.Items;
+using Content.Shared.Actions;
+using Content.Shared.DoAfter;
+using Content.Shared.Interaction;
+using Content.Shared.Verbs;
+using Robust.Server.GameObjects;
+using Robust.Shared.Audio;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Map;
+using Robust.Shared.Player;
+
+namespace Content.Server._Sunrise.BloodCult.Items.Systems;
+
+public sealed class CultSpellProviderSystem: EntitySystem
+{
+ [Dependency] private readonly UserInterfaceSystem _ui = default!;
+ [Dependency] private readonly PopupSystem _popupSystem = default!;
+ [Dependency] private readonly EntityManager _entityManager = default!;
+ [Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
+ [Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
+ [Dependency] private readonly BloodstreamSystem _bloodstreamSystem = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly EntityLookupSystem _lookup = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnCultMagicBlood);
+ SubscribeLocalEvent(OnCultMagicBloodSelected);
+ SubscribeLocalEvent>(OnDaggerActivationVerb);
+ SubscribeLocalEvent(OnDaggerActivate);
+ }
+
+ private void OnDaggerActivate(EntityUid uid, CultSpellProviderComponent component, ActivateInWorldEvent args)
+ {
+ if (!TryComp(args.User, out _) || !TryComp(args.User, out var actor))
+ return;
+
+ _ui.OpenUi(uid, CultSpellProviderUiKey.Key, actor.PlayerSession);
+ }
+
+ private void OnDaggerActivationVerb(EntityUid uid, CultSpellProviderComponent component, GetVerbsEvent args)
+ {
+ if (!args.CanAccess || !args.CanInteract)
+ return;
+
+ if (!TryComp(args.User, out _) || !TryComp(args.User, out var actor))
+ return;
+
+ args.Verbs.Add(new ActivationVerb()
+ {
+ Text = "Вырезать заклинание",
+ Act = () =>
+ {
+ _ui.OpenUi(uid, CultSpellProviderUiKey.Key, actor.PlayerSession);
+ }
+ });
+ }
+
+ private void OnCultMagicBloodSelected(EntityUid uid, CultSpellProviderComponent component, CultSpellProviderSelectedBuiMessage args)
+ {
+ if (!TryComp(args.Actor, out var comp) ||
+ !TryComp(args.Actor, out var actionsComponent))
+ return;
+
+ var cultistsActions = 0;
+
+ var action = BloodCultistComponent.CultistActions.FirstOrDefault(x => x.Equals(args.ActionType));
+
+ var duplicated = false;
+ foreach (var userAction in actionsComponent.Actions)
+ {
+ var entityPrototypeId = MetaData(userAction).EntityPrototype?.ID;
+ if (entityPrototypeId != null && BloodCultistComponent.CultistActions.Contains(entityPrototypeId))
+ cultistsActions++;
+
+ if (entityPrototypeId == action)
+ duplicated = true;
+ }
+
+ if (action == null)
+ return;
+
+ if (duplicated)
+ {
+ _popupSystem.PopupEntity(Loc.GetString("cult-duplicated-empowers"), uid);
+ return;
+ }
+
+ var maxAllowedActions = 1;
+ var timeToGetSpell = 10;
+ var bloodTake = 20;
+
+ var xform = Transform(uid);
+
+ if (CheckNearbyEmpowerRune(xform.Coordinates))
+ {
+ maxAllowedActions = 4;
+ timeToGetSpell = 4;
+ bloodTake = 8;
+ }
+
+ if (cultistsActions >= maxAllowedActions)
+ {
+ _popupSystem.PopupEntity(Loc.GetString("cult-too-much-empowers"), uid);
+ return;
+ }
+
+ var ev = new CultMagicBloodCallEvent
+ {
+ ActionId = action,
+ BloodTake = bloodTake
+ };
+
+ var argsDoAfterEvent = new DoAfterArgs(_entityManager, args.Actor, timeToGetSpell, ev, args.Actor)
+ {
+ BreakOnMove = true,
+ NeedHand = true
+ };
+
+ _doAfterSystem.TryStartDoAfter(argsDoAfterEvent);
+ }
+
+ private void OnCultMagicBlood(EntityUid uid, BloodCultistComponent comp, CultMagicBloodCallEvent args)
+ {
+ if (args.Cancelled)
+ return;
+
+ var howMuchBloodTake = -args.BloodTake;
+ var action = args.ActionId;
+ var user = args.User;
+
+ if (HasComp(user))
+ howMuchBloodTake /= 2;
+
+ if (!TryComp(user, out var bloodstreamComponent))
+ return;
+
+ _bloodstreamSystem.TryModifyBloodLevel(user, howMuchBloodTake, bloodstreamComponent);
+ _audio.PlayPvs("/Audio/_Sunrise/BloodCult/blood.ogg", user, AudioParams.Default.WithMaxDistance(2f));
+
+ EntityUid? actionId = null;
+ _actionsSystem.AddAction(user, ref actionId, action);
+ }
+
+ private bool CheckNearbyEmpowerRune(EntityCoordinates coordinates)
+ {
+ var radius = 1.0f;
+
+ foreach (var lookupUid in _lookup.GetEntitiesInRange(coordinates, radius))
+ {
+ if (HasComp(lookupUid))
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/Content.Server/_Sunrise/BloodCult/Items/Systems/CultWeaponSystem.cs b/Content.Server/_Sunrise/BloodCult/Items/Systems/CultWeaponSystem.cs
new file mode 100644
index 00000000000..001cd0e0be5
--- /dev/null
+++ b/Content.Server/_Sunrise/BloodCult/Items/Systems/CultWeaponSystem.cs
@@ -0,0 +1,109 @@
+using Content.Server.Body.Components;
+using Content.Server.Body.Systems;
+using Content.Shared._Sunrise.BloodCult.Components;
+using Content.Shared.Body.Components;
+using Content.Shared.Chemistry.Components;
+using Content.Shared.Chemistry.EntitySystems;
+using Content.Shared.Damage;
+using Content.Shared.Interaction;
+using Content.Shared.Popups;
+using Content.Shared.Stunnable;
+using Content.Shared.Weapons.Melee.Events;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Utility;
+using CultWeaponComponent = Content.Shared._Sunrise.BloodCult.Items.CultWeaponComponent;
+
+namespace Content.Server._Sunrise.BloodCult.Items.Systems;
+
+public sealed class CultWeaponSystem : EntitySystem
+{
+ [Dependency] private readonly SharedStunSystem _stunSystem = default!;
+ [Dependency] private readonly DamageableSystem _damageableSystem = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+ [Dependency] private readonly BodySystem _body = default!;
+ [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
+ [Dependency] private readonly StomachSystem _stomachSystem = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnMeleeHit);
+ SubscribeLocalEvent(OnInteractEvent);
+ }
+
+ private void OnInteractEvent(EntityUid uid, CultWeaponComponent component, AfterInteractEvent args)
+ {
+ if (!args.CanReach || args.Handled || args.Target == null)
+ return;
+
+ if (!HasComp(args.User))
+ return;
+
+ if (!TryComp(args.Target, out var cultistComponent))
+ return;
+
+ if (!TryComp(args.Target, out var body))
+ return;
+
+ var convert = false;
+
+ if (cultistComponent.HolyConvertToken != null)
+ {
+ cultistComponent.HolyConvertToken?.Cancel();
+ cultistComponent.HolyConvertToken = null;
+ convert = true;
+ }
+
+ if (_body.TryGetBodyOrganEntityComps((args.Target.Value, body), out var stomachs))
+ {
+ var firstStomach = stomachs.FirstOrNull();
+
+ if (firstStomach == null)
+ return;
+
+ if (!_solutionContainer.TryGetSolution(firstStomach.Value.Owner, firstStomach.Value.Comp1.BodySolutionName, out var bodySolution))
+ return;
+
+ if (_stomachSystem.TryChangeReagent(firstStomach.Value.Owner, component.ConvertedId, component.ConvertedToId))
+ convert = true;
+
+ if (ConvertHolyWater(bodySolution.Value.Comp.Solution, component.ConvertedId, component.ConvertedToId))
+ convert = true;
+ }
+
+ if (!convert)
+ return;
+
+ _audio.PlayPvs(component.ConvertHolyWaterSound, args.Target.Value);
+ _popup.PopupEntity(Loc.GetString("holy-water-deconverted"), args.User, args.User);
+ }
+
+ private bool ConvertHolyWater(Solution solution, string fromReagentId, string toReagentId)
+ {
+ foreach (var reagent in solution.Contents)
+ {
+ if (reagent.Reagent.Prototype != fromReagentId)
+ continue;
+
+ var amount = reagent.Quantity;
+
+ solution.RemoveReagent(reagent.Reagent.Prototype, reagent.Quantity);
+ solution.AddReagent(toReagentId, amount);
+
+ return true;
+ }
+
+ return false;
+ }
+
+ private void OnMeleeHit(EntityUid uid, CultWeaponComponent component, MeleeHitEvent args)
+ {
+ if (HasComp(args.User))
+ return;
+
+ _stunSystem.TryParalyze(args.User, TimeSpan.FromSeconds(component.StuhTime), true);
+ _damageableSystem.TryChangeDamage(args.User, component.Damage, origin: uid);
+ }
+}
diff --git a/Content.Server/_Sunrise/BloodCult/Items/Systems/ReturnItemOnThrowSystem.cs b/Content.Server/_Sunrise/BloodCult/Items/Systems/ReturnItemOnThrowSystem.cs
new file mode 100644
index 00000000000..f2b53c7472b
--- /dev/null
+++ b/Content.Server/_Sunrise/BloodCult/Items/Systems/ReturnItemOnThrowSystem.cs
@@ -0,0 +1,42 @@
+using Content.Server._Sunrise.BloodCult.Items.Components;
+using Content.Server.Hands.Systems;
+using Content.Server.Stunnable;
+using Content.Shared._Sunrise.BloodCult.Components;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Throwing;
+
+namespace Content.Server._Sunrise.BloodCult.Items.Systems;
+
+public sealed class ReturnItemOnThrowSystem : EntitySystem
+{
+ [Dependency] private readonly StunSystem _stun = default!;
+ [Dependency] private readonly HandsSystem _hands = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnThrowHit);
+ }
+
+ private void OnThrowHit(EntityUid uid, ReturnItemOnThrowComponent component, ThrowDoHitEvent args)
+ {
+ var isCultist = HasComp(args.Target);
+ var thrower = args.Component.Thrower;
+ if (!HasComp(thrower))
+ return;
+
+ if (!HasComp(args.Target))
+ return;
+
+ if (!_stun.IsParalyzed(args.Target))
+ {
+ if (!isCultist)
+ {
+ _stun.TryParalyze(args.Target, TimeSpan.FromSeconds(component.StunTime), true);
+ }
+ }
+
+ _hands.PickupOrDrop(thrower, uid);
+ }
+}
diff --git a/Content.Server/_Sunrise/BloodCult/Items/Systems/ShuttleCurseSystem.cs b/Content.Server/_Sunrise/BloodCult/Items/Systems/ShuttleCurseSystem.cs
new file mode 100644
index 00000000000..82ae6eac492
--- /dev/null
+++ b/Content.Server/_Sunrise/BloodCult/Items/Systems/ShuttleCurseSystem.cs
@@ -0,0 +1,80 @@
+using Content.Server._Sunrise.BloodCult.Items.Components;
+using Content.Server.RoundEnd;
+using Content.Server.Shuttles.Systems;
+using Content.Shared._Sunrise.BloodCult.Components;
+using Content.Shared.GameTicking;
+using Content.Shared.Hands.EntitySystems;
+using Content.Shared.Interaction.Events;
+using Content.Shared.Popups;
+using Robust.Shared.Timing;
+
+namespace Content.Server._Sunrise.BloodCult.Items.Systems;
+
+public sealed class ShuttleCurseSystem : EntitySystem
+{
+ [Dependency] private readonly IEntityManager _entMan = default!;
+ [Dependency] private readonly SharedHandsSystem _hands = default!;
+ [Dependency] private readonly RoundEndSystem _roundEnd = default!;
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+
+ private const int MaxCurses = 3;
+ private int _currentCurses = 0;
+ private TimeSpan? _nextCurse = TimeSpan.Zero;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnUse);
+ SubscribeLocalEvent(OnRoundEnd);
+ }
+
+ private void OnRoundEnd(RoundEndedEvent ev)
+ {
+ _currentCurses = 0;
+ _nextCurse = TimeSpan.Zero;
+ }
+
+ private void OnUse(EntityUid uid, ShuttleCurseComponent component, UseInHandEvent args)
+ {
+ if (!HasComp(args.User))
+ {
+ _hands.TryDrop(args.User);
+ _popup.PopupEntity(Loc.GetString("shuttle-curse-not-cultist"), args.User, args.User);
+ return;
+ }
+
+ if (!_roundEnd.ShuttleCalled())
+ {
+ _popup.PopupEntity(Loc.GetString("shuttle-curse-shuttle-not-called"), args.User, args.User);
+ return;
+ }
+
+ if (_currentCurses >= MaxCurses)
+ {
+ _popup.PopupEntity(Loc.GetString("shuttle-curse-max-curses"), args.User, args.User);
+ return;
+ }
+
+ if (_nextCurse > _gameTiming.CurTime)
+ {
+ _popup.PopupEntity(Loc.GetString("shuttle-curse-cooldown"), args.User, args.User);
+ return;
+ }
+
+ var shuttle = _entMan.System();
+
+ if (shuttle.EmergencyShuttleArrived)
+ {
+ _popup.PopupEntity(Loc.GetString("shuttle-curse-shuttle-arrived"), args.User, args.User);
+ return;
+ }
+
+ _roundEnd.DelayCursedShuttle(component.DelayTime);
+ _popup.PopupEntity(Loc.GetString("shuttle-curse-shuttle-delayed"), args.User, args.User);
+
+ _currentCurses++;
+ _nextCurse = _gameTiming.CurTime + component.Cooldown;
+ }
+}
diff --git a/Content.Server/_Sunrise/BloodCult/Items/Systems/TorchCultistsProviderSystem.cs b/Content.Server/_Sunrise/BloodCult/Items/Systems/TorchCultistsProviderSystem.cs
new file mode 100644
index 00000000000..00105f39663
--- /dev/null
+++ b/Content.Server/_Sunrise/BloodCult/Items/Systems/TorchCultistsProviderSystem.cs
@@ -0,0 +1,265 @@
+using Content.Server._Sunrise.BloodCult.Items.Components;
+using Content.Server.Atmos.EntitySystems;
+using Content.Server.Popups;
+using Content.Server.Station.Components;
+using Content.Server.Station.Systems;
+using Content.Shared._Sunrise.BloodCult.Components;
+using Content.Shared._Sunrise.BloodCult.Items;
+using Content.Shared.Hands.EntitySystems;
+using Content.Shared.Interaction;
+using Content.Shared.Item;
+using Content.Shared.Mind.Components;
+using Content.Shared.Physics;
+using Robust.Server.GameObjects;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Physics;
+using Robust.Shared.Physics.Components;
+using Robust.Shared.Player;
+using Robust.Shared.Random;
+using Robust.Shared.Timing;
+
+namespace Content.Server._Sunrise.BloodCult.Items.Systems;
+
+public sealed class TorchCultistsProviderSystem : EntitySystem
+{
+ [Dependency] private readonly AtmosphereSystem _atmosphere = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly SharedHandsSystem _hands = default!;
+ [Dependency] private readonly PopupSystem _popup = default!;
+ [Dependency] private readonly SharedTransformSystem _xform = default!;
+ [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+ [Dependency] private readonly StationSystem _station = default!;
+ [Dependency] private readonly SharedPointLightSystem _pointLight = default!;
+ [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
+ [Dependency] private readonly UserInterfaceSystem _ui = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnInteract);
+ SubscribeLocalEvent(OnCultistSelected);
+
+ SubscribeLocalEvent(OnInit);
+ }
+
+ private void OnInit(EntityUid uid, TorchCultistsProviderComponent component, ComponentInit args)
+ {
+ UpdateAppearance(uid, component);
+ }
+
+ private void OnInteract(EntityUid uid, TorchCultistsProviderComponent comp, AfterInteractEvent args)
+ {
+ if (!args.Target.HasValue)
+ {
+ return;
+ }
+
+ if (!_interactionSystem.InRangeUnobstructed(args.User, args.Target.Value))
+ {
+ return;
+ }
+
+ if (!TryComp(uid, out var provider))
+ return;
+
+ if (!HasComp(args.User))
+ {
+ _hands.TryDrop(args.User);
+ _popup.PopupEntity(Loc.GetString("cult-torch-not-cultist"), args.User, args.User);
+ return;
+ }
+
+ if (!provider.Active || provider.UsesLeft <= 0)
+ {
+ _popup.PopupEntity(Loc.GetString("cult-torch-drained"), args.User, args.User);
+ return;
+ }
+
+ if (provider.NextUse > _timing.CurTime)
+ {
+ _popup.PopupEntity(Loc.GetString("cult-torch-cooldown"), args.User, args.User);
+ return;
+ }
+
+ if (HasComp(args.Target))
+ {
+ TeleportToRandomLocation(uid, args, comp);
+ return;
+ }
+
+ if (!HasComp(args.Target))
+ {
+ return;
+ }
+
+ provider.ItemSelected = args.Target;
+
+ var cultists = EntityQuery();
+ var list = new Dictionary();
+
+ foreach (var cultist in cultists)
+ {
+ if (!TryComp(cultist.Owner, out var meta))
+ return;
+
+ if (cultist.Owner == args.User)
+ continue;
+
+ list.Add(meta.Owner.ToString(), meta.EntityName);
+ }
+
+ if (list.Count == 0)
+ {
+ _popup.PopupEntity(Loc.GetString("cult-torch-cultists-not-found"), args.User, args.User);
+ return;
+ }
+
+ _ui.SetUiState(uid, comp.UserInterfaceKey, new TorchWindowBUIState(list));
+
+ if (!TryComp(args.User, out var actorComponent))
+ return;
+
+ _ui.TryToggleUi(uid, comp.UserInterfaceKey, actorComponent.PlayerSession);
+ }
+
+ private void OnCultistSelected(
+ EntityUid uid,
+ TorchCultistsProviderComponent component,
+ TorchWindowItemSelectedMessage args)
+ {
+ var entityUid = args.Actor;
+ var cultists = EntityQuery();
+
+ foreach (var cultist in cultists)
+ {
+ if (cultist.Owner.ToString() == args.EntUid)
+ entityUid = cultist.Owner;
+ }
+
+ if (entityUid == args.Actor && entityUid != null)
+ {
+ _popup.PopupEntity(Loc.GetString("cult-torch-no-cultist"), entityUid, entityUid);
+ return;
+ }
+
+ if (component.ItemSelected != null)
+ {
+ var item = component.ItemSelected.Value;
+
+ if (!TryComp(entityUid, out var xForm))
+ return;
+
+ _xform.SetCoordinates(item, xForm.Coordinates);
+ _hands.PickupOrDrop(entityUid, item);
+ }
+
+ UpdateUsesCount(uid, args.Actor, component);
+ }
+
+ private void UpdateAppearance(EntityUid uid, TorchCultistsProviderComponent component)
+ {
+ AppearanceComponent? appearance = null;
+ if (!Resolve(uid, ref appearance, false))
+ return;
+
+ _appearance.SetData(uid, VoidTorchVisuals.Activated, component.Active, appearance);
+ }
+
+ private void TeleportToRandomLocation(EntityUid torch, InteractEvent args, TorchCultistsProviderComponent component)
+ {
+ if (!args.Target.HasValue)
+ {
+ return;
+ }
+
+ var ownerTransform = Transform(args.User);
+
+ if (_station.GetStationInMap(ownerTransform.MapID) is not { } station ||
+ !TryComp(station, out var data) ||
+ _station.GetLargestGrid(data) is not { } grid)
+ {
+ if (ownerTransform.GridUid == null)
+ return;
+
+ grid = ownerTransform.GridUid.Value;
+ }
+
+ if (!TryComp(grid, out var gridComp))
+ {
+ return;
+ }
+
+ var gridTransform = Transform(grid);
+ var gridBounds = gridComp.LocalAABB.Scale(0.7f); // чтобы не заспавнить на самом краю станции
+
+ var targetCoords = gridTransform.Coordinates;
+
+ for (var i = 0; i < 25; i++)
+ {
+ var randomX = _random.Next((int) gridBounds.Left, (int) gridBounds.Right);
+ var randomY = _random.Next((int) gridBounds.Bottom, (int) gridBounds.Top);
+
+ var tile = new Vector2i(randomX, randomY);
+
+ // no air-blocked areas.
+ if (_atmosphere.IsTileSpace(grid, gridTransform.MapUid, tile) ||
+ _atmosphere.IsTileAirBlocked(grid, tile, mapGridComp: gridComp))
+ {
+ continue;
+ }
+
+ // don't spawn inside of solid objects
+ var physQuery = GetEntityQuery();
+ var valid = true;
+ foreach (var ent in gridComp.GetAnchoredEntities(tile))
+ {
+ if (!physQuery.TryGetComponent(ent, out var body))
+ continue;
+
+ if (body.BodyType != BodyType.Static ||
+ !body.Hard ||
+ (body.CollisionLayer & (int) CollisionGroup.LargeMobMask) == 0)
+ continue;
+
+ valid = false;
+ break;
+ }
+
+ if (!valid)
+ continue;
+
+ targetCoords = gridComp.GridTileToLocal(tile);
+ break;
+ }
+
+ _xform.SetCoordinates(args.User, targetCoords);
+ _xform.SetCoordinates(args.Target.Value, targetCoords);
+
+ UpdateUsesCount(torch, args.User, component);
+ }
+
+ private void UpdateUsesCount(EntityUid torch, EntityUid? user, TorchCultistsProviderComponent component)
+ {
+ component.ItemSelected = null;
+ component.NextUse = _timing.CurTime + component.Cooldown;
+ component.UsesLeft--;
+
+ if (user.HasValue)
+ {
+ _popup.PopupEntity(Loc.GetString("cult-torch-item-send"), user.Value);
+ }
+
+ if (component.UsesLeft <= 0)
+ {
+ component.Active = false;
+ UpdateAppearance(torch, component);
+
+ if (!TryComp(torch, out var light))
+ return;
+
+ _pointLight.SetEnabled(torch, false, light);
+ }
+ }
+}
diff --git a/Content.Server/_Sunrise/BloodCult/Items/Systems/VoidTeleportSystem.cs b/Content.Server/_Sunrise/BloodCult/Items/Systems/VoidTeleportSystem.cs
new file mode 100644
index 00000000000..4b2ab17318c
--- /dev/null
+++ b/Content.Server/_Sunrise/BloodCult/Items/Systems/VoidTeleportSystem.cs
@@ -0,0 +1,167 @@
+using System.Threading;
+using Content.Shared._Sunrise.BloodCult.Components;
+using Content.Shared._Sunrise.BloodCult.Items;
+using Content.Shared.Coordinates.Helpers;
+using Content.Shared.Hands.EntitySystems;
+using Content.Shared.Interaction.Events;
+using Content.Shared.Maps;
+using Content.Shared.Movement.Pulling.Components;
+using Content.Shared.Physics;
+using Content.Shared.Popups;
+using Robust.Server.GameObjects;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Map;
+using Robust.Shared.Timing;
+using Timer = Robust.Shared.Timing.Timer;
+
+namespace Content.Server._Sunrise.BloodCult.Items.Systems;
+
+public sealed class VoidTeleportSystem : EntitySystem
+{
+ [Dependency] private readonly SharedHandsSystem _hands = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+ [Dependency] private readonly TurfSystem _turf = default!;
+ [Dependency] private readonly SharedTransformSystem _xform = default!;
+ [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly IEntityManager _entMan = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnUseInHand);
+ SubscribeLocalEvent(OnInit);
+ }
+
+ private void OnInit(EntityUid uid, VoidTeleportComponent component, ComponentInit args)
+ {
+ UpdateAppearance(uid, component);
+ }
+
+ private void OnUseInHand(EntityUid uid, VoidTeleportComponent component, UseInHandEvent args)
+ {
+ if (!HasComp(args.User))
+ {
+ _hands.TryDrop(args.User);
+ _popup.PopupEntity(Loc.GetString("void-teleport-not-cultist"), args.User, args.User);
+ return;
+ }
+
+ if (!component.Active || component.UsesLeft <= 0)
+ {
+ _popup.PopupEntity(Loc.GetString("void-teleport-drained"), args.User, args.User);
+ return;
+ }
+
+ if (component.NextUse > _timing.CurTime)
+ {
+ _popup.PopupEntity(Loc.GetString("void-teleport-cooldown"), args.User, args.User);
+ return;
+ }
+
+ if (!TryComp(args.User, out var transform))
+ return;
+
+ var oldCoords = transform.Coordinates;
+
+ EntityCoordinates coords = default;
+ var attempts = 10;
+ //Repeat until proper place for tp is found
+ while (attempts <= 10)
+ {
+ attempts--;
+ //Get coords to where tp
+ var random = new Random().Next(component.MinRange, component.MaxRange);
+ var offset = transform.LocalRotation.ToWorldVec().Normalized();
+ var direction = transform.LocalRotation.GetDir().ToVec();
+ var newOffset = offset + direction * random;
+ coords = transform.Coordinates.Offset(newOffset).SnapToGrid(EntityManager);
+
+ var tile = coords.GetTileRef();
+
+ //Check for walls
+ if (tile != null && _turf.IsTileBlocked(tile.Value, CollisionGroup.AllMask))
+ continue;
+
+ break;
+ }
+
+ CreatePulse(uid, component);
+
+ _xform.SetCoordinates(args.User, coords);
+ transform.AttachToGridOrMap();
+
+ var pulled = GetPulledEntity(args.User);
+ if (pulled != null)
+ {
+ _xform.SetCoordinates(pulled.Value, coords);
+
+ if (TryComp(pulled.Value, out var pulledTransform))
+ pulledTransform.AttachToGridOrMap();
+ }
+
+ //Play tp sound
+ _audio.PlayPvs(component.TeleportInSound, coords);
+ _audio.PlayPvs(component.TeleportOutSound,oldCoords);
+
+ //Create tp effect
+ _entMan.SpawnEntity(component.TeleportInEffect, coords);
+ _entMan.SpawnEntity(component.TeleportOutEffect, oldCoords);
+
+ component.UsesLeft--;
+ component.NextUse = _timing.CurTime + component.Cooldown;
+ }
+
+ private void UpdateAppearance(EntityUid uid, VoidTeleportComponent comp)
+ {
+ AppearanceComponent? appearance = null;
+ if (!Resolve(uid, ref appearance, false))
+ return;
+
+ _appearance.SetData(uid, VeilVisuals.Activated, comp.Active, appearance);
+ }
+
+ private EntityUid? GetPulledEntity(EntityUid user)
+ {
+ EntityUid? pulled = null;
+
+ if (TryComp(user, out var puller))
+ pulled = puller.Pulling;
+
+ return pulled;
+ }
+
+ private void CreatePulse(EntityUid uid, VoidTeleportComponent component)
+ {
+ if (TryComp(uid, out var light))
+#pragma warning disable RA0002
+ light.Energy = 5f;
+#pragma warning restore RA0002
+
+ Timer.Spawn(component.TimerDelay, () => TurnOffPulse(uid, component), component.Token.Token);
+ }
+
+ private void TurnOffPulse(EntityUid uid ,VoidTeleportComponent comp)
+ {
+ if (!TryComp(uid, out var light))
+ return;
+
+#pragma warning disable RA0002
+ light.Energy = 1f;
+#pragma warning restore RA0002
+
+ comp.Token = new CancellationTokenSource();
+
+ if (comp.UsesLeft <= 0)
+ {
+ comp.Active = false;
+ UpdateAppearance(uid, comp);
+
+#pragma warning disable RA0002
+ light.Enabled = false;
+#pragma warning restore RA0002
+ }
+ }
+}
diff --git a/Content.Server/_Sunrise/BloodCult/Juggernaut/JaggernautComponent.cs b/Content.Server/_Sunrise/BloodCult/Juggernaut/JaggernautComponent.cs
new file mode 100644
index 00000000000..507028b3f9f
--- /dev/null
+++ b/Content.Server/_Sunrise/BloodCult/Juggernaut/JaggernautComponent.cs
@@ -0,0 +1,11 @@
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Server._Sunrise.BloodCult.Juggernaut;
+[RegisterComponent]
+public sealed partial class JuggernautComponent : Component
+{
+ [ViewVariables(VVAccess.ReadWrite),
+ DataField("hummerSpawnId", customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string HummerSpawnId = "HammerJuggernaut";
+}
diff --git a/Content.Server/_Sunrise/BloodCult/Juggernaut/JuggernautSystem.cs b/Content.Server/_Sunrise/BloodCult/Juggernaut/JuggernautSystem.cs
new file mode 100644
index 00000000000..e7a776cc527
--- /dev/null
+++ b/Content.Server/_Sunrise/BloodCult/Juggernaut/JuggernautSystem.cs
@@ -0,0 +1,21 @@
+using Content.Server.Hands.Systems;
+using Content.Shared.Body.Events;
+
+namespace Content.Server._Sunrise.BloodCult.Juggernaut;
+
+public sealed class JuggernautSystem : EntitySystem
+{
+ [Dependency] private readonly HandsSystem _handsSystem = default!;
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnBodyInit);
+ }
+
+ private void OnBodyInit(EntityUid uid, JuggernautComponent component, BodyInitEvent args)
+ {
+ var hammer = Spawn(component.HummerSpawnId, Transform(uid).Coordinates);
+ _handsSystem.TryForcePickupAnyHand(uid, hammer);
+ }
+}
diff --git a/Content.Server/_Sunrise/BloodCult/Objectives/Components/KillCultistTargetConditionComponent.cs b/Content.Server/_Sunrise/BloodCult/Objectives/Components/KillCultistTargetConditionComponent.cs
new file mode 100644
index 00000000000..599e1c7cbf5
--- /dev/null
+++ b/Content.Server/_Sunrise/BloodCult/Objectives/Components/KillCultistTargetConditionComponent.cs
@@ -0,0 +1,13 @@
+using Content.Server._Sunrise.BloodCult.Objectives.Systems;
+using Content.Server.Objectives.Systems;
+
+namespace Content.Server._Sunrise.BloodCult.Objectives.Components;
+
+[RegisterComponent, Access(typeof(KillCultistTargetsConditionSystem))]
+public sealed partial class KillCultistTargetsConditionComponent : Component
+{
+ [DataField(required: true), ViewVariables(VVAccess.ReadWrite)]
+ public string Title = string.Empty;
+
+ public List Targets = new();
+}
diff --git a/Content.Server/_Sunrise/BloodCult/Objectives/Systems/KillCultistTargetConditionSystem.cs b/Content.Server/_Sunrise/BloodCult/Objectives/Systems/KillCultistTargetConditionSystem.cs
new file mode 100644
index 00000000000..d98a095b39e
--- /dev/null
+++ b/Content.Server/_Sunrise/BloodCult/Objectives/Systems/KillCultistTargetConditionSystem.cs
@@ -0,0 +1,101 @@
+using System.Diagnostics;
+using System.Linq;
+using Content.Server._Sunrise.BloodCult.GameRule;
+using Content.Server._Sunrise.BloodCult.Objectives.Components;
+using Content.Shared.Mind;
+using Content.Shared.Objectives.Components;
+using Content.Shared.Roles.Jobs;
+
+namespace Content.Server._Sunrise.BloodCult.Objectives.Systems;
+
+public sealed class KillCultistTargetsConditionSystem : EntitySystem
+{
+ [Dependency] private readonly MetaDataSystem _metaData = default!;
+ [Dependency] private readonly SharedMindSystem _mind = default!;
+ [Dependency] private readonly SharedJobSystem _job = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnGetProgress);
+
+ SubscribeLocalEvent(OnPersonAssigned);
+ SubscribeLocalEvent(OnAfterAssign);
+ }
+
+ private void OnAfterAssign(EntityUid uid, KillCultistTargetsConditionComponent comp, ref ObjectiveAfterAssignEvent args)
+ {
+ _metaData.SetEntityName(uid, GetTitle(comp.Targets, comp.Title), args.Meta);
+ }
+
+ private string GetTitle(List targets, string title)
+ {
+ var targetsList = "";
+ foreach (var target in targets)
+ {
+ if (!TryComp(target, out var mind) || mind.CharacterName == null)
+ continue;
+
+ var targetName = mind.CharacterName;
+ var jobName = _job.MindTryGetJobName(target);
+ targetsList += Loc.GetString("objective-condition-cult-kill-target", ("targetName", targetName), ("job", jobName));
+ targetsList += "\n";
+ }
+
+ return Loc.GetString(title, ("targets", targetsList));
+ }
+
+ private void OnGetProgress(EntityUid uid, KillCultistTargetsConditionComponent comp, ref ObjectiveGetProgressEvent args)
+ {
+ args.Progress = KillCultistTargetsProgress(args.MindId);
+ }
+
+ private void OnPersonAssigned(EntityUid uid, KillCultistTargetsConditionComponent component, ref ObjectiveAssignedEvent args)
+ {
+ // target already assigned
+ if (component.Targets.Count != 0)
+ return;
+
+ var cultistRule = EntityManager.EntityQuery().FirstOrDefault();
+ Debug.Assert(cultistRule != null, nameof(cultistRule) + " != null");
+ var cultTargets = cultistRule.CultTargets;
+
+ component.Targets = cultTargets;
+ }
+
+ private bool GetTagretProgress(EntityUid target)
+ {
+ // deleted or gibbed or something, counts as dead
+ if (!TryComp(target, out var mind) || mind.OwnedEntity == null)
+ return true;
+
+ // dead is success
+ return _mind.IsCharacterDeadIc(mind);
+ }
+
+ private float KillCultistTargetsProgress(EntityUid? mindId)
+ {
+ var cultistRule = EntityManager.EntityQuery().FirstOrDefault();
+ Debug.Assert(cultistRule != null, nameof(cultistRule) + " != null");
+ var cultTargets = cultistRule.CultTargets;
+
+ var targetsCount = cultTargets.Count;
+
+ // prevent divide-by-zero
+ if (targetsCount == 0)
+ return 1f;
+
+ var deadTargetsCount = 0;
+
+ foreach (var cultTarget in cultTargets)
+ {
+ if (GetTagretProgress(cultTarget))
+ {
+ deadTargetsCount += 1;
+ }
+ }
+
+ return deadTargetsCount / (float) targetsCount;
+ }
+}
diff --git a/Content.Server/_Sunrise/BloodCult/PentagramComponent.cs b/Content.Server/_Sunrise/BloodCult/PentagramComponent.cs
new file mode 100644
index 00000000000..097807a973c
--- /dev/null
+++ b/Content.Server/_Sunrise/BloodCult/PentagramComponent.cs
@@ -0,0 +1,9 @@
+using Content.Shared._Sunrise.BloodCult.Pentagram;
+using Robust.Shared.GameStates;
+
+namespace Content.Server._Sunrise.BloodCult;
+
+[NetworkedComponent, RegisterComponent]
+public sealed partial class PentagramComponent : SharedPentagramComponent
+{
+}
diff --git a/Content.Server/_Sunrise/BloodCult/Pylon/PylonSystem.cs b/Content.Server/_Sunrise/BloodCult/Pylon/PylonSystem.cs
new file mode 100644
index 00000000000..d69770e7db3
--- /dev/null
+++ b/Content.Server/_Sunrise/BloodCult/Pylon/PylonSystem.cs
@@ -0,0 +1,283 @@
+using System.Linq;
+using System.Numerics;
+using Content.Server.Body.Components;
+using Content.Server.Body.Systems;
+using Content.Shared._Sunrise.BloodCult.Components;
+using Content.Shared._Sunrise.BloodCult.Pylon;
+using Content.Shared.Damage;
+using Content.Shared.Doors.Components;
+using Content.Shared.Interaction;
+using Content.Shared.Maps;
+using Content.Shared.Mobs.Systems;
+using Content.Shared.Physics;
+using Content.Shared.Popups;
+using Content.Shared.Tag;
+using Robust.Server.GameObjects;
+using Robust.Server.Player;
+using Robust.Shared.Audio;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Player;
+using Robust.Shared.Timing;
+
+namespace Content.Server._Sunrise.BloodCult.Pylon;
+
+public sealed class PylonSystem : EntitySystem
+{
+ [Dependency] private readonly DamageableSystem _damageSystem = default!;
+ [Dependency] private readonly IPlayerManager _playerManager = default!;
+ [Dependency] private readonly MobStateSystem _mobStateSystem = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
+ [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+ [Dependency] private readonly ITileDefinitionManager _tileDefinition = default!;
+ [Dependency] private readonly IEntityManager _entMan = default!;
+ [Dependency] private readonly TileSystem _tile = default!;
+ [Dependency] private readonly BloodstreamSystem _blood = default!;
+ [Dependency] private readonly TurfSystem _turf = default!;
+ [Dependency] private readonly EntityLookupSystem _lookup = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnInteract);
+ SubscribeLocalEvent(OnInit);
+ SubscribeLocalEvent(OnAnchorStateChanged);
+ }
+
+ private void OnAnchorStateChanged(EntityUid uid, SharedPylonComponent component, AnchorStateChangedEvent args)
+ {
+ if (args.Anchored)
+ return;
+
+ component.Activated = false;
+
+ UpdateAppearance(uid, component);
+
+ }
+
+ private void OnInit(EntityUid uid, SharedPylonComponent component, ComponentInit args)
+ {
+ UpdateAppearance(uid, component);
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ var pylonsQuery = EntityQuery();
+
+ foreach (var comp in pylonsQuery)
+ {
+ if (comp.NextTileConvert == TimeSpan.Zero)
+ comp.NextTileConvert = _timing.CurTime + TimeSpan.FromSeconds(comp.TileConvertCooldown);
+
+ if (comp.NextHealTime == TimeSpan.Zero)
+ comp.NextHealTime = _timing.CurTime + TimeSpan.FromSeconds(comp.HealingAuraCooldown);
+
+ if (_timing.CurTime >= comp.NextHealTime)
+ {
+ comp.NextHealTime = _timing.CurTime + TimeSpan.FromSeconds(comp.HealingAuraCooldown);
+
+ if (comp.Activated)
+ HealPlayersInRange(comp);
+ }
+
+ if (_timing.CurTime >= comp.NextTileConvert)
+ {
+ comp.NextTileConvert = _timing.CurTime + TimeSpan.FromSeconds(comp.TileConvertCooldown);
+
+ if (comp.Activated)
+ ConvertNearbyTiles(comp);
+ }
+ }
+ }
+
+ private void ConvertNearbyTiles(SharedPylonComponent comp)
+ {
+ var tilesConverted = 0;
+ var random = new Random().Next(1, 3);
+
+ var uid = comp.Owner;
+ var gridUid = Transform(uid).GridUid;
+ var pylonPos = Transform(uid).Coordinates;
+
+ if (!TryComp(gridUid, out var grid))
+ return;
+
+ var radius = comp.TileConvertRange;
+ var tilesRefs = grid.GetLocalTilesIntersecting(new Box2(pylonPos.Position + new Vector2(-radius, -radius),
+ pylonPos.Position + new Vector2(radius, radius)));
+ var tiles = ShuffleTiles(tilesRefs);
+
+ if (comp.ConvertEverything)
+ ConvertEverything(comp, tiles);
+
+ var cultTileDef = (ContentTileDefinition) _tileDefinition[$"{comp.TileId}"];
+ var cultTile = new Tile(cultTileDef.TileId);
+
+ foreach (var tile in tiles)
+ {
+ if (tilesConverted >= random)
+ return;
+
+ var tilePos = _turf.GetTileCenter(tile);
+
+ if (pylonPos.InRange(EntityManager, tilePos, comp.TileConvertRange))
+ {
+ if (tile.Tile.TypeId == cultTile.TypeId)
+ continue;
+
+ _tile.ReplaceTile(tile, cultTileDef);
+ _entMan.SpawnEntity(comp.TileConvertEffect, tilePos);
+ _audio.PlayPvs(comp.ConvertTileSound, tilePos, AudioParams.Default.WithVolume(-5));
+
+ tilesConverted++;
+ }
+ }
+ }
+
+ private void ConvertEverything(SharedPylonComponent comp, IEnumerable tiles)
+ {
+ foreach (var tile in tiles)
+ {
+ if (!_turf.IsTileBlocked(tile, CollisionGroup.WallLayer)
+ || !_turf.IsTileBlocked(tile, CollisionGroup.AirlockLayer))
+ continue;
+
+ var posss = _turf.GetTileCenter(tile);
+
+ foreach (var entity in _lookup.GetEntitiesIntersecting(posss))
+ {
+ if (TryComp(entity, out var tag)
+ && tag.Tags.Contains("Wall")
+ && MetaData(entity).EntityPrototype?.ID != comp.WallId)
+ {
+ _entMan.SpawnEntity(comp.WallId, Transform(entity).Coordinates);
+ _entMan.SpawnEntity(comp.WallConvertEffect, Transform(entity).Coordinates);
+ _entMan.DeleteEntity(entity);
+ _audio.PlayPvs(comp.ConvertTileSound, posss, AudioParams.Default.WithVolume(-10));
+ return;
+ }
+
+ if (HasComp(entity) && MetaData(entity).EntityPrototype?.ID != comp.AirlockId)
+ {
+ _entMan.SpawnEntity(comp.AirlockId, Transform(entity).Coordinates);
+ _entMan.SpawnEntity(comp.AirlockConvertEffect, Transform(entity).Coordinates);
+ _entMan.DeleteEntity(entity);
+ _audio.PlayPvs(comp.ConvertTileSound, posss, AudioParams.Default.WithVolume(-10));
+ return;
+ }
+ }
+ }
+ }
+
+ private void HealPlayersInRange(SharedPylonComponent comp)
+ {
+ foreach (var player in _playerManager.Sessions)
+ {
+ if (player.AttachedEntity is not { Valid: true } playerEntity)
+ continue;
+
+ if (!EntityManager.TryGetComponent(playerEntity, out _))
+ continue;
+
+ if (_mobStateSystem.IsDead(playerEntity))
+ continue;
+
+ var playerDamageComp = EntityManager.TryGetComponent(playerEntity, out var damageComp)
+ ? damageComp
+ : null;
+
+ if (playerDamageComp == null || playerDamageComp.Damage.GetTotal() == 0)
+ continue;
+
+ var uid = comp.Owner;
+ var pylonXForm = Transform(uid);
+ var playerXForm = Transform(playerEntity);
+
+ if (pylonXForm.Coordinates.InRange(EntityManager, playerXForm.Coordinates, comp.HealingAuraRange))
+ {
+ var damage = comp.HealingAuraDamage;
+ _damageSystem.TryChangeDamage(playerEntity, damage, true);
+
+ if (!TryComp(playerEntity, out var bloodstream))
+ continue;
+
+ if (bloodstream.BleedAmount > 1)
+ {
+ _blood.TryModifyBleedAmount(playerEntity, -comp.BleedReductionAmount, bloodstream);
+ }
+
+ if (_blood.GetBloodLevelPercentage(playerEntity, bloodstream) < bloodstream.BloodMaxVolume)
+ {
+ _blood.TryModifyBloodLevel(playerEntity, comp.BloodRefreshAmount, bloodstream);
+ }
+ }
+ }
+ }
+
+ private void OnInteract(EntityUid uid, SharedPylonComponent comp, InteractHandEvent args)
+ {
+ var user = args.User;
+ var pylon = args.Target;
+
+ if (!TryComp