Хирургия (#607)

* Surgery

* translate strings

* ru translate

* second ru translate

* fix

* fix pizza icon

* пу-пу-пу

* fix damageeeee

* fix

* fix x2)
This commit is contained in:
Rinary 2024-11-13 03:05:51 +02:00 committed by GitHub
parent 8bec8a7b02
commit ad190f1865
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
156 changed files with 5118 additions and 137 deletions

1
.gitignore vendored
View file

@ -306,3 +306,4 @@ Resources/MapImages
# Direnv stuff
.direnv/
.idea/

View file

@ -0,0 +1,17 @@
<ui:ChoiceControl
xmlns="https://spacestation14.io"
xmlns:ui="clr-namespace:Content.Client._Sunrise">
<BoxContainer Orientation="Horizontal">
<Button Name="Button" Access="Public"
HorizontalExpand="True" VerticalExpand="False"
StyleClasses="ButtonSquare" Margin="0">
<BoxContainer Orientation="Horizontal" Margin="0">
<TextureRect Name="Texture" Access="Public"
HorizontalExpand="False" VerticalExpand="False"
Margin="1"/>
<Control MinWidth="5"/>
<RichTextLabel Name="NameLabel" Access="Public" VerticalAlignment="Center"/>
</BoxContainer>
</Button>
</BoxContainer>
</ui:ChoiceControl>

View file

@ -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;
}
}

View file

@ -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<EntProtoId> _previousSurgeries = new();
public SurgeryBui(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
_system = _entities.System<SurgerySystem>();
_hands = _entities.System<HandsSystem>();
_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<Entity<BodyPartComponent>>(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<BodyPartComponent> 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<MetaDataComponent>(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<MetaDataComponent>(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<SurgeryComponent> 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<MetaDataComponent>(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<SurgeryComponent> 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<SpriteComponent>(surgery)?.Icon?.Default;
var name = $"{_entities.GetComponent<MetaDataComponent>(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<SurgeryComponent>(_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<MetaDataComponent>(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<SpriteComponent>(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;
}
}

View file

@ -0,0 +1,4 @@
<controls:SurgeryStepButton
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client._Sunrise.Medical.Surgery">
</controls:SurgeryStepButton>

View file

@ -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);
}

View file

@ -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();
}
}
}

View file

@ -0,0 +1,30 @@
<controls:SurgeryWindow
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client._Sunrise.Medical.Surgery"
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
MinSize="400 400">
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="0 0 0 10">
<Button Name="PartsButton" Access="Public" Text="{Loc 'surgery-window-partsbutton-name'}"
HorizontalExpand="True" StyleClasses="OpenBoth" />
<Button Name="SurgeriesButton" Access="Public" Text="{Loc 'surgery-window-surgeriesbutton-name'}"
HorizontalExpand="True" StyleClasses="OpenBoth" />
<Button Name="StepsButton" Access="Public" Text="{Loc 'surgery-window-stepsbutton-name'}"
HorizontalExpand="True" StyleClasses="OpenBoth" />
</BoxContainer>
<cc:HSeparator Color="#4972A1" />
<ScrollContainer VScrollEnabled="True" HorizontalExpand="True" VerticalExpand="True">
<BoxContainer Name="Parts" Access="Public" Orientation="Vertical" Visible="False" />
<BoxContainer Name="Surgeries" Access="Public" Orientation="Vertical" Visible="False" />
<BoxContainer Name="Steps" Access="Public" Orientation="Vertical" Visible="False" />
</ScrollContainer>
</BoxContainer>
<PanelContainer Name="DisabledPanel" Access="Public" HorizontalExpand="True"
VerticalExpand="True" Visible="False">
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BackgroundColor="#000000BF" />
</PanelContainer.PanelOverride>
<RichTextLabel Name="DisabledLabel" Access="Public" HorizontalAlignment="Center" />
</PanelContainer>
</controls:SurgeryWindow>

View file

@ -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);
}

View file

