diff --git a/Content.Shared/Body/Organ/OrganComponent.cs b/Content.Shared/Body/Organ/OrganComponent.cs index 310a3f04a8..f0e8c22eba 100644 --- a/Content.Shared/Body/Organ/OrganComponent.cs +++ b/Content.Shared/Body/Organ/OrganComponent.cs @@ -1,13 +1,14 @@ using Content.Shared.Body.Systems; using Robust.Shared.Containers; using Robust.Shared.GameStates; +using Content.Shared.Medical.Surgery; using Content.Shared.Medical.Surgery.Tools; using Robust.Shared.Prototypes; namespace Content.Shared.Body.Organ; [RegisterComponent, NetworkedComponent, AutoGenerateComponentState] -[Access(typeof(SharedBodySystem))] +[Access(typeof(SharedBodySystem), typeof(SharedSurgerySystem))] public sealed partial class OrganComponent : Component, ISurgeryToolComponent { /// @@ -50,7 +51,7 @@ public sealed partial class OrganComponent : Component, ISurgeryToolComponent public ComponentRegistry? OnAdd; /// - /// When removed, the organ will ensure these components on the entity, and add them on removal. + /// When removed, the organ will ensure these components on the entity, and delete them on insertion. /// [DataField] public ComponentRegistry? OnRemove; diff --git a/Content.Shared/Medical/Surgery/Conditions/SurgeryBodyComponentConditionComponent.cs b/Content.Shared/Medical/Surgery/Conditions/SurgeryBodyComponentConditionComponent.cs new file mode 100644 index 0000000000..26b364c2cd --- /dev/null +++ b/Content.Shared/Medical/Surgery/Conditions/SurgeryBodyComponentConditionComponent.cs @@ -0,0 +1,24 @@ +using Content.Shared.Body.Part; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Medical.Surgery.Conditions; + +// +// What components are necessary in the body for the surgery to be valid. +// +[RegisterComponent, NetworkedComponent] +public sealed partial class SurgeryBodyComponentConditionComponent : Component +{ + // + // The components to check for. + // + [DataField(required: true)] + public ComponentRegistry Components; + + // + // If true, the lack of these components will instead make the surgery valid. + // + [DataField] + public bool Inverse = false; +} diff --git a/Content.Shared/Medical/Surgery/Conditions/SurgeryComponentConditionComponent.cs b/Content.Shared/Medical/Surgery/Conditions/SurgeryComponentConditionComponent.cs deleted file mode 100644 index 3f0fb6305c..0000000000 --- a/Content.Shared/Medical/Surgery/Conditions/SurgeryComponentConditionComponent.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Content.Shared.Body.Part; -using Robust.Shared.GameStates; -using Robust.Shared.Prototypes; - -namespace Content.Shared.Medical.Surgery.Conditions; - -[RegisterComponent, NetworkedComponent] -public sealed partial class SurgeryComponentConditionComponent : Component -{ - [DataField] - public ComponentRegistry Component; - - [DataField] - public bool Inverse; - -} \ No newline at end of file diff --git a/Content.Shared/Medical/Surgery/Conditions/SurgeryOrganOnAddConditionComponent.cs b/Content.Shared/Medical/Surgery/Conditions/SurgeryOrganOnAddConditionComponent.cs new file mode 100644 index 0000000000..62d9464971 --- /dev/null +++ b/Content.Shared/Medical/Surgery/Conditions/SurgeryOrganOnAddConditionComponent.cs @@ -0,0 +1,26 @@ +using Content.Shared.Body.Part; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Medical.Surgery.Conditions; + +// +// What components are necessary in the part's organs' OnAdd fields for the surgery to be valid. +// +// Not all components need to be present (or missing for Inverse = true). At least one component matching (or missing) can make the surgery valid. +// +[RegisterComponent, NetworkedComponent] +public sealed partial class SurgeryOrganOnAddConditionComponent : Component +{ + // + // The components to check for on each organ, with the key being the organ's SlotId. + // + [DataField(required: true)] + public Dictionary Components; + + // + // If true, the lack of these components will instead make the surgery valid. + // + [DataField] + public bool Inverse = false; +} diff --git a/Content.Shared/Medical/Surgery/Conditions/SurgeryPartComponentConditionComponent.cs b/Content.Shared/Medical/Surgery/Conditions/SurgeryPartComponentConditionComponent.cs new file mode 100644 index 0000000000..474cb9e0ec --- /dev/null +++ b/Content.Shared/Medical/Surgery/Conditions/SurgeryPartComponentConditionComponent.cs @@ -0,0 +1,24 @@ +using Content.Shared.Body.Part; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Medical.Surgery.Conditions; + +// +// What components are necessary in the targeted body part for the surgery to be valid. +// +[RegisterComponent, NetworkedComponent] +public sealed partial class SurgeryPartComponentConditionComponent : Component +{ + // + // The components to check for. + // + [DataField(required: true)] + public ComponentRegistry Components; + + // + // If true, the lack of these components will instead make the surgery valid. + // + [DataField] + public bool Inverse = false; +} diff --git a/Content.Shared/Medical/Surgery/SharedSurgerySystem.Steps.cs b/Content.Shared/Medical/Surgery/SharedSurgerySystem.Steps.cs index 476a23a21b..c914c13514 100644 --- a/Content.Shared/Medical/Surgery/SharedSurgerySystem.Steps.cs +++ b/Content.Shared/Medical/Surgery/SharedSurgerySystem.Steps.cs @@ -4,6 +4,7 @@ using Content.Shared.Body.Part; using Content.Shared.Body.Organ; using Content.Shared.Body.Events; +using Content.Shared.BodyEffects; using Content.Shared.Buckle.Components; using Content.Shared.Containers.ItemSlots; using Content.Shared.Damage; @@ -120,6 +121,45 @@ private void OnToolStep(Entity ent, ref SurgeryStepEvent a } } + if (ent.Comp.AddOrganOnAdd != null) + { + var organSlotIdToOrgan = _body.GetPartOrgans(args.Part).ToDictionary(o => o.Item2.SlotId, o => o); + + foreach (var (organSlotId, compsToAdd) in ent.Comp.AddOrganOnAdd) + { + if (!organSlotIdToOrgan.TryGetValue(organSlotId, out var organValue)) + continue; + var (organId, organ) = organValue; + + organ.OnAdd ??= new(); + + foreach (var (key, compToAdd) in compsToAdd) + organ.OnAdd[key] = compToAdd; + + EnsureComp(organId); + RaiseLocalEvent(organId, new OrganComponentsModifyEvent(args.Body, true)); + } + } + + if (ent.Comp.RemoveOrganOnAdd != null) + { + var organSlotIdToOrgan = _body.GetPartOrgans(args.Part).ToDictionary(o => o.Item2.SlotId, o => o); + + foreach (var (organSlotId, compsToRemove) in ent.Comp.RemoveOrganOnAdd) + { + if (!organSlotIdToOrgan.TryGetValue(organSlotId, out var organValue) || + organValue.Item2.OnAdd == null) + continue; + var (organId, organ) = organValue; + + // Need to raise this event first before removing the component entries so + // OrganEffectSystem still knows which components on the body to remove + RaiseLocalEvent(organId, new OrganComponentsModifyEvent(args.Body, false)); + foreach (var key in compsToRemove.Keys) + organ.OnAdd.Remove(key); + } + } + if (!HasComp(args.Body)) RaiseLocalEvent(args.Body, new MoodEffectEvent("SurgeryPain")); @@ -182,6 +222,38 @@ private void OnToolCheck(Entity ent, ref SurgeryStepComple } } } + + if (ent.Comp.AddOrganOnAdd != null) + { + var organSlotIdToOrgan = _body.GetPartOrgans(args.Part).ToDictionary(o => o.Item2.SlotId, o => o.Item2); + foreach (var (organSlotId, compsToAdd) in ent.Comp.AddOrganOnAdd) + { + if (!organSlotIdToOrgan.TryGetValue(organSlotId, out var organ)) + continue; + + if (organ.OnAdd == null || compsToAdd.Keys.Any(key => !organ.OnAdd.ContainsKey(key))) + { + args.Cancelled = true; + return; + } + } + } + + if (ent.Comp.RemoveOrganOnAdd != null) + { + var organSlotIdToOrgan = _body.GetPartOrgans(args.Part).ToDictionary(o => o.Item2.SlotId, o => o.Item2); + foreach (var (organSlotId, compsToRemove) in ent.Comp.RemoveOrganOnAdd) + { + if (!organSlotIdToOrgan.TryGetValue(organSlotId, out var organ) || organ.OnAdd == null) + continue; + + if (compsToRemove.Keys.Any(key => organ.OnAdd.ContainsKey(key))) + { + args.Cancelled = true; + return; + } + } + } } private void OnToolCanPerform(Entity ent, ref SurgeryCanPerformStepEvent args) diff --git a/Content.Shared/Medical/Surgery/SharedSurgerySystem.cs b/Content.Shared/Medical/Surgery/SharedSurgerySystem.cs index db8f65c33a..74b4ce9648 100644 --- a/Content.Shared/Medical/Surgery/SharedSurgerySystem.cs +++ b/Content.Shared/Medical/Surgery/SharedSurgerySystem.cs @@ -58,9 +58,11 @@ public override void Initialize() SubscribeLocalEvent(OnTargetDoAfter); SubscribeLocalEvent(OnCloseIncisionValid); //SubscribeLocalEvent(OnLarvaValid); - SubscribeLocalEvent(OnComponentConditionValid); + SubscribeLocalEvent(OnBodyComponentConditionValid); + SubscribeLocalEvent(OnPartComponentConditionValid); SubscribeLocalEvent(OnPartConditionValid); SubscribeLocalEvent(OnOrganConditionValid); + SubscribeLocalEvent(OnOrganOnAddConditionValid); SubscribeLocalEvent(OnWoundedValid); SubscribeLocalEvent(OnPartRemovedConditionValid); SubscribeLocalEvent(OnPartPresentConditionValid); @@ -129,10 +131,24 @@ private void OnWoundedValid(Entity ent, ref Su args.Cancelled = true; }*/ - private void OnComponentConditionValid(Entity ent, ref SurgeryValidEvent args) + private void OnBodyComponentConditionValid(Entity ent, ref SurgeryValidEvent args) { var present = true; - foreach (var reg in ent.Comp.Component.Values) + foreach (var reg in ent.Comp.Components.Values) + { + var compType = reg.Component.GetType(); + if (!HasComp(args.Body, compType)) + present = false; + } + + if (ent.Comp.Inverse ? present : !present) + args.Cancelled = true; + } + + private void OnPartComponentConditionValid(Entity ent, ref SurgeryValidEvent args) + { + var present = true; + foreach (var reg in ent.Comp.Components.Values) { var compType = reg.Component.GetType(); if (!HasComp(args.Part, compType)) @@ -185,6 +201,45 @@ private void OnOrganConditionValid(Entity ent, r } } + // This is literally a duplicate of the checks in OnToolCheck for SurgeryStepComponent.AddOrganOnAdd + private void OnOrganOnAddConditionValid(Entity ent, ref SurgeryValidEvent args) + { + if (!TryComp(args.Part, out var part) + || part.Body != args.Body) + { + args.Cancelled = true; + return; + } + + var organSlotIdToOrgan = _body.GetPartOrgans(args.Part, part).ToDictionary(o => o.Item2.SlotId, o => o.Item2); + + var allOnAddFound = true; + var zeroOnAddFound = true; + + foreach (var (organSlotId, components) in ent.Comp.Components) + { + if (!organSlotIdToOrgan.TryGetValue(organSlotId, out var organ)) + continue; + + if (organ.OnAdd == null) + { + allOnAddFound = false; + continue; + } + + foreach (var key in components.Keys) + { + if (!organ.OnAdd.ContainsKey(key)) + allOnAddFound = false; + else + zeroOnAddFound = false; + } + } + + if (ent.Comp.Inverse ? allOnAddFound : zeroOnAddFound) + args.Cancelled = true; + } + private void OnPartRemovedConditionValid(Entity ent, ref SurgeryValidEvent args) { if (!_body.CanAttachToSlot(args.Part, ent.Comp.Connection)) diff --git a/Content.Shared/Medical/Surgery/Steps/SurgeryStepComponent.cs b/Content.Shared/Medical/Surgery/Steps/SurgeryStepComponent.cs index 529fc5a566..9c46333fdc 100644 --- a/Content.Shared/Medical/Surgery/Steps/SurgeryStepComponent.cs +++ b/Content.Shared/Medical/Surgery/Steps/SurgeryStepComponent.cs @@ -23,6 +23,24 @@ public sealed partial class SurgeryStepComponent : Component [DataField] public ComponentRegistry? BodyRemove; + /// + /// These components will be added to the body part's organs' OnAdd field. + /// Each key is the SlotId of the organ to look for. + /// + /// Used to make organs add components to whatever body it's residing in. + /// + [DataField] + public Dictionary? AddOrganOnAdd; + + /// + /// These components will be removed from the body part's organs' OnAdd field. + /// Each key is the SlotId of the organ to look for. + /// + /// Used to stop organs from adding components to whatever body it's residing in. + /// + [DataField] + public Dictionary? RemoveOrganOnAdd; + [DataField] public float Duration = 2f; } diff --git a/Resources/Locale/en-US/surgery/surgery-popup.ftl b/Resources/Locale/en-US/surgery/surgery-popup.ftl index 8ded2fcaec..78656060ea 100644 --- a/Resources/Locale/en-US/surgery/surgery-popup.ftl +++ b/Resources/Locale/en-US/surgery/surgery-popup.ftl @@ -50,3 +50,6 @@ surgery-popup-step-SurgeryStepInsertHeart = {$user} is inserting a heart into {$ surgery-popup-step-SurgeryStepInsertStomach = {$user} is inserting a stomach into {$target}'s {$part}! surgery-popup-step-SurgeryStepSealOrganWound = {$user} is sealing the wounds on {$target}'s {$part}. + +surgery-popup-step-SurgeryStepLobotomize = {$user} is lobotomizing {$target}! +surgery-popup-step-SurgeryStepMendBrainTissue = {$user} is mending the brain tissue on {$target}'s {$part}. diff --git a/Resources/Prototypes/Entities/Surgery/surgeries.yml b/Resources/Prototypes/Entities/Surgery/surgeries.yml index ad71b27806..2db9782b67 100644 --- a/Resources/Prototypes/Entities/Surgery/surgeries.yml +++ b/Resources/Prototypes/Entities/Surgery/surgeries.yml @@ -54,12 +54,21 @@ steps: - SurgeryStepLobotomize - SurgeryStepCloseIncision - - type: SurgeryComponentCondition - component: - - type: OhioAccent - inverse: true - type: SurgeryPartCondition part: Head + - type: SurgeryOrganCondition + organ: + - type: Brain + - type: SurgeryOrganOnAddCondition + components: + brain: + - type: OhioAccent + - type: RatvarianLanguage + - type: Clumsy + clumsyDamage: # Placeholder values to be able to initialize the component + types: + Blunt: 0 + inverse: true - type: entity parent: SurgeryBase @@ -72,11 +81,20 @@ steps: - SurgeryStepMendBrainTissue - SurgeryStepCloseIncision - - type: SurgeryComponentCondition - component: - - type: OhioAccent - type: SurgeryPartCondition part: Head + - type: SurgeryOrganCondition + organ: + - type: Brain + - type: SurgeryOrganOnAddCondition + components: + brain: + - type: OhioAccent + - type: RatvarianLanguage + - type: Clumsy + clumsyDamage: + types: + Blunt: 0 - type: entity parent: SurgeryBase diff --git a/Resources/Prototypes/Entities/Surgery/surgery_steps.yml b/Resources/Prototypes/Entities/Surgery/surgery_steps.yml index a0fd45cabd..c2e213670e 100644 --- a/Resources/Prototypes/Entities/Surgery/surgery_steps.yml +++ b/Resources/Prototypes/Entities/Surgery/surgery_steps.yml @@ -421,8 +421,8 @@ - type: Tweezers duration: 8 - type: Sprite - sprite: Objects/Specific/Medical/Surgery/hemostat.rsi - state: hemostat + sprite: Objects/Specific/Medical/Surgery/retractor.rsi + state: retractor - type: SurgeryRemoveOrganStep - type: SurgeryStepEmoteEffect @@ -532,15 +532,22 @@ components: - type: SurgeryStep tool: - - type: Drill - bodyAdd: - - type: OhioAccent - - type: RatvarianLanguage - - type: SlurredAccent + - type: Scalpel + addOrganOnAdd: + brain: + - type: OhioAccent + - type: RatvarianLanguage + - type: Clumsy + clumsyDamage: + types: + Blunt: 5 + Piercing: 4 + groups: + Burn: 3 duration: 5 - type: Sprite - sprite: Objects/Specific/Medical/Surgery/drill.rsi - state: drill + sprite: Objects/Specific/Medical/Surgery/scalpel.rsi + state: scalpel - type: SurgeryStepEmoteEffect - type: SurgeryDamageChangeEffect damage: @@ -556,14 +563,18 @@ - type: SurgeryStep tool: - type: Hemostat - duration: 5 - bodyRemove: - - type: OhioAccent - - type: RatvarianLanguage - - type: SlurredAccent + duration: 4 + removeOrganOnAdd: + brain: + - type: OhioAccent + - type: RatvarianLanguage + - type: Clumsy + clumsyDamage: + types: + Blunt: 0 - type: Sprite - sprite: Objects/Specific/Medical/Surgery/drill.rsi - state: drill + sprite: Objects/Specific/Medical/Surgery/hemostat.rsi + state: hemostat - type: SurgeryStepEmoteEffect # The lengths I go to just for a joke... I HATE HARDCODING AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA