diff --git a/Content.Server/Medical/HealingSystem.cs b/Content.Server/Medical/HealingSystem.cs index 63665bdc7c9..45f0cb28fb1 100644 --- a/Content.Server/Medical/HealingSystem.cs +++ b/Content.Server/Medical/HealingSystem.cs @@ -1,6 +1,7 @@ using Content.Server.Administration.Logs; using Content.Server.Body.Components; using Content.Server.Body.Systems; +using Content.Shared.Body.Part; using Content.Server.Chemistry.Containers.EntitySystems; using Content.Server.Medical.Components; using Content.Server.Popups; @@ -95,17 +96,13 @@ entity.Comp.DamageContainerID is not null && */ if (healed != null && healed.GetTotal() == 0) { - if (TryComp(args.User, out var user) - && TryComp(args.Target, out var target) - && healing.Damage.GetTotal() < 0) - { - // If they are valid, we check for body part presence, - // and integrity, then apply a direct integrity change. - var (type, symmetry) = _bodySystem.ConvertTargetBodyPart(user.Target); - if (_bodySystem.GetBodyChildrenOfType(args.Target.Value, type, symmetry: symmetry).FirstOrDefault() is { } bodyPart - && bodyPart.Component.Integrity < bodyPart.Component.MaxIntegrity) - _bodySystem.TryChangeIntegrity(bodyPart, healing.Damage.GetTotal().Float(), false, target.Target, out var _); - } + var parts = _bodySystem.GetBodyChildren(args.Target).ToList(); + // We fetch the most damaged body part + var mostDamaged = parts.MinBy(x => x.Component.Integrity); + var targetBodyPart = _bodySystem.GetTargetBodyPart(mostDamaged); + + if (targetBodyPart != null) + _bodySystem.TryChangeIntegrity(mostDamaged, healing.Damage.GetTotal().Float(), false, targetBodyPart.Value, out _); } var total = healed?.GetTotal() ?? FixedPoint2.Zero; @@ -166,7 +163,7 @@ private bool ArePartsDamaged(EntityUid target) foreach (var part in _bodySystem.GetBodyChildren(target, body)) { - if (part.Component.Integrity < part.Component.MaxIntegrity) + if (part.Component.Integrity < BodyPartComponent.MaxIntegrity) return true; } return false; diff --git a/Content.Server/Medical/Surgery/SurgerySystem.cs b/Content.Server/Medical/Surgery/SurgerySystem.cs index 7b508409bb4..e96bc3229a6 100644 --- a/Content.Server/Medical/Surgery/SurgerySystem.cs +++ b/Content.Server/Medical/Surgery/SurgerySystem.cs @@ -5,6 +5,7 @@ using Content.Shared.Body.Part; using Content.Server.Chat.Systems; using Content.Server.Popups; +using Content.Shared.CCVar; using Content.Shared.Damage; using Content.Shared.Damage.Prototypes; using Content.Shared.Interaction; @@ -18,9 +19,9 @@ using Content.Server.Atmos.Rotting; using Content.Shared.Eye.Blinding.Components; using Content.Shared.Eye.Blinding.Systems; -//using Content.Shared.Medical.Wounds; using Content.Shared.Prototypes; using Robust.Server.GameObjects; +using Robust.Shared.Configuration; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Utility; @@ -32,14 +33,13 @@ public sealed class SurgerySystem : SharedSurgerySystem { [Dependency] private readonly BodySystem _body = default!; [Dependency] private readonly ChatSystem _chat = default!; - + [Dependency] private readonly IConfigurationManager _config = default!; [Dependency] private readonly DamageableSystem _damageableSystem = default!; [Dependency] private readonly IPrototypeManager _prototypes = default!; [Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly UserInterfaceSystem _ui = default!; [Dependency] private readonly RottingSystem _rot = default!; [Dependency] private readonly BlindableSystem _blindableSystem = default!; - //[Dependency] private readonly WoundsSystem _wounds = default!; private readonly List _surgeries = new(); @@ -108,12 +108,13 @@ private void OnToolAfterInteract(Entity ent, ref AfterInte { return; } - /* lmao bet - if (user == args.Target) + + if (user == args.Target && !_config.GetCVar(CCVars.CanOperateOnSelf)) { - _popup.PopupEntity("You can't perform surgery on yourself!", user, user); + _popup.PopupEntity(Loc.GetString("surgery-error-self-surgery"), user, user); return; - }*/ + } + args.Handled = true; _ui.OpenUi(args.Target.Value, SurgeryUIKey.Key, user); RefreshUI(args.Target.Value); diff --git a/Content.Server/Targeting/TargetingSystem.cs b/Content.Server/Targeting/TargetingSystem.cs index e2704b40a46..8245fe65d98 100644 --- a/Content.Server/Targeting/TargetingSystem.cs +++ b/Content.Server/Targeting/TargetingSystem.cs @@ -2,13 +2,10 @@ using Content.Shared.Mobs; using Content.Shared.Targeting; using Content.Shared.Targeting.Events; -using Robust.Server.Audio; -using Robust.Shared.Audio; namespace Content.Server.Targeting; public sealed class TargetingSystem : SharedTargetingSystem { - [Dependency] private readonly AudioSystem _audio = default!; [Dependency] private readonly SharedBodySystem _bodySystem = default!; public override void Initialize() @@ -23,8 +20,6 @@ private void OnTargetChange(TargetChangeEvent message, EntitySessionEventArgs ar if (!TryComp(GetEntity(message.Uid), out var target)) return; - // Todo, get a better sound for this shit. - //_audio.PlayGlobal(target.SwapSound, args.SenderSession, AudioParams.Default.WithVolume(-8f)); target.Target = message.BodyPart; Dirty(GetEntity(message.Uid), target); } @@ -54,4 +49,4 @@ private void OnMobStateChange(EntityUid uid, TargetingComponent component, MobSt RaiseNetworkEvent(new TargetIntegrityChangeEvent(GetNetEntity(uid)), uid); } } -} \ No newline at end of file +} diff --git a/Content.Shared/Body/Part/BodyPartComponent.cs b/Content.Shared/Body/Part/BodyPartComponent.cs index 8848bd4a867..62b455b5113 100644 --- a/Content.Shared/Body/Part/BodyPartComponent.cs +++ b/Content.Shared/Body/Part/BodyPartComponent.cs @@ -2,6 +2,7 @@ using Content.Shared.Medical.Surgery.Tools; using Content.Shared.Body.Components; using Content.Shared.Body.Systems; +using Content.Shared.FixedPoint; using Robust.Shared.Containers; using Robust.Shared.GameStates; using Robust.Shared.Serialization; @@ -36,6 +37,14 @@ public sealed partial class BodyPartComponent : Component, ISurgeryToolComponent [DataField("vital"), AutoNetworkedField] public bool IsVital; + /// + /// Amount of damage to deal when the part gets removed. + /// Only works if IsVital is true. + /// + [DataField, AutoNetworkedField] + public FixedPoint2 VitalDamage = MaxIntegrity; + + [DataField, AutoNetworkedField] public BodyPartSymmetry Symmetry = BodyPartSymmetry.None; @@ -66,8 +75,8 @@ public sealed partial class BodyPartComponent : Component, ISurgeryToolComponent /// /// What's the max health this body part can have? /// - [DataField, AutoNetworkedField] - public float MaxIntegrity = 100f; + [DataField] + public const float MaxIntegrity = 100f; /// /// Whether this body part is enabled or not. diff --git a/Content.Shared/Body/Systems/SharedBodySystem.Body.cs b/Content.Shared/Body/Systems/SharedBodySystem.Body.cs index 5132a239c6b..558a190f1ca 100644 --- a/Content.Shared/Body/Systems/SharedBodySystem.Body.cs +++ b/Content.Shared/Body/Systems/SharedBodySystem.Body.cs @@ -11,6 +11,7 @@ using Content.Shared.Gibbing.Events; using Content.Shared.Gibbing.Systems; using Content.Shared.Inventory; +using Content.Shared.Rejuvenate; using Content.Shared.Standing; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; @@ -49,6 +50,7 @@ private void InitializeBody() SubscribeLocalEvent(OnBodyCanDrag); SubscribeLocalEvent(OnDamageChanged); SubscribeLocalEvent(OnStandAttempt); + SubscribeLocalEvent(OnRejuvenate); } private void OnBodyInserted(Entity ent, ref EntInsertedIntoContainerMessage args) @@ -158,6 +160,14 @@ private void OnDamageChanged(Entity ent, ref DamageChangedEvent a } } + private void OnRejuvenate(Entity ent, ref RejuvenateEvent args) + { + foreach (var part in GetBodyChildren(ent, ent.Comp)) + { + TryChangeIntegrity(part, part.Component.Integrity - BodyPartComponent.MaxIntegrity, false, GetTargetBodyPart(part), out _); + } + } + /// /// Sets up all of the relevant body parts for a particular body entity and root part. /// @@ -340,7 +350,7 @@ public virtual HashSet GibBody( foreach (var part in parts) { - _gibbingSystem.TryGibEntityWithRef(bodyId, part.Id, GibType.Gib, GibContentsOption.Skip, ref gibs, + _gibbingSystem.TryGibEntityWithRef(bodyId, part.Id, GibType.Gib, GibContentsOption.Drop, ref gibs, playAudio: false, launchGibs: true, launchDirection: splatDirection, launchImpulse: GibletLaunchImpulse * splatModifier, launchImpulseVariance: GibletLaunchImpulseVariance, launchCone: splatCone); diff --git a/Content.Shared/Body/Systems/SharedBodySystem.Integrity.cs b/Content.Shared/Body/Systems/SharedBodySystem.Integrity.cs index 9c1d0cdb3be..415295561a3 100644 --- a/Content.Shared/Body/Systems/SharedBodySystem.Integrity.cs +++ b/Content.Shared/Body/Systems/SharedBodySystem.Integrity.cs @@ -1,15 +1,20 @@ using Content.Shared.Body.Components; using Content.Shared.Body.Part; using Content.Shared.Damage; -using Content.Shared.Mobs.Systems; using Content.Shared.Medical.Surgery.Steps.Parts; using Content.Shared.Mobs.Components; +using Content.Shared.Mobs.Systems; using Content.Shared.Standing; using Content.Shared.Targeting; using Content.Shared.Targeting.Events; +using Robust.Shared.CPUJob.JobQueues; +using Robust.Shared.CPUJob.JobQueues.Queues; using Robust.Shared.Network; using Robust.Shared.Random; +using Robust.Shared.Timing; using System.Linq; +using System.Threading; +using System.Threading.Tasks; namespace Content.Shared.Body.Systems; @@ -19,45 +24,88 @@ public partial class SharedBodySystem [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly IRobustRandom _random = default!; private readonly string[] _severingDamageTypes = { "Slash", "Pierce", "Blunt" }; + private const double IntegrityJobTime = 0.005; + private readonly JobQueue _integrityJobQueue = new(IntegrityJobTime); + + public sealed class IntegrityJob : Job + { + private readonly SharedBodySystem _self; + private readonly Entity _ent; + public IntegrityJob(SharedBodySystem self, Entity ent, double maxTime, CancellationToken cancellation = default) : base(maxTime, cancellation) + { + _self = self; + _ent = ent; + } + + public IntegrityJob(SharedBodySystem self, Entity ent, double maxTime, IStopwatch stopwatch, CancellationToken cancellation = default) : base(maxTime, stopwatch, cancellation) + { + _self = self; + _ent = ent; + } + + protected override Task Process() + { + _self.ProcessIntegrityTick(_ent); + + return Task.FromResult(null); + } + } + + private EntityQuery _queryTargeting; + private void InitializeIntegrityQueue() + { + _queryTargeting = GetEntityQuery(); + } + + private void ProcessIntegrityTick(Entity entity) + { + if (entity.Comp is { Body: { } body, Integrity: > 50 and < BodyPartComponent.MaxIntegrity } + && _queryTargeting.HasComp(body) + && !_mobState.IsDead(body)) + { + var healing = entity.Comp.SelfHealingAmount; + if (healing + entity.Comp.Integrity > BodyPartComponent.MaxIntegrity) + healing = entity.Comp.Integrity - BodyPartComponent.MaxIntegrity; + + TryChangeIntegrity(entity, + healing, + false, + GetTargetBodyPart(entity), + out _); + } + } + public override void Update(float frameTime) { base.Update(frameTime); + _integrityJobQueue.Process(); - var query = EntityQueryEnumerator(); + if (!_timing.IsFirstTimePredicted) + return; + + using var query = EntityQueryEnumerator(); while (query.MoveNext(out var ent, out var part)) { - if (!_timing.IsFirstTimePredicted - || !HasComp(ent)) - continue; - part.HealingTimer += frameTime; if (part.HealingTimer >= part.HealingTime) { part.HealingTimer = 0; - if (part.Body is not null - && part.Integrity > 50 - && part.Integrity < part.MaxIntegrity - && !_mobState.IsDead(part.Body.Value)) - { - var healing = part.SelfHealingAmount; - if (healing + part.Integrity > part.MaxIntegrity) - healing = part.Integrity - part.MaxIntegrity; - - TryChangeIntegrity((ent, part), healing, - false, GetTargetBodyPart(part.PartType, part.Symmetry), out _); - } + _integrityJobQueue.EnqueueJob(new IntegrityJob(this, (ent, part), IntegrityJobTime)); } - } - query.Dispose(); } + /// /// Propagates damage to the specified parts of the entity. /// - private void ApplyPartDamage(Entity partEnt, DamageSpecifier damage, - BodyPartType targetType, TargetBodyPart targetPart, bool canSever, float partMultiplier) + private void ApplyPartDamage(Entity partEnt, + DamageSpecifier damage, + BodyPartType targetType, + TargetBodyPart targetPart, + bool canSever, + float partMultiplier) { if (partEnt.Comp.Body is null) return; @@ -79,16 +127,19 @@ private void ApplyPartDamage(Entity partEnt, DamageSpecifier } } - public void TryChangeIntegrity(Entity partEnt, float integrity, bool canSever, - TargetBodyPart? targetPart, out bool severed) + public void TryChangeIntegrity(Entity partEnt, + float integrity, + bool canSever, + TargetBodyPart? targetPart, + out bool severed) { severed = false; - if (!HasComp(partEnt.Comp.Body) || !_timing.IsFirstTimePredicted) + if (!_timing.IsFirstTimePredicted || !_queryTargeting.HasComp(partEnt.Comp.Body)) return; var partIdSlot = GetParentPartAndSlotOrNull(partEnt)?.Slot; var originalIntegrity = partEnt.Comp.Integrity; - partEnt.Comp.Integrity = Math.Min(partEnt.Comp.MaxIntegrity, partEnt.Comp.Integrity - integrity); + partEnt.Comp.Integrity = Math.Min(BodyPartComponent.MaxIntegrity, partEnt.Comp.Integrity - integrity); if (canSever && !HasComp(partEnt) && !partEnt.Comp.Enabled @@ -110,9 +161,9 @@ public void TryChangeIntegrity(Entity partEnt, float integrit RaiseLocalEvent(partEnt, ref ev); } - if (partEnt.Comp.Integrity != originalIntegrity - && TryComp(partEnt.Comp.Body, out var targeting) - && TryComp(partEnt.Comp.Body, out var _)) + if (Math.Abs(partEnt.Comp.Integrity - originalIntegrity) > 0.01 + && _queryTargeting.TryComp(partEnt.Comp.Body, out var targeting) + && HasComp(partEnt.Comp.Body)) { var newIntegrity = GetIntegrityThreshold(partEnt.Comp.Integrity, severed, partEnt.Comp.Enabled); // We need to check if the part is dead to prevent the UI from showing dead parts as alive. @@ -161,6 +212,9 @@ public Dictionary GetBodyPartStatus(EntityUid e return result; } + public TargetBodyPart? GetTargetBodyPart(Entity part) => GetTargetBodyPart(part.Comp.PartType, part.Comp.Symmetry); + public TargetBodyPart? GetTargetBodyPart(BodyPartComponent part) => GetTargetBodyPart(part.PartType, part.Symmetry); + /// /// Converts Enums from BodyPartType to their Targeting system equivalent. /// diff --git a/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs b/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs index 985ff1f8ab9..06acd3d8552 100644 --- a/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs +++ b/Content.Shared/Body/Systems/SharedBodySystem.Parts.cs @@ -29,7 +29,6 @@ private void InitializeParts() SubscribeLocalEvent(OnBodyPartInserted); SubscribeLocalEvent(OnBodyPartRemoved); SubscribeLocalEvent(OnAmputateAttempt); - SubscribeLocalEvent(OnPartEnableChanged); } private void OnMapInit(Entity ent, ref MapInitEvent args) @@ -184,7 +183,6 @@ private void OnAmputateAttempt(Entity partEnt, ref AmputateAt { DropPart(partEnt); } - private void AddLeg(Entity legEnt, Entity bodyEnt) { if (!Resolve(bodyEnt, ref bodyEnt.Comp, logMissing: false)) @@ -248,8 +246,8 @@ private void PartRemoveDamage(Entity bodyEnt, Entity("Bloodloss"), 300); - Damageable.TryChangeDamage(bodyEnt, damage); + var damage = new DamageSpecifier(Prototypes.Index("Bloodloss"), partEnt.Comp.VitalDamage); + Damageable.TryChangeDamage(bodyEnt, damage, partMultiplier: 0f); } } diff --git a/Content.Shared/Body/Systems/SharedBodySystem.cs b/Content.Shared/Body/Systems/SharedBodySystem.cs index 2238224a80c..1d9aae69976 100644 --- a/Content.Shared/Body/Systems/SharedBodySystem.cs +++ b/Content.Shared/Body/Systems/SharedBodySystem.cs @@ -42,6 +42,8 @@ public override void Initialize() InitializeBody(); InitializeParts(); + // To try and mitigate the server load due to integrity checks, we set up a Job Queue. + InitializeIntegrityQueue(); } /// diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index 208d6d911d4..ac9da5bdc0f 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -2634,5 +2634,12 @@ public static readonly CVarDef CVarDef.Create("ghost.allow_same_character", false, CVar.SERVERONLY); #endregion + + #region Surgery + + public static readonly CVarDef CanOperateOnSelf = + CVarDef.Create("surgery.can_operate_on_self", false, CVar.SERVERONLY); + + #endregion } } diff --git a/Content.Shared/Medical/Surgery/SharedSurgerySystem.Steps.cs b/Content.Shared/Medical/Surgery/SharedSurgerySystem.Steps.cs index 337693efaf8..04f046bf816 100644 --- a/Content.Shared/Medical/Surgery/SharedSurgerySystem.Steps.cs +++ b/Content.Shared/Medical/Surgery/SharedSurgerySystem.Steps.cs @@ -204,7 +204,7 @@ private void OnTendWoundsStep(Entity ent, ref || !group.Any(damageType => damageable.Damage.DamageDict.TryGetValue(damageType, out var value) && value > 0) && (!TryComp(args.Part, out BodyPartComponent? bodyPart) - || bodyPart.Integrity == bodyPart.MaxIntegrity)) + || bodyPart.Integrity == BodyPartComponent.MaxIntegrity)) return; var bonus = ent.Comp.HealMultiplier * damageable.DamagePerGroup[ent.Comp.MainGroup]; @@ -221,14 +221,6 @@ private void OnTendWoundsStep(Entity ent, ref var ev = new SurgeryStepDamageEvent(args.User, args.Body, args.Part, args.Surgery, adjustedDamage, 0.5f); RaiseLocalEvent(args.Body, ref ev); - - if (ent.Comp.IsAutoRepeatable) - { - var stepProto = GetProtoId(ent); - var surgeryProto = GetProtoId(args.Surgery); - if (stepProto != null && surgeryProto != null) - CheckAndStartStep(args.User, args.Body, args.Part, ent, args.Surgery, stepProto.Value, surgeryProto.Value); - } } private void OnTendWoundsCheck(Entity ent, ref SurgeryStepCompleteCheckEvent args) @@ -239,7 +231,7 @@ private void OnTendWoundsCheck(Entity ent, ref || group.Any(damageType => damageable.Damage.DamageDict.TryGetValue(damageType, out var value) && value > 0) || !TryComp(args.Part, out BodyPartComponent? bodyPart) - || bodyPart.Integrity < bodyPart.MaxIntegrity) + || bodyPart.Integrity < BodyPartComponent.MaxIntegrity) args.Cancelled = true; } @@ -417,43 +409,6 @@ private void OnRemoveOrganCheck(Entity ent, ref } } } - - // Small duplicate for OnSurgeryTargetStepChosen, allows for continuously looping a given step. - private void CheckAndStartStep(EntityUid user, EntityUid body, EntityUid part, EntityUid step, EntityUid surgery, - EntProtoId stepProto, EntProtoId surgeryProto) - { - if (!CanPerformStep(user, body, part, step, true, out _, out _, out var validTools)) - return; - - if (_net.IsServer && validTools?.Count > 0) - { - foreach (var tool in validTools) - { - if (TryComp(tool, out SurgeryToolComponent? toolComp) && - toolComp.EndSound != null) - { - _audio.PlayEntity(toolComp.StartSound, user, tool); - } - } - } - - if (TryComp(body, out TransformComponent? xform)) - _rotateToFace.TryFaceCoordinates(user, _transform.GetMapCoordinates(body, xform).Position); - - var ev = new SurgeryDoAfterEvent(surgeryProto, stepProto); - // TODO: Make this serialized on a per surgery step basis, and also add penalties based on ghetto tools. - var duration = 2f; - if (TryComp(user, out SurgerySpeedModifierComponent? surgerySpeedMod)) - duration = duration / surgerySpeedMod.SpeedModifier; - - var doAfter = new DoAfterArgs(EntityManager, user, TimeSpan.FromSeconds(duration), ev, body, part) - { - BreakOnUserMove = true, - BreakOnTargetMove = true, - }; - _doAfter.TryStartDoAfter(doAfter); - } - private void OnSurgeryTargetStepChosen(Entity ent, ref SurgeryStepChosenBuiMsg args) { var user = args.Actor; @@ -497,7 +452,12 @@ private void OnSurgeryTargetStepChosen(Entity ent, ref S { BreakOnUserMove = true, BreakOnTargetMove = true, + CancelDuplicate = true, + DuplicateCondition = DuplicateConditions.SameEvent, + NeedHand = true, + BreakOnHandChange = true, }; + _doAfter.TryStartDoAfter(doAfter); } diff --git a/Content.Shared/Medical/Surgery/SharedSurgerySystem.cs b/Content.Shared/Medical/Surgery/SharedSurgerySystem.cs index 2734b34655a..1f9e1748109 100644 --- a/Content.Shared/Medical/Surgery/SharedSurgerySystem.cs +++ b/Content.Shared/Medical/Surgery/SharedSurgerySystem.cs @@ -2,6 +2,7 @@ using Content.Shared.Medical.Surgery.Conditions; using Content.Shared.Medical.Surgery.Effects.Complete; using Content.Shared.Body.Systems; +using Content.Shared.Medical.Surgery.Steps; using Content.Shared.Medical.Surgery.Steps.Parts; //using Content.Shared._RMC14.Xenonids.Parasite; using Content.Shared.Body.Part; @@ -52,7 +53,6 @@ public override void Initialize() SubscribeLocalEvent(OnRoundRestartCleanup); SubscribeLocalEvent(OnTargetDoAfter); - SubscribeLocalEvent(OnCloseIncisionValid); //SubscribeLocalEvent(OnLarvaValid); SubscribeLocalEvent(OnPartConditionValid); @@ -83,10 +83,9 @@ args.Target is not { } target || Log.Warning($"{ToPrettyString(args.User)} tried to start invalid surgery."); return; } - + args.Repeat = HasComp(step); var ev = new SurgeryStepEvent(args.User, ent, part, GetTools(args.User), surgery); RaiseLocalEvent(step, ref ev); - RefreshUI(ent); } @@ -107,7 +106,7 @@ private void OnWoundedValid(Entity ent, ref Su if (!TryComp(args.Body, out DamageableComponent? damageable) || !TryComp(args.Part, out BodyPartComponent? bodyPart) || damageable.TotalDamage <= 0 - && bodyPart.Integrity == bodyPart.MaxIntegrity + && bodyPart.Integrity == BodyPartComponent.MaxIntegrity && !HasComp(args.Part)) args.Cancelled = true; } @@ -196,13 +195,11 @@ protected bool IsSurgeryValid(EntityUid body, EntityUid targetPart, EntProtoId s !TryComp(surgeryEntId, out SurgeryComponent? surgeryComp) || !surgeryComp.Steps.Contains(stepId) || GetSingleton(stepId) is not { } stepEnt) - { return false; - } + if (!HasComp(targetPart) && !HasComp(targetPart)) - { return false; - } + var ev = new SurgeryValidEvent(body, targetPart); if (_timing.IsFirstTimePredicted) { diff --git a/Content.Shared/Medical/Surgery/Steps/SurgeryRepeatableStepComponent.cs b/Content.Shared/Medical/Surgery/Steps/SurgeryRepeatableStepComponent.cs new file mode 100644 index 00000000000..14010b7e962 --- /dev/null +++ b/Content.Shared/Medical/Surgery/Steps/SurgeryRepeatableStepComponent.cs @@ -0,0 +1,6 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Medical.Surgery.Steps; + +[RegisterComponent, NetworkedComponent] +public sealed partial class SurgeryRepeatableStepComponent : Component; diff --git a/Resources/Locale/en-US/guidebook/guides.ftl b/Resources/Locale/en-US/guidebook/guides.ftl index 84f3d9957f2..e2090d5af0a 100644 --- a/Resources/Locale/en-US/guidebook/guides.ftl +++ b/Resources/Locale/en-US/guidebook/guides.ftl @@ -37,6 +37,10 @@ guide-entry-chef = Chef guide-entry-foodrecipes = Food Recipes guide-entry-medical = Medical guide-entry-medicaldoctor = Medical Doctor +guide-entry-surgery = Surgery +guide-entry-partmanipulation = Part Manipulation +guide-entry-organmanipulation = Organ Manipulation +guide-entry-utilitysurgeries = Utility Surgeries guide-entry-chemist = Chemist guide-entry-medicine = Medicine guide-entry-brute = Advanced Brute Medication diff --git a/Resources/Locale/en-US/surgery/surgery-ui.ftl b/Resources/Locale/en-US/surgery/surgery-ui.ftl index 442763c8b1c..51fd9392c45 100644 --- a/Resources/Locale/en-US/surgery/surgery-ui.ftl +++ b/Resources/Locale/en-US/surgery/surgery-ui.ftl @@ -1,10 +1,11 @@ surgery-ui-window-title = Surgery surgery-ui-window-require = Requires -surgery-ui-window-parts = < Parts -surgery-ui-window-surgeries = < Surgeries -surgery-ui-window-steps = < Steps +surgery-ui-window-parts = < Parts +surgery-ui-window-surgeries = < Surgeries +surgery-ui-window-steps = < Steps surgery-ui-window-steps-error-skills = You have no surgical skills. surgery-ui-window-steps-error-table = You need an operating table for this. surgery-ui-window-steps-error-armor = You need to remove their armor! surgery-ui-window-steps-error-tools = You're missing tools for this surgery. surgery-ui-window-steps-error-laying = They need to be laying down! +surgery-error-self-surgery = You can't perform surgery on yourself! diff --git a/Resources/Prototypes/Entities/Surgery/surgery_steps.yml b/Resources/Prototypes/Entities/Surgery/surgery_steps.yml index d09ed14159d..ede2dce9060 100644 --- a/Resources/Prototypes/Entities/Surgery/surgery_steps.yml +++ b/Resources/Prototypes/Entities/Surgery/surgery_steps.yml @@ -319,6 +319,7 @@ damage: groups: Brute: -5 + - type: SurgeryRepeatableStep - type: entity parent: SurgeryStepBase @@ -337,6 +338,7 @@ damage: groups: Burn: -5 + - type: SurgeryRepeatableStep - type: entity parent: SurgeryStepBase diff --git a/Resources/Prototypes/Guidebook/medical.yml b/Resources/Prototypes/Guidebook/medical.yml index 95a4f1ca75f..8a6a02a69c5 100644 --- a/Resources/Prototypes/Guidebook/medical.yml +++ b/Resources/Prototypes/Guidebook/medical.yml @@ -7,6 +7,7 @@ - Chemist - Cloning - Cryogenics + - Surgery - type: guideEntry id: Medical Doctor @@ -48,3 +49,31 @@ id: AdvancedBrute name: guide-entry-brute text: "/ServerInfo/Guidebook/Medical/AdvancedBrute.xml" + +- type: guideEntry + id: Surgery + name: guide-entry-surgery + text: "/ServerInfo/Guidebook/Medical/Surgery.xml" + children: + - Part Manipulation + - Organ Manipulation + - Utility Surgeries + +- type: guideEntry + id: Part Manipulation + name: guide-entry-partmanipulation + text: "/ServerInfo/Guidebook/Medical/PartManipulation.xml" + filterEnabled: true + +- type: guideEntry + id: Organ Manipulation + name: guide-entry-organmanipulation + text: "/ServerInfo/Guidebook/Medical/OrganManipulation.xml" + filterEnabled: true + +- type: guideEntry + id: Utility Surgeries + name: guide-entry-utilitysurgeries + text: "/ServerInfo/Guidebook/Medical/UtilitySurgeries.xml" + filterEnabled: true + diff --git a/Resources/ServerInfo/Guidebook/Medical/OrganManipulation.xml b/Resources/ServerInfo/Guidebook/Medical/OrganManipulation.xml new file mode 100644 index 00000000000..144d474bcc4 --- /dev/null +++ b/Resources/ServerInfo/Guidebook/Medical/OrganManipulation.xml @@ -0,0 +1,51 @@ + +# Organ Manipulation +These surgeries allow you to remove or replace organs from a patient for healthy ones. Which can help treat conditions on their bodies. + +## Anatomy +Normally a body has the following organs: + + + + + + + + + + + + + +Certain species might have more or less organs, such as Diona having a combined organ that serves multiple functions, but this is the rough outline of what you'll see. +To remove an organ, you'll need to apply the respective Organ Removal surgery on it, which will then remove it without harming the patient. + +However if you want to transplant an organ, you'll need to apply the respective Organ Transplant surgery where it originally was. + +## What does each organ transplant do? + +- Transplanting a [color=#a4885c]brain[/color] will allow the patient to take over another body. An excellent alternative to cloning if you can afford to spare another body. +- Transplanting a [color=#a4885c]liver[/color] will cure severe poisoning from a patient's body. +- Transplanting [color=#a4885c]lungs[/color] will help cure patients from severe asphyxiation. +- Transplanting a [color=#a4885c]heart[/color] will cure a patient's body of rot and decay. +- Transplanting [color=#a4885c]eyes[/color] will allow the patient to see again, and heal any eye damage. + +## Where do I get more organs? + +Normally when your patients come in needing new organs, they'll need to get a new one, as their old ones will be either missing, or damaged beyond repair. +For this you can use the Medical Biofabricator with some Biomass. Which can manufacture Biosynthetic organs for all species. + + + + + + + +By default, every Chief Medical Officer has access to a Medical Biofabricator board in their locker. + +## Why didn't the organ transplant +## do anything? + +Your patient's body rejected the organ as it is in a bad shape, try using a fresh organ from a donor, or getting a new one from the Medical Biofabricator. + + diff --git a/Resources/ServerInfo/Guidebook/Medical/PartManipulation.xml b/Resources/ServerInfo/Guidebook/Medical/PartManipulation.xml new file mode 100644 index 00000000000..2de5facee63 --- /dev/null +++ b/Resources/ServerInfo/Guidebook/Medical/PartManipulation.xml @@ -0,0 +1,51 @@ + +# Part Manipulation +This is how you will turn felinids into humans, or vice versa if you're feeling particularly cruel. + +## Anatomy +Normally a body has 10 main parts. Those being: + + + + + + + + + + + + + + + + + + +Certain species might have more or less parts, such as tails, wings, or extra limbs, but this is the rough outline of what you'll see. +To detach a limb, you'll need to apply the Remove Part surgery on it, which will then remove it without harming the patient. + +However if you want to reattach the limb, you'll need to apply the Attach Part surgery where you want it attached. +Hands are attached to the arms, feet are attached to the legs. And everything else is attached to the torso. + + +## Where do I get more parts? + +Normally when your patients come in needing new limbs, they'll need to get a new one, as their old ones will be either missing, or damaged beyond repair. +For this you can use the Medical Biofabricator with some Biomass. Which can manufacture Biosynthetic limbs for all species. + + + + + + + +By default, every Chief Medical Officer has access to a Medical Biofabricator board in their locker. + +## I reattached my patient's limb, but +## it's not working? + +Your patient has taken enough damage to the point that their limb's nerves were badly damaged, and they need to be healed properly before it can work again. +For this you can try the Tend Wounds surgeries, or letting them use topicals on their wounds. + + diff --git a/Resources/ServerInfo/Guidebook/Medical/Surgery.xml b/Resources/ServerInfo/Guidebook/Medical/Surgery.xml new file mode 100644 index 00000000000..b37005e0038 --- /dev/null +++ b/Resources/ServerInfo/Guidebook/Medical/Surgery.xml @@ -0,0 +1,40 @@ + +# Surgery +Your usual weekly activity. By performing surgical operations on your patients, you can heal wounds, remove or add body parts, organs, and more. + +## The Basics +To start surgery your patient needs to be laying down, and ideally sedated. + +If they are not sedated, they will be in a lot of pain, which decreases their mood, and will make surgical wounds worse. + + + + + + +You also need to wear protective gear such as a mask and gloves to prevent harming the patient due to contamination. + + + + + + +## Getting Started + +To engage in surgery, you'll need a set of surgical tools. + + + + + + + + + + + + + +Once you've got tools in hand, interact with your patient to begin surgery. You'll be able to choose which part you want to operate on. + + diff --git a/Resources/ServerInfo/Guidebook/Medical/UtilitySurgeries.xml b/Resources/ServerInfo/Guidebook/Medical/UtilitySurgeries.xml new file mode 100644 index 00000000000..0a6841e1f34 --- /dev/null +++ b/Resources/ServerInfo/Guidebook/Medical/UtilitySurgeries.xml @@ -0,0 +1,24 @@ + +# Utility Surgeries + +## Tend Wounds + +Tend Wounds is a surgery that allows you to heal brute or burn damage from a patient without the need for topicals. +To begin you need to open an incision on the patient's body, and then apply a Hemostat to it until the part is fully healed. +At which point then you can close the incision with a Cautery. + + + + + + + +This surgery performs best the more damaged your patient is, especially if they are still alive. And allows you to bring otherwise unrecoverable +patients, such as the dumb Technical Assistant that just walked into the burn chamber. + +## Cavity Implant + +This surgery allows you to implant any Tiny or Small item into a patient's torso. To begin you need to open an incision, and then open their ribcage. +Your patient cannot access the implanted item normally, though there may be uses for this, such as hiding the Nuclear Authentication Disk. + +