@ -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<SurgeryStepBleedEffectComponent, SurgeryStepEvent>(OnStepBleedComplete);
SubscribeLocalEvent<SurgeryClampBleedEffectComponent, SurgeryStepEvent>(OnStepClampBleedComplete);
SubscribeLocalEvent<SurgeryStepEmoteEffectComponent, SurgeryStepEvent>(OnStepEmoteEffectComplete);
SubscribeLocalEvent<SurgeryStepSpawnEffectComponent, SurgeryStepEvent>(OnStepSpawnComplete);
SubscribeLocalEvent<SurgeryStepOrganExtractComponent, SurgeryStepEvent>(OnStepOrganExtractComplete);
SubscribeLocalEvent<SurgeryStepOrganInsertComponent, SurgeryStepEvent>(OnStepOrganInsertComplete);
SubscribeLocalEvent<SurgeryStepAttachLimbEffectComponent, SurgeryStepEvent>(OnStepAttachLimbComplete);
SubscribeLocalEvent<SurgeryStepAmputationEffectComponent, SurgeryStepEvent>(OnStepAmputationComplete);
SubscribeLocalEvent<SurgeryRemoveAccentComponent, SurgeryStepEvent>(OnRemoveAccent);
}
private void OnStepBleedComplete(Entity<SurgeryStepBleedEffectComponent> ent, ref SurgeryStepEvent args)
{
if(ent.Comp.Damage is not null && TryComp<DamageableComponent>(args.Body, out var comp))
_damageableSystem.SetDamage(args.Body, comp, ent.Comp.Damage);
//todo add wound
}
private void OnStepClampBleedComplete(Entity<SurgeryClampBleedEffectComponent> ent, ref SurgeryStepEvent args)
{
//todo remove wound
}
private void OnStepOrganInsertComplete(Entity<SurgeryStepOrganInsertComponent> ent, ref SurgeryStepEvent args)
{
if (args.Tools.Count == 0
|| !(args.Tools.FirstOrDefault() is var organId)
|| !TryComp<BodyPartComponent>(args.Part, out var bodyPart)
|| !TryComp<OrganComponent>(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<DamageableComponent>(organId, out var organDamageable)
&& TryComp<DamageableComponent>(body, out var bodyDamageable))
{
if (TryComp<OrganEyesComponent>(organId, out var organEyes)
&& TryComp<BlindableComponent>(body, out var blindable))
{
_blindable.SetMinDamage((body, blindable), organEyes.MinDamage ?? 0);
_blindable.AdjustEyeDamage((body, blindable), (organEyes.EyeDamage ?? 0) - blindable.MaxDamage);
}
if (TryComp<OrganTongueComponent>(organId, out var organTongue)
&& !organTongue.IsMuted)
RemComp<MutedComponent>(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<SurgeryStepOrganExtractComponent> ent, ref SurgeryStepEvent args)
{
if (ent.Comp.Organ?.Count != 1) return;
var organs = _body.GetPartOrgans(args.Part, Comp<BodyPartComponent>(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<OrganDamageComponent>(organ.Id, out var damageRule)
&& damageRule.Damage is not null
&& TryComp<DamageableComponent>(organ.Id, out var organDamageable)
&& TryComp<DamageableComponent>(args.Body, out var bodyDamageable))
{
if (TryComp<OrganEyesComponent>(organ.Id, out var organEyes)
&& TryComp<BlindableComponent>(args.Body, out var blindable))
{
organEyes.EyeDamage = blindable.EyeDamage;
organEyes.MinDamage = blindable.MinDamage;
_blindable.UpdateIsBlind((args.Body, blindable));
}
if (TryComp<OrganTongueComponent>(organ.Id, out var organTongue))
{
organTongue.IsMuted = HasComp<MutedComponent>(args.Body);
AddComp<MutedComponent>(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<SurgeryRemoveAccentComponent> ent, ref SurgeryStepEvent args)
{
foreach (var accent in _accents)
if (HasComp(args.Body, accent))
RemCompDeferred(args.Body, accent);
}
private void OnStepEmoteEffectComplete(Entity<SurgeryStepEmoteEffectComponent> ent, ref SurgeryStepEvent args)
=> _chat.TryEmoteWithChat(args.Body, ent.Comp.Emote);
private void OnStepSpawnComplete(Entity<SurgeryStepSpawnEffectComponent> ent, ref SurgeryStepEvent args)
{
if (TryComp(args.Body, out TransformComponent? xform))
SpawnAtPosition(ent.Comp.Entity, xform.Coordinates);
}
private void OnStepAttachLimbComplete(Entity<SurgeryStepAttachLimbEffectComponent> ent, ref SurgeryStepEvent args)
{
if (args.Tools.Count == 0
|| !(args.Tools.FirstOrDefault() is var limbId)
|| !TryComp<BodyPartComponent>(args.Part, out var bodyPart)
|| !TryComp<BodyPartComponent>(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<HumanoidAppearanceComponent>(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<BaseLayerIdComponent>(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<SurgeryStepAmputationEffectComponent> 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<HumanoidAppearanceComponent>(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<BaseLayerIdComponent>(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<HumanoidSpeciesBaseSpritesPrototype>(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;
}
}
}
}
}

View file

@ -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<EntProtoId> _surgeries = [];
public override void Initialize()
{
base.Initialize();
InitializeSteps();
SubscribeLocalEvent<SurgeryToolComponent, AfterInteractEvent>(OnToolAfterInteract);
SubscribeLocalEvent<PrototypesReloadedEventArgs>(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<SurgeryTargetComponent>(body))
return;
var surgeries = new Dictionary<NetEntity, List<(EntProtoId, string suffix, bool isCompleted)>>();
foreach (var part in _body.GetBodyChildren(body))
{
if (!TryComp<SurgeryProgressComponent>(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<SurgeryToolComponent> ent, ref AfterInteractEvent args)
{
var user = args.User;
if (args.Handled ||
!args.CanReach ||
args.Target == null ||
_ui.IsUiOpen(user, SurgeryUIKey.Key, user) ||
!HasComp<SurgeryTargetComponent>(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<EntityPrototype>())
LoadPrototypes();
}
private void LoadPrototypes()
{
_surgeries.Clear();
foreach (var entity in _prototypes.EnumeratePrototypes<EntityPrototype>())
{
if (entity.HasComponent<SurgeryComponent>())
_surgeries.Add(new EntProtoId(entity.ID));
}
}
}

View file

@ -88,9 +88,12 @@ public sealed partial class BodyPartComponent : Component
[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;

View file

@ -790,5 +790,46 @@ public partial class SharedBodySystem
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<string> 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
}

View file

@ -23,7 +23,7 @@ namespace Content.Shared.Damage
[JsonPropertyName("types")]
[DataField("types", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<FixedPoint2, DamageTypePrototype>))]
[UsedImplicitly]
private Dictionary<string,FixedPoint2>? _damageTypeDictionary;
private Dictionary<string, FixedPoint2>? _damageTypeDictionary;
[JsonPropertyName("groups")]
[DataField("groups", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<FixedPoint2, DamageGroupPrototype>))]
@ -71,6 +71,14 @@ namespace Content.Shared.Damage
return false;
}
public DamageSpecifier Invert()
{
var copy = new DamageSpecifier(this);
foreach (var key in copy.DamageDict.Keys)
copy.DamageDict[key] *= -1;
return copy;
}
/// <summary>
/// Whether this damage specifier has any entries.
/// </summary>
@ -152,7 +160,7 @@ namespace Content.Shared.Damage
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);
}

View file

@ -46,6 +46,9 @@ public sealed partial class DoAfterArgs
[DataField]
public bool Hidden;
[DataField]
public bool ForceNet;
#region Event options
/// <summary>
/// The event that will get raised when the DoAfter has finished. If null, this will simply raise a <see cref="SimpleDoAfterEvent"/>

View file

@ -188,7 +188,7 @@ public abstract partial class SharedDoAfterSystem : EntitySystem
{
DebugTools.Assert(args.Broadcast || Exists(args.EventTarget) || args.Event.GetType() == typeof(AwaitedDoAfterEvent));
DebugTools.Assert(args.Event.GetType().HasCustomAttribute<NetSerializableAttribute>()
|| 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 abstract partial class SharedDoAfterSystem : EntitySystem
{
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 @@ public abstract partial class SharedDoAfterSystem : EntitySystem
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)

View file

@ -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 sealed class BlindableSystem : EntitySystem
var old = blindable.Comp.IsBlind;
var forceBlind = false;
if(TryComp<BodyComponent>(blindable.Owner, out var body))
{
var eyes = _bodySystem.GetBodyOrganEntityComps<OrganEyesComponent>((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;
}

View file

@ -44,5 +44,5 @@ public sealed partial class TechDisciplinePrototype : IPrototype
/// Purchasing this tier of technology causes a server to become "locked" to this discipline.
/// </summary>
[DataField("lockoutTier")]
public int LockoutTier = 3;
public int LockoutTier = 4;
}

View file

@ -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;
}

View file

@ -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<EntProtoId> Steps = new();
}

View file

@ -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<EntProtoId> CompletedSteps = [];
[DataField, AutoNetworkedField]
public HashSet<EntProtoId> CompletedSurgeries = [];
[DataField, AutoNetworkedField]
public HashSet<EntProtoId> StartedSurgeries = [];
}

View file

@ -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;
}

View file

@ -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;

View file

@ -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<BodyPartType> 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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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<EmotePrototype> Emote = "Scream";
}
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedSurgerySystem))]
public sealed partial class SurgeryStepSpawnEffectComponent : Component
{
[DataField(required: true), AutoNetworkedField]
public EntProtoId Entity;
}

View file

@ -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";
}

View file

@ -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<EntityUid> Tools,
SlotFlags TargetSlots,
string? Popup = null,
StepInvalidReason Invalid = StepInvalidReason.None
) : IInventoryRelayEvent
{
public HashSet<EntityUid> ValidTools = [];
}

View file

@ -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;
}
}

View file

@ -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;
/// <summary>
/// Raised on the step entity.
/// </summary>
[ByRefEvent]
public record struct SurgeryStepEvent(EntityUid User, EntityUid Body, EntityUid Part, List<EntityUid> Tools)
{
public required EntProtoId StepProto { get; init; }
public required EntProtoId SurgeryProto { get; init; }
public required bool IsFinal { get; init; }
}

View file

@ -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
/// <summary>
/// Raised on the entity that is receiving surgery.
/// </summary>
[ByRefEvent]
public record struct SurgeryValidEvent(EntityUid Body, EntityUid Part, bool Cancelled = false, string Suffix = "");

View file

@ -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; }
}

View file

