surgerypatch

This commit is contained in:
Zekins3366
2026-06-07 20:46:45 +03:00
parent e4bc4b15a4
commit 2147ba8eb9
16 changed files with 612 additions and 135 deletions
@@ -2,15 +2,12 @@ using Content.Shared.Surgery;
using Content.Shared.Surgery.Components;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
using Robust.Shared.Player;
namespace Content.Client._Wega.Surgery.Ui;
[UsedImplicitly]
public sealed partial class SurgeryBoundUserInterface : BoundUserInterface
{
[Dependency] private ISharedPlayerManager _playerManager = default!;
[ViewVariables]
private SurgeryWindow? _window;
@@ -24,19 +21,22 @@ public sealed partial class SurgeryBoundUserInterface : BoundUserInterface
_window.OnStepPressed += (targetNode, stepIndex, isParallel) =>
{
var netEntity = EntMan.GetNetEntity(_playerManager.LocalSession?.AttachedEntity ?? EntityUid.Invalid);
SendMessage(new SurgeryStartMessage(netEntity, targetNode, stepIndex, isParallel));
SendMessage(new SurgeryStartMessage(targetNode, stepIndex, isParallel));
};
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (EntMan.TryGetComponent(Owner, out OperatedComponent? comp)
&& state is SurgeryProcedureDtoState procedureState)
_window?.UpdateState(procedureState, comp);
}
protected override void ReceiveMessage(BoundUserInterfaceMessage message)
{
if (_window == null || message is not SurgeryProcedureDto msg)
return;
if (EntMan.TryGetComponent(Owner, out OperatedComponent? comp))
{
_window.UpdateState(msg, comp);
}
if (message is SurgerySterilityUpdateMessage msg)
_window?.UpdateSterilityToolTip(msg.SterilityInfo);
}
}
@@ -11,13 +11,14 @@ using Robust.Shared.Prototypes;
using System.Numerics;
using Robust.Client.UserInterface;
using Content.Shared.Tools;
using Content.Shared.Guidebook;
namespace Content.Client._Wega.Surgery.Ui;
[GenerateTypedNameReferences]
public sealed partial class SurgeryWindow : FancyWindow
{
private readonly IPrototypeManager _prototypeManager;
[Dependency] private IPrototypeManager _prototype = default!;
private readonly StyleBoxFlat _groupButtonStyle = new()
{
@@ -49,7 +50,7 @@ public sealed partial class SurgeryWindow : FancyWindow
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_prototypeManager = IoCManager.Resolve<IPrototypeManager>();
HelpGuidebookIds = new List<ProtoId<GuideEntryPrototype>> { "Surgery" };
}
protected override void FrameUpdate(FrameEventArgs args)
@@ -64,7 +65,7 @@ public sealed partial class SurgeryWindow : FancyWindow
}
}
public void UpdateState(SurgeryProcedureDto state, OperatedComponent comp)
public void UpdateState(SurgeryProcedureDtoState state, OperatedComponent comp)
{
GroupListContainer.RemoveAllChildren();
ProtoId<SurgeryNodePrototype>? currentTargetNode = comp.CurrentTargetNode;
@@ -114,6 +115,30 @@ public sealed partial class SurgeryWindow : FancyWindow
}
}
}
UpdateSterilityToolTip(state.SterilityInfo);
}
public void UpdateSterilityToolTip(SurgerySterilityInfo info)
{
var percent = (int)(info.Sterility * 100);
var tooltipText = Loc.GetString("surgery-sterility-percent", ("percent", percent)) + "\n\n";
if (info.NegativeFactors.Count > 0)
{
tooltipText += Loc.GetString("surgery-sterility-negative-header") + "\n";
foreach (var factor in info.NegativeFactors)
tooltipText += $"• {factor}\n";
}
if (info.PositiveFactors.Count > 0)
{
tooltipText += Loc.GetString("surgery-sterility-positive-header") + "\n";
foreach (var factor in info.PositiveFactors)
tooltipText += $"• {factor}\n";
}
HelpButton.ToolTip = tooltipText;
}
private void SelectGroup(Button button)
@@ -212,8 +237,8 @@ public sealed partial class SurgeryWindow : FancyWindow
Margin = new Thickness(4)
};
if (_prototypeManager.TryIndex<ToolQualityPrototype>(toolQualityId, out var toolQuality) &&
_prototypeManager.TryIndex<EntityPrototype>(toolQuality.Spawn, out var toolProto))
if (_prototype.TryIndex<ToolQualityPrototype>(toolQualityId, out var toolQuality) &&
_prototype.TryIndex<EntityPrototype>(toolQuality.Spawn, out var toolProto))
{
var icon = new EntityPrototypeView
{
@@ -242,7 +267,7 @@ public sealed partial class SurgeryWindow : FancyWindow
Margin = new Thickness(4)
};
if (_prototypeManager.TryIndex<EntityPrototype>(entityPreview, out var entity))
if (_prototype.TryIndex<EntityPrototype>(entityPreview, out var entity))
{
var icon = new EntityPrototypeView
{
@@ -266,8 +291,8 @@ public sealed partial class SurgeryWindow : FancyWindow
{
var tooltip = "";
var tool = step.RequiredTool;
if (!string.IsNullOrEmpty(tool) && _prototypeManager.TryIndex<ToolQualityPrototype>(tool, out var toolQuality) &&
_prototypeManager.TryIndex<EntityPrototype>(toolQuality.Spawn, out var toolProto))
if (!string.IsNullOrEmpty(tool) && _prototype.TryIndex<ToolQualityPrototype>(tool, out var toolQuality) &&
_prototype.TryIndex<EntityPrototype>(toolQuality.Spawn, out var toolProto))
tool = toolProto.Name;
if (!string.IsNullOrEmpty(tool))
@@ -277,4 +302,4 @@ public sealed partial class SurgeryWindow : FancyWindow
("condition", Loc.GetString($"surgery-condition-required-{step.RequiredCondition.ToLower()}")));
return tooltip.Trim();
}
}
}
@@ -17,14 +17,14 @@ using Robust.Server.Containers;
using Robust.Shared.Map;
// Corvax-Wega-Add-start
using Content.Shared.Item;
using Content.Shared.Surgery.Components;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry;
using Robust.Shared.Prototypes;
using Content.Shared.FixedPoint;
using Content.Server.Chemistry.Components;
using Content.Shared.Vapor;
using Content.Server.Surgery; // Corvax-Wega-Surgery
// Corvax-Wega-Add-end
namespace Content.Server.Fluids.EntitySystems;
@@ -44,6 +44,7 @@ public sealed partial class SpraySystem : SharedSpraySystem
[Dependency] private SharedAppearanceSystem _appearance = default!; // Corvax-Wega-Add
[Dependency] private IPrototypeManager _proto = default!; // Corvax-Wega-Add
[Dependency] private ReactiveSystem _reactive = default!; // Corvax-Wega-Add
[Dependency] private SterileSystem _sterile = default!; // Corvax-Wega-Surgery
private float _gridImpulseMultiplier;
@@ -88,11 +89,10 @@ public sealed partial class SpraySystem : SharedSpraySystem
// Corvax-Wega-Add-end
// Corvax-Wega-Surgery-start
if (args.Target != null && HasComp<ItemComponent>(args.Target)
&& _solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out _, out var solution)
&& solution.GetTotalPrototypeQuantity("Ethanol") >= FixedPoint2.New(5))
if (args.Target != null && HasComp<ItemComponent>(args.Target) && _solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out _, out var solution)
&& Transform(entity).ParentUid != Transform(args.Target.Value).ParentUid) // Spray can't hand interact
{
EnsureComp<SterileComponent>(args.Target.Value);
_sterile.ApplySterilityFromSolution(args.Target.Value, solution, entity.Comp.TransferAmount.Float());
}
// Corvax-Wega-Surgery-end
@@ -0,0 +1,125 @@
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Examine;
using Content.Shared.Item;
using Content.Shared.Surgery.Components;
using Content.Shared.Throwing;
using Robust.Shared.Prototypes;
namespace Content.Server.Surgery;
public sealed partial class SterileSystem : EntitySystem
{
[Dependency] private IPrototypeManager _proto = default!;
private static readonly ProtoId<ReagentPrototype> EthanolReagent = "Ethanol";
private const float EthanolUnitsPerSterilePoint = 0.05f;
private const float MaxSterileAmount = 100f;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SterileComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<SterileComponent, ThrownEvent>(OnThrow);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var sterileQuery = EntityQueryEnumerator<SterileComponent>();
while (sterileQuery.MoveNext(out var uid, out var sterile))
{
if (sterile.AlwaysSterile)
continue;
if (sterile.NextUpdateTick <= 0)
{
sterile.NextUpdateTick = 5f;
sterile.Amount -= sterile.DecayRate;
if (sterile.Amount <= 0)
{
RemComp<SterileComponent>(uid);
}
}
sterile.NextUpdateTick -= frameTime;
}
}
private void OnExamined(Entity<SterileComponent> entity, ref ExaminedEvent args)
{
if (args.IsInDetailsRange)
args.AddMarkup(Loc.GetString("surgery-sterile-examined") + "\n");
}
private void OnThrow(Entity<SterileComponent> entity, ref ThrownEvent args)
=> RemCompDeferred<SterileComponent>(entity);
public float ApplySterilityFromSolution(EntityUid target, Solution solution, float transferAmount)
{
if (!HasComp<ItemComponent>(target))
return 0f;
var totalEthanol = GetTotalEthanolEquivalent(solution, transferAmount);
if (totalEthanol <= 0)
return 0f;
var sterileAmount = totalEthanol / EthanolUnitsPerSterilePoint;
sterileAmount = Math.Min(sterileAmount, MaxSterileAmount);
var sterileComp = EnsureComp<SterileComponent>(target);
sterileComp.Amount = Math.Min(sterileComp.Amount + sterileAmount, MaxSterileAmount);
sterileComp.NextUpdateTick = 0f;
return sterileAmount;
}
private float GetTotalEthanolEquivalent(Solution solution, float transferAmount)
{
if (transferAmount <= 0)
return 0f;
var totalSolutionVolume = solution.Volume.Float();
if (totalSolutionVolume <= 0)
return 0f;
var ratio = transferAmount / totalSolutionVolume;
float total = 0f;
foreach (var reagent in solution.Contents)
{
var reagentProto = _proto.Index<ReagentPrototype>(reagent.Reagent.Prototype);
float ethanolPerUnit = 0f;
if (reagent.Reagent.Prototype == EthanolReagent)
{
ethanolPerUnit = 1f;
}
else
{
if (reagentProto.Metabolisms?.Metabolisms.TryGetValue("Digestion", out var metabolism) == true
&& metabolism.Metabolites != null)
{
foreach (var metabolite in metabolism.Metabolites)
{
if (metabolite.Key == EthanolReagent)
{
ethanolPerUnit = metabolite.Value.Float();
break;
}
}
}
}
if (ethanolPerUnit > 0f)
{
var transferredReagentAmount = reagent.Quantity.Float() * ratio;
total += transferredReagentAmount * ethanolPerUnit;
}
}
return total;
}
}
@@ -37,7 +37,7 @@ public sealed partial class SurgerySystem
/// <param name="args">The SurgeryStartMessage containing details about the surgery start request.</param>
private void OnSurgeryStart(EntityUid uid, OperatedComponent comp, SurgeryStartMessage args)
{
var user = GetEntity(args.User);
var user = args.Actor;
if (comp.GraphId == null)
return;
@@ -162,7 +162,7 @@ public sealed partial class SurgerySystem
}
CheckTransitionProgress(uid, comp, graph, transition);
UpdateUi(uid, comp, graph);
UpdateUi(uid, args.User, comp, graph);
}
#region Handle Steps
@@ -388,12 +388,11 @@ public sealed partial class SurgerySystem
}
bool hasTool = step.Tool != null && step.Tool.Count != 0;
bool toolValid = step.Tool == null || step.Tool.Count == 0 || step.Action == SurgeryActionType.StoreItem
|| step.Tool.Any(tool => _tool.HasQuality(item.Value, tool));
bool tagValid = step.Tag == null || step.Tag.Count == 0 || step.Action == SurgeryActionType.StoreItem
|| step.Tag.Any(tag => _tag.HasTag(item.Value, tag));
bool hasTag = step.Tag != null && step.Tag.Count != 0;
bool toolValid = !hasTool || step.Action == SurgeryActionType.StoreItem || step.Tool!.Any(tool => _tool.HasQuality(item.Value, tool));
bool tagValid = !hasTag || step.Action == SurgeryActionType.StoreItem || step.Tag!.Any(tag => _tag.HasTag(item.Value, tag));
if (!toolValid || !hasTool && !tagValid)
if (hasTool && !toolValid && hasTag && !tagValid)
{
_popup.PopupEntity(Loc.GetString("surgery-missing-tool"), user, user);
return;
@@ -453,7 +452,6 @@ public sealed partial class SurgerySystem
comp.ResetOperationState(transition.Target);
comp.CurrentNode = transition.Target;
Dirty(uid, comp);
UpdateUi(uid, comp, graph);
}
}
@@ -12,6 +12,7 @@ using Content.Shared.FixedPoint;
using Content.Shared.IdentityManagement;
using Content.Shared.Implants;
using Content.Shared.Implants.Components;
using Content.Shared.Movement.Systems;
using Content.Shared.Popups;
using Content.Shared.Random.Helpers;
using Content.Shared.Silicons.Borgs.Components;
@@ -127,9 +128,11 @@ public sealed partial class SurgerySystem
break;
// Synthetic End
default: break;
default: return;
}
ApplySterilityConsequences((patient, comp));
// Any action without anesthesia will cause pain.
if (!HasComp<SleepingComponent>(patient) && !HasComp<PainNumbnessStatusEffectComponent>(patient) && !comp.OperatedPart
&& !_mobState.IsDead(patient) && !HasComp<SyntheticOperatedComponent>(patient))
@@ -175,10 +178,7 @@ public sealed partial class SurgerySystem
return;
if (!RollSuccess(patient, patient.Comp.Surgeon.Value, successChance))
{
HandleFailure(patient, failureEffect);
return;
}
if (!HasComp<BloodstreamComponent>(patient) || HasComp<SyntheticOperatedComponent>(patient))
return;
@@ -459,19 +459,71 @@ public sealed partial class SurgerySystem
private bool RollSuccess(Entity<OperatedComponent> ent, EntityUid surgeon, float baseChance)
{
var item = _hands.GetActiveItemOrSelf(surgeon);
if (HasComp<SurgicalSkillComponent>(surgeon) && ent.Comp.Sterility == 1f && HasComp<SterileComponent>(item)
&& SurgeryTools.Any(tool => _tool.HasQuality(item, tool))
|| Organs.Any(tag => _tag.HasTag(item, tag))
|| Parts.Any(tag => _tag.HasTag(item, tag)))
if (HasComp<SurgicalSkillComponent>(surgeon) && SurgeryTools.Any(tool => _tool.HasQuality(item, tool))
|| Organs.Any(tag => _tag.HasTag(item, tag)) || Parts.Any(tag => _tag.HasTag(item, tag)))
{
return true;
}
var adjustedChance = baseChance * Math.Clamp(ent.Comp.Sterility, 0f, 1.5f);
if (TryGetOperatingTable(ent, out var tableModifier))
adjustedChance *= tableModifier;
baseChance *= tableModifier;
return _random.Prob(adjustedChance);
return _random.Prob(baseChance);
}
private void ApplySterilityConsequences(Entity<OperatedComponent> patient)
{
if (patient.Comp.Surgeon == null || HasComp<SyntheticOperatedComponent>(patient))
return;
var sterility = patient.Comp.Sterility;
var item = _hands.GetActiveItemOrSelf(patient.Comp.Surgeon.Value);
if (sterility >= 0.7f && HasComp<SterileComponent>(item))
return;
if (sterility >= 0.5f)
{
var painChance = 0.3f + (0.75f - sterility) / 0.25f * 0.4f;
if (_random.Prob(painChance))
{
if (!HasComp<SleepingComponent>(patient) && !HasComp<PainNumbnessStatusEffectComponent>(patient)
&& !patient.Comp.OperatedPart && !_mobState.IsDead(patient) && !HasComp<SyntheticOperatedComponent>(patient))
_chat.TryEmoteWithoutChat(patient, _proto.Index(Scream), true);
_jittering.DoJitter(patient, TimeSpan.FromSeconds(4), true);
}
var slowChance = 0.15f + (0.75f - sterility) / 0.25f * 0.3f;
if (_random.Prob(slowChance))
{
_movementMod.TryUpdateMovementSpeedModDuration(patient, MovementModStatusSystem.Slowdown,
TimeSpan.FromSeconds(30), 0.85f, 0.85f);
}
return;
}
var painChanceLow = 0.7f + (0.5f - sterility) / 0.3f * 0.2f;
if (_random.Prob(painChanceLow))
{
if (!HasComp<SleepingComponent>(patient) && !HasComp<PainNumbnessStatusEffectComponent>(patient)
&& !patient.Comp.OperatedPart && !_mobState.IsDead(patient) && !HasComp<SyntheticOperatedComponent>(patient))
_chat.TryEmoteWithoutChat(patient, _proto.Index(Scream), true);
_jittering.DoJitter(patient, TimeSpan.FromSeconds(6), true);
}
var slowChanceLow = 0.45f + (0.5f - sterility) / 0.3f * 0.25f;
if (_random.Prob(slowChanceLow))
{
_movementMod.TryUpdateMovementSpeedModDuration(patient, MovementModStatusSystem.Slowdown,
TimeSpan.FromSeconds(45), 0.6f, 0.6f);
}
var sepsisChanceLow = 0.05f + (0.5f - sterility) / 0.3f * 0.05f;
if (_random.Prob(sepsisChanceLow))
{
_disease.TryAddDisease(patient, "SurgicalSepsis");
}
}
public void ApplyBloodToClothing(EntityUid surgeon, string? bloodReagentId, float bloodAmount)
@@ -494,8 +546,7 @@ public sealed partial class SurgerySystem
var effect = _random.Pick(failureEffect);
switch (effect)
{
case SurgeryFailedType.Empty:
return;
case SurgeryFailedType.Empty: return;
case SurgeryFailedType.Cut:
_damage.TryChangeDamage(patient.Owner, new DamageSpecifier { DamageDict = { { SlashDamage, 5 } } }, true);
break;
@@ -509,11 +560,14 @@ public sealed partial class SurgerySystem
TryAddInternalDamage(patient, "BoneFracture", bodyPart: bodyPart);
break;
case SurgeryFailedType.Pain:
if (!HasComp<SleepingComponent>(patient) && !HasComp<PainNumbnessStatusEffectComponent>(patient) && !patient.Comp.OperatedPart && !_mobState.IsDead(patient) && !HasComp<SyntheticOperatedComponent>(patient))
_chat.TryEmoteWithoutChat(patient, _proto.Index(Scream), true);
{
if (!HasComp<SleepingComponent>(patient) && !HasComp<PainNumbnessStatusEffectComponent>(patient) && !patient.Comp.OperatedPart
&& !_mobState.IsDead(patient) && !HasComp<SyntheticOperatedComponent>(patient))
_chat.TryEmoteWithoutChat(patient, _proto.Index(Scream), true);
_jittering.DoJitter(patient, TimeSpan.FromSeconds(5), true);
break;
_jittering.DoJitter(patient, TimeSpan.FromSeconds(5), true);
break;
}
}
if (effect != SurgeryFailedType.Empty && !_mobState.IsDead(patient))
@@ -5,6 +5,7 @@ using Content.Shared.DirtVisuals;
using Content.Shared.Ghost;
using Content.Shared.Shuttles.Components;
using Content.Shared.Silicons.Borgs.Components;
using Content.Shared.Surgery;
using Content.Shared.Surgery.Components;
namespace Content.Server.Surgery;
@@ -13,6 +14,8 @@ public sealed partial class SurgerySystem
{
[Dependency] private EntityLookupSystem _entityLookup = default!;
#region Sterility
private void UpdateOperationSterility(EntityUid patient, OperatedComponent operated)
{
if (operated.Surgeon == null || HasComp<SyntheticOperatedComponent>(patient))
@@ -20,52 +23,49 @@ public sealed partial class SurgerySystem
float sterility = 1f;
// Важные слоты (сильное влияние)
CheckClothingSlot(operated.Surgeon.Value, "gloves", ref sterility, 0.6f, true);
CheckClothingSlot(operated.Surgeon.Value, "mask", ref sterility, 0.6f, true);
// Важные слоты
CheckClothingSlot(operated.Surgeon.Value, "gloves", ref sterility, 0.15f, true);
CheckClothingSlot(operated.Surgeon.Value, "mask", ref sterility, 0.15f, true);
// Средние слоты (умеренное влияние)
CheckClothingSlot(operated.Surgeon.Value, "head", ref sterility, 0.2f);
CheckClothingSlot(operated.Surgeon.Value, "jumpsuit", ref sterility, 0.2f);
CheckClothingSlot(operated.Surgeon.Value, "outerClothing", ref sterility, 0.2f, ingnoreSlot: true);
// Средние слоты
CheckClothingSlot(operated.Surgeon.Value, "head", ref sterility, 0.05f);
CheckClothingSlot(operated.Surgeon.Value, "jumpsuit", ref sterility, 0.05f);
CheckClothingSlot(operated.Surgeon.Value, "outerClothing", ref sterility, 0.05f, ingnoreSlot: true);
// Нежелательные слоты (небольшой дебафф)
CheckClothingSlot(operated.Surgeon.Value, "back", ref sterility, 0.1f, ingnoreSlot: true);
CheckClothingSlot(operated.Surgeon.Value, "belt", ref sterility, 0.1f, ingnoreSlot: true);
// Нежелательные слоты
CheckClothingSlot(operated.Surgeon.Value, "back", ref sterility, 0.02f, ingnoreSlot: true);
CheckClothingSlot(operated.Surgeon.Value, "belt", ref sterility, 0.02f, ingnoreSlot: true);
var garbageCount = _entityLookup.GetEntitiesInRange<SpaceGarbageComponent>(
Transform(patient).Coordinates, 2f).Count;
Transform(patient).Coordinates, 1.5f).Count;
sterility *= Math.Max(0.1f, 1f - garbageCount * 0.1f);
sterility *= Math.Max(0.7f, 1f - garbageCount * 0.05f);
var item = _hands.GetActiveItemOrSelf(operated.Surgeon.Value);
if (!HasComp<SterileComponent>(item))
sterility *= 0.4f;
sterility *= 0.85f;
var bystanders = _entityLookup.GetEntitiesInRange<BodyComponent>(
Transform(patient).Coordinates, 2f)
var bystanders = _entityLookup.GetEntitiesInRange<BodyComponent>(Transform(patient).Coordinates, 2f)
.Where(e => e.Owner != patient && e.Owner != operated.Surgeon
&& !_mobState.IsDead(e.Owner) && !HasComp<GhostComponent>(e.Owner))
.Count();
&& !_mobState.IsDead(e.Owner) && !HasComp<GhostComponent>(e.Owner));
float bystanderModifier = bystanders switch
float bystanderModifier = bystanders.Count() switch
{
<= 2 => 1f,
<= 4 => 0.9f,
<= 6 => 0.8f,
_ => 0.7f
<= 4 => 0.97f,
<= 6 => 0.94f,
_ => 0.9f
};
sterility *= bystanderModifier;
var corpses = _entityLookup.GetEntitiesInRange<BodyComponent>(
Transform(patient).Coordinates, 2f)
var corpses = _entityLookup.GetEntitiesInRange<BodyComponent>(Transform(patient).Coordinates, 2f)
.Where(e => e.Owner != patient && e.Owner != operated.Surgeon
&& _mobState.IsDead(e.Owner) && !HasComp<GhostComponent>(e.Owner))
.Count();
&& _mobState.IsDead(e.Owner) && !HasComp<GhostComponent>(e.Owner));
sterility *= 1f - corpses * 0.05f;
sterility *= 1f - corpses.Count() * 0.03f;
operated.Sterility = Math.Clamp(sterility, 0f, 1f);
operated.Sterility = Math.Clamp(sterility, 0.2f, 1f);
SendSterilityUpdateToUi(patient, operated.Surgeon.Value);
}
private void CheckClothingSlot(EntityUid surgeon, string slot, ref float sterility, float penaltyModifier,
@@ -90,20 +90,156 @@ public sealed partial class SurgerySystem
if (TryComp<ClothingSterilityComponent>(clothing, out var sterilityComp) && !isMaskOff)
{
sterility *= sterilityComp.Modifier * (isDirty ? 0.8f : 1f);
sterility *= sterilityComp.Modifier * (isDirty ? 0.95f : 1f);
}
else
{
sterility *= (1f - penaltyModifier) * (isDirty ? 0.9f : 1f);
sterility *= (1f - penaltyModifier) * (isDirty ? 0.98f : 1f);
}
}
else if (isCritical)
{
sterility *= 0.5f;
sterility *= 0.85f;
}
else if (!ingnoreSlot)
{
sterility *= 1f - penaltyModifier;
sterility *= 1f - penaltyModifier * 0.5f;
}
}
#endregion
#region UI Info
private SurgerySterilityInfo GetSterilityInfo(EntityUid patient, EntityUid surgeon)
{
if (HasComp<SyntheticOperatedComponent>(patient))
return new SurgerySterilityInfo(1f, new List<string>(), new List<string>());
float sterility = 1f;
var negativeFactors = new List<string>();
var positiveFactors = new List<string>();
// Важные слоты
CheckClothingSlotWithFactors(surgeon, "gloves", ref sterility, 0.15f, true, negativeFactors, positiveFactors);
CheckClothingSlotWithFactors(surgeon, "mask", ref sterility, 0.15f, true, negativeFactors, positiveFactors);
// Средние слоты
CheckClothingSlotWithFactors(surgeon, "head", ref sterility, 0.05f, false, negativeFactors, positiveFactors);
CheckClothingSlotWithFactors(surgeon, "jumpsuit", ref sterility, 0.05f, false, negativeFactors, positiveFactors);
CheckClothingSlotWithFactors(surgeon, "outerClothing", ref sterility, 0.05f, true, negativeFactors, positiveFactors);
// Нежелательные слоты
CheckClothingSlotWithFactors(surgeon, "back", ref sterility, 0.02f, true, negativeFactors, positiveFactors);
CheckClothingSlotWithFactors(surgeon, "belt", ref sterility, 0.02f, true, negativeFactors, positiveFactors);
var garbageCount = _entityLookup.GetEntitiesInRange<SpaceGarbageComponent>(
Transform(patient).Coordinates, 1.5f).Count;
if (garbageCount > 0)
{
var garbageModifier = Math.Max(0.7f, 1f - garbageCount * 0.05f);
sterility *= garbageModifier;
negativeFactors.Add(Loc.GetString("surgery-sterility-garbage", ("count", garbageCount)));
}
var item = _hands.GetActiveItemOrSelf(surgeon);
if (!HasComp<SterileComponent>(item))
{
sterility *= 0.85f;
negativeFactors.Add(Loc.GetString("surgery-sterility-non-sterile-tool"));
}
else
{
positiveFactors.Add(Loc.GetString("surgery-sterility-sterile-tool"));
}
var bystanders = _entityLookup.GetEntitiesInRange<BodyComponent>(Transform(patient).Coordinates, 2f)
.Where(e => e.Owner != patient && e.Owner != surgeon
&& !_mobState.IsDead(e.Owner) && !HasComp<GhostComponent>(e.Owner));
int bystanderCount = bystanders.Count();
if (bystanderCount > 2)
{
float bystanderModifier = bystanderCount switch
{
<= 4 => 0.97f,
<= 6 => 0.94f,
_ => 0.9f
};
sterility *= bystanderModifier;
negativeFactors.Add(Loc.GetString("surgery-sterility-bystanders", ("count", bystanderCount)));
}
var corpses = _entityLookup.GetEntitiesInRange<BodyComponent>(Transform(patient).Coordinates, 2f)
.Where(e => e.Owner != patient && e.Owner != surgeon
&& _mobState.IsDead(e.Owner) && !HasComp<GhostComponent>(e.Owner));
int corpseCount = corpses.Count();
if (corpseCount > 0)
{
sterility *= 1f - corpseCount * 0.03f;
negativeFactors.Add(Loc.GetString("surgery-sterility-corpses", ("count", corpseCount)));
}
if (TryGetOperatingTable(patient, out _))
{
positiveFactors.Add(Loc.GetString("surgery-sterility-operating-table"));
}
sterility = Math.Clamp(sterility, 0.2f, 1f);
return new SurgerySterilityInfo(sterility, negativeFactors, positiveFactors);
}
private void CheckClothingSlotWithFactors(EntityUid surgeon, string slot, ref float sterility, float penaltyModifier,
bool ignoreSlot, List<string> negativeFactors, List<string> positiveFactors)
{
if (HasComp<BorgChassisComponent>(surgeon))
return;
if (_inventory.TryGetSlotEntity(surgeon, slot, out var clothing))
{
bool isMaskOff = false;
if (TryComp(clothing, out MaskComponent? mask))
isMaskOff = mask.IsToggled;
bool isDirty = false;
if (TryComp<DirtableComponent>(clothing, out var dirtable))
{
var dirtLevel = Math.Clamp(dirtable.CurrentDirtLevel.Float() / SharedDirtSystem.MaxDirtLevel * 100f, 0f, 100f);
if (dirtable.IsDirty && dirtLevel >= 50f)
isDirty = true;
}
if (TryComp<ClothingSterilityComponent>(clothing, out var sterilityComp) && !isMaskOff)
{
var modifier = sterilityComp.Modifier * (isDirty ? 0.95f : 1f);
sterility *= modifier;
if (modifier > 1f)
positiveFactors.Add(Loc.GetString($"surgery-sterility-sterile-{slot}"));
else if (modifier < 1f)
negativeFactors.Add(Loc.GetString($"surgery-sterility-non-sterile-{slot}"));
}
else
{
var modifier = (1f - penaltyModifier) * (isDirty ? 0.98f : 1f);
sterility *= modifier;
if (modifier < 1f)
negativeFactors.Add(Loc.GetString($"surgery-sterility-no-sterile-{slot}"));
}
}
else if (slot == "gloves" || slot == "mask")
{
sterility *= 0.85f;
negativeFactors.Add(Loc.GetString($"surgery-sterility-no-{slot}"));
}
else if (!ignoreSlot)
{
var modifier = 1f - penaltyModifier * 0.5f;
sterility *= modifier;
negativeFactors.Add(Loc.GetString($"surgery-sterility-no-{slot}"));
}
}
#endregion
}
@@ -4,7 +4,6 @@ using Content.Shared.Interaction;
using Content.Shared.Kitchen.Components;
using Content.Shared.Surgery;
using Content.Shared.Surgery.Components;
using Robust.Shared.Timing;
namespace Content.Server.Surgery;
@@ -47,10 +46,7 @@ public sealed partial class SurgerySystem
return;
var graph = _proto.Index<SurgeryGraphPrototype>(comp.GraphId);
Timer.Spawn(250, () =>
{
UpdateUi(uid, comp, graph);
});
UpdateUi(uid, args.Actor, comp, graph);
}
private void OnUnbuckled(Entity<OperatedComponent> ent, ref UnbuckledEvent args)
@@ -61,7 +57,7 @@ public sealed partial class SurgerySystem
_ui.CloseUi(ent.Owner, SurgeryUiKey.Key);
}
private void UpdateUi(EntityUid patient, OperatedComponent comp, SurgeryGraphPrototype graph)
private void UpdateUi(EntityUid patient, EntityUid surgeon, OperatedComponent comp, SurgeryGraphPrototype graph)
{
if (!_ui.HasUi(patient, SurgeryUiKey.Key))
return;
@@ -83,7 +79,16 @@ public sealed partial class SurgerySystem
AddNodeSteps(node, patient, comp, groups);
}
_ui.ServerSendUiMessage(patient, SurgeryUiKey.Key,
new SurgeryProcedureDto(groups, GetNetEntity(patient)));
var sterilityInfo = GetSterilityInfo(patient, surgeon);
_ui.SetUiState(patient, SurgeryUiKey.Key, new SurgeryProcedureDtoState(groups, sterilityInfo));
}
private void SendSterilityUpdateToUi(EntityUid patient, EntityUid surgeon)
{
if (!_ui.HasUi(patient, SurgeryUiKey.Key))
return;
var sterilityInfo = GetSterilityInfo(patient, surgeon);
_ui.ServerSendUiMessage(patient, SurgeryUiKey.Key, new SurgerySterilityUpdateMessage(sterilityInfo), surgeon);
}
}
@@ -7,7 +7,6 @@ using Content.Shared.Buckle.Components;
using Content.Shared.Chat.Prototypes;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Damage.Systems;
using Content.Shared.Examine;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Inventory;
using Content.Shared.Inventory.Events;
@@ -19,7 +18,6 @@ using Content.Shared.Rejuvenate;
using Content.Shared.Stunnable;
using Content.Shared.Surgery.Components;
using Content.Shared.Tag;
using Content.Shared.Throwing;
using Content.Shared.Tools;
using Content.Shared.Tools.Systems;
using Robust.Server.GameObjects;
@@ -89,9 +87,6 @@ public sealed partial class SurgerySystem : EntitySystem
SubscribeLocalEvent<OperatedComponent, RejuvenateEvent>(OnRejuvenate);
SubscribeLocalEvent<OperatedComponent, StandUpAttemptEvent>(OnStandUpAttempt);
SubscribeLocalEvent<OperatedComponent, IsEquippingAttemptEvent>(OnIsEquipping);
SubscribeLocalEvent<SterileComponent, ExaminedEvent>(OnSterileExamined);
SubscribeLocalEvent<SterileComponent, ThrownEvent>(OnThrow);
}
public override void Update(float frameTime)
@@ -130,24 +125,6 @@ public sealed partial class SurgerySystem : EntitySystem
operated.NextRegenerationTick -= frameTime;
}
}
var sterileQuery = EntityQueryEnumerator<SterileComponent>();
while (sterileQuery.MoveNext(out var uid, out var sterile))
{
if (sterile.AlwaysSterile)
continue;
if (sterile.NextUpdateTick <= 0)
{
sterile.NextUpdateTick = 5f;
sterile.Amount -= sterile.DecayRate;
if (sterile.Amount <= 0)
{
RemComp<SterileComponent>(uid);
}
}
sterile.NextUpdateTick -= frameTime;
}
}
private void OnRejuvenate(Entity<OperatedComponent> ent, ref RejuvenateEvent args)
@@ -341,15 +318,6 @@ public sealed partial class SurgerySystem : EntitySystem
return true;
}
private void OnSterileExamined(Entity<SterileComponent> entity, ref ExaminedEvent args)
{
if (args.IsInDetailsRange)
args.AddMarkup(Loc.GetString("surgery-sterile-examined") + "\n");
}
private void OnThrow(Entity<SterileComponent> entity, ref ThrownEvent args)
=> RemCompDeferred<SterileComponent>(entity);
private bool TryGetOperatingTable(EntityUid patient, out float tableModifier)
{
tableModifier = 1f;
@@ -3,13 +3,13 @@ namespace Content.Shared.Surgery.Components;
[RegisterComponent]
public sealed partial class SterileComponent : Component
{
[DataField("amount")]
public float Amount = 100f;
[DataField]
public float Amount = 0f;
[DataField("decayRate")]
[DataField]
public float DecayRate = 0.5f;
[DataField("alwaysSterile")]
[DataField]
public bool AlwaysSterile = false;
[ViewVariables]
+31 -7
View File
@@ -10,15 +10,15 @@ public enum SurgeryUiKey
}
[Serializable, NetSerializable]
public sealed partial class SurgeryProcedureDto : BoundUserInterfaceMessage
public sealed partial class SurgeryProcedureDtoState : BoundUserInterfaceState
{
public List<SurgeryGroupDto> Groups;
public NetEntity PatientId;
public SurgerySterilityInfo SterilityInfo;
public SurgeryProcedureDto(List<SurgeryGroupDto> groups, NetEntity patientId)
public SurgeryProcedureDtoState(List<SurgeryGroupDto> groups, SurgerySterilityInfo sterilityInfo)
{
Groups = groups;
PatientId = patientId;
SterilityInfo = sterilityInfo;
}
}
@@ -69,17 +69,41 @@ public sealed partial class SurgeryStepDto
}
}
[Serializable, NetSerializable]
public sealed partial class SurgerySterilityInfo
{
public float Sterility;
public List<string> NegativeFactors;
public List<string> PositiveFactors;
public SurgerySterilityInfo(float sterility, List<string> negativeFactors, List<string> positiveFactors)
{
Sterility = sterility;
NegativeFactors = negativeFactors;
PositiveFactors = positiveFactors;
}
}
[Serializable, NetSerializable]
public sealed partial class SurgerySterilityUpdateMessage : BoundUserInterfaceMessage
{
public SurgerySterilityInfo SterilityInfo;
public SurgerySterilityUpdateMessage(SurgerySterilityInfo sterilityInfo)
{
SterilityInfo = sterilityInfo;
}
}
[Serializable, NetSerializable]
public sealed partial class SurgeryStartMessage : BoundUserInterfaceMessage
{
public NetEntity User;
public ProtoId<SurgeryNodePrototype> TargetNode;
public int StepIndex;
public bool IsParallel;
public SurgeryStartMessage(NetEntity user, ProtoId<SurgeryNodePrototype> targetNode, int stepIndex, bool isParallel)
public SurgeryStartMessage(ProtoId<SurgeryNodePrototype> targetNode, int stepIndex, bool isParallel)
{
User = user;
TargetNode = targetNode;
StepIndex = stepIndex;
IsParallel = isParallel;
@@ -2,5 +2,6 @@ guide-entry-vampires = Вампиры
guide-entry-blood-cult = Культ крови
guide-entry-blood-brothers = Братья по крови
guide-entry-genetic = Генетик
guide-entry-surgery = Хирургия
guide-entry-mindchat = Чат разума
guide-entry-veil-cult = Праведник Ратвара
guide-entry-veil-cult = Праведник Ратвара
@@ -6,6 +6,49 @@ surgery-parallel = (П)
surgery-tool-required = Требуется инструмент: {$tool}
surgery-condition-required = Требуется часть тела: {$condition}
surgery-sterility-percent = Стерильность: {$percent}%
surgery-sterility-negative-header = Факторы риска:
surgery-sterility-positive-header = Положительные факторы:
# Negative
surgery-sterility-no-gloves = Нет перчаток
surgery-sterility-no-mask = Нет маски
surgery-sterility-no-head = Нет головного убора
surgery-sterility-no-jumpsuit = Нет комбинезона
surgery-sterility-no-outerClothing = Нет верхней одежды
surgery-sterility-no-back = Рюкзак создаёт помехи
surgery-sterility-no-belt = Пояс мешает
surgery-sterility-non-sterile-gloves = Нестерильные перчатки
surgery-sterility-non-sterile-mask = Нестерильная маска
surgery-sterility-non-sterile-head = Нестерильная шапочка
surgery-sterility-non-sterile-jumpsuit = Нестерильный комбинезон
surgery-sterility-non-sterile-outerClothing = Нестерильный халат
surgery-sterility-non-sterile-back = Грязный рюкзак
surgery-sterility-non-sterile-belt = Грязный пояс
surgery-sterility-no-sterile-gloves = Нет стерильных перчаток
surgery-sterility-no-sterile-mask = Нет стерильной маски
surgery-sterility-no-sterile-head = Нет стерильной шапочки
surgery-sterility-no-sterile-jumpsuit = Нет стерильного комбинезона
surgery-sterility-no-sterile-outerClothing = Нет стерильного халата
surgery-sterility-no-sterile-back = Нет стерильного рюкзака
surgery-sterility-no-sterile-belt = Нет стерильного пояса
surgery-sterility-non-sterile-tool = Нестерильный инструмент
surgery-sterility-mask-off = Маска опущена
surgery-sterility-garbage = Мусор рядом ({$count})
surgery-sterility-bystanders = Зеваки ({$count})
surgery-sterility-corpses = Трупы рядом ({$count})
# Positive
surgery-sterility-sterile-gloves = Стерильные перчатки
surgery-sterility-sterile-mask = Стерильная маска
surgery-sterility-sterile-head = Стерильная шапочка
surgery-sterility-sterile-jumpsuit = Стерильный комбинезон
surgery-sterility-sterile-outerClothing = Стерильный халат
surgery-sterility-sterile-back = Стерильный рюкзак
surgery-sterility-sterile-belt = Стерильный пояс
surgery-sterility-sterile-tool = Стерильный инструмент
surgery-sterility-operating-table = Операционный стол
surgery-condition-required-head = голова
surgery-condition-required-tooth = зубы
surgery-condition-required-torso = тело
@@ -8,6 +8,7 @@
- Cloning
- Cryogenics
- Genetic # Corvax-Wega-Genetics
- Surgery # Corvax-Wega-Surgery
- type: guideEntry
id: MedicalDoctor
@@ -2,3 +2,8 @@
id: Genetic
name: guide-entry-genetic
text: "/ServerInfo/_Wega/Guidebook/Medical/Genetic.xml"
- type: guideEntry
id: Surgery
name: guide-entry-surgery
text: "/ServerInfo/_Wega/Guidebook/Medical/Surgery.xml"
@@ -0,0 +1,92 @@
<Document>
# Хирургия
<Box>
[color=#999999][italic]"Остались лишь ручки... Да ножки..."[/italic][/color]
</Box>
<Box>
<GuideEntityEmbed Entity="computerBodyScanner" Caption="сканер тела"/>
<GuideEntityEmbed Entity="OperatingTable" Caption="операционный стол"/>
</Box>
## Начало работы
На большинстве станций в начале есть доступ к хирургическим наборам и начальному инструментарию. В случае отсутствия необходимого инструменты можно заказать в карго или изучить в РНД.
<Box>
<GuideEntityEmbed Entity="Scalpel" Caption="скальпель"/>
<GuideEntityEmbed Entity="Hemostat" Caption="гемостат"/>
<GuideEntityEmbed Entity="Retractor" Caption="ретрактор"/>
<GuideEntityEmbed Entity="BoneSetter" Caption="костоправ"/>
<GuideEntityEmbed Entity="BoneGel" Caption="костный гель"/>
</Box>
<Box>
<GuideEntityEmbed Entity="Cautery" Caption="прижигатель"/>
<GuideEntityEmbed Entity="FixOVein" Caption="фиксатор вен"/>
<GuideEntityEmbed Entity="Saw" Caption="пила"/>
<GuideEntityEmbed Entity="Drill" Caption="дрель"/>
</Box>
## Профилактика инфекций
Процесс проведения операций сильно связан со стерильностью операционной — её отсутствие может негативно сказываться на пациенте и приводить к осложнениям.
Чтобы избежать проблем, хирургу необходимо:
- Очистить операционную от мусора и трупов.
- Стерилизовать инструменты с помощью распылителя с этанолом.
- Носить латексные перчатки, стерильную маску и стерильную одежду.
- Снять пояса и сумки — они мешают.
- Убрать посторонних из операционной (допустимо не более 2 человек помимо хирурга и пациента).
Несоблюдение этих правил может привести к заражению, боли и замедлению пациента. В случае заражения обратитесь к вирусологу.
## Стерилизация инструментов
Используйте распылитель с этанолом или алкогольными напитками для стерилизации инструментов. Чистый этанол даёт максимум единиц стерильности за 5 унций.
Алкогольные напитки работают хуже — их эффективность напрямую зависит от содержания этанола.
Стерильность инструмента со временем уменьшается. [bold]Стерильный инструмент снижает риск осложнений при операции.[/bold]
## Гетто-хирургия
Хирургическое вмешательство может быть выполнено с помощью подручных инструментов, если соответствующие инструменты недоступны. Это снижает эффективность и может нанести сопутствующий ущерб.
- Скальпель → ножи, заточки, осколки стекла, кусачки.
- Гемостат → провода, монтировка.
- Ретрактор → монтировка.
- Костный гель → отвёртка.
- Костоправ → гаечный ключ.
- Прижигатель → сигареты, зажигалки, сварочный аппарат.
- Фиксатор вен → провода.
- Пила → топорик, пожарный топор.
- Дрель → электродрель, ручка, кирка/бур, отвёртка.
## Подготовка к операции
Перед началом операции осмотрите пациента с помощью продвинутого сканера тела. Пациент должен быть максимально здоров — многие операции могут стать для него последними.
Рекомендуется проводить анестезию (баллон с оксидом азота). Отказ от анестезии увеличивает боль...
## Операции
Любая операция представлена в меню хирургических манипуляций. Доступные процедуры включают:
- Ампутацию/пришивание конечностей.
- Пересадку органов.
- Имплантацию предметов и имплантов.
- Лечение внутренних повреждений.
## Имплантация
Современные технологии позволяют вживлять в тело пациента различные объекты:
- [bold]Торс[/bold] — до 3 единиц.
- [bold]Голова[/bold] — 1 крошечный предмет.
- [bold]Зубные импланты[/bold] — таблетки мгновенно усваиваются при активации.
## Синтетики
Для ремонта синтетических существ используется специальный набор инструментов: отвёртка, кусачки, сварочный аппарат, гаечный ключ, монтировка, провода.
Гетто-инструменты также работают, но менее эффективно.
</Document>