diff --git a/Content.Client/Content.Client.csproj b/Content.Client/Content.Client.csproj
index d5fd1d341e9..70087b2f47d 100644
--- a/Content.Client/Content.Client.csproj
+++ b/Content.Client/Content.Client.csproj
@@ -27,4 +27,9 @@
+
+
+ MSBuild:Compile
+
+
diff --git a/Content.Client/_CorvaxNext/Heretic/HereticCombatMarkSystem.cs b/Content.Client/_CorvaxNext/Heretic/HereticCombatMarkSystem.cs
new file mode 100644
index 00000000000..65cf992e371
--- /dev/null
+++ b/Content.Client/_CorvaxNext/Heretic/HereticCombatMarkSystem.cs
@@ -0,0 +1,61 @@
+using Content.Shared.Heretic;
+using Robust.Client.GameObjects;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Heretic;
+
+public sealed partial class HereticCombatMarkSystem : EntitySystem
+{
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnStartup);
+ SubscribeLocalEvent(OnShutdown);
+ }
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ // i can't think of a better way to do this. everything else has failed
+ // god i hate client server i hate client server i hate client server i hate
+ foreach (var mark in EntityQuery())
+ {
+ if (!TryComp(mark.Owner, out var sprite))
+ continue;
+
+ if (!sprite.LayerMapTryGet(0, out var layer))
+ continue;
+
+ sprite.LayerSetState(layer, mark.Path.ToLower());
+ }
+ }
+
+ private void OnStartup(Entity ent, ref ComponentStartup args)
+ {
+ if (!TryComp(ent, out var sprite))
+ return;
+
+ if (sprite.LayerMapTryGet(0, out var l))
+ {
+ sprite.LayerSetState(l, ent.Comp.Path.ToLower());
+ return;
+ }
+
+ var rsi = new SpriteSpecifier.Rsi(new ResPath("_CorvaxNext/Heretic/combat_marks.rsi"), ent.Comp.Path.ToLower());
+ var layer = sprite.AddLayer(rsi);
+
+ sprite.LayerMapSet(0, layer);
+ sprite.LayerSetShader(layer, "unshaded");
+ }
+ private void OnShutdown(Entity ent, ref ComponentShutdown args)
+ {
+ if (!TryComp(ent, out var sprite))
+ return;
+
+ if (!sprite.LayerMapTryGet(0, out var layer))
+ return;
+
+ sprite.RemoveLayer(layer);
+ }
+}
diff --git a/Content.Client/_CorvaxNext/Heretic/HereticRitualRuneBoundUserInterface.cs b/Content.Client/_CorvaxNext/Heretic/HereticRitualRuneBoundUserInterface.cs
new file mode 100644
index 00000000000..1b29c1f9f48
--- /dev/null
+++ b/Content.Client/_CorvaxNext/Heretic/HereticRitualRuneBoundUserInterface.cs
@@ -0,0 +1,39 @@
+using Content.Client._CorvaxNext.Heretic.UI;
+using Content.Shared._CorvaxNext.Heretic.Components;
+using Content.Shared.Heretic.Prototypes;
+using Robust.Client.Graphics;
+using Robust.Client.Input;
+using Robust.Client.UserInterface;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client._CorvaxNext.Heretic;
+
+public sealed class HereticRitualRuneBoundUserInterface : BoundUserInterface
+{
+ [Dependency] private readonly IClyde _displayManager = default!;
+ [Dependency] private readonly IInputManager _inputManager = default!;
+
+ private HereticRitualRuneRadialMenu? _hereticRitualMenu;
+
+ public HereticRitualRuneBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ IoCManager.InjectDependencies(this);
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _hereticRitualMenu = this.CreateWindow();
+ _hereticRitualMenu.SetEntity(Owner);
+ _hereticRitualMenu.SendHereticRitualRuneMessageAction += SendHereticRitualMessage;
+
+ var vpSize = _displayManager.ScreenSize;
+ _hereticRitualMenu.OpenCenteredAt(_inputManager.MouseScreenPosition.Position / vpSize);
+ }
+
+ private void SendHereticRitualMessage(ProtoId protoId)
+ {
+ SendMessage(new HereticRitualMessage(protoId));
+ }
+}
diff --git a/Content.Client/_CorvaxNext/Heretic/Ritual.CustomBehaviors.cs b/Content.Client/_CorvaxNext/Heretic/Ritual.CustomBehaviors.cs
new file mode 100644
index 00000000000..4f73a31a1e2
--- /dev/null
+++ b/Content.Client/_CorvaxNext/Heretic/Ritual.CustomBehaviors.cs
@@ -0,0 +1,68 @@
+using Content.Shared.Heretic.Prototypes;
+
+namespace Content.Client.Heretic;
+
+// these do nothing and are there just for yaml limter to shut the fuck up.
+// make sure they stay up in sync with the server counterpart.
+// regards.
+// - john
+
+public sealed partial class RitualAshAscendBehavior : RitualSacrificeBehavior { }
+public sealed partial class RitualBladeAscendBehavior : RitualSacrificeBehavior { }
+public sealed partial class RitualMuteGhoulifyBehavior : RitualSacrificeBehavior { }
+
+[Virtual] public partial class RitualSacrificeBehavior : RitualCustomBehavior
+{
+ public override bool Execute(RitualData args, out string? outstr)
+ {
+ outstr = null;
+ return true;
+ }
+
+ public override void Finalize(RitualData args)
+ {
+ // do nothing
+ }
+}
+
+public sealed partial class RitualTemperatureBehavior : RitualCustomBehavior
+{
+ public override bool Execute(RitualData args, out string? outstr)
+ {
+ outstr = null;
+ return true;
+ }
+
+ public override void Finalize(RitualData args)
+ {
+ // do nothing
+ }
+}
+
+public sealed partial class RitualReagentPuddleBehavior : RitualCustomBehavior
+{
+ public override bool Execute(RitualData args, out string? outstr)
+ {
+ outstr = null;
+ return true;
+ }
+
+ public override void Finalize(RitualData args)
+ {
+ // do nothing
+ }
+}
+
+public sealed partial class RitualKnowledgeBehavior : RitualCustomBehavior
+{
+ public override bool Execute(RitualData args, out string? outstr)
+ {
+ outstr = null;
+ return true;
+ }
+
+ public override void Finalize(RitualData args)
+ {
+ // do nothing
+ }
+}
diff --git a/Content.Client/_CorvaxNext/Heretic/UI/HereticRitualRuneRadialMenu.xaml b/Content.Client/_CorvaxNext/Heretic/UI/HereticRitualRuneRadialMenu.xaml
new file mode 100644
index 00000000000..425ba588c12
--- /dev/null
+++ b/Content.Client/_CorvaxNext/Heretic/UI/HereticRitualRuneRadialMenu.xaml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
diff --git a/Content.Client/_CorvaxNext/Heretic/UI/HereticRitualRuneRadialMenu.xaml.cs b/Content.Client/_CorvaxNext/Heretic/UI/HereticRitualRuneRadialMenu.xaml.cs
new file mode 100644
index 00000000000..612fc971166
--- /dev/null
+++ b/Content.Client/_CorvaxNext/Heretic/UI/HereticRitualRuneRadialMenu.xaml.cs
@@ -0,0 +1,101 @@
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Heretic;
+using Content.Shared.Heretic.Prototypes;
+using Robust.Client.GameObjects;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+using System.Numerics;
+
+namespace Content.Client._CorvaxNext.Heretic.UI;
+
+public sealed partial class HereticRitualRuneRadialMenu : RadialMenu
+{
+ [Dependency] private readonly EntityManager _entityManager = default!;
+ [Dependency] private readonly IEntitySystemManager _entitySystem = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly ISharedPlayerManager _playerManager = default!;
+ private readonly SpriteSystem _spriteSystem;
+
+ public event Action>? SendHereticRitualRuneMessageAction;
+
+ public EntityUid Entity { get; set; }
+
+ public HereticRitualRuneRadialMenu()
+ {
+ IoCManager.InjectDependencies(this);
+ RobustXamlLoader.Load(this);
+ _spriteSystem = _entitySystem.GetEntitySystem();
+ }
+
+ public void SetEntity(EntityUid uid)
+ {
+ Entity = uid;
+ RefreshUI();
+ }
+
+ private void RefreshUI()
+ {
+ var main = FindControl("Main");
+ if (main == null)
+ return;
+
+ var player = _playerManager.LocalEntity;
+
+ if (!_entityManager.TryGetComponent(player, out var heretic))
+ return;
+
+ foreach (var ritual in heretic.KnownRituals)
+ {
+ if (!_prototypeManager.TryIndex(ritual, out var ritualPrototype))
+ continue;
+
+ var button = new HereticRitualMenuButton
+ {
+ StyleClasses = { "RadialMenuButton" },
+ SetSize = new Vector2(64, 64),
+ ToolTip = Loc.GetString(ritualPrototype.LocName),
+ ProtoId = ritualPrototype.ID
+ };
+
+ var texture = new TextureRect
+ {
+ VerticalAlignment = VAlignment.Center,
+ HorizontalAlignment = HAlignment.Center,
+ Texture = _spriteSystem.Frame0(ritualPrototype.Icon),
+ TextureScale = new Vector2(2f, 2f)
+ };
+
+ button.AddChild(texture);
+ main.AddChild(button);
+ }
+
+ AddHereticRitualMenuButtonOnClickAction(main);
+ }
+
+ private void AddHereticRitualMenuButtonOnClickAction(RadialContainer mainControl)
+ {
+ if (mainControl == null)
+ return;
+
+ foreach(var child in mainControl.Children)
+ {
+ var castChild = child as HereticRitualMenuButton;
+
+ if (castChild == null)
+ continue;
+
+ castChild.OnButtonUp += _ =>
+ {
+ SendHereticRitualRuneMessageAction?.Invoke(castChild.ProtoId);
+ Close();
+ };
+ }
+ }
+
+ public sealed class HereticRitualMenuButton : RadialMenuTextureButton
+ {
+ public ProtoId ProtoId { get; set; }
+ }
+}
diff --git a/Content.Client/_CorvaxNext/Heretic/UI/LivingHeartMenu.xaml b/Content.Client/_CorvaxNext/Heretic/UI/LivingHeartMenu.xaml
new file mode 100644
index 00000000000..fd06facd081
--- /dev/null
+++ b/Content.Client/_CorvaxNext/Heretic/UI/LivingHeartMenu.xaml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
diff --git a/Content.Client/_CorvaxNext/Heretic/UI/LivingHeartMenu.xaml.cs b/Content.Client/_CorvaxNext/Heretic/UI/LivingHeartMenu.xaml.cs
new file mode 100644
index 00000000000..ecfd3815dc3
--- /dev/null
+++ b/Content.Client/_CorvaxNext/Heretic/UI/LivingHeartMenu.xaml.cs
@@ -0,0 +1,97 @@
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Heretic;
+using Robust.Client.Player;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+using System.Numerics;
+
+namespace Content.Client._CorvaxNext.Heretic.UI;
+
+public sealed partial class LivingHeartMenu : RadialMenu
+{
+ [Dependency] private readonly EntityManager _ent = default!;
+ [Dependency] private readonly IPrototypeManager _prot = default!;
+ [Dependency] private readonly IPlayerManager _player = default!;
+
+ public EntityUid Entity { get; private set; }
+
+ public event Action? SendActivateMessageAction;
+
+ public LivingHeartMenu()
+ {
+ IoCManager.InjectDependencies(this);
+ RobustXamlLoader.Load(this);
+ }
+
+ public void SetEntity(EntityUid ent)
+ {
+ Entity = ent;
+ UpdateUI();
+ }
+
+ private void UpdateUI()
+ {
+ var main = FindControl("Main");
+ if (main == null) return;
+
+ var player = _player.LocalEntity;
+
+ if (!_ent.TryGetComponent(player, out var heretic))
+ return;
+
+ foreach (var target in heretic.SacrificeTargets)
+ {
+ if (target == null) continue;
+
+ var ent = _ent.GetEntity(target);
+ if (ent == null)
+ continue;
+
+ var button = new EmbeddedEntityMenuButton
+ {
+ StyleClasses = { "RadialMenuButton" },
+ SetSize = new Vector2(64, 64),
+ ToolTip = _ent.TryGetComponent(ent.Value, out var md) ? md.EntityName : "Unknown",
+ NetEntity = (NetEntity) target,
+ };
+
+ var texture = new SpriteView(ent.Value, _ent)
+ {
+ OverrideDirection = Direction.South,
+ VerticalAlignment = VAlignment.Center,
+ SetSize = new Vector2(64, 64),
+ VerticalExpand = true,
+ Stretch = SpriteView.StretchMode.Fill,
+ };
+ button.AddChild(texture);
+
+ main.AddChild(button);
+ }
+ AddAction(main);
+ }
+
+ private void AddAction(RadialContainer main)
+ {
+ if (main == null)
+ return;
+
+ foreach (var child in main.Children)
+ {
+ var castChild = child as EmbeddedEntityMenuButton;
+ if (castChild == null)
+ continue;
+
+ castChild.OnButtonUp += _ =>
+ {
+ SendActivateMessageAction?.Invoke(castChild.NetEntity);
+ Close();
+ };
+ }
+ }
+
+ public sealed class EmbeddedEntityMenuButton : RadialMenuTextureButton
+ {
+ public NetEntity NetEntity;
+ }
+}
diff --git a/Content.Client/_CorvaxNext/Heretic/UI/LivingHeartMenuBoundUserInterface.cs b/Content.Client/_CorvaxNext/Heretic/UI/LivingHeartMenuBoundUserInterface.cs
new file mode 100644
index 00000000000..cac7c8505b0
--- /dev/null
+++ b/Content.Client/_CorvaxNext/Heretic/UI/LivingHeartMenuBoundUserInterface.cs
@@ -0,0 +1,34 @@
+using Content.Shared.Heretic;
+using Robust.Client.Graphics;
+using Robust.Client.Input;
+using Robust.Client.UserInterface;
+
+namespace Content.Client._CorvaxNext.Heretic.UI;
+
+public sealed partial class LivingHeartMenuBoundUserInterface : BoundUserInterface
+{
+ [Dependency] private readonly IClyde _displayManager = default!;
+ [Dependency] private readonly IInputManager _inputManager = default!;
+
+ [NonSerialized] private LivingHeartMenu? _menu;
+
+ public LivingHeartMenuBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ IoCManager.InjectDependencies(this);
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _menu = this.CreateWindow();
+ _menu.SetEntity(Owner);
+ _menu.SendActivateMessageAction += SendMessage;
+ _menu.OpenCenteredAt(_inputManager.MouseScreenPosition.Position / _displayManager.ScreenSize);
+ }
+
+ private void SendMessage(NetEntity netent)
+ {
+ base.SendMessage(new EventHereticLivingHeartActivate() { Target = netent });
+ }
+}
diff --git a/Content.IntegrationTests/Tests/_Goobstation/Heretic/RitualKnowledgeTests.cs b/Content.IntegrationTests/Tests/_Goobstation/Heretic/RitualKnowledgeTests.cs
new file mode 100644
index 00000000000..43405b02119
--- /dev/null
+++ b/Content.IntegrationTests/Tests/_Goobstation/Heretic/RitualKnowledgeTests.cs
@@ -0,0 +1,81 @@
+using System.Collections.Generic;
+using System.Linq;
+using Content.Server.Heretic.Ritual;
+using Content.Shared.Dataset;
+using Content.Shared.Tag;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Prototypes;
+
+namespace Content.IntegrationTests.Tests._Goobstation.Heretic;
+
+[TestFixture, TestOf(typeof(RitualKnowledgeBehavior))]
+public sealed class RitualKnowledgeTests
+{
+ [Test]
+ public async Task ValidateEligibleTags()
+ {
+ // As far as I can tell, there's no annotation to validate
+ // a dataset of tag prototype IDs, so we'll have to do it
+ // in a test fixture. Sad.
+
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var entMan = server.ResolveDependency();
+ var protoMan = server.ResolveDependency();
+
+ await server.WaitAssertion(() =>
+ {
+ // Get the eligible tags prototype
+ var dataset = protoMan.Index(RitualKnowledgeBehavior.EligibleTagsDataset);
+
+ // Validate that every value is a valid tag
+ Assert.Multiple(() =>
+ {
+ foreach (var tagId in dataset.Values)
+ {
+ Assert.That(protoMan.TryIndex(tagId, out var tagProto), Is.True, $"\"{tagId}\" is not a valid tag prototype ID");
+ }
+ });
+ });
+
+ await pair.CleanReturnAsync();
+ }
+
+ [Test]
+ public async Task ValidateTagsHaveItems()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var entMan = server.ResolveDependency();
+ var protoMan = server.ResolveDependency();
+ var compFactory = server.ResolveDependency();
+
+ await server.WaitAssertion(() =>
+ {
+ // Get the eligible tags prototype
+ var dataset = protoMan.Index(RitualKnowledgeBehavior.EligibleTagsDataset).Values.ToHashSet();
+
+ // Loop through every entity prototype and assemble a used tags set
+ var usedTags = new HashSet();
+
+ // Ensure that every tag is used by a non-abstract entity
+ foreach (var entProto in protoMan.EnumeratePrototypes())
+ {
+ if (entProto.Abstract)
+ continue;
+
+ if (entProto.TryGetComponent(out var tags, compFactory))
+ {
+ usedTags.UnionWith(tags.Tags.Select(t => t.Id));
+ }
+ }
+
+ var unusedTags = dataset.Except(usedTags).ToHashSet();
+ Assert.That(unusedTags, Is.Empty, $"The following ritual item tags are not used by any obtainable entity prototypes: {string.Join(", ", unusedTags)}");
+ });
+
+ await pair.CleanReturnAsync();
+ }
+}
diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs
index f4978cd65c8..6f5cece0f43 100644
--- a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs
+++ b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs
@@ -173,5 +173,20 @@ private void AddAntagVerbs(GetVerbsEvent args)
};
args.Verbs.Add(api);
// Corvax-Next-Api-End
+
+ // goobstation - heretics
+ Verb heretic = new()
+ {
+ Text = Loc.GetString("admin-verb-make-heretic"),
+ Category = VerbCategory.Antag,
+ Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/_Goobstation/Heretic/Blades/blade_blade.rsi"), "icon"),
+ Act = () =>
+ {
+ _antag.ForceMakeAntag(targetPlayer, "Heretic");
+ },
+ Impact = LogImpact.High,
+ Message = Loc.GetString("admin-verb-make-heretic"),
+ };
+ args.Verbs.Add(heretic);
}
}
diff --git a/Content.Server/Antag/AntagSelectionSystem.API.cs b/Content.Server/Antag/AntagSelectionSystem.API.cs
index 23e77c21028..bbb38f01558 100644
--- a/Content.Server/Antag/AntagSelectionSystem.API.cs
+++ b/Content.Server/Antag/AntagSelectionSystem.API.cs
@@ -78,6 +78,20 @@ public int GetTotalPlayerCount(IList pool)
return count;
}
+ // goob edit
+ public List GetAliveConnectedPlayers(IList pool)
+ {
+ var l = new List();
+ foreach (var session in pool)
+ {
+ if (session.Status is SessionStatus.Disconnected or SessionStatus.Zombie)
+ continue;
+ l.Add(session);
+ }
+ return l;
+ }
+ // goob edit end
+
///
/// Gets the number of antagonists that should be present for a given antag definition based on the provided pool.
/// A null pool will simply use the player count.
diff --git a/Content.Server/Polymorph/Systems/PolymorphSystem.cs b/Content.Server/Polymorph/Systems/PolymorphSystem.cs
index c9a71c53584..4eb99ffe108 100644
--- a/Content.Server/Polymorph/Systems/PolymorphSystem.cs
+++ b/Content.Server/Polymorph/Systems/PolymorphSystem.cs
@@ -345,6 +345,9 @@ private void OnDestruction(Entity ent, ref Destructi
parent);
QueueDel(uid);
+ // goob edit
+ RaiseLocalEvent(parent, new PolymorphRevertEvent());
+
return parent;
}
@@ -391,3 +394,6 @@ public void RemovePolymorphAction(ProtoId id, Entity(OnRequestUpdate);
@@ -176,6 +182,16 @@ private void OnBuyRequest(EntityUid uid, StoreComponent component, StoreBuyListi
component.BalanceSpent[currency] += amount;
}
+ // goobstation - heretics
+ // i am too tired of making separate systems for knowledge adding
+ // and all that shit. i've had like 4 failed attempts
+ // so i'm just gonna shitcode my way out of my misery
+ if (listing.ProductHereticKnowledge != null)
+ {
+ if (TryComp(buyer, out var heretic))
+ _heretic.AddKnowledge(buyer, heretic, (ProtoId) listing.ProductHereticKnowledge);
+ }
+
//spawn entity
if (listing.ProductEntity != null)
{
diff --git a/Content.Server/_CorvaxNext/Actions/ActionsProviderComponent.cs b/Content.Server/_CorvaxNext/Actions/ActionsProviderComponent.cs
new file mode 100644
index 00000000000..ea9a367ec1a
--- /dev/null
+++ b/Content.Server/_CorvaxNext/Actions/ActionsProviderComponent.cs
@@ -0,0 +1,9 @@
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Actions;
+
+[RegisterComponent]
+public sealed partial class ActionsProviderComponent : Component
+{
+ [DataField] public List Actions = new();
+}
diff --git a/Content.Server/_CorvaxNext/Actions/ActionsProviderSystem.cs b/Content.Server/_CorvaxNext/Actions/ActionsProviderSystem.cs
new file mode 100644
index 00000000000..f328dad9e6b
--- /dev/null
+++ b/Content.Server/_CorvaxNext/Actions/ActionsProviderSystem.cs
@@ -0,0 +1,20 @@
+using Content.Shared.Actions;
+
+namespace Content.Server.Actions;
+
+public sealed partial class ActionsProviderSystem : EntitySystem
+{
+ [Dependency] private readonly SharedActionsSystem _actions = default!;
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnInit);
+ }
+
+ private void OnInit(Entity ent, ref ComponentInit args)
+ {
+ foreach (var action in ent.Comp.Actions)
+ _actions.AddAction(ent, action);
+ }
+}
diff --git a/Content.Server/_CorvaxNext/Clothing/MadnessMaskSystem.cs b/Content.Server/_CorvaxNext/Clothing/MadnessMaskSystem.cs
new file mode 100644
index 00000000000..2e6148dd56d
--- /dev/null
+++ b/Content.Server/_CorvaxNext/Clothing/MadnessMaskSystem.cs
@@ -0,0 +1,54 @@
+using Content.Server.EntityEffects.Effects;
+using Content.Shared.Clothing.Components;
+using Content.Shared.Damage.Components;
+using Content.Shared.Damage.Systems;
+using Content.Shared.Drugs;
+using Content.Shared.Drunk;
+using Content.Shared.Heretic;
+using Content.Shared.Jittering;
+using Content.Shared.StatusEffect;
+using Content.Shared.Stunnable;
+using Robust.Shared.Random;
+
+namespace Content.Server._Goobstation.Clothing;
+
+public sealed partial class MadnessMaskSystem : EntitySystem
+{
+ [Dependency] private readonly EntityLookupSystem _lookup = default!;
+ [Dependency] private readonly StaminaSystem _stamina = default!;
+ [Dependency] private readonly SharedJitteringSystem _jitter = default!;
+ [Dependency] private readonly StatusEffectsSystem _statusEffect = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ foreach (var mask in EntityQuery())
+ {
+ mask.UpdateAccumulator += frameTime;
+ if (mask.UpdateAccumulator < mask.UpdateTimer)
+ continue;
+
+ mask.UpdateAccumulator = 0;
+
+ var lookup = _lookup.GetEntitiesInRange(mask.Owner, 5f);
+ foreach (var look in lookup)
+ {
+ // heathens exclusive
+ if (HasComp(look)
+ || HasComp(look))
+ continue;
+
+ if (HasComp(look) && _random.Prob(.4f))
+ _stamina.TakeStaminaDamage(look, 5f, visual: false);
+
+ if (_random.Prob(.4f))
+ _jitter.DoJitter(look, TimeSpan.FromSeconds(.5f), true, amplitude: 5, frequency: 10);
+
+ if (_random.Prob(.25f))
+ _statusEffect.TryAddStatusEffect(look, "SeeingRainbows", TimeSpan.FromSeconds(10f), false);
+ }
+ }
+ }
+}
diff --git a/Content.Server/_CorvaxNext/GameTicking/Rules/Components/HereticRuleComponent.cs b/Content.Server/_CorvaxNext/GameTicking/Rules/Components/HereticRuleComponent.cs
new file mode 100644
index 00000000000..5cdfe4b6295
--- /dev/null
+++ b/Content.Server/_CorvaxNext/GameTicking/Rules/Components/HereticRuleComponent.cs
@@ -0,0 +1,20 @@
+using Content.Shared.Store;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.GameTicking.Rules.Components;
+
+[RegisterComponent, Access(typeof(HereticRuleSystem))]
+public sealed partial class HereticRuleComponent : Component
+{
+ public readonly List Minds = new();
+
+ public readonly List> StoreCategories = new()
+ {
+ "HereticPathAsh",
+ //"HereticPathLock",
+ "HereticPathFlesh",
+ "HereticPathBlade",
+ "HereticPathVoid",
+ "HereticPathSide"
+ };
+}
diff --git a/Content.Server/_CorvaxNext/GameTicking/Rules/HereticRuleSystem.cs b/Content.Server/_CorvaxNext/GameTicking/Rules/HereticRuleSystem.cs
new file mode 100644
index 00000000000..9acb38393d3
--- /dev/null
+++ b/Content.Server/_CorvaxNext/GameTicking/Rules/HereticRuleSystem.cs
@@ -0,0 +1,122 @@
+using Content.Server.Antag;
+using Content.Server.GameTicking.Rules.Components;
+using Content.Server._CorvaxNext.Objectives.Components;
+using Content.Server.Mind;
+using Content.Server.Objectives;
+using Content.Server.Objectives.Components;
+using Content.Server.Roles;
+using Content.Shared.Heretic;
+using Content.Shared.NPC.Prototypes;
+using Content.Shared.NPC.Systems;
+using Content.Shared.Roles;
+using Content.Shared.Store;
+using Content.Shared.Store.Components;
+using Robust.Shared.Audio;
+using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using System.Linq;
+using System.Text;
+
+namespace Content.Server.GameTicking.Rules;
+
+public sealed partial class HereticRuleSystem : GameRuleSystem
+{
+ [Dependency] private readonly MindSystem _mind = default!;
+ [Dependency] private readonly AntagSelectionSystem _antag = default!;
+ [Dependency] private readonly SharedRoleSystem _role = default!;
+ [Dependency] private readonly NpcFactionSystem _npcFaction = default!;
+ [Dependency] private readonly ObjectivesSystem _objective = default!;
+ [Dependency] private readonly IRobustRandom _rand = default!;
+
+ public readonly SoundSpecifier BriefingSound = new SoundPathSpecifier("/Audio/_CorvaxNext/Heretic/Ambience/Antag/Heretic/heretic_gain.ogg");
+
+ [ValidatePrototypeId] public readonly ProtoId HereticFactionId = "Heretic";
+
+ [ValidatePrototypeId] public readonly ProtoId NanotrasenFactionId = "NanoTrasen";
+
+ [ValidatePrototypeId] public readonly ProtoId Currency = "KnowledgePoint";
+
+ [ValidatePrototypeId] static EntProtoId mindRole = "MindRoleHeretic";
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnAntagSelect);
+ SubscribeLocalEvent(OnTextPrepend);
+ }
+
+ private void OnAntagSelect(Entity ent, ref AfterAntagEntitySelectedEvent args)
+ {
+ TryMakeHeretic(args.EntityUid, ent.Comp);
+
+ for (int i = 0; i < _rand.Next(6, 12); i++)
+ if (TryFindRandomTile(out var _, out var _, out var _, out var coords))
+ Spawn("EldritchInfluence", coords);
+ }
+
+ public bool TryMakeHeretic(EntityUid target, HereticRuleComponent rule)
+ {
+ if (!_mind.TryGetMind(target, out var mindId, out var mind))
+ return false;
+
+ _role.MindAddRole(mindId, mindRole.Id, mind, true);
+
+ // briefing
+ if (HasComp(target))
+ {
+ var briefingShort = Loc.GetString("heretic-role-greeting-short");
+
+ _antag.SendBriefing(target, Loc.GetString("heretic-role-greeting-fluff"), Color.MediumPurple, null);
+ _antag.SendBriefing(target, Loc.GetString("heretic-role-greeting"), Color.Red, BriefingSound);
+
+ if (_role.MindHasRole(mindId, out var mr))
+ AddComp(mr.Value, new RoleBriefingComponent { Briefing = briefingShort }, overwrite: true);
+ }
+ _npcFaction.RemoveFaction(target, NanotrasenFactionId, false);
+ _npcFaction.AddFaction(target, HereticFactionId);
+
+ EnsureComp(target);
+
+ // add store
+ var store = EnsureComp(target);
+ foreach (var category in rule.StoreCategories)
+ store.Categories.Add(category);
+ store.CurrencyWhitelist.Add(Currency);
+ store.Balance.Add(Currency, 2);
+
+ rule.Minds.Add(mindId);
+
+ return true;
+ }
+
+ public void OnTextPrepend(Entity ent, ref ObjectivesTextPrependEvent args)
+ {
+ var sb = new StringBuilder();
+
+ var mostKnowledge = 0f;
+ var mostKnowledgeName = string.Empty;
+
+ foreach (var heretic in EntityQuery())
+ {
+ if (!_mind.TryGetMind(heretic.Owner, out var mindId, out var mind))
+ continue;
+
+ var name = _objective.GetTitle((mindId, mind), Name(heretic.Owner));
+ if (_mind.TryGetObjectiveComp(mindId, out var objective, mind))
+ {
+ if (objective.Researched > mostKnowledge)
+ mostKnowledge = objective.Researched;
+ mostKnowledgeName = name;
+ }
+
+ var str = Loc.GetString($"roundend-prepend-heretic-ascension-{(heretic.Ascended ? "success" : "fail")}", ("name", name));
+ sb.AppendLine(str);
+ }
+
+ sb.AppendLine("\n" + Loc.GetString("roundend-prepend-heretic-knowledge-named", ("name", mostKnowledgeName), ("number", mostKnowledge)));
+
+ args.Text = sb.ToString();
+ }
+}
diff --git a/Content.Server/_CorvaxNext/Heretic/Abilities/HereticAbilitySystem.Ash.cs b/Content.Server/_CorvaxNext/Heretic/Abilities/HereticAbilitySystem.Ash.cs
new file mode 100644
index 00000000000..79b315130e3
--- /dev/null
+++ b/Content.Server/_CorvaxNext/Heretic/Abilities/HereticAbilitySystem.Ash.cs
@@ -0,0 +1,164 @@
+using Content.Server.Atmos.Components;
+using Content.Shared.Heretic;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Mobs;
+using Content.Shared.Damage;
+using Content.Shared.Atmos;
+using Content.Server.Polymorph.Systems;
+using Content.Server.Temperature.Components;
+using Content.Shared.Temperature.Components;
+using Content.Server.Body.Components;
+using Content.Shared.Armor;
+
+namespace Content.Server.Heretic.Abilities;
+
+public sealed partial class HereticAbilitySystem : EntitySystem
+{
+ private void SubscribeAsh()
+ {
+ SubscribeLocalEvent(OnJaunt);
+ SubscribeLocalEvent(OnJauntGhoul);
+ SubscribeLocalEvent(OnJauntEnd);
+
+ SubscribeLocalEvent(OnVolcano);
+ SubscribeLocalEvent(OnNWRebirth);
+ SubscribeLocalEvent(OnFlames);
+ SubscribeLocalEvent(OnCascade);
+
+ SubscribeLocalEvent(OnAscensionAsh);
+ }
+
+ private void OnJaunt(Entity ent, ref EventHereticAshenShift args)
+ {
+ if (TryUseAbility(ent, args) && TryDoJaunt(ent))
+ args.Handled = true;
+ }
+ private void OnJauntGhoul(Entity ent, ref EventHereticAshenShift args)
+ {
+ if (TryUseAbility(ent, args) && TryDoJaunt(ent))
+ args.Handled = true;
+ }
+ private bool TryDoJaunt(EntityUid ent)
+ {
+ Spawn("PolymorphAshJauntAnimation", Transform(ent).Coordinates);
+ var urist = _poly.PolymorphEntity(ent, "AshJaunt");
+ if (urist == null)
+ return false;
+ return true;
+ }
+ private void OnJauntEnd(Entity ent, ref PolymorphRevertEvent args)
+ {
+ Spawn("PolymorphAshJauntEndAnimation", Transform(ent).Coordinates);
+ }
+
+ private void OnVolcano(Entity ent, ref EventHereticVolcanoBlast args)
+ {
+ if (!TryUseAbility(ent, args))
+ return;
+
+ var ignoredTargets = new List();
+
+ // all ghouls are immune to heretic shittery
+ foreach (var e in EntityQuery())
+ ignoredTargets.Add(e.Owner);
+
+ // all heretics with the same path are also immune
+ foreach (var e in EntityQuery())
+ if (e.CurrentPath == ent.Comp.CurrentPath)
+ ignoredTargets.Add(e.Owner);
+
+ if (!_splitball.Spawn(ent, ignoredTargets))
+ return;
+
+ if (ent.Comp.Ascended) // will only work on ash path
+ _flammable.AdjustFireStacks(ent, 20f, ignite: true);
+
+ args.Handled = true;
+ }
+ private void OnNWRebirth(Entity ent, ref EventHereticNightwatcherRebirth args)
+ {
+ if (!TryUseAbility(ent, args))
+ return;
+
+ var power = ent.Comp.CurrentPath == "Ash" ? ent.Comp.PathStage : 2.5f;
+ var lookup = _lookup.GetEntitiesInRange(ent, power);
+
+ foreach (var look in lookup)
+ {
+ if ((TryComp(look, out var th) && th.CurrentPath == ent.Comp.CurrentPath)
+ || HasComp(look))
+ continue;
+
+ if (TryComp(look, out var flam))
+ {
+ if (flam.OnFire && TryComp(ent, out var dmgc))
+ {
+ // heals everything by base + power for each burning target
+ _stam.TryTakeStamina(ent, -(10 + power));
+ var dmgdict = dmgc.Damage.DamageDict;
+ foreach (var key in dmgdict.Keys)
+ dmgdict[key] -= 10f + power;
+
+ var dmgspec = new DamageSpecifier() { DamageDict = dmgdict };
+ _dmg.TryChangeDamage(ent, dmgspec, true, false, dmgc);
+ }
+
+ if (!flam.OnFire)
+ _flammable.AdjustFireStacks(look, power, flam, true);
+
+ if (TryComp(look, out var mobstat))
+ if (mobstat.CurrentState == MobState.Critical)
+ _mobstate.ChangeMobState(look, MobState.Dead, mobstat);
+ }
+ }
+
+ args.Handled = true;
+ }
+ private void OnFlames(Entity ent, ref EventHereticFlames args)
+ {
+ if (!TryUseAbility(ent, args))
+ return;
+
+ EnsureComp(ent);
+
+ if (ent.Comp.Ascended)
+ _flammable.AdjustFireStacks(ent, 20f, ignite: true);
+
+ args.Handled = true;
+ }
+ private void OnCascade(Entity ent, ref EventHereticCascade args)
+ {
+ if (!TryUseAbility(ent, args) || !Transform(ent).GridUid.HasValue)
+ return;
+
+ // yeah. it just generates a ton of plasma which just burns.
+ // lame, but we don't have anything fire related atm, so, it works.
+ var tilepos = _transform.GetGridOrMapTilePosition(ent, Transform(ent));
+ var enumerator = _atmos.GetAdjacentTileMixtures(Transform(ent).GridUid!.Value, tilepos, false, false);
+ while (enumerator.MoveNext(out var mix))
+ {
+ mix.AdjustMoles(Gas.Plasma, 50f);
+ mix.Temperature = Atmospherics.T0C + 125f;
+ }
+
+ if (ent.Comp.Ascended)
+ _flammable.AdjustFireStacks(ent, 20f, ignite: true);
+
+ args.Handled = true;
+ }
+
+
+ private void OnAscensionAsh(Entity ent, ref HereticAscensionAshEvent args)
+ {
+ RemComp(ent);
+ RemComp(ent);
+ RemComp(ent);
+ RemComp(ent);
+
+ // fire immunity
+ var flam = EnsureComp(ent);
+ flam.Damage = new(); // reset damage dict
+ // this does NOT protect you against lasers and whatnot. for now. when i figure out THIS STUPID FUCKING LIMB SYSTEM!!!
+ // regards.
+ }
+}
diff --git a/Content.Server/_CorvaxNext/Heretic/Abilities/HereticAbilitySystem.Blade.cs b/Content.Server/_CorvaxNext/Heretic/Abilities/HereticAbilitySystem.Blade.cs
new file mode 100644
index 00000000000..a93d6c89fb5
--- /dev/null
+++ b/Content.Server/_CorvaxNext/Heretic/Abilities/HereticAbilitySystem.Blade.cs
@@ -0,0 +1,78 @@
+using Content.Server.Heretic.Components.PathSpecific;
+using Content.Shared.Body.Part;
+using Content.Shared.Damage.Components;
+using Content.Shared.Heretic;
+using Content.Shared.Slippery;
+
+namespace Content.Server.Heretic.Abilities;
+
+public sealed partial class HereticAbilitySystem : EntitySystem
+{
+ private void SubscribeBlade()
+ {
+ SubscribeLocalEvent(OnDanceOfTheBrand);
+ SubscribeLocalEvent(OnRealignment);
+ SubscribeLocalEvent(OnChampionStance);
+ SubscribeLocalEvent(OnFuriousSteel);
+
+ SubscribeLocalEvent(OnAscensionBlade);
+ }
+
+ private void OnDanceOfTheBrand(Entity ent, ref HereticDanceOfTheBrandEvent args)
+ {
+ EnsureComp(ent);
+ }
+ private void OnRealignment(Entity ent, ref EventHereticRealignment args)
+ {
+ if (!TryUseAbility(ent, args))
+ return;
+
+ _statusEffect.TryRemoveStatusEffect(ent, "Stun");
+ _statusEffect.TryRemoveStatusEffect(ent, "KnockedDown");
+ _statusEffect.TryRemoveStatusEffect(ent, "ForcedSleep");
+ _statusEffect.TryRemoveStatusEffect(ent, "Drowsiness");
+
+ if (TryComp(ent, out var stam))
+ {
+ if (stam.StaminaDamage >= stam.CritThreshold)
+ {
+ _stam.ExitStamCrit(ent, stam);
+ }
+
+ stam.StaminaDamage = 0;
+ RemComp(ent);
+ Dirty(ent, stam);
+ }
+
+ _statusEffect.TryAddStatusEffect(ent, "Pacified", TimeSpan.FromSeconds(10f), true);
+
+ args.Handled = true;
+ }
+
+ private void OnChampionStance(Entity ent, ref HereticChampionStanceEvent args)
+ {
+ // remove limbloss
+ foreach (var part in _body.GetBodyChildren(ent))
+ part.Component.CanSever = false;
+
+ EnsureComp(ent);
+ }
+ private void OnFuriousSteel(Entity ent, ref EventHereticFuriousSteel args)
+ {
+ if (!TryUseAbility(ent, args))
+ return;
+
+ for (int i = 0; i < 3; i++)
+ _pblade.AddProtectiveBlade(ent);
+
+ args.Handled = true;
+ }
+
+ private void OnAscensionBlade(Entity ent, ref HereticAscensionBladeEvent args)
+ {
+ EnsureComp(ent); // epic gamer move
+ RemComp(ent); // no stun
+
+ EnsureComp(ent);
+ }
+}
diff --git a/Content.Server/_CorvaxNext/Heretic/Abilities/HereticAbilitySystem.Flesh.cs b/Content.Server/_CorvaxNext/Heretic/Abilities/HereticAbilitySystem.Flesh.cs
new file mode 100644
index 00000000000..c15b56981d3
--- /dev/null
+++ b/Content.Server/_CorvaxNext/Heretic/Abilities/HereticAbilitySystem.Flesh.cs
@@ -0,0 +1,110 @@
+using Content.Server.Body.Components;
+using Content.Shared.Body.Components;
+using Content.Shared.Body.Part;
+using Content.Shared.Damage;
+using Content.Shared.DoAfter;
+using Content.Shared.Heretic;
+using Content.Shared.Popups;
+using Robust.Shared.Audio;
+using Robust.Shared.Player;
+
+namespace Content.Server.Heretic.Abilities;
+
+public sealed partial class HereticAbilitySystem : EntitySystem
+{
+ private void SubscribeFlesh()
+ {
+ SubscribeLocalEvent(OnFleshSurgery);
+ SubscribeLocalEvent(OnFleshSurgeryDoAfter);
+ SubscribeLocalEvent(OnAscensionFlesh);
+ }
+
+ private void OnFleshSurgery(Entity ent, ref EventHereticFleshSurgery args)
+ {
+ if (!TryUseAbility(ent, args))
+ return;
+
+ if (HasComp(args.Target)
+ || (TryComp(args.Target, out var th) && th.CurrentPath == ent.Comp.CurrentPath))
+ {
+ var dargs = new DoAfterArgs(EntityManager, ent, 10f, new EventHereticFleshSurgeryDoAfter(args.Target), ent, args.Target)
+ {
+ BreakOnDamage = true,
+ BreakOnMove = true,
+ BreakOnHandChange = false,
+ BreakOnDropItem = false,
+ };
+ _doafter.TryStartDoAfter(dargs);
+ args.Handled = true;
+ return;
+ }
+
+ // remove a random organ
+ if (TryComp(args.Target, out var body))
+ {
+ _vomit.Vomit(args.Target, -1000, -1000); // You feel hollow!
+
+ switch (_random.Next(0, 2))
+ {
+ // remove stomach
+ case 0:
+ foreach (var entity in _body.GetBodyOrganEntityComps((args.Target, body)))
+ QueueDel(entity.Owner);
+
+ _popup.PopupEntity(Loc.GetString("admin-smite-stomach-removal-self"), args.Target,
+ args.Target, PopupType.LargeCaution);
+ break;
+
+ // remove random hand
+ case 1:
+ var baseXform = Transform(args.Target);
+ foreach (var part in _body.GetBodyChildrenOfType(args.Target, BodyPartType.Hand, body))
+ {
+ _transform.AttachToGridOrMap(part.Id);
+ break;
+ }
+ _popup.PopupEntity(Loc.GetString("admin-smite-remove-hands-self"), args.Target, args.Target, PopupType.LargeCaution);
+ _popup.PopupCoordinates(Loc.GetString("admin-smite-remove-hands-other", ("name", args.Target)), baseXform.Coordinates,
+ Filter.PvsExcept(args.Target), true, PopupType.Medium);
+ break;
+
+ // remove lungs
+ case 2:
+ foreach (var entity in _body.GetBodyOrganEntityComps((args.Target, body)))
+ QueueDel(entity.Owner);
+
+ _popup.PopupEntity(Loc.GetString("admin-smite-lung-removal-self"), args.Target,
+ args.Target, PopupType.LargeCaution);
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ args.Handled = true;
+ }
+ private void OnFleshSurgeryDoAfter(Entity ent, ref EventHereticFleshSurgeryDoAfter args)
+ {
+ if (args.Cancelled)
+ return;
+
+ if (args.Target == null) // shouldn't really happen. just in case
+ return;
+
+ if (!TryComp(args.Target, out var dmg))
+ return;
+
+ // heal teammates, mostly ghouls
+ _dmg.SetAllDamage((EntityUid) args.Target, dmg, 0);
+ args.Handled = true;
+ }
+ private void OnAscensionFlesh(Entity ent, ref HereticAscensionFleshEvent args)
+ {
+ var urist = _poly.PolymorphEntity(ent, "EldritchHorror");
+ if (urist == null)
+ return;
+
+ _aud.PlayPvs(new SoundPathSpecifier("/Audio/Animals/space_dragon_roar.ogg"), (EntityUid) urist, AudioParams.Default.AddVolume(2f));
+ }
+}
diff --git a/Content.Server/_CorvaxNext/Heretic/Abilities/HereticAbilitySystem.Lock.cs b/Content.Server/_CorvaxNext/Heretic/Abilities/HereticAbilitySystem.Lock.cs
new file mode 100644
index 00000000000..d0cbe24fce3
--- /dev/null
+++ b/Content.Server/_CorvaxNext/Heretic/Abilities/HereticAbilitySystem.Lock.cs
@@ -0,0 +1,29 @@
+using Content.Shared.Heretic;
+
+namespace Content.Server.Heretic.Abilities;
+
+public sealed partial class HereticAbilitySystem : EntitySystem
+{
+ private void SubscribeLock()
+ {
+ SubscribeLocalEvent(OnBulglarFinesse);
+ SubscribeLocalEvent(OnLastRefugee);
+ // add eldritch id here
+
+ SubscribeLocalEvent(OnAscensionLock);
+ }
+
+ private void OnBulglarFinesse(Entity ent, ref EventHereticBulglarFinesse args)
+ {
+
+ }
+ private void OnLastRefugee(Entity ent, ref EventHereticLastRefugee args)
+ {
+
+ }
+
+ private void OnAscensionLock(Entity ent, ref HereticAscensionLockEvent args)
+ {
+
+ }
+}
diff --git a/Content.Server/_CorvaxNext/Heretic/Abilities/HereticAbilitySystem.Void.cs b/Content.Server/_CorvaxNext/Heretic/Abilities/HereticAbilitySystem.Void.cs
new file mode 100644
index 00000000000..650713690ce
--- /dev/null
+++ b/Content.Server/_CorvaxNext/Heretic/Abilities/HereticAbilitySystem.Void.cs
@@ -0,0 +1,124 @@
+using Content.Server.Atmos.Components;
+using Content.Server.Body.Components;
+using Content.Server.Heretic.Components.PathSpecific;
+using Content.Server.Magic;
+using Content.Server.Temperature.Components;
+using Content.Shared.Damage;
+using Content.Shared.Damage.Prototypes;
+using Content.Shared.Heretic;
+using Content.Shared.Temperature.Components;
+using Robust.Shared.Audio;
+using Robust.Shared.Physics.Components;
+using System.Linq;
+
+namespace Content.Server.Heretic.Abilities;
+
+public sealed partial class HereticAbilitySystem : EntitySystem
+{
+ private void SubscribeVoid()
+ {
+ SubscribeLocalEvent(OnAristocratWay);
+ SubscribeLocalEvent(OnAscensionVoid);
+
+ SubscribeLocalEvent(OnVoidBlast);
+ SubscribeLocalEvent(OnVoidBlink);
+ SubscribeLocalEvent(OnVoidPull);
+ }
+
+ private void OnAristocratWay(Entity ent, ref HereticAristocratWayEvent args)
+ {
+ RemComp(ent);
+ RemComp(ent);
+ RemComp(ent);
+ }
+ private void OnAscensionVoid(Entity ent, ref HereticAscensionVoidEvent args)
+ {
+ RemComp(ent);
+ EnsureComp(ent);
+ }
+
+ private void OnVoidBlast(Entity ent, ref HereticVoidBlastEvent args)
+ {
+ if (!TryUseAbility(ent, args))
+ return;
+
+ var rod = Spawn("ImmovableVoidRod", Transform(ent).Coordinates);
+ if (TryComp(rod, out var vrod))
+ vrod.User = ent;
+
+ if (TryComp(rod, out PhysicsComponent? phys))
+ {
+ _phys.SetLinearDamping(rod, phys, 0f);
+ _phys.SetFriction(rod, phys, 0f);
+ _phys.SetBodyStatus(rod, phys, BodyStatus.InAir);
+
+ var xform = Transform(rod);
+ var vel = Transform(ent).WorldRotation.ToWorldVec() * 15f;
+
+ _phys.SetLinearVelocity(rod, vel, body: phys);
+ xform.LocalRotation = Transform(ent).LocalRotation;
+ }
+
+ args.Handled = true;
+ }
+
+ private void OnVoidBlink(Entity ent, ref HereticVoidBlinkEvent args)
+ {
+ if (!TryUseAbility(ent, args))
+ return;
+
+ var condition = ent.Comp.CurrentPath == "Void";
+
+ var power = condition ? 1.5f + ent.Comp.PathStage / 5f : 1.5f;
+
+ _aud.PlayPvs(new SoundPathSpecifier("/Audio/Effects/tesla_consume.ogg"), ent);
+
+ foreach (var pookie in GetNearbyPeople(ent, power))
+ _stun.TryKnockdown(pookie, TimeSpan.FromSeconds(power), true);
+
+ _transform.SetCoordinates(ent, args.Target);
+
+ // repeating for both sides
+ _aud.PlayPvs(new SoundPathSpecifier("/Audio/Effects/tesla_consume.ogg"), ent);
+
+ foreach (var pookie in GetNearbyPeople(ent, power))
+ _stun.TryKnockdown(pookie, TimeSpan.FromSeconds(power), true);
+
+ args.Handled = true;
+ }
+
+ private void OnVoidPull(Entity ent, ref HereticVoidPullEvent args)
+ {
+ if (!TryUseAbility(ent, args))
+ return;
+
+ var topPriority = GetNearbyPeople(ent, 1.5f);
+ var midPriority = GetNearbyPeople(ent, 2.5f);
+ var farPriority = GetNearbyPeople(ent, 5f);
+
+ var power = ent.Comp.CurrentPath == "Void" ? 10f + ent.Comp.PathStage * 2 : 10f;
+
+ // damage closest ones
+ foreach (var pookie in topPriority)
+ {
+ if (!TryComp(pookie, out var dmgComp))
+ continue;
+
+ // total damage + power divided by all damage types.
+ var damage = (dmgComp.TotalDamage + power) / _prot.EnumeratePrototypes().Count();
+
+ // apply gaming.
+ _dmg.SetAllDamage(pookie, dmgComp, damage);
+ }
+
+ // stun close-mid range
+ foreach (var pookie in midPriority)
+ _stun.TryKnockdown(pookie, TimeSpan.FromSeconds(2.5f), true);
+
+ // pull in farthest ones
+ foreach (var pookie in farPriority)
+ _throw.TryThrow(pookie, Transform(ent).Coordinates);
+
+ args.Handled = true;
+ }
+}
diff --git a/Content.Server/_CorvaxNext/Heretic/Abilities/HereticAbilitySystem.cs b/Content.Server/_CorvaxNext/Heretic/Abilities/HereticAbilitySystem.cs
new file mode 100644
index 00000000000..07dcdc9274e
--- /dev/null
+++ b/Content.Server/_CorvaxNext/Heretic/Abilities/HereticAbilitySystem.cs
@@ -0,0 +1,263 @@
+using Content.Server.Atmos.EntitySystems;
+using Content.Server.Chat.Systems;
+using Content.Server.DoAfter;
+using Content.Server.Flash;
+using Content.Server.Hands.Systems;
+using Content.Server.Magic;
+using Content.Server.Polymorph.Systems;
+using Content.Server.Popups;
+using Content.Server.Radio.Components;
+using Content.Server.Store.Systems;
+using Content.Shared.Actions;
+using Content.Shared.Damage;
+using Content.Shared.Damage.Systems;
+using Content.Shared.DoAfter;
+using Content.Shared.Heretic;
+using Content.Shared.Mind.Components;
+using Content.Shared.Mobs.Systems;
+using Content.Shared.Store.Components;
+using Robust.Shared.Audio.Systems;
+using Content.Shared.Popups;
+using Robust.Shared.Random;
+using Content.Shared.Body.Systems;
+using Content.Server.Medical;
+using Robust.Server.GameObjects;
+using Content.Shared.Stunnable;
+using Robust.Shared.Map;
+using Content.Shared.StatusEffect;
+using Content.Shared.Throwing;
+using Content.Server.Station.Systems;
+using Content.Shared.Localizations;
+using Robust.Shared.Audio;
+using Content.Shared.Mobs.Components;
+using Robust.Shared.Prototypes;
+using Content.Server.Heretic.EntitySystems;
+
+namespace Content.Server.Heretic.Abilities;
+
+public sealed partial class HereticAbilitySystem : EntitySystem
+{
+ // keeping track of all systems in a single file
+ [Dependency] private readonly StoreSystem _store = default!;
+ [Dependency] private readonly PopupSystem _popup = default!;
+ [Dependency] private readonly HandsSystem _hands = default!;
+ [Dependency] private readonly ChatSystem _chat = default!;
+ [Dependency] private readonly PolymorphSystem _poly = default!;
+ [Dependency] private readonly ChainFireballSystem _splitball = default!;
+ [Dependency] private readonly EntityLookupSystem _lookup = default!;
+ [Dependency] private readonly MobStateSystem _mobstate = default!;
+ [Dependency] private readonly FlammableSystem _flammable = default!;
+ [Dependency] private readonly DamageableSystem _dmg = default!;
+ [Dependency] private readonly StaminaSystem _stam = default!;
+ [Dependency] private readonly AtmosphereSystem _atmos = default!;
+ [Dependency] private readonly SharedAudioSystem _aud = default!;
+ [Dependency] private readonly DoAfterSystem _doafter = default!;
+ [Dependency] private readonly FlashSystem _flash = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly SharedTransformSystem _transform = default!;
+ [Dependency] private readonly SharedBodySystem _body = default!;
+ [Dependency] private readonly VomitSystem _vomit = default!;
+ [Dependency] private readonly PhysicsSystem _phys = default!;
+ [Dependency] private readonly SharedStunSystem _stun = default!;
+ [Dependency] private readonly ThrowingSystem _throw = default!;
+ [Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
+ [Dependency] private readonly StationSystem _station = default!;
+ [Dependency] private readonly IMapManager _mapMan = default!;
+ [Dependency] private readonly IPrototypeManager _prot = default!;
+ [Dependency] private readonly ProtectiveBladeSystem _pblade = default!;
+ [Dependency] private readonly StatusEffectsSystem _statusEffect = default!;
+
+ private List GetNearbyPeople(Entity ent, float range)
+ {
+ var list = new List();
+ var lookup = _lookup.GetEntitiesInRange(Transform(ent).Coordinates, range);
+
+ foreach (var look in lookup)
+ {
+ // ignore heretics with the same path*, affect everyone else
+ if ((TryComp(look, out var th) && th.CurrentPath == ent.Comp.CurrentPath)
+ || HasComp(look))
+ continue;
+
+ if (!HasComp(look))
+ continue;
+
+ list.Add(look);
+ }
+ return list;
+ }
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnStore);
+ SubscribeLocalEvent(OnMansusGrasp);
+
+ SubscribeLocalEvent(OnLivingHeart);
+ SubscribeLocalEvent(OnLivingHeartActivate);
+
+ SubscribeLocalEvent(OnMansusLink);
+ SubscribeLocalEvent(OnMansusLinkDoafter);
+
+ SubscribeAsh();
+ SubscribeFlesh();
+ SubscribeVoid();
+ SubscribeBlade();
+ SubscribeLock();
+ }
+
+ private bool TryUseAbility(EntityUid ent, BaseActionEvent args)
+ {
+ if (args.Handled)
+ return false;
+
+ if (!TryComp(args.Action, out var actionComp))
+ return false;
+
+ // check if any magic items are worn
+ if (TryComp(ent, out var hereticComp) && actionComp.RequireMagicItem && !hereticComp.Ascended)
+ {
+ var ev = new CheckMagicItemEvent();
+ RaiseLocalEvent(ent, ev);
+
+ if (!ev.Handled)
+ {
+ _popup.PopupEntity(Loc.GetString("heretic-ability-fail-magicitem"), ent, ent);
+ return false;
+ }
+ }
+
+ // shout the spell out
+ if (!string.IsNullOrWhiteSpace(actionComp.MessageLoc))
+ _chat.TrySendInGameICMessage(ent, Loc.GetString(actionComp.MessageLoc!), InGameICChatType.Speak, false);
+
+ return true;
+ }
+ private void OnStore(Entity ent, ref EventHereticOpenStore args)
+ {
+ if (!TryComp(ent, out var store))
+ return;
+
+ _store.ToggleUi(ent, ent, store);
+ }
+ private void OnMansusGrasp(Entity ent, ref EventHereticMansusGrasp args)
+ {
+ if (!TryUseAbility(ent, args))
+ return;
+
+ if (ent.Comp.MansusGraspActive)
+ {
+ _popup.PopupEntity(Loc.GetString("heretic-ability-fail"), ent, ent);
+ return;
+ }
+
+ var st = Spawn("TouchSpellMansus", Transform(ent).Coordinates);
+
+ if (!_hands.TryForcePickupAnyHand(ent, st))
+ {
+ _popup.PopupEntity(Loc.GetString("heretic-ability-fail"), ent, ent);
+ QueueDel(st);
+ return;
+ }
+
+ ent.Comp.MansusGraspActive = true;
+ args.Handled = true;
+ }
+ private void OnLivingHeart(Entity ent, ref EventHereticLivingHeart args)
+ {
+ if (!TryUseAbility(ent, args))
+ return;
+
+ if (!TryComp(ent, out var uic))
+ return;
+
+ if (ent.Comp.SacrificeTargets.Count == 0)
+ {
+ _popup.PopupEntity(Loc.GetString("heretic-livingheart-notargets"), ent, ent);
+ args.Handled = true;
+ return;
+ }
+
+ _ui.OpenUi((ent, uic), HereticLivingHeartKey.Key, ent);
+ args.Handled = true;
+ }
+ private void OnLivingHeartActivate(Entity ent, ref EventHereticLivingHeartActivate args)
+ {
+ var loc = string.Empty;
+
+ var target = GetEntity(args.Target);
+ if (target == null)
+ return;
+
+ if (!TryComp(target, out var mobstate))
+ return;
+ var state = mobstate.CurrentState;
+
+ var xquery = GetEntityQuery();
+ var targetStation = _station.GetOwningStation(target);
+ var ownStation = _station.GetOwningStation(ent);
+
+ var isOnStation = targetStation != null && targetStation == ownStation;
+
+ var ang = Angle.Zero;
+ if (_mapMan.TryFindGridAt(_transform.GetMapCoordinates(Transform(ent)), out var grid, out var _))
+ ang = Transform(grid).LocalRotation;
+
+ var vector = _transform.GetWorldPosition((EntityUid) target, xquery) - _transform.GetWorldPosition(ent, xquery);
+ var direction = (vector.ToWorldAngle() - ang).GetDir();
+
+ var locdir = ContentLocalizationManager.FormatDirection(direction).ToLower();
+ var locstate = state.ToString().ToLower();
+
+ if (isOnStation)
+ loc = Loc.GetString("heretic-livingheart-onstation", ("state", locstate), ("direction", locdir));
+ else loc = Loc.GetString("heretic-livingheart-offstation", ("state", locstate), ("direction", locdir));
+
+ _popup.PopupEntity(loc, ent, ent, PopupType.Medium);
+ _aud.PlayPvs(new SoundPathSpecifier("/Audio/_CorvaxNext/Heretic/heartbeat.ogg"), ent, AudioParams.Default.WithVolume(-3f));
+ }
+
+ private void OnMansusLink(Entity ent, ref EventHereticMansusLink args)
+ {
+ if (!TryUseAbility(ent, args))
+ return;
+
+ if (!HasComp(args.Target))
+ {
+ _popup.PopupEntity(Loc.GetString("heretic-manselink-fail-nomind"), ent, ent);
+ return;
+ }
+
+ if (TryComp(args.Target, out var radio)
+ && radio.Channels.Contains("Mansus"))
+ {
+ _popup.PopupEntity(Loc.GetString("heretic-manselink-fail-exists"), ent, ent);
+ return;
+ }
+
+ var dargs = new DoAfterArgs(EntityManager, ent, 5f, new HereticMansusLinkDoAfter(args.Target), ent, args.Target)
+ {
+ BreakOnDamage = true,
+ BreakOnMove = true,
+ BreakOnWeightlessMove = true
+ };
+ _popup.PopupEntity(Loc.GetString("heretic-manselink-start"), ent, ent);
+ _popup.PopupEntity(Loc.GetString("heretic-manselink-start-target"), args.Target, args.Target, PopupType.MediumCaution);
+ _doafter.TryStartDoAfter(dargs);
+ }
+ private void OnMansusLinkDoafter(Entity ent, ref HereticMansusLinkDoAfter args)
+ {
+ if (args.Cancelled)
+ return;
+
+ var reciever = EnsureComp(args.Target);
+ var transmitter = EnsureComp(args.Target);
+ var radio = EnsureComp(args.Target);
+ radio.Channels = new() { "Mansus" };
+ transmitter.Channels = new() { "Mansus" };
+
+ // this "* 1000f" (divided by 1000 in FlashSystem) is gonna age like fine wine :clueless:
+ _flash.Flash(args.Target, null, null, 2f * 1000f, 0f, false, true, stunDuration: TimeSpan.FromSeconds(1f));
+ }
+}
diff --git a/Content.Server/_CorvaxNext/Heretic/Abilities/HereticFlames.cs b/Content.Server/_CorvaxNext/Heretic/Abilities/HereticFlames.cs
new file mode 100644
index 00000000000..7fa809f112f
--- /dev/null
+++ b/Content.Server/_CorvaxNext/Heretic/Abilities/HereticFlames.cs
@@ -0,0 +1,44 @@
+using Content.Server.Atmos.EntitySystems;
+using Content.Shared.Atmos;
+
+namespace Content.Server.Heretic.Abilities;
+
+[RegisterComponent]
+public sealed partial class HereticFlamesComponent : Component
+{
+ public float Timer = 0f;
+ public float TimerSeconds = 0f;
+ public float UpdateDuration = .2f;
+ [DataField] public float Duration = 60f;
+}
+
+public sealed partial class HereticFlamesSystem : EntitySystem
+{
+ [Dependency] private readonly AtmosphereSystem _atmos = default!;
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ foreach (var hfc in EntityQuery())
+ {
+ hfc.Timer += frameTime;
+ if (hfc.Timer < hfc.UpdateDuration)
+ continue;
+
+ hfc.Timer = 0f;
+ hfc.TimerSeconds += 1f;
+
+ if (hfc.TimerSeconds >= hfc.Duration)
+ RemComp(hfc.Owner, hfc);
+
+ var gasmix = _atmos.GetTileMixture((hfc.Owner, Transform(hfc.Owner)));
+
+ if (gasmix == null)
+ continue;
+
+ gasmix.AdjustMoles(Gas.Plasma, 2f);
+ gasmix.Temperature = Atmospherics.T0C + 125f;
+ }
+ }
+}
diff --git a/Content.Server/_CorvaxNext/Heretic/Components/EldritchInfluenceComponent.cs b/Content.Server/_CorvaxNext/Heretic/Components/EldritchInfluenceComponent.cs
new file mode 100644
index 00000000000..bf425c9d719
--- /dev/null
+++ b/Content.Server/_CorvaxNext/Heretic/Components/EldritchInfluenceComponent.cs
@@ -0,0 +1,12 @@
+using Content.Server.Heretic.EntitySystems;
+
+namespace Content.Server.Heretic.Components;
+
+[RegisterComponent, Access(typeof(EldritchInfluenceSystem))]
+public sealed partial class EldritchInfluenceComponent : Component
+{
+ [DataField] public bool Spent = false;
+
+ // make sure to update it with the prototype !!!
+ [NonSerialized] public static int LayerMask = 69; // 69 idk why not lolol
+}
diff --git a/Content.Server/_CorvaxNext/Heretic/Components/HereticBladeComponent.cs b/Content.Server/_CorvaxNext/Heretic/Components/HereticBladeComponent.cs
new file mode 100644
index 00000000000..a368da707b9
--- /dev/null
+++ b/Content.Server/_CorvaxNext/Heretic/Components/HereticBladeComponent.cs
@@ -0,0 +1,7 @@
+namespace Content.Server.Heretic.Components;
+
+[RegisterComponent]
+public sealed partial class HereticBladeComponent : Component
+{
+ [DataField] public string? Path;
+}
diff --git a/Content.Server/_CorvaxNext/Heretic/Components/MansusGraspComponent.cs b/Content.Server/_CorvaxNext/Heretic/Components/MansusGraspComponent.cs
new file mode 100644
index 00000000000..d7ae8b71d97
--- /dev/null
+++ b/Content.Server/_CorvaxNext/Heretic/Components/MansusGraspComponent.cs
@@ -0,0 +1,9 @@
+using Content.Server.Heretic.EntitySystems;
+
+namespace Content.Server.Heretic.Components;
+
+[RegisterComponent, Access(typeof(MansusGraspSystem))]
+public sealed partial class MansusGraspComponent : Component
+{
+ [DataField] public string? Path = null;
+}
diff --git a/Content.Server/_CorvaxNext/Heretic/Components/MansusInfusedComponent.cs b/Content.Server/_CorvaxNext/Heretic/Components/MansusInfusedComponent.cs
new file mode 100644
index 00000000000..f741164afcd
--- /dev/null
+++ b/Content.Server/_CorvaxNext/Heretic/Components/MansusInfusedComponent.cs
@@ -0,0 +1,8 @@
+namespace Content.Server.Heretic.Components;
+
+[RegisterComponent]
+public sealed partial class MansusInfusedComponent : Component
+{
+ [DataField] public float MaxCharges = 5f;
+ [ViewVariables(VVAccess.ReadWrite)] public float AvailableCharges = 5f;
+}
diff --git a/Content.Server/_CorvaxNext/Heretic/Components/PathSpecific/AristocratComponent.cs b/Content.Server/_CorvaxNext/Heretic/Components/PathSpecific/AristocratComponent.cs
new file mode 100644
index 00000000000..f91992e124e
--- /dev/null
+++ b/Content.Server/_CorvaxNext/Heretic/Components/PathSpecific/AristocratComponent.cs
@@ -0,0 +1,9 @@
+namespace Content.Server.Heretic.Components.PathSpecific;
+
+[RegisterComponent]
+public sealed partial class AristocratComponent : Component
+{
+ public float UpdateTimer = 0f;
+ [DataField] public float UpdateDelay = 1.5f;
+ [DataField] public float Range = 2.5f;
+}
diff --git a/Content.Server/_CorvaxNext/Heretic/Components/PathSpecific/ChampionStanceComponent.cs b/Content.Server/_CorvaxNext/Heretic/Components/PathSpecific/ChampionStanceComponent.cs
new file mode 100644
index 00000000000..36f10c6c26f
--- /dev/null
+++ b/Content.Server/_CorvaxNext/Heretic/Components/PathSpecific/ChampionStanceComponent.cs
@@ -0,0 +1,7 @@
+namespace Content.Server.Heretic.Components.PathSpecific;
+
+[RegisterComponent]
+public sealed partial class ChampionStanceComponent : Component
+{
+
+}
diff --git a/Content.Server/_CorvaxNext/Heretic/Components/PathSpecific/RiposteeComponent.cs b/Content.Server/_CorvaxNext/Heretic/Components/PathSpecific/RiposteeComponent.cs
new file mode 100644
index 00000000000..93d75b49dcc
--- /dev/null
+++ b/Content.Server/_CorvaxNext/Heretic/Components/PathSpecific/RiposteeComponent.cs
@@ -0,0 +1,10 @@
+namespace Content.Server.Heretic.Components.PathSpecific;
+
+[RegisterComponent]
+public sealed partial class RiposteeComponent : Component
+{
+ [DataField] public float Cooldown = 20f;
+ [ViewVariables(VVAccess.ReadWrite)] public float Timer = 20f;
+
+ [DataField] public bool CanRiposte = true;
+}
diff --git a/Content.Server/_CorvaxNext/Heretic/Components/PathSpecific/SilverMaelstromComponent.cs b/Content.Server/_CorvaxNext/Heretic/Components/PathSpecific/SilverMaelstromComponent.cs
new file mode 100644
index 00000000000..409dd93ee0c
--- /dev/null
+++ b/Content.Server/_CorvaxNext/Heretic/Components/PathSpecific/SilverMaelstromComponent.cs
@@ -0,0 +1,11 @@
+namespace Content.Server.Heretic.Components.PathSpecific;
+
+[RegisterComponent]
+public sealed partial class SilverMaelstromComponent : Component
+{
+ [DataField] public float RespawnCooldown = 5f;
+ [ViewVariables(VVAccess.ReadWrite)] public float RespawnTimer = 0f;
+
+ [ViewVariables(VVAccess.ReadOnly)] public int ActiveBlades = 0;
+ [DataField] public int MaxBlades = 10;
+}
diff --git a/Content.Server/_CorvaxNext/Heretic/Components/ProtectiveBladeComponent.cs b/Content.Server/_CorvaxNext/Heretic/Components/ProtectiveBladeComponent.cs
new file mode 100644
index 00000000000..61fbce4e494
--- /dev/null
+++ b/Content.Server/_CorvaxNext/Heretic/Components/ProtectiveBladeComponent.cs
@@ -0,0 +1,11 @@
+namespace Content.Server.Heretic.Components;
+
+///
+/// Indicates that an entity can act as a protective blade.
+///
+[RegisterComponent]
+public sealed partial class ProtectiveBladeComponent : Component
+{
+ [DataField] public float Lifetime = 60f;
+ [ViewVariables(VVAccess.ReadWrite)] public float Timer = 60f;
+}
diff --git a/Content.Server/_CorvaxNext/Heretic/EntitySystems/EldritchInfluenceSystem.cs b/Content.Server/_CorvaxNext/Heretic/EntitySystems/EldritchInfluenceSystem.cs
new file mode 100644
index 00000000000..f7a13dde4b5
--- /dev/null
+++ b/Content.Server/_CorvaxNext/Heretic/EntitySystems/EldritchInfluenceSystem.cs
@@ -0,0 +1,69 @@
+using Content.Server.Heretic.Components;
+using Content.Server.Popups;
+using Content.Shared.DoAfter;
+using Content.Shared.Heretic;
+using Content.Shared.Interaction;
+
+namespace Content.Server.Heretic.EntitySystems;
+
+public sealed partial class EldritchInfluenceSystem : EntitySystem
+{
+ [Dependency] private readonly SharedDoAfterSystem _doafter = default!;
+ [Dependency] private readonly PopupSystem _popup = default!;
+ [Dependency] private readonly HereticSystem _heretic = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnInteract);
+ SubscribeLocalEvent(OnInteractUsing);
+ SubscribeLocalEvent(OnDoAfter);
+ }
+
+ public bool CollectInfluence(Entity influence, Entity user, EntityUid? used = null)
+ {
+ if (influence.Comp.Spent)
+ return false;
+
+ var ev = new CheckMagicItemEvent();
+ RaiseLocalEvent(user, ev);
+ if (used != null) RaiseLocalEvent((EntityUid) used, ev);
+
+ var doAfter = new EldritchInfluenceDoAfterEvent()
+ {
+ MagicItemActive = ev.Handled,
+ };
+ var dargs = new DoAfterArgs(EntityManager, user, 10f, doAfter, influence, influence);
+ _popup.PopupEntity(Loc.GetString("heretic-influence-start"), influence, user);
+ return _doafter.TryStartDoAfter(dargs);
+ }
+
+ private void OnInteract(Entity ent, ref InteractHandEvent args)
+ {
+ if (args.Handled
+ || !TryComp(args.User, out var heretic))
+ return;
+
+ args.Handled = CollectInfluence(ent, (args.User, heretic));
+ }
+ private void OnInteractUsing(Entity ent, ref InteractUsingEvent args)
+ {
+ if (args.Handled
+ || !TryComp(args.User, out var heretic))
+ return;
+
+ args.Handled = CollectInfluence(ent, (args.User, heretic), args.Used);
+ }
+ private void OnDoAfter(Entity ent, ref EldritchInfluenceDoAfterEvent args)
+ {
+ if (args.Cancelled
+ || args.Target == null
+ || !TryComp(args.User, out var heretic))
+ return;
+
+ _heretic.UpdateKnowledge(args.User, heretic, args.MagicItemActive ? 2 : 1);
+
+ Spawn("EldritchInfluenceIntermediate", Transform((EntityUid) args.Target).Coordinates);
+ QueueDel(args.Target);
+ }
+}
diff --git a/Content.Server/_CorvaxNext/Heretic/EntitySystems/GhoulSystem.cs b/Content.Server/_CorvaxNext/Heretic/EntitySystems/GhoulSystem.cs
new file mode 100644
index 00000000000..14251855714
--- /dev/null
+++ b/Content.Server/_CorvaxNext/Heretic/EntitySystems/GhoulSystem.cs
@@ -0,0 +1,154 @@
+using Content.Server.Administration.Systems;
+using Content.Server.Antag;
+using Content.Server.Atmos.Components;
+using Content.Server.Body.Components;
+using Content.Server.Ghost.Roles.Components;
+using Content.Server.Humanoid;
+using Content.Server.Mind.Commands;
+using Content.Server.Roles;
+using Content.Server.Temperature.Components;
+using Content.Shared.Body.Systems;
+using Content.Shared.Damage;
+using Content.Shared.Examine;
+using Content.Shared.Ghost.Roles.Components;
+using Content.Shared.Heretic;
+using Content.Shared.Humanoid;
+using Content.Shared.IdentityManagement;
+using Content.Shared.Interaction.Events;
+using Content.Shared.Mind;
+using Content.Shared.Mobs;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Mobs.Systems;
+using Content.Shared.NPC.Systems;
+using Content.Shared.Nutrition.AnimalHusbandry;
+using Content.Shared.Nutrition.Components;
+using Content.Shared.Roles;
+using Robust.Shared.Audio;
+
+namespace Content.Server.Heretic.EntitySystems;
+
+public sealed partial class GhoulSystem : EntitySystem
+{
+ [Dependency] private readonly SharedMindSystem _mind = default!;
+ [Dependency] private readonly DamageableSystem _damage = default!;
+ [Dependency] private readonly AntagSelectionSystem _antag = default!;
+ [Dependency] private readonly HumanoidAppearanceSystem _humanoid = default!;
+ [Dependency] private readonly RejuvenateSystem _rejuvenate = default!;
+ [Dependency] private readonly NpcFactionSystem _faction = default!;
+ [Dependency] private readonly SharedRoleSystem _role = default!;
+ [Dependency] private readonly MobThresholdSystem _threshold = default!;
+ [Dependency] private readonly EntityLookupSystem _lookup = default!;
+ [Dependency] private readonly SharedBodySystem _body = default!;
+
+ public void GhoulifyEntity(Entity ent)
+ {
+ RemComp(ent);
+ RemComp(ent);
+ RemComp(ent);
+ RemComp(ent);
+ RemComp(ent);
+ RemComp(ent);
+ RemComp(ent);
+
+ var hasMind = _mind.TryGetMind(ent, out var mindId, out var mind);
+ if (hasMind && ent.Comp.BoundHeretic != null)
+ SendBriefing(ent, mindId, mind);
+
+ if (TryComp(ent, out var humanoid))
+ {
+ // make them "have no eyes" and grey
+ // this is clearly a reference to grey tide
+ var greycolor = Color.FromHex("#505050");
+ _humanoid.SetSkinColor(ent, greycolor, true, false, humanoid);
+ _humanoid.SetBaseLayerColor(ent, HumanoidVisualLayers.Eyes, greycolor, true, humanoid);
+ }
+
+ _rejuvenate.PerformRejuvenate(ent);
+ if (TryComp(ent, out var th))
+ {
+ _threshold.SetMobStateThreshold(ent, ent.Comp.TotalHealth, MobState.Dead, th);
+ _threshold.SetMobStateThreshold(ent, ent.Comp.TotalHealth / 1.25f, MobState.Critical, th);
+ }
+
+ MakeSentientCommand.MakeSentient(ent, EntityManager);
+
+ if (!HasComp(ent) && !hasMind)
+ {
+ var ghostRole = EnsureComp(ent);
+ ghostRole.RoleName = Loc.GetString("ghostrole-ghoul-name");
+ ghostRole.RoleDescription = Loc.GetString("ghostrole-ghoul-desc");
+ ghostRole.RoleRules = Loc.GetString("ghostrole-ghoul-rules");
+ }
+
+ if (!HasComp(ent) && !hasMind)
+ EnsureComp(ent);
+
+ _faction.ClearFactions((ent, null));
+ _faction.AddFaction((ent, null), "Heretic");
+ }
+
+ private void SendBriefing(Entity ent, EntityUid mindId, MindComponent? mind)
+ {
+ var brief = Loc.GetString("heretic-ghoul-greeting-noname");
+
+ if (ent.Comp.BoundHeretic != null)
+ brief = Loc.GetString("heretic-ghoul-greeting", ("ent", Identity.Entity((EntityUid) ent.Comp.BoundHeretic, EntityManager)));
+ var sound = new SoundPathSpecifier("/Audio/_CorvaxNext/Heretic/Ambience/Antag/Heretic/heretic_gain.ogg");
+ _antag.SendBriefing(ent, brief, Color.MediumPurple, sound);
+
+ if (!TryComp(ent, out _))
+ AddComp(mindId, new(), overwrite: true);
+
+ if (!TryComp(ent, out var rolebrief))
+ AddComp(mindId, new RoleBriefingComponent() { Briefing = brief }, overwrite: true);
+ else rolebrief.Briefing += $"\n{brief}";
+ }
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnInit);
+ SubscribeLocalEvent(OnTryAttack);
+ SubscribeLocalEvent(OnTakeGhostRole);
+ SubscribeLocalEvent(OnExamine);
+ SubscribeLocalEvent(OnMobStateChange);
+ }
+
+ private void OnInit(Entity ent, ref ComponentInit args)
+ {
+ foreach (var look in _lookup.GetEntitiesInRange(Transform(ent).Coordinates, 1.5f))
+ {
+ if (ent.Comp.BoundHeretic == null)
+ ent.Comp.BoundHeretic = look;
+ else break;
+ }
+
+ GhoulifyEntity(ent);
+ }
+ private void OnTakeGhostRole(Entity ent, ref TakeGhostRoleEvent args)
+ {
+ var hasMind = _mind.TryGetMind(ent, out var mindId, out var mind);
+ if (hasMind)
+ SendBriefing(ent, mindId, mind);
+ }
+
+ private void OnTryAttack(Entity ent, ref AttackAttemptEvent args)
+ {
+ // prevent attacking owner and other heretics
+ if (args.Target == ent.Owner
+ || HasComp(args.Target))
+ args.Cancel();
+ }
+
+ private void OnExamine(Entity ent, ref ExaminedEvent args)
+ {
+ args.PushMarkup(Loc.GetString("examine-system-cant-see-entity"));
+ }
+
+ private void OnMobStateChange(Entity ent, ref MobStateChangedEvent args)
+ {
+ if (args.NewMobState == MobState.Dead)
+ _body.GibBody(ent);
+ }
+}
diff --git a/Content.Server/_CorvaxNext/Heretic/EntitySystems/HereticBladeSystem.cs b/Content.Server/_CorvaxNext/Heretic/EntitySystems/HereticBladeSystem.cs
new file mode 100644
index 00000000000..16692600f75
--- /dev/null
+++ b/Content.Server/_CorvaxNext/Heretic/EntitySystems/HereticBladeSystem.cs
@@ -0,0 +1,169 @@
+using Content.Server.Atmos.EntitySystems;
+using Content.Server.Body.Systems;
+using Content.Server.Heretic.Components;
+using Content.Server.Heretic.Components.PathSpecific;
+using Content.Server.Teleportation;
+using Content.Server.Temperature.Components;
+using Content.Server.Temperature.Systems;
+using Content.Shared.Damage;
+using Content.Shared.Examine;
+using Content.Shared.Heretic;
+using Content.Shared.Interaction.Events;
+using Content.Shared.Popups;
+using Content.Shared.Weapons.Melee.Events;
+using Robust.Shared.Audio;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using System.Linq;
+using System.Text;
+
+namespace Content.Server.Heretic.EntitySystems;
+
+public sealed partial class HereticBladeSystem : EntitySystem
+{
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+ [Dependency] private readonly SharedTransformSystem _xform = default!;
+ [Dependency] private readonly EntityLookupSystem _lookupSystem = default!;
+ [Dependency] private readonly SharedMapSystem _mapSystem = default!;
+ [Dependency] private readonly HereticCombatMarkSystem _combatMark = default!;
+ [Dependency] private readonly FlammableSystem _flammable = default!;
+ [Dependency] private readonly BloodstreamSystem _blood = default!;
+ [Dependency] private readonly DamageableSystem _damage = default!;
+ [Dependency] private readonly IPrototypeManager _proto = default!;
+ [Dependency] private readonly TemperatureSystem _temp = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnInteract);
+ SubscribeLocalEvent(OnExamine);
+ SubscribeLocalEvent(OnMeleeHit);
+ }
+
+ public void ApplySpecialEffect(EntityUid performer, EntityUid target)
+ {
+ if (!TryComp(performer, out var hereticComp))
+ return;
+
+ switch (hereticComp.CurrentPath)
+ {
+ case "Ash":
+ _flammable.AdjustFireStacks(target, 2.5f, ignite: true);
+ break;
+
+ case "Blade":
+ // check event handler
+ break;
+
+ case "Flesh":
+ // ultra bleed
+ _blood.TryModifyBleedAmount(target, 1.5f);
+ break;
+
+ case "Lock":
+ // todo: do something that has weeping and avulsion in it
+ if (_random.Next(0, 10) >= 8)
+ _blood.TryModifyBleedAmount(target, 10f);
+ break;
+
+ case "Void":
+ if (TryComp(target, out var temp))
+ _temp.ForceChangeTemperature(target, temp.CurrentTemperature - 5f, temp);
+ break;
+
+ default:
+ return;
+ }
+ }
+
+ private void OnInteract(Entity ent, ref UseInHandEvent args)
+ {
+ if (!TryComp(args.User, out var heretic))
+ return;
+
+ var xform = Transform(args.User);
+ // 250 because for some reason it counts "10" as 1 tile
+ var targetCoords = SelectRandomTileInRange(xform, 250f);
+ var queuedel = true;
+
+ // void path exxclusive
+ if (heretic.CurrentPath == "Void" && heretic.PathStage >= 7)
+ {
+ var look = _lookupSystem.GetEntitiesInRange(Transform(ent).Coordinates, 20f);
+ if (look.Count > 0)
+ {
+ targetCoords = Transform(look.ToList()[0]).Coordinates;
+ queuedel = false;
+ }
+ }
+
+ if (targetCoords != null)
+ {
+ _xform.SetCoordinates(args.User, targetCoords.Value);
+ _audio.PlayPvs(new SoundPathSpecifier("/Audio/Effects/tesla_consume.ogg"), args.User);
+ args.Handled = true;
+ }
+
+ _popup.PopupEntity(Loc.GetString("heretic-blade-use"), args.User, args.User);
+
+ if (queuedel)
+ QueueDel(ent);
+ }
+
+ private void OnExamine(Entity ent, ref ExaminedEvent args)
+ {
+ if (!TryComp(args.Examiner, out var heretic))
+ return;
+
+ var isUpgradedVoid = heretic.CurrentPath == "Void" && heretic.PathStage >= 7;
+
+ var sb = new StringBuilder();
+ sb.AppendLine(Loc.GetString("heretic-blade-examine"));
+ if (isUpgradedVoid) sb.AppendLine(Loc.GetString("heretic-blade-void-examine"));
+
+ args.PushMarkup(sb.ToString());
+ }
+
+ private void OnMeleeHit(Entity ent, ref MeleeHitEvent args)
+ {
+ if (string.IsNullOrWhiteSpace(ent.Comp.Path))
+ return;
+
+ if (!TryComp(args.User, out var hereticComp))
+ return;
+
+ foreach (var hit in args.HitEntities)
+ {
+ // does not work on other heretics
+ if (HasComp(hit))
+ continue;
+
+ if (TryComp(hit, out var mark))
+ {
+ _combatMark.ApplyMarkEffect(hit, ent.Comp.Path, args.User);
+ RemComp(hit, mark);
+ }
+
+ if (hereticComp.PathStage >= 7)
+ ApplySpecialEffect(args.User, hit);
+ }
+
+ // blade path exclusive.
+ if (HasComp(args.User))
+ {
+ args.BonusDamage += args.BaseDamage; // double it.
+ if (TryComp(args.User, out var dmg))
+ {
+ var orig = dmg.Damage.DamageDict;
+ foreach (var k in orig.Keys)
+ orig[k] -= 5f; // -5 damage to all types. pretty good imo
+
+ _damage.SetDamage(args.User, dmg, new() { DamageDict = orig });
+ }
+ }
+ }
+}
diff --git a/Content.Server/_CorvaxNext/Heretic/EntitySystems/HereticCombatMarkSystem.cs b/Content.Server/_CorvaxNext/Heretic/EntitySystems/HereticCombatMarkSystem.cs
new file mode 100644
index 00000000000..957ebdaaf25
--- /dev/null
+++ b/Content.Server/_CorvaxNext/Heretic/EntitySystems/HereticCombatMarkSystem.cs
@@ -0,0 +1,143 @@
+using Content.Server.Atmos.EntitySystems;
+using Content.Server.Body.Systems;
+using Content.Server.Popups;
+using Content.Server.Temperature.Systems;
+using Content.Shared.Atmos;
+using Content.Shared.Popups;
+using Content.Shared.Doors.Components;
+using Content.Shared.Doors.Systems;
+using Content.Shared.Heretic;
+using Content.Shared.Inventory;
+using Robust.Shared.Audio;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Random;
+using Robust.Shared.Timing;
+using System.Linq;
+using Content.Shared.Humanoid;
+using Content.Server.Temperature.Components;
+using Content.Server.Body.Components;
+
+namespace Content.Server.Heretic.EntitySystems;
+
+public sealed partial class HereticCombatMarkSystem : EntitySystem
+{
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly FlammableSystem _flammable = default!;
+ [Dependency] private readonly SharedDoorSystem _door = default!;
+ [Dependency] private readonly EntityLookupSystem _lookup = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly TemperatureSystem _temperature = default!;
+ [Dependency] private readonly BloodstreamSystem _blood = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly PopupSystem _popup = default!;
+ [Dependency] private readonly ProtectiveBladeSystem _pbs = default!;
+
+ public bool ApplyMarkEffect(EntityUid target, string? path, EntityUid user)
+ {
+ if (string.IsNullOrWhiteSpace(path))
+ return false;
+
+ switch (path)
+ {
+ case "Ash":
+ // gives fire stacks
+ _flammable.AdjustFireStacks(target, 5, ignite: true);
+ break;
+
+ case "Blade":
+ _pbs.AddProtectiveBlade(user);
+ break;
+
+ case "Flesh":
+ if (TryComp(target, out var blood))
+ {
+ _blood.TryModifyBleedAmount(target, 5f, blood);
+ _blood.SpillAllSolutions(target, blood);
+ }
+ break;
+
+ case "Lock":
+ // bolts nearby doors
+ var lookup = _lookup.GetEntitiesInRange(target, 5f);
+ foreach (var door in lookup)
+ {
+ if (!TryComp(door, out var doorComp))
+ continue;
+ _door.SetBoltsDown((door, doorComp), true);
+ }
+ _audio.PlayPvs(new SoundPathSpecifier("/Audio/Magic/knock.ogg"), target);
+ break;
+
+ case "Rust":
+ // TODO: add item damage, for now just break a random item
+ if (!TryComp(target, out var inv))
+ break;
+
+ var contrandom = _random.Next(0, inv.Containers.Length - 1);
+ if (contrandom < 0)
+ break;
+ var cont = inv.Containers[contrandom];
+
+ var itemrandom = _random.Next(0, cont.ContainedEntities.Count - 1);
+ if (itemrandom < 0)
+ break;
+ var item = cont.ContainedEntities[itemrandom];
+
+ _popup.PopupEntity(Loc.GetString("heretic-rust-mark-itembreak", ("name", Name(item))), target, PopupType.LargeCaution);
+ QueueDel(item);
+ break;
+
+ case "Void":
+ // set target's temperature to -40C
+ // is really OP with the new temperature slowing thing :godo:
+ if (TryComp(target, out var temp))
+ _temperature.ForceChangeTemperature(target, temp.CurrentTemperature - 100f, temp);
+ break;
+
+ default:
+ return false;
+ }
+
+ // transfers the mark to the next nearby person
+ var look = _lookup.GetEntitiesInRange(target, 2.5f);
+ if (look.Count != 0)
+ {
+ var lookent = look.ToArray()[0];
+ if (HasComp(lookent)
+ && !HasComp(lookent))
+ {
+ var markComp = EnsureComp(lookent);
+ markComp.Path = path;
+ }
+ }
+
+ return true;
+ }
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnStart);
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ if (!_timing.IsFirstTimePredicted)
+ return;
+
+ foreach (var comp in EntityQuery())
+ {
+ if (_timing.CurTime > comp.Timer)
+ RemComp(comp.Owner, comp);
+ }
+ }
+
+ private void OnStart(Entity ent, ref ComponentStartup args)
+ {
+ if (ent.Comp.Timer == TimeSpan.Zero)
+ ent.Comp.Timer = _timing.CurTime + TimeSpan.FromSeconds(ent.Comp.DisappearTime);
+ }
+}
diff --git a/Content.Server/_CorvaxNext/Heretic/EntitySystems/HereticKnowledgeSystem.cs b/Content.Server/_CorvaxNext/Heretic/EntitySystems/HereticKnowledgeSystem.cs
new file mode 100644
index 00000000000..e9557b4c81f
--- /dev/null
+++ b/Content.Server/_CorvaxNext/Heretic/EntitySystems/HereticKnowledgeSystem.cs
@@ -0,0 +1,76 @@
+using Content.Shared.Actions;
+using Content.Shared.Heretic.Prototypes;
+using Content.Shared.Heretic;
+using Content.Shared.Popups;
+using Robust.Shared.Prototypes;
+using Content.Shared._CorvaxNext.Heretic.Components;
+
+namespace Content.Server.Heretic.EntitySystems;
+
+public sealed partial class HereticKnowledgeSystem : EntitySystem
+{
+ [Dependency] private readonly IPrototypeManager _proto = default!;
+ [Dependency] private readonly SharedActionsSystem _action = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+ [Dependency] private readonly HereticRitualSystem _ritual = default!;
+
+ public HereticKnowledgePrototype GetKnowledge(ProtoId id)
+ => _proto.Index(id);
+
+ public void AddKnowledge(EntityUid uid, HereticComponent comp, ProtoId id, bool silent = true)
+ {
+ var data = GetKnowledge(id);
+
+ if (data.Event != null)
+ RaiseLocalEvent(uid, (object) data.Event, true);
+
+ if (data.ActionPrototypes != null && data.ActionPrototypes.Count > 0)
+ foreach (var act in data.ActionPrototypes)
+ _action.AddAction(uid, act);
+
+ if (data.RitualPrototypes != null && data.RitualPrototypes.Count > 0)
+ foreach (var ritual in data.RitualPrototypes)
+ comp.KnownRituals.Add(_ritual.GetRitual(ritual));
+
+ Dirty(uid, comp);
+
+ // set path if out heretic doesn't have it, or if it's different from whatever he has atm
+ if (string.IsNullOrWhiteSpace(comp.CurrentPath))
+ {
+ if (!data.SideKnowledge && comp.CurrentPath != data.Path)
+ comp.CurrentPath = data.Path;
+ }
+
+ // make sure we only progress when buying current path knowledge
+ if (data.Stage > comp.PathStage && data.Path == comp.CurrentPath)
+ comp.PathStage = data.Stage;
+
+ if (!silent)
+ _popup.PopupEntity(Loc.GetString("heretic-knowledge-gain"), uid, uid);
+ }
+ public void RemoveKnowledge(EntityUid uid, HereticComponent comp, ProtoId id, bool silent = false)
+ {
+ var data = GetKnowledge(id);
+
+ if (data.ActionPrototypes != null && data.ActionPrototypes.Count > 0)
+ {
+ foreach (var act in data.ActionPrototypes)
+ {
+ var actionName = (EntityPrototype) _proto.Index(typeof(EntityPrototype), act);
+ // jesus christ.
+ foreach (var action in _action.GetActions(uid))
+ if (Name(action.Id) == actionName.Name)
+ _action.RemoveAction(action.Id);
+ }
+ }
+
+ if (data.RitualPrototypes != null && data.RitualPrototypes.Count > 0)
+ foreach (var ritual in data.RitualPrototypes)
+ comp.KnownRituals.Remove(_ritual.GetRitual(ritual));
+
+ Dirty(uid, comp);
+
+ if (!silent)
+ _popup.PopupEntity(Loc.GetString("heretic-knowledge-loss"), uid, uid);
+ }
+}
diff --git a/Content.Server/_CorvaxNext/Heretic/EntitySystems/HereticMagicItemSystem.cs b/Content.Server/_CorvaxNext/Heretic/EntitySystems/HereticMagicItemSystem.cs
new file mode 100644
index 00000000000..cda28da747c
--- /dev/null
+++ b/Content.Server/_CorvaxNext/Heretic/EntitySystems/HereticMagicItemSystem.cs
@@ -0,0 +1,34 @@
+using Content.Shared.Examine;
+using Content.Shared.Hands;
+using Content.Shared.Heretic;
+using Content.Shared.Inventory;
+
+namespace Content.Server.Heretic.EntitySystems;
+
+public sealed partial class HereticMagicItemSystem : EntitySystem
+{
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent