diff --git a/.gitignore b/.gitignore
index 2dd5912047c..78866a96f64 100644
--- a/.gitignore
+++ b/.gitignore
@@ -306,3 +306,4 @@ Resources/MapImages
# Direnv stuff
.direnv/
+.idea/
\ No newline at end of file
diff --git a/Content.Client/Vampire/VampireSystem.cs b/Content.Client/Vampire/VampireSystem.cs
index 863fa9f3435..933b4783fd7 100644
--- a/Content.Client/Vampire/VampireSystem.cs
+++ b/Content.Client/Vampire/VampireSystem.cs
@@ -1,25 +1,42 @@
+using System.Linq;
+using Content.Client.Alerts;
+using Content.Client.UserInterface.Systems.Alerts.Controls;
+using Content.Shared.StatusIcon;
+using Content.Shared.StatusIcon.Components;
using Content.Shared.Vampire;
using Content.Shared.Vampire.Components;
-using Content.Shared.StatusIcon.Components;
+using Robust.Client.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.Client.Vampire;
-public sealed partial class VampireSystem : EntitySystem
+public sealed class VampireSystem : EntitySystem
{
-
[Dependency] private readonly IPrototypeManager _prototype = default!;
-
+
public override void Initialize()
{
base.Initialize();
- SubscribeLocalEvent(GetVampireIcon);
+ SubscribeLocalEvent(GetVampireIcon);
+ SubscribeLocalEvent(OnUpdateAlert);
}
- private void GetVampireIcon(Entity ent, ref GetStatusIconsEvent args)
- {
- var iconPrototype = _prototype.Index(ent.Comp.StatusIcon);
+ private void GetVampireIcon(EntityUid uid, VampireIconComponent component, ref GetStatusIconsEvent args)
+ {
+ var iconPrototype = _prototype.Index(component.StatusIcon);
args.StatusIcons.Add(iconPrototype);
}
+
+ private void OnUpdateAlert(EntityUid uid, VampireAlertComponent component, ref UpdateAlertSpriteEvent args)
+ {
+ if (args.Alert.ID != component.BloodAlert)
+ return;
+
+ var sprite = args.SpriteViewEnt.Comp;
+ var blood = Math.Clamp(component.BloodAmount, 0, 999);
+ sprite.LayerSetState(VampireVisualLayers.Digit1, $"{(blood / 100) % 10}");
+ sprite.LayerSetState(VampireVisualLayers.Digit2, $"{(blood / 10) % 10}");
+ sprite.LayerSetState(VampireVisualLayers.Digit3, $"{blood % 10}");
+ }
}
\ No newline at end of file
diff --git a/Content.Client/_Sunrise/ChoiceControl.xaml b/Content.Client/_Sunrise/ChoiceControl.xaml
new file mode 100644
index 00000000000..a444070d107
--- /dev/null
+++ b/Content.Client/_Sunrise/ChoiceControl.xaml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_Sunrise/ChoiceControl.xaml.cs b/Content.Client/_Sunrise/ChoiceControl.xaml.cs
new file mode 100644
index 00000000000..841c9d6a2a0
--- /dev/null
+++ b/Content.Client/_Sunrise/ChoiceControl.xaml.cs
@@ -0,0 +1,27 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Utility;
+// Taken from RMC14 build.
+// https://github.com/RMC-14/RMC-14
+namespace Content.Client._Sunrise;
+
+[GenerateTypedNameReferences]
+[Virtual]
+public partial class ChoiceControl : Control
+{
+ public ChoiceControl() => RobustXamlLoader.Load(this);
+
+ public void Set(string name, Texture? texture)
+ {
+ NameLabel.SetMessage(name);
+ Texture.Texture = texture;
+ }
+
+ public void Set(FormattedMessage msg, Texture? texture)
+ {
+ NameLabel.SetMessage(msg);
+ Texture.Texture = texture;
+ }
+}
diff --git a/Content.Client/_Sunrise/Footprints/FootPrintsVisualizerSystem.cs b/Content.Client/_Sunrise/Footprints/FootPrintsVisualizerSystem.cs
new file mode 100644
index 00000000000..d17617e6ba1
--- /dev/null
+++ b/Content.Client/_Sunrise/Footprints/FootPrintsVisualizerSystem.cs
@@ -0,0 +1,148 @@
+using Content.Shared._Sunrise.Footprints;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Shared.Random;
+
+namespace Content.Client._Sunrise.Footprints;
+
+///
+/// Handles the visual appearance and updates of footprint entities on the client
+///
+public sealed class FootprintVisualizerSystem : VisualizerSystem
+{
+ [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+
+ ///
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnFootprintInitialized);
+ SubscribeLocalEvent(OnFootprintShutdown);
+ }
+
+ ///
+ /// Initializes the visual appearance of a new footprint
+ ///
+ private void OnFootprintInitialized(EntityUid uid, FootprintComponent component, ComponentInit args)
+ {
+ if (!TryComp(uid, out var sprite))
+ return;
+
+ InitializeSpriteLayers(sprite);
+ UpdateFootprintVisuals(uid, component, sprite);
+ }
+
+ ///
+ /// Cleans up the visual elements when a footprint is removed
+ ///
+ private void OnFootprintShutdown(EntityUid uid, FootprintComponent component, ComponentShutdown args)
+ {
+ if (!TryComp(uid, out var sprite))
+ return;
+
+ RemoveSpriteLayers(sprite);
+ }
+
+ ///
+ /// Sets up the initial sprite layers for the footprint
+ ///
+ private void InitializeSpriteLayers(SpriteComponent sprite)
+ {
+ sprite.LayerMapReserveBlank(FootprintSpriteLayer.MainLayer);
+ }
+
+ ///
+ /// Removes sprite layers when cleaning up footprint
+ ///
+ private void RemoveSpriteLayers(SpriteComponent sprite)
+ {
+ if (sprite.LayerMapTryGet(FootprintSpriteLayer.MainLayer, out var layer))
+ {
+ sprite.RemoveLayer(layer);
+ }
+ }
+
+ ///
+ /// Updates the visual appearance of a footprint based on its current state
+ ///
+ private void UpdateFootprintVisuals(EntityUid uid, FootprintComponent footprint, SpriteComponent sprite)
+ {
+ if (!sprite.LayerMapTryGet(FootprintSpriteLayer.MainLayer, out var layer)
+ || !TryComp(footprint.CreatorEntity, out var emitterComponent)
+ || !TryComp(uid, out var appearance))
+ return;
+
+ if (!_appearanceSystem.TryGetData(
+ uid,
+ FootprintVisualParameter.VisualState,
+ out var visualType,
+ appearance))
+ return;
+
+ UpdateSpriteState(sprite, layer, visualType, emitterComponent);
+ UpdateSpriteColor(sprite, layer, uid, appearance);
+ }
+
+ ///
+ /// Updates the sprite state based on the footprint type
+ ///
+ private void UpdateSpriteState(
+ SpriteComponent sprite,
+ int layer,
+ FootprintVisualType visualType,
+ FootprintEmitterComponent emitter)
+ {
+ var stateId = new RSI.StateId(GetStateId(visualType, emitter));
+ sprite.LayerSetState(layer, stateId, emitter.SpritePath);
+ }
+
+ ///
+ /// Determines the appropriate state ID for the footprint based on its type
+ ///
+ private string GetStateId(FootprintVisualType visualType, FootprintEmitterComponent emitter)
+ {
+ return visualType switch
+ {
+ FootprintVisualType.BareFootprint => emitter.IsRightStep
+ ? emitter.RightBareFootState
+ : emitter.LeftBareFootState,
+ FootprintVisualType.ShoeFootprint => emitter.ShoeFootState,
+ FootprintVisualType.SuitFootprint => emitter.PressureSuitFootState,
+ FootprintVisualType.DragMark => _random.Pick(emitter.DraggingStates),
+ _ => throw new ArgumentOutOfRangeException(
+ $"Unknown footprint visual type: {visualType}")
+ };
+ }
+
+ ///
+ /// Updates the sprite color based on appearance data
+ ///
+ private void UpdateSpriteColor(
+ SpriteComponent sprite,
+ int layer,
+ EntityUid uid,
+ AppearanceComponent appearance)
+ {
+ if (_appearanceSystem.TryGetData(uid,
+ FootprintVisualParameter.TrackColor,
+ out var color,
+ appearance))
+ {
+ sprite.LayerSetColor(layer, color);
+ }
+ }
+
+ ///
+ protected override void OnAppearanceChange(
+ EntityUid uid,
+ FootprintComponent component,
+ ref AppearanceChangeEvent args)
+ {
+ if (args.Sprite is not { } sprite)
+ return;
+
+ UpdateFootprintVisuals(uid, component, sprite);
+ }
+}
diff --git a/Content.Client/_Sunrise/Medical/Surgery/SurgeryBui.cs b/Content.Client/_Sunrise/Medical/Surgery/SurgeryBui.cs
new file mode 100644
index 00000000000..df19251fbe1
--- /dev/null
+++ b/Content.Client/_Sunrise/Medical/Surgery/SurgeryBui.cs
@@ -0,0 +1,443 @@
+using Content.Client.Administration.UI.CustomControls;
+using Content.Client.Hands.Systems;
+using Content.Shared._Sunrise.Medical.Surgery;
+using Content.Shared.Body.Part;
+using JetBrains.Annotations;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.Player;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+using static Robust.Client.UserInterface.Control;
+
+namespace Content.Client._Sunrise.Medical.Surgery;
+// Based on the RMC14 build.
+// https://github.com/RMC-14/RMC-14
+
+[UsedImplicitly]
+public sealed class SurgeryBui : BoundUserInterface
+{
+ [Dependency] private readonly IEntityManager _entities = default!;
+ [Dependency] private readonly IPlayerManager _player = default!;
+
+ private readonly SurgerySystem _system;
+ private readonly HandsSystem _hands;
+
+ [ViewVariables]
+ private SurgeryWindow? _window;
+
+ private EntityUid? _part;
+ private (EntityUid Ent, EntProtoId Proto)? _surgery;
+ private readonly List _previousSurgeries = new();
+
+ public SurgeryBui(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ _system = _entities.System();
+ _hands = _entities.System();
+
+ _system.OnRefresh += UpdateDisabledPanel;
+ _hands.OnPlayerItemAdded += OnPlayerItemAdded;
+ }
+ private DateTime _lastRefresh = DateTime.UtcNow;
+ private (string k1, EntityUid k2) _throttling = ("", new EntityUid());
+ private void OnPlayerItemAdded(string k1, EntityUid k2)
+ {
+ if (_throttling.k1.Equals(k1) && _throttling.k2.Equals(k2) && DateTime.UtcNow - _lastRefresh < TimeSpan.FromSeconds(1)) return;
+ _throttling = (k1, k2);
+ _lastRefresh = DateTime.UtcNow;
+ RefreshUI();
+ }
+ protected override void Open() => UpdateState(State);
+ protected override void UpdateState(BoundUserInterfaceState? state)
+ {
+ if (state is SurgeryBuiState s)
+ Update(s);
+ }
+
+ private void Update(SurgeryBuiState state)
+ {
+ TryInitWindow();
+
+ _window!.Surgeries.DisposeAllChildren();
+ _window.Steps.DisposeAllChildren();
+ _window.Parts.DisposeAllChildren();
+
+ View(ViewType.Parts);
+
+ var oldSurgery = _surgery;
+ var oldPart = _part;
+ _part = null;
+ _surgery = null;
+
+ var parts = new List>(state.Choices.Keys.Count);
+ foreach (var choice in state.Choices.Keys)
+ {
+ if (_entities.TryGetEntity(choice, out var ent) &&
+ _entities.TryGetComponent(ent, out BodyPartComponent? part))
+ {
+ parts.Add((ent.Value, part));
+ }
+ }
+
+ parts.Sort((a, b) =>
+ {
+ static int GetScore(Entity part)
+ => part.Comp.PartType switch
+ {
+ BodyPartType.Head => 1,
+ BodyPartType.Torso => 2,
+ BodyPartType.Arm => 3,
+ BodyPartType.Hand => 4,
+ BodyPartType.Leg => 5,
+ BodyPartType.Foot => 6,
+ BodyPartType.Tail => 7,
+ BodyPartType.Other => 8,
+ _ => 0
+ };
+
+ return GetScore(a) - GetScore(b);
+ });
+
+ foreach (var part in parts)
+ {
+ var netPart = _entities.GetNetEntity(part.Owner);
+ var surgeries = state.Choices[netPart];
+ var partName = _entities.GetComponent(part).EntityName;
+ var partButton = new ChoiceControl();
+
+ partButton.Set(partName, null);
+ partButton.Button.OnPressed += _ => OnPartPressed(netPart, surgeries);
+
+ _window.Parts.AddChild(partButton);
+
+ foreach (var (surgeryId, suffix, isCompleted) in surgeries)
+ {
+ if (_system.GetSingleton(surgeryId) is not { } surgery ||
+ !_entities.TryGetComponent(surgery, out SurgeryComponent? surgeryComp))
+ {
+ continue;
+ }
+
+ if (oldPart == part && oldSurgery?.Proto == surgeryId)
+ OnSurgeryPressed((surgery, surgeryComp), netPart, surgeryId);
+ }
+
+ if (oldPart == part && oldSurgery == null)
+ OnPartPressed(netPart, surgeries);
+ }
+
+ RefreshUI();
+
+ if (!_window.IsOpen)
+ _window.OpenCentered();
+ }
+
+ private void TryInitWindow()
+ {
+ if (_window != null) return;
+ _window = new SurgeryWindow();
+ _window.OnClose += Close;
+ _window.Title = Loc.GetString("surgery-window-name");
+
+ _window.PartsButton.OnPressed += _ =>
+ {
+ _part = null;
+ _surgery = null;
+ _previousSurgeries.Clear();
+ View(ViewType.Parts);
+ };
+
+ _window.SurgeriesButton.OnPressed += _ =>
+ {
+ _surgery = null;
+ _previousSurgeries.Clear();
+
+ if (!_entities.TryGetNetEntity(_part, out var netPart) ||
+ State is not SurgeryBuiState s ||
+ !s.Choices.TryGetValue(netPart.Value, out var surgeries))
+ {
+ return;
+ }
+
+ OnPartPressed(netPart.Value, surgeries);
+ };
+
+ _window.StepsButton.OnPressed += _ =>
+ {
+ if (!_entities.TryGetNetEntity(_part, out var netPart) ||
+ _previousSurgeries.Count == 0)
+ {
+ return;
+ }
+
+ var last = _previousSurgeries[^1];
+ _previousSurgeries.RemoveAt(_previousSurgeries.Count - 1);
+
+ if (_system.GetSingleton(last) is not { } previousId ||
+ !_entities.TryGetComponent(previousId, out SurgeryComponent? previous))
+ {
+ return;
+ }
+
+ OnSurgeryPressed((previousId, previous), netPart.Value, last);
+ };
+ }
+
+ private void AddStep(EntProtoId stepId, NetEntity netPart, EntProtoId surgeryId)
+ {
+ if (_window == null ||
+ _system.GetSingleton(stepId) is not { } step)
+ {
+ return;
+ }
+
+ var stepName = new FormattedMessage();
+ stepName.AddText(_entities.GetComponent(step).EntityName);
+
+ var stepButton = new SurgeryStepButton { Step = step };
+ stepButton.Button.OnPressed += _ => SendMessage(new SurgeryStepChosenBuiMsg()
+ {
+ Step = stepId,
+ Part = netPart,
+ Surgery = surgeryId,
+ });
+
+ _window.Steps.AddChild(stepButton);
+ }
+
+ private void OnSurgeryPressed(Entity surgery, NetEntity netPart, EntProtoId surgeryId)
+ {
+ if (_window == null)
+ return;
+
+ _part = _entities.GetEntity(netPart);
+ _surgery = (surgery, surgeryId);
+
+ _window.Steps.DisposeAllChildren();
+
+ if (surgery.Comp.Requirement is { } requirementId && _system.GetSingleton(requirementId) is { } requirement)
+ {
+ var label = new ChoiceControl();
+ label.Button.OnPressed += _ =>
+ {
+ _previousSurgeries.Add(surgeryId);
+
+ if (_entities.TryGetComponent(requirement, out SurgeryComponent? requirementComp))
+ OnSurgeryPressed((requirement, requirementComp), netPart, requirementId);
+ };
+
+ var msg = new FormattedMessage();
+ var surgeryName = _entities.GetComponent(requirement).EntityName;
+ msg.AddMarkupOrThrow(Loc.GetString("surgery-window-reguires", ("surgeryname", surgeryName)));
+ label.Set(msg, null);
+
+ _window.Steps.AddChild(label);
+ _window.Steps.AddChild(new HSeparator(Color.FromHex("#4972A1")) { Margin = new Thickness(0, 0, 0, 1) });
+ }
+
+ foreach (var stepId in surgery.Comp.Steps)
+ {
+ AddStep(stepId, netPart, surgeryId);
+ }
+
+ View(ViewType.Steps);
+ RefreshUI();
+ }
+
+ private void OnPartPressed(NetEntity netPart, List<(EntProtoId, string, bool)> surgeryIds)
+ {
+ if (_window == null)
+ return;
+
+ _part = _entities.GetEntity(netPart);
+
+ _window.Surgeries.DisposeAllChildren();
+
+ var surgeries = new List<(Entity Ent, EntProtoId Id, string Name, bool IsCompleted, Texture?)>();
+ foreach (var (surgeryId, suffix, isCompleted) in surgeryIds)
+ {
+ if (_system.GetSingleton(surgeryId) is not { } surgery ||
+ !_entities.TryGetComponent(surgery, out SurgeryComponent? surgeryComp))
+ {
+ continue;
+ }
+
+ var texture = _entities.GetComponentOrNull(surgery)?.Icon?.Default;
+ var name = $"{_entities.GetComponent(surgery).EntityName} {suffix}";
+ surgeries.Add(((surgery, surgeryComp), surgeryId, name, isCompleted, texture));
+ }
+
+ surgeries.Sort((a, b) =>
+ {
+ var priority = a.Ent.Comp.Priority.CompareTo(b.Ent.Comp.Priority);
+ if (priority != 0)
+ return priority;
+
+ return string.Compare(a.Name, b.Name, StringComparison.Ordinal);
+ });
+
+ foreach (var (Ent, Id, Name, IsCompleted, texture) in surgeries)
+ {
+ var surgeryButton = new ChoiceControl();
+
+ surgeryButton.Set(Name, texture);
+ if(IsCompleted)
+ surgeryButton.Button.Modulate = Color.Green;
+ surgeryButton.Button.OnPressed += _ => OnSurgeryPressed(Ent, netPart, Id);
+ _window.Surgeries.AddChild(surgeryButton);
+ }
+
+ RefreshUI();
+ View(ViewType.Surgeries);
+ }
+
+ private void RefreshUI()
+ {
+ if (_window == null ||
+ !_entities.HasComponent(_surgery?.Ent) ||
+ !_entities.TryGetComponent(_part, out BodyPartComponent? part))
+ {
+ return;
+ }
+
+ var next = _system.GetNextStep(Owner, _part.Value, _surgery.Value.Ent);
+ var i = 0;
+ foreach (var child in _window.Steps.Children)
+ {
+ if (child is not SurgeryStepButton stepButton)
+ continue;
+
+ var status = StepStatus.Incomplete;
+ if (next == null)
+ {
+ status = StepStatus.Complete;
+ }
+ else if (next.Value.Surgery.Owner != _surgery.Value.Ent)
+ {
+ status = StepStatus.Incomplete;
+ }
+ else if (next.Value.Step == i)
+ {
+ status = StepStatus.Next;
+ }
+ else if (i < next.Value.Step)
+ {
+ status = StepStatus.Complete;
+ }
+
+ stepButton.Button.Disabled = status != StepStatus.Next;
+
+ var stepName = new FormattedMessage();
+ stepName.AddText(_entities.GetComponent(stepButton.Step).EntityName);
+
+ if (status == StepStatus.Complete)
+ {
+ stepButton.Button.Modulate = Color.Green;
+ }
+ else if (status == StepStatus.Next)
+ {
+ stepButton.Button.Modulate = Color.White;
+ if (_player.LocalEntity is { } player &&
+ !_system.CanPerformStep(player, Owner, part.PartType, stepButton.Step, false, out var popup, out var reason, out _))
+ {
+ stepButton.ToolTip = popup;
+ stepButton.Button.Disabled = true;
+
+ switch (reason)
+ {
+ case StepInvalidReason.NeedsOperatingTable:
+ stepName.AddMarkupOrThrow(Loc.GetString("surgery-window-reguires-table"));
+ break;
+ case StepInvalidReason.Armor:
+ stepName.AddMarkupOrThrow(Loc.GetString("surgery-window-reguires-undress"));
+ break;
+ case StepInvalidReason.MissingTool:
+ stepName.AddMarkupOrThrow(Loc.GetString("surgery-window-reguires-tool"));
+ break;
+ }
+ }
+ }
+
+ var texture = _entities.GetComponentOrNull(stepButton.Step)?.Icon?.Default;
+ stepButton.Set(stepName, texture);
+ i++;
+ }
+ }
+
+ private void UpdateDisabledPanel()
+ {
+ if (_window == null)
+ return;
+
+ if (_system.IsLyingDown(Owner))
+ {
+ _window.DisabledPanel.Visible = false;
+ _window.DisabledPanel.MouseFilter = MouseFilterMode.Ignore;
+ return;
+ }
+
+ _window.DisabledPanel.Visible = true;
+ if (_window.DisabledLabel.GetMessage() is null)
+ {
+ var text = new FormattedMessage();
+ text.AddMarkupOrThrow(Loc.GetString("surgery-window-reguires-laydown"));
+ _window.DisabledLabel.SetMessage(text);
+ }
+
+ _window.DisabledPanel.MouseFilter = MouseFilterMode.Stop;
+ }
+
+ private void View(ViewType type)
+ {
+ if (_window == null)
+ return;
+
+ _window.PartsButton.Parent!.Margin = new Thickness(0, 0, 0, 10);
+
+ _window.Parts.Visible = type == ViewType.Parts;
+ _window.PartsButton.Disabled = type == ViewType.Parts;
+
+ _window.Surgeries.Visible = type == ViewType.Surgeries;
+ _window.SurgeriesButton.Disabled = type != ViewType.Steps;
+
+ _window.Steps.Visible = type == ViewType.Steps;
+ _window.StepsButton.Disabled = type != ViewType.Steps || _previousSurgeries.Count == 0;
+
+ if (_entities.TryGetComponent(_part, out MetaDataComponent? partMeta) &&
+ _entities.TryGetComponent(_surgery?.Ent, out MetaDataComponent? surgeryMeta))
+ {
+ _window.Title = $"{Loc.GetString("surgery-window-name")} - {partMeta.EntityName}, {surgeryMeta.EntityName}";
+ }
+ else if (partMeta != null)
+ {
+ _window.Title = $"{Loc.GetString("surgery-window-name")} - {partMeta.EntityName}";
+ }
+ else
+ {
+ _window.Title = Loc.GetString("surgery-window-name");
+ }
+ }
+
+ private enum ViewType
+ {
+ Parts,
+ Surgeries,
+ Steps
+ }
+
+ private enum StepStatus
+ {
+ Next,
+ Complete,
+ Incomplete
+ }
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+
+ if (disposing)
+ _window?.Dispose();
+ _system.OnRefresh -= UpdateDisabledPanel;
+ _hands.OnPlayerItemAdded -= OnPlayerItemAdded;
+ }
+}
diff --git a/Content.Client/_Sunrise/Medical/Surgery/SurgeryStepButton.xaml b/Content.Client/_Sunrise/Medical/Surgery/SurgeryStepButton.xaml
new file mode 100644
index 00000000000..6b08f040e6e
--- /dev/null
+++ b/Content.Client/_Sunrise/Medical/Surgery/SurgeryStepButton.xaml
@@ -0,0 +1,4 @@
+
+
diff --git a/Content.Client/_Sunrise/Medical/Surgery/SurgeryStepButton.xaml.cs b/Content.Client/_Sunrise/Medical/Surgery/SurgeryStepButton.xaml.cs
new file mode 100644
index 00000000000..90f3e3fdf45
--- /dev/null
+++ b/Content.Client/_Sunrise/Medical/Surgery/SurgeryStepButton.xaml.cs
@@ -0,0 +1,13 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client._Sunrise.Medical.Surgery;
+// Based on the RMC14.
+// https://github.com/RMC-14/RMC-14
+[GenerateTypedNameReferences]
+public sealed partial class SurgeryStepButton : ChoiceControl
+{
+ public EntityUid Step { get; set; }
+
+ public SurgeryStepButton() => RobustXamlLoader.Load(this);
+}
diff --git a/Content.Client/_Sunrise/Medical/Surgery/SurgerySystem.cs b/Content.Client/_Sunrise/Medical/Surgery/SurgerySystem.cs
new file mode 100644
index 00000000000..7668fed97c3
--- /dev/null
+++ b/Content.Client/_Sunrise/Medical/Surgery/SurgerySystem.cs
@@ -0,0 +1,17 @@
+using Content.Shared._Sunrise.Medical.Surgery;
+
+namespace Content.Client._Sunrise.Medical.Surgery;
+// Based on the RMC14.
+// https://github.com/RMC-14/RMC-14
+public sealed class SurgerySystem : SharedSurgerySystem
+{
+ public event Action? OnRefresh;
+ public override void Update(float frameTime)
+ {
+ _delayAccumulator += frameTime;
+ if (_delayAccumulator > 1) {
+ _delayAccumulator = 0;
+ OnRefresh?.Invoke();
+ }
+ }
+}
diff --git a/Content.Client/_Sunrise/Medical/Surgery/SurgeryWindow.xaml b/Content.Client/_Sunrise/Medical/Surgery/SurgeryWindow.xaml
new file mode 100644
index 00000000000..42b770950ac
--- /dev/null
+++ b/Content.Client/_Sunrise/Medical/Surgery/SurgeryWindow.xaml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_Sunrise/Medical/Surgery/SurgeryWindow.xaml.cs b/Content.Client/_Sunrise/Medical/Surgery/SurgeryWindow.xaml.cs
new file mode 100644
index 00000000000..5df31b5e6ce
--- /dev/null
+++ b/Content.Client/_Sunrise/Medical/Surgery/SurgeryWindow.xaml.cs
@@ -0,0 +1,12 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client._Sunrise.Medical.Surgery;
+// Based on the RMC14.
+// https://github.com/RMC-14/RMC-14
+[GenerateTypedNameReferences]
+public sealed partial class SurgeryWindow : DefaultWindow
+{
+ public SurgeryWindow() => RobustXamlLoader.Load(this);
+}
diff --git a/Content.IntegrationTests/Tests/PostMapInitTest.cs b/Content.IntegrationTests/Tests/PostMapInitTest.cs
index e9af43f5cb3..3e58914abfe 100644
--- a/Content.IntegrationTests/Tests/PostMapInitTest.cs
+++ b/Content.IntegrationTests/Tests/PostMapInitTest.cs
@@ -43,6 +43,7 @@ public sealed class PostMapInitTest
"/Maps/Shuttles/cargo.yml",
"/Maps/Shuttles/emergency.yml",
"/Maps/Shuttles/infiltrator.yml",
+ "/Maps/_Sunrise/Shuttles/infiltrator.yml",
};
private static readonly string[] GameMaps =
diff --git a/Content.Server/Administration/Managers/BanManager.cs b/Content.Server/Administration/Managers/BanManager.cs
index a9068e98863..df8c0641fe9 100644
--- a/Content.Server/Administration/Managers/BanManager.cs
+++ b/Content.Server/Administration/Managers/BanManager.cs
@@ -222,7 +222,8 @@ public async void CreateServerBan(NetUserId? target, string? targetUsername, Net
("name", targetName),
("ip", addressRangeString),
("hwid", hwidString),
- ("reason", reason));
+ ("reason", reason),
+ ("round", roundId == null ? Loc.GetString("server-ban-unknown-round") : roundId));
_sawmill.Info(logMessage);
_chat.SendAdminAlert(logMessage);
diff --git a/Content.Server/Changeling/ChangelingSystem.Abilities.cs b/Content.Server/Changeling/ChangelingSystem.Abilities.cs
index 2c6f7100d18..9d5a8b75384 100644
--- a/Content.Server/Changeling/ChangelingSystem.Abilities.cs
+++ b/Content.Server/Changeling/ChangelingSystem.Abilities.cs
@@ -550,7 +550,7 @@ private void OnEphedrineOverdose(EntityUid uid, ChangelingComponent comp, ref Ac
var reagents = new List<(string, FixedPoint2)>()
{
- ("Synaptizine", 5f)
+ ("Ephedrine", 15f)
};
if (TryInjectReagents(uid, reagents))
_popup.PopupEntity(Loc.GetString("changeling-inject"), uid, uid);
diff --git a/Content.Server/Changeling/ChangelingSystem.cs b/Content.Server/Changeling/ChangelingSystem.cs
index 61485762ddb..19f04cad870 100644
--- a/Content.Server/Changeling/ChangelingSystem.cs
+++ b/Content.Server/Changeling/ChangelingSystem.cs
@@ -168,7 +168,7 @@ private void UpdateChemicals(EntityUid uid, ChangelingComponent comp, float? amo
}
private void UpdateBiomass(EntityUid uid, ChangelingComponent comp, float? amount = null)
{
- comp.Biomass += amount ?? -1;
+ comp.Biomass += amount ?? 0;
comp.Biomass = Math.Clamp(comp.Biomass, 0, comp.MaxBiomass);
Dirty(uid, comp);
_alerts.ShowAlert(uid, "ChangelingBiomass");
@@ -387,13 +387,18 @@ public bool TryReagentSting(EntityUid uid, ChangelingComponent comp, EntityTarge
}
public bool TryToggleItem(EntityUid uid, EntProtoId proto, ChangelingComponent comp, string? clothingSlot = null)
{
- if (!comp.Equipment.TryGetValue(proto.Id, out var item) && item == null)
+ if (!comp.Equipment.TryGetValue(proto.Id, out var item))
{
item = Spawn(proto, Transform(uid).Coordinates);
- if (clothingSlot != null && !_inventory.TryEquip(uid, (EntityUid) item, clothingSlot, force: true))
+ if (clothingSlot != null)
{
- QueueDel(item);
- return false;
+ if (!_inventory.TryEquip(uid, (EntityUid) item, clothingSlot, force: true))
+ {
+ QueueDel(item);
+ return false;
+ }
+ comp.Equipment.Add(proto.Id, item);
+ return true;
}
else if (!_hands.TryForcePickupAnyHand(uid, (EntityUid) item))
{
diff --git a/Content.Server/Dragon/Components/DragonRiftComponent.cs b/Content.Server/Dragon/Components/DragonRiftComponent.cs
index f01c35cbbb0..fa49b1f1f18 100644
--- a/Content.Server/Dragon/Components/DragonRiftComponent.cs
+++ b/Content.Server/Dragon/Components/DragonRiftComponent.cs
@@ -21,7 +21,7 @@ public sealed partial class DragonRiftComponent : SharedDragonRiftComponent
///
/// The maximum amount we can accumulate before becoming impervious.
///
- [ViewVariables(VVAccess.ReadWrite), DataField("maxAccumuluator")] public float MaxAccumulator = 200f;
+ [ViewVariables(VVAccess.ReadWrite), DataField("maxAccumuluator")] public float MaxAccumulator = 300f;
///
/// Accumulation of the spawn timer.
@@ -33,7 +33,7 @@ public sealed partial class DragonRiftComponent : SharedDragonRiftComponent
/// How long it takes for a new spawn to be added.
///
[ViewVariables(VVAccess.ReadWrite), DataField("spawnCooldown")]
- public float SpawnCooldown = 20f;
+ public float SpawnCooldown = 30f;
[ViewVariables(VVAccess.ReadWrite), DataField("spawn", customTypeSerializer: typeof(PrototypeIdSerializer))]
public string SpawnPrototype = "MobCarpDragon";
diff --git a/Content.Server/GameTicking/Rules/VampireRuleSystem.cs b/Content.Server/GameTicking/Rules/VampireRuleSystem.cs
index b03ed8bdf67..91861c19228 100644
--- a/Content.Server/GameTicking/Rules/VampireRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/VampireRuleSystem.cs
@@ -5,6 +5,7 @@
using Content.Server.Objectives;
using Content.Server.Roles;
using Content.Server.Vampire;
+using Content.Shared.Alert;
using Content.Shared.Vampire.Components;
using Content.Shared.NPC.Prototypes;
using Content.Shared.NPC.Systems;
@@ -25,6 +26,7 @@ public sealed partial class VampireRuleSystem : GameRuleSystem(target);
+ EnsureComp(target);
+ var vampireAlertComponent = EnsureComp(target);
var interfaceComponent = EnsureComp(target);
if (HasComp(target))
@@ -99,6 +103,7 @@ public bool MakeVampire(EntityUid target, VampireRuleComponent rule)
_vampire.AddStartingAbilities(vampire);
_vampire.MakeVulnerableToHoly(vampire);
+ _alerts.ShowAlert(vampire, vampireAlertComponent.BloodAlert);
Random random = new Random();
diff --git a/Content.Server/Vampire/VampireSystem.Abilities.cs b/Content.Server/Vampire/VampireSystem.Abilities.cs
index 4cdf1a1104d..80beab048fe 100644
--- a/Content.Server/Vampire/VampireSystem.Abilities.cs
+++ b/Content.Server/Vampire/VampireSystem.Abilities.cs
@@ -22,6 +22,7 @@
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Popups;
+using Content.Shared.Polymorph;
using Content.Shared.Prying.Components;
using Content.Shared.Stealth.Components;
using Content.Shared.Store.Events;
@@ -324,10 +325,29 @@ private void Glare(Entity vampire, EntityUid? target, TimeSpan
}
private void PolymorphSelf(Entity vampire, string? polymorphTarget)
{
- if (polymorphTarget == null)
+ if (string.IsNullOrEmpty(polymorphTarget))
return;
+
+ var prototypeId = polymorphTarget switch
+ {
+ "MobMouse" => "VampireMouse",
+ "mobBatVampire" => "VampireBat",
+ _ => null
+ };
+
+ if (prototypeId == null)
+ {
+ Logger.Warning($"Unknown polymorph target: {polymorphTarget}. Polymorph operation aborted.");
+ return;
+ }
+
+ if (!_prototypeManager.TryIndex(prototypeId, out var prototype))
+ {
+ Logger.Warning($"Unknown prototype: {prototypeId}. Polymorph operation aborted.");
+ return;
+ }
- _polymorph.PolymorphEntity(vampire, polymorphTarget);
+ _polymorph.PolymorphEntity(vampire, prototype);
}
private void BloodSteal(Entity vampire)
{
@@ -652,7 +672,7 @@ private void DrinkDoAfter(Entity entity, ref VampireDrinkBlood
if (_mind.TryGetMind(entity, out var mindId, out var mind))
if (_mind.TryGetObjectiveComp(mindId, out var objective, mind))
- objective.BloodDranked += entity.Comp.TotalBloodDrank;
+ objective.BloodDranked = entity.Comp.TotalBloodDrank;
//Slurp
_audio.PlayPvs(entity.Comp.BloodDrainSound, entity.Owner, AudioParams.Default.WithVolume(-3f));
diff --git a/Content.Server/Vampire/VampireSystem.Objectives.cs b/Content.Server/Vampire/VampireSystem.Objectives.cs
index 3e94fb6494c..31fcd82a4c0 100644
--- a/Content.Server/Vampire/VampireSystem.Objectives.cs
+++ b/Content.Server/Vampire/VampireSystem.Objectives.cs
@@ -12,14 +12,13 @@ public sealed partial class VampireSystem
private void InitializeObjectives()
{
-
SubscribeLocalEvent(OnBloodDrainGetProgress);
}
private void OnBloodDrainGetProgress(EntityUid uid, BloodDrainConditionComponent comp, ref ObjectiveGetProgressEvent args)
{
var target = _number.GetTarget(uid);
- if (target != 0)
+ if (target > 0)
args.Progress = MathF.Min(comp.BloodDranked / target, 1f);
else args.Progress = 1f;
}
diff --git a/Content.Server/Vampire/VampireSystem.cs b/Content.Server/Vampire/VampireSystem.cs
index 464a6e929f9..499fdf90f0b 100644
--- a/Content.Server/Vampire/VampireSystem.cs
+++ b/Content.Server/Vampire/VampireSystem.cs
@@ -75,6 +75,7 @@ public sealed partial class VampireSystem : EntitySystem
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly MetabolizerSystem _metabolism = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
+ [Dependency] private readonly SharedVampireSystem _vampire = default!;
private Dictionary _actionEntities = new();
@@ -214,16 +215,19 @@ public void UpdateBloodDisplay(EntityUid vampire)
private void OnVampireBloodChangedEvent(EntityUid uid, VampireComponent component, VampireBloodChangedEvent args)
{
+ if (TryComp(uid, out var alertComp))
+ _vampire.SetAlertBloodAmount(alertComp,_vampire.GetBloodEssence(uid).Int());
+
EntityUid? newEntity = null;
EntityUid entity = default;
// Mutations
- if (GetBloodEssence(uid) >= FixedPoint2.New(50) && !_actionEntities.TryGetValue(VampireComponent.MutationsActionPrototype, out entity))
+ if (_vampire.GetBloodEssence(uid) >= FixedPoint2.New(50) && !_actionEntities.TryGetValue(VampireComponent.MutationsActionPrototype, out entity))
{
_action.AddAction(uid, ref newEntity, VampireComponent.MutationsActionPrototype);
if (newEntity != null)
_actionEntities[VampireComponent.MutationsActionPrototype] = newEntity.Value;
}
- else if (GetBloodEssence(uid) < FixedPoint2.New(50) && _actionEntities.TryGetValue(VampireComponent.MutationsActionPrototype, out entity))
+ else if (_vampire.GetBloodEssence(uid) < FixedPoint2.New(50) && _actionEntities.TryGetValue(VampireComponent.MutationsActionPrototype, out entity))
{
if (!TryComp(uid, out ActionsComponent? comp))
return;
@@ -235,7 +239,7 @@ private void OnVampireBloodChangedEvent(EntityUid uid, VampireComponent componen
//Hemomancer
- if (GetBloodEssence(uid) >= FixedPoint2.New(200) && !_actionEntities.TryGetValue("ActionVampireBloodSteal", out entity) && component.CurrentMutation == VampireMutationsType.Hemomancer)
+ if (_vampire.GetBloodEssence(uid) >= FixedPoint2.New(200) && !_actionEntities.TryGetValue("ActionVampireBloodSteal", out entity) && component.CurrentMutation == VampireMutationsType.Hemomancer)
{
_action.AddAction(uid, ref newEntity , "ActionVampireBloodSteal");
if (newEntity != null)
@@ -247,7 +251,7 @@ private void OnVampireBloodChangedEvent(EntityUid uid, VampireComponent componen
}
}
}
- else if (GetBloodEssence(uid) < FixedPoint2.New(200) && _actionEntities.TryGetValue("ActionVampireBloodSteal", out entity))
+ else if (_vampire.GetBloodEssence(uid) < FixedPoint2.New(200) && _actionEntities.TryGetValue("ActionVampireBloodSteal", out entity))
{
if (!TryComp(uid, out ActionsComponent? comp))
return;
@@ -257,7 +261,7 @@ private void OnVampireBloodChangedEvent(EntityUid uid, VampireComponent componen
_actionEntities.Remove("ActionVampireBloodSteal");
}
- if (GetBloodEssence(uid) >= FixedPoint2.New(300) && !_actionEntities.TryGetValue("ActionVampireScreech", out entity) && component.CurrentMutation == VampireMutationsType.Hemomancer)
+ if (_vampire.GetBloodEssence(uid) >= FixedPoint2.New(300) && !_actionEntities.TryGetValue("ActionVampireScreech", out entity) && component.CurrentMutation == VampireMutationsType.Hemomancer)
{
_action.AddAction(uid, ref newEntity , "ActionVampireScreech");
if (newEntity != null)
@@ -269,7 +273,7 @@ private void OnVampireBloodChangedEvent(EntityUid uid, VampireComponent componen
}
}
}
- else if (GetBloodEssence(uid) < FixedPoint2.New(300) && _actionEntities.TryGetValue("ActionVampireScreech", out entity))
+ else if (_vampire.GetBloodEssence(uid) < FixedPoint2.New(300) && _actionEntities.TryGetValue("ActionVampireScreech", out entity))
{
if (!TryComp(uid, out ActionsComponent? comp))
return;
@@ -281,7 +285,7 @@ private void OnVampireBloodChangedEvent(EntityUid uid, VampireComponent componen
//Umbrae
- if (GetBloodEssence(uid) >= FixedPoint2.New(200) && !_actionEntities.TryGetValue("ActionVampireGlare", out entity) && component.CurrentMutation == VampireMutationsType.Umbrae)
+ if (_vampire.GetBloodEssence(uid) >= FixedPoint2.New(200) && !_actionEntities.TryGetValue("ActionVampireGlare", out entity) && component.CurrentMutation == VampireMutationsType.Umbrae)
{
_action.AddAction(uid, ref newEntity , "ActionVampireGlare");
if (newEntity != null)
@@ -293,7 +297,7 @@ private void OnVampireBloodChangedEvent(EntityUid uid, VampireComponent componen
}
}
}
- else if (GetBloodEssence(uid) < FixedPoint2.New(200) && _actionEntities.TryGetValue("ActionVampireGlare", out entity))
+ else if (_vampire.GetBloodEssence(uid) < FixedPoint2.New(200) && _actionEntities.TryGetValue("ActionVampireGlare", out entity))
{
if (!TryComp(uid, out ActionsComponent? comp))
return;
@@ -303,7 +307,7 @@ private void OnVampireBloodChangedEvent(EntityUid uid, VampireComponent componen
_actionEntities.Remove("ActionVampireGlare");
}
- if (GetBloodEssence(uid) >= FixedPoint2.New(300) && !_actionEntities.TryGetValue("ActionVampireCloakOfDarkness", out entity) && component.CurrentMutation == VampireMutationsType.Umbrae)
+ if (_vampire.GetBloodEssence(uid) >= FixedPoint2.New(300) && !_actionEntities.TryGetValue("ActionVampireCloakOfDarkness", out entity) && component.CurrentMutation == VampireMutationsType.Umbrae)
{
_action.AddAction(uid, ref newEntity , "ActionVampireCloakOfDarkness");
if (newEntity != null)
@@ -315,7 +319,7 @@ private void OnVampireBloodChangedEvent(EntityUid uid, VampireComponent componen
}
}
}
- else if (GetBloodEssence(uid) < FixedPoint2.New(300) && _actionEntities.TryGetValue("ActionVampireCloakOfDarkness", out entity))
+ else if (_vampire.GetBloodEssence(uid) < FixedPoint2.New(300) && _actionEntities.TryGetValue("ActionVampireCloakOfDarkness", out entity))
{
if (!TryComp(uid, out ActionsComponent? comp))
return;
@@ -327,7 +331,7 @@ private void OnVampireBloodChangedEvent(EntityUid uid, VampireComponent componen
//Gargantua
- if (GetBloodEssence(uid) >= FixedPoint2.New(200) && !_actionEntities.TryGetValue("ActionVampireUnnaturalStrength", out entity) && component.CurrentMutation == VampireMutationsType.Gargantua)
+ if (_vampire.GetBloodEssence(uid) >= FixedPoint2.New(200) && !_actionEntities.TryGetValue("ActionVampireUnnaturalStrength", out entity) && component.CurrentMutation == VampireMutationsType.Gargantua)
{
var vampire = new Entity(uid, component);
@@ -336,7 +340,7 @@ private void OnVampireBloodChangedEvent(EntityUid uid, VampireComponent componen
_actionEntities["ActionVampireUnnaturalStrength"] = vampire;
}
- if (GetBloodEssence(uid) >= FixedPoint2.New(300) && !_actionEntities.TryGetValue("ActionVampireSupernaturalStrength", out entity) && component.CurrentMutation == VampireMutationsType.Gargantua)
+ if (_vampire.GetBloodEssence(uid) >= FixedPoint2.New(300) && !_actionEntities.TryGetValue("ActionVampireSupernaturalStrength", out entity) && component.CurrentMutation == VampireMutationsType.Gargantua)
{
var vampire = new Entity(uid, component);
@@ -347,7 +351,7 @@ private void OnVampireBloodChangedEvent(EntityUid uid, VampireComponent componen
//Bestia
- if (GetBloodEssence(uid) >= FixedPoint2.New(200) && !_actionEntities.TryGetValue("ActionVampireBatform", out entity) && component.CurrentMutation == VampireMutationsType.Bestia)
+ if (_vampire.GetBloodEssence(uid) >= FixedPoint2.New(200) && !_actionEntities.TryGetValue("ActionVampireBatform", out entity) && component.CurrentMutation == VampireMutationsType.Bestia)
{
_action.AddAction(uid, ref newEntity , "ActionVampireBatform");
if (newEntity != null)
@@ -359,7 +363,7 @@ private void OnVampireBloodChangedEvent(EntityUid uid, VampireComponent componen
}
}
}
- else if (GetBloodEssence(uid) < FixedPoint2.New(200) && _actionEntities.TryGetValue("ActionVampireBatform", out entity))
+ else if (_vampire.GetBloodEssence(uid) < FixedPoint2.New(200) && _actionEntities.TryGetValue("ActionVampireBatform", out entity))
{
if (!TryComp(uid, out ActionsComponent? comp))
return;
@@ -369,7 +373,7 @@ private void OnVampireBloodChangedEvent(EntityUid uid, VampireComponent componen
_actionEntities.Remove("ActionVampireBatform");
}
- if (GetBloodEssence(uid) >= FixedPoint2.New(300) && !_actionEntities.TryGetValue("ActionVampireMouseform", out entity) && component.CurrentMutation == VampireMutationsType.Bestia)
+ if (_vampire.GetBloodEssence(uid) >= FixedPoint2.New(300) && !_actionEntities.TryGetValue("ActionVampireMouseform", out entity) && component.CurrentMutation == VampireMutationsType.Bestia)
{
_action.AddAction(uid, ref newEntity , "ActionVampireMouseform");
if (newEntity != null)
@@ -381,7 +385,7 @@ private void OnVampireBloodChangedEvent(EntityUid uid, VampireComponent componen
}
}
}
- else if (GetBloodEssence(uid) < FixedPoint2.New(300) && _actionEntities.TryGetValue("ActionVampireMouseform", out entity))
+ else if (_vampire.GetBloodEssence(uid) < FixedPoint2.New(300) && _actionEntities.TryGetValue("ActionVampireMouseform", out entity))
{
if (!TryComp(uid, out ActionsComponent? comp))
return;
@@ -391,17 +395,6 @@ private void OnVampireBloodChangedEvent(EntityUid uid, VampireComponent componen
_actionEntities.Remove("ActionVampireMouseform");
}
}
-
- private FixedPoint2 GetBloodEssence(EntityUid vampire)
- {
- if (!TryComp(vampire, out var comp))
- return 0;
-
- if (!comp.Balance.TryGetValue(VampireComponent.CurrencyProto, out var val))
- return 0;
-
- return val;
- }
private void DoSpaceDamage(Entity vampire)
{
diff --git a/Content.Server/_Sunrise/Footprints/FootPrintsSystem.cs b/Content.Server/_Sunrise/Footprints/FootPrintsSystem.cs
new file mode 100644
index 00000000000..a610b1c0c77
--- /dev/null
+++ b/Content.Server/_Sunrise/Footprints/FootPrintsSystem.cs
@@ -0,0 +1,224 @@
+using Content.Server.Atmos.Components;
+using Content.Shared._Sunrise.Footprints;
+using Content.Shared.Chemistry.Components.SolutionManager;
+using Content.Shared.Chemistry.EntitySystems;
+using Content.Shared.Inventory;
+using Content.Shared.Mobs;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Standing;
+using Robust.Shared.Map;
+using Robust.Shared.Random;
+
+namespace Content.Server._Sunrise.Footprints;
+
+///
+/// Handles creation and management of footprints left by entities as they move.
+///
+public sealed class FootprintSystem : EntitySystem
+{
+ #region Dependencies
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly InventorySystem _inventory = default!;
+ [Dependency] private readonly IMapManager _mapManager = default!;
+ [Dependency] private readonly SharedSolutionContainerSystem _solutionSystem = default!;
+ [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
+ [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
+ #endregion
+
+ #region Entity Queries
+ private EntityQuery _transformQuery;
+ private EntityQuery _mobStateQuery;
+ private EntityQuery _appearanceQuery;
+ #endregion
+
+ #region Initialization
+ ///
+ /// Initializes the footprint system and sets up required queries and subscriptions.
+ ///
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ _transformQuery = GetEntityQuery();
+ _mobStateQuery = GetEntityQuery();
+ _appearanceQuery = GetEntityQuery();
+
+ SubscribeLocalEvent(OnEmitterStartup);
+ SubscribeLocalEvent(OnEntityMove);
+ }
+
+ ///
+ /// Handles initialization of footprint emitter components.
+ ///
+ private void OnEmitterStartup(EntityUid uid, FootprintEmitterComponent component, ComponentStartup args)
+ {
+ // Add small random variation to step interval
+ component.WalkStepInterval = Math.Max(0f, component.WalkStepInterval + _random.NextFloat(-0.05f, 0.05f));
+ }
+ #endregion
+
+ #region Event Handlers
+ ///
+ /// Handles entity movement and creates footprints when appropriate.
+ ///
+ private void OnEntityMove(EntityUid uid, FootprintEmitterComponent emitter, ref MoveEvent args)
+ {
+ // Check if footprints should be created.
+ if (emitter.TrackColor.A <= 0f
+ || !_transformQuery.TryComp(uid, out var transform)
+ || !_mobStateQuery.TryComp(uid, out var mobState)
+ || !_mapManager.TryFindGridAt(_transformSystem.GetMapCoordinates((uid, transform)), out var gridUid, out _))
+ return;
+
+ var isBeingDragged =
+ mobState.CurrentThresholdState is MobState.Critical or MobState.Dead ||
+ (TryComp(uid, out var stateComponent) &&
+ stateComponent.CurrentState is StandingState.Lying);
+
+ var distanceMoved = (transform.LocalPosition - emitter.LastStepPosition).Length();
+ var requiredDistance = isBeingDragged ? emitter.DragMarkInterval : emitter.WalkStepInterval;
+
+ if (!(distanceMoved > requiredDistance))
+ return;
+
+ emitter.IsRightStep = !emitter.IsRightStep;
+
+ // Create new footprint entity.
+ var footprintEntity = SpawnFootprint(gridUid, emitter, uid, transform, isBeingDragged);
+
+ // Update footprint position and transfer reagents if applicable.
+ UpdateFootprint(footprintEntity, emitter, transform, isBeingDragged);
+
+ // Update emitter state.
+ UpdateEmitterState(emitter, transform);
+ }
+ #endregion
+
+ #region Footprint Creation and Management
+ ///
+ /// Creates a new footprint entity at the calculated position.
+ ///
+ private EntityUid SpawnFootprint(
+ EntityUid gridUid,
+ FootprintEmitterComponent emitter,
+ EntityUid emitterOwner,
+ TransformComponent transform,
+ bool isDragging)
+ {
+ var coords = CalculateFootprintPosition(gridUid, emitter, transform, isDragging);
+ var entity = Spawn(emitter.FootprintPrototype, coords);
+
+ var footprint = EnsureComp(entity);
+ footprint.CreatorEntity = emitterOwner;
+ Dirty(entity, footprint);
+
+ if (_appearanceQuery.TryComp(entity, out var appearance))
+ {
+ _appearanceSystem.SetData(entity,
+ FootprintVisualParameter.VisualState,
+ DetermineVisualState(emitterOwner, isDragging),
+ appearance);
+
+ _appearanceSystem.SetData(entity,
+ FootprintVisualParameter.TrackColor,
+ emitter.TrackColor,
+ appearance);
+ }
+
+ return entity;
+ }
+
+ ///
+ /// Updates footprint rotation and reagent transfer.
+ ///
+ private void UpdateFootprint(
+ EntityUid footprintEntity,
+ FootprintEmitterComponent emitter,
+ TransformComponent transform,
+ bool isDragging)
+ {
+ if (!_transformQuery.TryComp(footprintEntity, out var footprintTransform))
+ return;
+
+ footprintTransform.LocalRotation = isDragging
+ ? (transform.LocalPosition - emitter.LastStepPosition).ToAngle() + Angle.FromDegrees(-90f)
+ : transform.LocalRotation + Angle.FromDegrees(180f);
+
+ TransferReagents(footprintEntity, emitter);
+ }
+ #endregion
+
+ #region State Management
+ ///
+ /// Updates emitter state after creating a footprint.
+ ///
+ private void UpdateEmitterState(FootprintEmitterComponent emitter, TransformComponent transform)
+ {
+ emitter.TrackColor = emitter.TrackColor.WithAlpha(Math.Max(0f, emitter.TrackColor.A - emitter.ColorFadeRate));
+ emitter.LastStepPosition = transform.LocalPosition;
+ }
+
+ ///
+ /// Transfers reagents from emitter to footprint if applicable.
+ ///
+ private void TransferReagents(EntityUid footprintEntity, FootprintEmitterComponent emitter)
+ {
+ if (!TryComp(footprintEntity, out var container)
+ || !TryComp(footprintEntity, out var footprint)
+ || !_solutionSystem.ResolveSolution((footprintEntity, container),
+ footprint.ContainerName,
+ ref footprint.SolutionContainer,
+ out var solution)
+ || string.IsNullOrWhiteSpace(emitter.CurrentReagent)
+ || solution.Volume >= 1)
+ return;
+
+ _solutionSystem.TryAddReagent(footprint.SolutionContainer.Value,
+ emitter.CurrentReagent,
+ 1,
+ out _);
+ }
+ #endregion
+
+ #region Utility Methods
+ ///
+ /// Calculates the position where a footprint should be placed.
+ ///
+ private EntityCoordinates CalculateFootprintPosition(
+ EntityUid uid,
+ FootprintEmitterComponent emitter,
+ TransformComponent transform,
+ bool isDragging)
+ {
+ if (isDragging)
+ return new EntityCoordinates(uid, transform.LocalPosition);
+
+ var offset = emitter.IsRightStep
+ ? new Angle(Angle.FromDegrees(180f) + transform.LocalRotation)
+ .RotateVec(emitter.PlacementOffset)
+ : new Angle(transform.LocalRotation).RotateVec(emitter.PlacementOffset);
+
+ return new EntityCoordinates(uid, transform.LocalPosition + offset);
+ }
+
+ ///
+ /// Determines the visual state for a footprint based on entity equipment.
+ ///
+ private FootprintVisualType DetermineVisualState(EntityUid uid, bool isDragging)
+ {
+ if (isDragging)
+ return FootprintVisualType.DragMark;
+
+ var state = FootprintVisualType.BareFootprint;
+
+ if (_inventory.TryGetSlotEntity(uid, "shoes", out _))
+ state = FootprintVisualType.ShoeFootprint;
+
+ if (_inventory.TryGetSlotEntity(uid, "outerClothing", out var suit)
+ && TryComp(suit, out _))
+ state = FootprintVisualType.SuitFootprint;
+
+ return state;
+ }
+ #endregion
+}
diff --git a/Content.Server/_Sunrise/Footprints/PuddleFootPrintsSystem.cs b/Content.Server/_Sunrise/Footprints/PuddleFootPrintsSystem.cs
new file mode 100644
index 00000000000..b401e6a1924
--- /dev/null
+++ b/Content.Server/_Sunrise/Footprints/PuddleFootPrintsSystem.cs
@@ -0,0 +1,61 @@
+using System.Linq;
+using Content.Shared._Sunrise.Footprints;
+using Content.Shared.Chemistry.Components.SolutionManager;
+using Content.Shared.Chemistry.EntitySystems;
+using Content.Shared.Fluids;
+using Content.Shared.Fluids.Components;
+using Robust.Shared.Physics.Events;
+
+namespace Content.Server._Sunrise.Footprints;
+
+///
+/// Handles footprint creation when entities interact with puddles
+///
+public sealed class PuddleFootprintSystem : EntitySystem
+{
+ [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
+ [Dependency] private readonly SharedSolutionContainerSystem _solutionSystem = default!;
+
+ ///
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnPuddleInteraction);
+ }
+
+ ///
+ /// Handles puddle interaction and footprint creation when entity exits the puddle
+ ///
+ private void OnPuddleInteraction(EntityUid uid, PuddleFootprintComponent component, ref EndCollideEvent args)
+ {
+ if (!TryComp(uid, out var appearance)
+ || !TryComp(uid, out var puddle)
+ || !TryComp(args.OtherEntity, out var emitter)
+ || !TryComp(uid, out var solutionManager)
+ || !_solutionSystem.ResolveSolution((uid, solutionManager), puddle.SolutionName, ref puddle.Solution, out var solutions))
+ return;
+
+ var totalSolutionQuantity = solutions.Contents.Sum(sol => (float)sol.Quantity);
+ var waterQuantity = (from sol in solutions.Contents where sol.Reagent.Prototype == "Water" select (float)sol.Quantity).FirstOrDefault();
+
+ if (waterQuantity / (totalSolutionQuantity / 100f) > component.WaterThresholdPercent || solutions.Contents.Count <= 0)
+ return;
+
+ emitter.CurrentReagent = solutions.Contents.Aggregate((l, r) => l.Quantity > r.Quantity ? l : r).Reagent.Prototype;
+
+ if (_appearanceSystem.TryGetData(uid, PuddleVisuals.SolutionColor, out var color, appearance)
+ && _appearanceSystem.TryGetData(uid, PuddleVisuals.CurrentVolume, out var volume, appearance))
+ UpdateTrackColor((Color)color, (float)volume * component.ColorTransferRatio, emitter);
+
+ _solutionSystem.RemoveEachReagent(puddle.Solution.Value, 1);
+ }
+
+ ///
+ /// Updates the color of footprints based on puddle properties
+ ///
+ private void UpdateTrackColor(Color color, float quantity, FootprintEmitterComponent emitter)
+ {
+ emitter.TrackColor = emitter.AccumulatedColor == 0f ? color : Color.InterpolateBetween(emitter.TrackColor, color, emitter.ColorBlendFactor);
+ emitter.AccumulatedColor += quantity;
+ }
+}
diff --git a/Content.Server/_Sunrise/Medical/Surgery/SurgerySystem.Steps.cs b/Content.Server/_Sunrise/Medical/Surgery/SurgerySystem.Steps.cs
new file mode 100644
index 00000000000..1778ccfaa25
--- /dev/null
+++ b/Content.Server/_Sunrise/Medical/Surgery/SurgerySystem.Steps.cs
@@ -0,0 +1,262 @@
+using System.Linq;
+using Content.Server.Body.Systems;
+using Content.Shared._Sunrise.Medical.Surgery;
+using Content.Shared._Sunrise.Medical.Surgery.Effects.Step;
+using Content.Shared._Sunrise.Medical.Surgery.Events;
+using Content.Shared._Sunrise.Medical.Surgery.Steps.Parts;
+using Content.Shared.Body.Components;
+using Content.Shared.Body.Organ;
+using Content.Shared.Body.Part;
+using Content.Shared.Damage;
+using Content.Shared.Damage.Prototypes;
+using Content.Shared.Eye.Blinding.Components;
+using Content.Shared.Hands.Components;
+using Content.Shared.Speech.Muting;
+using Robust.Shared.Prototypes;
+using Content.Shared.Humanoid;
+using Content.Shared._Sunrise;
+using Content.Shared.Humanoid.Prototypes;
+
+namespace Content.Server._Sunrise.Medical.Surgery;
+// Based on the RMC14.
+// https://github.com/RMC-14/RMC-14
+public sealed partial class SurgerySystem : SharedSurgerySystem
+{
+ public void InitializeSteps()
+ {
+ SubscribeLocalEvent(OnStepBleedComplete);
+ SubscribeLocalEvent(OnStepClampBleedComplete);
+ SubscribeLocalEvent(OnStepEmoteEffectComplete);
+ SubscribeLocalEvent(OnStepSpawnComplete);
+
+ SubscribeLocalEvent(OnStepOrganExtractComplete);
+ SubscribeLocalEvent(OnStepOrganInsertComplete);
+
+ SubscribeLocalEvent(OnStepAttachLimbComplete);
+ SubscribeLocalEvent(OnStepAmputationComplete);
+
+ SubscribeLocalEvent(OnRemoveAccent);
+
+ }
+
+ private void OnStepBleedComplete(Entity ent, ref SurgeryStepEvent args)
+ {
+ if(ent.Comp.Damage is not null && TryComp(args.Body, out var comp))
+ _damageableSystem.SetDamage(args.Body, comp, ent.Comp.Damage);
+ //todo add wound
+ }
+
+ private void OnStepClampBleedComplete(Entity ent, ref SurgeryStepEvent args)
+ {
+ //todo remove wound
+ }
+ private void OnStepOrganInsertComplete(Entity ent, ref SurgeryStepEvent args)
+ {
+ if (args.Tools.Count == 0
+ || !(args.Tools.FirstOrDefault() is var organId)
+ || !TryComp(args.Part, out var bodyPart)
+ || !TryComp(organId, out var organComp))
+ return;
+
+ var part = args.Part;
+ var body = args.Body;
+ _delayAccumulator = 0;
+ _delayQueue.Enqueue(() =>
+ {
+ if (_body.InsertOrgan(part, organId, ent.Comp.Slot, bodyPart, organComp)
+ && TryComp(organId, out var organDamageable)
+ && TryComp(body, out var bodyDamageable))
+ {
+ if (TryComp(organId, out var organEyes)
+ && TryComp(body, out var blindable))
+ {
+ _blindable.SetMinDamage((body, blindable), organEyes.MinDamage ?? 0);
+ _blindable.AdjustEyeDamage((body, blindable), (organEyes.EyeDamage ?? 0) - blindable.MaxDamage);
+ }
+ if (TryComp(organId, out var organTongue)
+ && !organTongue.IsMuted)
+ RemComp(body);
+
+ var change = _damageableSystem.TryChangeDamage(body, organDamageable.Damage, true, false, bodyDamageable);
+ if (change is not null)
+ _damageableSystem.TryChangeDamage(organId, change.Invert(), true, false, organDamageable);
+ }
+ });
+ }
+ private void OnStepOrganExtractComplete(Entity ent, ref SurgeryStepEvent args)
+ {
+ if (ent.Comp.Organ?.Count != 1) return;
+ var organs = _body.GetPartOrgans(args.Part, Comp(args.Part));
+ var type = ent.Comp.Organ.Values.First().Component.GetType();
+ foreach (var organ in organs)
+ {
+ if (HasComp(organ.Id, type))
+ {
+ if (_body.RemoveOrgan(organ.Id, organ.Component)
+ && TryComp(organ.Id, out var damageRule)
+ && damageRule.Damage is not null
+ && TryComp(organ.Id, out var organDamageable)
+ && TryComp(args.Body, out var bodyDamageable))
+ {
+ if (TryComp(organ.Id, out var organEyes)
+ && TryComp(args.Body, out var blindable))
+ {
+ organEyes.EyeDamage = blindable.EyeDamage;
+ organEyes.MinDamage = blindable.MinDamage;
+ _blindable.UpdateIsBlind((args.Body, blindable));
+ }
+ if (TryComp(organ.Id, out var organTongue))
+ {
+ organTongue.IsMuted = HasComp(args.Body);
+ AddComp(args.Body);
+ }
+ var change = _damageableSystem.TryChangeDamage(args.Body, damageRule.Damage.Invert(), true, false, bodyDamageable);
+ if (change is not null)
+ _damageableSystem.TryChangeDamage(organ.Id, change.Invert(), true, false, organDamageable);
+ }
+ return;
+ }
+ }
+ }
+
+ private void OnRemoveAccent(Entity ent, ref SurgeryStepEvent args)
+ {
+ foreach (var accent in _accents)
+ if (HasComp(args.Body, accent))
+ RemCompDeferred(args.Body, accent);
+ }
+
+ private void OnStepEmoteEffectComplete(Entity ent, ref SurgeryStepEvent args)
+ => _chat.TryEmoteWithChat(args.Body, ent.Comp.Emote);
+ private void OnStepSpawnComplete(Entity ent, ref SurgeryStepEvent args)
+ {
+ if (TryComp(args.Body, out TransformComponent? xform))
+ SpawnAtPosition(ent.Comp.Entity, xform.Coordinates);
+ }
+ private void OnStepAttachLimbComplete(Entity ent, ref SurgeryStepEvent args)
+ {
+ if (args.Tools.Count == 0
+ || !(args.Tools.FirstOrDefault() is var limbId)
+ || !TryComp(args.Part, out var bodyPart)
+ || !TryComp(limbId, out var limb))
+ return;
+
+ var part = args.Part;
+ var body = args.Body;
+
+ _delayAccumulator = 0;
+ _delayQueue.Enqueue(() =>
+ {
+ var slot = "";
+ foreach (var slotTemp in _body.TryGetFreePartSlots(part, bodyPart))
+ {
+ slot = slotTemp;
+ if (_body.AttachPart(part, slot, limbId, bodyPart, limb))
+ break;
+ }
+
+ if (TryComp(body, out var humanoid)) //todo move to system
+ {
+ var limbs = _body.GetBodyPartAdjacentParts(limbId, limb).Except([part]).Concat([limbId]);
+ foreach (var partLimbId in limbs)
+ {
+ if (TryComp(partLimbId, out var baseLayerStorage)
+ && TryComp(partLimbId, out BodyPartComponent? partLimb))
+ {
+ var layer = partLimb.ToHumanoidLayers();
+ if (layer is null) continue;
+ _humanoidAppearanceSystem.SetBaseLayerId(body, layer.Value, baseLayerStorage.Layer, true, humanoid);
+ }
+ }
+ }
+ switch (limb.PartType)
+ {
+ case BodyPartType.Arm: //todo move to systems
+ if (limb.Children.Keys.Count == 0)
+ {
+ _body.TryCreatePartSlot(limbId, limb.Symmetry == BodyPartSymmetry.Left ? "left hand" : "right hand", BodyPartType.Hand, out var slotId);
+ }
+ foreach (var slotId in limb.Children.Keys)
+ {
+ if (slotId is null) continue;
+ var slotFullId = BodySystem.GetPartSlotContainerId(slotId);
+ var child = _containers.GetContainer(limbId, slotFullId);
+
+ foreach (var containedEnt in child.ContainedEntities)
+ {
+ if (TryComp(containedEnt, out BodyPartComponent? innerPart)
+ && innerPart.PartType == BodyPartType.Hand)
+ _hands.AddHand(body, slotFullId, limb.Symmetry == BodyPartSymmetry.Left ? HandLocation.Left : HandLocation.Right);
+ }
+ }
+ break;
+ case BodyPartType.Hand:
+ _hands.AddHand(body, BodySystem.GetPartSlotContainerId(slot), limb.Symmetry == BodyPartSymmetry.Left ? HandLocation.Left : HandLocation.Right);
+ break;
+ case BodyPartType.Leg:
+ case BodyPartType.Foot:
+ break;
+ }
+ });
+ }
+ private void OnStepAmputationComplete(Entity ent, ref SurgeryStepEvent args)
+ {
+ if (TryComp(args.Body, out TransformComponent? xform)
+ && TryComp(args.Body, out BodyComponent? body)
+ && TryComp(args.Part, out BodyPartComponent? limb))
+ {
+
+ if (!_containers.TryGetContainingContainer((args.Part, null, null), out var container)) return;
+ if (_containers.Remove(args.Part, container, destination: xform.Coordinates))
+ {
+ if (TryComp(args.Body, out var humanoid)) //todo move to system
+ {
+ var limbs = _body.GetBodyPartAdjacentParts(args.Part, limb).Concat([args.Part]); ;
+ foreach (var partLimbId in limbs)
+ {
+ if (TryComp(partLimbId, out var baseLayerStorage)
+ && TryComp(partLimbId, out BodyPartComponent? partLimb))
+ {
+ var layer = partLimb.ToHumanoidLayers();
+ if (layer is null) continue;
+ if (humanoid.CustomBaseLayers.TryGetValue(layer.Value, out var customBaseLayer))
+ baseLayerStorage.Layer = customBaseLayer.Id;
+ else
+ {
+ var speciesProto = _prototypes.Index(humanoid.Species);
+ var baseSprites = _prototypes.Index(speciesProto.SpriteSet);
+ if (baseSprites.Sprites.TryGetValue(layer.Value, out var baseLayer))
+ baseLayerStorage.Layer = baseLayer;
+ }
+ }
+ }
+ }
+ switch (limb.PartType)
+ {
+ case BodyPartType.Arm: //todo move to systems
+ foreach (var slotId in limb.Children.Keys)
+ {
+ if (slotId is null) continue;
+ var child = _containers.GetContainer(args.Part, BodySystem.GetPartSlotContainerId(slotId));
+
+ foreach (var containedEnt in child.ContainedEntities)
+ {
+ if (TryComp(containedEnt, out BodyPartComponent? innerPart)
+ && innerPart.PartType == BodyPartType.Hand)
+ _hands.RemoveHand(args.Body, BodySystem.GetPartSlotContainerId(slotId));
+ }
+ }
+ break;
+ case BodyPartType.Hand:
+ var parentSlot = _body.GetParentPartAndSlotOrNull(args.Part);
+ if (parentSlot is not null)
+ _hands.RemoveHand(args.Body, BodySystem.GetPartSlotContainerId(parentSlot.Value.Slot));
+ break;
+ case BodyPartType.Leg:
+ case BodyPartType.Foot:
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/Content.Server/_Sunrise/Medical/Surgery/SurgerySystem.cs b/Content.Server/_Sunrise/Medical/Surgery/SurgerySystem.cs
new file mode 100644
index 00000000000..b2182f87f87
--- /dev/null
+++ b/Content.Server/_Sunrise/Medical/Surgery/SurgerySystem.cs
@@ -0,0 +1,135 @@
+using Content.Server.Body.Systems;
+using Content.Server.Chat.Systems;
+using Content.Server.Hands.Systems;
+using Content.Server.Humanoid;
+using Content.Server.Popups;
+using Content.Shared._Sunrise.Medical.Surgery;
+using Content.Shared._Sunrise.Medical.Surgery.Effects.Step;
+using Content.Shared._Sunrise.Medical.Surgery.Events;
+using Content.Shared.Damage;
+using Content.Shared.Eye.Blinding.Systems;
+using Content.Shared.HealthExaminable;
+using Content.Shared.Interaction;
+using Content.Shared.Prototypes;
+using Robust.Server.Containers;
+using Robust.Server.GameObjects;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Server._Sunrise.Medical.Surgery;
+// Based on the RMC14.
+// https://github.com/RMC-14/RMC-14
+public sealed partial class SurgerySystem : SharedSurgerySystem
+{
+ [Dependency] private readonly HandsSystem _hands = default!;
+ [Dependency] private readonly HumanoidAppearanceSystem _humanoidAppearanceSystem = default!;
+ [Dependency] private readonly BodySystem _body = default!;
+ [Dependency] private readonly DamageableSystem _damageableSystem = default!;
+ [Dependency] private readonly ChatSystem _chat = default!;
+ [Dependency] private readonly IPrototypeManager _prototypes = default!;
+ [Dependency] private readonly PopupSystem _popup = default!;
+ [Dependency] private readonly UserInterfaceSystem _ui = default!;
+ [Dependency] private readonly ContainerSystem _containers = default!;
+ [Dependency] private readonly BlindableSystem _blindable = default!;
+
+ private readonly List _surgeries = [];
+ public override void Initialize()
+ {
+ base.Initialize();
+ InitializeSteps();
+
+ SubscribeLocalEvent(OnToolAfterInteract);
+ SubscribeLocalEvent(OnPrototypesReloaded);
+
+ LoadPrototypes();
+ }
+
+ public override void Update(float frameTime)
+ {
+ _delayAccumulator += frameTime;
+ if (_delayAccumulator > 0.7)
+ {
+ _delayAccumulator = 0;
+ while (_delayQueue.TryDequeue(out var action))
+ action();
+ }
+ }
+
+ protected override void RefreshUI(EntityUid body)
+ {
+ if (!HasComp(body))
+ return;
+
+ var surgeries = new Dictionary>();
+ foreach (var part in _body.GetBodyChildren(body))
+ {
+ if (!TryComp(part.Id, out var progress))
+ {
+ progress = new SurgeryProgressComponent();
+ AddComp(part.Id, progress);
+ }
+
+ foreach (var surgery in _surgeries)
+ {
+ if (GetSingleton(surgery) is not { } surgeryEnt
+ || !TryComp(surgeryEnt, out SurgeryComponent? surgeryComp)
+ || (surgeryComp.Requirement is not null && !progress.CompletedSurgeries.Contains(surgeryComp.Requirement.Value)))
+ continue;
+
+ var ev = new SurgeryValidEvent(body, part.Id);
+
+ var isCompleted = progress.CompletedSurgeries.Contains(surgery);
+ if (!progress.StartedSurgeries.Contains(surgery)
+ && !isCompleted)
+ {
+ RaiseLocalEvent(surgeryEnt, ref ev);
+
+ if (ev.Cancelled)
+ continue;
+ }
+
+ surgeries.GetOrNew(GetNetEntity(part.Id)).Add((surgery, ev.Suffix, isCompleted));
+ }
+ }
+
+ _ui.SetUiState(body, SurgeryUIKey.Key, new SurgeryBuiState() { Choices = surgeries });
+ }
+
+ private void OnToolAfterInteract(Entity ent, ref AfterInteractEvent args)
+ {
+ var user = args.User;
+ if (args.Handled ||
+ !args.CanReach ||
+ args.Target == null ||
+ _ui.IsUiOpen(user, SurgeryUIKey.Key, user) ||
+ !HasComp(args.Target)) return;
+
+ if (user == args.Target)
+ {
+ _popup.PopupEntity("You can't perform surgery on yourself!", user, user);
+ return;
+ }
+
+ args.Handled = true;
+ _ui.OpenUi(args.Target.Value, SurgeryUIKey.Key, user);
+
+ RefreshUI(args.Target.Value);
+ }
+
+ private void OnPrototypesReloaded(PrototypesReloadedEventArgs args)
+ {
+ if (args.WasModified())
+ LoadPrototypes();
+ }
+
+ private void LoadPrototypes()
+ {
+ _surgeries.Clear();
+
+ foreach (var entity in _prototypes.EnumeratePrototypes())
+ {
+ if (entity.HasComponent())
+ _surgeries.Add(new EntProtoId(entity.ID));
+ }
+ }
+}
diff --git a/Content.Shared/Body/Part/BodyPartComponent.cs b/Content.Shared/Body/Part/BodyPartComponent.cs
index c4e65c06a3f..34c68159b1e 100644
--- a/Content.Shared/Body/Part/BodyPartComponent.cs
+++ b/Content.Shared/Body/Part/BodyPartComponent.cs
@@ -88,9 +88,12 @@ private List OrganSlotsVV
[DataRecord]
public partial struct BodyPartSlot
{
- public string Id;
+ [DataField("id")]
+ public string Id = "";
+ [DataField("type")]
public BodyPartType Type;
+ public BodyPartSlot() { }
public BodyPartSlot(string id, BodyPartType type)
{
Id = id;
diff --git a/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs b/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs
index 0917197e29f..daba5ac45e4 100644
--- a/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs
+++ b/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs
@@ -790,5 +790,46 @@ public bool TryGetBodyPartAdjacentPartsComponents(
return false;
}
+ public bool TryGetFreePartSlot(EntityUid partId, [NotNullWhen(true)] out string? freeSlotId, BodyPartComponent? part = null)
+ {
+ freeSlotId = null;
+
+ if (!Resolve(partId, ref part, logMissing: false))
+ return false;
+
+ foreach (var (slotId, slot) in part.Children)
+ {
+ var containerId = GetPartSlotContainerId(slotId);
+
+ if (!Containers.TryGetContainer(partId, containerId, out var container))
+ continue;
+
+ if (container.ContainedEntities.Count == 0)
+ {
+ freeSlotId = slotId;
+ return true;
+ }
+ }
+
+ return false;
+ }
+ public IEnumerable TryGetFreePartSlots(EntityUid partId, BodyPartComponent? part = null)
+ {
+ if (!Resolve(partId, ref part, logMissing: false))
+ yield break;
+
+ foreach (var (slotId, slot) in part.Children)
+ {
+ var containerId = GetPartSlotContainerId(slotId);
+
+ if (!Containers.TryGetContainer(partId, containerId, out var container))
+ continue;
+
+ if (container.ContainedEntities.Count == 0)
+ {
+ yield return slotId;
+ }
+ }
+ }
#endregion
}
diff --git a/Content.Shared/Changeling/ChangelingComponent.cs b/Content.Shared/Changeling/ChangelingComponent.cs
index 89e48a54a3f..32103330dae 100644
--- a/Content.Shared/Changeling/ChangelingComponent.cs
+++ b/Content.Shared/Changeling/ChangelingComponent.cs
@@ -38,8 +38,8 @@ public sealed partial class ChangelingComponent : Component
/// The status icon corresponding to the Changlings.
///
- [DataField, ViewVariables(VVAccess.ReadOnly)]
- public ProtoId StatusIcon { get; set; } = "HivemindFaction";
+ [DataField("changelingStatusIcon")]
+ public ProtoId StatusIcon { get; set; } = "HivemindFaction";
#endregion
diff --git a/Content.Shared/Damage/DamageSpecifier.cs b/Content.Shared/Damage/DamageSpecifier.cs
index a1cc48a89ee..89690eaa863 100644
--- a/Content.Shared/Damage/DamageSpecifier.cs
+++ b/Content.Shared/Damage/DamageSpecifier.cs
@@ -23,7 +23,7 @@ public sealed partial class DamageSpecifier : IEquatable
[JsonPropertyName("types")]
[DataField("types", customTypeSerializer: typeof(PrototypeIdDictionarySerializer))]
[UsedImplicitly]
- private Dictionary? _damageTypeDictionary;
+ private Dictionary? _damageTypeDictionary;
[JsonPropertyName("groups")]
[DataField("groups", customTypeSerializer: typeof(PrototypeIdDictionarySerializer))]
@@ -71,6 +71,14 @@ public bool AnyPositive()
return false;
}
+ public DamageSpecifier Invert()
+ {
+ var copy = new DamageSpecifier(this);
+ foreach (var key in copy.DamageDict.Keys)
+ copy.DamageDict[key] *= -1;
+ return copy;
+ }
+
///
/// Whether this damage specifier has any entries.
///
@@ -152,7 +160,7 @@ public static DamageSpecifier ApplyModifierSet(DamageSpecifier damageSpec, Damag
if (modifierSet.Coefficients.TryGetValue(key, out var coefficient))
newValue *= coefficient; // coefficients can heal you, e.g. cauterizing bleeding
- if(newValue != 0)
+ if (newValue != 0)
newDamage.DamageDict[key] = FixedPoint2.New(newValue);
}
diff --git a/Content.Shared/DoAfter/DoAfterArgs.cs b/Content.Shared/DoAfter/DoAfterArgs.cs
index ac662785385..9acbbe7c5dc 100644
--- a/Content.Shared/DoAfter/DoAfterArgs.cs
+++ b/Content.Shared/DoAfter/DoAfterArgs.cs
@@ -46,6 +46,9 @@ public sealed partial class DoAfterArgs
[DataField]
public bool Hidden;
+ [DataField]
+ public bool ForceNet;
+
#region Event options
///
/// The event that will get raised when the DoAfter has finished. If null, this will simply raise a
diff --git a/Content.Shared/DoAfter/SharedDoAfterSystem.cs b/Content.Shared/DoAfter/SharedDoAfterSystem.cs
index 86f29fd49c8..d92f15dadb0 100644
--- a/Content.Shared/DoAfter/SharedDoAfterSystem.cs
+++ b/Content.Shared/DoAfter/SharedDoAfterSystem.cs
@@ -188,7 +188,7 @@ public bool TryStartDoAfter(DoAfterArgs args, [NotNullWhen(true)] out DoAfterId?
{
DebugTools.Assert(args.Broadcast || Exists(args.EventTarget) || args.Event.GetType() == typeof(AwaitedDoAfterEvent));
DebugTools.Assert(args.Event.GetType().HasCustomAttribute()
- || args.Event.GetType().Namespace is {} ns && ns.StartsWith("Content.IntegrationTests"), // classes defined in tests cannot be marked as serializable.
+ || args.Event.GetType().Namespace is { } ns && ns.StartsWith("Content.IntegrationTests"), // classes defined in tests cannot be marked as serializable.
$"Do after event is not serializable. Event: {args.Event.GetType()}");
if (!Resolve(args.User, ref comp))
@@ -251,7 +251,10 @@ public bool TryStartDoAfter(DoAfterArgs args, [NotNullWhen(true)] out DoAfterId?
{
RaiseDoAfterEvents(doAfter, comp);
// We don't store instant do-afters. This is just a lazy way of hiding them from client-side visuals.
- return true;
+ if (!args.ForceNet)
+ return true;
+ else
+ args.Delay = TimeSpan.FromMilliseconds(100);
}
comp.DoAfters.Add(doAfter.Index, doAfter);
@@ -295,7 +298,7 @@ private bool IsDuplicate(DoAfterArgs args, DoAfterArgs otherArgs)
return IsDuplicate(args, otherArgs, otherArgs.DuplicateCondition);
}
- private bool IsDuplicate(DoAfterArgs args, DoAfterArgs otherArgs, DuplicateConditions conditions )
+ private bool IsDuplicate(DoAfterArgs args, DoAfterArgs otherArgs, DuplicateConditions conditions)
{
if ((conditions & DuplicateConditions.SameTarget) != 0
&& args.Target != otherArgs.Target)
diff --git a/Content.Shared/Eye/Blinding/Systems/BlindableSystem.cs b/Content.Shared/Eye/Blinding/Systems/BlindableSystem.cs
index 24eed3adcf5..c985a113152 100644
--- a/Content.Shared/Eye/Blinding/Systems/BlindableSystem.cs
+++ b/Content.Shared/Eye/Blinding/Systems/BlindableSystem.cs
@@ -1,3 +1,8 @@
+using System.Linq;
+using Content.Shared._Sunrise.Medical.Surgery.Steps.Parts;
+using Content.Shared.Body.Components;
+using Content.Shared.Body.Part;
+using Content.Shared.Body.Systems;
using Content.Shared.Eye.Blinding.Components;
using Content.Shared.Inventory;
using Content.Shared.Rejuvenate;
@@ -9,6 +14,7 @@ public sealed class BlindableSystem : EntitySystem
{
[Dependency] private readonly BlurryVisionSystem _blurriness = default!;
[Dependency] private readonly EyeClosingSystem _eyelids = default!;
+ [Dependency] private readonly SharedBodySystem _bodySystem = default!;
public override void Initialize()
{
@@ -36,8 +42,15 @@ public void UpdateIsBlind(Entity blindable)
var old = blindable.Comp.IsBlind;
+ var forceBlind = false;
+ if(TryComp(blindable.Owner, out var body))
+ {
+ var eyes = _bodySystem.GetBodyOrganEntityComps((blindable.Owner, body));
+ forceBlind = eyes.Count == 0;
+ }
+
// Don't bother raising an event if the eye is too damaged.
- if (blindable.Comp.EyeDamage >= blindable.Comp.MaxDamage)
+ if (blindable.Comp.EyeDamage >= blindable.Comp.MaxDamage || forceBlind)
{
blindable.Comp.IsBlind = true;
}
diff --git a/Content.Shared/Research/Prototypes/TechDisciplinePrototype.cs b/Content.Shared/Research/Prototypes/TechDisciplinePrototype.cs
index b48d8256b73..ea0a2f282ef 100644
--- a/Content.Shared/Research/Prototypes/TechDisciplinePrototype.cs
+++ b/Content.Shared/Research/Prototypes/TechDisciplinePrototype.cs
@@ -44,5 +44,5 @@ public sealed partial class TechDisciplinePrototype : IPrototype
/// Purchasing this tier of technology causes a server to become "locked" to this discipline.
///
[DataField("lockoutTier")]
- public int LockoutTier = 3;
+ public int LockoutTier = 4;
}
diff --git a/Content.Shared/Standing/LayingDownComponent.cs b/Content.Shared/Standing/LayingDownComponent.cs
index fddb0de0143..5b3a063e324 100644
--- a/Content.Shared/Standing/LayingDownComponent.cs
+++ b/Content.Shared/Standing/LayingDownComponent.cs
@@ -10,7 +10,7 @@ public sealed partial class LayingDownComponent : Component
public float StandingUpTime { get; set; } = 1f;
[DataField, AutoNetworkedField]
- public float SpeedModify { get; set; } = 0.4f;
+ public float SpeedModify { get; set; } = 0.25f;
[DataField, AutoNetworkedField]
public bool AutoGetUp;
diff --git a/Content.Shared/Vampire/SharedVampireSystem.cs b/Content.Shared/Vampire/SharedVampireSystem.cs
new file mode 100644
index 00000000000..db8592687b8
--- /dev/null
+++ b/Content.Shared/Vampire/SharedVampireSystem.cs
@@ -0,0 +1,24 @@
+using Content.Shared.Vampire.Components;
+using Content.Shared.FixedPoint;
+
+namespace Content.Shared.Vampire;
+
+public sealed class SharedVampireSystem : EntitySystem
+{
+ public FixedPoint2 GetBloodEssence(EntityUid vampire)
+ {
+ if (!TryComp(vampire, out var comp))
+ return 0;
+
+ if (comp.Balance != null && comp.Balance.TryGetValue(VampireComponent.CurrencyProto, out var val))
+ return val;
+
+ return 0;
+ }
+
+ public void SetAlertBloodAmount(VampireAlertComponent component, int amount)
+ {
+ component.BloodAmount = amount;
+ Dirty(component.Owner, component);
+ }
+}
\ No newline at end of file
diff --git a/Content.Shared/Vampire/VampireAlertComponent.cs b/Content.Shared/Vampire/VampireAlertComponent.cs
new file mode 100644
index 00000000000..52499168803
--- /dev/null
+++ b/Content.Shared/Vampire/VampireAlertComponent.cs
@@ -0,0 +1,15 @@
+using Content.Shared.Alert;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Vampire.Components;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class VampireAlertComponent : Component
+{
+ [DataField("vampireBloodAlert")]
+ public ProtoId BloodAlert { get; set; } = "VampireBlood";
+
+ [DataField, AutoNetworkedField]
+ public int BloodAmount = 0;
+}
\ No newline at end of file
diff --git a/Content.Shared/Vampire/VampireComponent.cs b/Content.Shared/Vampire/VampireComponent.cs
index e8291ed9ecf..e9988691854 100644
--- a/Content.Shared/Vampire/VampireComponent.cs
+++ b/Content.Shared/Vampire/VampireComponent.cs
@@ -75,9 +75,6 @@ public sealed partial class VampireComponent : Component
[ValidatePrototypeId]
public static readonly string DrinkBloodPrototype = "DrinkBlood";
-
- [DataField, ViewVariables(VVAccess.ReadOnly)]
- public ProtoId StatusIcon { get; set; } = "VampireFaction";
///
/// Total blood drank, counter for end of round screen
@@ -251,6 +248,14 @@ public enum VampireMutationUiKey : byte
Key
}
+[NetSerializable, Serializable]
+public enum VampireVisualLayers : byte
+{
+ Digit1,
+ Digit2,
+ Digit3
+}
+
/*[Serializable, NetSerializable]
public enum VampirePowerKey : byte
{
diff --git a/Content.Shared/Vampire/VampireIconComponent.cs b/Content.Shared/Vampire/VampireIconComponent.cs
new file mode 100644
index 00000000000..4cd87dfab77
--- /dev/null
+++ b/Content.Shared/Vampire/VampireIconComponent.cs
@@ -0,0 +1,12 @@
+using Content.Shared.StatusIcon;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Vampire.Components;
+
+[RegisterComponent, NetworkedComponent]
+public sealed partial class VampireIconComponent : Component
+{
+ [DataField("vampireStatusIcon")]
+ public ProtoId StatusIcon { get; set; } = "VampireFaction";
+}
\ No newline at end of file
diff --git a/Content.Shared/_Sunrise/BaseLayerIdComponent.cs b/Content.Shared/_Sunrise/BaseLayerIdComponent.cs
new file mode 100644
index 00000000000..f0a9164b1e1
--- /dev/null
+++ b/Content.Shared/_Sunrise/BaseLayerIdComponent.cs
@@ -0,0 +1,14 @@
+using Content.Shared.Humanoid;
+using Content.Shared.Humanoid.Prototypes;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+// Based on the RMC14.
+// https://github.com/RMC-14/RMC-14
+namespace Content.Shared._Sunrise;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class BaseLayerIdComponent : Component
+{
+ [DataField, AutoNetworkedField]
+ public string? Layer;
+}
diff --git a/Content.Shared/_Sunrise/Footprints/FootprintsSerializables.cs b/Content.Shared/_Sunrise/Footprints/FootprintsSerializables.cs
new file mode 100644
index 00000000000..21971087e5b
--- /dev/null
+++ b/Content.Shared/_Sunrise/Footprints/FootprintsSerializables.cs
@@ -0,0 +1,196 @@
+using System.Numerics;
+using Content.Shared.Chemistry.Components;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+using Robust.Shared.Utility;
+
+namespace Content.Shared._Sunrise.Footprints;
+
+///
+/// Component that represents a single footprint entity in the world
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class FootprintComponent : Component
+{
+ ///
+ /// Entity that created this footprint
+ ///
+ [AutoNetworkedField]
+ public EntityUid CreatorEntity;
+
+ ///
+ /// Name of the solution container for this footprint
+ ///
+ [DataField]
+ public string ContainerName = "step";
+
+ ///
+ /// Reference to the solution component containing reagents
+ ///
+ [DataField]
+ public Entity? SolutionContainer;
+}
+
+///
+/// Component that handles footprint creation when entities step in puddles
+///
+[RegisterComponent]
+public sealed partial class PuddleFootprintComponent : Component
+{
+ ///
+ /// Ratio determining how much of puddle's color transfers to footprints
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float ColorTransferRatio = 0.2f;
+
+ ///
+ /// Percentage of water content above which footprints won't be created
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float WaterThresholdPercent = 80f;
+}
+
+///
+/// Component that manages footprint creation for entities that can leave tracks
+///
+[RegisterComponent]
+public sealed partial class FootprintEmitterComponent : Component
+{
+ ///
+ /// Path to the RSI file containing footprint sprites
+ ///
+ [ViewVariables(VVAccess.ReadOnly), DataField]
+ public ResPath SpritePath = new("/Textures/_Sunrise/Effects/footprints.rsi");
+
+ ///
+ /// State ID for left bare footprint
+ ///
+ [ViewVariables(VVAccess.ReadOnly), DataField]
+ public string LeftBareFootState = "footprint-left-bare-human";
+
+ ///
+ /// State ID for right bare footprint
+ ///
+ [ViewVariables(VVAccess.ReadOnly), DataField]
+ public string RightBareFootState = "footprint-right-bare-human";
+
+ ///
+ /// State ID for shoe footprint
+ ///
+ [ViewVariables(VVAccess.ReadOnly), DataField]
+ public string ShoeFootState = "footprint-shoes";
+
+ ///
+ /// State ID for pressure suit footprint
+ ///
+ [ViewVariables(VVAccess.ReadOnly), DataField]
+ public string PressureSuitFootState = "footprint-suit";
+
+ ///
+ /// Array of state IDs for dragging animations
+ ///
+ [ViewVariables(VVAccess.ReadOnly), DataField]
+ public string[] DraggingStates =
+ [
+ "dragging-1",
+ "dragging-2",
+ "dragging-3",
+ "dragging-4",
+ "dragging-5",
+ ];
+
+ ///
+ /// Prototype ID for footprint entity
+ ///
+ [ViewVariables(VVAccess.ReadOnly), DataField]
+ public EntProtoId FootprintPrototype = "Footstep";
+
+ ///
+ /// Current color of footprints
+ ///
+ [ViewVariables(VVAccess.ReadOnly), DataField]
+ public Color TrackColor = Color.FromHex("#00000000");
+
+ ///
+ /// Distance between footprints when walking
+ ///
+ [DataField]
+ public float WalkStepInterval = 0.7f;
+
+ ///
+ /// Distance between marks when being dragged
+ ///
+ [DataField]
+ public float DragMarkInterval = 0.5f;
+
+ ///
+ /// Amount of color accumulated from puddles
+ ///
+ [DataField]
+ public float AccumulatedColor;
+
+ ///
+ /// Rate at which footprint color fades
+ ///
+ [DataField]
+ public float ColorFadeRate = 0.05f;
+
+ ///
+ /// Current reagent being transferred to footprints
+ ///
+ [DataField]
+ public string? CurrentReagent;
+
+ ///
+ /// Offset from entity center for footprint placement
+ ///
+ [DataField]
+ public Vector2 PlacementOffset = new(0.1f, 0f);
+
+ ///
+ /// Tracks which foot is currently stepping
+ ///
+ public bool IsRightStep = true;
+
+ ///
+ /// Position of last footprint
+ ///
+ public Vector2 LastStepPosition = Vector2.Zero;
+
+ ///
+ /// Factor for interpolating between colors when mixing
+ ///
+ public float ColorBlendFactor = 0.2f;
+}
+
+///
+/// Visual states for footprint appearances
+///
+[Serializable, NetSerializable]
+public enum FootprintVisualType : byte
+{
+ BareFootprint,
+ ShoeFootprint,
+ SuitFootprint,
+ DragMark
+}
+
+///
+/// Visual state parameters for footprints
+///
+[Serializable, NetSerializable]
+public enum FootprintVisualParameter : byte
+{
+ VisualState,
+ TrackColor
+}
+
+///
+/// Sprite layers for footprint visuals
+///
+[Serializable, NetSerializable]
+public enum FootprintSpriteLayer : byte
+{
+ MainLayer
+}
diff --git a/Content.Shared/_Sunrise/Medical/Surgery/Components/SurgeryComponent.cs b/Content.Shared/_Sunrise/Medical/Surgery/Components/SurgeryComponent.cs
new file mode 100644
index 00000000000..4dbd62844f3
--- /dev/null
+++ b/Content.Shared/_Sunrise/Medical/Surgery/Components/SurgeryComponent.cs
@@ -0,0 +1,20 @@
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+// Based on the RMC14.
+// https://github.com/RMC-14/RMC-14
+namespace Content.Shared._Sunrise.Medical.Surgery;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(SharedSurgerySystem))]
+[EntityCategory("Surgeries")]
+public sealed partial class SurgeryComponent : Component
+{
+ [DataField, AutoNetworkedField, Access(typeof(SharedSurgerySystem), Other = AccessPermissions.ReadWriteExecute)]
+ public int Priority;
+
+ [DataField, AutoNetworkedField]
+ public EntProtoId? Requirement;
+
+ [DataField(required: true), AutoNetworkedField]
+ public List Steps = new();
+}
diff --git a/Content.Shared/_Sunrise/Medical/Surgery/Components/SurgeryProgressComponent.cs b/Content.Shared/_Sunrise/Medical/Surgery/Components/SurgeryProgressComponent.cs
new file mode 100644
index 00000000000..6143b889733
--- /dev/null
+++ b/Content.Shared/_Sunrise/Medical/Surgery/Components/SurgeryProgressComponent.cs
@@ -0,0 +1,18 @@
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+// Based on the RMC14.
+// https://github.com/RMC-14/RMC-14
+namespace Content.Shared._Sunrise.Medical.Surgery.Effects.Step;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedSurgerySystem))]
+public sealed partial class SurgeryProgressComponent : Component
+{
+ [DataField, AutoNetworkedField]
+ public HashSet CompletedSteps = [];
+
+ [DataField, AutoNetworkedField]
+ public HashSet CompletedSurgeries = [];
+
+ [DataField, AutoNetworkedField]
+ public HashSet StartedSurgeries = [];
+}
\ No newline at end of file
diff --git a/Content.Shared/_Sunrise/Medical/Surgery/Components/SurgeryStepComponent.cs b/Content.Shared/_Sunrise/Medical/Surgery/Components/SurgeryStepComponent.cs
new file mode 100644
index 00000000000..d2f5ae40946
--- /dev/null
+++ b/Content.Shared/_Sunrise/Medical/Surgery/Components/SurgeryStepComponent.cs
@@ -0,0 +1,29 @@
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+// Based on the RMC14.
+// https://github.com/RMC-14/RMC-14
+namespace Content.Shared._Sunrise.Medical.Surgery.Steps;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(SharedSurgerySystem))]
+[EntityCategory("SurgerySteps")]
+public sealed partial class SurgeryStepComponent : Component
+{
+ [DataField, AutoNetworkedField]
+ public float Duration = 2;
+
+ [DataField]
+ public ComponentRegistry? Tools;
+
+ [DataField]
+ public ComponentRegistry? Add;
+
+ [DataField]
+ public ComponentRegistry? BodyAdd;
+
+ [DataField]
+ public ComponentRegistry? Remove;
+
+ [DataField]
+ public ComponentRegistry? BodyRemove;
+}
diff --git a/Content.Shared/_Sunrise/Medical/Surgery/Components/SurgeryTargetComponent.cs b/Content.Shared/_Sunrise/Medical/Surgery/Components/SurgeryTargetComponent.cs
new file mode 100644
index 00000000000..a4d5714ccfa
--- /dev/null
+++ b/Content.Shared/_Sunrise/Medical/Surgery/Components/SurgeryTargetComponent.cs
@@ -0,0 +1,8 @@
+using Robust.Shared.GameStates;
+// Based on the RMC14.
+// https://github.com/RMC-14/RMC-14
+namespace Content.Shared._Sunrise.Medical.Surgery;
+
+[RegisterComponent, NetworkedComponent]
+[Access(typeof(SharedSurgerySystem))]
+public sealed partial class SurgeryTargetComponent : Component;
diff --git a/Content.Shared/_Sunrise/Medical/Surgery/Components/_Conditions.cs b/Content.Shared/_Sunrise/Medical/Surgery/Components/_Conditions.cs
new file mode 100644
index 00000000000..8862b602254
--- /dev/null
+++ b/Content.Shared/_Sunrise/Medical/Surgery/Components/_Conditions.cs
@@ -0,0 +1,29 @@
+using Content.Shared.Body.Part;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+// Based on the RMC14.
+// https://github.com/RMC-14/RMC-14
+namespace Content.Shared._Sunrise.Medical.Surgery.Effects.Step;
+
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))] public sealed partial class SurgeryAnyAccentConditionComponent : Component;
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))] public sealed partial class SurgeryAnyLimbSlotConditionComponent : Component;
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))] public sealed partial class SurgeryOperatingTableConditionComponent : Component;
+
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))]
+public sealed partial class SurgeryPartConditionComponent : Component
+{
+ [DataField]
+ public List Parts = [];
+}
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))]
+public sealed partial class SurgeryOrganExistConditionComponent : Component
+{
+ [DataField]
+ public ComponentRegistry? Organ;
+}
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))]
+public sealed partial class SurgeryOrganDontExistConditionComponent : Component
+{
+ [DataField]
+ public ComponentRegistry? Organ;
+}
\ No newline at end of file
diff --git a/Content.Shared/_Sunrise/Medical/Surgery/Components/_Organs.cs b/Content.Shared/_Sunrise/Medical/Surgery/Components/_Organs.cs
new file mode 100644
index 00000000000..f4520232924
--- /dev/null
+++ b/Content.Shared/_Sunrise/Medical/Surgery/Components/_Organs.cs
@@ -0,0 +1,35 @@
+using Content.Shared.Damage;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+namespace Content.Shared._Sunrise.Medical.Surgery.Steps.Parts;
+
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))] public sealed partial class OrganBrainComponent : Component;
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))] public sealed partial class OrganAppendixComponent : Component;
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))] public sealed partial class OrganEarsComponent : Component;
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))] public sealed partial class OrganLungsComponent : Component;
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))] public sealed partial class OrganHeartComponent : Component;
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))] public sealed partial class OrganStomachComponent : Component;
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))] public sealed partial class OrganLiverComponent : Component;
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))] public sealed partial class OrganKidneysComponent : Component;
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))]
+public sealed partial class OrganTongueComponent : Component
+{
+ [DataField]
+ public bool IsMuted;
+}
+
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))]
+public sealed partial class OrganEyesComponent : Component
+{
+ [DataField]
+ public int? EyeDamage;
+ [DataField]
+ public int? MinDamage;
+}
+
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))]
+public sealed partial class OrganDamageComponent : Component
+{
+ [DataField]
+ public DamageSpecifier? Damage;
+}
\ No newline at end of file
diff --git a/Content.Shared/_Sunrise/Medical/Surgery/Components/_Parts.cs b/Content.Shared/_Sunrise/Medical/Surgery/Components/_Parts.cs
new file mode 100644
index 00000000000..470bd0769b3
--- /dev/null
+++ b/Content.Shared/_Sunrise/Medical/Surgery/Components/_Parts.cs
@@ -0,0 +1,22 @@
+using Content.Shared.Damage;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+// Based on the RMC14.
+// https://github.com/RMC-14/RMC-14
+namespace Content.Shared._Sunrise.Medical.Surgery.Steps.Parts;
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))] public sealed partial class IncisionOpenComponent : Component;
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))] public sealed partial class SkinRetractedComponent : Component;
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))] public sealed partial class BleedersClampedComponent : Component;
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))]
+public sealed partial class SurgeryStepOrganExtractComponent : Component
+{
+ [DataField]
+ public ComponentRegistry? Organ;
+}
+
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))]
+public sealed partial class SurgeryStepOrganInsertComponent : Component
+{
+ [DataField(required: true)]
+ public string Slot;
+}
diff --git a/Content.Shared/_Sunrise/Medical/Surgery/Components/_Steps.cs b/Content.Shared/_Sunrise/Medical/Surgery/Components/_Steps.cs
new file mode 100644
index 00000000000..5bc50e3ee69
--- /dev/null
+++ b/Content.Shared/_Sunrise/Medical/Surgery/Components/_Steps.cs
@@ -0,0 +1,32 @@
+using Content.Shared.Chat.Prototypes;
+using Content.Shared.Damage;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+// Based on the RMC14.
+// https://github.com/RMC-14/RMC-14
+namespace Content.Shared._Sunrise.Medical.Surgery.Effects.Step;
+
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))] public sealed partial class SurgeryClampBleedEffectComponent : Component;
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))] public sealed partial class SurgeryStepAttachLimbEffectComponent : Component;
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))] public sealed partial class SurgeryStepBleedEffectComponent : Component
+{
+ [DataField]
+ public DamageSpecifier? Damage;
+};
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))] public sealed partial class SurgeryStepAmputationEffectComponent : Component;
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))] public sealed partial class SurgeryRemoveAccentComponent : Component;
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))] public sealed partial class SurgeryClearProgressComponent : Component;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedSurgerySystem))]
+public sealed partial class SurgeryStepEmoteEffectComponent : Component
+{
+ [DataField, AutoNetworkedField]
+ public ProtoId Emote = "Scream";
+}
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedSurgerySystem))]
+public sealed partial class SurgeryStepSpawnEffectComponent : Component
+{
+ [DataField(required: true), AutoNetworkedField]
+ public EntProtoId Entity;
+}
diff --git a/Content.Shared/_Sunrise/Medical/Surgery/Components/_Tools.cs b/Content.Shared/_Sunrise/Medical/Surgery/Components/_Tools.cs
new file mode 100644
index 00000000000..2b93c2a6f42
--- /dev/null
+++ b/Content.Shared/_Sunrise/Medical/Surgery/Components/_Tools.cs
@@ -0,0 +1,71 @@
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+// Based on the RMC14.
+// https://github.com/RMC-14/RMC-14
+namespace Content.Shared._Sunrise.Medical.Surgery.Effects.Step;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(SharedSurgerySystem))]
+public sealed partial class SurgeryToolComponent : Component
+{
+ [DataField, AutoNetworkedField]
+ public float Speed = 1;
+
+ [DataField, AutoNetworkedField]
+ public SoundSpecifier? StartSound;
+
+ [DataField, AutoNetworkedField]
+ public SoundSpecifier? EndSound;
+}
+
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))]
+public sealed partial class OperatingTableComponent : Component;
+
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))]
+public sealed partial class BoneGelComponent : Component, ISurgeryToolComponent
+{
+ public string ToolName => "bone gel";
+}
+
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))]
+public sealed partial class BoneSawComponent : Component, ISurgeryToolComponent
+{
+ public string ToolName => "a bone saw";
+}
+
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))]
+public sealed partial class BoneSetterComponent : Component, ISurgeryToolComponent
+{
+ public string ToolName => "a bone setter";
+}
+
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))]
+public sealed partial class CauteryComponent : Component, ISurgeryToolComponent
+{
+ public string ToolName => "a cautery";
+}
+
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))]
+public sealed partial class HemostatComponent : Component, ISurgeryToolComponent
+{
+ public string ToolName => "a hemostat";
+}
+
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))]
+public sealed partial class RetractorComponent : Component, ISurgeryToolComponent
+{
+ public string ToolName => "a retractor";
+}
+
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))]
+public sealed partial class ScalpelComponent : Component, ISurgeryToolComponent
+{
+ public string ToolName => "a scalpel";
+}
+
+[RegisterComponent, NetworkedComponent, Access(typeof(SharedSurgerySystem))]
+public sealed partial class SurgicalDrillComponent : Component, ISurgeryToolComponent
+{
+ public string ToolName => "a surgical drill";
+}
diff --git a/Content.Shared/_Sunrise/Medical/Surgery/Events/SurgeryCanPerformStepEvent.cs b/Content.Shared/_Sunrise/Medical/Surgery/Events/SurgeryCanPerformStepEvent.cs
new file mode 100644
index 00000000000..b87bdc20d99
--- /dev/null
+++ b/Content.Shared/_Sunrise/Medical/Surgery/Events/SurgeryCanPerformStepEvent.cs
@@ -0,0 +1,17 @@
+using Content.Shared.Inventory;
+// Based on the RMC14.
+// https://github.com/RMC-14/RMC-14
+namespace Content.Shared._Sunrise.Medical.Surgery.Events;
+
+[ByRefEvent]
+public record struct SurgeryCanPerformStepEvent(
+ EntityUid User,
+ EntityUid Body,
+ List Tools,
+ SlotFlags TargetSlots,
+ string? Popup = null,
+ StepInvalidReason Invalid = StepInvalidReason.None
+) : IInventoryRelayEvent
+{
+ public HashSet ValidTools = [];
+}
diff --git a/Content.Shared/_Sunrise/Medical/Surgery/Events/SurgeryDoAfterEvent.cs b/Content.Shared/_Sunrise/Medical/Surgery/Events/SurgeryDoAfterEvent.cs
new file mode 100644
index 00000000000..5d88fcefaa0
--- /dev/null
+++ b/Content.Shared/_Sunrise/Medical/Surgery/Events/SurgeryDoAfterEvent.cs
@@ -0,0 +1,19 @@
+using Content.Shared.DoAfter;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+// Based on the RMC14.
+// https://github.com/RMC-14/RMC-14
+namespace Content.Shared._Sunrise.Medical.Surgery;
+
+[Serializable, NetSerializable]
+public sealed partial class SurgeryDoAfterEvent : SimpleDoAfterEvent
+{
+ public readonly EntProtoId Surgery;
+ public readonly EntProtoId Step;
+
+ public SurgeryDoAfterEvent(EntProtoId surgery, EntProtoId step)
+ {
+ Surgery = surgery;
+ Step = step;
+ }
+}
diff --git a/Content.Shared/_Sunrise/Medical/Surgery/Events/SurgeryStepEvent.cs b/Content.Shared/_Sunrise/Medical/Surgery/Events/SurgeryStepEvent.cs
new file mode 100644
index 00000000000..d681970514e
--- /dev/null
+++ b/Content.Shared/_Sunrise/Medical/Surgery/Events/SurgeryStepEvent.cs
@@ -0,0 +1,15 @@
+using Robust.Shared.Prototypes;
+// Based on the RMC14.
+// https://github.com/RMC-14/RMC-14
+namespace Content.Shared._Sunrise.Medical.Surgery.Events;
+
+///
+/// Raised on the step entity.
+///
+[ByRefEvent]
+public record struct SurgeryStepEvent(EntityUid User, EntityUid Body, EntityUid Part, List Tools)
+{
+ public required EntProtoId StepProto { get; init; }
+ public required EntProtoId SurgeryProto { get; init; }
+ public required bool IsFinal { get; init; }
+}
diff --git a/Content.Shared/_Sunrise/Medical/Surgery/Events/SurgeryValidEvent.cs b/Content.Shared/_Sunrise/Medical/Surgery/Events/SurgeryValidEvent.cs
new file mode 100644
index 00000000000..2fb56ed382c
--- /dev/null
+++ b/Content.Shared/_Sunrise/Medical/Surgery/Events/SurgeryValidEvent.cs
@@ -0,0 +1,10 @@
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared._Sunrise.Medical.Surgery.Events;
+// Based on the RMC14.
+// https://github.com/RMC-14/RMC-14
+///
+/// Raised on the entity that is receiving surgery.
+///
+[ByRefEvent]
+public record struct SurgeryValidEvent(EntityUid Body, EntityUid Part, bool Cancelled = false, string Suffix = "");
diff --git a/Content.Shared/_Sunrise/Medical/Surgery/ISurgeryToolComponent.cs b/Content.Shared/_Sunrise/Medical/Surgery/ISurgeryToolComponent.cs
new file mode 100644
index 00000000000..9bcd2bee96d
--- /dev/null
+++ b/Content.Shared/_Sunrise/Medical/Surgery/ISurgeryToolComponent.cs
@@ -0,0 +1,7 @@
+namespace Content.Shared._Sunrise.Medical.Surgery;
+// Based on the RMC14.
+// https://github.com/RMC-14/RMC-14
+public interface ISurgeryToolComponent
+{
+ public string ToolName { get; }
+}
diff --git a/Content.Shared/_Sunrise/Medical/Surgery/SharedSurgerySystem.BaseSteps.cs b/Content.Shared/_Sunrise/Medical/Surgery/SharedSurgerySystem.BaseSteps.cs
new file mode 100644
index 00000000000..671f618446c
--- /dev/null
+++ b/Content.Shared/_Sunrise/Medical/Surgery/SharedSurgerySystem.BaseSteps.cs
@@ -0,0 +1,290 @@
+using Content.Shared._Sunrise.Medical.Surgery.Steps;
+using Content.Shared.Body.Part;
+using Content.Shared.Buckle.Components;
+using Content.Shared.DoAfter;
+using Content.Shared.Inventory;
+using Content.Shared.Popups;
+using Robust.Shared.Prototypes;
+using Content.Shared._Sunrise.Medical.Surgery.Events;
+using Content.Shared._Sunrise.Medical.Surgery.Effects.Step;
+using System.Linq;
+
+namespace Content.Shared._Sunrise.Medical.Surgery;
+// Based on the RMC14.
+// https://github.com/RMC-14/RMC-14
+public abstract partial class SharedSurgerySystem
+{
+
+ protected float _delayAccumulator = 0f;
+ protected readonly Queue _delayQueue = new();
+ private void InitializeSteps()
+ {
+ SubscribeLocalEvent(OnStep);
+ SubscribeLocalEvent(OnClearProgressStep);
+ SubscribeLocalEvent(OnTargetDoAfter);
+
+ SubscribeLocalEvent(OnCanPerformStep);
+
+ Subs.BuiEvents(SurgeryUIKey.Key, subs => subs.Event(OnSurgeryTargetStepChosen));
+ }
+ private void OnTargetDoAfter(Entity ent, ref SurgeryDoAfterEvent args)
+ {
+ if (args.Cancelled ||
+ args.Handled ||
+ args.Target is not { } target ||
+ !IsSurgeryValid(ent, target, args.Surgery, args.Step, out var surgery, out var part, out var step) ||
+ !PreviousStepsComplete(ent, part, surgery, args.Step) ||
+ !CanPerformStep(args.User, ent, part.Comp.PartType, step, false))
+ {
+ Log.Warning($"{ToPrettyString(args.User)} tried to start invalid surgery.");
+ Dirty(ent);
+ if (args.Target.HasValue && TryComp(args.Target.Value, out var dirtyPart))
+ Dirty(args.Target.Value, dirtyPart, Comp(args.Target.Value));
+ return;
+ }
+
+ var ev = new SurgeryStepEvent(args.User, ent, part, GetTools(args.User))
+ {
+ StepProto = args.Step,
+ SurgeryProto = args.Surgery,
+ IsFinal = surgery.Comp.Steps[^1] == args.Step,
+ };
+ RaiseLocalEvent(step, ref ev);
+
+ if (_net.IsClient) return;
+ _delayAccumulator = 0f;
+ _delayQueue.Enqueue(() => RefreshUI(ent));
+ }
+
+ private void OnClearProgressStep(Entity ent, ref SurgeryStepEvent args)
+ {
+ var progress = Comp(args.Part);
+ progress.CompletedSteps.Clear();
+ progress.CompletedSurgeries.Clear();
+ }
+
+ private void OnStep(Entity ent, ref SurgeryStepEvent args)
+ {
+ if (!TryComp(ent, out _))
+ {
+ if (TryComp(args.Part, out var progress))
+ {
+ progress.CompletedSteps.Add($"{args.SurgeryProto}:{args.StepProto}");
+ if(!progress.StartedSurgeries.Contains(args.SurgeryProto) && !args.IsFinal)
+ progress.StartedSurgeries.Add(args.SurgeryProto);
+ if (progress.StartedSurgeries.Contains(args.SurgeryProto) && args.IsFinal)
+ progress.StartedSurgeries.Remove(args.SurgeryProto);
+ }
+ else
+ {
+ progress = new SurgeryProgressComponent { CompletedSteps = [$"{args.SurgeryProto}:{args.StepProto}"] };
+ AddComp(args.Part, progress);
+ }
+ if (args.IsFinal)
+ progress.CompletedSurgeries.Add(args.SurgeryProto);
+ }
+
+ foreach (var reg in (ent.Comp.Tools ?? []).Values)
+ {
+ var tool = args.Tools.FirstOrDefault(x => HasComp(x, reg.Component.GetType()));
+ if (tool == default) return;
+
+ if (_net.IsServer && TryComp(tool, out SurgeryToolComponent? toolComp) && toolComp.EndSound != null)
+ _audio.PlayPvs(toolComp.EndSound, tool);
+ }
+
+ foreach (var reg in (ent.Comp.Add ?? []).Values)
+ {
+ var compType = reg.Component.GetType();
+ if (HasComp(args.Part, compType))
+ continue;
+ var newComp = _compFactory.GetComponent(compType);
+ _serialization.CopyTo(reg.Component, ref newComp, notNullableOverride: true);
+ AddComp(args.Part, newComp);
+ }
+
+ foreach (var reg in (ent.Comp.BodyAdd ?? []).Values)
+ {
+ var compType = reg.Component.GetType();
+ if (HasComp(args.Body, compType))
+ continue;
+
+ AddComp(args.Part, _compFactory.GetComponent(compType));
+ }
+
+ foreach (var reg in (ent.Comp.Remove ?? []).Values)
+ RemComp(args.Part, reg.Component.GetType());
+
+ foreach (var reg in (ent.Comp.BodyRemove ?? []).Values)
+ RemComp(args.Body, reg.Component.GetType());
+ }
+
+ private void OnCanPerformStep(Entity ent, ref SurgeryCanPerformStepEvent args)
+ {
+ if (HasComp(ent)
+ && (!TryComp(args.Body, out BuckleComponent? buckle) || !HasComp(buckle.BuckledTo)))
+ {
+ args.Invalid = StepInvalidReason.NeedsOperatingTable;
+ return;
+ }
+
+ RaiseLocalEvent(args.Body, ref args);
+
+ if (args.Invalid != StepInvalidReason.None || ent.Comp.Tools == null)
+ return;
+
+ foreach (var reg in ent.Comp.Tools.Values)
+ {
+ var tool = args.Tools.FirstOrDefault(x => HasComp(x, reg.Component.GetType()));
+ if (tool == default)
+ {
+ args.Invalid = StepInvalidReason.MissingTool;
+
+ if (reg.Component is ISurgeryToolComponent toolComp)
+ args.Popup = $"You need {toolComp.ToolName} to perform this step!";
+
+ return;
+ }
+
+ args.ValidTools.Add(tool);
+ }
+ }
+
+ private void OnSurgeryTargetStepChosen(Entity ent, ref SurgeryStepChosenBuiMsg args)
+ {
+ var user = args.Actor;
+ if (GetEntity(args.Entity) is not { Valid: true } body
+ || GetEntity(args.Part) is not { Valid: true } targetPart
+ || !IsSurgeryValid(body, targetPart, args.Surgery, args.Step, out var surgery, out var part, out var step)
+ || GetSingleton(args.Step) is not { } stepEnt
+ || !TryComp(stepEnt, out SurgeryStepComponent? stepComp)
+ || !CanPerformStep(user, body, part.Comp.PartType, step, true, out _, out _, out var validTools))
+ {
+ return;
+ }
+ if(!PreviousStepsComplete(body, part, surgery, args.Step) || IsStepComplete(part, args.Surgery, args.Step))
+ {
+ var progress = Comp(part);
+ Dirty(part, progress);
+ _delayAccumulator = 0f;
+ _delayQueue.Enqueue(() => RefreshUI(body));
+ return;
+ }
+
+ var duration = stepComp.Duration;
+
+ foreach (var tool in validTools)
+ if (TryComp(tool, out SurgeryToolComponent? toolComp))
+ {
+ duration *= toolComp.Speed;
+ if (toolComp.StartSound != null) _audio.PlayPvs(toolComp.StartSound, tool);
+ }
+
+ if (TryComp(body, out TransformComponent? xform))
+ _rotateToFace.TryFaceCoordinates(user, _transform.GetMapCoordinates(body, xform).Position);
+
+ var ev = new SurgeryDoAfterEvent(args.Surgery, args.Step);
+ var doAfter = new DoAfterArgs(EntityManager, user, duration, ev, body, part)
+ {
+ BreakOnMove = true,
+ DuplicateCondition = DuplicateConditions.SameTarget,
+ ForceNet = true
+ };
+ _doAfter.TryStartDoAfter(doAfter);
+ }
+
+ public (Entity Surgery, int Step)? GetNextStep(EntityUid body, EntityUid part, EntityUid surgery) => GetNextStep(body, part, surgery, []);
+ private (Entity Surgery, int Step)? GetNextStep(EntityUid body, EntityUid part, Entity surgery, List requirements)
+ {
+ if (!Resolve(surgery, ref surgery.Comp))
+ return null;
+
+ if (requirements.Contains(surgery))
+ throw new ArgumentException($"Surgery {surgery} has a requirement loop: {string.Join(", ", requirements)}");
+
+ requirements.Add(surgery);
+
+ if (surgery.Comp.Requirement is { } requirementId &&
+ GetSingleton(requirementId) is { } requirement &&
+ GetNextStep(body, part, requirement, requirements) is { } requiredNext)
+ return requiredNext;
+
+ if (!TryComp(part, out var progress))
+ {
+ AddComp(part);
+ return ((surgery, surgery.Comp), 0);
+ }
+ var surgeryProto = Prototype(surgery);
+ for (var i = 0; i < surgery.Comp.Steps.Count; i++)
+ if (!progress.CompletedSteps.Contains($"{surgeryProto?.ID}:{surgery.Comp.Steps[i]}"))
+ return ((surgery, surgery.Comp), i);
+
+ return null;
+ }
+
+ public bool PreviousStepsComplete(EntityUid body, EntityUid part, Entity surgery, EntProtoId step)
+ {
+ if (surgery.Comp.Requirement is { } requirement)
+ {
+ if (GetSingleton(requirement) is not { } requiredEnt ||
+ !TryComp(requiredEnt, out SurgeryComponent? requiredComp) ||
+ !PreviousStepsComplete(body, part, (requiredEnt, requiredComp), step))
+ {
+ return false;
+ }
+ }
+
+ foreach (var surgeryStep in surgery.Comp.Steps)
+ {
+ if (surgeryStep == step)
+ break;
+
+ if (Prototype(surgery.Owner) is not EntityPrototype surgProto || !IsStepComplete(part, surgProto.ID, surgeryStep))
+ return false;
+ }
+
+ return true;
+ }
+
+ public bool CanPerformStep(EntityUid user, EntityUid body, BodyPartType part, EntityUid step, bool doPopup) => CanPerformStep(user, body, part, step, doPopup, out _, out _, out _);
+ public bool CanPerformStep(EntityUid user, EntityUid body, BodyPartType part, EntityUid step, bool doPopup, out string? popup, out StepInvalidReason reason, out HashSet validTools)
+ {
+ var slot = part switch
+ {
+ BodyPartType.Head => SlotFlags.HEAD,
+ BodyPartType.Torso => SlotFlags.OUTERCLOTHING | SlotFlags.INNERCLOTHING,
+ BodyPartType.Arm => SlotFlags.OUTERCLOTHING | SlotFlags.INNERCLOTHING,
+ BodyPartType.Hand => SlotFlags.GLOVES,
+ BodyPartType.Leg => SlotFlags.OUTERCLOTHING | SlotFlags.LEGS,
+ BodyPartType.Foot => SlotFlags.FEET,
+ BodyPartType.Tail => SlotFlags.NONE,
+ BodyPartType.Other => SlotFlags.NONE,
+ _ => SlotFlags.NONE
+ };
+
+ var check = new SurgeryCanPerformStepEvent(user, body, GetTools(user), slot);
+ RaiseLocalEvent(step, ref check);
+ popup = check.Popup;
+ validTools = check.ValidTools;
+
+ if (check.Invalid != StepInvalidReason.None)
+ {
+ if (doPopup && check.Popup != null)
+ _popup.PopupEntity(check.Popup, user, PopupType.SmallCaution);
+
+ reason = check.Invalid;
+ return false;
+ }
+
+ reason = default;
+ return true;
+ }
+
+ public bool IsStepComplete(EntityUid part, EntProtoId surgery, EntProtoId step)
+ {
+ if (TryComp(part, out var comp))
+ return comp.CompletedSteps.Contains($"{surgery}:{step}");
+ AddComp(part);
+ return false;
+ }
+}
diff --git a/Content.Shared/_Sunrise/Medical/Surgery/SharedSurgerySystem.Conditions.cs b/Content.Shared/_Sunrise/Medical/Surgery/SharedSurgerySystem.Conditions.cs
new file mode 100644
index 00000000000..ae2361b2fa8
--- /dev/null
+++ b/Content.Shared/_Sunrise/Medical/Surgery/SharedSurgerySystem.Conditions.cs
@@ -0,0 +1,74 @@
+using Content.Shared.Body.Part;
+using System.Linq;
+using Content.Shared._Sunrise.Medical.Surgery.Steps.Parts;
+using Content.Shared._Sunrise.Medical.Surgery.Events;
+using Content.Shared._Sunrise.Medical.Surgery.Effects.Step;
+
+namespace Content.Shared._Sunrise.Medical.Surgery;
+// Based on the RMC14.
+// https://github.com/RMC-14/RMC-14
+public abstract partial class SharedSurgerySystem
+{
+ protected List _accents = [];
+ private void InitializeConditions()
+ {
+ _accents = _reflectionManager.FindTypesWithAttribute()
+ .Where(type => type.Name.EndsWith("AccentComponent"))
+ .ToList();
+
+ SubscribeLocalEvent(OnPartConditionValid);
+ SubscribeLocalEvent(OnOrganExistConditionValid);
+ SubscribeLocalEvent(OnOrganDontExistConditionValid);
+ SubscribeLocalEvent(OnAnyAccentConditionValid);
+ SubscribeLocalEvent(OnAnyLimbSlotConditionValid);
+ }
+ private void OnOrganDontExistConditionValid(Entity ent, ref SurgeryValidEvent args)
+ {
+ if (ent.Comp.Organ?.Count != 1) return;
+ var type = ent.Comp.Organ.Values.First().Component.GetType();
+
+ var organs = _body.GetPartOrgans(args.Part, Comp(args.Part));
+ foreach (var organ in organs)
+ if (HasComp(organ.Id, type))
+ {
+ args.Cancelled = true;
+ return;
+ }
+ }
+ private void OnOrganExistConditionValid(Entity ent, ref SurgeryValidEvent args)
+ {
+ if (ent.Comp.Organ?.Count != 1) return;
+ var organs = _body.GetPartOrgans(args.Part, Comp(args.Part));
+ var type = ent.Comp.Organ.Values.First().Component.GetType();
+ foreach (var organ in organs)
+ if (HasComp(organ.Id, type))
+ return;
+ args.Cancelled = true;
+ }
+
+ private void OnPartConditionValid(Entity ent, ref SurgeryValidEvent args)
+ {
+ if (ent.Comp.Parts.Count == 0)
+ return;
+
+ if (CompOrNull(args.Part)?.PartType is BodyPartType part && !ent.Comp.Parts.Contains(part))
+ args.Cancelled = true;
+ }
+ private void OnAnyAccentConditionValid(Entity ent, ref SurgeryValidEvent args)
+ {
+ foreach (var accent in _accents)
+ if (HasComp(args.Body, accent))
+ return;
+ args.Cancelled = true;
+ }
+ private void OnAnyLimbSlotConditionValid(Entity ent, ref SurgeryValidEvent args)
+ {
+ if (CompOrNull(args.Part) is not BodyPartComponent bodyPartComponent)
+ return;
+
+ if (_body.TryGetFreePartSlot(args.Part, out var slotId, bodyPartComponent))
+ args.Suffix = slotId;
+ else
+ args.Cancelled = true;
+ }
+}
diff --git a/Content.Shared/_Sunrise/Medical/Surgery/SharedSurgerySystem.cs b/Content.Shared/_Sunrise/Medical/Surgery/SharedSurgerySystem.cs
new file mode 100644
index 00000000000..c85da147e3f
--- /dev/null
+++ b/Content.Shared/_Sunrise/Medical/Surgery/SharedSurgerySystem.cs
@@ -0,0 +1,142 @@
+using System;
+using System.Linq;
+using Content.Shared._Sunrise.Medical.Surgery.Effects.Step;
+using Content.Shared._Sunrise.Medical.Surgery.Events;
+using Content.Shared._Sunrise.Medical.Surgery.Steps.Parts;
+using Content.Shared.Body.Part;
+using Content.Shared.Body.Systems;
+using Content.Shared.Buckle.Components;
+using Content.Shared.Climbing.Systems;
+using Content.Shared.Damage;
+using Content.Shared.DoAfter;
+using Content.Shared.GameTicking;
+using Content.Shared.Hands.EntitySystems;
+using Content.Shared.Interaction;
+using Content.Shared.Popups;
+using Content.Shared.Standing;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Containers;
+using Robust.Shared.Map;
+using Robust.Shared.Network;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Reflection;
+using Robust.Shared.Serialization.Manager;
+using Robust.Shared.Timing;
+
+namespace Content.Shared._Sunrise.Medical.Surgery;
+// Based on the RMC14.
+// https://github.com/RMC-14/RMC-14
+public abstract partial class SharedSurgerySystem : EntitySystem
+{
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly IComponentFactory _compFactory = default!;
+ [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
+ [Dependency] private readonly SharedHandsSystem _hands = default!;
+ [Dependency] private readonly INetManager _net = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+ [Dependency] private readonly IPrototypeManager _prototypes = default!;
+ [Dependency] private readonly RotateToFaceSystem _rotateToFace = default!;
+ [Dependency] private readonly StandingStateSystem _standing = default!;
+ [Dependency] private readonly SharedTransformSystem _transform = default!;
+ [Dependency] private readonly SharedBodySystem _body = default!;
+ [Dependency] private readonly IReflectionManager _reflectionManager = default!;
+ [Dependency] private readonly ISerializationManager _serialization = default!;
+ [Dependency] private readonly DamageableSystem _damageableSystem = default!;
+ [Dependency] private readonly SharedContainerSystem _containers = default!;
+
+ private readonly Dictionary _surgeries = new();
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnRoundRestartCleanup);
+
+ InitializeSteps();
+ InitializeConditions();
+ }
+
+ private void OnRoundRestartCleanup(RoundRestartCleanupEvent ev)
+ {
+ _surgeries.Clear();
+ }
+
+ protected bool IsSurgeryValid(EntityUid body, EntityUid targetPart, EntProtoId surgery, EntProtoId stepId, out Entity surgeryEnt, out Entity part, out EntityUid step)
+ {
+ surgeryEnt = default;
+ part = default;
+ step = default;
+
+ if (!HasComp(body) ||
+ !IsLyingDown(body) ||
+ !TryComp(targetPart, out BodyPartComponent? partComp) ||
+ GetSingleton(surgery) is not { } surgeryEntId ||
+ !TryComp(surgeryEntId, out SurgeryComponent? surgeryComp) ||
+ !surgeryComp.Steps.Contains(stepId) ||
+ GetSingleton(stepId) is not { } stepEnt) return false;
+
+ var ev = new SurgeryValidEvent(body, targetPart);
+
+ if (!TryComp(targetPart, out var progress))
+ {
+ progress = new SurgeryProgressComponent();
+ AddComp(targetPart, progress);
+ }
+
+ if (!progress.StartedSurgeries.Contains(surgery))
+ {
+ RaiseLocalEvent(stepEnt, ref ev);
+ RaiseLocalEvent(surgeryEntId, ref ev);
+ }
+
+ if (ev.Cancelled)
+ return false;
+
+ surgeryEnt = (surgeryEntId, surgeryComp);
+ part = (targetPart, partComp);
+ step = stepEnt;
+ return true;
+ }
+
+ public EntityUid? GetSingleton(EntProtoId surgeryOrStep)
+ {
+ if (!_prototypes.HasIndex(surgeryOrStep))
+ return null;
+
+ // This (for now) assumes that surgery entity data remains unchanged between client
+ // and server
+ // if it does not you get the bullet
+ if (!_surgeries.TryGetValue(surgeryOrStep, out var ent) || TerminatingOrDeleted(ent))
+ {
+ ent = Spawn(surgeryOrStep, MapCoordinates.Nullspace);
+ _surgeries[surgeryOrStep] = ent;
+ }
+
+ return ent;
+ }
+
+ protected List GetTools(EntityUid surgeon)
+ {
+ return _hands.EnumerateHeld(surgeon).ToList();
+ }
+
+ public bool IsLyingDown(EntityUid entity)
+ {
+ if (_standing.IsDown(entity))
+ return true;
+
+ if (TryComp(entity, out BuckleComponent? buckle) &&
+ TryComp(buckle.BuckledTo, out StrapComponent? strap))
+ {
+ var rotation = strap.Rotation;
+ if (rotation.GetCardinalDir() is Direction.West or Direction.East)
+ return true;
+ }
+
+ return false;
+ }
+
+ protected virtual void RefreshUI(EntityUid body)
+ {
+ }
+}
diff --git a/Content.Shared/_Sunrise/Medical/Surgery/StepInvalidReason.cs b/Content.Shared/_Sunrise/Medical/Surgery/StepInvalidReason.cs
new file mode 100644
index 00000000000..7610102ca75
--- /dev/null
+++ b/Content.Shared/_Sunrise/Medical/Surgery/StepInvalidReason.cs
@@ -0,0 +1,10 @@
+namespace Content.Shared._Sunrise.Medical.Surgery;
+// Based on the RMC14.
+// https://github.com/RMC-14/RMC-14
+public enum StepInvalidReason
+{
+ None,
+ NeedsOperatingTable,
+ Armor,
+ MissingTool,
+}
diff --git a/Content.Shared/_Sunrise/Medical/Surgery/SurgeryUI.cs b/Content.Shared/_Sunrise/Medical/Surgery/SurgeryUI.cs
new file mode 100644
index 00000000000..e517a94be07
--- /dev/null
+++ b/Content.Shared/_Sunrise/Medical/Surgery/SurgeryUI.cs
@@ -0,0 +1,25 @@
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared._Sunrise.Medical.Surgery;
+// Based on the RMC14.
+// https://github.com/RMC-14/RMC-14
+[Serializable, NetSerializable]
+public enum SurgeryUIKey
+{
+ Key
+}
+
+[Serializable, NetSerializable]
+public sealed class SurgeryBuiState : BoundUserInterfaceState
+{
+ public required Dictionary> Choices { get; init; }
+}
+
+[Serializable, NetSerializable]
+public sealed class SurgeryStepChosenBuiMsg : BoundUserInterfaceMessage
+{
+ public required NetEntity Part { get; init; }
+ public required EntProtoId Surgery { get; init; }
+ public required EntProtoId Step { get; init; }
+}
diff --git a/README.en.md b/README.en.md
new file mode 100644
index 00000000000..fe3ca02c4c4
--- /dev/null
+++ b/README.en.md
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+Space Station 14 is a remake of SS13 that runs on [Robust Toolbox](https://github.com/space-wizards/RobustToolbox), our homegrown engine written in C#.
+
+This is the primary repo for Space Station 14. To prevent people forking RobustToolbox, a "content" pack is loaded by the client and server. This content pack contains everything needed to play the game on one specific server.
+
+If you want to host or create content for SS14, this is the repo you need. It contains both RobustToolbox and the content pack for development of new content packs.
+
+## Documentation
+
+The official [documentation site](https://docs.spacestation14.io/) has all the information you need about SS14 content, engine, game design and more. There is also a lot of useful materials for beginner developers.
+
+## Project activity
+
+![Alt](https://repobeats.axiom.co/api/embed/49651d1b93eecc08e01f1a268a917343cf661fc8.svg "Repobeats analytics image")
+
+## Project contributors
+
+List of people who have contributed to the project:
+
+[![Участники](https://contrib.rocks/image?repo=space-sunrise/space-station-14)](https://github.com/space-sunrise/space-station-14/graphs/contributors)
+
+---
+
+## Licences
+
+> [!CAUTION]
+> The repository code is licensed both under MIT - this applies to Space Wizards Federation code - and under the CLA - this applies to our changes as defined in the CLA text. We do not seek to fully distinguish our code from Space Wizards Federation code, so it is recommended to take their code from their repository to avoid incidents.
+
+### Нажмите на каждый баннер, чтобы получить дополнительную информацию
+
+---
+
+
+
+
+>Some files are licensed under [MIT license](https://opensource.org/license/MIT), these files are Space Wizards Federation code.
+
+
+
+
+
+>All other non-code Sunrise Assets, including icons and sound files, are licensed under the [Creative Commons 3.0 BY-SA](https://creativecommons.org/licenses/by-sa/3.0/) license unless otherwise noted in the folder or file.
+
+
+
+
+
+>All code as well as Sunrise assemblies are protected under the [CLA](https://github.com/space-sunrise/space-station-14/blob/master/CLA.txt) license.
+
\ No newline at end of file
diff --git a/README.md b/README.md
index a7b5c0953cf..ffe822994a7 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,12 @@
+
+🌐 **Read this in other languages:**
+
+- 🇺🇸 [English](README.en.md)
+- 🇷🇺 [Русский](README.ru.md)
+