@ -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<Action> _delayQueue = new();
private void InitializeSteps()
{
SubscribeLocalEvent<SurgeryStepComponent, SurgeryStepEvent>(OnStep);
SubscribeLocalEvent<SurgeryClearProgressComponent, SurgeryStepEvent>(OnClearProgressStep);
SubscribeLocalEvent<SurgeryTargetComponent, SurgeryDoAfterEvent>(OnTargetDoAfter);
SubscribeLocalEvent<SurgeryStepComponent, SurgeryCanPerformStepEvent>(OnCanPerformStep);
Subs.BuiEvents<SurgeryTargetComponent>(SurgeryUIKey.Key, subs => subs.Event<SurgeryStepChosenBuiMsg>(OnSurgeryTargetStepChosen));
}
private void OnTargetDoAfter(Entity<SurgeryTargetComponent> 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<BodyPartComponent>(args.Target.Value, out var dirtyPart))
Dirty(args.Target.Value, dirtyPart, Comp<MetaDataComponent>(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<SurgeryClearProgressComponent> ent, ref SurgeryStepEvent args)
{
var progress = Comp<SurgeryProgressComponent>(args.Part);
progress.CompletedSteps.Clear();
progress.CompletedSurgeries.Clear();
}
private void OnStep(Entity<SurgeryStepComponent> ent, ref SurgeryStepEvent args)
{
if (!TryComp<SurgeryClearProgressComponent>(ent, out _))
{
if (TryComp<SurgeryProgressComponent>(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<SurgeryStepComponent> ent, ref SurgeryCanPerformStepEvent args)
{
if (HasComp<SurgeryOperatingTableConditionComponent>(ent)
&& (!TryComp(args.Body, out BuckleComponent? buckle) || !HasComp<OperatingTableComponent>(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<SurgeryTargetComponent> 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<SurgeryProgressComponent>(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<SurgeryComponent> Surgery, int Step)? GetNextStep(EntityUid body, EntityUid part, EntityUid surgery) => GetNextStep(body, part, surgery, []);
private (Entity<SurgeryComponent> Surgery, int Step)? GetNextStep(EntityUid body, EntityUid part, Entity<SurgeryComponent?> surgery, List<EntityUid> 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<SurgeryProgressComponent>(part, out var progress))
{
AddComp<SurgeryProgressComponent>(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<SurgeryComponent> 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<EntityUid> 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<SurgeryProgressComponent>(part, out var comp))
return comp.CompletedSteps.Contains($"{surgery}:{step}");
AddComp<SurgeryProgressComponent>(part);
return false;
}
}

View file

@ -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<Type> _accents = [];
private void InitializeConditions()
{
_accents = _reflectionManager.FindTypesWithAttribute<RegisterComponentAttribute>()
.Where(type => type.Name.EndsWith("AccentComponent"))
.ToList();
SubscribeLocalEvent<SurgeryPartConditionComponent, SurgeryValidEvent>(OnPartConditionValid);
SubscribeLocalEvent<SurgeryOrganExistConditionComponent, SurgeryValidEvent>(OnOrganExistConditionValid);
SubscribeLocalEvent<SurgeryOrganDontExistConditionComponent, SurgeryValidEvent>(OnOrganDontExistConditionValid);
SubscribeLocalEvent<SurgeryAnyAccentConditionComponent, SurgeryValidEvent>(OnAnyAccentConditionValid);
SubscribeLocalEvent<SurgeryAnyLimbSlotConditionComponent, SurgeryValidEvent>(OnAnyLimbSlotConditionValid);
}
private void OnOrganDontExistConditionValid(Entity<SurgeryOrganDontExistConditionComponent> 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<BodyPartComponent>(args.Part));
foreach (var organ in organs)
if (HasComp(organ.Id, type))
{
args.Cancelled = true;
return;
}
}
private void OnOrganExistConditionValid(Entity<SurgeryOrganExistConditionComponent> ent, ref SurgeryValidEvent args)
{
if (ent.Comp.Organ?.Count != 1) return;
var organs = _body.GetPartOrgans(args.Part, Comp<BodyPartComponent>(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<SurgeryPartConditionComponent> ent, ref SurgeryValidEvent args)
{
if (ent.Comp.Parts.Count == 0)
return;
if (CompOrNull<BodyPartComponent>(args.Part)?.PartType is BodyPartType part && !ent.Comp.Parts.Contains(part))
args.Cancelled = true;
}
private void OnAnyAccentConditionValid(Entity<SurgeryAnyAccentConditionComponent> ent, ref SurgeryValidEvent args)
{
foreach (var accent in _accents)
if (HasComp(args.Body, accent))
return;
args.Cancelled = true;
}
private void OnAnyLimbSlotConditionValid(Entity<SurgeryAnyLimbSlotConditionComponent> ent, ref SurgeryValidEvent args)
{
if (CompOrNull<BodyPartComponent>(args.Part) is not BodyPartComponent bodyPartComponent)
return;
if (_body.TryGetFreePartSlot(args.Part, out var slotId, bodyPartComponent))
args.Suffix = slotId;
else
args.Cancelled = true;
}
}

View file

@ -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<EntProtoId, EntityUid> _surgeries = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestartCleanup);
InitializeSteps();
InitializeConditions();
}
private void OnRoundRestartCleanup(RoundRestartCleanupEvent ev)
{
_surgeries.Clear();
}
protected bool IsSurgeryValid(EntityUid body, EntityUid targetPart, EntProtoId surgery, EntProtoId stepId, out Entity<SurgeryComponent> surgeryEnt, out Entity<BodyPartComponent> part, out EntityUid step)
{
surgeryEnt = default;
part = default;
step = default;
if (!HasComp<SurgeryTargetComponent>(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<SurgeryProgressComponent>(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<EntityUid> 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)
{
}
}

View file

@ -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,
}

View file

@ -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<NetEntity, List<(EntProtoId, string, bool)>> 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; }
}

View file

@ -0,0 +1,49 @@
- files: ["cautery1.ogg"]
license: "CC-BY-SA-3.0"
copyright: "Taken from cmss13"
source: "https://github.com/cmss13-devs/cmss13/blob/fae73dfa5aedb0a253de04b60085ed8a178d3bf7/sound/surgery/cautery1.ogg"
- files: ["cautery2.ogg"]
license: "CC-BY-SA-3.0"
copyright: "Taken from cmss13"
source: "https://github.com/cmss13-devs/cmss13/blob/fae73dfa5aedb0a253de04b60085ed8a178d3bf7/sound/surgery/cautery2.ogg"
- files: ["hemostat.ogg"]
license: "CC-BY-SA-3.0"
copyright: "Taken from cmss13"
source: "https://github.com/cmss13-devs/cmss13/blob/fae73dfa5aedb0a253de04b60085ed8a178d3bf7/sound/surgery/hemostat.ogg"
- files: ["organ1.ogg"]
license: "CC-BY-SA-3.0"
copyright: "Taken from cmss13"
source: "https://github.com/cmss13-devs/cmss13/blob/fae73dfa5aedb0a253de04b60085ed8a178d3bf7/sound/surgery/organ1.ogg"
- files: ["organ2.ogg"]
license: "CC-BY-SA-3.0"
copyright: "Taken from cmss13"
source: "https://github.com/cmss13-devs/cmss13/blob/fae73dfa5aedb0a253de04b60085ed8a178d3bf7/sound/surgery/organ2.ogg"
- files: ["retractor1.ogg"]
license: "CC-BY-SA-3.0"
copyright: "Taken from cmss13"
source: "https://github.com/cmss13-devs/cmss13/blob/fae73dfa5aedb0a253de04b60085ed8a178d3bf7/sound/surgery/retractor1.ogg"
- files: ["retractor2.ogg"]
license: "CC-BY-SA-3.0"
copyright: "Taken from cmss13"
source: "https://github.com/cmss13-devs/cmss13/blob/fae73dfa5aedb0a253de04b60085ed8a178d3bf7/sound/surgery/retractor2.ogg"
- files: ["saw.ogg"]
license: "CC-BY-SA-3.0"
copyright: "Taken from cmss13"
source: "https://github.com/cmss13-devs/cmss13/blob/fae73dfa5aedb0a253de04b60085ed8a178d3bf7/sound/surgery/saw.ogg"
- files: ["scalpel1.ogg"]
license: "CC-BY-SA-3.0"
copyright: "Taken from cmss13"
source: "https://github.com/cmss13-devs/cmss13/blob/fae73dfa5aedb0a253de04b60085ed8a178d3bf7/sound/surgery/scalpel1.ogg"
- files: ["scalpel2.ogg"]
license: "CC-BY-SA-3.0"
copyright: "Taken from cmss13"
source: "https://github.com/cmss13-devs/cmss13/blob/fae73dfa5aedb0a253de04b60085ed8a178d3bf7/sound/surgery/scalpel2.ogg"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,2 @@
categories-surgeries = surgeries
categories-surgery-steps = surgery steps

View file

@ -1,3 +1,5 @@
lathe-category-surgery = Surgery
lathe-category-cyberlimbs = Limbs
lathe-category-reports = Reports
lathe-category-statements = Statements
lathe-category-inquiries-and-appeals = Inquiries and appeals

View file

@ -1,2 +1,8 @@
research-discipline-biochemical = Biochemical
research-technology-basic-surgery = Basic surgery
research-technology-basic-cyberlimbs = Basic cyberlimbs
research-technology-advanced-surgery = Advanced surgery
research-technology-handcraft-nvd = Кустарные ПНВ
research-technology-basic-nvd = Продвинутое ПНВ

View file

@ -0,0 +1,18 @@
ent-PartCyber = кибернетическая часть тела
.desc = { ent-BaseItem.desc }
ent-LeftArmCyber = левая кибернетическая рука
.desc = { ent-PartCyber.desc }
ent-RightArmCyber = правая кибернетическая рука
.desc = { ent-PartCyber.desc }
ent-LeftHandCyber = левая кибернетическая кисть
.desc = { ent-PartCyber.desc }
ent-RightHandCyber = правая кибернетическая кисть
.desc = { ent-PartCyber.desc }
ent-LeftLegCyber = левая кибернетическая нога
.desc = { ent-PartCyber.desc }
ent-RightLegCyber = правая кибернетическая нога
.desc = { ent-PartCyber.desc }
ent-LeftFootCyber = левая кибернетическая ступня
.desc = { ent-PartCyber.desc }
ent-RightFootCyber = правая кибернетическая ступня
.desc = { ent-PartCyber.desc }

View file

@ -1,2 +1,2 @@
ent-ExpCollarsKit = Коробка с взрывными ошейниками
ent-ExpCollarsKit = Коробка с взрывными ошейниками
.desc = Ужасающе

View file

@ -11,7 +11,7 @@ ent-ClothingBeltMedicalCMO = медицинский пояс главного в
.desc = Стерильный пояс со множеством карманов под таблетки и другие лекарства, подчёркивающий, что лечить нужно со стилем.
ent-ClothingBeltReaperWebbing = разгрузочный жилет
.desc = Тактическая разгрузка, которую носят десантники Синдиката.
ent-ClothingBeltSheathSyndicateFilled = {ent-ClothingBeltSheathSyndicate}
ent-ClothingBeltSheathSyndicateFilled = { ent-ClothingBeltSheathSyndicate }
.desc = { ent-ClothingBeltSheathSyndicate.desc }
ent-ClothingBeltSheathSyndicate = ножны для рапиры
.desc = Зловещие тонкие ножны, подходящие для рапиры.

View file

@ -1,3 +1,2 @@
ent-ClothingEyesUniversalMedicalHud = универсальный медицинский визор
.desc = Дисплей, сочетающий в себе преимущества медицинского сканера и очков для химического анализа.

View file

@ -1,2 +1,2 @@
ent-ClothingHandsClockDivine = божественные часы
.desc = Эти часы может носить только божественная личность.
.desc = Эти часы может носить только божественная личность.

View file

@ -49,4 +49,4 @@ ent-ClothingHeadHatGarrisonCapAdjutant = пилотка адъютанта
ent-ClothingHeadHatCapAdjutant = фуражка адъютанта
.desc = Синяя с белым, подозрительно напоминает капитанскую.
ent-ClothingHeadHatWithPaces = Шляпа с пейсами
.desc = Чёрная шляпа с пейсами, обычно такую носят евреи.
.desc = Чёрная шляпа с пейсами, обычно такую носят евреи.

View file

@ -42,4 +42,3 @@ ent-ClothingCloakAtmosian = плащ атмосианина
.desc = Плащ легендарного атмосианина.
ent-ClothingNeckCloakUeg = плащ десантника ОПЗ
.desc = Плащ знаменитых космических десантников Объединённого Правительства Земли. Его носят только поистине те, кто смог удостоится чести быть космическим десантником ОПЗ. Этот плащ олицетворяет свободу и демократию, к которым должна стремиться цивилизация. В ином случае - демократию и освобождение принесёт Правительство Земли.

View file

@ -1,2 +1,2 @@
ent-ClothingOuterSuitCargo = грузовой костюм
.desc = Специальный костюм для работников карго, немного напоминающий старый костюм из другой вселенной.
.desc = Специальный костюм для работников карго, немного напоминающий старый костюм из другой вселенной.

View file

@ -1,4 +1,4 @@
ent-ClothingShoesBootsMagCombat = боевые магнитные ботинки
.desc = Боевые магнитные ботинки, часто используемые во время выхода в открытый космос, чтобы гарантировать, что пользователь остается безопасно прикрепленным к станции.
ent-ClothingShoesBootsMagPirate = магнитные ботинки пирата
.desc = Боевые магнитки перекрашенные в пирацкие цвета
.desc = Боевые магнитки перекрашенные в пирацкие цвета

View file

@ -1,2 +1,2 @@
ent-PortalGate = { ent-BasePortal }
.desc = { ent-BasePortal.desc }
.desc = { ent-BasePortal.desc }

View file

@ -12,4 +12,4 @@ ent-ClothingOuterHardsuitSyndieCommanderBiocode = { ent-ClothingOuterHardsuitSyn
.desc = { ent-ClothingOuterHardsuitSyndieCommander.desc }
ent-ClothingOuterHardsuitJuggernautBiocode = { ent-ClothingOuterHardsuitJuggernaut }
.suffix = БИОКОД
.desc = { ent-ClothingOuterHardsuitJuggernaut.desc }
.desc = { ent-ClothingOuterHardsuitJuggernaut.desc }

View file

@ -1,4 +1,4 @@
ent-PrinterDocMachineCircuitboard = принтер документов (машинная плата)
.desc = Машинная плата принтера документов.
ent-PacificatorCircuitboard = генератор пацифизма (машинная плата)
.desc = { ent-BaseMachineCircuitboard.desc }
.desc = { ent-BaseMachineCircuitboard.desc }

View file

@ -1,4 +1,4 @@
ent-ExplosiveCollarBase = ошейник
ent-ExplosiveCollarBase = ошейник
.desc = Высокотехнологичный ошейник, основанный на инвертере квантового спина, использующийся синдикатом для взятия в заложники важных лиц.
ent-ExplosiveCollarRed = красный { ent-ExplosiveCollarBase }
.desc = { ent-ExplosiveCollarBase.desc }

View file

@ -1,2 +1,2 @@
ent-bouquet = Букет
ent-bouquet = Букет
.desc = Букет душистых цветов.

View file

@ -1,2 +1,2 @@
ent-BluespaceBox = блюспейс коробка
.desc = коробка, использующая блюспейс технологии для умещения любого предмета и увеличения вместимости.
.desc = коробка, использующая блюспейс технологии для умещения любого предмета и увеличения вместимости.

View file

@ -3,4 +3,4 @@ ent-EnergyDomeGeneratorBackpackSyndie = кроваво-красный наспи
ent-EnergyDomeGeneratorBackpackNT = BR-50c "Бастион"
.desc = Наспинный генератор щита, защищающий владельца от лазеров и пуль, но не позволяющий самому использовать оружие дальнего боя. Использует батареи.
ent-EnergyDomeGeneratorPersonalNT = BT-21b "Барьер"
.desc = Генератор щита, защищающий владельца от лазеров и пуль, но не позволяющий самому использовать оружие дальнего боя. Использует батареи.
.desc = Генератор щита, защищающий владельца от лазеров и пуль, но не позволяющий самому использовать оружие дальнего боя. Использует батареи.

View file

@ -1,2 +1,2 @@
ent-Pacificator = генератор пацифизма
.desc = Делает всех разумных существ в радиусе действия пацифистами.
.desc = Делает всех разумных существ в радиусе действия пацифистами.

View file

@ -0,0 +1,2 @@
ent-Reflector = рефлектор
.desc = { "" }

View file

@ -0,0 +1,6 @@
ent-SurgeryStepExposeVocalCords = Раскрыть голосовые связки
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepAdjustVocalCords = Отрегулировать голосовые связки
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepSutureIncision = Наложить швы
.desc = { ent-SurgeryStepBase.desc }

View file

@ -0,0 +1,2 @@
ent-SurgeryAmputationStep = Пропилить конечность
.desc = { ent-SurgeryStepBase.desc }

View file

@ -0,0 +1,12 @@
ent-SurgeryStepExposeNerves = Раскрыть нервы
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepExposeBloodVessels = Открыть кровеносные сосуды
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryLimbAttachmentStep = Прикрепить конечность
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepRejoinNerves = Восстановить нервы
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepRejoinBloodVessels = Восстановить кровеносные сосуды
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepRestoreCartilage = Восстановить хрящи
.desc = { ent-SurgeryStepBase.desc }

View file

@ -0,0 +1,136 @@
ent-SurgeryStepLocateLiver = Найти печень
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepClampLiverVessels = Зажать сосуды печени
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepRemoveLiver = Убрать печень
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepLocateAppendix = Найти аппендикс
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepClampAppendix = Зажать аппендикс
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepRemoveAppendix = Убрать аппендикс
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepLocateKidneys = Найти почки
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepClampKidneysVessels = Зажать сосуды почек
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepRemoveKidneys = Убрать почки
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepLocateStomach = Найти желудок
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepClampStomachVessels = Зажать сосуды желудка
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepRemoveStomach = Убрать желудок
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepLocateLungs = Найти лёгкие
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepClampLungVessels = Зажать сосуды лёгких
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepRemoveLungs = Убрать лёгкие
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepLocateHeart = Найти сердце
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepClampHeartVessels = Зажать сосуды сердца
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepRemoveHeart = Убрать сердце
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepLocateEyes = Найти глаза
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepClampOpticNerve = Зажать оптический нерв
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepRemoveEyes = Убрать глаза
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepGrabTongue = Взять язык
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepCutTongue = Отрезать язык
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepPreparePatient = Подготовить пациента
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepShaveHead = Побрить голову
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepDisinfectScalp = Дезинфицировать кожу головы
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepMakeIncisionScalp = Сделать надрез на коже головы
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepRetractScalp = Втянуть кожу лоскута головы
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepDrillBurrHoles = Просверлить отверстие
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepCutSkull = Распилить череп
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepRemoveBoneFlap = Удалить костный лоскут
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepClampDuraMater = Зажать мозговую оболочку
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepInciseDuraMater = Надрезать мозговую оболочку
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepRetractDuraMater = Втянуть мозговую оболочку
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepSeverCranialNerves = Перерезать черепные нервы
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepExtractBrain = Достать мозг
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepPrepareImplantSiteLiver = Подготовить место для имплантата
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepInsertLiver = Вставить печень
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepConnectLiverVessels = Соединить сосуды печени
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepPrepareImplantSiteKidneys = Подготовить место для имплантата
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepInsertKidneys = Вставить почки
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepConnectKidneysVessels = Соединить сосуды почек
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepPrepareImplantSiteStomach = Подготовить место для имплантата
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepInsertStomach = Вставить желудок
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepConnectStomachVessels = Соединить сосуды желудка
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepPrepareImplantSiteLungs = Подготовить место для имплантата
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepInsertLungs = Вставить лёгкие
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepConnectLungVessels = Соединить сосуды лёгких
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepPrepareImplantSiteHeart = Подготовить место для имплантата
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepInsertHeart = Вставить сердце
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepConnectHeartVessels = Соединить сосуды сердца
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepPrepareImplantSiteEyes = Подготовить место для имплантата
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepInsertEyes = Вставить глаз
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepConnectOpticNerve = Соединить оптический нерв
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepPrepareImplantSiteTongue = Подготовить место для имплантата
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepPositionTongue = Положить язык
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepAttachTongue = Прикрепить язык
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepCleanImplantSite = Очистить место для имплантата
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepPrepareScalp = Подготовить кожу головы
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepPrepareDuraMater = Подготовить мозговую оболочку
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepInsertBrain = Вставить мозг
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepReconnectCranialNerves = Соединить черепные нервы
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepReplaceDuraMater = Заменить мозговую оболочку
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepSealDuraMater = Уплотнить мозговую оболочку
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepReplaceBoneFlap = Заменить костный лоскут
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepSecureBoneFlap = Закрепить костный лоскут
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepReplaceScalp = Заменить кожу головы
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepSutureScalp = Наложить швы на кожу головы
.desc = { ent-SurgeryStepBase.desc }

View file

@ -0,0 +1,48 @@
ent-SurgeryOpenIncision = Открыть порез
.desc = { ent-SurgeryBase.desc }
ent-SurgeryCloseIncision = Закрыть порез
.desc = { ent-SurgeryBase.desc }
ent-SurgeryOpenRibcage = Открыть грудную клетку
.desc = { ent-SurgeryBase.desc }
ent-SurgeryOpenAbdomen = Открыть брюшную полость
.desc = { ent-SurgeryBase.desc }
ent-SurgeryEliminateVocalCordDefects = Устранить дефекты голосовых связок
.desc = Устранить дефекты речи, акценты.
ent-SurgeryAmputation = Ампутация
.desc = Хирургическое удаление конечности.
ent-SurgeryLimbAttachment = Прикрепить конечность
.desc = Хирургическое прикрепление конечности.
ent-SurgeryExtractLiver = Достать печень
.desc = { ent-SurgeryBase.desc }
ent-SurgeryImplantLiver = Имплантировать печень
.desc = { ent-SurgeryBase.desc }
ent-SurgeryExtractAppendix = Достать аппендикс
.desc = { ent-SurgeryBase.desc }
ent-SurgeryExtractKidneys = Достать почки
.desc = { ent-SurgeryBase.desc }
ent-SurgeryImplantKidneys = Имплантировать почки
.desc = { ent-SurgeryBase.desc }
ent-SurgeryExtractStomach = Достать желудок
.desc = { ent-SurgeryBase.desc }
ent-SurgeryImplantStomach = Имплантировать желудок
.desc = { ent-SurgeryBase.desc }
ent-SurgeryExtractLungs = Достать лёгкие
.desc = { ent-SurgeryBase.desc }
ent-SurgeryImplantLungs = Имплантировать лёгкие
.desc = { ent-SurgeryBase.desc }
ent-SurgeryExtractHeart = Достать сердце
.desc = { ent-SurgeryBase.desc }
ent-SurgeryImplantHeart = Имплантировать сердце
.desc = { ent-SurgeryBase.desc }
ent-SurgeryExtractEyes = Достать глаза
.desc = { ent-SurgeryBase.desc }
ent-SurgeryImplantEyes = Имплантировать глаза
.desc = { ent-SurgeryBase.desc }
ent-SurgeryExtractTongue = Достать язык
.desc = { ent-SurgeryBase.desc }
ent-SurgeryImplantTongue = Имплантировать язык
.desc = { ent-SurgeryBase.desc }
ent-SurgeryExtractBrain = Достать мозг
.desc = { ent-SurgeryBase.desc }
ent-SurgeryImplantBrain = Имплантировать мозг
.desc = { ent-SurgeryBase.desc }

View file

@ -0,0 +1,24 @@
ent-SurgeryStepOpenIncisionScalpel = Разрезать скальпелем
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepClampBleeders = Зажать источник кровотечения
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepRetractSkin = Втяните кожу
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepSawBones = Распилить кости
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepPriseOpenBones = Вскрыть кости
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepCloseBones = Закрыть кости
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepMendRibcage = Укрепить кости
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepCloseIncision = Закрыть разрез
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepCutAbdominalMuscles = Разрезать мышцы живота
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepRetractAbdominalWalls = Втягнуть брюшевые стенки
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepSutureMuscles = Сшыть мышцы живота
.desc = { ent-SurgeryStepBase.desc }
ent-SurgeryStepRestoreAbdominalWalls = Восстановить брюшные стенки
.desc = { ent-SurgeryStepBase.desc }

View file

@ -15,4 +15,4 @@ ent-ActionVampireBatform = Форма летучей мыши
ent-ActionVampireMouseform = Мышиная форма
.desc = Примите облик мыши. Быстрая, маленькая, невосприимчива к дверям. Стоимость активации: 20 крови. Время перезарядки: 30 секунд
ent-ActionVampireCloakOfDarkness = Плащ тьмы
.desc = Замаскируйте себя от глаз смертных, делая вас невидимым в неподвижном состоянии. Стоимость активации: 30 крови. Трата: 1 кровь/секунда Время перезарядки: 10 секунд
.desc = Замаскируйте себя от глаз смертных, делая вас невидимым в неподвижном состоянии. Стоимость активации: 30 крови. Трата: 1 кровь/секунда Время перезарядки: 10 секунд

View file

@ -0,0 +1,20 @@
ent-BaseOrganBrain = { "" }
.desc = { "" }
ent-BaseOrganEyes = { "" }
.desc = { "" }
ent-BaseOrganTongue = { "" }
.desc = { "" }
ent-BaseOrganAppendix = { "" }
.desc = { "" }
ent-BaseOrganEars = { "" }
.desc = { "" }
ent-BaseOrganLungs = { "" }
.desc = { "" }
ent-BaseOrganHeart = { "" }
.desc = { "" }
ent-BaseOrganStomach = { "" }
.desc = { "" }
ent-BaseOrganLiver = { "" }
.desc = { "" }
ent-BaseOrganKidneys = { "" }
.desc = { "" }

View file

@ -13,4 +13,4 @@ ent-MechAirTank = воздушный баллон экзокостюма
ent-MechThruster = ускоритель экзокостюма
.desc = Ускоритель, который позволяет экзокостюму безопасно двигаться при отсутствии гравитации.
ent-MechPhasicScanningModule = фазовый сканирующий модуль
.desc = Высокотехнологичный сканирующий модуль, позволяющий прорывать пространство и проходить сквозь твердые объекты.
.desc = Высокотехнологичный сканирующий модуль, позволяющий прорывать пространство и проходить сквозь твердые объекты.

View file

@ -14,8 +14,16 @@ ent-ScalpelLaser = лазерный скальпель
.desc = Скальпель, в котором вместо лезвия используется направленный лазер для разрезания, что обеспечивает более точную работу, а также прижигание при разрезе.
ent-Retractor = ретрактор
.desc = Хирургический инструмент, используемый для фиксации открытых разрезов.
ent-RetractorAdvanced = улучшенный ретрактор
.desc = { ent-Retractor.desc }
ent-Hemostat = гемостат
.desc = Хирургический инструмент, используемый для сжатия кровеносных сосудов с целью предотвращения кровотечения.
ent-HemostatAdvanced = улучшенный гемостат
.desc = { ent-Hemostat.desc }
ent-BoneSetter = костоправ
.desc = Хирургический инструмент, используемый для вправления костей.
ent-BoneSetterAdvanced = улучшенный костоправ
.desc = Хирургический инструмент, используемый для вправления костей. Кроме этого, он отлично разбивает их.
ent-Saw = пила по металлу
.desc = Для распиливания дерева и других предметов на куски. Или для распиливания костей, в случае крайней необходимости.
ent-SawImprov = чоппа
@ -24,3 +32,5 @@ ent-SawElectric = дисковая пила
.desc = Для интенсивной резки.
ent-SawAdvanced = улучшенная циркулярная пила
.desc = Вы уверены, что с её помощью сможете разрезать всё, что угодно.
ent-BoneGel = бутылочка костного геля
.desc = Контейнер для костного геля, который часто необходимо пополнять из специализированного аппарата.

View file

@ -1,4 +1,4 @@
ent-VampireSurviveObjective = Выжить
.desc = Я должен выжить, чего бы этого мне не стоило.
ent-VampireEscapeObjective = Улететь со станции живым и свободным.
.desc = Я должен улететь на эвакуационном шаттле. Свободным.
.desc = Я должен улететь на эвакуационном шаттле. Свободным.

View file

@ -1,56 +1,41 @@
vampires-title = Вампиры
vampire-fangs-extended-examine = Вы видите блеск [color=white]острых клыков[/color].
vampire-fangs-extended = Вы вытягиваете свои клыки
vampire-fangs-retracted = Вы втягиваете свои клыки
vampire-blooddrink-empty = Это тело лишено крови
vampire-blooddrink-rotted = Их тела гниют, а кровь запятнана
vampire-blooddrink-zombie = Их кровь запятнана смертью
vampire-startlight-burning = Вы чувствуете, как ваша кожа горит в свете тысячи солнц
vampire-not-enough-blood = У вас недостаточно крови
vampire-cuffed = Вам нужны свободные руки!
vampire-stunned = Вы не сможете сосредоточиться!
vampire-muffled = Ваш рот закрыт намордником
vampire-full-stomach = Вас раздуло от крови
vampire-deathsembrace-bind = Чувствуешь себя как дома
vampire-ingest-holyblood = Ваш рот горит!
vampire-cloak-enable = Вы окутываете тенью свою форму
vampire-cloak-disable = Вы ослабляете хватку теней.
vampire-bloodsteal-other = Вы чувствуете, как кровь вырывается из вашего тела!
vampire-hypnotise-other = {CAPITALIZE(THE($user))} пристально вглядывается в {THE($target)} глаза!
vampire-unnaturalstrength = Верхние мышцы {CAPITALIZE(THE($user))} увеличиваються делая его сильнее!
vampire-supernaturalstrength = Верхние мышцы {CAPITALIZE(THE($user))} набухают от мощи делая его сверхсильным!
vampire-hypnotise-other = { CAPITALIZE(THE($user)) } пристально вглядывается в { THE($target) } глаза!
vampire-unnaturalstrength = Верхние мышцы { CAPITALIZE(THE($user)) } увеличиваються делая его сильнее!
vampire-supernaturalstrength = Верхние мышцы { CAPITALIZE(THE($user)) } набухают от мощи делая его сверхсильным!
store-currency-display-blood-essence = Кровавая эссенция
store-category-vampirepowers = Силы
store-category-vampirepassives = Пассивные
#Powers
#Passives
vampire-passive-unholystrength = Нечестивая сила
vampire-passive-unholystrength-description = Наполните мышцы верхней части тела кровью, наделяя вас когтями и повышенной силой. Эффект: 10 порезов за удар
vampire-passive-supernaturalstrength = Сверхъестественная сила
vampire-passive-supernaturalstrength-description = Увеличьте силу мышц верхней части тела, и ни одна преграда не встанет на вашем пути. Эффект: 15 порезов за удар, возможность открывать двери руками.
vampire-passive-deathsembrace = Объятия смерти
vampire-passive-deathsembrace-description = Примите смерть, и она обойдет вас стороной. Эффект: исцеление в гробу, автоматическое возвращение в гроб после смерти за 100 эссенции крови.
#Mutation menu
vampire-mutation-menu-ui-window-name = Меню мутаций
vampire-mutation-none-info = Ничего не выбрано
vampire-mutation-hemomancer-info =
vampire-mutation-hemomancer-info =
Гемомансер
Фокусируеться на кровавой магии и манипуляции крови вокруг себя.
@ -59,8 +44,7 @@ vampire-mutation-hemomancer-info =
- Визг
- Кража крови
vampire-mutation-umbrae-info =
vampire-mutation-umbrae-info =
Тень
Фокусируется на темноте, стелсе, мобильности.
@ -69,8 +53,7 @@ vampire-mutation-umbrae-info =
- Блик
- Плащ тьмы
vampire-mutation-gargantua-info =
vampire-mutation-gargantua-info =
Гаргантюа
Фокусируется на ближнем уроне и стойкости.
@ -79,8 +62,7 @@ vampire-mutation-gargantua-info =
- Нечестивая сила
- Сверхъестественная сила
vampire-mutation-bestia-info =
vampire-mutation-bestia-info =
Бестия
Фокусируется на превращении и собирании трофеев
@ -89,8 +71,8 @@ vampire-mutation-bestia-info =
- Форма летучей мыши
- Мышиная форма
## Objectives
objective-condition-drain-title = Выпить { $count } крови.
objective-condition-drain-description = Я должен выпить { $count } крови. Это необходимо для моего выживания и дальнейшей эволюции.
objective-condition-drain-description = Я должен выпить { $count } крови. Это необходимо для моего выживания и дальнейшей эволюции.

View file

@ -0,0 +1,2 @@
categories-surgeries = операции
categories-surgery-steps = этапы операции

View file

@ -57,4 +57,4 @@ step-uranium-glass-shard-name = осколок уранового стекла
step-exosuit-air-tank-name = воздушный баллон экзокостюма
step-exosuit-thruster-name = ускоритель экзокостюма
step-ripley-peripherals-control-module-name = модуль управления периферией Рипли
step-ripley-central-control-module-name = центральный модуль управления Рипли
step-ripley-central-control-module-name = центральный модуль управления Рипли

View file

@ -1,4 +1,4 @@
expcollar-examine-armed = Боевой режим [color=red]включен[/color]
expcollar-examine-armed = Боевой режим [color=red]включен[/color]
expcoller-examine-disarmed = Боевой режим [color=green]выключен[/color]
expcollar-examine-virgin = [color=yellow]Ошейник ни разу не использовался до этого[/color]
expcollar-examine-unvirgin = [color=red]Ошейник уже был использован до этого и потерял свою эффективность[/color]

View file

@ -1,4 +1,4 @@
uplink-exp-collars-kit-name = Набор с взрывными ошейниками
uplink-exp-collars-kit-name = Набор с взрывными ошейниками
uplink-exp-collars-kit-desc = Набор включает два взрывных ошейника и свадебный букет. Если соединить ошейники, то при смерти носителя красного ошейника они оба взорвутся.
uplink-bouquet-name = Букет
uplink-bouquet-desc = Красивый набор цветов, соответствующий последним стандартам Синдиката.

View file

@ -1,4 +1,4 @@
expcollar-connect = Ошейники связываются!
expcollar-connect = Ошейники связываются!
expcollar-connected = Ошейник уже привязан.
expcollar-bolts-down = Болты ошейника отключаются!
expcollar-kill = Зафиксирована смерть хоста!

View file

@ -43,4 +43,4 @@ ent-MagazineFamasExtended = расширенный магазин Famas
ent-MagazineV31Extended = расширенный магазин V31
ent-MagazineBauer127Extended = расширенный магазин Bauer127
ent-MagazineBR64Extended = расширенный магазин BR64
ent-MagazineDragunovExtended = расширенный магазин Dragunov
ent-MagazineDragunovExtended = расширенный магазин Dragunov

View file

@ -1,3 +1,5 @@
lathe-category-surgery = Хирургия
lathe-category-cyberlimbs = Конечности
lathe-category-reports = Отчёты
lathe-category-statements = Заключения
lathe-category-inquiries-and-appeals = Запросы и обращения

View file

@ -2,7 +2,6 @@ background-type-Parallax = Паралакс
background-type-Animation = Анимация
background-type-Art = Арт
background-type-Random = Случайный
# Animations
lobby-animation-Random = Случайный
lobby-animation-AyakaAduare = Ayaka by Aduare
@ -50,7 +49,6 @@ lobby-animation-SpaceStarts = Space Starts
lobby-animation-SunnyCity = Sunny City
lobby-animation-SunnyPtl = Sunny PTL
lobby-animation-SunnyPtl2 = Sunny PTL 2
# Arts
lobby-art-Random = Случайный
lobby-art-BarLife = Bar Life
@ -106,7 +104,6 @@ lobby-art-Behonker = Behonker
lobby-art-TerminalStation = Terminal Station
lobby-art-JustAWeekAway = Just A Week Away
lobby-art-JaniShootout = Jani Shootout
# Parallaxes
lobby-parallax-Random = Случайный
lobby-parallax-FastSpace = Fast Space

View file

@ -1,38 +1,26 @@
alerts-mood-insane-name = Воодушевлён
alerts-mood-insane-name = Воодушевлён
alerts-mood-insane-desc = Я полон энергии и энтузиазма, готов горы свернуть!
alerts-mood-horrible-name = Подавлен
alerts-mood-horrible-desc = Сейчас тяжело, но это временно. Нужно собраться с силами.
alerts-mood-terrible-name = Расстроен
alerts-mood-terrible-desc = День выдался не из лёгких. Стоит немного отдохнуть.
alerts-mood-bad-name = Не в духе
alerts-mood-bad-desc = Что-то сегодня всё не так. Может, стоит сменить обстановку?
alerts-mood-meh-name = Так себе
alerts-mood-meh-desc = Ничего особенного. Обычный будний день.
alerts-mood-neutral-name = Спокоен
alerts-mood-neutral-desc = Всё идёт своим чередом, без особых взлётов и падений.
alerts-mood-good-name = Доволен
alerts-mood-good-desc = Дела идут неплохо, и это радует!
alerts-mood-great-name = Воодушевлён
alerts-mood-great-desc = Сегодня определённо удачный день! Хочется сделать что-то хорошее.
alerts-mood-exceptional-name = Вдохновлён
alerts-mood-exceptional-desc = Чувствую прилив сил и энергии. Горы готов свернуть!
alerts-mood-perfect-name = На высоте
alerts-mood-perfect-desc = Потрясающее настроение! Кажется, сегодня всё по плечу!
alerts-mood-dead-name = Мертв
alerts-mood-dead-desc = ...
mood-show-effects-start = [font size=12]Текущее настроение:[/font]
mood-effect-HungerOverfed = Я так наелся, что вот-вот лопну!
mood-effect-HungerOkay = Чувствую себя сытым.
mood-effect-HungerPeckish = Не отказался бы от перекуса.

View file

@ -1,3 +1,7 @@
research-discipline-biochemical = Биохимия
research-technology-basic-surgery = Базовая хирургия
research-technology-basic-cyberlimbs = Базовые кибер-конечности
research-technology-advanced-surgery = Продвинутая хирургия
research-technology-handcraft-nvd = Кустарные ПНВ
research-technology-basic-nvd = Продвинутое ПНВ
research-technology-combat-equipment = Боевое снаряжение

View file

@ -0,0 +1,9 @@
surgery-window-name = Хирургия
surgery-window-partsbutton-name = Части тела
surgery-window-surgeriesbutton-name = Операции
surgery-window-stepsbutton-name = Этапы
surgery-window-reguires = [bold]Требует: { $surgeryname }[/bold]
surgery-window-reguires-table = [color=red](Требует операционный стол)[/color]
surgery-window-reguires-undress = [color=red](Снимите с него броню!)[/color]
surgery-window-reguires-tool = [color=red](Отсутствует инструмент)[/color]
surgery-window-reguires-laydown = [color=red][font size=16]Он должен лежать![/font][/color]

View file

@ -16,4 +16,4 @@ admin-verb-text-make-pirate = Сделать пиратом
admin-verb-text-make-head-rev = Сделать Главой революции
admin-verb-text-make-thief = Сделать вором
admin-verb-text-make-changeling = Сделать генокрадом
admin-verb-text-make-vampire = Сделать вампиром
admin-verb-text-make-vampire = Сделать вампиром

View file

@ -22,4 +22,4 @@ hypospray-cant-inject = Нельзя сделать инъекцию в { $targe
## failure
hypospay-component-failure-hardsuit = Вы не сможете провести иглу через толстое покрытие!
hypospay-component-failure-hardsuit = Вы не сможете провести иглу через толстое покрытие!

View file

@ -31,4 +31,4 @@ injector-component-injecting-target = { CAPITALIZE($user) } начинает в
## failure
injector-component-failure-hardsuit = Вы не сможете провести иглу через толстое покрытие!
injector-component-failure-hardsuit = Вы не сможете провести иглу через толстое покрытие!

View file

@ -9,4 +9,4 @@ vampire-role-greeting =
Ваши задачи указаны в меню персонажа.
Пейте кровь и эволюционируйте, чтобы выполнить их!
vampire-role-greeting-short = Вы вампир, который пробрался на станцию под видом работника!
roles-antag-vamire-name = Вампир
roles-antag-vamire-name = Вампир

View file

@ -9,4 +9,4 @@ metabolizer-type-plant = Растение
metabolizer-type-dwarf = Дварф
metabolizer-type-moth = Ниан
metabolizer-type-arachnid = Арахнид
metabolizer-type-vampire = Вампир
metabolizer-type-vampire = Вампир

View file

@ -96,4 +96,4 @@ reagent-physical-desc-slimy = склизкое
reagent-physical-desc-neural = нейронное
reagent-physical-desc-vile = мерзкое
reagent-physical-desc-celliminol = сильно отдающее кровью
reagent-physical-desc-h-32 = сильно пахнущее радиоактивными ожогами
reagent-physical-desc-h-32 = сильно пахнущее радиоактивными ожогами

View file

@ -27,7 +27,7 @@
- type: entity
id: OrganArachnidStomach
parent: OrganAnimalStomach
parent: [OrganAnimalStomach, BaseOrganStomach]
name: stomach
description: "Gross. This is hard to stomach."
components:
@ -53,7 +53,7 @@
- type: entity
id: OrganArachnidLungs
parent: BaseArachnidOrgan
parent: [BaseArachnidOrgan, BaseOrganLungs]
name: lungs
description: "Filters oxygen from an atmosphere... just more greedily."
components:
@ -88,7 +88,7 @@
- type: entity
id: OrganArachnidHeart
parent: BaseArachnidOrgan
parent: [BaseArachnidOrgan, BaseOrganHeart]
name: heart
description: "A disgustingly persistent little biological pump made for spiders."
components:
@ -108,7 +108,7 @@
- type: entity
id: OrganArachnidLiver
parent: BaseHumanOrgan
parent: [BaseHumanOrgan, BaseOrganLiver]
name: liver
description: "Pairing suggestion: chianti and fava beans."
categories: [ HideSpawnMenu ]
@ -128,7 +128,7 @@
- type: entity
id: OrganArachnidKidneys
parent: BaseHumanOrgan
parent: [BaseHumanOrgan, BaseOrganKidneys]
name: kidneys
description: "Filters toxins from the bloodstream."
categories: [ HideSpawnMenu ]
@ -149,7 +149,7 @@
- type: entity
id: OrganArachnidEyes
parent: BaseArachnidOrgan
parent: [BaseArachnidOrgan, BaseOrganEyes]
name: eyes
description: "Two was already too many."
components:
@ -163,7 +163,7 @@
- type: entity
id: OrganArachnidTongue
parent: BaseArachnidOrgan
parent: [BaseArachnidOrgan, BaseOrganTongue]
name: tongue
description: "A fleshy muscle mostly used for lying."
components:

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