// SPDX-FileCopyrightText: 2024 Skubman // SPDX-FileCopyrightText: 2025 Aiden <28298836+Aidenkrz@users.noreply.github.com> // SPDX-FileCopyrightText: 2025 Janet Blackquill // SPDX-FileCopyrightText: 2025 Piras314 // SPDX-FileCopyrightText: 2025 deltanedas <39013340+deltanedas@users.noreply.github.com> // SPDX-FileCopyrightText: 2025 deltanedas <@deltanedas:kde.org> // SPDX-FileCopyrightText: 2025 gluesniffler <159397573+gluesniffler@users.noreply.github.com> // SPDX-FileCopyrightText: 2025 gus // // SPDX-License-Identifier: AGPL-3.0-or-later using Content.Shared.Humanoid; using Content.Shared.Humanoid.Markings; using Content.Shared.Body.Part; using Content.Shared.Body.Organ; using Content.Shared._Shitmed.BodyEffects; using Content.Shared._Shitmed.Body.Events; using Content.Shared.Buckle.Components; using Content.Shared.Containers.ItemSlots; using Content.Shared.Damage; using Content.Shared.Damage.Prototypes; using Content.Shared.DoAfter; using Content.Shared.IdentityManagement; using Content.Shared._Shitmed.Medical.Surgery.Conditions; using Content.Shared._Shitmed.Medical.Surgery.Effects.Step; using Content.Shared._Shitmed.Medical.Surgery.Steps; using Content.Shared._Shitmed.Medical.Surgery.Steps.Parts; using Content.Shared._Shitmed.Medical.Surgery.Tools; //using Content.Shared.Mood; using Content.Shared.Inventory; using Content.Shared.Item; using Content.Shared._Shitmed.Body.Organ; using Content.Shared._Shitmed.Body.Part; using Content.Shared.Popups; using Robust.Shared.Prototypes; using System.Linq; namespace Content.Shared._Shitmed.Medical.Surgery; public abstract partial class SharedSurgerySystem { private static readonly string[] BruteDamageTypes = { "Slash", "Blunt", "Piercing" }; private static readonly string[] BurnDamageTypes = { "Heat", "Shock", "Cold", "Caustic" }; private void InitializeSteps() { SubscribeLocalEvent(OnToolStep); SubscribeLocalEvent(OnToolCheck); SubscribeLocalEvent(OnToolCanPerform); //SubSurgery(OnCutLarvaRootsStep, OnCutLarvaRootsCheck); /* Abandon all hope ye who enter here. Now I am become shitcoder, the bloater of files. On a serious note, I really hate how much bloat this pattern of subscribing to a StepEvent and a CheckEvent creates in terms of readability. And while Check DOES only run on the server side, it's still annoying to parse through.*/ SubSurgery(OnTendWoundsStep, OnTendWoundsCheck); SubSurgery(OnCavityStep, OnCavityCheck); SubSurgery(OnAddPartStep, OnAddPartCheck); SubSurgery(OnAffixPartStep, OnAffixPartCheck); SubSurgery(OnRemovePartStep, OnRemovePartCheck); SubSurgery(OnAddOrganStep, OnAddOrganCheck); SubSurgery(OnRemoveOrganStep, OnRemoveOrganCheck); SubSurgery(OnAffixOrganStep, OnAffixOrganCheck); SubSurgery(OnAddMarkingStep, OnAddMarkingCheck); SubSurgery(OnRemoveMarkingStep, OnRemoveMarkingCheck); SubSurgery(OnAddOrganSlotStep, OnAddOrganSlotCheck); Subs.BuiEvents(SurgeryUIKey.Key, subs => { subs.Event(OnSurgeryTargetStepChosen); }); } private void SubSurgery(EntityEventRefHandler onStep, EntityEventRefHandler onComplete) where TComp : IComponent { SubscribeLocalEvent(onStep); SubscribeLocalEvent(onComplete); } #region Event Methods private void OnToolStep(Entity ent, ref SurgeryStepEvent args) { if(!TryToolAudio(ent, args)) return; AddOrRemoveComponentsToEntity(args.Part, ent.Comp.Add); AddOrRemoveComponentsToEntity(args.Part, ent.Comp.Remove, true); AddOrRemoveComponentsToEntity(args.Body, ent.Comp.BodyAdd); AddOrRemoveComponentsToEntity(args.Body, ent.Comp.BodyRemove,true); // Dude this fucking function is so bloated now what the fuck. HandleOrganModification(args.Part, args.Body, ent.Comp.AddOrganOnAdd); HandleOrganModification(args.Part, args.Body, ent.Comp.RemoveOrganOnAdd, false); HandleSanitization(args); } private void OnToolCheck(Entity ent, ref SurgeryStepCompleteCheckEvent args) { if (TryToolCheck(ent.Comp.Add, args.Part) || TryToolCheck(ent.Comp.Remove, args.Part, checkMissing: false) || TryToolCheck(ent.Comp.BodyAdd, args.Body) || TryToolCheck(ent.Comp.BodyRemove, args.Body, checkMissing: false)) { args.Cancelled = true; return; } if (TryToolOrganCheck(ent.Comp.AddOrganOnAdd, args.Part) || TryToolOrganCheck(ent.Comp.RemoveOrganOnAdd, args.Part, checkMissing: false)) args.Cancelled = true; } private void OnToolCanPerform(Entity ent, ref SurgeryCanPerformStepEvent args) { if (HasComp(ent)) { if (!TryComp(args.Body, out BuckleComponent? buckle) || !HasComp(buckle.BuckledTo)) { args.Invalid = StepInvalidReason.NeedsOperatingTable; return; } } if (_inventory.TryGetContainerSlotEnumerator(args.Body, out var containerSlotEnumerator, args.TargetSlots)) { if (HasComp(args.User)) return; while (containerSlotEnumerator.MoveNext(out var containerSlot)) { if (!containerSlot.ContainedEntity.HasValue) continue; args.Invalid = StepInvalidReason.Armor; args.Popup = Loc.GetString("surgery-ui-window-steps-error-armor"); return; } } RaiseLocalEvent(args.Body, ref args); if (args.Invalid != StepInvalidReason.None) return; if (ent.Comp.Tool != null) { args.ValidTools ??= new Dictionary(); foreach (var reg in ent.Comp.Tool.Values) { if (!AnyHaveComp(args.Tools, reg.Component, out var tool, out var speed)) { args.Invalid = StepInvalidReason.MissingTool; if (reg.Component is ISurgeryToolComponent required) args.Popup = $"You need {required.ToolName} to perform this step!"; return; } args.ValidTools[tool] = speed; } } } private void OnTendWoundsStep(Entity ent, ref SurgeryStepEvent args) { var group = ent.Comp.MainGroup == "Brute" ? BruteDamageTypes : BurnDamageTypes; if (!HasDamageGroup(args.Body, group, out var damageable) && !HasDamageGroup(args.Part, group, out var _) || damageable == null) // This shouldnt be possible but the compiler doesn't shut up. return; // Right now the bonus is based off the body's total damage, maybe we could make it based off each part in the future. var bonus = ent.Comp.HealMultiplier * damageable.DamagePerGroup[ent.Comp.MainGroup]; if (_mobState.IsDead(args.Body)) bonus *= 0.2; var adjustedDamage = new DamageSpecifier(ent.Comp.Damage); foreach (var type in group) adjustedDamage.DamageDict[type] -= bonus; var ev = new SurgeryStepDamageEvent(args.User, args.Body, args.Part, args.Surgery, adjustedDamage, 0.5f); RaiseLocalEvent(args.Body, ref ev); } private void OnTendWoundsCheck(Entity ent, ref SurgeryStepCompleteCheckEvent args) { var group = ent.Comp.MainGroup == "Brute" ? BruteDamageTypes : BurnDamageTypes; if (HasDamageGroup(args.Body, group, out var _) || HasDamageGroup(args.Part, group, out var _)) args.Cancelled = true; } /*private void OnCutLarvaRootsStep(Entity ent, ref SurgeryStepEvent args) { if (TryComp(args.Body, out VictimInfectedComponent? infected) && infected.BurstAt > _timing.CurTime && infected.SpawnedLarva == null) { infected.RootsCut = true; } } private void OnCutLarvaRootsCheck(Entity ent, ref SurgeryStepCompleteCheckEvent args) { if (!TryComp(args.Body, out VictimInfectedComponent? infected) || !infected.RootsCut) args.Cancelled = true; // The larva has fully developed and surgery is now impossible // TODO: Surgery should still be possible, but the fully developed larva should escape while also saving the hosts life if (infected != null && infected.SpawnedLarva != null) args.Cancelled = true; }*/ private void OnCavityStep(Entity ent, ref SurgeryStepEvent args) { if (!TryComp(args.Part, out BodyPartComponent? partComp) || partComp.PartType != BodyPartType.Torso) return; var activeHandEntity = _hands.EnumerateHeld(args.User).FirstOrDefault(); if (activeHandEntity != default && ent.Comp.Action == "Insert" && TryComp(activeHandEntity, out ItemComponent? itemComp) && (itemComp.Size.Id == "Tiny" || itemComp.Size.Id == "Small")) _itemSlotsSystem.TryInsert(ent, partComp.ItemInsertionSlot, activeHandEntity, args.User); else if (ent.Comp.Action == "Remove") _itemSlotsSystem.TryEjectToHands(ent, partComp.ItemInsertionSlot, args.User); } private void OnCavityCheck(Entity ent, ref SurgeryStepCompleteCheckEvent args) { // Normally this check would simply be partComp.ItemInsertionSlot.HasItem, but as mentioned before, // For whatever reason it's not instantiating the field on the clientside after the wizmerge. if (!TryComp(args.Part, out BodyPartComponent? partComp) || !TryComp(args.Part, out ItemSlotsComponent? itemComp) || ent.Comp.Action == "Insert" && !itemComp.Slots[partComp.ContainerName].HasItem || ent.Comp.Action == "Remove" && itemComp.Slots[partComp.ContainerName].HasItem) args.Cancelled = true; } private void OnAddPartStep(Entity ent, ref SurgeryStepEvent args) { if (!TryComp(args.Surgery, out SurgeryPartRemovedConditionComponent? removedComp)) return; foreach (var tool in args.Tools) { if (TryComp(tool, out BodyPartComponent? partComp) && partComp.PartType == removedComp.Part && (removedComp.Symmetry == null || partComp.Symmetry == removedComp.Symmetry)) { var slotName = removedComp.Symmetry != null ? $"{removedComp.Symmetry?.ToString().ToLower()} {removedComp.Part.ToString().ToLower()}" : removedComp.Part.ToString().ToLower(); _body.TryCreatePartSlot(args.Part, slotName, partComp.PartType, out var _); _body.AttachPart(args.Part, slotName, tool); EnsureComp(tool); var ev = new BodyPartAttachedEvent((tool, partComp)); RaiseLocalEvent(args.Body, ref ev); } } } private void OnAddOrganSlotStep(Entity ent, ref SurgeryStepEvent args) { if (!TryComp(args.Surgery, out SurgeryOrganSlotConditionComponent? condition)) return; _body.TryCreateOrganSlot(args.Part, condition.OrganSlot, out _); } private void OnAddOrganSlotCheck(Entity ent, ref SurgeryStepCompleteCheckEvent args) { if (!TryComp(args.Surgery, out SurgeryOrganSlotConditionComponent? condition)) return; args.Cancelled = !_body.CanInsertOrgan(args.Part, condition.OrganSlot); } private void OnAffixPartStep(Entity ent, ref SurgeryStepEvent args) { if (!TryComp(args.Surgery, out SurgeryPartRemovedConditionComponent? removedComp)) return; var targetPart = _body.GetBodyChildrenOfType(args.Body, removedComp.Part, symmetry: removedComp.Symmetry).FirstOrDefault(); if (targetPart != default) { // We reward players for properly affixing the parts by healing a little bit of damage, and enabling the part temporarily. var ev = new BodyPartEnableChangedEvent(true); RaiseLocalEvent(targetPart.Id, ref ev); _damageable.TryChangeDamage(args.Body, _body.GetHealingSpecifier(targetPart.Component) * 2, canSever: false, // Just in case we heal a brute damage specifier and the logic gets fucky lol targetPart: _body.GetTargetBodyPart(targetPart.Component.PartType, targetPart.Component.Symmetry)); RemComp(targetPart.Id); } } private void OnAffixPartCheck(Entity ent, ref SurgeryStepCompleteCheckEvent args) { if (!TryComp(args.Surgery, out SurgeryPartRemovedConditionComponent? removedComp)) return; var targetPart = _body.GetBodyChildrenOfType(args.Body, removedComp.Part, symmetry: removedComp.Symmetry).FirstOrDefault(); if (targetPart != default && HasComp(targetPart.Id)) args.Cancelled = true; } private void OnAddPartCheck(Entity ent, ref SurgeryStepCompleteCheckEvent args) { if (!TryComp(args.Surgery, out SurgeryPartRemovedConditionComponent? removedComp) || !_body.GetBodyChildrenOfType(args.Body, removedComp.Part, symmetry: removedComp.Symmetry).Any()) args.Cancelled = true; } private void OnRemovePartStep(Entity ent, ref SurgeryStepEvent args) { if (!TryComp(args.Part, out BodyPartComponent? partComp) || partComp.Body != args.Body) return; var ev = new AmputateAttemptEvent(args.Part); RaiseLocalEvent(args.Part, ref ev); _hands.TryPickupAnyHand(args.User, args.Part); } private void OnRemovePartCheck(Entity ent, ref SurgeryStepCompleteCheckEvent args) { if (!TryComp(args.Part, out BodyPartComponent? partComp) || partComp.Body == args.Body) args.Cancelled = true; } private void OnAddOrganStep(Entity ent, ref SurgeryStepEvent args) { if (!TryComp(args.Part, out BodyPartComponent? partComp) || partComp.Body != args.Body || !TryComp(args.Surgery, out SurgeryOrganConditionComponent? organComp) || organComp.Organ == null) return; // Adding organs is generally done for a single one at a time, so we only need to check for the first. var firstOrgan = organComp.Organ.Values.FirstOrDefault(); if (firstOrgan == default) return; foreach (var tool in args.Tools) { if (HasComp(tool, firstOrgan.Component.GetType()) && TryComp(tool, out var insertedOrgan) && _body.InsertOrgan(args.Part, tool, insertedOrgan.SlotId, partComp, insertedOrgan)) { EnsureComp(tool); if (_body.TrySetOrganUsed(tool, true, insertedOrgan) && insertedOrgan.OriginalBody != args.Body) { var ev = new SurgeryStepDamageChangeEvent(args.User, args.Body, args.Part, ent); RaiseLocalEvent(ent, ref ev); args.Complete = true; } break; } } } private void OnAddOrganCheck(Entity ent, ref SurgeryStepCompleteCheckEvent args) { if (!TryComp(args.Surgery, out var organComp) || organComp.Organ is null || !TryComp(args.Part, out BodyPartComponent? partComp) || partComp.Body != args.Body) return; // For now we naively assume that every entity will only have one of each organ type. // that we do surgery on, but in the future we'll need to reference their prototype somehow // to know if they need 2 hearts, 2 lungs, etc. foreach (var reg in organComp.Organ.Values) { if (!_body.TryGetBodyPartOrgans(args.Part, reg.Component.GetType(), out var _)) { args.Cancelled = true; } } } private void OnAffixOrganStep(Entity ent, ref SurgeryStepEvent args) { if (!TryComp(args.Surgery, out SurgeryOrganConditionComponent? removedOrganComp) || removedOrganComp.Organ == null || !removedOrganComp.Reattaching) return; foreach (var reg in removedOrganComp.Organ.Values) { _body.TryGetBodyPartOrgans(args.Part, reg.Component.GetType(), out var organs); if (organs != null && organs.Count > 0) RemComp(organs[0].Id); } } private void OnAffixOrganCheck(Entity ent, ref SurgeryStepCompleteCheckEvent args) { if (!TryComp(args.Surgery, out SurgeryOrganConditionComponent? removedOrganComp) || removedOrganComp.Organ == null || !removedOrganComp.Reattaching) return; foreach (var reg in removedOrganComp.Organ.Values) { _body.TryGetBodyPartOrgans(args.Part, reg.Component.GetType(), out var organs); if (organs != null && organs.Count > 0 && organs.Any(organ => HasComp(organ.Id))) args.Cancelled = true; } } private void OnRemoveOrganStep(Entity ent, ref SurgeryStepEvent args) { if (!TryComp(args.Surgery, out var organComp) || organComp.Organ == null) return; foreach (var reg in organComp.Organ.Values) { _body.TryGetBodyPartOrgans(args.Part, reg.Component.GetType(), out var organs); if (organs != null && organs.Count > 0) { _body.RemoveOrgan(organs[0].Id, organs[0].Organ); _hands.TryPickupAnyHand(args.User, organs[0].Id); } } } private void OnRemoveOrganCheck(Entity ent, ref SurgeryStepCompleteCheckEvent args) { if (!TryComp(args.Surgery, out var organComp) || organComp.Organ == null || !TryComp(args.Part, out BodyPartComponent? partComp) || partComp.Body != args.Body) return; foreach (var reg in organComp.Organ.Values) { if (_body.TryGetBodyPartOrgans(args.Part, reg.Component.GetType(), out var organs) && organs != null && organs.Count > 0) { args.Cancelled = true; return; } } } // TODO: Refactor bodies to include ears as a prototype instead of doing whatever the hell this is. private void OnAddMarkingStep(Entity ent, ref SurgeryStepEvent args) { if (!TryComp(args.Body, out HumanoidAppearanceComponent? bodyAppearance) || ent.Comp.Organ == null) return; var organType = ent.Comp.Organ.Values.FirstOrDefault(); if (organType == default) return; var markingCategory = MarkingCategoriesConversion.FromHumanoidVisualLayers(ent.Comp.MarkingCategory); foreach (var tool in args.Tools) { if (TryComp(tool, out MarkingContainerComponent? markingComp) && HasComp(tool, organType.Component.GetType())) { if (!bodyAppearance.MarkingSet.Markings.TryGetValue(markingCategory, out var markingList) || !markingList.Any(marking => marking.MarkingId.Contains(ent.Comp.MatchString))) { EnsureComp(args.Part); _body.ModifyMarkings(args.Body, args.Part, bodyAppearance, ent.Comp.MarkingCategory, markingComp.Marking); if (ent.Comp.Accent != null && ent.Comp.Accent.Values.FirstOrDefault() is { } accent) { var compType = accent.Component.GetType(); if (!HasComp(args.Body, compType)) AddComp(args.Body, _compFactory.GetComponent(compType)); } QueueDel(tool); // Again since this isnt actually being inserted we just delete it lol. } } } } private void OnAddMarkingCheck(Entity ent, ref SurgeryStepCompleteCheckEvent args) { var markingCategory = MarkingCategoriesConversion.FromHumanoidVisualLayers(ent.Comp.MarkingCategory); if (!TryComp(args.Body, out HumanoidAppearanceComponent? bodyAppearance) || !bodyAppearance.MarkingSet.Markings.TryGetValue(markingCategory, out var markingList) || !markingList.Any(marking => marking.MarkingId.Contains(ent.Comp.MatchString))) args.Cancelled = true; } private void OnRemoveMarkingStep(Entity ent, ref SurgeryStepEvent args) { } private void OnRemoveMarkingCheck(Entity ent, ref SurgeryStepCompleteCheckEvent args) { } private void OnSurgeryTargetStepChosen(Entity ent, ref SurgeryStepChosenBuiMsg args) { var user = args.Actor; if (GetEntity(args.Entity) is {} body && GetEntity(args.Part) is {} targetPart) { TryDoSurgeryStep(body, targetPart, user, args.Surgery, args.Step); } } #endregion #region Helper Methods // I wonder if theres not a function that can do this already. private bool HasDamageGroup(EntityUid entity, string[] group, out DamageableComponent? damageable) { if (!TryComp(entity, out var damageableComp)) { damageable = null; return false; } damageable = damageableComp; return group.Any(damageType => damageableComp.Damage.DamageDict.TryGetValue(damageType, out var value) && value > 0); } private void HandleSanitization(SurgeryStepEvent args) { if (_inventory.TryGetSlotEntity(args.User, "gloves", out var _) && _inventory.TryGetSlotEntity(args.User, "mask", out var _)) return; if (HasComp(args.User)) return; var sepsis = new DamageSpecifier(_prototypes.Index("Poison"), 5); var ev = new SurgeryStepDamageEvent(args.User, args.Body, args.Part, args.Surgery, sepsis, 0.5f); RaiseLocalEvent(args.Body, ref ev); } private bool TryToolAudio(Entity ent, SurgeryStepEvent args) { if (ent.Comp.Tool == null) return true; foreach (var reg in ent.Comp.Tool.Values) { if (!AnyHaveComp(args.Tools, reg.Component, out var tool, out _)) return false; if (_net.IsServer && TryComp(tool, out SurgeryToolComponent? toolComp) && toolComp.EndSound != null) { _audio.PlayPvs(toolComp.EndSound, tool); } } return true; } private void HandleOrganModification(EntityUid organTarget, EntityUid bodyTarget, Dictionary? modifications, bool remove = false) { if (modifications == null) return; var organSlotIdToOrgan = _body.GetPartOrgans(organTarget).ToDictionary(o => o.Item2.SlotId, o => o); foreach (var (slotId, components) in modifications) { if (!organSlotIdToOrgan.TryGetValue(slotId, out var organValue)) continue; var (organId, organ) = organValue; if (remove) { if (organValue.Item2.OnAdd == null || organ.OnAdd == null) continue; RaiseLocalEvent(organId, new OrganComponentsModifyEvent(bodyTarget, false)); foreach (var key in components.Keys) organ.OnAdd.Remove(key); } else { organ.OnAdd ??= new ComponentRegistry(); foreach (var (key, compToAdd) in components) organ.OnAdd[key] = compToAdd; EnsureComp(organId); RaiseLocalEvent(organId, new OrganComponentsModifyEvent(bodyTarget, true)); } } } private void AddOrRemoveComponentsToEntity(EntityUid ent, ComponentRegistry? componentRegistry, bool remove = false) { if(componentRegistry == null) return; foreach (var reg in componentRegistry.Values) { var compType = reg.Component.GetType(); if (remove) RemComp(ent, compType); else { if (HasComp(ent, compType)) continue; AddComp(ent, _compFactory.GetComponent(compType)); } } } private bool TryToolCheck(ComponentRegistry? components, EntityUid target, bool checkMissing = true) { if (components == null) return false; foreach (var (key,entry) in components) { var hasComponent = HasComp(target, entry.Component.GetType()); if (checkMissing != hasComponent) return true; // Early exit if condition fails } return false; } private bool TryToolOrganCheck(IReadOnlyDictionary? organChanges, EntityUid part, bool checkMissing = true) { if (organChanges == null) return false; var organSlotIdToOrgan = _body.GetPartOrgans(part).ToDictionary(o => o.Item2.SlotId, o => o.Item2); foreach (var (organSlotId, compsToAdd) in organChanges) { if (!organSlotIdToOrgan.TryGetValue(organSlotId, out var organ)) continue; if (checkMissing) { if (organ.OnAdd == null || compsToAdd.Keys.Any(key => !organ.OnAdd.ContainsKey(key))) { return true; } } else { if (organ.OnAdd == null) continue; if (compsToAdd.Keys.Any(key => organ.OnAdd != null && organ.OnAdd.ContainsKey(key))) { return true; } } } return false; } /// /// Do a surgery step on a part, if it can be done. /// Returns true if it succeeded. /// public bool TryDoSurgeryStep(EntityUid body, EntityUid targetPart, EntityUid user, EntProtoId surgeryId, EntProtoId stepId) { if (!IsSurgeryValid(body, targetPart, surgeryId, stepId, user, out var surgery, out var part, out var step)) return false; if (!PreviousStepsComplete(body, part, surgery, stepId) || IsStepComplete(body, part, stepId, surgery)) return false; if (!CanPerformStep(user, body, part, step, true, out _, out _, out var validTools)) return false; var speed = 1f; var usedEv = new SurgeryToolUsedEvent(user, body); // We need to check for nullability because of surgeries that dont require a tool, like Cavity Implants if (validTools?.Count > 0) { foreach (var (tool, toolSpeed) in validTools) { RaiseLocalEvent(tool, ref usedEv); if (usedEv.Cancelled) return false; speed *= toolSpeed; } if (_net.IsServer) { foreach (var tool in validTools.Keys) { if (TryComp(tool, out SurgeryToolComponent? toolComp) && toolComp.StartSound != null) { _audio.PlayPvs(toolComp.StartSound, tool); } } } } if (TryComp(body, out TransformComponent? xform)) _rotateToFace.TryFaceCoordinates(user, _transform.GetMapCoordinates(body, xform).Position); var ev = new SurgeryDoAfterEvent(surgeryId, stepId); // TODO: Move 2 seconds to a field of SurgeryStepComponent var duration = GetSurgeryDuration(step, user, body, speed); if (TryComp(user, out SurgerySpeedModifierComponent? surgerySpeedMod) && surgerySpeedMod is not null) duration = duration / surgerySpeedMod.SpeedModifier; var doAfter = new DoAfterArgs(EntityManager, user, TimeSpan.FromSeconds(duration), ev, body, part) { BreakOnMove = true, //BreakOnTargetMove = true, I fucking hate wizden dude. CancelDuplicate = true, DuplicateCondition = DuplicateConditions.SameEvent, NeedHand = true, BreakOnHandChange = true, }; if (!_doAfter.TryStartDoAfter(doAfter)) return false; var userName = Identity.Entity(user, EntityManager); var targetName = Identity.Entity(body, EntityManager); var locName = $"surgery-popup-procedure-{surgeryId}-step-{stepId}"; var locResult = Loc.GetString(locName, ("user", userName), ("target", targetName), ("part", part)); if (locResult == locName) locResult = Loc.GetString($"surgery-popup-step-{stepId}", ("user", userName), ("target", targetName), ("part", part)); _popup.PopupEntity(locResult, user); return true; } private float GetSurgeryDuration(EntityUid surgeryStep, EntityUid user, EntityUid target, float toolSpeed) { if (!TryComp(surgeryStep, out SurgeryStepComponent? stepComp)) return 2f; // Shouldnt really happen but just a failsafe. var speed = toolSpeed; if (TryComp(user, out SurgerySpeedModifierComponent? surgerySpeedMod)) speed *= surgerySpeedMod.SpeedModifier; return stepComp.Duration / speed; } private (Entity Surgery, int Step)? GetNextStep(EntityUid body, EntityUid part, Entity surgery, List requirements) { if (!Resolve(surgery, ref surgery.Comp)) return null; if (requirements.Contains(surgery)) throw new ArgumentException($"Surgery {surgery} has a requirement loop: {string.Join(", ", requirements)}"); requirements.Add(surgery); if (surgery.Comp.Requirement is { } requirementId && GetSingleton(requirementId) is { } requirement && GetNextStep(body, part, requirement, requirements) is { } requiredNext) { return requiredNext; } for (var i = 0; i < surgery.Comp.Steps.Count; i++) { var surgeryStep = surgery.Comp.Steps[i]; if (!IsStepComplete(body, part, surgeryStep, surgery)) return ((surgery, surgery.Comp), i); } return null; } public (Entity Surgery, int Step)? GetNextStep(EntityUid body, EntityUid part, EntityUid surgery) { return GetNextStep(body, part, surgery, new List()); } public bool PreviousStepsComplete(EntityUid body, EntityUid part, Entity surgery, EntProtoId step) { // TODO RMC14 use index instead of the prototype id if (surgery.Comp.Requirement is { } requirement) { if (GetSingleton(requirement) is not { } requiredEnt || !TryComp(requiredEnt, out SurgeryComponent? requiredComp) || !PreviousStepsComplete(body, part, (requiredEnt, requiredComp), step)) { return false; } } foreach (var surgeryStep in surgery.Comp.Steps) { if (surgeryStep == step) break; if (!IsStepComplete(body, part, surgeryStep, surgery)) return false; } return true; } public bool CanPerformStep(EntityUid user, EntityUid body, EntityUid part, EntityUid step, bool doPopup, out string? popup, out StepInvalidReason reason, out Dictionary? validTools) { var type = BodyPartType.Other; if (TryComp(part, out BodyPartComponent? partComp)) { type = partComp.PartType; } var slot = type switch { BodyPartType.Head => SlotFlags.HEAD, BodyPartType.Torso => SlotFlags.OUTERCLOTHING | SlotFlags.INNERCLOTHING, BodyPartType.Arm => SlotFlags.OUTERCLOTHING | SlotFlags.INNERCLOTHING, BodyPartType.Hand => SlotFlags.GLOVES, BodyPartType.Leg => SlotFlags.OUTERCLOTHING | SlotFlags.LEGS, BodyPartType.Foot => SlotFlags.FEET, BodyPartType.Tail => SlotFlags.NONE, BodyPartType.Other => SlotFlags.NONE, _ => SlotFlags.NONE }; var check = new SurgeryCanPerformStepEvent(user, body, GetTools(user), slot); RaiseLocalEvent(step, ref check); popup = check.Popup; validTools = check.ValidTools; if (check.Invalid != StepInvalidReason.None) { if (doPopup && check.Popup != null) _popup.PopupEntity(check.Popup, user, user, PopupType.SmallCaution); reason = check.Invalid; return false; } reason = default; return true; } public bool CanPerformStep(EntityUid user, EntityUid body, EntityUid part, EntityUid step, bool doPopup) { return CanPerformStep(user, body, part, step, doPopup, out _, out _, out _); } public bool IsStepComplete(EntityUid body, EntityUid part, EntProtoId step, EntityUid surgery) { if (GetSingleton(step) is not { } stepEnt) return false; var ev = new SurgeryStepCompleteCheckEvent(body, part, surgery); RaiseLocalEvent(stepEnt, ref ev); return !ev.Cancelled; } private bool AnyHaveComp(List tools, IComponent component, out EntityUid withComp, out float speed) { foreach (var tool in tools) { if (EntityManager.TryGetComponent(tool, component.GetType(), out var found) && found is ISurgeryToolComponent toolComp) { withComp = tool; speed = toolComp.Speed; return true; } } withComp = EntityUid.Invalid; speed = 1f; return false; } #endregion }