diff --git a/Content.Client/Vampire/VampireMutationBoundUserInterface.cs b/Content.Client/Vampire/VampireMutationBoundUserInterface.cs
new file mode 100644
index 00000000000..8b69f483c55
--- /dev/null
+++ b/Content.Client/Vampire/VampireMutationBoundUserInterface.cs
@@ -0,0 +1,42 @@
+using Content.Shared.Vampire;
+using Content.Shared.Vampire.Components;
+using JetBrains.Annotations;
+using Robust.Client.GameObjects;
+namespace Content.Client.Vampire;
+[UsedImplicitly]
+public sealed class VampireMutationBoundUserInterface : BoundUserInterface
+{
+ [ViewVariables]
+ private VampireMutationMenu? _menu;
+ public VampireMutationBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ }
+ protected override void Open()
+ {
+ base.Open();
+ _menu = new VampireMutationMenu();
+ _menu.OnClose += Close;
+ _menu.OnIdSelected += OnIdSelected;
+ _menu.OpenCentered();
+ }
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+ if (state is not VampireMutationBoundUserInterfaceState st)
+ return;
+ _menu?.UpdateState(st.MutationList, st.SelectedMutation);
+ }
+ private void OnIdSelected(VampireMutationsType selectedId)
+ {
+ SendMessage(new VampireMutationPrototypeSelectedMessage(selectedId));
+ }
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ if (disposing)
+ {
+ _menu?.Close();
+ _menu = null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Content.Client/Vampire/VampireMutationMenu.xaml b/Content.Client/Vampire/VampireMutationMenu.xaml
new file mode 100644
index 00000000000..5dca219d2ee
--- /dev/null
+++ b/Content.Client/Vampire/VampireMutationMenu.xaml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Content.Client/Vampire/VampireMutationMenu.xaml.cs b/Content.Client/Vampire/VampireMutationMenu.xaml.cs
new file mode 100644
index 00000000000..d4616dff9d7
--- /dev/null
+++ b/Content.Client/Vampire/VampireMutationMenu.xaml.cs
@@ -0,0 +1,92 @@
+using System.Numerics;
+using Content.Client.Stylesheets;
+using Content.Shared.Vampire;
+using Content.Shared.Vampire.Components;
+using Content.Client.Resources;
+using Robust.Client.ResourceManagement;
+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.Vampire;
+
+[GenerateTypedNameReferences]
+public sealed partial class VampireMutationMenu : DefaultWindow
+{
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly IEntityManager _entityManager = default!;
+ private IResourceCache _resourceCache;
+ private readonly SpriteSystem _sprite;
+ public event Action? OnIdSelected;
+
+ private HashSet _possibleMutations = new();
+ private VampireMutationsType _selectedId;
+
+ public VampireMutationMenu()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+ _sprite = _entityManager.System();
+ _resourceCache = IoCManager.Resolve();
+ }
+
+ public void UpdateState(HashSet mutationList, VampireMutationsType selectedMutation)
+ {
+ _possibleMutations = mutationList;
+ _selectedId = selectedMutation;
+ UpdateGrid();
+ }
+
+ private void UpdateGrid()
+ {
+ ClearGrid();
+
+ var group = new ButtonGroup();
+
+ foreach (var Mutation in _possibleMutations)
+ {
+ //if (!_prototypeManager.TryIndex("NormalBlobTile", out EntityPrototype? proto))
+ // continue;
+
+ string texturePath = Mutation switch
+ {
+ VampireMutationsType.None => "/Textures/Interface/Actions/actions_vampire.rsi/deathsembrace.png",
+ VampireMutationsType.Hemomancer => "/Textures/Interface/Actions/actions_vampire.rsi/hemomancer.png",
+ VampireMutationsType.Umbrae => "/Textures/Interface/Actions/actions_vampire.rsi/umbrae.png",
+ VampireMutationsType.Gargantua => "/Textures/Interface/Actions/actions_vampire.rsi/gargantua.png",
+ VampireMutationsType.Dantalion => "/Textures/Interface/Actions/actions_vampire.rsi/dantalion.png",
+ VampireMutationsType.Bestia => "/Textures/Interface/Actions/actions_vampire.rsi/bestia.png",
+ _ => "/Textures/Interface/Actions/actions_vampire.rsi/deathsembrace.png"
+ };
+
+ var button = new Button
+ {
+ MinSize = new Vector2(64, 64),
+ HorizontalExpand = true,
+ Group = group,
+ StyleClasses = {StyleBase.ButtonSquare},
+ ToggleMode = true,
+ Pressed = _selectedId == Mutation,
+ ToolTip = Loc.GetString($"vampire-mutation-{Mutation.ToString().ToLower()}-info"),
+ TooltipDelay = 0.01f,
+ };
+ button.OnPressed += _ => OnIdSelected?.Invoke(Mutation);
+ Grid.AddChild(button);
+
+ var texture = _resourceCache.GetTexture(texturePath);
+ button.AddChild(new TextureRect
+ {
+ Stretch = TextureRect.StretchMode.KeepAspectCentered,
+ Texture = texture,
+ });
+ }
+ }
+
+ private void ClearGrid()
+ {
+ Grid.RemoveAllChildren();
+ }
+}
\ No newline at end of file
diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs
index 0cf044e71be..6dabcdfd192 100644
--- a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs
+++ b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs
@@ -165,5 +165,19 @@ private void AddAntagVerbs(GetVerbsEvent args)
Message = Loc.GetString("admin-verb-make-changeling"),
};
args.Verbs.Add(ling);
+
+ Verb vampire = new()
+ {
+ Text = Loc.GetString("admin-verb-text-make-vampire"),
+ Category = VerbCategory.Antag,
+ Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Interface/Actions/actions_vampire.rsi"), "unholystrength"),
+ Act = () =>
+ {
+ _antag.ForceMakeAntag(targetPlayer, "Vampire");
+ },
+ Impact = LogImpact.High,
+ Message = Loc.GetString("admin-verb-make-vampire"),
+ };
+ args.Verbs.Add(vampire);
}
}
diff --git a/Content.Server/Bible/BibleSystem.cs b/Content.Server/Bible/BibleSystem.cs
index d6a109321ab..c193578f822 100644
--- a/Content.Server/Bible/BibleSystem.cs
+++ b/Content.Server/Bible/BibleSystem.cs
@@ -13,12 +13,16 @@
using Content.Shared.Mobs;
using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
+using Content.Shared.Stunnable;
using Content.Shared.Timing;
+using Content.Shared.Vampire.Components;
using Content.Shared.Verbs;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
+using Robust.Shared.Containers;
using Robust.Shared.Player;
using Robust.Shared.Random;
+using Robust.Shared.Timing;
namespace Content.Server.Bible
{
@@ -34,12 +38,14 @@ public sealed class BibleSystem : EntitySystem
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly UseDelaySystem _delay = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
+ [Dependency] private readonly SharedStunSystem _stun = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent(OnAfterInteract);
+ SubscribeLocalEvent(OnInsertedContainer);
SubscribeLocalEvent>(AddSummonVerb);
SubscribeLocalEvent(GetSummonAction);
SubscribeLocalEvent(OnSummon);
@@ -47,6 +53,20 @@ public override void Initialize()
SubscribeLocalEvent(OnSpawned);
}
+ private void OnInsertedContainer(EntityUid uid, BibleComponent component, EntGotInsertedIntoContainerMessage args)
+ {
+ //If an unholy creature picks up the bible, knock them down
+ if (HasComp(args.Container.Owner))
+ {
+ Timer.Spawn(500, () =>
+ {
+ _stun.TryParalyze(args.Container.Owner, TimeSpan.FromSeconds(10), true);
+ _damageableSystem.TryChangeDamage(args.Container.Owner, component.DamageOnUnholyUse);
+ _audio.PlayPvs(component.SizzleSoundPath, args.Container.Owner);
+ });
+ }
+ }
+
private readonly Queue _addQueue = new();
private readonly Queue _remQueue = new();
@@ -116,7 +136,21 @@ private void OnAfterInteract(EntityUid uid, BibleComponent component, AfterInter
return;
}
- // This only has a chance to fail if the target is not wearing anything on their head and is not a familiar.
+ //Damage unholy creatures
+ if (HasComp(args.Target))
+ {
+ _damageableSystem.TryChangeDamage(args.Target.Value, component.DamageUnholy, true, origin: uid);
+
+ var othersMessage = Loc.GetString(component.LocPrefix + "-damage-unholy-others", ("user", Identity.Entity(args.User, EntityManager)), ("target", Identity.Entity(args.Target.Value, EntityManager)), ("bible", uid));
+ _popupSystem.PopupEntity(othersMessage, args.User, Filter.PvsExcept(args.User), true, PopupType.MediumCaution);
+
+ var selfMessage = Loc.GetString(component.LocPrefix + "-damage-unholy-self", ("target", Identity.Entity(args.Target.Value, EntityManager)), ("bible", uid));
+ _popupSystem.PopupEntity(selfMessage, args.User, args.User, PopupType.LargeCaution);
+
+ return;
+ }
+
+ // This only has a chance to fail if the target is not wearing anything on their head and is not a familiar..
if (!_invSystem.TryGetSlotEntity(args.Target.Value, "head", out var _) && !HasComp(args.Target.Value))
{
if (_random.Prob(component.FailChance))
diff --git a/Content.Server/Bible/Components/BibleComponent.cs b/Content.Server/Bible/Components/BibleComponent.cs
index b7dc3db8e35..3201831017d 100644
--- a/Content.Server/Bible/Components/BibleComponent.cs
+++ b/Content.Server/Bible/Components/BibleComponent.cs
@@ -27,6 +27,14 @@ public sealed partial class BibleComponent : Component
[ViewVariables(VVAccess.ReadWrite)]
public DamageSpecifier DamageOnUntrainedUse = default!;
+ [DataField("damageOnUnholyUse", required: true)]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public DamageSpecifier DamageOnUnholyUse = default!;
+
+ [DataField("damageUnholy", required: true)]
+ [ViewVariables(VVAccess.ReadWrite)]
+ public DamageSpecifier DamageUnholy = default!;
+
///
/// Chance the bible will fail to heal someone with no helmet
///
diff --git a/Content.Server/Body/Systems/MetabolizerSystem.cs b/Content.Server/Body/Systems/MetabolizerSystem.cs
index 3497d4a6d78..3b7d9306ce1 100644
--- a/Content.Server/Body/Systems/MetabolizerSystem.cs
+++ b/Content.Server/Body/Systems/MetabolizerSystem.cs
@@ -2,6 +2,7 @@
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Administration.Logs;
using Content.Shared.Body.Organ;
+using Content.Shared.Body.Prototypes;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.Reagent;
@@ -230,6 +231,31 @@ private void TryMetabolize(Entity(metabolizerType))
+ return false;
+
+ if (component.MetabolizerTypes == null)
+ component.MetabolizerTypes = new();
+
+ return component.MetabolizerTypes.Add(metabolizerType);
+ }
+
+ public bool TryRemoveMetabolizerType(MetabolizerComponent component, string metabolizerType)
+ {
+ if (component.MetabolizerTypes == null)
+ return true;
+
+ return component.MetabolizerTypes.Remove(metabolizerType);
+ }
+
+ public void ClearMetabolizerTypes(MetabolizerComponent component)
+ {
+ if (component.MetabolizerTypes != null)
+ component.MetabolizerTypes.Clear();
+ }
}
// TODO REFACTOR THIS
diff --git a/Content.Server/Body/Systems/StomachSystem.cs b/Content.Server/Body/Systems/StomachSystem.cs
index 9fc7ff10e46..a8cf946f63b 100644
--- a/Content.Server/Body/Systems/StomachSystem.cs
+++ b/Content.Server/Body/Systems/StomachSystem.cs
@@ -3,6 +3,7 @@
using Content.Shared.Body.Organ;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
+using Content.Shared.Whitelist;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -130,5 +131,10 @@ public bool TryTransferSolution(
return true;
}
+
+ public void SetSpecialDigestible(StomachComponent component, EntityWhitelist? whitelist)
+ {
+ component.SpecialDigestible = whitelist;
+ }
}
}
diff --git a/Content.Server/GameTicking/Rules/Components/VampireRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/VampireRuleComponent.cs
new file mode 100644
index 00000000000..3fc2b7c2d06
--- /dev/null
+++ b/Content.Server/GameTicking/Rules/Components/VampireRuleComponent.cs
@@ -0,0 +1,38 @@
+using Content.Shared.NPC.Prototypes;
+using Content.Shared.Roles;
+using Content.Shared.Vampire.Components;
+using Robust.Shared.Audio;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.GameTicking.Rules.Components;
+
+[RegisterComponent, Access(typeof(VampireRuleSystem))]
+public sealed partial class VampireRuleComponent : Component
+{
+ public readonly List VampireMinds = new();
+
+
+ public readonly List> BaseObjectives = new()
+ {
+ "VampireKillRandomPersonObjective",
+ "VampireDrainObjective"
+ };
+
+ public readonly List> EscapeObjectives = new()
+ {
+ "VampireSurviveObjective",
+ "VampireEscapeObjective"
+ };
+
+ public readonly List> StealObjectives = new()
+ {
+ "CMOHyposprayVampireStealObjective",
+ "RDHardsuitVampireStealObjective",
+ "EnergyShotgunVampireStealObjective",
+ "MagbootsVampireStealObjective",
+ "ClipboardVampireStealObjective",
+ "CaptainIDVampireStealObjective",
+ "CaptainJetpackVampireStealObjective",
+ "CaptainGunVampireStealObjective"
+ };
+}
\ No newline at end of file
diff --git a/Content.Server/GameTicking/Rules/VampireRuleSystem.cs b/Content.Server/GameTicking/Rules/VampireRuleSystem.cs
new file mode 100644
index 00000000000..a5522848dcc
--- /dev/null
+++ b/Content.Server/GameTicking/Rules/VampireRuleSystem.cs
@@ -0,0 +1,159 @@
+using Content.Server.Antag;
+using Content.Server.GameTicking.Rules.Components;
+using Content.Server.Mind;
+using Content.Server.Objectives;
+using Content.Server.Roles;
+using Content.Server.Vampire;
+using Content.Shared.Vampire.Components;
+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.Server.GameObjects;
+using Robust.Shared.Audio;
+using Robust.Shared.Prototypes;
+using System.Text;
+
+namespace Content.Server.GameTicking.Rules;
+
+public sealed partial class VampireRuleSystem : 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 VampireSystem _vampire = default!;
+ [Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
+
+ public readonly SoundSpecifier BriefingSound = new SoundPathSpecifier("/Audio/Ambience/Antag/vampire_start.ogg");
+
+ public readonly ProtoId VampirePrototypeId = "Vampire";
+
+// public readonly ProtoId ChangelingFactionId = "Changeling";
+
+// public readonly ProtoId NanotrasenFactionId = "NanoTrasen";
+
+ public readonly ProtoId Currency = "BloodEssence";
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnGetBriefing);
+
+ SubscribeLocalEvent(OnSelectAntag);
+ //SubscribeLocalEvent(OnTextPrepend);
+ }
+
+ private void OnSelectAntag(EntityUid mindId, VampireRuleComponent comp, ref AfterAntagEntitySelectedEvent args)
+ {
+ var ent = args.EntityUid;
+ _antag.SendBriefing(ent, MakeBriefing(ent), Color.Yellow, BriefingSound);
+ MakeVampire(ent, comp);
+ }
+ public bool MakeVampire(EntityUid target, VampireRuleComponent rule)
+ {
+ if (!_mind.TryGetMind(target, out var mindId, out var mind))
+ return false;
+
+ // briefing
+ if (TryComp(target, out var metaData))
+ {
+ var briefing = Loc.GetString("vampire-role-greeting", ("name", metaData?.EntityName ?? "Unknown"));
+ var briefingShort = Loc.GetString("vampire-role-greeting-short", ("name", metaData?.EntityName ?? "Unknown"));
+
+ _role.MindHasRole(mindId, out var vampireRole);
+ _role.MindHasRole(mindId, out var briefingComp);
+ if (vampireRole is not null && briefingComp is null)
+ {
+ AddComp(vampireRole.Value.Owner);
+ Comp(vampireRole.Value.Owner).Briefing = briefing;
+ }
+ }
+ // vampire stuff
+// _npcFaction.RemoveFaction(target, NanotrasenFactionId, false);
+// _npcFaction.AddFaction(target, ChangelingFactionId);
+
+ // make sure it's initial chems are set to max
+ var vampireComponent = EnsureComp(target);
+ var interfaceComponent = EnsureComp(target);
+
+ if (HasComp(target))
+ _uiSystem.SetUiState(target, VampireMutationUiKey.Key, new VampireMutationBoundUserInterfaceState(vampireComponent.VampireMutations, vampireComponent.CurrentMutation));
+
+ vampireComponent.Balance = new() { { VampireComponent.CurrencyProto, 0 } };
+
+ rule.VampireMinds.Add(mindId);
+
+ if (HasComp(target))
+ _vampire.AddStartingAbilities(target);
+
+ Random random = new Random();
+
+ foreach (var objective in rule.BaseObjectives)
+ _mind.TryAddObjective(mindId, mind, objective);
+
+ if (rule.EscapeObjectives.Count > 0)
+ {
+ var randomEscapeObjective = rule.EscapeObjectives[random.Next(rule.EscapeObjectives.Count)];
+ _mind.TryAddObjective(mindId, mind, randomEscapeObjective);
+ }
+
+ if (rule.StealObjectives.Count > 0)
+ {
+ var randomEscapeObjective = rule.StealObjectives[random.Next(rule.StealObjectives.Count)];
+ _mind.TryAddObjective(mindId, mind, randomEscapeObjective);
+ }
+
+ return true;
+ }
+
+ private void OnGetBriefing(Entity role, ref GetBriefingEvent args)
+ {
+ var ent = args.Mind.Comp.OwnedEntity;
+
+ if (ent is null)
+ return;
+ args.Append(MakeBriefing(ent.Value));
+ }
+
+ private string MakeBriefing(EntityUid ent)
+ {
+ if (TryComp(ent, out var metaData))
+ {
+ var briefing = Loc.GetString("vampire-role-greeting", ("name", metaData?.EntityName ?? "Unknown"));
+
+ return briefing;
+ }
+
+ return "";
+ }
+
+ private void OnTextPrepend(EntityUid uid, VampireRuleComponent comp, ref ObjectivesTextPrependEvent args)
+ {
+ var mostDrainedName = string.Empty;
+ var mostDrained = 0f;
+
+ foreach (var vamp in EntityQuery())
+ {
+ if (!_mind.TryGetMind(vamp.Owner, out var mindId, out var mind))
+ continue;
+
+ if (!TryComp(vamp.Owner, out var metaData))
+ continue;
+
+ if (vamp.TotalBloodDrank > mostDrained)
+ {
+ mostDrained = vamp.TotalBloodDrank;
+ mostDrainedName = _objective.GetTitle((mindId, mind), metaData.EntityName);
+ }
+ }
+
+ var sb = new StringBuilder();
+ sb.AppendLine(Loc.GetString($"roundend-prepend-vampire-drained{(!string.IsNullOrWhiteSpace(mostDrainedName) ? "-named" : "")}", ("name", mostDrainedName), ("number", mostDrained)));
+
+ args.Text = sb.ToString();
+ }
+}
diff --git a/Content.Server/Objectives/Components/BloodDrainCondition.cs b/Content.Server/Objectives/Components/BloodDrainCondition.cs
new file mode 100644
index 00000000000..afaaafc3a8e
--- /dev/null
+++ b/Content.Server/Objectives/Components/BloodDrainCondition.cs
@@ -0,0 +1,11 @@
+using Content.Server.Vampire;
+using Content.Server.Objectives.Systems;
+
+namespace Content.Server.Objectives.Components;
+
+[RegisterComponent, Access(typeof(VampireSystem))]
+public sealed partial class BloodDrainConditionComponent : Component
+{
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public float BloodDranked = 0f;
+}
\ No newline at end of file
diff --git a/Content.Server/Roles/VampireRoleComponent.cs b/Content.Server/Roles/VampireRoleComponent.cs
new file mode 100644
index 00000000000..5242a143fcf
--- /dev/null
+++ b/Content.Server/Roles/VampireRoleComponent.cs
@@ -0,0 +1,8 @@
+using Content.Shared.Roles;
+
+namespace Content.Server.Roles;
+
+[RegisterComponent]
+public sealed partial class VampireRoleComponent : BaseMindRoleComponent
+{
+}
diff --git a/Content.Server/Store/Conditions/BuyBeforeCondition.cs b/Content.Server/Store/Conditions/BuyBeforeCondition.cs
index fcfb5f92c9d..ad38923b669 100644
--- a/Content.Server/Store/Conditions/BuyBeforeCondition.cs
+++ b/Content.Server/Store/Conditions/BuyBeforeCondition.cs
@@ -1,3 +1,4 @@
+using Content.Server.Store.Systems;
using Content.Shared.Store;
using Content.Shared.Store.Components;
using Robust.Shared.Prototypes;
@@ -15,6 +16,7 @@ public sealed partial class BuyBeforeCondition : ListingCondition
///
/// Listing(s) that if bought, block this purchase, if any.
///
+ [DataField]
public HashSet>? Blacklist;
public override bool Condition(ListingConditionArgs args)
diff --git a/Content.Server/Store/Systems/StoreSystem.Ui.cs b/Content.Server/Store/Systems/StoreSystem.Ui.cs
index f12e52235f4..928dcde79bb 100644
--- a/Content.Server/Store/Systems/StoreSystem.Ui.cs
+++ b/Content.Server/Store/Systems/StoreSystem.Ui.cs
@@ -161,8 +161,8 @@ private void OnBuyRequest(EntityUid uid, StoreComponent component, StoreBuyListi
{
return;
}
- }
+ }
if (!IsOnStartingMap(uid, component))
component.RefundAllowed = false;
@@ -180,7 +180,8 @@ private void OnBuyRequest(EntityUid uid, StoreComponent component, StoreBuyListi
RaiseLocalEvent(buyer, ref ev);
// Sunrise-End
- }
+ }
+
//spawn entity
if (listing.ProductEntity != null)
{
@@ -257,6 +258,7 @@ private void OnBuyRequest(EntityUid uid, StoreComponent component, StoreBuyListi
if (upgradeActionId != null)
HandleRefundComp(uid, component, upgradeActionId.Value);
+
}
if (listing.ProductEvent != null)
@@ -267,6 +269,7 @@ private void OnBuyRequest(EntityUid uid, StoreComponent component, StoreBuyListi
RaiseLocalEvent(buyer, listing.ProductEvent);
}
+
//log dat shit.
_admin.Add(LogType.StorePurchase,
LogImpact.Low,
diff --git a/Content.Server/Vampire/VampireSystem.Abilities.cs b/Content.Server/Vampire/VampireSystem.Abilities.cs
new file mode 100644
index 00000000000..9891eafe6a3
--- /dev/null
+++ b/Content.Server/Vampire/VampireSystem.Abilities.cs
@@ -0,0 +1,762 @@
+using Content.Server.Bible.Components;
+using Content.Server.Body.Components;
+using Content.Server.Flash;
+using Content.Server.Flash.Components;
+using Content.Server.Speech.Components;
+using Content.Server.Storage.Components;
+using Content.Server.Store.Components;
+using Content.Shared.Actions;
+using Content.Server.Objectives.Components;
+using Content.Shared.Bed.Sleep;
+using Content.Shared.Body.Components;
+using Content.Shared.Chat.Prototypes;
+using Content.Shared.Chemistry.Components;
+using Content.Shared.Cuffs.Components;
+using Content.Shared.Damage;
+using Content.Shared.Database;
+using Content.Shared.DoAfter;
+using Content.Shared.FixedPoint;
+using Content.Shared.Flash;
+using Content.Shared.Humanoid;
+using Content.Shared.Interaction;
+using Content.Shared.Mobs;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Popups;
+using Content.Shared.Prying.Components;
+using Content.Shared.Stealth.Components;
+using Content.Shared.Store.Events;
+using Content.Shared.Store.Components;
+using Content.Shared.Stunnable;
+using Content.Shared.Vampire;
+using Content.Shared.Vampire.Components;
+using Content.Shared.Weapons.Melee;
+using FastAccessors;
+using Robust.Shared.Audio;
+using Robust.Shared.Containers;
+using Robust.Shared.Utility;
+using System.Collections.Frozen;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Content.Server.Vampire;
+
+public sealed partial class VampireSystem
+{
+ private FrozenDictionary _powerCache = default!;
+ private FrozenDictionary _passiveCache = default!;
+
+ private void InitializePowers()
+ {
+ _powerCache = BuildPowerCache();
+ _passiveCache = BuildPassiveCache();
+
+ //Abilities
+ SubscribeLocalEvent(OnVampireOpenMutationsMenu);
+ SubscribeLocalEvent(OnVampireToggleFangs);
+ SubscribeLocalEvent(OnVampireGlare);
+ SubscribeLocalEvent(OnVampireScreech);
+ SubscribeLocalEvent(OnVampirePolymorph);
+ SubscribeLocalEvent(OnVampireHypnotise);
+ SubscribeLocalEvent(OnVampireBloodSteal);
+ SubscribeLocalEvent(OnVampireCloakOfDarkness);
+
+ //Hypnotise
+ SubscribeLocalEvent(HypnotiseDoAfter);
+
+ //Drink Blood
+ SubscribeLocalEvent(OnInteractHandEvent);
+ SubscribeLocalEvent(DrinkDoAfter);
+
+ //Deaths embrace
+ SubscribeLocalEvent(OnInsertedIntoContainer);
+ SubscribeLocalEvent(OnRemovedFromContainer);
+ SubscribeLocalEvent(OnVampireStateChanged);
+
+ }
+
+ #region Ability Entry Points
+ private void OnVampireOpenMutationsMenu(EntityUid uid, VampireComponent component, VampireOpenMutationsMenu ev)
+ {
+ TryOpenUi(uid, ev.Performer, component);
+ ev.Handled = true;
+ }
+ private void OnVampireToggleFangs(EntityUid entity, VampireComponent component, VampireToggleFangsEvent ev)
+ {
+ if (!TryGetPowerDefinition(ev.DefinitionName, out var def))
+ return;
+
+ var vampire = new Entity(entity, component);
+
+ if (!IsAbilityUsable(vampire, def))
+ return;
+
+ var actionEntity = GetPowerEntity(vampire, def.ID);
+ if (actionEntity == null)
+ return;
+
+ var toggled = ToggleFangs(vampire);
+
+ _action.SetToggled(actionEntity, toggled);
+
+ ev.Handled = true;
+ }
+ private void OnVampireGlare(EntityUid entity, VampireComponent component, VampireGlareEvent ev)
+ {
+ if (!TryGetPowerDefinition(ev.DefinitionName, out var def))
+ return;
+
+ var vampire = new Entity(entity, component);
+
+ if (!IsAbilityUsable(vampire, def))
+ return;
+
+ Glare(vampire, ev.Target, def.Duration, def.Damage);
+
+ ev.Handled = true;
+ }
+ private void OnVampireScreech(EntityUid entity, VampireComponent component, VampireScreechEvent ev)
+ {
+ if (!TryGetPowerDefinition(ev.DefinitionName, out var def))
+ return;
+
+ var vampire = new Entity(entity, component);
+
+ if (!IsAbilityUsable(vampire, def))
+ return;
+
+ Screech(vampire, def.Duration, def.Damage);
+
+ ev.Handled = true;
+ }
+ private void OnVampirePolymorph(EntityUid entity, VampireComponent component, VampirePolymorphEvent ev)
+ {
+ if (!TryGetPowerDefinition(ev.DefinitionName, out var def))
+ return;
+
+ var vampire = new Entity(entity, component);
+
+ if (!IsAbilityUsable(vampire, def))
+ return;
+
+ PolymorphSelf(vampire, def.PolymorphTarget);
+
+ ev.Handled = true;
+ }
+ private void OnVampireHypnotise(EntityUid entity, VampireComponent component, VampireHypnotiseEvent ev)
+ {
+ if (!TryGetPowerDefinition(ev.DefinitionName, out var def))
+ return;
+
+ var vampire = new Entity(entity, component);
+
+ if (!IsAbilityUsable(vampire, def))
+ return;
+
+ ev.Handled = TryHypnotise(vampire, ev.Target, def.Duration, def.DoAfterDelay);
+ }
+ private void OnVampireBloodSteal(EntityUid entity, VampireComponent component, VampireBloodStealEvent ev)
+ {
+ if (!TryGetPowerDefinition(ev.DefinitionName, out var def))
+ return;
+
+ var vampire = new Entity(entity, component);
+
+ if (!IsAbilityUsable(vampire, def))
+ return;
+
+ BloodSteal(vampire);
+
+ ev.Handled = true;
+ }
+ private void OnVampireCloakOfDarkness(EntityUid entity, VampireComponent component, VampireCloakOfDarknessEvent ev)
+ {
+ if (!TryGetPowerDefinition(ev.DefinitionName, out var def))
+ return;
+
+ var vampire = new Entity(entity, component);
+
+ if (!IsAbilityUsable(vampire, def))
+ return;
+
+ var actionEntity = GetPowerEntity(vampire.Comp, def.ID);
+ if (actionEntity == null)
+ return;
+
+ var toggled = CloakOfDarkness(vampire, def.Upkeep, 0.75f, -0.5f);
+
+ _action.SetToggled(actionEntity, toggled);
+
+ ev.Handled = true;
+ }
+ private void OnInteractHandEvent(EntityUid uid, VampireComponent component, BeforeInteractHandEvent args)
+ {
+ if (!HasComp(args.Target))
+ return;
+
+ if (args.Target == uid)
+ return;
+
+ if (!TryGetPowerDefinition(VampireComponent.DrinkBloodPrototype, out var def))
+ return;
+
+ var vampire = new Entity(uid, component);
+
+ args.Handled = TryDrink(vampire, args.Target, def.DoAfterDelay!.Value);
+ }
+ #endregion
+
+
+ private bool TryGetPowerDefinition(string name, [NotNullWhen(true)] out VampirePowerProtype? definition)
+ => _powerCache.TryGetValue(name, out definition);
+
+ private bool IsAbilityUsable(Entity vampire, VampirePowerProtype def)
+ {
+ if (!IsPowerUnlocked(vampire, def.ID))
+ return false;
+
+ //Block if we are cuffed
+ if (!def.UsableWhileCuffed && TryComp(vampire, out var cuffable) && !cuffable.CanStillInteract)
+ {
+ _popup.PopupEntity(Loc.GetString("vampire-cuffed"), vampire, vampire, PopupType.MediumCaution);
+ return false;
+ }
+
+ //Block if we are stunned
+ if (!def.UsableWhileStunned && HasComp(vampire))
+ {
+ _popup.PopupEntity(Loc.GetString("vampire-stunned"), vampire, vampire, PopupType.MediumCaution);
+ return false;
+ }
+
+ //Block if we are muzzled - so far only one item does this?
+ if (!def.UsableWhileMuffled && TryComp(vampire, out var accent) && accent.Accent.Equals("mumble"))
+ {
+ _popup.PopupEntity(Loc.GetString("vampire-muffled"), vampire, vampire, PopupType.MediumCaution);
+ return false;
+ }
+
+ //Block if we dont have enough essence
+ if (def.ActivationCost > 0 && !SubtractBloodEssence(vampire, def.ActivationCost))
+ {
+ _popup.PopupClient(Loc.GetString("vampire-not-enough-blood"), vampire, vampire, PopupType.MediumCaution);
+ return false;
+ }
+
+ //Check if we are near an anchored prayable entity - ie the chapel
+ if (IsNearPrayable(vampire))
+ {
+ //Warning about holy power
+ return false;
+ }
+
+ return true;
+ }
+
+
+ #region Passive Powers
+ private void UnnaturalStrength(Entity vampire)
+ {
+ var damage = new DamageSpecifier();
+ damage.DamageDict.Add("Slash", 15);
+
+ var meleeComp = EnsureComp(vampire);
+ meleeComp.Damage += damage;
+ }
+ private void SupernaturalStrength(Entity vampire)
+ {
+ var pryComp = EnsureComp(vampire);
+ pryComp.Force = true;
+ pryComp.PryPowered = true;
+
+ var damage = new DamageSpecifier();
+ damage.DamageDict.Add("Slash", 15);
+
+ var meleeComp = EnsureComp(vampire);
+ meleeComp.Damage += damage;
+ }
+ #endregion
+
+ #region Other Powers
+ private void Screech(Entity vampire, TimeSpan? duration, DamageSpecifier? damage = null)
+ {
+ foreach (var entity in _entityLookup.GetEntitiesInRange(vampire, 3, LookupFlags.Approximate | LookupFlags.Dynamic | LookupFlags.Static | LookupFlags.Sundries))
+ {
+ if (HasComp(entity))
+ continue;
+
+ if (HasComp(entity))
+ continue;
+
+ if (HasComp(entity))
+ {
+ _stun.TryParalyze(entity, duration ?? TimeSpan.FromSeconds(3), false);
+ _chat.TryEmoteWithoutChat(entity, _prototypeManager.Index(VampireComponent.ScreamEmoteProto), true);
+ }
+
+ if (damage != null)
+ _damageableSystem.TryChangeDamage(entity, damage);
+ }
+ }
+ private void Glare(Entity vampire, EntityUid? target, TimeSpan? duration, DamageSpecifier? damage = null)
+ {
+ if (!target.HasValue)
+ return;
+
+ if (HasComp(target))
+ return;
+
+ if (HasComp(target))
+ return;
+
+ if (HasComp(target))
+ {
+ _stun.TryParalyze(vampire, duration ?? TimeSpan.FromSeconds(3), true);
+ _chat.TryEmoteWithoutChat(vampire.Owner, _prototypeManager.Index(VampireComponent.ScreamEmoteProto), true);
+ if (damage != null)
+ _damageableSystem.TryChangeDamage(vampire.Owner, damage);
+ return;
+ }
+
+ _stun.TryParalyze(target.Value, duration ?? TimeSpan.FromSeconds(3), true);
+ }
+ private void PolymorphSelf(Entity vampire, string? polymorphTarget)
+ {
+ if (polymorphTarget == null)
+ return;
+
+ _polymorph.PolymorphEntity(vampire, polymorphTarget);
+ }
+ private void BloodSteal(Entity vampire)
+ {
+ var transform = Transform(vampire.Owner);
+
+ var targets = new HashSet();
+
+ foreach (var entity in _entityLookup.GetEntitiesInRange(transform.Coordinates, 3, LookupFlags.Approximate | LookupFlags.Dynamic))
+ {
+ if (entity == vampire.Owner)
+ continue;
+
+ if (!HasComp(entity))
+ continue;
+
+ if (_rotting.IsRotten(entity))
+ continue;
+
+ if (HasComp(entity))
+ continue;
+
+ if (!TryComp(entity, out var bloodstream) || bloodstream.BloodSolution == null)
+ continue;
+
+ var victimBloodRemaining = bloodstream.BloodSolution.Value.Comp.Solution.Volume;
+ if (victimBloodRemaining <= 0)
+ continue;
+
+ var volumeToConsume = (FixedPoint2) Math.Min((float) victimBloodRemaining.Value, 20); //HARDCODE, 20u of blood per person per use
+
+ targets.Add(entity);
+
+ //Transfer 80% to the vampire
+ var bloodSolution = _solution.SplitSolution(bloodstream.BloodSolution.Value, volumeToConsume * 0.80);
+ //And spill 20% on the floor
+ _blood.TryModifyBloodLevel(entity, -(volumeToConsume * 0.2));
+
+ //Dont check this time, if we are full - just continue anyway
+ TryIngestBlood(vampire, bloodSolution);
+
+ AddBloodEssence(vampire, volumeToConsume * 0.80);
+
+ _beam.TryCreateBeam(vampire, entity, "Lightning");
+
+ _popup.PopupEntity(Loc.GetString("vampire-bloodsteal-other"), entity, entity, Shared.Popups.PopupType.LargeCaution);
+ }
+
+
+
+ //Update abilities, add new unlocks
+ //UpdateAbilities(vampire);
+ }
+ private bool CloakOfDarkness(Entity vampire, float upkeep, float passiveVisibilityRate, float movementVisibilityRate)
+ {
+ if (HasComp(vampire))
+ {
+ RemComp(vampire);
+ RemComp(vampire);
+ RemComp(vampire);
+ _popup.PopupEntity(Loc.GetString("vampire-cloak-disable"), vampire, vampire);
+ return false;
+ }
+ else
+ {
+ EnsureComp(vampire);
+ var stealthMovement = EnsureComp(vampire);
+ stealthMovement.PassiveVisibilityRate = passiveVisibilityRate;
+ stealthMovement.MovementVisibilityRate = movementVisibilityRate;
+ var vampireStealth = EnsureComp(vampire);
+ vampireStealth.Upkeep = upkeep;
+ _popup.PopupEntity(Loc.GetString("vampire-cloak-enable"), vampire, vampire);
+ return true;
+ }
+ }
+ #endregion
+
+ #region Hypnotise
+ private bool TryHypnotise(Entity vampire, EntityUid? target, TimeSpan? duration, TimeSpan? delay)
+ {
+ if (target == null)
+ return false;
+
+ var attempt = new FlashAttemptEvent(target.Value, vampire.Owner, vampire.Owner);
+ RaiseLocalEvent(target.Value, attempt, true);
+
+ if (attempt.Cancelled)
+ return false;
+
+ var doAfterEventArgs = new DoAfterArgs(EntityManager, vampire, delay ?? TimeSpan.FromSeconds(5),
+ new VampireHypnotiseDoAfterEvent() { Duration = duration },
+ eventTarget: vampire,
+ target: target,
+ used: target)
+ {
+ BreakOnMove = true,
+ BreakOnDamage = true,
+ MovementThreshold = 0.01f,
+ DistanceThreshold = 1.0f,
+ NeedHand = false,
+ };
+
+ if (_doAfter.TryStartDoAfter(doAfterEventArgs))
+ {
+ _popup.PopupEntity(Loc.GetString("vampire-hypnotise-other"), target.Value, Shared.Popups.PopupType.SmallCaution);
+ }
+ else
+ {
+ return false;
+ }
+ return true;
+ }
+ private void HypnotiseDoAfter(Entity vampire, ref VampireHypnotiseDoAfterEvent args)
+ {
+ if (!args.Target.HasValue)
+ return;
+
+ if (args.Cancelled)
+ return;
+
+ _statusEffects.TryAddStatusEffect(args.Target.Value, VampireComponent.SleepStatusEffectProto, args.Duration ?? TimeSpan.FromSeconds(30), false);
+ }
+ #endregion
+
+ #region Deaths Embrace
+ ///
+ /// When the vampire dies, attempt to activate the Deaths Embrace power
+ ///
+ private void OnVampireStateChanged(EntityUid uid, VampireDeathsEmbraceComponent component, MobStateChangedEvent args)
+ {
+ if (args.OldMobState != MobState.Dead && args.NewMobState == MobState.Dead)
+ {
+ //Home still exists?
+ TryMoveToCoffin((uid, component));
+ }
+ }
+ ///
+ /// When the vampire is inserted into a container (ie locker, crate etc) check for a coffin, and bind their home to it
+ ///
+ private void OnInsertedIntoContainer(EntityUid uid, VampireDeathsEmbraceComponent component, EntGotInsertedIntoContainerMessage args)
+ {
+ if (HasComp(args.Container.Owner))
+ {
+ component.HomeCoffin = args.Container.Owner;
+ var vhComp = EnsureComp(args.Entity);
+ vhComp.Healing = component.CoffinHealing;
+ _popup.PopupEntity(Loc.GetString("vampire-deathsembrace-bind"), uid, uid);
+ _admin.Add(LogType.Damaged, LogImpact.Low, $"{ToPrettyString(uid):user} bound a new coffin");
+
+ }
+ }
+ ///
+ /// When leaving a container, remove the healing component
+ ///
+ private void OnRemovedFromContainer(EntityUid uid, VampireDeathsEmbraceComponent component, EntGotRemovedFromContainerMessage args)
+ {
+ RemComp(args.Entity);
+ }
+ ///
+ /// Attempt to move the vampire to their bound coffin
+ ///
+ private bool TryMoveToCoffin(Entity vampire)
+ {
+ if (!vampire.Comp.HomeCoffin.HasValue)
+ return false;
+
+ //Someone smashed your crib bro'
+ if (!Exists(vampire.Comp.HomeCoffin.Value) || LifeStage(vampire.Comp.HomeCoffin.Value) >= EntityLifeStage.Terminating)
+ {
+ vampire.Comp.HomeCoffin = null;
+ return false;
+ }
+
+ //Your crib.. is not a crib, how?
+ if (!TryComp(vampire.Comp.HomeCoffin, out var coffinEntityStorage))
+ {
+ vampire.Comp.HomeCoffin = null;
+ return false;
+ }
+
+ //I guess its full?
+ if (!_entityStorage.CanInsert(vampire, vampire.Comp.HomeCoffin.Value, coffinEntityStorage))
+ return false;
+
+ Spawn("Smoke", Transform(vampire).Coordinates);
+
+ _entityStorage.CloseStorage(vampire.Comp.HomeCoffin.Value, coffinEntityStorage);
+
+ if (_entityStorage.Insert(vampire, vampire.Comp.HomeCoffin.Value, coffinEntityStorage))
+ {
+ _admin.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(vampire):user} was moved to their home coffin");
+ return true;
+ }
+
+ return false;
+ }
+ ///
+ /// Heal the vampire while they are in the coffin
+ /// Revive them if they are dead and below a ~150ish damage
+ ///
+ private void DoCoffinHeal(EntityUid vampire, VampireHealingComponent healing)
+ {
+ if (healing.Healing == null)
+ return;
+
+ _damageableSystem.TryChangeDamage(vampire, healing.Healing, true, origin: vampire);
+
+ //If they are dead and we are below the death threshold - revive
+ if (!TryComp(vampire, out var mobStateComponent))
+ return;
+
+ if (!_mobState.IsDead(vampire, mobStateComponent))
+ return;
+
+ if (!_mobThreshold.TryGetThresholdForState(vampire, MobState.Dead, out var threshold))
+ return;
+
+ if (!TryComp(vampire, out var damageableComponent))
+ return;
+
+ //Should be around 150 total damage ish
+ if (damageableComponent.TotalDamage < threshold * 0.75)
+ {
+ _mobState.ChangeMobState(vampire, MobState.Critical, mobStateComponent, vampire);
+ }
+ }
+ #endregion
+
+ #region Blood Drinking
+ ///
+ /// Toggle if fangs are extended
+ ///
+ private bool ToggleFangs(Entity vampire)
+ {
+ if (HasComp(vampire))
+ {
+ RemComp(vampire);
+ var popupText = Loc.GetString("vampire-fangs-retracted");
+ _admin.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(vampire):user} retracted their fangs");
+ _popup.PopupEntity(popupText, vampire.Owner, vampire.Owner);
+ return false;
+ }
+ else
+ {
+ EnsureComp(vampire);
+ var popupText = Loc.GetString("vampire-fangs-extended");
+ _admin.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(vampire):user} extended their fangs");
+ _popup.PopupEntity(popupText, vampire.Owner, vampire.Owner);
+ return true;
+ }
+ }
+ ///
+ /// Check and start drinking blood from a humanoid
+ ///
+ private bool TryDrink(Entity vampire, EntityUid target, TimeSpan doAfterDelay)
+ {
+ //Do a precheck
+ if (!HasComp(vampire))
+ return false;
+
+ if (!HasComp(vampire))
+ return false;
+
+ if (!_interaction.InRangeUnobstructed(vampire.Owner, target, popup: true))
+ return false;
+
+ if (_food.IsMouthBlocked(target, vampire))
+ return false;
+
+ if (_rotting.IsRotten(target))
+ {
+ _popup.PopupEntity(Loc.GetString("vampire-blooddrink-rotted"), vampire, vampire, PopupType.SmallCaution);
+ return false;
+ }
+
+ var doAfterEventArgs = new DoAfterArgs(EntityManager, vampire, doAfterDelay,
+ new VampireDrinkBloodDoAfterEvent() { Volume = vampire.Comp.MouthVolume },
+ eventTarget: vampire,
+ target: target,
+ used: target)
+ {
+ BreakOnMove = true,
+ BreakOnDamage = true,
+ MovementThreshold = 0.01f,
+ DistanceThreshold = 1.0f,
+ NeedHand = false,
+ Hidden = true
+ };
+
+ _doAfter.TryStartDoAfter(doAfterEventArgs);
+ return true;
+ }
+ private void DrinkDoAfter(Entity entity, ref VampireDrinkBloodDoAfterEvent args)
+ {
+ if (args.Cancelled)
+ return;
+
+ if (!HasComp(entity))
+ return;
+
+ if (_food.IsMouthBlocked(entity, entity))
+ return;
+
+ if (_rotting.IsRotten(args.Target!.Value))
+ {
+ _popup.PopupEntity(Loc.GetString("vampire-blooddrink-rotted"), args.User, args.User, PopupType.SmallCaution);
+ return;
+ }
+
+ if (!TryComp(args.Target, out var targetBloodstream) || targetBloodstream == null || targetBloodstream.BloodSolution == null)
+ return;
+
+ //Ensure there is enough blood to drain
+ var victimBloodRemaining = targetBloodstream.BloodSolution.Value.Comp.Solution.Volume;
+ if (victimBloodRemaining <= 0)
+ {
+ _popup.PopupEntity(Loc.GetString("vampire-blooddrink-empty"), entity.Owner, entity.Owner, PopupType.SmallCaution);
+ return;
+ }
+
+ var volumeToConsume = (FixedPoint2) Math.Min((float) victimBloodRemaining.Value, args.Volume);
+ var volumeToDrain = (FixedPoint2) Math.Min((float) victimBloodRemaining.Value, args.Volume * 8);
+
+ if (_mind.TryGetMind(entity, out var mindId, out var mind))
+ if (_mind.TryGetObjectiveComp(mindId, out var objective, mind))
+ objective.BloodDranked += entity.Comp.TotalBloodDrank;
+
+ //Slurp
+ _audio.PlayPvs(entity.Comp.BloodDrainSound, entity.Owner, AudioParams.Default.WithVolume(-3f));
+
+ //Spill an extra 5% on the floor
+ _blood.TryModifyBloodLevel(args.Target.Value, -(volumeToDrain * 0.05));
+
+ //Thou shall not feed upon the blood of the holy
+ //TODO: Replace with raised event?
+ if (HasComp(args.Target))
+ {
+ _damageableSystem.TryChangeDamage(entity, VampireComponent.HolyDamage, true);
+ _popup.PopupEntity(Loc.GetString("vampire-ingest-holyblood"), entity, entity, PopupType.LargeCaution);
+ _admin.Add(LogType.Damaged, LogImpact.Low, $"{ToPrettyString(entity):user} attempted to drink {volumeToConsume}u of {ToPrettyString(args.Target):target}'s holy blood");
+ return;
+ }
+ //Check for zombie
+ else
+ {
+ //Pull out some of the blood
+ var bloodSolution = _solution.SplitSolution(targetBloodstream.BloodSolution.Value, volumeToConsume);
+
+ if (!TryIngestBlood(entity, bloodSolution))
+ {
+ //Undo, put the blood back
+ _solution.AddSolution(targetBloodstream.BloodSolution.Value, bloodSolution);
+ return;
+ }
+
+ _admin.Add(LogType.Damaged, LogImpact.Low, $"{ToPrettyString(entity):user} drank {volumeToConsume}u of {ToPrettyString(args.Target):target}'s blood");
+ AddBloodEssence(entity, volumeToConsume * 0.95);
+
+ args.Repeat = true;
+ }
+ }
+ ///
+ /// Attempt to insert the solution into the first stomach that has space available
+ ///
+ private bool TryIngestBlood(Entity vampire, Solution ingestedSolution, bool force = false)
+ {
+ //Get all stomaches
+ if (TryComp(vampire.Owner, out var body) && _body.TryGetBodyOrganEntityComps((vampire.Owner, body), out var stomachs))
+ {
+ //Pick the first one that has space available
+ var firstStomach = stomachs.FirstOrNull(stomach => _stomach.CanTransferSolution(stomach.Owner, ingestedSolution, stomach.Comp1));
+ if (firstStomach == null)
+ {
+ //We are full
+ _popup.PopupEntity(Loc.GetString("vampire-full-stomach"), vampire.Owner, vampire.Owner, PopupType.SmallCaution);
+ return false;
+ }
+ //Fill the stomach with that delicious blood
+ return _stomach.TryTransferSolution(firstStomach.Value.Owner, ingestedSolution, firstStomach.Value.Comp1);
+ }
+
+ //No stomach
+ return false;
+ }
+ #endregion
+
+ private bool IsPowerUnlocked(VampireComponent vampire, string name)
+ {
+ return vampire.UnlockedPowers.ContainsKey(name);
+ }
+ /*private bool IsPowerActive(VampireComponent vampire, VampirePowerProtype def) => IsPowerActive(vampire, def.ID);
+ private bool IsPowerActive(VampireComponent vampire, string name)
+ {
+ return vampire.ActivePowers.Contains(name);
+ }
+ private bool SetPowerActive(VampireComponent vampire, string name, bool active)
+ {
+ if (active)
+ {
+ return vampire.ActivePowers.Add(name);
+ }
+ else
+ {
+ return vampire.ActivePowers.Remove(name);
+ }
+ }*/
+ ///
+ /// Gets the Action EntityUid for a specific power
+ ///
+ private EntityUid? GetPowerEntity(VampireComponent vampire, string name)
+ {
+ if (!vampire.UnlockedPowers.TryGetValue(name, out var ability))
+ return null;
+
+ return ability;
+ }
+
+ ///
+ /// Cache all power prototypes in a dictionary by keyed by ID
+ ///
+ ///
+ private FrozenDictionary BuildPowerCache()
+ {
+ var protos = _prototypeManager.EnumeratePrototypes();
+ return protos.ToFrozenDictionary(x => x.ID);
+ }
+
+ ///
+ /// Cache all passive prototypes in a dictionary by keyed by listing id
+ ///
+ ///
+ private FrozenDictionary BuildPassiveCache()
+ {
+ var protos = _prototypeManager.EnumeratePrototypes();
+ return protos.ToFrozenDictionary(x => x.CatalogEntry);
+ }
+}
diff --git a/Content.Server/Vampire/VampireSystem.Objectives.cs b/Content.Server/Vampire/VampireSystem.Objectives.cs
new file mode 100644
index 00000000000..3e94fb6494c
--- /dev/null
+++ b/Content.Server/Vampire/VampireSystem.Objectives.cs
@@ -0,0 +1,26 @@
+using Content.Server.Objectives.Components;
+using Content.Server.Objectives.Systems;
+using Content.Shared.Objectives.Components;
+using Content.Shared.Vampire.Components;
+using Content.Shared.Vampire;
+
+namespace Content.Server.Vampire;
+
+public sealed partial class VampireSystem
+{
+ [Dependency] private readonly NumberObjectiveSystem _number = default!;
+
+ private void InitializeObjectives()
+ {
+
+ SubscribeLocalEvent(OnBloodDrainGetProgress);
+ }
+
+ private void OnBloodDrainGetProgress(EntityUid uid, BloodDrainConditionComponent comp, ref ObjectiveGetProgressEvent args)
+ {
+ var target = _number.GetTarget(uid);
+ if (target != 0)
+ args.Progress = MathF.Min(comp.BloodDranked / target, 1f);
+ else args.Progress = 1f;
+ }
+}
\ No newline at end of file
diff --git a/Content.Server/Vampire/VampireSystem.Transform.cs b/Content.Server/Vampire/VampireSystem.Transform.cs
new file mode 100644
index 00000000000..21f29b67f90
--- /dev/null
+++ b/Content.Server/Vampire/VampireSystem.Transform.cs
@@ -0,0 +1,145 @@
+using Content.Server.Atmos.Components;
+using Content.Server.Body.Components;
+using Content.Server.Temperature.Components;
+using Content.Shared.Actions;
+using Content.Shared.Atmos;
+using Content.Shared.Atmos.Rotting;
+using Content.Shared.Body.Components;
+using Content.Shared.Chemistry.Reaction;
+using Content.Shared.Chemistry.Reagent;
+using Content.Shared.Chemistry;
+using Content.Shared.Nutrition.Components;
+using Content.Shared.Vampire;
+using Content.Shared.Vampire.Components;
+using Content.Shared.Weapons.Melee;
+using Robust.Shared.Audio;
+
+namespace Content.Server.Vampire;
+
+public sealed partial class VampireSystem
+{
+ ///
+ /// Convert the players into a vampire, all programatic because i dont want to replace the players body
+ ///
+ private void MakeVampire(EntityUid vampireUid)
+ {
+ var vampireComponent = EnsureComp(vampireUid);
+ var vampire = new Entity(vampireUid, vampireComponent);
+
+ //Render them unable to rot, immune to pressure and thirst
+ RemComp(vampire);
+ RemComp(vampire);
+ RemComp(vampire); //Unsure, should vampires thirst.. or hunger?
+
+ //Render immune to cold, but not heat
+ if (TryComp(vampire, out var temperatureComponent))
+ temperatureComponent.ColdDamageThreshold = Atmospherics.TCMB;
+
+ MakeVulnerableToHoly(vampire);
+
+ //Initialise currency
+ vampireComponent.Balance = new() { { VampireComponent.CurrencyProto, 0 } };
+
+ //Add the summon heirloom ability
+ AddStartingAbilities(vampire);
+
+ //Order of operation requirement, must be called after initialising balance
+ UpdateBloodDisplay(vampire);
+ }
+
+ ///
+ /// Add vulnerability to holy water when ingested or slashed, and take damage from the bible
+ ///
+ private void MakeVulnerableToHoly(Entity vampire)
+ {
+ //React to being beaten with the bible
+ EnsureComp(vampire);
+
+ //Take damage from holy water splash
+ if (TryComp(vampire, out var reactive))
+ {
+ if (reactive.ReactiveGroups == null)
+ reactive.ReactiveGroups = new();
+
+ if (!reactive.ReactiveGroups.ContainsKey("Unholy"))
+ {
+ reactive.ReactiveGroups.Add("Unholy", new() { ReactionMethod.Touch });
+ }
+ }
+
+ if (!TryComp(vampire, out var bodyComponent))
+ return;
+
+ //Add vampire and bloodsucker to all metabolizing organs
+ //And restrict diet to Pills (and liquids)
+ foreach (var organ in _body.GetBodyOrgans(vampire, bodyComponent))
+ {
+ if (TryComp(organ.Id, out var metabolizer))
+ {
+ if (TryComp(organ.Id, out var stomachComponent))
+ {
+ //Override the stomach, prevents humans getting sick when ingesting blood
+ _metabolism.ClearMetabolizerTypes(metabolizer);
+ _stomach.SetSpecialDigestible(stomachComponent, VampireComponent.AcceptableFoods);
+ }
+
+ _metabolism.TryAddMetabolizerType(metabolizer, VampireComponent.MetabolizerVampire);
+ _metabolism.TryAddMetabolizerType(metabolizer, VampireComponent.MetabolizerBloodsucker);
+ }
+ }
+ }
+
+ public void AddStartingAbilities(EntityUid vampire)
+ {
+ if (!TryComp(vampire, out var comp))
+ return;
+
+ foreach (var actionId in comp.BaseVampireActions)
+ {
+ var action = _action.AddAction(vampire, actionId);
+
+ if (!action.HasValue)
+ return;
+
+ if (!TryComp(action, out var instantActionComponent))
+ return;
+
+ var actionEvent = instantActionComponent.Event as VampireSelfPowerEvent;
+
+ if (actionEvent == null)
+ return;
+
+ comp.UnlockedPowers.Add(actionEvent.DefinitionName, action);
+
+ }
+
+ UpdateBloodDisplay(vampire);
+ }
+
+ //Remove weakeness to holy items
+ private void MakeImmuneToHoly(EntityUid vampire)
+ {
+ if (!TryComp(vampire, out var bodyComponent))
+ return;
+
+ //Add vampire and bloodsucker to all metabolizing organs
+ //And restrict diet to Pills (and liquids)
+ foreach (var organ in _body.GetBodyOrgans(vampire, bodyComponent))
+ {
+ if (TryComp(organ.Id, out var metabolizer))
+ {
+ _metabolism.TryRemoveMetabolizerType(metabolizer, VampireComponent.MetabolizerVampire);
+ }
+ }
+
+ if (TryComp(vampire, out var reactive))
+ {
+ if (reactive.ReactiveGroups == null)
+ return;
+
+ reactive.ReactiveGroups.Remove("Unholy");
+ }
+
+ RemComp(vampire);
+ }
+}
diff --git a/Content.Server/Vampire/VampireSystem.cs b/Content.Server/Vampire/VampireSystem.cs
new file mode 100644
index 00000000000..464a6e929f9
--- /dev/null
+++ b/Content.Server/Vampire/VampireSystem.cs
@@ -0,0 +1,482 @@
+using Content.Server.Administration.Logs;
+using Content.Server.Atmos.Rotting;
+using Content.Server.Beam;
+using Content.Server.Body.Systems;
+using Content.Server.Chat.Systems;
+using Content.Server.Interaction;
+using Content.Server.Nutrition.EntitySystems;
+using Content.Server.Polymorph.Systems;
+using Content.Server.Storage.EntitySystems;
+using Content.Server.Mind;
+using Content.Shared.Actions;
+using Content.Shared.Body.Systems;
+using Content.Shared.Buckle;
+using Content.Shared.Bed.Sleep;
+using Content.Shared.Chemistry.EntitySystems;
+using Content.Shared.Construction.Components;
+using Content.Shared.Damage;
+using Content.Shared.DoAfter;
+using Content.Shared.Examine;
+using Content.Shared.FixedPoint;
+using Content.Shared.Hands.EntitySystems;
+using Content.Shared.Humanoid;
+using Content.Shared.Interaction;
+using Content.Shared.Interaction.Events;
+using Content.Shared.Maps;
+using Content.Shared.Mobs;
+using Content.Shared.Mobs.Systems;
+using Content.Shared.Popups;
+using Content.Shared.Prayer;
+using Content.Shared.StatusEffect;
+using Content.Shared.Stunnable;
+using Content.Shared.Vampire;
+using Content.Shared.Vampire.Components;
+using Robust.Server.GameObjects;
+using Robust.Shared.Player;
+using Robust.Shared.GameStates;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Containers;
+using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
+using System.Linq;
+
+namespace Content.Server.Vampire;
+
+public sealed partial class VampireSystem : EntitySystem
+{
+ [Dependency] private readonly MindSystem _mind = default!;
+ [Dependency] private readonly IAdminLogManager _admin = default!;
+ [Dependency] private readonly FoodSystem _food = default!;
+ [Dependency] private readonly EntityStorageSystem _entityStorage = default!;
+ [Dependency] private readonly BloodstreamSystem _blood = default!;
+ [Dependency] private readonly RottingSystem _rotting = default!;
+ [Dependency] private readonly StomachSystem _stomach = default!;
+ [Dependency] private readonly PolymorphSystem _polymorph = default!;
+ [Dependency] private readonly ChatSystem _chat = default!;
+ [Dependency] private readonly BeamSystem _beam = default!;
+ [Dependency] private readonly SharedInteractionSystem _interaction = default!;
+ [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly SharedTransformSystem _transform = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+ [Dependency] private readonly IMapManager _mapMan = default!;
+ [Dependency] private readonly SharedMapSystem _mapSystem = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly SharedActionsSystem _action = default!;
+ [Dependency] private readonly ActionContainerSystem _actionContainer = default!;
+ [Dependency] private readonly SharedBodySystem _body = default!;
+ [Dependency] private readonly SharedSolutionContainerSystem _solution = default!;
+ [Dependency] private readonly SharedStunSystem _stun = default!;
+ [Dependency] private readonly StatusEffectsSystem _statusEffects = default!;
+ [Dependency] private readonly DamageableSystem _damageableSystem = default!;
+ [Dependency] private readonly MobThresholdSystem _mobThreshold = default!;
+ [Dependency] private readonly MobStateSystem _mobState = default!;
+ [Dependency] private readonly EntityLookupSystem _entityLookup = default!;
+ [Dependency] private readonly SharedHandsSystem _hands = default!;
+ [Dependency] private readonly MetabolizerSystem _metabolism = default!;
+ [Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
+
+ private Dictionary _actionEntities = new();
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnComponentStartup);
+
+ //SubscribeLocalEvent(OnUseSelfPower);
+ //SubscribeLocalEvent(OnUseTargetedPower);
+ SubscribeLocalEvent(OnExamined);
+ SubscribeLocalEvent(OnVampireBloodChangedEvent);
+
+ SubscribeLocalEvent(GetState);
+ SubscribeLocalEvent(OnMutationSelected);
+
+ InitializePowers();
+ InitializeObjectives();
+ }
+
+ ///
+ /// Handles healing and damaging in space
+ ///
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ var stealthQuery = EntityQueryEnumerator();
+ while (stealthQuery.MoveNext(out var uid, out var vampire, out var stealth))
+ {
+ if (vampire == null || stealth == null)
+ continue;
+
+ if (stealth.NextStealthTick <= 0)
+ {
+ stealth.NextStealthTick = 1;
+ if (!SubtractBloodEssence((uid, vampire), stealth.Upkeep))
+ RemCompDeferred(uid);
+ }
+ stealth.NextStealthTick -= frameTime;
+ }
+
+ var healingQuery = EntityQueryEnumerator();
+ while (healingQuery.MoveNext(out var uid, out _, out var healing))
+ {
+ if (healing == null)
+ continue;
+
+ if (healing.NextHealTick <= 0)
+ {
+ healing.NextHealTick = 1;
+ DoCoffinHeal(uid, healing);
+ }
+ healing.NextHealTick -= frameTime;
+ }
+
+ /*var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var vampireComponent))
+ {
+ var vampire = (uid, vampireComponent);
+
+ if (IsInSpace(uid))
+ {
+ if (vampireComponent.NextSpaceDamageTick <= 0)
+ {
+ vampireComponent.NextSpaceDamageTick = 1;
+ DoSpaceDamage(vampire);
+ }
+ vampireComponent.NextSpaceDamageTick -= frameTime;
+ }
+ }*/
+ }
+
+ private void OnComponentStartup(EntityUid uid, VampireComponent component, ComponentStartup args)
+ {
+ //MakeVampire(uid);
+ }
+
+ private void OnExamined(EntityUid uid, VampireComponent component, ExaminedEvent args)
+ {
+ if (HasComp(uid) && args.IsInDetailsRange && !_food.IsMouthBlocked(uid))
+ args.AddMarkup($"{Loc.GetString("vampire-fangs-extended-examine")}{Environment.NewLine}");
+ }
+ private bool AddBloodEssence(Entity vampire, FixedPoint2 quantity)
+ {
+ if (quantity < 0)
+ return false;
+
+ vampire.Comp.TotalBloodDrank += quantity.Float();
+ vampire.Comp.Balance[VampireComponent.CurrencyProto] += quantity;
+
+ UpdateBloodDisplay(vampire);
+
+ var ev = new VampireBloodChangedEvent();
+ RaiseLocalEvent(vampire, ev);
+
+ return true;
+ }
+ private bool SubtractBloodEssence(Entity vampire, FixedPoint2 quantity)
+ {
+ if (quantity < 0)
+ return false;
+
+ if (vampire.Comp.Balance[VampireComponent.CurrencyProto] < quantity)
+ return false;
+
+ vampire.Comp.Balance[VampireComponent.CurrencyProto] -= quantity;
+
+ UpdateBloodDisplay(vampire);
+
+ var ev = new VampireBloodChangedEvent();
+ RaiseLocalEvent(vampire, ev);
+
+ return true;
+ }
+ ///
+ /// Use the charges display on SummonHeirloom to show the remaining blood essence
+ ///
+ ///
+ public void UpdateBloodDisplay(EntityUid vampire)
+ {
+ if (!TryComp(vampire, out var comp))
+ return;
+
+ //Sanity check, you never know who is going to touch this code
+ if (!comp.Balance.TryGetValue(VampireComponent.CurrencyProto, out var balance))
+ return;
+
+ var chargeDisplay = (int) Math.Round((decimal) balance);
+ var mutationsAction = GetPowerEntity(comp, VampireComponent.MutationsActionPrototype);
+
+ if (mutationsAction == null)
+ return;
+
+ _action.SetCharges(mutationsAction, chargeDisplay);
+ }
+
+ private void OnVampireBloodChangedEvent(EntityUid uid, VampireComponent component, VampireBloodChangedEvent args)
+ {
+ EntityUid? newEntity = null;
+ EntityUid entity = default;
+ // Mutations
+ if (GetBloodEssence(uid) >= FixedPoint2.New(50) && !_actionEntities.TryGetValue(VampireComponent.MutationsActionPrototype, out entity))
+ {
+ _action.AddAction(uid, ref newEntity, VampireComponent.MutationsActionPrototype);
+ if (newEntity != null)
+ _actionEntities[VampireComponent.MutationsActionPrototype] = newEntity.Value;
+ }
+ else if (GetBloodEssence(uid) < FixedPoint2.New(50) && _actionEntities.TryGetValue(VampireComponent.MutationsActionPrototype, out entity))
+ {
+ if (!TryComp(uid, out ActionsComponent? comp))
+ return;
+
+ _action.RemoveAction(uid, entity, comp);
+ _actionContainer.RemoveAction(entity);
+ _actionEntities.Remove(VampireComponent.MutationsActionPrototype);
+ }
+
+ //Hemomancer
+
+ if (GetBloodEssence(uid) >= FixedPoint2.New(200) && !_actionEntities.TryGetValue("ActionVampireBloodSteal", out entity) && component.CurrentMutation == VampireMutationsType.Hemomancer)
+ {
+ _action.AddAction(uid, ref newEntity , "ActionVampireBloodSteal");
+ if (newEntity != null)
+ {
+ _actionEntities["ActionVampireBloodSteal"] = newEntity.Value;
+ if (!component.UnlockedPowers.ContainsKey("BloodSteal"))
+ {
+ component.UnlockedPowers.Add("BloodSteal", newEntity);
+ }
+ }
+ }
+ else if (GetBloodEssence(uid) < FixedPoint2.New(200) && _actionEntities.TryGetValue("ActionVampireBloodSteal", out entity))
+ {
+ if (!TryComp(uid, out ActionsComponent? comp))
+ return;
+
+ _action.RemoveAction(uid, entity, comp);
+ _actionContainer.RemoveAction(entity);
+ _actionEntities.Remove("ActionVampireBloodSteal");
+ }
+
+ if (GetBloodEssence(uid) >= FixedPoint2.New(300) && !_actionEntities.TryGetValue("ActionVampireScreech", out entity) && component.CurrentMutation == VampireMutationsType.Hemomancer)
+ {
+ _action.AddAction(uid, ref newEntity , "ActionVampireScreech");
+ if (newEntity != null)
+ {
+ _actionEntities["ActionVampireScreech"] = newEntity.Value;
+ if (!component.UnlockedPowers.ContainsKey("Screech"))
+ {
+ component.UnlockedPowers.Add("Screech", newEntity);
+ }
+ }
+ }
+ else if (GetBloodEssence(uid) < FixedPoint2.New(300) && _actionEntities.TryGetValue("ActionVampireScreech", out entity))
+ {
+ if (!TryComp(uid, out ActionsComponent? comp))
+ return;
+
+ _action.RemoveAction(uid, entity, comp);
+ _actionContainer.RemoveAction(entity);
+ _actionEntities.Remove("ActionVampireScreech");
+ }
+
+ //Umbrae
+
+ if (GetBloodEssence(uid) >= FixedPoint2.New(200) && !_actionEntities.TryGetValue("ActionVampireGlare", out entity) && component.CurrentMutation == VampireMutationsType.Umbrae)
+ {
+ _action.AddAction(uid, ref newEntity , "ActionVampireGlare");
+ if (newEntity != null)
+ {
+ _actionEntities["ActionVampireGlare"] = newEntity.Value;
+ if (!component.UnlockedPowers.ContainsKey("Glare"))
+ {
+ component.UnlockedPowers.Add("Glare", newEntity);
+ }
+ }
+ }
+ else if (GetBloodEssence(uid) < FixedPoint2.New(200) && _actionEntities.TryGetValue("ActionVampireGlare", out entity))
+ {
+ if (!TryComp(uid, out ActionsComponent? comp))
+ return;
+
+ _action.RemoveAction(uid, entity, comp);
+ _actionContainer.RemoveAction(entity);
+ _actionEntities.Remove("ActionVampireGlare");
+ }
+
+ if (GetBloodEssence(uid) >= FixedPoint2.New(300) && !_actionEntities.TryGetValue("ActionVampireCloakOfDarkness", out entity) && component.CurrentMutation == VampireMutationsType.Umbrae)
+ {
+ _action.AddAction(uid, ref newEntity , "ActionVampireCloakOfDarkness");
+ if (newEntity != null)
+ {
+ _actionEntities["ActionVampireCloakOfDarkness"] = newEntity.Value;
+ if (!component.UnlockedPowers.ContainsKey("CloakOfDarkness"))
+ {
+ component.UnlockedPowers.Add("CloakOfDarkness", newEntity);
+ }
+ }
+ }
+ else if (GetBloodEssence(uid) < FixedPoint2.New(300) && _actionEntities.TryGetValue("ActionVampireCloakOfDarkness", out entity))
+ {
+ if (!TryComp(uid, out ActionsComponent? comp))
+ return;
+
+ _action.RemoveAction(uid, entity, comp);
+ _actionContainer.RemoveAction(entity);
+ _actionEntities.Remove("ActionVampireCloakOfDarkness");
+ }
+
+ //Gargantua
+
+ if (GetBloodEssence(uid) >= FixedPoint2.New(200) && !_actionEntities.TryGetValue("ActionVampireUnnaturalStrength", out entity) && component.CurrentMutation == VampireMutationsType.Gargantua)
+ {
+ var vampire = new Entity(uid, component);
+
+ UnnaturalStrength(vampire);
+
+ _actionEntities["ActionVampireUnnaturalStrength"] = vampire;
+ }
+
+ if (GetBloodEssence(uid) >= FixedPoint2.New(300) && !_actionEntities.TryGetValue("ActionVampireSupernaturalStrength", out entity) && component.CurrentMutation == VampireMutationsType.Gargantua)
+ {
+ var vampire = new Entity(uid, component);
+
+ SupernaturalStrength(vampire);
+
+ _actionEntities["ActionVampireSupernaturalStrength"] = vampire;
+ }
+
+ //Bestia
+
+ if (GetBloodEssence(uid) >= FixedPoint2.New(200) && !_actionEntities.TryGetValue("ActionVampireBatform", out entity) && component.CurrentMutation == VampireMutationsType.Bestia)
+ {
+ _action.AddAction(uid, ref newEntity , "ActionVampireBatform");
+ if (newEntity != null)
+ {
+ _actionEntities["ActionVampireBatform"] = newEntity.Value;
+ if (!component.UnlockedPowers.ContainsKey("PolymorphBat"))
+ {
+ component.UnlockedPowers.Add("PolymorphBat", newEntity);
+ }
+ }
+ }
+ else if (GetBloodEssence(uid) < FixedPoint2.New(200) && _actionEntities.TryGetValue("ActionVampireBatform", out entity))
+ {
+ if (!TryComp(uid, out ActionsComponent? comp))
+ return;
+
+ _action.RemoveAction(uid, entity, comp);
+ _actionContainer.RemoveAction(entity);
+ _actionEntities.Remove("ActionVampireBatform");
+ }
+
+ if (GetBloodEssence(uid) >= FixedPoint2.New(300) && !_actionEntities.TryGetValue("ActionVampireMouseform", out entity) && component.CurrentMutation == VampireMutationsType.Bestia)
+ {
+ _action.AddAction(uid, ref newEntity , "ActionVampireMouseform");
+ if (newEntity != null)
+ {
+ _actionEntities["ActionVampireMouseform"] = newEntity.Value;
+ if (!component.UnlockedPowers.ContainsKey("PolymorphMouse"))
+ {
+ component.UnlockedPowers.Add("PolymorphMouse", newEntity);
+ }
+ }
+ }
+ else if (GetBloodEssence(uid) < FixedPoint2.New(300) && _actionEntities.TryGetValue("ActionVampireMouseform", out entity))
+ {
+ if (!TryComp(uid, out ActionsComponent? comp))
+ return;
+
+ _action.RemoveAction(uid, entity, comp);
+ _actionContainer.RemoveAction(entity);
+ _actionEntities.Remove("ActionVampireMouseform");
+ }
+ }
+
+ private FixedPoint2 GetBloodEssence(EntityUid vampire)
+ {
+ if (!TryComp(vampire, out var comp))
+ return 0;
+
+ if (!comp.Balance.TryGetValue(VampireComponent.CurrencyProto, out var val))
+ return 0;
+
+ return val;
+ }
+
+ private void DoSpaceDamage(Entity vampire)
+ {
+ _damageableSystem.TryChangeDamage(vampire, VampireComponent.SpaceDamage, true, origin: vampire);
+ _popup.PopupEntity(Loc.GetString("vampire-startlight-burning"), vampire, vampire, PopupType.LargeCaution);
+ }
+ private bool IsInSpace(EntityUid vampireUid)
+ {
+ var vampireTransform = Transform(vampireUid);
+ var vampirePosition = _transform.GetMapCoordinates(vampireTransform);
+
+ if (!_mapMan.TryFindGridAt(vampirePosition, out _, out var grid))
+ return true;
+
+ if (!_mapSystem.TryGetTileRef(vampireUid, grid, vampireTransform.Coordinates, out var tileRef))
+ return true;
+
+ return tileRef.Tile.IsEmpty || tileRef.IsSpace();
+ }
+
+ private bool IsNearPrayable(EntityUid vampireUid)
+ {
+ var mapCoords = _transform.GetMapCoordinates(vampireUid);
+
+ var nearbyPrayables = _entityLookup.GetEntitiesInRange(mapCoords, 5);
+ foreach (var prayable in nearbyPrayables)
+ {
+ if (Transform(prayable).Anchored)
+ return true;
+ }
+
+ return false;
+ }
+
+ private void OnMutationSelected(EntityUid uid, VampireComponent component, VampireMutationPrototypeSelectedMessage args)
+ {
+ if (component.CurrentMutation == args.SelectedId)
+ return;
+ ChangeMutation(uid, args.SelectedId, component);
+ }
+ private void ChangeMutation(EntityUid uid, VampireMutationsType newMutation, VampireComponent component)
+ {
+ var vampire = new Entity(uid, component);
+ if (SubtractBloodEssence(vampire, FixedPoint2.New(50)))
+ {
+ component.CurrentMutation = newMutation;
+ UpdateUi(uid, component);
+ var ev = new VampireBloodChangedEvent();
+ RaiseLocalEvent(uid, ev);
+ TryOpenUi(uid, component.Owner, component);
+ }
+ }
+
+ private void GetState(EntityUid uid, VampireComponent component, ref ComponentGetState args)
+ {
+ args.State = new VampireMutationComponentState
+ {
+ SelectedMutation = component.CurrentMutation
+ };
+ }
+
+ private void TryOpenUi(EntityUid uid, EntityUid user, VampireComponent? component = null)
+ {
+ if (!Resolve(uid, ref component))
+ return;
+ if (!TryComp(user, out ActorComponent? actor))
+ return;
+ _uiSystem.TryToggleUi(uid, VampireMutationUiKey.Key, actor.PlayerSession);
+ }
+
+ public void UpdateUi(EntityUid uid, VampireComponent? component = null)
+ {
+ if (!Resolve(uid, ref component))
+ return;
+ var state = new VampireMutationBoundUserInterfaceState(component.VampireMutations, component.CurrentMutation);
+ _uiSystem.SetUiState(uid, VampireMutationUiKey.Key, state);
+ }
+}
diff --git a/Content.Shared/Store/Events/StorePurchasedListingEvent.cs b/Content.Shared/Store/Events/StorePurchasedListingEvent.cs
new file mode 100644
index 00000000000..89ff18e1eef
--- /dev/null
+++ b/Content.Shared/Store/Events/StorePurchasedListingEvent.cs
@@ -0,0 +1,3 @@
+namespace Content.Shared.Store.Events;
+
+public record struct StorePurchasedListingEvent(EntityUid Purchaser, ListingData Listing, EntityUid? Item, EntityUid? Action);
diff --git a/Content.Shared/Store/ListingPrototype.cs b/Content.Shared/Store/ListingPrototype.cs
index 05ac5cc4cd5..f3b3e408c7f 100644
--- a/Content.Shared/Store/ListingPrototype.cs
+++ b/Content.Shared/Store/ListingPrototype.cs
@@ -1,5 +1,6 @@
using System.Linq;
using Content.Shared.FixedPoint;
+using Content.Shared.Store.Events;
using Content.Shared.Store.Components;
using Content.Shared.StoreDiscount.Components;
using Robust.Shared.Prototypes;
diff --git a/Content.Shared/Vampire/VampireComponent.cs b/Content.Shared/Vampire/VampireComponent.cs
new file mode 100644
index 00000000000..231ea6d2fd9
--- /dev/null
+++ b/Content.Shared/Vampire/VampireComponent.cs
@@ -0,0 +1,268 @@
+using Content.Shared.Body.Prototypes;
+using Content.Shared.Chat.Prototypes;
+using Content.Shared.Chemistry.Reagent;
+using Content.Shared.Damage;
+using Content.Shared.FixedPoint;
+using Content.Shared.StatusEffect;
+using Content.Shared.Store;
+using Content.Shared.Whitelist;
+using Robust.Shared.Audio;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Vampire.Components;
+
+[RegisterComponent]
+public sealed partial class VampireComponent : Component
+{
+ //Static prototype references
+ [ValidatePrototypeId]
+ public static readonly string SleepStatusEffectProto = "ForcedSleep";
+ [ValidatePrototypeId]
+ public static readonly string ScreamEmoteProto = "Scream";
+ [ValidatePrototypeId]
+ public static readonly string CurrencyProto = "BloodEssence";
+
+ [ViewVariables(VVAccess.ReadOnly), DataField("defaultMutation")]
+ public VampireMutationsType DefaultMutation = VampireMutationsType.None;
+ [ViewVariables(VVAccess.ReadOnly), DataField("currentMutation")]
+ public VampireMutationsType CurrentMutation = VampireMutationsType.None;
+
+ public readonly HashSet VampireMutations = new()
+ {
+ VampireMutationsType.None,
+ VampireMutationsType.Hemomancer,
+ VampireMutationsType.Umbrae,
+ VampireMutationsType.Gargantua,
+ //VampireMutationsType.Dantalion,
+ VampireMutationsType.Bestia
+ };
+
+ public static readonly EntityWhitelist AcceptableFoods = new()
+ {
+ Tags = new() { "Pill" }
+ };
+ [ValidatePrototypeId]
+ public static readonly string MetabolizerVampire = "Vampire";
+ [ValidatePrototypeId]
+ public static readonly string MetabolizerBloodsucker = "Bloodsucker";
+
+ public static readonly DamageSpecifier MeleeDamage = new()
+ {
+ DamageDict = new Dictionary() { { "Slash", 10 } }
+ };
+ public static readonly DamageSpecifier HolyDamage = new()
+ {
+ DamageDict = new Dictionary() { { "Burn", 10 } }
+ };
+ public static readonly DamageSpecifier SpaceDamage = new()
+ {
+ DamageDict = new Dictionary() { { "Burn", 2.5 } }
+ };
+
+ [ValidatePrototypeId]
+ public static readonly string MutationsActionPrototype = "ActionVampireOpenMutationsMenu";
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ public EntityUid? MutationsAction;
+
+ public readonly List> BaseVampireActions = new()
+ {
+ "ActionVampireToggleFangs",
+ "ActionVampireHypnotise"
+ };
+
+ [ValidatePrototypeId]
+ public static readonly string DrinkBloodPrototype = "DrinkBlood";
+
+ ///
+ /// Total blood drank, counter for end of round screen
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float TotalBloodDrank = 0;
+
+ ///
+ /// How much blood per mouthful
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float MouthVolume = 5;
+
+ ///
+ /// All unlocked abilities
+ ///
+ public Dictionary UnlockedPowers = new();
+
+ ///
+ /// Current available balance, used to sync currency across heirlooms and add essence as we feed
+ ///
+ public Dictionary, FixedPoint2> Balance = default!;
+
+ public readonly SoundSpecifier BloodDrainSound = new SoundPathSpecifier("/Audio/Items/drink.ogg", new AudioParams() { Volume = -3f, MaxDistance = 3f });
+ public readonly SoundSpecifier AbilityPurchaseSound = new SoundPathSpecifier("/Audio/Items/drink.ogg");
+}
+
+
+///
+/// Contains all details about the ability and its effects or restrictions
+///
+[DataDefinition]
+[Prototype("vampirePower")]
+public sealed partial class VampirePowerProtype : IPrototype
+{
+ [ViewVariables]
+ [IdDataField]
+ public string ID { get; private set; }
+
+ [DataField]
+ public float ActivationCost = 0;
+ [DataField]
+ public bool UsableWhileCuffed = true;
+ [DataField]
+ public bool UsableWhileStunned = true;
+ [DataField]
+ public bool UsableWhileMuffled = true;
+ [DataField]
+ public DamageSpecifier? Damage = default!;
+ [DataField]
+ public TimeSpan? Duration = TimeSpan.Zero;
+ [DataField]
+ public TimeSpan? DoAfterDelay = TimeSpan.Zero;
+ [DataField]
+ public string? PolymorphTarget = default!;
+ [DataField]
+ public float Upkeep = 0;
+}
+
+[DataDefinition]
+[Prototype("vampirePassive")]
+public sealed partial class VampirePassiveProtype : IPrototype
+{
+ [ViewVariables]
+ [IdDataField]
+ public string ID { get; private set; }
+
+ [DataField(required: true)]
+ public string CatalogEntry = string.Empty;
+
+ [DataField]
+ public ComponentRegistry CompsToAdd = new();
+
+ [DataField]
+ public ComponentRegistry CompsToRemove = new();
+}
+
+///
+/// Marks an entity as taking damage when hit by a bible, rather than being healed
+///
+[RegisterComponent]
+public sealed partial class UnholyComponent : Component { }
+
+///
+/// Marks a container as a coffin, for the purposes of vampire healing
+///
+[RegisterComponent]
+public sealed partial class CoffinComponent : Component { }
+
+[RegisterComponent]
+public sealed partial class VampireFangsExtendedComponent : Component { }
+
+///
+/// When added, heals the entity by the specified amount
+///
+[RegisterComponent]
+public sealed partial class VampireHealingComponent : Component
+{
+ public double NextHealTick = 0;
+
+ public DamageSpecifier? Healing = default!;
+}
+
+[RegisterComponent]
+public sealed partial class VampireDeathsEmbraceComponent : Component
+{
+ [ViewVariables()]
+ public EntityUid? HomeCoffin = default!;
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField]
+ public float Cost = 0;
+
+ [DataField]
+ public DamageSpecifier CoffinHealing = default!;
+}
+[RegisterComponent]
+public sealed partial class VampireSealthComponent : Component
+{
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float NextStealthTick = 0;
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float Upkeep = 0;
+}
+
+[Serializable, NetSerializable]
+public enum VampireMutationsType : byte
+{
+ None,
+ Hemomancer,
+ Umbrae,
+ Gargantua,
+ Dantalion,
+ Bestia
+}
+
+[Serializable, NetSerializable]
+public sealed class VampireMutationComponentState : ComponentState
+{
+ public VampireMutationsType SelectedMutation;
+}
+
+[Serializable, NetSerializable]
+public sealed class VampireMutationBoundUserInterfaceState : BoundUserInterfaceState
+{
+ public readonly HashSet MutationList;
+ public readonly VampireMutationsType SelectedMutation;
+
+ public VampireMutationBoundUserInterfaceState(HashSet mutationList, VampireMutationsType selectedId)
+ {
+ MutationList = mutationList;
+ SelectedMutation = selectedId;
+ }
+}
+
+[Serializable, NetSerializable]
+public sealed class VampireMutationPrototypeSelectedMessage : BoundUserInterfaceMessage
+{
+ public readonly VampireMutationsType SelectedId;
+
+ public VampireMutationPrototypeSelectedMessage(VampireMutationsType selectedId)
+ {
+ SelectedId = selectedId;
+ }
+}
+
+[Serializable, NetSerializable]
+public enum VampireMutationUiKey : byte
+{
+ Key
+}
+
+/*[Serializable, NetSerializable]
+public enum VampirePowerKey : byte
+{
+ ToggleFangs,
+ Glare,
+ DeathsEmbrace,
+ Screech,
+ Hypnotise,
+ Polymorph,
+ NecroticTouch,
+ BloodSteal,
+ CloakOfDarkness,
+ StellarWeakness,
+ SummonHeirloom,
+
+ //Passives
+ UnnaturalStrength,
+ SupernaturalStrength
+}*/
diff --git a/Content.Shared/Vampire/VampireEvents.cs b/Content.Shared/Vampire/VampireEvents.cs
new file mode 100644
index 00000000000..149a7d19ec6
--- /dev/null
+++ b/Content.Shared/Vampire/VampireEvents.cs
@@ -0,0 +1,61 @@
+using Content.Shared.Actions;
+using Content.Shared.DoAfter;
+using Content.Shared.Vampire.Components;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Shared.Vampire;
+
+//Use power events
+public sealed partial class VampireToggleFangsEvent : VampireSelfPowerEvent { }
+public sealed partial class VampireOpenMutationsMenu : InstantActionEvent { }
+public sealed partial class VampireScreechEvent : VampireSelfPowerEvent { }
+public sealed partial class VampirePolymorphEvent : VampireSelfPowerEvent { }
+public sealed partial class VampireBloodStealEvent : VampireSelfPowerEvent { }
+public sealed partial class VampireCloakOfDarknessEvent : VampireSelfPowerEvent { }
+
+public sealed partial class VampireGlareEvent : VampireTargetedPowerEvent { }
+public sealed partial class VampireHypnotiseEvent : VampireTargetedPowerEvent { }
+
+
+public abstract partial class VampireSelfPowerEvent : InstantActionEvent
+{
+ [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string DefinitionName = default!;
+};
+public abstract partial class VampireTargetedPowerEvent : EntityTargetActionEvent
+{
+ [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string DefinitionName = default!;
+};
+public sealed partial class VampirePassiveActionEvent : BaseActionEvent
+{
+ [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string DefinitionName = default!;
+};
+
+//Purchase passive events
+[Serializable, NetSerializable]
+public sealed partial class VampirePurchaseUnnaturalStrength : EntityEventArgs { }
+
+[Serializable, NetSerializable]
+public sealed partial class VampireBloodChangedEvent : EntityEventArgs { }
+
+//Doafter events
+[Serializable, NetSerializable]
+public sealed partial class VampireDrinkBloodDoAfterEvent : DoAfterEvent
+{
+ [DataField]
+ public float Volume = 0;
+
+ public override DoAfterEvent Clone() => this;
+}
+
+[Serializable, NetSerializable]
+public sealed partial class VampireHypnotiseDoAfterEvent : DoAfterEvent
+{
+ [DataField]
+ public TimeSpan? Duration = TimeSpan.Zero;
+
+ public override DoAfterEvent Clone() => this;
+}
diff --git a/Resources/Audio/Ambience/Antag/vampire_start.ogg b/Resources/Audio/Ambience/Antag/vampire_start.ogg
new file mode 100644
index 00000000000..8d8616f6a44
Binary files /dev/null and b/Resources/Audio/Ambience/Antag/vampire_start.ogg differ
diff --git a/Resources/Audio/Effects/Vampire/glare.ogg b/Resources/Audio/Effects/Vampire/glare.ogg
new file mode 100644
index 00000000000..44e46ea28ef
Binary files /dev/null and b/Resources/Audio/Effects/Vampire/glare.ogg differ
diff --git a/Resources/Audio/Effects/Vampire/screech_tone.ogg b/Resources/Audio/Effects/Vampire/screech_tone.ogg
new file mode 100644
index 00000000000..4a7e072542f
Binary files /dev/null and b/Resources/Audio/Effects/Vampire/screech_tone.ogg differ
diff --git a/Resources/Locale/en-US/_strings/Vampires/vampires.ftl b/Resources/Locale/en-US/_strings/Vampires/vampires.ftl
new file mode 100644
index 00000000000..4274692292e
--- /dev/null
+++ b/Resources/Locale/en-US/_strings/Vampires/vampires.ftl
@@ -0,0 +1,72 @@
+vampires-title = Vampires
+
+vampire-fangs-extended-examine = You see a glint of [color=white]sharp teeth[/color]
+vampire-fangs-extended = You extend your fangs
+vampire-fangs-retracted = You retract your fangs
+
+vampire-blooddrink-empty = This body is devoid of blood
+vampire-blooddrink-rotted = Their body is rotting and their blood tainted
+vampire-blooddrink-zombie = Their blood is tainted by death
+
+vampire-startlight-burning = You feel your skin burn in the light of a thousand suns
+
+vampire-not-enough-blood = You dont have enough blood
+vampire-cuffed = You need your hands free!
+vampire-stunned = You cant concentrate enough!
+vampire-muffled = Your mouth is muzzled
+vampire-full-stomach = You are bloated with blood
+
+vampire-deathsembrace-bind = Feels like home
+
+vampire-ingest-holyblood = Your mouth burns!
+
+vampire-cloak-enable = You wrap shadows around your form
+vampire-cloak-disable = You release your grip on the shadows
+
+vampire-bloodsteal-other = You feel blood being ripped from your body!
+vampire-hypnotise-other = {CAPITALIZE(THE($user))} stares deeply into {MAKEPLURAL(THE($target))} eyes!
+
+store-currency-display-blood-essence = Blood Essence
+store-category-vampirepowers = Powers
+store-category-vampirepassives = Passives
+
+#Powers
+vampire-power-summonheirloom = Summon Heirloom
+vampire-power-summonheirloom-description = Summon a family heirloom, gifted by lilith herself.
+
+vampire-power-blessing = Blessing of Lilith
+vampire-power-blessing-description = Swear your soul to Lilith, receive her blessing, and feast upon the bounty around you.
+
+vampire-power-togglefangs = Toggle Fangs
+vampire-power-togglefangs-description = Extend or retract your fangs. Walking around with your fangs out might reveal your true nature.
+
+vampire-power-glare = Glare
+vampire-power-glare-description = Release a blinding flash from your eyes, stunning a unprotected mortal for 10 seconds. Activation Cost: 20 Essence. Cooldown: 60 Seconds
+
+vampire-power-hypnotise = Hypnotise
+vampire-power-hypnotise-description = Stare deeply into a mortals eyes, forcing them to sleep for 60 seconds. Activation Cost: 20 Essence. Activation Delay: 5 Seconds. Cooldown: 5 Minutes
+
+vampire-power-screech = Screech
+vampire-power-screech-description = Release a piercing scream, stunning unprotected mortals and shattering fragile objects nearby. Activation Cost: 20 Essence. Activation Delay: 5 Seconds. Cooldown: 5 Minutes
+
+vampire-power-bloodsteal = Blood Steal
+vampire-power-bloodsteal-description = Wrench the blood from all bodies nearby - living or dead. Activation Cost: 20 Essence. Cooldown: 60 Seconds
+
+vampire-power-batform = Bat Form
+vampire-power-batform-description = Assume for form of a bat. Fast, Hard to Hit, Likes fruit. Activation Cost: 20 Essence. Cooldown: 30 Seconds
+
+vampire-power-mouseform = Mouse Form
+vampire-power-mouseform-description = Assume for form of a mouse. Fast, Small, Immune to doors. Activation Cost: 20 Essence. Cooldown: 30 Seconds
+
+vampire-power-cloakofdarkness = Cloak of Darkness
+vampire-power-cloakofdarkness-description = Cloak yourself from mortal eyes, rendering you invisible while stationary. Activation Cost: 30 Essence. Upkeep: 1 Essence/Second Cooldown: 10 Seconds
+
+#Passives
+vampire-passive-unholystrength = Unholy Strength
+vampire-passive-unholystrength-description = Infuse your upper body muscles with essence, granting you claws and increased strength. Effect: 10 Slash per hit
+
+vampire-passive-supernaturalstrength = Supernatural Strength
+vampire-passive-supernaturalstrength-description = Increase your upper body muscles strength further, no barrier shall stand in your way. Effect: 15 Slash per hit, able to pry open doors by hand.
+
+vampire-passive-deathsembrace = Deaths Embrace
+vampire-passive-deathsembrace-description = Embrace death and it shall pass you over. Effect: Heal when in a coffin, automatically return to your coffin upon death for 100 blood essence.
\ No newline at end of file
diff --git a/Resources/Locale/en-US/_strings/chapel/bible.ftl b/Resources/Locale/en-US/_strings/chapel/bible.ftl
index c59492b70a6..191aa870a7f 100644
--- a/Resources/Locale/en-US/_strings/chapel/bible.ftl
+++ b/Resources/Locale/en-US/_strings/chapel/bible.ftl
@@ -3,6 +3,9 @@ bible-heal-success-others = {CAPITALIZE(THE($user))} hits {THE($target)} with {T
bible-heal-success-none-self = You hit {THE($target)} with {THE($bible)}, but they have no wounds you can heal!
bible-heal-success-none-others = {CAPITALIZE(THE($user))} hits {THE($target)} with {THE($bible)}!
+bible-damage-unholy-self = You hit {THE($target)} with {THE($bible)}, and they are burned by a flash of holy fire!
+bible-damage-unholy-others = {CAPITALIZE(THE($user))} hits {THE($target)} with {THE($bible)}, and they are burned by a flash of holy fire!
+
bible-heal-fail-self = You hit {THE($target)} with {THE($bible)}, and it lands with a sad thwack, dazing {OBJECT($target)}!
bible-heal-fail-others = {CAPITALIZE(THE($user))} hits {THE($target)} with {THE($bible)}, and it lands with a sad thack, dazing {OBJECT($target)}!
bible-sizzle = The book sizzles in your hands!
diff --git a/Resources/Locale/ru-RU/_prototypes/actions/vampire.ftl b/Resources/Locale/ru-RU/_prototypes/actions/vampire.ftl
new file mode 100644
index 00000000000..7b1ba86a452
--- /dev/null
+++ b/Resources/Locale/ru-RU/_prototypes/actions/vampire.ftl
@@ -0,0 +1,18 @@
+ent-ActionVampireOpenMutationsMenu = Меню мутаций
+ .desc = Открывает меню с мутациями вампира.
+ent-ActionVampireToggleFangs = Высунуть клыки
+ .desc = Выдвигайте или втягивайте клыки. Если вы будете ходить с выпущенными клыками, это может раскрыть вашу истинную сущность.
+ent-ActionVampireGlare = Блик
+ .desc = Выпустите из глаз ослепительную вспышку, оглушающую незащищенного смертного на 10 секунд. Стоимость активации: 20 крови. Время перезарядки: 60 секунд
+ent-ActionVampireHypnotise = Гипноз
+ .desc = Глубоко загляните в глаза смертного, заставляя его уснуть на 60 секунд. Стоимость активации: 20 крови. Задержка активации: 5 секунд. Время перезарядки: 5 минут
+ent-ActionVampireScreech = Визг
+ .desc = Издайте пронзительный крик, оглушая незащищенных смертных и разбивая вдребезги хрупкие предметы поблизости. Стоимость активации: 20 крови. Задержка активации: 5 секунд. Время перезарядки: 5 минут
+ent-ActionVampireBloodSteal = Кража крови
+ .desc = Выжимает кровь из всех тел поблизости - живых или мертвых. Стоимость активации: 20 крови. Время перезарядки: 60 секунд
+ent-ActionVampireBatform = Форма летучей мыши
+ .desc = Принимает форму летучей мыши. Быстрая, трудноуязвимая, любит фрукты. Стоимость активации: 20 крови. Время перезарядки: 30 секунд
+ent-ActionVampireMouseform = Мышиная форма
+ .desc = Примите облик мыши. Быстрая, маленькая, невосприимчива к дверям. Стоимость активации: 20 крови. Время перезарядки: 30 секунд
+ent-ActionVampireCloakOfDarkness = Плащ тьмы
+ .desc = Замаскируйте себя от глаз смертных, делая вас невидимым в неподвижном состоянии. Стоимость активации: 30 крови. Трата: 1 кровь/секунда Время перезарядки: 10 секунд
\ No newline at end of file
diff --git a/Resources/Locale/ru-RU/_prototypes/objectives/vampire.ftl b/Resources/Locale/ru-RU/_prototypes/objectives/vampire.ftl
new file mode 100644
index 00000000000..801a1ecf49c
--- /dev/null
+++ b/Resources/Locale/ru-RU/_prototypes/objectives/vampire.ftl
@@ -0,0 +1,4 @@
+ent-VampireSurviveObjective = Выжить
+ .desc = Я должен выжить, чего бы этого мне не стоило.
+ent-VampireEscapeObjective = Улететь со станции живым и свободным.
+ .desc = Я должен улететь на эвакуационном шаттле. Свободным.
\ No newline at end of file
diff --git a/Resources/Locale/ru-RU/_strings/Vampires/vampires.ftl b/Resources/Locale/ru-RU/_strings/Vampires/vampires.ftl
new file mode 100644
index 00000000000..ea8bf7c56c3
--- /dev/null
+++ b/Resources/Locale/ru-RU/_strings/Vampires/vampires.ftl
@@ -0,0 +1,94 @@
+vampires-title = Вампиры
+
+vampire-fangs-extended-examine = Вы видите блеск [color=white]острых клыков[/color].
+vampire-fangs-extended = Вы вытягиваете свои клыки
+vampire-fangs-retracted = Вы втягиваете свои клыки
+
+vampire-blooddrink-empty = Это тело лишено крови
+vampire-blooddrink-rotted = Их тела гниют, а кровь запятнана
+vampire-blooddrink-zombie = Их кровь запятнана смертью
+
+vampire-startlight-burning = Вы чувствуете, как ваша кожа горит в свете тысячи солнц
+
+vampire-not-enough-blood = У вас недостаточно крови
+vampire-cuffed = Вам нужны свободные руки!
+vampire-stunned = Вы не сможете сосредоточиться!
+vampire-muffled = Ваш рот закрыт намордником
+vampire-full-stomach = Вас раздуло от крови
+
+vampire-deathsembrace-bind = Чувствуешь себя как дома
+
+vampire-ingest-holyblood = Ваш рот горит!
+
+vampire-cloak-enable = Вы окутываете тенью свою форму
+vampire-cloak-disable = Вы ослабляете хватку теней.
+
+vampire-bloodsteal-other = Вы чувствуете, как кровь вырывается из вашего тела!
+vampire-hypnotise-other = {CAPITALIZE(THE($user))} пристально вглядывается в {MAKEPLURAL(THE($target))} глаза!
+
+store-currency-display-blood-essence = Кровавая эссенция
+store-category-vampirepowers = Силы
+store-category-vampirepassives = Пассивные
+
+#Powers
+
+#Passives
+vampire-passive-unholystrength = Нечестивая сила
+vampire-passive-unholystrength-description = Наполните мышцы верхней части тела кровью, наделяя вас когтями и повышенной силой. Эффект: 10 порезов за удар
+
+vampire-passive-supernaturalstrength = Сверхъестественная сила
+vampire-passive-supernaturalstrength-description = Увеличьте силу мышц верхней части тела, и ни одна преграда не встанет на вашем пути. Эффект: 15 порезов за удар, возможность открывать двери руками.
+
+vampire-passive-deathsembrace = Объятия смерти
+vampire-passive-deathsembrace-description = Примите смерть, и она обойдет вас стороной. Эффект: исцеление в гробу, автоматическое возвращение в гроб после смерти за 100 эссенции крови.
+
+#Mutation menu
+
+vampire-mutation-menu-ui-window-name = Меню мутаций
+
+vampire-mutation-none-info = Ничего не выбрано
+
+vampire-mutation-hemomancer-info =
+ Гемомансер
+
+ Фокусируеться на кровавой магии и манипуляции крови вокруг себя.
+
+ Способности:
+
+ - Визг
+ - Кража крови
+
+vampire-mutation-umbrae-info =
+ Тень
+
+ Фокусируется на темноте, стелсе, мобильности.
+
+ Способности:
+
+ - Блик
+ - Плащ тьмы
+
+vampire-mutation-gargantua-info =
+ Гаргантюа
+
+ Фокусируется на ближнем уроне и стойкости.
+
+ Способности:
+
+ - Нечестивая сила
+ - Сверхъестественная сила
+
+vampire-mutation-bestia-info =
+ Бестия
+
+ Фокусируется на превращении и собирании трофеев
+
+ Способности:
+
+ - Форма летучей мыши
+ - Мышиная форма
+
+## Objectives
+
+objective-condition-drain-title = Выпить { $count } крови.
+objective-condition-drain-description = Я должен выпить { $count } крови. Это необходимо для моего выживания и дальнейшей эволюции.
\ No newline at end of file
diff --git a/Resources/Locale/ru-RU/_strings/administration/antag.ftl b/Resources/Locale/ru-RU/_strings/administration/antag.ftl
index 9e898ce238a..9939236391d 100644
--- a/Resources/Locale/ru-RU/_strings/administration/antag.ftl
+++ b/Resources/Locale/ru-RU/_strings/administration/antag.ftl
@@ -6,7 +6,8 @@ admin-verb-make-nuclear-operative = Сделать цель одиноким Я
admin-verb-make-pirate = Сделать цель пиратом\капером. Учтите, что это не меняет игровой режим.
admin-verb-make-head-rev = Сделать цель главой революции.
admin-verb-make-thief = Сделать цель вором.
-admin-verb-make-changeling = Сделать цель генокрадом
+admin-verb-make-changeling = Сделать цель генокрадом.
+admin-verb-make-vampire = Сделать цель вампиром.
admin-verb-text-make-traitor = Сделать предателем
admin-verb-text-make-initial-infected = Сделать нулевым пациентом
admin-verb-text-make-zombie = Сделать зомби
@@ -15,3 +16,4 @@ admin-verb-text-make-pirate = Сделать пиратом
admin-verb-text-make-head-rev = Сделать Главой революции
admin-verb-text-make-thief = Сделать вором
admin-verb-text-make-changeling = Сделать генокрадом
+admin-verb-text-make-vampire = Сделать вампиром
\ No newline at end of file
diff --git a/Resources/Locale/ru-RU/_strings/game-ticking/game-presets/preset-vampire.ftl b/Resources/Locale/ru-RU/_strings/game-ticking/game-presets/preset-vampire.ftl
new file mode 100644
index 00000000000..4df784c9a4a
--- /dev/null
+++ b/Resources/Locale/ru-RU/_strings/game-ticking/game-presets/preset-vampire.ftl
@@ -0,0 +1,12 @@
+vampire-roundend-name = вампир
+objective-issuer-vampire = [color=red]Жажда крови[/color]
+roundend-prepend-vampire-drained-named = [color=white]{ $name }[/color] выпил в общей сложности [color=red]{ $number }[/color] крови.
+roundend-prepend-vampire-drained = Кто-то выпил в общей сложности [color=red]{ $number }[/color] крови.
+vampire-gamemode-title = Вампиры
+vampire-gamemode-description = Кровожадные вампиры пробрались на станцию чтобы вдоволь напиться крови!
+vampire-role-greeting =
+ Вы вампир, который пробрался на станцию под видом работника!
+ Ваши задачи указаны в меню персонажа.
+ Пейте кровь и эволюционируйте, чтобы выполнить их!
+vampire-role-greeting-short = Вы вампир, который пробрался на станцию под видом работника!
+roles-antag-vamire-name = Вампир
\ No newline at end of file
diff --git a/Resources/Locale/ru-RU/_strings/metabolism/metabolizer-types.ftl b/Resources/Locale/ru-RU/_strings/metabolism/metabolizer-types.ftl
index 7a17abf3656..f3766a35c03 100644
--- a/Resources/Locale/ru-RU/_strings/metabolism/metabolizer-types.ftl
+++ b/Resources/Locale/ru-RU/_strings/metabolism/metabolizer-types.ftl
@@ -9,3 +9,4 @@ metabolizer-type-plant = Растение
metabolizer-type-dwarf = Дварф
metabolizer-type-moth = Ниан
metabolizer-type-arachnid = Арахнид
+metabolizer-type-vampire = Вампир
\ No newline at end of file
diff --git a/Resources/Prototypes/Actions/vampire.yml b/Resources/Prototypes/Actions/vampire.yml
new file mode 100644
index 00000000000..5f7872ef9c4
--- /dev/null
+++ b/Resources/Prototypes/Actions/vampire.yml
@@ -0,0 +1,176 @@
+- type: entity
+ id: ActionVampireOpenMutationsMenu
+ name: Mutations menu
+ description: "Opens menu with vampires mutations."
+ categories: [ HideSpawnMenu ]
+ components:
+ - type: InstantAction
+ itemIconStyle: NoItem
+ icon:
+ sprite: Interface/Actions/actions_vampire.rsi
+ state: summonheirloom
+ event:
+ !type:VampireOpenMutationsMenu
+ useDelay: 5
+
+- type: entity
+ id: ActionVampireToggleFangs
+ name: Toggle Fangs
+ description: "Extend or retract your fangs. Walking around with your fangs out might reveal your true nature."
+ categories: [ HideSpawnMenu ]
+ components:
+ - type: InstantAction
+ checkCanInteract: false
+ priority: 1
+ itemIconStyle: NoItem
+ icon:
+ sprite: Interface/Actions/actions_vampire.rsi
+ state: fangs_retracted
+ iconOn:
+ sprite: Interface/Actions/actions_vampire.rsi
+ state: fangs_extended
+ event:
+ !type:VampireToggleFangsEvent
+ definitionName: ToggleFangs
+
+- type: entity
+ id: ActionVampireGlare
+ name: Glare
+ description: "Release a blinding flash from your eyes, stunning a unprotected mortal for 10 seconds. Activation Cost: 20 Essence. Cooldown: 60 Seconds"
+ categories: [ HideSpawnMenu ]
+ components:
+ - type: EntityTargetAction
+ whitelist:
+ components:
+ - Body
+ priority: 2
+ itemIconStyle: NoItem
+ icon:
+ sprite: Interface/Actions/actions_vampire.rsi
+ state: glare
+ sound: !type:SoundPathSpecifier
+ path: /Audio/Effects/Vampire/glare.ogg
+ event:
+ !type:VampireGlareEvent
+ definitionName: Glare
+ useDelay: 60
+
+- type: entity
+ id: ActionVampireHypnotise
+ name: Hypnotise
+ description: "Stare deeply into a mortals eyes, forcing them to sleep for 60 seconds. Activation Cost: 20 Essence. Activation Delay: 5 Seconds. Cooldown: 5 Minutes"
+ categories: [ HideSpawnMenu ]
+ components:
+ - type: EntityTargetAction
+ whitelist:
+ components:
+ - HumanoidAppearance
+ canTargetSelf: false
+ interactOnMiss: false
+ priority: 2
+ itemIconStyle: NoItem
+ icon:
+ sprite: Interface/Actions/actions_vampire.rsi
+ state: hypnotise
+ event:
+ !type:VampireHypnotiseEvent
+ definitionName: Hypnotise
+ useDelay: 300
+
+- type: entity
+ id: ActionVampireScreech
+ name: Screech
+ description: "Release a piercing scream, stunning unprotected mortals and shattering fragile objects nearby. Activation Cost: 20 Essence. Activation Delay: 5 Seconds. Cooldown: 5 Minutes"
+ categories: [ HideSpawnMenu ]
+ components:
+ - type: InstantAction
+ checkCanInteract: false
+ priority: 2
+ itemIconStyle: NoItem
+ icon:
+ sprite: Interface/Actions/actions_vampire.rsi
+ state: screech
+ sound: !type:SoundPathSpecifier
+ path: /Audio/Effects/Vampire/screech_tone.ogg
+ event:
+ !type:VampireScreechEvent
+ definitionName: Screech
+ useDelay: 60
+
+- type: entity
+ id: ActionVampireBloodSteal
+ name: Blood Steal
+ description: "Wrench the blood from all bodies nearby - living or dead. Activation Cost: 20 Essence. Cooldown: 60 Seconds"
+ categories: [ HideSpawnMenu ]
+ components:
+ - type: InstantAction
+ checkCanInteract: false
+ priority: 2
+ itemIconStyle: NoItem
+ icon:
+ sprite: Interface/Actions/actions_vampire.rsi
+ state: bloodsteal
+ sound: !type:SoundPathSpecifier
+ path: /Audio/Effects/demon_consume.ogg
+ event:
+ !type:VampireBloodStealEvent
+ definitionName: BloodSteal
+ useDelay: 60
+
+- type: entity
+ id: ActionVampireBatform
+ name: Bat Form
+ description: "Assume for form of a bat. Fast, Hard to Hit, Likes fruit. Activation Cost: 20 Essence. Cooldown: 30 Seconds"
+ categories: [ HideSpawnMenu ]
+ components:
+ - type: InstantAction
+ checkCanInteract: false
+ priority: 2
+ itemIconStyle: NoItem
+ icon:
+ sprite: Interface/Actions/actions_vampire.rsi
+ state: batform
+ sound: !type:SoundPathSpecifier
+ path: /Audio/Effects/teleport_arrival.ogg
+ event:
+ !type:VampirePolymorphEvent
+ definitionName: PolymorphBat
+ useDelay: 30
+
+- type: entity
+ id: ActionVampireMouseform
+ name: Mouse Form
+ description: "Assume for form of a mouse. Fast, Small, Immune to doors. Activation Cost: 20 Essence. Cooldown: 30 Seconds"
+ categories: [ HideSpawnMenu ]
+ components:
+ - type: InstantAction
+ checkCanInteract: false
+ priority: 2
+ itemIconStyle: NoItem
+ icon:
+ sprite: Interface/Actions/actions_vampire.rsi
+ state: mouseform
+ sound: !type:SoundPathSpecifier
+ path: /Audio/Effects/teleport_arrival.ogg
+ event:
+ !type:VampirePolymorphEvent
+ definitionName: PolymorphMouse
+ useDelay: 30
+
+- type: entity
+ id: ActionVampireCloakOfDarkness
+ name: Cloak of Darkness
+ description: "Cloak yourself from mortal eyes, rendering you invisible while stationary. Activation Cost: 30 Essence. Upkeep: 1 Essence/Second Cooldown: 10 Seconds"
+ categories: [ HideSpawnMenu ]
+ components:
+ - type: InstantAction
+ checkCanInteract: false
+ priority: 2
+ itemIconStyle: NoItem
+ icon:
+ sprite: Interface/Actions/actions_vampire.rsi
+ state: cloakofdarkness
+ event:
+ !type:VampireCloakOfDarknessEvent
+ definitionName: CloakOfDarkness
+ useDelay: 10
\ No newline at end of file
diff --git a/Resources/Prototypes/Chemistry/metabolizer_types.yml b/Resources/Prototypes/Chemistry/metabolizer_types.yml
index 3f7bf05b35e..bc6502967b5 100644
--- a/Resources/Prototypes/Chemistry/metabolizer_types.yml
+++ b/Resources/Prototypes/Chemistry/metabolizer_types.yml
@@ -44,3 +44,7 @@
- type: metabolizerType
id: Arachnid
name: metabolizer-type-arachnid
+
+- type: metabolizerType
+ id: Vampire
+ name: metabolizer-type-vampire
diff --git a/Resources/Prototypes/Chemistry/reactive_groups.yml b/Resources/Prototypes/Chemistry/reactive_groups.yml
index fa001f3083d..d9773289708 100644
--- a/Resources/Prototypes/Chemistry/reactive_groups.yml
+++ b/Resources/Prototypes/Chemistry/reactive_groups.yml
@@ -6,3 +6,6 @@
- type: reactiveGroup
id: Acidic
+
+- type: reactiveGroup
+ id: Unholy
diff --git a/Resources/Prototypes/Entities/Effects/vampire.yml b/Resources/Prototypes/Entities/Effects/vampire.yml
new file mode 100644
index 00000000000..6db9ba6dc23
--- /dev/null
+++ b/Resources/Prototypes/Entities/Effects/vampire.yml
@@ -0,0 +1,25 @@
+- type: entity
+ name: Blood Drain
+ id: beam_blooddrain
+ components:
+ - type: Sprite
+ sprite: /Textures/Effects/lightning.rsi
+ drawdepth: Effects
+ layers:
+ - state: "lightning_1"
+ shader: unshaded
+ - type: Physics
+ canCollide: false
+ - type: PointLight
+ enabled: true
+ color: "#800000"
+ radius: 3.5
+ softness: 1
+ autoRot: true
+ castShadows: false
+ - type: Beam
+ - type: TimedDespawn
+ lifetime: 3
+ - type: Tag
+ tags:
+ - HideContextMenu
\ No newline at end of file
diff --git a/Resources/Prototypes/Entities/Mobs/Player/vampire.yml b/Resources/Prototypes/Entities/Mobs/Player/vampire.yml
new file mode 100644
index 00000000000..500b3884909
--- /dev/null
+++ b/Resources/Prototypes/Entities/Mobs/Player/vampire.yml
@@ -0,0 +1,19 @@
+- type: entity
+ name: bat
+ parent: MobBat
+ id: MobBatVampire
+ description: Some cultures find them terrifying, others crunchy on the teeth.
+ components:
+ - type: Tag
+ tags:
+ - DoorBumpOpener
+ - VimPilot
+ - CannotSuicide
+ - type: MindContainer
+ showExamineInfo: true
+ - type: NpcFactionMember
+ factions:
+ - PetsNT
+ - type: Alerts
+
+
\ No newline at end of file
diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml
index 417808b9bf2..97019626814 100644
--- a/Resources/Prototypes/Entities/Mobs/Species/base.yml
+++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml
@@ -195,6 +195,8 @@
type: StrippableBoundUserInterface
enum.StoreUiKey.Key:
type: StoreBoundUserInterface
+ enum.VampireMutationUiKey.Key:
+ type: VampireMutationBoundUserInterface
- type: Puller
- type: Speech
speechSounds: Alto
diff --git a/Resources/Prototypes/Entities/Objects/Specific/Chapel/bibles.yml b/Resources/Prototypes/Entities/Objects/Specific/Chapel/bibles.yml
index f5e91e4fd86..6e805ea11c3 100644
--- a/Resources/Prototypes/Entities/Objects/Specific/Chapel/bibles.yml
+++ b/Resources/Prototypes/Entities/Objects/Specific/Chapel/bibles.yml
@@ -18,6 +18,12 @@
damageOnUntrainedUse: ## What a non-chaplain takes when attempting to heal someone
groups:
Burn: 10
+ damageOnUnholyUse: ## What an unholy creature takes when picking up the bible
+ groups:
+ Burn: 30
+ damageUnholy: ## What damage is dealt when a chaplain hits an unholy creature
+ groups:
+ Burn: 20
- type: Prayable
bibleUserOnly: true
- type: Summonable
@@ -65,6 +71,12 @@
damageOnUntrainedUse:
types:
Caustic: 50
+ damageOnUnholyUse: ## What an unholy creature takes when picking up the bible
+ types:
+ Caustic: 50
+ damageUnholy: ## What damage is dealt when a chaplain hits an unholy creature
+ types:
+ types: 20
failChance: 0
locPrefix: "necro"
healSound: "/Audio/Effects/lightburn.ogg"
diff --git a/Resources/Prototypes/Entities/Structures/Storage/Crates/crates.yml b/Resources/Prototypes/Entities/Structures/Storage/Crates/crates.yml
index 3ed0ee53489..1ec71798ee5 100644
--- a/Resources/Prototypes/Entities/Structures/Storage/Crates/crates.yml
+++ b/Resources/Prototypes/Entities/Structures/Storage/Crates/crates.yml
@@ -508,6 +508,7 @@
max: 4
- !type:DoActsBehavior
acts: [ "Destruction" ]
+ - type: Coffin
- type: Construction
graph: CrateCoffin
node: cratecoffin
diff --git a/Resources/Prototypes/GameRules/midround.yml b/Resources/Prototypes/GameRules/midround.yml
index 73cbd6d4806..603ca28ccf1 100644
--- a/Resources/Prototypes/GameRules/midround.yml
+++ b/Resources/Prototypes/GameRules/midround.yml
@@ -45,6 +45,21 @@
lateJoinAdditional: true
mindRoles:
- MindRoleChangeling
+
+- type: entity
+ parent: BaseGameRule
+ id: Vampire
+ components:
+ - type: VampireRule
+ - type: AntagSelection
+ agentName: vampire-roundend-name
+ definitions:
+ - prefRoles: [ Vampire ]
+ max: 4
+ playerRatio: 30
+ lateJoinAdditional: true
+ mindRoles:
+ - MindRoleVampire
- type: entity
categories: [ HideSpawnMenu ]
diff --git a/Resources/Prototypes/Objectives/objectiveGroups.yml b/Resources/Prototypes/Objectives/objectiveGroups.yml
index 4129c329773..27cd55f2980 100644
--- a/Resources/Prototypes/Objectives/objectiveGroups.yml
+++ b/Resources/Prototypes/Objectives/objectiveGroups.yml
@@ -117,5 +117,5 @@
id: ThiefObjectiveGroupEscape
weights:
EscapeThiefShuttleObjective: 1
-
+
#Crew, wizard, when you code it...
diff --git a/Resources/Prototypes/Objectives/vampire.yml b/Resources/Prototypes/Objectives/vampire.yml
new file mode 100644
index 00000000000..184e6b4608e
--- /dev/null
+++ b/Resources/Prototypes/Objectives/vampire.yml
@@ -0,0 +1,171 @@
+# Base
+
+- type: entity
+ abstract: true
+ parent: BaseObjective
+ id: BaseVampireObjective
+ components:
+ - type: Objective
+ difficulty: 1.5
+ issuer: objective-issuer-vampire
+ - type: RoleRequirement
+ roles:
+ mindRoles:
+ - VampireRole
+
+- type: entity
+ abstract: true
+ parent: [BaseVampireObjective, BaseStealObjective]
+ id: BaseVampireStealObjective
+ components:
+ - type: StealCondition
+ verifyMapExistence: false
+ - type: Objective
+ difficulty: 1.5
+ - type: ObjectiveLimit
+ limit: 2
+
+# Steal
+
+- type: entity
+ parent: BaseVampireStealObjective
+ id: CMOHyposprayVampireStealObjective
+ components:
+ - type: NotJobRequirement
+ job: ChiefMedicalOfficer
+ - type: StealCondition
+ owner: job-name-cmo
+ stealGroup: Hypospray
+
+- type: entity
+ parent: BaseVampireStealObjective
+ id: RDHardsuitVampireStealObjective
+ components:
+ - type: NotJobRequirement
+ job: ResearchDirector
+ - type: StealCondition
+ owner: job-name-rd
+ stealGroup: ClothingOuterHardsuitRd
+ - type: Objective
+ difficulty: 1
+
+- type: entity
+ parent: BaseVampireStealObjective
+ id: EnergyShotgunVampireStealObjective
+ components:
+ - type: Objective
+ difficulty: 2
+ - type: NotJobRequirement
+ job: HeadOfSecurity
+ - type: StealCondition
+ stealGroup: WeaponEnergyShotgun
+ owner: job-name-hos
+
+- type: entity
+ parent: BaseVampireStealObjective
+ id: MagbootsVampireStealObjective
+ components:
+ - type: NotJobRequirement
+ job: ChiefEngineer
+ - type: StealCondition
+ stealGroup: ClothingShoesBootsMagAdv
+ owner: job-name-ce
+
+- type: entity
+ parent: BaseVampireStealObjective
+ id: ClipboardVampireStealObjective
+ components:
+ - type: NotJobRequirement
+ job: Quartermaster
+ - type: StealCondition
+ stealGroup: BoxFolderQmClipboard
+ owner: job-name-qm
+
+- type: entity
+ abstract: true
+ parent: BaseVampireStealObjective
+ id: BaseCaptainVampireObjective
+ components:
+ - type: Objective
+ difficulty: 2.5
+ - type: NotJobRequirement
+ job: Captain
+
+- type: entity
+ parent: BaseCaptainVampireObjective
+ id: CaptainIDVampireStealObjective
+ components:
+ - type: StealCondition
+ stealGroup: CaptainIDCard
+
+- type: entity
+ parent: BaseCaptainVampireObjective
+ id: CaptainJetpackVampireStealObjective
+ components:
+ - type: StealCondition
+ stealGroup: JetpackCaptainFilled
+
+- type: entity
+ parent: BaseCaptainVampireObjective
+ id: CaptainGunVampireStealObjective
+ components:
+ - type: StealCondition
+ stealGroup: WeaponAntiqueLaser
+ owner: job-name-captain
+
+# States
+
+- type: entity
+ parent: [BaseVampireObjective, BaseSurviveObjective]
+ id: VampireSurviveObjective
+ name: Survive
+ description: I must survive no matter what.
+ components:
+ - type: Objective
+ icon:
+ sprite: Interface/Actions/actions_vampire.rsi
+ state: deathsembrace
+
+- type: entity
+ parent: [BaseVampireObjective, BaseLivingObjective]
+ id: VampireEscapeObjective
+ name: Escape to centcomm alive and unrestrained.
+ description: I need to escape on the evacuation shuttle. Undercover.
+ components:
+ - type: Objective
+ difficulty: 1.3
+ icon:
+ sprite: Structures/Furniture/chairs.rsi
+ state: shuttle
+ - type: EscapeShuttleCondition
+
+# Kill
+
+- type: entity
+ parent: [BaseVampireObjective, BaseKillObjective]
+ id: VampireKillRandomPersonObjective
+ description: Do it however you like, just make sure they don't make it to centcomm.
+ components:
+ - type: Objective
+ difficulty: 1.75
+ unique: false
+ - type: TargetObjective
+ title: objective-condition-kill-person-title
+ - type: PickRandomPerson
+
+# Drain
+
+- type: entity
+ parent: BaseVampireObjective
+ id: VampireDrainObjective
+ components:
+ - type: Objective
+ icon:
+ sprite: Interface/Actions/actions_vampire.rsi
+ state: fangs_extended
+ - type: NumberObjective
+ min: 100
+ max: 500
+ title: objective-condition-drain-title
+ description: objective-condition-drain-description
+ - type: BloodDrainCondition
\ No newline at end of file
diff --git a/Resources/Prototypes/Polymorphs/polymorph.yml b/Resources/Prototypes/Polymorphs/polymorph.yml
index 96739a50d3c..a732a3ad7c0 100644
--- a/Resources/Prototypes/Polymorphs/polymorph.yml
+++ b/Resources/Prototypes/Polymorphs/polymorph.yml
@@ -186,3 +186,11 @@
forced: true
revertOnCrit: false
revertOnDeath: false
+
+#Vampire
+- type: polymorph
+ id: VampireBat
+ configuration:
+ entity: MobBat
+ revertOnDeath: true
+ revertOnCrit: true
diff --git a/Resources/Prototypes/Reagents/medicine.yml b/Resources/Prototypes/Reagents/medicine.yml
index 105ae110487..03e5af7fe8f 100644
--- a/Resources/Prototypes/Reagents/medicine.yml
+++ b/Resources/Prototypes/Reagents/medicine.yml
@@ -1059,10 +1059,28 @@
factor: 3
Medicine:
effects:
+ #If vampire
+ - !type:HealthChange
+ conditions:
+ - !type:OrganType
+ type: Vampire
+ damage:
+ types:
+ Heat: 2
+ - !type:Emote
+ conditions:
+ - !type:OrganType
+ type: Vampire
+ emote: Scream
+ probability: 0.3
+ #If not vampire
- !type:HealthChange
conditions:
- !type:TotalDamage
max: 50
+ - !type:OrganType
+ type: Vampire
+ shouldHave: false
damage:
types:
Blunt: -0.2
@@ -1071,6 +1089,15 @@
Shock: -0.2
Cold: -0.2
reactiveEffects:
+ Unholy:
+ methods: [ Touch ]
+ effects:
+ - !type:HealthChange
+ damage:
+ types:
+ Heat: 5
+ - !type:Emote
+ emote: Scream
Extinguish:
methods: [ Touch ]
effects:
diff --git a/Resources/Prototypes/Roles/Antags/vampire.yml b/Resources/Prototypes/Roles/Antags/vampire.yml
new file mode 100644
index 00000000000..8a3a04f0c74
--- /dev/null
+++ b/Resources/Prototypes/Roles/Antags/vampire.yml
@@ -0,0 +1,7 @@
+- type: antag
+ id: Vampire
+ name: roles-antag-vamire-name
+ antagonist: true
+ setPreference: true
+ objective: roles-antag-vampire-description
+# guides: [ Changelings ]
\ No newline at end of file
diff --git a/Resources/Prototypes/Roles/MindRoles/mind_roles.yml b/Resources/Prototypes/Roles/MindRoles/mind_roles.yml
index 8e7534424ae..87ee2a4ff10 100644
--- a/Resources/Prototypes/Roles/MindRoles/mind_roles.yml
+++ b/Resources/Prototypes/Roles/MindRoles/mind_roles.yml
@@ -189,3 +189,12 @@
- type: MindRole
antagPrototype: Changeling
- type: ChangelingRole
+
+- type: entity
+ parent: BaseMindRoleAntag
+ id: MindRoleVampire
+ name: Vampire Role
+ components:
+ - type: MindRole
+ antagPrototype: Vampire
+ - type: VampireRole
diff --git a/Resources/Prototypes/StatusIcon/faction.yml b/Resources/Prototypes/StatusIcon/faction.yml
index e8400480083..1d36a9fa408 100644
--- a/Resources/Prototypes/StatusIcon/faction.yml
+++ b/Resources/Prototypes/StatusIcon/faction.yml
@@ -63,3 +63,14 @@
icon:
sprite: /Textures/Interface/Misc/job_icons.rsi
state: Changeling
+
+- type: factionIcon
+ id: VampireFaction
+ priority: 11
+ showTo:
+ components:
+ - ShowAntagIcons
+ - Vampire
+ icon:
+ sprite: /Textures/Interface/Misc/job_icons.rsi
+ state: Vampire
diff --git a/Resources/Prototypes/Store/currency.yml b/Resources/Prototypes/Store/currency.yml
index b2e29f146ad..7e05f108371 100644
--- a/Resources/Prototypes/Store/currency.yml
+++ b/Resources/Prototypes/Store/currency.yml
@@ -10,6 +10,11 @@
displayName: store-currency-display-stolen-essence
canWithdraw: false
+- type: currency
+ id: BloodEssence
+ displayName: store-currency-display-blood-essence
+ canWithdraw: false
+
- type: currency
id: WizCoin
displayName: store-currency-display-wizcoin
diff --git a/Resources/Prototypes/Vampire/vampire-powers.yml b/Resources/Prototypes/Vampire/vampire-powers.yml
new file mode 100644
index 00000000000..17ff5bdb7aa
--- /dev/null
+++ b/Resources/Prototypes/Vampire/vampire-powers.yml
@@ -0,0 +1,99 @@
+- type: vampirePower
+ id: SummonHeirloom
+ usableWhileCuffed: false
+ usableWhileStunned: false
+
+- type: vampirePower
+ id: ToggleFangs
+
+- type: vampirePower
+ id: DrinkBlood
+ doAfterDelay: 1
+
+- type: vampirePower
+ id: Glare
+ duration: 10
+
+- type: vampirePower
+ id: Hypnotise
+ duration: 60
+ doAfterDelay: 5
+
+- type: vampirePower
+ id: Screech
+ duration: 3
+ damage:
+ types:
+ Blunt: 10
+ Structural: 40
+ usableWhileMuffled: false
+ activationCost: 10
+
+- type: vampirePower
+ id: BloodSteal
+ usableWhileStunned: false
+ usableWhileCuffed: false
+ activationCost: 20
+
+- type: vampirePower
+ id: PolymorphBat
+ usableWhileStunned: false
+ usableWhileCuffed: false
+ polymorphTarget: mobBatVampire
+ activationCost: 20
+
+- type: vampirePower
+ id: PolymorphMouse
+ usableWhileStunned: false
+ usableWhileCuffed: false
+ polymorphTarget: MobMouse
+ activationCost: 20
+
+- type: vampirePower
+ id: CloakOfDarkness
+ usableWhileStunned: false
+ activationCost: 30
+ upkeep: 1
+
+- type: vampirePassive
+ id: UnholyStrength
+ catalogEntry: VampireUnholyStrength # Must match the catalog id
+ compsToAdd:
+ - type: MeleeWeapon
+ damage:
+ types:
+ Slash: 10
+ animation: WeaponArcClaw
+ soundHit: /Audio/Weapons/slash.ogg
+
+- type: vampirePassive
+ id: SupernaturalStrength
+ catalogEntry: VampireUnnaturalStrength # Must match the catalog id
+ compsToAdd:
+ - type: MeleeWeapon
+ damage:
+ types:
+ Slash: 15
+ animation: WeaponArcClaw
+ soundHit: /Audio/Weapons/slash.ogg
+ - type: Prying
+ force: True
+ pryPowered: True
+
+- type: vampirePassive
+ id: DeathsEmbrace
+ catalogEntry: VampireDeathsEmbrace # Must match the catalog id
+ compsToAdd:
+ - type: VampireDeathsEmbrace
+ cost: 100
+ coffinHealing:
+ groups:
+ Toxin: 2
+ Genetic: 2
+ Airloss: 2
+ Brute: 2
+ types:
+ Burn: 1
+ Cold: 2
+ Shock: 2
+ Caustic: 2
\ No newline at end of file
diff --git a/Resources/Prototypes/game_presets.yml b/Resources/Prototypes/game_presets.yml
index d9fd4f661cc..8a6962de55f 100644
--- a/Resources/Prototypes/game_presets.yml
+++ b/Resources/Prototypes/game_presets.yml
@@ -271,3 +271,21 @@
- BasicStationEventScheduler
- MeteorSwarmScheduler
- BasicRoundstartVariation
+
+- type: gamePreset
+ id: Vampire
+ alias:
+ - vamp
+ - vamps
+ - vampire
+ - vampires
+ name: vampire-gamemode-title
+ description: vampire-gamemode-description
+ showInVote: true
+ minPlayers: 50 # Sunrise-Edit: 50 players for VAMPIRES
+ rules:
+ - Vampire
+ - SubGamemodesRule
+ - BasicStationEventScheduler
+ - MeteorSwarmScheduler
+ - BasicRoundstartVariation
diff --git a/Resources/Textures/Interface/Actions/actions_vampire.rsi/batform.png b/Resources/Textures/Interface/Actions/actions_vampire.rsi/batform.png
new file mode 100644
index 00000000000..4e01f86eae2
Binary files /dev/null and b/Resources/Textures/Interface/Actions/actions_vampire.rsi/batform.png differ
diff --git a/Resources/Textures/Interface/Actions/actions_vampire.rsi/bestia.png b/Resources/Textures/Interface/Actions/actions_vampire.rsi/bestia.png
new file mode 100644
index 00000000000..6819a99811c
Binary files /dev/null and b/Resources/Textures/Interface/Actions/actions_vampire.rsi/bestia.png differ
diff --git a/Resources/Textures/Interface/Actions/actions_vampire.rsi/bloodsteal.png b/Resources/Textures/Interface/Actions/actions_vampire.rsi/bloodsteal.png
new file mode 100644
index 00000000000..1d8df631083
Binary files /dev/null and b/Resources/Textures/Interface/Actions/actions_vampire.rsi/bloodsteal.png differ
diff --git a/Resources/Textures/Interface/Actions/actions_vampire.rsi/cloakofdarkness.png b/Resources/Textures/Interface/Actions/actions_vampire.rsi/cloakofdarkness.png
new file mode 100644
index 00000000000..a45cec69736
Binary files /dev/null and b/Resources/Textures/Interface/Actions/actions_vampire.rsi/cloakofdarkness.png differ
diff --git a/Resources/Textures/Interface/Actions/actions_vampire.rsi/dantalion.png b/Resources/Textures/Interface/Actions/actions_vampire.rsi/dantalion.png
new file mode 100644
index 00000000000..aa34ab26c4d
Binary files /dev/null and b/Resources/Textures/Interface/Actions/actions_vampire.rsi/dantalion.png differ
diff --git a/Resources/Textures/Interface/Actions/actions_vampire.rsi/deathsembrace.png b/Resources/Textures/Interface/Actions/actions_vampire.rsi/deathsembrace.png
new file mode 100644
index 00000000000..d74908cd402
Binary files /dev/null and b/Resources/Textures/Interface/Actions/actions_vampire.rsi/deathsembrace.png differ
diff --git a/Resources/Textures/Interface/Actions/actions_vampire.rsi/fangs_extended.png b/Resources/Textures/Interface/Actions/actions_vampire.rsi/fangs_extended.png
new file mode 100644
index 00000000000..6e93d2169bf
Binary files /dev/null and b/Resources/Textures/Interface/Actions/actions_vampire.rsi/fangs_extended.png differ
diff --git a/Resources/Textures/Interface/Actions/actions_vampire.rsi/fangs_retracted.png b/Resources/Textures/Interface/Actions/actions_vampire.rsi/fangs_retracted.png
new file mode 100644
index 00000000000..84ef3097330
Binary files /dev/null and b/Resources/Textures/Interface/Actions/actions_vampire.rsi/fangs_retracted.png differ
diff --git a/Resources/Textures/Interface/Actions/actions_vampire.rsi/fullpotential.png b/Resources/Textures/Interface/Actions/actions_vampire.rsi/fullpotential.png
new file mode 100644
index 00000000000..7cf40988b4a
Binary files /dev/null and b/Resources/Textures/Interface/Actions/actions_vampire.rsi/fullpotential.png differ
diff --git a/Resources/Textures/Interface/Actions/actions_vampire.rsi/gargantua.png b/Resources/Textures/Interface/Actions/actions_vampire.rsi/gargantua.png
new file mode 100644
index 00000000000..87b87604a56
Binary files /dev/null and b/Resources/Textures/Interface/Actions/actions_vampire.rsi/gargantua.png differ
diff --git a/Resources/Textures/Interface/Actions/actions_vampire.rsi/glare.png b/Resources/Textures/Interface/Actions/actions_vampire.rsi/glare.png
new file mode 100644
index 00000000000..44245909936
Binary files /dev/null and b/Resources/Textures/Interface/Actions/actions_vampire.rsi/glare.png differ
diff --git a/Resources/Textures/Interface/Actions/actions_vampire.rsi/hemomancer.png b/Resources/Textures/Interface/Actions/actions_vampire.rsi/hemomancer.png
new file mode 100644
index 00000000000..402ced18a0c
Binary files /dev/null and b/Resources/Textures/Interface/Actions/actions_vampire.rsi/hemomancer.png differ
diff --git a/Resources/Textures/Interface/Actions/actions_vampire.rsi/hypnotise.png b/Resources/Textures/Interface/Actions/actions_vampire.rsi/hypnotise.png
new file mode 100644
index 00000000000..8c5d431f5d2
Binary files /dev/null and b/Resources/Textures/Interface/Actions/actions_vampire.rsi/hypnotise.png differ
diff --git a/Resources/Textures/Interface/Actions/actions_vampire.rsi/meta.json b/Resources/Textures/Interface/Actions/actions_vampire.rsi/meta.json
new file mode 100644
index 00000000000..ea1d753d5ff
--- /dev/null
+++ b/Resources/Textures/Interface/Actions/actions_vampire.rsi/meta.json
@@ -0,0 +1,59 @@
+{
+ "copyright" : "Taken from https://github.com/goonstation/goonstation at https://github.com/goonstation/goonstation/commit/4059e4be90832b02b1228b1bee3db342094e4f1e",
+ "license" : "CC-BY-SA-3.0",
+ "version": 1,
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "fangs_retracted"
+ },
+ {
+ "name": "fangs_extended"
+ },
+ {
+ "name": "glare"
+ },
+ {
+ "name": "screech"
+ },
+ {
+ "name": "hypnotise"
+ },
+ {
+ "name": "bloodsteal"
+ },
+ {
+ "name": "batform"
+ },
+ {
+ "name": "mouseform"
+ },
+ {
+ "name": "summonheirloom"
+ },
+ {
+ "name": "cloakofdarkness"
+ },
+ {
+ "name": "unholystrength"
+ },
+ {
+ "name": "supernaturalstrength"
+ },
+ {
+ "name": "deathsembrace"
+ },
+ {
+ "name": "mutation"
+ },
+ {
+ "name": "stellarweakness"
+ },
+ {
+ "name": "fullpotential"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Resources/Textures/Interface/Actions/actions_vampire.rsi/mouseform.png b/Resources/Textures/Interface/Actions/actions_vampire.rsi/mouseform.png
new file mode 100644
index 00000000000..88fcf650119
Binary files /dev/null and b/Resources/Textures/Interface/Actions/actions_vampire.rsi/mouseform.png differ
diff --git a/Resources/Textures/Interface/Actions/actions_vampire.rsi/mutation.png b/Resources/Textures/Interface/Actions/actions_vampire.rsi/mutation.png
new file mode 100644
index 00000000000..c063a59c355
Binary files /dev/null and b/Resources/Textures/Interface/Actions/actions_vampire.rsi/mutation.png differ
diff --git a/Resources/Textures/Interface/Actions/actions_vampire.rsi/screech.png b/Resources/Textures/Interface/Actions/actions_vampire.rsi/screech.png
new file mode 100644
index 00000000000..d9ac7edba56
Binary files /dev/null and b/Resources/Textures/Interface/Actions/actions_vampire.rsi/screech.png differ
diff --git a/Resources/Textures/Interface/Actions/actions_vampire.rsi/stellarweakness.png b/Resources/Textures/Interface/Actions/actions_vampire.rsi/stellarweakness.png
new file mode 100644
index 00000000000..e9aaeb59cfd
Binary files /dev/null and b/Resources/Textures/Interface/Actions/actions_vampire.rsi/stellarweakness.png differ
diff --git a/Resources/Textures/Interface/Actions/actions_vampire.rsi/summonheirloom.png b/Resources/Textures/Interface/Actions/actions_vampire.rsi/summonheirloom.png
new file mode 100644
index 00000000000..824c3e96309
Binary files /dev/null and b/Resources/Textures/Interface/Actions/actions_vampire.rsi/summonheirloom.png differ
diff --git a/Resources/Textures/Interface/Actions/actions_vampire.rsi/supernaturalstrength.png b/Resources/Textures/Interface/Actions/actions_vampire.rsi/supernaturalstrength.png
new file mode 100644
index 00000000000..39963eb9ef9
Binary files /dev/null and b/Resources/Textures/Interface/Actions/actions_vampire.rsi/supernaturalstrength.png differ
diff --git a/Resources/Textures/Interface/Actions/actions_vampire.rsi/umbrae.png b/Resources/Textures/Interface/Actions/actions_vampire.rsi/umbrae.png
new file mode 100644
index 00000000000..15a1240b89d
Binary files /dev/null and b/Resources/Textures/Interface/Actions/actions_vampire.rsi/umbrae.png differ
diff --git a/Resources/Textures/Interface/Actions/actions_vampire.rsi/unholystrength.png b/Resources/Textures/Interface/Actions/actions_vampire.rsi/unholystrength.png
new file mode 100644
index 00000000000..b549cb0ced2
Binary files /dev/null and b/Resources/Textures/Interface/Actions/actions_vampire.rsi/unholystrength.png differ
diff --git a/Resources/Textures/Interface/Misc/job_icons.rsi/Vampire.png b/Resources/Textures/Interface/Misc/job_icons.rsi/Vampire.png
new file mode 100644
index 00000000000..dc0cd27fb34
Binary files /dev/null and b/Resources/Textures/Interface/Misc/job_icons.rsi/Vampire.png differ
diff --git a/Resources/Textures/Interface/Misc/job_icons.rsi/meta.json b/Resources/Textures/Interface/Misc/job_icons.rsi/meta.json
index 14c4e356941..11dcd61f94e 100644
--- a/Resources/Textures/Interface/Misc/job_icons.rsi/meta.json
+++ b/Resources/Textures/Interface/Misc/job_icons.rsi/meta.json
@@ -205,6 +205,9 @@
},
{
"name": "Changeling"
+ },
+ {
+ "name": "Vampire"
}
]
}
diff --git a/Tools/_sunrise/Schemas/ignore_list.yml b/Tools/_sunrise/Schemas/ignore_list.yml
index 2342d75058a..9b1d3adaac0 100644
--- a/Tools/_sunrise/Schemas/ignore_list.yml
+++ b/Tools/_sunrise/Schemas/ignore_list.yml
@@ -137,13 +137,14 @@ ignore_list:
- 'TR-263'
ignore_files:
- - italian.ftl
- - russian.ftl
- - popup.ftl
- - controls.ftl
- - input.ftl
- - speech-chatsan.ftl
- - speech-liar.ftl
- - german.ftl
- - southern.ftl
- - tts-voices-sunrise.ftl
\ No newline at end of file
+ - 'italian.ftl'
+ - 'russian.ftl'
+ - 'popup.ftl'
+ - 'controls.ftl'
+ - 'input.ftl'
+ - 'speech-chatsan.ftl'
+ - 'speech-liar.ftl'
+ - 'german.ftl'
+ - 'southern.ftl'
+ - 'tts-voices-sunrise.ftl'
+ - 'lobby.ftl'
\ No newline at end of file