Time to Nuke Vampire (#416)

* timetonukevampire

* snap back to vampirity

* firstfix

* fixix xif
This commit is contained in:
Zekins
2026-05-29 02:29:16 +03:00
committed by GitHub
parent bb7a30b8ee
commit a43291233d
135 changed files with 8447 additions and 3092 deletions
@@ -0,0 +1,97 @@
using Content.Shared.NullRod.Components;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Enums;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Client.NullRod;
public sealed class NullDamageOverlay : Overlay
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
private static readonly ProtoId<ShaderPrototype> NullDamageShader = "NullDamage";
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
public override bool RequestScreenTexture => true;
private readonly ShaderInstance _shader;
private float _currentRatio;
private float _targetRatio;
public NullDamageOverlay()
{
IoCManager.InjectDependencies(this);
_shader = _prototypeManager.Index(NullDamageShader).InstanceUnique();
_currentRatio = 0f;
_targetRatio = 0f;
}
protected override bool BeforeDraw(in OverlayDrawArgs args)
{
if (!_entityManager.TryGetComponent(_playerManager.LocalEntity, out EyeComponent? eyeComp))
return false;
if (args.Viewport.Eye != eyeComp.Eye)
return false;
return true;
}
protected override void Draw(in OverlayDrawArgs args)
{
if (ScreenTexture == null)
return;
var playerEntity = _playerManager.LocalEntity;
var frameTime = (float)_gameTiming.CurTime.TotalSeconds;
if (playerEntity == null || !_entityManager.TryGetComponent<NullDamageComponent>(playerEntity, out var nullDamage))
{
_targetRatio = 0f;
UpdateRatio(frameTime);
_shader.SetParameter("SCREEN_TEXTURE", ScreenTexture);
_shader.SetParameter("ratio", _currentRatio);
var handle = args.ScreenHandle;
handle.UseShader(_shader);
handle.DrawRect(args.ViewportBounds, Color.White);
handle.UseShader(null);
return;
}
var rawRatio = nullDamage.MaxNullDamage > 0
? (float)(nullDamage.NullDamage / nullDamage.MaxNullDamage)
: 0f;
_targetRatio = Math.Clamp(rawRatio, 0f, 0.85f);
UpdateRatio(frameTime);
_shader.SetParameter("SCREEN_TEXTURE", ScreenTexture);
_shader.SetParameter("ratio", _currentRatio);
var drawHandle = args.ScreenHandle;
drawHandle.UseShader(_shader);
drawHandle.DrawRect(args.ViewportBounds, Color.White);
drawHandle.UseShader(null);
}
private void UpdateRatio(float frameTime)
{
const float maxChangePerSecond = 3f;
var maxDelta = maxChangePerSecond * frameTime;
var difference = _targetRatio - _currentRatio;
var delta = Math.Clamp(difference, -maxDelta, maxDelta);
float smoothingSpeed = 5f;
delta *= smoothingSpeed * frameTime;
delta = Math.Clamp(delta, -maxDelta, maxDelta);
_currentRatio += delta;
_currentRatio = Math.Clamp(_currentRatio, 0f, 0.85f);
}
}
@@ -0,0 +1,49 @@
using Content.Shared.NullRod.Components;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Player;
namespace Content.Client.NullRod;
public sealed class NullDamageSystem : EntitySystem
{
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IOverlayManager _overlayMan = default!;
private NullDamageOverlay _overlay = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NullDamageComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<NullDamageComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<NullDamageComponent, LocalPlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<NullDamageComponent, LocalPlayerDetachedEvent>(OnPlayerDetached);
_overlay = new();
}
private void OnPlayerAttached(EntityUid uid, NullDamageComponent component, LocalPlayerAttachedEvent args)
{
_overlayMan.AddOverlay(_overlay);
}
private void OnPlayerDetached(EntityUid uid, NullDamageComponent component, LocalPlayerDetachedEvent args)
{
_overlayMan.RemoveOverlay(_overlay);
}
private void OnInit(EntityUid uid, NullDamageComponent component, ComponentInit args)
{
if (_player.LocalEntity == uid)
_overlayMan.AddOverlay(_overlay);
}
private void OnShutdown(EntityUid uid, NullDamageComponent component, ComponentShutdown args)
{
if (_player.LocalEntity == uid)
_overlayMan.RemoveOverlay(_overlay);
}
}
@@ -0,0 +1,41 @@
using Content.Client.Eui;
using Content.Shared.Eui;
using Content.Shared.Vampire;
using JetBrains.Annotations;
namespace Content.Client._Wega.Vampire.Ui;
[UsedImplicitly]
public sealed class DissectSelectionEui : BaseEui
{
private DissectSelectionMenu _menu;
public DissectSelectionEui()
{
_menu = new DissectSelectionMenu();
_menu.OnOrganSelected += selectedOrgan =>
{
SendMessage(new DissectOrganSelectedMessage(selectedOrgan));
_menu.Close();
};
}
public override void Opened()
{
_menu.OpenCentered();
}
public override void Closed()
{
_menu.Close();
}
public override void HandleState(EuiStateBase state)
{
base.HandleState(state);
if (state is not DissectSelectionEuiState dissectState)
return;
_menu.Populate(dissectState);
}
}
@@ -0,0 +1,5 @@
<ui:RadialMenu xmlns="https://spacestation14.io"
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
MinSize="450 450">
<ui:RadialContainer Name="MainContainer" VerticalExpand="True" HorizontalExpand="True" InitialRadius="128"/>
</ui:RadialMenu>
@@ -0,0 +1,72 @@
using System.Numerics;
using Content.Client.UserInterface.Controls;
using Content.Shared.Vampire;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
namespace Content.Client._Wega.Vampire.Ui;
[GenerateTypedNameReferences]
public sealed partial class DissectSelectionMenu : RadialMenu
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
public event Action<NetEntity>? OnOrganSelected;
public DissectSelectionMenu()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
}
public void Populate(DissectSelectionEuiState state)
{
MainContainer.RemoveAllChildren();
foreach (var organNetEntity in state.AvailableOrgans)
{
var organEntity = _entityManager.GetEntity(organNetEntity);
if (!_entityManager.TryGetComponent<MetaDataComponent>(organEntity, out var metaData))
continue;
var prototypeId = metaData.EntityPrototype?.ID ?? string.Empty;
var button = new RadialMenuButton
{
ToolTip = metaData.EntityName,
SetSize = new Vector2(64, 64),
StyleClasses = { "RadialMenuButton" }
};
// Try to get icon for organ
if (!string.IsNullOrEmpty(prototypeId) && _prototype.TryIndex(prototypeId, out var prototype))
{
var entityView = new EntityPrototypeView
{
Scale = new Vector2(2, 2),
SetSize = new Vector2(64, 64),
Margin = new Thickness(4)
};
entityView.SetPrototype(prototype.ID);
button.AddChild(entityView);
}
else
{
// Fallback to text if no icon
var label = new Label
{
Text = metaData.EntityName,
Align = Label.AlignMode.Center,
HorizontalExpand = true,
VerticalExpand = true
};
button.AddChild(label);
}
var capturedOrgan = organNetEntity;
button.OnPressed += _ => OnOrganSelected?.Invoke(capturedOrgan);
MainContainer.AddChild(button);
}
}
}
@@ -1,43 +0,0 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.Vampire;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Player;
namespace Content.Client.Select.Class.UI;
[GenerateTypedNameReferences]
public sealed partial class SelectClassMenu : RadialMenu
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IEntityNetworkManager _entityNetworkManager = default!;
[Dependency] private readonly ISharedPlayerManager _playerManager = default!;
public event Action<string>? OnSelectClass;
public SelectClassMenu()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
InitializeButtons();
}
private void InitializeButtons()
{
HemomancerButton.OnButtonUp += _ => HandleClassSelection("Hemomancer");
UmbraeButton.OnButtonUp += _ => HandleClassSelection("Umbrae");
GargantuaButton.OnButtonUp += _ => HandleClassSelection("Gargantua");
DantalionButton.OnButtonUp += _ => HandleClassSelection("Dantalion");
//BestiaButton.OnButtonUp += _ => HandleClassSelection("Bestia");
}
private void HandleClassSelection(string className)
{
OnSelectClass?.Invoke(className);
var netEntity = _entityManager.GetNetEntity(_playerManager.LocalSession?.AttachedEntity ?? EntityUid.Invalid);
_entityNetworkManager.SendSystemNetworkMessage(new VampireSelectClassMenuClosedEvent(netEntity, className));
Close();
}
}
@@ -1,46 +0,0 @@
using Content.Client.Select.Class.UI;
using Content.Shared.Vampire;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
namespace Content.Client.UserInterface.Systems.Select.Class
{
public sealed class SelectClassUIController : UIController
{
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
private SelectClassMenu? _menu;
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<SelectClassPressedEvent>(OnSelectClassMenuReceived);
}
private void OnSelectClassMenuReceived(SelectClassPressedEvent args, EntitySessionEventArgs eventArgs)
{
var session = IoCManager.Resolve<IPlayerManager>().LocalSession;
var userEntity = _entityManager.GetEntity(args.Uid);
if (session?.AttachedEntity.HasValue == true && session.AttachedEntity.Value == userEntity)
{
if (_menu is null)
{
_menu = _uiManager.CreateWindow<SelectClassMenu>();
_menu.OnClose += OnMenuClosed;
_menu.OpenCentered();
}
else
{
_menu.OpenCentered();
}
}
}
private void OnMenuClosed()
{
_menu = null;
}
}
}
@@ -0,0 +1,68 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
Title="{Loc 'vampire-trophies-ui-title'}"
MinSize="550 680">
<PanelContainer Name="WindowBackground">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#0a0a0f" />
</PanelContainer.PanelOverride>
</PanelContainer>
<ScrollContainer VerticalExpand="True">
<BoxContainer Orientation="Vertical" Margin="10">
<!-- Trophies -->
<Label Text="{Loc 'vampire-trophies-ui-trophies'}" StyleClasses="LabelHeading"
HorizontalAlignment="Center" Margin="0 0 0 5" FontColorOverride="#c92a2a"/>
<PanelContainer Margin="0 0 0 10">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#4cc9f0" ContentMarginBottomOverride="2" ContentMarginLeftOverride="2"/>
</PanelContainer.PanelOverride>
</PanelContainer>
<PanelContainer StyleClasses="AngleRect" Margin="0 0 0 10">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#14141f" BorderColor="#c92a2a" BorderThickness="2" />
</PanelContainer.PanelOverride>
<GridContainer Name="OrganGrid" Columns="6" HorizontalExpand="True" Margin="5"/>
</PanelContainer>
<!-- Vampire Passives -->
<Label Text="{Loc 'vampire-trophies-ui-passives'}" StyleClasses="LabelHeading"
HorizontalAlignment="Center" Margin="0 15 0 5" FontColorOverride="#c92a2a"/>
<PanelContainer Margin="0 0 0 10">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#4cc9f0" ContentMarginBottomOverride="2" ContentMarginLeftOverride="2"/>
</PanelContainer.PanelOverride>
</PanelContainer>
<PanelContainer StyleClasses="AngleRect" Margin="0 0 0 10">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#14141f" BorderColor="#c92a2a" BorderThickness="2" />
</PanelContainer.PanelOverride>
<GridContainer Name="PassiveGrid" Columns="2" HSeparationOverride="15" VSeparationOverride="15"
HorizontalAlignment="Center" HorizontalExpand="True" Margin="5"/>
</PanelContainer>
<!-- Abilities -->
<Label Text="{Loc 'vampire-trophies-ui-abilities'}" StyleClasses="LabelHeading"
HorizontalAlignment="Center" Margin="0 15 0 5" FontColorOverride="#c92a2a"/>
<PanelContainer Margin="0 0 0 10">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#4cc9f0" ContentMarginBottomOverride="2" ContentMarginLeftOverride="2"/>
</PanelContainer.PanelOverride>
</PanelContainer>
<PanelContainer StyleClasses="AngleRect" Margin="0 0 0 10">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#14141f" BorderColor="#c92a2a" BorderThickness="2" />
</PanelContainer.PanelOverride>
<GridContainer Name="AbilitiesContainer" Columns="4" HorizontalAlignment="Center"
HorizontalExpand="True" Margin="5"/>
</PanelContainer>
</BoxContainer>
</ScrollContainer>
</controls:FancyWindow>
@@ -0,0 +1,155 @@
using System.Numerics;
using System.Text;
using System.Text.RegularExpressions;
using Content.Client.UserInterface.Controls;
using Content.Shared.Actions.Components;
using Content.Shared.Vampire;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
namespace Content.Client._Wega.Vampire.Ui;
[GenerateTypedNameReferences]
public sealed partial class TrophiesMenu : FancyWindow
{
[Dependency] private readonly IEntityManager _entityManager = default!;
private readonly SpriteSystem _spriteSystem;
private static readonly Dictionary<BestiaOrganType, EntProtoId> DefaultOrganProto = new()
{
{ BestiaOrganType.Heart, "OrganHumanHeart" },
{ BestiaOrganType.Lungs, "OrganHumanLungs" },
{ BestiaOrganType.Liver, "OrganHumanLiver" },
{ BestiaOrganType.Kidneys, "OrganHumanKidneys" },
{ BestiaOrganType.Eyes, "OrganHumanEyes" },
{ BestiaOrganType.Stomach, "OrganHumanStomach" }
};
private static readonly Regex ColorTagRegex = new Regex(
@"\[color=[^\]]+\]|\[/color\]",
RegexOptions.Compiled | RegexOptions.CultureInvariant
);
public TrophiesMenu()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_spriteSystem = _entityManager.System<SpriteSystem>();
}
public void Populate(TrophiesEuiState state)
{
OrganGrid.RemoveAllChildren();
foreach (var organ in state.Organs)
{
var widget = CreateOrganWidget(organ);
OrganGrid.AddChild(widget);
}
PassiveGrid.RemoveAllChildren();
foreach (var bonus in state.PassiveBonuses)
{
var label = new Label
{
Text = $"{bonus.Name}: {bonus.Value}" + (bonus.IsMaxed ? $" {Loc.GetString($"vampire-trophies-ui-max")}" : ""),
HorizontalExpand = true,
Margin = new Thickness(0, 2)
};
if (bonus.ValueColor != null) label.ModulateSelfOverride = bonus.ValueColor.Value;
PassiveGrid.AddChild(label);
}
AbilitiesContainer.RemoveAllChildren();
foreach (var ability in state.Abilities)
{
var button = new TextureButton
{
ToolTip = BuildTooltip(ability),
Scale = new Vector2(3f, 3f),
VerticalExpand = true,
Margin = new Thickness(5)
};
var entity = _entityManager.GetEntity(ability.Action);
if (_entityManager.TryGetComponent<ActionComponent>(entity, out var action) && action.Icon != null)
button.TextureNormal = _spriteSystem.Frame0(action.Icon);
AbilitiesContainer.AddChild(button);
}
}
private Control CreateOrganWidget(OrganDisplayInfo organ)
{
var container = new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Vertical,
HorizontalExpand = true,
VerticalExpand = true,
Margin = new Thickness(5)
};
var iconProto = DefaultOrganProto.GetValueOrDefault(organ.Type, "OrganHumanHeart");
string? actualProto = null;
if (organ.PreviewEntity != null)
{
var entity = _entityManager.GetEntity(organ.PreviewEntity.Value);
if (_entityManager.TryGetComponent<MetaDataComponent>(entity, out var meta))
actualProto = meta.EntityPrototype?.ID;
}
if (string.IsNullOrEmpty(actualProto))
actualProto = iconProto;
var icon = new EntityPrototypeView
{
Scale = new Vector2(2f, 2f),
HorizontalAlignment = HAlignment.Center,
VerticalAlignment = VAlignment.Center,
Margin = new Thickness(0, 0, 0, 2)
};
icon.SetPrototype(actualProto);
var countLabel = new Label
{
Text = $"{organ.Count}/{organ.MaxCount}"
+ $"\n{Loc.GetString($"vampire-bestia-{organ.Type.ToString().ToLower()}").ToUpper()}",
Align = Label.AlignMode.Center,
HorizontalExpand = true,
ModulateSelfOverride = organ.CountColor
};
container.AddChild(icon);
container.AddChild(countLabel);
return container;
}
private string BuildTooltip(AbilityDisplayInfo ability)
{
var sb = new StringBuilder();
var cleanName = ColorTagRegex.Replace(ability.Name, string.Empty);
sb.AppendLine(cleanName);
sb.AppendLine();
if (ability.Bonuses.Count == 0)
{
sb.AppendLine(Loc.GetString("vampire-trophies-ui-no-bonuses"));
}
else
{
foreach (var bonus in ability.Bonuses)
{
var maxTag = bonus.IsMaxed ? $" {Loc.GetString($"vampire-trophies-ui-max")}" : "";
sb.AppendLine($"• {bonus.OrganType}: {bonus.Description}{maxTag}");
}
}
return sb.ToString();
}
}
@@ -0,0 +1,36 @@
using Content.Client.Eui;
using Content.Shared.Eui;
using Content.Shared.Vampire;
using JetBrains.Annotations;
namespace Content.Client._Wega.Vampire.Ui;
[UsedImplicitly]
public sealed class TrophiesMenuEui : BaseEui
{
private TrophiesMenu _menu;
public TrophiesMenuEui()
{
_menu = new TrophiesMenu();
}
public override void Opened()
{
_menu.OpenCenteredLeft();
}
public override void Closed()
{
_menu.Close();
}
public override void HandleState(EuiStateBase state)
{
base.HandleState(state);
if (state is not TrophiesEuiState trophiesState)
return;
_menu.Populate(trophiesState);
}
}
@@ -0,0 +1,31 @@
using Content.Client.Eui;
using Content.Shared.Vampire;
using JetBrains.Annotations;
namespace Content.Client._Wega.Vampire.Ui;
[UsedImplicitly]
public sealed class VampireClassSelectionEui : BaseEui
{
private readonly VampireClassSelectionMenu _menu;
public VampireClassSelectionEui()
{
_menu = new VampireClassSelectionMenu();
_menu.OnClassSelected += className =>
{
SendMessage(new VampireClassSelectedMessage(className));
_menu.Close();
};
}
public override void Opened()
{
_menu.OpenCentered();
}
public override void Closed()
{
_menu.Close();
}
}
@@ -0,0 +1,23 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.Vampire;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._Wega.Vampire.Ui;
[GenerateTypedNameReferences]
public sealed partial class VampireClassSelectionMenu : RadialMenu
{
public event Action<VampireClassEnum>? OnClassSelected;
public VampireClassSelectionMenu()
{
RobustXamlLoader.Load(this);
HemomancerButton.OnButtonUp += _ => OnClassSelected?.Invoke(VampireClassEnum.Hemomancer);
UmbraeButton.OnButtonUp += _ => OnClassSelected?.Invoke(VampireClassEnum.Umbrae);
GargantuaButton.OnButtonUp += _ => OnClassSelected?.Invoke(VampireClassEnum.Gargantua);
DantalionButton.OnButtonUp += _ => OnClassSelected?.Invoke(VampireClassEnum.Dantalion);
BestiaButton.OnButtonUp += _ => OnClassSelected?.Invoke(VampireClassEnum.Bestia);
}
}
@@ -1,40 +1,36 @@
<ui:RadialMenu xmlns="https://spacestation14.io"
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:local="clr-namespace:Content.Client.Select.Class.UI;assembly=Content.Client"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Content.Client.Select.Class.UI.SelectClassMenu"
BackButtonStyleClass="RadialMenuBackButton"
CloseButtonStyleClass="RadialMenuCloseButton"
VerticalExpand="True"
HorizontalExpand="True"
VerticalExpand="True" HorizontalExpand="True"
MinSize="450 450">
<!-- Main Radial Menu Container -->
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" InitialRadius="64" ReserveSpaceForHiddenChildren="False">
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" InitialRadius="128" ReserveSpaceForHiddenChildren="False">
<!-- Button 1: Hemomancer -->
<ui:RadialMenuButton Name="HemomancerButton" StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'select-class-hemomancer'}" TargetLayerControlName="Hemomancer">
<ui:RadialMenuButton Name="HemomancerButton" StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'select-class-hemomancer'}">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/_Wega/Interface/Actions/claws.png"/>
</ui:RadialMenuButton>
<!-- Button 2: Umbrae -->
<ui:RadialMenuButton Name="UmbraeButton" StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'select-class-umbrae'}" TargetLayerControlName="Umbrae">
<ui:RadialMenuButton Name="UmbraeButton" StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'select-class-umbrae'}">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/_Wega/Interface/Actions/cloak.png"/>
</ui:RadialMenuButton>
<!-- Button 3: Gargantua -->
<ui:RadialMenuButton Name="GargantuaButton" StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'select-class-gargantua'}" TargetLayerControlName="Gargantua">
<ui:RadialMenuButton Name="GargantuaButton" StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'select-class-gargantua'}">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/_Wega/Interface/Actions/swell.png"/>
</ui:RadialMenuButton>
<!-- Button 4: Dantalion -->
<ui:RadialMenuButton Name="DantalionButton" StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'select-class-dantalion'}" TargetLayerControlName="Dantalion">
<ui:RadialMenuButton Name="DantalionButton" StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'select-class-dantalion'}">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/_Wega/Interface/Actions/enthrall.png"/>
</ui:RadialMenuButton>
<!-- Button 5: Bestia -->
<!-- <ui:RadialMenuButton Name="BestiaButton" StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'select-class-bestia'}" TargetLayerControlName="Bestia">
<ui:RadialMenuButton Name="BestiaButton" StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'select-class-bestia'}">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/_Wega/Interface/Actions/rush.png"/>
</ui:RadialMenuButton> -->
</ui:RadialMenuButton>
</ui:RadialContainer>
+148 -24
View File
@@ -1,5 +1,8 @@
using Content.Client.Administration.Managers;
using Content.Client.Alerts;
using Content.Client.Movement.Systems;
using Content.Client.Ghost;
using Content.Shared.Administration;
using Content.Shared.StatusIcon;
using Content.Shared.StatusIcon.Components;
using Content.Shared.Vampire;
using Content.Shared.Vampire.Components;
@@ -11,37 +14,53 @@ namespace Content.Client.Vampire;
public sealed class VampireSystem : SharedVampireSystem
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly GhostSystem? _ghost = default;
[Dependency] private readonly IClientAdminManager _admin = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly SharedEyeSystem _eye = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<VampireToggleFovEvent>(OnToggleFoV);
SubscribeLocalEvent<VampireComponent, GetStatusIconsEvent>(GetVampireIcons);
SubscribeLocalEvent<ThrallComponent, GetStatusIconsEvent>(GetThrallIcons);
SubscribeLocalEvent<VampireComponent, UpdateAlertSpriteEvent>(OnUpdateAlert);
SubscribeLocalEvent<VampireDiablerieComponent, ComponentStartup>(OnDiablerieStartup);
SubscribeLocalEvent<VampireDiablerieComponent, ComponentShutdown>(OnDiablerieShutdown);
SubscribeLocalEvent<VampireDiablerieComponent, AfterAutoHandleStateEvent>(OnDiablerieStateUpdated);
}
private void OnToggleFoV(VampireToggleFovEvent args)
{
var userEntity = _entityManager.GetEntity(args.User);
var eyeComponent = _entityManager.GetComponent<EyeComponent>(userEntity);
if (userEntity == _playerManager.LocalEntity)
{
eyeComponent.NetSyncEnabled = false;
_eye.SetDrawFov(userEntity, args.Enabled, eyeComponent);
}
}
// Okey, let's go
private void GetVampireIcons(Entity<VampireComponent> ent, ref GetStatusIconsEvent args)
{
var iconPrototype = _prototype.Index(ent.Comp.StatusIcon);
args.StatusIcons.Add(iconPrototype);
// If the local user is an admin in the ghost?
if (_admin.HasFlag(AdminFlags.Admin) && _ghost is { IsGhost: true })
{
ShowIcon(_prototype.Index(ent.Comp.StatusIcon), ref args);
return;
}
// --- Admins ignore this above and see all vampires ---
// If he's not the owner of the thralls, we will not see the icon
if (!HasComp<ThrallOwnerComponent>(ent))
return;
var localPlayer = _playerManager.LocalEntity;
if (localPlayer == ent.Owner) // Is that you?
{
ShowIcon(_prototype.Index(ent.Comp.StatusIcon), ref args);
return;
}
// If we're a vampire's servant?
if (TryComp<ThrallComponent>(localPlayer, out var thrall) && thrall.VampireOwner == ent.Owner)
{
ShowIcon(_prototype.Index(ent.Comp.StatusIcon), ref args);
return;
}
}
private void GetThrallIcons(Entity<ThrallComponent> ent, ref GetStatusIconsEvent args)
@@ -49,18 +68,123 @@ public sealed class VampireSystem : SharedVampireSystem
if (HasComp<VampireComponent>(ent))
return;
var iconPrototype = _prototype.Index(ent.Comp.StatusIcon);
args.StatusIcons.Add(iconPrototype);
// If the local user is an admin in the ghost?
if (_admin.HasFlag(AdminFlags.Admin) && _ghost is { IsGhost: true })
{
ShowIcon(_prototype.Index(ent.Comp.StatusIcon), ref args);
return;
}
var localPlayer = _playerManager.LocalEntity;
if (localPlayer == ent.Owner) // Is that you?
{
ShowIcon(_prototype.Index(ent.Comp.StatusIcon), ref args);
return;
}
// If we are the vampire owner of this servant?
if (ent.Comp.VampireOwner == localPlayer)
{
ShowIcon(_prototype.Index(ent.Comp.StatusIcon), ref args);
return;
}
// If we were another servant of the same vampire owner?
if (TryComp<ThrallComponent>(localPlayer, out var localThrall)
&& localThrall.VampireOwner == ent.Comp.VampireOwner)
{
ShowIcon(_prototype.Index(ent.Comp.StatusIcon), ref args);
return;
}
}
private void ShowIcon(FactionIconPrototype icon, ref GetStatusIconsEvent args)
=> args.StatusIcons.Add(icon);
private void OnUpdateAlert(Entity<VampireComponent> ent, ref UpdateAlertSpriteEvent args)
{
if (args.Alert.ID != ent.Comp.BloodAlert)
return;
var blood = Math.Clamp(ent.Comp.CurrentBlood.Int(), 0, 999);
_sprite.LayerSetRsiState(args.SpriteViewEnt.Owner, VampireVisualLayers.Digit1, $"{(blood / 100) % 10}");
_sprite.LayerSetRsiState(args.SpriteViewEnt.Owner, VampireVisualLayers.Digit2, $"{(blood / 10) % 10}");
_sprite.LayerSetRsiState(args.SpriteViewEnt.Owner, VampireVisualLayers.Digit3, $"{blood % 10}");
var blood = Math.Clamp(ent.Comp.CurrentBlood.Int(), 0, 9999);
_sprite.LayerSetRsiState(args.SpriteViewEnt.Owner, VampireVisualLayers.Digit1, $"{(blood / 1000) % 10}");
_sprite.LayerSetRsiState(args.SpriteViewEnt.Owner, VampireVisualLayers.Digit2, $"{(blood / 100) % 10}");
_sprite.LayerSetRsiState(args.SpriteViewEnt.Owner, VampireVisualLayers.Digit3, $"{(blood / 10) % 10}");
_sprite.LayerSetRsiState(args.SpriteViewEnt.Owner, VampireVisualLayers.Digit4, $"{blood % 10}");
}
private void OnDiablerieStartup(Entity<VampireDiablerieComponent> ent, ref ComponentStartup args)
=> UpdateDiablerieAura(ent);
private void OnDiablerieShutdown(Entity<VampireDiablerieComponent> ent, ref ComponentShutdown args)
=> RemoveDiablerieAura(ent);
private void OnDiablerieStateUpdated(Entity<VampireDiablerieComponent> ent, ref AfterAutoHandleStateEvent args)
=> UpdateDiablerieAura(ent);
private void UpdateDiablerieAura(Entity<VampireDiablerieComponent> ent)
{
if (!HasComp<SpriteComponent>(ent))
return;
// If the local user is an admin in the ghost?
if (_admin.HasFlag(AdminFlags.Admin) && _ghost is { IsGhost: true })
{
AddOrUpdateAuraLayer(ent);
return;
}
var localPlayer = _playerManager.LocalEntity;
var isLocalPlayer = localPlayer == ent.Owner;
var isExaminerVampire = localPlayer != null && HasComp<VampireComponent>(localPlayer);
var level = ent.Comp.DiablerieLevel;
// Level 1+ aura visible to vampires
// Level 3+ aura visible to everyone
var canSeeAura = level >= 1 && isExaminerVampire || level >= 3;
// Self visibility
if (isLocalPlayer && level >= 1)
canSeeAura = true;
if (!canSeeAura)
{
RemoveDiablerieAura(ent);
return;
}
AddOrUpdateAuraLayer(ent);
}
private void AddOrUpdateAuraLayer(Entity<VampireDiablerieComponent> ent)
{
if (!_sprite.LayerMapTryGet(ent.Owner, DiablerieKey.Aura, out var layer, true))
{
var layerData = new PrototypeLayerData
{
RsiPath = "/Textures/_Wega/Interface/Misc/vampire_aura.rsi",
State = "diablerie_aura"
};
layer = _sprite.AddLayer(ent.Owner, layerData, null);
_sprite.LayerMapSet(ent.Owner, DiablerieKey.Aura, layer);
}
_sprite.LayerSetVisible(ent.Owner, layer, true);
}
private void RemoveDiablerieAura(Entity<VampireDiablerieComponent> ent)
{
if (!HasComp<SpriteComponent>(ent))
return;
if (_sprite.LayerMapTryGet(ent.Owner, DiablerieKey.Aura, out var layer, true))
_sprite.RemoveLayer(ent.Owner, layer);
}
private enum DiablerieKey
{
Aura
}
}
+1 -1
View File
@@ -11,10 +11,10 @@ using Content.Shared.Interaction;
using Content.Shared.Inventory;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Systems;
using Content.Shared.NullRod.Components; // Corvax-Wega-Vampire
using Content.Shared.Popups;
using Content.Shared.Stunnable; // Corvax-Wega-Vampire
using Content.Shared.Timing;
using Content.Shared.Vampire.Components; // Corvax-Wega-Vampire
using Content.Shared.Verbs;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers; // Corvax-Wega-Vampire
+7 -6
View File
@@ -92,12 +92,13 @@ public sealed partial class CloningSystem : SharedCloningSystem
_identity.QueueIdentityUpdate(clone.Value); // We have to manually refresh the identity in case we did not raise events.
_adminLogger.Add(LogType.Chat, LogImpact.Medium, $"The body of {original:player} was cloned as {clone.Value:player}");
///Corvax-Wega-Tweak: Для компонентов, использующих ивент ComponentStartup
var finishedEv = new CloneFinishedEvent(original);
RaiseLocalEvent(clone.Value, ref finishedEv);
return true;
// Corvax-Wega-Tweak-start
var finishedEv = new CloneFinishedEvent(original);
RaiseLocalEvent(clone.Value, ref finishedEv);
// Corvax-Wega-Tweak-end
return true;
}
public override void CloneComponents(
@@ -1,17 +1,17 @@
using Robust.Shared.Containers;
using Content.Shared.Mobs.Components;
using Robust.Shared.Timing;
using Robust.Server.Containers;
using Content.Shared.Whitelist;
using Content.Shared.Damage;
using Content.Shared.Damage.Systems;
using Content.Shared.Damage.Components;
using System.Linq;
using Content.Server.Damage.Components;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Systems;
using Content.Shared.Mobs.Components;
using Content.Shared.Whitelist;
using Robust.Server.Containers;
using Robust.Shared.Containers;
using Robust.Shared.Timing;
namespace Content.Server.Damage.Systems;
public sealed class DamageInContainerSystem : SharedDamageInContainerSystem
public sealed class DamageInContainerSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
@@ -53,17 +53,43 @@ public sealed class DamageInContainerSystem : SharedDamageInContainerSystem
if (_whitelistSystem.IsWhitelistFail(comp.Whitelist, contained))
continue;
DamageSpecifier? finalDamage = null;
if (comp.Damage != null && !comp.Damage.Empty && comp.Damage.DamageDict.Values.Any(x => x < 0))
{
finalDamage = comp.Damage.Clone();
if (comp.DamageGroups != null && !comp.DamageGroups.Empty)
{
var groupsHeal = _damageable.CreateWeightedHealFromGroups(uid, comp.DamageGroups);
finalDamage += groupsHeal;
}
}
else if (comp.Damage != null && !comp.Damage.Empty)
{
finalDamage = comp.Damage;
}
else if (comp.DamageGroups != null && !comp.DamageGroups.Empty)
{
finalDamage = _damageable.CreateWeightedHealFromGroups(uid, comp.DamageGroups);
}
else
{
continue;
}
if (TryComp<MobStateComponent>(contained, out var mobState))
{
foreach (var allowedState in comp.AllowedStates)
{
if (allowedState == mobState.CurrentState)
_damageable.TryChangeDamage(contained, comp.Damage, true, false);
{
_damageable.TryChangeDamage(contained, finalDamage, true, false);
break;
}
}
continue;
}
_damageable.TryChangeDamage(contained, comp.Damage, true, false);
_damageable.TryChangeDamage(contained, finalDamage, true, false);
}
}
}
@@ -79,4 +105,3 @@ public sealed class DamageInContainerSystem : SharedDamageInContainerSystem
AddComp<ActiveDamageInContainerComponent>(uid);
}
}
@@ -1,4 +1,5 @@
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Content.Shared.FixedPoint;
using Content.Shared.Vampire;
namespace Content.Server.GameTicking.Rules.Components;
@@ -8,4 +9,13 @@ namespace Content.Server.GameTicking.Rules.Components;
[RegisterComponent, Access(typeof(VampireRuleSystem))]
public sealed partial class VampireRuleComponent : Component
{
[DataField]
public Dictionary<EntityUid, VampireRoundInfo> VampiresInfo = new();
}
public sealed partial class VampireRoundInfo
{
public string Name = string.Empty;
public VampireClassEnum Class = VampireClassEnum.NonSelected;
public FixedPoint2 TotalBloodDrank = FixedPoint2.Zero;
}
@@ -1,34 +1,21 @@
using System.Linq;
using System.Text;
using Content.Server.Antag;
using Content.Server.Atmos.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Roles;
using Content.Server.Actions;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Rotting;
using Content.Shared.Body.Components;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Clumsy;
using Content.Shared.CombatMode.Pacification;
using Content.Shared.FixedPoint;
using Content.Shared.GameTicking.Components;
using Content.Shared.Humanoid;
using Content.Shared.Nutrition.Components;
using Content.Shared.Temperature.Components;
using Content.Shared.Mind;
using Content.Shared.Vampire;
using Content.Shared.Vampire.Components;
using Content.Shared.Damage.Systems;
using Content.Shared.Body;
using Content.Shared.Metabolism;
using System.Linq;
namespace Content.Server.GameTicking.Rules
{
public sealed class VampireRuleSystem : GameRuleSystem<VampireRuleComponent>
{
[Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly MetabolizerSystem _metabolism = default!;
[Dependency] private readonly ActionsSystem _actions = default!;
[Dependency] private readonly DamageableSystem _damage = default!;
[Dependency] private readonly SharedVisualBodySystem _visualBody = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
public override void Initialize()
{
@@ -41,7 +28,6 @@ namespace Content.Server.GameTicking.Rules
private void OnVampireSelected(Entity<VampireRuleComponent> mindId, ref AfterAntagEntitySelectedEvent args)
{
var ent = args.EntityUid;
MakeVampire(ent);
_antag.SendBriefing(ent, MakeBriefing(ent), Color.Purple, null);
}
@@ -56,8 +42,7 @@ namespace Content.Server.GameTicking.Rules
private string MakeBriefing(EntityUid ent)
{
var isHuman = HasComp<HumanoidProfileComponent>(ent);
var briefing = isHuman
var briefing = HasComp<HumanoidProfileComponent>(ent)
? Loc.GetString("vampire-role-greeting-human")
: Loc.GetString("vampire-role-greeting-animal");
@@ -69,117 +54,131 @@ namespace Content.Server.GameTicking.Rules
GameRuleComponent gameRule,
ref RoundEndTextAppendEvent args)
{
var totalBloodDrank = GetTotalBloodDrankInRound();
args.AddLine(Loc.GetString("vampires-drank-total-blood", ("bloodAmount", totalBloodDrank)));
if (component.VampiresInfo.Count == 0)
return;
var sb = new StringBuilder();
sb.AppendLine(Loc.GetString("vampire-round-end-header"));
foreach (var (_, info) in component.VampiresInfo)
{
var name = !string.IsNullOrEmpty(info.Name) ? info.Name : Loc.GetString("generic-unknown");
var className = Loc.GetString($"select-class-{info.Class.ToString().ToLower()}");
var bloodAmount = info.TotalBloodDrank.Float().ToString("F2");
var classColor = GetClassColor(info.Class);
var line = Loc.GetString("vampire-round-end-info", ("name", name), ("class", className),
("blood", bloodAmount), ("color", classColor));
sb.AppendLine(line);
}
sb.AppendLine();
var totalBloodDrank = GetTotalBloodDrankInRound(component).ToString("F2");
sb.AppendLine(Loc.GetString("vampires-drank-total-blood", ("bloodAmount", totalBloodDrank)));
args.AddLine(sb.ToString());
}
private float GetTotalBloodDrankInRound()
private Color GetClassColor(VampireClassEnum vampireClass)
{
return vampireClass switch
{
VampireClassEnum.Hemomancer => Color.FromHex("#b82e2e"),
VampireClassEnum.Umbrae => Color.FromHex("#6709aa"),
VampireClassEnum.Gargantua => Color.FromHex("#b34019"),
VampireClassEnum.Dantalion => Color.FromHex("#2a9633"),
VampireClassEnum.Bestia => Color.FromHex("#2770c4"),
_ => Color.Yellow
};
}
private float GetTotalBloodDrankInRound(VampireRuleComponent component)
{
var totalBloodDrank = 0f;
foreach (var vampireEntity in EntityQuery<VampireComponent>(true))
{
totalBloodDrank += vampireEntity.TotalBloodDrank;
}
foreach (var (_, info) in component.VampiresInfo)
totalBloodDrank += info.TotalBloodDrank.Float();
return totalBloodDrank;
}
private void MakeVampire(EntityUid vampire)
{
var vampireComponent = EnsureComp<VampireComponent>(vampire);
#region Records
RemoveUnnecessaryComponents(vampire);
HandleMetabolismAndOrgans(vampire);
SetVampireComponents(vampire, vampireComponent);
UpdateAppearance(vampire);
AddVampireActions(vampire);
}
private void RemoveUnnecessaryComponents(EntityUid vampire)
public void InitVampireRecord(EntityUid vampireUid, VampireComponent? vampireComp = null)
{
var componentsToRemove = new[]
if (!Resolve(vampireUid, ref vampireComp, false))
return;
var rule = EntityQuery<VampireRuleComponent>().FirstOrDefault();
if (rule == null)
return;
var mindId = _mind.GetMind(vampireUid);
if (mindId == null)
return;
if (rule.VampiresInfo.ContainsKey(mindId.Value))
return;
var info = new VampireRoundInfo
{
typeof(PacifiedComponent),
typeof(PerishableComponent),
typeof(BarotraumaComponent),
typeof(TemperatureSpeedComponent),
typeof(ThirstComponent),
typeof(ClumsyComponent)
Name = Name(vampireUid)
};
foreach (var compType in componentsToRemove)
{
if (HasComp(vampire, compType))
RemComp(vampire, compType);
}
rule.VampiresInfo[mindId.Value] = info;
}
private void HandleMetabolismAndOrgans(EntityUid vampire)
public void RecordBloodDrank(EntityUid vampireUid, FixedPoint2 amount)
{
if (TryComp<BodyComponent>(vampire, out var bodyComponent) && bodyComponent.Organs != null)
if (amount <= 0)
return;
var rule = EntityQuery<VampireRuleComponent>().FirstOrDefault();
if (rule == null)
return;
var mindId = _mind.GetMind(vampireUid);
if (mindId == null)
return;
if (!rule.VampiresInfo.TryGetValue(mindId.Value, out var info))
{
foreach (var organ in bodyComponent.Organs.ContainedEntities)
info = new VampireRoundInfo
{
if (TryComp<MetabolizerComponent>(organ, out var metabolizer))
{
if (TryComp<StomachComponent>(organ, out _))
_metabolism.ClearMetabolizerTypes(metabolizer);
_metabolism.TryAddMetabolizerType(metabolizer, VampireComponent.MetabolizerVampire);
}
}
Name = Name(vampireUid),
Class = CompOrNull<VampireComponent>(vampireUid)?.CurrentEvolution ?? default
};
rule.VampiresInfo[mindId.Value] = info;
}
info.TotalBloodDrank += amount;
}
private void SetVampireComponents(EntityUid vampire, VampireComponent _)
public void RecordClassSelected(EntityUid vampireUid, VampireClassEnum selectedClass)
{
if (TryComp<TemperatureDamageComponent>(vampire, out var temperature))
temperature.ColdDamageThreshold = Atmospherics.TCMB;
var rule = EntityQuery<VampireRuleComponent>().FirstOrDefault();
if (rule == null)
return;
EnsureComp<UnholyComponent>(vampire);
EnsureComp<VampireComponent>(vampire);
var mindId = _mind.GetMind(vampireUid);
if (mindId == null)
return;
_damage.SetDamageModifierSetId(vampire, "Vampire");
if (TryComp<ReactiveComponent>(vampire, out var reactive))
if (!rule.VampiresInfo.TryGetValue(mindId.Value, out var info))
{
reactive.ReactiveGroups ??= new();
if (!reactive.ReactiveGroups.ContainsKey("Unholy"))
info = new VampireRoundInfo
{
reactive.ReactiveGroups.Add("Unholy", new() { ReactionMethod.Touch });
}
Name = Name(vampireUid),
Class = selectedClass
};
rule.VampiresInfo[mindId.Value] = info;
}
else
{
info.Class = selectedClass;
}
}
private void UpdateAppearance(EntityUid vampire)
{
if (_visualBody.TryGatherMarkingsData(vampire, null, out var profiles, out _, out _))
{
var newEyeColor = Color.FromHex("#E22218FF");
var updatedProfiles = profiles.ToDictionary(
pair => pair.Key,
pair => pair.Value with { EyeColor = newEyeColor });
_visualBody.ApplyProfiles(vampire, updatedProfiles);
}
}
private void AddVampireActions(EntityUid vampire)
{
var actionPrototypes = new[]
{
VampireComponent.DrinkActionPrototype,
VampireComponent.SelectClassActionPrototype,
VampireComponent.RejuvenateActionPrototype,
VampireComponent.GlareActionPrototype
};
foreach (var actionPrototype in actionPrototypes)
{
_actions.AddAction(vampire, actionPrototype);
}
}
#endregion
}
}
@@ -0,0 +1,112 @@
using Content.Server.Administration.Logs;
using Content.Server.Bible.Components;
using Content.Shared.Database;
using Content.Shared.FixedPoint;
using Content.Shared.NullRod.Components;
using Content.Shared.Hands.EntitySystems;
using Robust.Shared.Timing;
using Content.Shared.Damage.Systems;
using Content.Shared.Rejuvenate;
namespace Content.Server.NullRod;
public sealed class NullDamageSystem : EntitySystem
{
[Dependency] private readonly IAdminLogManager _admin = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<UnholyComponent, DamageChangedEvent>(OnUnholyDamageTaken);
SubscribeLocalEvent<NullDamageComponent, RejuvenateEvent>(OnRejuvenate);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
UpdateNullDamage();
}
private void UpdateNullDamage()
{
var currentTime = _timing.CurTime;
var query = EntityQueryEnumerator<NullDamageComponent>();
while (query.MoveNext(out var uid, out var nullDamage))
{
if (nullDamage.NullDamage <= 0)
{
RemComp<NullDamageComponent>(uid);
continue;
}
if (currentTime >= nullDamage.NextNullDamageTick)
{
RecoverNullDamage(nullDamage, nullDamage.NullDamageRecoveryPerTick);
nullDamage.NextNullDamageTick = currentTime + TimeSpan.FromSeconds(nullDamage.NullDamageRecoveryInterval);
Dirty(uid, nullDamage);
}
}
}
#region Null Damage Logic
private void OnUnholyDamageTaken(EntityUid uid, UnholyComponent component, ref DamageChangedEvent args)
{
if (!args.Origin.HasValue || !HasComp<BibleUserComponent>(args.Origin.Value))
return;
var heldEntity = _hands.GetActiveItem(args.Origin.Value);
if (!TryComp<NullRodComponent>(heldEntity, out var nullRodComp))
return;
var nullDamage = EnsureComp<NullDamageComponent>(uid);
var damageToApply = nullDamage.NullDamage > 0
? nullRodComp.NullDamage
: nullRodComp.FirstNullDamage;
AddNullDamage(nullDamage, damageToApply);
if (nullDamage.NextNullDamageTick == default)
{
nullDamage.NextNullDamageTick = _timing.CurTime + TimeSpan.FromSeconds(nullDamage.NullDamageRecoveryInterval);
}
Dirty(uid, nullDamage);
_admin.Add(LogType.Damaged, LogImpact.Low,
$"{ToPrettyString(uid):target} took {damageToApply} NullDamage from {ToPrettyString(args.Origin.Value):attacker}");
}
private void OnRejuvenate(EntityUid uid, NullDamageComponent component, ref RejuvenateEvent args)
=> RemCompDeferred<NullDamageComponent>(uid);
#endregion
#region Public API
public void AddNullDamage(NullDamageComponent nullDamage, FixedPoint2 amount)
{
nullDamage.NullDamage = FixedPoint2.Min(nullDamage.NullDamage + amount, nullDamage.MaxNullDamage);
}
public void RecoverNullDamage(NullDamageComponent nullDamage, FixedPoint2 amount)
{
nullDamage.NullDamage = FixedPoint2.Max(nullDamage.NullDamage - amount, FixedPoint2.Zero);
}
public FixedPoint2? GetNullDamage(EntityUid uid, NullDamageComponent? nullDamage = null)
{
if (!Resolve(uid, ref nullDamage, false))
return null;
return nullDamage.NullDamage;
}
#endregion
}
@@ -24,7 +24,7 @@ public sealed class ProjectileAoESystem : EntitySystem
var target = ev.Target;
var ents = _lookup.GetEntitiesInRange<DamageableComponent>(Transform(target).Coordinates, component.DamageRadius)
.Where(e => e.Owner != shooter);
.Where(e => e.Owner != shooter && e.Owner != target);
foreach (var ent in ents)
{
@@ -0,0 +1,29 @@
using Content.Server.Disease;
using Content.Shared.Projectiles;
using Robust.Shared.Random;
namespace Content.Server.Projectiles;
public sealed class ProjectileInfectSystem : EntitySystem
{
[Dependency] private readonly DiseaseSystem _disease = default!;
[Dependency] private readonly IRobustRandom _random = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ProjectileInfectComponent, ProjectileHitEvent>(OnProjectileHit);
}
private void OnProjectileHit(EntityUid entity, ProjectileInfectComponent component, ref ProjectileHitEvent ev)
{
var shooter = ev.Shooter;
if (shooter == null || shooter == ev.Target)
return;
if (!_random.Prob(component.Prob))
return;
_disease.TryAddDisease(ev.Target, component.Infection);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,424 @@
using System.Linq;
using Content.Server.Administration;
using Content.Server.Bible.Components;
using Content.Server.Cloning;
using Content.Server.Hallucinations;
using Content.Server.Prayer;
using Content.Shared.Body;
using Content.Shared.CombatMode.Pacification;
using Content.Shared.DoAfter;
using Content.Shared.Humanoid;
using Content.Shared.IdentityManagement;
using Content.Shared.Mindshield.Components;
using Content.Shared.Movement.Systems;
using Content.Shared.NullRod.Components;
using Content.Shared.Popups;
using Content.Shared.Stealth.Components;
using Content.Shared.Surgery.Components;
using Content.Shared.Vampire;
using Content.Shared.Vampire.Components;
using Robust.Shared.Audio;
using Robust.Shared.Player;
using Robust.Shared.Timing;
namespace Content.Server.Vampire;
public sealed partial class VampireSystem
{
[Dependency] private readonly CloningSystem _cloning = default!;
[Dependency] private readonly HallucinationsSystem _hallucinations = default!;
[Dependency] private readonly PrayerSystem _prayerSystem = default!;
[Dependency] private readonly QuickDialogSystem _quickDialog = default!;
private void InitializeDantalion()
{
SubscribeLocalEvent<VampireComponent, VampireEnthrallActionEvent>(OnAfterEnthrall);
SubscribeLocalEvent<VampireComponent, EnthrallDoAfterEvent>(OnEnthrallDoAfter);
SubscribeLocalEvent<VampireComponent, VampireCommuneActionEvent>(OnCommune);
SubscribeLocalEvent<VampireComponent, VampirePacifyActionEvent>(OnPacify);
SubscribeLocalEvent<VampireComponent, VampireSubspaceSwapActionEvent>(OnSubspaceSwap);
SubscribeLocalEvent<VampireComponent, VampireDeployDecoyActionEvent>(OnDeployDecoy);
SubscribeLocalEvent<VampireComponent, VampireRallyThrallsActionEvent>(OnRallyThralls);
SubscribeLocalEvent<VampireComponent, VampireBloodBondActionEvent>(OnBloodBond);
SubscribeLocalEvent<VampireComponent, VampireMassHysteriaActionEvent>(OnMassHysteria);
SubscribeLocalEvent<VampireComponent, VampireThrallHealActionEvent>(OnThrallHeal);
SubscribeLocalEvent<VampireComponent, VampirePacifyNearbyActionEvent>(OnPacifyNearby);
}
private void OnAfterEnthrall(Entity<VampireComponent> ent, ref VampireEnthrallActionEvent args)
{
var target = args.Target;
if (!TryComp<ThrallOwnerComponent>(ent, out var thrallOwner))
return;
if (!CanAddThrall(thrallOwner))
{
_popup.PopupEntity(Loc.GetString("vampire-max-trall-reached"), ent, ent, PopupType.Medium);
return;
}
if (HasComp<VampireComponent>(target) || HasComp<MindShieldComponent>(target) || HasComp<BibleUserComponent>(target)
|| HasComp<SyntheticOperatedComponent>(target))
{
_popup.PopupEntity(Loc.GetString("vampire-enthall-failed", ("target", Identity.Name(target, EntityManager))), ent, ent);
return;
}
if (TryComp<ThrallComponent>(target, out var trallComponent))
{
if (trallComponent.VampireOwner == ent.Owner)
{
_popup.PopupEntity(Loc.GetString("vampire-enthall-already", ("target", Identity.Name(target, EntityManager))), ent, ent);
return;
}
else
{
_popup.PopupEntity(Loc.GetString("vampire-enthall-failed", ("target", Identity.Name(target, EntityManager))), ent, ent);
return;
}
}
if (!CheckBloodEssence(ent.Owner, args.BloodCost))
{
SendFailedPopup(ent);
return;
}
args.Handled = true;
_popup.PopupEntity(Loc.GetString("vampire-blooddrink-countion"), ent, args.Target, PopupType.MediumCaution);
_doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, ent, TimeSpan.FromSeconds(15f), new EnthrallDoAfterEvent(args.BloodCost), ent, target)
{
BreakOnMove = true,
BreakOnDamage = true,
MovementThreshold = 0.01f,
DistanceThreshold = 0.5f,
NeedHand = true
});
}
private void OnEnthrallDoAfter(Entity<VampireComponent> ent, ref EnthrallDoAfterEvent args)
{
if (args.Cancelled || args.Args.Target == null) return;
var target = args.Args.Target.Value;
if (!HasComp<ActorComponent>(target))
return;
if (!TryComp<ThrallOwnerComponent>(ent, out var thrallOwner))
return;
EnsureComp<UnholyComponent>(target);
EnsureComp<BittenByVampireComponent>(target);
var newTrall = EnsureComp<ThrallComponent>(target);
newTrall.VampireOwner = ent.Owner;
Dirty(target, newTrall);
if (TryAddThrall(thrallOwner, target))
Dirty(ent.Owner, thrallOwner);
_popup.PopupEntity(Loc.GetString("vampire-enthall-success", ("target", Identity.Name(target, EntityManager))), ent, ent);
_antag.SendBriefing(target, Loc.GetString("thrall-greeting"), Color.Red,
new SoundPathSpecifier("/Audio/_Wega/Ambience/Antag/vampare_start.ogg"));
SubtractBloodEssence(ent.Owner, args.BloodCost);
}
private void OnCommune(Entity<VampireComponent> ent, ref VampireCommuneActionEvent args)
{
if (!TryComp<ThrallOwnerComponent>(ent, out var thrallOwner) || thrallOwner.ThrallOwned.Count == 0)
{
_popup.PopupEntity(Loc.GetString("vampire-no-thrall"), ent, ent, PopupType.Medium);
return;
}
if (!TryComp<ActorComponent>(ent, out var playerActor))
return;
var playerSession = playerActor.PlayerSession;
_quickDialog.OpenDialog(playerSession, Loc.GetString("vampire-commune-title"), Loc.GetString("vampire-commune-prompt"),
(string message) =>
{
var finalMessage = string.IsNullOrWhiteSpace(message)
? Loc.GetString("vampire-commune-default-message")
: message;
foreach (var thrallUid in thrallOwner.ThrallOwned)
{
if (!TryComp<ActorComponent>(thrallUid, out var thrallActor))
continue;
_prayerSystem.SendSubtleMessage(thrallActor.PlayerSession, thrallActor.PlayerSession, finalMessage, Loc.GetString("vampire-commune-default-message"));
_chat.SendMessageToOne(thrallUid, finalMessage);
}
_chat.SendMessageToOne(ent, finalMessage);
});
args.Handled = true;
}
private void OnPacify(Entity<VampireComponent> ent, ref VampirePacifyActionEvent args)
{
if (!CheckBloodEssence(ent.Owner, args.BloodCost))
{
SendFailedPopup(ent);
return;
}
var target = args.Target;
if (HasComp<HumanoidProfileComponent>(target))
{
if (HasComp<NullRodOwnerComponent>(target) && !HasTruePower(ent) || HasComp<ThrallComponent>(target))
{
_popup.PopupEntity(Loc.GetString("vampire-pacify-failed", ("target", Identity.Name(target, EntityManager))), ent, ent);
return;
}
if (!_mobState.IsDead(target) && !HasComp<PacifiedComponent>(target))
{
EnsureComp<PacifiedComponent>(target);
Timer.Spawn(args.Time, () => { RemComp<PacifiedComponent>(target); });
SubtractBloodEssence(ent.Owner, args.BloodCost);
args.Handled = true;
}
else
{
_popup.PopupEntity(Loc.GetString("vampire-pacify-failed", ("target", Identity.Name(target, EntityManager))), ent, ent);
args.Handled = true;
}
}
}
private void OnSubspaceSwap(Entity<VampireComponent> ent, ref VampireSubspaceSwapActionEvent args)
{
if (!CheckBloodEssence(ent.Owner, args.BloodCost))
{
SendFailedPopup(ent);
return;
}
var target = args.Target;
if (!HasComp<HumanoidProfileComponent>(target))
{
_popup.PopupEntity(Loc.GetString("vampire-teleport-failed"), ent, ent, PopupType.SmallCaution);
return;
}
if (HasComp<NullRodOwnerComponent>(target) && !HasTruePower(ent))
return;
var currentCoords = Transform(ent).Coordinates;
var targetCoords = Transform(target).Coordinates;
_transform.SetCoordinates(ent, targetCoords);
_transform.SetCoordinates(target, currentCoords);
_movementMod.TryUpdateMovementSpeedModDuration(target, MovementModStatusSystem.Slowdown, TimeSpan.FromSeconds(4f), 0.5f);
_hallucinations.StartHallucinations(target, "Hallucinations", TimeSpan.FromSeconds(15f), true, "MindBreaker");
SubtractBloodEssence(ent.Owner, args.BloodCost);
args.Handled = true;
}
private void OnDeployDecoy(Entity<VampireComponent> ent, ref VampireDeployDecoyActionEvent args)
{
if (!CheckBloodEssence(ent.Owner, args.BloodCost))
{
SendFailedPopup(ent);
return;
}
if (!_cloning.TryCloning(ent, _transform.GetMapCoordinates(ent), args.Settings, out var clone))
return;
EntityManager.AddComponents(clone.Value, args.EnsurableComponents);
var stealth = EnsureComp<StealthComponent>(ent);
_stealth.SetVisibility(ent, 0f, stealth);
Timer.Spawn(args.Time, () =>
{
RemComp<StealthComponent>(ent);
Del(clone);
});
SubtractBloodEssence(ent.Owner, args.BloodCost);
args.Handled = true;
}
private void OnRallyThralls(Entity<VampireComponent> ent, ref VampireRallyThrallsActionEvent args)
{
if (!CheckBloodEssence(ent.Owner, args.BloodCost))
{
SendFailedPopup(ent);
return;
}
var thrallsInRange = _entityLookup.GetEntitiesInRange<ThrallComponent>(Transform(ent).Coordinates, 7f);
foreach (var thrallEntity in thrallsInRange)
{
if (_mobState.IsDead(thrallEntity.Owner))
continue;
TryRemoveKnockdown(thrallEntity.Owner);
_stamina.RemoveStaminaDamage(thrallEntity.Owner);
_status.TryRemoveStatusEffect(thrallEntity.Owner, ForceSleeping);
}
SubtractBloodEssence(ent.Owner, args.BloodCost);
args.Handled = true;
}
private void OnBloodBond(Entity<VampireComponent> ent, ref VampireBloodBondActionEvent args)
{
var supreme = GetTruePower(ent);
if (supreme == null)
return;
if (!TryComp<ThrallOwnerComponent>(ent, out var thrallOwner) || thrallOwner.ThrallOwned.Count == 0)
{
_popup.PopupEntity(Loc.GetString("vampire-no-thrall"), ent, ent, PopupType.Medium);
return;
}
if (!CheckBloodEssence(ent.Owner, args.BloodCost))
{
SendFailedPopup(ent);
return;
}
if (supreme.Active)
{
supreme.Active = false;
thrallOwner.DamageSharing = false;
Dirty(ent.Owner, supreme);
args.Handled = true;
return;
}
supreme.Active = true;
thrallOwner.DamageSharing = true;
Dirty(ent.Owner, supreme);
ExecuteBloodBondTick(ent, supreme, thrallOwner, args);
args.Handled = true;
}
private void OnThrallHeal(Entity<VampireComponent> ent, ref VampireThrallHealActionEvent args)
{
var thrallsInRange = _entityLookup.GetEntitiesInRange<ThrallComponent>(Transform(ent).Coordinates, 5);
int thrallCount = thrallsInRange.Count;
if (thrallCount == 0)
return;
if (!CheckBloodEssence(ent.Owner, thrallCount * args.BloodCost))
{
SendFailedPopup(ent);
return;
}
foreach (var thrallEntity in thrallsInRange)
{
ExecuteThrallHealTick(thrallEntity.Owner, ent, 0, args);
}
SubtractBloodEssence(ent.Owner, thrallCount * args.BloodCost);
args.Handled = true;
}
private void OnPacifyNearby(Entity<VampireComponent> ent, ref VampirePacifyNearbyActionEvent args)
{
if (!CheckBloodEssence(ent.Owner, args.BloodCost))
{
SendFailedPopup(ent);
return;
}
var peopleInRange = _entityLookup.GetEntitiesInRange<HumanoidProfileComponent>(Transform(ent).Coordinates, 6);
foreach (var person in peopleInRange)
{
if (HasComp<ThrallComponent>(person) || HasComp<VampireComponent>(person))
continue;
if (HasComp<NullRodOwnerComponent>(person) && !HasTruePower(ent))
continue;
if (_mobState.IsDead(person))
continue;
if (HasComp<PacifiedComponent>(person))
continue;
var target = person;
EnsureComp<PacifiedComponent>(target);
Timer.Spawn(args.Time, () => RemComp<PacifiedComponent>(target));
}
SubtractBloodEssence(ent.Owner, args.BloodCost);
args.Handled = true;
}
private void OnMassHysteria(Entity<VampireComponent> ent, ref VampireMassHysteriaActionEvent args)
{
if (!CheckBloodEssence(ent.Owner, args.BloodCost))
{
SendFailedPopup(ent);
return;
}
var victimInRange = _entityLookup.GetEntitiesInRange<BodyComponent>(Transform(ent).Coordinates, 8f)
.Where(entity => entity.Owner != ent.Owner).ToList();
foreach (var victimEntity in victimInRange)
{
if (HasComp<NullRodOwnerComponent>(victimEntity) && !HasTruePower(ent))
continue;
_flash.Flash(victimEntity, ent, null, TimeSpan.FromSeconds(4f), 0.5f);
_hallucinations.StartHallucinations(victimEntity, "Hallucinations", TimeSpan.FromSeconds(30f), true, "MindBreaker");
}
SubtractBloodEssence(ent.Owner, args.BloodCost);
args.Handled = true;
}
#region Utility Methods
private void ExecuteBloodBondTick(Entity<VampireComponent> ent, SupremeVampireComponent supreme, ThrallOwnerComponent thrallOwner, VampireBloodBondActionEvent args)
{
if (!Exists(ent) || !supreme.Active)
{
supreme.Active = false;
thrallOwner.DamageSharing = false;
Dirty(ent.Owner, supreme);
return;
}
if (!CheckBloodEssence(ent.Owner, args.BloodCost))
{
SendFailedPopup(ent);
supreme.Active = false;
thrallOwner.DamageSharing = false;
Dirty(ent.Owner, supreme);
return;
}
SubtractBloodEssence(ent.Owner, args.BloodCost);
Timer.Spawn(args.TimeInterval, () => ExecuteBloodBondTick(ent, supreme, thrallOwner, args));
}
private void ExecuteThrallHealTick(EntityUid thrallUid, Entity<VampireComponent> vampire, int currentTick, VampireThrallHealActionEvent args)
{
if (!Exists(thrallUid) || !Exists(vampire) || currentTick >= args.Repeats)
return;
var healingSpec = CalculateScaledHealing(thrallUid, args.Heal, args.HealGroups);
_damage.TryChangeDamage(thrallUid, healingSpec, true, false, origin: vampire);
Timer.Spawn(args.TimeInterval, () => ExecuteThrallHealTick(thrallUid, vampire, currentTick + 1, args));
}
#endregion
}
@@ -0,0 +1,289 @@
using System.Numerics;
using Content.Shared.Body;
using Content.Shared.CombatMode;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Ensnaring.Components;
using Content.Shared.FixedPoint;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Pulling.Components;
using Content.Shared.NullRod.Components;
using Content.Shared.Popups;
using Content.Shared.Prying.Components;
using Content.Shared.Vampire;
using Content.Shared.Vampire.Components;
using Content.Shared.Weapons.Melee;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.Vampire;
public sealed partial class VampireSystem
{
public static readonly ProtoId<DamageModifierSetPrototype> BloodSwell = "VampireBloodSwell";
private void InitializeGargantua()
{
SubscribeLocalEvent<VampireComponent, VampireBloodSwellActionEvent>(OnBloodSwell);
SubscribeLocalEvent<VampireComponent, VampireBloodRushActionEvent>(OnBloodRush);
SubscribeLocalEvent<VampireComponent, VampireSeismicStompActionEvent>(OnSeismicStomp);
SubscribeLocalEvent<VampireComponent, VampireOverwhelmingForceActionEvent>(OnOverwhelmingForce);
SubscribeLocalEvent<VampireComponent, VampireDemonicGraspActionEvent>(OnDemonicGrasp);
SubscribeLocalEvent<VampireComponent, VampireChargeActionEvent>(OnCharge);
}
private void OnBloodSwell(Entity<VampireComponent> ent, ref VampireBloodSwellActionEvent args)
{
if (!CheckBloodEssence(ent.Owner, args.BloodCost))
{
SendFailedPopup(ent);
return;
}
_damage.SetDamageModifierSetId(ent.Owner, BloodSwell);
if (args.Advanced)
{
if (TryComp(ent, out MeleeWeaponComponent? meleeWeapon))
{
FixedPoint2? oldDamageValue = null;
var damageDict = meleeWeapon.Damage.DamageDict;
if (damageDict.ContainsKey(args.BonusDamageType))
{
oldDamageValue = damageDict[args.BonusDamageType];
damageDict[args.BonusDamageType] += args.BonusDamageAmount;
}
else
{
damageDict[args.BonusDamageType] = args.BonusDamageAmount;
}
var savedOldDamage = oldDamageValue;
var damageType = args.BonusDamageType;
var bonusAmount = args.BonusDamageAmount;
Timer.Spawn(args.Time, () =>
{
if (TryComp(ent, out MeleeWeaponComponent? weapon))
{
if (savedOldDamage.HasValue && weapon.Damage.DamageDict.ContainsKey(damageType))
{
weapon.Damage.DamageDict[damageType] = savedOldDamage.Value;
}
else if (!savedOldDamage.HasValue)
{
weapon.Damage.DamageDict.Remove(damageType);
}
}
_damage.SetDamageModifierSetId(ent.Owner, VampireComponent.VampireDamageModifier);
});
}
}
else
{
Timer.Spawn(args.Time, () =>
{
_damage.SetDamageModifierSetId(ent.Owner, VampireComponent.VampireDamageModifier);
});
}
SubtractBloodEssence(ent.Owner, args.BloodCost);
args.Handled = true;
}
private void OnBloodRush(Entity<VampireComponent> ent, ref VampireBloodRushActionEvent args)
{
if (!CheckBloodEssence(ent.Owner, args.BloodCost))
{
SendFailedPopup(ent);
return;
}
if (TryComp(ent, out MovementSpeedModifierComponent? speedmodComponent))
{
var originalWalkSpeed = speedmodComponent.BaseWalkSpeed;
var originalSprintSpeed = speedmodComponent.BaseSprintSpeed;
_speed.ChangeBaseSpeed(ent, originalWalkSpeed * 2, originalSprintSpeed * 2, speedmodComponent.Acceleration);
var time = HasTruePower(ent) ? args.Time * 2 : args.Time;
Timer.Spawn(time, () =>
{
_speed.ChangeBaseSpeed(ent, originalWalkSpeed, originalSprintSpeed, speedmodComponent.Acceleration);
});
}
SubtractBloodEssence(ent.Owner, args.BloodCost);
args.Handled = true;
}
private void OnSeismicStomp(Entity<VampireComponent> ent, ref VampireSeismicStompActionEvent args)
{
if (TryComp(ent, out EnsnareableComponent? ensnareable) && ensnareable.IsEnsnared)
{
_popup.PopupEntity(Loc.GetString("vampire-legs-ensnared"), ent, ent, PopupType.Medium);
return;
}
if (!CheckBloodEssence(ent.Owner, args.BloodCost))
{
SendFailedPopup(ent);
return;
}
var vampirePos = _transform.GetWorldPosition(ent);
var gridUid = _transform.GetGrid(ent.Owner);
if (gridUid != null && TryComp<MapGridComponent>(gridUid.Value, out var grid))
{
var tiles = _map.GetTilesIntersecting(gridUid.Value, grid,
Box2.CenteredAround(vampirePos, new Vector2(6, 6)), ignoreEmpty: true);
foreach (var tile in tiles)
{
if (!_random.Prob(0.5f))
continue;
_tile.PryTile(tile);
}
}
var nearbyHumanoids = _entityLookup.GetEntitiesInRange<BodyComponent>(Transform(ent).Coordinates, 3f);
foreach (var humanoid in nearbyHumanoids)
{
var humanoidUid = humanoid.Owner;
if (humanoidUid == ent.Owner) continue;
if (HasComp<NullRodOwnerComponent>(humanoidUid) && !HasTruePower(ent))
continue;
if (!TryComp(humanoid, out PhysicsComponent? physics))
continue;
var humanoidPosition = _transform.GetWorldPosition(humanoid);
var direction = (humanoidPosition - vampirePos).Normalized();
var force = 10f;
if (physics.Mass <= 50f)
force *= 2;
_throwing.TryThrow(humanoidUid, direction * (force / 2), force);
}
_audio.PlayPvs(args.Sound, ent);
SubtractBloodEssence(ent.Owner, args.BloodCost);
args.Handled = true;
}
private void OnOverwhelmingForce(Entity<VampireComponent> ent, ref VampireOverwhelmingForceActionEvent args)
{
if (TryComp(ent, out EnsnareableComponent? ensnareable) && ensnareable.IsEnsnared)
{
_popup.PopupEntity(Loc.GetString("vampire-legs-ensnared"), ent, ent, PopupType.Medium);
return;
}
bool prying = HasComp<PryingComponent>(ent);
if (!prying && HasComp<PullableComponent>(ent))
{
if (!CheckBloodEssence(ent.Owner, args.BloodCost))
{
SendFailedPopup(ent);
return;
}
}
if (prying)
{
RemComp<PryingComponent>(ent);
EnsureComp<EnsnareableComponent>(ent);
EnsureComp<PullableComponent>(ent);
}
else
{
var pryComponent = EnsureComp<PryingComponent>(ent);
pryComponent.PryPowered = true;
pryComponent.Force = true;
pryComponent.SpeedModifier = 2.5f;
SubtractBloodEssence(ent.Owner, args.BloodCost);
RemComp<EnsnareableComponent>(ent);
RemComp<PullableComponent>(ent);
}
args.Handled = true;
}
private void OnDemonicGrasp(Entity<VampireComponent> ent, ref VampireDemonicGraspActionEvent args)
{
if (!TryComp<CombatModeComponent>(ent, out var combatMode))
return;
if (!CheckBloodEssence(ent.Owner, args.BloodCost))
{
SendFailedPopup(ent);
return;
}
var target = args.Target;
if (HasComp<NullRodOwnerComponent>(target) && !HasTruePower(ent))
return;
var vampirePosition = _transform.GetWorldPosition(ent);
var targetPosition = _transform.GetWorldPosition(target);
var direction = (vampirePosition - targetPosition).Normalized();
if (HasComp<PhysicsComponent>(target))
{
if (!combatMode.IsInCombatMode)
{
_throwing.TryThrow(target, -direction * 3);
_stun.TryUpdateStunDuration(target, TimeSpan.FromSeconds(3f));
}
else
{
_throwing.TryThrow(target, direction * 3);
_stun.TryUpdateStunDuration(target, TimeSpan.FromSeconds(3f));
}
}
SubtractBloodEssence(ent.Owner, args.BloodCost);
args.Handled = true;
}
private void OnCharge(Entity<VampireComponent> ent, ref VampireChargeActionEvent args)
{
if (TryComp(ent, out EnsnareableComponent? ensnareable) && ensnareable.IsEnsnared)
{
_popup.PopupEntity(Loc.GetString("vampire-legs-ensnared"), ent, ent, PopupType.Medium);
return;
}
if (!CheckBloodEssence(ent.Owner, args.BloodCost))
{
SendFailedPopup(ent);
return;
}
var coords = args.Target;
var vampirePosition = _transform.GetWorldPosition(ent);
var targetPosition = _transform.ToMapCoordinates(coords, true).Position;
var direction = (targetPosition - vampirePosition).Normalized();
// Well, that might cause it to deal damage when the space wind,
// but that doesn't seem like a problem, does it?
EntityManager.AddComponents(ent, args.EnsurableComponents, false);
_throwing.TryThrow(ent, direction * 3);
_audio.PlayPvs(args.Sound, ent);
SubtractBloodEssence(ent.Owner, args.BloodCost);
args.Handled = true;
}
}
@@ -0,0 +1,396 @@
using System.Linq;
using System.Numerics;
using Content.Shared.Body.Components;
using Content.Shared.Chemistry.Components;
using Content.Shared.Clothing;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Systems;
using Content.Shared.Fluids.Components;
using Content.Shared.Humanoid;
using Content.Shared.Localizations;
using Content.Shared.Mobs.Components;
using Content.Shared.NullRod.Components;
using Content.Shared.Physics;
using Content.Shared.Popups;
using Content.Shared.Random.Helpers;
using Content.Shared.Roles;
using Content.Shared.Standing;
using Content.Shared.Surgery.Components;
using Content.Shared.Vampire;
using Content.Shared.Vampire.Components;
using Content.Shared.Weapons.Melee.Events;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.Vampire;
public sealed partial class VampireSystem
{
[Dependency] private readonly LoadoutSystem _loadout = default!;
private void InitializeHemomancer()
{
SubscribeLocalEvent<VampireClawsComponent, MeleeHitEvent>(OnClawsHit);
SubscribeLocalEvent<VampireClawsComponent, BeforeDamageChangedEvent>(OnBeforeDamageChanged);
SubscribeLocalEvent<VampireComponent, VampireClawsActionEvent>(GiveVampireClaws);
SubscribeLocalEvent<VampireComponent, VampireBloodTentacleAction>(OnBloodTendrils);
SubscribeLocalEvent<VampireComponent, VampireBloodBarrierActionEvent>(OnBloodBarrierAction);
SubscribeLocalEvent<VampireComponent, VampireSanguinePoolActionEvent>(OnSanguinePoolAction);
SubscribeLocalEvent<VampireComponent, VampirePredatorSensesActionEvent>(OnVampirePredatorSensesAction);
SubscribeLocalEvent<VampireComponent, VampireBloodEruptionActionEvent>(OnVampireBloodEruptionAction);
SubscribeLocalEvent<VampireComponent, VampireBloodBringersRiteActionEvent>(OnBloodBringersRite);
}
private void OnClawsHit(Entity<VampireClawsComponent> ent, ref MeleeHitEvent args)
{
if (args.HitEntities.Count == 0)
return;
foreach (var hitEnt in args.HitEntities)
{
if (HasComp<SyntheticOperatedComponent>(hitEnt))
continue;
if (!HasComp<BloodstreamComponent>(hitEnt))
continue;
var groupsHeal = _damage.CreateWeightedHealFromGroups(args.User, ent.Comp.HealGroups);
_damage.TryChangeDamage(ent.Owner, groupsHeal, true, false, origin: ent);
_stamina.TakeStaminaDamage(ent, ent.Comp.StaminaMod, visual: false);
AddBloodEssence(args.User, ent.Comp.BloodStealAmount);
_blood.TryModifyBleedAmount(hitEnt, -ent.Comp.BloodStealAmount.Float() * 2);
}
}
private void OnBeforeDamageChanged(Entity<VampireClawsComponent> ent, ref BeforeDamageChangedEvent args)
{
var vampire = Transform(ent).ParentUid; // We assume that the current parent is a vampire.
var supreme = GetTruePower(vampire);
if (supreme == null)
return;
if (supreme.Active) args.Cancelled = true;
}
private void GiveVampireClaws(Entity<VampireComponent> ent, ref VampireClawsActionEvent args)
{
if (!CheckBloodEssence(ent.Owner, args.BloodCost))
{
SendFailedPopup(ent);
return;
}
var dropEvent = new DropHandItemsEvent();
RaiseLocalEvent(ent, ref dropEvent);
List<ProtoId<StartingGearPrototype>> gear = new() { args.ProtoId };
_loadout.Equip(ent, gear, null);
SubtractBloodEssence(ent.Owner, args.BloodCost);
args.Handled = true;
}
private void OnBloodTendrils(Entity<VampireComponent> ent, ref VampireBloodTentacleAction args)
{
if (!CheckBloodEssence(ent.Owner, args.BloodCost))
{
SendFailedPopup(ent);
return;
}
var coords = args.Target;
List<EntityCoordinates> spawnPos = new();
spawnPos.Add(coords);
var dirs = new List<Direction>();
dirs.AddRange(args.OffsetDirections);
for (var i = 0; i < args.ExtraSpawns; i++)
{
var dir = _random.PickAndTake(dirs);
var vector = DirectionToVector2(dir);
spawnPos.Add(coords.Offset(vector));
}
if (_transform.GetGrid(coords) is not { } grid || !TryComp<MapGridComponent>(grid, out var gridComp))
return;
foreach (var pos in spawnPos)
{
if (!_map.TryGetTileRef(grid, gridComp, pos, out var tileRef)
|| _turf.IsTileBlocked(tileRef, CollisionGroup.Impassable))
continue;
Spawn(args.EntityId, pos);
}
SubtractBloodEssence(ent.Owner, args.BloodCost);
args.Handled = true;
}
private void OnBloodBarrierAction(Entity<VampireComponent> ent, ref VampireBloodBarrierActionEvent args)
{
if (!CheckBloodEssence(ent.Owner, args.BloodCost))
{
SendFailedPopup(ent);
return;
}
var targetCoords = args.Target;
var transform = Transform(ent);
var direction = transform.LocalRotation.ToWorldVec().Normalized();
var perpendicularDirection = new Vector2(-direction.Y, direction.X);
var objectCount = 0;
for (int i = -1; i <= 1 && objectCount < 3; i++)
{
var spawnPosition = targetCoords.Offset(perpendicularDirection * (1f * i));
if (TrySpawnObjectAtPosition(spawnPosition, args.EntityId, ent))
objectCount++;
}
SubtractBloodEssence(ent.Owner, args.BloodCost);
args.Handled = true;
}
private void OnSanguinePoolAction(Entity<VampireComponent> ent, ref VampireSanguinePoolActionEvent args)
{
if (!CheckBloodEssence(ent.Owner, args.BloodCost))
{
SendFailedPopup(ent);
return;
}
var polymorphedEntity = _polymorph.PolymorphEntity(ent, args.PolymorphProto);
if (polymorphedEntity == null)
return;
SubtractBloodEssence(ent.Owner, args.BloodCost);
args.Handled = true;
}
private void OnVampirePredatorSensesAction(Entity<VampireComponent> ent, ref VampirePredatorSensesActionEvent args)
{
var centerCoords = Transform(ent).Coordinates;
var nearbyHumanoids = _entityLookup.GetEntitiesInRange<HumanoidProfileComponent>(centerCoords, 6f);
foreach (var humanoidEntity in nearbyHumanoids)
{
var humanoid = humanoidEntity.Owner;
if (humanoid == ent.Owner)
continue;
if (_mobState.IsIncapacitated(humanoid))
continue;
Spawn(args.EntityId, Transform(humanoid).Coordinates);
_audio.PlayPvs(args.Sound, humanoid);
_popup.PopupEntity(Loc.GetString("vampire-predator-senses-puddle"), humanoid, humanoid, PopupType.SmallCaution);
_stun.TryUpdateParalyzeDuration(humanoid, TimeSpan.FromSeconds(4));
args.Handled = true;
return;
}
var closestHumanoid = FindClosestHumanoidOnMap(ent);
if (closestHumanoid != null)
{
var direction = GetDirectionToTarget(ent, closestHumanoid.Value);
var directionString = ContentLocalizationManager.FormatDirection(direction).ToLower();
var msg = Loc.GetString("vampire-predator-senses-warning", ("direction", directionString));
_popup.PopupEntity(msg, ent, ent, PopupType.Medium);
}
else
{
_popup.PopupEntity(Loc.GetString("vampire-predator-senses-nobody"), ent, ent, PopupType.SmallCaution);
}
args.Handled = true;
}
private void OnVampireBloodEruptionAction(Entity<VampireComponent> ent, ref VampireBloodEruptionActionEvent args)
{
if (!CheckBloodEssence(ent.Owner, args.BloodCost))
{
SendFailedPopup(ent);
return;
}
var puddlesInRange = _entityLookup.GetEntitiesInRange<PuddleComponent>(Transform(ent).Coordinates, 4f)
.Where(puddle => TryComp(puddle.Owner, out ContainerManagerComponent? containerManager)
&& containerManager.Containers.TryGetValue("solution@puddle", out var container)
&& container.ContainedEntities.Any(containedEntity =>
TryComp(containedEntity, out SolutionComponent? solutionComponent)
&& solutionComponent.Solution.Contents.Any(r =>
BloodProto.Contains(r.Reagent.Prototype))))
.ToList();
foreach (var puddleEntity in puddlesInRange)
{
var entitiesOnPuddle = _entityLookup.GetEntitiesInRange<DamageableComponent>(Transform(puddleEntity.Owner).Coordinates, 0.1f)
.Where(entity => entity.Owner != ent.Owner).ToList();
foreach (var targetEntity in entitiesOnPuddle)
{
if (HasComp<NullRodOwnerComponent>(targetEntity.Owner) && !HasTruePower(ent))
continue;
_damage.TryChangeDamage(targetEntity.Owner, args.Damage, origin: ent);
_stun.TryUpdateParalyzeDuration(targetEntity.Owner, TimeSpan.FromSeconds(3));
_popup.PopupEntity(Loc.GetString("vampire-blood-eruption-effect-message"), targetEntity.Owner, ent, PopupType.MediumCaution);
}
}
SubtractBloodEssence(ent.Owner, args.BloodCost);
args.Handled = true;
}
private void OnBloodBringersRite(Entity<VampireComponent> ent, ref VampireBloodBringersRiteActionEvent args)
{
var supreme = GetTruePower(ent);
if (supreme == null)
return;
if (!CheckBloodEssence(ent.Owner, args.BloodCost))
{
SendFailedPopup(ent);
return;
}
if (supreme.Active)
{
supreme.Active = false;
_alerts.ShowAlert(ent.Owner, args.Alert, 0);
Dirty(ent.Owner, supreme);
args.Handled = true;
return;
}
supreme.Active = true;
_alerts.ShowAlert(ent.Owner, args.Alert, 1);
Dirty(ent.Owner, supreme);
_popup.PopupEntity(Loc.GetString("vampire-blood-true-power-started"), ent, ent, PopupType.SmallCaution);
ExecuteBloodBringersRiteTick(ent, supreme, args);
SubtractBloodEssence(ent.Owner, args.BloodCost);
}
#region Utility Methods
private void ExecuteBloodBringersRiteTick(Entity<VampireComponent> ent, SupremeVampireComponent supreme, VampireBloodBringersRiteActionEvent args)
{
if (!Exists(ent) || !supreme.Active)
{
supreme.Active = false;
_alerts.ShowAlert(ent.Owner, args.Alert, 0);
Dirty(ent.Owner, supreme);
return;
}
if (!CheckBloodEssence(ent.Owner, args.BloodCost))
{
SendFailedPopup(ent);
supreme.Active = false;
_alerts.ShowAlert(ent.Owner, args.Alert, 0);
Dirty(ent.Owner, supreme);
return;
}
SubtractBloodEssence(ent.Owner, args.BloodCost);
var nearbyEntities = _entityLookup.GetEntitiesInRange<MobStateComponent>(Transform(ent).Coordinates, 7f)
.Where(entity => !_mobState.IsDead(entity.Owner) && !HasComp<SyntheticOperatedComponent>(entity.Owner))
.ToList();
if (nearbyEntities.Count > 0)
{
var groupHealSpec = _damage.CreateHealFromGroups(ent.Owner, args.HealGroups);
var scaledHealingSpec = (args.Heal + groupHealSpec) * nearbyEntities.Count;
_damage.TryChangeDamage(ent.Owner, scaledHealingSpec, true, false, origin: ent);
_stamina.TakeStaminaDamage(ent, args.StaminaMod * nearbyEntities.Count, visual: false);
foreach (var entity in nearbyEntities)
{
if (HasComp<NullRodOwnerComponent>(entity.Owner) && !HasTruePower(ent))
continue;
_audio.PlayPvs(args.Sound, entity);
_blood.TryBleedOut(entity.Owner, args.BloodCost);
_popup.PopupEntity(Loc.GetString("vampire-blood-true-power-affected"), entity.Owner, entity.Owner, PopupType.SmallCaution);
}
}
Timer.Spawn(args.TimeInterval, () => ExecuteBloodBringersRiteTick(ent, supreme, args));
}
private EntityUid? FindClosestHumanoidOnMap(Entity<VampireComponent> ent)
{
var currentMap = Transform(ent).MapID;
var currentCoords = Transform(ent).Coordinates;
EntityUid? closestHumanoid = null;
float closestDistance = float.MaxValue;
var query = EntityQueryEnumerator<HumanoidProfileComponent, TransformComponent>();
while (query.MoveNext(out var humanoid, out _, out var transform))
{
if (humanoid == ent.Owner)
continue;
if (transform.MapID != currentMap)
continue;
if (_mobState.IsIncapacitated(humanoid))
continue;
var humanoidCoords = _transform.GetMapCoordinates(humanoid, transform);
var distance = Vector2.Distance(currentCoords.Position, humanoidCoords.Position);
if (distance < closestDistance)
{
closestDistance = distance;
closestHumanoid = humanoid;
}
}
return closestHumanoid;
}
private Direction GetDirectionToTarget(Entity<VampireComponent> source, EntityUid target)
{
var sourceCoords = Transform(source).Coordinates;
var targetCoords = Transform(target).Coordinates;
var directionVector = targetCoords.Position - sourceCoords.Position;
return directionVector.GetDir();
}
private Vector2 DirectionToVector2(Direction direction)
{
return direction switch
{
Direction.North => new Vector2(0, 1),
Direction.South => new Vector2(0, -1),
Direction.East => new Vector2(1, 0),
Direction.West => new Vector2(-1, 0),
Direction.NorthEast => new Vector2(1, 1).Normalized(),
Direction.NorthWest => new Vector2(-1, 1).Normalized(),
Direction.SouthEast => new Vector2(1, -1).Normalized(),
Direction.SouthWest => new Vector2(-1, -1).Normalized(),
_ => Vector2.Zero,
};
}
#endregion
}
@@ -0,0 +1,318 @@
using System.Linq;
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.DoAfter;
using Content.Shared.Emp;
using Content.Shared.Mobs.Components;
using Content.Shared.Movement.Components;
using Content.Shared.NullRod.Components;
using Content.Shared.Physics;
using Content.Shared.Popups;
using Content.Shared.Stealth.Components;
using Content.Shared.Vampire;
using Content.Shared.Vampire.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Timing;
namespace Content.Server.Vampire;
public sealed partial class VampireSystem
{
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
[Dependency] private readonly SharedEmpSystem _emp = default!;
private void InitializeUmbrae()
{
SubscribeLocalEvent<VampireComponent, VampireCloakOfDarknessActionEvent>(OnCloakOfDarkness);
SubscribeLocalEvent<VampireComponent, VampireShadowSnareActionEvent>(OnShadowSnare);
SubscribeLocalEvent<VampireComponent, VampireSoulAnchorActionEvent>(OnAfterSoulAnchor);
SubscribeLocalEvent<VampireComponent, SoulAnchorDoAfterEvent>(OnSoulAnchorDoAfter);
SubscribeLocalEvent<VampireComponent, VampireDarkPassageActionEvent>(OnVampireDarkPassage);
SubscribeLocalEvent<VampireComponent, VampireExtinguishActionEvent>(OnExtinguish);
SubscribeLocalEvent<VampireComponent, VampireShadowBoxingActionEvent>(OnShadowBoxing);
SubscribeLocalEvent<VampireComponent, VampireEternalDarknessActionEvent>(OnEternalDarkness);
}
private void OnCloakOfDarkness(Entity<VampireComponent> ent, ref VampireCloakOfDarknessActionEvent args)
{
if (!TryComp<StealthComponent>(ent, out var stealth))
{
stealth = EnsureComp<StealthComponent>(ent);
_stealth.SetVisibility(ent, 0.3f, stealth);
_stealth.SetEnabled(ent, false, stealth);
}
if (TryComp(ent, out MovementSpeedModifierComponent? speedmodComponent))
{
var originalWalkSpeed = speedmodComponent.BaseWalkSpeed;
var originalSprintSpeed = speedmodComponent.BaseSprintSpeed;
if (stealth.Enabled)
{
_stealth.SetEnabled(ent, false, stealth);
_speed.ChangeBaseSpeed(ent, originalWalkSpeed / args.SpeedMod, originalSprintSpeed / args.SpeedMod, speedmodComponent.Acceleration);
_popup.PopupEntity(Loc.GetString("vampire-stealth-disabled"), ent, ent, PopupType.Small);
}
else
{
if (!CheckBloodEssence(ent.Owner, args.BloodCost))
{
SendFailedPopup(ent);
return;
}
_stealth.SetEnabled(ent, true, stealth);
_speed.ChangeBaseSpeed(ent, originalWalkSpeed * args.SpeedMod, originalSprintSpeed * args.SpeedMod, speedmodComponent.Acceleration);
_popup.PopupEntity(Loc.GetString("vampire-stealth-enabled"), ent, ent, PopupType.Small);
SubtractBloodEssence(ent.Owner, args.BloodCost);
}
}
args.Handled = true;
}
private void OnShadowSnare(Entity<VampireComponent> ent, ref VampireShadowSnareActionEvent args)
{
if (!CheckBloodEssence(ent.Owner, args.BloodCost))
{
SendFailedPopup(ent);
return;
}
var targetCoords = args.Target;
if (TrySpawnObjectAtPosition(targetCoords, args.EntityId, ent))
{
SubtractBloodEssence(ent.Owner, args.BloodCost);
args.Handled = true;
}
}
private void OnAfterSoulAnchor(Entity<VampireComponent> ent, ref VampireSoulAnchorActionEvent args)
{
if (!CheckBloodEssence(ent.Owner, args.BloodCost))
{
SendFailedPopup(ent);
return;
}
EntityUid? beaconEntity = null;
var beaconQuery = EntityQueryEnumerator<BeaconSoulComponent>();
while (beaconQuery.MoveNext(out var beaconUid, out var beaconComp))
{
if (beaconComp.VampireOwner == ent.Owner)
{
beaconEntity = beaconUid;
break;
}
}
if (beaconEntity.HasValue)
{
RaiseLocalEvent(ent, new SoulAnchorDoAfterEvent(args.BloodCost));
args.Handled = true;
return;
}
args.Handled = true;
_doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, ent, TimeSpan.FromSeconds(15f), new SoulAnchorDoAfterEvent(args.BloodCost), ent)
{
BreakOnMove = false,
NeedHand = false,
});
}
private void OnSoulAnchorDoAfter(Entity<VampireComponent> ent, ref SoulAnchorDoAfterEvent args)
{
EntityUid? beaconEntity = null;
var beaconQuery = EntityQueryEnumerator<BeaconSoulComponent>();
while (beaconQuery.MoveNext(out var beaconUid, out var beaconComp))
{
if (beaconComp.VampireOwner == ent.Owner)
{
beaconEntity = beaconUid;
break;
}
}
if (beaconEntity.HasValue)
{
_transform.SetCoordinates(ent, Transform(beaconEntity.Value).Coordinates);
Del(beaconEntity.Value);
SubtractBloodEssence(ent.Owner, args.BloodCost);
}
else
{
var beaconEntityNew = Spawn(args.EntityId, Transform(ent).Coordinates);
var beaconComponent = EnsureComp<BeaconSoulComponent>(beaconEntityNew);
beaconComponent.VampireOwner = ent;
}
}
private void OnVampireDarkPassage(Entity<VampireComponent> ent, ref VampireDarkPassageActionEvent args)
{
var targetCoords = args.Target;
if (!HasTruePower(ent))
{
if (!_interaction.InRangeUnobstructed(ent, targetCoords, range: 1000F, collisionMask: CollisionGroup.Impassable, popup: false))
{
_popup.PopupEntity(Loc.GetString("vampire-teleport-failed"), ent, ent, PopupType.Small);
return;
}
}
if (!CheckBloodEssence(ent.Owner, args.BloodCost))
{
SendFailedPopup(ent);
return;
}
if (HasComp<NullRodOwnerComponent>(targetCoords.EntityId) && !HasTruePower(ent))
return;
var currentCoords = Transform(ent).Coordinates;
_transform.SetCoordinates(ent, targetCoords);
Spawn(args.MistEffect, currentCoords);
Spawn(args.MistReappearEffect, targetCoords);
SubtractBloodEssence(ent.Owner, args.BloodCost);
args.Handled = true;
}
private void OnExtinguish(Entity<VampireComponent> ent, ref VampireExtinguishActionEvent args)
{
if (!CheckBloodEssence(ent.Owner, args.BloodCost))
{
SendFailedPopup(ent);
return;
}
DamageLightsInRange(ent, 15f, args.Damage);
_emp.EmpPulse(Transform(ent).Coordinates, 4, 5000, TimeSpan.FromSeconds(30), ent);
SubtractBloodEssence(ent.Owner, args.BloodCost);
args.Handled = true;
}
private void OnShadowBoxing(Entity<VampireComponent> ent, ref VampireShadowBoxingActionEvent args)
{
if (!CheckBloodEssence(ent.Owner, args.BloodCost))
{
SendFailedPopup(ent);
return;
}
ExecuteShadowBoxingTick(ent, args, 0);
SubtractBloodEssence(ent.Owner, args.BloodCost);
args.Handled = true;
}
private void OnEternalDarkness(Entity<VampireComponent> ent, ref VampireEternalDarknessActionEvent args)
{
var supreme = GetTruePower(ent);
if (supreme == null)
return;
if (!CheckBloodEssence(ent.Owner, args.BloodCost))
{
SendFailedPopup(ent);
return;
}
var netEntity = GetNetEntity(ent);
if (supreme.Active)
{
supreme.Active = false;
_alerts.ClearAlert(ent.Owner, args.Alert);
Dirty(ent, supreme);
args.Handled = true;
return;
}
_alerts.ShowAlert(ent.Owner, args.Alert, 0);
supreme.Active = true;
Dirty(ent, supreme);
_popup.PopupEntity(Loc.GetString("vampire-blood-true-power-started"), ent, ent, PopupType.SmallCaution);
ExecuteEternalDarknessTick(ent, supreme, args, netEntity);
SubtractBloodEssence(ent.Owner, args.BloodCost);
args.Handled = true;
}
#region Utility Methods
private void ExecuteShadowBoxingTick(EntityUid uid, VampireShadowBoxingActionEvent args, int currentTick)
{
if (!Exists(uid) || currentTick >= args.Repeats)
return;
Spawn(args.EntityId, Transform(uid).Coordinates);
Timer.Spawn(args.TimeInterval, () => ExecuteShadowBoxingTick(uid, args, currentTick + 1));
}
private void ExecuteEternalDarknessTick(Entity<VampireComponent> ent, SupremeVampireComponent supreme, VampireEternalDarknessActionEvent args, NetEntity netEntity)
{
if (!Exists(ent) || !supreme.Active)
{
_alerts.ClearAlert(ent.Owner, args.Alert);
supreme.Active = false;
Dirty(ent, supreme);
return;
}
if (!CheckBloodEssence(ent.Owner, args.BloodCost))
{
SendFailedPopup(ent);
supreme.Active = false;
_alerts.ClearAlert(ent.Owner, args.Alert);
Dirty(ent, supreme);
return;
}
CoolSurroundingAtmosphere(ent);
DamageLightsInRange(ent, 4f, args.Damage);
SubtractBloodEssence(ent.Owner, args.BloodCost);
Timer.Spawn(args.TimeInterval, () => ExecuteEternalDarknessTick(ent, supreme, args, netEntity));
}
private void DamageLightsInRange(Entity<VampireComponent> ent, float radius, DamageSpecifier damage)
{
var coords = Transform(ent).Coordinates;
var lightsInRange = _entityLookup.GetEntitiesInRange<PointLightComponent>(coords, radius)
.Where(entity => HasComp<DamageableComponent>(entity.Owner)
&& !HasComp<MobStateComponent>(entity.Owner)).ToList();
foreach (var lightEntity in lightsInRange)
{
_damage.TryChangeDamage(lightEntity.Owner, damage, true, origin: ent);
}
}
private void CoolSurroundingAtmosphere(EntityUid ent)
{
if (_atmosphere.GetContainingMixture(ent, excite: true) is { } atmosphere)
{
const float targetTemperature = 233.15f;
const float coolingRate = 20000f;
var deltaT = targetTemperature - atmosphere.Temperature;
if (deltaT < 0)
{
var heatCapacity = _atmosphere.GetHeatCapacity(atmosphere, true);
var energyToRemove = Math.Min(Math.Abs(deltaT) * heatCapacity, coolingRate);
_atmosphere.AddHeat(atmosphere, -energyToRemove);
}
}
}
#endregion
}
@@ -0,0 +1,182 @@
using Content.Shared.Damage.Components;
using Content.Shared.Physics;
using Content.Shared.Popups;
using Content.Shared.Vampire;
using Content.Shared.Vampire.Components;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
using Content.Shared.Flash.Components;
using Content.Server.Bible.Components;
using Robust.Shared.Timing;
using Content.Shared.Movement.Systems;
using Content.Shared.Damage.Systems;
using Content.Shared.Flash;
using Content.Shared.StatusEffectNew;
using Content.Shared.Stunnable;
using Content.Shared.Damage;
using Content.Shared.Mobs.Systems;
using Content.Shared.Mobs;
using Content.Shared.Stealth;
using Content.Server.Polymorph.Systems;
using Content.Server.Surgery;
using Content.Shared.Surgery;
namespace Content.Server.Vampire;
public sealed partial class VampireSystem
{
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly MobThresholdSystem _threshold = default!;
[Dependency] private readonly MovementSpeedModifierSystem _speed = default!;
[Dependency] private readonly SharedStaminaSystem _stamina = default!;
[Dependency] private readonly StatusEffectsSystem _status = default!;
[Dependency] private readonly SharedStealthSystem _stealth = default!;
[Dependency] private readonly SharedFlashSystem _flash = default!;
[Dependency] private readonly MovementModStatusSystem _movementMod = default!;
[Dependency] private readonly PolymorphSystem _polymorph = default!;
[Dependency] private readonly SurgerySystem _surgery = default!;
private static readonly ProtoId<InternalDamagePrototype> InternalBleeding = "ArterialBleeding";
private static readonly EntProtoId ForceSleeping = "StatusEffectForcedSleeping";
private void InitializePowers()
{
InitializeHemomancer();
InitializeUmbrae();
InitializeGargantua();
InitializeDantalion();
InitializeBestia();
// Basic Abilities
SubscribeLocalEvent<VampireComponent, VampireRejuvenateActionEvent>(OnRejuvenate);
SubscribeLocalEvent<VampireComponent, VampireGlareActionEvent>(OnVampireGlare);
}
#region Basic Abilities
private void OnRejuvenate(Entity<VampireComponent> ent, ref VampireRejuvenateActionEvent args)
{
if (_mobState.IsDead(args.Performer))
{
_popup.PopupEntity(Loc.GetString("vampire-heal-dead"), args.Performer, args.Performer, PopupType.MediumCaution);
return;
}
TryRemoveKnockdown(args.Performer);
_stamina.RemoveStaminaDamage(args.Performer);
if (args.Advanced || ent.Comp.CurrentBlood >= args.BloodCost)
{
ExecuteRejuvenateHealTick(args.Performer, 0, args);
if (TryComp<VampireDiablerieComponent>(ent, out var diablerie) && diablerie.DiablerieLevel >= 3)
_surgery.TryRemoveInternalDamage(ent, InternalBleeding);
}
args.Handled = true;
}
private void OnVampireGlare(Entity<VampireComponent> ent, ref VampireGlareActionEvent args)
{
var target = args.Target;
if (HasComp<VampireComponent>(target) || HasComp<FlashImmunityComponent>(target))
return;
if (HasComp<BibleUserComponent>(target) && !HasTruePower(ent))
{
_stun.TryUpdateParalyzeDuration(args.Performer, TimeSpan.FromSeconds(5f));
_chat.TryEmoteWithoutChat(args.Performer, _proto.Index(Scream), true);
_damage.TryChangeDamage(args.Performer, ent.Comp.HolyDamage);
return;
}
args.Handled = true;
var ev = new FlashAttemptEvent(target, args.Performer, null);
RaiseLocalEvent(target, ref ev, true);
if (ev.Cancelled)
return;
_stun.TryUpdateParalyzeDuration(target, TimeSpan.FromSeconds(5f));
_flash.Flash(target, args.Performer, null, TimeSpan.FromSeconds(3f), 0.8f);
_status.TryAddStatusEffectDuration(target, "Muted", TimeSpan.FromSeconds(8f));
}
#endregion
#region Utility Methods
private void SendFailedPopup(EntityUid uid)
{
_popup.PopupEntity(Loc.GetString("vampire-blood-sacrifice-insufficient-blood"), uid, uid, PopupType.SmallCaution);
}
private bool TryRemoveKnockdown(Entity<StaminaComponent?> entity)
{
if (!Resolve(entity, ref entity.Comp, false))
return false;
_status.TryRemoveStatusEffect(entity.Owner, SharedStunSystem.StunId);
_stun.ForceStandUp(entity.Owner);
return true;
}
private void ExecuteRejuvenateHealTick(EntityUid uid, int currentTick, VampireRejuvenateActionEvent args)
{
if (!Exists(uid) || currentTick >= args.Repeats)
return;
var healingSpec = CalculateScaledHealing(uid, args.Heal, args.HealGroups);
var stomachCount = GetOrganTypeCount(uid, BestiaOrganType.Stomach);
var bonus = stomachCount * 3;
if (bonus > 0)
{
var bonusMultiplier = 1f + (bonus / 100f);
healingSpec *= bonusMultiplier;
}
_damage.TryChangeDamage(uid, healingSpec, true, false, origin: uid);
Timer.Spawn(args.TimeInterval, () => ExecuteRejuvenateHealTick(uid, currentTick + 1, args));
}
private DamageSpecifier CalculateScaledHealing(EntityUid uid, DamageSpecifier heal, GroupHealSpecifier healGroups)
{
var totalDamage = _damage.GetTotalDamage(uid).Float();
if (!_threshold.TryGetThresholdForState(uid, MobState.Dead, out var threshold))
return new DamageSpecifier();
var maxDamage = threshold.Value.Float();
var damagePercent = maxDamage > 0 ? (totalDamage / maxDamage) * 100 : 0;
var modifier = Math.Clamp(damagePercent / 20.0, 1.0, 5.0);
var groupHealSpec = _damage.CreateWeightedHealFromGroups(uid, healGroups);
var scaledHeal = (heal + groupHealSpec) * modifier;
return scaledHeal;
}
private bool TrySpawnObjectAtPosition(EntityCoordinates coords, EntProtoId entityId, EntityUid uid)
{
var grid = _transform.GetGrid(coords);
if (grid == null) return false;
var gridEntityUid = grid.Value;
if (!TryComp<MapGridComponent>(gridEntityUid, out var gridComp))
return false;
if (!_map.TryGetTileRef(gridEntityUid, gridComp, coords, out var tileRef)
|| _turf.IsTileBlocked(tileRef, CollisionGroup.Impassable))
return false;
var ent = Spawn(entityId, coords);
var comp = EnsureComp<PreventCollideComponent>(ent);
comp.Uid = uid;
return true;
}
#endregion
}
@@ -0,0 +1,50 @@
using Content.Shared.HealthExaminable;
using Content.Shared.Surgery.Components;
using Content.Shared.Vampire.Components;
using Robust.Shared.Timing;
namespace Content.Server.Vampire;
/// <summary>
/// Medics can see bite on your neck.
/// </summary>
public sealed class BittenByVampireSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<BittenByVampireComponent, ComponentAdd>(OnComponentAdd);
SubscribeLocalEvent<BittenByVampireComponent, HealthBeingExaminedEvent>(OnHealthBeingExamined);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var currentTime = _timing.CurTime;
var query = EntityQueryEnumerator<BittenByVampireComponent>();
while (query.MoveNext(out var uid, out var bitten))
{
if (currentTime >= bitten.ExpirationTime)
RemComp<BittenByVampireComponent>(uid);
}
}
private void OnComponentAdd(EntityUid uid, BittenByVampireComponent component, ComponentAdd args)
{
component.ExpirationTime = _timing.CurTime + TimeSpan.FromSeconds(component.LifetimeSeconds);
}
private void OnHealthBeingExamined(Entity<BittenByVampireComponent> ent, ref HealthBeingExaminedEvent args)
{
if (HasComp<SurgicalSkillComponent>(args.Examiner))
{
args.Message.PushNewline();
args.Message.AddMarkupOrThrow(Loc.GetString("vampire-bittenbyvampire-examine"));
}
}
}
@@ -0,0 +1,51 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Chat.Systems;
using Content.Shared.Atmos.Components;
using Content.Shared.Chat.Prototypes;
using Content.Shared.Popups;
using Content.Shared.Vampire.Components;
using Robust.Shared.Prototypes;
namespace Content.Server.Vampire;
public sealed class HolyPointSystem : EntitySystem
{
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly EntityLookupSystem _entityLookup = default!;
[Dependency] private readonly FlammableSystem _flammable = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
private static readonly ProtoId<EmotePrototype> Scream = "Scream";
private static readonly float FireStackCount = 2.5f;
public override void Update(float frameTime)
{
base.Update(frameTime);
var holyPointQuery = EntityQueryEnumerator<HolyPointComponent>();
while (holyPointQuery.MoveNext(out var uid, out var holyPoint))
{
if (holyPoint.NextTimeTick <= 0)
{
holyPoint.NextTimeTick = 10;
var vampires = _entityLookup.GetEntitiesInRange<VampireComponent>(Transform(uid).Coordinates, holyPoint.Range);
foreach (var vampire in vampires)
{
if (HasComp<SupremeVampireComponent>(vampire))
continue;
if (TryComp(vampire.Owner, out FlammableComponent? flammable))
{
flammable.FireStacks += FireStackCount;
_flammable.Ignite(vampire.Owner, uid);
_chat.TryEmoteWithoutChat(vampire, _proto.Index(Scream), true);
_popup.PopupEntity(Loc.GetString("vampire-holy-point"), vampire.Owner, vampire.Owner, PopupType.LargeCaution);
}
}
}
holyPoint.NextTimeTick -= frameTime;
}
}
}
@@ -0,0 +1,39 @@
using Content.Server.EUI;
using Content.Shared.Vampire;
using Content.Shared.Eui;
using JetBrains.Annotations;
using Content.Shared.FixedPoint;
namespace Content.Server.Vampire;
[UsedImplicitly]
public sealed class DissectSelectionEui : BaseEui
{
private readonly EntityUid _vampire;
private readonly EntityUid _target;
private readonly FixedPoint2 _blood;
private readonly VampireSystem _vampireSystem;
private readonly DissectSelectionEuiState _state;
public DissectSelectionEui(EntityUid vampire, EntityUid target, FixedPoint2 bloodCost, VampireSystem vampireSystem, DissectSelectionEuiState state)
{
_vampire = vampire;
_target = target;
_blood = bloodCost;
_vampireSystem = vampireSystem;
_state = state;
}
public override EuiStateBase GetNewState() => _state;
public override void HandleMessage(EuiMessageBase msg)
{
base.HandleMessage(msg);
if (msg is DissectOrganSelectedMessage selected)
{
_vampireSystem.StartDissection(_vampire, _target, selected.Target, _blood);
Close();
}
}
}
@@ -0,0 +1,19 @@
using Content.Server.EUI;
using Content.Shared.Vampire;
using Content.Shared.Eui;
using JetBrains.Annotations;
namespace Content.Server.Vampire;
[UsedImplicitly]
public sealed class TrophiesMenuEui : BaseEui
{
private readonly TrophiesEuiState _state;
public TrophiesMenuEui(TrophiesEuiState state)
{
_state = state;
}
public override EuiStateBase GetNewState() => _state;
}
@@ -0,0 +1,30 @@
using Content.Server.EUI;
using Content.Shared.Vampire;
using Content.Shared.Eui;
namespace Content.Server.Vampire;
public sealed class VampireClassSelectionEui : BaseEui
{
private readonly EntityUid _vampire;
private readonly VampireSystem _vampireSystem;
public VampireClassSelectionEui(EntityUid vampire, VampireSystem vampireSystem)
{
_vampire = vampire;
_vampireSystem = vampireSystem;
}
public override EuiStateBase GetNewState() => new VampireClassSelectionState();
public override void HandleMessage(EuiMessageBase msg)
{
base.HandleMessage(msg);
if (msg is VampireClassSelectedMessage selected)
{
_vampireSystem.OnClassSelected(_vampire, selected.SelectedClass);
Close();
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,232 @@
using Content.Server.Bible.Components;
using Content.Shared.Administration.Systems;
using Content.Shared.Charges.Systems;
using Content.Shared.Database;
using Content.Shared.Examine;
using Content.Shared.FixedPoint;
using Content.Shared.Humanoid;
using Content.Shared.IdentityManagement;
using Content.Shared.IdentityManagement.Components;
using Content.Shared.Inventory;
using Content.Shared.Mindshield.Components;
using Content.Shared.Popups;
using Content.Shared.Surgery.Components;
using Content.Shared.Vampire;
using Content.Shared.Vampire.Components;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
namespace Content.Server.Vampire;
public sealed partial class VampireSystem
{
[Dependency] private readonly RejuvenateSystem _rejuvenate = default!;
[Dependency] private readonly SharedChargesSystem _charges = default!;
private static readonly EntProtoId Ash = "Ash";
private void InitializeDiablerie()
{
SubscribeLocalEvent<VampireDiablerieComponent, ComponentStartup>(OnDiablerieStartup);
SubscribeLocalEvent<VampireDiablerieComponent, ExaminedEvent>(OnDiablerieExamined);
SubscribeLocalEvent<VampireDiablerieComponent, VampireSacramentInitiationActionEvent>(OnSacramentInitiation);
}
private void OnDiablerieStartup(Entity<VampireDiablerieComponent> ent, ref ComponentStartup args)
=> ApplyDiablerieBonuses(ent);
private void OnDiablerieExamined(EntityUid uid, VampireDiablerieComponent component, ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
var name = Identity.Name(uid, EntityManager, args.Examiner);
if (component.DiablerieLevel >= 1 && HasComp<VampireComponent>(args.Examiner) || component.DiablerieLevel >= 3)
args.PushMarkup(Loc.GetString("vampire-diablerie-aura-visible", ("name", name)));
if (component.DiablerieLevel >= 2)
{
bool eyesCovered = false;
var clothes = _inventory.GetSlotEnumerator(uid, SlotFlags.WITHOUT_POCKET);
while (clothes.NextItem(out var cloth, out _))
{
if (TryComp<IdentityBlockerComponent>(cloth, out var blocker) && blocker.Coverage.HasFlag(IdentityBlockerCoverage.EYES)
&& blocker.Enabled)
{
eyesCovered = true;
break;
}
}
if (!eyesCovered)
{
args.PushMarkup(Loc.GetString("vampire-diablerie-eyes-glow-examined", ("name", name)));
}
}
}
private void OnSacramentInitiation(Entity<VampireDiablerieComponent> ent, ref VampireSacramentInitiationActionEvent args)
{
if (!CheckBloodEssence(ent.Owner, args.BloodCost))
{
SendFailedPopup(ent);
return;
}
var target = args.Target;
if (ent.Owner == target)
return;
if (!_mobState.IsDead(target))
{
_popup.PopupEntity(Loc.GetString("vampire-initiation-failed"), ent, ent, PopupType.SmallCaution);
return;
}
if (!HasComp<HumanoidProfileComponent>(target) || HasComp<VampireComponent>(target) || HasComp<MindShieldComponent>(target))
{
_popup.PopupEntity(Loc.GetString("vampire-initiation-failed"), ent, ent, PopupType.SmallCaution);
return;
}
if (HasComp<SyntheticOperatedComponent>(target) || HasComp<BibleUserComponent>(target))
{
_popup.PopupEntity(Loc.GetString("vampire-initiation-failed"), ent, ent, PopupType.SmallCaution);
return;
}
_rejuvenate.PerformRejuvenate(target);
EnsureComp<VampireInferiorComponent>(target);
var vampire = EnsureComp<VampireComponent>(target);
var state = EnsureComp<VampireOriginalStateComponent>(target);
SaveOriginalState((target, vampire), state);
MakeVampire((target, vampire), true);
_antag.SendBriefing(target, Loc.GetString("free-vampire-greeting"), Color.Purple,
new SoundPathSpecifier("/Audio/_Wega/Ambience/Antag/vampare_start.ogg"));
SubtractBloodEssence(ent.Owner, args.BloodCost);
args.Handled = true;
}
private bool TryPerformDiablerie(Entity<VampireComponent> vampire, Entity<VampireComponent> targetVampire, FixedPoint2 volumeToEssence)
{
if (vampire.Comp.CurrentEvolution == VampireClassEnum.NonSelected)
{
_popup.PopupEntity(Loc.GetString("vampire-diablerie-no-class"), vampire, vampire, PopupType.SmallCaution);
return false;
}
var bloodToDrain = FixedPoint2.Min(targetVampire.Comp.CurrentBlood, volumeToEssence);
SubtractBloodEssence(targetVampire.Owner, bloodToDrain);
if (targetVampire.Comp.CurrentBlood <= 0)
{
CompleteDiablerie(vampire, targetVampire);
return false;
}
else
{
_popup.PopupEntity(Loc.GetString("vampire-diablerie-draining",
("target", Identity.Name(targetVampire, EntityManager))), vampire, vampire);
_popup.PopupEntity(Loc.GetString("vampire-diablerie-target-draining"), targetVampire, targetVampire);
}
return true;
}
private void CompleteDiablerie(Entity<VampireComponent> vampire, Entity<VampireComponent> targetVampire)
{
if (targetVampire.Comp.CurrentEvolution == VampireClassEnum.NonSelected)
{
_popup.PopupEntity(Loc.GetString("vampire-diablerie-target-no-class"), vampire, vampire, PopupType.MediumCaution);
return;
}
var targetName = Identity.Name(targetVampire, EntityManager);
_popup.PopupCoordinates(Loc.GetString("vampire-diablerie-dust", ("target", targetName)),
Transform(targetVampire).Coordinates, PopupType.Large);
_admin.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(vampire)} performed diablerie on {ToPrettyString(targetVampire)}");
var diablerie = EnsureComp<VampireDiablerieComponent>(vampire);
IncreaseDiablerieLevel((vampire, diablerie));
Spawn(Ash, Transform(targetVampire).Coordinates);
foreach (var item in _inventory.GetHandOrInventoryEntities(targetVampire.Owner))
_transform.DropNextTo(item, targetVampire.Owner);
QueueDel(targetVampire);
}
private void IncreaseDiablerieLevel(Entity<VampireDiablerieComponent> ent)
{
if (ent.Comp.DiablerieLevel >= ent.Comp.MaxDiablerieLevel)
return;
ent.Comp.DiablerieLevel++;
ApplyDiablerieBonuses(ent);
Dirty(ent.Owner, ent.Comp);
}
private void ApplyDiablerieBonuses(Entity<VampireDiablerieComponent> ent, VampireComponent? vampire = null)
{
if (!Resolve(ent.Owner, ref vampire, false))
return;
var level = ent.Comp.DiablerieLevel;
switch (level)
{
case 1:
{
if (vampire.RejuvenateActionEntity != null)
{
_charges.SetMaxCharges(vampire.RejuvenateActionEntity.Value, 2);
}
_popup.PopupEntity(Loc.GetString("vampire-diablerie-level-one"), ent, ent, PopupType.Medium);
break;
}
case 2:
{
if (vampire.GlareActionEntity != null)
{
_charges.SetMaxCharges(vampire.GlareActionEntity.Value, 3);
}
_popup.PopupEntity(Loc.GetString("vampire-diablerie-level-two"), ent, ent, PopupType.Medium);
break;
}
case 3:
_popup.PopupEntity(Loc.GetString("vampire-diablerie-level-three"), ent, ent, PopupType.Medium);
break;
case 4:
{
AnnounceVampireAscended(ent);
GrantSacramentInitiationAbility(ent);
_popup.PopupEntity(Loc.GetString("vampire-diablerie-level-four"), ent, ent, PopupType.Large);
break;
}
}
}
private void AnnounceVampireAscended(Entity<VampireDiablerieComponent> ent)
{
_chat.DispatchGlobalAnnouncement(Loc.GetString("vampire-ascended-announcement", ("name", Name(ent))), colorOverride: Color.Red);
_admin.Add(LogType.Action, LogImpact.Extreme, $"{ToPrettyString(ent)} has reached maximum diablerie level ({ent.Comp.MaxDiablerieLevel})");
}
private void GrantSacramentInitiationAbility(Entity<VampireDiablerieComponent> ent)
{
if (ent.Comp.SacramentInitiationActionEntity != null)
return;
ent.Comp.SacramentInitiationActionEntity = _action.AddAction(ent.Owner,
VampireDiablerieComponent.SacramentInitiationActionPrototype);
}
}
@@ -0,0 +1,551 @@
using System.Linq;
using Content.Shared.Body;
using Content.Shared.Eye.Blinding.Components;
using Content.Shared.Flash.Components;
using Content.Shared.Metabolism;
using Content.Shared.Vampire;
using Content.Shared.Vampire.Components;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Vampire;
public sealed partial class VampireSystem
{
private static readonly ProtoId<MetabolismStagePrototype>[] CriticalStages = new ProtoId<MetabolismStagePrototype>[]
{
"Bloodstream", "Respiration"
};
private static readonly ProtoId<OrganCategoryPrototype> Eyes = "Eyes";
private void InitializeOrgans()
{
SubscribeLocalEvent<BestiaContainerComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<BestiaContainerComponent, ComponentShutdown>(OnShutdown);
}
private void OnMapInit(EntityUid uid, BestiaContainerComponent component, ref MapInitEvent args)
{
component.OrgansContainer = _container.EnsureContainer<Container>(uid, BestiaContainerComponent.ContainerId);
}
private void OnShutdown(EntityUid uid, BestiaContainerComponent component, ref ComponentShutdown args)
{
var container = component.OrgansContainer;
if (container.ContainedEntities.Count == 0)
return;
var coords = Transform(uid).Coordinates;
foreach (var organ in container.ContainedEntities)
{
_container.Remove(organ, container, force: true, destination: coords);
_throwing.TryThrow(organ, _random.NextVector2(), 5f);
}
}
#region Eui State
public TrophiesEuiState? GetTrophiesState(Entity<VampireComponent?, BestiaContainerComponent?> vampire)
{
if (!Resolve(vampire, ref vampire.Comp1, false) || !Resolve(vampire, ref vampire.Comp2, false))
return null;
var organs = GetOrganDisplayInfo(vampire.Comp2);
var passives = GetPassiveBonuses((vampire.Owner, vampire.Comp2));
var abilities = GetAbilityBonuses(vampire.Owner, vampire.Comp1, vampire.Comp2);
return new TrophiesEuiState(organs, passives, abilities);
}
private List<OrganDisplayInfo> GetOrganDisplayInfo(BestiaContainerComponent bestia)
{
var result = new List<OrganDisplayInfo>();
var counts = GetOrganCounts(bestia.OrgansContainer);
var maxCritical = bestia.MaxCriticalOrgans;
var maxRegular = bestia.MaxRegularOrgans;
foreach (BestiaOrganType type in Enum.GetValues<BestiaOrganType>())
{
if (type == BestiaOrganType.Unknown)
continue;
var count = counts.GetValueOrDefault(type, 0);
var max = IsCriticalOrganType(type) ? maxCritical : maxRegular;
var (color, _) = GetOrganProgressColor(count, max);
var preview = GetPreviewForOrganType(bestia.OrgansContainer, type);
result.Add(new OrganDisplayInfo
{
Type = type,
Count = count,
MaxCount = max,
CountColor = color,
PreviewEntity = preview
});
}
return result;
}
private List<PassiveBonusInfo> GetPassiveBonuses(Entity<BestiaContainerComponent> entity)
{
var heartCount = GetOrganTypeCount(entity, BestiaOrganType.Heart);
var liverCount = GetOrganTypeCount(entity, BestiaOrganType.Liver);
var lungsCount = GetOrganTypeCount(entity, BestiaOrganType.Lungs);
var kidneysCount = GetOrganTypeCount(entity, BestiaOrganType.Kidneys);
var eyesCount = GetOrganTypeCount(entity, BestiaOrganType.Eyes);
var stomachCount = GetOrganTypeCount(entity, BestiaOrganType.Stomach);
var maxCritical = entity.Comp.MaxCriticalOrgans;
var maxRegular = entity.Comp.MaxRegularOrgans;
var passives = new List<PassiveBonusInfo>();
var heartColor = GetOrganProgressColor(heartCount, maxCritical);
passives.Add(new()
{
Name = Loc.GetString("vampire-bestia-passive-brute-protection"),
Value = $"-{(heartCount * 5)}%",
ValueColor = heartColor.color,
IsMaxed = heartColor.isMaxed
});
passives.Add(new()
{
Name = Loc.GetString("vampire-bestia-passive-burn-protection"),
Value = $"-{(heartCount * 5)}%",
ValueColor = heartColor.color,
IsMaxed = heartColor.isMaxed
});
var lungsColor = GetOrganProgressColor(lungsCount, maxCritical);
passives.Add(new()
{
Name = Loc.GetString("vampire-bestia-passive-oxy-protection"),
Value = $"-{(lungsCount * 5)}%",
ValueColor = lungsColor.color,
IsMaxed = lungsColor.isMaxed
});
passives.Add(new()
{
Name = Loc.GetString("vampire-bestia-passive-stamina"),
Value = $"+{(lungsCount * 5)}%",
ValueColor = lungsColor.color,
IsMaxed = lungsColor.isMaxed
});
var liverColor = GetOrganProgressColor(liverCount, maxRegular);
passives.Add(new()
{
Name = Loc.GetString("vampire-bestia-passive-toxin-protection"),
Value = $"-{(liverCount * 3)}%",
ValueColor = liverColor.color,
IsMaxed = liverColor.isMaxed
});
passives.Add(new()
{
Name = Loc.GetString("vampire-bestia-passive-blood-cost-reduction"),
Value = $"-{(liverCount * 2)}%",
ValueColor = liverColor.color,
IsMaxed = liverColor.isMaxed
});
var kidneysColor = GetOrganProgressColor(kidneysCount, maxRegular);
passives.Add(new()
{
Name = Loc.GetString("vampire-bestia-passive-suck-rate"),
Value = $"-{(kidneysCount * 0.3)}s",
ValueColor = kidneysColor.color,
IsMaxed = kidneysColor.isMaxed
});
passives.Add(new()
{
Name = Loc.GetString("vampire-bestia-passive-cellular-protection"),
Value = $"-{(kidneysCount * 3)}%",
ValueColor = kidneysColor.color,
IsMaxed = kidneysColor.isMaxed
});
var eyesColor = GetOrganProgressColor(eyesCount, maxRegular);
// passives.Add(new()
// {
// Name = Loc.GetString("vampire-bestia-passive-xray-vision"),
// Value = eyesCount > 0 ? Loc.GetString("vampire-bestia-passive-unlocked")
// : Loc.GetString("vampire-bestia-passive-locked"),
// ValueColor = eyesCount > 0 ? eyesColor.color : Color.Orange,
// IsMaxed = eyesColor.isMaxed
// });
passives.Add(new()
{
Name = Loc.GetString("vampire-bestia-passive-welding-protection"),
Value = eyesCount > 1 ? Loc.GetString("vampire-bestia-passive-unlocked")
: Loc.GetString("vampire-bestia-passive-locked"),
ValueColor = eyesCount > 1 ? eyesColor.color : Color.Orange,
IsMaxed = eyesColor.isMaxed
});
passives.Add(new()
{
Name = Loc.GetString("vampire-bestia-passive-flash-protection"),
Value = eyesCount > 5 ? Loc.GetString("vampire-bestia-passive-unlocked")
: Loc.GetString("vampire-bestia-passive-locked"),
ValueColor = eyesCount > 5 ? eyesColor.color : Color.Orange,
IsMaxed = eyesColor.isMaxed
});
var stomachColor = GetOrganProgressColor(stomachCount, maxRegular);
passives.Add(new()
{
Name = Loc.GetString("vampire-bestia-passive-blood-gain"),
Value = $"+{(stomachCount * 0.25)}",
ValueColor = stomachColor.color,
IsMaxed = stomachColor.isMaxed
});
passives.Add(new()
{
Name = Loc.GetString("vampire-bestia-passive-healing-efficiency"),
Value = $"+{(stomachCount * 3)}%",
ValueColor = stomachColor.color,
IsMaxed = stomachColor.isMaxed
});
return passives;
}
private List<AbilityDisplayInfo> GetAbilityBonuses(EntityUid uid, VampireComponent vampire, BestiaContainerComponent bestia)
{
var result = new List<AbilityDisplayInfo>();
var skills = vampire.AcquiredSkills.Keys.ToList();
var targetActions = new HashSet<EntProtoId>
{
"ActionVampireInfectedTrophy",
"ActionVampireLunge",
"ActionVampireMarkPrey",
"ActionVampireMetamorphosisBats",
"ActionVampireSummonBats",
"ActionVampireMetamorphosisHound"
};
foreach (var actionId in skills)
{
if (!targetActions.Contains(actionId))
continue;
var actionEntity = vampire.AcquiredSkills.GetValueOrDefault(actionId);
if (actionEntity == null)
continue;
var name = Name(actionEntity.Value) ?? actionId;
var bonuses = GetBonusesForAbility(uid, bestia, actionId);
result.Add(new AbilityDisplayInfo
{
Action = GetNetEntity(actionEntity.Value),
Name = name,
Bonuses = bonuses
});
}
return result;
}
private List<OrganBonusDetail> GetBonusesForAbility(EntityUid uid, BestiaContainerComponent bestia, EntProtoId actionId)
{
var heart = GetOrganTypeCount((uid, bestia), BestiaOrganType.Heart);
var lungs = GetOrganTypeCount((uid, bestia), BestiaOrganType.Lungs);
var liver = GetOrganTypeCount((uid, bestia), BestiaOrganType.Liver);
var kidneys = GetOrganTypeCount((uid, bestia), BestiaOrganType.Kidneys);
var eyes = GetOrganTypeCount((uid, bestia), BestiaOrganType.Eyes);
var stomach = GetOrganTypeCount((uid, bestia), BestiaOrganType.Stomach);
var maxCritical = bestia.MaxCriticalOrgans;
var maxRegular = bestia.MaxRegularOrgans;
var bonuses = new List<OrganBonusDetail>();
switch (actionId)
{
case "ActionVampireInfectedTrophy":
{
var heartBonus = heart * 0.5f;
var liverBonus = Math.Min(40, liver * 3);
var eyesBonus = Math.Min(0.525f, eyes * 0.035f);
var stomachBonus = Math.Min(5f, stomach);
AddBonus(bonuses, BestiaOrganType.Heart,
Loc.GetString("vampire-bestia-bonus-infected-trophy-heart", ("value", heartBonus)), heart, maxCritical);
AddBonus(bonuses, BestiaOrganType.Liver,
Loc.GetString("vampire-bestia-bonus-infected-trophy-liver", ("value", liverBonus)), liver, maxRegular);
AddBonus(bonuses, BestiaOrganType.Eyes,
Loc.GetString("vampire-bestia-bonus-infected-trophy-eyes", ("value", eyesBonus.ToString("F2"))), eyes, maxRegular);
AddBonus(bonuses, BestiaOrganType.Stomach,
Loc.GetString("vampire-bestia-bonus-infected-trophy-stomach", ("value", stomachBonus)), stomach, maxRegular);
break;
}
case "ActionVampireLunge":
{
var heartBonus = heart * 0.5f;
var lungsBonus = Math.Min(11f, 5f + lungs);
var kidneysBonus = Math.Min(50, 5 + kidneys * 5);
var stomachBonus = Math.Min(1.5f, 0.5f + stomach * 0.5f);
AddBonus(bonuses, BestiaOrganType.Heart,
Loc.GetString("vampire-bestia-bonus-lunge-heart", ("value", heartBonus)), heart, maxCritical);
AddBonus(bonuses, BestiaOrganType.Lungs,
Loc.GetString("vampire-bestia-bonus-lunge-lungs", ("value", lungsBonus)), lungs, maxCritical);
AddBonus(bonuses, BestiaOrganType.Kidneys,
Loc.GetString("vampire-bestia-bonus-lunge-kidneys", ("value", kidneysBonus)), kidneys, maxRegular);
AddBonus(bonuses, BestiaOrganType.Stomach,
Loc.GetString("vampire-bestia-bonus-lunge-stomach", ("value", stomachBonus)), stomach, maxRegular);
break;
}
case "ActionVampireMarkPrey":
{
var heartDamage = Math.Min(6, heart);
var heartChance = Math.Min(60, heart * 10);
var eyesBonus = Math.Min(8f, 3f + eyes * 0.5f);
var kidneysBonus = Math.Min(15, 5 + kidneys);
AddBonus(bonuses, BestiaOrganType.Heart,
Loc.GetString("vampire-bestia-bonus-mark-prey-heart", ("damage", heartDamage), ("chance", heartChance)), heart, maxCritical);
AddBonus(bonuses, BestiaOrganType.Eyes,
Loc.GetString("vampire-bestia-bonus-mark-prey-eyes", ("value", eyesBonus)), eyes, maxRegular);
AddBonus(bonuses, BestiaOrganType.Kidneys,
Loc.GetString("vampire-bestia-bonus-mark-prey-kidneys", ("value", kidneysBonus)), kidneys, maxRegular);
break;
}
case "ActionVampireMetamorphosisBats":
{
var heartHealth = Math.Min(250f, 130f + heart * 20f);
var heartDamage = Math.Min(8f, heart * 0.75f);
var lungsBonus = Math.Min(30, lungs * 5);
var liverBonus = Math.Min(3f, liver * 0.5f);
var kidneysBonus = Math.Min(10, 1 + kidneys);
AddBonus(bonuses, BestiaOrganType.Heart,
Loc.GetString("vampire-bestia-bonus-metamorphosis-bats-heart", ("health", heartHealth), ("damage", heartDamage)), heart, maxCritical);
AddBonus(bonuses, BestiaOrganType.Lungs,
Loc.GetString("vampire-bestia-bonus-metamorphosis-bats-lungs", ("value", lungsBonus)), lungs, maxCritical);
AddBonus(bonuses, BestiaOrganType.Liver,
Loc.GetString("vampire-bestia-bonus-metamorphosis-bats-liver", ("value", liverBonus)), liver, maxRegular);
AddBonus(bonuses, BestiaOrganType.Kidneys,
Loc.GetString("vampire-bestia-bonus-metamorphosis-bats-kidneys", ("value", kidneysBonus)), kidneys, maxRegular);
break;
}
case "ActionVampireSummonBats":
{
var heartHealth = Math.Min(140f, 80f + heart * 10f);
var heartDamage = Math.Min(6f, heart * 0.75f);
var lungsBonus = Math.Min(60, lungs * 10);
var liverBonus = Math.Min(10f, liver * 0.5f);
var kidneysBonus = kidneys;
AddBonus(bonuses, BestiaOrganType.Heart,
Loc.GetString("vampire-bestia-bonus-summon-bats-heart", ("health", heartHealth), ("damage", heartDamage)), heart, maxCritical);
AddBonus(bonuses, BestiaOrganType.Lungs,
Loc.GetString("vampire-bestia-bonus-summon-bats-lungs", ("value", lungsBonus)), lungs, maxCritical);
AddBonus(bonuses, BestiaOrganType.Liver,
Loc.GetString("vampire-bestia-bonus-summon-bats-liver", ("value", liverBonus)), liver, maxRegular);
AddBonus(bonuses, BestiaOrganType.Kidneys,
Loc.GetString("vampire-bestia-bonus-summon-bats-kidneys", ("value", kidneysBonus)), kidneys, maxRegular);
break;
}
case "ActionVampireMetamorphosisHound":
{
var heartHealth = Math.Min(320f, 140f + heart * 30f);
var heartDamage = Math.Min(6f, heart);
var lungsBonus = Math.Min(30, lungs * 5);
var kidneysBonus = kidneys;
var liverBonus = Math.Min(10f, liver * 0.5f);
AddBonus(bonuses, BestiaOrganType.Heart,
Loc.GetString("vampire-bestia-bonus-metamorphosis-hound-heart", ("health", heartHealth), ("damage", heartDamage)), heart, maxCritical);
AddBonus(bonuses, BestiaOrganType.Lungs,
Loc.GetString("vampire-bestia-bonus-metamorphosis-hound-lungs", ("value", lungsBonus)), lungs, maxCritical);
AddBonus(bonuses, BestiaOrganType.Kidneys,
Loc.GetString("vampire-bestia-bonus-metamorphosis-hound-kidneys", ("value", kidneysBonus)), kidneys, maxRegular);
AddBonus(bonuses, BestiaOrganType.Liver,
Loc.GetString("vampire-bestia-bonus-metamorphosis-hound-liver", ("value", liverBonus)), liver, maxRegular);
break;
}
}
return bonuses;
}
private void AddBonus(List<OrganBonusDetail> list, BestiaOrganType organType, string description, int count, int max)
{
var (_, isMaxed) = GetOrganProgressColor(count, max);
list.Add(new OrganBonusDetail
{
OrganType = Loc.GetString($"vampire-bestia-{organType.ToString().ToLower()}"),
Description = description,
IsMaxed = isMaxed
});
}
private NetEntity? GetPreviewForOrganType(Container container, BestiaOrganType type)
{
var organsOfType = new List<EntityUid>();
foreach (var organ in container.ContainedEntities)
{
if (GetOrganTypeEnum(organ) == type)
organsOfType.Add(organ);
}
return organsOfType.Count > 0 ? GetNetEntity(_random.Pick(organsOfType)) : null;
}
private Dictionary<BestiaOrganType, int> GetOrganCounts(Container container)
{
var counts = new Dictionary<BestiaOrganType, int>();
foreach (var organ in container.ContainedEntities)
{
var type = GetOrganTypeEnum(organ);
if (type != BestiaOrganType.Unknown)
counts[type] = counts.GetValueOrDefault(type) + 1;
}
return counts;
}
private bool IsCriticalOrganType(BestiaOrganType type)
{
return type == BestiaOrganType.Heart || type == BestiaOrganType.Lungs;
}
private (Color color, bool isMaxed) GetOrganProgressColor(int current, int max)
{
if (current <= 0) return (Color.Orange, false);
if (current >= max) return (Color.YellowGreen, true);
var t = (float)current / max;
var color = Color.InterpolateBetween(Color.Orange, Color.YellowGreen, t);
return (color, false);
}
#endregion
#region Organs Manipulation
private List<NetEntity> GetAvailableOrgans(EntityUid vampire, EntityUid target, BestiaContainerComponent? bestia = null)
{
var organs = new List<NetEntity>();
if (!Resolve(vampire, ref bestia, false))
return organs;
if (!TryComp<BodyComponent>(target, out var body) || body.Organs == null)
return organs;
foreach (var organ in body.Organs.ContainedEntities)
{
if (!(TryComp<OrganComponent>(organ, out var organComp) && organComp.Category == Eyes)
&& !HasComp<MetabolizerComponent>(organ))
continue;
var organType = GetOrganTypeEnum(organ);
if (organType == BestiaOrganType.Unknown)
continue;
var currentCount = GetOrganTypeCount((vampire, bestia), organType);
var maxCount = IsCriticalOrgan(organ) ? bestia.MaxCriticalOrgans : bestia.MaxRegularOrgans;
if (currentCount >= maxCount)
continue;
organs.Add(GetNetEntity(organ));
}
return organs;
}
private int GetOrganTypeCount(Entity<BestiaContainerComponent> entity, BestiaOrganType organType)
{
var counts = GetOrganCounts(entity.Comp.OrgansContainer);
return counts.GetValueOrDefault(organType, 0);
}
private int GetOrganTypeCount(EntityUid uid, BestiaOrganType organType, BestiaContainerComponent? bestia = null)
{
if (!Resolve(uid, ref bestia, false))
return 0;
var counts = GetOrganCounts(bestia.OrgansContainer);
return counts.GetValueOrDefault(organType, 0);
}
private BestiaOrganType GetOrganTypeEnum(EntityUid organ, OrganComponent? organComp = null)
{
if (Resolve(organ, ref organComp, false) && organComp.Category != null)
{
var id = organComp.Category.Value.Id;
return id switch
{
"Heart" => BestiaOrganType.Heart,
"Lungs" => BestiaOrganType.Lungs,
"Liver" => BestiaOrganType.Liver,
"Kidneys" => BestiaOrganType.Kidneys,
"Eyes" => BestiaOrganType.Eyes,
"Stomach" => BestiaOrganType.Stomach,
_ => BestiaOrganType.Unknown
};
}
return BestiaOrganType.Unknown;
}
private bool IsCriticalOrgan(EntityUid organ, MetabolizerComponent? metabolizer = null)
{
if (!Resolve(organ, ref metabolizer, false) || metabolizer.Stages == null)
return false;
var stages = metabolizer.Stages;
foreach (var stage in CriticalStages)
{
if (stages.Contains(stage.Id))
return true;
}
return false;
}
#endregion
private void UpdateBestiaLimits(Entity<VampireComponent?, BestiaContainerComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp1, false) || !Resolve(ent, ref ent.Comp2, false))
return;
var blood = ent.Comp1.CurrentBlood.Float();
var bestia = ent.Comp2;
var sortedCritical = bestia.CriticalOrganThresholds.Keys.OrderBy(x => x).ToList();
foreach (var threshold in sortedCritical)
{
if (blood >= threshold && !bestia.UnlockedCriticalThresholds.Contains(threshold))
{
bestia.UnlockedCriticalThresholds.Add(threshold);
bestia.MaxCriticalOrgans += bestia.CriticalOrganThresholds[threshold];
}
}
var sortedVictim = bestia.OrgansPerVictimThresholds.Keys.OrderBy(x => x).ToList();
foreach (var threshold in sortedVictim)
{
if (blood >= threshold && !bestia.UnlockedVictimThresholds.Contains(threshold))
{
bestia.UnlockedVictimThresholds.Add(threshold);
bestia.MaxOrgansPerVictim += bestia.OrgansPerVictimThresholds[threshold];
}
}
}
private void UpdateProtections(Entity<BestiaContainerComponent> ent)
{
var eyesCount = GetOrganTypeCount(ent, BestiaOrganType.Eyes);
if (eyesCount >= 2 && !HasComp<EyeProtectionComponent>(ent))
EnsureComp<EyeProtectionComponent>(ent);
if (eyesCount >= 6 && !HasComp<FlashImmunityComponent>(ent))
EnsureComp<FlashImmunityComponent>(ent);
}
}
@@ -0,0 +1,125 @@
using System.Linq;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Systems;
using Content.Shared.IdentityManagement;
using Content.Shared.Mindshield.Components;
using Content.Shared.NullRod.Components;
using Content.Shared.Popups;
using Content.Shared.Vampire.Components;
namespace Content.Server.Vampire;
public sealed partial class VampireSystem
{
private void InitializeThralls()
{
SubscribeLocalEvent<ThrallOwnerComponent, DamageChangedEvent>(OnVampireDamageChanged);
SubscribeLocalEvent<ThrallComponent, DamageChangedEvent>(OnThrallDamageChanged);
SubscribeLocalEvent<MindShieldComponent, ComponentStartup>(MindShieldImplanted); // TODO: Replace this with a specific event
}
#region Damage Sharing Logic
private void OnVampireDamageChanged(EntityUid uid, ThrallOwnerComponent component, ref DamageChangedEvent args)
{
if (args.DamageDelta is null || !args.DamageIncreased)
return;
if (!TryComp<ThrallOwnerComponent>(uid, out var thrallOwner) || !thrallOwner.DamageSharing)
return;
var aliveThralls = GetAliveThralls(thrallOwner);
if (aliveThralls.Count == 0)
return;
_damage.TryChangeDamage(uid, -args.DamageDelta, true, false);
DistributeDamage(uid, args.DamageDelta, aliveThralls);
}
private void OnThrallDamageChanged(EntityUid uid, ThrallComponent component, ref DamageChangedEvent args)
{
if (args.DamageDelta is null || !args.DamageIncreased)
return;
if (component.VampireOwner is not { } vampireOwner)
return;
if (!TryComp<ThrallOwnerComponent>(vampireOwner, out var thrallOwner) || !thrallOwner.DamageSharing)
return;
var aliveThralls = GetAliveThralls(thrallOwner);
if (aliveThralls.Count == 0)
return;
_damage.TryChangeDamage(uid, -args.DamageDelta, true, false);
DistributeDamage(vampireOwner, args.DamageDelta, aliveThralls);
}
private void DistributeDamage(EntityUid vampireUid, DamageSpecifier originalDamage, List<EntityUid> aliveThralls)
{
var participants = new List<EntityUid> { vampireUid };
participants.AddRange(aliveThralls);
if (participants.Count == 0)
return;
var sharedDamage = GetSharedDamage(originalDamage, participants.Count);
foreach (var participant in participants)
{
if (!HasComp<DamageableComponent>(participant))
continue;
_damage.TryChangeDamage(participant, sharedDamage, true, false);
}
}
#endregion
private void MindShieldImplanted(EntityUid uid, MindShieldComponent comp, ComponentStartup init)
{
if (!TryComp<ThrallComponent>(uid, out var thrall) || thrall.VampireOwner is not { } owner)
return;
var stunTime = TimeSpan.FromSeconds(4);
var name = Identity.Entity(uid, EntityManager);
if (TryComp<ThrallOwnerComponent>(owner, out var thrallOwner))
{
TryRemoveThrall(thrallOwner, uid);
Dirty(owner, thrallOwner);
}
RemComp<ThrallComponent>(uid);
RemComp<UnholyComponent>(uid);
RemComp<NullDamageComponent>(uid);
_stun.TryUpdateParalyzeDuration(uid, stunTime);
_popup.PopupEntity(Loc.GetString("thrall-break-control", ("name", name)), uid);
}
private void UpdateThrallCount(Entity<VampireComponent?, ThrallOwnerComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp1, false) || !Resolve(ent, ref ent.Comp2, false))
return;
var blood = ent.Comp1.CurrentBlood.Float();
var sortedThresholds = ent.Comp2.ThrallCountThresholds.Keys.OrderBy(x => x).ToList();
foreach (var threshold in sortedThresholds)
{
if (blood >= threshold && !ent.Comp2.UnlockedThresholds.Contains(threshold))
ent.Comp2.UnlockedThresholds.Add(threshold);
}
var totalBonus = ent.Comp2.UnlockedThresholds.Sum(t => ent.Comp2.ThrallCountThresholds[t]);
var newMaxCount = 1 + totalBonus;
if (ent.Comp2.MaxThrallCount != newMaxCount)
{
ent.Comp2.MaxThrallCount = newMaxCount;
Dirty(ent.Owner, ent.Comp2);
_popup.PopupEntity(Loc.GetString("vampire-trall-count-update", ("count", newMaxCount)), ent, ent, PopupType.Medium);
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,64 @@
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Systems;
using Content.Shared.Mobs.Systems;
using Content.Shared.Weapons.Melee.Events;
using Content.Shared.Whitelist;
namespace Content.Server.Weapons.Marker;
public sealed class LeechMeleeWeaponSystem : EntitySystem
{
[Dependency] private readonly EntityWhitelistSystem _entityWhitelist = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<LeechMeleeWeaponComponent, MeleeHitEvent>(OnMeleeHit);
}
private void OnMeleeHit(EntityUid uid, LeechMeleeWeaponComponent component, MeleeHitEvent args)
{
if (args.HitEntities.Count == 0)
return;
DamageSpecifier? heal = component.Heal?.Clone();
DamageSpecifier? groupsHeal = null;
if (component.HealGroups != null && !component.HealGroups.Empty)
{
if (component.Weighted)
{
groupsHeal = _damageable.CreateWeightedHealFromGroups(args.User, component.HealGroups);
}
else
{
groupsHeal = _damageable.CreateHealFromGroups(args.User, component.HealGroups);
}
}
if (heal == null && groupsHeal != null)
heal = groupsHeal;
else if (heal != null && groupsHeal != null)
heal += groupsHeal;
if (heal == null || heal.Empty)
return;
foreach (var hitEnt in args.HitEntities)
{
if (!_entityWhitelist.IsWhitelistPass(component.Whitelist, hitEnt))
continue;
if (_entityWhitelist.IsWhitelistPass(component.Blacklist, hitEnt))
continue;
if (_mobState.IsDead(hitEnt))
continue;
_damageable.TryChangeDamage(args.User, heal, true, false, origin: args.Weapon);
}
}
}
@@ -548,7 +548,9 @@ public abstract partial class SharedActionsSystem : EntitySystem
// Note that attached entity and attached container are allowed to be null here.
if (action.Comp.AttachedEntity != null && action.Comp.AttachedEntity != performer)
{
#if DEBUG // Corvax-Wega-Add | Disable Error for the non-debug version
Log.Error($"{ToPrettyString(performer)} is attempting to perform an action {ToPrettyString(action)} that is attached to another entity {ToPrettyString(action.Comp.AttachedEntity)}");
#endif // Corvax-Wega-Add
return;
}
@@ -11,7 +11,7 @@ namespace Content.Shared.Damage.Prototypes
/// to change/get/set damage in a <see cref="DamageableComponent"/>.
/// </remarks>
[Prototype(2)]
[Obsolete("Do not rely on DamageGroupPrototype for anything besides grouping logically similar damage in UIs")]
// [Obsolete("Do not rely on DamageGroupPrototype for anything besides grouping logically similar damage in UIs")] Fuck yourself
public sealed partial class DamageGroupPrototype : IPrototype
{
[IdDataField] public string ID { get; private set; } = default!;
@@ -460,7 +460,7 @@ public abstract partial class SharedStaminaSystem : EntitySystem
// Corvax-Wega-Add-start
public void RemoveStaminaDamage(Entity<StaminaComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp))
if (!Resolve(ent, ref ent.Comp, false))
return;
if (ent.Comp.StaminaDamage >= ent.Comp.CritThreshold)
+13 -13
View File
@@ -294,22 +294,22 @@ public abstract class SharedFlashSystem : EntitySystem
}
// Corvax-Wega-Wielder-end
// Corvax-Wega-Arsenal-start
private void OnFlashPulse(Entity<ImpulseFlashComponent> ent,ref MaskFlashActionEvent args)
{
FlashArea(ent,ent, ent.Comp.Range, ent.Comp.Duration, ent.Comp.SlowTo, probability: ent.Comp.Probability);
args.Handled = true;
}
// Corvax-Wega-Arsenal-start
private void OnFlashPulse(Entity<ImpulseFlashComponent> ent,ref MaskFlashActionEvent args)
{
FlashArea(ent,ent, ent.Comp.Range, ent.Comp.Duration, ent.Comp.SlowTo, probability: ent.Comp.Probability);
args.Handled = true;
}
private void OnGetItemActions(Entity<ImpulseFlashComponent> ent, ref GetItemActionsEvent args)
private void OnGetItemActions(Entity<ImpulseFlashComponent> ent, ref GetItemActionsEvent args)
{
if (_inventorySystem.InSlotWithFlags(ent.Owner, SlotFlags.HEAD) || _inventorySystem.InSlotWithFlags(ent.Owner, SlotFlags.NECK))
{
var comp = ent.Comp;
args.AddAction(ref comp.FlashActionEntity, comp.FlashAction);
}
}
// Corvax-Wega-Arsenal-end
{
var comp = ent.Comp;
args.AddAction(ref comp.FlashActionEntity, comp.FlashAction);
}
}
// Corvax-Wega-Arsenal-end
private void OnTemporaryBlindnessFlashAttempt(Entity<TemporaryBlindnessComponent> ent, ref FlashAttemptEvent args)
{
@@ -46,6 +46,7 @@ public abstract partial class SharedStunSystem : EntitySystem
SubscribeLocalEvent<StunnedComponent, ComponentShutdown>(OnStunShutdown);
SubscribeLocalEvent<StunOnContactComponent, StartCollideEvent>(OnStunOnContactCollide);
SubscribeLocalEvent<SlowdownOnContactComponent, StartCollideEvent>(OnSlowdownOnContactCollide); // Corvax-Wega-Add
// Attempt event subscriptions.
SubscribeLocalEvent<StunnedComponent, ChangeDirectionAttemptEvent>(OnAttempt);
@@ -8,15 +8,18 @@ namespace Content.Shared.Damage.Components;
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class DamageInContainerComponent : Component
{
[ViewVariables(VVAccess.ReadOnly)]
public TimeSpan NextTickTime;
[DataField("interval")]
public float Interval = 1f;
[DataField("nextTickTime")]
public TimeSpan NextTickTime;
[DataField(required: true), AutoNetworkedField]
[DataField, AutoNetworkedField]
public DamageSpecifier Damage = default!;
[DataField, AutoNetworkedField]
public GroupHealSpecifier DamageGroups = default!;
[DataField]
public EntityWhitelist? Whitelist;
@@ -0,0 +1,23 @@
using Content.Shared.Whitelist;
using Robust.Shared.GameStates;
namespace Content.Shared.Damage.Components;
[RegisterComponent, NetworkedComponent]
public sealed partial class LeechMeleeWeaponComponent : Component
{
[DataField]
public DamageSpecifier? Heal = default!;
[DataField]
public GroupHealSpecifier? HealGroups = default!;
[DataField]
public bool Weighted = false;
[DataField]
public EntityWhitelist? Whitelist;
[DataField]
public EntityWhitelist? Blacklist;
}
@@ -0,0 +1,121 @@
using System.Linq;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.Damage;
/// <summary>
/// Represents a collection of damage groups and healing values.
/// Designed specifically for healing operations where the healing amount
/// should be distributed only among existing damage types of each group.
/// </summary>
[DataDefinition, Serializable, NetSerializable]
public sealed partial class GroupHealSpecifier : IEquatable<GroupHealSpecifier>
{
/// <summary>
/// Dictionary mapping damage group prototypes to total healing amounts for that group.
/// Healing amounts should be negative values (e.g., -8 for 8 points of healing).
/// </summary>
[DataField("groups")]
public Dictionary<ProtoId<DamageGroupPrototype>, FixedPoint2> GroupHealDict { get; set; } = new();
/// <summary>
/// Returns true if the specifier contains no healing entries.
/// </summary>
public bool Empty => GroupHealDict.Count == 0;
/// <summary>
/// Returns the total healing amount across all groups.
/// </summary>
public FixedPoint2 GetTotalHealing()
{
var total = FixedPoint2.Zero;
foreach (var value in GroupHealDict.Values)
{
total += value;
}
return total;
}
public GroupHealSpecifier()
{
}
public GroupHealSpecifier(GroupHealSpecifier other)
{
GroupHealDict = new Dictionary<ProtoId<DamageGroupPrototype>, FixedPoint2>(other.GroupHealDict);
}
public GroupHealSpecifier(ProtoId<DamageGroupPrototype> group, FixedPoint2 healingAmount)
{
GroupHealDict = new() { { group, healingAmount } };
}
public GroupHealSpecifier Clone()
{
return new GroupHealSpecifier(this);
}
public override string ToString()
{
return "GroupHealSpecifier(" + string.Join("; ", GroupHealDict.Select(x => x.Key + ":" + x.Value)) + ")";
}
#region Operators
public static GroupHealSpecifier operator *(GroupHealSpecifier spec, FixedPoint2 factor)
{
var result = new GroupHealSpecifier();
foreach (var (group, amount) in spec.GroupHealDict)
{
result.GroupHealDict.Add(group, amount * factor);
}
return result;
}
public static GroupHealSpecifier operator *(GroupHealSpecifier spec, float factor)
{
var result = new GroupHealSpecifier();
foreach (var (group, amount) in spec.GroupHealDict)
{
result.GroupHealDict.Add(group, amount * factor);
}
return result;
}
public static GroupHealSpecifier operator *(FixedPoint2 factor, GroupHealSpecifier spec) => spec * factor;
public static GroupHealSpecifier operator *(float factor, GroupHealSpecifier spec) => spec * factor;
public static GroupHealSpecifier operator +(GroupHealSpecifier a, GroupHealSpecifier b)
{
var result = new GroupHealSpecifier(a);
foreach (var (group, amount) in b.GroupHealDict)
{
if (!result.GroupHealDict.TryAdd(group, amount))
{
result.GroupHealDict[group] += amount;
}
}
return result;
}
#endregion
public bool Equals(GroupHealSpecifier? other)
{
if (other == null || GroupHealDict.Count != other.GroupHealDict.Count)
return false;
foreach (var (key, value) in GroupHealDict)
{
if (!other.GroupHealDict.TryGetValue(key, out var otherValue) || value != otherValue)
return false;
}
return true;
}
public FixedPoint2 this[string key] => GroupHealDict[key];
}
@@ -0,0 +1,131 @@
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
namespace Content.Shared.Damage.Systems;
public sealed partial class DamageableSystem
{
/// <summary>
/// Creates a <see cref="DamageSpecifier"/> from a <see cref="GroupHealSpecifier"/>, distributing healing
/// evenly only among the damage types from each group that are actually present on the target.
/// </summary>
/// <param name="target">The target entity whose current damage is being examined</param>
/// <param name="groupHealSpec">The group healing specification</param>
/// <param name="damageableQuery">Optional query for accessing <see cref="DamageableComponent"/></param>
/// <returns><see cref="DamageSpecifier"/> with healing distributed only to existing damage types</returns>
public DamageSpecifier CreateHealFromGroups(
EntityUid target,
GroupHealSpecifier groupHealSpec,
EntityQuery<DamageableComponent>? damageableQuery = null)
{
var result = new DamageSpecifier();
if (groupHealSpec.Empty)
return result;
var query = damageableQuery ?? _damageableQuery;
if (!query.TryGetComponent(target, out var damageable))
return result;
foreach (var (groupId, healAmount) in groupHealSpec.GroupHealDict)
{
if (healAmount >= FixedPoint2.Zero)
continue;
if (!_prototypeManager.TryIndex(groupId, out var groupProto))
continue;
var existingTypes = new List<ProtoId<DamageTypePrototype>>();
foreach (var damageType in groupProto.DamageTypes)
{
if (damageable.Damage.DamageDict.TryGetValue(damageType, out var value) && value > 0)
existingTypes.Add(damageType);
}
if (existingTypes.Count == 0)
continue;
var remainingHealing = -healAmount;
var remainingTypes = existingTypes.Count;
foreach (var damageType in existingTypes)
{
var healForType = remainingHealing / remainingTypes;
if (result.DamageDict.TryGetValue(damageType, out var currentValue))
result.DamageDict[damageType] = currentValue - healForType;
else
result.DamageDict.Add(damageType, -healForType);
remainingHealing -= healForType;
remainingTypes--;
}
}
return result;
}
/// <summary>
/// Creates a <see cref="DamageSpecifier"/> from a <see cref="GroupHealSpecifier"/>, distributing healing
/// by weight (proportional to how much damage of each type is present) only among the damage
/// types from each group that are actually present on the target.
/// </summary>
/// <param name="target">The target entity whose current damage is being examined</param>
/// <param name="groupHealSpec">The group healing specification</param>
/// <param name="damageableQuery">Optional query for accessing <see cref="DamageableComponent"/></param>
/// <returns><see cref="DamageSpecifier"/> with healing distributed by weight to existing damage types</returns>
public DamageSpecifier CreateWeightedHealFromGroups(
EntityUid target,
GroupHealSpecifier groupHealSpec,
EntityQuery<DamageableComponent>? damageableQuery = null)
{
var result = new DamageSpecifier();
if (groupHealSpec.Empty)
return result;
var query = damageableQuery ?? _damageableQuery;
if (!query.TryGetComponent(target, out var damageable))
return result;
foreach (var (groupId, healAmount) in groupHealSpec.GroupHealDict)
{
if (healAmount >= FixedPoint2.Zero)
continue;
if (!_prototypeManager.TryIndex(groupId, out var groupProto))
continue;
var existingDamage = new List<(ProtoId<DamageTypePrototype> Type, FixedPoint2 Amount)>();
FixedPoint2 totalDamage = FixedPoint2.Zero;
foreach (var damageType in groupProto.DamageTypes)
{
if (damageable.Damage.DamageDict.TryGetValue(damageType, out var value) && value > 0)
{
existingDamage.Add((damageType, value));
totalDamage += value;
}
}
if (existingDamage.Count == 0 || totalDamage == 0)
continue;
var totalHealing = -healAmount;
foreach (var (damageType, currentDamage) in existingDamage)
{
var weight = currentDamage / totalDamage;
var healForType = totalHealing * weight;
if (healForType > 0)
{
if (result.DamageDict.TryGetValue(damageType, out var currentValue))
result.DamageDict[damageType] = currentValue - healForType;
else
result.DamageDict.Add(damageType, -healForType);
}
}
}
return result;
}
}
@@ -1,6 +0,0 @@
namespace Content.Shared.Damage.Systems;
public abstract class SharedDamageInContainerSystem : EntitySystem
{
}
@@ -0,0 +1,26 @@
using Content.Shared.FixedPoint;
using Robust.Shared.GameStates;
namespace Content.Shared.NullRod.Components;
/// <summary>
/// Indicates the presence and amount of zero damage in an unholy entity.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
public sealed partial class NullDamageComponent : Component
{
[ViewVariables(VVAccess.ReadOnly)]
public TimeSpan NextNullDamageTick { get; set; }
[ViewVariables, AutoNetworkedField]
public FixedPoint2 NullDamage = 0;
[DataField]
public FixedPoint2 MaxNullDamage = 120;
[DataField]
public FixedPoint2 NullDamageRecoveryPerTick = 2;
[DataField]
public float NullDamageRecoveryInterval = 2f;
}
@@ -0,0 +1,7 @@
namespace Content.Shared.NullRod.Components;
/// <summary>
/// It notes whether the entity is unholy
/// </summary>
[RegisterComponent]
public sealed partial class UnholyComponent : Component;
@@ -0,0 +1,12 @@
using Content.Shared.Disease;
using Robust.Shared.Prototypes;
namespace Content.Shared.Projectiles;
[RegisterComponent]
public sealed partial class ProjectileInfectComponent : Component
{
[DataField(required: true)]
public ProtoId<DiseasePrototype> Infection;
[DataField] public float Prob = 0.1f;
}
@@ -0,0 +1,20 @@
using Content.Shared.Movement.Systems;
using Robust.Shared.Physics.Events;
namespace Content.Shared.Stunnable;
public abstract partial class SharedStunSystem
{
[Dependency] private readonly MovementModStatusSystem _movementMod = default!;
private void OnSlowdownOnContactCollide(Entity<SlowdownOnContactComponent> ent, ref StartCollideEvent args)
{
if (args.OurFixtureId != ent.Comp.FixtureId)
return;
if (_entityWhitelist.IsWhitelistPass(ent.Comp.Blacklist, args.OtherEntity))
return;
_movementMod.TryUpdateMovementSpeedModDuration(args.OtherEntity, MovementModStatusSystem.Slowdown, ent.Comp.Duration, ent.Comp.Multiplier);
}
}
@@ -0,0 +1,20 @@
using Content.Shared.Whitelist;
using Robust.Shared.GameStates;
namespace Content.Shared.Stunnable;
[RegisterComponent, NetworkedComponent, Access(typeof(SharedStunSystem))]
public sealed partial class SlowdownOnContactComponent : Component
{
[DataField]
public string FixtureId = "fix";
[DataField]
public TimeSpan Duration = TimeSpan.FromSeconds(5);
[DataField]
public float Multiplier = 1f;
[DataField]
public EntityWhitelist Blacklist = new();
}
@@ -0,0 +1,12 @@
namespace Content.Shared.Vampire.Components;
/// <summary>
/// Just a chill guy.
/// Defines an entity as a beacon of the soul.
/// </summary>
[RegisterComponent]
public sealed partial class BeaconSoulComponent : Component
{
[DataField]
public EntityUid VampireOwner = default!;
}
@@ -0,0 +1,47 @@
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
namespace Content.Shared.Vampire.Components;
[Access(typeof(SharedVampireSystem))]
[RegisterComponent, NetworkedComponent]
public sealed partial class BestiaContainerComponent : Component
{
public const string ContainerId = "organs_container";
[ViewVariables]
public Container OrgansContainer = default!;
// Mew Mew Mew Mew Mew Mew Mew Mew~!
[ViewVariables(VVAccess.ReadOnly)]
public Dictionary<EntityUid, int> OrgansExtractedFromVictim = new();
[ViewVariables(VVAccess.ReadOnly)]
public int MaxRegularOrgans = 10;
[ViewVariables(VVAccess.ReadOnly)]
public int MaxCriticalOrgans = 2;
[ViewVariables(VVAccess.ReadOnly)]
public int MaxOrgansPerVictim = 1;
[ViewVariables(VVAccess.ReadOnly)]
public List<float> UnlockedCriticalThresholds = new();
[ViewVariables(VVAccess.ReadOnly)]
public List<float> UnlockedVictimThresholds = new();
[DataField("criticalThresholds")]
public Dictionary<float, int> CriticalOrganThresholds { get; set; } = new()
{
{ 600f, 2 },
{ 1000f, 2 }
};
[DataField("organsThresholds")]
public Dictionary<float, int> OrgansPerVictimThresholds { get; set; } = new()
{
{ 600f, 1 },
{ 1000f, 1 }
};
}
@@ -0,0 +1,14 @@
namespace Content.Shared.Vampire.Components;
/// <summary>
/// A mark that allows you to see the presence of fang marks on the victim.
/// </summary>
[RegisterComponent]
public sealed partial class BittenByVampireComponent : Component
{
[ViewVariables(VVAccess.ReadOnly)]
public TimeSpan ExpirationTime { get; set; }
[DataField]
public float LifetimeSeconds = 900f;
}
@@ -0,0 +1,13 @@
namespace Content.Shared.Vampire.Components;
/// <summary>
/// A component for testing vampire arson near holy sites.
/// </summary>
[RegisterComponent]
public sealed partial class HolyPointComponent : Component
{
[DataField]
public float Range = 6f;
public float NextTimeTick { get; set; }
}
@@ -0,0 +1,14 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Vampire.Components;
/// <summary>
/// The target is a supreme vampire.
/// </summary>
[Access(typeof(SharedVampireSystem))]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
public sealed partial class SupremeVampireComponent : Component
{
[ViewVariables(VVAccess.ReadOnly), AutoNetworkedField]
public bool Active = false;
}
@@ -0,0 +1,19 @@
using Content.Shared.StatusIcon;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.Vampire.Components;
/// <summary>
/// Determines what the entity is and who it belongs to.
/// </summary>
[Access(typeof(SharedVampireSystem))]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
public sealed partial class ThrallComponent : Component
{
[ViewVariables(VVAccess.ReadOnly), AutoNetworkedField]
public EntityUid? VampireOwner = default!;
[DataField]
public ProtoId<FactionIconPrototype> StatusIcon = "ThrallFaction";
}
@@ -0,0 +1,31 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Vampire.Components;
/// <summary>
/// Determines whether an entity is the owner of the tralls and allows them to be manipulated.
/// </summary>
[Access(typeof(SharedVampireSystem))]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
public sealed partial class ThrallOwnerComponent : Component
{
[DataField, ViewVariables(VVAccess.ReadOnly), AutoNetworkedField]
public int MaxThrallCount = 1;
[ViewVariables(VVAccess.ReadOnly), AutoNetworkedField]
public List<EntityUid> ThrallOwned = new();
[ViewVariables(VVAccess.ReadOnly)]
public bool DamageSharing = false;
[ViewVariables(VVAccess.ReadOnly)]
public List<float> UnlockedThresholds = new();
[DataField, ViewVariables(VVAccess.ReadOnly)]
public Dictionary<float, int> ThrallCountThresholds { get; set; } = new()
{
{ 400f, 1 },
{ 600f, 1 },
{ 1000f, 1 }
};
}
@@ -0,0 +1,13 @@
using Content.Shared.FixedPoint;
namespace Content.Shared.Vampire.Components;
[RegisterComponent]
public sealed partial class VampireBloodAbsorptionComponent : Component
{
[DataField]
public FixedPoint2 BloodStealAmount = 0;
[ViewVariables(VVAccess.ReadOnly)]
public EntityUid VampireOwner = default!;
}
@@ -0,0 +1,17 @@
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
namespace Content.Shared.Vampire.Components;
[RegisterComponent]
public sealed partial class VampireClawsComponent : Component
{
[DataField]
public FixedPoint2 BloodStealAmount = 5;
[DataField]
public GroupHealSpecifier HealGroups = default!;
[DataField]
public float StaminaMod = -10f;
}
@@ -0,0 +1,4 @@
namespace Content.Shared.Vampire.Components;
[RegisterComponent]
public sealed partial class VampireCoffinComponent : Component;
@@ -0,0 +1,123 @@
using Content.Shared.Alert;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
using Content.Shared.Metabolism;
using Content.Shared.StatusIcon;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.Vampire.Components;
/// <summary>
/// The basic component that defines a vampire.
/// </summary>
[Access(typeof(SharedVampireSystem))]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
public sealed partial class VampireComponent : Component
{
public static readonly ProtoId<DamageModifierSetPrototype> VampireDamageModifier = "Vampire";
public static readonly ProtoId<MetabolizerTypePrototype> MetabolizerVampire = "Vampire";
public static readonly EntProtoId DrinkActionPrototype = "ActionDrinkBlood";
public static readonly EntProtoId SelectClassActionPrototype = "ActionVampireSelectClass";
public static readonly EntProtoId RejuvenateActionPrototype = "ActionVampireRejuvenate";
public static readonly EntProtoId GlareActionPrototype = "ActionVampireGlare";
public EntityUid? DrinkActionEntity;
public EntityUid? SelectClassActionEntity;
public EntityUid? RejuvenateActionEntity;
public EntityUid? GlareActionEntity;
[ViewVariables(VVAccess.ReadOnly)]
public float NextSpaceDamageTick { get; set; }
[DataField, ViewVariables(VVAccess.ReadOnly)]
public VampireClassEnum CurrentEvolution { get; set; } = VampireClassEnum.NonSelected;
[ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
public FixedPoint2 CurrentBlood = 0;
[ViewVariables(VVAccess.ReadOnly)]
public float TotalBloodDrank = 0;
[ViewVariables(VVAccess.ReadOnly), AutoNetworkedField]
public Dictionary<EntityUid, FixedPoint2> BloodConsumedFromVictim = new();
[ViewVariables(VVAccess.ReadOnly)]
public Dictionary<EntProtoId, EntityUid?> AcquiredSkills = new();
[DataField, ViewVariables(VVAccess.ReadOnly)]
public Dictionary<VampireClassEnum, Dictionary<float, List<EntProtoId>>> ClassThresholds { get; set; } = new()
{
[VampireClassEnum.Hemomancer] = new()
{
{ 150f, new() { "ActionVampireClaws" } },
{ 250f, new() { "ActionVampireBloodTendrils", "ActionVampireBloodBarrier" } },
{ 400f, new() { "ActionVampireSanguinePool" } },
{ 600f, new() { "ActionVampirePredatorSenses" } },
{ 800f, new() { "ActionVampireBloodEruption" } },
{ 1000f, new() { "ActionVampireBloodBringersRite" } }
},
[VampireClassEnum.Umbrae] = new()
{
{ 150f, new() { "ActionVampireCloakOfDarkness" } },
{ 250f, new() { "ActionVampireShadowSnare", "ActionVampireSoulAnchor" } },
{ 400f, new() { "ActionVampireDarkPassage" } },
{ 600f, new() { "ActionVampireExtinguish" } },
{ 800f, new() { "ActionVampireShadowBoxing" } },
{ 1000f, new() { "ActionVampireEternalDarkness" } }
},
[VampireClassEnum.Gargantua] = new()
{
{ 150f, new() { "ActionVampireBloodSwell" } },
{ 250f, new() { "ActionVampireBloodRush", "ActionVampireSeismicStomp" } },
{ 400f, new() { "ActionVampireBloodSwellAdvanced" } },
{ 600f, new() { "ActionVampireOverwhelmingForce" } },
{ 800f, new() { "ActionDemonicGrasp" } },
{ 1000f, new() { "ActionVampireCharge" } }
},
[VampireClassEnum.Dantalion] = new()
{
{ 150f, new() { "ActionEnthrall", "ActionCommune" } },
{ 250f, new() { "ActionPacify", "ActionSubspaceSwap" } },
{ 400f, new() { "ActionDeployDecoy" } },
{ 600f, new() { "ActionRallyThralls", "ActionVampirePacifyNearby" } },
{ 800f, new() { "ActionBloodBond", "ActionVampireThrallHeal" } },
{ 1000f, new() { "ActionMassHysteria" } }
},
[VampireClassEnum.Bestia] = new()
{
{ 150f, new() { "ActionVampireCheckTrophies", "ActionVampireDissect", "ActionVampireInfectedTrophy" } },
{ 250f, new() { "ActionVampireLunge", "ActionVampireMarkPrey" } },
{ 400f, new() { "ActionVampireMetamorphosisBats" } },
{ 600f, new() { "ActionVampireAnabiosis" } },
{ 800f, new() { "ActionVampireSummonBats" } },
{ 1000f, new() { "ActionVampireMetamorphosisHound" } }
}
};
[DataField]
public ProtoId<AlertPrototype> BloodAlert = "BloodAlert";
[DataField]
public ProtoId<FactionIconPrototype> StatusIcon { get; set; } = "VampireFaction";
[DataField]
public DamageSpecifier HolyDamage = new()
{
DamageDict = { { "Heat", 10 } }
};
[DataField]
public DamageSpecifier SpaceDamage = new()
{
DamageDict = { { "Heat", 2.5 } }
};
[DataField]
public SoundSpecifier BloodDrainSound = new SoundPathSpecifier("/Audio/Items/drink.ogg",
new AudioParams { Volume = -3f, MaxDistance = 3f }
);
}
@@ -0,0 +1,22 @@
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.Vampire.Components;
[Access(typeof(SharedVampireSystem))]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
public sealed partial class VampireDiablerieComponent : Component
{
public static readonly EntProtoId SacramentInitiationActionPrototype = "ActionVampireSacramentInitiation";
public EntityUid? SacramentInitiationActionEntity;
[ViewVariables(VVAccess.ReadOnly), AutoNetworkedField]
public int DiablerieLevel = 0;
[DataField]
public int MaxDiablerieLevel = 4;
[DataField]
public float SuckingBonusPerLevel = 5f;
}
@@ -0,0 +1,4 @@
namespace Content.Shared.Vampire.Components;
[RegisterComponent]
public sealed partial class VampireInferiorComponent : Component;
@@ -0,0 +1,28 @@
using Content.Shared.Damage.Prototypes;
using Content.Shared.Metabolism;
using Robust.Shared.Prototypes;
namespace Content.Shared.Vampire.Components;
/// <summary>
/// Stores the creature's original data before it becomes a vampire.
/// Used for restoration when the vampire component is removed.
/// </summary>
[RegisterComponent, Access(typeof(SharedVampireSystem))]
public sealed partial class VampireOriginalStateComponent : Component
{
[DataField]
public HashSet<Type> RemovedComponents = new();
[DataField]
public Dictionary<EntityUid, HashSet<ProtoId<MetabolizerTypePrototype>>> OriginalMetabolizerTypes = new();
[DataField]
public float? OriginalColdDamageThreshold;
[DataField]
public ProtoId<DamageModifierSetPrototype>? OriginalDamageModifierSetId;
[DataField]
public Color? OriginalEyeColor;
}
@@ -0,0 +1,8 @@
namespace Content.Shared.Vampire.Components;
[RegisterComponent, Access(typeof(SharedVampireSystem))]
public sealed partial class VampirePolymorphComponent : Component
{
[ViewVariables(VVAccess.ReadOnly)]
public EntityUid Body;
}
@@ -0,0 +1,458 @@
using Content.Shared.Actions;
using Content.Shared.Alert;
using Content.Shared.Cloning;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
using Content.Shared.Polymorph;
using Content.Shared.Projectiles;
using Content.Shared.Roles;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
namespace Content.Shared.Vampire;
/// <summary>
/// Interface for vampire action events that require blood costs.
/// Allows you to unify the verification and deduction of blood costs through the <see cref="BloodCost"/>.
/// </summary>
public interface IVampireActionEvent
{
FixedPoint2 BloodCost { get; }
}
// Base
public sealed partial class VampireDrinkingBloodActionEvent : EntityTargetActionEvent
{
[DataField]
public TimeSpan Delay = TimeSpan.FromSeconds(3);
}
public sealed partial class VampireSelectClassActionEvent : InstantActionEvent, IVampireActionEvent
{
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
public sealed partial class VampireRejuvenateActionEvent : InstantActionEvent, IVampireActionEvent
{
[DataField]
public bool Advanced = false;
[DataField]
public int Repeats = 5;
[DataField]
public DamageSpecifier Heal = default!;
[DataField]
public GroupHealSpecifier HealGroups = default!;
[DataField]
public TimeSpan TimeInterval = TimeSpan.FromSeconds(3.5);
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
public sealed partial class VampireGlareActionEvent : EntityTargetActionEvent { }
// Diablerie
public sealed partial class VampireSacramentInitiationActionEvent : EntityTargetActionEvent, IVampireActionEvent
{
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
// Hemomancer Abilities
public sealed partial class VampireClawsActionEvent : InstantActionEvent, IVampireActionEvent
{
[DataField]
public ProtoId<StartingGearPrototype> ProtoId = "VampireClawsGear";
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
public sealed partial class VampireBloodTentacleAction : WorldTargetActionEvent, IVampireActionEvent
{
[DataField]
public EntProtoId EntityId = "EffectBloodTentacleSpawn";
[DataField]
public List<Direction> OffsetDirections = new()
{
Direction.North,
Direction.South,
Direction.East,
Direction.West,
Direction.NorthEast,
Direction.NorthWest,
Direction.SouthEast,
Direction.SouthWest,
};
[DataField]
public int ExtraSpawns = 8;
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
public sealed partial class VampireBloodBarrierActionEvent : WorldTargetActionEvent, IVampireActionEvent
{
[DataField]
public EntProtoId EntityId = "BloodBarrier";
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
public sealed partial class VampireSanguinePoolActionEvent : InstantActionEvent, IVampireActionEvent
{
[DataField]
public ProtoId<PolymorphPrototype> PolymorphProto = "VampireBlood";
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
public sealed partial class VampirePredatorSensesActionEvent : InstantActionEvent
{
[DataField]
public EntProtoId EntityId = "PuddleBlood";
[DataField]
public SoundSpecifier Sound;
}
public sealed partial class VampireBloodEruptionActionEvent : InstantActionEvent, IVampireActionEvent
{
[DataField]
public DamageSpecifier Damage = default!;
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
public sealed partial class VampireBloodBringersRiteActionEvent : InstantActionEvent, IVampireActionEvent
{
[DataField]
public ProtoId<AlertPrototype> Alert = "AlertBloodRite";
[DataField]
public DamageSpecifier Heal = default!;
[DataField]
public GroupHealSpecifier HealGroups = default!;
[DataField]
public float StaminaMod = -15f;
[DataField]
public SoundSpecifier Sound;
[DataField]
public TimeSpan TimeInterval = TimeSpan.FromSeconds(1);
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
// Umbrae Abilities
public sealed partial class VampireCloakOfDarknessActionEvent : InstantActionEvent, IVampireActionEvent
{
[DataField]
public float SpeedMod = 1.3f;
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
public sealed partial class VampireShadowSnareActionEvent : WorldTargetActionEvent, IVampireActionEvent
{
[DataField]
public EntProtoId EntityId = "ShadowTrap";
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
public sealed partial class VampireSoulAnchorActionEvent : InstantActionEvent, IVampireActionEvent
{
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
public sealed partial class VampireDarkPassageActionEvent : WorldTargetActionEvent, IVampireActionEvent
{
[DataField]
public EntProtoId MistEffect = "VampireMistEffect";
[DataField]
public EntProtoId MistReappearEffect = "VampireMistReappearEffect";
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
public sealed partial class VampireExtinguishActionEvent : InstantActionEvent, IVampireActionEvent
{
[DataField]
public DamageSpecifier Damage = default!;
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
public sealed partial class VampireShadowBoxingActionEvent : EntityTargetActionEvent, IVampireActionEvent
{
[DataField]
public EntProtoId EntityId = "MobFollowerShadow";
[DataField]
public int Repeats = 10;
[DataField]
public TimeSpan TimeInterval = TimeSpan.FromSeconds(1);
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
public sealed partial class VampireEternalDarknessActionEvent : InstantActionEvent, IVampireActionEvent
{
[DataField]
public ProtoId<AlertPrototype> Alert = "AlertEternalDarkness";
[DataField]
public DamageSpecifier Damage = default!;
[DataField]
public TimeSpan TimeInterval = TimeSpan.FromSeconds(1);
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
// Gargantua Abilities
public sealed partial class VampireBloodSwellActionEvent : InstantActionEvent, IVampireActionEvent
{
[DataField]
public TimeSpan Time = TimeSpan.FromSeconds(30);
[DataField]
public bool Advanced = false;
[DataField]
public ProtoId<DamageTypePrototype> BonusDamageType = "Blunt";
[DataField]
public float BonusDamageAmount = 14f;
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
public sealed partial class VampireBloodRushActionEvent : InstantActionEvent, IVampireActionEvent
{
[DataField]
public TimeSpan Time = TimeSpan.FromSeconds(10);
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
public sealed partial class VampireSeismicStompActionEvent : InstantActionEvent, IVampireActionEvent
{
[DataField]
public SoundSpecifier Sound;
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
public sealed partial class VampireOverwhelmingForceActionEvent : InstantActionEvent, IVampireActionEvent
{
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
public sealed partial class VampireDemonicGraspActionEvent : EntityTargetActionEvent, IVampireActionEvent
{
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
public sealed partial class VampireChargeActionEvent : WorldTargetActionEvent, IVampireActionEvent
{
[DataField("components")]
public ComponentRegistry EnsurableComponents;
[DataField]
public SoundSpecifier Sound;
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
// Dantalion Abilities
public sealed partial class VampireEnthrallActionEvent : EntityTargetActionEvent, IVampireActionEvent
{
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
public sealed partial class VampireCommuneActionEvent : InstantActionEvent { }
public sealed partial class VampirePacifyActionEvent : EntityTargetActionEvent, IVampireActionEvent
{
[DataField]
public TimeSpan Time = TimeSpan.FromSeconds(40);
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
public sealed partial class VampireSubspaceSwapActionEvent : EntityTargetActionEvent, IVampireActionEvent
{
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
public sealed partial class VampireDeployDecoyActionEvent : InstantActionEvent, IVampireActionEvent
{
[DataField("components")]
public ComponentRegistry EnsurableComponents;
[DataField]
public ProtoId<CloningSettingsPrototype> Settings = "BaseClone";
[DataField]
public TimeSpan Time = TimeSpan.FromSeconds(6);
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
public sealed partial class VampireRallyThrallsActionEvent : InstantActionEvent, IVampireActionEvent
{
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
public sealed partial class VampireBloodBondActionEvent : InstantActionEvent, IVampireActionEvent
{
[DataField]
public TimeSpan TimeInterval = TimeSpan.FromSeconds(1);
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
public sealed partial class VampireMassHysteriaActionEvent : InstantActionEvent, IVampireActionEvent
{
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
public sealed partial class VampireThrallHealActionEvent : InstantActionEvent, IVampireActionEvent
{
[DataField]
public int Repeats = 3;
[DataField]
public DamageSpecifier Heal = default!;
[DataField]
public GroupHealSpecifier HealGroups = default!;
[DataField]
public TimeSpan TimeInterval = TimeSpan.FromSeconds(4);
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
public sealed partial class VampirePacifyNearbyActionEvent : InstantActionEvent, IVampireActionEvent
{
[DataField]
public TimeSpan Time = TimeSpan.FromSeconds(8);
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
// Bestia Abilities
public sealed partial class VampireCheckTrophiesActionEvent : InstantActionEvent { }
public sealed partial class VampireDissectActionEvent : EntityTargetActionEvent, IVampireActionEvent
{
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
public sealed partial class VampireInfectedTrophyActionEvent : EntityTargetActionEvent, IVampireActionEvent
{
[DataField]
public EntProtoId<ProjectileComponent> ProjectileId = "ProjectileInfectedTrophy";
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
public sealed partial class VampireLungeActionEvent : WorldTargetActionEvent, IVampireActionEvent
{
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
public sealed partial class VampireMarkPreyActionEvent : EntityTargetActionEvent, IVampireActionEvent
{
[DataField]
public ProtoId<DamageTypePrototype> DamageType = "Heat";
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
public sealed partial class VampireMetamorphosisBatsActionEvent : InstantActionEvent, IVampireActionEvent
{
[DataField]
public ProtoId<PolymorphPrototype> PolymorphProto = "VampireBats";
[DataField]
public ProtoId<DamageTypePrototype> BonusDamageType = "Piercing";
[DataField]
public EntProtoId MistEffect = "VampireMistEffect";
[DataField]
public EntProtoId MistReappearEffect = "VampireMistReappearEffect";
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
public sealed partial class VampireAnabiosisActionEvent : InstantActionEvent, IVampireActionEvent
{
[DataField]
public EntProtoId CoffinProto = "CrateCoffinVampire";
[DataField]
public TimeSpan Duration = TimeSpan.FromSeconds(30);
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
public sealed partial class VampireSummonBatsActionEvent : InstantActionEvent, IVampireActionEvent
{
[DataField]
public EntProtoId BatsProto = "MobBats";
[DataField]
public ProtoId<DamageTypePrototype> BonusDamageType = "Piercing";
[DataField]
public SoundSpecifier Sound;
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
public sealed partial class VampireMetamorphosisHoundActionEvent : InstantActionEvent, IVampireActionEvent
{
[DataField]
public ProtoId<PolymorphPrototype> PolymorphProto = "VampireHound";
[DataField]
public ProtoId<DamageTypePrototype> BonusDamageType = "Piercing";
[DataField]
public EntProtoId MistEffect = "VampireMistEffect";
[DataField]
public EntProtoId MistReappearEffect = "VampireMistReappearEffect";
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
// Polymorph Abilities
public sealed partial class VampireResonantShriekActionEvent : InstantActionEvent, IVampireActionEvent
{
[DataField]
public DamageSpecifier Damage = default!;
[DataField]
public SoundSpecifier Sound;
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
public sealed partial class VampireLungeFinaleActionEvent : InstantActionEvent, IVampireActionEvent
{
[DataField] public FixedPoint2 BloodCost { get; private set; }
}
@@ -0,0 +1,49 @@
using Content.Shared.DoAfter;
using Content.Shared.FixedPoint;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.Vampire;
[Serializable, NetSerializable]
public sealed partial class VampireDrinkingBloodDoAfterEvent : SimpleDoAfterEvent
{
public float Volume = 5;
public float AbsorptionRatio = 0.33f;
}
[Serializable, NetSerializable]
public sealed partial class SoulAnchorDoAfterEvent : SimpleDoAfterEvent
{
public EntProtoId EntityId = "BeaconSoul";
public FixedPoint2 BloodCost { get; }
public SoulAnchorDoAfterEvent(FixedPoint2 cost)
{
BloodCost = cost;
}
}
[Serializable, NetSerializable]
public sealed partial class EnthrallDoAfterEvent : SimpleDoAfterEvent
{
public FixedPoint2 BloodCost { get; }
public EnthrallDoAfterEvent(FixedPoint2 cost)
{
BloodCost = cost;
}
}
[Serializable, NetSerializable]
public sealed partial class VampireDissectDoAfterEvent : SimpleDoAfterEvent
{
public FixedPoint2 BloodCost;
public SoundSpecifier DissectSound = new SoundCollectionSpecifier("Organ");
public VampireDissectDoAfterEvent(FixedPoint2 cost)
{
BloodCost = cost;
}
}
@@ -1,6 +1,256 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using Content.Shared.Body;
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
using Content.Shared.Mobs.Systems;
using Content.Shared.Vampire.Components;
using Robust.Shared.Prototypes;
namespace Content.Shared.Vampire;
public abstract class SharedVampireSystem : EntitySystem
{
// Clear
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SharedVisualBodySystem _visualBody = default!;
#region Blood Management
public bool CheckBloodEssence(VampireComponent component, FixedPoint2 requiredAmount, FixedPoint2? nullDamage = null)
{
var adjustedQuantity = requiredAmount * (1 + (nullDamage?.Float() ?? 0) / 100);
return component.CurrentBlood >= adjustedQuantity;
}
public bool TryAddBlood(VampireComponent component, FixedPoint2 quantity, out FixedPoint2 newAmount)
{
newAmount = component.CurrentBlood + quantity;
if (quantity <= 0)
return false;
component.CurrentBlood = newAmount;
component.TotalBloodDrank += (float)quantity;
return true;
}
public bool TrySubtractBlood(VampireComponent component, FixedPoint2 quantity, FixedPoint2? nullDamage = null)
{
var adjustedQuantity = quantity * (1 + (nullDamage?.Float() ?? 0) / 100);
if (adjustedQuantity <= 0 || component.CurrentBlood < adjustedQuantity)
return false;
component.CurrentBlood -= adjustedQuantity;
return true;
}
#endregion
#region Skill Management
public bool TryGetThresholdsForClass(VampireComponent component, [NotNullWhen(true)] out Dictionary<float, List<EntProtoId>>? thresholds)
{
thresholds = null;
if (component.CurrentEvolution == VampireClassEnum.NonSelected)
return false;
return component.ClassThresholds.TryGetValue(component.CurrentEvolution, out thresholds);
}
public bool HasSkill(VampireComponent component, EntProtoId skill)
{
return component.AcquiredSkills.ContainsKey(skill);
}
public List<EntProtoId> GetNewSkillsToAdd(VampireComponent component)
{
var newSkills = new List<EntProtoId>();
if (component.CurrentEvolution == VampireClassEnum.NonSelected)
return newSkills;
if (!TryGetThresholdsForClass(component, out var thresholds))
return newSkills;
foreach (var (threshold, skills) in thresholds.OrderBy(kv => kv.Key))
{
if (component.CurrentBlood >= threshold)
{
foreach (var skill in skills)
{
if (!HasSkill(component, skill))
newSkills.Add(skill);
}
}
}
return newSkills;
}
#endregion
#region Thrall Management
public bool CanAddThrall(ThrallOwnerComponent owner)
{
return owner.ThrallOwned.Count < owner.MaxThrallCount;
}
public bool TryAddThrall(ThrallOwnerComponent owner, EntityUid thrallUid)
{
if (!CanAddThrall(owner))
return false;
if (!owner.ThrallOwned.Contains(thrallUid))
{
owner.ThrallOwned.Add(thrallUid);
return true;
}
return false;
}
public bool TryRemoveThrall(ThrallOwnerComponent owner, EntityUid thrallUid)
{
if (owner.ThrallOwned.Remove(thrallUid))
return true;
return false;
}
public List<EntityUid> GetAliveThralls(ThrallOwnerComponent owner)
{
return owner.ThrallOwned.Where(Exists)
.Where(t => !_mobState.IsDead(t))
.ToList();
}
#endregion
#region Bestia Management
public void RecordExtraction(EntityUid vampire, EntityUid victim, [NotNullWhen(true)] out BestiaContainerComponent? bestia)
{
if (!TryComp(vampire, out bestia))
return;
if (!bestia.OrgansExtractedFromVictim.TryAdd(victim, 1))
bestia.OrgansExtractedFromVictim[victim]++;
}
#endregion
#region True Power Management
public bool ShouldHaveTruePower(FixedPoint2 currentBlood)
{
return currentBlood >= 1000;
}
public bool HasTruePower(EntityUid uid)
{
return HasComp<SupremeVampireComponent>(uid);
}
public SupremeVampireComponent? GetTruePower(EntityUid uid)
{
return CompOrNull<SupremeVampireComponent>(uid);
}
#endregion
#region Night Vision
public Color GetNightVisionColorForClass(VampireClassEnum vampireClass)
{
return vampireClass switch
{
VampireClassEnum.Umbrae => Color.FromHex("#663ca3"),
_ => Color.FromHex("#adadad")
};
}
public float GetNightVisionRadiusForClass(VampireClassEnum vampireClass, bool hasTruePower = false)
{
if (hasTruePower)
return 15;
return vampireClass switch
{
VampireClassEnum.Umbrae => 12,
_ => 8
};
}
#endregion
#region Eye Color Management
public void SetEyeColor(EntityUid uid, Color color)
{
if (_visualBody.TryGatherMarkingsData(uid, null, out var profiles, out _, out _))
{
var newProfiles = profiles.ToDictionary(
kv => kv.Key,
kv => kv.Value with { EyeColor = color }
);
_visualBody.ApplyProfiles(uid, newProfiles);
}
}
public Color GetCurrentEyeColor(EntityUid uid)
{
if (_visualBody.TryGatherMarkingsData(uid, null, out var profiles, out _, out _))
{
var firstProfile = profiles.Values.FirstOrDefault();
return firstProfile.EyeColor;
}
return Color.White;
}
public Color GetVampireEyeColor(VampireClassEnum vampireClass)
{
return vampireClass switch
{
VampireClassEnum.Hemomancer => Color.FromHex("#eb251b"),
VampireClassEnum.Umbrae => Color.FromHex("#b8188a"),
VampireClassEnum.Gargantua => Color.FromHex("#d43a18"),
VampireClassEnum.Dantalion => Color.FromHex("#6ab820"),
VampireClassEnum.Bestia => Color.FromHex("#c43088"),
_ => Color.FromHex("#e22218")
};
}
#endregion
#region Space Damage
public bool ShouldTakeSpaceDamage(VampireComponent component, float frameTime, out bool shouldDamage)
{
shouldDamage = false;
if (component.NextSpaceDamageTick <= 0)
{
shouldDamage = true;
component.NextSpaceDamageTick = 1;
return true;
}
component.NextSpaceDamageTick -= frameTime;
return false;
}
#endregion
#region Damage Sharing
public DamageSpecifier GetSharedDamage(DamageSpecifier originalDamage, int participantCount)
{
if (participantCount <= 0)
return originalDamage;
return originalDamage / participantCount;
}
#endregion
}
@@ -1,15 +0,0 @@
using Content.Shared.StatusIcon;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.Vampire.Components;
[RegisterComponent, NetworkedComponent]
public sealed partial class ThrallComponent : Component
{
[DataField]
public EntityUid? VampireOwner = null;
[DataField]
public ProtoId<FactionIconPrototype> StatusIcon = "ThrallFaction";
}
@@ -0,0 +1,80 @@
using Content.Shared.Eui;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.Vampire;
[Serializable, NetSerializable]
public sealed class TrophiesEuiState : EuiStateBase
{
public List<OrganDisplayInfo> Organs { get; }
public List<PassiveBonusInfo> PassiveBonuses { get; }
public List<AbilityDisplayInfo> Abilities { get; }
public TrophiesEuiState(
List<OrganDisplayInfo> organs,
List<PassiveBonusInfo> passiveBonuses,
List<AbilityDisplayInfo> abilities)
{
Organs = organs;
PassiveBonuses = passiveBonuses;
Abilities = abilities;
}
}
[Serializable, NetSerializable]
public sealed class OrganDisplayInfo
{
public BestiaOrganType Type { get; set; }
public int Count { get; set; }
public int MaxCount { get; set; }
public Color CountColor { get; set; } = Color.White;
public NetEntity? PreviewEntity { get; set; }
}
[Serializable, NetSerializable]
public sealed class PassiveBonusInfo
{
public string Name { get; set; } = "";
public string Value { get; set; } = "";
public Color? ValueColor { get; set; }
public bool IsMaxed { get; set; }
}
[Serializable, NetSerializable]
public sealed class AbilityDisplayInfo
{
public NetEntity Action { get; set; }
public string Name { get; set; } = "";
public List<OrganBonusDetail> Bonuses { get; set; } = new();
}
[Serializable, NetSerializable]
public sealed class OrganBonusDetail
{
public string OrganType { get; set; } = "";
public string Description { get; set; } = "";
public bool IsMaxed { get; set; }
}
[Serializable, NetSerializable]
public sealed class DissectSelectionEuiState : EuiStateBase
{
public List<NetEntity> AvailableOrgans { get; }
public DissectSelectionEuiState(List<NetEntity> availableOrgans)
{
AvailableOrgans = availableOrgans;
}
}
[Serializable, NetSerializable]
public sealed class DissectOrganSelectedMessage : EuiMessageBase
{
public NetEntity Target { get; }
public DissectOrganSelectedMessage(NetEntity target)
{
Target = target;
}
}
@@ -0,0 +1,16 @@
using Content.Shared.Eui;
using Robust.Shared.Serialization;
namespace Content.Shared.Vampire;
[Serializable, NetSerializable]
public sealed class VampireClassSelectionState : EuiStateBase
{
}
[Serializable, NetSerializable]
public sealed class VampireClassSelectedMessage : EuiMessageBase
{
public VampireClassEnum SelectedClass { get; }
public VampireClassSelectedMessage(VampireClassEnum selectedClass) => SelectedClass = selectedClass;
}
@@ -1,124 +0,0 @@
using Content.Shared.Alert;
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
using Content.Shared.Metabolism;
using Content.Shared.StatusIcon;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.Vampire.Components;
[RegisterComponent, NetworkedComponent]
[AutoGenerateComponentState]
public sealed partial class VampireComponent : Component
{
public static readonly ProtoId<MetabolizerTypePrototype> MetabolizerVampire = "Vampire";
public static readonly DamageSpecifier HolyDamage = new()
{
DamageDict = { { "Heat", 10 } }
};
public static readonly DamageSpecifier SpaceDamage = new()
{
DamageDict = { { "Heat", 2.5 } }
};
public static readonly EntProtoId DrinkActionPrototype = "ActionDrinkBlood";
public static readonly EntProtoId SelectClassActionPrototype = "ActionVampireSelectClass";
public static readonly EntProtoId RejuvenateActionPrototype = "ActionVampireRejuvenate";
public static readonly EntProtoId GlareActionPrototype = "ActionVampireGlare";
public readonly SoundSpecifier BloodDrainSound = new SoundPathSpecifier(
"/Audio/Items/drink.ogg",
new AudioParams() { Volume = -3f, MaxDistance = 3f }
);
[DataField, ViewVariables(VVAccess.ReadOnly)]
public string? CurrentEvolution { get; set; }
/// <summary>
/// The current amount of blood in the vampire's account.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[AutoNetworkedField]
public FixedPoint2 CurrentBlood = 0;
[DataField("vampireStatusIcon")]
public ProtoId<FactionIconPrototype> StatusIcon { get; set; } = "VampireFaction";
[DataField]
public ProtoId<AlertPrototype> BloodAlert = "BloodAlert";
/// <summary>
/// Fields for counting the total amount of blood consumed after the end of the round
/// </summary>
public float TotalBloodDrank = 0;
public float NextSpaceDamageTick { get; set; }
public float NextNullDamageTick { get; set; }
public bool TruePowerActive = false;
public bool IsCharging = false;
public HashSet<EntityUid> AlreadyHit { get; } = new HashSet<EntityUid>();
public bool PowerActive = false;
public bool IsDamageSharingActive = false;
[DataField]
public FixedPoint2 NullDamage = 0;
[DataField]
public int ThrallCount = 0;
[DataField]
public int MaxThrallCount = 1;
[DataField, ViewVariables(VVAccess.ReadOnly)]
public List<EntityUid> ThrallOwned = new();
[DataField, ViewVariables(VVAccess.ReadOnly)]
public List<string> AcquiredSkills = new List<string>();
}
/// <summary>
/// Marks an entity as taking damage when hit by a bible, rather than being healed
/// </summary>
[RegisterComponent]
public sealed partial class UnholyComponent : Component;
[RegisterComponent]
public sealed partial class BeaconSoulComponent : Component
{
[DataField]
public EntityUid VampireOwner = EntityUid.Invalid;
}
[RegisterComponent, NetworkedComponent]
public sealed partial class BittenByVampireComponent : Component;
/// <summary>
/// A component for testing vampire arson near holy sites.
/// </summary>
[RegisterComponent]
public sealed partial class HolyPointComponent : Component
{
[DataField]
public float Range = 6f;
public float NextTimeTick { get; set; }
}
[Serializable, NetSerializable]
public enum VampireVisualLayers : byte
{
Digit1,
Digit2,
Digit3
}
@@ -0,0 +1,36 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Vampire;
[Serializable, NetSerializable]
public enum VampireClassEnum : byte
{
NonSelected = 0,
Hemomancer,
Umbrae,
Gargantua,
Dantalion,
Bestia // Here! Don't bite me!
}
[Serializable, NetSerializable]
public enum VampireVisualLayers : byte
{
Digit1,
Digit2,
Digit3,
Digit4
}
// Bestia enum
[Serializable, NetSerializable]
public enum BestiaOrganType : byte
{
Unknown = 0,
Heart,
Lungs,
Liver,
Kidneys,
Eyes,
Stomach
}
@@ -1,202 +0,0 @@
using Content.Shared.Actions;
using Content.Shared.DoAfter;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Content.Shared.Alert;
namespace Content.Shared.Vampire;
// Base
public sealed partial class VampireSelectClassActionEvent : InstantActionEvent { }
public sealed partial class VampireRejuvenateActionEvent : InstantActionEvent { }
public sealed partial class VampireGlareActionEvent : EntityTargetActionEvent { }
public sealed partial class VampireDrinkingBloodActionEvent : EntityTargetActionEvent { }
[Serializable, NetSerializable]
public sealed partial class VampireDrinkingBloodDoAfterEvent : SimpleDoAfterEvent
{
[DataField]
public float Volume = 0;
}
[Serializable, NetSerializable]
public sealed class SelectClassPressedEvent : EntityEventArgs
{
public NetEntity Uid { get; }
public SelectClassPressedEvent(NetEntity uid)
{
Uid = uid;
}
}
[Serializable, NetSerializable]
public sealed class VampireSelectClassMenuClosedEvent : EntityEventArgs
{
public NetEntity Uid { get; }
public string SelectedClass { get; }
public VampireSelectClassMenuClosedEvent(NetEntity uid, string selectedClass)
{
Uid = uid;
SelectedClass = selectedClass;
}
}
// Hemomancer Abilities
public sealed partial class VampireClawsActionEvent : InstantActionEvent { }
public sealed partial class VampireBloodTentacleAction : WorldTargetActionEvent
{
[DataField]
public EntProtoId EntityId = "EffectBloodTentacleSpawn";
[DataField]
public List<Direction> OffsetDirections = new()
{
Direction.North,
Direction.South,
Direction.East,
Direction.West,
Direction.NorthEast,
Direction.NorthWest,
Direction.SouthEast,
Direction.SouthWest,
};
[DataField]
public int ExtraSpawns = 8;
}
public sealed partial class VampireBloodBarrierActionEvent : WorldTargetActionEvent
{
[DataField]
public EntProtoId EntityId = "BloodBarrier";
public bool UseCasterDirection { get; set; } = true;
}
public sealed partial class VampireSanguinePoolActionEvent : InstantActionEvent
{
[DataField]
public string PolymorphProto = "VampireBlood";
public SoundSpecifier Sound = new SoundPathSpecifier("/Audio/Effects/Fluids/splat.ogg");
}
public sealed partial class VampirePredatorSensesActionEvent : InstantActionEvent
{
[DataField]
public string Proto = "PuddleBlood";
public SoundSpecifier Sound = new SoundPathSpecifier("/Audio/Effects/Fluids/splat.ogg");
}
public sealed partial class VampireBloodEruptionActionEvent : InstantActionEvent { }
public sealed partial class VampireBloodBringersRiteActionEvent : InstantActionEvent
{
[DataField]
public string Proto = "PuddleBlood";
public SoundSpecifier Sound = new SoundPathSpecifier("/Audio/Effects/Fluids/splat.ogg");
}
public sealed partial class VampireBloodRiteAlertEvent : BaseAlertEvent;
// Umbrae Abilities
public sealed partial class VampireCloakOfDarknessActionEvent : InstantActionEvent { }
public sealed partial class VampireShadowSnareActionEvent : WorldTargetActionEvent
{
[DataField]
public EntProtoId EntityId = "ShadowTrap";
}
public sealed partial class VampireSoulAnchorActionEvent : InstantActionEvent { }
[Serializable, NetSerializable]
public sealed partial class SoulAnchorDoAfterEvent : SimpleDoAfterEvent { }
public sealed partial class VampireDarkPassageActionEvent : WorldTargetActionEvent { }
public sealed partial class VampireExtinguishActionEvent : InstantActionEvent { }
public sealed partial class VampireShadowBoxingActionEvent : EntityTargetActionEvent { }
public sealed partial class VampireEternalDarknessActionEvent : InstantActionEvent { }
[Serializable, NetSerializable]
public sealed partial class VampireToggleFovEvent : EntityEventArgs
{
public NetEntity User { get; }
public bool Enabled { get; }
public VampireToggleFovEvent(NetEntity user, bool enabled)
{
User = user;
Enabled = enabled;
}
}
// Gargantua Abilities
public sealed partial class VampireRejuvenateAdvancedActionEvent : InstantActionEvent { }
public sealed partial class VampireBloodSwellActionEvent : InstantActionEvent { }
public sealed partial class VampireBloodRushActionEvent : InstantActionEvent { }
public sealed partial class VampireSeismicStompActionEvent : InstantActionEvent
{
public SoundSpecifier Sound = new SoundPathSpecifier("/Audio/Effects/Footsteps/largethud.ogg");
}
public sealed partial class VampireBloodSwellAdvancedActionEvent : InstantActionEvent { }
public sealed partial class VampireOverwhelmingForceActionEvent : InstantActionEvent { }
public sealed partial class VampireDemonicGraspActionEvent : EntityTargetActionEvent { }
public sealed partial class VampireChargeActionEvent : WorldTargetActionEvent
{
public SoundSpecifier Sound = new SoundPathSpecifier("/Audio/Effects/Footsteps/largethud.ogg");
}
// Dantalion Abilities
public sealed partial class MaxThrallCountUpdateEvent : InstantActionEvent { }
public sealed partial class VampireEnthrallActionEvent : EntityTargetActionEvent { }
[Serializable, NetSerializable]
public sealed partial class EnthrallDoAfterEvent : SimpleDoAfterEvent
{
public new NetEntity Target { get; set; }
public EnthrallDoAfterEvent(NetEntity target)
{
Target = target;
}
}
public sealed partial class VampireCommuneActionEvent : InstantActionEvent { }
public sealed partial class VampirePacifyActionEvent : EntityTargetActionEvent { }
public sealed partial class VampireSubspaceSwapActionEvent : EntityTargetActionEvent { }
//public sealed partial class VampireDeployDecoyActionEvent : InstantActionEvent { }
public sealed partial class VampireRallyThrallsActionEvent : InstantActionEvent { }
public sealed partial class VampireBloodBondActionEvent : InstantActionEvent { }
public sealed partial class VampireMassHysteriaActionEvent : InstantActionEvent { }
public sealed partial class VampireThrallHealActionEvent : InstantActionEvent { }
public sealed partial class VampirePacifyNearbyActionEvent : InstantActionEvent { }
Binary file not shown.
@@ -1,5 +1,5 @@
alerts-vampire-blood-name = Кровь
alerts-vampire-blood-desc = Накопленная жизненная сила вампира позволяющая ему использовать сверх естественные способности.
alerts-vampire-blood-desc = Накопленная жизненная сила вампира позволяющая ему использовать сверхъестественные способности.
alerts-strangle-name = Душат
alerts-strangle-desc = Вас [color=red]ДУШАТ[/color]. Щёлкните по иконке, чтобы попытаться выбраться
alerts-offer-name = Предложение
@@ -7,4 +7,4 @@ alerts-offer-desc = Кто-то предлагает вам взять пред
alerts-bloodrite-name = [color=purple]Обряд крови[/color]
alerts-bloodrite-desc = Обратите это место в кровавую баню. Отображает статус способности [color=purple]Обряд крови[/color].
alerts-eternaldarkness-name = [color=purple]Вечная темнота[/color]
alerts-eternaldarkness-desc = Жертвы в радиусе 6 тайлов замерзают и вы видите через стены. Отображает статус способности [color=purple]Вечная темнота[/color].
alerts-eternaldarkness-desc = Жертвы в радиусе 6 тайлов замерзают. Отображает статус способности [color=purple]Вечная темнота[/color].
@@ -4,6 +4,7 @@ disease-proto-spectral-tiredness = спектральное утомление
disease-proto-lung-cancer = рак лёгких IIIA стадии
disease-proto-slimeitis = слаймитис
disease-proto-surgical-sepsis = сепсис внутренних тканей
disease-vampire-grave-fever = могильная лихорадка
# Infectious
disease-proto-space-cold = космическая простуда
disease-proto-vent-cough = трубный кашель
@@ -1,3 +1,6 @@
disease-slime-drip = Ваша кожа становится липкой и вязкой.
disease-polymorph-slime = Ваше тело становится полужидким... вы превращаетесь в разумного слайма!
disease-surgical-wound-pain = Ваша рана от операции пульсирует и болит!
disease-surgical-wound-pain = Ваша рана от операции пульсирует и болит!
disease-grave-fever-start = Вы чувствуете необъяснимую слабость...
disease-grave-fever-weakness = Ваше тело слабеет...
disease-grave-fever-terminal = Ваше тело разрушается...
@@ -13,9 +13,19 @@ vampire-role-greeting-human =
vampire-role-greeting-animal =
Вы - кровососущее животное.
Ваша жизнь лишь в том, что вам нужна кровь.
free-vampire-greeting =
Вы - Вампир!
Вы были прокляты могущественным вампиром и восстали из мёртвых в "новом" для вас теле. Теперь вы вынуждены пить кровь, чтобы жить.
Вы не подчиняетесь проклявшему вас вампиру и обладаете свободной волей.
Вы не являетесь полноценным антагонистом и не должны убивать, за исключением случаев самообороны.
objective-issuer-vampire = [color=purple]Вампир[/color]
vampires-drank-total-blood = Было выпито { $bloodAmount } единиц крови.
vampire-round-end-name = вампир
vampire-round-end-header = [color=purple]Вампиры:[/color]
vampire-round-end-info = [color=white]{ $name }[/color] ([color={ $color }]{ $class }[/color]) выпил [color=crimson]{ $blood }[/color] ед. крови.
vampires-drank-total-blood = Всего было выпито [color=crimson]{ $bloodAmount }[/color] ед. крови.
thrall-greeting =
Ваш разум затуманился, вы чувствуете холод.
Ваша цель следовать приказам хозяина!
@@ -1,40 +1,72 @@
# Ui
# Classes
select-class-nonselected = Без класса
select-class-hemomancer = Гемомансер
select-class-umbrae = Умбра
select-class-gargantua = Гаргантюа
select-class-dantalion = Данталион
select-class-bestia = Бестия
# System
vampire-holy-point = СВЯТАЯ СИЛА ВОСПЛАМЕНЯЕТ ВАШУ КОЖУ!
vampire-blooddrink-countion = Клыки впиваются в вашу шею
vampire-blooddrink-countion = Клыки { $vampire } впиваются в вашу шею
vampire-blooddrink-self = Вы не можете пить свою кровь
vampire-blooddrink-rotted = Оно гниет!
vampire-blooddrink-not-vampire = Вы не можете испить кровь вампира
vampire-blooddrink-rotted = { $target } гниет!
vampire-blooddrink-not-thrall = Вы не можете испить кровь тралла
vampire-blooddrink-not-sentient = Вы не можете испить кровь неразумного
vampire-blooddrink-empty = Жертва иссякла
vampire-blooddrink-maxed-out = Вы уже насытились достаточно с этой жертвы
vampire-blooddrink-ssd = Вы не можете испить кровь спящего
vampire-blooddrink-countion-doafter = Вы чувствуете как становится холоднее
vampire-ingest-holyblood = ВЫ НЕ МОЖЕТЕ ПИТЬ СВЯТУЮ КРОВЬ!!!
vampire-full-stomach = Вы не можете пить больше
vampire-startlight-burning = ВАША КОЖА ПОЛЫХАЕТ!
vampire-true-power = Вы чувствуете истинную силу
vampire-bittenbyvampire-examine = [color=red]Кажется, на шее есть укус[/color]
# Diablerie
vampire-diablerie-aura-visible = [color=purple]Вокруг { $name } клубится тёмная, пульсирующая аура, которая пронизывает всё вокруг.[/color]
vampire-diablerie-eyes-glow-examined = [color=red]Глаза { $name } созерцают неестественным красным цветом, это не к добру...[/color]
vampire-diablerie-no-class = Вы должны выбрать класс, чтобы иметь возможность поглотить другого вампира.
vampire-diablerie-target-no-class = Ничего не произошло...
vampire-diablerie-draining = Вы высасываете тёмную сущность из { $target }...
vampire-diablerie-target-draining = Вы ощущаете боль по всему телу, теряя драгоценную часть своей силы.
vampire-diablerie-dust = { $target } рассыпается в пепел!
vampire-diablerie-level-one = Сила вашего "Восстановления" возросла, и вы можете применять его больше раз.
vampire-diablerie-level-two = Сила вашего взгляда возросла, и вы можете больше раз применять "Вспышку". Ваши глаза наливаются кроваво-красным светом.
vampire-diablerie-level-three = Сила вашего "Восстановления" возросла, и теперь с его помощью вы можете восстанавливать внутренние кровотечения. Ваша аура теперь видна даже простым смертным. Вы всего в шаге от вершины могущества!
vampire-diablerie-level-four = Сила вашего тела возросла. Вы достигли пика своего могущества. Теперь долгом всех смертных будет уничтожить вас!
vampire-ascended-announcement =
Сканерами дальнего действия зафиксирован мощный всплеск блюспейс энергии, указывающий на появление вампира особого класса.
Его личность — { $name }. Дальнейшее возвышение вампира должно быть немедленно предотвращено.
vampire-initiation-failed = Это существо не подходит для таинства посвящения...
# Abilities
vampire-hungry = Вы ещё слишком голодны для эволюции
vampire-blooddrink-countion-doafter = Вы чувствуете как становится холоднее
vampire-heal-dead = Вы мертвы
vampire-blood-sacrifice-insufficient-blood = У вас недостаточно крови для использования
vampire-predator-senses-warning = Вы чувствуете что жертва { $location }
vampire-predator-senses-nobody = Вы не чувствуете жертв по близости
# Hemomancer
vampire-predator-senses-warning = С { $direction }а доносится запах крови...
vampire-predator-senses-nobody = Тишина. Ни одного бьющегося сердца поблизости...
vampire-predator-senses-puddle = Под вашими ногами образуется лужа крови. Это не к добру...
vampire-blood-eruption-effect-message = Кровь бурлящая под вашими ногами поражает вас и сбивает с ног
vampire-blood-true-power-started = Вы ощущаете силу
vampire-blood-true-power-affected = Вы чувствуете как ваша кровь изливается из вас...
# Umbrae
vampire-stealth-enabled = Вы вошли в покров
vampire-stealth-disabled = Вы вышли из покрова
vampire-teleport-failed = Перемещение не возможно
# Gargantua
vampire-legs-ensnared = Ваши ноги связаны!
# Dantalion
vampire-trall-count-update = Ваше максимальное количество слуг теперь { $count }
vampire-max-trall-reached = Вы достигли предела траллов
vampire-enthall-failed = Невозможно обратить { $target }
vampire-enthall-already = { $target } уже обращен
@@ -42,5 +74,85 @@ vampire-no-thrall = У вас нету траллов
vampire-commune-title = Связь
vampire-commune-prompt = Сообшение
vampire-commune-default-message = Вы слышите голос у себя в голове...
vampire-commune-sent = Послание отправлено
vampire-pacify-failed = Невозможно применить на { $target }
# Bestia
vampire-trophies-ui-title = Статус Трофеев
vampire-trophies-ui-trophies = Трофеи
vampire-trophies-ui-passives = Пассивные бонусы
vampire-trophies-ui-abilities = Способности
vampire-trophies-ui-max = (MAX)
vampire-trophies-ui-no-bonuses = Нет бонусов
vampire-bestia-unknown = неизвестно
vampire-bestia-heart = сердце
vampire-bestia-lungs = легкие
vampire-bestia-liver = печень
vampire-bestia-kidneys = почки
vampire-bestia-eyes = глаза
vampire-bestia-stomach = желудок
vampire-bestia-dissect-self = Нельзя применить это действие к самому себе
vampire-bestia-dissect-out-of-range = Цель слишком далеко
vampire-bestia-dissect-dead = Цель мертва. Невозможно извлечь органы из трупа
vampire-bestia-dissect-ssd = Вы не можете извлечь органы из спящего
vampire-bestia-dissect-no-organs = У цели нет подходящих органов для извлечения
vampire-bestia-dissect-no-blood = В крови цели нет органики
vampire-bestia-dissect-inappropriate = Цель не подходит для извлечения органов
vampire-bestia-dissect-victim-limit-reached = Вы уже извлекли максимальное количество органов с этой жертвы
vampire-bestia-dissect-limit-reached = Вы достигли лимита по органам для {$organ}
vampire-bestia-dissect-extract-fail = Не удалось извлечь орган
vampire-bestia-dissect-success = Вы успешно извлекли {$organ}
vampire-bestia-anabiosis-polymorphed = Невозможно впасть в анабиоз будучи обращенным
vampire-bestia-passive-brute-protection = Травмы
vampire-bestia-passive-burn-protection = Ожоги
vampire-bestia-passive-oxy-protection = Гипоксия
vampire-bestia-passive-toxin-protection = Токсины
vampire-bestia-passive-stamina = Выносливость
vampire-bestia-passive-blood-cost-reduction = Затраты крови
vampire-bestia-passive-suck-rate = Скорость укуса
vampire-bestia-passive-cellular-protection = Клеточный
vampire-bestia-passive-xray-vision = X-Ray
vampire-bestia-passive-welding-protection = Сварка
vampire-bestia-passive-flash-protection = Вспышки
vampire-bestia-passive-blood-gain = Восполнение
vampire-bestia-passive-healing-efficiency = Лечение
vampire-bestia-passive-unlocked = Да
vampire-bestia-passive-locked = Нет
# Infected Trophy
vampire-bestia-bonus-infected-trophy-heart = Урон +{$value}
vampire-bestia-bonus-infected-trophy-liver = Шанс болезни +{$value}%
vampire-bestia-bonus-infected-trophy-eyes = Время жизни +{$value}с
vampire-bestia-bonus-infected-trophy-stomach = Радиус взрыва до {$value}
# Lunge
vampire-bestia-bonus-lunge-heart = Оглушение +{$value}с
vampire-bestia-bonus-lunge-lungs = Дальность до {$value} тайлов
vampire-bestia-bonus-lunge-kidneys = Кровь за удар до {$value}
vampire-bestia-bonus-lunge-stomach = Радиус урона до {$value} тайлов
# Mark Prey
vampire-bestia-bonus-mark-prey-heart = {$damage} урона ожогами, шанс {$chance}%
vampire-bestia-bonus-mark-prey-eyes = Дальность до {$value} тайлов
vampire-bestia-bonus-mark-prey-kidneys = Длительность метки +{$value}с
# Metamorphosis Bats
vampire-bestia-bonus-metamorphosis-bats-heart = Здоровье {$health}, урон +{$damage}
vampire-bestia-bonus-metamorphosis-bats-lungs = Скорость +{$value}%
vampire-bestia-bonus-metamorphosis-bats-liver = Вампир получает {$value} крови за укус
vampire-bestia-bonus-metamorphosis-bats-kidneys = Лечение ×{$value}
# Summon Bats
vampire-bestia-bonus-summon-bats-heart = Здоровье мышей {$health}, урон +{$damage}
vampire-bestia-bonus-summon-bats-lungs = Скорость мышей +{$value}%
vampire-bestia-bonus-summon-bats-liver = Вампир получает {$value} крови за укус мыши
vampire-bestia-bonus-summon-bats-kidneys = Мыши лечат себя ×{$value}
# Metamorphosis Hound
vampire-bestia-bonus-metamorphosis-hound-heart = Здоровье {$health}, урон +{$damage}
vampire-bestia-bonus-metamorphosis-hound-lungs = Скорость +{$value}%
vampire-bestia-bonus-metamorphosis-hound-kidneys = Лечение ×{$value}
vampire-bestia-bonus-metamorphosis-hound-liver = Вампир получает {$value} крови за укус
@@ -8,6 +8,10 @@ ent-ActionVampireRejuvenate = [color=purple]Восстановление[/color]
ent-ActionVampireGlare = [color=purple]Взгляд[/color]
.desc = Поразите свою жертву лучом яркого света так, чтобы она потеряла сознание.
# Diablerie
ent-ActionVampireSacramentInitiation = [color=purple]Таинство посвящения[/color]
.desc = Вы достигли пика своей силы. За 50 крови вы можете воскрешать мёртвых как свободных вампиров. Они не являются вашими рабами.
# Hemomancer
ent-ActionVampireClaws = [color=purple]Когти вампира[/color]
.desc = Обножи свои когти и наноси вред своим врагам.
@@ -41,8 +45,8 @@ ent-ActionVampireEternalDarkness = [color=purple]Вечная темнота[/co
.desc = Погрузите это место в вечную тьму.
# Gargantua
ent-ActionVampireRejuvenateAdvanced = [color=purple]Восстановление[/color]
.desc = Твой второй шанс, твое второе дыхание.
ent-ActionVampireRejuvenateAdvanced = { ent-ActionVampireRejuvenate }
.desc = { ent-ActionVampireRejuvenate.desc }
ent-ActionVampireBloodSwell = [color=purple]Нарастание крови[/color]
.desc = Твоя кровь густеет, ты становишься сильнее.
ent-ActionVampireBloodRush = [color=purple]Прилив крови[/color]
@@ -59,9 +63,6 @@ ent-ActionVampireCharge = [color=purple]Рывок[/color]
.desc = Сделайте рывок.
# Dantalion
ent-ActionMaxThrallCountUpdate1 = [color=purple]Улучшить траллов[/color]
ent-ActionMaxThrallCountUpdate2 = [color=purple]Улучшить траллов[/color]
ent-ActionMaxThrallCountUpdate3 = [color=purple]Улучшить траллов[/color]
ent-ActionEnthrall = [color=purple]Порабощение[/color]
.desc = Обратите человека на свою сторону.
ent-ActionCommune = [color=purple]Общение[/color]
@@ -70,6 +71,8 @@ ent-ActionPacify = [color=purple]Умиротворение[/color]
.desc = Успокой своего недоброжелателя.
ent-ActionSubspaceSwap = [color=purple]Подпространственный обмен[/color]
.desc = Поменяйтесь местами.
ent-ActionDeployDecoy = [color=purple]Поставить приманку[/color]
.desc = Создаёт вашу копию, а вы становитесь невидимым.
ent-ActionRallyThralls = [color=purple]Сплотить рабов[/color]
.desc = Дай силу своим рабам.
ent-ActionBloodBond = [color=purple]Кровавая связь[/color]
@@ -80,3 +83,29 @@ ent-ActionVampirePacifyNearby = [color=purple]Псионические волн
.desc = Создайте псионическую волну, что успокоит ваших врагов в некотором радиусе.
ent-ActionVampireThrallHeal = [color=purple]Поддержка Лидера[/color]
.desc = Пусть услышат ваши поданные ваш голос. Восстанавливает траллов в некотором радиусе и вводит в них стимулирующие реагенты.
# Bestia
ent-ActionVampireCheckTrophies = [color=purple]Проверить трофеи[/color]
.desc = Открыть интерфейс для просмотра всех собранных органов и получаемых от них бонусов.
ent-ActionVampireDissect = [color=purple]Препарировать[/color]
.desc = Хирургически извлечь органы из живой жертвы. Каждый орган даёт уникальные пассивные или активные бонусы.
ent-ActionVampireInfectedTrophy = [color=purple]Инфицированный трофей[/color]
.desc = Запустить снаряд, наносящий урон, заражающий цель и взрывающийся при попадании. Урон, шанс заражения, время жизни и радиус урона зависят от собранных органов.
ent-ActionVampireLunge = [color=purple]Рывок[/color]
.desc = Устремляетесь к указанной точке. При приземлении оглушаете и вызываете кровотечение у всех врагов в области. Дальность, длительность оглушения, кража крови и радиус зависят от собранных органов.
ent-ActionVampireMarkPrey = [color=purple]Отметить жертву[/color]
.desc = Отмечаете цель, замедляя её и нанося периодический урон ожогами. Длительность метки, шанс ожога, урон и дальность зависят от собранных органов.
ent-ActionVampireMetamorphosisBats = [color=purple]Метаморфоз Летучие мыши[/color]
.desc = Превращаетесь в рой летучих мышей. В этой форме ваше здоровье, урон, скорость, вампиризм и самолечение зависят от собранных органов.
ent-ActionVampireAnabiosis = [color=purple]Анабиоз[/color]
.desc = Укрываетесь в гробу, чтобы регенерировать. Гроб защищает вас и погружает в глубокий сон, восстанавливая здоровье со временем.
ent-ActionVampireSummonBats = [color=purple]Призвать летучих мышей[/color]
.desc = Призываете 1-4 летучих мышей для битвы. Здоровье, урон, скорость и вампиризм каждой мыши зависят от собранных органов.
ent-ActionVampireMetamorphosisHound = [color=purple]Метаморфоз Гончая[/color]
.desc = Превращаетесь в могучего пса. В этой форме ваше здоровье, урон, скорость и самолечение зависят от собранных органов.
# Polymorph
ent-ActionVampireResonantShriek = [color=purple]Резонирующий крик[/color]
.desc = Издаёте мощный крик, который разбивает окна, ломает лампы и оглушает всех вокруг. Длительность оглушения и дальность зависят от собранных органов.
ent-ActionVampireLungeFinale = [color=purple]Финальный Рывок[/color]
.desc = Выполняете серию рывков на случайных жертвах поблизости. Каждые 10 трофеев дают дополнительный прыжок. Дальность, длительность оглушения, кража крови и радиус зависят от собранных органов.
@@ -21,4 +21,10 @@ ent-MobWolfling = вольпин
.desc = Собрат по генетике Вульпакинам. Их очень весело сжигать но аккуратно, у них есть план.
ent-MobTajkey = фарва
.desc = Маленькая версия Таярина. Он не похож на себя побольше. Чёрт возьми, что он вообще такое?
.desc = Маленькая версия Таярина. Он не похож на себя побольше. Чёрт возьми, что он вообще такое?
ent-MobBats = летучие мыши
.desc = Стая летучих мышей, подчиняющихся воле вампира. Они роятся вокруг, атакуя врагов и высасывая кровь.
ent-MobHellHound = гончая
.desc = Ужасающий пёс из преисподней, вызванный вампиром. Его когти и зубы способны разорвать даже сталь.
@@ -2,3 +2,6 @@ ent-CrateNecropolis = сундук некрополя
.desc = Древний саркофаг, испускающий зловещее свечение. Содержит награды за победу над ужасами некрополя. Обладает странными защитными свойствами.
ent-HoloCreate = голоящик
.desc = Проекция обычного ящика, которая существует 8 минут!
ent-CrateCoffinVampire = { ent-CrateCoffin }
.desc = { ent-CrateCoffin.desc }
.suffix = НЕ МАППИТЬ
+7 -1
View File
@@ -28,7 +28,7 @@
- !type:EvenHealthChange
conditions:
- !type:MetabolizerTypeCondition
type: [ Bloodsucker ]
type: [ Bloodsucker, Vampire ] # Corvax-Wega-Edit
damage:
Brute: -3
Burn: -1.25
@@ -38,6 +38,12 @@
damage:
types:
Poison: 0.2
# Corvax-Wega-Vampire-start
conditions:
- !type:MetabolizerTypeCondition
type: [ Vampire ]
inverted: true
# Corvax-Wega-Vampire-end
plantMetabolism:
- !type:PlantAdjustWater
amount: 0.5
@@ -24,3 +24,18 @@
amount: 5
- !type:EmptyAllContainers
- !type:DeleteEntity
# Corvax-Wega-Vampire-start
- node: cratecoffinvampire
entity: CrateCoffinVampire
edges:
- to: start
steps:
- tool: Prying
doAfter: 31
completed:
- !type:SpawnPrototype
prototype: MaterialWoodPlank1
amount: 5
- !type:DeleteEntity
# Corvax-Wega-Vampire-end
+5
View File
@@ -145,3 +145,8 @@
id: Rave
kind: source
path: "/Textures/_Wega/Shaders/rave.swsl"
- type: shader
id: NullDamage
kind: source
path: "/Textures/_Wega/Shaders/nulldamage.swsl"
+346 -69
View File
@@ -1,4 +1,4 @@
# Basic
#region Basic
- type: entity
id: ActionVampireSelectClass
parent: BaseAction
@@ -11,6 +11,7 @@
useDelay: 5
- type: InstantAction
event: !type:VampireSelectClassActionEvent
bloodCost: 150
- type: entity
id: ActionDrinkBlood
@@ -39,10 +40,23 @@
icon: { sprite: _Wega/Interface/Actions/actions_vampire.rsi, state: "rejuvenate" }
checkCanInteract: false
checkConsciousness: false
useDelay: 20
useDelay: 5
priority: 1
- type: LimitedCharges
maxCharges: 1
- type: AutoRecharge
rechargeDuration: 20
- type: InstantAction
event: !type:VampireRejuvenateActionEvent
bloodCost: 200
heal:
types:
Asphyxiation: -5
Heat: -2
Poison: -2
healGroups:
groups:
Brute: -2
- type: entity
id: ActionVampireGlare
@@ -57,13 +71,36 @@
sound: !type:SoundPathSpecifier
path: /Audio/_Wega/Effects/Vampire/glare.ogg
checkCanInteract: false
useDelay: 30
useDelay: 3
priority: 1
- type: LimitedCharges
maxCharges: 2
- type: AutoRecharge
rechargeDuration: 30
- type: TargetAction
- type: EntityTargetAction
event: !type:VampireGlareActionEvent
# Hemomancer
# Diablerie
- type: entity
id: ActionVampireSacramentInitiation
parent: BaseAction
name: "[color=purple]Sacrament of Initiation[/color]"
description: "You have reached the peak of your power. For 50 blood, you can raise the dead as free-willed vampires. They are not your thralls."
categories: [ HideSpawnMenu ]
components:
- type: Action
icon: { sprite: _Wega/Interface/Actions/actions_vampire.rsi, state: "revive" }
itemIconStyle: BigAction
useDelay: 300
priority: -1
- type: TargetAction
- type: EntityTargetAction
event: !type:VampireSacramentInitiationActionEvent
bloodCost: 50
#endregion
#region Hemomancer
- type: entity
id: ActionVampireClaws
parent: BaseAction
@@ -77,6 +114,7 @@
useDelay: 15
- type: InstantAction
event: !type:VampireClawsActionEvent
bloodCost: 20
- type: entity
id: ActionVampireBloodTendrils
@@ -94,6 +132,7 @@
range: 10
- type: WorldTargetAction
event: !type:VampireBloodTentacleAction
bloodCost: 10
- type: entity
id: ActionVampireBloodBarrier
@@ -111,6 +150,7 @@
range: 10
- type: WorldTargetAction
event: !type:VampireBloodBarrierActionEvent
bloodCost: 20
- type: entity
id: ActionVampireSanguinePool
@@ -125,6 +165,7 @@
useDelay: 25
- type: InstantAction
event: !type:VampireSanguinePoolActionEvent
bloodCost: 30
- type: entity
id: ActionVampirePredatorSenses
@@ -139,6 +180,8 @@
useDelay: 10
- type: InstantAction
event: !type:VampirePredatorSensesActionEvent
sound:
path: "/Audio/Effects/Fluids/splat.ogg"
- type: entity
id: ActionVampireBloodEruption
@@ -153,6 +196,10 @@
useDelay: 60
- type: InstantAction
event: !type:VampireBloodEruptionActionEvent
bloodCost: 50
damage:
types:
Blunt: 20
- type: entity
id: ActionVampireBloodBringersRite
@@ -167,8 +214,18 @@
useDelay: 10
- type: InstantAction
event: !type:VampireBloodBringersRiteActionEvent
bloodCost: 5
heal:
types:
Heat: -1.5
healGroups:
groups:
Brute: -8
sound:
path: "/Audio/Effects/Fluids/splat.ogg"
#endregion
# Umbrae
#region Umbrae
- type: entity
id: ActionVampireCloakOfDarkness
parent: BaseAction
@@ -182,6 +239,7 @@
useDelay: 2
- type: InstantAction
event: !type:VampireCloakOfDarknessActionEvent
bloodCost: 10
- type: entity
id: ActionVampireShadowSnare
@@ -199,6 +257,7 @@
range: 2
- type: WorldTargetAction
event: !type:VampireShadowSnareActionEvent
bloodCost: 20
- type: entity
id: ActionVampireSoulAnchor
@@ -213,6 +272,7 @@
useDelay: 130
- type: InstantAction
event: !type:VampireSoulAnchorActionEvent
bloodCost: 30
- type: entity
id: ActionVampireDarkPassage
@@ -230,6 +290,7 @@
checkCanAccess: false
- type: WorldTargetAction
event: !type:VampireDarkPassageActionEvent
bloodCost: 30
- type: entity
id: ActionVampireExtinguish
@@ -244,6 +305,10 @@
useDelay: 30
- type: InstantAction
event: !type:VampireExtinguishActionEvent
bloodCost: 20
damage:
types:
Blunt: 5
- type: entity
id: ActionVampireShadowBoxing
@@ -260,6 +325,7 @@
range: 5
- type: EntityTargetAction
event: !type:VampireShadowBoxingActionEvent
bloodCost: 50
- type: entity
id: ActionVampireEternalDarkness
@@ -274,8 +340,13 @@
useDelay: 10
- type: InstantAction
event: !type:VampireEternalDarknessActionEvent
bloodCost: 5
damage:
types:
Blunt: 5
#endregion
# Gargantua
#region Gargantua
- type: entity
id: ActionVampireRejuvenateAdvanced
parent: BaseAction
@@ -287,10 +358,24 @@
icon: { sprite: _Wega/Interface/Actions/actions_vampire.rsi, state: "rejuvenate" }
checkCanInteract: false
checkConsciousness: false
useDelay: 20
useDelay: 5
priority: 1
- type: LimitedCharges
maxCharges: 1
- type: AutoRecharge
rechargeDuration: 20
- type: InstantAction
event: !type:VampireRejuvenateAdvancedActionEvent
event: !type:VampireRejuvenateActionEvent
bloodCost: 200
advanced: true
heal:
types:
Asphyxiation: -5
Heat: -2
Poison: -2
healGroups:
groups:
Brute: -2
- type: entity
id: ActionVampireBloodSwell
@@ -305,6 +390,7 @@
useDelay: 40
- type: InstantAction
event: !type:VampireBloodSwellActionEvent
bloodCost: 30
- type: entity
id: ActionVampireBloodRush
@@ -319,6 +405,7 @@
useDelay: 30
- type: InstantAction
event: !type:VampireBloodRushActionEvent
bloodCost: 15
- type: entity
id: ActionVampireSeismicStomp
@@ -333,6 +420,9 @@
useDelay: 30
- type: InstantAction
event: !type:VampireSeismicStompActionEvent
bloodCost: 25
sound:
path: "/Audio/Effects/Footsteps/largethud.ogg"
- type: entity
id: ActionVampireBloodSwellAdvanced
@@ -346,7 +436,9 @@
itemIconStyle: BigAction
useDelay: 40
- type: InstantAction
event: !type:VampireBloodSwellAdvancedActionEvent
event: !type:VampireBloodSwellActionEvent
bloodCost: 30
advanced: true
- type: entity
id: ActionVampireOverwhelmingForce
@@ -361,6 +453,7 @@
useDelay: 2
- type: InstantAction
event: !type:VampireOverwhelmingForceActionEvent
bloodCost: 80
- type: entity
id: ActionDemonicGrasp
@@ -377,6 +470,7 @@
range: 16
- type: EntityTargetAction
event: !type:VampireDemonicGraspActionEvent
bloodCost: 10
- type: entity
id: ActionVampireCharge
@@ -390,48 +484,22 @@
itemIconStyle: BigAction
useDelay: 30
- type: TargetAction
range: 16
checkCanAccess: false
range: 16
- type: WorldTargetAction
event: !type:VampireChargeActionEvent
bloodCost: 30
components:
- type: DamageOtherOnHit
damage:
types:
Blunt: 50
Structural: 800
sound:
path: "/Audio/Effects/Footsteps/largethud.ogg"
#endregion
# Dantalion
- type: entity
id: ActionMaxThrallCountUpdate1
parent: BaseAction
name: "[color=purple]Upgrade thralls[/color]"
categories: [ HideSpawnMenu ]
components:
- type: Action
icon: { sprite: _Wega/Interface/Actions/actions_vampire.rsi, state: "enthrall" }
itemIconStyle: BigAction
- type: InstantAction
event: !type:MaxThrallCountUpdateEvent
- type: entity
id: ActionMaxThrallCountUpdate2
parent: BaseAction
name: "[color=purple]Upgrade thralls[/color]"
categories: [ HideSpawnMenu ]
components:
- type: Action
icon: { sprite: _Wega/Interface/Actions/actions_vampire.rsi, state: "enthrall" }
itemIconStyle: BigAction
- type: InstantAction
event: !type:MaxThrallCountUpdateEvent
- type: entity
id: ActionMaxThrallCountUpdate3
parent: BaseAction
name: "[color=purple]Upgrade thralls[/color]"
categories: [ HideSpawnMenu ]
components:
- type: Action
icon: { sprite: _Wega/Interface/Actions/actions_vampire.rsi, state: "enthrall" }
itemIconStyle: BigAction
- type: InstantAction
event: !type:MaxThrallCountUpdateEvent
#region Dantalion
- type: entity
id: ActionEnthrall
parent: BaseAction
@@ -446,6 +514,7 @@
- type: TargetAction
- type: EntityTargetAction
event: !type:VampireEnthrallActionEvent
bloodCost: 150
- type: entity
id: ActionCommune
@@ -473,8 +542,10 @@
itemIconStyle: BigAction
useDelay: 10
- type: TargetAction
range: 7
- type: EntityTargetAction
event: !type:VampirePacifyActionEvent
bloodCost: 30
- type: entity
id: ActionSubspaceSwap
@@ -491,20 +562,37 @@
range: 16
- type: EntityTargetAction
event: !type:VampireSubspaceSwapActionEvent
bloodCost: 15
#- type: entity
# id: ActionDeployDecoy
# parent: BaseAction
# name: "[color=purple]Deploy decoy[/color]"
# description: ""
# categories: [ HideSpawnMenu ]
# components:
# - type: Action
# icon: { sprite: _Wega/Interface/Actions/actions_vampire.rsi, state: "decoy" }
# itemIconStyle: BigAction
# useDelay: 20
# - type: InstantAction
# event: !type:VampireDeployDecoyActionEvent
- type: entity
id: ActionDeployDecoy
parent: BaseAction
name: "[color=purple]Deploy decoy[/color]"
description: "It creates a copy of you, and you become invisible."
categories: [ HideSpawnMenu ]
components:
- type: Action
icon: { sprite: _Wega/Interface/Actions/actions_vampire.rsi, state: "decoy" }
itemIconStyle: BigAction
useDelay: 20
- type: InstantAction
event: !type:VampireDeployDecoyActionEvent
bloodCost: 30
components:
- type: HTN
rootTask:
task: SimpleHostileCompound
- type: NPCIgnoringOptimize
- type: NpcFactionMember
factions:
- Passive
- type: NoSlip
- type: Tag
tags:
- CannotSuicide
- DoorBumpOpener
- StunImmune
- SlowImmune
- type: entity
id: ActionRallyThralls
@@ -519,6 +607,7 @@
useDelay: 30
- type: InstantAction
event: !type:VampireRallyThrallsActionEvent
bloodCost: 40
- type: entity
id: ActionBloodBond
@@ -533,6 +622,30 @@
useDelay: 2
- type: InstantAction
event: !type:VampireBloodBondActionEvent
bloodCost: 5
- type: entity
id: ActionVampireThrallHeal
parent: BaseAction
name: "[color=purple]Leader's support[/color]"
description: "Support your thralls with your speak. Heal them by it and make them rise."
categories: [ HideSpawnMenu ]
components:
- type: Action
icon: { sprite: _Wega/Interface/Actions/actions_vampire.rsi, state: "thrallheal" }
itemIconStyle: BigAction
useDelay: 90
- type: InstantAction
event: !type:VampireThrallHealActionEvent
bloodCost: 50
heal:
types:
Heat: -10
Poison: -10
healGroups:
groups:
Airloss: -10
Brute: -10
- type: entity
id: ActionMassHysteria
@@ -547,7 +660,8 @@
useDelay: 60
- type: InstantAction
event: !type:VampireMassHysteriaActionEvent
bloodCost: 40
- type: entity
id: ActionVampirePacifyNearby
parent: BaseAction
@@ -561,17 +675,180 @@
useDelay: 40
- type: InstantAction
event: !type:VampirePacifyNearbyActionEvent
bloodCost: 50
#endregion
#region Bestia
- type: entity
id: ActionVampireThrallHeal
id: ActionVampireCheckTrophies
parent: BaseAction
name: "[color=purple]Leader's support[/color]"
description: "Support your thralls with your speak. Heal them by it and make them rise."
name: "[color=purple]Check Trophies[/color]"
description: "Open the interface to view all the organs you have collected and the bonuses they provide."
categories: [ HideSpawnMenu ]
components:
- type: Action
icon: { sprite: _Wega/Interface/Actions/actions_vampire.rsi, state: "thrallheal" }
itemIconStyle: BigAction
useDelay: 90
icon: { sprite: _Wega/Interface/Actions/actions_vampire.rsi, state: "rush" }
useDelay: 1
- type: InstantAction
event: !type:VampireThrallHealActionEvent
event: !type:VampireCheckTrophiesActionEvent
- type: entity
id: ActionVampireDissect
parent: BaseAction
name: "[color=purple]Dissect[/color]"
description: "Surgically extract organs from a living victim. Each organ gives you unique passive or active bonuses."
categories: [ HideSpawnMenu ]
components:
- type: Action
icon: { sprite: _Wega/Interface/Actions/actions_vampire.rsi, state: "claws" }
itemIconStyle: BigAction
useDelay: 5
- type: TargetAction
- type: EntityTargetAction
event: !type:VampireDissectActionEvent
bloodCost: 10
- type: entity
id: ActionVampireInfectedTrophy
parent: BaseAction
name: "[color=purple]Infected Trophy[/color]"
description: "Launch a projectile that deals damage, infects the target, and explodes on impact. Damage, infection chance, lifetime, and damage radius scale with your collected organs."
categories: [ HideSpawnMenu ]
components:
- type: Action
icon: { sprite: _Wega/Interface/Actions/actions_vampire.rsi, state: "infected_trophy" }
itemIconStyle: BigAction
useDelay: 10
- type: TargetAction
range: 8
- type: EntityTargetAction
event: !type:VampireInfectedTrophyActionEvent
bloodCost: 30
- type: entity
id: ActionVampireLunge
parent: BaseAction
name: "[color=purple]Lunge[/color]"
description: "Launch yourself at a target location. Upon landing, you stun and cause bleeding to all enemies in an area. Range, stun duration, blood stolen, and radius scale with your collected organs."
categories: [ HideSpawnMenu ]
components:
- type: Action
icon: { sprite: _Wega/Interface/Actions/actions_vampire.rsi, state: "charge" }
itemIconStyle: BigAction
useDelay: 5
- type: TargetAction
checkCanAccess: false
range: 11
- type: WorldTargetAction
event: !type:VampireLungeActionEvent
bloodCost: 25
- type: entity
id: ActionVampireMarkPrey
parent: BaseAction
name: "[color=purple]Mark the Prey[/color]"
description: "Mark a target, slowing them and dealing periodic burn damage. Mark duration, burn chance, damage, and range scale with your collected organs."
categories: [ HideSpawnMenu ]
components:
- type: Action
icon: { sprite: _Wega/Interface/Actions/actions_vampire.rsi, state: "predator_sense" }
itemIconStyle: BigAction
useDelay: 20
- type: TargetAction
range: 8
- type: EntityTargetAction
event: !type:VampireMarkPreyActionEvent
bloodCost: 25
- type: entity
id: ActionVampireMetamorphosisBats
parent: BaseAction
name: "[color=purple]Metamorphosis Bats[/color]"
description: "Transform into a swarm of bats. In this form, your health, damage, speed, leeching, and self-healing scale with your collected organs."
categories: [ HideSpawnMenu ]
components:
- type: Action
icon: { sprite: _Wega/Interface/Actions/actions_vampire.rsi, state: "meta_bats" }
useDelay: 30
- type: InstantAction
event: !type:VampireMetamorphosisBatsActionEvent
bloodCost: 45
- type: entity
id: ActionVampireAnabiosis
parent: BaseAction
name: "[color=purple]Anabiosis[/color]"
description: "Retreat into a coffin to regenerate. The coffin protects you and puts you into a deep sleep, restoring your health over time."
categories: [ HideSpawnMenu ]
components:
- type: Action
icon: { sprite: _Wega/Interface/Actions/actions_vampire.rsi, state: "coffin" }
useDelay: 180
- type: InstantAction
event: !type:VampireAnabiosisActionEvent
bloodCost: 70
- type: entity
id: ActionVampireSummonBats
parent: BaseAction
name: "[color=purple]Summon Bats[/color]"
description: "Summon 1-4 bats to fight for you. Each bat's health, damage, speed, and leeching scale with your collected organs."
categories: [ HideSpawnMenu ]
components:
- type: Action
icon: { sprite: _Wega/Interface/Actions/actions_vampire.rsi, state: "bats" }
useDelay: 30
- type: InstantAction
event: !type:VampireSummonBatsActionEvent
bloodCost: 50
sound:
path: /Audio/_Wega/Effects/Vampire/bats_spawn.ogg
- type: entity
id: ActionVampireMetamorphosisHound
parent: BaseAction
name: "[color=purple]Metamorphosis Hound[/color]"
description: "Transform into a powerful hound. In this form, your health, damage, speed, and self-healing scale with your collected organs."
categories: [ HideSpawnMenu ]
components:
- type: Action
icon: { sprite: _Wega/Interface/Actions/actions_vampire.rsi, state: "blood_hound" }
useDelay: 30
- type: InstantAction
event: !type:VampireMetamorphosisHoundActionEvent
bloodCost: 60
# Polymorph
- type: entity
id: ActionVampireResonantShriek
parent: BaseAction
name: "[color=purple]Resonant Shriek[/color]"
description: "Let out a powerful shriek that shatters windows, breaks lights, and stuns everyone nearby. Stun duration and range scale with your collected organs."
categories: [ HideSpawnMenu ]
components:
- type: Action
icon: { sprite: _Wega/Interface/Actions/actions_vampire.rsi, state: "shriek" }
useDelay: 20
- type: InstantAction
event: !type:VampireResonantShriekActionEvent
bloodCost: 40
damage:
types:
Structural: 200
sound:
path: /Audio/_Wega/Effects/Vampire/creepyshriek.ogg
- type: entity
id: ActionVampireLungeFinale
parent: BaseAction
name: "[color=purple]Lunge Finale[/color]"
description: "Perform a series of lunges on random nearby victims. Each 10 trophies grants an additional jump. Range, stun duration, blood stolen, and damage radius scale with your collected organs."
categories: [ HideSpawnMenu ]
components:
- type: Action
icon: { sprite: _Wega/Interface/Actions/actions_vampire.rsi, state: "lunge_finale" }
useDelay: 20
- type: InstantAction
event: !type:VampireLungeFinaleActionEvent
bloodCost: 80
#endregion
@@ -17,11 +17,13 @@
layers:
- map: [ "enum.AlertVisualLayers.Base" ]
- map: [ "enum.VampireVisualLayers.Digit1" ]
offset: 0.2, 0.25
offset: 0.16, 0.25
- map: [ "enum.VampireVisualLayers.Digit2" ]
offset: 0.325, 0.25
offset: 0.28, 0.25
- map: [ "enum.VampireVisualLayers.Digit3" ]
offset: 0.45, 0.25
offset: 0.40, 0.25
- map: [ "enum.VampireVisualLayers.Digit4" ]
offset: 0.52, 0.25
- type: alert
id: AlertBloodRite
@@ -34,7 +36,7 @@
description: alerts-bloodrite-desc
minSeverity: 0
maxSeverity: 1
- type: alert
id: AlertEternalDarkness
icons:
@@ -43,4 +45,4 @@
name: alerts-eternaldarkness-name
description: alerts-eternaldarkness-desc
minSeverity: 0
maxSeverity: 0
maxSeverity: 0
@@ -108,10 +108,6 @@
damage:
types:
Blunt: 5
- type: DamageOtherOnHit
damage:
types:
Blunt: 5
- type: MobThresholds
thresholds:
0: Alive
@@ -4,8 +4,8 @@
Blunt: 0.8
Piercing: 0.8
Cold: 0.2
Heat: 1
Shock: 1
Heat: 1.5
Shock: 1.5
Poison: 0.0
Radiation: 0.0
@@ -15,8 +15,8 @@
Blunt: 0.4
Piercing: 0.4
Cold: 0.1
Heat: 0.5
Shock: 0.5
Heat: 1.2
Shock: 1.2
Poison: 0.0
Radiation: 0.0
@@ -225,4 +225,107 @@
- !type:DiseaseBedrestCure
maxLength: 300
- type: disease
id: VampireGraveFever
name: disease-vampire-grave-fever
infectious: false
cureResist: 0.15
stages:
- 0
- 180
- 600
effects:
- !type:DiseasePopUp
probability: 0.05
message: disease-grave-fever-start
visualType: Small
stages:
- 0
- !type:DiseaseSnough
probability: 0.03
emote: Cough
stages:
- 0
- !type:DiseaseAdjustReagent
probability: 0.2
reagent: Toxin
amount: 0.5
stages:
- 1
- 2
- !type:DiseaseHealthChange
probability: 0.1
damage:
groups:
Toxin: 2
stages:
- 1
- 2
- !type:DiseasePopUp
probability: 0.03
message: disease-grave-fever-weakness
visualType: Medium
stages:
- 1
- 2
- !type:DiseaseGenericStatusEffect
probability: 0.05
key: Stun
component: Stunned
time: 3
type: Add
stages:
- 1
- 2
- !type:DiseaseHealthChange
probability: 0.15
damage:
groups:
Toxin: 3
Airloss: 4
stages:
- 2
- !type:DiseaseHealthChange
probability: 0.05
damage:
types:
Cellular: 1
stages:
- 2
- !type:DiseaseGenericStatusEffect
probability: 0.1
key: Stun
component: Stunned
time: 6
type: Add
stages:
- 2
- !type:DiseaseVomit
probability: 0.02
stages:
- 2
- !type:DiseasePopUp
probability: 0.02
message: disease-grave-fever-terminal
visualType: Large
stages:
- 2
cures:
- !type:DiseaseReagentCure
reagent: Spaceacillin
min: 10
stages:
- 0
- 1
- 2
- !type:DiseaseBedrestCure
maxLength: 90
stages:
- 0
- !type:DiseaseJustWaitCure
maxLength: 1800
stages:
- 0
### Once radiation is refactored I want it to have a small chance of giving you regular cancer
@@ -12,9 +12,11 @@
sprite: _Wega/Effects/bloodtentacle.rsi
layers:
- state: blood_tendril
- type: StunOnContact
- type: SlowdownOnContact
multiplier: 0.4
duration: 6
blacklist:
tags:
components:
- Vampire
- type: Fixtures
fixtures:
@@ -74,11 +76,9 @@
id: BloodBarrier
name: blood barrier
components:
- type: TimedDespawn
lifetime: 25
- type: Tag
tags:
- Wall
- type: Sprite
sprite: _Wega/Effects/bloodbarrier.rsi
state: blood_barrier
- type: Physics
bodyType: Static
- type: Fixtures
@@ -92,12 +92,6 @@
layer:
- WallLayer
- type: Airtight
- type: Sprite
sprite: _Wega/Effects/bloodbarrier.rsi
state: blood_barrier
- type: Icon
sprite: _Wega/Effects/bloodbarrier.rsi
state: blood_barrier
- type: Damageable
damageContainer: Biological
- type: Destructible
@@ -116,6 +110,13 @@
- !type:PlaySoundBehavior
sound:
path: /Audio/Items/drink.ogg
- type: TimedDespawn
lifetime: 25
- type: SpawnOnDespawn
prototype: PuddleBlood
- type: Tag
tags:
- Wall
- type: entity
parent: [ CollideFloorTrap, BaseShadow ]
@@ -129,6 +130,7 @@
lastVisibility: 0.1
- type: TriggerOnCollide
fixtureID: floortrap
maxTriggers: 1
- type: FlashOnTrigger
- type: DamageOnTrigger
targetUser: true
@@ -136,11 +138,14 @@
damage:
types:
Blunt: 20
blacklist:
components:
- Vampire
- type: StunOnContact
fixtureId: floortrap
duration: 3
blacklist:
tags:
components:
- Vampire
- type: entity
@@ -153,7 +158,7 @@
state: beacon
- type: BeaconSoul
- type: TimedDespawn
lifetime: 420.0
lifetime: 250.0 # 130s CD + 120s action
- type: Fixtures
fixtures:
fix1:
@@ -192,30 +197,34 @@
- type: Sprite
sprite: _Wega/Effects/shadowboxing.rsi
state: shadow
- type: Damageable
damageContainer: Inorganic
- type: Destructible
thresholds:
- trigger:
!type:DamageTrigger
damage: 20
behaviors:
- !type:DoActsBehavior
acts: [ "Destruction" ]
- type: MovementSpeedModifier
baseWalkSpeed: 20
baseSprintSpeed: 20
- type: CombatMode
- type: MeleeWeapon
damage:
types:
Blunt: 15
- type: DamageOnHit
damage:
types:
Blunt: 20
- type: NPCMeleeCombat
- type: NPCIgnoringOptimize
- type: NpcFactionMember
factions:
- Vampire
- type: HTN
rootTask:
task: FollowerShadowCompound
task: SimpleHostileCompound
- type: TimedDespawn
lifetime: 0.8
- type: htnCompound
id: FollowerShadowCompound
branches:
- tasks:
- !type:HTNCompoundTask
task: MeleeCombatCompound
- tasks:
- !type:HTNCompoundTask
task: IdleCompound
lifetime: 20.0
@@ -6,4 +6,6 @@
- type: Sprite
layers:
- state: blue
- sprite: Objects/Specific/Chapel/bible.rsi
state: icon
- type: HolyPoint

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