diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs
index 5c1f94f3332..fe679554ae6 100644
--- a/Content.Client/Entry/EntryPoint.cs
+++ b/Content.Client/Entry/EntryPoint.cs
@@ -122,6 +122,19 @@ public override void Init()
_prototypeManager.RegisterIgnore("nukeopsRole");
_prototypeManager.RegisterIgnore("ghostRoleRaffleDecider");
+ //DarkStation
+ _prototypeManager.RegisterIgnore("symptom");
+ _prototypeManager.RegisterIgnore("stationGoal");
+ _prototypeManager.RegisterIgnore("narsiAbilityPrototype");
+ _prototypeManager.RegisterIgnore("narsiRitualCategory");
+ _prototypeManager.RegisterIgnore("narsiRitual");
+ _prototypeManager.RegisterIgnore("diseaseBlacklistPrototype");
+ _prototypeManager.RegisterIgnore("disease");
+ _prototypeManager.RegisterIgnore("diseaseCure");
+ _prototypeManager.RegisterIgnore("diseaseStage");
+ _prototypeManager.RegisterIgnore("SCPStationGoal");
+ _prototypeManager.RegisterIgnore("salaries");
+
_componentFactory.GenerateNetIds();
_adminManager.Initialize();
_screenshotHook.Initialize();
diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml
index aae8785b1fe..8804b5ee33c 100644
--- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml
+++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml
@@ -1,64 +1,65 @@
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
index fd3615d59f5..ae354b209d1 100644
--- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
+++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
@@ -1,20 +1,13 @@
using System.Linq;
using System.Numerics;
-using Content.Client.Message;
+using System.Text;
using Content.Shared.Atmos;
using Content.Client.UserInterface.Controls;
-using Content.Shared.Alert;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
-using Content.Shared.Humanoid;
-using Content.Shared.Humanoid.Prototypes;
using Content.Shared.IdentityManagement;
-using Content.Shared.Inventory;
using Content.Shared.MedicalScanner;
-using Content.Shared.Mobs;
-using Content.Shared.Mobs.Components;
-using Content.Shared.Mobs.Systems;
using Content.Shared.Nutrition.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
@@ -36,6 +29,9 @@ public sealed partial class HealthAnalyzerWindow : FancyWindow
private readonly IPrototypeManager _prototypes;
private readonly IResourceCache _cache;
+ private const int AnalyzerHeight = 430;
+ private const int AnalyzerWidth = 600;
+
public HealthAnalyzerWindow()
{
RobustXamlLoader.Load(this);
@@ -45,10 +41,20 @@ public HealthAnalyzerWindow()
_spriteSystem = _entityManager.System();
_prototypes = dependencies.Resolve();
_cache = dependencies.Resolve();
+ SetupSplitContainer();
+ }
+ private void SetupSplitContainer()
+ {
+ SplitContainer.ResizeMode = SplitContainer.SplitResizeMode.RespectChildrenMinSize;
+ SplitContainer.SplitWidth = 2;
+ SplitContainer.SplitEdgeSeparation = 1f;
+ SplitContainer.StretchDirection = SplitContainer.SplitStretchDirection.TopLeft;
}
public void Populate(HealthAnalyzerScannedUserMessage msg)
{
+ GroupsContainer.RemoveAllChildren();
+
var target = _entityManager.GetEntity(msg.TargetEntity);
if (target == null
@@ -60,109 +66,92 @@ public void Populate(HealthAnalyzerScannedUserMessage msg)
NoPatientDataText.Visible = false;
- // Scan Mode
-
- ScanModeLabel.Text = msg.ScanMode.HasValue
- ? msg.ScanMode.Value
- ? Loc.GetString("health-analyzer-window-scan-mode-active")
- : Loc.GetString("health-analyzer-window-scan-mode-inactive")
- : Loc.GetString("health-analyzer-window-entity-unknown-text");
-
- ScanModeLabel.FontColorOverride = msg.ScanMode.HasValue && msg.ScanMode.Value ? Color.Green : Color.Red;
-
- // Patient Information
-
- SpriteView.SetEntity(target.Value);
- SpriteView.Visible = msg.ScanMode.HasValue && msg.ScanMode.Value;
- NoDataTex.Visible = !SpriteView.Visible;
-
- var name = new FormattedMessage();
- name.PushColor(Color.White);
- name.AddText(_entityManager.HasComponent(target.Value)
- ? Identity.Name(target.Value, _entityManager)
- : Loc.GetString("health-analyzer-window-entity-unknown-text"));
- NameLabel.SetMessage(name);
-
- SpeciesLabel.Text =
- _entityManager.TryGetComponent(target.Value,
- out var humanoidAppearanceComponent)
- ? Loc.GetString(_prototypes.Index(humanoidAppearanceComponent.Species).Name)
- : Loc.GetString("health-analyzer-window-entity-unknown-species-text");
-
- // Basic Diagnostic
-
- TemperatureLabel.Text = !float.IsNaN(msg.Temperature)
- ? $"{msg.Temperature - Atmospherics.T0C:F1} °C ({msg.Temperature:F1} K)"
- : Loc.GetString("health-analyzer-window-entity-unknown-value-text");
-
- BloodLabel.Text = !float.IsNaN(msg.BloodLevel)
- ? $"{msg.BloodLevel * 100:F1} %"
- : Loc.GetString("health-analyzer-window-entity-unknown-value-text");
-
- StatusLabel.Text =
- _entityManager.TryGetComponent(target.Value, out var mobStateComponent)
- ? GetStatus(mobStateComponent.CurrentState)
- : Loc.GetString("health-analyzer-window-entity-unknown-text");
-
- // Total Damage
-
- DamageLabel.Text = damageable.TotalDamage.ToString();
-
- // Alerts
+ string entityName = Loc.GetString("health-analyzer-window-entity-unknown-text");
+ if (_entityManager.HasComponent(target.Value))
+ {
+ entityName = Identity.Name(target.Value, _entityManager);
+ }
- var showAlerts = msg.Unrevivable == true || msg.Bleeding == true;
+ if (msg.ScanMode.HasValue)
+ {
+ ScanModePanel.Visible = true;
+ ScanModeText.Text = Loc.GetString(msg.ScanMode.Value ? "health-analyzer-window-scan-mode-active" : "health-analyzer-window-scan-mode-inactive");
+ ScanModeText.FontColorOverride = msg.ScanMode.Value ? Color.Green : Color.Red;
+ }
+ else
+ {
+ ScanModePanel.Visible = false;
+ }
- AlertsDivider.Visible = showAlerts;
- AlertsContainer.Visible = showAlerts;
+ PatientName.Text = Loc.GetString(
+ "health-analyzer-window-entity-health-text",
+ ("entityName", entityName)
+ );
- if (showAlerts)
- AlertsContainer.DisposeAllChildren();
+ Temperature.Text = Loc.GetString("health-analyzer-window-entity-temperature-text",
+ ("temperature", float.IsNaN(msg.Temperature) ? "N/A" : $"{msg.Temperature - Atmospherics.T0C:F1} °C ({msg.Temperature:F1} K)")
+ );
- if (msg.Unrevivable == true)
- AlertsContainer.AddChild(new RichTextLabel
- {
- Text = Loc.GetString("health-analyzer-window-entity-unrevivable-text"),
- Margin = new Thickness(0, 4),
- MaxWidth = 300
- });
+ BloodLevel.Text = Loc.GetString("health-analyzer-window-entity-blood-level-text",
+ ("bloodLevel", float.IsNaN(msg.BloodLevel) ? "N/A" : $"{msg.BloodLevel * 100:F1} %")
+ );
if (msg.Bleeding == true)
- AlertsContainer.AddChild(new RichTextLabel
- {
- Text = Loc.GetString("health-analyzer-window-entity-bleeding-text"),
- Margin = new Thickness(0, 4),
- MaxWidth = 300
- });
+ {
+ Bleeding.Text = Loc.GetString("health-analyzer-window-entity-bleeding-text");
+ Bleeding.FontColorOverride = Color.Red;
+ }
+ else
+ {
+ Bleeding.Text = string.Empty; // Clear the text
+ }
- // Damage Groups
+ patientDamageAmount.Text = Loc.GetString(
+ "health-analyzer-window-entity-damage-total-text",
+ ("amount", damageable.TotalDamage)
+ );
var damageSortedGroups =
- damageable.DamagePerGroup.OrderByDescending(damage => damage.Value)
+ damageable.DamagePerGroup.OrderBy(damage => damage.Value)
.ToDictionary(x => x.Key, x => x.Value);
-
IReadOnlyDictionary damagePerType = damageable.Damage.DamageDict;
DrawDiagnosticGroups(damageSortedGroups, damagePerType);
+ DrawOrgansState(msg.OrganConditions);
+
+ if (_entityManager.TryGetComponent(target, out HungerComponent? hunger)
+ && hunger.StarvationDamage != null
+ && hunger.CurrentThreshold <= HungerThreshold.Starving)
+ {
+ var box = new Control { Margin = new Thickness(0, 0, 0, 15) };
+
+ box.AddChild(CreateDiagnosticGroupTitle(
+ Loc.GetString("health-analyzer-window-malnutrition"),
+ "malnutrition"));
+
+ GroupsContainer.AddChild(box);
+ }
+
+ SetHeight = AnalyzerHeight;
+ SetWidth = AnalyzerWidth;
}
- private static string GetStatus(MobState mobState)
+ private void DrawOrgansState(Dictionary organs)
{
- return mobState switch
+ var organsState = new StringBuilder("Состояние органов: \n");
+ foreach (var organ in organs)
{
- MobState.Alive => Loc.GetString("health-analyzer-window-entity-alive-text"),
- MobState.Critical => Loc.GetString("health-analyzer-window-entity-critical-text"),
- MobState.Dead => Loc.GetString("health-analyzer-window-entity-dead-text"),
- _ => Loc.GetString("health-analyzer-window-entity-unknown-text"),
- };
+ organsState.Append($"\n{organ.Key}: {organ.Value}\n");
+ }
+ OrganStatus.SetMessage(FormattedMessage.FromMarkup(organsState.ToString()));
}
-
private void DrawDiagnosticGroups(
- Dictionary groups,
- IReadOnlyDictionary damageDict)
+ Dictionary groups, IReadOnlyDictionary damageDict)
{
- GroupsContainer.RemoveAllChildren();
+ HashSet shownTypes = new();
- foreach (var (damageGroupId, damageAmount) in groups)
+ // Show the total damage and type breakdown for each damage group.
+ foreach (var (damageGroupId, damageAmount) in groups.Reverse())
{
if (damageAmount == 0)
continue;
@@ -175,6 +164,7 @@ private void DrawDiagnosticGroups(
var groupContainer = new BoxContainer
{
+ Margin = new Thickness(0, 0, 0, 15),
Align = BoxContainer.AlignMode.Begin,
Orientation = BoxContainer.LayoutOrientation.Vertical,
};
@@ -188,16 +178,23 @@ private void DrawDiagnosticGroups(
foreach (var type in group.DamageTypes)
{
- if (!damageDict.TryGetValue(type, out var typeAmount) || typeAmount <= 0)
- continue;
-
- var damageString = Loc.GetString(
- "health-analyzer-window-damage-type-text",
- ("damageType", _prototypes.Index(type).LocalizedName),
- ("amount", typeAmount)
- );
-
- groupContainer.AddChild(CreateDiagnosticItemLabel(damageString.Insert(0, " · ")));
+ if (damageDict.TryGetValue(type, out var typeAmount) && typeAmount > 0)
+ {
+ // If damage types are allowed to belong to more than one damage group,
+ // they may appear twice here. Mark them as duplicate.
+ if (shownTypes.Contains(type))
+ continue;
+
+ shownTypes.Add(type);
+
+ var damageString = Loc.GetString(
+ "health-analyzer-window-damage-type-text",
+ ("damageType", _prototypes.Index(type).LocalizedName),
+ ("amount", typeAmount)
+ );
+
+ groupContainer.AddChild(CreateDiagnosticItemLabel(damageString.Insert(0, "- ")));
+ }
}
}
}
@@ -220,6 +217,7 @@ private static Label CreateDiagnosticItemLabel(string text)
{
return new Label
{
+ Margin = new Thickness(2, 2),
Text = text,
};
}
@@ -228,13 +226,13 @@ private BoxContainer CreateDiagnosticGroupTitle(string text, string id)
{
var rootContainer = new BoxContainer
{
- Margin = new Thickness(0, 6, 0, 0),
VerticalAlignment = VAlignment.Bottom,
- Orientation = BoxContainer.LayoutOrientation.Horizontal,
+ Orientation = BoxContainer.LayoutOrientation.Horizontal
};
rootContainer.AddChild(new TextureRect
{
+ Margin = new Thickness(0, 3),
SetSize = new Vector2(30, 30),
Texture = GetTexture(id.ToLower())
});
diff --git a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs b/Content.Client/Humanoid/HumanoidAppearanceSystem.cs
index 6eb5dd9ec98..068806823eb 100644
--- a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs
+++ b/Content.Client/Humanoid/HumanoidAppearanceSystem.cs
@@ -48,14 +48,14 @@ private void UpdateLayers(HumanoidAppearanceComponent component, SpriteComponent
{
oldLayers.Remove(key);
if (!component.CustomBaseLayers.ContainsKey(key))
- SetLayerData(component, sprite, key, id, sexMorph: true);
+ SetLayerData(component, sprite, key, id, false, sexMorph: true);
}
// add custom layers
foreach (var (key, info) in component.CustomBaseLayers)
{
oldLayers.Remove(key);
- SetLayerData(component, sprite, key, info.Id, sexMorph: false, color: info.Color);
+ SetLayerData(component, sprite, key, info.Id, false, sexMorph: false, color: info.Color);
}
// hide old layers
@@ -72,6 +72,7 @@ private void SetLayerData(
SpriteComponent sprite,
HumanoidVisualLayers key,
string? protoId,
+ bool ignoreSkinByCustom,
bool sexMorph = false,
Color? color = null)
{
@@ -91,7 +92,7 @@ private void SetLayerData(
var proto = _prototypeManager.Index(protoId);
component.BaseLayers[key] = proto;
- if (proto.MatchSkin)
+ if (proto.MatchSkin && !ignoreSkinByCustom)
layer.Color = component.SkinColor.WithAlpha(proto.LayerAlpha);
if (proto.BaseSprite != null)
diff --git a/Content.Client/Humanoid/HumanoidMarkingModifierWindow.xaml.cs b/Content.Client/Humanoid/HumanoidMarkingModifierWindow.xaml.cs
index 4cde587c58c..86ae3f9f362 100644
--- a/Content.Client/Humanoid/HumanoidMarkingModifierWindow.xaml.cs
+++ b/Content.Client/Humanoid/HumanoidMarkingModifierWindow.xaml.cs
@@ -57,7 +57,7 @@ private void OnStateChanged(HumanoidVisualLayers layer, HumanoidBaseLayerModifie
}
string? state = _protoMan.HasIndex(modifier.Text) ? modifier.Text : null;
- OnLayerInfoModified?.Invoke(layer, new CustomBaseLayerInfo(state, modifier.Color));
+ OnLayerInfoModified?.Invoke(layer, new CustomBaseLayerInfo(state, false, modifier.Color));
}
public void SetState(
MarkingSet markings,
diff --git a/Content.Client/IoC/ClientContentIoC.cs b/Content.Client/IoC/ClientContentIoC.cs
index 370188e3c61..1d3e96651d7 100644
--- a/Content.Client/IoC/ClientContentIoC.cs
+++ b/Content.Client/IoC/ClientContentIoC.cs
@@ -1,3 +1,4 @@
+using Content.Client._c4llv07e.Bridges;
using Content.Client.Administration.Managers;
using Content.Client.Changelog;
using Content.Client.Chat.Managers;
@@ -59,6 +60,8 @@ public static void Register()
collection.Register();
collection.Register();
collection.Register();
+ // Adventure Space
+ collection.RegisterInstance(new StubTargetDollWidgetBridge());
}
}
}
diff --git a/Content.Client/SurveillanceCamera/UI/SurveillanceCameraNavMapControl.cs b/Content.Client/SurveillanceCamera/UI/SurveillanceCameraNavMapControl.cs
new file mode 100644
index 00000000000..bd4f6a7c349
--- /dev/null
+++ b/Content.Client/SurveillanceCamera/UI/SurveillanceCameraNavMapControl.cs
@@ -0,0 +1,13 @@
+using Content.Client.Pinpointer.UI;
+
+namespace Content.Client.SurveillanceCamera.UI;
+
+public sealed partial class SurveillanceCameraNavMapControl : NavMapControl
+{
+ public SurveillanceCameraNavMapControl() : base()
+ {
+ WallColor = new Color(100, 100, 100);
+ TileColor = new(71, 42, 72, 0);
+ BackgroundColor = Color.FromSrgb(TileColor.WithAlpha(BackgroundOpacity));
+ }
+}
diff --git a/Content.Client/UserInterface/Screens/DefaultGameScreen.xaml b/Content.Client/UserInterface/Screens/DefaultGameScreen.xaml
index 54aeffe72c9..a0ebdbe7b79 100644
--- a/Content.Client/UserInterface/Screens/DefaultGameScreen.xaml
+++ b/Content.Client/UserInterface/Screens/DefaultGameScreen.xaml
@@ -1,4 +1,4 @@
-
+
diff --git a/Content.Client/UserInterface/Screens/DefaultGameScreen.xaml.cs b/Content.Client/UserInterface/Screens/DefaultGameScreen.xaml.cs
index 12f8422aeb2..b45ac6491ad 100644
--- a/Content.Client/UserInterface/Screens/DefaultGameScreen.xaml.cs
+++ b/Content.Client/UserInterface/Screens/DefaultGameScreen.xaml.cs
@@ -1,4 +1,5 @@
-using System.Numerics;
+using System.Numerics;
+using Content.Client._Adventure.Medical.Surgery.UI;
using Content.Client.UserInterface.Systems.Chat.Widgets;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
@@ -22,6 +23,7 @@ public DefaultGameScreen()
SetAnchorAndMarginPreset(Hotbar, LayoutPreset.BottomWide, margin: 5);
SetAnchorAndMarginPreset(Chat, LayoutPreset.TopRight, margin: 10);
SetAnchorAndMarginPreset(Alerts, LayoutPreset.TopRight, margin: 10);
+ SetAnchorAndMarginPreset(TargetDoll, LayoutPreset.BottomRight, margin: 70);
Chat.OnResized += ChatOnResized;
Chat.OnChatResizeFinish += ChatOnResizeFinish;
diff --git a/Content.Client/VendingMachines/UI/EconomyVendingMachineMenu.xaml b/Content.Client/VendingMachines/UI/EconomyVendingMachineMenu.xaml
new file mode 100644
index 00000000000..da1acb48f63
--- /dev/null
+++ b/Content.Client/VendingMachines/UI/EconomyVendingMachineMenu.xaml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/VendingMachines/UI/EconomyVendingMachineMenu.xaml.cs b/Content.Client/VendingMachines/UI/EconomyVendingMachineMenu.xaml.cs
new file mode 100644
index 00000000000..926cef4eefe
--- /dev/null
+++ b/Content.Client/VendingMachines/UI/EconomyVendingMachineMenu.xaml.cs
@@ -0,0 +1,151 @@
+using System.Numerics;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.VendingMachines;
+using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.VendingMachines.UI
+{
+ [GenerateTypedNameReferences]
+ public sealed partial class EconomyVendingMachineMenu : FancyWindow
+ {
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+
+ public event Action? OnItemSelected;
+
+ public event Action? OnSelectedItemRequestUpdate;
+ public event Action? OnSearchChanged;
+ public event Action? OnBuyButtonPressed;
+
+ private int _selectedItemIndex = -1;
+
+ public EconomyVendingMachineMenu()
+ {
+ MinSize = new Vector2(500, 500);
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+
+ SearchBar.OnTextChanged += _ =>
+ {
+ OnSearchChanged?.Invoke(SearchBar.Text);
+ };
+
+ VendingContents.OnItemSelected += args =>
+ {
+ OnItemSelected?.Invoke(args);
+ };
+ BuyButton.OnPressed += _ =>
+ {
+ OnBuyButtonPressed?.Invoke(_selectedItemIndex);
+ };
+
+ SplitContainer.State = SplitContainer.SplitState.Auto;
+ SplitContainer.ResizeMode = SplitContainer.SplitResizeMode.NotResizable;
+ SplitContainer.SplitWidth = 2;
+ SplitContainer.SplitEdgeSeparation = 1f;
+ SplitContainer.StretchDirection = SplitContainer.SplitStretchDirection.TopLeft;
+ }
+
+ public void SetSelectedProductState(VendingMachineInventoryEntry selectedProduct, int index)
+ {
+ var spriteSystem = IoCManager.Resolve().GetEntitySystem();
+ (var name, Texture? icon) = GetItemNameAndIcon(selectedProduct.ID, spriteSystem);
+
+ ChosenProductIcon.Texture = icon;
+ ChosenProduct.Text = name;
+ ChosenProductAmount.Text = Loc.GetString("vending-machine-component-product-amount", ("amount", selectedProduct.Amount));
+ ChosenProductPrice.Text = selectedProduct.Price > 0 ? Loc.GetString("vending-machine-component-price", ("price", selectedProduct.Price)) : Loc.GetString("vending-machine-component-price-free");
+
+ ChosenProduct.Visible = true;
+ ChosenProductIcon.Visible = true;
+
+ ChosenProductCountContainer.Visible = selectedProduct.Amount > 0;
+ ChosenProductPriceContainer.Visible = selectedProduct.Amount > 0;
+ ChosenProductEnd.Visible = selectedProduct.Amount <= 0;
+
+ ProductNotSelected.Visible = false;
+
+ BuyButton.Text = selectedProduct.Price > 0 ? Loc.GetString("vending-machine-component-buy") : Loc.GetString("vending-machine-component-get");
+ BuyButton.Visible = selectedProduct.Amount > 0;
+ _selectedItemIndex = index;
+ }
+
+ ///
+ /// Populates the list of available items on the vending machine interface
+ /// and sets icons based on their prototypes
+ ///
+ public void Populate(List inventory, out List filteredInventory,
+ string? filter = null)
+ {
+ filteredInventory = new List();
+
+ if (inventory.Count == 0)
+ {
+ VendingContents.Clear();
+ var outOfStockText = Loc.GetString("vending-machine-component-try-eject-out-of-stock");
+ VendingContents.AddItem(outOfStockText);
+ return;
+ }
+
+ while (inventory.Count != VendingContents.Count)
+ {
+ if (inventory.Count > VendingContents.Count)
+ VendingContents.AddItem(string.Empty);
+ else
+ VendingContents.RemoveAt(VendingContents.Count - 1);
+ }
+
+ var longestEntry = string.Empty;
+ var spriteSystem = IoCManager.Resolve().GetEntitySystem();
+
+ var filterCount = 0;
+ for (var i = 0; i < inventory.Count; i++)
+ {
+ var entry = inventory[i];
+ var vendingItem = VendingContents[i - filterCount];
+ vendingItem.Text = string.Empty;
+ vendingItem.Icon = null;
+
+ (var itemName, Texture? icon) = GetItemNameAndIcon(entry.ID, spriteSystem);
+
+ // search filter
+ if (!string.IsNullOrEmpty(filter) &&
+ !itemName.ToLowerInvariant().Contains(filter.Trim().ToLowerInvariant()))
+ {
+ VendingContents.Remove(vendingItem);
+ filterCount++;
+ continue;
+ }
+
+ if (itemName.Length > longestEntry.Length)
+ longestEntry = itemName;
+
+ vendingItem.Text = $"{itemName}";
+ vendingItem.Icon = icon;
+ filteredInventory.Add(i);
+ }
+ }
+
+ private (string, Texture?) GetItemNameAndIcon(string id, SpriteSystem spriteSystem)
+ {
+ if (_prototypeManager.TryIndex(id, out var prototype))
+ {
+ return (prototype.Name, spriteSystem.GetPrototypeIcon(prototype).Default);
+ }
+
+ return (id, null);
+ }
+
+ public void UpdateSelectedProduct()
+ {
+ if (_selectedItemIndex != -1)
+ {
+ OnSelectedItemRequestUpdate?.Invoke(_selectedItemIndex);
+ }
+ }
+ }
+}
diff --git a/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml b/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml
index 44b1ff95e7f..0619dea28d5 100644
--- a/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml
+++ b/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml
@@ -2,12 +2,18 @@
xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
- xmlns:co="clr-namespace:Content.Client.UserInterface.Controls"
- MinHeight="210">
-
-
-
-
+ xmlns:style="clr-namespace:Content.Client.Stylesheets">
+
+
+
+
+
diff --git a/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml.cs b/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml.cs
index ee7a0e41fae..4633a6dcebb 100644
--- a/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml.cs
+++ b/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml.cs
@@ -1,14 +1,15 @@
using System.Numerics;
+using Content.Client.UserInterface.Controls;
using Content.Shared.VendingMachines;
using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using FancyWindow = Content.Client.UserInterface.Controls.FancyWindow;
-using Robust.Client.UserInterface;
-using Content.Client.UserInterface.Controls;
using Content.Shared.IdentityManagement;
-using Robust.Client.Graphics;
+using Robust.Shared.Timing;
namespace Content.Client.VendingMachines.UI
{
@@ -20,9 +21,8 @@ public sealed partial class VendingMachineMenu : FancyWindow
private readonly Dictionary _dummies = [];
- public event Action? OnItemSelected;
-
- private readonly StyleBoxFlat _styleBox = new() { BackgroundColor = new Color(70, 73, 102) };
+ public event Action? OnItemSelected;
+ public event Action? OnSearchChanged;
public VendingMachineMenu()
{
@@ -30,10 +30,15 @@ public VendingMachineMenu()
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
- VendingContents.SearchBar = SearchBar;
- VendingContents.DataFilterCondition += DataFilterCondition;
- VendingContents.GenerateItem += GenerateButton;
- VendingContents.ItemKeyBindDown += (args, data) => OnItemSelected?.Invoke(args, data);
+ SearchBar.OnTextChanged += _ =>
+ {
+ OnSearchChanged?.Invoke(SearchBar.Text);
+ };
+
+ VendingContents.OnItemSelected += args =>
+ {
+ OnItemSelected?.Invoke(args);
+ };
}
protected override void Dispose(bool disposing)
@@ -52,64 +57,41 @@ protected override void Dispose(bool disposing)
_dummies.Clear();
}
- private bool DataFilterCondition(string filter, ListData data)
- {
- if (data is not VendorItemsListData { ItemText: var text })
- return false;
-
- if (string.IsNullOrEmpty(filter))
- return true;
-
- return text.Contains(filter, StringComparison.CurrentCultureIgnoreCase);
- }
-
- private void GenerateButton(ListData data, ListContainerButton button)
- {
- if (data is not VendorItemsListData { ItemProtoID: var protoID, ItemText: var text })
- return;
-
- button.AddChild(new VendingMachineItem(protoID, text));
-
- button.ToolTip = text;
- button.StyleBoxOverride = _styleBox;
- }
-
///
/// Populates the list of available items on the vending machine interface
/// and sets icons based on their prototypes
///
- public void Populate(List inventory)
+ public void Populate(List inventory, out List filteredInventory, string? filter = null)
{
- if (inventory.Count == 0 && VendingContents.Visible)
- {
- SearchBar.Visible = false;
- VendingContents.Visible = false;
-
- var outOfStockLabel = new Label()
- {
- Text = Loc.GetString("vending-machine-component-try-eject-out-of-stock"),
- Margin = new Thickness(4, 4),
- HorizontalExpand = true,
- VerticalAlignment = VAlignment.Stretch,
- HorizontalAlignment = HAlignment.Center
- };
-
- MainContainer.AddChild(outOfStockLabel);
-
- SetSizeAfterUpdate(outOfStockLabel.Text.Length, 0);
+ filteredInventory = new();
+ if (inventory.Count == 0)
+ {
+ VendingContents.Clear();
+ var outOfStockText = Loc.GetString("vending-machine-component-try-eject-out-of-stock");
+ VendingContents.AddItem(outOfStockText);
+ SetSizeAfterUpdate(outOfStockText.Length, VendingContents.Count);
return;
}
+ while (inventory.Count != VendingContents.Count)
+ {
+ if (inventory.Count > VendingContents.Count)
+ VendingContents.AddItem(string.Empty);
+ else
+ VendingContents.RemoveAt(VendingContents.Count - 1);
+ }
+
var longestEntry = string.Empty;
- var listData = new List();
+ var spriteSystem = IoCManager.Resolve().GetEntitySystem();
+ var filterCount = 0;
for (var i = 0; i < inventory.Count; i++)
{
var entry = inventory[i];
-
- if (!_prototypeManager.TryIndex(entry.ID, out var prototype))
- continue;
+ var vendingItem = VendingContents[i - filterCount];
+ vendingItem.Text = string.Empty;
+ vendingItem.Icon = null;
if (!_dummies.TryGetValue(entry.ID, out var dummy))
{
@@ -118,25 +100,36 @@ public void Populate(List inventory)
}
var itemName = Identity.Name(dummy, _entityManager);
- var itemText = $"{itemName} [{entry.Amount}]";
+ Texture? icon = null;
+ if (_prototypeManager.TryIndex(entry.ID, out var prototype))
+ {
+ icon = spriteSystem.GetPrototypeIcon(prototype).Default;
+ }
- if (itemText.Length > longestEntry.Length)
- longestEntry = itemText;
+ // search filter
+ if (!string.IsNullOrEmpty(filter) &&
+ !itemName.ToLowerInvariant().Contains(filter.Trim().ToLowerInvariant()))
+ {
+ VendingContents.Remove(vendingItem);
+ filterCount++;
+ continue;
+ }
- listData.Add(new VendorItemsListData(prototype.ID, itemText, i));
- }
+ if (itemName.Length > longestEntry.Length)
+ longestEntry = itemName;
- VendingContents.PopulateList(listData);
+ vendingItem.Text = $"{itemName} [{entry.Amount}]";
+ vendingItem.Icon = icon;
+ filteredInventory.Add(i);
+ }
SetSizeAfterUpdate(longestEntry.Length, inventory.Count);
}
private void SetSizeAfterUpdate(int longestEntryLength, int contentCount)
{
- SetSize = new Vector2(Math.Clamp((longestEntryLength + 2) * 12, 250, 400),
+ SetSize = new Vector2(Math.Clamp((longestEntryLength + 2) * 12, 250, 300),
Math.Clamp(contentCount * 50, 150, 350));
}
}
}
-
-public record VendorItemsListData(EntProtoId ItemProtoID, string ItemText, int ItemIndex) : ListData;
diff --git a/Content.Client/VendingMachines/VendingMachineBoundUserInterface.cs b/Content.Client/VendingMachines/VendingMachineBoundUserInterface.cs
index 28b1b25adef..066ccd45ef1 100644
--- a/Content.Client/VendingMachines/VendingMachineBoundUserInterface.cs
+++ b/Content.Client/VendingMachines/VendingMachineBoundUserInterface.cs
@@ -1,20 +1,27 @@
-using Content.Client.UserInterface.Controls;
using Content.Client.VendingMachines.UI;
using Content.Shared.VendingMachines;
-using Robust.Client.UserInterface;
-using Robust.Shared.Input;
+using Robust.Client.UserInterface.Controls;
using System.Linq;
+using Content.Client.UserInterface.Controls;
+using Content.Shared._Adventure.CCVars;
+using Robust.Client.UserInterface;
+using Robust.Shared.Configuration;
namespace Content.Client.VendingMachines
{
public sealed class VendingMachineBoundUserInterface : BoundUserInterface
{
+ [Dependency] private readonly IConfigurationManager _cfg = default!;
+
[ViewVariables]
- private VendingMachineMenu? _menu;
+ private FancyWindow? _menu;
[ViewVariables]
private List _cachedInventory = new();
+ [ViewVariables]
+ private List _cachedFilteredIndex = new();
+
public VendingMachineBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
@@ -23,11 +30,27 @@ protected override void Open()
{
base.Open();
- _menu = this.CreateWindow();
+ var vendingMachineSys = EntMan.System();
+
+ _cachedInventory = vendingMachineSys.GetAllInventory(Owner);
+
+ if (!_cfg.GetCVar(SecretCCVars.EconomyEnabled))
+ {
+ _menu = this.CreateWindow();
+ _menu.Title = EntMan.GetComponent(Owner).EntityName;
+
+ SetupOldVendingMenu((VendingMachineMenu)_menu);
+ }
+ else
+ {
+ _menu = this.CreateWindow();
+ _menu.Title = EntMan.GetComponent(Owner).EntityName;
+
+ SetupNewVendingMenu((EconomyVendingMachineMenu)_menu);
+ }
+
+ _menu.OnClose += Close;
_menu.OpenCenteredLeft();
- _menu.Title = EntMan.GetComponent(Owner).EntityName;
- _menu.OnItemSelected += OnItemSelected;
- Refresh();
}
public void Refresh()
@@ -35,40 +58,127 @@ public void Refresh()
var system = EntMan.System();
_cachedInventory = system.GetAllInventory(Owner);
- _menu?.Populate(_cachedInventory);
+ if (_menu is VendingMachineMenu menu)
+ menu.Populate(_cachedInventory, out _cachedFilteredIndex);
+ else if (_menu is EconomyVendingMachineMenu economyMenu)
+ economyMenu.Populate(_cachedInventory, out _cachedFilteredIndex);
}
- private void OnItemSelected(GUIBoundKeyEventArgs args, ListData data)
+ private void SetupOldVendingMenu(VendingMachineMenu menu)
{
- if (args.Function != EngineKeyFunctions.UIClick)
- return;
+ menu.OnItemSelected += OnItemSelected;
+ menu.OnSearchChanged += OnSearchChanged;
+ menu.Populate(_cachedInventory, out _cachedFilteredIndex);
+ }
+
+ private void SetupNewVendingMenu(EconomyVendingMachineMenu menu)
+ {
+ menu.OnItemSelected += OnItemSelected;
+ menu.OnSearchChanged += OnSearchChanged;
+ menu.OnBuyButtonPressed += OnBuyButtonPressed;
+ menu.OnSelectedItemRequestUpdate += OnSelectedItemRequestUpdate;
+ menu.Populate(_cachedInventory, out _cachedFilteredIndex);
+ }
+
+ private void OnSelectedItemRequestUpdate(int index)
+ {
+ var selected = GetSelectedItem(index);
+ if (selected != null && _menu is EconomyVendingMachineMenu economyMenu)
+ {
+ economyMenu.SetSelectedProductState(selected, index);
+ }
+ }
- if (data is not VendorItemsListData { ItemIndex: var itemIndex })
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ if (state is not VendingMachineInterfaceState newState)
return;
- if (_cachedInventory.Count == 0)
+ _cachedInventory = newState.Inventory;
+
+ switch (_menu)
+ {
+ case EconomyVendingMachineMenu economyMenu:
+ economyMenu.Populate(_cachedInventory, out _cachedFilteredIndex, economyMenu.SearchBar.Text);
+ economyMenu.UpdateSelectedProduct();
+ break;
+ case VendingMachineMenu menu:
+ menu.Populate(_cachedInventory, out _cachedFilteredIndex, menu.SearchBar.Text);
+ break;
+ }
+ }
+
+ private void OnItemSelected(ItemList.ItemListSelectedEventArgs args)
+ {
+ var selectedItem = GetSelectedItem(args.ItemIndex);
+ if (selectedItem == null)
return;
- var selectedItem = _cachedInventory.ElementAtOrDefault(itemIndex);
+ switch (_menu)
+ {
+ case EconomyVendingMachineMenu economyMenu:
+ economyMenu.SetSelectedProductState(selectedItem, args.ItemIndex);
+ break;
+ case VendingMachineMenu:
+ SendMessage(new VendingMachineEjectMessage(selectedItem.Type, selectedItem.ID));
+ break;
+ }
+ }
+ private void OnBuyButtonPressed(int index)
+ {
+ var selectedItem = GetSelectedItem(index);
if (selectedItem == null)
return;
SendMessage(new VendingMachineEjectMessage(selectedItem.Type, selectedItem.ID));
}
+ private VendingMachineInventoryEntry? GetSelectedItem(int index)
+ {
+ return _cachedInventory.Count == 0
+ ? null
+ : _cachedInventory.ElementAtOrDefault(_cachedFilteredIndex.ElementAtOrDefault(index));
+ }
+
+
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
return;
- if (_menu == null)
- return;
+ switch (_menu)
+ {
+ case null:
+ return;
+ case EconomyVendingMachineMenu economyMenu:
+ economyMenu.OnItemSelected -= OnItemSelected;
+ break;
+ case VendingMachineMenu menu:
+ menu.OnItemSelected -= OnItemSelected;
+ break;
+ }
- _menu.OnItemSelected -= OnItemSelected;
_menu.OnClose -= Close;
_menu.Dispose();
}
+
+ private void OnSearchChanged(string? filter)
+ {
+ switch (_menu)
+ {
+ case null:
+ return;
+ case EconomyVendingMachineMenu economyMenu:
+ economyMenu.Populate(_cachedInventory, out _cachedFilteredIndex, filter);
+ break;
+ case VendingMachineMenu menu:
+ menu.Populate(_cachedInventory, out _cachedFilteredIndex, filter);
+ break;
+ }
+ }
}
}
diff --git a/Content.Client/_Adventure/CollectionUtils.cs b/Content.Client/_Adventure/CollectionUtils.cs
new file mode 100644
index 00000000000..db352ec24e8
--- /dev/null
+++ b/Content.Client/_Adventure/CollectionUtils.cs
@@ -0,0 +1,11 @@
+using System.Linq;
+
+namespace Content.Client._Adventure;
+
+public static class CollectionUtils
+{
+ public static IEnumerable<(T item, int index)> WithIndex(this IEnumerable source)
+ {
+ return source.Select((item, index) => (item, index));
+ }
+}
diff --git a/Content.Client/_Adventure/DarkStation/DarkForces/Desecrated/PontificVisualLayers.cs b/Content.Client/_Adventure/DarkStation/DarkForces/Desecrated/PontificVisualLayers.cs
new file mode 100644
index 00000000000..7d062a50cb2
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/DarkForces/Desecrated/PontificVisualLayers.cs
@@ -0,0 +1,9 @@
+namespace Content.Client._Adventure.DarkForces.Desecrated;
+
+public enum PontificVisualLayers : byte
+{
+ Base,
+ Dead,
+ Flame,
+ Prayer,
+}
diff --git a/Content.Client/_Adventure/DarkStation/DarkForces/Narsi/NarsiRuneVisualLayers.cs b/Content.Client/_Adventure/DarkStation/DarkForces/Narsi/NarsiRuneVisualLayers.cs
new file mode 100644
index 00000000000..25810805872
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/DarkForces/Narsi/NarsiRuneVisualLayers.cs
@@ -0,0 +1,7 @@
+namespace Content.Client._Adventure.DarkForces.Narsi;
+
+public enum NarsiRuneVisualLayers : byte
+{
+ Idle,
+ Active,
+}
diff --git a/Content.Client/_Adventure/DarkStation/DarkForces/Ratvar/Enchantment/RatvarEnchantmentBUI.cs b/Content.Client/_Adventure/DarkStation/DarkForces/Ratvar/Enchantment/RatvarEnchantmentBUI.cs
new file mode 100644
index 00000000000..d87a3f20e72
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/DarkForces/Ratvar/Enchantment/RatvarEnchantmentBUI.cs
@@ -0,0 +1,54 @@
+using Content.Shared._Adventure.DarkForces.Ratvar.UI;
+using Robust.Client.Graphics;
+using Robust.Client.Input;
+
+namespace Content.Client._Adventure.DarkForces.Ratvar.Enchantment;
+
+public sealed class RatvarEnchantmentBUI : BoundUserInterface
+{
+ [Dependency] private readonly IClyde _displayManager = default!;
+ [Dependency] private readonly IInputManager _inputManager = default!;
+
+ private RatvarEnchantmentMenu? _menu;
+
+ public RatvarEnchantmentBUI(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ IoCManager.InjectDependencies(this);
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _menu = new RatvarEnchantmentMenu(this);
+ _menu.OnClose += Close;
+
+ var vpSize = _displayManager.ScreenSize;
+ _menu.OpenCenteredAt(_inputManager.MouseScreenPosition.Position / vpSize);
+ }
+
+ public void Select(string id, string visuals)
+ {
+ SendMessage(new RatvarEnchantmentSelectedMessage(id, visuals));
+ Close();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ if (!disposing)
+ return;
+
+ _menu?.Dispose();
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ if (state is not RatvarEnchantmentBUIState castState)
+ return;
+
+ _menu?.PopulateRadial(castState.Models);
+ }
+}
diff --git a/Content.Client/_Adventure/DarkStation/DarkForces/Ratvar/Enchantment/RatvarEnchantmentMenu.xaml b/Content.Client/_Adventure/DarkStation/DarkForces/Ratvar/Enchantment/RatvarEnchantmentMenu.xaml
new file mode 100644
index 00000000000..d7407115e24
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/DarkForces/Ratvar/Enchantment/RatvarEnchantmentMenu.xaml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/Content.Client/_Adventure/DarkStation/DarkForces/Ratvar/Enchantment/RatvarEnchantmentMenu.xaml.cs b/Content.Client/_Adventure/DarkStation/DarkForces/Ratvar/Enchantment/RatvarEnchantmentMenu.xaml.cs
new file mode 100644
index 00000000000..b258c07074a
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/DarkForces/Ratvar/Enchantment/RatvarEnchantmentMenu.xaml.cs
@@ -0,0 +1,53 @@
+using System.Numerics;
+using Content.Client.UserInterface.Controls;
+using Content.Shared._Adventure.DarkForces.Ratvar.UI;
+using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client._Adventure.DarkForces.Ratvar.Enchantment;
+
+[GenerateTypedNameReferences]
+public sealed partial class RatvarEnchantmentMenu : RadialMenu
+{
+ [Dependency] private readonly EntityManager _entManager = default!;
+
+ private readonly SpriteSystem _spriteSystem;
+ private readonly RatvarEnchantmentBUI _bui;
+
+ public RatvarEnchantmentMenu(RatvarEnchantmentBUI bui)
+ {
+ IoCManager.InjectDependencies(this);
+ RobustXamlLoader.Load(this);
+
+ _spriteSystem = _entManager.System();
+ _bui = bui;
+ }
+
+ public void PopulateRadial(IReadOnlyCollection models)
+ {
+ foreach (var model in models)
+ {
+ var button = new RadialMenuTextureButton
+ {
+ StyleClasses = { "RadialMenuButton" },
+ SetSize = new Vector2(64f, 64f),
+ ToolTip = model.Name,
+ };
+
+ var texture = new TextureRect
+ {
+ VerticalAlignment = VAlignment.Center,
+ HorizontalAlignment = HAlignment.Center,
+ Texture = _spriteSystem.Frame0(model.Icon),
+ TextureScale = new Vector2(2f, 2f),
+ };
+
+ button.OnPressed += _ => _bui.Select(model.Id, model.Visuals);
+ button.AddChild(texture);
+
+ Main.AddChild(button);
+ }
+ }
+}
diff --git a/Content.Client/_Adventure/DarkStation/DarkForces/Ratvar/Enchantment/Visuals/RatvarEnchantableVisualSystem.cs b/Content.Client/_Adventure/DarkStation/DarkForces/Ratvar/Enchantment/Visuals/RatvarEnchantableVisualSystem.cs
new file mode 100644
index 00000000000..c1ca9ecee2e
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/DarkForces/Ratvar/Enchantment/Visuals/RatvarEnchantableVisualSystem.cs
@@ -0,0 +1,100 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using Content.Client.Items.Systems;
+using Content.Shared._Adventure.DarkForces.Ratvar.Righteous.Abilities;
+using Content.Shared.Hands;
+using Content.Shared.Item;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.ResourceManagement;
+using Robust.Shared.Serialization.TypeSerializers.Implementations;
+
+namespace Content.Client._Adventure.DarkForces.Ratvar.Enchantment.Visuals;
+
+public sealed class RatvarEnchantableVisualSystem : EntitySystem
+{
+ [Dependency] private readonly ItemSystem _itemSystem = default!;
+ [Dependency] private readonly IResourceCache _resCache = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnGetVisuals);
+ SubscribeLocalEvent(OnAppearanceChangeEvent);
+ }
+
+ private void OnAppearanceChangeEvent(EntityUid uid,
+ RatvarEnchantmentableComponent component,
+ AppearanceChangeEvent args)
+ {
+ _itemSystem.VisualsChanged(uid);
+ }
+
+ private void OnGetVisuals(EntityUid uid, RatvarEnchantmentableComponent component, GetInhandVisualsEvent args)
+ {
+ if (!TryComp(uid, out var itemComponent) ||
+ !TryComp(uid, out var spriteComponent))
+ return;
+
+ var layersIndex = spriteComponent.LayerMapReserveBlank("overlay");
+ if (!spriteComponent.TryGetLayer(layersIndex, out var overlayLayer))
+ return;
+
+ var state = overlayLayer.State.Name;
+ if (state == null || !overlayLayer.Visible)
+ return;
+
+ var defaultKey = $"inhand-{args.Location.ToString().ToLowerInvariant()}";
+ var overlayKey = defaultKey + $"-{state.Split('-').Last()}";
+
+ if (!TryGetDefaultVisuals(uid, itemComponent, overlayKey, out var layers))
+ return;
+
+ var i = 0;
+ foreach (var layer in layers)
+ {
+ var key = layer.MapKeys?.FirstOrDefault();
+ if (key == null)
+ {
+ key = i == 0 ? defaultKey : $"{overlayKey}-{i}";
+ i++;
+ }
+
+ args.Layers.Add((key, layer));
+ }
+ }
+
+ private bool TryGetDefaultVisuals(EntityUid uid,
+ ItemComponent item,
+ string defaultKey,
+ [NotNullWhen(true)] out List? result)
+ {
+ result = null;
+
+ RSI? rsi = null;
+
+ if (item.RsiPath != null)
+ rsi = _resCache.GetResource(SpriteSpecifierSerializer.TextureRoot / item.RsiPath).RSI;
+ else if (TryComp(uid, out SpriteComponent? sprite))
+ rsi = sprite.BaseRSI;
+
+ if (rsi == null)
+ return false;
+
+ var state = item.HeldPrefix == null ? defaultKey : $"{item.HeldPrefix}-{defaultKey}";
+
+ if (!rsi.TryGetState(state, out _))
+ return false;
+
+ var layer = new PrototypeLayerData
+ {
+ RsiPath = rsi.Path.ToString(),
+ State = state,
+ MapKeys = new HashSet { state },
+ };
+
+ result = new List { layer };
+ return true;
+ }
+}
diff --git a/Content.Client/_Adventure/DarkStation/DarkForces/Ratvar/Midas/RatvarMidasTouchBUI.cs b/Content.Client/_Adventure/DarkStation/DarkForces/Ratvar/Midas/RatvarMidasTouchBUI.cs
new file mode 100644
index 00000000000..4e250cb1920
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/DarkForces/Ratvar/Midas/RatvarMidasTouchBUI.cs
@@ -0,0 +1,54 @@
+using Content.Shared._Adventure.DarkForces.Ratvar.UI;
+using Robust.Client.Graphics;
+using Robust.Client.Input;
+
+namespace Content.Client._Adventure.DarkForces.Ratvar.Midas;
+
+public sealed class RatvarMidasTouchBUI : BoundUserInterface
+{
+ [Dependency] private readonly IClyde _displayManager = default!;
+ [Dependency] private readonly IInputManager _inputManager = default!;
+
+ private RatvarMidasTouchMenu? _menu;
+
+ public RatvarMidasTouchBUI(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ IoCManager.InjectDependencies(this);
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _menu = new RatvarMidasTouchMenu(this);
+ _menu.OnClose += Close;
+
+ var vpSize = _displayManager.ScreenSize;
+ _menu.OpenCenteredAt(_inputManager.MouseScreenPosition.Position / vpSize);
+ }
+
+ public void Select(string id)
+ {
+ SendMessage(new RatvarTouchSelectedMessage(id));
+ Close();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ if (!disposing)
+ return;
+
+ _menu?.Dispose();
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ if (state is not RatvarMidasTouchBUIState castState)
+ return;
+
+ _menu?.Populate(castState.Ids);
+ }
+}
diff --git a/Content.Client/_Adventure/DarkStation/DarkForces/Ratvar/Midas/RatvarMidasTouchMenu.xaml b/Content.Client/_Adventure/DarkStation/DarkForces/Ratvar/Midas/RatvarMidasTouchMenu.xaml
new file mode 100644
index 00000000000..8fd9cd7ee40
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/DarkForces/Ratvar/Midas/RatvarMidasTouchMenu.xaml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/Content.Client/_Adventure/DarkStation/DarkForces/Ratvar/Midas/RatvarMidasTouchMenu.xaml.cs b/Content.Client/_Adventure/DarkStation/DarkForces/Ratvar/Midas/RatvarMidasTouchMenu.xaml.cs
new file mode 100644
index 00000000000..49ee563ac53
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/DarkForces/Ratvar/Midas/RatvarMidasTouchMenu.xaml.cs
@@ -0,0 +1,58 @@
+using System.Numerics;
+using Content.Client.UserInterface.Controls;
+using Content.Shared._Adventure.DarkForces.Ratvar.Prototypes;
+using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client._Adventure.DarkForces.Ratvar.Midas;
+
+[GenerateTypedNameReferences]
+public sealed partial class RatvarMidasTouchMenu : RadialMenu
+{
+ [Dependency] private readonly IEntityManager _entManager = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+
+ private readonly SpriteSystem _spriteSystem;
+ private readonly RatvarMidasTouchBUI _bui;
+
+ public RatvarMidasTouchMenu(RatvarMidasTouchBUI bui)
+ {
+ IoCManager.InjectDependencies(this);
+ RobustXamlLoader.Load(this);
+
+ _bui = bui;
+ _spriteSystem = _entManager.System();
+ }
+
+ public void Populate(IReadOnlyCollection ids)
+ {
+ foreach (var id in ids)
+ {
+ if (!_prototypeManager.TryIndex(id, out var prototype))
+ continue;
+
+ var button = new RadialMenuTextureButton
+ {
+ StyleClasses = { "RadialMenuButton" },
+ SetSize = new Vector2(64f, 64f),
+ ToolTip = prototype.Name,
+ };
+
+ var texture = new TextureRect
+ {
+ VerticalAlignment = VAlignment.Center,
+ HorizontalAlignment = HAlignment.Center,
+ Texture = _spriteSystem.Frame0(prototype.Icon!),
+ TextureScale = new Vector2(2f, 2f),
+ };
+
+ button.OnPressed += _ => _bui.Select(id);
+ button.AddChild(texture);
+
+ Main.AddChild(button);
+ }
+ }
+}
diff --git a/Content.Client/_Adventure/DarkStation/DarkForces/Ratvar/Structures/Altar/AltarGlowSystem.cs b/Content.Client/_Adventure/DarkStation/DarkForces/Ratvar/Structures/Altar/AltarGlowSystem.cs
new file mode 100644
index 00000000000..151bce51b20
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/DarkForces/Ratvar/Structures/Altar/AltarGlowSystem.cs
@@ -0,0 +1,46 @@
+using Content.Shared._Adventure.DarkForces.Ratvar.Structures.Altar;
+using Robust.Client.Animations;
+using Robust.Client.GameObjects;
+
+namespace Content.Client._Adventure.DarkForces.Ratvar.Structures.Altar;
+
+public sealed class AltarGlowSystem : EntitySystem
+{
+ private const string AltarGlowAnimationKey = "ratvarAltarGlow";
+ private const float RevealAlpha = 0.8f;
+ private const double AnimationLength = 0.7;
+ [Dependency] private readonly AnimationPlayerSystem _animation = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnComponentInit);
+ }
+
+ private void OnComponentInit(EntityUid uid, RatvarAltarGlowComponent component, ComponentInit args)
+ {
+ if (!TryComp(uid, out var sprite))
+ return;
+
+ _animation.Play(uid,
+ new Animation
+ {
+ Length = TimeSpan.FromSeconds(AnimationLength),
+ AnimationTracks =
+ {
+ new AnimationTrackComponentProperty
+ {
+ ComponentType = typeof(SpriteComponent),
+ Property = nameof(SpriteComponent.Color),
+ KeyFrames =
+ {
+ new AnimationTrackProperty.KeyFrame(sprite.Color.WithAlpha(0f), 0f),
+ new AnimationTrackProperty.KeyFrame(sprite.Color.WithAlpha(RevealAlpha),
+ (float)AnimationLength),
+ },
+ },
+ },
+ },
+ AltarGlowAnimationKey);
+ }
+}
diff --git a/Content.Client/_Adventure/DarkStation/DarkForces/Ratvar/Structures/RatvarWorkshopBUI.cs b/Content.Client/_Adventure/DarkStation/DarkForces/Ratvar/Structures/RatvarWorkshopBUI.cs
new file mode 100644
index 00000000000..0cb983410b6
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/DarkForces/Ratvar/Structures/RatvarWorkshopBUI.cs
@@ -0,0 +1,42 @@
+using Content.Shared._Adventure.DarkForces.Ratvar.UI;
+
+namespace Content.Client._Adventure.DarkForces.Ratvar.Structures;
+
+public sealed class RatvarWorkshopBUI : BoundUserInterface
+{
+ private readonly RatvarWorkshopWindow _window = new();
+
+ public RatvarWorkshopBUI(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ _window.OnCraftPressed += (entityProduce, brass, power, craftTime) =>
+ SendMessage(new RatvarWorkshopCraftSelected(entityProduce, brass, power, craftTime));
+ _window.OnClose += Close;
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ if (State != null)
+ UpdateState(State);
+
+ _window.OpenCentered();
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ if (state is not RatvarWorkshopUIState newState)
+ return;
+
+ _window.UpdateState(newState);
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ if (disposing)
+ _window.Dispose();
+ }
+}
diff --git a/Content.Client/_Adventure/DarkStation/DarkForces/Ratvar/Structures/RatvarWorkshopWindow.xaml b/Content.Client/_Adventure/DarkStation/DarkForces/Ratvar/Structures/RatvarWorkshopWindow.xaml
new file mode 100644
index 00000000000..ea5ebf367d8
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/DarkForces/Ratvar/Structures/RatvarWorkshopWindow.xaml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_Adventure/DarkStation/DarkForces/Ratvar/Structures/RatvarWorkshopWindow.xaml.cs b/Content.Client/_Adventure/DarkStation/DarkForces/Ratvar/Structures/RatvarWorkshopWindow.xaml.cs
new file mode 100644
index 00000000000..04a1baa8b63
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/DarkForces/Ratvar/Structures/RatvarWorkshopWindow.xaml.cs
@@ -0,0 +1,129 @@
+using System.Linq;
+using System.Numerics;
+using Content.Client.Stylesheets;
+using Content.Client.UserInterface.Controls;
+using Content.Shared._Adventure.DarkForces.Ratvar.Prototypes;
+using Content.Shared._Adventure.DarkForces.Ratvar.UI;
+using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
+
+namespace Content.Client._Adventure.DarkForces.Ratvar.Structures;
+
+[GenerateTypedNameReferences]
+public sealed partial class RatvarWorkshopWindow : FancyWindow
+{
+ private readonly HashSet _categories;
+ private readonly Color _categoryBackgroundColor;
+ private readonly Thickness _defaultMargin;
+ [Dependency] private readonly IEntitySystemManager _entitySystem = default!;
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+
+ private readonly SpriteSystem _spriteSystem;
+
+ public Action? OnCraftPressed;
+
+ public RatvarWorkshopWindow()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+
+ _spriteSystem = _entitySystem.GetEntitySystem();
+ _categories = _prototype.EnumeratePrototypes().ToHashSet();
+ _defaultMargin = new Thickness(8, 8, 8, 8);
+
+ Color.TryParse("#25252a80", out _categoryBackgroundColor);
+ }
+
+ public void UpdateState(RatvarWorkshopUIState state)
+ {
+ BrassCount.Text = $"{state.Brass}";
+ PowerCount.Text = $"{state.Power}";
+ ProgressState.Text = state.InProgress ? "В работе" : "Готово к работе";
+
+ foreach (var child in CraftList.Children.ToArray())
+ {
+ child.Dispose();
+ }
+
+ CreateReceipts(state.Brass, state.Power, state.InProgress);
+ }
+
+ private void CreateReceipts(int brass, int power, bool inProgress)
+ {
+ foreach (var category in _categories)
+ {
+ var container = new BoxContainer
+ {
+ Orientation = BoxContainer.LayoutOrientation.Vertical,
+ };
+
+ var categoryLabel = new Label
+ {
+ Text = $"{category.Name}",
+ Margin = _defaultMargin,
+ };
+ container.AddChild(categoryLabel);
+
+ var panelContainer = new PanelContainer
+ {
+ PanelOverride = new StyleBoxFlat
+ {
+ BackgroundColor = _categoryBackgroundColor,
+ },
+ Margin = _defaultMargin,
+ };
+
+ panelContainer.AddChild(container);
+
+ foreach (var receiptProtId in category.Receipts)
+ {
+ var receipt = _prototype.Index(receiptProtId);
+ var receiptContainer = new BoxContainer
+ {
+ Orientation = BoxContainer.LayoutOrientation.Horizontal,
+ };
+ var textureRect = new TextureRect
+ {
+ Texture = _spriteSystem.Frame0(receipt.Icon),
+ TextureScale = new Vector2(1.5f, 1.5f),
+ Stretch = TextureRect.StretchMode.KeepCentered,
+ };
+ var button = new Button
+ {
+ Text = receipt.Name,
+ MaxHeight = 28f,
+ Margin = _defaultMargin,
+ };
+ var requirementsLabel = new Label
+ {
+ Text = $"Латунь: {receipt.BrassCost}, Мощность: {receipt.PowerCost}",
+ StyleClasses = { StyleBase.StyleClassLabelSubText },
+ Align = Label.AlignMode.Right,
+ Margin = _defaultMargin,
+ HorizontalExpand = true,
+ };
+
+ button.Disabled = inProgress || receipt.BrassCost > brass || receipt.PowerCost > power;
+ button.OnPressed += _ =>
+ OnCraftPressed?.Invoke(receipt.EntityProduce,
+ receipt.BrassCost,
+ receipt.PowerCost,
+ receipt.CraftingTime);
+
+ receiptContainer.AddChild(textureRect);
+ receiptContainer.AddChild(button);
+ receiptContainer.AddChild(requirementsLabel);
+
+ container.AddChild(receiptContainer);
+ }
+
+ CraftList.AddChild(panelContainer);
+ }
+ }
+}
diff --git a/Content.Client/_Adventure/DarkStation/DarkForces/Vampire/Overlay/VampireIconsSystem.cs b/Content.Client/_Adventure/DarkStation/DarkForces/Vampire/Overlay/VampireIconsSystem.cs
new file mode 100644
index 00000000000..b9233851b36
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/DarkForces/Vampire/Overlay/VampireIconsSystem.cs
@@ -0,0 +1,30 @@
+using Content.Shared._Adventure.DarkForces.Vampire.Components;
+using Content.Shared.StatusIcon.Components;
+using Robust.Shared.Prototypes;
+using VampireComponent = Content.Shared._Adventure.DarkForces.Vampire.Components.VampireComponent;
+
+namespace Content.Client._Adventure.DarkForces.Vampire.Overlay;
+
+public sealed class VampireIconsSystem : EntitySystem
+{
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnGetStatusIcon);
+ SubscribeLocalEvent(OnGetTrallStatusIcon);
+ }
+
+ private void OnGetTrallStatusIcon(Entity ent, ref GetStatusIconsEvent args)
+ {
+ var icon = _prototype.Index(ent.Comp.StatusIcon);
+ args.StatusIcons.Add(icon);
+ }
+
+ private void OnGetStatusIcon(Entity ent, ref GetStatusIconsEvent args)
+ {
+ var icon = _prototype.Index(ent.Comp.StatusIcon);
+ args.StatusIcons.Add(icon);
+ }
+}
diff --git a/Content.Client/_Adventure/DarkStation/DarkForces/Vampire/VampireAbilitiesEUI.cs b/Content.Client/_Adventure/DarkStation/DarkForces/Vampire/VampireAbilitiesEUI.cs
new file mode 100644
index 00000000000..aaa247e735b
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/DarkForces/Vampire/VampireAbilitiesEUI.cs
@@ -0,0 +1,53 @@
+using Content.Client.Eui;
+using Content.Shared._Adventure.DarkForces.Vampire;
+using Content.Shared.Eui;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client._Adventure.DarkForces.Vampire;
+
+public sealed class VampireAbilitiesEUI : BaseEui
+{
+ private readonly VampireAbilitiesWindow _window;
+ private NetEntity _netEntity = NetEntity.Invalid;
+
+ public VampireAbilitiesEUI()
+ {
+ _window = new VampireAbilitiesWindow();
+ _window.OnClose += OnClosed;
+ _window.OnLearnButtonPressed += OnAbilitySelected;
+ }
+
+ public override void Opened()
+ {
+ base.Opened();
+
+ _window.OpenCentered();
+ }
+
+ public override void Closed()
+ {
+ base.Closed();
+ _window.Close();
+ }
+
+ public override void HandleMessage(EuiMessageBase msg)
+ {
+ base.HandleMessage(msg);
+
+ if (msg is not VampireAbilitiesState data)
+ return;
+
+ _netEntity = data.NetEntity;
+ _window.UpdateState(data);
+ }
+
+ private void OnClosed()
+ {
+ SendMessage(new CloseEuiMessage());
+ }
+
+ private void OnAbilitySelected(EntProtoId? replaceId, string actionId, int bloodRequired)
+ {
+ SendMessage(new VampireAbilitySelected(_netEntity, replaceId, actionId, bloodRequired));
+ }
+}
diff --git a/Content.Client/_Adventure/DarkStation/DarkForces/Vampire/VampireAbilitiesWindow.xaml b/Content.Client/_Adventure/DarkStation/DarkForces/Vampire/VampireAbilitiesWindow.xaml
new file mode 100644
index 00000000000..726d3e28102
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/DarkForces/Vampire/VampireAbilitiesWindow.xaml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_Adventure/DarkStation/DarkForces/Vampire/VampireAbilitiesWindow.xaml.cs b/Content.Client/_Adventure/DarkStation/DarkForces/Vampire/VampireAbilitiesWindow.xaml.cs
new file mode 100644
index 00000000000..d27da5e8538
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/DarkForces/Vampire/VampireAbilitiesWindow.xaml.cs
@@ -0,0 +1,197 @@
+using System.Numerics;
+using Content.Client.Administration.UI.CustomControls;
+using Content.Client.Stylesheets;
+using Content.Shared._Adventure.DarkForces.Vampire;
+using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Client._Adventure.DarkForces.Vampire;
+
+[GenerateTypedNameReferences]
+public sealed partial class VampireAbilitiesWindow : DefaultWindow
+{
+ private readonly Thickness _defaultMargin;
+ [Dependency] private readonly IEntityManager _entityManager = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ private readonly IEnumerable _prototypes;
+
+ private readonly SpriteSystem _spriteSystem;
+
+ public Action? OnLearnButtonPressed;
+
+ public VampireAbilitiesWindow()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+
+ _prototypes = _prototypeManager.EnumeratePrototypes();
+ _spriteSystem = _entityManager.System();
+
+ _defaultMargin = new Thickness(8, 8, 8, 8);
+
+ Title = Loc.GetString("vampire-abilities-title");
+ }
+
+ public void UpdateState(VampireAbilitiesState state)
+ {
+ StoreListingsContainer.Children.Clear();
+
+ CurrentBloodLevel.SetMessage(state.CurrentBlood.ToString());
+ TotalBloodLevel.SetMessage(state.TotalBlood.ToString());
+
+ foreach (var (prototype, index) in _prototypes.WithIndex())
+ {
+ if (state.OpenedAbilities.Contains(prototype.ActionId))
+ continue;
+
+ var panel = GetPanelContainer(index);
+
+ var mainContainer = GetBoxContainer(BoxContainer.LayoutOrientation.Vertical);
+ var innerContainer = GetBoxContainer(BoxContainer.LayoutOrientation.Vertical);
+
+ panel.AddChild(mainContainer);
+
+ mainContainer.AddChild(GetHeader(prototype.Icon, prototype.Name, prototype.BloodCost));
+ mainContainer.AddChild(GetHorizontalDivider());
+ mainContainer.AddChild(innerContainer);
+
+ var button = GetButton(Loc.GetString("vampire-abilities-learn"), state.CurrentBlood >= prototype.BloodCost);
+ button.OnPressed += _ =>
+ OnLearnButtonPressed?.Invoke(prototype.ReplaceId, prototype.ActionId, prototype.BloodCost);
+
+ innerContainer.AddChild(GetDescription(prototype.Description));
+ innerContainer.AddChild(button);
+
+ StoreListingsContainer.AddChild(panel);
+ }
+ }
+
+ private Control GetHorizontalDivider()
+ {
+ return new HSeparator
+ {
+ Margin = _defaultMargin,
+ };
+ }
+
+
+ private BoxContainer GetHeader(SpriteSpecifier icon, string name, int bloodCost)
+ {
+ var mainContainer = GetBoxContainer(BoxContainer.LayoutOrientation.Horizontal);
+ var textContainer = GetBoxContainer(BoxContainer.LayoutOrientation.Vertical);
+
+ mainContainer.AddChild(GetTextureRect(icon));
+ mainContainer.AddChild(textContainer);
+
+ textContainer.AddChild(GetTitle(name));
+ textContainer.AddChild(GetBloodCost(bloodCost));
+
+ return mainContainer;
+ }
+
+ private BoxContainer GetTitle(string name)
+ {
+ var container = new BoxContainer
+ {
+ Orientation = BoxContainer.LayoutOrientation.Horizontal,
+ };
+
+ var label = new Label
+ {
+ Text = Loc.GetString("vampire-abilities-name"),
+ StyleClasses = { StyleNano.StyleClassLabelKeyText },
+ Margin = new Thickness(8, 0, 0, 0),
+ HorizontalExpand = true,
+ };
+
+ var nameLabel = new Label();
+ nameLabel.Text = Loc.GetString(name);
+
+ container.AddChild(label);
+ container.AddChild(nameLabel);
+
+ return container;
+ }
+
+ private BoxContainer GetBloodCost(int bloodCost)
+ {
+ var container = new BoxContainer
+ {
+ Orientation = BoxContainer.LayoutOrientation.Horizontal,
+ };
+
+ var label = new Label
+ {
+ Text = Loc.GetString("vampire-abilities-bloodrequired"),
+ StyleClasses = { StyleNano.StyleClassLabelKeyText },
+ Margin = new Thickness(8, 0, 0, 0),
+ HorizontalExpand = true,
+ };
+
+ var bloodCostLabel = new Label();
+ bloodCostLabel.Text = bloodCost.ToString();
+
+ container.AddChild(label);
+ container.AddChild(bloodCostLabel);
+
+ return container;
+ }
+
+ private PanelContainer GetPanelContainer(int index)
+ {
+ return new PanelContainer
+ {
+ PanelOverride = new StyleBoxFlat
+ {
+ BackgroundColor = index % 2 == 0 ? Color.FromHex("#2F2F2F") : Color.FromHex("#1E1E22"),
+ BorderColor = Color.FromHex("#1E1E22"),
+ },
+ Margin = _defaultMargin,
+ };
+ }
+
+ private BoxContainer GetBoxContainer(BoxContainer.LayoutOrientation orientation)
+ {
+ return new BoxContainer
+ {
+ Orientation = orientation,
+ Margin = _defaultMargin,
+ };
+ }
+
+ private TextureRect GetTextureRect(SpriteSpecifier icon)
+ {
+ return new TextureRect
+ {
+ Texture = _spriteSystem.Frame0(icon),
+ TextureScale = new Vector2(1.5f, 1.5f),
+ Stretch = TextureRect.StretchMode.KeepCentered,
+ };
+ }
+
+ private RichTextLabel GetDescription(string description)
+ {
+ var label = new RichTextLabel();
+ label.SetMessage(FormattedMessage.FromMarkup(Loc.GetString(description)));
+
+ return label;
+ }
+
+ private Button GetButton(string text, bool enabled)
+ {
+ return new Button
+ {
+ Text = text,
+ HorizontalAlignment = HAlignment.Left,
+ Margin = new Thickness(0, 8, 0, 0),
+ Disabled = !enabled,
+ };
+ }
+}
diff --git a/Content.Client/_Adventure/DarkStation/GameRules/Hunter/Desecrated/PontificVisualizerSystem.cs b/Content.Client/_Adventure/DarkStation/GameRules/Hunter/Desecrated/PontificVisualizerSystem.cs
new file mode 100644
index 00000000000..3189bc07f4c
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/GameRules/Hunter/Desecrated/PontificVisualizerSystem.cs
@@ -0,0 +1,27 @@
+using Content.Client._Adventure.DarkForces.Desecrated;
+using Content.Shared._Adventure.Hunter.Desecrated.Pontific;
+using Robust.Client.GameObjects;
+
+namespace Content.Client._Adventure.GameRules.Hunter.Desecrated;
+
+public sealed class PontificDamageStateVisualizerSystem : VisualizerSystem
+{
+ protected override void OnAppearanceChange(
+ EntityUid uid,
+ PontificVisualsComponent component,
+ ref AppearanceChangeEvent args
+ )
+ {
+ var sprite = args.Sprite;
+ if (sprite == null || !AppearanceSystem.TryGetData(uid,
+ PontificStateVisuals.State,
+ out var pontificState,
+ args.Component))
+ return;
+
+ sprite.LayerSetVisible(PontificVisualLayers.Base, pontificState == PontificState.Base);
+ sprite.LayerSetVisible(PontificVisualLayers.Dead, pontificState == PontificState.Dead);
+ sprite.LayerSetVisible(PontificVisualLayers.Flame, pontificState == PontificState.Flame);
+ // sprite.LayerSetVisible(PontificVisualLayers.Prayer, pontificState == PontificState.Prayer);
+ }
+}
diff --git a/Content.Client/_Adventure/DarkStation/GameRules/Hunter/Desecrated/PontificVisualsComponent.cs b/Content.Client/_Adventure/DarkStation/GameRules/Hunter/Desecrated/PontificVisualsComponent.cs
new file mode 100644
index 00000000000..dbb24ba1ba3
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/GameRules/Hunter/Desecrated/PontificVisualsComponent.cs
@@ -0,0 +1,6 @@
+namespace Content.Client._Adventure.GameRules.Hunter.Desecrated;
+
+[RegisterComponent]
+public sealed partial class PontificVisualsComponent : Component
+{
+}
diff --git a/Content.Client/_Adventure/DarkStation/Medical/Disease/DiseasesVisualizerSystem.cs b/Content.Client/_Adventure/DarkStation/Medical/Disease/DiseasesVisualizerSystem.cs
new file mode 100644
index 00000000000..1f4c950b8c8
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/Medical/Disease/DiseasesVisualizerSystem.cs
@@ -0,0 +1,37 @@
+using Content.Shared._Adventure.Medical.Diseases.Effects;
+using Robust.Client.GameObjects;
+using Robust.Shared.Utility;
+
+namespace Content.Client._Adventure.Medical.Disease;
+
+public sealed class DiseasesVisualizerSystem : VisualizerSystem
+{
+ protected override void OnAppearanceChange(EntityUid uid,
+ DiseasesVisualsComponent component,
+ ref AppearanceChangeEvent args)
+ {
+ if (args.Sprite == null)
+ return;
+
+ foreach (DiseaseVisualLayers layer in Enum.GetValues(typeof(DiseaseVisualLayers)))
+ {
+ if (!AppearanceSystem.TryGetData(uid, layer, out var data))
+ continue;
+
+ switch (data)
+ {
+ case DiseaseClearKey.Clear:
+ args.Sprite.LayerSetVisible(layer, false);
+ break;
+
+ case DiseaseVisualsData visualsData:
+ var spriteSpecifier = new SpriteSpecifier.Rsi(new ResPath(visualsData.Sprite), visualsData.State);
+
+ args.Sprite.LayerSetSprite(layer, spriteSpecifier);
+ args.Sprite.LayerSetVisible(layer, true);
+
+ break;
+ }
+ }
+ }
+}
diff --git a/Content.Client/_Adventure/DarkStation/Medical/Disease/DiseasesVisualsComponent.cs b/Content.Client/_Adventure/DarkStation/Medical/Disease/DiseasesVisualsComponent.cs
new file mode 100644
index 00000000000..cecb343243b
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/Medical/Disease/DiseasesVisualsComponent.cs
@@ -0,0 +1,6 @@
+namespace Content.Client._Adventure.Medical.Disease;
+
+[RegisterComponent]
+public sealed partial class DiseasesVisualsComponent : Component
+{
+}
diff --git a/Content.Client/_Adventure/DarkStation/Medical/Surgery/UI/Doll/Widgets/Systems/Controls/TargetDollPartBox.cs b/Content.Client/_Adventure/DarkStation/Medical/Surgery/UI/Doll/Widgets/Systems/Controls/TargetDollPartBox.cs
new file mode 100644
index 00000000000..794c9cbcd5f
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/Medical/Surgery/UI/Doll/Widgets/Systems/Controls/TargetDollPartBox.cs
@@ -0,0 +1,29 @@
+using Content.Shared.Body.Part;
+
+namespace Content.Client._Adventure.Medical.Surgery.UI.Doll.Widgets.Systems.Controls;
+
+public sealed class TargetDollPartBox
+{
+ private string _texturePath = string.Empty;
+
+ public Box2 Pos;
+ public BodyPartSymmetry Symmetry;
+ public BodyPartType? Type;
+
+ public TargetDollPartBox(string texturePath,
+ Box2 pos,
+ BodyPartType? type = null,
+ BodyPartSymmetry symmetry = BodyPartSymmetry.None)
+ {
+ TexturePath = texturePath;
+ Pos = pos;
+ Type = type;
+ Symmetry = symmetry;
+ }
+
+ public string TexturePath
+ {
+ set => _texturePath = "/Textures/DarkStation/MainGame/Interface/Default/TargetDoll/" + value;
+ get => _texturePath;
+ }
+}
diff --git a/Content.Client/_Adventure/DarkStation/Medical/Surgery/UI/Doll/Widgets/Systems/Controls/TargetDollTextureButton.cs b/Content.Client/_Adventure/DarkStation/Medical/Surgery/UI/Doll/Widgets/Systems/Controls/TargetDollTextureButton.cs
new file mode 100644
index 00000000000..edec3f9f295
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/Medical/Surgery/UI/Doll/Widgets/Systems/Controls/TargetDollTextureButton.cs
@@ -0,0 +1,95 @@
+using System.Linq;
+using System.Numerics;
+using Content.Shared.Body.Part;
+using Robust.Client.UserInterface.Controls;
+
+namespace Content.Client._Adventure.Medical.Surgery.UI.Doll.Widgets.Systems.Controls;
+
+public sealed class TargetDollTextureButton : TextureButton
+{
+ private readonly List _bodyParts = new()
+ {
+ new TargetDollPartBox(
+ "surgery_deactivated.png",
+ Box2.Empty
+ ),
+ new TargetDollPartBox(
+ "surgery_arm_left.png",
+ new Box2(new Vector2(80, 86), new Vector2(97, 54)),
+ BodyPartType.Arm,
+ BodyPartSymmetry.Left
+ ),
+ new TargetDollPartBox(
+ "surgery_arm_right.png",
+ new Box2(new Vector2(30, 45), new Vector2(40, 84)),
+ BodyPartType.Arm,
+ BodyPartSymmetry.Right
+ ),
+ new TargetDollPartBox(
+ "surgery_chest.png",
+ new Box2(new Vector2(47, 85), new Vector2(73, 43)),
+ BodyPartType.Torso
+ ),
+ new TargetDollPartBox(
+ "surgery_head.png",
+ new Box2(new Vector2(45, 35), new Vector2(74, 14)),
+ BodyPartType.Head
+ ),
+ new TargetDollPartBox(
+ "surgery_leg_left.png",
+ new Box2(new Vector2(63, 124), new Vector2(87, 94)),
+ BodyPartType.Leg,
+ BodyPartSymmetry.Left
+ ),
+ new TargetDollPartBox(
+ "surgery_leg_right.png",
+ new Box2(new Vector2(34, 123), new Vector2(57, 94)),
+ BodyPartType.Leg,
+ BodyPartSymmetry.Right
+ ),
+ };
+
+ private TargetDollPartBox? _selectedPart;
+
+ public EventHandler<(BodyPartType?, BodyPartSymmetry)>? OnTargetBodyPartChanged;
+
+ public TargetDollTextureButton()
+ {
+ SetSize = new Vector2(128, 128);
+ HorizontalAlignment = HAlignment.Left;
+ VerticalAlignment = VAlignment.Top;
+ OnButtonUp += SelectPart;
+ }
+
+ private void SelectPart(ButtonEventArgs obj)
+ {
+ var pos = obj.Event.RelativePosition;
+ foreach (var part in _bodyParts.Where(part => part.Pos.Contains(pos)))
+ {
+ if (part == _selectedPart)
+ {
+ SelectPart();
+ OnTargetBodyPartChanged?.Invoke(this, (null, BodyPartSymmetry.None));
+ break;
+ }
+
+ _selectedPart = part;
+ SelectPart(part.Type, part.Symmetry);
+ OnTargetBodyPartChanged?.Invoke(this, (part.Type, part.Symmetry));
+ break;
+ }
+ }
+
+ public void SelectPart(BodyPartType? type = null, BodyPartSymmetry symmetry = BodyPartSymmetry.None)
+ {
+ var targetPart = _bodyParts.Find(part => part.Type == type && part.Symmetry == symmetry);
+ if (targetPart == null)
+ {
+ TexturePath = _bodyParts.First().TexturePath;
+ return;
+ }
+
+ _selectedPart = targetPart;
+ TexturePath = targetPart.TexturePath;
+ }
+}
diff --git a/Content.Client/_Adventure/DarkStation/Medical/Surgery/UI/Doll/Widgets/Systems/TargetDollSystem.cs b/Content.Client/_Adventure/DarkStation/Medical/Surgery/UI/Doll/Widgets/Systems/TargetDollSystem.cs
new file mode 100644
index 00000000000..909245a7893
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/Medical/Surgery/UI/Doll/Widgets/Systems/TargetDollSystem.cs
@@ -0,0 +1,57 @@
+using Content.Shared._Adventure.Medical.Surgery;
+using Content.Shared._Adventure.Medical.Surgery.Events.Doll;
+using Content.Shared.Body.Part;
+using Robust.Client.Player;
+using Robust.Shared.Configuration;
+using Robust.Shared.Player;
+using SecretCCVars = Content.Shared._Adventure.CCVars.SecretCCVars;
+
+namespace Content.Client._Adventure.Medical.Surgery.UI.Doll.Widgets.Systems;
+
+public sealed class TargetDollSystem : EntitySystem
+{
+ [Dependency] private readonly IConfigurationManager _configuration = default!;
+ [Dependency] private readonly IPlayerManager _player = default!;
+
+ public event EventHandler<(BodyPartType?, BodyPartSymmetry)>? SyncTargetPart;
+ public event EventHandler? Dispose;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnPlayerAttached);
+ SubscribeLocalEvent(OnPlayerDetached);
+ }
+
+ private void OnPlayerAttached(Entity ent, ref LocalPlayerAttachedEvent args)
+ {
+ if (!_configuration.GetCVar(SecretCCVars.IsTargetDollEnabled))
+ return;
+
+ SyncTargetPart?.Invoke(this, (ent.Comp.TargetBodyPart, ent.Comp.BodyPartSymmetry));
+ }
+
+ private void OnPlayerDetached(Entity ent, ref LocalPlayerDetachedEvent args)
+ {
+ Dispose?.Invoke(this, EventArgs.Empty);
+ }
+
+ public void OnTagetBodyPartChanged(BodyPartType? type, BodyPartSymmetry symmetry)
+ {
+ if (!_configuration.GetCVar(SecretCCVars.IsTargetDollEnabled))
+ return;
+
+ if (_player.LocalEntity is not { } userEnt)
+ return;
+
+ var @event = new TargetDollChangedEvent
+ {
+ Entity = GetNetEntity(userEnt),
+ Type = type,
+ Symmetry = symmetry,
+ };
+
+ RaiseNetworkEvent(@event);
+ }
+}
diff --git a/Content.Client/_Adventure/DarkStation/Medical/Surgery/UI/Doll/Widgets/Systems/TargetDollUiController.cs b/Content.Client/_Adventure/DarkStation/Medical/Surgery/UI/Doll/Widgets/Systems/TargetDollUiController.cs
new file mode 100644
index 00000000000..5f438816611
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/Medical/Surgery/UI/Doll/Widgets/Systems/TargetDollUiController.cs
@@ -0,0 +1,101 @@
+using Content.Client.Gameplay;
+using Content.Client.UserInterface.Systems.Gameplay;
+using Content.Shared._Adventure.Medical.Surgery;
+using Content.Shared.Body.Part;
+using Robust.Client.Player;
+using Robust.Client.UserInterface.Controllers;
+using Robust.Shared.Configuration;
+using SecretCCVars = Content.Shared._Adventure.CCVars.SecretCCVars;
+
+namespace Content.Client._Adventure.Medical.Surgery.UI.Doll.Widgets.Systems;
+
+public sealed class TargetDollUiController : UIController, IOnStateEntered,
+ IOnSystemChanged
+{
+ [Dependency] private readonly IConfigurationManager _cfg = default!;
+ [Dependency] private readonly IEntityManager _entityManager = default!;
+ [Dependency] private readonly IPlayerManager _playerManager = default!;
+
+ private EntityUid? Player => _playerManager.LocalEntity;
+
+ private TargetDollUI? UI => UIManager.GetActiveUIWidgetOrNull();
+
+ public void OnStateEntered(GameplayState state)
+ {
+ if (!_cfg.GetCVar(SecretCCVars.IsTargetDollEnabled))
+ return;
+
+ SyncTargetPart();
+ }
+
+ public void OnSystemLoaded(TargetDollSystem system)
+ {
+ system.SyncTargetPart += SystemOnSyncTargetPart;
+ system.Dispose += ClearAllControls;
+ }
+
+ public void OnSystemUnloaded(TargetDollSystem system)
+ {
+ system.SyncTargetPart -= SystemOnSyncTargetPart;
+ system.Dispose -= ClearAllControls;
+ }
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ var gameplayStateLoad = UIManager.GetUIController();
+ gameplayStateLoad.OnScreenLoad += OnScreenLoad;
+ gameplayStateLoad.OnScreenUnload += OnScreenUnload;
+ }
+
+ private void OnScreenUnload()
+ {
+ UI?.Clear();
+ }
+
+ private void OnScreenLoad()
+ {
+ if (!_cfg.GetCVar(SecretCCVars.IsTargetDollEnabled))
+ return;
+
+ UI?.SetupWidget();
+ SyncTargetPart();
+ }
+
+ private void ClearAllControls(object? sender, EventArgs eventArgs)
+ {
+ UI?.Hide();
+ }
+
+ private void SystemOnSyncTargetPart(object? sender, (BodyPartType?, BodyPartSymmetry) args)
+ {
+ if (!_cfg.GetCVar(SecretCCVars.IsTargetDollEnabled))
+ return;
+
+ if (sender is not TargetDollSystem)
+ return;
+
+ if (!_entityManager.HasComponent(Player))
+ {
+ UI?.Hide();
+ return;
+ }
+
+ UI?.Show();
+ UI?.SelectBodyPart(args.Item1, args.Item2);
+ }
+
+ private void SyncTargetPart()
+ {
+ if (!_entityManager.TryGetComponent(Player, out var targetDoll) ||
+ !_cfg.GetCVar(SecretCCVars.IsTargetDollEnabled))
+ {
+ UI?.Hide();
+ return;
+ }
+
+ UI?.Show();
+ UI?.SelectBodyPart(targetDoll.TargetBodyPart, targetDoll?.BodyPartSymmetry ?? BodyPartSymmetry.None);
+ }
+}
diff --git a/Content.Client/_Adventure/DarkStation/Medical/Surgery/UI/Doll/Widgets/Systems/TargetDollWidgetBridge.cs b/Content.Client/_Adventure/DarkStation/Medical/Surgery/UI/Doll/Widgets/Systems/TargetDollWidgetBridge.cs
new file mode 100644
index 00000000000..c3ae9a9a0d4
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/Medical/Surgery/UI/Doll/Widgets/Systems/TargetDollWidgetBridge.cs
@@ -0,0 +1,66 @@
+using Content.Client._c4llv07e.Bridges;
+using Content.Client._Adventure.Medical.Surgery.UI.Doll.Widgets.Systems.Controls;
+using Content.Shared.Body.Part;
+using Robust.Client.UserInterface;
+
+namespace Content.Client._Adventure.Medical.Surgery.UI.Doll.Widgets.Systems;
+
+public sealed class TargetDollWidgetBridge : ITargetDollWidgetBridge
+{
+ [Dependency] private readonly IEntityManager _entityManager = default!;
+
+ private TargetDollTextureButton? _dollTextureRect;
+ private Control? _surface;
+
+ public void InitializeWidget()
+ {
+ IoCManager.InjectDependencies(this);
+ }
+
+ public void SetupWidget(Control surface)
+ {
+ if (_surface != null)
+ Clear();
+
+ var dollTexture = new TargetDollTextureButton();
+ dollTexture.OnTargetBodyPartChanged += OnTargetBodyPartChanged;
+
+ surface.AddChild(dollTexture);
+
+ _surface = surface;
+ _dollTextureRect = dollTexture;
+ _surface = surface;
+ _dollTextureRect = dollTexture;
+ }
+
+ public void SelectBodyPart(BodyPartType? targetBodyPart, BodyPartSymmetry bodyPartSymmetry)
+ {
+ _dollTextureRect?.SelectPart(targetBodyPart, bodyPartSymmetry);
+ }
+
+ public void Clear()
+ {
+ _surface?.RemoveAllChildren();
+ _surface = null;
+
+ _dollTextureRect = null;
+ _dollTextureRect?.RemoveAllChildren();
+ }
+
+ public void Hide()
+ {
+ if (_surface != null)
+ _surface.Visible = false;
+ }
+
+ public void Show()
+ {
+ if (_surface != null)
+ _surface.Visible = true;
+ }
+
+ private void OnTargetBodyPartChanged(object? sender, (BodyPartType?, BodyPartSymmetry) e)
+ {
+ _entityManager.System().OnTagetBodyPartChanged(e.Item1, e.Item2);
+ }
+}
diff --git a/Content.Client/_Adventure/DarkStation/Medical/Surgery/UI/SurgeryBoundUserInterface.cs b/Content.Client/_Adventure/DarkStation/Medical/Surgery/UI/SurgeryBoundUserInterface.cs
new file mode 100644
index 00000000000..0bc050541c0
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/Medical/Surgery/UI/SurgeryBoundUserInterface.cs
@@ -0,0 +1,62 @@
+using Content.Shared._Adventure.Medical.Surgery;
+using JetBrains.Annotations;
+
+namespace Content.Client._Adventure.Medical.Surgery.UI;
+
+///
+/// Initializes a and updates it when new server messages are received.
+///
+[UsedImplicitly]
+public sealed class SurgeryBoundUserInterface : BoundUserInterface
+{
+ [Dependency] private readonly IEntityManager _entityManager = default!;
+ private SurgeryWindow? _window;
+
+ public SurgeryBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ }
+
+ ///
+ ///
+ protected override void Open()
+ {
+ base.Open();
+
+ // Setup window layout/elements
+ _window = new SurgeryWindow
+ {
+ Title = _entityManager.GetComponent(Owner).EntityName,
+ };
+
+ _window.OpenCentered();
+ _window.OnClose += Close;
+
+ // Setup surgery slot button actions.
+ _window.OnSurgerySlotButtonPressed += (args, button) => SendMessage(new SurgerySlotButtonPressed(button.Slot));
+ _window.OnOrganSlotButtonPressed += (args, button) => SendMessage(new OrganSlotButtonPressed(button.Slot));
+ }
+
+ ///
+ /// Update the UI each time new state data is sent from the server.
+ ///
+ ///
+ /// Data of the that this UI represents.
+ /// Sent from the server.
+ ///
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ var castState = (SurgeryBoundUserInterfaceState)state;
+
+ _window?.UpdateState(castState); //Update window state
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+
+ if (disposing)
+ _window?.Dispose();
+ }
+}
diff --git a/Content.Client/_Adventure/DarkStation/Medical/Surgery/UI/SurgeryWindow.xaml b/Content.Client/_Adventure/DarkStation/Medical/Surgery/UI/SurgeryWindow.xaml
new file mode 100644
index 00000000000..6aefc09418a
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/Medical/Surgery/UI/SurgeryWindow.xaml
@@ -0,0 +1,8 @@
+
+
+
diff --git a/Content.Client/_Adventure/DarkStation/Medical/Surgery/UI/SurgeryWindow.xaml.cs b/Content.Client/_Adventure/DarkStation/Medical/Surgery/UI/SurgeryWindow.xaml.cs
new file mode 100644
index 00000000000..e9392c514b4
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/Medical/Surgery/UI/SurgeryWindow.xaml.cs
@@ -0,0 +1,503 @@
+using System.Numerics;
+using Content.Shared._Adventure.Medical.Surgery;
+using Content.Shared._Adventure.Medical.Surgery.Components;
+using Content.Shared.Body.Organ;
+using Content.Shared.Body.Part;
+using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client._Adventure.Medical.Surgery.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class SurgeryWindow : DefaultWindow
+{
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly IEntityManager _entityManager = default!;
+ public event Action? OnSurgerySlotButtonPressed;
+ public event Action? OnOrganSlotButtonPressed;
+ public static int DefaultButtonSize = 112;
+
+ private static readonly string BasePath = "/Textures/DarkStation/MainGame/Interface/Default/SurgerySlots/";
+ private static readonly string CauterisedIcon = BasePath + "Cauterised_Icon";
+ private static readonly string BleedingIcon = BasePath + "Bleeding_Icon";
+ private static readonly string FallBackIcon = BasePath + "Fallback_Icon";
+ private static readonly string FallBackSlotOrPart = BasePath + "Fallback_Part";
+
+ ///
+ ///
+ ///
+ public SurgeryWindow()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+ }
+
+ private string GetPartStatusStr(SurgeryBodyPartComponent part, SharedPartStatus status)
+ {
+ if (status.Opened && part.Container) //
+ return "_opened";
+ if (status.EndoOpened && part.State.EndoSkeleton)
+ return "_endo_open";
+ if (status.Retracted && part.State.Incisable)
+ return "_retracted";
+ if (status.Incised && part.State.Incisable)
+ return "_incised";
+ if (status.ExoOpened && part.State.ExoSkeleton)
+ return "_exo_opened";
+ return "";
+ }
+
+ //TODO probably should use an actual visualiser here but it works for now
+ private string FormatState(string symmetry, string type, string status)
+ {
+ var symString = "";
+
+ if (symmetry == "Left")
+ symString = "l_";
+ else if (symmetry == "Right")
+ symString = "r_";
+
+ return symString + type.ToLower() + status + "_icon";
+ }
+
+ ///
+ /// Adding body part slots to interface, part slot buttons are assigned to rows based on their type
+ ///
+ public void UpdateSurgeryMenu(SurgeryBoundUserInterfaceState state)
+ {
+ SurgeryLayout.Children.Clear();
+ var bodyPartSlotList = new BodyPartSlotList();
+ var slotParts = state.SlotPartsStatus;
+
+ var headWingSlotsRow = new SurgerySlotRow();
+ var headWingSlotButtons = new List();
+
+ var armTorsoSlotsRow = new SurgerySlotRow();
+ var armTorsoSlotButtons = new List();
+
+ var handOtherSlotsRow = new SurgerySlotRow();
+ var handOtherSlotButtons = new List();
+
+ var legTailSlotsRow = new SurgerySlotRow();
+ var legTailSlotButtons = new List();
+
+ var footSlotsRow = new SurgerySlotRow();
+ var footSlotButtons = new List();
+
+ var slotButtons = new List();
+
+ var partOrgans = new Dictionary();
+
+ foreach (var bodyPartSlot in state.BodyPartSlots)
+ {
+ var iconRow = new SlotButtonIconRow();
+
+ var typeVal = bodyPartSlot.Type ?? BodyPartType.Other;
+ var slotType = typeVal.ToString();
+
+ var buttonContainer = new SlotButtonContainer(bodyPartSlot, slotType);
+ var button = new SurgerySlotButton(bodyPartSlot, slotType);
+ button.OnPressed += args => OnSurgerySlotButtonPressed?.Invoke(args, button);
+
+ var attachmentUid = _entityManager.GetEntity(bodyPartSlot.Attachment);
+
+ var cautIcon = new SlotIconContainer("Cauterised");
+ var attchIcon = new SlotIconContainer("Attachment");
+ var bleedIcon = new SlotIconContainer("Bleeding");
+
+ if (attachmentUid is not null)
+ {
+ if (_entityManager.HasComponent(attachmentUid))
+ {
+ var attachmentSprite = new StatusIconSprite();
+ attachmentSprite.SetEntity(attachmentUid);
+ attchIcon.Children.Add(attachmentSprite);
+ }
+ }
+
+ if (bodyPartSlot.Cauterised)
+ {
+ cautIcon.TextureNormal = Theme.ResolveTexture(CauterisedIcon);
+ }
+
+ var partUid = _entityManager.GetEntity(bodyPartSlot.BodyPart);
+ if (!bodyPartSlot.Cauterised && attachmentUid is null && partUid is null) //this is currently assuming any attachment will prevent bleeding... which is fine for now but may need to change
+ {
+ bleedIcon.TextureNormal = Theme.ResolveTexture(BleedingIcon);
+ }
+
+ iconRow.Children.Add(attchIcon);
+ iconRow.Children.Add(cautIcon);
+ iconRow.Children.Add(bleedIcon);
+
+ if (partUid is not null)
+ {
+ if (_entityManager.TryGetComponent(partUid, out var bodyPart))
+ buttonContainer.Symmetry = bodyPart.Symmetry.ToString();
+
+ if (!_entityManager.TryGetComponent(partUid, out var surgeryBodyPart))
+ continue;
+
+ if (_entityManager.TryGetComponent(partUid, out var sprite))
+ {
+ var bodyPartSprite = new BodyPartSprite();
+ if (surgeryBodyPart != null && bodyPart is not null && bodyPartSlot.BodyPart != null && slotParts.TryGetValue(bodyPartSlot.BodyPart.Value, out var slotValue))
+ {
+ var status = GetPartStatusStr(surgeryBodyPart, slotValue);
+ sprite.LayerSetState(0, FormatState(bodyPart.Symmetry.ToString(), slotType, status));
+ }
+
+ bodyPartSprite.SetEntity(_entityManager.GetEntity(bodyPartSlot.BodyPart)!.Value);
+
+ button.Children.Add(bodyPartSprite);
+ }
+ }
+
+ buttonContainer.Children.Add(iconRow);
+ buttonContainer.Children.Add(button);
+
+ slotButtons.Add(buttonContainer);
+ }
+
+ for (var i = 0; i < slotButtons.Count; i++)
+ {
+ //symmetrical part types are added to the start/end of list, rest are placed in centre
+ //TODO assign symmetry via part slot
+ void AddToSlotRow(SlotButtonContainer button, List list, bool symmetry)
+ {
+ if (!symmetry)
+ {
+ var index = (list.Count + 1) / 2;
+ list.Insert(index, button);
+ }
+ else
+ {
+ var counterpartFound = false;
+ for (var j = 0; j < list.Count; j++)
+ {
+ if (list[j].SlotType == button.SlotType && !list[j].Counterpart)
+ {
+ counterpartFound = true;
+ if (button.Symmetry == "Left") //if we have assigned a left value from the constituent body part above, it overrides here
+ {
+ list.Insert(0, button);
+ break;
+ }
+ else
+ {
+ button.Symmetry = "Right"; //right now we are assuming left is always found first, followed by right... this is not necessarily correct //TODO fix by assigning symmetry to the slot
+ button.Counterpart = true;
+ if (list.Count > 0)
+ list.Insert(list.Count - (j - 1), button);
+ else
+ list.Insert(0, button);
+ break;
+ }
+ }
+ }
+
+ if (!counterpartFound)
+ {
+ if (button.Symmetry == "Right") //if we have assigned a right value from the constituent body part above, it overrides here
+ {
+ button.Counterpart = true;
+ if (list.Count > 0)
+ list.Insert(list.Count - 1, button); //TODO fix still not ideal but better than assuming its position, just wouldn't work on something with four arms as well
+ else
+ list.Insert(0, button);
+ }
+ else
+ {
+ button.Symmetry = "Left"; //TODO fix
+ button.Counterpart = true;
+ list.Insert(0, button);
+ }
+ }
+ }
+ }
+
+ switch (slotButtons[i].SlotType)
+ {
+ case "Head":
+ AddToSlotRow(slotButtons[i], headWingSlotButtons, false);
+ break;
+ case "Wing":
+ AddToSlotRow(slotButtons[i], headWingSlotButtons, true);
+ break;
+ case "Torso":
+ AddToSlotRow(slotButtons[i], armTorsoSlotButtons, false);
+ break;
+ case "Arm":
+ AddToSlotRow(slotButtons[i], armTorsoSlotButtons, true);
+ break;
+ case "Hand":
+ AddToSlotRow(slotButtons[i], handOtherSlotButtons, true);
+ break;
+ case "Leg":
+ AddToSlotRow(slotButtons[i], legTailSlotButtons, true);
+ break;
+ case "Foot":
+ AddToSlotRow(slotButtons[i], footSlotButtons, true);
+ break;
+ case "Tail":
+ AddToSlotRow(slotButtons[i], legTailSlotButtons, false);
+ break;
+ default: //other
+ AddToSlotRow(slotButtons[i], handOtherSlotButtons, false);
+ break;
+ }
+ }
+
+ for (var i = 0; i < headWingSlotButtons.Count; i++)
+ headWingSlotsRow.Children.Add(headWingSlotButtons[i]);
+
+ for (var i = 0; i < armTorsoSlotButtons.Count; i++)
+ armTorsoSlotsRow.Children.Add(armTorsoSlotButtons[i]);
+
+ for (var i = 0; i < handOtherSlotButtons.Count; i++)
+ handOtherSlotsRow.Children.Add(handOtherSlotButtons[i]);
+
+ for (var i = 0; i < legTailSlotButtons.Count; i++)
+ legTailSlotsRow.Children.Add(legTailSlotButtons[i]);
+
+ for (var i = 0; i < footSlotButtons.Count; i++)
+ footSlotsRow.Children.Add(footSlotButtons[i]);
+
+ bodyPartSlotList.Children.Add(headWingSlotsRow);
+ bodyPartSlotList.Children.Add(armTorsoSlotsRow);
+ bodyPartSlotList.Children.Add(handOtherSlotsRow);
+ bodyPartSlotList.Children.Add(legTailSlotsRow);
+ bodyPartSlotList.Children.Add(footSlotsRow);
+
+ for (var i = 0; i < state.OrganSlots.Count; i++)
+ {
+ //create button
+ var iconRow = new SlotButtonIconRow();
+ var buttonContainer = new OrganSlotButtonContainer();
+
+ var typeVal = state.OrganSlots[i].Type ?? OrganType.Other;
+
+ var organSlotType = typeVal.ToString();
+ var button = new OrganSlotButton(state.OrganSlots[i], organSlotType);
+ button.OnPressed += args => OnOrganSlotButtonPressed?.Invoke(args, button);
+
+ var attachmentUid = state.OrganSlots[i].Attachment;
+
+ var cautIcon = new SlotIconContainer("Cauterised");
+ var attchIcon = new SlotIconContainer("Attachment");
+ var bleedIcon = new SlotIconContainer("Bleeding");
+
+ if (attachmentUid is not null)
+ {
+ if (IoCManager.Resolve().TryGetComponent(_entityManager.GetEntity(state.OrganSlots[i].Attachment), out var isprite))
+ {
+ var attachmentSprite = new StatusIconSprite();
+ attachmentSprite.SetEntity(_entityManager.GetEntity(state.OrganSlots[i].Attachment));
+
+ attchIcon.Children.Add(attachmentSprite);
+ }
+ }
+
+ if (state.OrganSlots[i].Cauterised)
+ {
+ cautIcon.TextureNormal = Theme.ResolveTexture(CauterisedIcon);
+ }
+
+ if (!state.OrganSlots[i].Cauterised && attachmentUid is null && state.OrganSlots[i].Child is null)
+ {
+ bleedIcon.TextureNormal = Theme.ResolveTexture(BleedingIcon);
+ }
+
+ iconRow.Children.Add(attchIcon);
+ iconRow.Children.Add(cautIcon);
+ iconRow.Children.Add(bleedIcon);
+
+ //add button sprite
+ if (state.OrganSlots[i].Child != null && IoCManager.Resolve().TryGetComponent(_entityManager.GetEntity(state.OrganSlots[i].Child), out var sprite))
+ {
+ var organSprite = new BodyPartSprite();
+ organSprite.SetEntity(_entityManager.GetEntity(state.OrganSlots[i].Child));
+ button.Children.Add(organSprite);
+ }
+
+ if (!partOrgans.ContainsKey(state.OrganSlots[i].Parent.ToString()))
+ {
+ partOrgans.Add(state.OrganSlots[i].Parent.ToString(), new OrganSlotCol());
+ if (slotParts.ContainsKey(state.OrganSlots[i].Parent))
+ partOrgans[state.OrganSlots[i].Parent.ToString()].Children.Add(new Label {Text = slotParts[state.OrganSlots[i].Parent].PartType.ToString()});
+ ;
+ }
+
+ buttonContainer.Children.Add(iconRow);
+ buttonContainer.Children.Add(button);
+
+ //add button to dict
+ partOrgans[state.OrganSlots[i].Parent.ToString()].Children.Add(buttonContainer);
+ }
+
+ SurgeryLayout.Children.Add(bodyPartSlotList);
+ //iterate partOrgans, add cols to surgery menu
+ var width = 625;
+ foreach (KeyValuePair entry in partOrgans)
+ {
+ width += 100;
+ SurgeryLayout.Children.Add(new Padding());
+ SurgeryLayout.Children.Add(entry.Value);
+ }
+
+ SetSize = new Vector2(width, 675);
+ }
+
+ ///
+ /// Update the UI state when new state data is received from the server.
+ ///
+ /// State data sent by the server.
+ public void UpdateState(BoundUserInterfaceState state)
+ {
+ var castState = (SurgeryBoundUserInterfaceState) state;
+ UpdateSurgeryMenu(castState);
+ }
+
+ public sealed class SlotButtonContainer : BoxContainer
+ {
+ public BodyPartSlot Slot { get; }
+ public string Symmetry = "None";
+ public bool Counterpart = false;
+ public string SlotType { get; }
+
+ public SlotButtonContainer(BodyPartSlot slot, string slotType)
+ {
+ Slot = slot;
+ SlotType = slotType;
+ Orientation = LayoutOrientation.Horizontal;
+ Align = AlignMode.Center;
+ }
+ }
+
+ public sealed class OrganSlotButtonContainer : BoxContainer
+ {
+ public OrganSlotButtonContainer()
+ {
+ Orientation = LayoutOrientation.Horizontal;
+ Align = AlignMode.Center;
+ }
+ }
+
+ public sealed class SlotButtonIconRow : BoxContainer
+ {
+ public SlotButtonIconRow()
+ {
+ Orientation = LayoutOrientation.Vertical;
+ }
+ }
+
+ public sealed class SlotIconContainer : TextureButton
+ {
+ public SlotIconContainer(string iconType)
+ {
+ MinSize = new Vector2(DefaultButtonSize / 3, DefaultButtonSize / 3);
+ MaxSize = new Vector2(DefaultButtonSize / 3, DefaultButtonSize / 3);
+
+ if (Theme.TryResolveTexture(BasePath + iconType, out var texture))
+ TextureNormal = texture;
+ else
+ TextureNormal = Theme.ResolveTexture(FallBackIcon);
+ }
+ }
+
+ public sealed class SurgerySlotButton : TextureButton
+ {
+ public BodyPartSlot Slot { get; }
+ public string SlotType { get; }
+
+ public SurgerySlotButton(BodyPartSlot slot, string slotType)
+ {
+ Slot = slot;
+ SlotType = slotType;
+ MinSize = new Vector2(DefaultButtonSize, DefaultButtonSize);
+ MaxSize = new Vector2(DefaultButtonSize, DefaultButtonSize);
+
+ if (Theme.TryResolveTexture(BasePath + SlotType, out var texture))
+ TextureNormal = texture;
+ else
+ TextureNormal = Theme.ResolveTexture(FallBackIcon);
+ }
+ }
+
+ public sealed class OrganSlotButton : TextureButton
+ {
+ public OrganSlot Slot { get; }
+ public string SlotType { get; }
+
+ public OrganSlotButton(OrganSlot slot, string slotType)
+ {
+ Slot = slot;
+ SlotType = slotType;
+ MinSize = new Vector2(DefaultButtonSize, DefaultButtonSize);
+ MaxSize = new Vector2(DefaultButtonSize, DefaultButtonSize);
+
+ if (Theme.TryResolveTexture(BasePath + SlotType, out var texture))
+ TextureNormal = texture;
+ else
+ TextureNormal = Theme.ResolveTexture(FallBackIcon);
+ }
+ }
+
+ public sealed class Padding : Control
+ {
+ public Padding()
+ {
+ MinSize = new Vector2(0, 10);
+ }
+ }
+
+ public sealed class SurgerySlotRow : BoxContainer
+ {
+ public SurgerySlotRow()
+ {
+ Orientation = LayoutOrientation.Horizontal;
+ Align = AlignMode.Center;
+ }
+ }
+
+ public sealed class BodyPartSlotList : BoxContainer
+ {
+ public BodyPartSlotList()
+ {
+ Orientation = LayoutOrientation.Vertical;
+ HorizontalExpand = true;
+ }
+ }
+
+ public sealed class OrganSlotCol : BoxContainer
+ {
+ public OrganSlotCol()
+ {
+ Orientation = LayoutOrientation.Vertical;
+ Align = AlignMode.Center;
+ }
+ }
+
+ public sealed class BodyPartSprite : SpriteView
+ {
+ public BodyPartSprite()
+ {
+ OverrideDirection = Direction.South;
+ Scale = new Vector2(3, 3);
+ }
+ }
+
+ public sealed class StatusIconSprite : SpriteView
+ {
+ public StatusIconSprite()
+ {
+ OverrideDirection = Direction.South;
+ Scale = new(2, 2);
+ }
+ }
+}
diff --git a/Content.Client/_Adventure/DarkStation/Medical/Surgery/UI/TargetDollUI.xaml b/Content.Client/_Adventure/DarkStation/Medical/Surgery/UI/TargetDollUI.xaml
new file mode 100644
index 00000000000..77d570fc0be
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/Medical/Surgery/UI/TargetDollUI.xaml
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/Content.Client/_Adventure/DarkStation/Medical/Surgery/UI/TargetDollUI.xaml.cs b/Content.Client/_Adventure/DarkStation/Medical/Surgery/UI/TargetDollUI.xaml.cs
new file mode 100644
index 00000000000..10afc45118e
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/Medical/Surgery/UI/TargetDollUI.xaml.cs
@@ -0,0 +1,45 @@
+using Content.Client._c4llv07e.Bridges;
+using Content.Shared.Body.Part;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client._Adventure.Medical.Surgery.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class TargetDollUI : UIWidget
+{
+ private readonly ITargetDollWidgetBridge _targetDollWidget;
+
+ public TargetDollUI()
+ {
+ RobustXamlLoader.Load(this);
+ _targetDollWidget = IoCManager.Resolve();
+ _targetDollWidget.InitializeWidget();
+ }
+
+ public void SetupWidget()
+ {
+ _targetDollWidget.SetupWidget(Surface);
+ }
+
+ public void Clear()
+ {
+ _targetDollWidget.Clear();
+ }
+
+ public void Hide()
+ {
+ _targetDollWidget.Hide();
+ }
+
+ public void Show()
+ {
+ _targetDollWidget.Show();
+ }
+
+ public void SelectBodyPart(BodyPartType? targetBodyPart, BodyPartSymmetry bodyPartSymmetry)
+ {
+ _targetDollWidget.SelectBodyPart(targetBodyPart, bodyPartSymmetry);
+ }
+}
diff --git a/Content.Client/_Adventure/DarkStation/Roles/CCO/Console/CCOConsoleBoundInterface.cs b/Content.Client/_Adventure/DarkStation/Roles/CCO/Console/CCOConsoleBoundInterface.cs
new file mode 100644
index 00000000000..9acb6621278
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/Roles/CCO/Console/CCOConsoleBoundInterface.cs
@@ -0,0 +1,88 @@
+using Content.Shared._Adventure.Roles.CCO;
+
+namespace Content.Client._Adventure.Roles.CCO.Console;
+
+public sealed class CcoConsoleBoundInterface : BoundUserInterface
+{
+ private CcoConsoleWindow? _window;
+
+ public CcoConsoleBoundInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ if (_window is not null)
+ return;
+
+ _window = new CcoConsoleWindow(this);
+ _window.OnClose += Close;
+ _window.OpenCentered();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+
+ if (!disposing)
+ return;
+
+ _window?.Close();
+ _window?.Dispose();
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ if (state is not CcoConsoleUIState ccoConsoleUIState || _window == null)
+ return;
+
+ _window.UpdateState(ccoConsoleUIState);
+ }
+
+ public void OnEmergencyShuttlePressed(EmergencyShuttleState state)
+ {
+ switch (state)
+ {
+ case EmergencyShuttleState.Idle:
+ SendMessage(new CcoConsoleSendEmergencyShuttleMessage());
+ break;
+ case EmergencyShuttleState.OnWay:
+ SendMessage(new CcoConsoleCancelEmergencyShuttleMessage());
+ break;
+ }
+ }
+
+ public void SendSpecialSquad(string prototypeId)
+ {
+ SendMessage(new CcoConsoleSendSpecialSquadMessage(prototypeId));
+ }
+
+ public void SendAnnouncement(string message)
+ {
+ SendMessage(new CcoConsoleSendAnnouncementMessage(message));
+ }
+
+ public void OpenManifest()
+ {
+ SendMessage(new CcoConsoleOpenCrewManifestMessage());
+ }
+
+ public void CrewMemberApplySalaryBonus(int bonus, uint record)
+ {
+ SendMessage(new CcoConsoleCrewMemberSalaryBonusMessage(bonus, record));
+ }
+
+ public void CrewMemberApplySalaryPenalty(int penalty, uint record)
+ {
+ SendMessage(new CcoConsoleCrewMemberSalaryPenaltyMessage(penalty, record));
+ }
+
+ public void OnStationSelected(NetEntity station)
+ {
+ SendMessage(new CcoConsoleStationSelected(station));
+ }
+}
diff --git a/Content.Client/_Adventure/DarkStation/Roles/CCO/Console/CcoConsoleSquadControl.xaml b/Content.Client/_Adventure/DarkStation/Roles/CCO/Console/CcoConsoleSquadControl.xaml
new file mode 100644
index 00000000000..5c3183a257f
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/Roles/CCO/Console/CcoConsoleSquadControl.xaml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_Adventure/DarkStation/Roles/CCO/Console/CcoConsoleSquadControl.xaml.cs b/Content.Client/_Adventure/DarkStation/Roles/CCO/Console/CcoConsoleSquadControl.xaml.cs
new file mode 100644
index 00000000000..5c1f7f1fc7d
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/Roles/CCO/Console/CcoConsoleSquadControl.xaml.cs
@@ -0,0 +1,24 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client._Adventure.Roles.CCO.Console;
+
+[GenerateTypedNameReferences]
+public sealed partial class CcoConsoleSquadControl : Control
+{
+ public CcoConsoleSquadControl(CcoConsoleBoundInterface bui,
+ string prototypeID,
+ string name,
+ string description,
+ bool canCall)
+ {
+ RobustXamlLoader.Load(this);
+
+ Name.Text = name;
+ Description.SetMessage(description);
+
+ StartRitualButton.OnPressed += _ => bui.SendSpecialSquad(prototypeID);
+ StartRitualButton.Disabled = canCall;
+ }
+}
diff --git a/Content.Client/_Adventure/DarkStation/Roles/CCO/Console/CcoConsoleWindow.EmergencyShuttle.cs b/Content.Client/_Adventure/DarkStation/Roles/CCO/Console/CcoConsoleWindow.EmergencyShuttle.cs
new file mode 100644
index 00000000000..f4a7ba61221
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/Roles/CCO/Console/CcoConsoleWindow.EmergencyShuttle.cs
@@ -0,0 +1,54 @@
+using Content.Shared._Adventure.Roles.CCO;
+using Robust.Shared.Utility;
+
+namespace Content.Client._Adventure.Roles.CCO.Console;
+
+public sealed partial class CcoConsoleWindow
+{
+ private void SetupEmergencyShuttle()
+ {
+ StationCallEmergencyShuttle.OnPressed += _ =>
+ {
+ if (_state?.ConsoleBase.SelectedStation is not { } selectedStation)
+ return;
+
+ var station = _state.Stations.AvailableStation[selectedStation];
+ _bui.OnEmergencyShuttlePressed(station.EmergencyShuttle.ShuttleState);
+ };
+ }
+
+ private void BindEmergencyShuttle(CcoConsoleEmergencyShuttle emergencyShuttle)
+ {
+ var status = "Потеря связи";
+ var buttonText = "Отправить эвакуационный шаттл";
+
+ switch (emergencyShuttle.ShuttleState)
+ {
+ case EmergencyShuttleState.Idle:
+ status = "Готов к отправке";
+ buttonText = "Отправить эвакуационный шаттл";
+ break;
+ case EmergencyShuttleState.Arrived:
+ status = "Прибыл на станцию";
+ buttonText = "Недоступно";
+ break;
+ case EmergencyShuttleState.OnWay:
+ status = "В пути на станцию";
+ buttonText = "Отозвать эвакуационный шаттл";
+ break;
+ case EmergencyShuttleState.OnWayCentcom:
+ status = "В пути на центком";
+ buttonText = "Недоступно";
+ break;
+ case EmergencyShuttleState.Unknown:
+ status = "Потеря связи";
+ buttonText = "Недоступно";
+ break;
+ }
+
+ EmergencyShuttle.SetMessage(FormattedMessage.FromMarkup(status));
+ StationCallEmergencyShuttle.Disabled = emergencyShuttle.ShuttleState != EmergencyShuttleState.Idle &&
+ emergencyShuttle.ShuttleState != EmergencyShuttleState.OnWay;
+ StationCallEmergencyShuttle.Text = buttonText;
+ }
+}
diff --git a/Content.Client/_Adventure/DarkStation/Roles/CCO/Console/CcoConsoleWindow.Salary.cs b/Content.Client/_Adventure/DarkStation/Roles/CCO/Console/CcoConsoleWindow.Salary.cs
new file mode 100644
index 00000000000..0cff91fda3b
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/Roles/CCO/Console/CcoConsoleWindow.Salary.cs
@@ -0,0 +1,188 @@
+using System.Linq;
+using System.Numerics;
+using Content.Shared._Adventure.Roles.CCO;
+using Content.Shared._Adventure.Roles.Salary;
+using Content.Shared.StationRecords;
+using Content.Shared.StatusIcon;
+using Robust.Client.UserInterface.Controls;
+using SecretCCVars = Content.Shared._Adventure.CCVars.SecretCCVars;
+
+namespace Content.Client._Adventure.Roles.CCO.Console;
+
+public sealed partial class CcoConsoleWindow
+{
+ private KeyValuePair? _selectedCrewMemberRecord;
+
+ private void SetupSalaries()
+ {
+ SalaryCount.OnTextChanged += OnSalaryTextChanged;
+ CrewMemberBonusButton.OnPressed += _ => CrewMemberApplySalaryBonus();
+ CrewMemberPenaltyButton.OnPressed += _ => CrewMemberApplySalaryPenalty();
+ }
+
+ private void CrewMemberApplySalaryBonus()
+ {
+ if (_selectedCrewMemberRecord is not { } record)
+ return;
+
+ var bonus = GetSalaryCountInput();
+ _bui.CrewMemberApplySalaryBonus(bonus, record.Key);
+ }
+
+ private void CrewMemberApplySalaryPenalty()
+ {
+ if (_selectedCrewMemberRecord is not { } record)
+ return;
+
+ var penalty = GetSalaryCountInput();
+ _bui.CrewMemberApplySalaryPenalty(penalty, record.Key);
+ }
+
+ private int GetSalaryCountInput()
+ {
+ return int.Parse(SalaryCount.Text);
+ }
+
+ private void OnSalaryTextChanged(LineEdit.LineEditEventArgs args)
+ {
+ var isSalaryCorrect = int.TryParse(args.Text, out var salary) && salary > 0;
+
+ CrewMemberBonusButton.Disabled =
+ !isSalaryCorrect || _selectedCrewMemberRecord?.Value.Salary?.SalaryBonuses.Count != 0;
+ CrewMemberPenaltyButton.Disabled = !isSalaryCorrect;
+ }
+
+ private void BindSalaries(CcoConsoleSalaries salaries)
+ {
+ CrewMembersList.DisposeAllChildren();
+ CrewMembersList.RemoveAllChildren();
+
+ if (salaries.Records == null || !_cfg.GetCVar(SecretCCVars.EconomyEnabled))
+ return;
+
+ foreach (var record in salaries.Records)
+ {
+ var crewButton = GetCrewContainer(record);
+ CrewMembersList.AddChild(crewButton);
+ }
+ }
+
+ private void BindSelectedUserSalary(string userName,
+ string jobName,
+ KeyValuePair? record)
+ {
+ _selectedCrewMemberRecord = record;
+
+ CrewMemberName.Text = userName;
+ CrewMemberJob.Text = jobName;
+
+ if (record?.Value.Salary is not { } salary)
+ {
+ CrewMemberSalary.Text = Loc.GetString("cco-console-no-salary");
+
+ CrewMemberPenaltiesContainer.Visible = false;
+ CrewMemberBonusesContainer.Visible = false;
+
+ return;
+ }
+
+ CrewMemberSalary.Text = Loc.GetString("cco-console-salary", ("credits", salary.Salary));
+
+ BindSalaryPenalties(salary.SalaryPenalties);
+ BindSalaryBonuses(salary.SalaryBonuses);
+
+ if (salary.SalaryBonuses.Count == 0)
+ return;
+
+ CrewMemberBonusButton.Disabled = true;
+ CrewMemberBonusButton.ToolTip = Loc.GetString("cco-console-max-bonus-tooltip");
+ }
+
+ private void BindSalaryPenalties(List penalties)
+ {
+ if (penalties.Count == 0)
+ {
+ CrewMemberPenaltiesContainer.Visible = false;
+ return;
+ }
+
+ CrewMemberPenaltiesContainer.Visible = true;
+ CrewMemberPenaltiesSum.Text = Loc.GetString("cco-console-bonus-or-penalty",
+ ("credits", penalties.Sum(penalty => penalty.Penalty)),
+ ("count", penalties.Count));
+ }
+
+
+ private void BindSalaryBonuses(List bonuses)
+ {
+ if (bonuses.Count == 0)
+ {
+ CrewMemberBonusesContainer.Visible = false;
+ return;
+ }
+
+ CrewMemberBonusesContainer.Visible = true;
+ CrewMemberBonusesSum.Text = Loc.GetString("cco-console-bonus-or-penalty",
+ ("credits", bonuses.Sum(bonus => bonus.Bonus)),
+ ("count", bonuses.Count));
+ }
+
+ private ContainerButton GetCrewContainer(KeyValuePair record)
+ {
+ var button = new Button
+ {
+ HorizontalExpand = true,
+ };
+
+ var jobContainer = new BoxContainer
+ {
+ Orientation = BoxContainer.LayoutOrientation.Horizontal,
+ HorizontalExpand = true,
+ };
+
+ if (_prototypeManager.TryIndex(record.Value.JobIcon, out var proto))
+ {
+ var jobIcon = new TextureRect
+ {
+ TextureScale = new Vector2(2f, 2f),
+ Stretch = TextureRect.StretchMode.KeepCentered,
+ Texture = _spriteSystem.Frame0(proto.Icon),
+ Margin = new Thickness(5, 0, 5, 0),
+ };
+
+ jobContainer.AddChild(jobIcon);
+ }
+
+ var jobLabel = new Label
+ {
+ Text = record.Value.JobTitle,
+ HorizontalExpand = true,
+ ClipText = true,
+ };
+
+ var nameLabel = new Label
+ {
+ Text = record.Value.Name,
+ HorizontalExpand = true,
+ ClipText = true,
+ };
+
+ jobContainer.AddChild(jobLabel);
+ jobContainer.AddChild(nameLabel);
+
+ button.AddChild(jobContainer);
+ button.OnPressed += _ =>
+ {
+ CrewMemberSalaryContainer.Visible = true;
+ BindSelectedUserSalary(record.Value.Name, record.Value.JobTitle, record);
+ };
+
+ if (_selectedCrewMemberRecord is not { } selectedRecord)
+ return button;
+
+ if (record.Key == selectedRecord.Key)
+ BindSelectedUserSalary(record.Value.Name, record.Value.JobTitle, record);
+
+ return button;
+ }
+}
diff --git a/Content.Client/_Adventure/DarkStation/Roles/CCO/Console/CcoConsoleWindow.xaml b/Content.Client/_Adventure/DarkStation/Roles/CCO/Console/CcoConsoleWindow.xaml
new file mode 100644
index 00000000000..6066db166b4
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/Roles/CCO/Console/CcoConsoleWindow.xaml
@@ -0,0 +1,185 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_Adventure/DarkStation/Roles/CCO/Console/CcoConsoleWindow.xaml.cs b/Content.Client/_Adventure/DarkStation/Roles/CCO/Console/CcoConsoleWindow.xaml.cs
new file mode 100644
index 00000000000..fcb282f96b8
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/Roles/CCO/Console/CcoConsoleWindow.xaml.cs
@@ -0,0 +1,140 @@
+using Content.Client.UserInterface.Controls;
+using Content.Shared._Adventure.Roles.CCO;
+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.Configuration;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+using SecretCCVars = Content.Shared._Adventure.CCVars.SecretCCVars;
+
+namespace Content.Client._Adventure.Roles.CCO.Console;
+
+[GenerateTypedNameReferences]
+public sealed partial class CcoConsoleWindow : FancyWindow
+{
+ private readonly CcoConsoleBoundInterface _bui;
+ [Dependency] private readonly IConfigurationManager _cfg = default!;
+ [Dependency] private readonly IEntityManager _entityManager = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+
+ private readonly SpriteSystem _spriteSystem;
+ private CcoConsoleUIState? _state;
+
+ public CcoConsoleWindow(CcoConsoleBoundInterface bui)
+ {
+ IoCManager.InjectDependencies(this);
+ RobustXamlLoader.Load(this);
+ _bui = bui;
+
+ AnnounceButton.OnPressed += _ => _bui.SendAnnouncement(Rope.Collapse(MessageInput.TextRope));
+ StationOpenManifest.OnPressed += _ => _bui.OpenManifest();
+ MessageInput.OnKeyBindUp += AnnouncementOnOnTextChanged;
+ StationsSelector.OnItemSelected += ItemSelected;
+
+ _spriteSystem = _entityManager.System();
+ SetupSalaries();
+ SetupEmergencyShuttle();
+ }
+
+ private void ItemSelected(OptionButton.ItemSelectedEventArgs obj)
+ {
+ var netEntity = new NetEntity(obj.Id);
+ _bui.OnStationSelected(netEntity);
+ }
+
+ private void AnnouncementOnOnTextChanged(GUIBoundKeyEventArgs args)
+ {
+ AnnounceButton.Disabled = Rope.Collapse(MessageInput.TextRope).TrimStart() == "";
+ }
+
+ public void UpdateState(CcoConsoleUIState state)
+ {
+ _state = state;
+
+ if (state.ConsoleBase.SelectedStation is not { } selectedStation)
+ {
+ BindStationNotSelected();
+ BindAvailableStations(state.Stations);
+ return;
+ }
+
+ BindAvailableStations(state.Stations, selectedStation);
+ BindCcoConsoleBase(state.ConsoleBase);
+ BindSelectedStation(state.Stations.AvailableStation[selectedStation]);
+
+ CTabContainer.SetTabTitle(0, "Статус");
+ CTabContainer.SetTabTitle(1, "Объявления");
+ CTabContainer.SetTabTitle(2, "Спец.отряды");
+ CTabContainer.SetTabTitle(3, "Зарплаты");
+
+ CTabContainer.SetTabVisible(3, _cfg.GetCVar(SecretCCVars.EconomyEnabled));
+ }
+
+ private void BindStationNotSelected()
+ {
+ CTabContainer.Visible = false;
+ }
+
+ private void BindAvailableStations(CcoConsoleAvailableStations stations, NetEntity? selected = null)
+ {
+ StationsSelector.Clear();
+
+ foreach (var (key, value) in stations.AvailableStation)
+ {
+ StationsSelector.AddItem(value.StationName, key.Id);
+ if (key == selected)
+ StationsSelector.SelectId(key.Id);
+ }
+ }
+
+ private void BindSelectedStation(CcoConsoleStationState stationState)
+ {
+ CTabContainer.Visible = true;
+
+ BindStationAlert(stationState.AlertState);
+ BindEmergencyShuttle(stationState.EmergencyShuttle);
+ BindSpecialSquads(stationState.Squads);
+ BindSalaries(stationState.Salaries);
+ }
+
+ private void BindSpecialSquads(CcoConsoleSpecialSquadModel squads)
+ {
+ SpecialSquadContainer.DisposeAllChildren();
+ SpecialSquadContainer.RemoveAllChildren();
+
+ foreach (var squad in squads.Squads)
+ {
+ var control = new CcoConsoleSquadControl(_bui,
+ squad.Id,
+ squad.Name,
+ squad.Description,
+ squads.WasCalled
+ );
+ SpecialSquadContainer.AddChild(control);
+ }
+ }
+
+ private void BindCcoConsoleBase(CcoConsoleBase consoleBase)
+ {
+ if (FormattedMessage.TryFromMarkup(consoleBase.OperatorName, out var message))
+ CCOOperator.SetMessage(message);
+ else
+ CCOOperator.SetMessage(consoleBase.OperatorName);
+ }
+
+ private void BindStationAlert(CcoConsoleAlert alert)
+ {
+ if (FormattedMessage.TryFromMarkup(alert.AlertName, out var alertMessage))
+ StationAlertCode.SetMessage(alertMessage, defaultColor: alert.AlertColor);
+ else
+ StationAlertCode.SetMessage(alert.AlertName, defaultColor: alert.AlertColor);
+
+ if (FormattedMessage.TryFromMarkup(alert.AlertDescription, out var alertDescription))
+ StationAlertCodeDescription.SetMessage(alertDescription);
+ else
+ StationAlertCodeDescription.SetMessage(alert.AlertDescription);
+ }
+}
diff --git a/Content.Client/_Adventure/DarkStation/Roles/StationAI/UI/Borgs/StationAIBorgsInterface.cs b/Content.Client/_Adventure/DarkStation/Roles/StationAI/UI/Borgs/StationAIBorgsInterface.cs
new file mode 100644
index 00000000000..32c387b3605
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/Roles/StationAI/UI/Borgs/StationAIBorgsInterface.cs
@@ -0,0 +1,47 @@
+using Content.Shared._Adventure.Roles.StationAI.UI;
+
+namespace Content.Client._Adventure.Roles.StationAI.UI.Borgs;
+
+public sealed class StationAIBorgsInterface : BoundUserInterface
+{
+ private StationAIBorgsWindow? _window;
+
+ public StationAIBorgsInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ if (_window is not null)
+ return;
+
+ _window = new StationAIBorgsWindow();
+ _window.OnClose += Close;
+ _window.OpenCentered();
+ _window.TryUpdateBorgList += () => SendMessage(new StationAIRequestBorgsList());
+ _window.BackToBodyEvent += () => SendMessage(new StationAIRequestBackToBody());
+ _window.UseCamera += uid => SendMessage(new StationAIBorgCameraRequest(uid));
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ if (state is not StationAIBorgInterfaceState cast)
+ return;
+
+ _window?.UpdateBorgs(cast);
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ if (!disposing)
+ return;
+
+ _window?.Close();
+ _window?.Dispose();
+ }
+}
diff --git a/Content.Client/_Adventure/DarkStation/Roles/StationAI/UI/Borgs/StationAIBorgsWindow.xaml b/Content.Client/_Adventure/DarkStation/Roles/StationAI/UI/Borgs/StationAIBorgsWindow.xaml
new file mode 100644
index 00000000000..d9ba5a045ec
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/Roles/StationAI/UI/Borgs/StationAIBorgsWindow.xaml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_Adventure/DarkStation/Roles/StationAI/UI/Borgs/StationAIBorgsWindow.xaml.cs b/Content.Client/_Adventure/DarkStation/Roles/StationAI/UI/Borgs/StationAIBorgsWindow.xaml.cs
new file mode 100644
index 00000000000..545eb5fb72d
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/Roles/StationAI/UI/Borgs/StationAIBorgsWindow.xaml.cs
@@ -0,0 +1,137 @@
+using System.Linq;
+using System.Text;
+using Content.Client.UserInterface.Controls;
+using Content.Shared._Adventure.Roles.StationAI.UI;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Utility;
+
+namespace Content.Client._Adventure.Roles.StationAI.UI.Borgs;
+
+[GenerateTypedNameReferences]
+public sealed partial class StationAIBorgsWindow : FancyWindow
+{
+ private StationAIBorgInterfaceState? _actualState = null;
+
+ public StationAIBorgsWindow()
+ {
+ RobustXamlLoader.Load(this);
+
+ Refresh.OnPressed += _ => TryUpdateBorgList?.Invoke();
+ // ButtonSyncLaws.OnPressed += _ =>
+ // {
+ // var borg = GetSelectedBorg();
+ // if (borg != null)
+ // {
+ // SyncLaws?.Invoke(borg.Value);
+ // }
+ // };
+ ButtonUseCamera.OnPressed += _ =>
+ {
+ var borg = GetSelectedBorg();
+ if (borg != null)
+ UseCamera?.Invoke(borg.Value);
+ };
+ BackToBody.OnPressed += _ =>
+ {
+ BackToBodyEvent?.Invoke();
+ };
+ SubnetList.OnItemSelected += ItemSelected;
+
+ ChargeBar.MaxValue = 1f;
+ ChargeBar.MinValue = 0f;
+ }
+
+ public event Action? SyncLaws;
+
+ public event Action? UseCamera;
+
+ public event Action? BackToBodyEvent;
+
+ public event Action? TryUpdateBorgList;
+
+ private NetEntity? GetSelectedBorg()
+ {
+ var selectedItem = SubnetList.GetSelected().FirstOrDefault();
+ if (selectedItem?.Metadata is not StationAIBorgUIModel borgState)
+ return null;
+
+ return borgState.Borg;
+ }
+
+ private void ItemSelected(ItemList.ItemListSelectedEventArgs obj)
+ {
+ var meta = obj.ItemList[obj.ItemIndex].Metadata;
+ if (meta is not StationAIBorgUIModel borg)
+ return;
+
+ BorgName.Text = borg.Name;
+ BorgCoordinates.Text = borg.Coordinates;
+ ChargeLabel.Text = $"Заряд: {borg.Percent * 100f}%";
+ ChargeBar.Value = borg.Percent;
+
+ var laws = new StringBuilder();
+ foreach (var law in borg.Laws.Laws)
+ {
+ laws.Append(law.Order)
+ .Append(": ")
+ .Append(Loc.GetString(law.LawString))
+ .AppendLine();
+ }
+
+ BorgLaws.SetMessage(FormattedMessage.FromMarkup(laws.ToString()));
+ BorgInfoContainer.Visible = true;
+ }
+
+ public void UpdateBorgs(StationAIBorgInterfaceState state)
+ {
+ if (IsSameState(state))
+ return;
+
+ _actualState = state;
+
+ SubnetList.Clear();
+
+ if (state.Borgs.Count == 0)
+ {
+ Text.Visible = true;
+ BorgInfoContainer.Visible = false;
+ return;
+ }
+
+ Text.Visible = false;
+ BorgInfoContainer.Visible = false;
+
+ foreach (var borg in state.Borgs)
+ {
+ ItemList.Item camera = new(SubnetList)
+ {
+ Metadata = borg,
+ Text = borg.Name,
+ };
+
+ SubnetList.Add(camera);
+ }
+ }
+
+ public bool IsSameState(StationAIBorgInterfaceState state)
+ {
+ if (_actualState == null)
+ return false;
+
+ if (state.Borgs.Count != _actualState?.Borgs.Count)
+ return false;
+
+ foreach (var (borg, index) in state.Borgs.WithIndex())
+ {
+ if (!_actualState.Borgs.TryGetValue(index, out var oldBorg))
+ return false;
+
+ if (!oldBorg.Equals(borg))
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/Content.Client/_Adventure/DarkStation/Roles/StationAI/UI/Cameras/StationAICameras.xaml b/Content.Client/_Adventure/DarkStation/Roles/StationAI/UI/Cameras/StationAICameras.xaml
new file mode 100644
index 00000000000..fe81139ddba
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/Roles/StationAI/UI/Cameras/StationAICameras.xaml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_Adventure/DarkStation/Roles/StationAI/UI/Cameras/StationAICameras.xaml.cs b/Content.Client/_Adventure/DarkStation/Roles/StationAI/UI/Cameras/StationAICameras.xaml.cs
new file mode 100644
index 00000000000..07a87bb98e1
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/Roles/StationAI/UI/Cameras/StationAICameras.xaml.cs
@@ -0,0 +1,84 @@
+using Content.Client.Pinpointer.UI;
+using Content.Client.UserInterface.Controls;
+using Content.Shared._Adventure.Roles.StationAI.UI;
+using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Utility;
+
+namespace Content.Client._Adventure.Roles.StationAI.UI.Cameras;
+
+[GenerateTypedNameReferences]
+public sealed partial class StationAICameras : FancyWindow
+{
+ private readonly Texture? _blipTexture;
+ [Dependency] private readonly IEntityManager _entManager = default!;
+
+ private List _cameras = new();
+
+ public StationAICameras(EntityUid? mapUid)
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+
+ var textureSprite = new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png"));
+ _blipTexture = _entManager.System().Frame0(textureSprite);
+
+ if (!_entManager.TryGetComponent(mapUid, out var xform))
+ {
+ Close();
+ return;
+ }
+
+ NavMap.MapUid = xform.GridUid;
+ NavMap.TrackedEntitySelectedAction += camera =>
+ {
+ if (camera != null)
+ WarpToCamera?.Invoke(camera.Value);
+ };
+
+ Refresh.OnPressed += _ => UpdateCameraList();
+ BackToBody.OnPressed += _ => BackToBodyAction?.Invoke();
+ }
+
+ public event Action? TryUpdateCameraList;
+ public event Action? BackToBodyAction;
+ public event Action? WarpToCamera;
+
+ private void FillCameraList()
+ {
+ if (!NavMap.Visible || _blipTexture == null)
+ return;
+
+ NavMap.TrackedCoordinates.Clear();
+ NavMap.TrackedEntities.Clear();
+
+ foreach (var entry in _cameras)
+ {
+ var coordinates = _entManager.GetCoordinates(entry.Coordinates);
+ NavMap.TrackedEntities.TryAdd(
+ entry.Camera,
+ new NavMapBlip(
+ coordinates,
+ _blipTexture,
+ entry.Available ? Color.Green : Color.Red,
+ false,
+ entry.Available
+ )
+ );
+ }
+ }
+
+ public void UpdateCameraList(List? cameras = null)
+ {
+ if (cameras == null)
+ {
+ TryUpdateCameraList?.Invoke();
+ return;
+ }
+
+ _cameras = cameras;
+ FillCameraList();
+ }
+}
diff --git a/Content.Client/_Adventure/DarkStation/Roles/StationAI/UI/Cameras/StationAICamerasInterface.cs b/Content.Client/_Adventure/DarkStation/Roles/StationAI/UI/Cameras/StationAICamerasInterface.cs
new file mode 100644
index 00000000000..aef1626d9df
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/Roles/StationAI/UI/Cameras/StationAICamerasInterface.cs
@@ -0,0 +1,56 @@
+using Content.Shared._Adventure.Roles.StationAI.UI;
+
+namespace Content.Client._Adventure.Roles.StationAI.UI.Cameras;
+
+///
+/// Initializes a and updates it when new server messages are received.
+///
+public sealed class StationAICamerasInterface : BoundUserInterface
+{
+ private StationAICameras? _window;
+
+ public StationAICamerasInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ if (!EntMan.TryGetComponent(Owner, out TransformComponent? xForm))
+ return;
+
+ _window = new StationAICameras(xForm.GridUid);
+
+ if (State != null)
+ UpdateState(State);
+
+ _window.OpenCentered();
+
+ _window.TryUpdateCameraList += () => SendMessage(new StationAIRequestCameraList());
+ _window.BackToBodyAction += () => SendMessage(new StationAIRequestBackToBody());
+ _window.WarpToCamera += uid => SendMessage(new StationAISelectedCamera(uid));
+ }
+
+ ///
+ /// Update the UI state based on server-sent info
+ ///
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ if (_window == null || state is not StationAICamerasInterfaceState cast)
+ return;
+
+ _window.UpdateCameraList(cast.Cameras);
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ if (!disposing)
+ return;
+
+ _window?.Dispose();
+ }
+}
diff --git a/Content.Client/_Adventure/DarkStation/Roles/StationAI/UI/StationAIGhostAttemptSystem.cs b/Content.Client/_Adventure/DarkStation/Roles/StationAI/UI/StationAIGhostAttemptSystem.cs
new file mode 100644
index 00000000000..2e843ceff41
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/Roles/StationAI/UI/StationAIGhostAttemptSystem.cs
@@ -0,0 +1,20 @@
+using Content.Shared._Adventure.Roles.StationAI.Components;
+using Content.Shared.Interaction.Events;
+
+namespace Content.Client._Adventure.Roles.StationAI.UI;
+
+public sealed class StationAIGhostAttemptSystem : EntitySystem
+{
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnRichAttemptEvent);
+ }
+
+ private void OnRichAttemptEvent(EntityUid uid,
+ StationAIGhostComponent component,
+ CannotRichMessageAttemptEvent args)
+ {
+ args.Cancel();
+ }
+}
diff --git a/Content.Client/_Adventure/DarkStation/SCP/SCP049/SCP049System.cs b/Content.Client/_Adventure/DarkStation/SCP/SCP049/SCP049System.cs
new file mode 100644
index 00000000000..35b6fde96a7
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/SCP/SCP049/SCP049System.cs
@@ -0,0 +1,36 @@
+using Content.Shared._Adventure.SCP.SCP_049;
+using Content.Shared.StatusIcon;
+using Content.Shared.StatusIcon.Components;
+using Robust.Shared.Prototypes;
+using SCP049Component = Content.Shared._Adventure.SCP.SCP_049.Components.SCP049Component;
+using SCP049ThrallComponent = Content.Shared._Adventure.SCP.SCP_049.Components.SCP049ThrallComponent;
+
+namespace Content.Client._Adventure.SCP.SCP049;
+
+public sealed class SCP049System : SharedSCP049System
+{
+ [ValidatePrototypeId]
+ private const string SCPPlagueZombie = "SCPPlagueZombie";
+
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(GetStatusIcon);
+ SubscribeLocalEvent(GetThrallStatusIcon);
+ }
+
+ private void GetStatusIcon(Entity ent, ref GetStatusIconsEvent args)
+ {
+ var icon = _prototype.Index(SCPPlagueZombie);
+ args.StatusIcons.Add(icon);
+ }
+
+ private void GetThrallStatusIcon(Entity ent, ref GetStatusIconsEvent args)
+ {
+ var icon = _prototype.Index(SCPPlagueZombie);
+ args.StatusIcons.Add(icon);
+ }
+}
diff --git a/Content.Client/_Adventure/DarkStation/SCP/SCP173System.cs b/Content.Client/_Adventure/DarkStation/SCP/SCP173System.cs
new file mode 100644
index 00000000000..f41ffdde6f7
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/SCP/SCP173System.cs
@@ -0,0 +1,40 @@
+using Content.Shared._Adventure.SCP._173;
+using Content.Shared.Interaction.Events;
+using Content.Shared.Movement.Components;
+using Robust.Shared.Timing;
+
+namespace Content.Client._Adventure.SCP;
+
+public sealed class SCP173System : SharedSCP173System
+{
+ [Dependency] private readonly IGameTiming _timing = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnTryAttack);
+ SubscribeLocalEvent(OnLookStateChanged);
+ }
+
+ private void OnLookStateChanged(EntityUid uid, SCP173FreezeComponent component, OnLookStateChangedEvent args)
+ {
+ if (!args.IsLookedAt)
+ return;
+
+ if (TryComp(uid, out var input))
+ input.CanMove = false;
+ }
+
+ private void OnTryAttack(EntityUid uid, SCP173FreezeComponent component, AttackAttemptEvent args)
+ {
+ if (!_timing.IsFirstTimePredicted)
+ return;
+
+ var target = args.Target.GetValueOrDefault();
+ if (!target.IsValid())
+ return;
+
+ if (!CanAttack(uid, target, component))
+ args.Cancel();
+ }
+}
diff --git a/Content.Client/_Adventure/DarkStation/Sponsors/PatronCategoryNotAvailable.xaml b/Content.Client/_Adventure/DarkStation/Sponsors/PatronCategoryNotAvailable.xaml
new file mode 100644
index 00000000000..893417defd3
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/Sponsors/PatronCategoryNotAvailable.xaml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_Adventure/DarkStation/Sponsors/PatronCategoryNotAvailable.xaml.cs b/Content.Client/_Adventure/DarkStation/Sponsors/PatronCategoryNotAvailable.xaml.cs
new file mode 100644
index 00000000000..25913b5e5c1
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/Sponsors/PatronCategoryNotAvailable.xaml.cs
@@ -0,0 +1,19 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client._c4llv07e.Sponsors;
+
+[GenerateTypedNameReferences]
+public sealed partial class PatronCategoryNotAvailable : Control
+{
+ [Dependency] private readonly IUriOpener _uri = default!;
+
+ public PatronCategoryNotAvailable()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+
+ Boosty.OnPressed += _ => _uri.OpenUri("https://boosty.to/darkeneez"); //TODO BY UR
+ }
+}
diff --git a/Content.Client/_Adventure/DarkStation/Sponsors/PatronItemsPicker.xaml b/Content.Client/_Adventure/DarkStation/Sponsors/PatronItemsPicker.xaml
new file mode 100644
index 00000000000..23cf84c3aba
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/Sponsors/PatronItemsPicker.xaml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_Adventure/DarkStation/Sponsors/PatronItemsPicker.xaml.cs b/Content.Client/_Adventure/DarkStation/Sponsors/PatronItemsPicker.xaml.cs
new file mode 100644
index 00000000000..32a30900795
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/Sponsors/PatronItemsPicker.xaml.cs
@@ -0,0 +1,286 @@
+using System.Linq;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.ResourceManagement;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client._c4llv07e.Sponsors;
+
+[GenerateTypedNameReferences]
+public sealed partial class PatronItemsPicker : Control
+{
+ // [Dependency] private readonly SponsorsManager _sponsorsManager = default!; //TODO BY UR
+ [Dependency] private readonly IPrototypeManager _prototypeMan = default!;
+ [Dependency] private readonly IResourceCache _resource = default!;
+ [Dependency] private readonly IEntityManager _entMan = default!;
+
+ private readonly Dictionary _cachedItems = new();
+
+ public Action>? ItemsChanged;
+ public Action? PetChanged;
+
+ private ItemList.Item? _selectedUiItem;
+ private ItemList.Item? _selectedUiUsedItem;
+ private List _selectedItems = [];
+
+ private (string, string)? _selectedPet;
+ private EntityUid? _dummyPetPreview;
+
+ // private SponsorTier? _tier; //TODO BY UR
+
+ public PatronItemsPicker()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+
+ InitializeItems();
+ InitializePets();
+ }
+
+ public void SetPatronData(List selectedItems, string petId, string petName)
+ {
+ // _sponsorsManager.TryGetInfo(out var sponsorInfo); //TODO BY UR
+ // _tier = sponsorInfo; //TODO BY UR
+
+ SetupPatronItems(selectedItems);
+ SetupPatronPets(petId, petName);
+ }
+
+ private void SetupPatronItems(List selectedItems)
+ {
+ // var tierAvailableItems = _tier?.AvailableItems; //TODO BY UR
+ // if (tierAvailableItems > 0)
+ // {
+ // _selectedItems = selectedItems.Select(item => _prototypeMan.Index(item)).ToList();
+ // UpdateAvailableItems();
+ // PopulateItems("");
+ // return;
+ // }
+
+ CPatronCategoryItems.Visible = true;
+ CPatronItemsContainer.Visible = false;
+ }
+
+ private void UpdateAvailableItems()
+ {
+ // var availableItems = _tier?.AvailableItems - _selectedItems.Count; //TODO BY UR
+ // CPatronItemsAvailableCount.Text = $"{availableItems}";
+ // CPatronAddItem.Disabled = availableItems <= 0;
+ }
+
+ private void SetupPatronPets(string petId, string petName)
+ {
+ // if (_tier?.PetCategories.Count > 0) //TODO BY UR
+ // {
+ // if (!string.IsNullOrEmpty(petId))
+ // {
+ // _selectedPet = (petId, petName);
+ // CPetName.Text = petName;
+ // UpdatePetSprite(petId);
+ // }
+ //
+ // PopulatePets("");
+ // return;
+ // }
+
+ CPatronCategoryPet.Visible = true;
+ CPatronPetContainer.Visible = false;
+ }
+
+ #region Items
+
+ private void InitializeItems()
+ {
+ CSearch.OnTextChanged += args => PopulateItems(args.Text);
+
+ CPatronAvailableItems.OnItemSelected += args => _selectedUiItem = CPatronAvailableItems[args.ItemIndex];
+ CPatronSelectedItems.OnItemSelected += args => _selectedUiUsedItem = CPatronSelectedItems[args.ItemIndex];
+
+ CPatronAddItem.OnPressed += _ => AddItem();
+ CPatronRemoveItem.OnPressed += _ => RemoveItem();
+
+ _prototypeMan.PrototypesReloaded += OnProtoReload;
+ LoadItems();
+ }
+
+ private void OnProtoReload(PrototypesReloadedEventArgs obj)
+ {
+ // if (!obj.WasModified()) //TODO BY UR
+ // return;
+
+ LoadItems();
+ PopulateItems("");
+ }
+
+ private void LoadItems()
+ {
+ _cachedItems.Clear();
+
+ // var prototypes = _prototypeMan.EnumeratePrototypes(); //TODO BY UR
+ // foreach (var prototype in prototypes)
+ // {
+ // foreach (var item in prototype.Items)
+ // {
+ // if (!_prototypeMan.TryIndex(item, out var itemPrototype))
+ // continue;
+ //
+ // var spriteTextures = SpriteComponent.GetPrototypeTextures(itemPrototype, _resource).First();
+ // var sprite = spriteTextures.Default;
+ //
+ // _cachedItems[itemPrototype] = sprite;
+ // }
+ // }
+ }
+
+ private void AddItem()
+ {
+ if (_selectedUiItem == null)
+ return;
+
+ CPatronAvailableItems.Remove(_selectedUiItem);
+
+ var item = new ItemList.Item(CPatronSelectedItems)
+ {
+ Text = _selectedUiItem.Text,
+ Icon = _selectedUiItem.Icon,
+ Metadata = _selectedUiItem.Metadata
+ };
+ CPatronSelectedItems.Insert(0, item);
+
+ if (item.Metadata is { } metadata)
+ {
+ _selectedItems.Add((EntityPrototype) metadata);
+ }
+
+ _selectedUiItem = null;
+
+ UpdateAvailableItems();
+ OnItemsChanged();
+ }
+
+ private void RemoveItem()
+ {
+ if (_selectedUiUsedItem == null)
+ return;
+
+ CPatronSelectedItems.Remove(_selectedUiUsedItem);
+
+ var item = new ItemList.Item(CPatronAvailableItems)
+ {
+ Text = _selectedUiUsedItem.Text,
+ Icon = _selectedUiUsedItem.Icon,
+ Metadata = _selectedUiUsedItem.Metadata
+ };
+ CPatronAvailableItems.Insert(0, item);
+
+ if (item.Metadata is { } metadata)
+ {
+ _selectedItems.Remove((EntityPrototype) metadata);
+ }
+
+ _selectedUiUsedItem = null;
+
+ UpdateAvailableItems();
+ OnItemsChanged();
+ }
+
+ private void OnItemsChanged()
+ {
+ var items = _selectedItems.Select(item => item.ID).ToList();
+ ItemsChanged?.Invoke(items);
+ }
+
+ private void OnPetChanged()
+ {
+ if (_selectedPet is not { } pet)
+ return;
+
+ PetChanged?.Invoke(pet.Item1, pet.Item2);
+ }
+
+ private void PopulateItems(string filter)
+ {
+ CPatronAvailableItems.Clear();
+ CPatronSelectedItems.Clear();
+
+ foreach (var (item, sprite) in _cachedItems)
+ {
+ if (!string.IsNullOrEmpty(filter) && !item.Name.Contains(filter, StringComparison.OrdinalIgnoreCase))
+ continue;
+
+ var listItem = _selectedItems.Contains(item)
+ ? CPatronSelectedItems.AddItem(item.Name, sprite)
+ : CPatronAvailableItems.AddItem(item.Name, sprite);
+ listItem.Metadata = item;
+ }
+ }
+
+ #endregion
+
+ #region Pets
+
+ private void InitializePets()
+ {
+ CPetsList.OnItemSelected += OnPetSelected;
+ CPetName.OnTextChanged += _ => OnPetNameChanged();
+ }
+
+ private void OnPetSelected(ItemList.ItemListSelectedEventArgs args)
+ {
+ var item = CPetsList[args.ItemIndex];
+ if (item.Metadata is not EntityPrototype petEntity)
+ return;
+
+ _selectedPet = (petEntity.ID, CPetName.Text);
+ UpdatePetSprite(petEntity.ID);
+ OnPetChanged();
+ }
+
+ private void OnPetNameChanged()
+ {
+ if (_selectedPet == null)
+ return;
+
+ _selectedPet = (_selectedPet.Value.Item1, CPetName.Text);
+ OnPetChanged();
+ }
+
+ private void UpdatePetSprite(string petId)
+ {
+ if (_dummyPetPreview != null)
+ _entMan.DeleteEntity(_dummyPetPreview);
+
+ _dummyPetPreview = _entMan.SpawnEntity(petId, MapCoordinates.Nullspace);
+ CPetSpriteView.SetEntity(_dummyPetPreview);
+ }
+
+ private void PopulatePets(string filter)
+ {
+ CPetsList.Clear();
+
+ // var prototypes = _prototypeMan.EnumeratePrototypes(); //TODO BY UR
+ // foreach (var category in prototypes)
+ // {
+ // foreach (var petId in category.Pets)
+ // {
+ // if (!_prototypeMan.TryIndex(petId, out var petPrototype))
+ // continue;
+ //
+ // if (!petPrototype.Name.Contains(filter))
+ // continue;
+ //
+ // var spriteTextures = SpriteComponent.GetPrototypeTextures(petPrototype, _resource).First();
+ // var sprite = spriteTextures.Default;
+ //
+ // var item = CPetsList.AddItem(petPrototype.Name, sprite);
+ // item.Metadata = petPrototype;
+ // }
+ // }
+ }
+
+ #endregion
+}
diff --git a/Content.Client/_Adventure/DarkStation/Zombie/ZombieSmokerOverlay.cs b/Content.Client/_Adventure/DarkStation/Zombie/ZombieSmokerOverlay.cs
new file mode 100644
index 00000000000..d3795102b79
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/Zombie/ZombieSmokerOverlay.cs
@@ -0,0 +1,64 @@
+using Content.Shared._Adventure.Zombie.Smoker.Components;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Shared.Enums;
+
+namespace Content.Client._Adventure.Zombie;
+
+public sealed class ZombieSmokerOverlay : Overlay
+{
+ private readonly IEntityManager _entManager;
+
+ public ZombieSmokerOverlay(IEntityManager entManager)
+ {
+ _entManager = entManager;
+ }
+
+ public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV;
+
+ protected override void Draw(in OverlayDrawArgs args)
+ {
+ var worldHandle = args.WorldHandle;
+
+ var query = _entManager.EntityQueryEnumerator();
+ var xformQuery = _entManager.GetEntityQuery();
+ var targets = _entManager.GetEntityQuery();
+
+ var xformSystem = _entManager.System();
+ var spriteSystem = _entManager.System();
+
+ while (query.MoveNext(out var uid, out var smoker, out var xForm))
+ {
+ var target = smoker.CurrentTarget;
+ if (target == EntityUid.Invalid)
+ continue;
+
+ if (!targets.HasComponent(target))
+ continue;
+
+ if (!xformQuery.TryGetComponent(target, out var targetXForm))
+ continue;
+
+ if (xForm.MapID != targetXForm.MapID)
+ continue;
+
+ var texture = spriteSystem.Frame0(smoker.Tongue);
+ var width = texture.Width / (float)EyeManager.PixelsPerMeter;
+
+ var coordsA = xForm.Coordinates;
+ var coordsB = targetXForm.Coordinates;
+
+ var posA = coordsA.ToMapPos(_entManager, xformSystem);
+ var posB = coordsB.ToMapPos(_entManager, xformSystem);
+ var diff = posB - posA;
+ var length = diff.Length();
+
+ var midPoint = diff / 2f + posA;
+ var angle = (posB - posA).ToWorldAngle();
+ var box = new Box2(-width / 2f, -length / 2f, width / 2f, length / 2f);
+ var rotate = new Box2Rotated(box.Translated(midPoint), angle, midPoint);
+
+ worldHandle.DrawTextureRect(texture, rotate);
+ }
+ }
+}
diff --git a/Content.Client/_Adventure/DarkStation/Zombie/ZombieSmokerSystem.cs b/Content.Client/_Adventure/DarkStation/Zombie/ZombieSmokerSystem.cs
new file mode 100644
index 00000000000..1c0a21b7336
--- /dev/null
+++ b/Content.Client/_Adventure/DarkStation/Zombie/ZombieSmokerSystem.cs
@@ -0,0 +1,54 @@
+using Content.Shared._Adventure.Zombie.Smoker;
+using Content.Shared._Adventure.Zombie.Smoker.Components;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.Player;
+using Robust.Shared.Input;
+using Robust.Shared.Physics;
+using Robust.Shared.Physics.Dynamics.Joints;
+
+namespace Content.Client._Adventure.Zombie;
+
+public sealed class ZombieSmokerSystem : SharedZombieSmokerSystem
+{
+ [Dependency] private readonly InputSystem _input = default!;
+ [Dependency] private readonly IOverlayManager _overlay = default!;
+ [Dependency] private readonly IPlayerManager _player = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ _overlay.AddOverlay(new ZombieSmokerOverlay(EntityManager));
+ }
+
+ public override void Shutdown()
+ {
+ base.Shutdown();
+ _overlay.RemoveOverlay();
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ if (!Timing.IsFirstTimePredicted)
+ return;
+
+ var player = _player.LocalSession?.AttachedEntity;
+ if (player == null || !TryComp(player, out var smoker))
+ return;
+
+ if (!TryComp(player, out var jointComp) ||
+ !jointComp.GetJoints.TryGetValue(SmokeCufJoint, out var joint) || joint is not DistanceJoint distance)
+ return;
+
+ if (distance.MaxLength <= distance.MinLength)
+ return;
+
+ var reelKey = _input.CmdStates.GetState(EngineKeyFunctions.UseSecondary) == BoundKeyState.Down;
+ if (smoker.Reeling == reelKey)
+ return;
+
+ RaisePredictiveEvent(new ZombieSmokerMoveTargetRequestEvent(reelKey));
+ }
+}
diff --git a/Content.Client/_Adventure/ITargetDollWidgetBridge.cs b/Content.Client/_Adventure/ITargetDollWidgetBridge.cs
new file mode 100644
index 00000000000..b896ca8a730
--- /dev/null
+++ b/Content.Client/_Adventure/ITargetDollWidgetBridge.cs
@@ -0,0 +1,43 @@
+using Content.Shared.Body.Part;
+using Robust.Client.UserInterface;
+
+namespace Content.Client._c4llv07e.Bridges;
+
+public interface ITargetDollWidgetBridge
+{
+ public void SelectBodyPart(BodyPartType? targetBodyPart, BodyPartSymmetry bodyPartSymmetry);
+ public void Clear();
+ public void Hide();
+ public void Show();
+ public void InitializeWidget();
+ public void SetupWidget(Control surface);
+}
+
+public sealed class StubTargetDollWidgetBridge : ITargetDollWidgetBridge
+{
+ public void SelectBodyPart(BodyPartType? targetBodyPart, BodyPartSymmetry bodyPartSymmetry)
+ {
+ }
+
+ public void Clear()
+ {
+
+ }
+
+ public void Hide()
+ {
+ }
+
+ public void Show()
+ {
+ }
+
+ public void InitializeWidget()
+ {
+ }
+
+ public void SetupWidget(Control surface)
+ {
+
+ }
+}
diff --git a/Content.IntegrationTests/Tests/Body/SaveLoadReparentTest.cs b/Content.IntegrationTests/Tests/Body/SaveLoadReparentTest.cs
index 01482ba8ee2..4a3b18dd95a 100644
--- a/Content.IntegrationTests/Tests/Body/SaveLoadReparentTest.cs
+++ b/Content.IntegrationTests/Tests/Body/SaveLoadReparentTest.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Linq;
using Content.Shared.Body.Components;
using Content.Shared.Body.Systems;
diff --git a/Content.Server.Database/Model.cs b/Content.Server.Database/Model.cs
index 570fbbe074d..80b2717901b 100644
--- a/Content.Server.Database/Model.cs
+++ b/Content.Server.Database/Model.cs
@@ -45,6 +45,8 @@ protected ServerDbContext(DbContextOptions options) : base(options)
public DbSet AdminMessages { get; set; } = null!;
public DbSet RoleWhitelists { get; set; } = null!;
public DbSet BanTemplate { get; set; } = null!;
+ public DbSet PatronProfilePets { get; set; } = null!;
+
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
@@ -370,6 +372,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
.OwnsOne(p => p.HWId)
.Property(p => p.Type)
.HasDefaultValue(HwidType.Legacy);
+ modelBuilder.Entity()
+ .HasIndex(p => new { HumanoidProfileId = p.ProfileId, p.PetId })
+ .IsUnique();
}
public virtual IQueryable SearchLogs(IQueryable query, string searchText)
@@ -403,6 +408,8 @@ public class Profile
public int Age { get; set; }
public string Sex { get; set; } = null!;
public string Gender { get; set; } = null!;
+ public int BankBalance { get; set; }
+
public string Species { get; set; } = null!;
public string Voice { get; set; } = null!; // c4llv07e tts
[Column(TypeName = "jsonb")] public JsonDocument? Markings { get; set; } = null!;
@@ -413,6 +420,8 @@ public class Profile
public string EyeColor { get; set; } = null!;
public string SkinColor { get; set; } = null!;
public int SpawnPriority { get; set; } = 0;
+ public PatronProfilePet PatronProfilePet { get; set; } = null!;
+
public List Jobs { get; } = new();
public List Antags { get; } = new();
public List Traits { get; } = new();
@@ -425,6 +434,15 @@ public class Profile
public Preference Preference { get; set; } = null!;
}
+ public class PatronProfilePet
+ {
+ public int Id { get; set; }
+ public string PetId { get; set; } = null!;
+ public string PetName { get; set; } = null!;
+ public Profile Profile { get; set; } = null!;
+ public int ProfileId { get; set; }
+ }
+
public class Job
{
public int Id { get; set; }
diff --git a/Content.Server/Administration/Commands/AddBodyPartCommand.cs b/Content.Server/Administration/Commands/AddBodyPartCommand.cs
index 892a88d41ae..0ef73664cc2 100644
--- a/Content.Server/Administration/Commands/AddBodyPartCommand.cs
+++ b/Content.Server/Administration/Commands/AddBodyPartCommand.cs
@@ -12,7 +12,7 @@ public sealed class AddBodyPartCommand : IConsoleCommand
public string Command => "addbodypart";
public string Description => "Adds a given entity to a containing body.";
- public string Help => "Usage: addbodypart ";
+ public string Help => "Usage: addbodypart ";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
@@ -38,10 +38,14 @@ public void Execute(IConsoleShell shell, string argStr, string[] args)
var parentId = _entManager.GetEntity(parentNetId);
var bodySystem = _entManager.System();
-
+ var partSymmetry = BodyPartSymmetry.None;
+ if (args.Length == 5)
+ {
+ Enum.TryParse(args[4], out partSymmetry);
+ }
if (Enum.TryParse(args[3], out var partType) &&
- bodySystem.TryCreatePartSlotAndAttach(parentId, args[2], childId, partType))
+ bodySystem.TryCreatePartSlotAndAttach(parentId, args[2], childId, partType, partSymmetry))
{
shell.WriteLine($@"Added {childId} to {parentId}.");
}
diff --git a/Content.Server/Antag/AntagSelectionSystem.cs b/Content.Server/Antag/AntagSelectionSystem.cs
index 45daa7a312f..4069ff1db29 100644
--- a/Content.Server/Antag/AntagSelectionSystem.cs
+++ b/Content.Server/Antag/AntagSelectionSystem.cs
@@ -13,7 +13,6 @@
using Content.Server.Shuttles.Components;
using Content.Shared.Antag;
using Content.Shared.Clothing;
-using Content.Shared.GameTicking;
using Content.Shared.GameTicking.Components;
using Content.Shared.Ghost;
using Content.Shared.Humanoid;
@@ -102,6 +101,8 @@ private void OnPlayerSpawning(RulePlayerSpawningEvent args)
args.PlayerPool.Remove(session);
GameTicker.PlayerJoinGame(session);
}
+ var ev = new AntagSelectionEnd(comp.SelectedSessions, AntagSelectionTime.PrePlayerSpawn);
+ RaiseLocalEvent(uid, ref ev);
}
}
@@ -114,7 +115,10 @@ private void OnJobsAssigned(RulePlayerJobsAssignedEvent args)
continue;
ChooseAntags((uid, comp), args.Players);
+ var ev = new AntagSelectionEnd(comp.SelectedSessions, AntagSelectionTime.PostPlayerSpawn);
+ RaiseLocalEvent(uid, ref ev);
}
+
}
private void OnSpawnComplete(PlayerSpawnCompleteEvent args)
@@ -297,7 +301,7 @@ public void MakeAntag(Entity ent, ICommonSession? sessi
if (!antagEnt.HasValue)
{
- var getEntEv = new AntagSelectEntityEvent(session, ent);
+ var getEntEv = new AntagSelectEntityEvent(session, ent, def.PrefRoles);
RaiseLocalEvent(ent, ref getEntEv, true);
antagEnt = getEntEv.Entity;
}
@@ -310,7 +314,7 @@ public void MakeAntag(Entity ent, ICommonSession? sessi
return;
}
- var getPosEv = new AntagSelectLocationEvent(session, ent);
+ var getPosEv = new AntagSelectLocationEvent(session, ent, player);
RaiseLocalEvent(ent, ref getPosEv, true);
if (getPosEv.Handled)
{
@@ -488,7 +492,7 @@ private void OnObjectivesTextGetInfo(Entity ent, ref Ob
/// Only raised if the selected player's current entity is invalid.
///
[ByRefEvent]
-public record struct AntagSelectEntityEvent(ICommonSession? Session, Entity GameRule)
+public record struct AntagSelectEntityEvent(ICommonSession? Session, Entity GameRule, List> PrefRoles)
{
public readonly ICommonSession? Session = Session;
@@ -501,7 +505,7 @@ public record struct AntagSelectEntityEvent(ICommonSession? Session, Entity
[ByRefEvent]
-public record struct AntagSelectLocationEvent(ICommonSession? Session, Entity GameRule)
+public record struct AntagSelectLocationEvent(ICommonSession? Session, Entity GameRule, EntityUid Entity)
{
public readonly ICommonSession? Session = Session;
@@ -516,3 +520,6 @@ public record struct AntagSelectLocationEvent(ICommonSession? Session, Entity
[ByRefEvent]
public readonly record struct AfterAntagEntitySelectedEvent(ICommonSession? Session, EntityUid EntityUid, Entity GameRule, AntagSelectionDefinition Def);
+
+[ByRefEvent]
+public readonly record struct AntagSelectionEnd(IEnumerable? Sessions, AntagSelectionTime selectionTime);
diff --git a/Content.Server/Bible/BibleSystem.cs b/Content.Server/Bible/BibleSystem.cs
index 76efe3290bf..25879c9e31c 100644
--- a/Content.Server/Bible/BibleSystem.cs
+++ b/Content.Server/Bible/BibleSystem.cs
@@ -1,15 +1,20 @@
+using Content.Server._c4llv07e.Bridges;
using Content.Server.Bible.Components;
+using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Server.Ghost.Roles.Events;
using Content.Server.Popups;
using Content.Shared.ActionBlocker;
using Content.Shared.Actions;
+using Content.Shared.Chemistry.Reagent;
using Content.Shared.Bible;
+using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Damage;
using Content.Shared.Ghost.Roles.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Inventory;
using Content.Shared.Mobs;
+using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
using Content.Shared.Timing;
@@ -33,6 +38,14 @@ public sealed class BibleSystem : EntitySystem
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly UseDelaySystem _delay = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
+ [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
+ [Dependency] private readonly ISaintedBridge _saintedBridge = default!;
+
+ [ValidatePrototypeId]
+ private const string Water = "Water";
+
+ [ValidatePrototypeId]
+ private const string Holywater = "Holywater";
public override void Initialize()
{
@@ -60,12 +73,14 @@ public override void Update(float frameTime)
{
EnsureComp(entity);
}
+
_addQueue.Clear();
foreach (var entity in _remQueue)
{
RemComp(entity);
}
+
_remQueue.Clear();
var query = EntityQueryEnumerator();
@@ -76,12 +91,14 @@ public override void Update(float frameTime)
{
continue;
}
+
// Clean up the old body
if (summonableComp.Summon != null)
{
EntityManager.DeleteEntity(summonableComp.Summon.Value);
summonableComp.Summon = null;
}
+
summonableComp.AlreadySummoned = false;
_popupSystem.PopupEntity(Loc.GetString("bible-summon-respawn-ready", ("book", uid)), uid, PopupType.Medium);
_audio.PlayPvs("/Audio/Effects/radpulse9.ogg", uid, AudioParams.Default.WithVolume(-4f));
@@ -99,10 +116,6 @@ private void OnAfterInteract(EntityUid uid, BibleComponent component, AfterInter
if (!TryComp(uid, out UseDelayComponent? useDelay) || _delay.IsDelayed((uid, useDelay)))
return;
- if (args.Target == null || args.Target == args.User || !_mobStateSystem.IsAlive(args.Target.Value))
- {
- return;
- }
if (!HasComp(args.User))
{
@@ -115,8 +128,29 @@ private void OnAfterInteract(EntityUid uid, BibleComponent component, AfterInter
return;
}
+ if (args.Target == null)
+ return;
+
+ if (_saintedBridge.TryMakeSainted(args.User, args.Target.Value))
+ {
+ _audio.PlayEntity(component.HealSoundPath.GetSound(), Filter.Pvs(args.Target.Value), args.User, true);
+ return;
+ }
+
+ if (HasComp(args.Target) && !HasComp(args.Target))
+ {
+ MakeWaterSaint(uid, args.Target.Value, component);
+ return;
+ }
+
+ if (args.Target == args.User || !_mobStateSystem.IsAlive(args.Target.Value))
+ {
+ return;
+ }
+
// This only has a chance to fail if the target is not wearing anything on their head and is not a familiar.
- if (!_invSystem.TryGetSlotEntity(args.Target.Value, "head", out var _) && !HasComp(args.Target.Value))
+ if (!_invSystem.TryGetSlotEntity(args.Target.Value, "head", out var _) &&
+ !HasComp(args.Target.Value))
{
if (_random.Prob(component.FailChance))
{
@@ -155,9 +189,43 @@ private void OnAfterInteract(EntityUid uid, BibleComponent component, AfterInter
}
}
+ private void MakeWaterSaint(EntityUid user, EntityUid target, BibleComponent component)
+ {
+ if (!TryComp(target, out var managerComponent))
+ return;
+
+ var waterReagentId = new ReagentId(Water, null);
+ var saintWater = new ReagentId(Holywater, null);
+ var isSainted = false;
+
+ foreach (var (_, (_, solution)) in _solutionContainerSystem.EnumerateSolutions((target, managerComponent)))
+ {
+ var waterInSolution = solution.Solution.GetReagentQuantity(waterReagentId);
+ if (waterInSolution <= 0)
+ continue;
+
+ solution.Solution.RemoveReagent(waterReagentId, waterInSolution);
+ solution.Solution.AddReagent(saintWater, waterInSolution);
+
+ // _solutionContainerSystem.UpdateChemicals(solution); TODO
+
+ isSainted = true;
+ }
+
+ if (!isSainted)
+ {
+ _popupSystem.PopupEntity("В емкости нет простой воды!", target, PopupType.Large);
+ return;
+ }
+
+ _popupSystem.PopupEntity("Простая вода в емкости стала святой!", target, PopupType.Large);
+ _audio.PlayEntity(component.HealSoundPath.GetSound(), Filter.Pvs(target), user, true);
+ }
+
private void AddSummonVerb(EntityUid uid, SummonableComponent component, GetVerbsEvent args)
{
- if (!args.CanInteract || !args.CanAccess || component.AlreadySummoned || component.SpecialItemPrototype == null)
+ if (!args.CanInteract || !args.CanAccess || component.AlreadySummoned ||
+ component.SpecialItemPrototype == null)
return;
if (component.RequiresBibleUser && !HasComp(args.User))
@@ -244,6 +312,7 @@ private void AttemptSummon(Entity ent, EntityUid user, Tran
_popupSystem.PopupEntity(Loc.GetString("bible-summon-requested"), user, user, PopupType.Medium);
_transform.SetParent(familiar, uid);
}
+
component.AlreadySummoned = true;
_actionsSystem.RemoveAction(user, component.SummonActionEntity);
}
diff --git a/Content.Server/Body/Commands/AddHandCommand.cs b/Content.Server/Body/Commands/AddHandCommand.cs
index 3e006c539c7..13302c93e0e 100644
--- a/Content.Server/Body/Commands/AddHandCommand.cs
+++ b/Content.Server/Body/Commands/AddHandCommand.cs
@@ -135,7 +135,7 @@ public void Execute(IConsoleShell shell, string argStr, string[] args)
var slotId = part.GetHashCode().ToString();
- if (!bodySystem.TryCreatePartSlotAndAttach(attachAt.Id, slotId, hand, BodyPartType.Hand, attachAt.Component, part))
+ if (!bodySystem.TryCreatePartSlotAndAttach(attachAt.Id, slotId, hand, BodyPartType.Hand, BodyPartSymmetry.None, attachAt.Component, part))
{
shell.WriteError($"Couldn't create a slot with id {slotId} on entity {_entManager.ToPrettyString(entity)}");
return;
diff --git a/Content.Server/Body/Commands/AttachBodyPartCommand.cs b/Content.Server/Body/Commands/AttachBodyPartCommand.cs
index 82f71619370..5de199fac25 100644
--- a/Content.Server/Body/Commands/AttachBodyPartCommand.cs
+++ b/Content.Server/Body/Commands/AttachBodyPartCommand.cs
@@ -108,7 +108,7 @@ public void Execute(IConsoleShell shell, string argStr, string[] args)
else
{
var (rootPartId, rootPart) = bodySystem.GetRootPartOrNull(bodyId, body)!.Value;
- if (!bodySystem.TryCreatePartSlotAndAttach(rootPartId, slotId, partUid.Value, part.PartType, rootPart, part))
+ if (!bodySystem.TryCreatePartSlotAndAttach(rootPartId, slotId, partUid.Value, part.PartType, part.Symmetry, rootPart, part))
{
shell.WriteError($"Could not create slot {slotId} on entity {_entManager.ToPrettyString(bodyId)}");
return;
diff --git a/Content.Server/Body/Events/OnEntityBreath.cs b/Content.Server/Body/Events/OnEntityBreath.cs
new file mode 100644
index 00000000000..1b4c19f0ea0
--- /dev/null
+++ b/Content.Server/Body/Events/OnEntityBreath.cs
@@ -0,0 +1,4 @@
+namespace Content.Server.Body.Events;
+
+[ByRefEvent]
+public record struct OnEntityBreathGas(string? Reagent, float Amount);
diff --git a/Content.Server/Body/Events/OnEntityMetabolize.cs b/Content.Server/Body/Events/OnEntityMetabolize.cs
new file mode 100644
index 00000000000..f738565da01
--- /dev/null
+++ b/Content.Server/Body/Events/OnEntityMetabolize.cs
@@ -0,0 +1,14 @@
+using Content.Server.Body.Components;
+using Content.Shared.Chemistry.Components;
+using Content.Shared.FixedPoint;
+
+namespace Content.Server.Body.Events;
+
+[ByRefEvent]
+public record struct OnEntityMetabolize(MetabolismGroupEntry Entry, FixedPoint2 MostToRemove);
+
+[ByRefEvent]
+public record struct OnEntityMetabolizeAfterReagent(MetabolismGroupEntry Entry, float Scale);
+
+[ByRefEvent]
+public record struct OnEntityAfterMetabolize(FixedPoint2 MostToRemove, Entity Solution);
diff --git a/Content.Server/Body/Events/OnEntitySaturationAttempt.cs b/Content.Server/Body/Events/OnEntitySaturationAttempt.cs
new file mode 100644
index 00000000000..53aacc56823
--- /dev/null
+++ b/Content.Server/Body/Events/OnEntitySaturationAttempt.cs
@@ -0,0 +1,7 @@
+namespace Content.Server.Body.Events;
+
+[ByRefEvent]
+public record struct OnEntitySaturationAttempt(bool HasSaturation);
+
+[ByRefEvent]
+public record struct CanProcessEntitySaturation(bool IgnoreAttempt = true);
diff --git a/Content.Server/Body/Events/OnEntityStomachUpdated.cs b/Content.Server/Body/Events/OnEntityStomachUpdated.cs
new file mode 100644
index 00000000000..6aa4b3c95e7
--- /dev/null
+++ b/Content.Server/Body/Events/OnEntityStomachUpdated.cs
@@ -0,0 +1,6 @@
+using Content.Shared.Chemistry.Reagent;
+
+namespace Content.Server.Body.Events;
+
+[ByRefEvent]
+public record struct OnEntityStomachUpdated(ReagentQuantity Quantity);
diff --git a/Content.Server/Body/Systems/BrainSystem.cs b/Content.Server/Body/Systems/BrainSystem.cs
index 86d2cb61ffe..2976186c497 100644
--- a/Content.Server/Body/Systems/BrainSystem.cs
+++ b/Content.Server/Body/Systems/BrainSystem.cs
@@ -3,6 +3,7 @@
using Content.Shared.Body.Components;
using Content.Shared.Body.Events;
using Content.Shared.Mind;
+using Content.Shared._Adventure.Medical.Surgery.Events.Organs;
using Content.Shared.Mind.Components;
using Content.Shared.Pointing;
@@ -23,6 +24,7 @@ public override void Initialize()
private void HandleMind(EntityUid newEntity, EntityUid oldEntity)
{
+ Log.Debug($"HandleMind {newEntity} {oldEntity}");
if (TerminatingOrDeleted(newEntity) || TerminatingOrDeleted(oldEntity))
return;
@@ -37,6 +39,8 @@ private void HandleMind(EntityUid newEntity, EntityUid oldEntity)
return;
_mindSystem.TransferTo(mindId, newEntity, mind: mind);
+ var pumpEv = new SurgeryRequestPump();
+ RaiseLocalEvent(newEntity, ref pumpEv);
}
private void OnPointAttempt(Entity ent, ref PointAttemptEvent args)
diff --git a/Content.Server/Body/Systems/LungSystem.cs b/Content.Server/Body/Systems/LungSystem.cs
index 859618ae1a2..4a9b50ee7a7 100644
--- a/Content.Server/Body/Systems/LungSystem.cs
+++ b/Content.Server/Body/Systems/LungSystem.cs
@@ -6,6 +6,14 @@
using Content.Shared.Chemistry.Components;
using Content.Shared.Clothing;
using Content.Shared.Inventory.Events;
+using Content.Server.Body.Events;
+using System.Linq;
+using Content.Server.Chemistry.Containers.EntitySystems;
+using Robust.Shared.Prototypes;
+using Content.Shared.Chemistry.Reagent;
+using Content.Server.Popups;
+using Content.Shared.Rejuvenate;
+using Content.Shared.Body.Organ;
namespace Content.Server.Body.Systems;
@@ -74,15 +82,34 @@ private void OnMaskToggled(Entity ent, ref ItemMaskToggledE
}
}
- public void GasToReagent(EntityUid uid, LungComponent lung)
+ public void GasToReagent(EntityUid lungs, EntityUid user, LungComponent lung)
{
- if (!_solutionContainerSystem.ResolveSolution(uid, lung.SolutionName, ref lung.Solution, out var solution))
+ if (!_solutionContainerSystem.ResolveSolution(lungs, lung.SolutionName, ref lung.Solution, out var solution))
return;
- GasToReagent(lung.Air, solution);
+ GasToReagent(user, lung, solution);
_solutionContainerSystem.UpdateChemicals(lung.Solution.Value);
}
+ private void GasToReagent(EntityUid user, LungComponent lungs, Solution solution)
+ {
+ foreach (var gasId in Enum.GetValues())
+ {
+ var i = (int)gasId;
+ var moles = lungs.Air[i];
+ if (moles <= 0)
+ continue;
+ var reagent = _atmos.GasReagents[i];
+ if (reagent is null)
+ continue;
+ var amount = moles * Atmospherics.BreathMolesToReagentMultiplier;
+ solution.AddReagent(reagent, amount);
+ var ev = new OnEntityBreathGas(reagent, amount);
+ RaiseLocalEvent(user, ref ev);
+ }
+ }
+
+
private void GasToReagent(GasMixture gas, Solution solution)
{
foreach (var gasId in Enum.GetValues())
diff --git a/Content.Server/Body/Systems/MetabolizerSystem.cs b/Content.Server/Body/Systems/MetabolizerSystem.cs
index 3497d4a6d78..37ecc8925f7 100644
--- a/Content.Server/Body/Systems/MetabolizerSystem.cs
+++ b/Content.Server/Body/Systems/MetabolizerSystem.cs
@@ -1,4 +1,5 @@
using Content.Server.Body.Components;
+using Content.Server.Body.Events;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Administration.Logs;
using Content.Shared.Body.Organ;
@@ -181,6 +182,7 @@ private void TryMetabolize(Entity FixedPoint2.Zero)
{
solution.RemoveReagent(reagent, mostToRemove);
-
+ var afterMetabolize = new OnEntityAfterMetabolize(mostToRemove, soln.Value);
+ RaiseLocalEvent(solutionEntityUid.Value, ref afterMetabolize);
// We have processed a reagant, so count it towards the cap
reagents += 1;
}
diff --git a/Content.Server/Body/Systems/RespiratorSystem.cs b/Content.Server/Body/Systems/RespiratorSystem.cs
index 6209f00419d..048d54c7b5d 100644
--- a/Content.Server/Body/Systems/RespiratorSystem.cs
+++ b/Content.Server/Body/Systems/RespiratorSystem.cs
@@ -17,6 +17,7 @@
using Content.Shared.Mobs.Systems;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
+using Content.Server.Body.Events;
using Robust.Shared.Timing;
namespace Content.Server.Body.Systems;
@@ -76,7 +77,12 @@ public override void Update(float frameTime)
UpdateSaturation(uid, -(float) respirator.UpdateInterval.TotalSeconds, respirator);
- if (!_mobState.IsIncapacitated(uid)) // cannot breathe in crit.
+ var processSaturationEv = new CanProcessEntitySaturation();
+ RaiseLocalEvent(ref processSaturationEv);
+ var saturationAttempt = new OnEntitySaturationAttempt();
+ RaiseLocalEvent(uid, ref saturationAttempt);
+ var saturationValid = !processSaturationEv.IgnoreAttempt && saturationAttempt.HasSaturation;
+ if (!_mobState.IsIncapacitated(uid) && saturationValid) // cannot breathe in crit.
{
switch (respirator.Status)
{
@@ -133,9 +139,18 @@ public void Inhale(EntityUid uid, BodyComponent? body = null)
var gas = organs.Count == 1 ? actualGas : actualGas.RemoveRatio(lungRatio);
foreach (var (organUid, lung, _) in organs)
{
- // Merge doesn't remove gas from the giver.
- _atmosSys.Merge(lung.Air, gas);
- _lungSystem.GasToReagent(organUid, lung);
+ var lungEv = new OnEntityInhaleToLungs();
+ RaiseLocalEvent(uid, ref lungEv);
+ if (lungEv.DamageLoss > 0)
+ {
+ var remainderGas = gas.RemoveRatio(1.0f - lungEv.DamageLoss);
+ var removedGas = gas.RemoveRatio(lungEv.DamageLoss);
+ _atmosSys.Merge(lung.Air, remainderGas);
+ _atmosSys.Merge(ev.Gas, removedGas);
+ }
+ else
+ _atmosSys.Merge(lung.Air, gas);
+ _lungSystem.GasToReagent(organUid, uid, lung);
}
}
@@ -352,3 +367,6 @@ public record struct InhaleLocationEvent(GasMixture? Gas);
[ByRefEvent]
public record struct ExhaleLocationEvent(GasMixture? Gas);
+
+[ByRefEvent]
+public record struct OnEntityInhaleToLungs(float DamageLoss = 0f);
diff --git a/Content.Server/Body/Systems/StomachSystem.cs b/Content.Server/Body/Systems/StomachSystem.cs
index 9fc7ff10e46..d9f5f56bb5d 100644
--- a/Content.Server/Body/Systems/StomachSystem.cs
+++ b/Content.Server/Body/Systems/StomachSystem.cs
@@ -5,13 +5,22 @@
using Content.Shared.Chemistry.Components.SolutionManager;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
+using System.Linq;
+using Content.Server.Body.Events;
+using Content.Server.Popups;
+using Content.Shared.Chemistry.Reagent;
+using Content.Shared.Rejuvenate;
+using Robust.Shared.Prototypes;
+using Content.Server.Chemistry.Containers.EntitySystems;
namespace Content.Server.Body.Systems
{
public sealed class StomachSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
- [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
+ [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly PopupSystem _popupSystem = default!;
public const string DefaultSolutionName = "stomach";
@@ -43,10 +52,12 @@ public override void Update(float frameTime)
stomach.NextUpdate += stomach.UpdateInterval;
// Get our solutions
- if (!_solutionContainerSystem.ResolveSolution((uid, sol), DefaultSolutionName, ref stomach.Solution, out var stomachSolution))
+ if (!_solutionContainerSystem.ResolveSolution((uid, sol), DefaultSolutionName, ref stomach.Solution,
+ out var stomachSolution))
continue;
- if (organ.Body is not { } body || !_solutionContainerSystem.TryGetSolution(body, stomach.BodySolutionName, out var bodySolution))
+ if (organ.Body is not { } body ||
+ !_solutionContainerSystem.TryGetSolution(body, stomach.BodySolutionName, out var bodySolution))
continue;
var transferSolution = new Solution();
@@ -55,19 +66,23 @@ public override void Update(float frameTime)
foreach (var delta in stomach.ReagentDeltas)
{
delta.Increment(stomach.UpdateInterval);
- if (delta.Lifetime > stomach.DigestionDelay)
+
+ if (delta.Lifetime <= stomach.DigestionDelay)
+ continue;
+
+ if (stomachSolution.TryGetReagent(delta.ReagentQuantity.Reagent, out var reagent))
{
- if (stomachSolution.TryGetReagent(delta.ReagentQuantity.Reagent, out var reagent))
- {
- if (reagent.Quantity > delta.ReagentQuantity.Quantity)
- reagent = new(reagent.Reagent, delta.ReagentQuantity.Quantity);
+ if (reagent.Quantity > delta.ReagentQuantity.Quantity)
+ reagent = new ReagentQuantity(reagent.Reagent, delta.ReagentQuantity.Quantity);
- stomachSolution.RemoveReagent(reagent);
- transferSolution.AddReagent(reagent);
- }
+ stomachSolution.RemoveReagent(reagent);
+ transferSolution.AddReagent(reagent);
- queue.Add(delta);
+ var ev = new OnEntityStomachUpdated(delta.ReagentQuantity);
+ RaiseLocalEvent(uid, ref ev);
}
+
+ queue.Add(delta);
}
foreach (var item in queue)
@@ -76,7 +91,6 @@ public override void Update(float frameTime)
}
_solutionContainerSystem.UpdateChemicals(stomach.Solution.Value);
-
// Transfer everything to the body solution!
_solutionContainerSystem.TryAddSolution(bodySolution.Value, transferSolution);
}
@@ -103,9 +117,10 @@ public bool CanTransferSolution(
SolutionContainerManagerComponent? solutions = null)
{
return Resolve(uid, ref stomach, ref solutions, logMissing: false)
- && _solutionContainerSystem.ResolveSolution((uid, solutions), DefaultSolutionName, ref stomach.Solution, out var stomachSolution)
- // TODO: For now no partial transfers. Potentially change by design
- && stomachSolution.CanAddSolution(solution);
+ && _solutionContainerSystem.ResolveSolution((uid, solutions), DefaultSolutionName, ref stomach.Solution,
+ out var stomachSolution)
+ // TODO: For now no partial transfers. Potentially change by design
+ && stomachSolution.CanAddSolution(solution);
}
public bool TryTransferSolution(
@@ -120,14 +135,12 @@ public bool TryTransferSolution(
{
return false;
}
-
_solutionContainerSystem.TryAddSolution(stomach.Solution.Value, solution);
// Add each reagent to ReagentDeltas. Used to track how long each reagent has been in the stomach
foreach (var reagent in solution.Contents)
{
stomach.ReagentDeltas.Add(new StomachComponent.ReagentDelta(reagent));
}
-
return true;
}
}
diff --git a/Content.Server/Cargo/Components/CargoSaleEvent.cs b/Content.Server/Cargo/Components/CargoSaleEvent.cs
new file mode 100644
index 00000000000..3a46a984abb
--- /dev/null
+++ b/Content.Server/Cargo/Components/CargoSaleEvent.cs
@@ -0,0 +1,11 @@
+namespace Content.Server.Cargo.Components;
+
+public sealed class CargoSaleEvent
+{
+ public EntityUid CashStack;
+
+ public CargoSaleEvent(EntityUid cashStack)
+ {
+ CashStack = cashStack;
+ }
+}
diff --git a/Content.Server/Content.Server.csproj b/Content.Server/Content.Server.csproj
index 99b834683ce..522797fe9e5 100644
--- a/Content.Server/Content.Server.csproj
+++ b/Content.Server/Content.Server.csproj
@@ -26,5 +26,8 @@
+
+
+
diff --git a/Content.Server/Corvax/GameTicking/RoundEndedEvent.cs b/Content.Server/Corvax/GameTicking/RoundEndedEvent.cs
new file mode 100644
index 00000000000..360d4dd22c9
--- /dev/null
+++ b/Content.Server/Corvax/GameTicking/RoundEndedEvent.cs
@@ -0,0 +1,13 @@
+namespace Content.Shared.GameTicking;
+
+public sealed class RoundEndedEvent : EntityEventArgs
+{
+ public int RoundId { get; }
+ public TimeSpan RoundDuration { get; }
+
+ public RoundEndedEvent(int roundId, TimeSpan roundDuration)
+ {
+ RoundId = roundId;
+ RoundDuration = roundDuration;
+ }
+}
diff --git a/Content.Server/Corvax/GameTicking/RoundStartedEvent.cs b/Content.Server/Corvax/GameTicking/RoundStartedEvent.cs
new file mode 100644
index 00000000000..f8066e16e0f
--- /dev/null
+++ b/Content.Server/Corvax/GameTicking/RoundStartedEvent.cs
@@ -0,0 +1,11 @@
+namespace Content.Shared.GameTicking;
+
+public sealed class RoundStartedEvent : EntityEventArgs
+{
+ public int RoundId { get; }
+
+ public RoundStartedEvent(int roundId)
+ {
+ RoundId = roundId;
+ }
+}
diff --git a/Content.Server/Explosion/EntitySystems/ExplosionHitEvent.cs b/Content.Server/Explosion/EntitySystems/ExplosionHitEvent.cs
new file mode 100644
index 00000000000..641ca2efe7c
--- /dev/null
+++ b/Content.Server/Explosion/EntitySystems/ExplosionHitEvent.cs
@@ -0,0 +1,4 @@
+using Content.Shared.Damage;
+namespace Content.Server.Explosion.EntitySystems;
+[ByRefEvent]
+public record struct ExplosionHitEvent(DamageSpecifier Damage);
diff --git a/Content.Server/Explosion/EntitySystems/ExplosionSystem.Processing.cs b/Content.Server/Explosion/EntitySystems/ExplosionSystem.Processing.cs
index ca2b0dbf9d1..2322b372d01 100644
--- a/Content.Server/Explosion/EntitySystems/ExplosionSystem.Processing.cs
+++ b/Content.Server/Explosion/EntitySystems/ExplosionSystem.Processing.cs
@@ -465,6 +465,8 @@ private void ProcessEntity(
// TODO EXPLOSIONS turn explosions into entities, and pass the the entity in as the damage origin.
_damageableSystem.TryChangeDamage(entity, damage, ignoreResistances: true);
+ var explosionEv = new ExplosionHitEvent(damage);
+ RaiseLocalEvent(uid, ref explosionEv);
}
}
diff --git a/Content.Server/GameTicking/GameTicker.Spawning.cs b/Content.Server/GameTicking/GameTicker.Spawning.cs
index 4bda391ae27..3e3ac8edb5f 100644
--- a/Content.Server/GameTicking/GameTicker.Spawning.cs
+++ b/Content.Server/GameTicking/GameTicker.Spawning.cs
@@ -3,9 +3,11 @@
using System.Numerics;
using Content.Server.Administration.Managers;
using Content.Server.GameTicking.Events;
+using Content.Server.Ghost;
using Content.Server.Spawners.Components;
using Content.Server.Speech.Components;
using Content.Server.Station.Components;
+using Content.Shared.CCVar;
using Content.Shared.Database;
using Content.Shared.GameTicking;
using Content.Shared.Mind;
@@ -13,6 +15,7 @@
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Content.Shared.Roles.Jobs;
+using JetBrains.Annotations;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Network;
@@ -455,4 +458,68 @@ public EntityCoordinates GetObserverSpawnPoint()
#endregion
}
+
+ ///
+ /// Event raised broadcast before a player is spawned by the GameTicker.
+ /// You can use this event to spawn a player off-station on late-join but also at round start.
+ /// When this event is handled, the GameTicker will not perform its own player-spawning logic.
+ ///
+ [PublicAPI]
+ public sealed class PlayerBeforeSpawnEvent : HandledEntityEventArgs
+ {
+ public ICommonSession Player { get; }
+ public HumanoidCharacterProfile Profile { get; }
+ public string? JobId { get; }
+ public bool LateJoin { get; }
+ public EntityUid Station { get; }
+
+ public PlayerBeforeSpawnEvent(ICommonSession player,
+ HumanoidCharacterProfile profile,
+ string? jobId,
+ bool lateJoin,
+ EntityUid station)
+ {
+ Player = player;
+ Profile = profile;
+ JobId = jobId;
+ LateJoin = lateJoin;
+ Station = station;
+ }
+ }
+
+ ///
+ /// Event raised both directed and broadcast when a player has been spawned by the GameTicker.
+ /// You can use this to handle people late-joining, or to handle people being spawned at round start.
+ /// Can be used to give random players a role, modify their equipment, etc.
+ ///
+ [PublicAPI]
+ public sealed class PlayerSpawnCompleteEvent : EntityEventArgs
+ {
+ public EntityUid Mob { get; }
+ public ICommonSession Player { get; }
+ public string? JobId { get; }
+ public bool LateJoin { get; }
+ public EntityUid Station { get; }
+ public HumanoidCharacterProfile Profile { get; }
+
+ // Ex. If this is the 27th person to join, this will be 27.
+ public int JoinOrder { get; }
+
+ public PlayerSpawnCompleteEvent(EntityUid mob,
+ ICommonSession player,
+ string? jobId,
+ bool lateJoin,
+ int joinOrder,
+ EntityUid station,
+ HumanoidCharacterProfile profile)
+ {
+ Mob = mob;
+ Player = player;
+ JobId = jobId;
+ LateJoin = lateJoin;
+ Station = station;
+ Profile = profile;
+ JoinOrder = joinOrder;
+ }
+ }
}
diff --git a/Content.Server/Medical/CryoPodSystem.cs b/Content.Server/Medical/CryoPodSystem.cs
index 15fe2a69cf9..175c367ef37 100644
--- a/Content.Server/Medical/CryoPodSystem.cs
+++ b/Content.Server/Medical/CryoPodSystem.cs
@@ -4,6 +4,7 @@
using Content.Server.Atmos.Piping.Unary.EntitySystems;
using Content.Server.Body.Components;
using Content.Server.Body.Systems;
+using Content.Server._Adventure.Medical.Surgery.Components;
using Content.Server.Medical.Components;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.NodeGroups;
@@ -49,6 +50,7 @@ public sealed partial class CryoPodSystem : SharedCryoPodSystem
[Dependency] private readonly MetaDataSystem _metaDataSystem = default!;
[Dependency] private readonly ReactiveSystem _reactiveSystem = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
+ [Dependency] private readonly HealthAnalyzerSystem _healthAnalyzerSystem = default!;
[Dependency] private readonly NodeContainerSystem _nodeContainer = default!;
public override void Initialize()
@@ -103,8 +105,8 @@ public override void Update(float frameTime)
&& fitsInDispenserQuery.TryGetComponent(container, out var fitsInDispenserComponent)
&& solutionContainerManagerQuery.TryGetComponent(container,
out var solutionContainerManagerComponent)
- && _solutionContainerSystem.TryGetFitsInDispenser((container.Value, fitsInDispenserComponent, solutionContainerManagerComponent),
- out var containerSolution, out _))
+ && _solutionContainerSystem.TryGetFitsInDispenser(
+ (container.Value, fitsInDispenserComponent, solutionContainerManagerComponent), out var containerSolution, out _))
{
if (!bloodStreamQuery.TryGetComponent(patient, out var bloodstream))
{
@@ -179,31 +181,35 @@ private void OnActivateUIAttempt(Entity entity, ref Activatabl
private void OnActivateUI(Entity entity, ref AfterActivatableUIOpenEvent args)
{
- if (!entity.Comp.BodyContainer.ContainedEntity.HasValue)
+ if (entity.Comp.BodyContainer.ContainedEntity is not { } target)
return;
- TryComp(entity.Comp.BodyContainer.ContainedEntity, out var temp);
- TryComp(entity.Comp.BodyContainer.ContainedEntity, out var bloodstream);
+ var bodyTemperature = float.NaN;
- if (TryComp(entity, out var healthAnalyzer))
- {
- healthAnalyzer.ScannedEntity = entity.Comp.BodyContainer.ContainedEntity;
- }
+ if (TryComp(target, out var temp))
+ bodyTemperature = temp.CurrentTemperature;
+ var bloodAmount = float.NaN;
+ if (TryComp(target, out var bloodstream) &&
+ _solutionContainerSystem.ResolveSolution(target, bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution))
+ bloodAmount = bloodSolution.FillFraction;
+ TryComp(target, out var surgery);
+ var organFunctionConditions = _healthAnalyzerSystem.GetOrgans(target);
// TODO: This should be a state my dude
_uiSystem.ServerSendUiMessage(
entity.Owner,
HealthAnalyzerUiKey.Key,
- new HealthAnalyzerScannedUserMessage(GetNetEntity(entity.Comp.BodyContainer.ContainedEntity),
- temp?.CurrentTemperature ?? 0,
- (bloodstream != null && _solutionContainerSystem.ResolveSolution(entity.Comp.BodyContainer.ContainedEntity.Value,
- bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution))
- ? bloodSolution.FillFraction
- : 0,
- null,
- null,
- null
- ));
+ new HealthAnalyzerScannedUserMessage(
+ GetNetEntity(target),
+ bodyTemperature,
+ bloodAmount,
+ null,
+ null,
+ null,
+ organFunctionConditions,
+ surgery?.Sedated ?? false
+ )
+ );
}
private void OnInteractUsing(Entity entity, ref InteractUsingEvent args)
@@ -211,13 +217,15 @@ private void OnInteractUsing(Entity entity, ref InteractUsingE
if (args.Handled || !entity.Comp.Locked || entity.Comp.BodyContainer.ContainedEntity == null)
return;
- args.Handled = _toolSystem.UseTool(args.Used, args.User, entity.Owner, entity.Comp.PryDelay, "Prying", new CryoPodPryFinished());
+ args.Handled = _toolSystem.UseTool(args.Used, args.User, entity.Owner, entity.Comp.PryDelay, "Prying",
+ new CryoPodPryFinished());
}
private void OnExamined(Entity entity, ref ExaminedEvent args)
{
var container = _itemSlotsSystem.GetItemOrNull(entity.Owner, entity.Comp.SolutionContainerName);
- if (args.IsInDetailsRange && container != null && _solutionContainerSystem.TryGetFitsInDispenser(container.Value, out _, out var containerSolution))
+ if (args.IsInDetailsRange && container != null &&
+ _solutionContainerSystem.TryGetFitsInDispenser(container.Value, out _, out var containerSolution))
{
using (args.PushGroup(nameof(CryoPodComponent)))
{
diff --git a/Content.Server/Medical/DefibrillatorSystem.cs b/Content.Server/Medical/DefibrillatorSystem.cs
index fa0ea26385d..eb63b734676 100644
--- a/Content.Server/Medical/DefibrillatorSystem.cs
+++ b/Content.Server/Medical/DefibrillatorSystem.cs
@@ -24,6 +24,8 @@
using Robust.Shared.Audio.Systems;
using Robust.Shared.Player;
using Robust.Shared.Timing;
+using Content.Shared._Adventure.Medical.Surgery.Events.Organs;
+
namespace Content.Server.Medical;
@@ -227,6 +229,9 @@ public void Zap(EntityUid uid, EntityUid target, EntityUid user, DefibrillatorCo
}
}
+ var pumpEv = new SurgeryRequestPump();
+ RaiseLocalEvent(target, ref pumpEv);
+
var sound = dead || session == null
? component.FailureSound
: component.SuccessSound;
diff --git a/Content.Server/Medical/EntityHealedEvent.cs b/Content.Server/Medical/EntityHealedEvent.cs
new file mode 100644
index 00000000000..afdda3dda52
--- /dev/null
+++ b/Content.Server/Medical/EntityHealedEvent.cs
@@ -0,0 +1,5 @@
+
+using Content.Shared.FixedPoint;
+namespace Content.Server.Medical;
+[ByRefEvent]
+public record struct EntityHealedEvent(FixedPoint2 Healed);
diff --git a/Content.Server/Medical/Events/GetOrgansState.cs b/Content.Server/Medical/Events/GetOrgansState.cs
new file mode 100644
index 00000000000..97d6e68481d
--- /dev/null
+++ b/Content.Server/Medical/Events/GetOrgansState.cs
@@ -0,0 +1,4 @@
+
+namespace Content.Server.Medical.Events;
+[ByRefEvent]
+public record struct GetOrgansState(Dictionary OrgansState);
diff --git a/Content.Server/Medical/HealingSystem.cs b/Content.Server/Medical/HealingSystem.cs
index cf5869d1cbb..a6add2eb360 100644
--- a/Content.Server/Medical/HealingSystem.cs
+++ b/Content.Server/Medical/HealingSystem.cs
@@ -37,6 +37,7 @@ public sealed class HealingSystem : EntitySystem
[Dependency] private readonly MobThresholdSystem _mobThresholdSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
+ // [Dependency] private readonly IDiseasesBridge _diseasesBridge = default!;
public override void Initialize()
{
@@ -56,12 +57,15 @@ private void OnDoAfter(Entity entity, ref HealingDoAfterEve
if (args.Handled || args.Cancelled)
return;
- if (healing.DamageContainers is not null &&
- entity.Comp.DamageContainerID is not null &&
- !healing.DamageContainers.Contains(entity.Comp.DamageContainerID))
- {
- return;
- }
+ //
+ // var healedDisease = _diseasesBridge.TryHealDisease(args.Used.Value, entity.Owner);
+ // if (!healedDisease &&
+ // healing.DamageContainers is not null &&
+ // entity.Comp.DamageContainerID is not null &&
+ // !healing.DamageContainers.Contains(entity.Comp.DamageContainerID))
+ // {
+ // return;
+ // }
// Heal some bloodloss damage.
if (healing.BloodlossModifier != 0)
@@ -82,8 +86,8 @@ entity.Comp.DamageContainerID is not null &&
var healed = _damageable.TryChangeDamage(entity.Owner, healing.Damage, true, origin: args.Args.User);
- if (healed == null && healing.BloodlossModifier != 0)
- return;
+ // if (!healedDisease && healed == null && healing.BloodlossModifier != 0)
+ // return;
var total = healed?.GetTotal() ?? FixedPoint2.Zero;
@@ -119,6 +123,8 @@ entity.Comp.DamageContainerID is not null &&
if (!args.Repeat && !dontRepeat)
_popupSystem.PopupEntity(Loc.GetString("medical-item-finished-using", ("item", args.Used)), entity.Owner, args.User);
args.Handled = true;
+ var ev = new EntityHealedEvent(total);
+ RaiseLocalEvent(entity.Owner, ref ev);
}
private bool HasDamage(DamageableComponent component, HealingComponent healing)
@@ -159,12 +165,13 @@ private bool TryHeal(EntityUid uid, EntityUid user, EntityUid target, HealingCom
if (!TryComp(target, out var targetDamage))
return false;
- if (component.DamageContainers is not null &&
- targetDamage.DamageContainerID is not null &&
- !component.DamageContainers.Contains(targetDamage.DamageContainerID))
- {
- return false;
- }
+ // var canHealDisease = _diseasesBridge.CanHealDisease(uid, target);
+ // if (component.DamageContainers is not null &&
+ // targetDamage.DamageContainerID is not null &&
+ // !component.DamageContainers.Contains(targetDamage.DamageContainerID) && !canHealDisease)
+ // {
+ // return false;
+ // }
if (user != target && !_interactionSystem.InRangeUnobstructed(user, target, popup: true))
return false;
@@ -173,6 +180,7 @@ targetDamage.DamageContainerID is not null &&
return false;
var anythingToDo =
+ // canHealDisease || HasDamage(targetDamage, component) ||
HasDamage(targetDamage, component) ||
component.ModifyBloodLevel > 0 // Special case if healing item can restore lost blood...
&& TryComp(target, out var bloodstream)
diff --git a/Content.Server/Medical/HealthAnalyzerSystem.cs b/Content.Server/Medical/HealthAnalyzerSystem.cs
index 90646725bb7..7fdb605d4e7 100644
--- a/Content.Server/Medical/HealthAnalyzerSystem.cs
+++ b/Content.Server/Medical/HealthAnalyzerSystem.cs
@@ -18,6 +18,12 @@
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Timing;
+using System.Linq;
+using Content.Server._Adventure.Medical.Surgery.Components;
+using Content.Server.Body.Systems;
+using Content.Server.Medical.Events;
+using Content.Shared.Body.Components;
+
namespace Content.Server.Medical;
@@ -32,6 +38,8 @@ public sealed class HealthAnalyzerSystem : EntitySystem
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly TransformSystem _transformSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
+ [Dependency] private readonly BodySystem _bodySystem = default!;
+
public override void Initialize()
{
@@ -206,6 +214,8 @@ public void UpdateScannedUser(EntityUid healthAnalyzer, EntityUid target, bool s
bloodAmount = bloodSolution.FillFraction;
bleeding = bloodstream.BleedAmount > 0;
}
+ TryComp(target, out var surgery);
+ var organFunctionConditions = GetOrgans(target);
if (HasComp(target))
unrevivable = true;
@@ -216,7 +226,18 @@ public void UpdateScannedUser(EntityUid healthAnalyzer, EntityUid target, bool s
bloodAmount,
scanMode,
bleeding,
- unrevivable
+ unrevivable,
+ organFunctionConditions,
+ surgery?.Sedated ?? false
));
}
+ public Dictionary GetOrgans(EntityUid uid)
+ {
+ var organs = new Dictionary();
+ if (!TryComp(uid, out var body))
+ return organs;
+ var ev = new GetOrgansState(organs);
+ RaiseLocalEvent(uid, ref ev);
+ return organs.OrderBy(d => d.Value).ToDictionary();
+ }
}
diff --git a/Content.Server/Nutrition/EntitySystems/SmokingSystem.cs b/Content.Server/Nutrition/EntitySystems/SmokingSystem.cs
index 0d637139d82..d820f6076b7 100644
--- a/Content.Server/Nutrition/EntitySystems/SmokingSystem.cs
+++ b/Content.Server/Nutrition/EntitySystems/SmokingSystem.cs
@@ -14,6 +14,7 @@
using Content.Shared.Nutrition.Components;
using Content.Shared.Smoking;
using Content.Shared.Temperature;
+using Content.Shared.Body.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Containers;
using System.Linq;
@@ -33,6 +34,9 @@ public sealed partial class SmokingSystem : EntitySystem
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly ForensicsSystem _forensics = default!;
+ [Dependency] private readonly LungSystem _lungSystem = default!;
+ [Dependency] private readonly BodySystem _bodySystem = default!;
+ // [Dependency] private readonly IDiseasesBridge _diseasesBridge = default!;
private const float UpdateTimer = 3f;
@@ -94,6 +98,7 @@ private void OnSmokeableEquipEvent(Entity entity, ref GotEqui
if (args.Slot == "mask")
{
_forensics.TransferDna(entity.Owner, args.Equipee, false);
+ // _diseasesBridge.TransferDiseasesContact(entity.Owner, args.Equipee);
}
}
@@ -142,7 +147,7 @@ public override void Update(float frameTime)
// This is awful. I hate this so much.
// TODO: Please, someone refactor containers and free me from this bullshit.
- if (!_container.TryGetContainingContainer((uid, null, null), out var containerManager) ||
+ if (!_container.TryGetContainingContainer(uid, out var containerManager) ||
!(_inventorySystem.TryGetSlotEntity(containerManager.Owner, "mask", out var inMaskSlotUid) && inMaskSlotUid == uid) ||
!TryComp(containerManager.Owner, out BloodstreamComponent? bloodstream))
{
@@ -151,6 +156,26 @@ public override void Update(float frameTime)
_reactiveSystem.DoEntityReaction(containerManager.Owner, inhaledSolution, ReactionMethod.Ingestion);
_bloodstreamSystem.TryAddToChemicals(containerManager.Owner, inhaledSolution, bloodstream);
+
+ if (TryComp(containerManager.Owner, out var body))
+ {
+ var lungs = _bodySystem.GetBodyOrganEntityComps(body.Owner);
+ var numLungs = lungs.Count;
+ foreach (var lung in lungs)
+ {
+ //go through solution, check if it does any lung damage
+ foreach (var reagent in inhaledSolution.Contents)
+ {
+ var lungEv = new OnEntityInhaleToLungs();
+ RaiseLocalEvent(body.Owner, ref lungEv);
+ if (lungEv.DamageLoss > 1.0f)
+ lungEv.DamageLoss = 1.0f;
+ var amount = (float)reagent.Quantity / numLungs * (1.0f - lungEv.DamageLoss);
+ var smokeEv = new OnEntitySmoke(amount);
+ RaiseLocalEvent(body.Owner, ref smokeEv);
+ }
+ }
+ }
}
_timer -= UpdateTimer;
@@ -163,4 +188,7 @@ public override void Update(float frameTime)
public sealed class SmokableSolutionEmptyEvent : EntityEventArgs
{
}
+
+ [ByRefEvent]
+ public record struct OnEntitySmoke(float Amount);
}
diff --git a/Content.Server/Polymorph/Systems/PolymorphRevertedEvent.cs b/Content.Server/Polymorph/Systems/PolymorphRevertedEvent.cs
new file mode 100644
index 00000000000..cf37f572167
--- /dev/null
+++ b/Content.Server/Polymorph/Systems/PolymorphRevertedEvent.cs
@@ -0,0 +1,11 @@
+namespace Content.Server.Polymorph.Systems;
+public sealed class PolymorphRevertedEvent : EntityEventArgs
+{
+ public EntityUid Original;
+ public EntityUid Polymorph;
+ public PolymorphRevertedEvent(EntityUid original, EntityUid polymorph)
+ {
+ Original = original;
+ Polymorph = polymorph;
+ }
+}
diff --git a/Content.Server/Polymorph/Systems/PolymorphSystem.cs b/Content.Server/Polymorph/Systems/PolymorphSystem.cs
index c9a71c53584..8dc4bfc296d 100644
--- a/Content.Server/Polymorph/Systems/PolymorphSystem.cs
+++ b/Content.Server/Polymorph/Systems/PolymorphSystem.cs
@@ -343,6 +343,11 @@ private void OnDestruction(Entity ent, ref Destructi
("parent", Identity.Entity(uid, EntityManager)),
("child", Identity.Entity(parent, EntityManager))),
parent);
+
+ var ev = new PolymorphRevertedEvent(parent, uid);
+ RaiseLocalEvent(uid, ev);
+ RaiseLocalEvent(parent, ev);
+
QueueDel(uid);
return parent;
diff --git a/Content.Server/Roles/DragonRoleComponent.cs b/Content.Server/Roles/DragonRoleComponent.cs
index c47455d8f6f..b85fd53eb0c 100644
--- a/Content.Server/Roles/DragonRoleComponent.cs
+++ b/Content.Server/Roles/DragonRoleComponent.cs
@@ -4,9 +4,9 @@
namespace Content.Server.Roles;
///
-/// Added to mind role entities to tag that they are a space dragon.
+/// Role used to keep track of space dragons for antag purposes.
///
-[RegisterComponent, Access(typeof(DragonSystem))]
-public sealed partial class DragonRoleComponent : BaseMindRoleComponent
+[RegisterComponent, Access(typeof(DragonSystem)), ExclusiveAntagonist]
+public sealed partial class DragonRoleComponent : AntagonistRoleComponent
{
}
diff --git a/Content.Server/Roles/InitialInfectedRoleComponent.cs b/Content.Server/Roles/InitialInfectedRoleComponent.cs
index 475cd3ba603..52d3db41643 100644
--- a/Content.Server/Roles/InitialInfectedRoleComponent.cs
+++ b/Content.Server/Roles/InitialInfectedRoleComponent.cs
@@ -2,11 +2,8 @@
namespace Content.Server.Roles;
-///
-/// Added to mind role entities to tag that they are an initial infected.
-///
-[RegisterComponent]
-public sealed partial class InitialInfectedRoleComponent : BaseMindRoleComponent
+[RegisterComponent, ExclusiveAntagonist]
+public sealed partial class InitialInfectedRoleComponent : AntagonistRoleComponent
{
}
diff --git a/Content.Server/Roles/NinjaRoleComponent.cs b/Content.Server/Roles/NinjaRoleComponent.cs
index 7bdffe67a31..cb60e5bdf03 100644
--- a/Content.Server/Roles/NinjaRoleComponent.cs
+++ b/Content.Server/Roles/NinjaRoleComponent.cs
@@ -2,10 +2,7 @@
namespace Content.Server.Roles;
-///
-/// Added to mind role entities to tag that they are a space ninja.
-///
-[RegisterComponent]
-public sealed partial class NinjaRoleComponent : BaseMindRoleComponent
+[RegisterComponent, ExclusiveAntagonist]
+public sealed partial class NinjaRoleComponent : AntagonistRoleComponent
{
}
diff --git a/Content.Server/Roles/NukeopsRoleComponent.cs b/Content.Server/Roles/NukeopsRoleComponent.cs
index 41561088ea8..a6ff0b71b06 100644
--- a/Content.Server/Roles/NukeopsRoleComponent.cs
+++ b/Content.Server/Roles/NukeopsRoleComponent.cs
@@ -3,9 +3,9 @@
namespace Content.Server.Roles;
///
-/// Added to mind role entities to tag that they are a nuke operative.
+/// Added to mind entities to tag that they are a nuke operative.
///
-[RegisterComponent]
-public sealed partial class NukeopsRoleComponent : BaseMindRoleComponent
+[RegisterComponent, ExclusiveAntagonist]
+public sealed partial class NukeopsRoleComponent : AntagonistRoleComponent
{
}
diff --git a/Content.Server/Roles/RemoveRoleCommand.cs b/Content.Server/Roles/RemoveRoleCommand.cs
index fd4bb09317a..feba63a253f 100644
--- a/Content.Server/Roles/RemoveRoleCommand.cs
+++ b/Content.Server/Roles/RemoveRoleCommand.cs
@@ -45,7 +45,7 @@ public void Execute(IConsoleShell shell, string argStr, string[] args)
var roles = _entityManager.System();
var jobs = _entityManager.System();
if (jobs.MindHasJobWithId(mind, args[1]))
- roles.MindTryRemoveRole(mind.Value);
+ roles.MindRemoveRole(mind.Value);
}
}
}
diff --git a/Content.Server/Roles/RevolutionaryRoleComponent.cs b/Content.Server/Roles/RevolutionaryRoleComponent.cs
index dcdb131b9d0..bf56b960084 100644
--- a/Content.Server/Roles/RevolutionaryRoleComponent.cs
+++ b/Content.Server/Roles/RevolutionaryRoleComponent.cs
@@ -3,10 +3,10 @@
namespace Content.Server.Roles;
///
-/// Added to mind role entities to tag that they are a Revolutionary.
+/// Added to mind entities to tag that they are a Revolutionary.
///
-[RegisterComponent]
-public sealed partial class RevolutionaryRoleComponent : BaseMindRoleComponent
+[RegisterComponent, ExclusiveAntagonist]
+public sealed partial class RevolutionaryRoleComponent : AntagonistRoleComponent
{
///
/// For headrevs, how many people you have converted.
diff --git a/Content.Server/Roles/SubvertedSiliconRoleComponent.cs b/Content.Server/Roles/SubvertedSiliconRoleComponent.cs
index 55727573b9d..70056fbec9e 100644
--- a/Content.Server/Roles/SubvertedSiliconRoleComponent.cs
+++ b/Content.Server/Roles/SubvertedSiliconRoleComponent.cs
@@ -2,10 +2,7 @@
namespace Content.Server.Roles;
-///
-/// Added to mind role entities to tag that they are a hacked borg.
-///
[RegisterComponent]
-public sealed partial class SubvertedSiliconRoleComponent : BaseMindRoleComponent
+public sealed partial class SubvertedSiliconRoleComponent : AntagonistRoleComponent
{
}
diff --git a/Content.Server/Roles/ThiefRoleComponent.cs b/Content.Server/Roles/ThiefRoleComponent.cs
index c0ddee71a49..82e350ef630 100644
--- a/Content.Server/Roles/ThiefRoleComponent.cs
+++ b/Content.Server/Roles/ThiefRoleComponent.cs
@@ -2,10 +2,7 @@
namespace Content.Server.Roles;
-///
-/// Added to mind role entities to tag that they are a thief.
-///
[RegisterComponent]
-public sealed partial class ThiefRoleComponent : BaseMindRoleComponent
+public sealed partial class ThiefRoleComponent : AntagonistRoleComponent
{
}
diff --git a/Content.Server/Roles/TraitorRoleComponent.cs b/Content.Server/Roles/TraitorRoleComponent.cs
index a8a11a8f1bd..96bfe8dd801 100644
--- a/Content.Server/Roles/TraitorRoleComponent.cs
+++ b/Content.Server/Roles/TraitorRoleComponent.cs
@@ -2,10 +2,7 @@
namespace Content.Server.Roles;
-///
-/// Added to mind role entities to tag that they are a syndicate traitor.
-///
-[RegisterComponent]
-public sealed partial class TraitorRoleComponent : BaseMindRoleComponent
+[RegisterComponent, ExclusiveAntagonist]
+public sealed partial class TraitorRoleComponent : AntagonistRoleComponent
{
}
diff --git a/Content.Server/Roles/ZombieRoleComponent.cs b/Content.Server/Roles/ZombieRoleComponent.cs
index cff25e53e80..2f9948022b3 100644
--- a/Content.Server/Roles/ZombieRoleComponent.cs
+++ b/Content.Server/Roles/ZombieRoleComponent.cs
@@ -2,10 +2,7 @@
namespace Content.Server.Roles;
-///
-/// Added to mind role entities to tag that they are a zombie.
-///
-[RegisterComponent]
-public sealed partial class ZombieRoleComponent : BaseMindRoleComponent
+[RegisterComponent, ExclusiveAntagonist]
+public sealed partial class ZombieRoleComponent : AntagonistRoleComponent
{
}
diff --git a/Content.Server/Shuttles/Systems/ArrivalsSystem.cs b/Content.Server/Shuttles/Systems/ArrivalsSystem.cs
index 1f972d96756..3a565d57b5f 100644
--- a/Content.Server/Shuttles/Systems/ArrivalsSystem.cs
+++ b/Content.Server/Shuttles/Systems/ArrivalsSystem.cs
@@ -19,7 +19,6 @@
using Content.Shared.CCVar;
using Content.Shared.Damage.Components;
using Content.Shared.DeviceNetwork;
-using Content.Shared.GameTicking;
using Content.Shared.Mobs.Components;
using Content.Shared.Movement.Components;
using Content.Shared.Parallax.Biomes;
diff --git a/Content.Server/Station/Components/StationJobsComponent.cs b/Content.Server/Station/Components/StationJobsComponent.cs
index 3681ec9674f..b7880e900c2 100644
--- a/Content.Server/Station/Components/StationJobsComponent.cs
+++ b/Content.Server/Station/Components/StationJobsComponent.cs
@@ -1,4 +1,4 @@
-using System.Linq;
+using System.Linq;
using Content.Server.Station.Systems;
using Content.Shared.Roles;
using JetBrains.Annotations;
@@ -12,7 +12,7 @@ namespace Content.Server.Station.Components;
///
/// Stores information about a station's job selection.
///
-[RegisterComponent, Access(typeof(StationJobsSystem)), PublicAPI]
+[RegisterComponent, PublicAPI]
public sealed partial class StationJobsComponent : Component
{
///
diff --git a/Content.Server/Station/Systems/StationJobsSystem.cs b/Content.Server/Station/Systems/StationJobsSystem.cs
index 91a8b7ade4a..8f7a48b8550 100644
--- a/Content.Server/Station/Systems/StationJobsSystem.cs
+++ b/Content.Server/Station/Systems/StationJobsSystem.cs
@@ -508,7 +508,7 @@ private TickerJobsAvailableEvent GenerateJobsAvailableEvent()
///
/// Updates the cached available jobs. Moderately expensive.
///
- private void UpdateJobsAvailable()
+ public void UpdateJobsAvailable()
{
_availableJobsDirty = true;
}
diff --git a/Content.Server/StationRecords/Systems/StationRecordsSystem.cs b/Content.Server/StationRecords/Systems/StationRecordsSystem.cs
index 6dbc58f4d38..e5cb9544da4 100644
--- a/Content.Server/StationRecords/Systems/StationRecordsSystem.cs
+++ b/Content.Server/StationRecords/Systems/StationRecordsSystem.cs
@@ -312,6 +312,13 @@ public void AddRecordEntry(StationRecordKey key, T record,
records.Records.AddRecordEntry(key.Id, record);
}
+ public void ClearRecentlyAccessed(EntityUid station, StationRecordsComponent? records = null)
+ {
+ if (!Resolve(station, ref records))
+ return;
+ records.Records.ClearRecentlyAccessed();
+ }
+
///
/// Synchronizes a station's records with any systems that need it.
///
diff --git a/Content.Server/VendingMachines/VendingMachineSystem.cs b/Content.Server/VendingMachines/VendingMachineSystem.cs
index c20a6a46446..c8940e9e390 100644
--- a/Content.Server/VendingMachines/VendingMachineSystem.cs
+++ b/Content.Server/VendingMachines/VendingMachineSystem.cs
@@ -1,5 +1,6 @@
using System.Linq;
using System.Numerics;
+using Content.Server._c4llv07e.Bridges;
using Content.Server.Advertise;
using Content.Server.Advertise.Components;
using Content.Server.Cargo.Systems;
@@ -9,6 +10,7 @@
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Actions;
+using Content.Shared._Adventure.CCVars;
using Content.Shared.Damage;
using Content.Shared.Destructible;
using Content.Shared.DoAfter;
@@ -23,10 +25,13 @@
using Content.Shared.Wall;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
+using Robust.Shared.Configuration;
+using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
+
namespace Content.Server.VendingMachines
{
public sealed class VendingMachineSystem : SharedVendingMachineSystem
@@ -34,11 +39,14 @@ public sealed class VendingMachineSystem : SharedVendingMachineSystem
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
[Dependency] private readonly AppearanceSystem _appearanceSystem = default!;
+ [Dependency] private readonly SharedActionsSystem _action = default!;
[Dependency] private readonly PricingSystem _pricing = default!;
[Dependency] private readonly ThrowingSystem _throwingSystem = default!;
+ [Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
[Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly IBankBridge _bankBridge = default!;
+ [Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly SpeakOnUIClosedSystem _speakOnUIClosed = default!;
- [Dependency] private readonly SharedPointLightSystem _light = default!;
private const float WallVendEjectDistanceFromWall = 1f;
@@ -46,6 +54,7 @@ public override void Initialize()
{
base.Initialize();
+ SubscribeLocalEvent(OnComponentMapInit);
SubscribeLocalEvent