diff --git a/Content.Client/_Wega/Surgery/Ui/SurgeryBoundUserInterface.cs b/Content.Client/_Wega/Surgery/Ui/SurgeryBoundUserInterface.cs index 7c0324f467..2de7faf869 100644 --- a/Content.Client/_Wega/Surgery/Ui/SurgeryBoundUserInterface.cs +++ b/Content.Client/_Wega/Surgery/Ui/SurgeryBoundUserInterface.cs @@ -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); } } diff --git a/Content.Client/_Wega/Surgery/Ui/SurgeryWindow.xaml.cs b/Content.Client/_Wega/Surgery/Ui/SurgeryWindow.xaml.cs index d0b4f959c9..681ac1ee83 100644 --- a/Content.Client/_Wega/Surgery/Ui/SurgeryWindow.xaml.cs +++ b/Content.Client/_Wega/Surgery/Ui/SurgeryWindow.xaml.cs @@ -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(); + HelpGuidebookIds = new List> { "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? 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(toolQualityId, out var toolQuality) && - _prototypeManager.TryIndex(toolQuality.Spawn, out var toolProto)) + if (_prototype.TryIndex(toolQualityId, out var toolQuality) && + _prototype.TryIndex(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(entityPreview, out var entity)) + if (_prototype.TryIndex(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(tool, out var toolQuality) && - _prototypeManager.TryIndex(toolQuality.Spawn, out var toolProto)) + if (!string.IsNullOrEmpty(tool) && _prototype.TryIndex(tool, out var toolQuality) && + _prototype.TryIndex(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(); } -} \ No newline at end of file +} diff --git a/Content.Server/Fluids/EntitySystems/SpraySystem.cs b/Content.Server/Fluids/EntitySystems/SpraySystem.cs index c63d8f3625..3b3a989cba 100644 --- a/Content.Server/Fluids/EntitySystems/SpraySystem.cs +++ b/Content.Server/Fluids/EntitySystems/SpraySystem.cs @@ -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(args.Target) - && _solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out _, out var solution) - && solution.GetTotalPrototypeQuantity("Ethanol") >= FixedPoint2.New(5)) + if (args.Target != null && HasComp(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(args.Target.Value); + _sterile.ApplySterilityFromSolution(args.Target.Value, solution, entity.Comp.TransferAmount.Float()); } // Corvax-Wega-Surgery-end diff --git a/Content.Server/_Wega/Surgery/SterileSystem.cs b/Content.Server/_Wega/Surgery/SterileSystem.cs new file mode 100644 index 0000000000..f1ca9566fc --- /dev/null +++ b/Content.Server/_Wega/Surgery/SterileSystem.cs @@ -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 EthanolReagent = "Ethanol"; + + private const float EthanolUnitsPerSterilePoint = 0.05f; + private const float MaxSterileAmount = 100f; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnExamined); + SubscribeLocalEvent(OnThrow); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var sterileQuery = EntityQueryEnumerator(); + 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(uid); + } + } + sterile.NextUpdateTick -= frameTime; + } + } + + private void OnExamined(Entity entity, ref ExaminedEvent args) + { + if (args.IsInDetailsRange) + args.AddMarkup(Loc.GetString("surgery-sterile-examined") + "\n"); + } + + private void OnThrow(Entity entity, ref ThrownEvent args) + => RemCompDeferred(entity); + + public float ApplySterilityFromSolution(EntityUid target, Solution solution, float transferAmount) + { + if (!HasComp(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(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(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; + } +} diff --git a/Content.Server/_Wega/Surgery/SurgerySystem.Graphs.cs b/Content.Server/_Wega/Surgery/SurgerySystem.Graphs.cs index 1c3e44b9af..05d41de4f6 100644 --- a/Content.Server/_Wega/Surgery/SurgerySystem.Graphs.cs +++ b/Content.Server/_Wega/Surgery/SurgerySystem.Graphs.cs @@ -37,7 +37,7 @@ public sealed partial class SurgerySystem /// The SurgeryStartMessage containing details about the surgery start request. 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); } } diff --git a/Content.Server/_Wega/Surgery/SurgerySystem.Operation.cs b/Content.Server/_Wega/Surgery/SurgerySystem.Operation.cs index fcc15a52b8..d16552b6b3 100644 --- a/Content.Server/_Wega/Surgery/SurgerySystem.Operation.cs +++ b/Content.Server/_Wega/Surgery/SurgerySystem.Operation.cs @@ -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(patient) && !HasComp(patient) && !comp.OperatedPart && !_mobState.IsDead(patient) && !HasComp(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(patient) || HasComp(patient)) return; @@ -459,19 +459,71 @@ public sealed partial class SurgerySystem private bool RollSuccess(Entity ent, EntityUid surgeon, float baseChance) { var item = _hands.GetActiveItemOrSelf(surgeon); - if (HasComp(surgeon) && ent.Comp.Sterility == 1f && HasComp(item) - && SurgeryTools.Any(tool => _tool.HasQuality(item, tool)) - || Organs.Any(tag => _tag.HasTag(item, tag)) - || Parts.Any(tag => _tag.HasTag(item, tag))) + if (HasComp(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 patient) + { + if (patient.Comp.Surgeon == null || HasComp(patient)) + return; + + var sterility = patient.Comp.Sterility; + var item = _hands.GetActiveItemOrSelf(patient.Comp.Surgeon.Value); + if (sterility >= 0.7f && HasComp(item)) + return; + + if (sterility >= 0.5f) + { + var painChance = 0.3f + (0.75f - sterility) / 0.25f * 0.4f; + if (_random.Prob(painChance)) + { + if (!HasComp(patient) && !HasComp(patient) + && !patient.Comp.OperatedPart && !_mobState.IsDead(patient) && !HasComp(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(patient) && !HasComp(patient) + && !patient.Comp.OperatedPart && !_mobState.IsDead(patient) && !HasComp(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(patient) && !HasComp(patient) && !patient.Comp.OperatedPart && !_mobState.IsDead(patient) && !HasComp(patient)) - _chat.TryEmoteWithoutChat(patient, _proto.Index(Scream), true); + { + if (!HasComp(patient) && !HasComp(patient) && !patient.Comp.OperatedPart + && !_mobState.IsDead(patient) && !HasComp(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)) diff --git a/Content.Server/_Wega/Surgery/SurgerySystem.Sterility.cs b/Content.Server/_Wega/Surgery/SurgerySystem.Sterility.cs index 6e6df48765..2eb50ee2db 100644 --- a/Content.Server/_Wega/Surgery/SurgerySystem.Sterility.cs +++ b/Content.Server/_Wega/Surgery/SurgerySystem.Sterility.cs @@ -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(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( - 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(item)) - sterility *= 0.4f; + sterility *= 0.85f; - var bystanders = _entityLookup.GetEntitiesInRange( - Transform(patient).Coordinates, 2f) + var bystanders = _entityLookup.GetEntitiesInRange(Transform(patient).Coordinates, 2f) .Where(e => e.Owner != patient && e.Owner != operated.Surgeon - && !_mobState.IsDead(e.Owner) && !HasComp(e.Owner)) - .Count(); + && !_mobState.IsDead(e.Owner) && !HasComp(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( - Transform(patient).Coordinates, 2f) + var corpses = _entityLookup.GetEntitiesInRange(Transform(patient).Coordinates, 2f) .Where(e => e.Owner != patient && e.Owner != operated.Surgeon - && _mobState.IsDead(e.Owner) && !HasComp(e.Owner)) - .Count(); + && _mobState.IsDead(e.Owner) && !HasComp(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(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(patient)) + return new SurgerySterilityInfo(1f, new List(), new List()); + + float sterility = 1f; + var negativeFactors = new List(); + var positiveFactors = new List(); + + // Важные слоты + 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( + 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(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(Transform(patient).Coordinates, 2f) + .Where(e => e.Owner != patient && e.Owner != surgeon + && !_mobState.IsDead(e.Owner) && !HasComp(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(Transform(patient).Coordinates, 2f) + .Where(e => e.Owner != patient && e.Owner != surgeon + && _mobState.IsDead(e.Owner) && !HasComp(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 negativeFactors, List positiveFactors) + { + if (HasComp(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(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(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 } diff --git a/Content.Server/_Wega/Surgery/SurgerySystem.Ui.cs b/Content.Server/_Wega/Surgery/SurgerySystem.Ui.cs index 004f96306d..7933e8b8d2 100644 --- a/Content.Server/_Wega/Surgery/SurgerySystem.Ui.cs +++ b/Content.Server/_Wega/Surgery/SurgerySystem.Ui.cs @@ -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(comp.GraphId); - Timer.Spawn(250, () => - { - UpdateUi(uid, comp, graph); - }); + UpdateUi(uid, args.Actor, comp, graph); } private void OnUnbuckled(Entity 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); } } diff --git a/Content.Server/_Wega/Surgery/SurgerySystem.cs b/Content.Server/_Wega/Surgery/SurgerySystem.cs index 2162ae35a9..8261da2ff0 100644 --- a/Content.Server/_Wega/Surgery/SurgerySystem.cs +++ b/Content.Server/_Wega/Surgery/SurgerySystem.cs @@ -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(OnRejuvenate); SubscribeLocalEvent(OnStandUpAttempt); SubscribeLocalEvent(OnIsEquipping); - - SubscribeLocalEvent(OnSterileExamined); - SubscribeLocalEvent(OnThrow); } public override void Update(float frameTime) @@ -130,24 +125,6 @@ public sealed partial class SurgerySystem : EntitySystem operated.NextRegenerationTick -= frameTime; } } - - var sterileQuery = EntityQueryEnumerator(); - 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(uid); - } - } - sterile.NextUpdateTick -= frameTime; - } } private void OnRejuvenate(Entity ent, ref RejuvenateEvent args) @@ -341,15 +318,6 @@ public sealed partial class SurgerySystem : EntitySystem return true; } - private void OnSterileExamined(Entity entity, ref ExaminedEvent args) - { - if (args.IsInDetailsRange) - args.AddMarkup(Loc.GetString("surgery-sterile-examined") + "\n"); - } - - private void OnThrow(Entity entity, ref ThrownEvent args) - => RemCompDeferred(entity); - private bool TryGetOperatingTable(EntityUid patient, out float tableModifier) { tableModifier = 1f; diff --git a/Content.Shared/_Wega/Surgery/Components/SterileComponent.cs b/Content.Shared/_Wega/Surgery/Components/SterileComponent.cs index 14b622f293..dfc825e6ea 100644 --- a/Content.Shared/_Wega/Surgery/Components/SterileComponent.cs +++ b/Content.Shared/_Wega/Surgery/Components/SterileComponent.cs @@ -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] diff --git a/Content.Shared/_Wega/Surgery/Ui/SurgeryMenu.cs b/Content.Shared/_Wega/Surgery/Ui/SurgeryMenu.cs index b47cd21cee..cf3d60a7f3 100644 --- a/Content.Shared/_Wega/Surgery/Ui/SurgeryMenu.cs +++ b/Content.Shared/_Wega/Surgery/Ui/SurgeryMenu.cs @@ -10,15 +10,15 @@ public enum SurgeryUiKey } [Serializable, NetSerializable] -public sealed partial class SurgeryProcedureDto : BoundUserInterfaceMessage +public sealed partial class SurgeryProcedureDtoState : BoundUserInterfaceState { public List Groups; - public NetEntity PatientId; + public SurgerySterilityInfo SterilityInfo; - public SurgeryProcedureDto(List groups, NetEntity patientId) + public SurgeryProcedureDtoState(List 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 NegativeFactors; + public List PositiveFactors; + + public SurgerySterilityInfo(float sterility, List negativeFactors, List 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 TargetNode; public int StepIndex; public bool IsParallel; - public SurgeryStartMessage(NetEntity user, ProtoId targetNode, int stepIndex, bool isParallel) + public SurgeryStartMessage(ProtoId targetNode, int stepIndex, bool isParallel) { - User = user; TargetNode = targetNode; StepIndex = stepIndex; IsParallel = isParallel; diff --git a/Resources/Locale/ru-RU/_wega/guidebook/guides.ftl b/Resources/Locale/ru-RU/_wega/guidebook/guides.ftl index eed562187c..da3dde5a2e 100644 --- a/Resources/Locale/ru-RU/_wega/guidebook/guides.ftl +++ b/Resources/Locale/ru-RU/_wega/guidebook/guides.ftl @@ -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 = Праведник Ратвара \ No newline at end of file +guide-entry-veil-cult = Праведник Ратвара diff --git a/Resources/Locale/ru-RU/_wega/surgery/surgery.ftl b/Resources/Locale/ru-RU/_wega/surgery/surgery.ftl index e4bbacfb50..eba29752bf 100644 --- a/Resources/Locale/ru-RU/_wega/surgery/surgery.ftl +++ b/Resources/Locale/ru-RU/_wega/surgery/surgery.ftl @@ -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 = тело diff --git a/Resources/Prototypes/Guidebook/medical.yml b/Resources/Prototypes/Guidebook/medical.yml index c27cc34ebb..479b5f8c18 100644 --- a/Resources/Prototypes/Guidebook/medical.yml +++ b/Resources/Prototypes/Guidebook/medical.yml @@ -8,6 +8,7 @@ - Cloning - Cryogenics - Genetic # Corvax-Wega-Genetics + - Surgery # Corvax-Wega-Surgery - type: guideEntry id: MedicalDoctor diff --git a/Resources/Prototypes/_Wega/Guidebook/medical.yml b/Resources/Prototypes/_Wega/Guidebook/medical.yml index 2b9fb50f47..b6f7fabf57 100644 --- a/Resources/Prototypes/_Wega/Guidebook/medical.yml +++ b/Resources/Prototypes/_Wega/Guidebook/medical.yml @@ -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" diff --git a/Resources/ServerInfo/_Wega/Guidebook/Medical/Surgery.xml b/Resources/ServerInfo/_Wega/Guidebook/Medical/Surgery.xml new file mode 100644 index 0000000000..c28a3e2f83 --- /dev/null +++ b/Resources/ServerInfo/_Wega/Guidebook/Medical/Surgery.xml @@ -0,0 +1,92 @@ + + + # Хирургия + + + [color=#999999][italic]"Остались лишь ручки... Да ножки..."[/italic][/color] + + + + + + + + ## Начало работы + + На большинстве станций в начале есть доступ к хирургическим наборам и начальному инструментарию. В случае отсутствия необходимого инструменты можно заказать в карго или изучить в РНД. + + + + + + + + + + + + + + + + ## Профилактика инфекций + + Процесс проведения операций сильно связан со стерильностью операционной — её отсутствие может негативно сказываться на пациенте и приводить к осложнениям. + + Чтобы избежать проблем, хирургу необходимо: + - Очистить операционную от мусора и трупов. + - Стерилизовать инструменты с помощью распылителя с этанолом. + - Носить латексные перчатки, стерильную маску и стерильную одежду. + - Снять пояса и сумки — они мешают. + - Убрать посторонних из операционной (допустимо не более 2 человек помимо хирурга и пациента). + + Несоблюдение этих правил может привести к заражению, боли и замедлению пациента. В случае заражения обратитесь к вирусологу. + + ## Стерилизация инструментов + + Используйте распылитель с этанолом или алкогольными напитками для стерилизации инструментов. Чистый этанол даёт максимум единиц стерильности за 5 унций. + Алкогольные напитки работают хуже — их эффективность напрямую зависит от содержания этанола. + + Стерильность инструмента со временем уменьшается. [bold]Стерильный инструмент снижает риск осложнений при операции.[/bold] + + ## Гетто-хирургия + + Хирургическое вмешательство может быть выполнено с помощью подручных инструментов, если соответствующие инструменты недоступны. Это снижает эффективность и может нанести сопутствующий ущерб. + + - Скальпель → ножи, заточки, осколки стекла, кусачки. + - Гемостат → провода, монтировка. + - Ретрактор → монтировка. + - Костный гель → отвёртка. + - Костоправ → гаечный ключ. + - Прижигатель → сигареты, зажигалки, сварочный аппарат. + - Фиксатор вен → провода. + - Пила → топорик, пожарный топор. + - Дрель → электродрель, ручка, кирка/бур, отвёртка. + + ## Подготовка к операции + + Перед началом операции осмотрите пациента с помощью продвинутого сканера тела. Пациент должен быть максимально здоров — многие операции могут стать для него последними. + + Рекомендуется проводить анестезию (баллон с оксидом азота). Отказ от анестезии увеличивает боль... + + ## Операции + + Любая операция представлена в меню хирургических манипуляций. Доступные процедуры включают: + - Ампутацию/пришивание конечностей. + - Пересадку органов. + - Имплантацию предметов и имплантов. + - Лечение внутренних повреждений. + + ## Имплантация + + Современные технологии позволяют вживлять в тело пациента различные объекты: + - [bold]Торс[/bold] — до 3 единиц. + - [bold]Голова[/bold] — 1 крошечный предмет. + - [bold]Зубные импланты[/bold] — таблетки мгновенно усваиваются при активации. + + ## Синтетики + + Для ремонта синтетических существ используется специальный набор инструментов: отвёртка, кусачки, сварочный аппарат, гаечный ключ, монтировка, провода. + Гетто-инструменты также работают, но менее эффективно. + +